@orbinum/sdk 1.4.0 → 2.1.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/dist/{chunk-A2ZRMEYW.mjs → chunk-VYKKBXOE.mjs} +51 -28
- package/dist/{index-V5Z9igEN.d.mts → index-CLpM1984.d.mts} +42 -9
- package/dist/{index-V5Z9igEN.d.ts → index-CLpM1984.d.ts} +42 -9
- package/dist/index.d.mts +445 -116
- package/dist/index.d.ts +445 -116
- package/dist/index.js +573 -325
- package/dist/index.mjs +465 -244
- package/dist/wallet/worker/index.d.mts +1 -1
- package/dist/wallet/worker/index.d.ts +1 -1
- package/dist/wallet/worker/index.js +18 -11
- package/dist/wallet/worker/index.mjs +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
bytesToBjjScalar,
|
|
30
30
|
canPairWith,
|
|
31
31
|
clearKnownEphWindow,
|
|
32
|
+
collectOutgoingFacts,
|
|
32
33
|
commitmentHexOf,
|
|
33
34
|
computeNoteCommitment,
|
|
34
35
|
computeNullifier,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
hexToBigint,
|
|
60
61
|
hexToNumber,
|
|
61
62
|
isAbortError,
|
|
63
|
+
isHexOfLength,
|
|
62
64
|
isSpendable,
|
|
63
65
|
isValidLeafIndex,
|
|
64
66
|
leHexToBigint,
|
|
@@ -77,7 +79,7 @@ import {
|
|
|
77
79
|
tryDecryptNote,
|
|
78
80
|
tryDecryptNoteVerbose,
|
|
79
81
|
tryRecoverOutgoing
|
|
80
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-VYKKBXOE.mjs";
|
|
81
83
|
|
|
82
84
|
// src/foundation/encoding/base64.ts
|
|
83
85
|
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -382,6 +384,7 @@ var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
|
|
|
382
384
|
var NONCE_PREFIX = new TextEncoder().encode("SLP1");
|
|
383
385
|
var EPH_PK_SIZE = 32;
|
|
384
386
|
var NONCE_SUFFIX_SIZE = 8;
|
|
387
|
+
var MAX_SLIP_ENVELOPE_SIZE = 4096;
|
|
385
388
|
function deriveSlipKey(sharedSecret) {
|
|
386
389
|
return hkdf(sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
|
|
387
390
|
}
|
|
@@ -416,6 +419,7 @@ function sealPaymentSlip(recipientIvkPacked, fields) {
|
|
|
416
419
|
}
|
|
417
420
|
function openPaymentSlip(recipientIvsk, envelope) {
|
|
418
421
|
if (envelope.length < EPH_PK_SIZE + NONCE_SUFFIX_SIZE + 16) return null;
|
|
422
|
+
if (envelope.length > MAX_SLIP_ENVELOPE_SIZE) return null;
|
|
419
423
|
try {
|
|
420
424
|
const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE);
|
|
421
425
|
const ephPkPoint = unpackPoint(bytesToBigintLE(ephPkPacked));
|
|
@@ -427,11 +431,19 @@ function openPaymentSlip(recipientIvsk, envelope) {
|
|
|
427
431
|
const sealed = envelope.subarray(EPH_PK_SIZE + NONCE_SUFFIX_SIZE);
|
|
428
432
|
const cipher = chacha20poly1305(slipKey, buildNonce(suffix));
|
|
429
433
|
const plaintext = cipher.decrypt(sealed);
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return
|
|
434
|
+
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
435
|
+
if (!isHexOfLength(parsed["commitmentHex"], 32)) return null;
|
|
436
|
+
if (!isHexOfLength(parsed["encryptedMemo"], ENCRYPTED_MEMO_SIZE)) return null;
|
|
437
|
+
const leafIndex = parsed["leafIndex"];
|
|
438
|
+
if (leafIndex !== void 0 && !isValidLeafIndex(leafIndex)) return null;
|
|
439
|
+
const rawTxHash = parsed["txHash"];
|
|
440
|
+
const txHash = isHexOfLength(rawTxHash, 32) ? rawTxHash : void 0;
|
|
441
|
+
return {
|
|
442
|
+
commitmentHex: parsed["commitmentHex"],
|
|
443
|
+
encryptedMemo: parsed["encryptedMemo"],
|
|
444
|
+
...leafIndex !== void 0 ? { leafIndex } : {},
|
|
445
|
+
...txHash !== void 0 ? { txHash } : {}
|
|
446
|
+
};
|
|
435
447
|
} catch {
|
|
436
448
|
return null;
|
|
437
449
|
}
|
|
@@ -1536,11 +1548,17 @@ var SubstrateClient = class _SubstrateClient {
|
|
|
1536
1548
|
|
|
1537
1549
|
// src/chain/evm/EvmClient.ts
|
|
1538
1550
|
var EvmClient = class {
|
|
1539
|
-
/**
|
|
1540
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`).
|
|
1553
|
+
* @param peerRpcUrl - Optional second endpoint, used only to tell a genuinely
|
|
1554
|
+
* pending transaction from one stranded on `rpcUrl` alone. See `waitForReceipt`.
|
|
1555
|
+
*/
|
|
1556
|
+
constructor(rpcUrl, peerRpcUrl) {
|
|
1541
1557
|
this.rpcUrl = rpcUrl;
|
|
1558
|
+
this.peerRpcUrl = peerRpcUrl;
|
|
1542
1559
|
}
|
|
1543
1560
|
rpcUrl;
|
|
1561
|
+
peerRpcUrl;
|
|
1544
1562
|
/**
|
|
1545
1563
|
* Performs a single JSON-RPC call and returns the typed result.
|
|
1546
1564
|
* Throws on HTTP errors, RPC-level errors, or a `null` result.
|
|
@@ -1598,10 +1616,18 @@ var EvmClient = class {
|
|
|
1598
1616
|
const hex = await this.request("eth_getTransactionCount", [address, "latest"]);
|
|
1599
1617
|
return hexToNumber(hex);
|
|
1600
1618
|
}
|
|
1601
|
-
/**
|
|
1602
|
-
|
|
1619
|
+
/**
|
|
1620
|
+
* Returns the current gas price in wei, padded by `bumpPercent`.
|
|
1621
|
+
*
|
|
1622
|
+
* `eth_gasPrice` reports the base fee exactly, and the base fee moves
|
|
1623
|
+
* between signing and the pool's next revalidation. A transaction priced at
|
|
1624
|
+
* the bare minimum is evicted as `GasPriceTooLow` the moment it rises, which
|
|
1625
|
+
* leaves every later nonce from that account stranded in the future queue.
|
|
1626
|
+
* The default 25% pad absorbs the usual movement.
|
|
1627
|
+
*/
|
|
1628
|
+
async getGasPrice(bumpPercent = 25) {
|
|
1603
1629
|
const hex = await this.request("eth_gasPrice", []);
|
|
1604
|
-
return hexToBigint(hex);
|
|
1630
|
+
return hexToBigint(hex) * BigInt(100 + bumpPercent) / 100n;
|
|
1605
1631
|
}
|
|
1606
1632
|
/** Submits a signed raw transaction. Returns the transaction hash. */
|
|
1607
1633
|
async sendRawTransaction(signedHex) {
|
|
@@ -1719,10 +1745,41 @@ var EvmClient = class {
|
|
|
1719
1745
|
deadline = Math.min(deadline + timeoutMs, hardDeadline);
|
|
1720
1746
|
}
|
|
1721
1747
|
}
|
|
1748
|
+
if (await this.isStrandedOnThisNode(txHash)) {
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`Transaction dropped from the tx pool (never propagated beyond the submitting node, ${Date.now() - start}ms): ${txHash}`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1722
1753
|
throw new Error(
|
|
1723
1754
|
`Transaction still pending after ${Date.now() - start}ms: ${txHash} \u2014 it may still confirm; check the hash on the explorer before retrying`
|
|
1724
1755
|
);
|
|
1725
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* True when `rpcUrl` knows the transaction but the configured peer does not.
|
|
1759
|
+
*
|
|
1760
|
+
* Returns false without a peer configured, and on any peer error — an
|
|
1761
|
+
* unreachable peer is not evidence that a live transaction is stranded.
|
|
1762
|
+
*/
|
|
1763
|
+
async isStrandedOnThisNode(txHash) {
|
|
1764
|
+
if (!this.peerRpcUrl) return false;
|
|
1765
|
+
try {
|
|
1766
|
+
const res = await postJsonWithRetry(
|
|
1767
|
+
this.peerRpcUrl,
|
|
1768
|
+
JSON.stringify({
|
|
1769
|
+
id: 1,
|
|
1770
|
+
jsonrpc: "2.0",
|
|
1771
|
+
method: "eth_getTransactionByHash",
|
|
1772
|
+
params: [txHash]
|
|
1773
|
+
})
|
|
1774
|
+
);
|
|
1775
|
+
if (!res.ok) return false;
|
|
1776
|
+
const json = await res.json();
|
|
1777
|
+
if (json.error) return false;
|
|
1778
|
+
return json.result === null;
|
|
1779
|
+
} catch {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1726
1783
|
};
|
|
1727
1784
|
|
|
1728
1785
|
// src/chain/evm/explorer/EvmExplorer.ts
|
|
@@ -2143,7 +2200,11 @@ var ShieldedPoolModule = class {
|
|
|
2143
2200
|
* Withdraws tokens from the shielded pool to a public address.
|
|
2144
2201
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
2145
2202
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
2146
|
-
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
2203
|
+
* Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, circuitVersion)
|
|
2204
|
+
*
|
|
2205
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
2206
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
2207
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
2147
2208
|
*/
|
|
2148
2209
|
async unshield(params, signer, options) {
|
|
2149
2210
|
const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
|
|
@@ -2167,8 +2228,6 @@ var ShieldedPoolModule = class {
|
|
|
2167
2228
|
fee: params.fee ?? 0n,
|
|
2168
2229
|
change_commitment: changeCommitment,
|
|
2169
2230
|
change_encrypted_memo: changeEncryptedMemo,
|
|
2170
|
-
relayer: void 0,
|
|
2171
|
-
// Option<H160> — None for direct Substrate submissions
|
|
2172
2231
|
circuit_version: params.circuitVersion
|
|
2173
2232
|
});
|
|
2174
2233
|
if (signer) {
|
|
@@ -2180,7 +2239,11 @@ var ShieldedPoolModule = class {
|
|
|
2180
2239
|
* Performs a private (shielded) transfer between two notes.
|
|
2181
2240
|
* Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
|
|
2182
2241
|
* Pass a `signer` to fall back to signed submission (e.g. for testing).
|
|
2183
|
-
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee,
|
|
2242
|
+
* Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, circuitVersion)
|
|
2243
|
+
*
|
|
2244
|
+
* The relay fee recipient is NOT a parameter: the chain takes it from the
|
|
2245
|
+
* dispatch origin. Submitting unsigned credits the block author; submitting
|
|
2246
|
+
* through the EVM precompile credits whoever signed that transaction.
|
|
2184
2247
|
*/
|
|
2185
2248
|
async privateTransfer(params, signer, options) {
|
|
2186
2249
|
const nullifiers = params.inputs.map((inp) => inp.nullifier);
|
|
@@ -2201,8 +2264,6 @@ var ShieldedPoolModule = class {
|
|
|
2201
2264
|
encrypted_memos: memos,
|
|
2202
2265
|
asset_id: params.assetId,
|
|
2203
2266
|
fee: params.fee ?? 0n,
|
|
2204
|
-
relayer: void 0,
|
|
2205
|
-
// Option<H160> — None for direct Substrate submissions
|
|
2206
2267
|
circuit_version: params.circuitVersion
|
|
2207
2268
|
});
|
|
2208
2269
|
if (signer) {
|
|
@@ -2441,6 +2502,23 @@ function padTo32Multiple(data) {
|
|
|
2441
2502
|
padded.set(data);
|
|
2442
2503
|
return padded;
|
|
2443
2504
|
}
|
|
2505
|
+
function bytes32Slot(value) {
|
|
2506
|
+
if (value.length !== 32) {
|
|
2507
|
+
throw new Error(`encodeAbi: bytes32 needs 32 bytes, got ${value.length}`);
|
|
2508
|
+
}
|
|
2509
|
+
const slot = new Uint8Array(32);
|
|
2510
|
+
slot.set(value);
|
|
2511
|
+
return slot;
|
|
2512
|
+
}
|
|
2513
|
+
function addressSlot(address) {
|
|
2514
|
+
const clean = address.startsWith("0x") ? address.slice(2) : address;
|
|
2515
|
+
if (clean.length > 40) {
|
|
2516
|
+
throw new Error(`encodeAbi: address needs at most 20 bytes, got ${clean.length / 2}`);
|
|
2517
|
+
}
|
|
2518
|
+
const slot = new Uint8Array(32);
|
|
2519
|
+
slot.set(fromHex("0x" + clean.padStart(40, "0")), 12);
|
|
2520
|
+
return slot;
|
|
2521
|
+
}
|
|
2444
2522
|
function encodeStaticParam(param) {
|
|
2445
2523
|
const buf = new Uint8Array(32);
|
|
2446
2524
|
switch (param.type) {
|
|
@@ -2448,14 +2526,10 @@ function encodeStaticParam(param) {
|
|
|
2448
2526
|
return bigintTo32Be(param.value);
|
|
2449
2527
|
}
|
|
2450
2528
|
case "bytes32": {
|
|
2451
|
-
|
|
2452
|
-
return buf;
|
|
2529
|
+
return bytes32Slot(param.value);
|
|
2453
2530
|
}
|
|
2454
2531
|
case "address": {
|
|
2455
|
-
|
|
2456
|
-
const bytes = fromHex("0x" + clean.padStart(40, "0"));
|
|
2457
|
-
buf.set(bytes, 12);
|
|
2458
|
-
return buf;
|
|
2532
|
+
return addressSlot(param.value);
|
|
2459
2533
|
}
|
|
2460
2534
|
case "bool": {
|
|
2461
2535
|
buf[31] = param.value ? 1 : 0;
|
|
@@ -2478,25 +2552,13 @@ function encodeDynamicParam(param) {
|
|
|
2478
2552
|
return concat([bigintTo32Be(BigInt(data.length)), padTo32Multiple(data)]);
|
|
2479
2553
|
}
|
|
2480
2554
|
case "bytes32[]": {
|
|
2481
|
-
const
|
|
2482
|
-
const
|
|
2483
|
-
for (const b32 of param.value) {
|
|
2484
|
-
const slot = new Uint8Array(32);
|
|
2485
|
-
slot.set(b32.slice(0, 32));
|
|
2486
|
-
parts.push(slot);
|
|
2487
|
-
}
|
|
2555
|
+
const parts = [bigintTo32Be(BigInt(param.value.length))];
|
|
2556
|
+
for (const b32 of param.value) parts.push(bytes32Slot(b32));
|
|
2488
2557
|
return concat(parts);
|
|
2489
2558
|
}
|
|
2490
2559
|
case "address[]": {
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2493
|
-
for (const addr of param.value) {
|
|
2494
|
-
const slot = new Uint8Array(32);
|
|
2495
|
-
const clean = addr.startsWith("0x") ? addr.slice(2) : addr;
|
|
2496
|
-
const bytes = fromHex("0x" + clean.padStart(40, "0"));
|
|
2497
|
-
slot.set(bytes, 12);
|
|
2498
|
-
parts.push(slot);
|
|
2499
|
-
}
|
|
2560
|
+
const parts = [bigintTo32Be(BigInt(param.value.length))];
|
|
2561
|
+
for (const addr of param.value) parts.push(addressSlot(addr));
|
|
2500
2562
|
return concat(parts);
|
|
2501
2563
|
}
|
|
2502
2564
|
case "bytes[]": {
|
|
@@ -2552,6 +2614,122 @@ function decodeUint(data, offset = 0) {
|
|
|
2552
2614
|
return result;
|
|
2553
2615
|
}
|
|
2554
2616
|
|
|
2617
|
+
// src/chain/evm/precompiles/shieldedPoolCalldata.ts
|
|
2618
|
+
var CLAIM_PUBLIC_SIGNALS_SIZE = 76;
|
|
2619
|
+
function bytes32(hex, field) {
|
|
2620
|
+
if (!isHexOfLength(hex, 32)) {
|
|
2621
|
+
throw new Error(`${field}: expected a 0x-prefixed 32-byte hex string, got ${hex}`);
|
|
2622
|
+
}
|
|
2623
|
+
return fromHex(hex);
|
|
2624
|
+
}
|
|
2625
|
+
function uint32(value, field) {
|
|
2626
|
+
if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
|
|
2627
|
+
throw new Error(`${field}: expected a uint32 (0..4294967295), got ${value}`);
|
|
2628
|
+
}
|
|
2629
|
+
return BigInt(value);
|
|
2630
|
+
}
|
|
2631
|
+
function accountId32(address, field) {
|
|
2632
|
+
const raw = address.startsWith("0x") ? address.slice(2) : address;
|
|
2633
|
+
if (raw.length > 64) {
|
|
2634
|
+
throw new Error(`${field}: expected at most 32 bytes, got ${raw.length / 2}`);
|
|
2635
|
+
}
|
|
2636
|
+
return bytes32("0x" + raw.padEnd(64, "0"), field);
|
|
2637
|
+
}
|
|
2638
|
+
function buildShieldCalldata(params) {
|
|
2639
|
+
EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
|
|
2640
|
+
const commitment = bytes32(params.commitment, "buildShieldCalldata.commitment");
|
|
2641
|
+
return encodeHex(
|
|
2642
|
+
SP_SEL.SHIELD,
|
|
2643
|
+
{ type: "uint", value: uint32(params.assetId, "buildShieldCalldata.assetId") },
|
|
2644
|
+
{ type: "bytes32", value: commitment },
|
|
2645
|
+
{ type: "bytes", value: params.encryptedMemo }
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
function buildPrivateTransferCalldata(params) {
|
|
2649
|
+
const nullifiers = params.inputs.map(
|
|
2650
|
+
(input, i) => bytes32(input.nullifier, `buildPrivateTransferCalldata.inputs[${i}].nullifier`)
|
|
2651
|
+
);
|
|
2652
|
+
const commitments = params.outputs.map(
|
|
2653
|
+
(output, i) => bytes32(output.commitment, `buildPrivateTransferCalldata.outputs[${i}].commitment`)
|
|
2654
|
+
);
|
|
2655
|
+
const memos = params.outputs.map((output, i) => {
|
|
2656
|
+
EncryptedMemo.validate(
|
|
2657
|
+
output.encryptedMemo,
|
|
2658
|
+
`buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
|
|
2659
|
+
);
|
|
2660
|
+
return output.encryptedMemo;
|
|
2661
|
+
});
|
|
2662
|
+
const root = bytes32(params.merkleRoot, "buildPrivateTransferCalldata.merkleRoot");
|
|
2663
|
+
return encodeHex(
|
|
2664
|
+
SP_SEL.PRIVATE_TRANSFER,
|
|
2665
|
+
{ type: "bytes", value: params.proof },
|
|
2666
|
+
{ type: "bytes32", value: root },
|
|
2667
|
+
{ type: "bytes32[]", value: nullifiers },
|
|
2668
|
+
{ type: "bytes32[]", value: commitments },
|
|
2669
|
+
{ type: "bytes[]", value: memos },
|
|
2670
|
+
{ type: "uint", value: uint32(params.assetId, "buildPrivateTransferCalldata.assetId") },
|
|
2671
|
+
{ type: "uint", value: params.fee ?? 0n },
|
|
2672
|
+
{
|
|
2673
|
+
type: "uint",
|
|
2674
|
+
value: uint32(params.circuitVersion, "buildPrivateTransferCalldata.circuitVersion")
|
|
2675
|
+
}
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
function buildUnshieldCalldata(params) {
|
|
2679
|
+
const root = bytes32(params.merkleRoot, "buildUnshieldCalldata.merkleRoot");
|
|
2680
|
+
const nullifier = bytes32(params.nullifier, "buildUnshieldCalldata.nullifier");
|
|
2681
|
+
const recipient = accountId32(
|
|
2682
|
+
params.recipientAddress,
|
|
2683
|
+
"buildUnshieldCalldata.recipientAddress"
|
|
2684
|
+
);
|
|
2685
|
+
const changeCommitment = bytes32(
|
|
2686
|
+
params.changeCommitment ?? "0x" + "00".repeat(32),
|
|
2687
|
+
"buildUnshieldCalldata.changeCommitment"
|
|
2688
|
+
);
|
|
2689
|
+
const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
|
|
2690
|
+
return encodeHex(
|
|
2691
|
+
SP_SEL.UNSHIELD,
|
|
2692
|
+
{ type: "bytes", value: params.proof },
|
|
2693
|
+
{ type: "bytes32", value: root },
|
|
2694
|
+
{ type: "bytes32", value: nullifier },
|
|
2695
|
+
{ type: "uint", value: uint32(params.assetId, "buildUnshieldCalldata.assetId") },
|
|
2696
|
+
{ type: "uint", value: params.amount },
|
|
2697
|
+
{ type: "bytes32", value: recipient },
|
|
2698
|
+
{ type: "uint", value: params.fee ?? 0n },
|
|
2699
|
+
{ type: "bytes32", value: changeCommitment },
|
|
2700
|
+
{ type: "bytes", value: changeEncryptedMemo },
|
|
2701
|
+
{
|
|
2702
|
+
type: "uint",
|
|
2703
|
+
value: uint32(params.circuitVersion, "buildUnshieldCalldata.circuitVersion")
|
|
2704
|
+
}
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
function buildClaimShieldedFeesCalldata(params) {
|
|
2708
|
+
EncryptedMemo.validate(params.encryptedMemo, "buildClaimShieldedFeesCalldata.encryptedMemo");
|
|
2709
|
+
if (params.proof.length === 0) {
|
|
2710
|
+
throw new Error("claimShieldedFees: proof must not be empty");
|
|
2711
|
+
}
|
|
2712
|
+
if (params.publicSignals.length !== CLAIM_PUBLIC_SIGNALS_SIZE) {
|
|
2713
|
+
throw new Error(
|
|
2714
|
+
`claimShieldedFees: publicSignals must be ${CLAIM_PUBLIC_SIGNALS_SIZE} bytes, got ${params.publicSignals.length}`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
const commitment = bytes32(params.commitment, "buildClaimShieldedFeesCalldata.commitment");
|
|
2718
|
+
return encodeHex(
|
|
2719
|
+
SP_SEL.CLAIM_SHIELDED_FEES,
|
|
2720
|
+
{ type: "bytes32", value: commitment },
|
|
2721
|
+
{ type: "uint", value: params.amount },
|
|
2722
|
+
{ type: "uint", value: uint32(params.assetId, "buildClaimShieldedFeesCalldata.assetId") },
|
|
2723
|
+
{ type: "bytes", value: params.encryptedMemo },
|
|
2724
|
+
{ type: "bytes", value: params.proof },
|
|
2725
|
+
{ type: "bytes", value: params.publicSignals },
|
|
2726
|
+
{
|
|
2727
|
+
type: "uint",
|
|
2728
|
+
value: uint32(params.circuitVersion, "buildClaimShieldedFeesCalldata.circuitVersion")
|
|
2729
|
+
}
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2555
2733
|
// src/chain/evm/precompiles/ShieldedPoolPrecompile.ts
|
|
2556
2734
|
var ShieldedPoolPrecompile = class {
|
|
2557
2735
|
constructor(evm) {
|
|
@@ -2559,228 +2737,127 @@ var ShieldedPoolPrecompile = class {
|
|
|
2559
2737
|
}
|
|
2560
2738
|
evm;
|
|
2561
2739
|
addr = PRECOMPILE_ADDR.SHIELDED_POOL;
|
|
2562
|
-
// ───
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
*/
|
|
2740
|
+
// ─── Calldata ────────────────────────────────────────────────────────────
|
|
2741
|
+
//
|
|
2742
|
+
// Thin delegates to `shieldedPoolCalldata`, kept because they are public
|
|
2743
|
+
// API. New code should import those functions directly: they are pure, so
|
|
2744
|
+
// using them needs no `EvmClient` to construct.
|
|
2568
2745
|
buildShieldCalldata(params) {
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2746
|
+
return buildShieldCalldata(params);
|
|
2747
|
+
}
|
|
2748
|
+
buildPrivateTransferCalldata(params) {
|
|
2749
|
+
return buildPrivateTransferCalldata(params);
|
|
2750
|
+
}
|
|
2751
|
+
buildUnshieldCalldata(params) {
|
|
2752
|
+
return buildUnshieldCalldata(params);
|
|
2753
|
+
}
|
|
2754
|
+
buildClaimShieldedFeesCalldata(params) {
|
|
2755
|
+
return buildClaimShieldedFeesCalldata(params);
|
|
2577
2756
|
}
|
|
2757
|
+
// ─── shield ──────────────────────────────────────────────────────────────
|
|
2578
2758
|
/**
|
|
2579
2759
|
* Deposits tokens into the shielded pool from a payable EVM transaction.
|
|
2580
2760
|
*
|
|
2581
|
-
* The
|
|
2582
|
-
* the
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
* precompile → pool (via pallet transfer)
|
|
2586
|
-
* This avoids double-deduction while keeping the displayed amount accurate.
|
|
2761
|
+
* The amount rides as `msg.value` so EVM wallets show the correct figure on
|
|
2762
|
+
* the confirmation screen. The precompile then dispatches with its OWN
|
|
2763
|
+
* address as origin, so funds flow caller → precompile → pool. That avoids
|
|
2764
|
+
* a double deduction while keeping the displayed amount accurate.
|
|
2587
2765
|
*
|
|
2588
2766
|
* Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
|
|
2589
2767
|
*/
|
|
2590
2768
|
async shield(params, signer) {
|
|
2591
2769
|
return signer({
|
|
2592
2770
|
to: this.addr,
|
|
2593
|
-
data:
|
|
2771
|
+
data: buildShieldCalldata(params),
|
|
2594
2772
|
value: params.amount
|
|
2595
2773
|
});
|
|
2596
2774
|
}
|
|
2597
|
-
// ─── privateTransfer
|
|
2775
|
+
// ─── privateTransfer ─────────────────────────────────────────────────────
|
|
2598
2776
|
/**
|
|
2599
|
-
*
|
|
2600
|
-
* `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256, uint32)`.
|
|
2601
|
-
* The trailing `uint32` is the circuit version the input notes were created under.
|
|
2602
|
-
*/
|
|
2603
|
-
buildPrivateTransferCalldata(params) {
|
|
2604
|
-
const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
|
|
2605
|
-
const commitments = params.outputs.map((o) => fromHex(o.commitment));
|
|
2606
|
-
const memos = params.outputs.map((o, i) => {
|
|
2607
|
-
EncryptedMemo.validate(
|
|
2608
|
-
o.encryptedMemo,
|
|
2609
|
-
`buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
|
|
2610
|
-
);
|
|
2611
|
-
return o.encryptedMemo;
|
|
2612
|
-
});
|
|
2613
|
-
const root = fromHex(params.merkleRoot);
|
|
2614
|
-
return encodeHex(
|
|
2615
|
-
SP_SEL.PRIVATE_TRANSFER,
|
|
2616
|
-
{ type: "bytes", value: params.proof },
|
|
2617
|
-
{ type: "bytes32", value: root },
|
|
2618
|
-
{ type: "bytes32[]", value: nullifiers },
|
|
2619
|
-
{ type: "bytes32[]", value: commitments },
|
|
2620
|
-
{ type: "bytes[]", value: memos },
|
|
2621
|
-
{ type: "uint", value: BigInt(params.assetId) },
|
|
2622
|
-
{ type: "uint", value: params.fee ?? 0n },
|
|
2623
|
-
{ type: "uint", value: BigInt(params.circuitVersion) }
|
|
2624
|
-
);
|
|
2625
|
-
}
|
|
2626
|
-
/**
|
|
2627
|
-
* Submits a private transfer within the shielded pool from an EVM transaction.
|
|
2777
|
+
* Submits a private transfer within the shielded pool.
|
|
2628
2778
|
*
|
|
2629
|
-
* The EVM caller identity is
|
|
2630
|
-
* hidden by design
|
|
2779
|
+
* The EVM caller identity is IRRELEVANT to the ZK proof — the sender is
|
|
2780
|
+
* hidden by design, so any address (a relayer included) can submit a valid
|
|
2781
|
+
* proof.
|
|
2631
2782
|
*
|
|
2632
|
-
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
2783
|
+
* Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
|
|
2784
|
+
* commitments, memos, assetId, fee, circuitVersion)` — eight arguments; see
|
|
2785
|
+
* `buildPrivateTransferCalldata` for the encoding order.
|
|
2633
2786
|
*/
|
|
2634
2787
|
async privateTransfer(params, signer) {
|
|
2635
|
-
return signer({ to: this.addr, data:
|
|
2636
|
-
}
|
|
2637
|
-
// ─── unshield ──────────────────────────────────────────────────────────────
|
|
2638
|
-
/**
|
|
2639
|
-
* Params for an `unshield` call via the EVM precompile.
|
|
2640
|
-
* The `recipient` is a full 32-byte AccountId32 (Substrate account or
|
|
2641
|
-
* EeSuffix-derived: `H160 ++ [0x00; 12]`).
|
|
2642
|
-
*/
|
|
2643
|
-
buildUnshieldCalldata(params) {
|
|
2644
|
-
const proof = params.proof;
|
|
2645
|
-
const root = fromHex(params.merkleRoot);
|
|
2646
|
-
const nullifier = fromHex(params.nullifier);
|
|
2647
|
-
const recipientRaw = params.recipientAddress.startsWith("0x") ? params.recipientAddress.slice(2) : params.recipientAddress;
|
|
2648
|
-
const recipientBytes = fromHex(
|
|
2649
|
-
"0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
|
|
2650
|
-
);
|
|
2651
|
-
const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
|
|
2652
|
-
const changeCommitment = fromHex(changeCommitmentHex);
|
|
2653
|
-
const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
|
|
2654
|
-
return encodeHex(
|
|
2655
|
-
SP_SEL.UNSHIELD,
|
|
2656
|
-
{ type: "bytes", value: proof },
|
|
2657
|
-
{ type: "bytes32", value: root },
|
|
2658
|
-
{ type: "bytes32", value: nullifier },
|
|
2659
|
-
{ type: "uint", value: BigInt(params.assetId) },
|
|
2660
|
-
{ type: "uint", value: params.amount },
|
|
2661
|
-
{ type: "bytes32", value: recipientBytes },
|
|
2662
|
-
{ type: "uint", value: params.fee ?? 0n },
|
|
2663
|
-
{ type: "bytes32", value: changeCommitment },
|
|
2664
|
-
{ type: "bytes", value: changeEncryptedMemo },
|
|
2665
|
-
{ type: "uint", value: BigInt(params.circuitVersion) }
|
|
2666
|
-
);
|
|
2788
|
+
return signer({ to: this.addr, data: buildPrivateTransferCalldata(params) });
|
|
2667
2789
|
}
|
|
2790
|
+
// ─── unshield ────────────────────────────────────────────────────────────
|
|
2668
2791
|
/**
|
|
2669
2792
|
* Withdraws tokens from the shielded pool to a recipient account.
|
|
2670
2793
|
*
|
|
2671
|
-
* `params.recipientAddress` must be a 0x-prefixed
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2794
|
+
* `params.recipientAddress` must be a 0x-prefixed AccountId32. To send to an
|
|
2795
|
+
* EVM address, derive it first with `evmToImplicitSubstrate(evmAddr)`.
|
|
2796
|
+
*
|
|
2797
|
+
* Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
|
|
2798
|
+
* amount, recipient, fee, changeCommitment, changeEncryptedMemo,
|
|
2799
|
+
* circuitVersion)` — ten arguments; see `buildUnshieldCalldata`.
|
|
2674
2800
|
*
|
|
2675
|
-
*
|
|
2801
|
+
* **The relay fee goes to whoever `signer` is.** The chain takes the recipient
|
|
2802
|
+
* from `msg.sender`, not from calldata, so the account behind this signer is
|
|
2803
|
+
* the one credited — and it is also the one paying gas. Relaying on someone
|
|
2804
|
+
* else's behalf and being paid for it is the same act here.
|
|
2676
2805
|
*/
|
|
2677
2806
|
async unshield(params, signer) {
|
|
2678
|
-
return signer({ to: this.addr, data:
|
|
2807
|
+
return signer({ to: this.addr, data: buildUnshieldCalldata(params) });
|
|
2679
2808
|
}
|
|
2680
|
-
// ───
|
|
2809
|
+
// ─── claimShieldedFees ───────────────────────────────────────────────────
|
|
2681
2810
|
/**
|
|
2682
|
-
*
|
|
2683
|
-
*
|
|
2811
|
+
* Claims accrued relay fees as a private shielded note.
|
|
2812
|
+
*
|
|
2813
|
+
* For validators/relayers holding fees in `pallet-relayer` who want them
|
|
2814
|
+
* paid privately into the shielded pool rather than as a public balance
|
|
2815
|
+
* credit. The ZK `value_proof` binds `commitment` to
|
|
2816
|
+
* `(amount, assetId, ownerPk, blinding)`, so the runtime can verify the note
|
|
2817
|
+
* encodes exactly the claimed amount and a malicious relayer cannot inflate
|
|
2818
|
+
* the withdrawal.
|
|
2819
|
+
*
|
|
2820
|
+
* The `msg.sender` address is the validator identity, and must match the
|
|
2821
|
+
* one with pending fees.
|
|
2822
|
+
*
|
|
2823
|
+
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
|
|
2824
|
+
* memo, proof, publicSignals, circuitVersion)` — seven arguments; see
|
|
2825
|
+
* `buildClaimShieldedFeesCalldata`.
|
|
2684
2826
|
*/
|
|
2685
|
-
async
|
|
2686
|
-
return
|
|
2687
|
-
from,
|
|
2827
|
+
async claimShieldedFees(params, signer) {
|
|
2828
|
+
return signer({
|
|
2688
2829
|
to: this.addr,
|
|
2689
|
-
data:
|
|
2830
|
+
data: buildClaimShieldedFeesCalldata(params)
|
|
2690
2831
|
});
|
|
2691
2832
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2833
|
+
// ─── Gas estimation ──────────────────────────────────────────────────────
|
|
2834
|
+
//
|
|
2835
|
+
// `from` must be the real sender: the precompile resolves it to an
|
|
2836
|
+
// AccountId32, so estimating from a different address measures a different
|
|
2837
|
+
// call.
|
|
2838
|
+
async estimateShieldGas(params, from) {
|
|
2696
2839
|
return this.evm.estimateGas({
|
|
2697
2840
|
from,
|
|
2698
2841
|
to: this.addr,
|
|
2699
|
-
data:
|
|
2842
|
+
data: buildShieldCalldata(params),
|
|
2843
|
+
value: `0x${params.amount.toString(16)}`
|
|
2700
2844
|
});
|
|
2701
2845
|
}
|
|
2702
|
-
|
|
2703
|
-
* Estimates the EVM gas for an `unshield` call.
|
|
2704
|
-
*/
|
|
2705
|
-
async estimateUnshieldGas(params, from) {
|
|
2846
|
+
async estimatePrivateTransferGas(params, from) {
|
|
2706
2847
|
return this.evm.estimateGas({
|
|
2707
2848
|
from,
|
|
2708
2849
|
to: this.addr,
|
|
2709
|
-
data:
|
|
2850
|
+
data: buildPrivateTransferCalldata(params)
|
|
2710
2851
|
});
|
|
2711
2852
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
* Returns the ABI-encoded calldata for
|
|
2715
|
-
* `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)`.
|
|
2716
|
-
*
|
|
2717
|
-
* ABI layout (params after selector):
|
|
2718
|
-
* - `commitment` — bytes32 (fixed)
|
|
2719
|
-
* - `amount` — uint256 (fixed)
|
|
2720
|
-
* - `asset_id` — uint32 (fixed, right-aligned)
|
|
2721
|
-
* - `memo` — bytes (dynamic)
|
|
2722
|
-
* - `proof` — bytes (dynamic, 128 bytes Groth16)
|
|
2723
|
-
* - `publicSignals` — bytes (dynamic, 76 bytes)
|
|
2724
|
-
* - `circuitVersion` — uint32 (fixed, right-aligned)
|
|
2725
|
-
*
|
|
2726
|
-
* The validator identity is derived from `msg.sender` in the precompile —
|
|
2727
|
-
* do NOT include it in the calldata.
|
|
2728
|
-
*/
|
|
2729
|
-
buildClaimShieldedFeesCalldata(params) {
|
|
2730
|
-
EncryptedMemo.validate(
|
|
2731
|
-
params.encryptedMemo,
|
|
2732
|
-
"buildClaimShieldedFeesCalldata.encryptedMemo"
|
|
2733
|
-
);
|
|
2734
|
-
if (params.proof.length === 0) {
|
|
2735
|
-
throw new Error("claimShieldedFees: proof must not be empty");
|
|
2736
|
-
}
|
|
2737
|
-
if (params.publicSignals.length !== 76) {
|
|
2738
|
-
throw new Error(
|
|
2739
|
-
`claimShieldedFees: publicSignals must be 76 bytes, got ${params.publicSignals.length}`
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
const commitment = fromHex(params.commitment);
|
|
2743
|
-
return encodeHex(
|
|
2744
|
-
SP_SEL.CLAIM_SHIELDED_FEES,
|
|
2745
|
-
{ type: "bytes32", value: commitment },
|
|
2746
|
-
{ type: "uint", value: params.amount },
|
|
2747
|
-
{ type: "uint", value: BigInt(params.assetId) },
|
|
2748
|
-
{ type: "bytes", value: params.encryptedMemo },
|
|
2749
|
-
{ type: "bytes", value: params.proof },
|
|
2750
|
-
{ type: "bytes", value: params.publicSignals },
|
|
2751
|
-
{ type: "uint", value: BigInt(params.circuitVersion) }
|
|
2752
|
-
);
|
|
2753
|
-
}
|
|
2754
|
-
/**
|
|
2755
|
-
* Claims accumulated relay fees as a private shielded note.
|
|
2756
|
-
*
|
|
2757
|
-
* This extrinsic is for **validators/relayers** who have accrued fees in
|
|
2758
|
-
* `pallet-relayer` and want to receive them privately inside the shielded pool
|
|
2759
|
-
* instead of as a public balance credit.
|
|
2760
|
-
*
|
|
2761
|
-
* The ZK `value_proof` binds `commitment` to `(amount, assetId, ownerPk, blinding)`
|
|
2762
|
-
* so the runtime can verify the note encodes exactly the claimed fee amount,
|
|
2763
|
-
* preventing a malicious relayer from inflating the withdrawal.
|
|
2764
|
-
*
|
|
2765
|
-
* The `msg.sender` EVM address is used as the validator identity; it must match
|
|
2766
|
-
* the address that has pending relay fees in `pallet-relayer`.
|
|
2767
|
-
*
|
|
2768
|
-
* Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId, memo, proof, publicSignals)`
|
|
2769
|
-
*/
|
|
2770
|
-
async claimShieldedFees(params, signer) {
|
|
2771
|
-
return signer({
|
|
2772
|
-
to: this.addr,
|
|
2773
|
-
data: this.buildClaimShieldedFeesCalldata(params)
|
|
2774
|
-
});
|
|
2853
|
+
async estimateUnshieldGas(params, from) {
|
|
2854
|
+
return this.evm.estimateGas({ from, to: this.addr, data: buildUnshieldCalldata(params) });
|
|
2775
2855
|
}
|
|
2776
|
-
/**
|
|
2777
|
-
* Estimates the EVM gas for a `claimShieldedFees` call.
|
|
2778
|
-
*/
|
|
2779
2856
|
async estimateClaimShieldedFeesGas(params, from) {
|
|
2780
2857
|
return this.evm.estimateGas({
|
|
2781
2858
|
from,
|
|
2782
2859
|
to: this.addr,
|
|
2783
|
-
data:
|
|
2860
|
+
data: buildClaimShieldedFeesCalldata(params)
|
|
2784
2861
|
});
|
|
2785
2862
|
}
|
|
2786
2863
|
};
|
|
@@ -2952,7 +3029,7 @@ var OrbinumClient = class _OrbinumClient {
|
|
|
2952
3029
|
*/
|
|
2953
3030
|
static async connect(config) {
|
|
2954
3031
|
const substrate = config.papi ? SubstrateClient.adopt(config.papi, config.substrateHttp) : await SubstrateClient.connect(config.substrateWs, config.connectTimeoutMs ?? 15e3);
|
|
2955
|
-
const evm = config.evmRpc ? new EvmClient(config.evmRpc) : null;
|
|
3032
|
+
const evm = config.evmRpc ? new EvmClient(config.evmRpc, config.evmRpcPeer) : null;
|
|
2956
3033
|
return new _OrbinumClient(substrate, evm, config.circuitsBaseUrl);
|
|
2957
3034
|
}
|
|
2958
3035
|
/**
|
|
@@ -3092,6 +3169,7 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
|
|
|
3092
3169
|
connectTimeoutMs: this.connectTimeoutMs
|
|
3093
3170
|
};
|
|
3094
3171
|
if (this.config.evmRpc) connectConfig.evmRpc = this.config.evmRpc;
|
|
3172
|
+
if (this.config.evmRpcPeer) connectConfig.evmRpcPeer = this.config.evmRpcPeer;
|
|
3095
3173
|
if (this.config.circuitsBaseUrl)
|
|
3096
3174
|
connectConfig.circuitsBaseUrl = this.config.circuitsBaseUrl;
|
|
3097
3175
|
clientPromise = OrbinumClient.connect(connectConfig);
|
|
@@ -3877,9 +3955,13 @@ function methodOf(fnSig) {
|
|
|
3877
3955
|
if (fnSig.startsWith("unshield(")) return "unshield";
|
|
3878
3956
|
if (fnSig.startsWith("privateTransfer(")) return "privateTransfer";
|
|
3879
3957
|
if (fnSig.startsWith("shieldBatch(")) return "shieldBatch";
|
|
3958
|
+
if (fnSig.startsWith("claimShieldedFees(")) return "claimShieldedFees";
|
|
3880
3959
|
if (fnSig.startsWith("shield(")) return "shield";
|
|
3881
3960
|
return null;
|
|
3882
3961
|
}
|
|
3962
|
+
function hasFullHead(data, slots) {
|
|
3963
|
+
return data.length >= slots * 32;
|
|
3964
|
+
}
|
|
3883
3965
|
function decodePrecompileCalldata(address, input) {
|
|
3884
3966
|
const info = KNOWN_PRECOMPILES[address.toLowerCase()];
|
|
3885
3967
|
if (!info || !input || input.length < 10) return null;
|
|
@@ -3889,6 +3971,7 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3889
3971
|
if (fnSig.startsWith("shield(")) {
|
|
3890
3972
|
try {
|
|
3891
3973
|
const data = fromHex(input.slice(10));
|
|
3974
|
+
if (!hasFullHead(data, 3)) return { fnSig, method: methodOf(fnSig), args: {} };
|
|
3892
3975
|
const assetId = decodeUint(data, 0);
|
|
3893
3976
|
const commitment = toHex(data.slice(32, 64));
|
|
3894
3977
|
return { fnSig, method: methodOf(fnSig), args: { assetId, commitment } };
|
|
@@ -3899,6 +3982,7 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3899
3982
|
if (fnSig.startsWith("unshield(")) {
|
|
3900
3983
|
try {
|
|
3901
3984
|
const data = fromHex(input.slice(10));
|
|
3985
|
+
if (!hasFullHead(data, 10)) return { fnSig, method: methodOf(fnSig), args: {} };
|
|
3902
3986
|
const root = toHex(data.slice(32, 64));
|
|
3903
3987
|
const nullifier = toHex(data.slice(64, 96));
|
|
3904
3988
|
const assetId = decodeUint(data, 96);
|
|
@@ -3928,18 +4012,25 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3928
4012
|
if (fnSig.startsWith("privateTransfer(")) {
|
|
3929
4013
|
try {
|
|
3930
4014
|
const data = fromHex(input.slice(10));
|
|
4015
|
+
if (!hasFullHead(data, 8)) return { fnSig, method: methodOf(fnSig), args: {} };
|
|
3931
4016
|
const root = toHex(data.slice(32, 64));
|
|
3932
|
-
const nullOffset = Number(decodeUint(data, 64));
|
|
3933
|
-
const commOffset = Number(decodeUint(data, 96));
|
|
3934
|
-
const nullifiers = Number(decodeUint(data, nullOffset));
|
|
3935
|
-
const commitments = Number(decodeUint(data, commOffset));
|
|
3936
4017
|
const assetId = decodeUint(data, 160);
|
|
3937
4018
|
const fee = decodeUint(data, 192);
|
|
3938
4019
|
const circuitVersion = decodeUint(data, 224);
|
|
4020
|
+
const counts = {};
|
|
4021
|
+
for (const [name, slot] of [
|
|
4022
|
+
["nullifiers", 64],
|
|
4023
|
+
["commitments", 96]
|
|
4024
|
+
]) {
|
|
4025
|
+
const offset = decodeUint(data, slot);
|
|
4026
|
+
if (offset <= BigInt(data.length - 32)) {
|
|
4027
|
+
counts[name] = Number(decodeUint(data, Number(offset)));
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
3939
4030
|
return {
|
|
3940
4031
|
fnSig,
|
|
3941
4032
|
method: methodOf(fnSig),
|
|
3942
|
-
args: { root,
|
|
4033
|
+
args: { root, ...counts, assetId, fee, circuitVersion }
|
|
3943
4034
|
};
|
|
3944
4035
|
} catch {
|
|
3945
4036
|
return { fnSig, method: methodOf(fnSig), args: {} };
|
|
@@ -3948,6 +4039,7 @@ function decodePrecompileCalldata(address, input) {
|
|
|
3948
4039
|
if (fnSig.startsWith("claimShieldedFees(")) {
|
|
3949
4040
|
try {
|
|
3950
4041
|
const data = fromHex(input.slice(10));
|
|
4042
|
+
if (!hasFullHead(data, 7)) return { fnSig, method: methodOf(fnSig), args: {} };
|
|
3951
4043
|
const commitment = toHex(data.slice(0, 32));
|
|
3952
4044
|
const amount = decodeUint(data, 32);
|
|
3953
4045
|
const assetId = decodeUint(data, 64);
|
|
@@ -4154,7 +4246,7 @@ var MemoryVaultStorage = class {
|
|
|
4154
4246
|
};
|
|
4155
4247
|
|
|
4156
4248
|
// src/wallet/vault/storage/config.ts
|
|
4157
|
-
var VAULT_SCHEMA_VERSION =
|
|
4249
|
+
var VAULT_SCHEMA_VERSION = 5;
|
|
4158
4250
|
function normalizeChainFingerprint(chainFingerprint) {
|
|
4159
4251
|
return chainFingerprint ? chainFingerprint.toLowerCase() : void 0;
|
|
4160
4252
|
}
|
|
@@ -4303,7 +4395,7 @@ async function noteBlindTag(blindKey, hex) {
|
|
|
4303
4395
|
|
|
4304
4396
|
// src/wallet/vault/notes/meta.ts
|
|
4305
4397
|
function noteOrigin(note) {
|
|
4306
|
-
return note.
|
|
4398
|
+
return note.sourcePk === 0n ? "shield" : "private-transfer";
|
|
4307
4399
|
}
|
|
4308
4400
|
function noteCreatedAt(note) {
|
|
4309
4401
|
return note.createdAt ?? null;
|
|
@@ -4372,9 +4464,9 @@ var NOTE_BIGINT_FIELDS = [
|
|
|
4372
4464
|
"spendingKey",
|
|
4373
4465
|
"commitment",
|
|
4374
4466
|
"nullifier",
|
|
4375
|
-
"
|
|
4467
|
+
"sourcePk"
|
|
4376
4468
|
];
|
|
4377
|
-
var ABSENT_MEANS_ZERO = /* @__PURE__ */ new Set(["
|
|
4469
|
+
var ABSENT_MEANS_ZERO = /* @__PURE__ */ new Set(["sourcePk"]);
|
|
4378
4470
|
function normalizeNote(note) {
|
|
4379
4471
|
let patch = null;
|
|
4380
4472
|
for (const field of NOTE_BIGINT_FIELDS) {
|
|
@@ -4627,12 +4719,12 @@ var VaultStore = class {
|
|
|
4627
4719
|
const { blindKey } = this.keys();
|
|
4628
4720
|
if (commitmentHexes.length === 0) return 0;
|
|
4629
4721
|
const toRemove = new Set(commitmentHexes);
|
|
4630
|
-
const
|
|
4631
|
-
if (
|
|
4632
|
-
const tags = await Promise.all(
|
|
4722
|
+
const present2 = this.deps.notes.get().filter((n) => toRemove.has(n.commitmentHex));
|
|
4723
|
+
if (present2.length === 0) return 0;
|
|
4724
|
+
const tags = await Promise.all(present2.map((n) => noteBlindTag(blindKey, n.commitmentHex)));
|
|
4633
4725
|
await this.deps.storage.deleteNotes(tags);
|
|
4634
4726
|
this.deps.notes.set(removeByCommitment(this.deps.notes.get(), toRemove));
|
|
4635
|
-
return
|
|
4727
|
+
return present2.length;
|
|
4636
4728
|
}
|
|
4637
4729
|
/**
|
|
4638
4730
|
* Stores an outgoing transaction, encrypted.
|
|
@@ -5223,26 +5315,147 @@ function parseFeeArg(argsJson) {
|
|
|
5223
5315
|
}
|
|
5224
5316
|
}
|
|
5225
5317
|
|
|
5318
|
+
// src/wallet/provenance/selectDescribingNote.ts
|
|
5319
|
+
function hasSourcePk(note) {
|
|
5320
|
+
return typeof note.sourcePk === "bigint" && note.sourcePk !== 0n;
|
|
5321
|
+
}
|
|
5322
|
+
function selectDescribingNote(candidates) {
|
|
5323
|
+
return candidates.find(hasSourcePk) ?? candidates[0];
|
|
5324
|
+
}
|
|
5325
|
+
function selectDescribingNoteByCommitment(commitments, noteByCommitment) {
|
|
5326
|
+
const owned = commitments.map((hex) => noteByCommitment.get(hex)).filter((note) => note !== void 0);
|
|
5327
|
+
return selectDescribingNote(owned);
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5330
|
+
// src/wallet/provenance/merge.ts
|
|
5331
|
+
var SOURCE_RANK = {
|
|
5332
|
+
witnessed: 3,
|
|
5333
|
+
memo: 2,
|
|
5334
|
+
chain: 1,
|
|
5335
|
+
inferred: 0
|
|
5336
|
+
};
|
|
5337
|
+
function rankOf(source) {
|
|
5338
|
+
return SOURCE_RANK[source] ?? -1;
|
|
5339
|
+
}
|
|
5340
|
+
function outranks(a, b) {
|
|
5341
|
+
return rankOf(a) > rankOf(b);
|
|
5342
|
+
}
|
|
5343
|
+
function mergeProvenance(existing, incoming) {
|
|
5344
|
+
if (existing.id !== incoming.id) {
|
|
5345
|
+
throw new Error(
|
|
5346
|
+
`mergeProvenance: id mismatch \u2014 refusing to merge ${existing.id} with ${incoming.id}`
|
|
5347
|
+
);
|
|
5348
|
+
}
|
|
5349
|
+
const incomingWins = outranks(incoming.source, existing.source);
|
|
5350
|
+
const base = incomingWins ? incoming : existing;
|
|
5351
|
+
const other = incomingWins ? existing : incoming;
|
|
5352
|
+
const fee = base.feePlanck ?? other.feePlanck;
|
|
5353
|
+
const publicRecipient = firstPresent(base.publicRecipient, other.publicRecipient);
|
|
5354
|
+
const slip = present(base.slip?.encoded) ? base.slip : other.slip ?? base.slip;
|
|
5355
|
+
const note = base.note ?? other.note;
|
|
5356
|
+
return {
|
|
5357
|
+
// The loser is spread FIRST so a field only it carries survives. A host
|
|
5358
|
+
// stores its own record type through this — `ReconstructedTxRecord` has
|
|
5359
|
+
// `amountApproximate`, which marks an amount derived without subtracting
|
|
5360
|
+
// the fee — and dropping such a field turns an approximate figure into
|
|
5361
|
+
// one that merely looks exact. Every key the winner knows about is
|
|
5362
|
+
// overwritten below, so rank still decides every shared fact.
|
|
5363
|
+
...other,
|
|
5364
|
+
...base,
|
|
5365
|
+
// A known peer beats an unknown one even when the winner is silent:
|
|
5366
|
+
// backfilling the recipient is the whole point of re-running this.
|
|
5367
|
+
// `scope: 'none'` is "this operation has no counterparty", so it is a
|
|
5368
|
+
// gap too — treating it as known would block the backfill it exists for.
|
|
5369
|
+
peer: copyPeer(knownPeer(base.peer) ?? knownPeer(other.peer) ?? base.peer ?? other.peer),
|
|
5370
|
+
// A number that stands for "not known yet" must not win by rank. Zero is
|
|
5371
|
+
// exactly that here: `RECOVERED_TX_RESULT` and a failed submission both
|
|
5372
|
+
// report block 0, and their own comment says a caller who needs the
|
|
5373
|
+
// value must look it up rather than trust the field.
|
|
5374
|
+
blockNumber: firstPositive(base.blockNumber, other.blockNumber),
|
|
5375
|
+
timestampMs: firstPositive(base.timestampMs, other.timestampMs),
|
|
5376
|
+
// Reconstruction writes an empty hash when the extrinsic was not decoded.
|
|
5377
|
+
hash: firstPresent(base.hash, other.hash) ?? base.hash,
|
|
5378
|
+
// `exact` is a property of the FIGURE, not of the source. A `witnessed`
|
|
5379
|
+
// row whose amount was marked approximate is not better data than an
|
|
5380
|
+
// exact one from a weaker source, so rank only breaks a tie between two
|
|
5381
|
+
// figures of equal standing.
|
|
5382
|
+
amount: { ...betterAmount(base.amount, other.amount) },
|
|
5383
|
+
// The chain's outcome is not something one source knows better than
|
|
5384
|
+
// another — anyone who looks sees the same thing. Letting rank decide
|
|
5385
|
+
// would let a row written at submit time mark a transaction failed that
|
|
5386
|
+
// the chain went on to accept.
|
|
5387
|
+
status: base.status === "success" || other.status === "success" ? "success" : "failed",
|
|
5388
|
+
...fee !== void 0 && { feePlanck: fee },
|
|
5389
|
+
...publicRecipient !== void 0 && { publicRecipient },
|
|
5390
|
+
...slip !== void 0 && { slip: { ...slip } },
|
|
5391
|
+
...note !== void 0 && { note: { ...note } }
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
5394
|
+
function betterAmount(base, other) {
|
|
5395
|
+
if (base.exact === other.exact) return base;
|
|
5396
|
+
return base.exact ? base : other;
|
|
5397
|
+
}
|
|
5398
|
+
function present(value) {
|
|
5399
|
+
return value !== void 0 && value.length > 0;
|
|
5400
|
+
}
|
|
5401
|
+
function firstPresent(base, other) {
|
|
5402
|
+
return present(base) ? base : present(other) ? other : void 0;
|
|
5403
|
+
}
|
|
5404
|
+
function firstPositive(base, other) {
|
|
5405
|
+
return base > 0 ? base : other > 0 ? other : base;
|
|
5406
|
+
}
|
|
5407
|
+
function copyPeer(peer) {
|
|
5408
|
+
return peer ? { ...peer } : null;
|
|
5409
|
+
}
|
|
5410
|
+
function knownPeer(peer) {
|
|
5411
|
+
return peer && peer.scope !== "none" ? peer : null;
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
// src/wallet/provenance/regenerateSlip.ts
|
|
5415
|
+
function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
|
|
5416
|
+
if (!isHexOfLength(facts.commitmentHex, 32)) {
|
|
5417
|
+
throw new Error("regeneratePaymentSlip: commitmentHex must be 32 bytes of hex");
|
|
5418
|
+
}
|
|
5419
|
+
if (!isHexOfLength(facts.encryptedMemo, ENCRYPTED_MEMO_SIZE)) {
|
|
5420
|
+
throw new Error(
|
|
5421
|
+
`regeneratePaymentSlip: encryptedMemo must be ${ENCRYPTED_MEMO_SIZE} bytes of hex`
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
if (facts.leafIndex !== void 0 && !isValidLeafIndex(facts.leafIndex)) {
|
|
5425
|
+
throw new Error("regeneratePaymentSlip: leafIndex must be a real tree position");
|
|
5426
|
+
}
|
|
5427
|
+
const envelope = sealPaymentSlip(recipientIvkPacked, {
|
|
5428
|
+
commitmentHex: facts.commitmentHex,
|
|
5429
|
+
encryptedMemo: facts.encryptedMemo,
|
|
5430
|
+
...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
|
|
5431
|
+
// Rendered as an explorer link by the recipient, where an unconstrained
|
|
5432
|
+
// string is a URL injection wearing the authority of a decrypted slip.
|
|
5433
|
+
// Dropped rather than fatal: the slip still rebuilds the note, which is
|
|
5434
|
+
// the part that matters.
|
|
5435
|
+
...isHexOfLength(txHash, 32) ? { txHash } : {}
|
|
5436
|
+
});
|
|
5437
|
+
return encodePaymentSlip(envelope);
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5226
5440
|
// src/wallet/scanner/history/reconstruct.ts
|
|
5227
5441
|
var ZERO_PK = "0x" + "00".repeat(32);
|
|
5228
5442
|
function extrinsicKey(row) {
|
|
5229
5443
|
return `${row.blockNumber}:${row.extrinsicIndex ?? "null"}`;
|
|
5230
5444
|
}
|
|
5231
|
-
function toPkHex(
|
|
5232
|
-
return
|
|
5233
|
-
}
|
|
5234
|
-
function findChangeNote(commitments, noteByCommitment) {
|
|
5235
|
-
const candidates = commitments.map((h) => noteByCommitment.get(h)).filter((n) => n !== void 0);
|
|
5236
|
-
return candidates.find((n) => n.counterpartyPk != null && n.counterpartyPk !== 0n) ?? candidates[0];
|
|
5445
|
+
function toPkHex(sourcePk) {
|
|
5446
|
+
return typeof sourcePk === "bigint" && hasSourcePk({ sourcePk }) ? scalarToHex(sourcePk) : ZERO_PK;
|
|
5237
5447
|
}
|
|
5238
5448
|
async function loadExistingRecords(vault) {
|
|
5239
5449
|
try {
|
|
5240
5450
|
const records = await vault.getTxRecords();
|
|
5241
|
-
return new Map(records.map((r) => [r.
|
|
5451
|
+
return new Map(records.map((r) => [r.id, r]));
|
|
5242
5452
|
} catch {
|
|
5243
5453
|
return /* @__PURE__ */ new Map();
|
|
5244
5454
|
}
|
|
5245
5455
|
}
|
|
5456
|
+
function recordKey(transfer) {
|
|
5457
|
+
return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
|
|
5458
|
+
}
|
|
5246
5459
|
async function reconstructOutgoingTxRecords(deps) {
|
|
5247
5460
|
const { vault, transfers } = deps;
|
|
5248
5461
|
const now = deps.now ?? Date.now;
|
|
@@ -5259,17 +5472,17 @@ async function reconstructOutgoingTxRecords(deps) {
|
|
|
5259
5472
|
const commitmentsByExtrinsic = new Map(
|
|
5260
5473
|
commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
|
|
5261
5474
|
);
|
|
5262
|
-
const
|
|
5475
|
+
const existingByKey = await loadExistingRecords(vault);
|
|
5263
5476
|
for (const transfer of outgoingTransfers) {
|
|
5264
|
-
const existing =
|
|
5477
|
+
const existing = existingByKey.get(recordKey(transfer));
|
|
5265
5478
|
if (existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK) continue;
|
|
5266
5479
|
const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
|
|
5267
5480
|
if (inputNotes.length === 0) continue;
|
|
5268
|
-
const changeNote =
|
|
5481
|
+
const changeNote = selectDescribingNoteByCommitment(
|
|
5269
5482
|
commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
|
|
5270
5483
|
noteByCommitment
|
|
5271
5484
|
);
|
|
5272
|
-
const recipientPkHex = toPkHex(changeNote?.
|
|
5485
|
+
const recipientPkHex = toPkHex(changeNote?.sourcePk);
|
|
5273
5486
|
if (existing) {
|
|
5274
5487
|
if (recipientPkHex === ZERO_PK) continue;
|
|
5275
5488
|
await vault.saveTxRecord({ ...existing, recipientPkHex });
|
|
@@ -5280,7 +5493,7 @@ async function reconstructOutgoingTxRecords(deps) {
|
|
|
5280
5493
|
const transferAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
|
|
5281
5494
|
if (transferAmount <= 0n) continue;
|
|
5282
5495
|
const record = {
|
|
5283
|
-
id: transfer
|
|
5496
|
+
id: recordKey(transfer),
|
|
5284
5497
|
type: "private_transfer",
|
|
5285
5498
|
blockNumber: transfer.blockNumber,
|
|
5286
5499
|
hash: transfer.hash ?? "",
|
|
@@ -5331,7 +5544,7 @@ async function buildZkNote(params, deps) {
|
|
|
5331
5544
|
circuitVersion: params.circuitVersion,
|
|
5332
5545
|
// Omitted rather than passed as undefined: the builder distinguishes an
|
|
5333
5546
|
// absent recipient (a self note) from one explicitly set.
|
|
5334
|
-
...params.
|
|
5547
|
+
...params.sourcePk !== void 0 && { sourcePk: params.sourcePk },
|
|
5335
5548
|
...params.recipientOwnerPk !== void 0 && {
|
|
5336
5549
|
recipientOwnerPk: params.recipientOwnerPk
|
|
5337
5550
|
},
|
|
@@ -5650,7 +5863,7 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
5650
5863
|
value: transferAmount,
|
|
5651
5864
|
assetId: noteA.assetId,
|
|
5652
5865
|
ownerPk: recipientPk,
|
|
5653
|
-
|
|
5866
|
+
sourcePk: effectiveSenderPk,
|
|
5654
5867
|
// Undefined → dummy memo; the recipient finds the note by scanning.
|
|
5655
5868
|
...recipientViewingPublicKey !== void 0 ? { viewingPublicKey: recipientViewingPublicKey } : {},
|
|
5656
5869
|
// With a viewing key present this activates stealth derivation.
|
|
@@ -5661,7 +5874,7 @@ async function transferNotes(deps, params, onProgress) {
|
|
|
5661
5874
|
assetId: noteA.assetId,
|
|
5662
5875
|
ownerPk: effectiveSenderPk,
|
|
5663
5876
|
spendingKey: noteA.spendingKey,
|
|
5664
|
-
|
|
5877
|
+
sourcePk: recipientNote.ownerPk,
|
|
5665
5878
|
viewingPublicKey: senderViewingPublicKey
|
|
5666
5879
|
});
|
|
5667
5880
|
onProgress?.("generating-zk");
|
|
@@ -5846,7 +6059,7 @@ async function unshieldNote(deps, params, onProgress) {
|
|
|
5846
6059
|
import { CircuitType as CircuitType7 } from "@orbinum/proof-generator";
|
|
5847
6060
|
async function claimFees(deps, { assetId, amount, signer }, onStep) {
|
|
5848
6061
|
onStep?.("building-note");
|
|
5849
|
-
const note = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
6062
|
+
const { note } = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
|
|
5850
6063
|
const { provider } = await deps.resolver.resolve(CircuitType7.ValueProof, note.circuitVersion);
|
|
5851
6064
|
onStep?.("generating-proof");
|
|
5852
6065
|
const proofOutput = await generateFeeClaimProof(
|
|
@@ -6415,6 +6628,7 @@ export {
|
|
|
6415
6628
|
clearKnownEphWindow,
|
|
6416
6629
|
clearSession,
|
|
6417
6630
|
collectNullifiersToQuery,
|
|
6631
|
+
collectOutgoingFacts,
|
|
6418
6632
|
collectScanEntries,
|
|
6419
6633
|
commitmentHexOf,
|
|
6420
6634
|
computeNoteCommitment,
|
|
@@ -6490,6 +6704,7 @@ export {
|
|
|
6490
6704
|
getPolkadotSignerFromPjs as getSubstrateSignerFromExtension,
|
|
6491
6705
|
hasCachedSession,
|
|
6492
6706
|
hasInjectedExtensions,
|
|
6707
|
+
hasSourcePk,
|
|
6493
6708
|
hexToBigint,
|
|
6494
6709
|
hexToNumber,
|
|
6495
6710
|
implicitSubstrateToEvm,
|
|
@@ -6501,6 +6716,7 @@ export {
|
|
|
6501
6716
|
isConnectionLossError,
|
|
6502
6717
|
isEvmAddress,
|
|
6503
6718
|
isGhostNoteError,
|
|
6719
|
+
isHexOfLength,
|
|
6504
6720
|
isImplicitEvmAccount,
|
|
6505
6721
|
isNativeAsset,
|
|
6506
6722
|
isNoteSelfConsistent,
|
|
@@ -6513,6 +6729,7 @@ export {
|
|
|
6513
6729
|
mapExtrinsicArgs,
|
|
6514
6730
|
mapZkEventData,
|
|
6515
6731
|
markInputsSpent,
|
|
6732
|
+
mergeProvenance,
|
|
6516
6733
|
normalizeChainFingerprint,
|
|
6517
6734
|
normalizeEvmAddress,
|
|
6518
6735
|
normalizeNote,
|
|
@@ -6527,6 +6744,7 @@ export {
|
|
|
6527
6744
|
noteTxKind,
|
|
6528
6745
|
openOutgoingBlob,
|
|
6529
6746
|
openPaymentSlip,
|
|
6747
|
+
outranks,
|
|
6530
6748
|
pairwiseEphWindow,
|
|
6531
6749
|
palletErrorKind,
|
|
6532
6750
|
parseAmount,
|
|
@@ -6541,6 +6759,7 @@ export {
|
|
|
6541
6759
|
recoverOwnerPkPoint,
|
|
6542
6760
|
recoverSelfStealthNote,
|
|
6543
6761
|
refuseIfAlreadySpent,
|
|
6762
|
+
regeneratePaymentSlip,
|
|
6544
6763
|
removeByCommitment,
|
|
6545
6764
|
requireSessionKeys,
|
|
6546
6765
|
reservePairwiseIndex,
|
|
@@ -6554,6 +6773,8 @@ export {
|
|
|
6554
6773
|
scanAbortError,
|
|
6555
6774
|
sealOutgoingBlob,
|
|
6556
6775
|
sealPaymentSlip,
|
|
6776
|
+
selectDescribingNote,
|
|
6777
|
+
selectDescribingNoteByCommitment,
|
|
6557
6778
|
selectGhosts,
|
|
6558
6779
|
selectNotes,
|
|
6559
6780
|
selfEphWindow,
|