@continuumdao/ctm-mpc-defi 0.2.60 → 0.2.62
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/agent/catalog.cjs +67 -17
- package/dist/agent/catalog.cjs.map +1 -1
- package/dist/agent/catalog.d.ts +134 -7
- package/dist/agent/catalog.js +66 -18
- package/dist/agent/catalog.js.map +1 -1
- package/dist/agent/skills/railgun/SKILL.md +14 -5
- package/dist/protocols/evm/railgun/index.cjs +1341 -167
- package/dist/protocols/evm/railgun/index.cjs.map +1 -1
- package/dist/protocols/evm/railgun/index.d.ts +139 -4
- package/dist/protocols/evm/railgun/index.js +1317 -170
- package/dist/protocols/evm/railgun/index.js.map +1 -1
- package/package.json +4 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getAddress, parseAbi, zeroAddress, keccak256, encodeFunctionData, defineChain, createPublicClient, parseGwei, serializeTransaction,
|
|
1
|
+
import { getAddress, parseAbi, zeroAddress, keccak256, hashMessage, recoverAddress, encodeFunctionData, defineChain, createPublicClient, parseGwei, serializeTransaction, stringToHex, http } from 'viem';
|
|
2
2
|
import { fetchChainFeeParams, gasLimitFromEstimateAndChainConfig, gweiToDecimalString, proposalTxParamsToFeeSnapshot, alignEip1559FeesWithLatestBase, getClientIdFromKeyGenResult } from '@continuumdao/continuum-node-sdk';
|
|
3
|
-
import { TXIDVersion,
|
|
3
|
+
import { TXIDVersion, RailgunWalletBalanceBucket, ChainType, EVMGasType, getEVMGasTypeForTransaction, calculateGasPrice, NETWORK_CONFIG } from '@railgun-community/shared-models';
|
|
4
4
|
|
|
5
5
|
// src/core/registry.ts
|
|
6
6
|
var modules = [];
|
|
@@ -540,11 +540,44 @@ async function buildEvmMultisignBatch(args) {
|
|
|
540
540
|
return result;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
// src/protocols/evm/railgun/data-dir.ts
|
|
544
|
+
var RAILGUN_CONTAINER_DATA_DIR = "/app/railgun_data";
|
|
545
|
+
function railgunDataRoot() {
|
|
546
|
+
if (typeof process === "undefined") return "";
|
|
547
|
+
const fromEnv = process.env.RAILGUN_DATA_DIR?.trim() ?? "";
|
|
548
|
+
if (fromEnv) return fromEnv;
|
|
549
|
+
if (process.versions?.node && process.env.MCP_HTTP_PORT?.trim() && process.cwd() === "/app") {
|
|
550
|
+
return RAILGUN_CONTAINER_DATA_DIR;
|
|
551
|
+
}
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
async function resolveRailgunDataRoot() {
|
|
555
|
+
return railgunDataRoot();
|
|
556
|
+
}
|
|
557
|
+
function safeKeyGenFolderName(keyGenId) {
|
|
558
|
+
const name = keyGenId.trim();
|
|
559
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(name)) {
|
|
560
|
+
throw new Error("KeyGen id cannot be used as a Railgun data folder name.");
|
|
561
|
+
}
|
|
562
|
+
return name;
|
|
563
|
+
}
|
|
564
|
+
function under(root, ...parts) {
|
|
565
|
+
return [root.replace(/\/+$/, ""), ...parts].join("/");
|
|
566
|
+
}
|
|
567
|
+
function railgunEnginePath(root) {
|
|
568
|
+
return under(root, "engine");
|
|
569
|
+
}
|
|
570
|
+
function railgunArtifactPath(root) {
|
|
571
|
+
return under(root, "artifacts");
|
|
572
|
+
}
|
|
573
|
+
function railgunKeyGenWalletPath(root, keyGenId) {
|
|
574
|
+
return under(root, "keygens", safeKeyGenFolderName(keyGenId), "wallet.json");
|
|
575
|
+
}
|
|
576
|
+
|
|
543
577
|
// src/protocols/evm/railgun/engine.ts
|
|
544
578
|
var artifacts = /* @__PURE__ */ new Map();
|
|
545
579
|
var started = false;
|
|
546
580
|
var loadedProviders = /* @__PURE__ */ new Set();
|
|
547
|
-
var lastEngineError = "";
|
|
548
581
|
function errorChain(err) {
|
|
549
582
|
const parts = [];
|
|
550
583
|
let current = err;
|
|
@@ -559,24 +592,72 @@ function errorChain(err) {
|
|
|
559
592
|
}
|
|
560
593
|
return parts.join(": ");
|
|
561
594
|
}
|
|
562
|
-
function latestRailgunEngineError() {
|
|
563
|
-
return lastEngineError;
|
|
564
|
-
}
|
|
565
595
|
var POI_NODE_URLS = ["https://ppoi.fdi.network"];
|
|
566
596
|
async function walletModule() {
|
|
567
597
|
return await import('@railgun-community/wallet');
|
|
568
598
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
async store(_dir, path, item) {
|
|
574
|
-
artifacts.set(path, item);
|
|
575
|
-
},
|
|
576
|
-
async exists(path) {
|
|
577
|
-
return artifacts.has(path);
|
|
599
|
+
function artifactFile(root, itemPath) {
|
|
600
|
+
const relative = itemPath.replace(/^\/+/, "");
|
|
601
|
+
if (!relative || relative.split("/").some((part) => part === ".." || part === "")) {
|
|
602
|
+
throw new Error("Railgun artifact path is not valid.");
|
|
578
603
|
}
|
|
579
|
-
}
|
|
604
|
+
return `${railgunArtifactPath(root)}/${relative}`;
|
|
605
|
+
}
|
|
606
|
+
function memoryArtifactStore() {
|
|
607
|
+
return {
|
|
608
|
+
async get(path) {
|
|
609
|
+
return artifacts.get(path) ?? null;
|
|
610
|
+
},
|
|
611
|
+
async store(_dir, path, item) {
|
|
612
|
+
artifacts.set(path, item);
|
|
613
|
+
},
|
|
614
|
+
async exists(path) {
|
|
615
|
+
return artifacts.has(path);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function diskArtifactStore(root) {
|
|
620
|
+
const load = new Function("id", "return import(id)");
|
|
621
|
+
return {
|
|
622
|
+
async get(itemPath) {
|
|
623
|
+
const fs = await load("node:fs/promises");
|
|
624
|
+
try {
|
|
625
|
+
return await fs.readFile(artifactFile(root, itemPath));
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
async store(_dir, itemPath, item) {
|
|
631
|
+
const fs = await load("node:fs/promises");
|
|
632
|
+
const file = artifactFile(root, itemPath);
|
|
633
|
+
await fs.mkdir(file.slice(0, file.lastIndexOf("/")), { recursive: true });
|
|
634
|
+
await fs.writeFile(file, item);
|
|
635
|
+
},
|
|
636
|
+
async exists(itemPath) {
|
|
637
|
+
const fs = await load("node:fs/promises");
|
|
638
|
+
try {
|
|
639
|
+
await fs.access(artifactFile(root, itemPath));
|
|
640
|
+
return true;
|
|
641
|
+
} catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async function railgunDatabase(root) {
|
|
648
|
+
if (!root) {
|
|
649
|
+
const memdown = (await import('memdown')).default;
|
|
650
|
+
return new memdown();
|
|
651
|
+
}
|
|
652
|
+
const load = new Function("id", "return import(id)");
|
|
653
|
+
const fs = await load("node:fs/promises");
|
|
654
|
+
const engineDir = railgunEnginePath(root);
|
|
655
|
+
await fs.mkdir(engineDir, { recursive: true });
|
|
656
|
+
await fs.mkdir(railgunArtifactPath(root), { recursive: true });
|
|
657
|
+
const leveldownModule = await load("leveldown");
|
|
658
|
+
const open = leveldownModule.default ?? leveldownModule;
|
|
659
|
+
return open(engineDir);
|
|
660
|
+
}
|
|
580
661
|
async function attachEngineErrorLogger() {
|
|
581
662
|
if (typeof window !== "undefined") return;
|
|
582
663
|
if (typeof process === "undefined" || !process.versions?.node) return;
|
|
@@ -589,21 +670,39 @@ async function attachEngineErrorLogger() {
|
|
|
589
670
|
setLoggers(
|
|
590
671
|
() => void 0,
|
|
591
672
|
(err) => {
|
|
592
|
-
|
|
673
|
+
errorChain(err);
|
|
593
674
|
}
|
|
594
675
|
);
|
|
595
676
|
}
|
|
677
|
+
async function attachSnarkJsGroth16() {
|
|
678
|
+
if (typeof window !== "undefined") return;
|
|
679
|
+
if (typeof process === "undefined" || !process.versions?.node) return;
|
|
680
|
+
const load = new Function("id", "return import(id)");
|
|
681
|
+
const nodeModule = await load("node:module");
|
|
682
|
+
const nodePath = await load("node:path");
|
|
683
|
+
const require2 = nodeModule.createRequire(import.meta.url);
|
|
684
|
+
const walletEntry = require2.resolve("@railgun-community/wallet");
|
|
685
|
+
const { getProver } = require2(nodePath.join(nodePath.dirname(walletEntry), "services/railgun/core/prover.js"));
|
|
686
|
+
const snark = require2("snarkjs");
|
|
687
|
+
const groth16 = snark.groth16;
|
|
688
|
+
if (!groth16 || typeof groth16.fullProve !== "function") {
|
|
689
|
+
throw new Error("snarkjs groth16 prover is missing.");
|
|
690
|
+
}
|
|
691
|
+
getProver().setSnarkJSGroth16(groth16);
|
|
692
|
+
}
|
|
596
693
|
async function ensureRailgunEngine() {
|
|
597
694
|
if (started) return;
|
|
598
695
|
const wallet = await walletModule();
|
|
599
696
|
await attachEngineErrorLogger();
|
|
600
|
-
const
|
|
697
|
+
const root = await resolveRailgunDataRoot();
|
|
698
|
+
const files = root ? diskArtifactStore(root) : memoryArtifactStore();
|
|
601
699
|
const store = new wallet.ArtifactStore(
|
|
602
|
-
(path) =>
|
|
603
|
-
(dir, path, item) =>
|
|
604
|
-
(path) =>
|
|
700
|
+
(path) => files.get(path),
|
|
701
|
+
(dir, path, item) => files.store(dir, path, item),
|
|
702
|
+
(path) => files.exists(path)
|
|
605
703
|
);
|
|
606
|
-
await wallet.startRailgunEngine("continuum",
|
|
704
|
+
await wallet.startRailgunEngine("continuum", await railgunDatabase(root), true, store, false, false, POI_NODE_URLS, [], false);
|
|
705
|
+
await attachSnarkJsGroth16();
|
|
607
706
|
started = true;
|
|
608
707
|
}
|
|
609
708
|
async function ensureRailgunProvider(chainId, rpcUrl) {
|
|
@@ -625,21 +724,80 @@ async function ensureRailgunProvider(chainId, rpcUrl) {
|
|
|
625
724
|
}
|
|
626
725
|
return networkName;
|
|
627
726
|
}
|
|
727
|
+
var RAILGUN_TRANSFER_GAS_ESTIMATE = 2900000n;
|
|
728
|
+
var RAILGUN_UNSHIELD_GAS_ESTIMATE = 3400000n;
|
|
729
|
+
function weiFromGwei(gwei, fallback = 1) {
|
|
730
|
+
return BigInt(Math.round(Number(gwei ?? fallback) * 1e9));
|
|
731
|
+
}
|
|
628
732
|
function railgunGasDetails(chainDetail, gasEstimate) {
|
|
629
733
|
if (chainDetail.legacy) {
|
|
630
734
|
return {
|
|
631
735
|
evmGasType: EVMGasType.Type0,
|
|
632
736
|
gasEstimate,
|
|
633
|
-
gasPrice:
|
|
737
|
+
gasPrice: weiFromGwei(chainDetail.gasPrice)
|
|
634
738
|
};
|
|
635
739
|
}
|
|
636
740
|
return {
|
|
637
741
|
evmGasType: EVMGasType.Type2,
|
|
638
742
|
gasEstimate,
|
|
639
|
-
maxFeePerGas:
|
|
640
|
-
maxPriorityFeePerGas:
|
|
743
|
+
maxFeePerGas: weiFromGwei(chainDetail.baseFee),
|
|
744
|
+
maxPriorityFeePerGas: weiFromGwei(chainDetail.priorityFee)
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function railgunBroadcasterGasDetails(networkName, chainDetail, gasEstimate) {
|
|
748
|
+
const evmGasType = getEVMGasTypeForTransaction(networkName, false);
|
|
749
|
+
if (evmGasType === EVMGasType.Type2 || evmGasType === EVMGasType.Type4) {
|
|
750
|
+
return {
|
|
751
|
+
evmGasType,
|
|
752
|
+
gasEstimate,
|
|
753
|
+
maxFeePerGas: weiFromGwei(chainDetail.baseFee),
|
|
754
|
+
maxPriorityFeePerGas: weiFromGwei(chainDetail.priorityFee)
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const gasPrice = evmGasType === EVMGasType.Type0 || chainDetail.legacy ? weiFromGwei(chainDetail.gasPrice) : weiFromGwei(chainDetail.baseFee) + weiFromGwei(chainDetail.priorityFee);
|
|
758
|
+
return { evmGasType, gasEstimate, gasPrice };
|
|
759
|
+
}
|
|
760
|
+
function gweiFromWei(wei) {
|
|
761
|
+
return Number(wei) / 1e9;
|
|
762
|
+
}
|
|
763
|
+
function chainDetailWithFetchedGas(chainDetail, live) {
|
|
764
|
+
if (chainDetail.legacy) {
|
|
765
|
+
if (chainDetail.gasPrice != null && chainDetail.gasPrice > 0) return chainDetail;
|
|
766
|
+
if (live.gasPriceWei == null) return chainDetail;
|
|
767
|
+
return { ...chainDetail, gasPrice: gweiFromWei(live.gasPriceWei) };
|
|
768
|
+
}
|
|
769
|
+
if (chainDetail.baseFee != null && chainDetail.baseFee > 0) return chainDetail;
|
|
770
|
+
return {
|
|
771
|
+
...chainDetail,
|
|
772
|
+
baseFee: live.baseFeeWei != null ? gweiFromWei(live.baseFeeWei) : chainDetail.baseFee,
|
|
773
|
+
priorityFee: chainDetail.priorityFee != null && chainDetail.priorityFee > 0 ? chainDetail.priorityFee : live.priorityFeeWei != null ? gweiFromWei(live.priorityFeeWei) : chainDetail.priorityFee
|
|
641
774
|
};
|
|
642
775
|
}
|
|
776
|
+
async function chainDetailWithLiveGas(rpcUrl, chainDetail) {
|
|
777
|
+
const needsLive = chainDetail.legacy ? chainDetail.gasPrice == null || chainDetail.gasPrice <= 0 : chainDetail.baseFee == null || chainDetail.baseFee <= 0;
|
|
778
|
+
if (!needsLive) return chainDetail;
|
|
779
|
+
const client = createPublicClient({ transport: defiEvmTransport(rpcUrl) });
|
|
780
|
+
if (chainDetail.legacy) {
|
|
781
|
+
const gasPriceWei = await client.getGasPrice();
|
|
782
|
+
return chainDetailWithFetchedGas(chainDetail, { gasPriceWei });
|
|
783
|
+
}
|
|
784
|
+
const block = await client.getBlock({ blockTag: "latest" });
|
|
785
|
+
let priorityFeeWei = 0n;
|
|
786
|
+
try {
|
|
787
|
+
priorityFeeWei = await client.estimateMaxPriorityFeePerGas();
|
|
788
|
+
} catch {
|
|
789
|
+
priorityFeeWei = 1000000000n;
|
|
790
|
+
}
|
|
791
|
+
return chainDetailWithFetchedGas(chainDetail, {
|
|
792
|
+
baseFeeWei: block.baseFeePerGas ?? 0n,
|
|
793
|
+
priorityFeeWei
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function broadcasterTokenFee(feePerUnitGas, gasDetails) {
|
|
797
|
+
const price = gasDetails.evmGasType === EVMGasType.Type2 || gasDetails.evmGasType === EVMGasType.Type4 ? gasDetails.maxFeePerGas : gasDetails.gasPrice;
|
|
798
|
+
const maximumGas = gasDetails.gasEstimate * 12000n / 10000n * price;
|
|
799
|
+
return feePerUnitGas * maximumGas / 10n ** 18n;
|
|
800
|
+
}
|
|
643
801
|
|
|
644
802
|
// src/protocols/evm/railgun/shield.ts
|
|
645
803
|
var erc20Abi = parseAbi([
|
|
@@ -659,15 +817,41 @@ async function shieldSignatureMessage() {
|
|
|
659
817
|
const wallet = await import('@railgun-community/wallet');
|
|
660
818
|
return wallet.getShieldPrivateKeySignatureMessage();
|
|
661
819
|
}
|
|
662
|
-
function shieldPrivateKeyFromSignature(signature) {
|
|
663
|
-
|
|
664
|
-
|
|
820
|
+
async function shieldPrivateKeyFromSignature(signature, executorAddress) {
|
|
821
|
+
return keccak256(await canonicalRailgunShieldSignature(signature, executorAddress));
|
|
822
|
+
}
|
|
823
|
+
async function canonicalRailgunShieldSignature(signature, executorAddress) {
|
|
824
|
+
const body = signature.trim().replace(/^0x/i, "").toLowerCase();
|
|
825
|
+
if (!/^[0-9a-f]+$/.test(body) || body.length % 2 !== 0) {
|
|
665
826
|
throw new Error("Shield-key signature must be hex.");
|
|
666
827
|
}
|
|
667
|
-
|
|
828
|
+
const rs = body.length === 130 ? body.slice(0, 128) : body;
|
|
829
|
+
if (rs.length !== 128) {
|
|
830
|
+
throw new Error("Shield-key signature must be the 65-byte personal_sign of RAILGUN_SHIELD.");
|
|
831
|
+
}
|
|
832
|
+
const stated = body.length === 130 ? body.slice(128) : "";
|
|
833
|
+
if (!executorAddress) {
|
|
834
|
+
if (stated === "00") return `0x${rs}1b`;
|
|
835
|
+
if (stated === "01") return `0x${rs}1c`;
|
|
836
|
+
if (body.length === 130) return `0x${body}`;
|
|
668
837
|
throw new Error("Shield-key signature must be the 65-byte personal_sign of RAILGUN_SHIELD.");
|
|
669
838
|
}
|
|
670
|
-
|
|
839
|
+
const preferred = stated === "01" || stated === "1c" ? ["1c", "1b"] : ["1b", "1c"];
|
|
840
|
+
const hash = hashMessage("RAILGUN_SHIELD");
|
|
841
|
+
const executor = getAddress(executorAddress).toLowerCase();
|
|
842
|
+
for (const v of preferred) {
|
|
843
|
+
const candidate = `0x${rs}${v}`;
|
|
844
|
+
try {
|
|
845
|
+
const recovered = await recoverAddress({ hash, signature: candidate });
|
|
846
|
+
if (recovered.toLowerCase() === executor) return candidate;
|
|
847
|
+
} catch {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (body.length === 130 && stated !== "00" && stated !== "01" && stated !== "1b" && stated !== "1c") {
|
|
852
|
+
return `0x${body}`;
|
|
853
|
+
}
|
|
854
|
+
throw new Error("Shield-key signature does not recover to this KeyGen address.");
|
|
671
855
|
}
|
|
672
856
|
function personalSignBody(args, message) {
|
|
673
857
|
const executor = getAddress(args.executorAddress);
|
|
@@ -749,7 +933,6 @@ async function buildEvmMultisignBodyRailgunShield(args) {
|
|
|
749
933
|
if (!signature) {
|
|
750
934
|
return personalSignBody(args, await shieldSignatureMessage());
|
|
751
935
|
}
|
|
752
|
-
const shieldPrivateKey = shieldPrivateKeyFromSignature(signature);
|
|
753
936
|
const network = await railgunNetworkByChainId(args.chainId);
|
|
754
937
|
if (!network) throw new Error("Railgun is not deployed on this chain.");
|
|
755
938
|
const tokenAddress = getAddress(args.tokenAddress);
|
|
@@ -759,6 +942,7 @@ async function buildEvmMultisignBodyRailgunShield(args) {
|
|
|
759
942
|
const amount = BigInt(args.amountWei);
|
|
760
943
|
if (amount <= 0n) throw new Error("Amount must be greater than zero.");
|
|
761
944
|
const executor = getAddress(args.executorAddress);
|
|
945
|
+
const shieldPrivateKey = await shieldPrivateKeyFromSignature(signature, executor);
|
|
762
946
|
const spender = getAddress(network.proxyContract);
|
|
763
947
|
const rpcUrl = args.rpcUrl.trim();
|
|
764
948
|
if (!rpcUrl) throw new Error("rpcUrl is required.");
|
|
@@ -836,6 +1020,117 @@ async function buildEvmMultisignBodyRailgunShield(args) {
|
|
|
836
1020
|
})
|
|
837
1021
|
});
|
|
838
1022
|
}
|
|
1023
|
+
|
|
1024
|
+
// src/chains/evm/coingecko.ts
|
|
1025
|
+
var COINGECKO_PLATFORM_BY_CHAIN_ID = {
|
|
1026
|
+
"1": "ethereum",
|
|
1027
|
+
"10": "optimistic-ethereum",
|
|
1028
|
+
"30": "rootstock",
|
|
1029
|
+
"56": "binance-smart-chain",
|
|
1030
|
+
"100": "xdai",
|
|
1031
|
+
"137": "polygon-pos",
|
|
1032
|
+
"146": "sonic",
|
|
1033
|
+
"169": "manta-pacific",
|
|
1034
|
+
"196": "x-layer",
|
|
1035
|
+
"204": "op-bnb",
|
|
1036
|
+
"239": "tac",
|
|
1037
|
+
"250": "fantom",
|
|
1038
|
+
"252": "fraxtal",
|
|
1039
|
+
"288": "boba",
|
|
1040
|
+
"324": "zk-sync-era",
|
|
1041
|
+
"480": "world-chain",
|
|
1042
|
+
"999": "hyperevm",
|
|
1043
|
+
"1088": "metis-andromeda",
|
|
1044
|
+
"1101": "polygon-zkevm",
|
|
1045
|
+
"1116": "core",
|
|
1046
|
+
"1135": "lisk",
|
|
1047
|
+
"130": "unichain",
|
|
1048
|
+
"1868": "soneium",
|
|
1049
|
+
"1923": "swellchain",
|
|
1050
|
+
"2222": "kava",
|
|
1051
|
+
"2818": "morph-l2",
|
|
1052
|
+
"34443": "mode",
|
|
1053
|
+
"42161": "arbitrum-one",
|
|
1054
|
+
"42220": "celo",
|
|
1055
|
+
"43114": "avalanche",
|
|
1056
|
+
"4663": "robinhood",
|
|
1057
|
+
"5042": "arc",
|
|
1058
|
+
"48900": "zircuit",
|
|
1059
|
+
"5000": "mantle",
|
|
1060
|
+
"534352": "scroll",
|
|
1061
|
+
"57073": "ink",
|
|
1062
|
+
"59144": "linea",
|
|
1063
|
+
"60808": "bob-network",
|
|
1064
|
+
"80084": "berachain",
|
|
1065
|
+
"80094": "berachain",
|
|
1066
|
+
"81457": "blast",
|
|
1067
|
+
"8453": "base",
|
|
1068
|
+
"9745": "plasma",
|
|
1069
|
+
"167000": "taiko",
|
|
1070
|
+
"7777777": "zora"
|
|
1071
|
+
};
|
|
1072
|
+
function coingeckoPlatformForChainId(chainId) {
|
|
1073
|
+
return COINGECKO_PLATFORM_BY_CHAIN_ID[String(chainId).trim()];
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/protocols/evm/railgun/broadcaster-choice.ts
|
|
1077
|
+
var FEE_RATIO_ONE_TO_ONE_MILLI = 1e3;
|
|
1078
|
+
function feeRatioMilli(feePerUnitGas, tokenDecimals, tokenUsd, nativeUsd) {
|
|
1079
|
+
if (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 36) return null;
|
|
1080
|
+
if (!(tokenUsd > 0) || !(nativeUsd > 0) || !Number.isFinite(tokenUsd) || !Number.isFinite(nativeUsd)) return null;
|
|
1081
|
+
const tokenMicro = BigInt(Math.round(tokenUsd * 1e6));
|
|
1082
|
+
const nativeMicro = BigInt(Math.round(nativeUsd * 1e6));
|
|
1083
|
+
if (tokenMicro <= 0n || nativeMicro <= 0n) return null;
|
|
1084
|
+
const denom = 10n ** BigInt(tokenDecimals) * nativeMicro;
|
|
1085
|
+
if (denom <= 0n) return null;
|
|
1086
|
+
const milli = feePerUnitGas * tokenMicro * 1000n / denom;
|
|
1087
|
+
if (milli > BigInt(Number.MAX_SAFE_INTEGER)) return null;
|
|
1088
|
+
return Number(milli);
|
|
1089
|
+
}
|
|
1090
|
+
function formatFeeRatioMilli(milli) {
|
|
1091
|
+
const abs = Math.abs(Math.trunc(milli));
|
|
1092
|
+
const whole = Math.floor(abs / 1e3);
|
|
1093
|
+
const frac = String(abs % 1e3).padStart(3, "0").slice(0, 2);
|
|
1094
|
+
return `${milli < 0 ? "-" : ""}${whole}.${frac}`;
|
|
1095
|
+
}
|
|
1096
|
+
function coversGasCost(ratioMilli) {
|
|
1097
|
+
return ratioMilli != null && ratioMilli >= FEE_RATIO_ONE_TO_ONE_MILLI;
|
|
1098
|
+
}
|
|
1099
|
+
function orderBroadcasterChoices(rows) {
|
|
1100
|
+
return [...rows].sort((left, right) => {
|
|
1101
|
+
if (left.coversGas !== right.coversGas) return left.coversGas ? -1 : 1;
|
|
1102
|
+
if (left.ratioMilli != null && right.ratioMilli != null && left.ratioMilli !== right.ratioMilli) {
|
|
1103
|
+
return left.coversGas ? left.ratioMilli - right.ratioMilli : right.ratioMilli - left.ratioMilli;
|
|
1104
|
+
}
|
|
1105
|
+
if (left.ratioMilli == null && right.ratioMilli != null) return 1;
|
|
1106
|
+
if (left.ratioMilli != null && right.ratioMilli == null) return -1;
|
|
1107
|
+
if (left.feePerUnitGas !== right.feePerUnitGas) return left.feePerUnitGas < right.feePerUnitGas ? -1 : 1;
|
|
1108
|
+
return right.reliability - left.reliability;
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
function defaultBroadcasterChoice(rows) {
|
|
1112
|
+
return rows.find((row) => row.coversGas) ?? rows[0];
|
|
1113
|
+
}
|
|
1114
|
+
function currentBroadcasterChoice(rows, feesId, railgunAddress, refused) {
|
|
1115
|
+
const id = feesId.trim();
|
|
1116
|
+
if (id && !refused.has(id)) {
|
|
1117
|
+
const exact = rows.find((row2) => row2.feesId === id);
|
|
1118
|
+
if (exact) return { row: exact, refreshed: false };
|
|
1119
|
+
}
|
|
1120
|
+
const address = railgunAddress.trim();
|
|
1121
|
+
if (!address) return void 0;
|
|
1122
|
+
const same = rows.filter((row2) => row2.railgunAddress === address && !refused.has(row2.feesId));
|
|
1123
|
+
const row = same.find((item) => item.coversGas) ?? same[0];
|
|
1124
|
+
return row ? { row, refreshed: true } : void 0;
|
|
1125
|
+
}
|
|
1126
|
+
function nextCoveringBroadcaster(rows, refused) {
|
|
1127
|
+
return rows.find((row) => row.coversGas && !refused.has(row.feesId));
|
|
1128
|
+
}
|
|
1129
|
+
function isBroadcasterGasRefusal(message) {
|
|
1130
|
+
return /gas cost is too high/i.test(message);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/protocols/evm/railgun/private-actions.ts
|
|
839
1134
|
function chainFor(chainId) {
|
|
840
1135
|
return { type: ChainType.EVM, id: chainId };
|
|
841
1136
|
}
|
|
@@ -849,6 +1144,53 @@ function parsedAmount(amountWei) {
|
|
|
849
1144
|
if (amount <= 0n) throw new Error("Amount must be greater than zero.");
|
|
850
1145
|
return amount;
|
|
851
1146
|
}
|
|
1147
|
+
function railgunDetailedError(err) {
|
|
1148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1149
|
+
let current = err;
|
|
1150
|
+
let fallback = "Railgun spend proof failed.";
|
|
1151
|
+
while (current != null && !seen.has(current)) {
|
|
1152
|
+
seen.add(current);
|
|
1153
|
+
if (!(current instanceof Error)) break;
|
|
1154
|
+
const message = current.message.trim();
|
|
1155
|
+
if (message && /amount required|balance:/i.test(message)) return message;
|
|
1156
|
+
if (message) fallback = message;
|
|
1157
|
+
current = current.cause;
|
|
1158
|
+
}
|
|
1159
|
+
return fallback;
|
|
1160
|
+
}
|
|
1161
|
+
function broadcasterAskedToRefreshProofStatus(err) {
|
|
1162
|
+
const message = railgunDetailedError(err);
|
|
1163
|
+
return message.includes(BROADCASTER_POI_INCOMPLETE) || /could not validate proof of innocence/i.test(message) || /failed to validate poi merkleroots/i.test(message) || /invalid poi merkleroots/i.test(message);
|
|
1164
|
+
}
|
|
1165
|
+
function broadcasterRefusedForGas(err) {
|
|
1166
|
+
return isBroadcasterGasRefusal(railgunDetailedError(err));
|
|
1167
|
+
}
|
|
1168
|
+
function chainForNetwork(networkName) {
|
|
1169
|
+
return NETWORK_CONFIG[networkName].chain;
|
|
1170
|
+
}
|
|
1171
|
+
async function refreshSpendingProofStatus(networkName, walletId) {
|
|
1172
|
+
const wallet = await import('@railgun-community/wallet');
|
|
1173
|
+
const chain = chainForNetwork(networkName);
|
|
1174
|
+
wallet.resumePPOIBatching(chain);
|
|
1175
|
+
await wallet.walletForID(walletId).refreshPOIsForTXIDVersion(chain, TXIDVersion.V2_PoseidonMerkle, true);
|
|
1176
|
+
}
|
|
1177
|
+
async function assertTokenSpendable(networkName, walletId, tokenAddress, amount) {
|
|
1178
|
+
const wallet = await import('@railgun-community/wallet');
|
|
1179
|
+
const balance = await wallet.walletForID(walletId).getBalanceERC20(
|
|
1180
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1181
|
+
chainForNetwork(networkName),
|
|
1182
|
+
tokenAddress,
|
|
1183
|
+
["Spendable"]
|
|
1184
|
+
);
|
|
1185
|
+
if ((balance ?? 0n) < amount) {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
"These notes are still waiting for proof-of-innocence. The private transfer can start once that status is spendable."
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function delay(ms) {
|
|
1192
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1193
|
+
}
|
|
852
1194
|
async function broadcasterClient() {
|
|
853
1195
|
try {
|
|
854
1196
|
return await import('@railgun-community/waku-broadcaster-client-web');
|
|
@@ -856,40 +1198,343 @@ async function broadcasterClient() {
|
|
|
856
1198
|
throw new Error("Railgun broadcaster client is not available.");
|
|
857
1199
|
}
|
|
858
1200
|
}
|
|
859
|
-
|
|
1201
|
+
var BROADCASTER_CACHE_MS = 6e4;
|
|
1202
|
+
var BROADCASTER_QUOTE_WAIT_MS = 2e4;
|
|
1203
|
+
var BROADCASTER_PROOF_WAIT_MS = 3 * 60 * 1e3;
|
|
1204
|
+
var BROADCASTER_FEE_MIN_REMAINING_MS = 4e4;
|
|
1205
|
+
var BROADCASTER_PROOF_ATTEMPTS = 3;
|
|
1206
|
+
var POI_STATUS_RETRY_MS = 2e4;
|
|
1207
|
+
var POI_ROOT_WAIT_MS = 3 * 60 * 1e3;
|
|
1208
|
+
var BROADCASTER_POI_INCOMPLETE = "Proof-of-innocence validation did not complete";
|
|
1209
|
+
function broadcasterFeeStillValid(expiration, now = Date.now()) {
|
|
1210
|
+
return Number.isFinite(expiration) && expiration >= now + BROADCASTER_FEE_MIN_REMAINING_MS;
|
|
1211
|
+
}
|
|
1212
|
+
var BROADCASTER_LIST_CACHE_MS = 15e3;
|
|
1213
|
+
var MAX_GAS_BROADCASTERS = 3;
|
|
1214
|
+
var NATIVE_COINGECKO_ID = {
|
|
1215
|
+
1: "ethereum",
|
|
1216
|
+
10: "ethereum",
|
|
1217
|
+
56: "binancecoin",
|
|
1218
|
+
100: "xdai",
|
|
1219
|
+
137: "polygon-ecosystem-token",
|
|
1220
|
+
250: "fantom",
|
|
1221
|
+
324: "ethereum",
|
|
1222
|
+
1101: "ethereum",
|
|
1223
|
+
5e3: "mantle",
|
|
1224
|
+
8453: "ethereum",
|
|
1225
|
+
34443: "ethereum",
|
|
1226
|
+
42161: "ethereum",
|
|
1227
|
+
43114: "avalanche-2",
|
|
1228
|
+
59144: "ethereum",
|
|
1229
|
+
81457: "ethereum",
|
|
1230
|
+
534352: "ethereum",
|
|
1231
|
+
167e3: "ethereum"
|
|
1232
|
+
};
|
|
1233
|
+
var broadcasterListCache = /* @__PURE__ */ new Map();
|
|
1234
|
+
var tokenDecimalsCache = /* @__PURE__ */ new Map();
|
|
1235
|
+
var tokenUsdCache = /* @__PURE__ */ new Map();
|
|
1236
|
+
async function ensureBroadcasterWaku(chain) {
|
|
860
1237
|
const { WakuBroadcasterClient } = await broadcasterClient();
|
|
861
1238
|
if (!WakuBroadcasterClient.isStarted()) {
|
|
862
1239
|
await WakuBroadcasterClient.start(chain, { useDNSDiscovery: true }, () => void 0);
|
|
863
1240
|
} else {
|
|
864
1241
|
await WakuBroadcasterClient.setChain(chain);
|
|
865
1242
|
}
|
|
866
|
-
|
|
867
|
-
|
|
1243
|
+
}
|
|
1244
|
+
async function findTokenBroadcasters(chain, tokenAddress) {
|
|
1245
|
+
const node = await nodeRequire();
|
|
1246
|
+
if (!node) throw new Error("Railgun broadcasters are listed from the node.");
|
|
1247
|
+
const entry = node.require.resolve("@railgun-community/waku-broadcaster-client-web");
|
|
1248
|
+
const loaded = node.require(node.path.join(node.path.dirname(entry), "search/best-broadcaster.js"));
|
|
1249
|
+
return loaded.BroadcasterSearch.findBroadcastersForToken(chain, tokenAddress, false) ?? [];
|
|
1250
|
+
}
|
|
1251
|
+
async function erc20Decimals(rpcUrl, tokenAddress) {
|
|
1252
|
+
const key = tokenAddress.toLowerCase();
|
|
1253
|
+
const cached2 = tokenDecimalsCache.get(key);
|
|
1254
|
+
if (cached2 != null) return cached2;
|
|
1255
|
+
const client = createPublicClient({ transport: defiEvmTransport(rpcUrl) });
|
|
1256
|
+
const decimals = await client.readContract({
|
|
1257
|
+
address: getAddress(tokenAddress),
|
|
1258
|
+
abi: parseAbi(["function decimals() view returns (uint8)"]),
|
|
1259
|
+
functionName: "decimals"
|
|
1260
|
+
});
|
|
1261
|
+
const value = Number(decimals);
|
|
1262
|
+
if (!Number.isInteger(value) || value < 0 || value > 36) throw new Error("Token decimals are not available.");
|
|
1263
|
+
tokenDecimalsCache.set(key, value);
|
|
1264
|
+
return value;
|
|
1265
|
+
}
|
|
1266
|
+
async function readUsd(url) {
|
|
1267
|
+
try {
|
|
1268
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
1269
|
+
if (!response.ok) return null;
|
|
1270
|
+
const body = await response.json();
|
|
1271
|
+
for (const row of Object.values(body)) {
|
|
1272
|
+
if (typeof row?.usd === "number" && row.usd > 0) return row.usd;
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
async function broadcasterUsdPrices(chainId, tokenAddress, wrappedAddress) {
|
|
1280
|
+
const key = `${chainId}:${tokenAddress.toLowerCase()}`;
|
|
1281
|
+
const cached2 = tokenUsdCache.get(key);
|
|
1282
|
+
if (cached2 && Date.now() - cached2.at < 6e4) return cached2;
|
|
1283
|
+
const nativeId = NATIVE_COINGECKO_ID[chainId];
|
|
1284
|
+
const nativeUsd = nativeId ? await readUsd(`https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(nativeId)}&vs_currencies=usd`) : null;
|
|
1285
|
+
const wrapped = wrappedAddress.toLowerCase() === tokenAddress.toLowerCase();
|
|
1286
|
+
let tokenUsd = wrapped ? nativeUsd : null;
|
|
1287
|
+
if (!wrapped) {
|
|
1288
|
+
const platform = coingeckoPlatformForChainId(chainId);
|
|
1289
|
+
tokenUsd = platform ? await readUsd(
|
|
1290
|
+
`https://api.coingecko.com/api/v3/simple/token_price/${encodeURIComponent(platform)}?contract_addresses=${encodeURIComponent(tokenAddress)}&vs_currencies=usd`
|
|
1291
|
+
) : null;
|
|
1292
|
+
}
|
|
1293
|
+
const prices = { tokenUsd, nativeUsd };
|
|
1294
|
+
tokenUsdCache.set(key, { at: Date.now(), ...prices });
|
|
1295
|
+
return prices;
|
|
1296
|
+
}
|
|
1297
|
+
async function loadRailgunBroadcasters(args) {
|
|
1298
|
+
const tokenAddress = getAddress(args.tokenAddress);
|
|
1299
|
+
const cacheKey = `${args.kind}:${args.chainId}:${tokenAddress.toLowerCase()}`;
|
|
1300
|
+
const cached2 = broadcasterListCache.get(cacheKey);
|
|
1301
|
+
if (cached2 && Date.now() - cached2.at < BROADCASTER_LIST_CACHE_MS) return cached2.rows;
|
|
1302
|
+
const chain = chainFor(args.chainId);
|
|
1303
|
+
await ensureBroadcasterWaku(chain);
|
|
1304
|
+
const deadline = Date.now() + (args.waitMs ?? BROADCASTER_QUOTE_WAIT_MS);
|
|
1305
|
+
let found = [];
|
|
1306
|
+
while (Date.now() < deadline) {
|
|
1307
|
+
found = (await findTokenBroadcasters(chain, tokenAddress)).filter((row) => broadcasterFeeStillValid(row.tokenFee.expiration));
|
|
1308
|
+
if (found.length > 0) break;
|
|
1309
|
+
await delay(1e3);
|
|
1310
|
+
}
|
|
1311
|
+
if (found.length === 0) throw new Error("No Railgun broadcaster is available for this token.");
|
|
1312
|
+
const network = await railgunNetworkByChainId(args.chainId);
|
|
1313
|
+
const [decimals, prices, priced] = await Promise.all([
|
|
1314
|
+
erc20Decimals(args.rpcUrl, tokenAddress),
|
|
1315
|
+
broadcasterUsdPrices(args.chainId, tokenAddress, network?.wrappedAddress ?? ""),
|
|
1316
|
+
chainDetailWithLiveGas(args.rpcUrl, args.chainDetail)
|
|
1317
|
+
]);
|
|
1318
|
+
const networkName = network?.networkName ?? "";
|
|
1319
|
+
const gasEstimate = args.kind === "unshield" ? RAILGUN_UNSHIELD_GAS_ESTIMATE : RAILGUN_TRANSFER_GAS_ESTIMATE;
|
|
1320
|
+
const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
|
|
1321
|
+
const byFeesId = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const selected of found) {
|
|
1323
|
+
const feesId = selected.tokenFee.feesID;
|
|
1324
|
+
if (!feesId) continue;
|
|
1325
|
+
const feePerUnitGas = BigInt(selected.tokenFee.feePerUnitGas);
|
|
1326
|
+
const ratioMilli = prices.tokenUsd != null && prices.nativeUsd != null ? feeRatioMilli(feePerUnitGas, decimals, prices.tokenUsd, prices.nativeUsd) : null;
|
|
1327
|
+
const row = {
|
|
1328
|
+
feesId,
|
|
1329
|
+
railgunAddress: selected.railgunAddress,
|
|
1330
|
+
feeWei: broadcasterTokenFee(feePerUnitGas, gasDetails).toString(),
|
|
1331
|
+
ratio: ratioMilli == null ? null : formatFeeRatioMilli(ratioMilli),
|
|
1332
|
+
coversGas: coversGasCost(ratioMilli),
|
|
1333
|
+
reliability: Number(selected.tokenFee.reliability) || 0,
|
|
1334
|
+
expiration: selected.tokenFee.expiration,
|
|
1335
|
+
tokenDecimals: decimals,
|
|
1336
|
+
feePerUnitGas,
|
|
1337
|
+
ratioMilli,
|
|
1338
|
+
selected
|
|
1339
|
+
};
|
|
1340
|
+
const previous = byFeesId.get(feesId);
|
|
1341
|
+
if (!previous || row.reliability > previous.reliability) byFeesId.set(feesId, row);
|
|
1342
|
+
}
|
|
1343
|
+
const rows = orderBroadcasterChoices([...byFeesId.values()]);
|
|
1344
|
+
broadcasterListCache.set(cacheKey, { at: Date.now(), rows });
|
|
1345
|
+
return rows;
|
|
1346
|
+
}
|
|
1347
|
+
async function listRailgunBroadcasters(args) {
|
|
1348
|
+
const kind = args.kind.trim();
|
|
1349
|
+
if (kind !== "transfer" && kind !== "unshield") throw new Error("kind must be transfer or unshield.");
|
|
1350
|
+
const rows = await loadRailgunBroadcasters({ ...args, kind });
|
|
1351
|
+
return rows.map((row) => ({
|
|
1352
|
+
feesId: row.feesId,
|
|
1353
|
+
railgunAddress: row.railgunAddress,
|
|
1354
|
+
feeWei: row.feeWei,
|
|
1355
|
+
ratio: row.ratio,
|
|
1356
|
+
coversGas: row.coversGas,
|
|
1357
|
+
reliability: row.reliability,
|
|
1358
|
+
expiration: row.expiration,
|
|
1359
|
+
tokenDecimals: row.tokenDecimals
|
|
1360
|
+
}));
|
|
1361
|
+
}
|
|
1362
|
+
function broadcasterNote(row, mode) {
|
|
1363
|
+
const ratioLabel = row.ratio ? `${row.ratio}:1` : "the selected fee";
|
|
1364
|
+
if (mode === "next") return `Trying the next broadcaster at ${ratioLabel}`;
|
|
1365
|
+
if (mode === "refreshed") return `The chosen fee expired. Broadcaster ${ratioLabel}`;
|
|
1366
|
+
return `Broadcaster ${ratioLabel}`;
|
|
1367
|
+
}
|
|
1368
|
+
function poiListsPresent(pois) {
|
|
1369
|
+
return Object.values(pois).some((leaves) => Object.keys(leaves).length > 0);
|
|
1370
|
+
}
|
|
1371
|
+
function poiRoots(poi) {
|
|
1372
|
+
if (!poi || typeof poi !== "object" || !("poiMerkleroots" in poi)) return [];
|
|
1373
|
+
const roots = poi.poiMerkleroots;
|
|
1374
|
+
return Array.isArray(roots) ? roots.filter((root) => typeof root === "string") : [];
|
|
1375
|
+
}
|
|
1376
|
+
async function nodeRequire() {
|
|
1377
|
+
if (typeof window !== "undefined") return null;
|
|
1378
|
+
if (typeof process === "undefined" || !process.versions?.node) return null;
|
|
1379
|
+
const load = new Function("id", "return import(id)");
|
|
1380
|
+
const nodeModule = await load("node:module");
|
|
1381
|
+
const nodePath = await load("node:path");
|
|
1382
|
+
return { require: nodeModule.createRequire(import.meta.url), path: nodePath };
|
|
1383
|
+
}
|
|
1384
|
+
async function poiRootValidator() {
|
|
1385
|
+
const node = await nodeRequire();
|
|
1386
|
+
if (!node) throw new Error("Proof-of-innocence checks need the node Railgun engine.");
|
|
1387
|
+
const walletEntry = node.require.resolve("@railgun-community/wallet");
|
|
1388
|
+
const engineEntry = node.require.resolve("@railgun-community/engine", {
|
|
1389
|
+
paths: [node.path.dirname(walletEntry)]
|
|
1390
|
+
});
|
|
1391
|
+
const loaded = node.require(engineEntry);
|
|
1392
|
+
if (!loaded.POI) throw new Error("The Railgun engine did not load its proof-of-innocence checker.");
|
|
1393
|
+
return loaded.POI;
|
|
1394
|
+
}
|
|
1395
|
+
async function poiMerklerootsConfirmed(chain, pois) {
|
|
1396
|
+
const poi = await poiRootValidator();
|
|
1397
|
+
for (const [listKey, leaves] of Object.entries(pois)) {
|
|
1398
|
+
for (const entry of Object.values(leaves)) {
|
|
1399
|
+
const roots = poiRoots(entry);
|
|
1400
|
+
if (roots.length === 0) return false;
|
|
1401
|
+
const confirmed = await poi.validatePOIMerkleroots(TXIDVersion.V2_PoseidonMerkle, chain, listKey, roots);
|
|
1402
|
+
if (!confirmed) return false;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
async function waitForConfirmedPoiRoots(chain, proved, onProofStatusRefresh) {
|
|
1408
|
+
const pois = proved.populated.preTransactionPOIsPerTxidLeafPerList;
|
|
1409
|
+
if (!poiListsPresent(pois)) {
|
|
1410
|
+
throw new Error("The proof did not include a proof of innocence.");
|
|
1411
|
+
}
|
|
1412
|
+
const deadline = Date.now() + POI_ROOT_WAIT_MS;
|
|
868
1413
|
while (Date.now() < deadline) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1414
|
+
try {
|
|
1415
|
+
if (await poiMerklerootsConfirmed(chain, pois)) return;
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
if (!broadcasterAskedToRefreshProofStatus(err)) throw err;
|
|
1418
|
+
}
|
|
1419
|
+
onProofStatusRefresh?.();
|
|
1420
|
+
await delay(POI_STATUS_RETRY_MS);
|
|
1421
|
+
}
|
|
1422
|
+
throw new Error("The proof-of-innocence list has not confirmed these notes. Wait a few minutes and try the private transfer again.");
|
|
1423
|
+
}
|
|
1424
|
+
async function submitFreshProof(args) {
|
|
1425
|
+
if (args.refreshProofStatus) {
|
|
1426
|
+
args.onProofStatusRefresh?.();
|
|
1427
|
+
await args.refreshProofStatus().catch(() => void 0);
|
|
1428
|
+
}
|
|
1429
|
+
await args.assertSpendable?.();
|
|
1430
|
+
const refused = /* @__PURE__ */ new Set();
|
|
1431
|
+
const chosenFeesId = args.broadcasterFeesId?.trim() ?? "";
|
|
1432
|
+
const chosenAddress = args.broadcasterAddress?.trim() ?? "";
|
|
1433
|
+
let feesId = chosenFeesId;
|
|
1434
|
+
let lastError = new Error("No Railgun broadcaster is available for this token.");
|
|
1435
|
+
let builds = 0;
|
|
1436
|
+
while (builds < MAX_GAS_BROADCASTERS) {
|
|
1437
|
+
args.onBroadcasterSearch?.();
|
|
1438
|
+
const rows = await loadRailgunBroadcasters({
|
|
1439
|
+
chainId: args.chainId,
|
|
1440
|
+
rpcUrl: args.rpcUrl,
|
|
1441
|
+
chainDetail: args.chainDetail,
|
|
1442
|
+
tokenAddress: args.tokenAddress,
|
|
1443
|
+
kind: args.kind,
|
|
1444
|
+
waitMs: BROADCASTER_PROOF_WAIT_MS
|
|
1445
|
+
});
|
|
1446
|
+
const picked = refused.size === 0 ? currentBroadcasterChoice(rows, feesId, chosenAddress, refused) : void 0;
|
|
1447
|
+
const row = picked?.row ?? nextCoveringBroadcaster(rows, refused) ?? (refused.size === 0 ? defaultBroadcasterChoice(rows) : void 0);
|
|
1448
|
+
if (!row || !broadcasterFeeStillValid(row.expiration)) {
|
|
1449
|
+
if (!row) break;
|
|
1450
|
+
refused.add(row.feesId);
|
|
1451
|
+
feesId = "";
|
|
1452
|
+
if (refused.size >= rows.length) break;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
builds += 1;
|
|
1456
|
+
feesId = row.feesId;
|
|
1457
|
+
const replaced = Boolean(chosenFeesId) && row.feesId !== chosenFeesId;
|
|
1458
|
+
const mode = refused.size > 0 ? "next" : replaced ? "refreshed" : "selected";
|
|
1459
|
+
args.onBroadcasterNote?.(broadcasterNote(row, mode));
|
|
1460
|
+
const broadcaster = row.selected;
|
|
1461
|
+
const proved = await args.build(broadcaster);
|
|
1462
|
+
if (!broadcasterFeeStillValid(broadcaster.tokenFee.expiration)) {
|
|
1463
|
+
broadcasterListCache.clear();
|
|
1464
|
+
refused.add(feesId);
|
|
1465
|
+
feesId = "";
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
await waitForConfirmedPoiRoots(args.chain, proved, args.onProofStatusRefresh);
|
|
1469
|
+
let switchBroadcaster = false;
|
|
1470
|
+
for (let poiTry = 1; poiTry <= BROADCASTER_PROOF_ATTEMPTS && !switchBroadcaster; poiTry += 1) {
|
|
1471
|
+
try {
|
|
1472
|
+
return {
|
|
1473
|
+
txHash: await submitProved({
|
|
1474
|
+
chain: args.chain,
|
|
1475
|
+
populated: proved.populated,
|
|
1476
|
+
broadcaster,
|
|
1477
|
+
overallBatchMinGasPrice: proved.overallBatchMinGasPrice
|
|
1478
|
+
})
|
|
1479
|
+
};
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
lastError = err;
|
|
1482
|
+
if (broadcasterRefusedForGas(err)) {
|
|
1483
|
+
refused.add(feesId);
|
|
1484
|
+
feesId = "";
|
|
1485
|
+
switchBroadcaster = true;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
if (poiTry === BROADCASTER_PROOF_ATTEMPTS || !broadcasterAskedToRefreshProofStatus(err)) throw err;
|
|
1489
|
+
args.onProofStatusRefresh?.();
|
|
1490
|
+
await delay(POI_STATUS_RETRY_MS);
|
|
1491
|
+
await args.refreshProofStatus?.().catch(() => void 0);
|
|
1492
|
+
if (!broadcasterFeeStillValid(broadcaster.tokenFee.expiration)) {
|
|
1493
|
+
broadcasterListCache.clear();
|
|
1494
|
+
refused.add(feesId);
|
|
1495
|
+
feesId = "";
|
|
1496
|
+
switchBroadcaster = true;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
await waitForConfirmedPoiRoots(args.chain, proved, args.onProofStatusRefresh);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
872
1502
|
}
|
|
873
|
-
|
|
1503
|
+
if (lastError instanceof Error) throw lastError;
|
|
1504
|
+
throw new Error("The broadcaster fee expired while the proof was building. Try the private transfer again.");
|
|
874
1505
|
}
|
|
875
1506
|
async function submitProved(args) {
|
|
876
1507
|
const to = args.populated.transaction.to;
|
|
877
1508
|
const data = args.populated.transaction.data;
|
|
878
1509
|
if (!to || !data) throw new Error("Railgun proof did not produce a transaction.");
|
|
879
1510
|
const { BroadcasterTransaction } = await broadcasterClient();
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1511
|
+
const detail = await broadcasterDetailConfig();
|
|
1512
|
+
const previousDetail = detail?.IS_DEV;
|
|
1513
|
+
if (detail) detail.IS_DEV = true;
|
|
1514
|
+
try {
|
|
1515
|
+
const tx = await BroadcasterTransaction.create(
|
|
1516
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1517
|
+
to,
|
|
1518
|
+
data,
|
|
1519
|
+
args.broadcaster.railgunAddress,
|
|
1520
|
+
args.broadcaster.tokenFee.feesID,
|
|
1521
|
+
args.chain,
|
|
1522
|
+
args.populated.nullifiers ?? [],
|
|
1523
|
+
args.overallBatchMinGasPrice,
|
|
1524
|
+
false,
|
|
1525
|
+
args.populated.preTransactionPOIsPerTxidLeafPerList
|
|
1526
|
+
);
|
|
1527
|
+
return tx.send();
|
|
1528
|
+
} finally {
|
|
1529
|
+
if (detail && previousDetail !== void 0) detail.IS_DEV = previousDetail;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async function broadcasterDetailConfig() {
|
|
1533
|
+
const node = await nodeRequire();
|
|
1534
|
+
if (!node) return null;
|
|
1535
|
+
const entry = node.require.resolve("@railgun-community/waku-broadcaster-client-web");
|
|
1536
|
+
const loaded = node.require(node.path.join(node.path.dirname(entry), "models/broadcaster-config.js"));
|
|
1537
|
+
return loaded.BroadcasterConfig;
|
|
893
1538
|
}
|
|
894
1539
|
function feeRecipient(wallet, broadcaster, gasDetails) {
|
|
895
1540
|
const fee = wallet.calculateBroadcasterFeeERC20Amount(
|
|
@@ -905,71 +1550,176 @@ function feeRecipient(wallet, broadcaster, gasDetails) {
|
|
|
905
1550
|
recipientAddress: broadcaster.railgunAddress
|
|
906
1551
|
};
|
|
907
1552
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
await assertDeployed(args.chainId);
|
|
1553
|
+
var broadcasterFeeCache = /* @__PURE__ */ new Map();
|
|
1554
|
+
async function quoteRailgunBroadcasterFee(args) {
|
|
911
1555
|
const tokenAddress = getAddress(args.tokenAddress);
|
|
1556
|
+
const cacheKey = `${args.kind}:${args.chainId}:${tokenAddress.toLowerCase()}`;
|
|
1557
|
+
const cached2 = broadcasterFeeCache.get(cacheKey);
|
|
1558
|
+
if (cached2 && Date.now() - cached2.at < BROADCASTER_CACHE_MS) {
|
|
1559
|
+
return { feeWei: cached2.feeWei, tokenAddress: cached2.tokenAddress };
|
|
1560
|
+
}
|
|
912
1561
|
const amount = parsedAmount(args.amountWei);
|
|
913
|
-
const
|
|
914
|
-
if (!recipientAddress.startsWith("0zk")) throw new Error("Private transfer recipient must be a 0zk address.");
|
|
915
|
-
const session2 = requireRailgunWalletSession();
|
|
916
|
-
const rpcUrl = args.rpcUrl?.trim() ?? "";
|
|
917
|
-
if (!rpcUrl || !args.chainDetail) throw new Error("rpcUrl and chain details are required.");
|
|
918
|
-
const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
|
|
1562
|
+
const networkName = await ensureRailgunProvider(args.chainId, args.rpcUrl);
|
|
919
1563
|
const wallet = await import('@railgun-community/wallet');
|
|
920
|
-
const
|
|
921
|
-
const
|
|
922
|
-
|
|
1564
|
+
const priced = await chainDetailWithLiveGas(args.rpcUrl, args.chainDetail);
|
|
1565
|
+
const listed = await loadRailgunBroadcasters({
|
|
1566
|
+
chainId: args.chainId,
|
|
1567
|
+
rpcUrl: args.rpcUrl,
|
|
1568
|
+
chainDetail: args.chainDetail,
|
|
1569
|
+
tokenAddress,
|
|
1570
|
+
kind: args.kind
|
|
1571
|
+
});
|
|
1572
|
+
const chosen = defaultBroadcasterChoice(listed);
|
|
1573
|
+
if (!chosen) throw new Error("No Railgun broadcaster is available for this token.");
|
|
1574
|
+
const broadcaster = chosen.selected;
|
|
923
1575
|
const feeToken = {
|
|
924
1576
|
tokenAddress: broadcaster.tokenAddress,
|
|
925
1577
|
feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
|
|
926
1578
|
};
|
|
927
|
-
const original =
|
|
928
|
-
const
|
|
1579
|
+
const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
|
|
1580
|
+
const recipientAddress = args.kind === "unshield" ? getAddress(args.recipientAddress || "0x0000000000000000000000000000000000000001") : args.recipientAddress.trim();
|
|
1581
|
+
if (args.kind === "transfer" && !recipientAddress.startsWith("0zk")) {
|
|
1582
|
+
throw new Error("A 0zk address is required to estimate the private-transfer fee.");
|
|
1583
|
+
}
|
|
1584
|
+
const recipients = [{ tokenAddress, amount, recipientAddress }];
|
|
1585
|
+
const estimated = args.kind === "unshield" ? await wallet.gasEstimateForUnprovenUnshield(
|
|
929
1586
|
TXIDVersion.V2_PoseidonMerkle,
|
|
930
1587
|
networkName,
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
args.memoText ?? null,
|
|
1588
|
+
args.walletId,
|
|
1589
|
+
args.encryptionKey,
|
|
934
1590
|
recipients,
|
|
935
1591
|
[],
|
|
936
1592
|
original,
|
|
937
1593
|
feeToken,
|
|
938
1594
|
false
|
|
939
|
-
)
|
|
940
|
-
const gasDetails = railgunGasDetails(args.chainDetail, gasEstimate);
|
|
941
|
-
const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
|
|
942
|
-
const overallBatchMinGasPrice = calculateGasPrice(gasDetails);
|
|
943
|
-
await wallet.generateTransferProof(
|
|
944
|
-
TXIDVersion.V2_PoseidonMerkle,
|
|
945
|
-
networkName,
|
|
946
|
-
session2.walletId,
|
|
947
|
-
session2.encryptionKey,
|
|
948
|
-
false,
|
|
949
|
-
args.memoText ?? null,
|
|
950
|
-
recipients,
|
|
951
|
-
[],
|
|
952
|
-
broadcasterFee,
|
|
953
|
-
false,
|
|
954
|
-
overallBatchMinGasPrice,
|
|
955
|
-
(progress) => args.onProofProgress?.(progress)
|
|
956
|
-
);
|
|
957
|
-
const populated = await wallet.populateProvedTransfer(
|
|
1595
|
+
) : await wallet.gasEstimateForUnprovenTransfer(
|
|
958
1596
|
TXIDVersion.V2_PoseidonMerkle,
|
|
959
1597
|
networkName,
|
|
960
|
-
|
|
961
|
-
|
|
1598
|
+
args.walletId,
|
|
1599
|
+
args.encryptionKey,
|
|
962
1600
|
args.memoText ?? null,
|
|
963
1601
|
recipients,
|
|
964
1602
|
[],
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
gasDetails
|
|
1603
|
+
original,
|
|
1604
|
+
feeToken,
|
|
1605
|
+
false
|
|
969
1606
|
);
|
|
970
|
-
|
|
1607
|
+
const fee = feeRecipient(wallet, broadcaster, railgunBroadcasterGasDetails(networkName, priced, estimated.gasEstimate));
|
|
1608
|
+
const quoted = { feeWei: fee.amount.toString(), tokenAddress: getAddress(fee.tokenAddress) };
|
|
1609
|
+
broadcasterFeeCache.set(cacheKey, { at: Date.now(), ...quoted });
|
|
1610
|
+
return quoted;
|
|
1611
|
+
}
|
|
1612
|
+
async function quoteRailgunBroadcasterFeeRate(args) {
|
|
1613
|
+
const tokenAddress = getAddress(args.tokenAddress);
|
|
1614
|
+
const network = await railgunNetworkByChainId(args.chainId);
|
|
1615
|
+
if (!network) throw new Error("Railgun is not deployed on this chain.");
|
|
1616
|
+
const listed = await loadRailgunBroadcasters({
|
|
1617
|
+
chainId: args.chainId,
|
|
1618
|
+
rpcUrl: args.rpcUrl,
|
|
1619
|
+
chainDetail: args.chainDetail,
|
|
1620
|
+
tokenAddress,
|
|
1621
|
+
kind: args.kind
|
|
1622
|
+
});
|
|
1623
|
+
const chosen = defaultBroadcasterChoice(listed);
|
|
1624
|
+
if (!chosen) throw new Error("No Railgun broadcaster is available for this token.");
|
|
1625
|
+
return { feeWei: chosen.feeWei, tokenAddress };
|
|
1626
|
+
}
|
|
1627
|
+
async function railgunPrivateTransfer(args) {
|
|
1628
|
+
try {
|
|
1629
|
+
return await provePrivateTransfer(args);
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
throw new Error(railgunDetailedError(err));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
async function provePrivateTransfer(args) {
|
|
1635
|
+
assertRailgunCtmAllowed(args.chainId, args.tokenAddress);
|
|
1636
|
+
await assertDeployed(args.chainId);
|
|
1637
|
+
const tokenAddress = getAddress(args.tokenAddress);
|
|
1638
|
+
const amount = parsedAmount(args.amountWei);
|
|
1639
|
+
const recipientAddress = args.recipientAddress.trim();
|
|
1640
|
+
if (!recipientAddress.startsWith("0zk")) throw new Error("Private transfer recipient must be a 0zk address.");
|
|
1641
|
+
const session2 = requireRailgunWalletSession();
|
|
1642
|
+
const rpcUrl = args.rpcUrl?.trim() ?? "";
|
|
1643
|
+
if (!rpcUrl || !args.chainDetail) throw new Error("rpcUrl and chain details are required.");
|
|
1644
|
+
const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
|
|
1645
|
+
const wallet = await import('@railgun-community/wallet');
|
|
1646
|
+
const chain = chainFor(args.chainId);
|
|
1647
|
+
const priced = await chainDetailWithLiveGas(rpcUrl, args.chainDetail);
|
|
1648
|
+
const recipients = [{ tokenAddress, amount, recipientAddress }];
|
|
1649
|
+
return submitFreshProof({
|
|
1650
|
+
chainId: args.chainId,
|
|
1651
|
+
rpcUrl,
|
|
1652
|
+
chainDetail: args.chainDetail,
|
|
1653
|
+
kind: "transfer",
|
|
1654
|
+
chain,
|
|
1655
|
+
tokenAddress,
|
|
1656
|
+
broadcasterAddress: args.broadcasterAddress,
|
|
1657
|
+
broadcasterFeesId: args.broadcasterFeesId,
|
|
1658
|
+
onBroadcasterSearch: args.onBroadcasterSearch,
|
|
1659
|
+
onBroadcasterNote: args.onBroadcasterNote,
|
|
1660
|
+
onProofStatusRefresh: args.onProofStatusRefresh,
|
|
1661
|
+
refreshProofStatus: () => refreshSpendingProofStatus(networkName, session2.walletId),
|
|
1662
|
+
assertSpendable: () => assertTokenSpendable(networkName, session2.walletId, tokenAddress, amount),
|
|
1663
|
+
build: async (broadcaster) => {
|
|
1664
|
+
const feeToken = {
|
|
1665
|
+
tokenAddress: broadcaster.tokenAddress,
|
|
1666
|
+
feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
|
|
1667
|
+
};
|
|
1668
|
+
const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
|
|
1669
|
+
const { gasEstimate } = await wallet.gasEstimateForUnprovenTransfer(
|
|
1670
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1671
|
+
networkName,
|
|
1672
|
+
session2.walletId,
|
|
1673
|
+
session2.encryptionKey,
|
|
1674
|
+
args.memoText ?? null,
|
|
1675
|
+
recipients,
|
|
1676
|
+
[],
|
|
1677
|
+
original,
|
|
1678
|
+
feeToken,
|
|
1679
|
+
false
|
|
1680
|
+
);
|
|
1681
|
+
const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
|
|
1682
|
+
const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
|
|
1683
|
+
const overallBatchMinGasPrice = calculateGasPrice(gasDetails);
|
|
1684
|
+
await wallet.generateTransferProof(
|
|
1685
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1686
|
+
networkName,
|
|
1687
|
+
session2.walletId,
|
|
1688
|
+
session2.encryptionKey,
|
|
1689
|
+
false,
|
|
1690
|
+
args.memoText ?? null,
|
|
1691
|
+
recipients,
|
|
1692
|
+
[],
|
|
1693
|
+
broadcasterFee,
|
|
1694
|
+
false,
|
|
1695
|
+
overallBatchMinGasPrice,
|
|
1696
|
+
(progress) => args.onProofProgress?.(progress)
|
|
1697
|
+
);
|
|
1698
|
+
const populated = await wallet.populateProvedTransfer(
|
|
1699
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1700
|
+
networkName,
|
|
1701
|
+
session2.walletId,
|
|
1702
|
+
false,
|
|
1703
|
+
args.memoText ?? null,
|
|
1704
|
+
recipients,
|
|
1705
|
+
[],
|
|
1706
|
+
broadcasterFee,
|
|
1707
|
+
false,
|
|
1708
|
+
overallBatchMinGasPrice,
|
|
1709
|
+
gasDetails
|
|
1710
|
+
);
|
|
1711
|
+
return { populated, overallBatchMinGasPrice };
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
971
1714
|
}
|
|
972
1715
|
async function railgunUnshield(args) {
|
|
1716
|
+
try {
|
|
1717
|
+
return await proveUnshield(args);
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
throw new Error(railgunDetailedError(err));
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
async function proveUnshield(args) {
|
|
973
1723
|
assertRailgunCtmAllowed(args.chainId, args.tokenAddress);
|
|
974
1724
|
await assertDeployed(args.chainId);
|
|
975
1725
|
const tokenAddress = getAddress(args.tokenAddress);
|
|
@@ -981,51 +1731,149 @@ async function railgunUnshield(args) {
|
|
|
981
1731
|
const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
|
|
982
1732
|
const wallet = await import('@railgun-community/wallet');
|
|
983
1733
|
const chain = chainFor(args.chainId);
|
|
984
|
-
const
|
|
1734
|
+
const priced = await chainDetailWithLiveGas(rpcUrl, args.chainDetail);
|
|
985
1735
|
const recipients = [{ tokenAddress, amount, recipientAddress }];
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1736
|
+
return submitFreshProof({
|
|
1737
|
+
chainId: args.chainId,
|
|
1738
|
+
rpcUrl,
|
|
1739
|
+
chainDetail: args.chainDetail,
|
|
1740
|
+
kind: "unshield",
|
|
1741
|
+
chain,
|
|
1742
|
+
tokenAddress,
|
|
1743
|
+
broadcasterAddress: args.broadcasterAddress,
|
|
1744
|
+
broadcasterFeesId: args.broadcasterFeesId,
|
|
1745
|
+
onBroadcasterSearch: args.onBroadcasterSearch,
|
|
1746
|
+
onBroadcasterNote: args.onBroadcasterNote,
|
|
1747
|
+
onProofStatusRefresh: args.onProofStatusRefresh,
|
|
1748
|
+
refreshProofStatus: () => refreshSpendingProofStatus(networkName, session2.walletId),
|
|
1749
|
+
assertSpendable: () => assertTokenSpendable(networkName, session2.walletId, tokenAddress, amount),
|
|
1750
|
+
build: async (broadcaster) => {
|
|
1751
|
+
const feeToken = {
|
|
1752
|
+
tokenAddress: broadcaster.tokenAddress,
|
|
1753
|
+
feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
|
|
1754
|
+
};
|
|
1755
|
+
const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
|
|
1756
|
+
const { gasEstimate } = await wallet.gasEstimateForUnprovenUnshield(
|
|
1757
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1758
|
+
networkName,
|
|
1759
|
+
session2.walletId,
|
|
1760
|
+
session2.encryptionKey,
|
|
1761
|
+
recipients,
|
|
1762
|
+
[],
|
|
1763
|
+
original,
|
|
1764
|
+
feeToken,
|
|
1765
|
+
false
|
|
1766
|
+
);
|
|
1767
|
+
const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
|
|
1768
|
+
const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
|
|
1769
|
+
const overallBatchMinGasPrice = calculateGasPrice(gasDetails);
|
|
1770
|
+
await wallet.generateUnshieldProof(
|
|
1771
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1772
|
+
networkName,
|
|
1773
|
+
session2.walletId,
|
|
1774
|
+
session2.encryptionKey,
|
|
1775
|
+
recipients,
|
|
1776
|
+
[],
|
|
1777
|
+
broadcasterFee,
|
|
1778
|
+
false,
|
|
1779
|
+
overallBatchMinGasPrice,
|
|
1780
|
+
(progress) => args.onProofProgress?.(progress)
|
|
1781
|
+
);
|
|
1782
|
+
const populated = await wallet.populateProvedUnshield(
|
|
1783
|
+
TXIDVersion.V2_PoseidonMerkle,
|
|
1784
|
+
networkName,
|
|
1785
|
+
session2.walletId,
|
|
1786
|
+
recipients,
|
|
1787
|
+
[],
|
|
1788
|
+
broadcasterFee,
|
|
1789
|
+
false,
|
|
1790
|
+
overallBatchMinGasPrice,
|
|
1791
|
+
gasDetails
|
|
1792
|
+
);
|
|
1793
|
+
return { populated, overallBatchMinGasPrice };
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
var feeAbi = parseAbi([
|
|
1798
|
+
"function shieldFee() view returns (uint256)",
|
|
1799
|
+
"function unshieldFee() view returns (uint256)"
|
|
1800
|
+
]);
|
|
1801
|
+
var BASIS_POINTS = 10000n;
|
|
1802
|
+
var FEE_CACHE_MS = 6e4;
|
|
1803
|
+
var basisCache = /* @__PURE__ */ new Map();
|
|
1804
|
+
function railgunProtocolFee(amount, basisPoints) {
|
|
1805
|
+
const value = amount > 0n ? amount : 0n;
|
|
1806
|
+
const bps = basisPoints > 0n ? basisPoints : 0n;
|
|
1807
|
+
const fee = value * bps / BASIS_POINTS;
|
|
1808
|
+
return { fee, received: value - fee };
|
|
1809
|
+
}
|
|
1810
|
+
async function protocolBasisPoints(chainId, rpcUrl) {
|
|
1811
|
+
const cached2 = basisCache.get(chainId);
|
|
1812
|
+
if (cached2 && Date.now() - cached2.at < FEE_CACHE_MS) return cached2;
|
|
1813
|
+
const network = await railgunNetworkByChainId(chainId);
|
|
1814
|
+
if (!network) throw new Error("Railgun is not deployed on this chain.");
|
|
1815
|
+
const client = createPublicClient({ transport: defiEvmTransport(rpcUrl) });
|
|
1816
|
+
const address = getAddress(network.proxyContract);
|
|
1817
|
+
const [shield, unshield] = await Promise.all([
|
|
1818
|
+
client.readContract({ address, abi: feeAbi, functionName: "shieldFee" }),
|
|
1819
|
+
client.readContract({ address, abi: feeAbi, functionName: "unshieldFee" })
|
|
1820
|
+
]);
|
|
1821
|
+
const next = { shield, unshield, at: Date.now() };
|
|
1822
|
+
basisCache.set(chainId, next);
|
|
1823
|
+
return next;
|
|
1824
|
+
}
|
|
1825
|
+
function clearRailgunFeeCache() {
|
|
1826
|
+
basisCache.clear();
|
|
1827
|
+
}
|
|
1828
|
+
async function quoteRailgunFee(args) {
|
|
1829
|
+
const rpcUrl = args.rpcUrl.trim();
|
|
1830
|
+
if (!rpcUrl) throw new Error("rpcUrl is required.");
|
|
1831
|
+
const amount = (() => {
|
|
1832
|
+
try {
|
|
1833
|
+
return BigInt(args.amountWei.trim() || "0");
|
|
1834
|
+
} catch {
|
|
1835
|
+
throw new Error("Amount must be an integer in wei.");
|
|
1836
|
+
}
|
|
1837
|
+
})();
|
|
1838
|
+
if (amount < 0n) throw new Error("Amount must be greater than zero.");
|
|
1839
|
+
const rates = await protocolBasisPoints(args.chainId, rpcUrl);
|
|
1840
|
+
const basisPoints = args.kind === "shield" ? rates.shield : args.kind === "unshield" ? rates.unshield : 0n;
|
|
1841
|
+
const protocol = railgunProtocolFee(amount, basisPoints);
|
|
1842
|
+
let broadcasterFee = 0n;
|
|
1843
|
+
let broadcasterTokenAddress = "";
|
|
1844
|
+
let broadcasterError;
|
|
1845
|
+
if (args.kind !== "shield") {
|
|
1846
|
+
if (!args.chainDetail) {
|
|
1847
|
+
broadcasterError = "Chain gas details are required to estimate the broadcaster fee.";
|
|
1848
|
+
} else {
|
|
1849
|
+
try {
|
|
1850
|
+
const quoted = await quoteRailgunBroadcasterFeeRate({
|
|
1851
|
+
kind: args.kind,
|
|
1852
|
+
chainId: args.chainId,
|
|
1853
|
+
rpcUrl,
|
|
1854
|
+
chainDetail: args.chainDetail,
|
|
1855
|
+
tokenAddress: args.tokenAddress
|
|
1856
|
+
});
|
|
1857
|
+
broadcasterFee = BigInt(quoted.feeWei);
|
|
1858
|
+
broadcasterTokenAddress = quoted.tokenAddress;
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
broadcasterError = err instanceof Error ? err.message : "Broadcaster fee is not available.";
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const totalDebit = args.kind === "shield" ? amount : amount + broadcasterFee;
|
|
1865
|
+
const quote = {
|
|
1866
|
+
kind: args.kind,
|
|
1867
|
+
amountWei: amount.toString(),
|
|
1868
|
+
protocolFeeBasisPoints: basisPoints.toString(),
|
|
1869
|
+
protocolFeeWei: protocol.fee.toString(),
|
|
1870
|
+
receivedWei: (args.kind === "transfer" ? amount : protocol.received).toString(),
|
|
1871
|
+
broadcasterFeeWei: broadcasterFee.toString(),
|
|
1872
|
+
broadcasterTokenAddress,
|
|
1873
|
+
totalDebitWei: totalDebit.toString()
|
|
989
1874
|
};
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
TXIDVersion.V2_PoseidonMerkle,
|
|
993
|
-
networkName,
|
|
994
|
-
session2.walletId,
|
|
995
|
-
session2.encryptionKey,
|
|
996
|
-
recipients,
|
|
997
|
-
[],
|
|
998
|
-
original,
|
|
999
|
-
feeToken,
|
|
1000
|
-
false
|
|
1001
|
-
);
|
|
1002
|
-
const gasDetails = railgunGasDetails(args.chainDetail, gasEstimate);
|
|
1003
|
-
const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
|
|
1004
|
-
const overallBatchMinGasPrice = calculateGasPrice(gasDetails);
|
|
1005
|
-
await wallet.generateUnshieldProof(
|
|
1006
|
-
TXIDVersion.V2_PoseidonMerkle,
|
|
1007
|
-
networkName,
|
|
1008
|
-
session2.walletId,
|
|
1009
|
-
session2.encryptionKey,
|
|
1010
|
-
recipients,
|
|
1011
|
-
[],
|
|
1012
|
-
broadcasterFee,
|
|
1013
|
-
false,
|
|
1014
|
-
overallBatchMinGasPrice,
|
|
1015
|
-
(progress) => args.onProofProgress?.(progress)
|
|
1016
|
-
);
|
|
1017
|
-
const populated = await wallet.populateProvedUnshield(
|
|
1018
|
-
TXIDVersion.V2_PoseidonMerkle,
|
|
1019
|
-
networkName,
|
|
1020
|
-
session2.walletId,
|
|
1021
|
-
recipients,
|
|
1022
|
-
[],
|
|
1023
|
-
broadcasterFee,
|
|
1024
|
-
false,
|
|
1025
|
-
overallBatchMinGasPrice,
|
|
1026
|
-
gasDetails
|
|
1027
|
-
);
|
|
1028
|
-
return { txHash: await submitProved({ chain, populated, broadcaster, overallBatchMinGasPrice }) };
|
|
1875
|
+
if (broadcasterError) quote.broadcasterError = broadcasterError;
|
|
1876
|
+
return quote;
|
|
1029
1877
|
}
|
|
1030
1878
|
async function walletModule2() {
|
|
1031
1879
|
return await import('@railgun-community/wallet');
|
|
@@ -1064,6 +1912,16 @@ async function railgunShareableViewingKey(walletId) {
|
|
|
1064
1912
|
if (!viewingKey) throw new Error("Railgun viewing key is not available.");
|
|
1065
1913
|
return viewingKey;
|
|
1066
1914
|
}
|
|
1915
|
+
async function rememberKeyGenWallet(keyGenId, walletId, railgunAddress) {
|
|
1916
|
+
const root = await resolveRailgunDataRoot();
|
|
1917
|
+
if (!root) return;
|
|
1918
|
+
const load = new Function("id", "return import(id)");
|
|
1919
|
+
const fs = await load("node:fs/promises");
|
|
1920
|
+
const file = railgunKeyGenWalletPath(root, keyGenId);
|
|
1921
|
+
await fs.mkdir(file.slice(0, file.lastIndexOf("/")), { recursive: true });
|
|
1922
|
+
await fs.writeFile(file, `${JSON.stringify({ walletId, railgunAddress })}
|
|
1923
|
+
`, { mode: 384 });
|
|
1924
|
+
}
|
|
1067
1925
|
async function openRailgunViewOnlyWallet(args) {
|
|
1068
1926
|
await ensureRailgunEngine();
|
|
1069
1927
|
const network = await railgunNetworkByChainId(args.chainId);
|
|
@@ -1073,6 +1931,7 @@ async function openRailgunViewOnlyWallet(args) {
|
|
|
1073
1931
|
args.shareableViewingKey,
|
|
1074
1932
|
network ? { [network.networkName]: network.deploymentBlock } : void 0
|
|
1075
1933
|
);
|
|
1934
|
+
if (args.keyGenId) await rememberKeyGenWallet(args.keyGenId, created.id, created.railgunAddress);
|
|
1076
1935
|
return { walletId: created.id, railgunAddress: created.railgunAddress };
|
|
1077
1936
|
}
|
|
1078
1937
|
async function discardRailgunWallet(walletId) {
|
|
@@ -1085,11 +1944,15 @@ function railgunAddressIsValid(address) {
|
|
|
1085
1944
|
return walletModule2().then((wallet) => wallet.validateRailgunAddress(address));
|
|
1086
1945
|
}
|
|
1087
1946
|
var buckets = /* @__PURE__ */ new Map();
|
|
1088
|
-
function
|
|
1089
|
-
|
|
1947
|
+
function bucketKey(walletId, chainId) {
|
|
1948
|
+
return `${walletId}:${chainId}`;
|
|
1949
|
+
}
|
|
1950
|
+
function rowMap(walletId, chainId) {
|
|
1951
|
+
const key = bucketKey(walletId, chainId);
|
|
1952
|
+
let rows = buckets.get(key);
|
|
1090
1953
|
if (!rows) {
|
|
1091
1954
|
rows = /* @__PURE__ */ new Map();
|
|
1092
|
-
buckets.set(
|
|
1955
|
+
buckets.set(key, rows);
|
|
1093
1956
|
}
|
|
1094
1957
|
return rows;
|
|
1095
1958
|
}
|
|
@@ -1102,9 +1965,18 @@ function cellFor(rows, tokenAddress) {
|
|
|
1102
1965
|
}
|
|
1103
1966
|
return cell;
|
|
1104
1967
|
}
|
|
1968
|
+
function recordRailgunWalletBalance(event) {
|
|
1969
|
+
applyBalanceEvent(event);
|
|
1970
|
+
}
|
|
1971
|
+
function railgunWalletBalanceSnapshot(walletId, chainId) {
|
|
1972
|
+
return snapshotRows(walletId, chainId);
|
|
1973
|
+
}
|
|
1974
|
+
function clearRailgunWalletBalances() {
|
|
1975
|
+
buckets.clear();
|
|
1976
|
+
}
|
|
1105
1977
|
function applyBalanceEvent(event) {
|
|
1106
|
-
if (event.txidVersion !== TXIDVersion.V2_PoseidonMerkle) return;
|
|
1107
|
-
const rows = rowMap(event.chain.id);
|
|
1978
|
+
if (!event.railgunWalletID || event.txidVersion !== TXIDVersion.V2_PoseidonMerkle) return;
|
|
1979
|
+
const rows = rowMap(event.railgunWalletID, event.chain.id);
|
|
1108
1980
|
const spendable = event.balanceBucket === RailgunWalletBalanceBucket.Spendable;
|
|
1109
1981
|
const pending = event.balanceBucket === RailgunWalletBalanceBucket.ShieldPending || event.balanceBucket === RailgunWalletBalanceBucket.ProofSubmitted || event.balanceBucket === RailgunWalletBalanceBucket.MissingExternalPOI || event.balanceBucket === RailgunWalletBalanceBucket.MissingInternalPOI;
|
|
1110
1982
|
if (!spendable && !pending) return;
|
|
@@ -1120,8 +1992,8 @@ function applyBalanceEvent(event) {
|
|
|
1120
1992
|
if (pending) cell.pending.set(event.balanceBucket, amount.amount);
|
|
1121
1993
|
}
|
|
1122
1994
|
}
|
|
1123
|
-
function snapshotRows(chainId) {
|
|
1124
|
-
const rows = buckets.get(chainId);
|
|
1995
|
+
function snapshotRows(walletId, chainId) {
|
|
1996
|
+
const rows = buckets.get(bucketKey(walletId, chainId));
|
|
1125
1997
|
if (!rows) return [];
|
|
1126
1998
|
return [...rows.entries()].filter(([, cell]) => cell.spendable > 0n || [...cell.pending.values()].some((amount) => amount > 0n)).map(([tokenAddress, cell]) => ({
|
|
1127
1999
|
tokenAddress,
|
|
@@ -1134,49 +2006,315 @@ function utxoScanTerminalStatus(scanStatus) {
|
|
|
1134
2006
|
if (scanStatus === "Incomplete") return "failed";
|
|
1135
2007
|
return "pending";
|
|
1136
2008
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
2009
|
+
var RAILGUN_RESCAN_QUIET_MS = 4e3;
|
|
2010
|
+
var RAILGUN_TXID_WAIT_AFTER_NOTES_MS = 45e3;
|
|
2011
|
+
var RAILGUN_CATCH_UP_MS = 12e4;
|
|
2012
|
+
var RAILGUN_MERKLETREE_STALL_MS = 5 * 60 * 1e3;
|
|
2013
|
+
var RAILGUN_SCAN_BEHIND_NOTE = "The shielded balance is saved. The newest blocks did not finish syncing, so a shield that just landed may be missing until the next scan.";
|
|
2014
|
+
var RAILGUN_SCAN_STOPPED_NOTE = "The note scan stopped before it reached the latest block. The shielded balance is unchanged until a scan finishes.";
|
|
2015
|
+
function merkletreeScanStillRunning(args) {
|
|
2016
|
+
return args.sawProgress && !args.notesFinished && args.terminal === "pending";
|
|
2017
|
+
}
|
|
2018
|
+
function railgunScanRecord(merkletree, balanceCount, caughtUp) {
|
|
2019
|
+
if (merkletree !== "done") {
|
|
2020
|
+
return { scanStatus: "error", notes: RAILGUN_SCAN_STOPPED_NOTE };
|
|
2021
|
+
}
|
|
2022
|
+
if (!caughtUp) {
|
|
2023
|
+
return {
|
|
2024
|
+
scanStatus: balanceCount === 0 ? "empty" : "ready",
|
|
2025
|
+
notes: RAILGUN_SCAN_BEHIND_NOTE
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
return {
|
|
2029
|
+
scanStatus: balanceCount === 0 ? "empty" : "ready",
|
|
2030
|
+
notes: balanceCount === 0 ? "No private balance on this chain yet." : ""
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function balancesToKeepAfterNoteScan(decrypted, afterRefresh, previous) {
|
|
2034
|
+
if (afterRefresh.length > 0) return afterRefresh;
|
|
2035
|
+
if (decrypted.length > 0) return decrypted;
|
|
2036
|
+
return previous;
|
|
2037
|
+
}
|
|
2038
|
+
var utxoListeners = /* @__PURE__ */ new Set();
|
|
2039
|
+
var utxoHooked = false;
|
|
2040
|
+
var balanceListeners = /* @__PURE__ */ new Set();
|
|
2041
|
+
var balanceHooked = false;
|
|
2042
|
+
var scansInFlight = /* @__PURE__ */ new Map();
|
|
2043
|
+
function delay2(ms) {
|
|
2044
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2045
|
+
}
|
|
2046
|
+
async function hookUtxoScans(wallet) {
|
|
2047
|
+
if (utxoHooked) return;
|
|
2048
|
+
utxoHooked = true;
|
|
2049
|
+
wallet.setOnUTXOMerkletreeScanCallback((event) => {
|
|
2050
|
+
for (const listener of utxoListeners) listener(event);
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
async function hookBalanceUpdates(wallet) {
|
|
2054
|
+
if (balanceHooked) return;
|
|
2055
|
+
balanceHooked = true;
|
|
2056
|
+
wallet.setOnBalanceUpdateCallback((event) => {
|
|
2057
|
+
for (const listener of balanceListeners) listener(event);
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function publishScan(chainId, scanStatus, balances, settled, notes) {
|
|
2061
|
+
const text = notes ?? (balances.length === 0 ? "No private balance on this chain yet." : void 0);
|
|
2062
|
+
publishRailgunPrivateBalanceSnapshot({
|
|
2063
|
+
chainId,
|
|
2064
|
+
scanStatus,
|
|
2065
|
+
balances,
|
|
2066
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2067
|
+
notes: text
|
|
2068
|
+
});
|
|
2069
|
+
return { scanStatus, balances, settled, notes: text ?? "" };
|
|
2070
|
+
}
|
|
2071
|
+
async function scanRailgunPrivateBalancesOnce(args) {
|
|
2072
|
+
const previous = snapshotRows(args.walletId, args.chainId);
|
|
1141
2073
|
publishRailgunPrivateBalanceSnapshot({
|
|
1142
2074
|
chainId: args.chainId,
|
|
1143
2075
|
scanStatus: "scanning",
|
|
1144
|
-
balances:
|
|
2076
|
+
balances: previous,
|
|
1145
2077
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1146
2078
|
});
|
|
1147
2079
|
const wallet = await walletModule2();
|
|
1148
2080
|
let resolveScan = () => {
|
|
1149
2081
|
};
|
|
1150
|
-
|
|
2082
|
+
let scanSettled = new Promise((resolve) => {
|
|
1151
2083
|
resolveScan = resolve;
|
|
1152
2084
|
});
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
2085
|
+
const armScanWait = () => {
|
|
2086
|
+
scanSettled = new Promise((resolve) => {
|
|
2087
|
+
resolveScan = resolve;
|
|
2088
|
+
});
|
|
2089
|
+
};
|
|
2090
|
+
let sawUtxoEvent = false;
|
|
2091
|
+
let lastUtxoAt = 0;
|
|
2092
|
+
let replacedBuckets = false;
|
|
2093
|
+
let resolveNotesEnd = () => {
|
|
2094
|
+
};
|
|
2095
|
+
const notesEnded = new Promise((resolve) => {
|
|
2096
|
+
resolveNotesEnd = resolve;
|
|
1156
2097
|
});
|
|
1157
|
-
|
|
2098
|
+
const publishLiveBalances = () => {
|
|
2099
|
+
const rows = snapshotRows(args.walletId, args.chainId);
|
|
2100
|
+
if (rows.length > 0) args.onBalances?.(rows);
|
|
2101
|
+
};
|
|
2102
|
+
let notesFinished = false;
|
|
2103
|
+
const listener = (event) => {
|
|
1158
2104
|
if (event.chain.id !== args.chainId) return;
|
|
2105
|
+
if (!replacedBuckets) {
|
|
2106
|
+
buckets.set(bucketKey(args.walletId, args.chainId), /* @__PURE__ */ new Map());
|
|
2107
|
+
replacedBuckets = true;
|
|
2108
|
+
}
|
|
2109
|
+
sawUtxoEvent = true;
|
|
2110
|
+
lastUtxoAt = Date.now();
|
|
1159
2111
|
args.onProgress?.(event.progress);
|
|
1160
2112
|
const terminal = utxoScanTerminalStatus(event.scanStatus);
|
|
2113
|
+
if (event.progress >= 1 || terminal === "done") {
|
|
2114
|
+
notesFinished = true;
|
|
2115
|
+
publishLiveBalances();
|
|
2116
|
+
resolveNotesEnd();
|
|
2117
|
+
}
|
|
1161
2118
|
if (terminal !== "pending") resolveScan(terminal);
|
|
1162
|
-
}
|
|
2119
|
+
};
|
|
2120
|
+
await hookUtxoScans(wallet);
|
|
2121
|
+
await hookBalanceUpdates(wallet);
|
|
2122
|
+
utxoListeners.add(listener);
|
|
2123
|
+
const onBalance = (event) => {
|
|
2124
|
+
if (event.railgunWalletID !== args.walletId || event.chain.id !== args.chainId) return;
|
|
2125
|
+
recordRailgunWalletBalance(event);
|
|
2126
|
+
publishLiveBalances();
|
|
2127
|
+
};
|
|
2128
|
+
balanceListeners.add(onBalance);
|
|
2129
|
+
try {
|
|
2130
|
+
await ensureRailgunProvider(args.chainId, args.rpcUrl);
|
|
2131
|
+
const chain = { type: ChainType.EVM, id: args.chainId };
|
|
2132
|
+
const refresh = wallet.refreshBalances(chain, [args.walletId]).then(
|
|
2133
|
+
() => "returned",
|
|
2134
|
+
() => "returned"
|
|
2135
|
+
);
|
|
2136
|
+
const quiet = delay2(RAILGUN_RESCAN_QUIET_MS).then(() => "quiet");
|
|
2137
|
+
const notesTimeout = notesEnded.then(() => delay2(RAILGUN_TXID_WAIT_AFTER_NOTES_MS)).then(() => "notes-timeout");
|
|
2138
|
+
let status = await Promise.race([
|
|
2139
|
+
scanSettled,
|
|
2140
|
+
refresh,
|
|
2141
|
+
quiet,
|
|
2142
|
+
notesTimeout
|
|
2143
|
+
]);
|
|
2144
|
+
if (status === "quiet") {
|
|
2145
|
+
status = await Promise.race([
|
|
2146
|
+
scanSettled,
|
|
2147
|
+
refresh,
|
|
2148
|
+
notesTimeout,
|
|
2149
|
+
delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
|
|
2150
|
+
]);
|
|
2151
|
+
}
|
|
2152
|
+
let historyFinished = status === "done" || (status === "notes-timeout" || status === "returned") && notesFinished;
|
|
2153
|
+
const terminal = status === "done" || status === "failed" ? status : "pending";
|
|
2154
|
+
if (!historyFinished && merkletreeScanStillRunning({ sawProgress: sawUtxoEvent, notesFinished, terminal })) {
|
|
2155
|
+
let walked = "stalled";
|
|
2156
|
+
while (walked === "stalled") {
|
|
2157
|
+
const idle = Date.now() - lastUtxoAt;
|
|
2158
|
+
const slice = Math.max(250, RAILGUN_MERKLETREE_STALL_MS - idle);
|
|
2159
|
+
const step = await Promise.race([scanSettled, delay2(slice).then(() => "tick")]);
|
|
2160
|
+
if (step === "done" || step === "failed") {
|
|
2161
|
+
walked = step;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
if (Date.now() - lastUtxoAt >= RAILGUN_MERKLETREE_STALL_MS) break;
|
|
2165
|
+
}
|
|
2166
|
+
if (walked !== "done") {
|
|
2167
|
+
const rows = snapshotRows(args.walletId, args.chainId);
|
|
2168
|
+
const balances2 = rows.length > 0 ? rows : previous;
|
|
2169
|
+
const record2 = railgunScanRecord("failed", balances2.length, false);
|
|
2170
|
+
return publishScan(args.chainId, record2.scanStatus, balances2, false, record2.notes);
|
|
2171
|
+
}
|
|
2172
|
+
notesFinished = true;
|
|
2173
|
+
historyFinished = true;
|
|
2174
|
+
}
|
|
2175
|
+
if (!historyFinished) {
|
|
2176
|
+
armScanWait();
|
|
2177
|
+
const retry = wallet.refreshBalances(chain, [args.walletId]).then(
|
|
2178
|
+
() => "returned",
|
|
2179
|
+
() => "returned"
|
|
2180
|
+
);
|
|
2181
|
+
const retried = await Promise.race([
|
|
2182
|
+
scanSettled,
|
|
2183
|
+
retry,
|
|
2184
|
+
delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
|
|
2185
|
+
]);
|
|
2186
|
+
if (retried !== "done" && !notesFinished) {
|
|
2187
|
+
const rows = snapshotRows(args.walletId, args.chainId);
|
|
2188
|
+
const balances2 = rows.length > 0 ? rows : previous;
|
|
2189
|
+
const record2 = railgunScanRecord("failed", balances2.length, false);
|
|
2190
|
+
return publishScan(args.chainId, record2.scanStatus, balances2, false, record2.notes);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
const decrypted = snapshotRows(args.walletId, args.chainId);
|
|
2194
|
+
publishLiveBalances();
|
|
2195
|
+
const refreshed = await Promise.race([
|
|
2196
|
+
refresh.then(() => true),
|
|
2197
|
+
delay2(RAILGUN_TXID_WAIT_AFTER_NOTES_MS).then(() => false)
|
|
2198
|
+
]);
|
|
2199
|
+
if (!refreshed) {
|
|
2200
|
+
await Promise.race([refresh.then(() => void 0), delay2(RAILGUN_CATCH_UP_MS).then(() => void 0)]);
|
|
2201
|
+
}
|
|
2202
|
+
sawUtxoEvent = false;
|
|
2203
|
+
armScanWait();
|
|
2204
|
+
const followUpStarted = Date.now();
|
|
2205
|
+
const followUp = wallet.refreshBalances(chain, [args.walletId]).then(
|
|
2206
|
+
() => "returned",
|
|
2207
|
+
() => "returned"
|
|
2208
|
+
);
|
|
2209
|
+
let followStatus = await Promise.race([
|
|
2210
|
+
scanSettled,
|
|
2211
|
+
followUp,
|
|
2212
|
+
delay2(RAILGUN_RESCAN_QUIET_MS).then(() => "quiet"),
|
|
2213
|
+
delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
|
|
2214
|
+
]);
|
|
2215
|
+
if ((followStatus === "quiet" || followStatus === "returned") && sawUtxoEvent) {
|
|
2216
|
+
const remaining = Math.max(0, RAILGUN_CATCH_UP_MS - (Date.now() - followUpStarted));
|
|
2217
|
+
followStatus = await Promise.race([
|
|
2218
|
+
scanSettled,
|
|
2219
|
+
delay2(remaining).then(() => "catch-up-timeout")
|
|
2220
|
+
]);
|
|
2221
|
+
}
|
|
2222
|
+
const caughtUp = followStatus === "done" || followStatus === "quiet" && !sawUtxoEvent;
|
|
2223
|
+
const balances = balancesToKeepAfterNoteScan(decrypted, snapshotRows(args.walletId, args.chainId), previous);
|
|
2224
|
+
const record = railgunScanRecord("done", balances.length, caughtUp);
|
|
2225
|
+
return publishScan(args.chainId, record.scanStatus, balances, refreshed && caughtUp, record.notes || void 0);
|
|
2226
|
+
} finally {
|
|
2227
|
+
utxoListeners.delete(listener);
|
|
2228
|
+
balanceListeners.delete(onBalance);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
var SPEND_DECRYPT_WAIT_MS = 3 * 60 * 1e3;
|
|
2232
|
+
var RAILGUN_SPEND_SCAN_STOPPED = "The spending wallet did not finish scanning private notes.";
|
|
2233
|
+
function spendingScanReadyAfterHistory(progress, scanStatus) {
|
|
2234
|
+
return progress >= 1 || utxoScanTerminalStatus(scanStatus) === "done";
|
|
2235
|
+
}
|
|
2236
|
+
function spendingNoteScanGaveUp(args) {
|
|
2237
|
+
if (args.walletReady || args.notesFinished) return false;
|
|
2238
|
+
if (args.sawProgress) return args.msSinceProgress >= RAILGUN_MERKLETREE_STALL_MS;
|
|
2239
|
+
if (args.msWaitingForDecrypt == null) return false;
|
|
2240
|
+
return args.msWaitingForDecrypt >= SPEND_DECRYPT_WAIT_MS;
|
|
2241
|
+
}
|
|
2242
|
+
async function scanSpendingWalletNotes(args) {
|
|
2243
|
+
const rpcUrl = args.rpcUrl.trim();
|
|
2244
|
+
if (!rpcUrl) throw new Error("rpcUrl is required.");
|
|
1163
2245
|
await ensureRailgunProvider(args.chainId, rpcUrl);
|
|
2246
|
+
const wallet = await walletModule2();
|
|
1164
2247
|
const chain = { type: ChainType.EVM, id: args.chainId };
|
|
1165
|
-
await wallet
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
2248
|
+
await hookUtxoScans(wallet);
|
|
2249
|
+
let sawUtxoEvent = false;
|
|
2250
|
+
let lastUtxoAt = Date.now();
|
|
2251
|
+
let notesFinished = false;
|
|
2252
|
+
let walletReady = false;
|
|
2253
|
+
const listener = (event) => {
|
|
2254
|
+
if (event.chain.id !== args.chainId) return;
|
|
2255
|
+
sawUtxoEvent = true;
|
|
2256
|
+
lastUtxoAt = Date.now();
|
|
2257
|
+
if (spendingScanReadyAfterHistory(event.progress, event.scanStatus)) {
|
|
2258
|
+
notesFinished = true;
|
|
2259
|
+
walletReady = true;
|
|
2260
|
+
}
|
|
2261
|
+
args.onProgress?.(event.progress);
|
|
2262
|
+
};
|
|
2263
|
+
utxoListeners.add(listener);
|
|
2264
|
+
const listenForDecrypt = () => {
|
|
2265
|
+
wallet.awaitWalletScan(args.walletId, chain).then(
|
|
2266
|
+
() => {
|
|
2267
|
+
walletReady = true;
|
|
2268
|
+
},
|
|
2269
|
+
() => {
|
|
2270
|
+
listenForDecrypt();
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
};
|
|
2274
|
+
listenForDecrypt();
|
|
2275
|
+
let refreshSettled = false;
|
|
2276
|
+
let refreshError = null;
|
|
2277
|
+
void wallet.refreshBalances(chain, [args.walletId]).then(
|
|
2278
|
+
() => {
|
|
2279
|
+
refreshSettled = true;
|
|
2280
|
+
},
|
|
2281
|
+
(err) => {
|
|
2282
|
+
refreshError = err;
|
|
2283
|
+
refreshSettled = true;
|
|
2284
|
+
}
|
|
2285
|
+
);
|
|
2286
|
+
let decryptWaitStarted = null;
|
|
2287
|
+
try {
|
|
2288
|
+
while (!walletReady) {
|
|
2289
|
+
if (refreshError) throw refreshError;
|
|
2290
|
+
const waitingForDecrypt = notesFinished || !sawUtxoEvent && refreshSettled ? decryptWaitStarted ??= Date.now() : null;
|
|
2291
|
+
if (waitingForDecrypt == null) decryptWaitStarted = null;
|
|
2292
|
+
if (spendingNoteScanGaveUp({
|
|
2293
|
+
walletReady,
|
|
2294
|
+
sawProgress: sawUtxoEvent,
|
|
2295
|
+
notesFinished,
|
|
2296
|
+
msSinceProgress: Date.now() - lastUtxoAt,
|
|
2297
|
+
msWaitingForDecrypt: waitingForDecrypt == null ? null : Date.now() - waitingForDecrypt
|
|
2298
|
+
})) {
|
|
2299
|
+
throw new Error(RAILGUN_SPEND_SCAN_STOPPED);
|
|
2300
|
+
}
|
|
2301
|
+
await delay2(250);
|
|
2302
|
+
}
|
|
2303
|
+
} finally {
|
|
2304
|
+
utxoListeners.delete(listener);
|
|
1169
2305
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
2306
|
+
}
|
|
2307
|
+
async function scanRailgunPrivateBalances(args) {
|
|
2308
|
+
const rpcUrl = args.rpcUrl.trim();
|
|
2309
|
+
if (!rpcUrl) throw new Error("rpcUrl is required.");
|
|
2310
|
+
const key = `${args.chainId}:${args.walletId}`;
|
|
2311
|
+
const existing = scansInFlight.get(key);
|
|
2312
|
+
if (existing) return existing;
|
|
2313
|
+
const run = scanRailgunPrivateBalancesOnce({ ...args, rpcUrl }).finally(() => {
|
|
2314
|
+
if (scansInFlight.get(key) === run) scansInFlight.delete(key);
|
|
1178
2315
|
});
|
|
1179
|
-
|
|
2316
|
+
scansInFlight.set(key, run);
|
|
2317
|
+
return run;
|
|
1180
2318
|
}
|
|
1181
2319
|
|
|
1182
2320
|
// src/protocols/evm/railgun/index.ts
|
|
@@ -1213,7 +2351,16 @@ var railgunProtocolModule = {
|
|
|
1213
2351
|
params: {
|
|
1214
2352
|
tokenAddress: { type: "string", required: true, description: "Shielded ERC-20" },
|
|
1215
2353
|
amountWei: { type: "string", required: true, description: "Amount in wei" },
|
|
1216
|
-
recipientAddress: {
|
|
2354
|
+
recipientAddress: {
|
|
2355
|
+
type: "string",
|
|
2356
|
+
required: false,
|
|
2357
|
+
description: "Recipient 0zk address. Omit when toContactName is set."
|
|
2358
|
+
},
|
|
2359
|
+
toContactName: {
|
|
2360
|
+
type: "string",
|
|
2361
|
+
required: false,
|
|
2362
|
+
description: "Address-book contact name. Uses that contact\u2019s saved 0zk address, not their Ethereum address."
|
|
2363
|
+
}
|
|
1217
2364
|
}
|
|
1218
2365
|
},
|
|
1219
2366
|
{
|
|
@@ -1232,6 +2379,6 @@ var railgunProtocolModule = {
|
|
|
1232
2379
|
};
|
|
1233
2380
|
registerProtocolModule(railgunProtocolModule);
|
|
1234
2381
|
|
|
1235
|
-
export { RAILGUN_CTM_DISABLED_MESSAGE, RAILGUN_CTM_ENABLED, RAILGUN_POPULAR_ASSET_LIMIT, RAILGUN_PROTOCOL_ID, RAILGUN_WALLET_LOCKED_MESSAGE, assertRailgunCtmAllowed, buildEvmMultisignBodyRailgunShield, buildEvmMultisignBodyWethUnwrap, clearRailgunNetworkCache, clearRailgunPopularAssetCache, clearRailgunPrivateBalanceSnapshots, commitmentsFromShieldIndex, createRailgunBrowserWallet, discardRailgunWallet, getRailgunWalletSession, loadRailgunNetworks, loadRailgunPopularAssets, loadRailgunSupportedChainIds, openRailgunViewOnlyWallet, publishRailgunPrivateBalanceSnapshot, railgunAddressIsValid, railgunFetchPopularAssets, railgunFetchPrivateBalances, railgunFetchPrivateBalancesSummary, railgunNetworkByChainId, railgunPrivateTransfer, railgunProtocolModule, railgunShareableViewingKey, railgunUnshield, rankPopularShieldTokens, readRailgunPrivateBalanceSnapshot, requireRailgunWalletSession, scanRailgunPrivateBalances, selectRailgunNetworks, setRailgunAllowanceReaderForTests, setRailgunPopulateShieldForTests, setRailgunShieldLogReaderForTests, setRailgunWalletSession, shieldIndexEndpoint, shieldPrivateKeyFromSignature, unlockRailgunBrowserWallet, utxoScanTerminalStatus };
|
|
2382
|
+
export { BROADCASTER_FEE_MIN_REMAINING_MS, BROADCASTER_PROOF_WAIT_MS, RAILGUN_CATCH_UP_MS, RAILGUN_CTM_DISABLED_MESSAGE, RAILGUN_CTM_ENABLED, RAILGUN_MERKLETREE_STALL_MS, RAILGUN_POPULAR_ASSET_LIMIT, RAILGUN_PROTOCOL_ID, RAILGUN_SCAN_BEHIND_NOTE, RAILGUN_SCAN_STOPPED_NOTE, RAILGUN_SPEND_SCAN_STOPPED, RAILGUN_TXID_WAIT_AFTER_NOTES_MS, RAILGUN_WALLET_LOCKED_MESSAGE, assertRailgunCtmAllowed, balancesToKeepAfterNoteScan, broadcasterAskedToRefreshProofStatus, broadcasterFeeStillValid, broadcasterRefusedForGas, buildEvmMultisignBodyRailgunShield, buildEvmMultisignBodyWethUnwrap, clearRailgunFeeCache, clearRailgunNetworkCache, clearRailgunPopularAssetCache, clearRailgunPrivateBalanceSnapshots, clearRailgunWalletBalances, commitmentsFromShieldIndex, createRailgunBrowserWallet, discardRailgunWallet, getRailgunWalletSession, listRailgunBroadcasters, loadRailgunNetworks, loadRailgunPopularAssets, loadRailgunSupportedChainIds, merkletreeScanStillRunning, openRailgunViewOnlyWallet, publishRailgunPrivateBalanceSnapshot, quoteRailgunBroadcasterFee, quoteRailgunBroadcasterFeeRate, quoteRailgunFee, railgunAddressIsValid, railgunDetailedError, railgunFetchPopularAssets, railgunFetchPrivateBalances, railgunFetchPrivateBalancesSummary, railgunNetworkByChainId, railgunPrivateTransfer, railgunProtocolFee, railgunProtocolModule, railgunScanRecord, railgunShareableViewingKey, railgunUnshield, railgunWalletBalanceSnapshot, rankPopularShieldTokens, readRailgunPrivateBalanceSnapshot, recordRailgunWalletBalance, requireRailgunWalletSession, scanRailgunPrivateBalances, scanSpendingWalletNotes, selectRailgunNetworks, setRailgunAllowanceReaderForTests, setRailgunPopulateShieldForTests, setRailgunShieldLogReaderForTests, setRailgunWalletSession, shieldIndexEndpoint, shieldPrivateKeyFromSignature, spendingNoteScanGaveUp, spendingScanReadyAfterHistory, unlockRailgunBrowserWallet, utxoScanTerminalStatus };
|
|
1236
2383
|
//# sourceMappingURL=index.js.map
|
|
1237
2384
|
//# sourceMappingURL=index.js.map
|