diff --git a/docs/architecture.md b/docs/architecture.md index 995e7fc..7d107a6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,6 +30,7 @@ flowchart TB | Wallet | starknet.js 10, get-starknet discovery 6, Ready. Connect on desktop via the injected picker. Connect on phone inside Ready's in-app browser | | Actions | `strk20InvokeTransaction` in `src/lib/escrow.ts` | | Helper | Cairo `privacy_invoke` escrow in `cairo/` | +| Private swaps | AVNU's deployed executor for shielded STRK ↔ USDC, quoted from AVNU's public API and batched in `src/lib/avnu.ts` | | Pool | Official STRK20 pool on Mainnet and Sepolia. Addresses live in `src/utils/constants.ts` | ## What the helper does diff --git a/docs/judging.md b/docs/judging.md index de2129f..4c0c370 100644 --- a/docs/judging.md +++ b/docs/judging.md @@ -22,6 +22,7 @@ How far into the stack, not how many buzzwords. | Shielded balances | Yes, via the user's Ready wallet. The dapp does not read or display a counterparty balance | Account shield/unshield in the PWA; Pay spends shielded notes | | Private transfers into app logic | Yes. Pay, claim, and cancel are `strk20InvokeTransaction` batches | `src/lib/escrow.ts` | | Anonymizer contract | Yes. Custom `privacy_invoke` escrow: Deposit, Claim, Cancel | `cairo/src/lib.cairo` | +| Private swaps | Yes. Shielded STRK ↔ USDC via AVNU's deployed executor, batched as `strk20InvokeTransaction` actions | `src/lib/avnu.ts` | | Privacy SDK | No, on purpose. A marketplace must not hold viewing keys | Wallet API route only | | Stealth / shadow accounts | No. A different STRK20 surface, not needed for the cash-like promise | See [Architecture](architecture.md) | diff --git a/package.json b/package.json index 70ada56..096f5b0 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "lint": "eslint ." }, "dependencies": { + "@avnu/avnu-sdk": "^4.2.0", "@starknet-io/get-starknet-discovery": "6.0.2", "@starknet-io/get-starknet-wallet-standard": "6.0.2", + "ethers": "^6.15.0", "next": "^16.0.8", "qrcode": "1.5.3", "react": "19.2.1", diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index 1c89efb..2cf9f19 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import ConnectGate from "@/app/components/ghost/ConnectGate"; import { useStoreWallet } from "@/app/components/Wallet/walletContext"; import { useFrontendProvider } from "@/app/components/client/provider/providerContext"; @@ -14,13 +14,19 @@ import { poolAddressForIndex, poolFeeAmount, priceToWei, + publicTokenBalance, readShieldedBalance, + requireWalletApi0103, + forceReleasePrivateOp, shieldTokens, STRK_DECIMALS, tokenAddressForListing, unshieldTokens, + waitForPublicBalanceMove, } from "@/lib/escrow"; +import { fetchSwapQuote, planPrivateSwap, submitPrivateSwapBatch, SWAP_SLIPPAGE, type Quote } from "@/lib/avnu"; import { friendlyPrivateError, isPrivateTokensOffError, privateErrorText } from "@/lib/privateWalletError"; +import { Strk20Networks } from "@/utils/constants"; // Whole STRK covering one pool fee, rounded up. Used to seed the shield input // with a fee-based default instead of a hard-coded demo amount. @@ -32,6 +38,7 @@ function wholeStrkForFee(feeWei: bigint): bigint { export default function AccountPage() { const isConnected = useStoreWallet((s) => s.isConnected); const account = useStoreWallet((s) => s.myWalletAccount); + const wallet = useStoreWallet((s) => s.StarknetWalletObject); const address = useStoreWallet((s) => s.address); const providerIndex = useFrontendProvider((s) => s.currentFrontendProviderIndex); @@ -48,8 +55,22 @@ export default function AccountPage() { const [fee, setFee] = useState(null); const [alias, setAlias] = useState(""); const [editing, setEditing] = useState(false); + // Convert flow: quote fetched first, then the user confirms the private swap. + const [convertQuote, setConvertQuote] = useState(null); + const [converting, setConverting] = useState(false); + // The wallet request stays live for the whole proving run (minutes); the + // Cancel-wait escape hatch only appears after a generous delay so a slow + // proof is never mistaken for a failure worth retrying. + const [waitLong, setWaitLong] = useState(false); + const waitTimer = useRef | null>(null); + // Convert owns its pair and amount so it never borrows the Shield form's. + const [swapAsset, setSwapAsset] = useState("STRK"); + const [swapAmount, setSwapAmount] = useState(""); const escrowReady = !isZeroAddress(escrowAddressForIndex(providerIndex)); + const swapNetworkReady = + providerIndex in Strk20Networks && !isZeroAddress(poolAddressForIndex(providerIndex)); + const swapBuyToken: ListingToken = swapAsset === "STRK" ? "USDC" : "STRK"; // STRK only: seed the input with ~2 pool fees so fees for later private ops are covered. const suggestedAmount = useMemo(() => { if (asset !== "STRK" || fee === null || fee <= 0n) return null; @@ -62,6 +83,7 @@ export default function AccountPage() { let cancelled = false; setAmountTouched(false); setFee(null); + setConvertQuote(null); poolFeeAmount(myFrontendProviders[providerIndex], poolAddressForIndex(providerIndex)).then((value) => { if (!cancelled) setFee(value); }); @@ -77,6 +99,11 @@ export default function AccountPage() { if (asset === "USDC") setAmount(""); }, [asset]); + // A quote is only valid for the pair it was fetched for. + useEffect(() => { + setConvertQuote(null); + }, [swapAsset]); + useEffect(() => { if (amountTouched || !suggestedAmount) return; setAmount(suggestedAmount); @@ -121,19 +148,72 @@ export default function AccountPage() { setEditing(false); } - function parseAmountWei(): bigint | null { + function parseWholeAmount(value: string, token: ListingToken): bigint | null { try { - const amountWei = priceToWei(amount, asset); + const amountWei = priceToWei(value, token); return amountWei > 0n ? amountWei : null; } catch { return null; } } + function parseAmountWei(): bigint | null { + return parseWholeAmount(amount, asset); + } + + function beginWait() { + setWaitLong(false); + waitTimer.current = setTimeout(() => setWaitLong(true), 45_000); + } + + function endWait() { + if (waitTimer.current) clearTimeout(waitTimer.current); + waitTimer.current = null; + setWaitLong(false); + } + + function onCancelWait() { + forceReleasePrivateOp(); + endWait(); + setBusy(false); + setConverting(false); + setNote("Wait cancelled. The operation may still land — check Show balance before repeating it."); + } + + // The wallet's promise can lag minutes behind a transaction that already + // landed. Whichever signal arrives first wins: the op settling on its own, or + // the chain showing the public balance move. A late op rejection after the + // chain confirmed is ignored — the chain is the source of truth. + async function settleOrChainConfirm( + op: Promise, + landed: Promise | null, + chainNote: string, + settledNote: (result: T) => string, + ) { + let opError: unknown = null; + const settled = op.then( + () => "op" as const, + (err: unknown) => { + opError = err; + return "op" as const; + }, + ); + const never = new Promise(() => {}); + const winner = await Promise.race([settled, (landed ?? never).then(() => "chain" as const)]); + if (winner === "chain") { + forceReleasePrivateOp(); + setNote(chainNote); + } else if (opError) { + throw opError; + } else { + setNote(settledNote(await op)); + } + } + async function onShield() { setError(""); setNote(""); - if (!account) { + if (!account || !address) { setError("Reconnect, then retry Shield."); return; } @@ -142,10 +222,22 @@ export default function AccountPage() { setError(`Enter a whole number of ${asset}.`); return; } + const token = tokenAddressForListing(asset, providerIndex); + const provider = myFrontendProviders[providerIndex]; + beginWait(); setBusy(true); + let done = false; try { - await shieldTokens({ account, token: tokenAddressForListing(asset, providerIndex), amountWei }); - setNote(`Shielded ${amount} ${asset}. Refresh to see it.`); + const before = await publicTokenBalance(provider, token, address); + const op = shieldTokens({ account, token, amountWei }); + await settleOrChainConfirm( + op, + before !== null + ? waitForPublicBalanceMove(provider, token, address, before, "down", 300_000, () => done) + : null, + `Shielded ${amount} ${asset} — confirmed on-chain. Refresh to see it.`, + () => `Shielded ${amount} ${asset}. Refresh to see it.`, + ); } catch (err: unknown) { console.error("[GhostDeal] shield failed:", err); if (isPrivateTokensOffError(err)) { @@ -156,6 +248,8 @@ export default function AccountPage() { setError(friendlyPrivateError(err, "Shield failed.")); } } finally { + done = true; + endWait(); setBusy(false); } } @@ -163,7 +257,7 @@ export default function AccountPage() { async function onUnshield() { setError(""); setNote(""); - if (!account) { + if (!account || !address) { setError("Reconnect, then retry."); return; } @@ -172,14 +266,22 @@ export default function AccountPage() { setError(`Enter a whole number of ${asset}.`); return; } + const token = tokenAddressForListing(asset, providerIndex); + const provider = myFrontendProviders[providerIndex]; + beginWait(); setBusy(true); + let done = false; try { - const hash = await unshieldTokens({ - account, - token: tokenAddressForListing(asset, providerIndex), - amountWei, - }); - setNote(`Unshielded ${asset}. Tx ${hash.slice(0, 10)}…`); + const before = await publicTokenBalance(provider, token, address); + const op = unshieldTokens({ account, token, amountWei }); + await settleOrChainConfirm( + op, + before !== null + ? waitForPublicBalanceMove(provider, token, address, before, "up", 300_000, () => done) + : null, + `Unshielded ${asset} — confirmed on-chain.`, + (hash) => `Unshielded ${asset}. Tx ${hash.slice(0, 10)}…`, + ); } catch (err: unknown) { console.error("[GhostDeal] unshield failed:", err); if (isPrivateTokensOffError(err)) { @@ -190,10 +292,96 @@ export default function AccountPage() { setError(friendlyPrivateError(err, "Unshield failed.")); } } finally { + done = true; + endWait(); setBusy(false); } } + // Convert step 1: quote the pair off-chain (no wallet prompt, no consent). + async function onConvertQuote() { + setError(""); + setNote(""); + if (!account) { + setError("Reconnect, then retry Convert."); + return; + } + if (!swapNetworkReady) { + setError("Private swaps run on mainnet."); + return; + } + const amountWei = parseWholeAmount(swapAmount, swapAsset); + if (!amountWei) { + setError(`Enter a whole number of ${swapAsset} to convert.`); + return; + } + // The displayed balance only matches when converting the chip-selected asset. + if (swapAsset === asset && balance !== null && balance < amountWei) { + setError(`Not enough shielded ${swapAsset}: you have ${formatWei(balance, swapAsset)}.`); + return; + } + setConverting(true); + try { + const quote = await fetchSwapQuote({ + providerIndex, + sellToken: tokenAddressForListing(swapAsset, providerIndex), + buyToken: tokenAddressForListing(swapBuyToken, providerIndex), + sellAmountWei: amountWei, + takerAddress: account.address, + }); + if (!quote) { + setError(`No conversion route ${swapAsset} → ${swapBuyToken} on this network right now.`); + return; + } + setConvertQuote(quote); + } catch (err: unknown) { + console.error("[GhostDeal] convert quote failed:", err); + setError("Could not fetch a conversion quote. Try again."); + } finally { + setConverting(false); + } + } + + // Convert step 2: build the private batch and hand it to the wallet to prove + // and submit. Same relay as Pay; the pool fee is charged in shielded STRK. + async function onConvertConfirm() { + setError(""); + setNote(""); + if (!account || !wallet) { + setError("Connect a wallet first."); + return; + } + if (!convertQuote) return; + beginWait(); + setConverting(true); + try { + await requireWalletApi0103(wallet); + const actions = await planPrivateSwap({ + providerIndex, + quote: convertQuote, + takerAddress: account.address, + }); + const hash = await submitPrivateSwapBatch({ account, actions }); + setConvertQuote(null); + setNote( + `Converting ${swapAmount} ${swapAsset} → ~${formatWei(convertQuote.buyAmount, swapBuyToken)} ${swapBuyToken}. ` + + `The new shielded ${swapBuyToken} is spendable in about a minute (~10 blocks). Tx ${hash.slice(0, 10)}…`, + ); + } catch (err: unknown) { + console.error("[GhostDeal] private swap failed:", err); + if (isPrivateTokensOffError(err)) { + setError(friendlyPrivateError(err, "Convert failed.")); + } else if (/timed out|stopped responding/i.test(privateErrorText(err))) { + setError("The wallet timed out while proving. The conversion can still land; refresh in a bit."); + } else { + setError(friendlyPrivateError(err, "Convert failed.")); + } + } finally { + endWait(); + setConverting(false); + } + } + return ( <>
@@ -286,10 +474,19 @@ export default function AccountPage() { {!escrowReady ?

Escrow not deployed on this network.

: null} {note ?

{note}

: null} - {busy ? ( -

- Waiting on wallet… -

+ {busy || converting ? ( + <> +

+ {waitLong + ? "Still proving… private proofs can take a few minutes." + : "Waiting on wallet… proving can take a couple of minutes."} +

+ {waitLong ? ( + + ) : null} + ) : null}

@@ -329,6 +526,61 @@ export default function AccountPage() { Unshield is public. {fee !== null ? ` Fee ${formatWei(fee)} STRK per op.` : ""}

+ +

+ Convert inside the pool via AVNU. Pool fee charged in STRK. +

+ {!swapNetworkReady ?

Private swaps run on mainnet.

: null} +
+ {(["STRK", "USDC"] as ListingToken[]).map((token) => ( + + ))} +
+ {convertQuote ? ( +
+ + Convert {swapAmount} {swapAsset} → receive ~{formatWei(convertQuote.buyAmount, swapBuyToken)} {swapBuyToken} ({SWAP_SLIPPAGE * 100}% max slippage) + + + +
+ ) : ( +
{ + e.preventDefault(); + onConvertQuote(); + }} + > +
+ setSwapAmount(e.target.value)} + placeholder="amount" + inputMode="numeric" + aria-label={`${swapAsset} amount to convert`} + style={{ width: 90 }} + /> + + {swapAsset} + + +
+
+ )} ); } diff --git a/src/lib/avnu.ts b/src/lib/avnu.ts new file mode 100644 index 0000000..0634ecd --- /dev/null +++ b/src/lib/avnu.ts @@ -0,0 +1,117 @@ +import { getQuotes, quoteToCalls, type Quote } from "@avnu/avnu-sdk"; + +export type { Quote }; +import { + transaction, + type Call, + type STRK20_ACTION, + type WalletAccountV6, +} from "starknet"; +import { felt, singlePrivateOp } from "@/lib/escrow"; + +// AVNU's public swap API per network, indexed like myFrontendProviders. +// Sepolia currently returns no routes for native USDC, so conversion runs on +// mainnet first; the code is network-generic. +const AVNU_BASE_URLS: Record = { + 0: "https://starknet.api.avnu.fi", + 2: "https://sepolia.api.avnu.fi", +}; + +export const SWAP_SLIPPAGE = 0.01; + +// Off-chain quote for converting a shielded amount. Null when AVNU lists no +// route for the pair on this network. +export async function fetchSwapQuote(input: { + providerIndex: number; + sellToken: string; + buyToken: string; + sellAmountWei: bigint; + takerAddress: string; +}): Promise { + const baseUrl = AVNU_BASE_URLS[input.providerIndex]; + if (!baseUrl) return null; + try { + const quotes = await getQuotes( + { + sellTokenAddress: input.sellToken, + buyTokenAddress: input.buyToken, + sellAmount: input.sellAmountWei, + takerAddress: input.takerAddress, + size: 1, + }, + { baseUrl }, + ); + return quotes[0] ?? null; + } catch (err: unknown) { + console.warn("[avnu] quote failed:", err); + return null; + } +} + +// The private swap batch, mirroring AVNU's own buildStrk20Actions minus the +// paymaster fee withdraw (the wallet relay charges the pool fee in STRK like +// every other private op): the pool pulls the shielded sell amount into AVNU's +// executor, the executor runs the route, and the buy amount comes back as an +// open note credited to the taker. +export function buildAvnuSwapActions(input: { + quote: Quote; + calls: Call[]; + executorAddress: string; + takerAddress: string; +}): STRK20_ACTION[] { + return [ + { + type: "withdraw", + token: felt(input.quote.sellTokenAddress), + amount: felt(input.quote.sellAmount), + recipient: felt(input.executorAddress), + }, + { + type: "transfer", + token: felt(input.quote.buyTokenAddress), + amount: "OPEN", + recipient: felt(input.takerAddress), + }, + { + type: "invoke", + contract: felt(input.executorAddress), + calldata: [ + felt(input.quote.buyTokenAddress), + ...transaction.fromCallsToExecuteCalldata_cairo1(input.calls).map((item) => felt(item)), + "${openNoteIds[0]}", + ], + }, + ]; +} + +// Turns a quote into the concrete private batch via AVNU's build endpoint +// (private mode sets the API's taker to its executor). +export async function planPrivateSwap(input: { + providerIndex: number; + quote: Quote; + takerAddress: string; +}): Promise { + const baseUrl = AVNU_BASE_URLS[input.providerIndex]; + if (!baseUrl) throw new Error("STRK20 is not available on this network."); + const { calls, executorAddress } = await quoteToCalls( + { quoteId: input.quote.quoteId, slippage: SWAP_SLIPPAGE, private: true }, + { baseUrl }, + ); + if (!executorAddress) { + throw new Error("AVNU returned no private-swap executor. Try again in a moment."); + } + return buildAvnuSwapActions({ + quote: input.quote, + calls, + executorAddress, + takerAddress: input.takerAddress, + }); +} + +export async function submitPrivateSwapBatch(input: { + account: WalletAccountV6; + actions: STRK20_ACTION[]; +}): Promise { + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(input.actions)); + return transaction_hash; +} diff --git a/src/lib/escrow.ts b/src/lib/escrow.ts index 7860d33..28d3e4d 100644 --- a/src/lib/escrow.ts +++ b/src/lib/escrow.ts @@ -30,7 +30,7 @@ export const STRK_DECIMALS = 18n; export const USDC_DECIMALS = 6n; // Wallet API felts must be unpadded hex; the literal "0x0" and ${...} placeholders are the only exemptions. -const felt = (value: string | bigint): string => num.toHex(BigInt(value)); +export const felt = (value: string | bigint): string => num.toHex(BigInt(value)); export function isZeroAddress(value: string): boolean { try { @@ -113,24 +113,30 @@ function sameFelt(a: string, b: string): boolean { } } -// strk20InvokeTransaction can stall forever when the proving relay wedges; a -// submit that times out may still land, so callers must poll the chain. -const PRIVATE_SUBMIT_TIMEOUT_MS = 180_000; - -function boundPrivateSubmit(work: Promise): Promise { - let timer: ReturnType | undefined; - const expiry = new Promise((_, reject) => { - timer = setTimeout( - () => - reject( - new Error( - "The wallet stopped responding while proving. Wait a few minutes and check the deal status on the chain before retrying. The transaction can still land.", - ), - ), - PRIVATE_SUBMIT_TIMEOUT_MS, - ); - }); - return Promise.race([work, expiry]).finally(() => clearTimeout(timer)); +// One private wallet request at a time, enforced synchronously. The wallet's +// promise settles only when it hands back the hash, and proving can run for +// minutes with the request still live inside the wallet. Auto-rejecting that +// wait re-enabled the UI mid-proof and the natural retry then submitted the +// operation a second time (mainnet 2026-08-30: two identical shields two +// minutes apart), so the wait is unbounded here and an explicit human cancel +// is the only early release. +let privateOpInFlight = false; +export async function singlePrivateOp(work: () => Promise): Promise { + if (privateOpInFlight) { + throw new Error("A private operation is already waiting on your wallet. Let it finish first."); + } + privateOpInFlight = true; + try { + return await work(); + } finally { + privateOpInFlight = false; + } +} + +// Escape hatch for a wedged request, to be called only from an explicit user +// action that has been told the abandoned operation may still land on its own. +export function forceReleasePrivateOp(): void { + privateOpInFlight = false; } // Wallet-mediated read of the user's shielded balance. No viewing key ever @@ -214,7 +220,7 @@ export async function shieldTokens(input: { amount: felt(input.amountWei), }, ]; - const { transaction_hash } = await boundPrivateSubmit(input.account.strk20InvokeTransaction(actions)); + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(actions)); return transaction_hash; } @@ -228,7 +234,7 @@ export async function unshieldTokens(input: { const actions: STRK20_ACTION[] = [ { type: "withdraw", token: felt(input.token), amount: felt(input.amountWei), recipient: felt(input.account.address) }, ]; - const { transaction_hash } = await boundPrivateSubmit(input.account.strk20InvokeTransaction(actions)); + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(actions)); return transaction_hash; } @@ -296,7 +302,7 @@ export async function payListingDeposit(input: { }); // Direct submit: no strk20PrepareInvoke first: it re-triggers balance // permissions and can loop the wallet permission popup. - const { transaction_hash } = await boundPrivateSubmit(input.account.strk20InvokeTransaction(actions)); + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(actions)); return transaction_hash; } @@ -382,7 +388,7 @@ export async function claimEscrowFunds(input: { recipient: input.account.address, token: input.token, }); - const { transaction_hash } = await boundPrivateSubmit(input.account.strk20InvokeTransaction(actions)); + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(actions)); return transaction_hash; } @@ -404,7 +410,7 @@ export async function cancelEscrowFunds(input: { recipient: input.account.address, token: input.token, }); - const { transaction_hash } = await boundPrivateSubmit(input.account.strk20InvokeTransaction(actions)); + const { transaction_hash } = await singlePrivateOp(() => input.account.strk20InvokeTransaction(actions)); return transaction_hash; } @@ -427,3 +433,47 @@ export async function escrowCommitmentToken( return null; } } + +// Plain RPC read of a public ERC20 balance (u256: low, high). No wallet prompt, +// so it is safe to poll while a private request is being proven. +export async function publicTokenBalance( + provider: ProviderInterface, + token: string, + address: string, +): Promise { + try { + const r = await provider.callContract({ + contractAddress: token, + entrypoint: "balance_of", + calldata: [felt(address)], + }); + return BigInt(r[0]) + (BigInt(r[1] ?? "0x0") << 128n); + } catch { + return null; + } +} + +// Poll until the public balance moves in `direction` or the window closes. +// This is the sync signal when the wallet's promise lags behind a transaction +// that already landed: the chain is the source of truth, never the wallet's +// response. +export async function waitForPublicBalanceMove( + provider: ProviderInterface, + token: string, + address: string, + before: bigint, + direction: "down" | "up", + timeoutMs = 300_000, + shouldStop: () => boolean = () => false, +): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + if (shouldStop()) return false; + const now = await publicTokenBalance(provider, token, address); + if (now !== null && (direction === "down" ? now < before : now > before)) { + return true; + } + if (Date.now() >= deadline) return false; + await new Promise((resolve) => setTimeout(resolve, 8_000)); + } +} diff --git a/yarn.lock b/yarn.lock index 8a8b3b4..b40b96b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,16 @@ resolved "https://registry.yarnpkg.com/@argent/x-ui/-/x-ui-1.109.1.tgz#585671bb630f050c87cf77732f5f22ccec0b8e8f" integrity sha512-AGPN1YC87EQK/byUYuRUrYR57/NYB6wQjoibUf/OnRYepWWW83+VgdR0GVRYLHsD0CT73VpY6iSFLqrY61LSDg== +"@avnu/avnu-sdk@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@avnu/avnu-sdk/-/avnu-sdk-4.2.0.tgz#0aef92e272b18832dbbfc98bf2f5c768edd0a9f1" + integrity sha512-7oEf+BavAhesZgFlwkOBL8i2JBx92uhJcBm8SfzefkFkQoM3IwAUsrl6vsKxua9STL5Nzc8nDXpxRVFLkV3Jug== + dependencies: + dayjs "^1.11.19" + moment "^2.30.1" + qs "^6.14.1" + zod "^4.3.6" + "@babel/code-frame@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" @@ -3120,6 +3130,11 @@ dayjs@1.11.13: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== +dayjs@^1.11.19: + version "1.11.23" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.23.tgz#b0a363506dde5f36cf5075e42ebe8115165a8c79" + integrity sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ== + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3630,7 +3645,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -ethers@^6.13.5: +ethers@^6.13.5, ethers@^6.15.0: version "6.17.0" resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.17.0.tgz#ad85ee1fc45fe2d95cb1e21c4e3ff9f7f1b093cc" integrity sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang== @@ -4551,6 +4566,11 @@ mipd@^0.0.7: resolved "https://registry.yarnpkg.com/mipd/-/mipd-0.0.7.tgz#bb5559e21fa18dc3d9fe1c08902ef14b7ce32fd9" integrity sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg== +moment@^2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -4996,6 +5016,14 @@ qrcode@1.5.3: pngjs "^5.0.0" yargs "^15.3.1" +qs@^6.14.1: + version "6.16.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" + integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5346,7 +5374,7 @@ side-channel-weakmap@^1.0.2: object-inspect "^1.13.3" side-channel-map "^1.0.1" -side-channel@^1.1.0: +side-channel@^1.1.0, side-channel@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== @@ -6041,6 +6069,11 @@ zod@^3.25.76: resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== +zod@^4.3.6: + version "4.5.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.5.4.tgz#e215c62420c528dd7951e31fb52c5438f1fd184a" + integrity sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA== + zustand@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.3.tgz#b323435b73d06b2512e93c77239634374b0e407f"