@quantovainc/qcore 0.1.27 → 0.1.30

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/README.md CHANGED
@@ -18,6 +18,8 @@ npm install @quantovainc/qcore
18
18
  ```js
19
19
  const { Client, core, generateSeed } = require('@quantovainc/qcore');
20
20
 
21
+ const GATEWAY = 'https://rpc-testnet.quantova.org';
22
+
21
23
  async function main() {
22
24
  // Create an account. The seed is the only backup and it never leaves the device.
23
25
  const seed = generateSeed();
@@ -28,12 +30,39 @@ async function main() {
28
30
  const to = core.address(seed, 1n);
29
31
  console.log('from', from);
30
32
 
31
- // Open a client against a node you trust. Plaintext http is allowed only to a loopback node.
32
- const client = new Client('http://127.0.0.1:8645');
33
+ // Open a client against a node you trust. The address above is the public testnet
34
+ // gateway. A remote gateway must be https, plaintext http is allowed only to a loopback node.
35
+ const client = new Client(GATEWAY);
33
36
 
34
37
  // A fixed ceiling your app is willing to pay in fees, chosen here and never read back from the gateway.
35
38
  const MAX_FEE_QUON = '2000';
36
39
 
40
+ // A new account holds nothing, so take testnet funds before anything else.
41
+ // The faucet sends 10, 50 or 100 TQTOV to one address at a time.
42
+ await fetch(GATEWAY + '/faucet/api/claim', {
43
+ method: 'POST',
44
+ headers: { 'content-type': 'application/json' },
45
+ body: JSON.stringify({ address: from, amount: 10 }),
46
+ });
47
+
48
+ // The faucet pays you with a transaction, so wait for the balance to arrive before spending it.
49
+ for (let i = 0; i < 40; i += 1) {
50
+ const funded = await client.account(from).catch(() => null);
51
+ if (funded && BigInt(funded.balance || 0) > 0n) break;
52
+ await new Promise((wake) => setTimeout(wake, 1500));
53
+ }
54
+
55
+ // An account publishes its key once before it is allowed to send. This happens a single
56
+ // time for each account and it pays a fee like any other transaction.
57
+ await client.register(seed, 0, MAX_FEE_QUON);
58
+
59
+ // Registration is itself a transaction, so wait for the key to be on chain.
60
+ for (let i = 0; i < 30; i += 1) {
61
+ const ready = await client.account(from).catch(() => null);
62
+ if (ready && ready.has_key) break;
63
+ await new Promise((wake) => setTimeout(wake, 1500));
64
+ }
65
+
37
66
  // Amounts and the ceiling are decimal strings or BigInt values, never JavaScript numbers.
38
67
  const { signed, outcome } = await client.transfer(seed, 0, to, '1000', MAX_FEE_QUON);
39
68
  console.log('submitted', signed.tx_id, outcome.verdict);
package/index.d.ts CHANGED
@@ -162,6 +162,8 @@ export class Network {
162
162
  export interface ClientOptions {
163
163
  acknowledgeMainnet?: boolean;
164
164
  network?: Network;
165
+ /** Bind signatures to this chain name; a gateway reporting any other is refused. */
166
+ expectedChainId?: string;
165
167
  }
166
168
 
167
169
  export class Client {
@@ -228,6 +230,7 @@ export class Client {
228
230
  ownerIndex: number,
229
231
  meterLimit: number | bigint,
230
232
  maxFeeQuon: string | bigint,
233
+ expectedOrderNonce?: bigint | number | string
231
234
  ): Promise<SignedOrderResult>;
232
235
  }
233
236
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@quantovainc/qcore",
3
- "version": "0.1.27",
3
+ "version": "0.1.30",
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-conformance.js && node test-sign-validate.js && node test-transport.js && node test-fee-cap.js && node test-network.js"
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"
8
8
  },
9
9
  "keywords": [
10
10
  "post-quantum",
@@ -83,5 +83,8 @@
83
83
  ],
84
84
  "engines": {
85
85
  "node": ">=18"
86
+ },
87
+ "devDependencies": {
88
+ "typescript": "^5.6.0"
86
89
  }
87
90
  }
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.27",
5
+ "version": "0.1.29",
6
6
  "license": "Apache-2.0 OR MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -13,8 +13,8 @@
13
13
  "qcore_js.js",
14
14
  "qcore_js_bg.js",
15
15
  "qcore_js.d.ts",
16
- "LICENSE-APACHE",
17
- "LICENSE-MIT"
16
+ "LICENSE-MIT",
17
+ "LICENSE-APACHE"
18
18
  ],
19
19
  "main": "qcore_js.js",
20
20
  "types": "qcore_js.d.ts",
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.27",
4
+ "version": "0.1.29",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11,8 +11,8 @@
11
11
  "qcore_js_bg.wasm",
12
12
  "qcore_js.js",
13
13
  "qcore_js.d.ts",
14
- "LICENSE-APACHE",
15
- "LICENSE-MIT"
14
+ "LICENSE-MIT",
15
+ "LICENSE-APACHE"
16
16
  ],
17
17
  "main": "qcore_js.js",
18
18
  "types": "qcore_js.d.ts"
Binary file
package/shared.js CHANGED
@@ -146,7 +146,7 @@ class Network {
146
146
  static testnet() {
147
147
  return new Network({
148
148
  name: 'testnet',
149
- chainId: 'Q-test-net-1',
149
+ chainId: 'Q-test-net-3',
150
150
  rpcUrl: 'https://rpc-testnet.quantova.org',
151
151
  explorerUrl: 'https://qvmscan.io',
152
152
  isMainnet: false,
@@ -198,6 +198,17 @@ function makeClient(core) {
198
198
  this.network = opts.network instanceof Network ? opts.network : Network.forUrl(base);
199
199
  }
200
200
  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
+ this.expectedChainId =
208
+ typeof opts.expectedChainId === 'string' && opts.expectedChainId.length > 0
209
+ ? opts.expectedChainId
210
+ : null;
211
+ this._pinnedChainName = null;
201
212
  }
202
213
 
203
214
  _guardMainnet() {
@@ -212,13 +223,25 @@ function makeClient(core) {
212
223
  if (!name) throw new Error('the gateway did not report a chain id to bind the signature to');
213
224
  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');
214
225
  const id = BigInt(core.chainIdFromName(name));
215
- const configured = this.network && this.network.chainId;
226
+ const configured =
227
+ this.expectedChainId || (this.network && this.network.chainId) || null;
216
228
  if (configured && name !== configured) {
217
229
  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`);
218
230
  }
219
231
  if (!this.acknowledgeMainnet && this._isMainnetId(id)) {
220
232
  throw new Error(`the gateway reports the mainnet chain ${name}; refusing to sign a mainnet transaction without acknowledgeMainnet`);
221
233
  }
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
+ if (this._pinnedChainName === null) {
241
+ this._pinnedChainName = name;
242
+ } else if (this._pinnedChainName !== name) {
243
+ throw new Error(`the gateway reported chain ${this._pinnedChainName} earlier and now reports ${name}; refusing to sign, the endpoint is not naming one network`);
244
+ }
222
245
  return id;
223
246
  }
224
247
 
@@ -378,7 +401,7 @@ function makeClient(core) {
378
401
  return BigInt(core.storageValue(JSON.stringify(resp), core.scalarSlotKey(BigInt(slot))));
379
402
  }
380
403
 
381
- async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon) {
404
+ async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce) {
382
405
  if (!core.valid_address(contract)) throw new Error('the contract is not a q1 address');
383
406
  const ceiling = feeCeiling(maxFeeQuon);
384
407
  const info = await this.nodeInfo();
@@ -390,7 +413,18 @@ function makeClient(core) {
390
413
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
391
414
  }
392
415
  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.
393
422
  const nonce = await this.contractNonce(contract, signer);
423
+ if (expectedOrderNonce != null && BigInt(expectedOrderNonce) !== nonce) {
424
+ throw new Error(
425
+ `the gateway reported order nonce ${nonce} but you expected ${expectedOrderNonce}, refusing to sign`
426
+ );
427
+ }
394
428
  const order = JSON.parse(core.buildTypedOrderCall(
395
429
  chainId,
396
430
  contract,
@@ -410,7 +444,7 @@ function makeClient(core) {
410
444
  core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonce(acct.nonce), BigInt(meterLimit), String(fee), chainId)
411
445
  );
412
446
  const outcome = await this.submit(signed.tx_hex);
413
- return { order, signed, outcome };
447
+ return { order, signed, outcome, orderNonce: nonce };
414
448
  }
415
449
  };
416
450
  }