@b3dotfun/sdk 0.1.70-alpha.16 → 0.1.70-alpha.18

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.
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const anyspend_1 = require("../../../anyspend");
6
6
  const react_1 = require("../../../global-account/react");
7
7
  const cn_1 = require("../../../shared/utils/cn");
8
+ const number_1 = require("../../../shared/utils/number");
8
9
  const react_2 = require("@web3icons/react");
9
10
  const lucide_react_1 = require("lucide-react");
10
11
  const react_3 = require("react");
@@ -228,7 +229,9 @@ function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentT
228
229
  "anyspend-deposit-option-button anyspend-deposit-crypto-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left shadow-sm transition-all", children: [(0, jsx_runtime_1.jsx)("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content", children: (0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [(0, jsx_runtime_1.jsx)("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Deposit Crypto" }), (0, jsx_runtime_1.jsx)("p", { className: depositClasses?.optionDescription ||
229
230
  "anyspend-deposit-option-description text-as-secondary text-xs", children: "Swap from any token on any chain" })] }) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), (0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.divider || "anyspend-deposit-divider flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: depositClasses?.dividerLine || "bg-as-stroke h-px flex-1" }), (0, jsx_runtime_1.jsx)("span", { className: depositClasses?.dividerText || "anyspend-deposit-divider-text text-as-secondary text-sm", children: "More options" }), (0, jsx_runtime_1.jsx)("div", { className: depositClasses?.dividerLine || "bg-as-stroke h-px flex-1" })] }), (0, jsx_runtime_1.jsxs)("button", { onClick: handleSelectQrDeposit, className: depositClasses?.qrButton ||
230
231
  "anyspend-deposit-option-button anyspend-deposit-qr-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left shadow-sm transition-all", children: [(0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(QrCodeIcon_1.QrCodeIcon, { className: depositClasses?.optionIcon || "anyspend-deposit-option-icon h-10 w-10" }), (0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [(0, jsx_runtime_1.jsx)("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Deposit with QR Code" }), (0, jsx_runtime_1.jsx)("p", { className: depositClasses?.optionDescription ||
231
- "anyspend-deposit-option-description text-as-secondary text-xs", children: "Send tokens directly to deposit address" })] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), showFiatOption && ((0, jsx_runtime_1.jsxs)("button", { onClick: handleSelectFiat, className: depositClasses?.fiatButton ||
232
+ "anyspend-deposit-option-description text-as-secondary text-xs", children: pureTransferOnly
233
+ ? "Send tokens directly to deposit address"
234
+ : "Send any token — we'll convert it for you" })] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), showFiatOption && ((0, jsx_runtime_1.jsxs)("button", { onClick: handleSelectFiat, className: depositClasses?.fiatButton ||
232
235
  "anyspend-deposit-option-button anyspend-deposit-fiat-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left transition-all", children: [(0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(CreditCardIcon_1.CreditCardIcon, { className: depositClasses?.optionIcon || "anyspend-deposit-option-icon h-10 w-10" }), (0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [(0, jsx_runtime_1.jsx)("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Fund with Fiat" }), (0, jsx_runtime_1.jsx)("p", { className: depositClasses?.optionDescription ||
233
236
  "anyspend-deposit-option-description text-as-secondary text-xs", children: "Pay with card or bank transfer" })] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }))] }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "mt-2" } })] })] }));
234
237
  }
@@ -245,10 +248,21 @@ function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentT
245
248
  // `KNOWN_FUNDING_TOKENS` key — so `hasKnownDestination` is always true and this
246
249
  // branch never fires for them. The guard is defensive for FUTURE callers that pass
247
250
  // an unknown token together with `pureTransferOnly=true`.
251
+ // Only surface the fixed destination amount on the QR path when its decimals are trustworthy
252
+ // (a known funding token or a resolved API lookup) — otherwise `destinationToken.decimals`
253
+ // is the generic 18 fallback and formatting a 6-decimal token's wei against it would be off by
254
+ // 10^12. When unresolved we still show the destination token/chain, just without an amount.
255
+ const destinationDecimalsResolved = hasKnownDestination || !!destinationTokenData;
256
+ const destinationAmountDisplay = destinationTokenAmount &&
257
+ /^\d+$/.test(destinationTokenAmount) &&
258
+ BigInt(destinationTokenAmount) > 0n &&
259
+ destinationDecimalsResolved
260
+ ? (0, number_1.formatTokenAmount)(BigInt(destinationTokenAmount), destinationToken.decimals, 6, false)
261
+ : undefined;
248
262
  if (pureTransferOnly && !hasKnownDestination && !destinationTokenData && isDestinationTokenLoading) {
249
263
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("anyspend-deposit anyspend-deposit-qr-loading font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6", mode === "page" && "border-as-border-secondary overflow-hidden rounded-2xl border shadow-xl"), children: (0, jsx_runtime_1.jsxs)("div", { className: "anyspend-deposit-qr-loading-content flex flex-col items-center justify-center gap-4 py-12", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-8 w-8 rounded-full" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-40" })] }) }));
250
264
  }
251
- return ((0, jsx_runtime_1.jsx)(QRDeposit_1.QRDeposit, { mode: mode, recipientAddress: recipientAddress, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, pureTransferOnly: pureTransferOnly, depositContractConfig: depositContractConfig, onBack: handleBack, onClose: onClose ?? handleBack, onSuccess: handleQRDepositSuccess, classes: classes?.qrDeposit }));
265
+ return ((0, jsx_runtime_1.jsx)(QRDeposit_1.QRDeposit, { mode: mode, recipientAddress: recipientAddress, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, destinationAmountDisplay: destinationAmountDisplay, pureTransferOnly: pureTransferOnly, depositContractConfig: depositContractConfig, onBack: handleBack, onClose: onClose ?? handleBack, onSuccess: handleQRDepositSuccess, classes: classes?.qrDeposit }));
252
266
  }
253
267
  // Deposit view
254
268
  return ((0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.form || "anyspend-deposit anyspend-deposit-form relative", children: [shouldShowChainSelection && ((0, jsx_runtime_1.jsxs)("button", { onClick: handleBack, className: depositClasses?.backButton ||
@@ -14,6 +14,14 @@ export interface QRDepositProps {
14
14
  destinationToken: components["schemas"]["Token"];
15
15
  /** The destination chain ID */
16
16
  destinationChainId: number;
17
+ /**
18
+ * Human-formatted destination amount (e.g. "9.365547"), shown as an APPROXIMATE
19
+ * target in swap mode only. The QR deposit path accepts an arbitrary sent amount,
20
+ * so this is never a guaranteed receive figure — the UI frames it as "any amount
21
+ * works". Omitted when no fixed amount is set or the destination decimals are
22
+ * unresolved (to avoid a mis-scaled number). Ignored in pure-transfer mode.
23
+ */
24
+ destinationAmountDisplay?: string;
17
25
  /**
18
26
  * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
19
27
  * the user's currently selected source token/chain, so the exact token the user
@@ -54,4 +62,4 @@ export interface QRDepositProps {
54
62
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
55
63
  * />
56
64
  */
57
- export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, destinationAmountDisplay, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
@@ -42,7 +42,7 @@ const DEFAULT_ETH_ON_BASE = {
42
42
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
43
43
  * />
44
44
  */
45
- function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly = false, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }) {
45
+ function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, destinationAmountDisplay, pureTransferOnly = false, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }) {
46
46
  const [copied, setCopied] = (0, react_2.useState)(false);
47
47
  const [orderId, setOrderId] = (0, react_2.useState)();
48
48
  const [globalAddress, setGlobalAddress] = (0, react_2.useState)();
@@ -174,8 +174,8 @@ function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenP
174
174
  }
175
175
  return ((0, jsx_runtime_1.jsx)("div", { className: classes?.container ||
176
176
  (0, cn_1.cn)("anyspend-container anyspend-qr-deposit font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6", mode === "page" && "border-as-border-secondary overflow-hidden rounded-2xl border shadow-xl"), children: (0, jsx_runtime_1.jsxs)("div", { className: classes?.content || "anyspend-qr-deposit-content flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: classes?.header || "anyspend-qr-header flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("button", { onClick: handleBack, className: classes?.backButton || "anyspend-qr-back-button text-as-secondary hover:text-as-primary", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), (0, jsx_runtime_1.jsx)("h2", { className: classes?.title || "anyspend-qr-title text-as-primary text-base font-semibold", children: "Deposit" }), onClose ? ((0, jsx_runtime_1.jsx)("button", { onClick: handleClose, className: classes?.closeButton || "anyspend-qr-close-button text-as-secondary hover:text-as-primary", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-5" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.tokenSelectorContainer || "anyspend-qr-token-selector flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)("label", { className: classes?.tokenSelectorLabel || "anyspend-qr-token-label text-as-secondary text-sm", children: "Send" }), (0, jsx_runtime_1.jsx)(relay_kit_ui_1.TokenSelector, { chainIdsFilter: (0, anyspend_1.getAvailableChainIds)("from"), context: "from", fromChainWalletVMSupported: true, isValidAddress: true, lockedChainIds: (0, anyspend_1.getAvailableChainIds)("from"), multiWalletSupportEnabled: true, onAnalyticEvent: undefined, setToken: handleTokenSelect, supportedWalletVMs: ["evm"], token: undefined, trigger: (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "outline", role: "combobox", className: classes?.tokenSelectorTrigger ||
177
- "anyspend-qr-token-trigger border-as-stroke bg-as-surface-secondary flex h-auto w-full items-center justify-between gap-2 rounded-xl border px-3 py-2.5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [sourceToken.metadata?.logoURI ? ((0, jsx_runtime_1.jsx)(ChainTokenIcon_1.ChainTokenIcon, { chainUrl: anyspend_1.ALL_CHAINS[sourceChainId]?.logoUrl, tokenUrl: sourceToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : ((0, jsx_runtime_1.jsx)("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary font-semibold", children: sourceToken.symbol }), (0, jsx_runtime_1.jsx)("div", { className: "text-as-primary/70 text-xs", children: anyspend_1.ALL_CHAINS[sourceChainId]?.name ?? "Unknown" })] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-70" })] }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.qrContent || "anyspend-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2", children: [(0, jsx_runtime_1.jsx)("div", { className: classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2", children: (0, jsx_runtime_1.jsx)(qrcode_react_1.QRCodeSVG, { value: qrValue, size: 120, level: "M", marginSize: 0 }) }), (0, jsx_runtime_1.jsxs)("span", { className: classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs", children: ["SCAN WITH ", (0, jsx_runtime_1.jsx)("span", { className: "inline-block", children: "\uD83E\uDD8A" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.addressContainer || "anyspend-qr-address-container flex flex-1 flex-col gap-1", children: [(0, jsx_runtime_1.jsx)("span", { className: classes?.addressLabel || "anyspend-qr-address-label text-as-secondary text-sm", children: "Deposit address:" }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.addressRow || "anyspend-qr-address-row flex items-start gap-1", children: [(0, jsx_runtime_1.jsx)("span", { className: classes?.address || "anyspend-qr-address text-as-primary break-all font-mono text-sm leading-relaxed", children: displayAddress }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCopyAddress, className: classes?.addressCopyIcon ||
178
- "anyspend-qr-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0", children: copied ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-4 w-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { className: "h-4 w-4" }) })] })] })] }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: effectiveDestinationChainId }), (0, jsx_runtime_1.jsxs)(WarningText_1.WarningText, { children: ["Only send ", sourceToken.symbol, " on ", anyspend_1.ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", ". Other tokens will not be converted."] }), isPureTransfer && isWatchingTransfer && ((0, jsx_runtime_1.jsxs)("div", { className: classes?.watchingIndicator ||
177
+ "anyspend-qr-token-trigger border-as-stroke bg-as-surface-secondary flex h-auto w-full items-center justify-between gap-2 rounded-xl border px-3 py-2.5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [sourceToken.metadata?.logoURI ? ((0, jsx_runtime_1.jsx)(ChainTokenIcon_1.ChainTokenIcon, { chainUrl: anyspend_1.ALL_CHAINS[sourceChainId]?.logoUrl, tokenUrl: sourceToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : ((0, jsx_runtime_1.jsx)("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary font-semibold", children: sourceToken.symbol }), (0, jsx_runtime_1.jsx)("div", { className: "text-as-primary/70 text-xs", children: anyspend_1.ALL_CHAINS[sourceChainId]?.name ?? "Unknown" })] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-70" })] }) })] }), !isPureTransfer && ((0, jsx_runtime_1.jsxs)("div", { className: "anyspend-qr-receive flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)("label", { className: "anyspend-qr-receive-label text-as-secondary text-sm", children: "Receive" }), (0, jsx_runtime_1.jsxs)("div", { className: "anyspend-qr-receive-row border-as-stroke bg-as-surface-secondary flex w-full items-center gap-2 rounded-xl border px-3 py-2.5", children: [effectiveDestinationToken.metadata?.logoURI ? ((0, jsx_runtime_1.jsx)(ChainTokenIcon_1.ChainTokenIcon, { chainUrl: anyspend_1.ALL_CHAINS[effectiveDestinationChainId]?.logoUrl, tokenUrl: effectiveDestinationToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : ((0, jsx_runtime_1.jsx)("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary font-semibold", children: effectiveDestinationToken.symbol || "Token" }), (0, jsx_runtime_1.jsx)("div", { className: "text-as-primary/70 text-xs", children: anyspend_1.ALL_CHAINS[effectiveDestinationChainId]?.name ?? "Unknown" })] })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: classes?.qrContent || "anyspend-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2", children: [(0, jsx_runtime_1.jsx)("div", { className: classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2", children: (0, jsx_runtime_1.jsx)(qrcode_react_1.QRCodeSVG, { value: qrValue, size: 120, level: "M", marginSize: 0 }) }), (0, jsx_runtime_1.jsxs)("span", { className: classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs", children: ["SCAN WITH ", (0, jsx_runtime_1.jsx)("span", { className: "inline-block", children: "\uD83E\uDD8A" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.addressContainer || "anyspend-qr-address-container flex flex-1 flex-col gap-1", children: [(0, jsx_runtime_1.jsx)("span", { className: classes?.addressLabel || "anyspend-qr-address-label text-as-secondary text-sm", children: "Deposit address:" }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.addressRow || "anyspend-qr-address-row flex items-start gap-1", children: [(0, jsx_runtime_1.jsx)("span", { className: classes?.address || "anyspend-qr-address text-as-primary break-all font-mono text-sm leading-relaxed", children: displayAddress }), (0, jsx_runtime_1.jsx)("button", { onClick: handleCopyAddress, className: classes?.addressCopyIcon ||
178
+ "anyspend-qr-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0", children: copied ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-4 w-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { className: "h-4 w-4" }) })] })] })] }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: effectiveDestinationChainId }), isPureTransfer ? ((0, jsx_runtime_1.jsxs)(WarningText_1.WarningText, { children: ["Only send ", sourceToken.symbol, " on ", anyspend_1.ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", ". Other tokens will not be converted."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(WarningText_1.WarningText, { children: ["Only send ", sourceToken.symbol, " on ", anyspend_1.ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", " to this address \u2014 it will be converted to ", effectiveDestinationToken.symbol || "the destination token", " on", " ", anyspend_1.ALL_CHAINS[effectiveDestinationChainId]?.name ?? "the destination chain", "."] }), destinationAmountDisplay && ((0, jsx_runtime_1.jsxs)("p", { className: "anyspend-qr-amount-hint text-as-secondary text-xs", children: ["Target \u2248 ", destinationAmountDisplay, " ", effectiveDestinationToken.symbol || "tokens", ". Any amount of", " ", sourceToken.symbol || "tokens", " works \u2014 you'll receive whatever your deposit converts to."] }))] })), isPureTransfer && isWatchingTransfer && ((0, jsx_runtime_1.jsxs)("div", { className: classes?.watchingIndicator ||
179
179
  "anyspend-qr-watching flex items-center justify-center gap-2 rounded-lg bg-blue-500/10 p-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "h-4 w-4 animate-spin text-blue-500" }), (0, jsx_runtime_1.jsx)("span", { className: "text-sm text-blue-500", children: "Watching for incoming transfer..." })] })), (0, jsx_runtime_1.jsx)("button", { onClick: handleCopyAddress, className: classes?.copyButton ||
180
180
  "anyspend-qr-copy-button flex w-full items-center justify-center gap-2 rounded-xl bg-blue-500 py-3.5 font-medium text-white transition-all hover:bg-blue-600", children: "Copy deposit address" })] }) }));
181
181
  }
@@ -5,6 +5,7 @@ export interface UnifiedTransactionParams {
5
5
  }
6
6
  export declare function useUnifiedChainSwitchAndExecute(): {
7
7
  switchChainAndExecute: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
8
+ switchChainAndExecuteWithGlobalAccount: (targetChainId: number, params: UnifiedTransactionParams, expectedGlobalAccountAddress?: string) => Promise<string | undefined>;
8
9
  switchChainAndExecuteWithEOA: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
9
10
  isSwitchingOrExecuting: boolean;
10
11
  isActiveSmartWallet: boolean | undefined;
@@ -15,13 +15,11 @@ const thirdweb_2 = require("thirdweb");
15
15
  const viem_1 = require("viem");
16
16
  const wagmi_1 = require("wagmi");
17
17
  const B3ConfigProvider_1 = require("../components/B3Provider/B3ConfigProvider");
18
- const useB3Account_1 = require("../components/B3Provider/useB3Account");
19
18
  const useAccountWallet_1 = require("./useAccountWallet");
20
19
  function useUnifiedChainSwitchAndExecute() {
21
20
  const { switchChainAsync } = (0, wagmi_1.useSwitchChain)();
22
21
  const [isSwitchingOrExecuting, setIsSwitchingOrExecuting] = (0, react_2.useState)(false);
23
- const { isActiveSmartWallet, isActiveEOAWallet, connectedEOAWallet } = (0, useAccountWallet_1.useAccountWallet)();
24
- const aaAccount = (0, useB3Account_1.useB3Account)();
22
+ const { isActiveSmartWallet, isActiveEOAWallet, connectedSmartWallet, connectedEOAWallet } = (0, useAccountWallet_1.useAccountWallet)();
25
23
  // From context, not process.env: partnerId is hashed into the server intent key and must match the ecosystem wallet.
26
24
  const { partnerId } = (0, B3ConfigProvider_1.useB3Config)();
27
25
  // Handle EOA wallet chain switch and execute transaction
@@ -100,10 +98,18 @@ function useUnifiedChainSwitchAndExecute() {
100
98
  setIsSwitchingOrExecuting(false);
101
99
  }
102
100
  }, [connectedEOAWallet, switchChainAsync]);
103
- // Handle AA wallet transaction (no chain switch needed for AA)
104
- const handleAASendTransaction = (0, react_2.useCallback)(async (targetChainId, params) => {
105
- if (!aaAccount) {
106
- react_1.toast.error("Smart wallet not connected");
101
+ // Handle Global Account transaction (no chain switch needed for AA).
102
+ // Resolve the signer from the connected ecosystem wallet rather than the active
103
+ // wallet so callers can explicitly require the Global Account.
104
+ const handleGlobalAccountSendTransaction = (0, react_2.useCallback)(async (targetChainId, params, expectedGlobalAccountAddress) => {
105
+ const globalAccount = connectedSmartWallet?.getAccount();
106
+ if (!globalAccount) {
107
+ react_1.toast.error("Global Account not connected");
108
+ return;
109
+ }
110
+ if (expectedGlobalAccountAddress &&
111
+ globalAccount.address.toLowerCase() !== expectedGlobalAccountAddress.toLowerCase()) {
112
+ react_1.toast.error("Connected Global Account does not match the authenticated account");
107
113
  return;
108
114
  }
109
115
  try {
@@ -137,7 +143,7 @@ function useUnifiedChainSwitchAndExecute() {
137
143
  react_1.toast.info("Sending transaction…");
138
144
  const start = performance.now();
139
145
  const sendTxResponse = await (0, thirdweb_2.sendTransaction)({
140
- account: aaAccount,
146
+ account: globalAccount,
141
147
  transaction,
142
148
  });
143
149
  const end = performance.now();
@@ -153,17 +159,18 @@ function useUnifiedChainSwitchAndExecute() {
153
159
  finally {
154
160
  setIsSwitchingOrExecuting(false);
155
161
  }
156
- }, [aaAccount, partnerId]);
162
+ }, [connectedSmartWallet, partnerId]);
157
163
  // Unified switch chain and execute function
158
164
  const switchChainAndExecute = (0, react_2.useCallback)(async (targetChainId, params) => {
159
165
  // Check which wallet type is active
160
166
  if (isActiveSmartWallet) {
161
- return handleAASendTransaction(targetChainId, params);
167
+ return handleGlobalAccountSendTransaction(targetChainId, params);
162
168
  }
163
169
  return handleEOASwitchChainAndSendTransaction(targetChainId, params);
164
- }, [isActiveSmartWallet, handleAASendTransaction, handleEOASwitchChainAndSendTransaction]);
170
+ }, [isActiveSmartWallet, handleGlobalAccountSendTransaction, handleEOASwitchChainAndSendTransaction]);
165
171
  return {
166
172
  switchChainAndExecute,
173
+ switchChainAndExecuteWithGlobalAccount: handleGlobalAccountSendTransaction,
167
174
  switchChainAndExecuteWithEOA: handleEOASwitchChainAndSendTransaction,
168
175
  isSwitchingOrExecuting,
169
176
  isActiveSmartWallet,
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { USDC_BASE } from "../../../anyspend/index.js";
3
3
  import { Skeleton, useAccountWallet, useSimBalance, useTokenData } from "../../../global-account/react/index.js";
4
4
  import { cn } from "../../../shared/utils/cn.js";
5
+ import { formatTokenAmount } from "../../../shared/utils/number.js";
5
6
  import { NetworkArbitrumOne, NetworkBase, NetworkBinanceSmartChain, NetworkEthereum, NetworkOptimism, NetworkPolygonPos, } from "@web3icons/react";
6
7
  import { ChevronRight } from "lucide-react";
7
8
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -225,7 +226,9 @@ export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, p
225
226
  "anyspend-deposit-option-button anyspend-deposit-crypto-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left shadow-sm transition-all", children: [_jsx("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content", children: _jsxs("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [_jsx("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Deposit Crypto" }), _jsx("p", { className: depositClasses?.optionDescription ||
226
227
  "anyspend-deposit-option-description text-as-secondary text-xs", children: "Swap from any token on any chain" })] }) }), _jsx(ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), _jsxs("div", { className: depositClasses?.divider || "anyspend-deposit-divider flex items-center gap-3", children: [_jsx("div", { className: depositClasses?.dividerLine || "bg-as-stroke h-px flex-1" }), _jsx("span", { className: depositClasses?.dividerText || "anyspend-deposit-divider-text text-as-secondary text-sm", children: "More options" }), _jsx("div", { className: depositClasses?.dividerLine || "bg-as-stroke h-px flex-1" })] }), _jsxs("button", { onClick: handleSelectQrDeposit, className: depositClasses?.qrButton ||
227
228
  "anyspend-deposit-option-button anyspend-deposit-qr-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left shadow-sm transition-all", children: [_jsxs("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content flex items-center gap-3", children: [_jsx(QrCodeIcon, { className: depositClasses?.optionIcon || "anyspend-deposit-option-icon h-10 w-10" }), _jsxs("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [_jsx("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Deposit with QR Code" }), _jsx("p", { className: depositClasses?.optionDescription ||
228
- "anyspend-deposit-option-description text-as-secondary text-xs", children: "Send tokens directly to deposit address" })] })] }), _jsx(ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), showFiatOption && (_jsxs("button", { onClick: handleSelectFiat, className: depositClasses?.fiatButton ||
229
+ "anyspend-deposit-option-description text-as-secondary text-xs", children: pureTransferOnly
230
+ ? "Send tokens directly to deposit address"
231
+ : "Send any token — we'll convert it for you" })] })] }), _jsx(ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }), showFiatOption && (_jsxs("button", { onClick: handleSelectFiat, className: depositClasses?.fiatButton ||
229
232
  "anyspend-deposit-option-button anyspend-deposit-fiat-button border-border-primary hover:border-as-brand hover:bg-as-surface-secondary flex w-full items-center justify-between rounded-xl border p-4 text-left transition-all", children: [_jsxs("div", { className: depositClasses?.optionContent || "anyspend-deposit-option-content flex items-center gap-3", children: [_jsx(CreditCardIcon, { className: depositClasses?.optionIcon || "anyspend-deposit-option-icon h-10 w-10" }), _jsxs("div", { className: depositClasses?.optionInfo || "anyspend-deposit-option-info", children: [_jsx("span", { className: depositClasses?.optionTitle || "anyspend-deposit-option-title text-as-primary font-medium", children: "Fund with Fiat" }), _jsx("p", { className: depositClasses?.optionDescription ||
230
233
  "anyspend-deposit-option-description text-as-secondary text-xs", children: "Pay with card or bank transfer" })] })] }), _jsx(ChevronRight, { className: depositClasses?.optionChevron || "anyspend-deposit-option-chevron text-as-secondary h-5 w-5" })] }))] }), _jsx(ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "mt-2" } })] })] }));
231
234
  }
@@ -242,10 +245,21 @@ export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, p
242
245
  // `KNOWN_FUNDING_TOKENS` key — so `hasKnownDestination` is always true and this
243
246
  // branch never fires for them. The guard is defensive for FUTURE callers that pass
244
247
  // an unknown token together with `pureTransferOnly=true`.
248
+ // Only surface the fixed destination amount on the QR path when its decimals are trustworthy
249
+ // (a known funding token or a resolved API lookup) — otherwise `destinationToken.decimals`
250
+ // is the generic 18 fallback and formatting a 6-decimal token's wei against it would be off by
251
+ // 10^12. When unresolved we still show the destination token/chain, just without an amount.
252
+ const destinationDecimalsResolved = hasKnownDestination || !!destinationTokenData;
253
+ const destinationAmountDisplay = destinationTokenAmount &&
254
+ /^\d+$/.test(destinationTokenAmount) &&
255
+ BigInt(destinationTokenAmount) > 0n &&
256
+ destinationDecimalsResolved
257
+ ? formatTokenAmount(BigInt(destinationTokenAmount), destinationToken.decimals, 6, false)
258
+ : undefined;
245
259
  if (pureTransferOnly && !hasKnownDestination && !destinationTokenData && isDestinationTokenLoading) {
246
260
  return (_jsx("div", { className: cn("anyspend-deposit anyspend-deposit-qr-loading font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6", mode === "page" && "border-as-border-secondary overflow-hidden rounded-2xl border shadow-xl"), children: _jsxs("div", { className: "anyspend-deposit-qr-loading-content flex flex-col items-center justify-center gap-4 py-12", children: [_jsx(Skeleton, { className: "h-8 w-8 rounded-full" }), _jsx(Skeleton, { className: "h-4 w-40" })] }) }));
247
261
  }
248
- return (_jsx(QRDeposit, { mode: mode, recipientAddress: recipientAddress, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, pureTransferOnly: pureTransferOnly, depositContractConfig: depositContractConfig, onBack: handleBack, onClose: onClose ?? handleBack, onSuccess: handleQRDepositSuccess, classes: classes?.qrDeposit }));
262
+ return (_jsx(QRDeposit, { mode: mode, recipientAddress: recipientAddress, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, destinationAmountDisplay: destinationAmountDisplay, pureTransferOnly: pureTransferOnly, depositContractConfig: depositContractConfig, onBack: handleBack, onClose: onClose ?? handleBack, onSuccess: handleQRDepositSuccess, classes: classes?.qrDeposit }));
249
263
  }
250
264
  // Deposit view
251
265
  return (_jsxs("div", { className: depositClasses?.form || "anyspend-deposit anyspend-deposit-form relative", children: [shouldShowChainSelection && (_jsxs("button", { onClick: handleBack, className: depositClasses?.backButton ||
@@ -14,6 +14,14 @@ export interface QRDepositProps {
14
14
  destinationToken: components["schemas"]["Token"];
15
15
  /** The destination chain ID */
16
16
  destinationChainId: number;
17
+ /**
18
+ * Human-formatted destination amount (e.g. "9.365547"), shown as an APPROXIMATE
19
+ * target in swap mode only. The QR deposit path accepts an arbitrary sent amount,
20
+ * so this is never a guaranteed receive figure — the UI frames it as "any amount
21
+ * works". Omitted when no fixed amount is set or the destination decimals are
22
+ * unresolved (to avoid a mis-scaled number). Ignored in pure-transfer mode.
23
+ */
24
+ destinationAmountDisplay?: string;
17
25
  /**
18
26
  * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
19
27
  * the user's currently selected source token/chain, so the exact token the user
@@ -54,4 +62,4 @@ export interface QRDepositProps {
54
62
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
55
63
  * />
56
64
  */
57
- export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, destinationAmountDisplay, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ALL_CHAINS, getAvailableChainIds, getPaymentUrl, isSameChainAndToken, ZERO_ADDRESS, } from "../../../anyspend/index.js";
3
3
  import { Button, toast } from "../../../global-account/react/index.js";
4
4
  import { cn } from "../../../shared/utils/cn.js";
@@ -39,7 +39,7 @@ const DEFAULT_ETH_ON_BASE = {
39
39
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
40
40
  * />
41
41
  */
42
- export function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly = false, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }) {
42
+ export function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, destinationAmountDisplay, pureTransferOnly = false, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }) {
43
43
  const [copied, setCopied] = useState(false);
44
44
  const [orderId, setOrderId] = useState();
45
45
  const [globalAddress, setGlobalAddress] = useState();
@@ -171,8 +171,8 @@ export function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourc
171
171
  }
172
172
  return (_jsx("div", { className: classes?.container ||
173
173
  cn("anyspend-container anyspend-qr-deposit font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6", mode === "page" && "border-as-border-secondary overflow-hidden rounded-2xl border shadow-xl"), children: _jsxs("div", { className: classes?.content || "anyspend-qr-deposit-content flex flex-col gap-4", children: [_jsxs("div", { className: classes?.header || "anyspend-qr-header flex items-center justify-between", children: [_jsx("button", { onClick: handleBack, className: classes?.backButton || "anyspend-qr-back-button text-as-secondary hover:text-as-primary", children: _jsx("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsx("h2", { className: classes?.title || "anyspend-qr-title text-as-primary text-base font-semibold", children: "Deposit" }), onClose ? (_jsx("button", { onClick: handleClose, className: classes?.closeButton || "anyspend-qr-close-button text-as-secondary hover:text-as-primary", children: _jsx("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })) : (_jsx("div", { className: "w-5" }))] }), _jsxs("div", { className: classes?.tokenSelectorContainer || "anyspend-qr-token-selector flex flex-col gap-1.5", children: [_jsx("label", { className: classes?.tokenSelectorLabel || "anyspend-qr-token-label text-as-secondary text-sm", children: "Send" }), _jsx(TokenSelector, { chainIdsFilter: getAvailableChainIds("from"), context: "from", fromChainWalletVMSupported: true, isValidAddress: true, lockedChainIds: getAvailableChainIds("from"), multiWalletSupportEnabled: true, onAnalyticEvent: undefined, setToken: handleTokenSelect, supportedWalletVMs: ["evm"], token: undefined, trigger: _jsxs(Button, { variant: "outline", role: "combobox", className: classes?.tokenSelectorTrigger ||
174
- "anyspend-qr-token-trigger border-as-stroke bg-as-surface-secondary flex h-auto w-full items-center justify-between gap-2 rounded-xl border px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [sourceToken.metadata?.logoURI ? (_jsx(ChainTokenIcon, { chainUrl: ALL_CHAINS[sourceChainId]?.logoUrl, tokenUrl: sourceToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : (_jsx("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), _jsxs("div", { className: "flex flex-col items-start gap-0", children: [_jsx("div", { className: "text-as-primary font-semibold", children: sourceToken.symbol }), _jsx("div", { className: "text-as-primary/70 text-xs", children: ALL_CHAINS[sourceChainId]?.name ?? "Unknown" })] })] }), _jsx(ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-70" })] }) })] }), _jsxs("div", { className: classes?.qrContent || "anyspend-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4", children: [_jsxs("div", { className: classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2", children: [_jsx("div", { className: classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2", children: _jsx(QRCodeSVG, { value: qrValue, size: 120, level: "M", marginSize: 0 }) }), _jsxs("span", { className: classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs", children: ["SCAN WITH ", _jsx("span", { className: "inline-block", children: "\uD83E\uDD8A" })] })] }), _jsxs("div", { className: classes?.addressContainer || "anyspend-qr-address-container flex flex-1 flex-col gap-1", children: [_jsx("span", { className: classes?.addressLabel || "anyspend-qr-address-label text-as-secondary text-sm", children: "Deposit address:" }), _jsxs("div", { className: classes?.addressRow || "anyspend-qr-address-row flex items-start gap-1", children: [_jsx("span", { className: classes?.address || "anyspend-qr-address text-as-primary break-all font-mono text-sm leading-relaxed", children: displayAddress }), _jsx("button", { onClick: handleCopyAddress, className: classes?.addressCopyIcon ||
175
- "anyspend-qr-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0", children: copied ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(Copy, { className: "h-4 w-4" }) })] })] })] }), _jsx(ChainWarningText, { chainId: effectiveDestinationChainId }), _jsxs(WarningText, { children: ["Only send ", sourceToken.symbol, " on ", ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", ". Other tokens will not be converted."] }), isPureTransfer && isWatchingTransfer && (_jsxs("div", { className: classes?.watchingIndicator ||
174
+ "anyspend-qr-token-trigger border-as-stroke bg-as-surface-secondary flex h-auto w-full items-center justify-between gap-2 rounded-xl border px-3 py-2.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [sourceToken.metadata?.logoURI ? (_jsx(ChainTokenIcon, { chainUrl: ALL_CHAINS[sourceChainId]?.logoUrl, tokenUrl: sourceToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : (_jsx("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), _jsxs("div", { className: "flex flex-col items-start gap-0", children: [_jsx("div", { className: "text-as-primary font-semibold", children: sourceToken.symbol }), _jsx("div", { className: "text-as-primary/70 text-xs", children: ALL_CHAINS[sourceChainId]?.name ?? "Unknown" })] })] }), _jsx(ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-70" })] }) })] }), !isPureTransfer && (_jsxs("div", { className: "anyspend-qr-receive flex flex-col gap-1.5", children: [_jsx("label", { className: "anyspend-qr-receive-label text-as-secondary text-sm", children: "Receive" }), _jsxs("div", { className: "anyspend-qr-receive-row border-as-stroke bg-as-surface-secondary flex w-full items-center gap-2 rounded-xl border px-3 py-2.5", children: [effectiveDestinationToken.metadata?.logoURI ? (_jsx(ChainTokenIcon, { chainUrl: ALL_CHAINS[effectiveDestinationChainId]?.logoUrl, tokenUrl: effectiveDestinationToken.metadata.logoURI, className: "h-8 min-h-8 w-8 min-w-8" })) : (_jsx("div", { className: "h-8 w-8 rounded-full bg-gray-700" })), _jsxs("div", { className: "flex flex-col items-start gap-0", children: [_jsx("div", { className: "text-as-primary font-semibold", children: effectiveDestinationToken.symbol || "Token" }), _jsx("div", { className: "text-as-primary/70 text-xs", children: ALL_CHAINS[effectiveDestinationChainId]?.name ?? "Unknown" })] })] })] })), _jsxs("div", { className: classes?.qrContent || "anyspend-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4", children: [_jsxs("div", { className: classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2", children: [_jsx("div", { className: classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2", children: _jsx(QRCodeSVG, { value: qrValue, size: 120, level: "M", marginSize: 0 }) }), _jsxs("span", { className: classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs", children: ["SCAN WITH ", _jsx("span", { className: "inline-block", children: "\uD83E\uDD8A" })] })] }), _jsxs("div", { className: classes?.addressContainer || "anyspend-qr-address-container flex flex-1 flex-col gap-1", children: [_jsx("span", { className: classes?.addressLabel || "anyspend-qr-address-label text-as-secondary text-sm", children: "Deposit address:" }), _jsxs("div", { className: classes?.addressRow || "anyspend-qr-address-row flex items-start gap-1", children: [_jsx("span", { className: classes?.address || "anyspend-qr-address text-as-primary break-all font-mono text-sm leading-relaxed", children: displayAddress }), _jsx("button", { onClick: handleCopyAddress, className: classes?.addressCopyIcon ||
175
+ "anyspend-qr-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0", children: copied ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(Copy, { className: "h-4 w-4" }) })] })] })] }), _jsx(ChainWarningText, { chainId: effectiveDestinationChainId }), isPureTransfer ? (_jsxs(WarningText, { children: ["Only send ", sourceToken.symbol, " on ", ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", ". Other tokens will not be converted."] })) : (_jsxs(_Fragment, { children: [_jsxs(WarningText, { children: ["Only send ", sourceToken.symbol, " on ", ALL_CHAINS[sourceChainId]?.name ?? "the specified chain", " to this address \u2014 it will be converted to ", effectiveDestinationToken.symbol || "the destination token", " on", " ", ALL_CHAINS[effectiveDestinationChainId]?.name ?? "the destination chain", "."] }), destinationAmountDisplay && (_jsxs("p", { className: "anyspend-qr-amount-hint text-as-secondary text-xs", children: ["Target \u2248 ", destinationAmountDisplay, " ", effectiveDestinationToken.symbol || "tokens", ". Any amount of", " ", sourceToken.symbol || "tokens", " works \u2014 you'll receive whatever your deposit converts to."] }))] })), isPureTransfer && isWatchingTransfer && (_jsxs("div", { className: classes?.watchingIndicator ||
176
176
  "anyspend-qr-watching flex items-center justify-center gap-2 rounded-lg bg-blue-500/10 p-3", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin text-blue-500" }), _jsx("span", { className: "text-sm text-blue-500", children: "Watching for incoming transfer..." })] })), _jsx("button", { onClick: handleCopyAddress, className: classes?.copyButton ||
177
177
  "anyspend-qr-copy-button flex w-full items-center justify-center gap-2 rounded-xl bg-blue-500 py-3.5 font-medium text-white transition-all hover:bg-blue-600", children: "Copy deposit address" })] }) }));
178
178
  }
@@ -5,6 +5,7 @@ export interface UnifiedTransactionParams {
5
5
  }
6
6
  export declare function useUnifiedChainSwitchAndExecute(): {
7
7
  switchChainAndExecute: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
8
+ switchChainAndExecuteWithGlobalAccount: (targetChainId: number, params: UnifiedTransactionParams, expectedGlobalAccountAddress?: string) => Promise<string | undefined>;
8
9
  switchChainAndExecuteWithEOA: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
9
10
  isSwitchingOrExecuting: boolean;
10
11
  isActiveSmartWallet: boolean | undefined;
@@ -9,13 +9,11 @@ import { prepareTransaction, sendTransaction as twSendTransaction } from "thirdw
9
9
  import { isAddress } from "viem";
10
10
  import { useSwitchChain } from "wagmi";
11
11
  import { useB3Config } from "../components/B3Provider/B3ConfigProvider.js";
12
- import { useB3Account } from "../components/B3Provider/useB3Account.js";
13
12
  import { useAccountWallet } from "./useAccountWallet.js";
14
13
  export function useUnifiedChainSwitchAndExecute() {
15
14
  const { switchChainAsync } = useSwitchChain();
16
15
  const [isSwitchingOrExecuting, setIsSwitchingOrExecuting] = useState(false);
17
- const { isActiveSmartWallet, isActiveEOAWallet, connectedEOAWallet } = useAccountWallet();
18
- const aaAccount = useB3Account();
16
+ const { isActiveSmartWallet, isActiveEOAWallet, connectedSmartWallet, connectedEOAWallet } = useAccountWallet();
19
17
  // From context, not process.env: partnerId is hashed into the server intent key and must match the ecosystem wallet.
20
18
  const { partnerId } = useB3Config();
21
19
  // Handle EOA wallet chain switch and execute transaction
@@ -94,10 +92,18 @@ export function useUnifiedChainSwitchAndExecute() {
94
92
  setIsSwitchingOrExecuting(false);
95
93
  }
96
94
  }, [connectedEOAWallet, switchChainAsync]);
97
- // Handle AA wallet transaction (no chain switch needed for AA)
98
- const handleAASendTransaction = useCallback(async (targetChainId, params) => {
99
- if (!aaAccount) {
100
- toast.error("Smart wallet not connected");
95
+ // Handle Global Account transaction (no chain switch needed for AA).
96
+ // Resolve the signer from the connected ecosystem wallet rather than the active
97
+ // wallet so callers can explicitly require the Global Account.
98
+ const handleGlobalAccountSendTransaction = useCallback(async (targetChainId, params, expectedGlobalAccountAddress) => {
99
+ const globalAccount = connectedSmartWallet?.getAccount();
100
+ if (!globalAccount) {
101
+ toast.error("Global Account not connected");
102
+ return;
103
+ }
104
+ if (expectedGlobalAccountAddress &&
105
+ globalAccount.address.toLowerCase() !== expectedGlobalAccountAddress.toLowerCase()) {
106
+ toast.error("Connected Global Account does not match the authenticated account");
101
107
  return;
102
108
  }
103
109
  try {
@@ -131,7 +137,7 @@ export function useUnifiedChainSwitchAndExecute() {
131
137
  toast.info("Sending transaction…");
132
138
  const start = performance.now();
133
139
  const sendTxResponse = await twSendTransaction({
134
- account: aaAccount,
140
+ account: globalAccount,
135
141
  transaction,
136
142
  });
137
143
  const end = performance.now();
@@ -147,17 +153,18 @@ export function useUnifiedChainSwitchAndExecute() {
147
153
  finally {
148
154
  setIsSwitchingOrExecuting(false);
149
155
  }
150
- }, [aaAccount, partnerId]);
156
+ }, [connectedSmartWallet, partnerId]);
151
157
  // Unified switch chain and execute function
152
158
  const switchChainAndExecute = useCallback(async (targetChainId, params) => {
153
159
  // Check which wallet type is active
154
160
  if (isActiveSmartWallet) {
155
- return handleAASendTransaction(targetChainId, params);
161
+ return handleGlobalAccountSendTransaction(targetChainId, params);
156
162
  }
157
163
  return handleEOASwitchChainAndSendTransaction(targetChainId, params);
158
- }, [isActiveSmartWallet, handleAASendTransaction, handleEOASwitchChainAndSendTransaction]);
164
+ }, [isActiveSmartWallet, handleGlobalAccountSendTransaction, handleEOASwitchChainAndSendTransaction]);
159
165
  return {
160
166
  switchChainAndExecute,
167
+ switchChainAndExecuteWithGlobalAccount: handleGlobalAccountSendTransaction,
161
168
  switchChainAndExecuteWithEOA: handleEOASwitchChainAndSendTransaction,
162
169
  isSwitchingOrExecuting,
163
170
  isActiveSmartWallet,
@@ -14,6 +14,14 @@ export interface QRDepositProps {
14
14
  destinationToken: components["schemas"]["Token"];
15
15
  /** The destination chain ID */
16
16
  destinationChainId: number;
17
+ /**
18
+ * Human-formatted destination amount (e.g. "9.365547"), shown as an APPROXIMATE
19
+ * target in swap mode only. The QR deposit path accepts an arbitrary sent amount,
20
+ * so this is never a guaranteed receive figure — the UI frames it as "any amount
21
+ * works". Omitted when no fixed amount is set or the destination decimals are
22
+ * unresolved (to avoid a mis-scaled number). Ignored in pure-transfer mode.
23
+ */
24
+ destinationAmountDisplay?: string;
17
25
  /**
18
26
  * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
19
27
  * the user's currently selected source token/chain, so the exact token the user
@@ -54,4 +62,4 @@ export interface QRDepositProps {
54
62
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
55
63
  * />
56
64
  */
57
- export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, destinationAmountDisplay, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,7 @@ export interface UnifiedTransactionParams {
5
5
  }
6
6
  export declare function useUnifiedChainSwitchAndExecute(): {
7
7
  switchChainAndExecute: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
8
+ switchChainAndExecuteWithGlobalAccount: (targetChainId: number, params: UnifiedTransactionParams, expectedGlobalAccountAddress?: string) => Promise<string | undefined>;
8
9
  switchChainAndExecuteWithEOA: (targetChainId: number, params: UnifiedTransactionParams) => Promise<string | undefined>;
9
10
  isSwitchingOrExecuting: boolean;
10
11
  isActiveSmartWallet: boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.70-alpha.16",
3
+ "version": "0.1.70-alpha.18",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -3,6 +3,7 @@ import { components } from "@b3dotfun/sdk/anyspend/types/api";
3
3
  import { GetQuoteResponse } from "@b3dotfun/sdk/anyspend/types/api_req_res";
4
4
  import { Skeleton, useAccountWallet, useSimBalance, useTokenData } from "@b3dotfun/sdk/global-account/react";
5
5
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
6
+ import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
6
7
  import {
7
8
  NetworkArbitrumOne,
8
9
  NetworkBase,
@@ -631,7 +632,9 @@ export function AnySpendDeposit({
631
632
  "anyspend-deposit-option-description text-as-secondary text-xs"
632
633
  }
633
634
  >
634
- Send tokens directly to deposit address
635
+ {pureTransferOnly
636
+ ? "Send tokens directly to deposit address"
637
+ : "Send any token — we'll convert it for you"}
635
638
  </p>
636
639
  </div>
637
640
  </div>
@@ -700,6 +703,19 @@ export function AnySpendDeposit({
700
703
  // `KNOWN_FUNDING_TOKENS` key — so `hasKnownDestination` is always true and this
701
704
  // branch never fires for them. The guard is defensive for FUTURE callers that pass
702
705
  // an unknown token together with `pureTransferOnly=true`.
706
+ // Only surface the fixed destination amount on the QR path when its decimals are trustworthy
707
+ // (a known funding token or a resolved API lookup) — otherwise `destinationToken.decimals`
708
+ // is the generic 18 fallback and formatting a 6-decimal token's wei against it would be off by
709
+ // 10^12. When unresolved we still show the destination token/chain, just without an amount.
710
+ const destinationDecimalsResolved = hasKnownDestination || !!destinationTokenData;
711
+ const destinationAmountDisplay =
712
+ destinationTokenAmount &&
713
+ /^\d+$/.test(destinationTokenAmount) &&
714
+ BigInt(destinationTokenAmount) > 0n &&
715
+ destinationDecimalsResolved
716
+ ? formatTokenAmount(BigInt(destinationTokenAmount), destinationToken.decimals, 6, false)
717
+ : undefined;
718
+
703
719
  if (pureTransferOnly && !hasKnownDestination && !destinationTokenData && isDestinationTokenLoading) {
704
720
  return (
705
721
  <div
@@ -722,6 +738,7 @@ export function AnySpendDeposit({
722
738
  recipientAddress={recipientAddress}
723
739
  destinationToken={destinationToken}
724
740
  destinationChainId={destinationTokenChainId}
741
+ destinationAmountDisplay={destinationAmountDisplay}
725
742
  pureTransferOnly={pureTransferOnly}
726
743
  depositContractConfig={depositContractConfig}
727
744
  onBack={handleBack}
@@ -36,6 +36,14 @@ export interface QRDepositProps {
36
36
  destinationToken: components["schemas"]["Token"];
37
37
  /** The destination chain ID */
38
38
  destinationChainId: number;
39
+ /**
40
+ * Human-formatted destination amount (e.g. "9.365547"), shown as an APPROXIMATE
41
+ * target in swap mode only. The QR deposit path accepts an arbitrary sent amount,
42
+ * so this is never a guaranteed receive figure — the UI frames it as "any amount
43
+ * works". Omitted when no fixed amount is set or the destination decimals are
44
+ * unresolved (to avoid a mis-scaled number). Ignored in pure-transfer mode.
45
+ */
46
+ destinationAmountDisplay?: string;
39
47
  /**
40
48
  * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
41
49
  * the user's currently selected source token/chain, so the exact token the user
@@ -96,6 +104,7 @@ export function QRDeposit({
96
104
  sourceChainId: sourceChainIdProp,
97
105
  destinationToken,
98
106
  destinationChainId,
107
+ destinationAmountDisplay,
99
108
  pureTransferOnly = false,
100
109
  creatorAddress,
101
110
  depositContractConfig,
@@ -410,6 +419,32 @@ export function QRDeposit({
410
419
  />
411
420
  </div>
412
421
 
422
+ {/* Receive (swap mode only) — makes the conversion explicit: what the sent token
423
+ becomes and on which chain. Gated on the computed isPureTransfer (not the static
424
+ prop) so a runtime source change back to the same token correctly hides it. */}
425
+ {!isPureTransfer && (
426
+ <div className="anyspend-qr-receive flex flex-col gap-1.5">
427
+ <label className="anyspend-qr-receive-label text-as-secondary text-sm">Receive</label>
428
+ <div className="anyspend-qr-receive-row border-as-stroke bg-as-surface-secondary flex w-full items-center gap-2 rounded-xl border px-3 py-2.5">
429
+ {effectiveDestinationToken.metadata?.logoURI ? (
430
+ <ChainTokenIcon
431
+ chainUrl={ALL_CHAINS[effectiveDestinationChainId]?.logoUrl}
432
+ tokenUrl={effectiveDestinationToken.metadata.logoURI}
433
+ className="h-8 min-h-8 w-8 min-w-8"
434
+ />
435
+ ) : (
436
+ <div className="h-8 w-8 rounded-full bg-gray-700" />
437
+ )}
438
+ <div className="flex flex-col items-start gap-0">
439
+ <div className="text-as-primary font-semibold">{effectiveDestinationToken.symbol || "Token"}</div>
440
+ <div className="text-as-primary/70 text-xs">
441
+ {ALL_CHAINS[effectiveDestinationChainId]?.name ?? "Unknown"}
442
+ </div>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ )}
447
+
413
448
  {/* QR Code and Address - horizontal layout */}
414
449
  <div
415
450
  className={
@@ -454,10 +489,26 @@ export function QRDeposit({
454
489
 
455
490
  {/* Warnings */}
456
491
  <ChainWarningText chainId={effectiveDestinationChainId} />
457
- <WarningText>
458
- Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"}. Other tokens
459
- will not be converted.
460
- </WarningText>
492
+ {isPureTransfer ? (
493
+ <WarningText>
494
+ Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"}. Other tokens
495
+ will not be converted.
496
+ </WarningText>
497
+ ) : (
498
+ <>
499
+ <WarningText>
500
+ Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"} to this
501
+ address — it will be converted to {effectiveDestinationToken.symbol || "the destination token"} on{" "}
502
+ {ALL_CHAINS[effectiveDestinationChainId]?.name ?? "the destination chain"}.
503
+ </WarningText>
504
+ {destinationAmountDisplay && (
505
+ <p className="anyspend-qr-amount-hint text-as-secondary text-xs">
506
+ Target ≈ {destinationAmountDisplay} {effectiveDestinationToken.symbol || "tokens"}. Any amount of{" "}
507
+ {sourceToken.symbol || "tokens"} works — you'll receive whatever your deposit converts to.
508
+ </p>
509
+ )}
510
+ </>
511
+ )}
461
512
 
462
513
  {/* Watching indicator for pure transfers */}
463
514
  {isPureTransfer && isWatchingTransfer && (
@@ -164,6 +164,11 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
164
164
 
165
165
  // The displayed deposit address is the recipient's own address, NOT a global/order address.
166
166
  expect(screen.getByText(RECIPIENT)).toBeTruthy();
167
+
168
+ // Pure-transfer UI is unchanged: NO "Receive" row (there is no distinct destination),
169
+ // and the accurate "will not be converted" warning is kept verbatim.
170
+ expect(screen.queryByText("Receive")).toBeNull();
171
+ expect(screen.getByText(/Other tokens will not be converted/)).toBeTruthy();
167
172
  });
168
173
 
169
174
  it("Test B — switching the source token stays a pure transfer (no order; mirror follows selection)", async () => {
@@ -206,7 +211,14 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
206
211
 
207
212
  // No pureTransferOnly: source defaults to ETH-on-Base, which differs from the USDC dest,
208
213
  // so this is a real swap and the normal deposit_first order path must run.
209
- render(<QRDeposit recipientAddress={RECIPIENT} destinationToken={USDC_BASE} destinationChainId={8453} />);
214
+ const { container } = render(
215
+ <QRDeposit
216
+ recipientAddress={RECIPIENT}
217
+ destinationToken={USDC_BASE}
218
+ destinationChainId={8453}
219
+ destinationAmountDisplay="9.365547"
220
+ />,
221
+ );
210
222
 
211
223
  // The flag is what gates the pure-transfer behavior: without it, an order IS created once.
212
224
  expect(createOrderSpy).toHaveBeenCalledTimes(1);
@@ -218,6 +230,32 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
218
230
  expect(orderArgs.dstToken.decimals).toBe(6);
219
231
  // Source defaulted to native ETH on Base (the differing token that forces a swap).
220
232
  expect(orderArgs.srcToken.address).toBe("0x0000000000000000000000000000000000000000");
233
+
234
+ // Swap-mode UX clarity: a read-only "Receive" row now discloses the destination token…
235
+ expect(screen.getByText("Receive")).toBeTruthy();
236
+ // …the misleading "will not be converted" copy is gone…
237
+ expect(screen.queryByText(/will not be converted/)).toBeNull();
238
+ // …the warning states the conversion explicitly, and the fixed amount shows as an
239
+ // approximate target (never a guaranteed receive figure).
240
+ expect(container.textContent).toContain("it will be converted to USDC on Base");
241
+ expect(container.textContent).toContain("Target ≈ 9.365547 USDC");
242
+ });
243
+
244
+ it("Test C2 — swap mode without a fixed amount shows the Receive row but no target hint", async () => {
245
+ const QRDeposit = await loadQRDeposit();
246
+
247
+ // Swap mode (source ETH-on-Base != USDC dest) but NO destinationAmountDisplay — the common
248
+ // open-ended funding case. The destination must still be disclosed, but there must be no
249
+ // dangling "Target ≈" line (and no blank amount).
250
+ const { container } = render(
251
+ <QRDeposit recipientAddress={RECIPIENT} destinationToken={USDC_BASE} destinationChainId={8453} />,
252
+ );
253
+
254
+ // Destination is still disclosed…
255
+ expect(screen.getByText("Receive")).toBeTruthy();
256
+ expect(container.textContent).toContain("it will be converted to USDC on Base");
257
+ // …but no amount hint is rendered when no amount is provided.
258
+ expect(container.textContent).not.toContain("Target ≈");
221
259
  });
222
260
  });
223
261
 
@@ -0,0 +1,156 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useUnifiedChainSwitchAndExecute } from "../useUnifiedChainSwitchAndExecute";
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ eoaGetAccount: vi.fn(),
7
+ globalAccountGetAccount: vi.fn(),
8
+ intentCreate: vi.fn(),
9
+ prepareTransaction: vi.fn(),
10
+ sendTransaction: vi.fn(),
11
+ toastError: vi.fn(),
12
+ toastInfo: vi.fn(),
13
+ toastSuccess: vi.fn(),
14
+ useAccountWallet: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("@b3dotfun/sdk/anyspend", () => ({
18
+ getChainName: (chainId: number) => `Chain ${chainId}`,
19
+ getNativeToken: () => ({ name: "Ether", symbol: "ETH", decimals: 18 }),
20
+ }));
21
+
22
+ vi.mock("@b3dotfun/sdk/global-account/app", () => ({
23
+ default: {
24
+ service: () => ({ create: mocks.intentCreate }),
25
+ },
26
+ }));
27
+
28
+ vi.mock("@b3dotfun/sdk/global-account/react", () => ({
29
+ toast: {
30
+ dismiss: vi.fn(),
31
+ error: mocks.toastError,
32
+ info: mocks.toastInfo,
33
+ success: mocks.toastSuccess,
34
+ },
35
+ }));
36
+
37
+ vi.mock("@b3dotfun/sdk/shared/constants/chains/supported", () => ({
38
+ getThirdwebChain: (chainId: number) => ({ id: chainId }),
39
+ supportedChains: [],
40
+ }));
41
+
42
+ vi.mock("@b3dotfun/sdk/shared/utils/thirdweb", () => ({ client: {} }));
43
+
44
+ vi.mock("thirdweb", () => ({
45
+ prepareTransaction: mocks.prepareTransaction,
46
+ sendTransaction: mocks.sendTransaction,
47
+ }));
48
+
49
+ vi.mock("viem", () => ({ isAddress: () => true }));
50
+
51
+ vi.mock("wagmi", () => ({
52
+ useSwitchChain: () => ({ switchChainAsync: vi.fn() }),
53
+ }));
54
+
55
+ vi.mock("../../components/B3Provider/B3ConfigProvider", () => ({
56
+ useB3Config: () => ({ partnerId: "test-partner" }),
57
+ }));
58
+
59
+ vi.mock("../useAccountWallet", () => ({
60
+ useAccountWallet: mocks.useAccountWallet,
61
+ }));
62
+
63
+ const GLOBAL_ACCOUNT_ADDRESS = "0x1111111111111111111111111111111111111111";
64
+ const OTHER_GLOBAL_ACCOUNT_ADDRESS = "0x2222222222222222222222222222222222222222";
65
+ const EOA_ADDRESS = "0x3333333333333333333333333333333333333333";
66
+ const TRANSACTION_HASH = `0x${"a".repeat(64)}`;
67
+
68
+ const globalAccount = { address: GLOBAL_ACCOUNT_ADDRESS };
69
+ const preparedTransaction = { prepared: true };
70
+ const transactionParams = {
71
+ to: "0x4444444444444444444444444444444444444444",
72
+ data: "0x1234",
73
+ value: BigInt(0),
74
+ };
75
+
76
+ describe("useUnifiedChainSwitchAndExecute", () => {
77
+ beforeEach(() => {
78
+ vi.clearAllMocks();
79
+
80
+ mocks.globalAccountGetAccount.mockReturnValue(globalAccount);
81
+ mocks.eoaGetAccount.mockReturnValue({ address: EOA_ADDRESS });
82
+ mocks.intentCreate.mockResolvedValue({});
83
+ mocks.prepareTransaction.mockReturnValue(preparedTransaction);
84
+ mocks.sendTransaction.mockResolvedValue({ transactionHash: TRANSACTION_HASH });
85
+ mocks.useAccountWallet.mockReturnValue({
86
+ connectedSmartWallet: { getAccount: mocks.globalAccountGetAccount },
87
+ connectedEOAWallet: { getAccount: mocks.eoaGetAccount },
88
+ isActiveSmartWallet: false,
89
+ isActiveEOAWallet: true,
90
+ });
91
+ });
92
+
93
+ it("uses the connected Global Account even when the EOA wallet is active", async () => {
94
+ const { result } = renderHook(() => useUnifiedChainSwitchAndExecute());
95
+
96
+ let transactionHash: string | undefined;
97
+ await act(async () => {
98
+ transactionHash = await result.current.switchChainAndExecuteWithGlobalAccount(
99
+ 8453,
100
+ transactionParams,
101
+ GLOBAL_ACCOUNT_ADDRESS,
102
+ );
103
+ });
104
+
105
+ expect(transactionHash).toBe(TRANSACTION_HASH);
106
+ expect(mocks.globalAccountGetAccount).toHaveBeenCalledOnce();
107
+ expect(mocks.eoaGetAccount).not.toHaveBeenCalled();
108
+ expect(mocks.sendTransaction).toHaveBeenCalledWith({
109
+ account: globalAccount,
110
+ transaction: preparedTransaction,
111
+ });
112
+ });
113
+
114
+ it("rejects a signer that does not match the authenticated Global Account", async () => {
115
+ const { result } = renderHook(() => useUnifiedChainSwitchAndExecute());
116
+
117
+ let transactionHash: string | undefined;
118
+ await act(async () => {
119
+ transactionHash = await result.current.switchChainAndExecuteWithGlobalAccount(
120
+ 8453,
121
+ transactionParams,
122
+ OTHER_GLOBAL_ACCOUNT_ADDRESS,
123
+ );
124
+ });
125
+
126
+ expect(transactionHash).toBeUndefined();
127
+ expect(mocks.toastError).toHaveBeenCalledWith("Connected Global Account does not match the authenticated account");
128
+ expect(mocks.prepareTransaction).not.toHaveBeenCalled();
129
+ expect(mocks.sendTransaction).not.toHaveBeenCalled();
130
+ expect(mocks.eoaGetAccount).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it("fails closed when no Global Account signer is connected", async () => {
134
+ mocks.useAccountWallet.mockReturnValue({
135
+ connectedSmartWallet: undefined,
136
+ connectedEOAWallet: { getAccount: mocks.eoaGetAccount },
137
+ isActiveSmartWallet: false,
138
+ isActiveEOAWallet: true,
139
+ });
140
+ const { result } = renderHook(() => useUnifiedChainSwitchAndExecute());
141
+
142
+ let transactionHash: string | undefined;
143
+ await act(async () => {
144
+ transactionHash = await result.current.switchChainAndExecuteWithGlobalAccount(
145
+ 8453,
146
+ transactionParams,
147
+ GLOBAL_ACCOUNT_ADDRESS,
148
+ );
149
+ });
150
+
151
+ expect(transactionHash).toBeUndefined();
152
+ expect(mocks.toastError).toHaveBeenCalledWith("Global Account not connected");
153
+ expect(mocks.sendTransaction).not.toHaveBeenCalled();
154
+ expect(mocks.eoaGetAccount).not.toHaveBeenCalled();
155
+ });
156
+ });
@@ -10,7 +10,6 @@ import { prepareTransaction, sendTransaction as twSendTransaction } from "thirdw
10
10
  import { isAddress } from "viem";
11
11
  import { useSwitchChain } from "wagmi";
12
12
  import { useB3Config } from "../components/B3Provider/B3ConfigProvider";
13
- import { useB3Account } from "../components/B3Provider/useB3Account";
14
13
  import { useAccountWallet } from "./useAccountWallet";
15
14
 
16
15
  export interface UnifiedTransactionParams {
@@ -23,8 +22,7 @@ export function useUnifiedChainSwitchAndExecute() {
23
22
  const { switchChainAsync } = useSwitchChain();
24
23
  const [isSwitchingOrExecuting, setIsSwitchingOrExecuting] = useState(false);
25
24
 
26
- const { isActiveSmartWallet, isActiveEOAWallet, connectedEOAWallet } = useAccountWallet();
27
- const aaAccount = useB3Account();
25
+ const { isActiveSmartWallet, isActiveEOAWallet, connectedSmartWallet, connectedEOAWallet } = useAccountWallet();
28
26
 
29
27
  // From context, not process.env: partnerId is hashed into the server intent key and must match the ecosystem wallet.
30
28
  const { partnerId } = useB3Config();
@@ -119,11 +117,26 @@ export function useUnifiedChainSwitchAndExecute() {
119
117
  [connectedEOAWallet, switchChainAsync],
120
118
  );
121
119
 
122
- // Handle AA wallet transaction (no chain switch needed for AA)
123
- const handleAASendTransaction = useCallback(
124
- async (targetChainId: number, params: UnifiedTransactionParams): Promise<string | undefined> => {
125
- if (!aaAccount) {
126
- toast.error("Smart wallet not connected");
120
+ // Handle Global Account transaction (no chain switch needed for AA).
121
+ // Resolve the signer from the connected ecosystem wallet rather than the active
122
+ // wallet so callers can explicitly require the Global Account.
123
+ const handleGlobalAccountSendTransaction = useCallback(
124
+ async (
125
+ targetChainId: number,
126
+ params: UnifiedTransactionParams,
127
+ expectedGlobalAccountAddress?: string,
128
+ ): Promise<string | undefined> => {
129
+ const globalAccount = connectedSmartWallet?.getAccount();
130
+ if (!globalAccount) {
131
+ toast.error("Global Account not connected");
132
+ return;
133
+ }
134
+
135
+ if (
136
+ expectedGlobalAccountAddress &&
137
+ globalAccount.address.toLowerCase() !== expectedGlobalAccountAddress.toLowerCase()
138
+ ) {
139
+ toast.error("Connected Global Account does not match the authenticated account");
127
140
  return;
128
141
  }
129
142
 
@@ -162,7 +175,7 @@ export function useUnifiedChainSwitchAndExecute() {
162
175
  toast.info("Sending transaction…");
163
176
  const start = performance.now();
164
177
  const sendTxResponse = await twSendTransaction({
165
- account: aaAccount,
178
+ account: globalAccount,
166
179
  transaction,
167
180
  });
168
181
  const end = performance.now();
@@ -178,7 +191,7 @@ export function useUnifiedChainSwitchAndExecute() {
178
191
  setIsSwitchingOrExecuting(false);
179
192
  }
180
193
  },
181
- [aaAccount, partnerId],
194
+ [connectedSmartWallet, partnerId],
182
195
  );
183
196
 
184
197
  // Unified switch chain and execute function
@@ -186,15 +199,16 @@ export function useUnifiedChainSwitchAndExecute() {
186
199
  async (targetChainId: number, params: UnifiedTransactionParams): Promise<string | undefined> => {
187
200
  // Check which wallet type is active
188
201
  if (isActiveSmartWallet) {
189
- return handleAASendTransaction(targetChainId, params);
202
+ return handleGlobalAccountSendTransaction(targetChainId, params);
190
203
  }
191
204
  return handleEOASwitchChainAndSendTransaction(targetChainId, params);
192
205
  },
193
- [isActiveSmartWallet, handleAASendTransaction, handleEOASwitchChainAndSendTransaction],
206
+ [isActiveSmartWallet, handleGlobalAccountSendTransaction, handleEOASwitchChainAndSendTransaction],
194
207
  );
195
208
 
196
209
  return {
197
210
  switchChainAndExecute,
211
+ switchChainAndExecuteWithGlobalAccount: handleGlobalAccountSendTransaction,
198
212
  switchChainAndExecuteWithEOA: handleEOASwitchChainAndSendTransaction,
199
213
  isSwitchingOrExecuting,
200
214
  isActiveSmartWallet,