@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 +31 -2
- 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 +38 -4
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.
|
|
32
|
-
|
|
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.
|
|
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.
|
|
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-
|
|
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.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-
|
|
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
|
@@ -146,7 +146,7 @@ class Network {
|
|
|
146
146
|
static testnet() {
|
|
147
147
|
return new Network({
|
|
148
148
|
name: 'testnet',
|
|
149
|
-
chainId: 'Q-test-net-
|
|
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 =
|
|
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
|
}
|