@b3dotfun/sdk 0.1.65-alpha.1 → 0.1.65-alpha.3

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.
@@ -131,6 +131,8 @@ function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenP
131
131
  (0, useOnOrderSuccess_1.useOnOrderSuccess)({ orderData: oat, orderId, onSuccess });
132
132
  // For pure transfers, always use recipient address; for orders, use global address
133
133
  const displayAddress = isPureTransfer ? recipientAddress : globalAddress || recipientAddress;
134
+ // Generate EIP-681 payment URI for the QR code so wallets know which chain/token to use
135
+ const qrValue = (0, anyspend_1.getPaymentUrl)(displayAddress, undefined, sourceToken.address === anyspend_1.ZERO_ADDRESS ? "ETH" : sourceToken.address, sourceChainId, sourceToken.decimals);
134
136
  const handleCopyAddress = async () => {
135
137
  if (displayAddress) {
136
138
  await navigator.clipboard.writeText(displayAddress);
@@ -163,7 +165,7 @@ function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourceTokenP
163
165
  }
164
166
  return ((0, jsx_runtime_1.jsx)("div", { className: classes?.container ||
165
167
  (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 ||
166
- "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: displayAddress, 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 ||
168
+ "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 ||
167
169
  "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: destinationChainId }), (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 ||
168
170
  "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 ||
169
171
  "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" })] }) }));
@@ -82,7 +82,7 @@ export declare function isTestnet(chainId: number): boolean;
82
82
  export declare function getDefaultToken(chainId: number): components["schemas"]["Token"];
83
83
  export declare function getChainName(chainId: number): string;
84
84
  export declare function getCoingeckoName(chainId: number): string | null;
85
- export declare function getPaymentUrl(address: string, amount: bigint, currency: string, chainId: number, decimals?: number): string;
85
+ export declare function getPaymentUrl(address: string, amount: bigint | undefined, currency: string, chainId: number, decimals?: number): string;
86
86
  export declare function getExplorerTxUrl(chainId: number, txHash: string): string;
87
87
  export declare function getExplorerAddressUrl(chainId: number, address: string): string;
88
88
  export declare function getMulticall3Address(chainId: number): string;
@@ -392,8 +392,8 @@ function getPaymentUrl(address, amount, currency, chainId, decimals) {
392
392
  // For EVM chains, follow EIP-681 format
393
393
  // Format: ethereum:[address]@[chainId]?value=[amount]&symbol=[symbol]
394
394
  const params = new URLSearchParams();
395
- // Add value for native token transfers
396
- if (currency === chain.nativeToken.symbol) {
395
+ // Add value for native token transfers (skip if amount not provided, e.g. deposit_first)
396
+ if (currency === chain.nativeToken.symbol && amount !== undefined) {
397
397
  params.append("value", amount.toString());
398
398
  }
399
399
  // Handle token transfers differently from native transfers
@@ -408,28 +408,31 @@ function getPaymentUrl(address, amount, currency, chainId, decimals) {
408
408
  }
409
409
  // For ERC20 tokens, convert from smallest unit to display units using decimals
410
410
  // For example: 2400623 (raw) with 6 decimals becomes "2.400623"
411
- let displayAmount;
412
- if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
413
- // Convert from smallest unit to display unit for ERC20 tokens
414
- const divisor = BigInt(10 ** decimals);
415
- const wholePart = amount / divisor;
416
- const fractionalPart = amount % divisor;
417
- if (fractionalPart === BigInt(0)) {
418
- displayAmount = wholePart.toString();
411
+ // Skip amount if not provided (e.g. deposit_first orders)
412
+ if (amount !== undefined) {
413
+ let displayAmount;
414
+ if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
415
+ // Convert from smallest unit to display unit for ERC20 tokens
416
+ const divisor = BigInt(10 ** decimals);
417
+ const wholePart = amount / divisor;
418
+ const fractionalPart = amount % divisor;
419
+ if (fractionalPart === BigInt(0)) {
420
+ displayAmount = wholePart.toString();
421
+ }
422
+ else {
423
+ // Format fractional part with leading zeros if needed
424
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
425
+ // Remove trailing zeros
426
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
427
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
428
+ }
419
429
  }
420
430
  else {
421
- // Format fractional part with leading zeros if needed
422
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
423
- // Remove trailing zeros
424
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
425
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
431
+ // For native tokens or when decimals not provided, use raw amount
432
+ displayAmount = amount.toString();
426
433
  }
434
+ tokenParams.append("amount", displayAmount);
427
435
  }
428
- else {
429
- // For native tokens or when decimals not provided, use raw amount
430
- displayAmount = amount.toString();
431
- }
432
- tokenParams.append("amount", displayAmount);
433
436
  tokenParams.append("address", address); // recipient address
434
437
  // For Arbitrum and other L2s, try a more explicit format
435
438
  if (chainId !== chains_1.mainnet.id) {
@@ -449,7 +452,9 @@ function getPaymentUrl(address, amount, currency, chainId, decimals) {
449
452
  // to make sure wallets recognize the correct chain
450
453
  const nativeParams = new URLSearchParams();
451
454
  nativeParams.append("chainId", chainId.toString());
452
- nativeParams.append("value", amount.toString());
455
+ if (amount !== undefined) {
456
+ nativeParams.append("value", amount.toString());
457
+ }
453
458
  const url = `ethereum:${address}@${chainId}?${nativeParams.toString()}`;
454
459
  return url;
455
460
  }
@@ -468,60 +473,65 @@ function getPaymentUrl(address, amount, currency, chainId, decimals) {
468
473
  const isNativeSOL = currency === chain.nativeToken.symbol || currency === "SOL" || currency === "11111111111111111111111111111111";
469
474
  if (isNativeSOL) {
470
475
  // Native SOL transfers - convert from lamports to SOL
471
- let displayAmount;
472
- if (decimals !== undefined) {
473
- const divisor = BigInt(10 ** decimals);
474
- const wholePart = amount / divisor;
475
- const fractionalPart = amount % divisor;
476
- if (fractionalPart === BigInt(0)) {
477
- displayAmount = wholePart.toString();
478
- }
479
- else {
480
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
481
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
482
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
483
- }
484
- }
485
- else {
486
- // Fallback: assume SOL has 9 decimals
487
- const divisor = BigInt(1000000000); // 1e9
488
- const wholePart = amount / divisor;
489
- const fractionalPart = amount % divisor;
490
- if (fractionalPart === BigInt(0)) {
491
- displayAmount = wholePart.toString();
476
+ if (amount !== undefined) {
477
+ let displayAmount;
478
+ if (decimals !== undefined) {
479
+ const divisor = BigInt(10 ** decimals);
480
+ const wholePart = amount / divisor;
481
+ const fractionalPart = amount % divisor;
482
+ if (fractionalPart === BigInt(0)) {
483
+ displayAmount = wholePart.toString();
484
+ }
485
+ else {
486
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
487
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
488
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
489
+ }
492
490
  }
493
491
  else {
494
- const fractionalStr = fractionalPart.toString().padStart(9, "0");
495
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
496
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
492
+ // Fallback: assume SOL has 9 decimals
493
+ const divisor = BigInt(1000000000); // 1e9
494
+ const wholePart = amount / divisor;
495
+ const fractionalPart = amount % divisor;
496
+ if (fractionalPart === BigInt(0)) {
497
+ displayAmount = wholePart.toString();
498
+ }
499
+ else {
500
+ const fractionalStr = fractionalPart.toString().padStart(9, "0");
501
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
502
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
503
+ }
497
504
  }
505
+ // For native SOL, use simple format without spl-token parameter
506
+ params.append("amount", displayAmount);
498
507
  }
499
- // For native SOL, use simple format without spl-token parameter
500
- params.append("amount", displayAmount);
501
508
  }
502
509
  else {
503
510
  // SPL token transfers
504
- let displayAmount;
505
- if (decimals !== undefined) {
506
- const divisor = BigInt(10 ** decimals);
507
- const wholePart = amount / divisor;
508
- const fractionalPart = amount % divisor;
509
- if (fractionalPart === BigInt(0)) {
510
- displayAmount = wholePart.toString();
511
+ if (amount !== undefined) {
512
+ let displayAmount;
513
+ if (decimals !== undefined) {
514
+ const divisor = BigInt(10 ** decimals);
515
+ const wholePart = amount / divisor;
516
+ const fractionalPart = amount % divisor;
517
+ if (fractionalPart === BigInt(0)) {
518
+ displayAmount = wholePart.toString();
519
+ }
520
+ else {
521
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
522
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
523
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
524
+ }
511
525
  }
512
526
  else {
513
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
514
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
515
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
527
+ displayAmount = amount.toString();
516
528
  }
529
+ params.append("amount", displayAmount);
517
530
  }
518
- else {
519
- displayAmount = amount.toString();
520
- }
521
- params.append("amount", displayAmount);
522
531
  params.append("spl-token", currency); // token mint address
523
532
  }
524
- const url = `solana:${address}?${params.toString()}`;
533
+ const queryString = params.toString();
534
+ const url = queryString ? `solana:${address}?${queryString}` : `solana:${address}`;
525
535
  console.log("Solana URL (isNativeSOL:", isNativeSOL, "):", url);
526
536
  return url;
527
537
  }
@@ -14,6 +14,16 @@ function useUserQuery() {
14
14
  const user = (0, userStore_1.useUserStore)(state => state.user);
15
15
  const setUserStore = (0, userStore_1.useUserStore)(state => state.setUser);
16
16
  const clearUserStore = (0, userStore_1.useUserStore)(state => state.clearUser);
17
+ // Manually rehydrate persisted store inside useLayoutEffect to avoid
18
+ // updating AuthenticationProvider state during Hydrate render.
19
+ // useLayoutEffect (not useEffect) ensures rehydration triggers a
20
+ // synchronous re-render before any useEffect callbacks fire, so
21
+ // downstream effects always see the persisted user value.
22
+ (0, react_1.useLayoutEffect)(() => {
23
+ if (!userStore_1.useUserStore.persist.hasHydrated()) {
24
+ userStore_1.useUserStore.persist.rehydrate();
25
+ }
26
+ }, []);
17
27
  // Listen for storage events from other tabs/windows
18
28
  (0, react_1.useEffect)(() => {
19
29
  const handleStorageChange = (e) => {
@@ -22,6 +22,7 @@ exports.useUserStore = (0, zustand_1.create)()((0, middleware_1.persist)(set =>
22
22
  },
23
23
  }), {
24
24
  name: "b3-user",
25
+ skipHydration: true,
25
26
  onRehydrateStorage: () => (_, error) => {
26
27
  if (error) {
27
28
  console.warn("Failed to rehydrate user store:", error);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ALL_CHAINS, getAvailableChainIds, isSameChainAndToken } from "../../../anyspend/index.js";
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";
5
5
  import { TokenSelector } from "@relayprotocol/relay-kit-ui";
@@ -128,6 +128,8 @@ export function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourc
128
128
  useOnOrderSuccess({ orderData: oat, orderId, onSuccess });
129
129
  // For pure transfers, always use recipient address; for orders, use global address
130
130
  const displayAddress = isPureTransfer ? recipientAddress : globalAddress || recipientAddress;
131
+ // Generate EIP-681 payment URI for the QR code so wallets know which chain/token to use
132
+ const qrValue = getPaymentUrl(displayAddress, undefined, sourceToken.address === ZERO_ADDRESS ? "ETH" : sourceToken.address, sourceChainId, sourceToken.decimals);
131
133
  const handleCopyAddress = async () => {
132
134
  if (displayAddress) {
133
135
  await navigator.clipboard.writeText(displayAddress);
@@ -160,7 +162,7 @@ export function QRDeposit({ mode = "modal", recipientAddress, sourceToken: sourc
160
162
  }
161
163
  return (_jsx("div", { className: classes?.container ||
162
164
  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 ||
163
- "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: displayAddress, 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 ||
165
+ "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 ||
164
166
  "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: destinationChainId }), _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 ||
165
167
  "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 ||
166
168
  "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" })] }) }));
@@ -82,7 +82,7 @@ export declare function isTestnet(chainId: number): boolean;
82
82
  export declare function getDefaultToken(chainId: number): components["schemas"]["Token"];
83
83
  export declare function getChainName(chainId: number): string;
84
84
  export declare function getCoingeckoName(chainId: number): string | null;
85
- export declare function getPaymentUrl(address: string, amount: bigint, currency: string, chainId: number, decimals?: number): string;
85
+ export declare function getPaymentUrl(address: string, amount: bigint | undefined, currency: string, chainId: number, decimals?: number): string;
86
86
  export declare function getExplorerTxUrl(chainId: number, txHash: string): string;
87
87
  export declare function getExplorerAddressUrl(chainId: number, address: string): string;
88
88
  export declare function getMulticall3Address(chainId: number): string;
@@ -362,8 +362,8 @@ export function getPaymentUrl(address, amount, currency, chainId, decimals) {
362
362
  // For EVM chains, follow EIP-681 format
363
363
  // Format: ethereum:[address]@[chainId]?value=[amount]&symbol=[symbol]
364
364
  const params = new URLSearchParams();
365
- // Add value for native token transfers
366
- if (currency === chain.nativeToken.symbol) {
365
+ // Add value for native token transfers (skip if amount not provided, e.g. deposit_first)
366
+ if (currency === chain.nativeToken.symbol && amount !== undefined) {
367
367
  params.append("value", amount.toString());
368
368
  }
369
369
  // Handle token transfers differently from native transfers
@@ -378,28 +378,31 @@ export function getPaymentUrl(address, amount, currency, chainId, decimals) {
378
378
  }
379
379
  // For ERC20 tokens, convert from smallest unit to display units using decimals
380
380
  // For example: 2400623 (raw) with 6 decimals becomes "2.400623"
381
- let displayAmount;
382
- if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
383
- // Convert from smallest unit to display unit for ERC20 tokens
384
- const divisor = BigInt(10 ** decimals);
385
- const wholePart = amount / divisor;
386
- const fractionalPart = amount % divisor;
387
- if (fractionalPart === BigInt(0)) {
388
- displayAmount = wholePart.toString();
381
+ // Skip amount if not provided (e.g. deposit_first orders)
382
+ if (amount !== undefined) {
383
+ let displayAmount;
384
+ if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
385
+ // Convert from smallest unit to display unit for ERC20 tokens
386
+ const divisor = BigInt(10 ** decimals);
387
+ const wholePart = amount / divisor;
388
+ const fractionalPart = amount % divisor;
389
+ if (fractionalPart === BigInt(0)) {
390
+ displayAmount = wholePart.toString();
391
+ }
392
+ else {
393
+ // Format fractional part with leading zeros if needed
394
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
395
+ // Remove trailing zeros
396
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
397
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
398
+ }
389
399
  }
390
400
  else {
391
- // Format fractional part with leading zeros if needed
392
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
393
- // Remove trailing zeros
394
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
395
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
401
+ // For native tokens or when decimals not provided, use raw amount
402
+ displayAmount = amount.toString();
396
403
  }
404
+ tokenParams.append("amount", displayAmount);
397
405
  }
398
- else {
399
- // For native tokens or when decimals not provided, use raw amount
400
- displayAmount = amount.toString();
401
- }
402
- tokenParams.append("amount", displayAmount);
403
406
  tokenParams.append("address", address); // recipient address
404
407
  // For Arbitrum and other L2s, try a more explicit format
405
408
  if (chainId !== mainnet.id) {
@@ -419,7 +422,9 @@ export function getPaymentUrl(address, amount, currency, chainId, decimals) {
419
422
  // to make sure wallets recognize the correct chain
420
423
  const nativeParams = new URLSearchParams();
421
424
  nativeParams.append("chainId", chainId.toString());
422
- nativeParams.append("value", amount.toString());
425
+ if (amount !== undefined) {
426
+ nativeParams.append("value", amount.toString());
427
+ }
423
428
  const url = `ethereum:${address}@${chainId}?${nativeParams.toString()}`;
424
429
  return url;
425
430
  }
@@ -438,60 +443,65 @@ export function getPaymentUrl(address, amount, currency, chainId, decimals) {
438
443
  const isNativeSOL = currency === chain.nativeToken.symbol || currency === "SOL" || currency === "11111111111111111111111111111111";
439
444
  if (isNativeSOL) {
440
445
  // Native SOL transfers - convert from lamports to SOL
441
- let displayAmount;
442
- if (decimals !== undefined) {
443
- const divisor = BigInt(10 ** decimals);
444
- const wholePart = amount / divisor;
445
- const fractionalPart = amount % divisor;
446
- if (fractionalPart === BigInt(0)) {
447
- displayAmount = wholePart.toString();
448
- }
449
- else {
450
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
451
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
452
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
453
- }
454
- }
455
- else {
456
- // Fallback: assume SOL has 9 decimals
457
- const divisor = BigInt(1000000000); // 1e9
458
- const wholePart = amount / divisor;
459
- const fractionalPart = amount % divisor;
460
- if (fractionalPart === BigInt(0)) {
461
- displayAmount = wholePart.toString();
446
+ if (amount !== undefined) {
447
+ let displayAmount;
448
+ if (decimals !== undefined) {
449
+ const divisor = BigInt(10 ** decimals);
450
+ const wholePart = amount / divisor;
451
+ const fractionalPart = amount % divisor;
452
+ if (fractionalPart === BigInt(0)) {
453
+ displayAmount = wholePart.toString();
454
+ }
455
+ else {
456
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
457
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
458
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
459
+ }
462
460
  }
463
461
  else {
464
- const fractionalStr = fractionalPart.toString().padStart(9, "0");
465
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
466
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
462
+ // Fallback: assume SOL has 9 decimals
463
+ const divisor = BigInt(1000000000); // 1e9
464
+ const wholePart = amount / divisor;
465
+ const fractionalPart = amount % divisor;
466
+ if (fractionalPart === BigInt(0)) {
467
+ displayAmount = wholePart.toString();
468
+ }
469
+ else {
470
+ const fractionalStr = fractionalPart.toString().padStart(9, "0");
471
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
472
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
473
+ }
467
474
  }
475
+ // For native SOL, use simple format without spl-token parameter
476
+ params.append("amount", displayAmount);
468
477
  }
469
- // For native SOL, use simple format without spl-token parameter
470
- params.append("amount", displayAmount);
471
478
  }
472
479
  else {
473
480
  // SPL token transfers
474
- let displayAmount;
475
- if (decimals !== undefined) {
476
- const divisor = BigInt(10 ** decimals);
477
- const wholePart = amount / divisor;
478
- const fractionalPart = amount % divisor;
479
- if (fractionalPart === BigInt(0)) {
480
- displayAmount = wholePart.toString();
481
+ if (amount !== undefined) {
482
+ let displayAmount;
483
+ if (decimals !== undefined) {
484
+ const divisor = BigInt(10 ** decimals);
485
+ const wholePart = amount / divisor;
486
+ const fractionalPart = amount % divisor;
487
+ if (fractionalPart === BigInt(0)) {
488
+ displayAmount = wholePart.toString();
489
+ }
490
+ else {
491
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
492
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
493
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
494
+ }
481
495
  }
482
496
  else {
483
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
484
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
485
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
497
+ displayAmount = amount.toString();
486
498
  }
499
+ params.append("amount", displayAmount);
487
500
  }
488
- else {
489
- displayAmount = amount.toString();
490
- }
491
- params.append("amount", displayAmount);
492
501
  params.append("spl-token", currency); // token mint address
493
502
  }
494
- const url = `solana:${address}?${params.toString()}`;
503
+ const queryString = params.toString();
504
+ const url = queryString ? `solana:${address}?${queryString}` : `solana:${address}`;
495
505
  console.log("Solana URL (isNativeSOL:", isNativeSOL, "):", url);
496
506
  return url;
497
507
  }
@@ -1,4 +1,4 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useLayoutEffect } from "react";
2
2
  import { useUserStore } from "../stores/userStore.js";
3
3
  const USER_QUERY_KEY = ["b3-user"];
4
4
  /**
@@ -11,6 +11,16 @@ export function useUserQuery() {
11
11
  const user = useUserStore(state => state.user);
12
12
  const setUserStore = useUserStore(state => state.setUser);
13
13
  const clearUserStore = useUserStore(state => state.clearUser);
14
+ // Manually rehydrate persisted store inside useLayoutEffect to avoid
15
+ // updating AuthenticationProvider state during Hydrate render.
16
+ // useLayoutEffect (not useEffect) ensures rehydration triggers a
17
+ // synchronous re-render before any useEffect callbacks fire, so
18
+ // downstream effects always see the persisted user value.
19
+ useLayoutEffect(() => {
20
+ if (!useUserStore.persist.hasHydrated()) {
21
+ useUserStore.persist.rehydrate();
22
+ }
23
+ }, []);
14
24
  // Listen for storage events from other tabs/windows
15
25
  useEffect(() => {
16
26
  const handleStorageChange = (e) => {
@@ -19,6 +19,7 @@ export const useUserStore = create()(persist(set => ({
19
19
  },
20
20
  }), {
21
21
  name: "b3-user",
22
+ skipHydration: true,
22
23
  onRehydrateStorage: () => (_, error) => {
23
24
  if (error) {
24
25
  console.warn("Failed to rehydrate user store:", error);
@@ -82,7 +82,7 @@ export declare function isTestnet(chainId: number): boolean;
82
82
  export declare function getDefaultToken(chainId: number): components["schemas"]["Token"];
83
83
  export declare function getChainName(chainId: number): string;
84
84
  export declare function getCoingeckoName(chainId: number): string | null;
85
- export declare function getPaymentUrl(address: string, amount: bigint, currency: string, chainId: number, decimals?: number): string;
85
+ export declare function getPaymentUrl(address: string, amount: bigint | undefined, currency: string, chainId: number, decimals?: number): string;
86
86
  export declare function getExplorerTxUrl(chainId: number, txHash: string): string;
87
87
  export declare function getExplorerAddressUrl(chainId: number, address: string): string;
88
88
  export declare function getMulticall3Address(chainId: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.65-alpha.1",
3
+ "version": "0.1.65-alpha.3",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -1,4 +1,10 @@
1
- import { ALL_CHAINS, getAvailableChainIds, isSameChainAndToken } from "@b3dotfun/sdk/anyspend";
1
+ import {
2
+ ALL_CHAINS,
3
+ getAvailableChainIds,
4
+ getPaymentUrl,
5
+ isSameChainAndToken,
6
+ ZERO_ADDRESS,
7
+ } from "@b3dotfun/sdk/anyspend";
2
8
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
3
9
  import { Button, toast } from "@b3dotfun/sdk/global-account/react";
4
10
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
@@ -194,6 +200,15 @@ export function QRDeposit({
194
200
  // For pure transfers, always use recipient address; for orders, use global address
195
201
  const displayAddress = isPureTransfer ? recipientAddress : globalAddress || recipientAddress;
196
202
 
203
+ // Generate EIP-681 payment URI for the QR code so wallets know which chain/token to use
204
+ const qrValue = getPaymentUrl(
205
+ displayAddress,
206
+ undefined,
207
+ sourceToken.address === ZERO_ADDRESS ? "ETH" : sourceToken.address,
208
+ sourceChainId,
209
+ sourceToken.decimals,
210
+ );
211
+
197
212
  const handleCopyAddress = async () => {
198
213
  if (displayAddress) {
199
214
  await navigator.clipboard.writeText(displayAddress);
@@ -376,7 +391,7 @@ export function QRDeposit({
376
391
  {/* QR Code */}
377
392
  <div className={classes?.qrCodeContainer || "anyspend-qr-code-container flex flex-col items-center gap-2"}>
378
393
  <div className={classes?.qrCode || "anyspend-qr-code rounded-lg bg-white p-2"}>
379
- <QRCodeSVG value={displayAddress} size={120} level="M" marginSize={0} />
394
+ <QRCodeSVG value={qrValue} size={120} level="M" marginSize={0} />
380
395
  </div>
381
396
  <span className={classes?.qrScanHint || "anyspend-qr-scan-hint text-as-secondary text-xs"}>
382
397
  SCAN WITH <span className="inline-block">🦊</span>
@@ -421,7 +421,13 @@ export function getCoingeckoName(chainId: number): string | null {
421
421
  return ALL_CHAINS[chainId].coingeckoName;
422
422
  }
423
423
 
424
- export function getPaymentUrl(address: string, amount: bigint, currency: string, chainId: number, decimals?: number) {
424
+ export function getPaymentUrl(
425
+ address: string,
426
+ amount: bigint | undefined,
427
+ currency: string,
428
+ chainId: number,
429
+ decimals?: number,
430
+ ) {
425
431
  // Get chain type to determine URL format
426
432
  const chainType = getChainType(chainId);
427
433
  const chain = ALL_CHAINS[chainId];
@@ -433,8 +439,8 @@ export function getPaymentUrl(address: string, amount: bigint, currency: string,
433
439
  // Format: ethereum:[address]@[chainId]?value=[amount]&symbol=[symbol]
434
440
  const params = new URLSearchParams();
435
441
 
436
- // Add value for native token transfers
437
- if (currency === chain.nativeToken.symbol) {
442
+ // Add value for native token transfers (skip if amount not provided, e.g. deposit_first)
443
+ if (currency === chain.nativeToken.symbol && amount !== undefined) {
438
444
  params.append("value", amount.toString());
439
445
  }
440
446
 
@@ -453,28 +459,31 @@ export function getPaymentUrl(address: string, amount: bigint, currency: string,
453
459
 
454
460
  // For ERC20 tokens, convert from smallest unit to display units using decimals
455
461
  // For example: 2400623 (raw) with 6 decimals becomes "2.400623"
456
- let displayAmount: string;
457
- if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
458
- // Convert from smallest unit to display unit for ERC20 tokens
459
- const divisor = BigInt(10 ** decimals);
460
- const wholePart = amount / divisor;
461
- const fractionalPart = amount % divisor;
462
-
463
- if (fractionalPart === BigInt(0)) {
464
- displayAmount = wholePart.toString();
462
+ // Skip amount if not provided (e.g. deposit_first orders)
463
+ if (amount !== undefined) {
464
+ let displayAmount: string;
465
+ if (decimals !== undefined && currency !== chain.nativeToken.symbol) {
466
+ // Convert from smallest unit to display unit for ERC20 tokens
467
+ const divisor = BigInt(10 ** decimals);
468
+ const wholePart = amount / divisor;
469
+ const fractionalPart = amount % divisor;
470
+
471
+ if (fractionalPart === BigInt(0)) {
472
+ displayAmount = wholePart.toString();
473
+ } else {
474
+ // Format fractional part with leading zeros if needed
475
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
476
+ // Remove trailing zeros
477
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
478
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
479
+ }
465
480
  } else {
466
- // Format fractional part with leading zeros if needed
467
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
468
- // Remove trailing zeros
469
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
470
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
481
+ // For native tokens or when decimals not provided, use raw amount
482
+ displayAmount = amount.toString();
471
483
  }
472
- } else {
473
- // For native tokens or when decimals not provided, use raw amount
474
- displayAmount = amount.toString();
475
- }
476
484
 
477
- tokenParams.append("amount", displayAmount);
485
+ tokenParams.append("amount", displayAmount);
486
+ }
478
487
  tokenParams.append("address", address); // recipient address
479
488
 
480
489
  // For Arbitrum and other L2s, try a more explicit format
@@ -495,7 +504,9 @@ export function getPaymentUrl(address: string, amount: bigint, currency: string,
495
504
  // to make sure wallets recognize the correct chain
496
505
  const nativeParams = new URLSearchParams();
497
506
  nativeParams.append("chainId", chainId.toString());
498
- nativeParams.append("value", amount.toString());
507
+ if (amount !== undefined) {
508
+ nativeParams.append("value", amount.toString());
509
+ }
499
510
  const url = `ethereum:${address}@${chainId}?${nativeParams.toString()}`;
500
511
  return url;
501
512
  } else {
@@ -517,60 +528,65 @@ export function getPaymentUrl(address: string, amount: bigint, currency: string,
517
528
 
518
529
  if (isNativeSOL) {
519
530
  // Native SOL transfers - convert from lamports to SOL
520
- let displayAmount: string;
521
- if (decimals !== undefined) {
522
- const divisor = BigInt(10 ** decimals);
523
- const wholePart = amount / divisor;
524
- const fractionalPart = amount % divisor;
525
-
526
- if (fractionalPart === BigInt(0)) {
527
- displayAmount = wholePart.toString();
531
+ if (amount !== undefined) {
532
+ let displayAmount: string;
533
+ if (decimals !== undefined) {
534
+ const divisor = BigInt(10 ** decimals);
535
+ const wholePart = amount / divisor;
536
+ const fractionalPart = amount % divisor;
537
+
538
+ if (fractionalPart === BigInt(0)) {
539
+ displayAmount = wholePart.toString();
540
+ } else {
541
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
542
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
543
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
544
+ }
528
545
  } else {
529
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
530
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
531
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
546
+ // Fallback: assume SOL has 9 decimals
547
+ const divisor = BigInt(1000000000); // 1e9
548
+ const wholePart = amount / divisor;
549
+ const fractionalPart = amount % divisor;
550
+
551
+ if (fractionalPart === BigInt(0)) {
552
+ displayAmount = wholePart.toString();
553
+ } else {
554
+ const fractionalStr = fractionalPart.toString().padStart(9, "0");
555
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
556
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
557
+ }
532
558
  }
533
- } else {
534
- // Fallback: assume SOL has 9 decimals
535
- const divisor = BigInt(1000000000); // 1e9
536
- const wholePart = amount / divisor;
537
- const fractionalPart = amount % divisor;
538
559
 
539
- if (fractionalPart === BigInt(0)) {
540
- displayAmount = wholePart.toString();
541
- } else {
542
- const fractionalStr = fractionalPart.toString().padStart(9, "0");
543
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
544
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
545
- }
560
+ // For native SOL, use simple format without spl-token parameter
561
+ params.append("amount", displayAmount);
546
562
  }
547
-
548
- // For native SOL, use simple format without spl-token parameter
549
- params.append("amount", displayAmount);
550
563
  } else {
551
564
  // SPL token transfers
552
- let displayAmount: string;
553
- if (decimals !== undefined) {
554
- const divisor = BigInt(10 ** decimals);
555
- const wholePart = amount / divisor;
556
- const fractionalPart = amount % divisor;
557
-
558
- if (fractionalPart === BigInt(0)) {
559
- displayAmount = wholePart.toString();
565
+ if (amount !== undefined) {
566
+ let displayAmount: string;
567
+ if (decimals !== undefined) {
568
+ const divisor = BigInt(10 ** decimals);
569
+ const wholePart = amount / divisor;
570
+ const fractionalPart = amount % divisor;
571
+
572
+ if (fractionalPart === BigInt(0)) {
573
+ displayAmount = wholePart.toString();
574
+ } else {
575
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
576
+ const trimmedFractional = fractionalStr.replace(/0+$/, "");
577
+ displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
578
+ }
560
579
  } else {
561
- const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
562
- const trimmedFractional = fractionalStr.replace(/0+$/, "");
563
- displayAmount = trimmedFractional ? `${wholePart}.${trimmedFractional}` : wholePart.toString();
580
+ displayAmount = amount.toString();
564
581
  }
565
- } else {
566
- displayAmount = amount.toString();
567
- }
568
582
 
569
- params.append("amount", displayAmount);
583
+ params.append("amount", displayAmount);
584
+ }
570
585
  params.append("spl-token", currency); // token mint address
571
586
  }
572
587
 
573
- const url = `solana:${address}?${params.toString()}`;
588
+ const queryString = params.toString();
589
+ const url = queryString ? `solana:${address}?${queryString}` : `solana:${address}`;
574
590
  console.log("Solana URL (isNativeSOL:", isNativeSOL, "):", url);
575
591
  return url;
576
592
  }
@@ -1,5 +1,5 @@
1
1
  import { Users } from "@b3dotfun/b3-api";
2
- import { useEffect } from "react";
2
+ import { useEffect, useLayoutEffect } from "react";
3
3
  import { useUserStore } from "../stores/userStore";
4
4
 
5
5
  const USER_QUERY_KEY = ["b3-user"];
@@ -15,6 +15,17 @@ export function useUserQuery() {
15
15
  const setUserStore = useUserStore(state => state.setUser);
16
16
  const clearUserStore = useUserStore(state => state.clearUser);
17
17
 
18
+ // Manually rehydrate persisted store inside useLayoutEffect to avoid
19
+ // updating AuthenticationProvider state during Hydrate render.
20
+ // useLayoutEffect (not useEffect) ensures rehydration triggers a
21
+ // synchronous re-render before any useEffect callbacks fire, so
22
+ // downstream effects always see the persisted user value.
23
+ useLayoutEffect(() => {
24
+ if (!useUserStore.persist.hasHydrated()) {
25
+ useUserStore.persist.rehydrate();
26
+ }
27
+ }, []);
28
+
18
29
  // Listen for storage events from other tabs/windows
19
30
  useEffect(() => {
20
31
  const handleStorageChange = (e: StorageEvent) => {
@@ -31,6 +31,7 @@ export const useUserStore = create<UserStore>()(
31
31
  }),
32
32
  {
33
33
  name: "b3-user",
34
+ skipHydration: true,
34
35
  onRehydrateStorage: () => (_, error) => {
35
36
  if (error) {
36
37
  console.warn("Failed to rehydrate user store:", error);