@quantovainc/qcore 0.1.29 → 0.1.31
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 +1 -1
- package/index.d.ts +3 -0
- package/package.json +5 -2
- package/pkg/package.json +3 -3
- package/pkg/qcore_js_bg.wasm +0 -0
- package/pkg-node/package.json +3 -3
- package/pkg-node/qcore_js_bg.wasm +0 -0
- package/shared.js +56 -6
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Quantova is post quantum from the ground. There is no elliptic curve anywhere in
|
|
|
91
91
|
|
|
92
92
|
3. The address. A Quantova address is the Bech32m Q1 string that renders the SHA3 256 hash of the scheme byte together with the whole module lattice public key of one thousand nine hundred fifty two bytes. The entire public key is bound into the address. Nothing is truncated to a twenty byte hash and there is no key recovery from a signature, so one address names exactly one post quantum key.
|
|
93
93
|
|
|
94
|
-
4. The transaction. A transaction body carries the sender, the nonce, the meter limit, the fee, and the call. The bytes a signature stands over are the SHA3 256 hash of the canonical body
|
|
94
|
+
4. The transaction. A transaction body carries the sender, the nonce, the meter limit, the fee, and the call. The bytes a signature stands over are the SHA3 256 hash of the canonical body, and a fixed Quantova transaction domain tag is bound in as the post quantum signing context, so a transaction signature can never be replayed as another kind of signed message. QCore.js assembles the body, signs the digest inside the compiled core, and returns the wrapper bytes and the transaction id ready for the gateway.
|
|
95
95
|
|
|
96
96
|
5. The payable call. `core.signPayableCall` carries a native value and an explicit chain id alongside the sender, the nonce, the meter limit, the fee, and the call, so a call to a contract can move value in the same signed body a transfer does, and the same signature can be pinned to one network. The sender and the target never enter that signature as the rendered Q1 string, since a display convention can change over time. Each is parsed back to its raw thirty two byte payload first, and that payload, never the string, is what the preimage carries, so the signature stays bound to the account and never to how an address happened to be printed.
|
|
97
97
|
|
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.
|
|
3
|
+
"version": "0.1.31",
|
|
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.
|
|
5
|
+
"version": "0.1.31",
|
|
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-
|
|
17
|
-
"LICENSE-
|
|
16
|
+
"LICENSE-MIT",
|
|
17
|
+
"LICENSE-APACHE"
|
|
18
18
|
],
|
|
19
19
|
"main": "qcore_js.js",
|
|
20
20
|
"types": "qcore_js.d.ts",
|
package/pkg/qcore_js_bg.wasm
CHANGED
|
Binary file
|
package/pkg-node/package.json
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.1.31",
|
|
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-
|
|
15
|
-
"LICENSE-
|
|
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
|
@@ -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 =
|
|
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
|
|
|
@@ -252,7 +275,22 @@ function makeClient(core) {
|
|
|
252
275
|
|
|
253
276
|
nodeInfo() { return this._call('node_info', '{}'); }
|
|
254
277
|
head() { return this._call('head', '{}'); }
|
|
255
|
-
account(address) {
|
|
278
|
+
async account(address) {
|
|
279
|
+
const acct = await this._call('get_account', core.account_body(address));
|
|
280
|
+
if (acct && acct.address != null && acct.address !== address) {
|
|
281
|
+
throw new Error(`the gateway answered for ${acct.address} when asked about ${address}, refusing to trust it`);
|
|
282
|
+
}
|
|
283
|
+
return acct;
|
|
284
|
+
}
|
|
285
|
+
_checkedNonce(reported, expected) {
|
|
286
|
+
const n = accountNonce(reported);
|
|
287
|
+
if (expected != null) {
|
|
288
|
+
const e = BigInt(expected);
|
|
289
|
+
if (n < e) throw new Error(`the gateway reported nonce ${n} below the expected ${e}; refusing so a replayed lower nonce cannot force a second payment`);
|
|
290
|
+
if (n > e + 16n) throw new Error(`the gateway reported nonce ${n} far above the expected ${e}; refusing`);
|
|
291
|
+
}
|
|
292
|
+
return n;
|
|
293
|
+
}
|
|
256
294
|
transaction(txId) { return this._call('get_transaction', core.transaction_body(txId)); }
|
|
257
295
|
block(height) { return this._call('get_block', core.block_by_height_body(BigInt(height))); }
|
|
258
296
|
submit(txHex) { return this._call('submit_transaction', core.submit_body(txHex)); }
|
|
@@ -262,7 +300,7 @@ function makeClient(core) {
|
|
|
262
300
|
|
|
263
301
|
address(seedHex, index) { return core.address(seedHex, accountIndex(index)); }
|
|
264
302
|
|
|
265
|
-
async transfer(seedHex, index, to, amount, maxFeeQuon) {
|
|
303
|
+
async transfer(seedHex, index, to, amount, maxFeeQuon, expectedNonce) {
|
|
266
304
|
if (!core.valid_address(to)) throw new Error('the recipient is not a q1 address');
|
|
267
305
|
checkAmount(amount);
|
|
268
306
|
const ceiling = feeCeiling(maxFeeQuon);
|
|
@@ -277,8 +315,9 @@ function makeClient(core) {
|
|
|
277
315
|
const from = core.address(seedHex, accountIndex(index));
|
|
278
316
|
const acct = await this.account(from);
|
|
279
317
|
if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
|
|
318
|
+
const nonce = this._checkedNonce(acct.nonce, expectedNonce);
|
|
280
319
|
const signed = JSON.parse(
|
|
281
|
-
core.sign_transfer(seedHex, accountIndex(index), to, String(amount),
|
|
320
|
+
core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId)
|
|
282
321
|
);
|
|
283
322
|
const outcome = await this.submit(signed.tx_hex);
|
|
284
323
|
return { signed, outcome };
|
|
@@ -378,7 +417,7 @@ function makeClient(core) {
|
|
|
378
417
|
return BigInt(core.storageValue(JSON.stringify(resp), core.scalarSlotKey(BigInt(slot))));
|
|
379
418
|
}
|
|
380
419
|
|
|
381
|
-
async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon) {
|
|
420
|
+
async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce) {
|
|
382
421
|
if (!core.valid_address(contract)) throw new Error('the contract is not a q1 address');
|
|
383
422
|
const ceiling = feeCeiling(maxFeeQuon);
|
|
384
423
|
const info = await this.nodeInfo();
|
|
@@ -390,7 +429,18 @@ function makeClient(core) {
|
|
|
390
429
|
throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
|
|
391
430
|
}
|
|
392
431
|
const signer = core.orderSigner(ownerSeedHex, accountIndex(ownerIndex));
|
|
432
|
+
// The owner authorisation carries no deadline, so this nonce is the only thing
|
|
433
|
+
// that stops it being replayed. Reading it from the gateway hands the party the
|
|
434
|
+
// signature protects against the one term that expires it: answer with a future
|
|
435
|
+
// nonce and the reply is a valid order banked until the contract counter reaches
|
|
436
|
+
// it. Pass expectedOrderNonce to bind what you meant to sign; without it you are
|
|
437
|
+
// trusting the endpoint with the owner key.
|
|
393
438
|
const nonce = await this.contractNonce(contract, signer);
|
|
439
|
+
if (expectedOrderNonce != null && BigInt(expectedOrderNonce) !== nonce) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`the gateway reported order nonce ${nonce} but you expected ${expectedOrderNonce}, refusing to sign`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
394
444
|
const order = JSON.parse(core.buildTypedOrderCall(
|
|
395
445
|
chainId,
|
|
396
446
|
contract,
|
|
@@ -410,7 +460,7 @@ function makeClient(core) {
|
|
|
410
460
|
core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonce(acct.nonce), BigInt(meterLimit), String(fee), chainId)
|
|
411
461
|
);
|
|
412
462
|
const outcome = await this.submit(signed.tx_hex);
|
|
413
|
-
return { order, signed, outcome };
|
|
463
|
+
return { order, signed, outcome, orderNonce: nonce };
|
|
414
464
|
}
|
|
415
465
|
};
|
|
416
466
|
}
|