@b3dotfun/sdk 0.1.65-alpha.2 → 0.1.65-alpha.4

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.
@@ -69,5 +69,10 @@ export interface AnySpendCollectorClubPurchaseProps {
69
69
  * Force fiat payment
70
70
  */
71
71
  forceFiatPayment?: boolean;
72
+ /**
73
+ * Optional discount code to apply to the purchase.
74
+ * When provided, validates on-chain and adjusts the price accordingly.
75
+ */
76
+ discountCode?: string;
72
77
  }
73
- export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
78
+ export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, discountCode, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
@@ -29,9 +29,11 @@ const jsx_runtime_1 = require("react/jsx-runtime");
29
29
  * ```
30
30
  */
31
31
  const constants_1 = require("../../../anyspend/constants");
32
+ const constants_2 = require("../../../shared/constants");
32
33
  const number_1 = require("../../../shared/utils/number");
33
34
  const react_1 = require("react");
34
35
  const viem_1 = require("viem");
36
+ const chains_1 = require("viem/chains");
35
37
  const AnySpendCustom_1 = require("./AnySpendCustom");
36
38
  // Collector Club Shop contract addresses on Base
37
39
  const CC_SHOP_ADDRESS = "0x47366E64E4917dd4DdC04Fb9DC507c1dD2b87294";
@@ -49,7 +51,35 @@ const BUY_PACKS_FOR_ABI = {
49
51
  stateMutability: "nonpayable",
50
52
  type: "function",
51
53
  };
52
- function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = constants_1.USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, }) {
54
+ // ABI for buyPacksForWithDiscount function (with discount code)
55
+ const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
56
+ inputs: [
57
+ { internalType: "address", name: "user", type: "address" },
58
+ { internalType: "uint256", name: "packId", type: "uint256" },
59
+ { internalType: "uint256", name: "amount", type: "uint256" },
60
+ { internalType: "string", name: "discountCode", type: "string" },
61
+ ],
62
+ name: "buyPacksForWithDiscount",
63
+ outputs: [],
64
+ stateMutability: "nonpayable",
65
+ type: "function",
66
+ };
67
+ // ABI for isDiscountCodeValid view function
68
+ const IS_DISCOUNT_CODE_VALID_ABI = {
69
+ inputs: [{ internalType: "string", name: "code", type: "string" }],
70
+ name: "isDiscountCodeValid",
71
+ outputs: [
72
+ { internalType: "bool", name: "isValid", type: "bool" },
73
+ { internalType: "uint256", name: "discountAmount", type: "uint256" },
74
+ ],
75
+ stateMutability: "view",
76
+ type: "function",
77
+ };
78
+ const basePublicClient = (0, viem_1.createPublicClient)({
79
+ chain: chains_1.base,
80
+ transport: (0, viem_1.http)(constants_2.PUBLIC_BASE_RPC_URL),
81
+ });
82
+ function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = constants_1.USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, discountCode, }) {
53
83
  const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
54
84
  // Calculate total amount needed (pricePerPack * packAmount)
55
85
  const totalAmount = (0, react_1.useMemo)(() => {
@@ -61,15 +91,89 @@ function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab =
61
91
  return "0";
62
92
  }
63
93
  }, [pricePerPack, packAmount]);
64
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
94
+ // Discount code validation state
95
+ const [discountInfo, setDiscountInfo] = (0, react_1.useState)({
96
+ isValid: false,
97
+ discountAmount: BigInt(0),
98
+ isLoading: false,
99
+ error: null,
100
+ });
101
+ // Validate discount code on-chain when provided
102
+ (0, react_1.useEffect)(() => {
103
+ if (!discountCode) {
104
+ setDiscountInfo({ isValid: false, discountAmount: BigInt(0), isLoading: false, error: null });
105
+ return;
106
+ }
107
+ let cancelled = false;
108
+ const validateDiscount = async () => {
109
+ setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
110
+ try {
111
+ const result = await basePublicClient.readContract({
112
+ address: ccShopAddress,
113
+ abi: [IS_DISCOUNT_CODE_VALID_ABI],
114
+ functionName: "isDiscountCodeValid",
115
+ args: [discountCode],
116
+ });
117
+ if (cancelled)
118
+ return;
119
+ const [isValid, discountAmount] = result;
120
+ if (!isValid) {
121
+ setDiscountInfo({
122
+ isValid: false,
123
+ discountAmount: BigInt(0),
124
+ isLoading: false,
125
+ error: "Invalid or expired discount code",
126
+ });
127
+ return;
128
+ }
129
+ setDiscountInfo({ isValid: true, discountAmount, isLoading: false, error: null });
130
+ }
131
+ catch (error) {
132
+ if (cancelled)
133
+ return;
134
+ console.error("Failed to validate discount code", { discountCode, error });
135
+ setDiscountInfo({
136
+ isValid: false,
137
+ discountAmount: BigInt(0),
138
+ isLoading: false,
139
+ error: "Failed to validate discount code",
140
+ });
141
+ }
142
+ };
143
+ validateDiscount();
144
+ return () => {
145
+ cancelled = true;
146
+ };
147
+ }, [discountCode, ccShopAddress]);
148
+ // Calculate effective dstAmount after discount
149
+ const effectiveDstAmount = (0, react_1.useMemo)(() => {
150
+ if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
151
+ return totalAmount;
152
+ }
153
+ const total = BigInt(totalAmount);
154
+ const discount = discountInfo.discountAmount;
155
+ if (discount >= total) {
156
+ console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
157
+ return "0";
158
+ }
159
+ return (total - discount).toString();
160
+ }, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
161
+ // Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
65
162
  const srcFiatAmount = (0, react_1.useMemo)(() => {
66
- if (!totalAmount || totalAmount === "0")
163
+ if (!effectiveDstAmount || effectiveDstAmount === "0")
67
164
  return "0";
68
- return (0, number_1.formatUnits)(totalAmount, constants_1.USDC_BASE.decimals);
69
- }, [totalAmount]);
70
- // Encode the buyPacksFor function call
165
+ return (0, number_1.formatUnits)(effectiveDstAmount, constants_1.USDC_BASE.decimals);
166
+ }, [effectiveDstAmount]);
167
+ // Encode the contract function call (with or without discount)
71
168
  const encodedData = (0, react_1.useMemo)(() => {
72
169
  try {
170
+ if (discountCode && discountInfo.isValid) {
171
+ return (0, viem_1.encodeFunctionData)({
172
+ abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
173
+ functionName: "buyPacksForWithDiscount",
174
+ args: [recipientAddress, BigInt(packId), BigInt(packAmount), discountCode],
175
+ });
176
+ }
73
177
  return (0, viem_1.encodeFunctionData)({
74
178
  abi: [BUY_PACKS_FOR_ABI],
75
179
  functionName: "buyPacksFor",
@@ -77,17 +181,30 @@ function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab =
77
181
  });
78
182
  }
79
183
  catch (error) {
80
- console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
184
+ console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
81
185
  return "0x";
82
186
  }
83
- }, [recipientAddress, packId, packAmount]);
187
+ }, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
84
188
  // Default header if not provided
85
189
  const defaultHeader = () => ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-as-primary text-xl font-bold", children: "Buy Collector Club Packs" }), (0, jsx_runtime_1.jsxs)("p", { className: "text-as-secondary text-sm", children: ["Purchase ", packAmount, " pack", packAmount !== 1 ? "s" : "", " using any token"] })] }) }));
86
- return ((0, jsx_runtime_1.jsx)(AnySpendCustom_1.AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: totalAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
190
+ // Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
191
+ if (discountCode && discountInfo.isLoading) {
192
+ return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-as-secondary text-sm", children: "Validating discount code..." }) }));
193
+ }
194
+ if (discountCode && discountInfo.error) {
195
+ return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: discountInfo.error }) }));
196
+ }
197
+ if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
198
+ return ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: "Discount exceeds total price" }) }));
199
+ }
200
+ return ((0, jsx_runtime_1.jsx)(AnySpendCustom_1.AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: effectiveDstAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
87
201
  packId,
88
202
  packAmount,
89
203
  pricePerPack,
90
204
  vendingMachineId,
91
205
  packType,
206
+ ...(discountCode && discountInfo.isValid
207
+ ? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
208
+ : {}),
92
209
  }, header: header || defaultHeader, onSuccess: onSuccess, showRecipient: showRecipient, srcFiatAmount: srcFiatAmount, forceFiatPayment: forceFiatPayment }));
93
210
  }
@@ -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
  }
@@ -470,6 +470,8 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
470
470
  forceFiatPayment?: boolean;
471
471
  /** Staging environment support */
472
472
  isStaging?: boolean;
473
+ /** Optional discount code to apply to the purchase */
474
+ discountCode?: string;
473
475
  }
474
476
  /**
475
477
  * Props for the AnySpend Deposit modal
@@ -69,5 +69,10 @@ export interface AnySpendCollectorClubPurchaseProps {
69
69
  * Force fiat payment
70
70
  */
71
71
  forceFiatPayment?: boolean;
72
+ /**
73
+ * Optional discount code to apply to the purchase.
74
+ * When provided, validates on-chain and adjusts the price accordingly.
75
+ */
76
+ discountCode?: string;
72
77
  }
73
- export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
78
+ export declare function AnySpendCollectorClubPurchase({ loadOrder, mode, activeTab, packId, packAmount, pricePerPack, paymentToken, recipientAddress, spenderAddress, isStaging, onSuccess, header, showRecipient, vendingMachineId, packType, forceFiatPayment, discountCode, }: AnySpendCollectorClubPurchaseProps): import("react/jsx-runtime").JSX.Element;
@@ -26,9 +26,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
26
  * ```
27
27
  */
28
28
  import { USDC_BASE } from "../../../anyspend/constants/index.js";
29
+ import { PUBLIC_BASE_RPC_URL } from "../../../shared/constants/index.js";
29
30
  import { formatUnits } from "../../../shared/utils/number.js";
30
- import { useMemo } from "react";
31
- import { encodeFunctionData } from "viem";
31
+ import { useEffect, useMemo, useState } from "react";
32
+ import { createPublicClient, encodeFunctionData, http } from "viem";
33
+ import { base } from "viem/chains";
32
34
  import { AnySpendCustom } from "./AnySpendCustom.js";
33
35
  // Collector Club Shop contract addresses on Base
34
36
  const CC_SHOP_ADDRESS = "0x47366E64E4917dd4DdC04Fb9DC507c1dD2b87294";
@@ -46,7 +48,35 @@ const BUY_PACKS_FOR_ABI = {
46
48
  stateMutability: "nonpayable",
47
49
  type: "function",
48
50
  };
49
- export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, }) {
51
+ // ABI for buyPacksForWithDiscount function (with discount code)
52
+ const BUY_PACKS_FOR_WITH_DISCOUNT_ABI = {
53
+ inputs: [
54
+ { internalType: "address", name: "user", type: "address" },
55
+ { internalType: "uint256", name: "packId", type: "uint256" },
56
+ { internalType: "uint256", name: "amount", type: "uint256" },
57
+ { internalType: "string", name: "discountCode", type: "string" },
58
+ ],
59
+ name: "buyPacksForWithDiscount",
60
+ outputs: [],
61
+ stateMutability: "nonpayable",
62
+ type: "function",
63
+ };
64
+ // ABI for isDiscountCodeValid view function
65
+ const IS_DISCOUNT_CODE_VALID_ABI = {
66
+ inputs: [{ internalType: "string", name: "code", type: "string" }],
67
+ name: "isDiscountCodeValid",
68
+ outputs: [
69
+ { internalType: "bool", name: "isValid", type: "bool" },
70
+ { internalType: "uint256", name: "discountAmount", type: "uint256" },
71
+ ],
72
+ stateMutability: "view",
73
+ type: "function",
74
+ };
75
+ const basePublicClient = createPublicClient({
76
+ chain: base,
77
+ transport: http(PUBLIC_BASE_RPC_URL),
78
+ });
79
+ export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activeTab = "crypto", packId, packAmount, pricePerPack, paymentToken = USDC_BASE, recipientAddress, spenderAddress, isStaging = false, onSuccess, header, showRecipient = true, vendingMachineId, packType, forceFiatPayment, discountCode, }) {
50
80
  const ccShopAddress = isStaging ? CC_SHOP_ADDRESS_STAGING : CC_SHOP_ADDRESS;
51
81
  // Calculate total amount needed (pricePerPack * packAmount)
52
82
  const totalAmount = useMemo(() => {
@@ -58,15 +88,89 @@ export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activ
58
88
  return "0";
59
89
  }
60
90
  }, [pricePerPack, packAmount]);
61
- // Calculate fiat amount (totalAmount in USD, assuming USDC with 6 decimals)
91
+ // Discount code validation state
92
+ const [discountInfo, setDiscountInfo] = useState({
93
+ isValid: false,
94
+ discountAmount: BigInt(0),
95
+ isLoading: false,
96
+ error: null,
97
+ });
98
+ // Validate discount code on-chain when provided
99
+ useEffect(() => {
100
+ if (!discountCode) {
101
+ setDiscountInfo({ isValid: false, discountAmount: BigInt(0), isLoading: false, error: null });
102
+ return;
103
+ }
104
+ let cancelled = false;
105
+ const validateDiscount = async () => {
106
+ setDiscountInfo(prev => ({ ...prev, isLoading: true, error: null }));
107
+ try {
108
+ const result = await basePublicClient.readContract({
109
+ address: ccShopAddress,
110
+ abi: [IS_DISCOUNT_CODE_VALID_ABI],
111
+ functionName: "isDiscountCodeValid",
112
+ args: [discountCode],
113
+ });
114
+ if (cancelled)
115
+ return;
116
+ const [isValid, discountAmount] = result;
117
+ if (!isValid) {
118
+ setDiscountInfo({
119
+ isValid: false,
120
+ discountAmount: BigInt(0),
121
+ isLoading: false,
122
+ error: "Invalid or expired discount code",
123
+ });
124
+ return;
125
+ }
126
+ setDiscountInfo({ isValid: true, discountAmount, isLoading: false, error: null });
127
+ }
128
+ catch (error) {
129
+ if (cancelled)
130
+ return;
131
+ console.error("Failed to validate discount code", { discountCode, error });
132
+ setDiscountInfo({
133
+ isValid: false,
134
+ discountAmount: BigInt(0),
135
+ isLoading: false,
136
+ error: "Failed to validate discount code",
137
+ });
138
+ }
139
+ };
140
+ validateDiscount();
141
+ return () => {
142
+ cancelled = true;
143
+ };
144
+ }, [discountCode, ccShopAddress]);
145
+ // Calculate effective dstAmount after discount
146
+ const effectiveDstAmount = useMemo(() => {
147
+ if (!discountCode || !discountInfo.isValid || discountInfo.discountAmount === BigInt(0)) {
148
+ return totalAmount;
149
+ }
150
+ const total = BigInt(totalAmount);
151
+ const discount = discountInfo.discountAmount;
152
+ if (discount >= total) {
153
+ console.error("Discount exceeds total price", { totalAmount, discountAmount: discount.toString() });
154
+ return "0";
155
+ }
156
+ return (total - discount).toString();
157
+ }, [totalAmount, discountCode, discountInfo.isValid, discountInfo.discountAmount]);
158
+ // Calculate fiat amount (effectiveDstAmount in USD, assuming USDC with 6 decimals)
62
159
  const srcFiatAmount = useMemo(() => {
63
- if (!totalAmount || totalAmount === "0")
160
+ if (!effectiveDstAmount || effectiveDstAmount === "0")
64
161
  return "0";
65
- return formatUnits(totalAmount, USDC_BASE.decimals);
66
- }, [totalAmount]);
67
- // Encode the buyPacksFor function call
162
+ return formatUnits(effectiveDstAmount, USDC_BASE.decimals);
163
+ }, [effectiveDstAmount]);
164
+ // Encode the contract function call (with or without discount)
68
165
  const encodedData = useMemo(() => {
69
166
  try {
167
+ if (discountCode && discountInfo.isValid) {
168
+ return encodeFunctionData({
169
+ abi: [BUY_PACKS_FOR_WITH_DISCOUNT_ABI],
170
+ functionName: "buyPacksForWithDiscount",
171
+ args: [recipientAddress, BigInt(packId), BigInt(packAmount), discountCode],
172
+ });
173
+ }
70
174
  return encodeFunctionData({
71
175
  abi: [BUY_PACKS_FOR_ABI],
72
176
  functionName: "buyPacksFor",
@@ -74,17 +178,30 @@ export function AnySpendCollectorClubPurchase({ loadOrder, mode = "modal", activ
74
178
  });
75
179
  }
76
180
  catch (error) {
77
- console.error("Failed to encode function data", { recipientAddress, packId, packAmount, error });
181
+ console.error("Failed to encode function data", { recipientAddress, packId, packAmount, discountCode, error });
78
182
  return "0x";
79
183
  }
80
- }, [recipientAddress, packId, packAmount]);
184
+ }, [recipientAddress, packId, packAmount, discountCode, discountInfo.isValid]);
81
185
  // Default header if not provided
82
186
  const defaultHeader = () => (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsxs("div", { children: [_jsx("h1", { className: "text-as-primary text-xl font-bold", children: "Buy Collector Club Packs" }), _jsxs("p", { className: "text-as-secondary text-sm", children: ["Purchase ", packAmount, " pack", packAmount !== 1 ? "s" : "", " using any token"] })] }) }));
83
- return (_jsx(AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: totalAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
187
+ // Don't render AnySpendCustom while discount is being validated (avoids showing wrong price)
188
+ if (discountCode && discountInfo.isLoading) {
189
+ return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-as-secondary text-sm", children: "Validating discount code..." }) }));
190
+ }
191
+ if (discountCode && discountInfo.error) {
192
+ return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-sm text-red-500", children: discountInfo.error }) }));
193
+ }
194
+ if (discountCode && discountInfo.isValid && effectiveDstAmount === "0") {
195
+ return (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsx("p", { className: "text-sm text-red-500", children: "Discount exceeds total price" }) }));
196
+ }
197
+ return (_jsx(AnySpendCustom, { loadOrder: loadOrder, mode: mode, activeTab: activeTab, recipientAddress: recipientAddress, spenderAddress: spenderAddress ?? ccShopAddress, orderType: "custom", dstChainId: BASE_CHAIN_ID, dstToken: paymentToken, dstAmount: effectiveDstAmount, contractAddress: ccShopAddress, encodedData: encodedData, metadata: {
84
198
  packId,
85
199
  packAmount,
86
200
  pricePerPack,
87
201
  vendingMachineId,
88
202
  packType,
203
+ ...(discountCode && discountInfo.isValid
204
+ ? { discountCode, discountAmount: discountInfo.discountAmount.toString() }
205
+ : {}),
89
206
  }, header: header || defaultHeader, onSuccess: onSuccess, showRecipient: showRecipient, srcFiatAmount: srcFiatAmount, forceFiatPayment: forceFiatPayment }));
90
207
  }