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