@b3dotfun/sdk 0.1.2-alpha.2 → 0.1.2-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.
Files changed (35) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.d.ts +1 -0
  2. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +110 -38
  3. package/dist/cjs/anyspend/react/components/AnySpendStakeUpsideExactIn.d.ts +2 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendStakeUpsideExactIn.js +2 -2
  5. package/dist/cjs/anyspend/react/components/common/OrderDetails.js +4 -6
  6. package/dist/cjs/anyspend/react/components/common/OrderDetailsCollapsible.js +6 -7
  7. package/dist/cjs/anyspend/react/hooks/useAnyspendFlow.d.ts +20 -1
  8. package/dist/cjs/anyspend/react/hooks/useAnyspendFlow.js +118 -15
  9. package/dist/cjs/anyspend/react/hooks/useRecipientAddressState.js +1 -1
  10. package/dist/cjs/anyspend/utils/format.js +12 -2
  11. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +2 -0
  12. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.d.ts +1 -0
  13. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +111 -39
  14. package/dist/esm/anyspend/react/components/AnySpendStakeUpsideExactIn.d.ts +2 -1
  15. package/dist/esm/anyspend/react/components/AnySpendStakeUpsideExactIn.js +2 -2
  16. package/dist/esm/anyspend/react/components/common/OrderDetails.js +4 -6
  17. package/dist/esm/anyspend/react/components/common/OrderDetailsCollapsible.js +6 -7
  18. package/dist/esm/anyspend/react/hooks/useAnyspendFlow.d.ts +20 -1
  19. package/dist/esm/anyspend/react/hooks/useAnyspendFlow.js +118 -16
  20. package/dist/esm/anyspend/react/hooks/useRecipientAddressState.js +1 -1
  21. package/dist/esm/anyspend/utils/format.js +12 -2
  22. package/dist/esm/global-account/react/stores/useModalStore.d.ts +2 -0
  23. package/dist/types/anyspend/react/components/AnySpendCustomExactIn.d.ts +1 -0
  24. package/dist/types/anyspend/react/components/AnySpendStakeUpsideExactIn.d.ts +2 -1
  25. package/dist/types/anyspend/react/hooks/useAnyspendFlow.d.ts +20 -1
  26. package/dist/types/global-account/react/stores/useModalStore.d.ts +2 -0
  27. package/package.json +1 -1
  28. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +125 -41
  29. package/src/anyspend/react/components/AnySpendStakeUpsideExactIn.tsx +3 -0
  30. package/src/anyspend/react/components/common/OrderDetails.tsx +4 -6
  31. package/src/anyspend/react/components/common/OrderDetailsCollapsible.tsx +7 -6
  32. package/src/anyspend/react/hooks/useAnyspendFlow.ts +140 -17
  33. package/src/anyspend/react/hooks/useRecipientAddressState.ts +1 -1
  34. package/src/anyspend/utils/format.ts +13 -2
  35. package/src/global-account/react/stores/useModalStore.ts +2 -0
@@ -1,10 +1,11 @@
1
1
  import { B3_TOKEN, getDefaultToken, USDC_BASE } from "../../../anyspend/index.js";
2
2
  import { useAnyspendCreateOnrampOrder, useAnyspendCreateOrder, useAnyspendOrderAndTransactions, useAnyspendQuote, useGeoOnrampOptions, } from "../../../anyspend/react/index.js";
3
3
  import { anyspendService } from "../../../anyspend/services/anyspend.js";
4
+ import { normalizeAddress } from "../../../anyspend/utils/index.js";
4
5
  import { toast, useAccountWallet, useProfile, useRouter, useSearchParamsSSR, useTokenBalance, } from "../../../global-account/react/index.js";
5
6
  import { formatTokenAmount, formatUnits } from "../../../shared/utils/number.js";
6
7
  import { useEffect, useMemo, useState } from "react";
7
- import { parseUnits } from "viem";
8
+ import { encodeFunctionData, parseUnits } from "viem";
8
9
  import { base, mainnet } from "viem/chains";
9
10
  import { CryptoPaymentMethodType } from "../components/common/CryptoPaymentMethod.js";
10
11
  import { FiatPaymentMethod } from "../components/common/FiatPaymentMethod.js";
@@ -23,8 +24,50 @@ export var PanelView;
23
24
  PanelView[PanelView["POINTS_DETAIL"] = 6] = "POINTS_DETAIL";
24
25
  PanelView[PanelView["FEE_DETAIL"] = 7] = "FEE_DETAIL";
25
26
  })(PanelView || (PanelView = {}));
27
+ /**
28
+ * Generates encoded function data for custom contract calls.
29
+ * Handles amount placeholder replacement and BigInt conversion.
30
+ */
31
+ export function generateEncodedData(config, amountInWei) {
32
+ if (!config || !config.functionAbi || !config.functionName || !config.functionArgs) {
33
+ console.warn("customExactInConfig missing required fields for encoding:", {
34
+ hasConfig: !!config,
35
+ hasFunctionAbi: !!config?.functionAbi,
36
+ hasFunctionName: !!config?.functionName,
37
+ hasFunctionArgs: !!config?.functionArgs,
38
+ });
39
+ return undefined;
40
+ }
41
+ try {
42
+ const abi = JSON.parse(config.functionAbi);
43
+ const processedArgs = config.functionArgs.map(arg => {
44
+ // Replace amount placeholders ({{dstAmount}}, {{amount_out}}, etc.)
45
+ if (arg === "{{dstAmount}}" || arg === "{{amount_out}}") {
46
+ return BigInt(amountInWei);
47
+ }
48
+ // Convert numeric strings to BigInt for uint256 args
49
+ if (/^\d+$/.test(arg)) {
50
+ return BigInt(arg);
51
+ }
52
+ return arg;
53
+ });
54
+ return encodeFunctionData({
55
+ abi,
56
+ functionName: config.functionName,
57
+ args: processedArgs,
58
+ });
59
+ }
60
+ catch (e) {
61
+ console.error("Failed to encode function data:", e, {
62
+ functionAbi: config.functionAbi,
63
+ functionName: config.functionName,
64
+ functionArgs: config.functionArgs,
65
+ });
66
+ return undefined;
67
+ }
68
+ }
26
69
  // This hook serves for order hype_duel and custom_exact_in
27
- export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, loadOrder, onOrderSuccess, onTransactionSuccess, sourceTokenAddress, sourceTokenChainId, destinationTokenAddress, destinationTokenChainId, slippage = 0, disableUrlParamManagement = false, orderType = "hype_duel", }) {
70
+ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, loadOrder, onOrderSuccess, onTransactionSuccess, sourceTokenAddress, sourceTokenChainId, destinationTokenAddress, destinationTokenChainId, slippage = 0, disableUrlParamManagement = false, orderType = "hype_duel", customExactInConfig, }) {
28
71
  const searchParams = useSearchParamsSSR();
29
72
  const router = useRouter();
30
73
  // Panel and order state
@@ -39,6 +82,8 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
39
82
  const [selectedDstToken, setSelectedDstToken] = useState(defaultDstToken);
40
83
  const [srcAmount, setSrcAmount] = useState(paymentType === "fiat" ? "5" : "0");
41
84
  const [dstAmount, setDstAmount] = useState("");
85
+ const [dstAmountInput, setDstAmountInput] = useState(""); // User input for destination amount (EXACT_OUTPUT mode)
86
+ const [debouncedDstAmountInput, setDebouncedDstAmountInput] = useState(""); // Debounced version for quote requests
42
87
  const [isSrcInputDirty, setIsSrcInputDirty] = useState(true);
43
88
  // Derive destination chain ID from token or prop (cannot change)
44
89
  const selectedDstChainId = destinationTokenChainId || selectedDstToken.chainId;
@@ -116,6 +161,16 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
116
161
  };
117
162
  fetchDestinationToken();
118
163
  }, [destinationTokenAddress, destinationTokenChainId]);
164
+ // Check if destination token is ready (matches the expected address from props)
165
+ // This is important for EXACT_OUTPUT mode where we need correct decimals
166
+ const isDestinationTokenReady = !destinationTokenAddress || selectedDstToken.address.toLowerCase() === destinationTokenAddress.toLowerCase();
167
+ // Debounce destination amount input for quote requests (500ms delay)
168
+ useEffect(() => {
169
+ const timer = setTimeout(() => {
170
+ setDebouncedDstAmountInput(dstAmountInput);
171
+ }, 500);
172
+ return () => clearTimeout(timer);
173
+ }, [dstAmountInput]);
119
174
  // Helper function for onramp vendor mapping
120
175
  const getOnrampVendor = (paymentMethod) => {
121
176
  switch (paymentMethod) {
@@ -129,8 +184,16 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
129
184
  };
130
185
  // Get quote
131
186
  // For fiat payments, always use USDC decimals (6) regardless of selectedSrcToken
132
- const effectiveDecimals = paymentType === "fiat" ? USDC_BASE.decimals : selectedSrcToken.decimals;
133
- const activeInputAmountInWei = parseUnits(srcAmount.replace(/,/g, ""), effectiveDecimals).toString();
187
+ const effectiveSrcDecimals = paymentType === "fiat" ? USDC_BASE.decimals : selectedSrcToken.decimals;
188
+ const activeInputAmountInWei = parseUnits(srcAmount.replace(/,/g, ""), effectiveSrcDecimals).toString();
189
+ // Calculate output amount in wei for EXACT_OUTPUT mode
190
+ // Only calculate when destination token is ready (has correct decimals)
191
+ // Use debounced value to reduce quote API calls
192
+ const activeOutputAmountInWei = isDestinationTokenReady
193
+ ? parseUnits(debouncedDstAmountInput.replace(/,/g, "") || "0", selectedDstToken.decimals).toString()
194
+ : "0";
195
+ // Determine trade type based on which input was last edited
196
+ const tradeType = isSrcInputDirty ? "EXACT_INPUT" : "EXACT_OUTPUT";
134
197
  // Build quote request based on order type
135
198
  const quoteRequest = (() => {
136
199
  const baseParams = {
@@ -145,8 +208,8 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
145
208
  return {
146
209
  ...baseParams,
147
210
  type: "swap",
148
- tradeType: "EXACT_INPUT",
149
- amount: activeInputAmountInWei,
211
+ tradeType: tradeType,
212
+ amount: tradeType === "EXACT_INPUT" ? activeInputAmountInWei : activeOutputAmountInWei,
150
213
  };
151
214
  }
152
215
  else if (orderType === "hype_duel") {
@@ -157,6 +220,22 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
157
220
  };
158
221
  }
159
222
  else {
223
+ // custom_exact_in - for EXACT_OUTPUT, use custom type to get the quote
224
+ if (tradeType === "EXACT_OUTPUT") {
225
+ const encodedData = generateEncodedData(customExactInConfig, activeOutputAmountInWei);
226
+ return {
227
+ ...baseParams,
228
+ type: "custom",
229
+ payload: {
230
+ amount: activeOutputAmountInWei,
231
+ data: encodedData || "",
232
+ to: customExactInConfig ? normalizeAddress(customExactInConfig.to) : "",
233
+ spenderAddress: customExactInConfig?.spenderAddress
234
+ ? normalizeAddress(customExactInConfig.spenderAddress)
235
+ : undefined,
236
+ },
237
+ };
238
+ }
160
239
  return {
161
240
  ...baseParams,
162
241
  type: "custom_exact_in",
@@ -165,22 +244,40 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
165
244
  }
166
245
  })();
167
246
  const { anyspendQuote, isLoadingAnyspendQuote, getAnyspendQuoteError } = useAnyspendQuote(quoteRequest);
247
+ // Combined loading state: includes debounce waiting period and actual quote fetching
248
+ // For EXACT_OUTPUT mode, also check if we're waiting for debounce
249
+ const isDebouncingDstAmount = tradeType === "EXACT_OUTPUT" && dstAmountInput !== debouncedDstAmountInput;
250
+ const isQuoteLoading = isLoadingAnyspendQuote || isDebouncingDstAmount;
168
251
  // Get geo options for fiat
169
252
  const { geoData, coinbaseAvailablePaymentMethods, stripeWeb2Support } = useGeoOnrampOptions(paymentType === "fiat" ? formatUnits(activeInputAmountInWei, USDC_BASE.decimals) : "0");
170
- // Update destination amount when quote changes
253
+ // Update amounts when quote changes based on trade type
171
254
  useEffect(() => {
172
- if (anyspendQuote?.data?.currencyOut?.amount && anyspendQuote.data.currencyOut.currency?.decimals) {
173
- const amount = anyspendQuote.data.currencyOut.amount;
174
- const decimals = anyspendQuote.data.currencyOut.currency.decimals;
175
- // Apply slippage (0-100) - reduce amount by slippage percentageFixed slippage value
176
- const amountWithSlippage = (BigInt(amount) * BigInt(100 - slippage)) / BigInt(100);
177
- const formattedAmount = formatTokenAmount(amountWithSlippage, decimals, 6, false);
178
- setDstAmount(formattedAmount);
255
+ if (isSrcInputDirty) {
256
+ // EXACT_INPUT mode: update destination amount from quote
257
+ if (anyspendQuote?.data?.currencyOut?.amount && anyspendQuote.data.currencyOut.currency?.decimals) {
258
+ const amount = anyspendQuote.data.currencyOut.amount;
259
+ const decimals = anyspendQuote.data.currencyOut.currency.decimals;
260
+ // Apply slippage (0-100) - reduce amount by slippage percentage
261
+ const amountWithSlippage = (BigInt(amount) * BigInt(100 - slippage)) / BigInt(100);
262
+ const formattedAmount = formatTokenAmount(amountWithSlippage, decimals, 6, false);
263
+ setDstAmount(formattedAmount);
264
+ }
265
+ else {
266
+ setDstAmount("");
267
+ }
179
268
  }
180
269
  else {
181
- setDstAmount("");
270
+ // EXACT_OUTPUT mode: update source amount from quote
271
+ if (anyspendQuote?.data?.currencyIn?.amount && anyspendQuote.data.currencyIn.currency?.decimals) {
272
+ const amount = anyspendQuote.data.currencyIn.amount;
273
+ const decimals = anyspendQuote.data.currencyIn.currency.decimals;
274
+ const formattedAmount = formatTokenAmount(BigInt(amount), decimals, 6, false);
275
+ setSrcAmount(formattedAmount);
276
+ }
277
+ // Also set the display destination amount from the user input
278
+ setDstAmount(dstAmountInput);
182
279
  }
183
- }, [anyspendQuote, slippage]);
280
+ }, [anyspendQuote, slippage, isSrcInputDirty, dstAmountInput]);
184
281
  // Update useEffect for URL parameter to not override loadOrder
185
282
  useEffect(() => {
186
283
  if (loadOrder || disableUrlParamManagement)
@@ -265,8 +362,11 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
265
362
  setSrcAmount,
266
363
  dstAmount,
267
364
  setDstAmount,
365
+ dstAmountInput,
366
+ setDstAmountInput,
268
367
  isSrcInputDirty,
269
368
  setIsSrcInputDirty,
369
+ tradeType,
270
370
  // Payment methods
271
371
  cryptoPaymentMethod,
272
372
  setCryptoPaymentMethod,
@@ -287,8 +387,10 @@ export function useAnyspendFlow({ paymentType = "crypto", recipientAddress, load
287
387
  // Quote data
288
388
  anyspendQuote,
289
389
  isLoadingAnyspendQuote,
390
+ isQuoteLoading, // Combined loading state (includes debounce + quote fetching)
290
391
  getAnyspendQuoteError,
291
392
  activeInputAmountInWei,
393
+ activeOutputAmountInWei, // User's destination amount input in wei (for EXACT_OUTPUT mode)
292
394
  // Geo/onramp data
293
395
  geoData,
294
396
  coinbaseAvailablePaymentMethods,
@@ -35,7 +35,7 @@ export function useRecipientAddressState({ recipientAddressFromProps, walletAddr
35
35
  // selectedRecipientAddress: explicitly selected by user (undefined means no explicit selection)
36
36
  const [selectedRecipientAddress, setSelectedRecipientAddress] = useState(undefined);
37
37
  // The effective recipient address, derived on each render, respecting priority.
38
- const effectiveRecipientAddress = recipientAddressFromProps || selectedRecipientAddress || walletAddress || globalAddress;
38
+ const effectiveRecipientAddress = selectedRecipientAddress || recipientAddressFromProps || walletAddress || globalAddress;
39
39
  // Helper function to reset user's manual selection.
40
40
  const resetRecipientAddress = () => {
41
41
  setSelectedRecipientAddress(undefined);
@@ -3,7 +3,11 @@ export const getStatusDisplay = (order) => {
3
3
  const srcToken = order.metadata?.srcToken;
4
4
  const dstToken = order.metadata?.dstToken;
5
5
  const formattedSrcAmount = srcToken ? formatTokenAmount(BigInt(order.srcAmount), srcToken.decimals) : undefined;
6
- const actualDstAmount = order.settlement?.actualDstAmount;
6
+ // For custom orders, use payload.amount as fallback if actualDstAmount is not available
7
+ const actualDstAmount = order.settlement?.actualDstAmount ||
8
+ (order.type === "custom" || order.type === "custom_exact_in" || order.type === "deposit_first"
9
+ ? order.payload.amount?.toString()
10
+ : undefined);
7
11
  const formattedActualDstAmount = actualDstAmount && dstToken ? formatTokenAmount(BigInt(actualDstAmount), dstToken.decimals) : undefined;
8
12
  switch (order.status) {
9
13
  case "scanning_deposit_transaction": {
@@ -40,6 +44,7 @@ export const getStatusDisplay = (order) => {
40
44
  };
41
45
  case "executed": {
42
46
  const receivedText = formattedActualDstAmount && dstToken ? `Received ${formattedActualDstAmount} ${dstToken.symbol}` : undefined;
47
+ const actionText = order.metadata?.action || "Order";
43
48
  const { text, description } = order.type === "swap"
44
49
  ? { text: receivedText || "Swap Complete", description: "Your swap has been completed successfully." }
45
50
  : order.type === "mint_nft"
@@ -48,7 +53,12 @@ export const getStatusDisplay = (order) => {
48
53
  ? { text: "Tournament Joined", description: "You have joined the tournament" }
49
54
  : order.type === "fund_tournament"
50
55
  ? { text: "Tournament Funded", description: "You have funded the tournament" }
51
- : { text: receivedText || "Order Complete", description: "Your order has been completed" };
56
+ : order.type === "custom" || order.type === "custom_exact_in"
57
+ ? {
58
+ text: receivedText || `${actionText} Complete`,
59
+ description: "Your order has been completed successfully.",
60
+ }
61
+ : { text: receivedText || "Order Complete", description: "Your order has been completed" };
52
62
  return { text, status: "success", description };
53
63
  }
54
64
  case "refunding":
@@ -306,6 +306,8 @@ export interface AnySpendDepositUpsideProps extends BaseModalProps {
306
306
  depositContractAddress: string;
307
307
  /** Token to deposit */
308
308
  token: components["schemas"]["Token"];
309
+ /** The exact amount of destination tokens to receive, in wei. This will pre-fill the output amount and switch to an exact output swap. */
310
+ destinationTokenAmount?: string;
309
311
  /** Callback function called when the deposit is successful */
310
312
  onSuccess?: () => void;
311
313
  }
@@ -18,6 +18,7 @@ export interface AnySpendCustomExactInProps {
18
18
  sourceTokenChainId?: number;
19
19
  destinationToken: components["schemas"]["Token"];
20
20
  destinationChainId: number;
21
+ destinationTokenAmount?: string;
21
22
  onSuccess?: (amount: string) => void;
22
23
  onOpenCustomModal?: () => void;
23
24
  mainFooter?: React.ReactNode;
@@ -1,5 +1,5 @@
1
1
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
2
- export declare function AnySpendStakeUpsideExactIn({ loadOrder, mode, recipientAddress, sourceTokenAddress, sourceTokenChainId, stakingContractAddress, token, onSuccess, }: {
2
+ export declare function AnySpendStakeUpsideExactIn({ loadOrder, mode, recipientAddress, sourceTokenAddress, sourceTokenChainId, destinationTokenAmount, stakingContractAddress, token, onSuccess, }: {
3
3
  loadOrder?: string;
4
4
  mode?: "modal" | "page";
5
5
  recipientAddress: string;
@@ -7,5 +7,6 @@ export declare function AnySpendStakeUpsideExactIn({ loadOrder, mode, recipientA
7
7
  sourceTokenChainId?: number;
8
8
  stakingContractAddress: string;
9
9
  token: components["schemas"]["Token"];
10
+ destinationTokenAmount?: string;
10
11
  onSuccess?: (amount: string) => void;
11
12
  }): import("react/jsx-runtime").JSX.Element | null;
@@ -11,6 +11,19 @@ export declare enum PanelView {
11
11
  POINTS_DETAIL = 6,
12
12
  FEE_DETAIL = 7
13
13
  }
14
+ export type CustomExactInConfig = {
15
+ functionAbi: string;
16
+ functionName: string;
17
+ functionArgs: string[];
18
+ to: string;
19
+ spenderAddress?: string;
20
+ action?: string;
21
+ };
22
+ /**
23
+ * Generates encoded function data for custom contract calls.
24
+ * Handles amount placeholder replacement and BigInt conversion.
25
+ */
26
+ export declare function generateEncodedData(config: CustomExactInConfig | undefined, amountInWei: string): string | undefined;
14
27
  interface UseAnyspendFlowProps {
15
28
  paymentType?: "crypto" | "fiat";
16
29
  recipientAddress?: string;
@@ -25,8 +38,9 @@ interface UseAnyspendFlowProps {
25
38
  slippage?: number;
26
39
  disableUrlParamManagement?: boolean;
27
40
  orderType?: "hype_duel" | "custom_exact_in" | "swap";
41
+ customExactInConfig?: CustomExactInConfig;
28
42
  }
29
- export declare function useAnyspendFlow({ paymentType, recipientAddress, loadOrder, onOrderSuccess, onTransactionSuccess, sourceTokenAddress, sourceTokenChainId, destinationTokenAddress, destinationTokenChainId, slippage, disableUrlParamManagement, orderType, }: UseAnyspendFlowProps): {
43
+ export declare function useAnyspendFlow({ paymentType, recipientAddress, loadOrder, onOrderSuccess, onTransactionSuccess, sourceTokenAddress, sourceTokenChainId, destinationTokenAddress, destinationTokenChainId, slippage, disableUrlParamManagement, orderType, customExactInConfig, }: UseAnyspendFlowProps): {
30
44
  activePanel: PanelView;
31
45
  setActivePanel: import("react").Dispatch<import("react").SetStateAction<PanelView>>;
32
46
  orderId: string | undefined;
@@ -91,8 +105,11 @@ export declare function useAnyspendFlow({ paymentType, recipientAddress, loadOrd
91
105
  setSrcAmount: import("react").Dispatch<import("react").SetStateAction<string>>;
92
106
  dstAmount: string;
93
107
  setDstAmount: import("react").Dispatch<import("react").SetStateAction<string>>;
108
+ dstAmountInput: string;
109
+ setDstAmountInput: import("react").Dispatch<import("react").SetStateAction<string>>;
94
110
  isSrcInputDirty: boolean;
95
111
  setIsSrcInputDirty: import("react").Dispatch<import("react").SetStateAction<boolean>>;
112
+ tradeType: string;
96
113
  cryptoPaymentMethod: CryptoPaymentMethodType;
97
114
  setCryptoPaymentMethod: (method: CryptoPaymentMethodType) => void;
98
115
  selectedCryptoPaymentMethod: CryptoPaymentMethodType;
@@ -176,8 +193,10 @@ export declare function useAnyspendFlow({ paymentType, recipientAddress, loadOrd
176
193
  statusCode: number;
177
194
  } | undefined;
178
195
  isLoadingAnyspendQuote: boolean;
196
+ isQuoteLoading: boolean;
179
197
  getAnyspendQuoteError: Error | null;
180
198
  activeInputAmountInWei: string;
199
+ activeOutputAmountInWei: string;
181
200
  geoData: import("@b3dotfun/sdk/anyspend/react").GeoData | undefined;
182
201
  coinbaseAvailablePaymentMethods: {
183
202
  id?: string;
@@ -306,6 +306,8 @@ export interface AnySpendDepositUpsideProps extends BaseModalProps {
306
306
  depositContractAddress: string;
307
307
  /** Token to deposit */
308
308
  token: components["schemas"]["Token"];
309
+ /** The exact amount of destination tokens to receive, in wei. This will pre-fill the output amount and switch to an exact output swap. */
310
+ destinationTokenAmount?: string;
309
311
  /** Callback function called when the deposit is successful */
310
312
  onSuccess?: () => void;
311
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.2-alpha.2",
3
+ "version": "0.1.2-alpha.4",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -12,15 +12,15 @@ import {
12
12
  } from "@b3dotfun/sdk/global-account/react";
13
13
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
14
14
  import { formatUnits } from "@b3dotfun/sdk/shared/utils/number";
15
- import type { AnySpendCustomExactInClasses } from "./types/classes";
16
15
  import invariant from "invariant";
17
16
  import { ArrowDown, Loader2 } from "lucide-react";
18
17
  import { motion } from "motion/react";
19
18
  import { useEffect, useMemo, useRef } from "react";
19
+ import type { AnySpendCustomExactInClasses } from "./types/classes";
20
20
 
21
21
  import { useSetActiveWallet } from "thirdweb/react";
22
22
  import { B3_TOKEN } from "../../constants";
23
- import { PanelView, useAnyspendFlow } from "../hooks/useAnyspendFlow";
23
+ import { generateEncodedData, PanelView, useAnyspendFlow } from "../hooks/useAnyspendFlow";
24
24
  import { AnySpendFingerprintWrapper, getFingerprintConfig } from "./AnySpendFingerprintWrapper";
25
25
  import { CryptoPaySection } from "./common/CryptoPaySection";
26
26
  import { CryptoPaymentMethod, CryptoPaymentMethodType } from "./common/CryptoPaymentMethod";
@@ -53,6 +53,7 @@ export interface AnySpendCustomExactInProps {
53
53
  sourceTokenChainId?: number;
54
54
  destinationToken: components["schemas"]["Token"];
55
55
  destinationChainId: number;
56
+ destinationTokenAmount?: string;
56
57
  onSuccess?: (amount: string) => void;
57
58
  onOpenCustomModal?: () => void;
58
59
  mainFooter?: React.ReactNode;
@@ -105,6 +106,7 @@ function AnySpendCustomExactInInner({
105
106
  customUsdInputValues,
106
107
  preferEoa,
107
108
  customExactInConfig,
109
+ destinationTokenAmount,
108
110
  orderType = "custom_exact_in",
109
111
  minDestinationAmount,
110
112
  header,
@@ -135,8 +137,11 @@ function AnySpendCustomExactInInner({
135
137
  srcAmount,
136
138
  setSrcAmount,
137
139
  dstAmount,
140
+ dstAmountInput,
141
+ setDstAmountInput,
138
142
  isSrcInputDirty,
139
143
  setIsSrcInputDirty,
144
+ tradeType,
140
145
  selectedCryptoPaymentMethod,
141
146
  effectiveCryptoPaymentMethod,
142
147
  setSelectedCryptoPaymentMethod,
@@ -150,7 +155,9 @@ function AnySpendCustomExactInInner({
150
155
  isBalanceLoading,
151
156
  anyspendQuote,
152
157
  isLoadingAnyspendQuote,
158
+ isQuoteLoading,
153
159
  activeInputAmountInWei,
160
+ activeOutputAmountInWei,
154
161
  geoData,
155
162
  coinbaseAvailablePaymentMethods,
156
163
  stripeWeb2Support,
@@ -170,6 +177,7 @@ function AnySpendCustomExactInInner({
170
177
  slippage: SLIPPAGE_PERCENT,
171
178
  disableUrlParamManagement: true,
172
179
  orderType,
180
+ customExactInConfig,
173
181
  });
174
182
 
175
183
  const { connectedEOAWallet } = useAccountWallet();
@@ -188,6 +196,18 @@ function AnySpendCustomExactInInner({
188
196
  }
189
197
  }, [preferEoa, connectedEOAWallet, setActiveWallet]);
190
198
 
199
+ // Prefill destination amount if provided (for EXACT_OUTPUT mode)
200
+ const appliedDestinationAmount = useRef(false);
201
+ useEffect(() => {
202
+ if (destinationTokenAmount && !appliedDestinationAmount.current) {
203
+ appliedDestinationAmount.current = true;
204
+ // Convert wei to human-readable format
205
+ const formattedAmount = formatUnits(destinationTokenAmount, destinationToken.decimals);
206
+ setDstAmountInput(formattedAmount);
207
+ setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
208
+ }
209
+ }, [destinationTokenAmount, destinationToken.decimals, setDstAmountInput, setIsSrcInputDirty]);
210
+
191
211
  const selectedRecipientOrDefault = selectedRecipientAddress ?? recipientAddress;
192
212
 
193
213
  const expectedDstAmountRaw = anyspendQuote?.data?.currencyOut?.amount ?? "0";
@@ -216,11 +236,15 @@ function AnySpendCustomExactInInner({
216
236
  };
217
237
 
218
238
  const btnInfo: { text: string; disable: boolean; error: boolean; loading: boolean } = useMemo(() => {
219
- if (activeInputAmountInWei === "0") return { text: "Enter an amount", disable: true, error: false, loading: false };
239
+ // Check for empty amount based on trade type
240
+ const isAmountEmpty =
241
+ tradeType === "EXACT_OUTPUT" ? !dstAmountInput || dstAmountInput === "0" : activeInputAmountInWei === "0";
242
+
243
+ if (isAmountEmpty) return { text: "Enter an amount", disable: true, error: false, loading: false };
220
244
  if (orderType === "hype_duel" && selectedSrcToken?.address?.toLowerCase() === B3_TOKEN.address.toLowerCase()) {
221
245
  return { text: "Convert to HYPE using B3", disable: false, error: false, loading: false };
222
246
  }
223
- if (isLoadingAnyspendQuote) return { text: "Loading quote...", disable: true, error: false, loading: true };
247
+ if (isQuoteLoading) return { text: "Loading quote...", disable: true, error: false, loading: true };
224
248
  if (isCreatingOrder || isCreatingOnrampOrder)
225
249
  return { text: "Creating order...", disable: true, error: false, loading: true };
226
250
  if (!selectedRecipientOrDefault) return { text: "Select recipient", disable: false, error: false, loading: false };
@@ -275,7 +299,7 @@ function AnySpendCustomExactInInner({
275
299
  return { text: "Continue", disable: false, error: false, loading: false };
276
300
  }, [
277
301
  activeInputAmountInWei,
278
- isLoadingAnyspendQuote,
302
+ isQuoteLoading,
279
303
  isCreatingOrder,
280
304
  isCreatingOnrampOrder,
281
305
  selectedRecipientOrDefault,
@@ -290,6 +314,8 @@ function AnySpendCustomExactInInner({
290
314
  DESTINATION_TOKEN_DETAILS.SYMBOL,
291
315
  orderType,
292
316
  selectedSrcToken,
317
+ tradeType,
318
+ dstAmountInput,
293
319
  ]);
294
320
 
295
321
  const onMainButtonClick = async () => {
@@ -405,12 +431,12 @@ function AnySpendCustomExactInInner({
405
431
  {paymentType === "crypto" && (
406
432
  <CryptoReceiveSection
407
433
  isDepositMode={false}
408
- isBuyMode={true}
434
+ isBuyMode={false}
409
435
  effectiveRecipientAddress={selectedRecipientOrDefault}
410
436
  recipientName={recipientName || undefined}
411
437
  customRecipientLabel={customRecipientLabel}
412
438
  onSelectRecipient={() => setActivePanel(PanelView.RECIPIENT_SELECTION)}
413
- dstAmount={dstAmount}
439
+ dstAmount={isSrcInputDirty ? dstAmount : dstAmountInput}
414
440
  dstToken={selectedDstToken}
415
441
  dstTokenSymbol={DESTINATION_TOKEN_DETAILS.SYMBOL}
416
442
  dstTokenLogoURI={DESTINATION_TOKEN_DETAILS.LOGO_URI}
@@ -420,7 +446,7 @@ function AnySpendCustomExactInInner({
420
446
  isSrcInputDirty={isSrcInputDirty}
421
447
  onChangeDstAmount={value => {
422
448
  setIsSrcInputDirty(false);
423
- setSrcAmount(value);
449
+ setDstAmountInput(value);
424
450
  }}
425
451
  anyspendQuote={anyspendQuote}
426
452
  onShowPointsDetail={() => setActivePanel(PanelView.POINTS_DETAIL)}
@@ -472,21 +498,51 @@ function AnySpendCustomExactInInner({
472
498
  invariant(anyspendQuote, "Relay price is not found");
473
499
  invariant(selectedRecipientOrDefault, "Recipient address is not found");
474
500
 
475
- const srcAmountBigInt = BigInt(activeInputAmountInWei);
476
- const payload = buildCustomPayload(selectedRecipientOrDefault);
477
-
478
- createOrder({
479
- recipientAddress: selectedRecipientOrDefault,
480
- orderType,
481
- srcChain: selectedSrcChainId,
482
- dstChain: selectedDstChainId,
483
- srcToken: selectedSrcToken,
484
- dstToken: selectedDstToken,
485
- srcAmount: srcAmountBigInt.toString(),
486
- expectedDstAmount: expectedDstAmountRaw,
487
- creatorAddress: globalAddress,
488
- payload,
489
- });
501
+ if (tradeType === "EXACT_OUTPUT") {
502
+ // EXACT_OUTPUT mode: create a custom order (like AnySpendStakeUpside)
503
+ const srcAmountFromQuote = anyspendQuote.data?.currencyIn?.amount;
504
+ invariant(srcAmountFromQuote, "Source amount from quote is not found");
505
+
506
+ const expectedDstAmount = anyspendQuote.data?.currencyOut?.amount ?? "0";
507
+ const encodedData = generateEncodedData(customExactInConfig, activeOutputAmountInWei);
508
+
509
+ createOrder({
510
+ recipientAddress: selectedRecipientOrDefault,
511
+ orderType: "custom",
512
+ srcChain: selectedSrcChainId,
513
+ dstChain: selectedDstChainId,
514
+ srcToken: selectedSrcToken,
515
+ dstToken: selectedDstToken,
516
+ srcAmount: srcAmountFromQuote,
517
+ expectedDstAmount,
518
+ creatorAddress: globalAddress,
519
+ payload: {
520
+ amount: activeOutputAmountInWei,
521
+ data: encodedData,
522
+ to: customExactInConfig ? normalizeAddress(customExactInConfig.to) : undefined,
523
+ spenderAddress: customExactInConfig?.spenderAddress
524
+ ? normalizeAddress(customExactInConfig.spenderAddress)
525
+ : undefined,
526
+ },
527
+ });
528
+ } else {
529
+ // EXACT_INPUT mode: create custom_exact_in order (original behavior)
530
+ const srcAmountBigInt = BigInt(activeInputAmountInWei);
531
+ const payload = buildCustomPayload(selectedRecipientOrDefault);
532
+
533
+ createOrder({
534
+ recipientAddress: selectedRecipientOrDefault,
535
+ orderType,
536
+ srcChain: selectedSrcChainId,
537
+ dstChain: selectedDstChainId,
538
+ srcToken: selectedSrcToken,
539
+ dstToken: selectedDstToken,
540
+ srcAmount: srcAmountBigInt.toString(),
541
+ expectedDstAmount: expectedDstAmountRaw,
542
+ creatorAddress: globalAddress,
543
+ payload,
544
+ });
545
+ }
490
546
  } catch (err: any) {
491
547
  console.error(err);
492
548
  toast.error("Failed to create order: " + err.message);
@@ -524,24 +580,52 @@ function AnySpendCustomExactInInner({
524
580
  return;
525
581
  }
526
582
 
527
- const payload = buildCustomPayload(selectedRecipientOrDefault);
528
-
529
- createOnrampOrder({
530
- recipientAddress: selectedRecipientOrDefault,
531
- orderType,
532
- dstChain: selectedDstChainId,
533
- dstToken: selectedDstToken,
534
- srcFiatAmount: srcAmount,
535
- onramp: {
536
- vendor,
537
- paymentMethod: paymentMethodString,
538
- country: geoData?.country || "US",
539
- redirectUrl: window.location.origin,
540
- },
541
- expectedDstAmount: expectedDstAmountRaw,
542
- creatorAddress: globalAddress,
543
- payload,
544
- });
583
+ const onrampOptions = {
584
+ vendor,
585
+ paymentMethod: paymentMethodString,
586
+ country: geoData?.country || "US",
587
+ redirectUrl: window.location.origin,
588
+ };
589
+
590
+ if (tradeType === "EXACT_OUTPUT") {
591
+ // EXACT_OUTPUT mode: create a custom order (like AnySpendStakeUpside)
592
+ const expectedDstAmount = anyspendQuote.data?.currencyOut?.amount ?? "0";
593
+ const encodedData = generateEncodedData(customExactInConfig, activeOutputAmountInWei);
594
+
595
+ createOnrampOrder({
596
+ recipientAddress: selectedRecipientOrDefault,
597
+ orderType: "custom",
598
+ dstChain: selectedDstChainId,
599
+ dstToken: selectedDstToken,
600
+ srcFiatAmount: srcAmount,
601
+ onramp: onrampOptions,
602
+ expectedDstAmount,
603
+ creatorAddress: globalAddress,
604
+ payload: {
605
+ amount: activeOutputAmountInWei,
606
+ data: encodedData,
607
+ to: customExactInConfig ? normalizeAddress(customExactInConfig.to) : undefined,
608
+ spenderAddress: customExactInConfig?.spenderAddress
609
+ ? normalizeAddress(customExactInConfig.spenderAddress)
610
+ : undefined,
611
+ },
612
+ });
613
+ } else {
614
+ // EXACT_INPUT mode: create custom_exact_in order (original behavior)
615
+ const payload = buildCustomPayload(selectedRecipientOrDefault);
616
+
617
+ createOnrampOrder({
618
+ recipientAddress: selectedRecipientOrDefault,
619
+ orderType,
620
+ dstChain: selectedDstChainId,
621
+ dstToken: selectedDstToken,
622
+ srcFiatAmount: srcAmount,
623
+ onramp: onrampOptions,
624
+ expectedDstAmount: expectedDstAmountRaw,
625
+ creatorAddress: globalAddress,
626
+ payload,
627
+ });
628
+ }
545
629
  } catch (err: any) {
546
630
  console.error(err);
547
631
  toast.error("Failed to create order: " + err.message);