@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.
@@ -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
- var artifactStore = {
573
- async get(path) {
574
- return artifacts.get(path) ?? null;
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
- lastEngineError = errorChain(err);
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 memdown = (await import('memdown')).default;
700
+ const root = await resolveRailgunDataRoot();
701
+ const files = root ? diskArtifactStore(root) : memoryArtifactStore();
604
702
  const store = new wallet.ArtifactStore(
605
- (path) => artifactStore.get(path),
606
- (dir, path, item) => artifactStore.store(dir, path, item),
607
- (path) => artifactStore.exists(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", new memdown(), true, store, false, false, POI_NODE_URLS, [], false);
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: BigInt(Math.round(Number(chainDetail.gasPrice ?? 1) * 1e9))
740
+ gasPrice: weiFromGwei(chainDetail.gasPrice)
637
741
  };
638
742
  }
639
743
  return {
640
744
  evmGasType: sharedModels.EVMGasType.Type2,
641
745
  gasEstimate,
642
- maxFeePerGas: BigInt(Math.round(Number(chainDetail.baseFee ?? 1) * 1e9)),
643
- maxPriorityFeePerGas: BigInt(Math.round(Number(chainDetail.priorityFee ?? 1) * 1e9))
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,15 +820,41 @@ async function shieldSignatureMessage() {
662
820
  const wallet = await import('@railgun-community/wallet');
663
821
  return wallet.getShieldPrivateKeySignatureMessage();
664
822
  }
665
- function shieldPrivateKeyFromSignature(signature) {
666
- const hex = signature.startsWith("0x") ? signature : `0x${signature}`;
667
- if (!/^0x[0-9a-fA-F]+$/.test(hex) || (hex.length - 2) % 2 !== 0) {
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) {
668
829
  throw new Error("Shield-key signature must be hex.");
669
830
  }
670
- if ((hex.length - 2) / 2 !== 65) {
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}`;
671
840
  throw new Error("Shield-key signature must be the 65-byte personal_sign of RAILGUN_SHIELD.");
672
841
  }
673
- return viem.keccak256(hex);
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.");
674
858
  }
675
859
  function personalSignBody(args, message) {
676
860
  const executor = viem.getAddress(args.executorAddress);
@@ -752,7 +936,6 @@ async function buildEvmMultisignBodyRailgunShield(args) {
752
936
  if (!signature) {
753
937
  return personalSignBody(args, await shieldSignatureMessage());
754
938
  }
755
- const shieldPrivateKey = shieldPrivateKeyFromSignature(signature);
756
939
  const network = await railgunNetworkByChainId(args.chainId);
757
940
  if (!network) throw new Error("Railgun is not deployed on this chain.");
758
941
  const tokenAddress = viem.getAddress(args.tokenAddress);
@@ -762,6 +945,7 @@ async function buildEvmMultisignBodyRailgunShield(args) {
762
945
  const amount = BigInt(args.amountWei);
763
946
  if (amount <= 0n) throw new Error("Amount must be greater than zero.");
764
947
  const executor = viem.getAddress(args.executorAddress);
948
+ const shieldPrivateKey = await shieldPrivateKeyFromSignature(signature, executor);
765
949
  const spender = viem.getAddress(network.proxyContract);
766
950
  const rpcUrl = args.rpcUrl.trim();
767
951
  if (!rpcUrl) throw new Error("rpcUrl is required.");
@@ -839,6 +1023,117 @@ async function buildEvmMultisignBodyRailgunShield(args) {
839
1023
  })
840
1024
  });
841
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 currentBroadcasterChoice(rows, feesId, railgunAddress, refused) {
1118
+ const id = feesId.trim();
1119
+ if (id && !refused.has(id)) {
1120
+ const exact = rows.find((row2) => row2.feesId === id);
1121
+ if (exact) return { row: exact, refreshed: false };
1122
+ }
1123
+ const address = railgunAddress.trim();
1124
+ if (!address) return void 0;
1125
+ const same = rows.filter((row2) => row2.railgunAddress === address && !refused.has(row2.feesId));
1126
+ const row = same.find((item) => item.coversGas) ?? same[0];
1127
+ return row ? { row, refreshed: true } : void 0;
1128
+ }
1129
+ function nextCoveringBroadcaster(rows, refused) {
1130
+ return rows.find((row) => row.coversGas && !refused.has(row.feesId));
1131
+ }
1132
+ function isBroadcasterGasRefusal(message) {
1133
+ return /gas cost is too high/i.test(message);
1134
+ }
1135
+
1136
+ // src/protocols/evm/railgun/private-actions.ts
842
1137
  function chainFor(chainId) {
843
1138
  return { type: sharedModels.ChainType.EVM, id: chainId };
844
1139
  }
@@ -852,6 +1147,53 @@ function parsedAmount(amountWei) {
852
1147
  if (amount <= 0n) throw new Error("Amount must be greater than zero.");
853
1148
  return amount;
854
1149
  }
1150
+ function railgunDetailedError(err) {
1151
+ const seen = /* @__PURE__ */ new Set();
1152
+ let current = err;
1153
+ let fallback = "Railgun spend proof failed.";
1154
+ while (current != null && !seen.has(current)) {
1155
+ seen.add(current);
1156
+ if (!(current instanceof Error)) break;
1157
+ const message = current.message.trim();
1158
+ if (message && /amount required|balance:/i.test(message)) return message;
1159
+ if (message) fallback = message;
1160
+ current = current.cause;
1161
+ }
1162
+ return fallback;
1163
+ }
1164
+ function broadcasterAskedToRefreshProofStatus(err) {
1165
+ const message = railgunDetailedError(err);
1166
+ 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);
1167
+ }
1168
+ function broadcasterRefusedForGas(err) {
1169
+ return isBroadcasterGasRefusal(railgunDetailedError(err));
1170
+ }
1171
+ function chainForNetwork(networkName) {
1172
+ return sharedModels.NETWORK_CONFIG[networkName].chain;
1173
+ }
1174
+ async function refreshSpendingProofStatus(networkName, walletId) {
1175
+ const wallet = await import('@railgun-community/wallet');
1176
+ const chain = chainForNetwork(networkName);
1177
+ wallet.resumePPOIBatching(chain);
1178
+ await wallet.walletForID(walletId).refreshPOIsForTXIDVersion(chain, sharedModels.TXIDVersion.V2_PoseidonMerkle, true);
1179
+ }
1180
+ async function assertTokenSpendable(networkName, walletId, tokenAddress, amount) {
1181
+ const wallet = await import('@railgun-community/wallet');
1182
+ const balance = await wallet.walletForID(walletId).getBalanceERC20(
1183
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1184
+ chainForNetwork(networkName),
1185
+ tokenAddress,
1186
+ ["Spendable"]
1187
+ );
1188
+ if ((balance ?? 0n) < amount) {
1189
+ throw new Error(
1190
+ "These notes are still waiting for proof-of-innocence. The private transfer can start once that status is spendable."
1191
+ );
1192
+ }
1193
+ }
1194
+ function delay(ms) {
1195
+ return new Promise((resolve) => setTimeout(resolve, ms));
1196
+ }
855
1197
  async function broadcasterClient() {
856
1198
  try {
857
1199
  return await import('@railgun-community/waku-broadcaster-client-web');
@@ -859,40 +1201,343 @@ async function broadcasterClient() {
859
1201
  throw new Error("Railgun broadcaster client is not available.");
860
1202
  }
861
1203
  }
862
- async function selectedBroadcaster(chain, tokenAddress) {
1204
+ var BROADCASTER_CACHE_MS = 6e4;
1205
+ var BROADCASTER_QUOTE_WAIT_MS = 2e4;
1206
+ var BROADCASTER_PROOF_WAIT_MS = 3 * 60 * 1e3;
1207
+ var BROADCASTER_FEE_MIN_REMAINING_MS = 4e4;
1208
+ var BROADCASTER_PROOF_ATTEMPTS = 3;
1209
+ var POI_STATUS_RETRY_MS = 2e4;
1210
+ var POI_ROOT_WAIT_MS = 3 * 60 * 1e3;
1211
+ var BROADCASTER_POI_INCOMPLETE = "Proof-of-innocence validation did not complete";
1212
+ function broadcasterFeeStillValid(expiration, now = Date.now()) {
1213
+ return Number.isFinite(expiration) && expiration >= now + BROADCASTER_FEE_MIN_REMAINING_MS;
1214
+ }
1215
+ var BROADCASTER_LIST_CACHE_MS = 15e3;
1216
+ var MAX_GAS_BROADCASTERS = 3;
1217
+ var NATIVE_COINGECKO_ID = {
1218
+ 1: "ethereum",
1219
+ 10: "ethereum",
1220
+ 56: "binancecoin",
1221
+ 100: "xdai",
1222
+ 137: "polygon-ecosystem-token",
1223
+ 250: "fantom",
1224
+ 324: "ethereum",
1225
+ 1101: "ethereum",
1226
+ 5e3: "mantle",
1227
+ 8453: "ethereum",
1228
+ 34443: "ethereum",
1229
+ 42161: "ethereum",
1230
+ 43114: "avalanche-2",
1231
+ 59144: "ethereum",
1232
+ 81457: "ethereum",
1233
+ 534352: "ethereum",
1234
+ 167e3: "ethereum"
1235
+ };
1236
+ var broadcasterListCache = /* @__PURE__ */ new Map();
1237
+ var tokenDecimalsCache = /* @__PURE__ */ new Map();
1238
+ var tokenUsdCache = /* @__PURE__ */ new Map();
1239
+ async function ensureBroadcasterWaku(chain) {
863
1240
  const { WakuBroadcasterClient } = await broadcasterClient();
864
1241
  if (!WakuBroadcasterClient.isStarted()) {
865
1242
  await WakuBroadcasterClient.start(chain, { useDNSDiscovery: true }, () => void 0);
866
1243
  } else {
867
1244
  await WakuBroadcasterClient.setChain(chain);
868
1245
  }
869
- const deadline = Date.now() + 2e4;
870
- let selected;
1246
+ }
1247
+ async function findTokenBroadcasters(chain, tokenAddress) {
1248
+ const node = await nodeRequire();
1249
+ if (!node) throw new Error("Railgun broadcasters are listed from the node.");
1250
+ const entry = node.require.resolve("@railgun-community/waku-broadcaster-client-web");
1251
+ const loaded = node.require(node.path.join(node.path.dirname(entry), "search/best-broadcaster.js"));
1252
+ return loaded.BroadcasterSearch.findBroadcastersForToken(chain, tokenAddress, false) ?? [];
1253
+ }
1254
+ async function erc20Decimals(rpcUrl, tokenAddress) {
1255
+ const key = tokenAddress.toLowerCase();
1256
+ const cached2 = tokenDecimalsCache.get(key);
1257
+ if (cached2 != null) return cached2;
1258
+ const client = viem.createPublicClient({ transport: defiEvmTransport(rpcUrl) });
1259
+ const decimals = await client.readContract({
1260
+ address: viem.getAddress(tokenAddress),
1261
+ abi: viem.parseAbi(["function decimals() view returns (uint8)"]),
1262
+ functionName: "decimals"
1263
+ });
1264
+ const value = Number(decimals);
1265
+ if (!Number.isInteger(value) || value < 0 || value > 36) throw new Error("Token decimals are not available.");
1266
+ tokenDecimalsCache.set(key, value);
1267
+ return value;
1268
+ }
1269
+ async function readUsd(url) {
1270
+ try {
1271
+ const response = await fetch(url, { signal: AbortSignal.timeout(8e3) });
1272
+ if (!response.ok) return null;
1273
+ const body = await response.json();
1274
+ for (const row of Object.values(body)) {
1275
+ if (typeof row?.usd === "number" && row.usd > 0) return row.usd;
1276
+ }
1277
+ } catch {
1278
+ return null;
1279
+ }
1280
+ return null;
1281
+ }
1282
+ async function broadcasterUsdPrices(chainId, tokenAddress, wrappedAddress) {
1283
+ const key = `${chainId}:${tokenAddress.toLowerCase()}`;
1284
+ const cached2 = tokenUsdCache.get(key);
1285
+ if (cached2 && Date.now() - cached2.at < 6e4) return cached2;
1286
+ const nativeId = NATIVE_COINGECKO_ID[chainId];
1287
+ const nativeUsd = nativeId ? await readUsd(`https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(nativeId)}&vs_currencies=usd`) : null;
1288
+ const wrapped = wrappedAddress.toLowerCase() === tokenAddress.toLowerCase();
1289
+ let tokenUsd = wrapped ? nativeUsd : null;
1290
+ if (!wrapped) {
1291
+ const platform = coingeckoPlatformForChainId(chainId);
1292
+ tokenUsd = platform ? await readUsd(
1293
+ `https://api.coingecko.com/api/v3/simple/token_price/${encodeURIComponent(platform)}?contract_addresses=${encodeURIComponent(tokenAddress)}&vs_currencies=usd`
1294
+ ) : null;
1295
+ }
1296
+ const prices = { tokenUsd, nativeUsd };
1297
+ tokenUsdCache.set(key, { at: Date.now(), ...prices });
1298
+ return prices;
1299
+ }
1300
+ async function loadRailgunBroadcasters(args) {
1301
+ const tokenAddress = viem.getAddress(args.tokenAddress);
1302
+ const cacheKey = `${args.kind}:${args.chainId}:${tokenAddress.toLowerCase()}`;
1303
+ const cached2 = broadcasterListCache.get(cacheKey);
1304
+ if (cached2 && Date.now() - cached2.at < BROADCASTER_LIST_CACHE_MS) return cached2.rows;
1305
+ const chain = chainFor(args.chainId);
1306
+ await ensureBroadcasterWaku(chain);
1307
+ const deadline = Date.now() + (args.waitMs ?? BROADCASTER_QUOTE_WAIT_MS);
1308
+ let found = [];
1309
+ while (Date.now() < deadline) {
1310
+ found = (await findTokenBroadcasters(chain, tokenAddress)).filter((row) => broadcasterFeeStillValid(row.tokenFee.expiration));
1311
+ if (found.length > 0) break;
1312
+ await delay(1e3);
1313
+ }
1314
+ if (found.length === 0) throw new Error("No Railgun broadcaster is available for this token.");
1315
+ const network = await railgunNetworkByChainId(args.chainId);
1316
+ const [decimals, prices, priced] = await Promise.all([
1317
+ erc20Decimals(args.rpcUrl, tokenAddress),
1318
+ broadcasterUsdPrices(args.chainId, tokenAddress, network?.wrappedAddress ?? ""),
1319
+ chainDetailWithLiveGas(args.rpcUrl, args.chainDetail)
1320
+ ]);
1321
+ const networkName = network?.networkName ?? "";
1322
+ const gasEstimate = args.kind === "unshield" ? RAILGUN_UNSHIELD_GAS_ESTIMATE : RAILGUN_TRANSFER_GAS_ESTIMATE;
1323
+ const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
1324
+ const byFeesId = /* @__PURE__ */ new Map();
1325
+ for (const selected of found) {
1326
+ const feesId = selected.tokenFee.feesID;
1327
+ if (!feesId) continue;
1328
+ const feePerUnitGas = BigInt(selected.tokenFee.feePerUnitGas);
1329
+ const ratioMilli = prices.tokenUsd != null && prices.nativeUsd != null ? feeRatioMilli(feePerUnitGas, decimals, prices.tokenUsd, prices.nativeUsd) : null;
1330
+ const row = {
1331
+ feesId,
1332
+ railgunAddress: selected.railgunAddress,
1333
+ feeWei: broadcasterTokenFee(feePerUnitGas, gasDetails).toString(),
1334
+ ratio: ratioMilli == null ? null : formatFeeRatioMilli(ratioMilli),
1335
+ coversGas: coversGasCost(ratioMilli),
1336
+ reliability: Number(selected.tokenFee.reliability) || 0,
1337
+ expiration: selected.tokenFee.expiration,
1338
+ tokenDecimals: decimals,
1339
+ feePerUnitGas,
1340
+ ratioMilli,
1341
+ selected
1342
+ };
1343
+ const previous = byFeesId.get(feesId);
1344
+ if (!previous || row.reliability > previous.reliability) byFeesId.set(feesId, row);
1345
+ }
1346
+ const rows = orderBroadcasterChoices([...byFeesId.values()]);
1347
+ broadcasterListCache.set(cacheKey, { at: Date.now(), rows });
1348
+ return rows;
1349
+ }
1350
+ async function listRailgunBroadcasters(args) {
1351
+ const kind = args.kind.trim();
1352
+ if (kind !== "transfer" && kind !== "unshield") throw new Error("kind must be transfer or unshield.");
1353
+ const rows = await loadRailgunBroadcasters({ ...args, kind });
1354
+ return rows.map((row) => ({
1355
+ feesId: row.feesId,
1356
+ railgunAddress: row.railgunAddress,
1357
+ feeWei: row.feeWei,
1358
+ ratio: row.ratio,
1359
+ coversGas: row.coversGas,
1360
+ reliability: row.reliability,
1361
+ expiration: row.expiration,
1362
+ tokenDecimals: row.tokenDecimals
1363
+ }));
1364
+ }
1365
+ function broadcasterNote(row, mode) {
1366
+ const ratioLabel = row.ratio ? `${row.ratio}:1` : "the selected fee";
1367
+ if (mode === "next") return `Trying the next broadcaster at ${ratioLabel}`;
1368
+ if (mode === "refreshed") return `The chosen fee expired. Broadcaster ${ratioLabel}`;
1369
+ return `Broadcaster ${ratioLabel}`;
1370
+ }
1371
+ function poiListsPresent(pois) {
1372
+ return Object.values(pois).some((leaves) => Object.keys(leaves).length > 0);
1373
+ }
1374
+ function poiRoots(poi) {
1375
+ if (!poi || typeof poi !== "object" || !("poiMerkleroots" in poi)) return [];
1376
+ const roots = poi.poiMerkleroots;
1377
+ return Array.isArray(roots) ? roots.filter((root) => typeof root === "string") : [];
1378
+ }
1379
+ async function nodeRequire() {
1380
+ if (typeof window !== "undefined") return null;
1381
+ if (typeof process === "undefined" || !process.versions?.node) return null;
1382
+ const load = new Function("id", "return import(id)");
1383
+ const nodeModule = await load("node:module");
1384
+ const nodePath = await load("node:path");
1385
+ 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 };
1386
+ }
1387
+ async function poiRootValidator() {
1388
+ const node = await nodeRequire();
1389
+ if (!node) throw new Error("Proof-of-innocence checks need the node Railgun engine.");
1390
+ const walletEntry = node.require.resolve("@railgun-community/wallet");
1391
+ const engineEntry = node.require.resolve("@railgun-community/engine", {
1392
+ paths: [node.path.dirname(walletEntry)]
1393
+ });
1394
+ const loaded = node.require(engineEntry);
1395
+ if (!loaded.POI) throw new Error("The Railgun engine did not load its proof-of-innocence checker.");
1396
+ return loaded.POI;
1397
+ }
1398
+ async function poiMerklerootsConfirmed(chain, pois) {
1399
+ const poi = await poiRootValidator();
1400
+ for (const [listKey, leaves] of Object.entries(pois)) {
1401
+ for (const entry of Object.values(leaves)) {
1402
+ const roots = poiRoots(entry);
1403
+ if (roots.length === 0) return false;
1404
+ const confirmed = await poi.validatePOIMerkleroots(sharedModels.TXIDVersion.V2_PoseidonMerkle, chain, listKey, roots);
1405
+ if (!confirmed) return false;
1406
+ }
1407
+ }
1408
+ return true;
1409
+ }
1410
+ async function waitForConfirmedPoiRoots(chain, proved, onProofStatusRefresh) {
1411
+ const pois = proved.populated.preTransactionPOIsPerTxidLeafPerList;
1412
+ if (!poiListsPresent(pois)) {
1413
+ throw new Error("The proof did not include a proof of innocence.");
1414
+ }
1415
+ const deadline = Date.now() + POI_ROOT_WAIT_MS;
871
1416
  while (Date.now() < deadline) {
872
- selected = await WakuBroadcasterClient.findBestBroadcaster(chain, tokenAddress, false);
873
- if (selected) return selected;
874
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1417
+ try {
1418
+ if (await poiMerklerootsConfirmed(chain, pois)) return;
1419
+ } catch (err) {
1420
+ if (!broadcasterAskedToRefreshProofStatus(err)) throw err;
1421
+ }
1422
+ onProofStatusRefresh?.();
1423
+ await delay(POI_STATUS_RETRY_MS);
1424
+ }
1425
+ throw new Error("The proof-of-innocence list has not confirmed these notes. Wait a few minutes and try the private transfer again.");
1426
+ }
1427
+ async function submitFreshProof(args) {
1428
+ if (args.refreshProofStatus) {
1429
+ args.onProofStatusRefresh?.();
1430
+ await args.refreshProofStatus().catch(() => void 0);
1431
+ }
1432
+ await args.assertSpendable?.();
1433
+ const refused = /* @__PURE__ */ new Set();
1434
+ const chosenFeesId = args.broadcasterFeesId?.trim() ?? "";
1435
+ const chosenAddress = args.broadcasterAddress?.trim() ?? "";
1436
+ let feesId = chosenFeesId;
1437
+ let lastError = new Error("No Railgun broadcaster is available for this token.");
1438
+ let builds = 0;
1439
+ while (builds < MAX_GAS_BROADCASTERS) {
1440
+ args.onBroadcasterSearch?.();
1441
+ const rows = await loadRailgunBroadcasters({
1442
+ chainId: args.chainId,
1443
+ rpcUrl: args.rpcUrl,
1444
+ chainDetail: args.chainDetail,
1445
+ tokenAddress: args.tokenAddress,
1446
+ kind: args.kind,
1447
+ waitMs: BROADCASTER_PROOF_WAIT_MS
1448
+ });
1449
+ const picked = refused.size === 0 ? currentBroadcasterChoice(rows, feesId, chosenAddress, refused) : void 0;
1450
+ const row = picked?.row ?? nextCoveringBroadcaster(rows, refused) ?? (refused.size === 0 ? defaultBroadcasterChoice(rows) : void 0);
1451
+ if (!row || !broadcasterFeeStillValid(row.expiration)) {
1452
+ if (!row) break;
1453
+ refused.add(row.feesId);
1454
+ feesId = "";
1455
+ if (refused.size >= rows.length) break;
1456
+ continue;
1457
+ }
1458
+ builds += 1;
1459
+ feesId = row.feesId;
1460
+ const replaced = Boolean(chosenFeesId) && row.feesId !== chosenFeesId;
1461
+ const mode = refused.size > 0 ? "next" : replaced ? "refreshed" : "selected";
1462
+ args.onBroadcasterNote?.(broadcasterNote(row, mode));
1463
+ const broadcaster = row.selected;
1464
+ const proved = await args.build(broadcaster);
1465
+ if (!broadcasterFeeStillValid(broadcaster.tokenFee.expiration)) {
1466
+ broadcasterListCache.clear();
1467
+ refused.add(feesId);
1468
+ feesId = "";
1469
+ continue;
1470
+ }
1471
+ await waitForConfirmedPoiRoots(args.chain, proved, args.onProofStatusRefresh);
1472
+ let switchBroadcaster = false;
1473
+ for (let poiTry = 1; poiTry <= BROADCASTER_PROOF_ATTEMPTS && !switchBroadcaster; poiTry += 1) {
1474
+ try {
1475
+ return {
1476
+ txHash: await submitProved({
1477
+ chain: args.chain,
1478
+ populated: proved.populated,
1479
+ broadcaster,
1480
+ overallBatchMinGasPrice: proved.overallBatchMinGasPrice
1481
+ })
1482
+ };
1483
+ } catch (err) {
1484
+ lastError = err;
1485
+ if (broadcasterRefusedForGas(err)) {
1486
+ refused.add(feesId);
1487
+ feesId = "";
1488
+ switchBroadcaster = true;
1489
+ break;
1490
+ }
1491
+ if (poiTry === BROADCASTER_PROOF_ATTEMPTS || !broadcasterAskedToRefreshProofStatus(err)) throw err;
1492
+ args.onProofStatusRefresh?.();
1493
+ await delay(POI_STATUS_RETRY_MS);
1494
+ await args.refreshProofStatus?.().catch(() => void 0);
1495
+ if (!broadcasterFeeStillValid(broadcaster.tokenFee.expiration)) {
1496
+ broadcasterListCache.clear();
1497
+ refused.add(feesId);
1498
+ feesId = "";
1499
+ switchBroadcaster = true;
1500
+ break;
1501
+ }
1502
+ await waitForConfirmedPoiRoots(args.chain, proved, args.onProofStatusRefresh);
1503
+ }
1504
+ }
875
1505
  }
876
- throw new Error("No Railgun broadcaster is available for this token.");
1506
+ if (lastError instanceof Error) throw lastError;
1507
+ throw new Error("The broadcaster fee expired while the proof was building. Try the private transfer again.");
877
1508
  }
878
1509
  async function submitProved(args) {
879
1510
  const to = args.populated.transaction.to;
880
1511
  const data = args.populated.transaction.data;
881
1512
  if (!to || !data) throw new Error("Railgun proof did not produce a transaction.");
882
1513
  const { BroadcasterTransaction } = await broadcasterClient();
883
- const tx = await BroadcasterTransaction.create(
884
- sharedModels.TXIDVersion.V2_PoseidonMerkle,
885
- to,
886
- data,
887
- args.broadcaster.railgunAddress,
888
- args.broadcaster.tokenFee.feesID,
889
- args.chain,
890
- args.populated.nullifiers ?? [],
891
- args.overallBatchMinGasPrice,
892
- false,
893
- args.populated.preTransactionPOIsPerTxidLeafPerList
894
- );
895
- return tx.send();
1514
+ const detail = await broadcasterDetailConfig();
1515
+ const previousDetail = detail?.IS_DEV;
1516
+ if (detail) detail.IS_DEV = true;
1517
+ try {
1518
+ const tx = await BroadcasterTransaction.create(
1519
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1520
+ to,
1521
+ data,
1522
+ args.broadcaster.railgunAddress,
1523
+ args.broadcaster.tokenFee.feesID,
1524
+ args.chain,
1525
+ args.populated.nullifiers ?? [],
1526
+ args.overallBatchMinGasPrice,
1527
+ false,
1528
+ args.populated.preTransactionPOIsPerTxidLeafPerList
1529
+ );
1530
+ return tx.send();
1531
+ } finally {
1532
+ if (detail && previousDetail !== void 0) detail.IS_DEV = previousDetail;
1533
+ }
1534
+ }
1535
+ async function broadcasterDetailConfig() {
1536
+ const node = await nodeRequire();
1537
+ if (!node) return null;
1538
+ const entry = node.require.resolve("@railgun-community/waku-broadcaster-client-web");
1539
+ const loaded = node.require(node.path.join(node.path.dirname(entry), "models/broadcaster-config.js"));
1540
+ return loaded.BroadcasterConfig;
896
1541
  }
897
1542
  function feeRecipient(wallet, broadcaster, gasDetails) {
898
1543
  const fee = wallet.calculateBroadcasterFeeERC20Amount(
@@ -908,71 +1553,176 @@ function feeRecipient(wallet, broadcaster, gasDetails) {
908
1553
  recipientAddress: broadcaster.railgunAddress
909
1554
  };
910
1555
  }
911
- async function railgunPrivateTransfer(args) {
912
- assertRailgunCtmAllowed(args.chainId, args.tokenAddress);
913
- await assertDeployed(args.chainId);
1556
+ var broadcasterFeeCache = /* @__PURE__ */ new Map();
1557
+ async function quoteRailgunBroadcasterFee(args) {
914
1558
  const tokenAddress = viem.getAddress(args.tokenAddress);
1559
+ const cacheKey = `${args.kind}:${args.chainId}:${tokenAddress.toLowerCase()}`;
1560
+ const cached2 = broadcasterFeeCache.get(cacheKey);
1561
+ if (cached2 && Date.now() - cached2.at < BROADCASTER_CACHE_MS) {
1562
+ return { feeWei: cached2.feeWei, tokenAddress: cached2.tokenAddress };
1563
+ }
915
1564
  const amount = parsedAmount(args.amountWei);
916
- const recipientAddress = args.recipientAddress.trim();
917
- if (!recipientAddress.startsWith("0zk")) throw new Error("Private transfer recipient must be a 0zk address.");
918
- const session2 = requireRailgunWalletSession();
919
- const rpcUrl = args.rpcUrl?.trim() ?? "";
920
- if (!rpcUrl || !args.chainDetail) throw new Error("rpcUrl and chain details are required.");
921
- const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
1565
+ const networkName = await ensureRailgunProvider(args.chainId, args.rpcUrl);
922
1566
  const wallet = await import('@railgun-community/wallet');
923
- const chain = chainFor(args.chainId);
924
- const broadcaster = await selectedBroadcaster(chain, tokenAddress);
925
- const recipients = [{ tokenAddress, amount, recipientAddress }];
1567
+ const priced = await chainDetailWithLiveGas(args.rpcUrl, args.chainDetail);
1568
+ const listed = await loadRailgunBroadcasters({
1569
+ chainId: args.chainId,
1570
+ rpcUrl: args.rpcUrl,
1571
+ chainDetail: args.chainDetail,
1572
+ tokenAddress,
1573
+ kind: args.kind
1574
+ });
1575
+ const chosen = defaultBroadcasterChoice(listed);
1576
+ if (!chosen) throw new Error("No Railgun broadcaster is available for this token.");
1577
+ const broadcaster = chosen.selected;
926
1578
  const feeToken = {
927
1579
  tokenAddress: broadcaster.tokenAddress,
928
1580
  feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
929
1581
  };
930
- const original = railgunGasDetails(args.chainDetail, 0n);
931
- const { gasEstimate } = await wallet.gasEstimateForUnprovenTransfer(
1582
+ const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
1583
+ const recipientAddress = args.kind === "unshield" ? viem.getAddress(args.recipientAddress || "0x0000000000000000000000000000000000000001") : args.recipientAddress.trim();
1584
+ if (args.kind === "transfer" && !recipientAddress.startsWith("0zk")) {
1585
+ throw new Error("A 0zk address is required to estimate the private-transfer fee.");
1586
+ }
1587
+ const recipients = [{ tokenAddress, amount, recipientAddress }];
1588
+ const estimated = args.kind === "unshield" ? await wallet.gasEstimateForUnprovenUnshield(
932
1589
  sharedModels.TXIDVersion.V2_PoseidonMerkle,
933
1590
  networkName,
934
- session2.walletId,
935
- session2.encryptionKey,
936
- args.memoText ?? null,
1591
+ args.walletId,
1592
+ args.encryptionKey,
937
1593
  recipients,
938
1594
  [],
939
1595
  original,
940
1596
  feeToken,
941
1597
  false
942
- );
943
- const gasDetails = railgunGasDetails(args.chainDetail, gasEstimate);
944
- const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
945
- const overallBatchMinGasPrice = sharedModels.calculateGasPrice(gasDetails);
946
- await wallet.generateTransferProof(
947
- sharedModels.TXIDVersion.V2_PoseidonMerkle,
948
- networkName,
949
- session2.walletId,
950
- session2.encryptionKey,
951
- false,
952
- args.memoText ?? null,
953
- recipients,
954
- [],
955
- broadcasterFee,
956
- false,
957
- overallBatchMinGasPrice,
958
- (progress) => args.onProofProgress?.(progress)
959
- );
960
- const populated = await wallet.populateProvedTransfer(
1598
+ ) : await wallet.gasEstimateForUnprovenTransfer(
961
1599
  sharedModels.TXIDVersion.V2_PoseidonMerkle,
962
1600
  networkName,
963
- session2.walletId,
964
- false,
1601
+ args.walletId,
1602
+ args.encryptionKey,
965
1603
  args.memoText ?? null,
966
1604
  recipients,
967
1605
  [],
968
- broadcasterFee,
969
- false,
970
- overallBatchMinGasPrice,
971
- gasDetails
1606
+ original,
1607
+ feeToken,
1608
+ false
972
1609
  );
973
- return { txHash: await submitProved({ chain, populated, broadcaster, overallBatchMinGasPrice }) };
1610
+ const fee = feeRecipient(wallet, broadcaster, railgunBroadcasterGasDetails(networkName, priced, estimated.gasEstimate));
1611
+ const quoted = { feeWei: fee.amount.toString(), tokenAddress: viem.getAddress(fee.tokenAddress) };
1612
+ broadcasterFeeCache.set(cacheKey, { at: Date.now(), ...quoted });
1613
+ return quoted;
1614
+ }
1615
+ async function quoteRailgunBroadcasterFeeRate(args) {
1616
+ const tokenAddress = viem.getAddress(args.tokenAddress);
1617
+ const network = await railgunNetworkByChainId(args.chainId);
1618
+ if (!network) throw new Error("Railgun is not deployed on this chain.");
1619
+ const listed = await loadRailgunBroadcasters({
1620
+ chainId: args.chainId,
1621
+ rpcUrl: args.rpcUrl,
1622
+ chainDetail: args.chainDetail,
1623
+ tokenAddress,
1624
+ kind: args.kind
1625
+ });
1626
+ const chosen = defaultBroadcasterChoice(listed);
1627
+ if (!chosen) throw new Error("No Railgun broadcaster is available for this token.");
1628
+ return { feeWei: chosen.feeWei, tokenAddress };
1629
+ }
1630
+ async function railgunPrivateTransfer(args) {
1631
+ try {
1632
+ return await provePrivateTransfer(args);
1633
+ } catch (err) {
1634
+ throw new Error(railgunDetailedError(err));
1635
+ }
1636
+ }
1637
+ async function provePrivateTransfer(args) {
1638
+ assertRailgunCtmAllowed(args.chainId, args.tokenAddress);
1639
+ await assertDeployed(args.chainId);
1640
+ const tokenAddress = viem.getAddress(args.tokenAddress);
1641
+ const amount = parsedAmount(args.amountWei);
1642
+ const recipientAddress = args.recipientAddress.trim();
1643
+ if (!recipientAddress.startsWith("0zk")) throw new Error("Private transfer recipient must be a 0zk address.");
1644
+ const session2 = requireRailgunWalletSession();
1645
+ const rpcUrl = args.rpcUrl?.trim() ?? "";
1646
+ if (!rpcUrl || !args.chainDetail) throw new Error("rpcUrl and chain details are required.");
1647
+ const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
1648
+ const wallet = await import('@railgun-community/wallet');
1649
+ const chain = chainFor(args.chainId);
1650
+ const priced = await chainDetailWithLiveGas(rpcUrl, args.chainDetail);
1651
+ const recipients = [{ tokenAddress, amount, recipientAddress }];
1652
+ return submitFreshProof({
1653
+ chainId: args.chainId,
1654
+ rpcUrl,
1655
+ chainDetail: args.chainDetail,
1656
+ kind: "transfer",
1657
+ chain,
1658
+ tokenAddress,
1659
+ broadcasterAddress: args.broadcasterAddress,
1660
+ broadcasterFeesId: args.broadcasterFeesId,
1661
+ onBroadcasterSearch: args.onBroadcasterSearch,
1662
+ onBroadcasterNote: args.onBroadcasterNote,
1663
+ onProofStatusRefresh: args.onProofStatusRefresh,
1664
+ refreshProofStatus: () => refreshSpendingProofStatus(networkName, session2.walletId),
1665
+ assertSpendable: () => assertTokenSpendable(networkName, session2.walletId, tokenAddress, amount),
1666
+ build: async (broadcaster) => {
1667
+ const feeToken = {
1668
+ tokenAddress: broadcaster.tokenAddress,
1669
+ feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
1670
+ };
1671
+ const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
1672
+ const { gasEstimate } = await wallet.gasEstimateForUnprovenTransfer(
1673
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1674
+ networkName,
1675
+ session2.walletId,
1676
+ session2.encryptionKey,
1677
+ args.memoText ?? null,
1678
+ recipients,
1679
+ [],
1680
+ original,
1681
+ feeToken,
1682
+ false
1683
+ );
1684
+ const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
1685
+ const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
1686
+ const overallBatchMinGasPrice = sharedModels.calculateGasPrice(gasDetails);
1687
+ await wallet.generateTransferProof(
1688
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1689
+ networkName,
1690
+ session2.walletId,
1691
+ session2.encryptionKey,
1692
+ false,
1693
+ args.memoText ?? null,
1694
+ recipients,
1695
+ [],
1696
+ broadcasterFee,
1697
+ false,
1698
+ overallBatchMinGasPrice,
1699
+ (progress) => args.onProofProgress?.(progress)
1700
+ );
1701
+ const populated = await wallet.populateProvedTransfer(
1702
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1703
+ networkName,
1704
+ session2.walletId,
1705
+ false,
1706
+ args.memoText ?? null,
1707
+ recipients,
1708
+ [],
1709
+ broadcasterFee,
1710
+ false,
1711
+ overallBatchMinGasPrice,
1712
+ gasDetails
1713
+ );
1714
+ return { populated, overallBatchMinGasPrice };
1715
+ }
1716
+ });
974
1717
  }
975
1718
  async function railgunUnshield(args) {
1719
+ try {
1720
+ return await proveUnshield(args);
1721
+ } catch (err) {
1722
+ throw new Error(railgunDetailedError(err));
1723
+ }
1724
+ }
1725
+ async function proveUnshield(args) {
976
1726
  assertRailgunCtmAllowed(args.chainId, args.tokenAddress);
977
1727
  await assertDeployed(args.chainId);
978
1728
  const tokenAddress = viem.getAddress(args.tokenAddress);
@@ -984,51 +1734,149 @@ async function railgunUnshield(args) {
984
1734
  const networkName = await ensureRailgunProvider(args.chainId, rpcUrl);
985
1735
  const wallet = await import('@railgun-community/wallet');
986
1736
  const chain = chainFor(args.chainId);
987
- const broadcaster = await selectedBroadcaster(chain, tokenAddress);
1737
+ const priced = await chainDetailWithLiveGas(rpcUrl, args.chainDetail);
988
1738
  const recipients = [{ tokenAddress, amount, recipientAddress }];
989
- const feeToken = {
990
- tokenAddress: broadcaster.tokenAddress,
991
- feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
1739
+ return submitFreshProof({
1740
+ chainId: args.chainId,
1741
+ rpcUrl,
1742
+ chainDetail: args.chainDetail,
1743
+ kind: "unshield",
1744
+ chain,
1745
+ tokenAddress,
1746
+ broadcasterAddress: args.broadcasterAddress,
1747
+ broadcasterFeesId: args.broadcasterFeesId,
1748
+ onBroadcasterSearch: args.onBroadcasterSearch,
1749
+ onBroadcasterNote: args.onBroadcasterNote,
1750
+ onProofStatusRefresh: args.onProofStatusRefresh,
1751
+ refreshProofStatus: () => refreshSpendingProofStatus(networkName, session2.walletId),
1752
+ assertSpendable: () => assertTokenSpendable(networkName, session2.walletId, tokenAddress, amount),
1753
+ build: async (broadcaster) => {
1754
+ const feeToken = {
1755
+ tokenAddress: broadcaster.tokenAddress,
1756
+ feePerUnitGas: BigInt(broadcaster.tokenFee.feePerUnitGas)
1757
+ };
1758
+ const original = railgunBroadcasterGasDetails(networkName, priced, 0n);
1759
+ const { gasEstimate } = await wallet.gasEstimateForUnprovenUnshield(
1760
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1761
+ networkName,
1762
+ session2.walletId,
1763
+ session2.encryptionKey,
1764
+ recipients,
1765
+ [],
1766
+ original,
1767
+ feeToken,
1768
+ false
1769
+ );
1770
+ const gasDetails = railgunBroadcasterGasDetails(networkName, priced, gasEstimate);
1771
+ const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
1772
+ const overallBatchMinGasPrice = sharedModels.calculateGasPrice(gasDetails);
1773
+ await wallet.generateUnshieldProof(
1774
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1775
+ networkName,
1776
+ session2.walletId,
1777
+ session2.encryptionKey,
1778
+ recipients,
1779
+ [],
1780
+ broadcasterFee,
1781
+ false,
1782
+ overallBatchMinGasPrice,
1783
+ (progress) => args.onProofProgress?.(progress)
1784
+ );
1785
+ const populated = await wallet.populateProvedUnshield(
1786
+ sharedModels.TXIDVersion.V2_PoseidonMerkle,
1787
+ networkName,
1788
+ session2.walletId,
1789
+ recipients,
1790
+ [],
1791
+ broadcasterFee,
1792
+ false,
1793
+ overallBatchMinGasPrice,
1794
+ gasDetails
1795
+ );
1796
+ return { populated, overallBatchMinGasPrice };
1797
+ }
1798
+ });
1799
+ }
1800
+ var feeAbi = viem.parseAbi([
1801
+ "function shieldFee() view returns (uint256)",
1802
+ "function unshieldFee() view returns (uint256)"
1803
+ ]);
1804
+ var BASIS_POINTS = 10000n;
1805
+ var FEE_CACHE_MS = 6e4;
1806
+ var basisCache = /* @__PURE__ */ new Map();
1807
+ function railgunProtocolFee(amount, basisPoints) {
1808
+ const value = amount > 0n ? amount : 0n;
1809
+ const bps = basisPoints > 0n ? basisPoints : 0n;
1810
+ const fee = value * bps / BASIS_POINTS;
1811
+ return { fee, received: value - fee };
1812
+ }
1813
+ async function protocolBasisPoints(chainId, rpcUrl) {
1814
+ const cached2 = basisCache.get(chainId);
1815
+ if (cached2 && Date.now() - cached2.at < FEE_CACHE_MS) return cached2;
1816
+ const network = await railgunNetworkByChainId(chainId);
1817
+ if (!network) throw new Error("Railgun is not deployed on this chain.");
1818
+ const client = viem.createPublicClient({ transport: defiEvmTransport(rpcUrl) });
1819
+ const address = viem.getAddress(network.proxyContract);
1820
+ const [shield, unshield] = await Promise.all([
1821
+ client.readContract({ address, abi: feeAbi, functionName: "shieldFee" }),
1822
+ client.readContract({ address, abi: feeAbi, functionName: "unshieldFee" })
1823
+ ]);
1824
+ const next = { shield, unshield, at: Date.now() };
1825
+ basisCache.set(chainId, next);
1826
+ return next;
1827
+ }
1828
+ function clearRailgunFeeCache() {
1829
+ basisCache.clear();
1830
+ }
1831
+ async function quoteRailgunFee(args) {
1832
+ const rpcUrl = args.rpcUrl.trim();
1833
+ if (!rpcUrl) throw new Error("rpcUrl is required.");
1834
+ const amount = (() => {
1835
+ try {
1836
+ return BigInt(args.amountWei.trim() || "0");
1837
+ } catch {
1838
+ throw new Error("Amount must be an integer in wei.");
1839
+ }
1840
+ })();
1841
+ if (amount < 0n) throw new Error("Amount must be greater than zero.");
1842
+ const rates = await protocolBasisPoints(args.chainId, rpcUrl);
1843
+ const basisPoints = args.kind === "shield" ? rates.shield : args.kind === "unshield" ? rates.unshield : 0n;
1844
+ const protocol = railgunProtocolFee(amount, basisPoints);
1845
+ let broadcasterFee = 0n;
1846
+ let broadcasterTokenAddress = "";
1847
+ let broadcasterError;
1848
+ if (args.kind !== "shield") {
1849
+ if (!args.chainDetail) {
1850
+ broadcasterError = "Chain gas details are required to estimate the broadcaster fee.";
1851
+ } else {
1852
+ try {
1853
+ const quoted = await quoteRailgunBroadcasterFeeRate({
1854
+ kind: args.kind,
1855
+ chainId: args.chainId,
1856
+ rpcUrl,
1857
+ chainDetail: args.chainDetail,
1858
+ tokenAddress: args.tokenAddress
1859
+ });
1860
+ broadcasterFee = BigInt(quoted.feeWei);
1861
+ broadcasterTokenAddress = quoted.tokenAddress;
1862
+ } catch (err) {
1863
+ broadcasterError = err instanceof Error ? err.message : "Broadcaster fee is not available.";
1864
+ }
1865
+ }
1866
+ }
1867
+ const totalDebit = args.kind === "shield" ? amount : amount + broadcasterFee;
1868
+ const quote = {
1869
+ kind: args.kind,
1870
+ amountWei: amount.toString(),
1871
+ protocolFeeBasisPoints: basisPoints.toString(),
1872
+ protocolFeeWei: protocol.fee.toString(),
1873
+ receivedWei: (args.kind === "transfer" ? amount : protocol.received).toString(),
1874
+ broadcasterFeeWei: broadcasterFee.toString(),
1875
+ broadcasterTokenAddress,
1876
+ totalDebitWei: totalDebit.toString()
992
1877
  };
993
- const original = railgunGasDetails(args.chainDetail, 0n);
994
- const { gasEstimate } = await wallet.gasEstimateForUnprovenUnshield(
995
- sharedModels.TXIDVersion.V2_PoseidonMerkle,
996
- networkName,
997
- session2.walletId,
998
- session2.encryptionKey,
999
- recipients,
1000
- [],
1001
- original,
1002
- feeToken,
1003
- false
1004
- );
1005
- const gasDetails = railgunGasDetails(args.chainDetail, gasEstimate);
1006
- const broadcasterFee = feeRecipient(wallet, broadcaster, gasDetails);
1007
- const overallBatchMinGasPrice = sharedModels.calculateGasPrice(gasDetails);
1008
- await wallet.generateUnshieldProof(
1009
- sharedModels.TXIDVersion.V2_PoseidonMerkle,
1010
- networkName,
1011
- session2.walletId,
1012
- session2.encryptionKey,
1013
- recipients,
1014
- [],
1015
- broadcasterFee,
1016
- false,
1017
- overallBatchMinGasPrice,
1018
- (progress) => args.onProofProgress?.(progress)
1019
- );
1020
- const populated = await wallet.populateProvedUnshield(
1021
- sharedModels.TXIDVersion.V2_PoseidonMerkle,
1022
- networkName,
1023
- session2.walletId,
1024
- recipients,
1025
- [],
1026
- broadcasterFee,
1027
- false,
1028
- overallBatchMinGasPrice,
1029
- gasDetails
1030
- );
1031
- return { txHash: await submitProved({ chain, populated, broadcaster, overallBatchMinGasPrice }) };
1878
+ if (broadcasterError) quote.broadcasterError = broadcasterError;
1879
+ return quote;
1032
1880
  }
1033
1881
  async function walletModule2() {
1034
1882
  return await import('@railgun-community/wallet');
@@ -1067,6 +1915,16 @@ async function railgunShareableViewingKey(walletId) {
1067
1915
  if (!viewingKey) throw new Error("Railgun viewing key is not available.");
1068
1916
  return viewingKey;
1069
1917
  }
1918
+ async function rememberKeyGenWallet(keyGenId, walletId, railgunAddress) {
1919
+ const root = await resolveRailgunDataRoot();
1920
+ if (!root) return;
1921
+ const load = new Function("id", "return import(id)");
1922
+ const fs = await load("node:fs/promises");
1923
+ const file = railgunKeyGenWalletPath(root, keyGenId);
1924
+ await fs.mkdir(file.slice(0, file.lastIndexOf("/")), { recursive: true });
1925
+ await fs.writeFile(file, `${JSON.stringify({ walletId, railgunAddress })}
1926
+ `, { mode: 384 });
1927
+ }
1070
1928
  async function openRailgunViewOnlyWallet(args) {
1071
1929
  await ensureRailgunEngine();
1072
1930
  const network = await railgunNetworkByChainId(args.chainId);
@@ -1076,6 +1934,7 @@ async function openRailgunViewOnlyWallet(args) {
1076
1934
  args.shareableViewingKey,
1077
1935
  network ? { [network.networkName]: network.deploymentBlock } : void 0
1078
1936
  );
1937
+ if (args.keyGenId) await rememberKeyGenWallet(args.keyGenId, created.id, created.railgunAddress);
1079
1938
  return { walletId: created.id, railgunAddress: created.railgunAddress };
1080
1939
  }
1081
1940
  async function discardRailgunWallet(walletId) {
@@ -1088,11 +1947,15 @@ function railgunAddressIsValid(address) {
1088
1947
  return walletModule2().then((wallet) => wallet.validateRailgunAddress(address));
1089
1948
  }
1090
1949
  var buckets = /* @__PURE__ */ new Map();
1091
- function rowMap(chainId) {
1092
- let rows = buckets.get(chainId);
1950
+ function bucketKey(walletId, chainId) {
1951
+ return `${walletId}:${chainId}`;
1952
+ }
1953
+ function rowMap(walletId, chainId) {
1954
+ const key = bucketKey(walletId, chainId);
1955
+ let rows = buckets.get(key);
1093
1956
  if (!rows) {
1094
1957
  rows = /* @__PURE__ */ new Map();
1095
- buckets.set(chainId, rows);
1958
+ buckets.set(key, rows);
1096
1959
  }
1097
1960
  return rows;
1098
1961
  }
@@ -1105,9 +1968,18 @@ function cellFor(rows, tokenAddress) {
1105
1968
  }
1106
1969
  return cell;
1107
1970
  }
1971
+ function recordRailgunWalletBalance(event) {
1972
+ applyBalanceEvent(event);
1973
+ }
1974
+ function railgunWalletBalanceSnapshot(walletId, chainId) {
1975
+ return snapshotRows(walletId, chainId);
1976
+ }
1977
+ function clearRailgunWalletBalances() {
1978
+ buckets.clear();
1979
+ }
1108
1980
  function applyBalanceEvent(event) {
1109
- if (event.txidVersion !== sharedModels.TXIDVersion.V2_PoseidonMerkle) return;
1110
- const rows = rowMap(event.chain.id);
1981
+ if (!event.railgunWalletID || event.txidVersion !== sharedModels.TXIDVersion.V2_PoseidonMerkle) return;
1982
+ const rows = rowMap(event.railgunWalletID, event.chain.id);
1111
1983
  const spendable = event.balanceBucket === sharedModels.RailgunWalletBalanceBucket.Spendable;
1112
1984
  const pending = event.balanceBucket === sharedModels.RailgunWalletBalanceBucket.ShieldPending || event.balanceBucket === sharedModels.RailgunWalletBalanceBucket.ProofSubmitted || event.balanceBucket === sharedModels.RailgunWalletBalanceBucket.MissingExternalPOI || event.balanceBucket === sharedModels.RailgunWalletBalanceBucket.MissingInternalPOI;
1113
1985
  if (!spendable && !pending) return;
@@ -1123,8 +1995,8 @@ function applyBalanceEvent(event) {
1123
1995
  if (pending) cell.pending.set(event.balanceBucket, amount.amount);
1124
1996
  }
1125
1997
  }
1126
- function snapshotRows(chainId) {
1127
- const rows = buckets.get(chainId);
1998
+ function snapshotRows(walletId, chainId) {
1999
+ const rows = buckets.get(bucketKey(walletId, chainId));
1128
2000
  if (!rows) return [];
1129
2001
  return [...rows.entries()].filter(([, cell]) => cell.spendable > 0n || [...cell.pending.values()].some((amount) => amount > 0n)).map(([tokenAddress, cell]) => ({
1130
2002
  tokenAddress,
@@ -1137,49 +2009,315 @@ function utxoScanTerminalStatus(scanStatus) {
1137
2009
  if (scanStatus === "Incomplete") return "failed";
1138
2010
  return "pending";
1139
2011
  }
1140
- async function scanRailgunPrivateBalances(args) {
1141
- const rpcUrl = args.rpcUrl.trim();
1142
- if (!rpcUrl) throw new Error("rpcUrl is required.");
1143
- buckets.set(args.chainId, /* @__PURE__ */ new Map());
2012
+ var RAILGUN_RESCAN_QUIET_MS = 4e3;
2013
+ var RAILGUN_TXID_WAIT_AFTER_NOTES_MS = 45e3;
2014
+ var RAILGUN_CATCH_UP_MS = 12e4;
2015
+ var RAILGUN_MERKLETREE_STALL_MS = 5 * 60 * 1e3;
2016
+ 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.";
2017
+ var RAILGUN_SCAN_STOPPED_NOTE = "The note scan stopped before it reached the latest block. The shielded balance is unchanged until a scan finishes.";
2018
+ function merkletreeScanStillRunning(args) {
2019
+ return args.sawProgress && !args.notesFinished && args.terminal === "pending";
2020
+ }
2021
+ function railgunScanRecord(merkletree, balanceCount, caughtUp) {
2022
+ if (merkletree !== "done") {
2023
+ return { scanStatus: "error", notes: RAILGUN_SCAN_STOPPED_NOTE };
2024
+ }
2025
+ if (!caughtUp) {
2026
+ return {
2027
+ scanStatus: balanceCount === 0 ? "empty" : "ready",
2028
+ notes: RAILGUN_SCAN_BEHIND_NOTE
2029
+ };
2030
+ }
2031
+ return {
2032
+ scanStatus: balanceCount === 0 ? "empty" : "ready",
2033
+ notes: balanceCount === 0 ? "No private balance on this chain yet." : ""
2034
+ };
2035
+ }
2036
+ function balancesToKeepAfterNoteScan(decrypted, afterRefresh, previous) {
2037
+ if (afterRefresh.length > 0) return afterRefresh;
2038
+ if (decrypted.length > 0) return decrypted;
2039
+ return previous;
2040
+ }
2041
+ var utxoListeners = /* @__PURE__ */ new Set();
2042
+ var utxoHooked = false;
2043
+ var balanceListeners = /* @__PURE__ */ new Set();
2044
+ var balanceHooked = false;
2045
+ var scansInFlight = /* @__PURE__ */ new Map();
2046
+ function delay2(ms) {
2047
+ return new Promise((resolve) => setTimeout(resolve, ms));
2048
+ }
2049
+ async function hookUtxoScans(wallet) {
2050
+ if (utxoHooked) return;
2051
+ utxoHooked = true;
2052
+ wallet.setOnUTXOMerkletreeScanCallback((event) => {
2053
+ for (const listener of utxoListeners) listener(event);
2054
+ });
2055
+ }
2056
+ async function hookBalanceUpdates(wallet) {
2057
+ if (balanceHooked) return;
2058
+ balanceHooked = true;
2059
+ wallet.setOnBalanceUpdateCallback((event) => {
2060
+ for (const listener of balanceListeners) listener(event);
2061
+ });
2062
+ }
2063
+ function publishScan(chainId, scanStatus, balances, settled, notes) {
2064
+ const text = notes ?? (balances.length === 0 ? "No private balance on this chain yet." : void 0);
2065
+ publishRailgunPrivateBalanceSnapshot({
2066
+ chainId,
2067
+ scanStatus,
2068
+ balances,
2069
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2070
+ notes: text
2071
+ });
2072
+ return { scanStatus, balances, settled, notes: text ?? "" };
2073
+ }
2074
+ async function scanRailgunPrivateBalancesOnce(args) {
2075
+ const previous = snapshotRows(args.walletId, args.chainId);
1144
2076
  publishRailgunPrivateBalanceSnapshot({
1145
2077
  chainId: args.chainId,
1146
2078
  scanStatus: "scanning",
1147
- balances: [],
2079
+ balances: previous,
1148
2080
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1149
2081
  });
1150
2082
  const wallet = await walletModule2();
1151
2083
  let resolveScan = () => {
1152
2084
  };
1153
- const scanSettled = new Promise((resolve) => {
2085
+ let scanSettled = new Promise((resolve) => {
1154
2086
  resolveScan = resolve;
1155
2087
  });
1156
- wallet.setOnBalanceUpdateCallback((event) => {
1157
- if (event.railgunWalletID !== args.walletId) return;
1158
- applyBalanceEvent(event);
2088
+ const armScanWait = () => {
2089
+ scanSettled = new Promise((resolve) => {
2090
+ resolveScan = resolve;
2091
+ });
2092
+ };
2093
+ let sawUtxoEvent = false;
2094
+ let lastUtxoAt = 0;
2095
+ let replacedBuckets = false;
2096
+ let resolveNotesEnd = () => {
2097
+ };
2098
+ const notesEnded = new Promise((resolve) => {
2099
+ resolveNotesEnd = resolve;
1159
2100
  });
1160
- wallet.setOnUTXOMerkletreeScanCallback((event) => {
2101
+ const publishLiveBalances = () => {
2102
+ const rows = snapshotRows(args.walletId, args.chainId);
2103
+ if (rows.length > 0) args.onBalances?.(rows);
2104
+ };
2105
+ let notesFinished = false;
2106
+ const listener = (event) => {
1161
2107
  if (event.chain.id !== args.chainId) return;
2108
+ if (!replacedBuckets) {
2109
+ buckets.set(bucketKey(args.walletId, args.chainId), /* @__PURE__ */ new Map());
2110
+ replacedBuckets = true;
2111
+ }
2112
+ sawUtxoEvent = true;
2113
+ lastUtxoAt = Date.now();
1162
2114
  args.onProgress?.(event.progress);
1163
2115
  const terminal = utxoScanTerminalStatus(event.scanStatus);
2116
+ if (event.progress >= 1 || terminal === "done") {
2117
+ notesFinished = true;
2118
+ publishLiveBalances();
2119
+ resolveNotesEnd();
2120
+ }
1164
2121
  if (terminal !== "pending") resolveScan(terminal);
1165
- });
2122
+ };
2123
+ await hookUtxoScans(wallet);
2124
+ await hookBalanceUpdates(wallet);
2125
+ utxoListeners.add(listener);
2126
+ const onBalance = (event) => {
2127
+ if (event.railgunWalletID !== args.walletId || event.chain.id !== args.chainId) return;
2128
+ recordRailgunWalletBalance(event);
2129
+ publishLiveBalances();
2130
+ };
2131
+ balanceListeners.add(onBalance);
2132
+ try {
2133
+ await ensureRailgunProvider(args.chainId, args.rpcUrl);
2134
+ const chain = { type: sharedModels.ChainType.EVM, id: args.chainId };
2135
+ const refresh = wallet.refreshBalances(chain, [args.walletId]).then(
2136
+ () => "returned",
2137
+ () => "returned"
2138
+ );
2139
+ const quiet = delay2(RAILGUN_RESCAN_QUIET_MS).then(() => "quiet");
2140
+ const notesTimeout = notesEnded.then(() => delay2(RAILGUN_TXID_WAIT_AFTER_NOTES_MS)).then(() => "notes-timeout");
2141
+ let status = await Promise.race([
2142
+ scanSettled,
2143
+ refresh,
2144
+ quiet,
2145
+ notesTimeout
2146
+ ]);
2147
+ if (status === "quiet") {
2148
+ status = await Promise.race([
2149
+ scanSettled,
2150
+ refresh,
2151
+ notesTimeout,
2152
+ delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
2153
+ ]);
2154
+ }
2155
+ let historyFinished = status === "done" || (status === "notes-timeout" || status === "returned") && notesFinished;
2156
+ const terminal = status === "done" || status === "failed" ? status : "pending";
2157
+ if (!historyFinished && merkletreeScanStillRunning({ sawProgress: sawUtxoEvent, notesFinished, terminal })) {
2158
+ let walked = "stalled";
2159
+ while (walked === "stalled") {
2160
+ const idle = Date.now() - lastUtxoAt;
2161
+ const slice = Math.max(250, RAILGUN_MERKLETREE_STALL_MS - idle);
2162
+ const step = await Promise.race([scanSettled, delay2(slice).then(() => "tick")]);
2163
+ if (step === "done" || step === "failed") {
2164
+ walked = step;
2165
+ break;
2166
+ }
2167
+ if (Date.now() - lastUtxoAt >= RAILGUN_MERKLETREE_STALL_MS) break;
2168
+ }
2169
+ if (walked !== "done") {
2170
+ const rows = snapshotRows(args.walletId, args.chainId);
2171
+ const balances2 = rows.length > 0 ? rows : previous;
2172
+ const record2 = railgunScanRecord("failed", balances2.length, false);
2173
+ return publishScan(args.chainId, record2.scanStatus, balances2, false, record2.notes);
2174
+ }
2175
+ notesFinished = true;
2176
+ historyFinished = true;
2177
+ }
2178
+ if (!historyFinished) {
2179
+ armScanWait();
2180
+ const retry = wallet.refreshBalances(chain, [args.walletId]).then(
2181
+ () => "returned",
2182
+ () => "returned"
2183
+ );
2184
+ const retried = await Promise.race([
2185
+ scanSettled,
2186
+ retry,
2187
+ delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
2188
+ ]);
2189
+ if (retried !== "done" && !notesFinished) {
2190
+ const rows = snapshotRows(args.walletId, args.chainId);
2191
+ const balances2 = rows.length > 0 ? rows : previous;
2192
+ const record2 = railgunScanRecord("failed", balances2.length, false);
2193
+ return publishScan(args.chainId, record2.scanStatus, balances2, false, record2.notes);
2194
+ }
2195
+ }
2196
+ const decrypted = snapshotRows(args.walletId, args.chainId);
2197
+ publishLiveBalances();
2198
+ const refreshed = await Promise.race([
2199
+ refresh.then(() => true),
2200
+ delay2(RAILGUN_TXID_WAIT_AFTER_NOTES_MS).then(() => false)
2201
+ ]);
2202
+ if (!refreshed) {
2203
+ await Promise.race([refresh.then(() => void 0), delay2(RAILGUN_CATCH_UP_MS).then(() => void 0)]);
2204
+ }
2205
+ sawUtxoEvent = false;
2206
+ armScanWait();
2207
+ const followUpStarted = Date.now();
2208
+ const followUp = wallet.refreshBalances(chain, [args.walletId]).then(
2209
+ () => "returned",
2210
+ () => "returned"
2211
+ );
2212
+ let followStatus = await Promise.race([
2213
+ scanSettled,
2214
+ followUp,
2215
+ delay2(RAILGUN_RESCAN_QUIET_MS).then(() => "quiet"),
2216
+ delay2(RAILGUN_CATCH_UP_MS).then(() => "catch-up-timeout")
2217
+ ]);
2218
+ if ((followStatus === "quiet" || followStatus === "returned") && sawUtxoEvent) {
2219
+ const remaining = Math.max(0, RAILGUN_CATCH_UP_MS - (Date.now() - followUpStarted));
2220
+ followStatus = await Promise.race([
2221
+ scanSettled,
2222
+ delay2(remaining).then(() => "catch-up-timeout")
2223
+ ]);
2224
+ }
2225
+ const caughtUp = followStatus === "done" || followStatus === "quiet" && !sawUtxoEvent;
2226
+ const balances = balancesToKeepAfterNoteScan(decrypted, snapshotRows(args.walletId, args.chainId), previous);
2227
+ const record = railgunScanRecord("done", balances.length, caughtUp);
2228
+ return publishScan(args.chainId, record.scanStatus, balances, refreshed && caughtUp, record.notes || void 0);
2229
+ } finally {
2230
+ utxoListeners.delete(listener);
2231
+ balanceListeners.delete(onBalance);
2232
+ }
2233
+ }
2234
+ var SPEND_DECRYPT_WAIT_MS = 3 * 60 * 1e3;
2235
+ var RAILGUN_SPEND_SCAN_STOPPED = "The spending wallet did not finish scanning private notes.";
2236
+ function spendingScanReadyAfterHistory(progress, scanStatus) {
2237
+ return progress >= 1 || utxoScanTerminalStatus(scanStatus) === "done";
2238
+ }
2239
+ function spendingNoteScanGaveUp(args) {
2240
+ if (args.walletReady || args.notesFinished) return false;
2241
+ if (args.sawProgress) return args.msSinceProgress >= RAILGUN_MERKLETREE_STALL_MS;
2242
+ if (args.msWaitingForDecrypt == null) return false;
2243
+ return args.msWaitingForDecrypt >= SPEND_DECRYPT_WAIT_MS;
2244
+ }
2245
+ async function scanSpendingWalletNotes(args) {
2246
+ const rpcUrl = args.rpcUrl.trim();
2247
+ if (!rpcUrl) throw new Error("rpcUrl is required.");
1166
2248
  await ensureRailgunProvider(args.chainId, rpcUrl);
2249
+ const wallet = await walletModule2();
1167
2250
  const chain = { type: sharedModels.ChainType.EVM, id: args.chainId };
1168
- await wallet.refreshBalances(chain, [args.walletId]);
1169
- if (await scanSettled === "failed") {
1170
- const cause = latestRailgunEngineError();
1171
- throw new Error(cause ? `Railgun note scan stopped before it finished. ${cause}` : "Railgun note scan stopped before it finished.");
2251
+ await hookUtxoScans(wallet);
2252
+ let sawUtxoEvent = false;
2253
+ let lastUtxoAt = Date.now();
2254
+ let notesFinished = false;
2255
+ let walletReady = false;
2256
+ const listener = (event) => {
2257
+ if (event.chain.id !== args.chainId) return;
2258
+ sawUtxoEvent = true;
2259
+ lastUtxoAt = Date.now();
2260
+ if (spendingScanReadyAfterHistory(event.progress, event.scanStatus)) {
2261
+ notesFinished = true;
2262
+ walletReady = true;
2263
+ }
2264
+ args.onProgress?.(event.progress);
2265
+ };
2266
+ utxoListeners.add(listener);
2267
+ const listenForDecrypt = () => {
2268
+ wallet.awaitWalletScan(args.walletId, chain).then(
2269
+ () => {
2270
+ walletReady = true;
2271
+ },
2272
+ () => {
2273
+ listenForDecrypt();
2274
+ }
2275
+ );
2276
+ };
2277
+ listenForDecrypt();
2278
+ let refreshSettled = false;
2279
+ let refreshError = null;
2280
+ void wallet.refreshBalances(chain, [args.walletId]).then(
2281
+ () => {
2282
+ refreshSettled = true;
2283
+ },
2284
+ (err) => {
2285
+ refreshError = err;
2286
+ refreshSettled = true;
2287
+ }
2288
+ );
2289
+ let decryptWaitStarted = null;
2290
+ try {
2291
+ while (!walletReady) {
2292
+ if (refreshError) throw refreshError;
2293
+ const waitingForDecrypt = notesFinished || !sawUtxoEvent && refreshSettled ? decryptWaitStarted ??= Date.now() : null;
2294
+ if (waitingForDecrypt == null) decryptWaitStarted = null;
2295
+ if (spendingNoteScanGaveUp({
2296
+ walletReady,
2297
+ sawProgress: sawUtxoEvent,
2298
+ notesFinished,
2299
+ msSinceProgress: Date.now() - lastUtxoAt,
2300
+ msWaitingForDecrypt: waitingForDecrypt == null ? null : Date.now() - waitingForDecrypt
2301
+ })) {
2302
+ throw new Error(RAILGUN_SPEND_SCAN_STOPPED);
2303
+ }
2304
+ await delay2(250);
2305
+ }
2306
+ } finally {
2307
+ utxoListeners.delete(listener);
1172
2308
  }
1173
- const balances = snapshotRows(args.chainId);
1174
- const scanStatus = balances.length === 0 ? "empty" : "ready";
1175
- publishRailgunPrivateBalanceSnapshot({
1176
- chainId: args.chainId,
1177
- scanStatus,
1178
- balances,
1179
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1180
- notes: balances.length === 0 ? "No private balance on this chain yet." : void 0
2309
+ }
2310
+ async function scanRailgunPrivateBalances(args) {
2311
+ const rpcUrl = args.rpcUrl.trim();
2312
+ if (!rpcUrl) throw new Error("rpcUrl is required.");
2313
+ const key = `${args.chainId}:${args.walletId}`;
2314
+ const existing = scansInFlight.get(key);
2315
+ if (existing) return existing;
2316
+ const run = scanRailgunPrivateBalancesOnce({ ...args, rpcUrl }).finally(() => {
2317
+ if (scansInFlight.get(key) === run) scansInFlight.delete(key);
1181
2318
  });
1182
- return { scanStatus, balances };
2319
+ scansInFlight.set(key, run);
2320
+ return run;
1183
2321
  }
1184
2322
 
1185
2323
  // src/protocols/evm/railgun/index.ts
@@ -1216,7 +2354,16 @@ var railgunProtocolModule = {
1216
2354
  params: {
1217
2355
  tokenAddress: { type: "string", required: true, description: "Shielded ERC-20" },
1218
2356
  amountWei: { type: "string", required: true, description: "Amount in wei" },
1219
- recipientAddress: { type: "string", required: true, description: "Recipient 0zk address" }
2357
+ recipientAddress: {
2358
+ type: "string",
2359
+ required: false,
2360
+ description: "Recipient 0zk address. Omit when toContactName is set."
2361
+ },
2362
+ toContactName: {
2363
+ type: "string",
2364
+ required: false,
2365
+ description: "Address-book contact name. Uses that contact\u2019s saved 0zk address, not their Ethereum address."
2366
+ }
1220
2367
  }
1221
2368
  },
1222
2369
  {
@@ -1235,39 +2382,64 @@ var railgunProtocolModule = {
1235
2382
  };
1236
2383
  registerProtocolModule(railgunProtocolModule);
1237
2384
 
2385
+ exports.BROADCASTER_FEE_MIN_REMAINING_MS = BROADCASTER_FEE_MIN_REMAINING_MS;
2386
+ exports.BROADCASTER_PROOF_WAIT_MS = BROADCASTER_PROOF_WAIT_MS;
2387
+ exports.RAILGUN_CATCH_UP_MS = RAILGUN_CATCH_UP_MS;
1238
2388
  exports.RAILGUN_CTM_DISABLED_MESSAGE = RAILGUN_CTM_DISABLED_MESSAGE;
1239
2389
  exports.RAILGUN_CTM_ENABLED = RAILGUN_CTM_ENABLED;
2390
+ exports.RAILGUN_MERKLETREE_STALL_MS = RAILGUN_MERKLETREE_STALL_MS;
1240
2391
  exports.RAILGUN_POPULAR_ASSET_LIMIT = RAILGUN_POPULAR_ASSET_LIMIT;
1241
2392
  exports.RAILGUN_PROTOCOL_ID = RAILGUN_PROTOCOL_ID;
2393
+ exports.RAILGUN_SCAN_BEHIND_NOTE = RAILGUN_SCAN_BEHIND_NOTE;
2394
+ exports.RAILGUN_SCAN_STOPPED_NOTE = RAILGUN_SCAN_STOPPED_NOTE;
2395
+ exports.RAILGUN_SPEND_SCAN_STOPPED = RAILGUN_SPEND_SCAN_STOPPED;
2396
+ exports.RAILGUN_TXID_WAIT_AFTER_NOTES_MS = RAILGUN_TXID_WAIT_AFTER_NOTES_MS;
1242
2397
  exports.RAILGUN_WALLET_LOCKED_MESSAGE = RAILGUN_WALLET_LOCKED_MESSAGE;
1243
2398
  exports.assertRailgunCtmAllowed = assertRailgunCtmAllowed;
2399
+ exports.balancesToKeepAfterNoteScan = balancesToKeepAfterNoteScan;
2400
+ exports.broadcasterAskedToRefreshProofStatus = broadcasterAskedToRefreshProofStatus;
2401
+ exports.broadcasterFeeStillValid = broadcasterFeeStillValid;
2402
+ exports.broadcasterRefusedForGas = broadcasterRefusedForGas;
1244
2403
  exports.buildEvmMultisignBodyRailgunShield = buildEvmMultisignBodyRailgunShield;
1245
2404
  exports.buildEvmMultisignBodyWethUnwrap = buildEvmMultisignBodyWethUnwrap;
2405
+ exports.clearRailgunFeeCache = clearRailgunFeeCache;
1246
2406
  exports.clearRailgunNetworkCache = clearRailgunNetworkCache;
1247
2407
  exports.clearRailgunPopularAssetCache = clearRailgunPopularAssetCache;
1248
2408
  exports.clearRailgunPrivateBalanceSnapshots = clearRailgunPrivateBalanceSnapshots;
2409
+ exports.clearRailgunWalletBalances = clearRailgunWalletBalances;
1249
2410
  exports.commitmentsFromShieldIndex = commitmentsFromShieldIndex;
1250
2411
  exports.createRailgunBrowserWallet = createRailgunBrowserWallet;
1251
2412
  exports.discardRailgunWallet = discardRailgunWallet;
1252
2413
  exports.getRailgunWalletSession = getRailgunWalletSession;
2414
+ exports.listRailgunBroadcasters = listRailgunBroadcasters;
1253
2415
  exports.loadRailgunNetworks = loadRailgunNetworks;
1254
2416
  exports.loadRailgunPopularAssets = loadRailgunPopularAssets;
1255
2417
  exports.loadRailgunSupportedChainIds = loadRailgunSupportedChainIds;
2418
+ exports.merkletreeScanStillRunning = merkletreeScanStillRunning;
1256
2419
  exports.openRailgunViewOnlyWallet = openRailgunViewOnlyWallet;
1257
2420
  exports.publishRailgunPrivateBalanceSnapshot = publishRailgunPrivateBalanceSnapshot;
2421
+ exports.quoteRailgunBroadcasterFee = quoteRailgunBroadcasterFee;
2422
+ exports.quoteRailgunBroadcasterFeeRate = quoteRailgunBroadcasterFeeRate;
2423
+ exports.quoteRailgunFee = quoteRailgunFee;
1258
2424
  exports.railgunAddressIsValid = railgunAddressIsValid;
2425
+ exports.railgunDetailedError = railgunDetailedError;
1259
2426
  exports.railgunFetchPopularAssets = railgunFetchPopularAssets;
1260
2427
  exports.railgunFetchPrivateBalances = railgunFetchPrivateBalances;
1261
2428
  exports.railgunFetchPrivateBalancesSummary = railgunFetchPrivateBalancesSummary;
1262
2429
  exports.railgunNetworkByChainId = railgunNetworkByChainId;
1263
2430
  exports.railgunPrivateTransfer = railgunPrivateTransfer;
2431
+ exports.railgunProtocolFee = railgunProtocolFee;
1264
2432
  exports.railgunProtocolModule = railgunProtocolModule;
2433
+ exports.railgunScanRecord = railgunScanRecord;
1265
2434
  exports.railgunShareableViewingKey = railgunShareableViewingKey;
1266
2435
  exports.railgunUnshield = railgunUnshield;
2436
+ exports.railgunWalletBalanceSnapshot = railgunWalletBalanceSnapshot;
1267
2437
  exports.rankPopularShieldTokens = rankPopularShieldTokens;
1268
2438
  exports.readRailgunPrivateBalanceSnapshot = readRailgunPrivateBalanceSnapshot;
2439
+ exports.recordRailgunWalletBalance = recordRailgunWalletBalance;
1269
2440
  exports.requireRailgunWalletSession = requireRailgunWalletSession;
1270
2441
  exports.scanRailgunPrivateBalances = scanRailgunPrivateBalances;
2442
+ exports.scanSpendingWalletNotes = scanSpendingWalletNotes;
1271
2443
  exports.selectRailgunNetworks = selectRailgunNetworks;
1272
2444
  exports.setRailgunAllowanceReaderForTests = setRailgunAllowanceReaderForTests;
1273
2445
  exports.setRailgunPopulateShieldForTests = setRailgunPopulateShieldForTests;
@@ -1275,6 +2447,8 @@ exports.setRailgunShieldLogReaderForTests = setRailgunShieldLogReaderForTests;
1275
2447
  exports.setRailgunWalletSession = setRailgunWalletSession;
1276
2448
  exports.shieldIndexEndpoint = shieldIndexEndpoint;
1277
2449
  exports.shieldPrivateKeyFromSignature = shieldPrivateKeyFromSignature;
2450
+ exports.spendingNoteScanGaveUp = spendingNoteScanGaveUp;
2451
+ exports.spendingScanReadyAfterHistory = spendingScanReadyAfterHistory;
1278
2452
  exports.unlockRailgunBrowserWallet = unlockRailgunBrowserWallet;
1279
2453
  exports.utxoScanTerminalStatus = utxoScanTerminalStatus;
1280
2454
  //# sourceMappingURL=index.cjs.map