opacity-issuer-core 0.2.1 → 0.2.2

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/index.cjs CHANGED
@@ -8641,6 +8641,777 @@ async function transferSandboxUser(publicClient, params) {
8641
8641
  active2.delete(key);
8642
8642
  }
8643
8643
  }
8644
+ var sandboxWalletLinkAbi = [
8645
+ {
8646
+ type: "function",
8647
+ name: "sandboxWalletLinkingVersion",
8648
+ stateMutability: "pure",
8649
+ inputs: [],
8650
+ outputs: [{ type: "uint256" }]
8651
+ },
8652
+ {
8653
+ type: "function",
8654
+ name: "sandboxWalletLinkUsed",
8655
+ stateMutability: "view",
8656
+ inputs: [{ name: "digest", type: "bytes32" }],
8657
+ outputs: [{ type: "bool" }]
8658
+ },
8659
+ {
8660
+ type: "function",
8661
+ name: "sandboxLinkWallet",
8662
+ stateMutability: "nonpayable",
8663
+ inputs: [
8664
+ { name: "newWallet", type: "address" },
8665
+ { name: "nonce", type: "bytes32" },
8666
+ { name: "deadline", type: "uint64" },
8667
+ { name: "newWalletSignature", type: "bytes" }
8668
+ ],
8669
+ outputs: []
8670
+ },
8671
+ {
8672
+ type: "event",
8673
+ name: "SandboxWalletLinked",
8674
+ anonymous: false,
8675
+ inputs: [
8676
+ { name: "existingWallet", type: "address", indexed: true },
8677
+ { name: "newWallet", type: "address", indexed: true },
8678
+ { name: "userId", type: "bytes32", indexed: true },
8679
+ { name: "nonce", type: "bytes32", indexed: false },
8680
+ { name: "deadline", type: "uint64", indexed: false }
8681
+ ]
8682
+ }
8683
+ ];
8684
+ var sandboxWalletLinkTypes = {
8685
+ SandboxWalletLink: [
8686
+ { name: "existingWallet", type: "address" },
8687
+ { name: "newWallet", type: "address" },
8688
+ { name: "userId", type: "bytes32" },
8689
+ { name: "nonce", type: "bytes32" },
8690
+ { name: "deadline", type: "uint64" }
8691
+ ]
8692
+ };
8693
+ var SandboxWalletLinkError = class extends Error {
8694
+ constructor(code, message, progress) {
8695
+ super(message);
8696
+ this.code = code;
8697
+ this.progress = progress;
8698
+ this.name = "SandboxWalletLinkError";
8699
+ }
8700
+ code;
8701
+ progress;
8702
+ };
8703
+ function walletLinkData(value, label, keys) {
8704
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
8705
+ invalid(`${label} must be a plain object.`);
8706
+ for (const key of Reflect.ownKeys(value)) {
8707
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
8708
+ if (typeof key !== "string" || !keys.includes(key) || !descriptor.enumerable || !Object.hasOwn(descriptor, "value"))
8709
+ invalid(`${label} must contain only recognized enumerable data fields.`);
8710
+ }
8711
+ }
8712
+ var fields = [
8713
+ "chainId",
8714
+ "registryAddress",
8715
+ "existingWallet",
8716
+ "newWallet",
8717
+ "nonce",
8718
+ "deadline",
8719
+ "checkedAssets"
8720
+ ];
8721
+ function validateSandboxWalletLinkParams(input) {
8722
+ walletLinkData(input, "Wallet link", fields);
8723
+ if (!Array.isArray(input.checkedAssets) || input.checkedAssets.length === 0)
8724
+ invalid("checkedAssets must include every relevant fund token.");
8725
+ for (const key of Reflect.ownKeys(input.checkedAssets)) {
8726
+ if (key === "length") continue;
8727
+ const descriptor = Object.getOwnPropertyDescriptor(input.checkedAssets, key);
8728
+ if (typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= input.checkedAssets.length || !descriptor.enumerable || !Object.hasOwn(descriptor, "value"))
8729
+ invalid("checkedAssets must contain data entries only.");
8730
+ }
8731
+ if (Object.keys(input.checkedAssets).length !== input.checkedAssets.length)
8732
+ invalid("checkedAssets must not contain missing entries.");
8733
+ const seen = /* @__PURE__ */ new Set();
8734
+ const checkedAssets = input.checkedAssets.map((asset) => {
8735
+ walletLinkData(asset, "Checked asset", ["token", "tokenId"]);
8736
+ const token = issuerAddress(asset.token, "token");
8737
+ const tokenId = issuerUint(asset.tokenId, "tokenId");
8738
+ const key = `${token.toLowerCase()}:${tokenId}`;
8739
+ if (seen.has(key)) invalid("checkedAssets must not contain duplicates.");
8740
+ seen.add(key);
8741
+ return Object.freeze({ token, tokenId });
8742
+ });
8743
+ const result = {
8744
+ checkedAssets: Object.freeze(checkedAssets),
8745
+ chainId: issuerChainId(input.chainId),
8746
+ registryAddress: issuerAddress(input.registryAddress, "registryAddress"),
8747
+ existingWallet: issuerAddress(input.existingWallet, "existingWallet"),
8748
+ newWallet: issuerAddress(input.newWallet, "newWallet"),
8749
+ nonce: issuerBytes(input.nonce, "nonce", 32),
8750
+ deadline: issuerUint(input.deadline, "deadline")
8751
+ };
8752
+ if (result.deadline === 0n || result.deadline >= 1n << 64n)
8753
+ invalid("deadline must be a positive uint64 timestamp.");
8754
+ if (sameIssuerAddress(result.existingWallet, result.newWallet))
8755
+ invalid("Choose a different wallet to link.");
8756
+ return Object.freeze(result);
8757
+ }
8758
+ function validateSandboxWalletLinkIntent(input) {
8759
+ walletLinkData(input, "Wallet link intent", [...fields, "userId"]);
8760
+ const { userId, ...params } = input;
8761
+ const id = issuerBytes(userId, "userId", 32);
8762
+ if (id.toLowerCase() === viem.zeroHash) invalid("The existing wallet must have a user identity.");
8763
+ return Object.freeze({ ...validateSandboxWalletLinkParams(params), userId: id });
8764
+ }
8765
+ function buildSandboxWalletLinkTypedData(input) {
8766
+ const { chainId: chainId4, registryAddress, existingWallet, newWallet, userId, nonce, deadline } = validateSandboxWalletLinkIntent(input);
8767
+ const message = { existingWallet, newWallet, userId, nonce, deadline };
8768
+ return {
8769
+ domain: {
8770
+ name: "AllowlistRegistry",
8771
+ version: "1",
8772
+ chainId: chainId4,
8773
+ verifyingContract: registryAddress
8774
+ },
8775
+ types: sandboxWalletLinkTypes,
8776
+ primaryType: "SandboxWalletLink",
8777
+ message
8778
+ };
8779
+ }
8780
+ function buildSandboxWalletLinkTransaction(input, signature) {
8781
+ const intent = validateSandboxWalletLinkIntent(input);
8782
+ return {
8783
+ from: intent.existingWallet,
8784
+ to: intent.registryAddress,
8785
+ chainId: intent.chainId,
8786
+ value: 0n,
8787
+ data: viem.encodeFunctionData({
8788
+ abi: sandboxWalletLinkAbi,
8789
+ functionName: "sandboxLinkWallet",
8790
+ args: [intent.newWallet, intent.nonce, intent.deadline, issuerBytes(signature, "signature")]
8791
+ })
8792
+ };
8793
+ }
8794
+ function walletLinkWaitOptions(value = {}) {
8795
+ walletLinkData(value, "wait", ["confirmations", "timeout", "pollingInterval"]);
8796
+ for (const n of Object.values(value))
8797
+ if (n !== void 0 && (!Number.isSafeInteger(n) || n <= 0))
8798
+ invalid("Wait options must be positive safe integers.");
8799
+ return { ...value };
8800
+ }
8801
+ async function checkChain3(client, chainId4) {
8802
+ if (client.chain && client.chain.id !== chainId4 || await client.getChainId() !== chainId4)
8803
+ invalid("The wallet, RPC and requested chains must match.");
8804
+ }
8805
+ async function checkWallet(wallet, chainId4, expected) {
8806
+ if (!wallet) invalid("Wallet linking requires a wallet client.");
8807
+ await checkChain3(wallet, chainId4);
8808
+ const selected = wallet.account?.type === "local" ? wallet.account.address : (await wallet.getAddresses())[0];
8809
+ if (!selected || !sameIssuerAddress(selected, expected) || wallet.account && !sameIssuerAddress(wallet.account.address, selected))
8810
+ invalid("The selected wallet changed. Review the link with the expected wallet.");
8811
+ return wallet;
8812
+ }
8813
+ function absentCapability(error) {
8814
+ if (!(error instanceof viem.BaseError)) return false;
8815
+ return !!error.walk(
8816
+ (cause) => cause instanceof viem.ContractFunctionRevertedError || cause instanceof viem.ContractFunctionZeroDataError
8817
+ );
8818
+ }
8819
+ function rejected(error) {
8820
+ const seen = /* @__PURE__ */ new Set();
8821
+ for (let e = error; e && typeof e === "object" && !seen.has(e); e = e.cause) {
8822
+ seen.add(e);
8823
+ if (e.code === 4001) return true;
8824
+ }
8825
+ return false;
8826
+ }
8827
+ var pending = /* @__PURE__ */ new Map();
8828
+ var keyFor = (i) => `${i.chainId}:${i.existingWallet.toLowerCase()}`;
8829
+ var sameIntent = (a, b) => viem.hashTypedData(buildSandboxWalletLinkTypedData(a)) === viem.hashTypedData(buildSandboxWalletLinkTypedData(b));
8830
+ function createSandboxWalletLinksNamespace(options) {
8831
+ async function clientFor(chainId4) {
8832
+ if (options.chainId !== void 0 && options.chainId !== chainId4)
8833
+ invalid("Configured and requested wallet linking chains must match.");
8834
+ await checkChain3(options.publicClient, chainId4);
8835
+ return options.publicClient;
8836
+ }
8837
+ async function prepareWalletLink(input) {
8838
+ const params = validateSandboxWalletLinkParams(input);
8839
+ const client = await clientFor(params.chainId);
8840
+ const block = await client.getBlock();
8841
+ const blockNumber = block.number;
8842
+ const context = { address: params.registryAddress, blockNumber };
8843
+ let supported = false;
8844
+ try {
8845
+ supported = await client.readContract({
8846
+ ...context,
8847
+ abi: sandboxWalletLinkAbi,
8848
+ functionName: "sandboxWalletLinkingVersion"
8849
+ }) === 1n;
8850
+ } catch (error) {
8851
+ if (!absentCapability(error)) throw error;
8852
+ }
8853
+ const [paused, allowed, userId, targetUserId, targetStatus, targetIssuer, sourceExpiry] = await Promise.all([
8854
+ client.readContract({ ...context, abi: issuerRegistryAbi, functionName: "paused" }),
8855
+ client.readContract({
8856
+ ...context,
8857
+ abi: issuerRegistryAbi,
8858
+ functionName: "isAllowed",
8859
+ args: [params.existingWallet]
8860
+ }),
8861
+ client.readContract({
8862
+ ...context,
8863
+ abi: issuerRegistryAbi,
8864
+ functionName: "getUserId",
8865
+ args: [params.existingWallet]
8866
+ }),
8867
+ client.readContract({
8868
+ ...context,
8869
+ abi: issuerRegistryAbi,
8870
+ functionName: "getUserId",
8871
+ args: [params.newWallet]
8872
+ }),
8873
+ client.readContract({
8874
+ ...context,
8875
+ abi: issuerRegistryAbi,
8876
+ functionName: "getEntryStatus",
8877
+ args: [params.newWallet]
8878
+ }),
8879
+ client.readContract({
8880
+ ...context,
8881
+ abi: issuerRegistryAbi,
8882
+ functionName: "isAuthorizedIssuer",
8883
+ args: [params.newWallet]
8884
+ }),
8885
+ client.readContract({
8886
+ ...context,
8887
+ abi: issuerRegistryAbi,
8888
+ functionName: "getEntryExpiry",
8889
+ args: [params.existingWallet]
8890
+ })
8891
+ ]);
8892
+ const intent = { ...params, userId };
8893
+ const blockers = [];
8894
+ if (!supported) blockers.push("unsupported-wallet-linking");
8895
+ if (paused) blockers.push("registry-paused");
8896
+ if (!allowed) blockers.push("source-not-approved");
8897
+ if (userId === viem.zeroHash) blockers.push("source-identity-missing");
8898
+ if (targetUserId !== viem.zeroHash) blockers.push("target-already-linked");
8899
+ if (targetStatus !== 0) blockers.push("target-not-fresh");
8900
+ if (targetIssuer) blockers.push("target-is-issuer");
8901
+ for (const asset of params.checkedAssets) {
8902
+ const registry = await client.readContract({
8903
+ address: asset.token,
8904
+ blockNumber,
8905
+ abi: issuerTokenAbi,
8906
+ functionName: "allowlistRegistry"
8907
+ });
8908
+ if (!sameIssuerAddress(registry, params.registryAddress))
8909
+ invalid("A checked fund token uses a different registry.");
8910
+ const balance = await client.readContract({
8911
+ address: asset.token,
8912
+ blockNumber,
8913
+ abi: issuerTokenAbi,
8914
+ functionName: "balanceOf",
8915
+ args: [params.newWallet, asset.tokenId]
8916
+ });
8917
+ if (balance !== 0n && !blockers.includes("target-holds-assets"))
8918
+ blockers.push("target-holds-assets");
8919
+ }
8920
+ if (params.deadline < block.timestamp) blockers.push("consent-expired");
8921
+ if (supported && userId !== viem.zeroHash && await client.readContract({
8922
+ ...context,
8923
+ abi: sandboxWalletLinkAbi,
8924
+ functionName: "sandboxWalletLinkUsed",
8925
+ args: [viem.hashTypedData(buildSandboxWalletLinkTypedData(intent))]
8926
+ }))
8927
+ blockers.push("consent-used");
8928
+ await clientFor(params.chainId);
8929
+ return {
8930
+ intent: Object.freeze(intent),
8931
+ blockNumber,
8932
+ sourceExpiry,
8933
+ blockers,
8934
+ ready: blockers.length === 0
8935
+ };
8936
+ }
8937
+ async function fresh(input) {
8938
+ const intent = validateSandboxWalletLinkIntent(input);
8939
+ const { userId: _id, ...params } = intent;
8940
+ const prepared = await prepareWalletLink(params);
8941
+ if (prepared.blockers.length)
8942
+ throw new SandboxWalletLinkError(
8943
+ "blocked",
8944
+ `Wallet linking is blocked: ${prepared.blockers.join(", ")}.`
8945
+ );
8946
+ if (!sameIntent(intent, prepared.intent))
8947
+ throw new SandboxWalletLinkError(
8948
+ "stale-intent",
8949
+ "The existing wallet identity changed. Prepare and review a new link."
8950
+ );
8951
+ return prepared;
8952
+ }
8953
+ async function signWalletLink(input, newWalletClient) {
8954
+ const intent = validateSandboxWalletLinkIntent(input);
8955
+ await fresh(intent);
8956
+ const wallet = await checkWallet(newWalletClient, intent.chainId, intent.newWallet);
8957
+ let signature;
8958
+ try {
8959
+ signature = await wallet.signTypedData({
8960
+ ...buildSandboxWalletLinkTypedData(intent),
8961
+ account: wallet.account ?? intent.newWallet
8962
+ });
8963
+ } catch (error) {
8964
+ throw new SandboxWalletLinkError(
8965
+ rejected(error) ? "rejected" : "blocked",
8966
+ "Wallet consent was not signed. No transaction was sent."
8967
+ );
8968
+ }
8969
+ await checkWallet(wallet, intent.chainId, intent.newWallet);
8970
+ return issuerBytes(signature, "signature");
8971
+ }
8972
+ async function preflightWalletLink(input, signature) {
8973
+ const intent = validateSandboxWalletLinkIntent(input);
8974
+ const transaction = buildSandboxWalletLinkTransaction(intent, signature);
8975
+ const prepared = await fresh(intent);
8976
+ const client = await clientFor(intent.chainId);
8977
+ try {
8978
+ await client.call({
8979
+ account: transaction.from,
8980
+ to: transaction.to,
8981
+ data: transaction.data,
8982
+ value: 0n
8983
+ });
8984
+ } catch (error) {
8985
+ const reverted = error instanceof viem.BaseError && !!error.walk(
8986
+ (cause) => cause instanceof viem.ContractFunctionRevertedError || cause instanceof viem.ExecutionRevertedError
8987
+ );
8988
+ throw new SandboxWalletLinkError(
8989
+ reverted ? "blocked" : "check-unavailable",
8990
+ reverted ? "This wallet link was rejected. Check the consent and current wallet eligibility." : "The connection could not check this wallet link. Try the check again before sending."
8991
+ );
8992
+ }
8993
+ let gas;
8994
+ try {
8995
+ const [estimatedGas, gasPrice, nativeBalance] = await Promise.all([
8996
+ client.estimateGas({
8997
+ account: transaction.from,
8998
+ to: transaction.to,
8999
+ data: transaction.data,
9000
+ value: 0n
9001
+ }),
9002
+ client.getGasPrice(),
9003
+ client.getBalance({ address: intent.existingWallet })
9004
+ ]);
9005
+ const estimatedCost = estimatedGas * gasPrice;
9006
+ gas = { estimatedGas, gasPrice, estimatedCost, nativeBalance };
9007
+ if (nativeBalance < estimatedCost || nativeBalance === 0n)
9008
+ throw new SandboxWalletLinkError(
9009
+ "insufficient-gas",
9010
+ "The existing wallet needs more test ETH to link this wallet."
9011
+ );
9012
+ } catch (error) {
9013
+ if (error instanceof SandboxWalletLinkError) throw error;
9014
+ throw new SandboxWalletLinkError(
9015
+ "check-unavailable",
9016
+ "The connection could not estimate the network fee. Check again before sending."
9017
+ );
9018
+ }
9019
+ await clientFor(intent.chainId);
9020
+ return { ...prepared, gas };
9021
+ }
9022
+ async function sendWalletLink(input) {
9023
+ walletLinkData(input, "Send wallet link", ["intent", "signature", "persist"]);
9024
+ const intent = validateSandboxWalletLinkIntent(input.intent);
9025
+ const signature = issuerBytes(input.signature, "signature");
9026
+ const persist = input.persist;
9027
+ if (typeof persist !== "function")
9028
+ invalid("persist must save wallet linking progress before sending.");
9029
+ const key = keyFor(intent);
9030
+ if (pending.has(key))
9031
+ throw new SandboxWalletLinkError(
9032
+ "send-in-progress",
9033
+ "Check the existing wallet link before starting another.",
9034
+ structuredClone(pending.get(key))
9035
+ );
9036
+ const progress = { kind: "wallet-link", intent, stage: "prepared" };
9037
+ pending.set(key, structuredClone(progress));
9038
+ let attempted = false;
9039
+ let persistence = false;
9040
+ const save = async () => {
9041
+ pending.set(key, structuredClone(progress));
9042
+ persistence = true;
9043
+ await persist(structuredClone(progress));
9044
+ persistence = false;
9045
+ };
9046
+ try {
9047
+ await save();
9048
+ await preflightWalletLink(intent, signature);
9049
+ await checkWallet(options.walletClient, intent.chainId, intent.existingWallet);
9050
+ progress.senderNonce = await (await clientFor(intent.chainId)).getTransactionCount({ address: intent.existingWallet, blockTag: "pending" });
9051
+ if (!Number.isSafeInteger(progress.senderNonce) || progress.senderNonce < 0)
9052
+ invalid("The RPC returned an invalid transaction nonce.");
9053
+ progress.stage = "sending";
9054
+ await save();
9055
+ await preflightWalletLink(intent, signature);
9056
+ const wallet = await checkWallet(options.walletClient, intent.chainId, intent.existingWallet);
9057
+ const client = await clientFor(intent.chainId);
9058
+ if (await client.getTransactionCount({
9059
+ address: intent.existingWallet,
9060
+ blockTag: "pending"
9061
+ }) !== progress.senderNonce)
9062
+ throw new SandboxWalletLinkError(
9063
+ "stale-intent",
9064
+ "The existing wallet has another pending transaction. Check it before reviewing a new link."
9065
+ );
9066
+ const tx = buildSandboxWalletLinkTransaction(intent, signature);
9067
+ attempted = true;
9068
+ const hash = await wallet.sendTransaction({
9069
+ account: wallet.account ?? intent.existingWallet,
9070
+ chain: wallet.chain ?? viem.defineChain({
9071
+ id: intent.chainId,
9072
+ name: `Sandbox ${intent.chainId}`,
9073
+ nativeCurrency: { name: "Native", symbol: "NATIVE", decimals: 18 },
9074
+ rpcUrls: { default: { http: [] } }
9075
+ }),
9076
+ to: tx.to,
9077
+ data: tx.data,
9078
+ value: 0n,
9079
+ nonce: progress.senderNonce
9080
+ });
9081
+ progress.hash = issuerBytes(hash, "transaction hash", 32);
9082
+ progress.stage = "submitted";
9083
+ await save();
9084
+ return structuredClone(progress);
9085
+ } catch (error) {
9086
+ if (!attempted || !progress.hash && !persistence && rejected(error)) {
9087
+ pending.delete(key);
9088
+ progress.stage = "rejected";
9089
+ try {
9090
+ await persist(structuredClone(progress));
9091
+ } catch {
9092
+ }
9093
+ if (error instanceof SandboxWalletLinkError)
9094
+ throw new SandboxWalletLinkError(error.code, error.message, structuredClone(progress));
9095
+ throw new SandboxWalletLinkError(
9096
+ persistence ? "persistence-failed" : "rejected",
9097
+ persistence ? "Wallet link progress could not be saved. No transaction was sent." : "Wallet linking stopped before a transaction was submitted.",
9098
+ structuredClone(progress)
9099
+ );
9100
+ }
9101
+ pending.set(key, structuredClone(progress));
9102
+ throw new SandboxWalletLinkError(
9103
+ "uncertain",
9104
+ "Check the existing transaction before trying another wallet link.",
9105
+ structuredClone(progress)
9106
+ );
9107
+ }
9108
+ }
9109
+ async function waitForWalletLink(input, wait = {}) {
9110
+ walletLinkData(input, "Wallet link submission", [
9111
+ "kind",
9112
+ "intent",
9113
+ "stage",
9114
+ "hash",
9115
+ "senderNonce",
9116
+ "result"
9117
+ ]);
9118
+ if (input.kind !== "wallet-link" || !["submitted", "complete"].includes(input.stage))
9119
+ invalid("Expected a submitted wallet link.");
9120
+ const intent = validateSandboxWalletLinkIntent(input.intent);
9121
+ const hash = issuerBytes(input.hash, "hash", 32);
9122
+ const senderNonce = input.senderNonce;
9123
+ if (!Number.isSafeInteger(senderNonce) || senderNonce < 0)
9124
+ invalid("senderNonce must be a nonnegative safe integer.");
9125
+ const client = await clientFor(intent.chainId);
9126
+ const waitOptions3 = walletLinkWaitOptions(wait);
9127
+ let replacementReason;
9128
+ const receipt = structuredClone(
9129
+ await client.waitForTransactionReceipt({
9130
+ hash,
9131
+ ...waitOptions3,
9132
+ onReplaced: ({ reason }) => {
9133
+ replacementReason = reason;
9134
+ }
9135
+ })
9136
+ );
9137
+ await clientFor(intent.chainId);
9138
+ const base = { submittedHash: hash, hash: receipt.transactionHash, receipt };
9139
+ let exactTransaction = false;
9140
+ let result = { ...base, status: "unverified" };
9141
+ if (replacementReason || receipt.transactionHash.toLowerCase() !== hash.toLowerCase())
9142
+ result = { ...base, status: "replaced", ...replacementReason ? { replacementReason } : {} };
9143
+ else if (receipt.status === "reverted") result = { ...base, status: "reverted" };
9144
+ else if (receipt.status === "success") {
9145
+ try {
9146
+ const tx = await client.getTransaction({ hash });
9147
+ await clientFor(intent.chainId);
9148
+ const decoded = viem.decodeFunctionData({ abi: sandboxWalletLinkAbi, data: tx.input });
9149
+ const matches = sameIssuerAddress(receipt.from, intent.existingWallet) && receipt.to !== null && sameIssuerAddress(receipt.to, intent.registryAddress) && tx.blockHash === receipt.blockHash && tx.blockNumber === receipt.blockNumber && tx.hash.toLowerCase() === hash.toLowerCase() && tx.chainId === intent.chainId && sameIssuerAddress(tx.from, intent.existingWallet) && tx.to !== null && sameIssuerAddress(tx.to, intent.registryAddress) && tx.value === 0n && (senderNonce === void 0 || tx.nonce === senderNonce) && decoded.functionName === "sandboxLinkWallet" && sameIssuerAddress(decoded.args[0], intent.newWallet) && decoded.args[1].toLowerCase() === intent.nonce.toLowerCase() && decoded.args[2] === intent.deadline;
9150
+ exactTransaction = matches;
9151
+ if (matches)
9152
+ for (const log of receipt.logs) {
9153
+ if (!sameIssuerAddress(log.address, intent.registryAddress) || log.removed || log.blockHash !== receipt.blockHash || log.blockNumber !== receipt.blockNumber || log.transactionHash?.toLowerCase() !== hash.toLowerCase())
9154
+ continue;
9155
+ try {
9156
+ const { args } = viem.decodeEventLog({
9157
+ abi: sandboxWalletLinkAbi,
9158
+ eventName: "SandboxWalletLinked",
9159
+ data: log.data,
9160
+ topics: log.topics,
9161
+ strict: true
9162
+ });
9163
+ if (sameIssuerAddress(args.existingWallet, intent.existingWallet) && sameIssuerAddress(args.newWallet, intent.newWallet) && args.userId.toLowerCase() === intent.userId.toLowerCase() && args.nonce.toLowerCase() === intent.nonce.toLowerCase() && args.deadline === intent.deadline)
9164
+ result = { ...base, status: "confirmed" };
9165
+ } catch {
9166
+ }
9167
+ }
9168
+ } catch {
9169
+ }
9170
+ }
9171
+ const current = pending.get(keyFor(intent));
9172
+ if (current?.hash === void 0 && current?.stage === "sending" && result.status === "reverted") {
9173
+ try {
9174
+ const tx = await client.getTransaction({ hash });
9175
+ await clientFor(intent.chainId);
9176
+ const decoded = viem.decodeFunctionData({ abi: sandboxWalletLinkAbi, data: tx.input });
9177
+ exactTransaction = tx.hash.toLowerCase() === hash.toLowerCase() && tx.chainId === intent.chainId && sameIssuerAddress(tx.from, intent.existingWallet) && tx.to !== null && sameIssuerAddress(tx.to, intent.registryAddress) && tx.value === 0n && tx.nonce === senderNonce && tx.blockHash === receipt.blockHash && tx.blockNumber === receipt.blockNumber && decoded.functionName === "sandboxLinkWallet" && sameIssuerAddress(decoded.args[0], intent.newWallet) && decoded.args[1].toLowerCase() === intent.nonce.toLowerCase() && decoded.args[2] === intent.deadline;
9178
+ } catch {
9179
+ }
9180
+ }
9181
+ if (current && pending.get(keyFor(intent)) === current && sameIntent(current.intent, intent) && (current.hash?.toLowerCase() === hash.toLowerCase() || current.hash === void 0 && current.stage === "sending" && current.senderNonce === senderNonce && exactTransaction) && (result.status === "confirmed" || result.status === "reverted" || result.status === "replaced"))
9182
+ pending.delete(keyFor(intent));
9183
+ return result;
9184
+ }
9185
+ return {
9186
+ prepareWalletLink,
9187
+ signWalletLink,
9188
+ preflightWalletLink,
9189
+ sendWalletLink,
9190
+ waitForWalletLink
9191
+ };
9192
+ }
9193
+ var SandboxLinkedWalletCreationError = class extends Error {
9194
+ constructor(progress) {
9195
+ super(
9196
+ "Wallet creation stopped. Recover the saved wallet and check its pending link before another attempt."
9197
+ );
9198
+ this.progress = progress;
9199
+ this.name = "SandboxLinkedWalletCreationError";
9200
+ Object.defineProperty(this, "progress", { enumerable: false });
9201
+ }
9202
+ progress;
9203
+ };
9204
+ async function createSandboxLinkedWallet(publicClient, input) {
9205
+ walletLinkData(input, "Create linked wallet", [
9206
+ "chainId",
9207
+ "registryAddress",
9208
+ "existingWallet",
9209
+ "checkedAssets",
9210
+ "sourceWalletClient",
9211
+ "persist",
9212
+ "privateKey",
9213
+ "nonce",
9214
+ "deadline",
9215
+ "wait"
9216
+ ]);
9217
+ const chainId4 = issuerChainId(input.chainId);
9218
+ const registryAddress = issuerAddress(input.registryAddress, "registryAddress");
9219
+ const existingWallet = issuerAddress(input.existingWallet, "existingWallet");
9220
+ const persist = input.persist;
9221
+ if (typeof persist !== "function") invalid("persist must save the new wallet before sending.");
9222
+ if (!input.sourceWalletClient || typeof input.sourceWalletClient.sendTransaction !== "function")
9223
+ invalid("sourceWalletClient is required.");
9224
+ const sourceWalletClient = input.sourceWalletClient;
9225
+ const privateKey = input.privateKey === void 0 ? accounts.generatePrivateKey() : issuerBytes(input.privateKey, "privateKey", 32);
9226
+ let account;
9227
+ try {
9228
+ account = accounts.privateKeyToAccount(privateKey);
9229
+ } catch {
9230
+ invalid("privateKey must be a valid test-wallet private key.");
9231
+ }
9232
+ const nonce = input.nonce === void 0 ? accounts.generatePrivateKey() : issuerBytes(input.nonce, "nonce", 32);
9233
+ const wait = walletLinkWaitOptions(input.wait);
9234
+ const provisional = validateSandboxWalletLinkParams({
9235
+ chainId: chainId4,
9236
+ registryAddress,
9237
+ existingWallet,
9238
+ newWallet: account.address,
9239
+ checkedAssets: input.checkedAssets,
9240
+ nonce,
9241
+ deadline: input.deadline ?? 1n
9242
+ });
9243
+ const explicitDeadline = input.deadline;
9244
+ const links = createSandboxWalletLinksNamespace({
9245
+ publicClient,
9246
+ walletClient: sourceWalletClient,
9247
+ chainId: chainId4
9248
+ });
9249
+ const deadline = explicitDeadline ?? (await publicClient.getBlock()).timestamp + 900n;
9250
+ const prepared = await links.prepareWalletLink({ ...provisional, deadline });
9251
+ if (!prepared.ready)
9252
+ throw new SandboxWalletLinkError(
9253
+ "blocked",
9254
+ `Wallet linking is blocked: ${prepared.blockers.join(", ")}.`
9255
+ );
9256
+ const state = {
9257
+ kind: "linked-wallet",
9258
+ wallet: account.address,
9259
+ existingWallet,
9260
+ privateKey,
9261
+ chainId: chainId4,
9262
+ registryAddress,
9263
+ userId: prepared.intent.userId,
9264
+ stage: "created",
9265
+ link: { kind: "wallet-link", intent: prepared.intent, stage: "prepared" }
9266
+ };
9267
+ const targetWallet = viem.createWalletClient({
9268
+ account,
9269
+ transport: viem.custom({
9270
+ request: async ({ method }) => {
9271
+ if (method === "eth_chainId") return `0x${chainId4.toString(16)}`;
9272
+ throw new Error("Generated wallet transport supports only local signing.");
9273
+ }
9274
+ })
9275
+ });
9276
+ const save = () => persist(structuredClone(state));
9277
+ try {
9278
+ await save();
9279
+ const signature = await links.signWalletLink(prepared.intent, targetWallet);
9280
+ state.stage = "linking";
9281
+ const submission = await links.sendWalletLink({
9282
+ intent: prepared.intent,
9283
+ signature,
9284
+ persist: async (link) => {
9285
+ state.link = structuredClone(link);
9286
+ await save();
9287
+ }
9288
+ });
9289
+ const result = await links.waitForWalletLink(submission, wait);
9290
+ state.link = {
9291
+ ...submission,
9292
+ result,
9293
+ ...result.status === "confirmed" ? { stage: "complete" } : {}
9294
+ };
9295
+ if (result.status !== "confirmed") {
9296
+ await save();
9297
+ throw new Error("Link is not confirmed.");
9298
+ }
9299
+ state.stage = "complete";
9300
+ await save();
9301
+ return structuredClone(state);
9302
+ } catch (error) {
9303
+ if (error instanceof SandboxWalletLinkError && error.progress)
9304
+ state.link = structuredClone(error.progress);
9305
+ throw new SandboxLinkedWalletCreationError(structuredClone(state));
9306
+ }
9307
+ }
9308
+ function savedData(value, ancestors = /* @__PURE__ */ new Set()) {
9309
+ if (value === null || value === void 0 || ["string", "boolean", "bigint"].includes(typeof value))
9310
+ return;
9311
+ if (typeof value === "number" && Number.isFinite(value)) return;
9312
+ if (typeof value !== "object" || value === null || ancestors.has(value))
9313
+ invalid("Saved wallet must contain acyclic data.");
9314
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
9315
+ invalid("Saved wallet must contain plain data.");
9316
+ ancestors.add(value);
9317
+ for (const key of Reflect.ownKeys(value)) {
9318
+ if (Array.isArray(value) && key === "length") continue;
9319
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
9320
+ if (typeof key !== "string" || !descriptor.enumerable || !Object.hasOwn(descriptor, "value"))
9321
+ invalid("Saved wallet must contain enumerable data fields.");
9322
+ savedData(descriptor.value, ancestors);
9323
+ }
9324
+ ancestors.delete(value);
9325
+ }
9326
+ function validateSandboxLinkedWalletProgress(input) {
9327
+ savedData(input);
9328
+ walletLinkData(input, "Saved linked wallet", [
9329
+ "kind",
9330
+ "wallet",
9331
+ "existingWallet",
9332
+ "privateKey",
9333
+ "chainId",
9334
+ "registryAddress",
9335
+ "userId",
9336
+ "stage",
9337
+ "link"
9338
+ ]);
9339
+ if (input.kind !== "linked-wallet" || !["created", "linking", "complete"].includes(input.stage))
9340
+ invalid("Invalid saved linked-wallet state.");
9341
+ walletLinkData(input.link, "Saved wallet link", [
9342
+ "kind",
9343
+ "intent",
9344
+ "stage",
9345
+ "hash",
9346
+ "senderNonce",
9347
+ "result"
9348
+ ]);
9349
+ const intent = validateSandboxWalletLinkIntent(input.link.intent);
9350
+ if (input.link.kind !== "wallet-link" || !["prepared", "sending", "submitted", "rejected", "complete"].includes(input.link.stage))
9351
+ invalid("Invalid saved link journal.");
9352
+ if (input.link.hash !== void 0) issuerBytes(input.link.hash, "hash", 32);
9353
+ if (["submitted", "complete"].includes(input.link.stage) && input.link.hash === void 0)
9354
+ invalid("A submitted link requires its transaction hash.");
9355
+ if (input.link.senderNonce !== void 0 && (!Number.isSafeInteger(input.link.senderNonce) || input.link.senderNonce < 0))
9356
+ invalid("Invalid saved sender nonce.");
9357
+ if (["sending", "submitted", "complete"].includes(input.link.stage) && input.link.senderNonce === void 0)
9358
+ invalid("A pending link requires its sender nonce.");
9359
+ if (input.link.result !== void 0) {
9360
+ const result = input.link.result;
9361
+ walletLinkData(result, "Saved link result", [
9362
+ "status",
9363
+ "submittedHash",
9364
+ "hash",
9365
+ "receipt",
9366
+ "replacementReason"
9367
+ ]);
9368
+ if (!["confirmed", "reverted", "unverified", "replaced"].includes(result.status) || result.submittedHash.toLowerCase() !== input.link.hash?.toLowerCase() || result.receipt.transactionHash.toLowerCase() !== result.hash.toLowerCase())
9369
+ invalid("Saved link result does not match its submission.");
9370
+ issuerBytes(result.hash, "result hash", 32);
9371
+ if (result.status === "confirmed" && (result.hash.toLowerCase() !== result.submittedHash.toLowerCase() || result.receipt.status !== "success"))
9372
+ invalid("Saved confirmed result is inconsistent.");
9373
+ }
9374
+ if (input.stage === "complete" && (input.link.stage !== "complete" || input.link.result?.status !== "confirmed"))
9375
+ invalid("Saved wallet completion requires a confirmed link.");
9376
+ if (input.link.stage === "complete" && input.link.result?.status !== "confirmed")
9377
+ invalid("Saved link completion requires confirmation.");
9378
+ if (issuerChainId(input.chainId) !== intent.chainId || !sameIssuerAddress(
9379
+ issuerAddress(input.registryAddress, "registryAddress"),
9380
+ intent.registryAddress
9381
+ ) || !sameIssuerAddress(issuerAddress(input.wallet, "wallet"), intent.newWallet) || !sameIssuerAddress(
9382
+ issuerAddress(input.existingWallet, "existingWallet"),
9383
+ intent.existingWallet
9384
+ ) || issuerBytes(input.userId, "userId", 32).toLowerCase() !== intent.userId.toLowerCase())
9385
+ invalid("Saved wallet and link identities do not match.");
9386
+ try {
9387
+ if (!sameIssuerAddress(
9388
+ accounts.privateKeyToAccount(issuerBytes(input.privateKey, "privateKey", 32)).address,
9389
+ intent.newWallet
9390
+ ))
9391
+ invalid("Saved key and wallet do not match.");
9392
+ } catch {
9393
+ invalid("Saved key and wallet do not match.");
9394
+ }
9395
+ return structuredClone(input);
9396
+ }
9397
+ function restoreSandboxLinkedWallet(input, options) {
9398
+ const progress = validateSandboxLinkedWalletProgress(input);
9399
+ if (progress.stage !== "complete")
9400
+ invalid("Check the pending link before restoring this wallet for transactions.");
9401
+ return restoreSandboxUserWallet(
9402
+ {
9403
+ wallet: progress.wallet,
9404
+ privateKey: progress.privateKey,
9405
+ userId: progress.userId,
9406
+ chainId: progress.chainId,
9407
+ registryAddress: progress.registryAddress,
9408
+ stage: "complete",
9409
+ traits: [],
9410
+ submissions: []
9411
+ },
9412
+ options
9413
+ );
9414
+ }
8644
9415
 
8645
9416
  Object.defineProperty(exports, "SANDBOX_CHAIN_ID", {
8646
9417
  enumerable: true,
@@ -8830,8 +9601,10 @@ exports.OpacityPurchaseError = OpacityPurchaseError;
8830
9601
  exports.OpacityTransferError = OpacityTransferError;
8831
9602
  exports.SANDBOX_ISSUER_DEPLOYMENT = SANDBOX_ISSUER_DEPLOYMENT;
8832
9603
  exports.SANDBOX_PURCHASE_DEPLOYMENT = SANDBOX_PURCHASE_DEPLOYMENT;
9604
+ exports.SandboxLinkedWalletCreationError = SandboxLinkedWalletCreationError;
8833
9605
  exports.SandboxTransferError = SandboxTransferError;
8834
9606
  exports.SandboxUserCreationError = SandboxUserCreationError;
9607
+ exports.SandboxWalletLinkError = SandboxWalletLinkError;
8835
9608
  exports.buildAdapterOperation = buildAdapterOperation;
8836
9609
  exports.buildConfigureVaultTransaction = buildConfigureVaultTransaction;
8837
9610
  exports.buildForcedTransferTransaction = buildForcedTransferTransaction;
@@ -8856,6 +9629,8 @@ exports.buildPurchaseWriteRequest = buildPurchaseWriteRequest;
8856
9629
  exports.buildRuleSetOperation = buildRuleSetOperation;
8857
9630
  exports.buildSandboxRegistryOperation = buildSandboxRegistryOperation;
8858
9631
  exports.buildSandboxVaultOperation = buildSandboxVaultOperation;
9632
+ exports.buildSandboxWalletLinkTransaction = buildSandboxWalletLinkTransaction;
9633
+ exports.buildSandboxWalletLinkTypedData = buildSandboxWalletLinkTypedData;
8859
9634
  exports.buildSetIssuerAdminTransaction = buildSetIssuerAdminTransaction;
8860
9635
  exports.buildSetUserTraitsTransactions = buildSetUserTraitsTransactions;
8861
9636
  exports.buildSetWalletsTransaction = buildSetWalletsTransaction;
@@ -8869,7 +9644,9 @@ exports.createPolicyAttachment = createPolicyAttachment;
8869
9644
  exports.createPolicyBatch = createPolicyBatch;
8870
9645
  exports.createPolicyReplacementBatch = createPolicyReplacementBatch;
8871
9646
  exports.createPurchasesNamespace = createPurchasesNamespace;
9647
+ exports.createSandboxLinkedWallet = createSandboxLinkedWallet;
8872
9648
  exports.createSandboxUser = createSandboxUser;
9649
+ exports.createSandboxWalletLinksNamespace = createSandboxWalletLinksNamespace;
8873
9650
  exports.createTransfersNamespace = createTransfersNamespace;
8874
9651
  exports.encodeAllowedTransfereeConfig = encodeAllowedTransfereeConfig;
8875
9652
  exports.encodeCeilingBalanceConfig = encodeCeilingBalanceConfig;
@@ -8907,11 +9684,19 @@ exports.purchasePaymentTokenAbi = purchasePaymentTokenAbi;
8907
9684
  exports.purchaseRegistryAbi = purchaseRegistryAbi;
8908
9685
  exports.purchaseSecurityTokenAbi = purchaseSecurityTokenAbi;
8909
9686
  exports.resolveSandboxUserTraits = resolveSandboxUserTraits;
9687
+ exports.restoreSandboxLinkedWallet = restoreSandboxLinkedWallet;
8910
9688
  exports.restoreSandboxUserWallet = restoreSandboxUserWallet;
8911
9689
  exports.sandboxJurisdictionTraitId = sandboxJurisdictionTraitId;
8912
9690
  exports.sandboxTraitId = sandboxTraitId;
9691
+ exports.sandboxWalletLinkAbi = sandboxWalletLinkAbi;
9692
+ exports.sandboxWalletLinkTypes = sandboxWalletLinkTypes;
8913
9693
  exports.securityTokenVaultAbi = securityTokenVaultAbi;
8914
9694
  exports.transferSandboxUser = transferSandboxUser;
9695
+ exports.validateSandboxLinkedWalletProgress = validateSandboxLinkedWalletProgress;
9696
+ exports.validateSandboxWalletLinkIntent = validateSandboxWalletLinkIntent;
9697
+ exports.validateSandboxWalletLinkParams = validateSandboxWalletLinkParams;
8915
9698
  exports.waitForSandboxUserFunding = waitForSandboxUserFunding;
9699
+ exports.walletLinkData = walletLinkData;
9700
+ exports.walletLinkWaitOptions = walletLinkWaitOptions;
8916
9701
  //# sourceMappingURL=index.cjs.map
8917
9702
  //# sourceMappingURL=index.cjs.map