@quantovainc/qcore 0.1.3
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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +131 -0
- package/browser.mjs +9 -0
- package/index.d.ts +174 -0
- package/index.js +9 -0
- package/package.json +76 -0
- package/pkg/package.json +25 -0
- package/pkg/qcore_js.d.ts +56 -0
- package/pkg/qcore_js.js +9 -0
- package/pkg/qcore_js_bg.js +708 -0
- package/pkg/qcore_js_bg.wasm +0 -0
- package/pkg/qcore_js_bg.wasm.d.ts +36 -0
- package/pkg-node/package.json +19 -0
- package/pkg-node/qcore_js.d.ts +56 -0
- package/pkg-node/qcore_js.js +740 -0
- package/pkg-node/qcore_js_bg.wasm +0 -0
- package/shared.js +323 -0
|
Binary file
|
package/shared.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// Copyright 2026 Quantova Inc
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
|
+
|
|
4
|
+
const MAX_RESPONSE = 8 * 1024 * 1024;
|
|
5
|
+
const TIMEOUT_MS = 20000;
|
|
6
|
+
|
|
7
|
+
function feeCeiling(maxFeeQuon) {
|
|
8
|
+
if (typeof maxFeeQuon === 'number') {
|
|
9
|
+
throw new Error('pass the maximum fee as a decimal string or a BigInt, never a JavaScript number, because a number silently rounds above 2^53 and could set the ceiling higher than you intended');
|
|
10
|
+
}
|
|
11
|
+
let ceiling;
|
|
12
|
+
try {
|
|
13
|
+
ceiling = BigInt(maxFeeQuon);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error('the maximum fee must be an integer number of Quon');
|
|
16
|
+
}
|
|
17
|
+
if (ceiling < 0n) throw new Error('the maximum fee cannot be negative');
|
|
18
|
+
return ceiling;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkAmount(amount) {
|
|
22
|
+
if (typeof amount === 'number') {
|
|
23
|
+
throw new Error('pass the amount as a decimal string or a BigInt, never a JavaScript number, because a number silently rounds above 2^53 and would sign a wrong amount');
|
|
24
|
+
}
|
|
25
|
+
if (typeof amount !== 'string' && typeof amount !== 'bigint') {
|
|
26
|
+
throw new Error('the amount must be a decimal string or a BigInt');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readBounded(res) {
|
|
31
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
32
|
+
const header = res.headers.get('content-length');
|
|
33
|
+
const len = header == null ? NaN : Number(header);
|
|
34
|
+
if (!Number.isFinite(len) || len <= 0) {
|
|
35
|
+
throw new Error('the response has no content-length to bound it and cannot be read safely');
|
|
36
|
+
}
|
|
37
|
+
if (len > MAX_RESPONSE) throw new Error('the response is too large');
|
|
38
|
+
return await res.text();
|
|
39
|
+
}
|
|
40
|
+
const reader = res.body.getReader();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
total += value.byteLength;
|
|
47
|
+
if (total > MAX_RESPONSE) {
|
|
48
|
+
try { await reader.cancel(); } catch { /* already closing */ }
|
|
49
|
+
throw new Error('the response is too large');
|
|
50
|
+
}
|
|
51
|
+
chunks.push(value);
|
|
52
|
+
}
|
|
53
|
+
const merged = new Uint8Array(total);
|
|
54
|
+
let at = 0;
|
|
55
|
+
for (const chunk of chunks) {
|
|
56
|
+
merged.set(chunk, at);
|
|
57
|
+
at += chunk.byteLength;
|
|
58
|
+
}
|
|
59
|
+
return new TextDecoder().decode(merged);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isLoopbackHost(hostname) {
|
|
63
|
+
if (!hostname) return false;
|
|
64
|
+
const host = hostname.toLowerCase();
|
|
65
|
+
if (host === 'localhost') return true;
|
|
66
|
+
if (host === '::1' || host === '[::1]') return true;
|
|
67
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
68
|
+
if (m) {
|
|
69
|
+
const octets = [m[1], m[2], m[3], m[4]].map(Number);
|
|
70
|
+
if (octets.every((o) => o <= 255) && octets[0] === 127) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function requireSafeTransport(base) {
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(base);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error('the gateway base must be an absolute http:// or https:// URL');
|
|
81
|
+
}
|
|
82
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
83
|
+
throw new Error('the gateway base must start with http:// or https://');
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`refusing plaintext http to a non loopback gateway (${url.hostname}); its fee and nonce would be unauthenticated and rewritable to drain funds, use https or a loopback node`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DENOMINATION = 'Quon';
|
|
94
|
+
const DECIMALS = 6;
|
|
95
|
+
|
|
96
|
+
class Network {
|
|
97
|
+
constructor(fields) {
|
|
98
|
+
this.name = fields.name;
|
|
99
|
+
this.chainId = fields.chainId;
|
|
100
|
+
this.rpcUrl = fields.rpcUrl || null;
|
|
101
|
+
this.explorerUrl = fields.explorerUrl || null;
|
|
102
|
+
this.denomination = fields.denomination || DENOMINATION;
|
|
103
|
+
this.decimals = fields.decimals == null ? DECIMALS : fields.decimals;
|
|
104
|
+
this.isMainnet = fields.isMainnet === true;
|
|
105
|
+
Object.freeze(this);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static testnet() {
|
|
109
|
+
return new Network({
|
|
110
|
+
name: 'testnet',
|
|
111
|
+
chainId: 'Q-test-net-1',
|
|
112
|
+
rpcUrl: 'https://rpc-testnet.quantova.org',
|
|
113
|
+
explorerUrl: 'https://qvmscan.io',
|
|
114
|
+
isMainnet: false,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static mainnet() {
|
|
119
|
+
return new Network({
|
|
120
|
+
name: 'mainnet',
|
|
121
|
+
chainId: 'Q-main-net-1',
|
|
122
|
+
rpcUrl: null,
|
|
123
|
+
explorerUrl: 'https://qvmscan.io',
|
|
124
|
+
isMainnet: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static forUrl(base) {
|
|
129
|
+
return new Network({ name: 'custom', chainId: null, rpcUrl: base, isMainnet: false });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function generateSeed() {
|
|
134
|
+
const source = (typeof globalThis !== 'undefined' && globalThis.crypto) || (typeof crypto !== 'undefined' ? crypto : null);
|
|
135
|
+
if (!source || typeof source.getRandomValues !== 'function') {
|
|
136
|
+
throw new Error('no cryptographic random source is available; a secure context provides crypto.getRandomValues');
|
|
137
|
+
}
|
|
138
|
+
const bytes = new Uint8Array(32);
|
|
139
|
+
source.getRandomValues(bytes);
|
|
140
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function makeClient(core) {
|
|
144
|
+
// The signature binds the chain the gateway says it serves, computed from the reported network
|
|
145
|
+
// name with the same hash the node uses, so a transaction is valid only on that network and a
|
|
146
|
+
// renamed network is followed without a code change.
|
|
147
|
+
function signingChainId(info) {
|
|
148
|
+
const name = info && info.chain_id;
|
|
149
|
+
if (!name) throw new Error('the gateway did not report a chain id to bind the signature to');
|
|
150
|
+
return BigInt(core.chainIdFromName(name));
|
|
151
|
+
}
|
|
152
|
+
return class Client {
|
|
153
|
+
constructor(target, options) {
|
|
154
|
+
const opts = options || {};
|
|
155
|
+
this.acknowledgeMainnet = opts.acknowledgeMainnet === true;
|
|
156
|
+
let base;
|
|
157
|
+
if (target instanceof Network) {
|
|
158
|
+
this.network = target;
|
|
159
|
+
base = target.rpcUrl;
|
|
160
|
+
if (!base) {
|
|
161
|
+
throw new Error(`the ${target.name} network has no rpc endpoint yet, pass the endpoint explicitly with new Client(url)`);
|
|
162
|
+
}
|
|
163
|
+
if (target.isMainnet && !this.acknowledgeMainnet) {
|
|
164
|
+
throw new Error('refusing to open a mainnet client without acknowledgeMainnet true, a mainnet transaction moves real value so the network must be chosen on purpose');
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
base = String(target);
|
|
168
|
+
this.network = opts.network instanceof Network ? opts.network : Network.forUrl(base);
|
|
169
|
+
}
|
|
170
|
+
this.base = requireSafeTransport(base).replace(/\/$/, '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_guardMainnet() {
|
|
174
|
+
// The mainnet decision comes from the network this Client was configured with, never from
|
|
175
|
+
// the gateway's self reported chain id, so a hostile gateway cannot suppress the prompt by
|
|
176
|
+
// claiming to be a testnet.
|
|
177
|
+
const onMainnet = this.network && this.network.isMainnet === true;
|
|
178
|
+
if (onMainnet && !this.acknowledgeMainnet) {
|
|
179
|
+
throw new Error(`refusing to sign for the mainnet network ${this.network.chainId || ''} without acknowledgeMainnet true, pass it when you mean to move real value`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async _call(method, body) {
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(this.base + '/v1/' + method, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: body || '{}',
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
});
|
|
193
|
+
const text = await readBounded(res);
|
|
194
|
+
let data;
|
|
195
|
+
try { data = JSON.parse(text); } catch { throw new Error('the node returned a non JSON response'); }
|
|
196
|
+
if (!res.ok) throw new Error(data.message || data.error || ('status ' + res.status));
|
|
197
|
+
return data;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
nodeInfo() { return this._call('node_info', '{}'); }
|
|
204
|
+
head() { return this._call('head', '{}'); }
|
|
205
|
+
account(address) { return this._call('get_account', core.account_body(address)); }
|
|
206
|
+
transaction(txId) { return this._call('get_transaction', core.transaction_body(txId)); }
|
|
207
|
+
block(height) { return this._call('get_block', core.block_by_height_body(BigInt(height))); }
|
|
208
|
+
submit(txHex) { return this._call('submit_transaction', core.submit_body(txHex)); }
|
|
209
|
+
container(address) { return this._call('get_container', JSON.stringify({ address })); }
|
|
210
|
+
storage(address) { return this._call('get_storage', JSON.stringify({ address })); }
|
|
211
|
+
events(height) { return this._call('get_events', JSON.stringify({ height: Number(BigInt(height)) })); }
|
|
212
|
+
|
|
213
|
+
address(seedHex, index) { return core.address(seedHex, BigInt(index)); }
|
|
214
|
+
|
|
215
|
+
async transfer(seedHex, index, to, amount, maxFeeQuon) {
|
|
216
|
+
if (!core.valid_address(to)) throw new Error('the recipient is not a q1 address');
|
|
217
|
+
checkAmount(amount);
|
|
218
|
+
const ceiling = feeCeiling(maxFeeQuon);
|
|
219
|
+
const info = await this.nodeInfo();
|
|
220
|
+
this._guardMainnet();
|
|
221
|
+
const chainId = signingChainId(info);
|
|
222
|
+
const fee = info && info.fee && info.fee.transfer_quon;
|
|
223
|
+
if (fee == null) throw new Error('the gateway did not report a transfer fee');
|
|
224
|
+
if (BigInt(fee) > ceiling) {
|
|
225
|
+
throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
|
|
226
|
+
}
|
|
227
|
+
const from = core.address(seedHex, BigInt(index));
|
|
228
|
+
const acct = await this.account(from);
|
|
229
|
+
if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
|
|
230
|
+
const signed = JSON.parse(
|
|
231
|
+
core.sign_transfer(seedHex, BigInt(index), to, String(amount), BigInt(acct.nonce), String(fee), chainId)
|
|
232
|
+
);
|
|
233
|
+
const outcome = await this.submit(signed.tx_hex);
|
|
234
|
+
return { signed, outcome };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async register(seedHex, index, maxFeeQuon) {
|
|
238
|
+
const ceiling = feeCeiling(maxFeeQuon);
|
|
239
|
+
const info = await this.nodeInfo();
|
|
240
|
+
this._guardMainnet();
|
|
241
|
+
const chainId = signingChainId(info);
|
|
242
|
+
const fee = info && info.fee && info.fee.transfer_quon;
|
|
243
|
+
if (fee == null) throw new Error('the gateway did not report a transfer fee');
|
|
244
|
+
if (BigInt(fee) > ceiling) {
|
|
245
|
+
throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
|
|
246
|
+
}
|
|
247
|
+
const from = core.address(seedHex, BigInt(index));
|
|
248
|
+
const acct = await this.account(from);
|
|
249
|
+
if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
|
|
250
|
+
const signed = JSON.parse(core.signRegister(seedHex, BigInt(index), BigInt(acct.nonce), String(fee), chainId));
|
|
251
|
+
const outcome = await this.submit(signed.tx_hex);
|
|
252
|
+
return { signed, outcome };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async call(seedHex, index, target, argsHex, meterLimit, maxFeeQuon) {
|
|
256
|
+
if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
|
|
257
|
+
const ceiling = feeCeiling(maxFeeQuon);
|
|
258
|
+
const info = await this.nodeInfo();
|
|
259
|
+
this._guardMainnet();
|
|
260
|
+
const chainId = signingChainId(info);
|
|
261
|
+
const fee = info && info.fee && info.fee.transfer_quon;
|
|
262
|
+
if (fee == null) throw new Error('the gateway did not report a transfer fee');
|
|
263
|
+
if (BigInt(fee) > ceiling) {
|
|
264
|
+
throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
|
|
265
|
+
}
|
|
266
|
+
const from = core.address(seedHex, BigInt(index));
|
|
267
|
+
const acct = await this.account(from);
|
|
268
|
+
if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
|
|
269
|
+
const signed = JSON.parse(
|
|
270
|
+
core.sign_call(seedHex, BigInt(index), target, argsHex, BigInt(acct.nonce), BigInt(meterLimit), String(fee), chainId)
|
|
271
|
+
);
|
|
272
|
+
const outcome = await this.submit(signed.tx_hex);
|
|
273
|
+
return { signed, outcome };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async contractNonce(contract, signerHex) {
|
|
277
|
+
const resp = await this._call('get_storage', core.storageBody(contract));
|
|
278
|
+
return BigInt(core.storageValue(JSON.stringify(resp), core.nonceSlotKey(signerHex)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async contractScalar(contract, slot) {
|
|
282
|
+
const resp = await this._call('get_storage', core.storageBody(contract));
|
|
283
|
+
return BigInt(core.storageValue(JSON.stringify(resp), core.scalarSlotKey(BigInt(slot))));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, layout, fields, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon) {
|
|
287
|
+
if (!core.valid_address(contract)) throw new Error('the contract is not a q1 address');
|
|
288
|
+
const ceiling = feeCeiling(maxFeeQuon);
|
|
289
|
+
const signer = core.orderSigner(ownerSeedHex, BigInt(ownerIndex));
|
|
290
|
+
const nonce = await this.contractNonce(contract, signer);
|
|
291
|
+
const order = JSON.parse(core.buildSignedOrderCall(
|
|
292
|
+
contract,
|
|
293
|
+
selectorHex,
|
|
294
|
+
BigInt(layout.schemeOff),
|
|
295
|
+
BigInt(layout.ptrOff),
|
|
296
|
+
(layout.fieldOffs || []).join(','),
|
|
297
|
+
(fields || []).map(String).join(','),
|
|
298
|
+
BigInt(layout.regionOff || 0),
|
|
299
|
+
ownerSeedHex,
|
|
300
|
+
BigInt(ownerIndex),
|
|
301
|
+
nonce,
|
|
302
|
+
));
|
|
303
|
+
const info = await this.nodeInfo();
|
|
304
|
+
this._guardMainnet();
|
|
305
|
+
const chainId = signingChainId(info);
|
|
306
|
+
const fee = info && info.fee && info.fee.transfer_quon;
|
|
307
|
+
if (fee == null) throw new Error('the gateway did not report a transfer fee');
|
|
308
|
+
if (BigInt(fee) > ceiling) {
|
|
309
|
+
throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
|
|
310
|
+
}
|
|
311
|
+
const from = core.address(callerSeedHex, BigInt(callerIndex));
|
|
312
|
+
const acct = await this.account(from);
|
|
313
|
+
if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
|
|
314
|
+
const signed = JSON.parse(
|
|
315
|
+
core.sign_call(callerSeedHex, BigInt(callerIndex), contract, order.call_args, BigInt(acct.nonce), BigInt(meterLimit), String(fee), chainId)
|
|
316
|
+
);
|
|
317
|
+
const outcome = await this.submit(signed.tx_hex);
|
|
318
|
+
return { order, signed, outcome };
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = { makeClient, feeCeiling, checkAmount, generateSeed, readBounded, requireSafeTransport, Network };
|