@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
@@ -22,6 +22,7 @@ export function AnySpendStakeUpsideExactIn({
22
22
  recipientAddress,
23
23
  sourceTokenAddress,
24
24
  sourceTokenChainId,
25
+ destinationTokenAmount,
25
26
  stakingContractAddress,
26
27
  token,
27
28
  onSuccess,
@@ -33,6 +34,7 @@ export function AnySpendStakeUpsideExactIn({
33
34
  sourceTokenChainId?: number;
34
35
  stakingContractAddress: string;
35
36
  token: components["schemas"]["Token"];
37
+ destinationTokenAmount?: string;
36
38
  onSuccess?: (amount: string) => void;
37
39
  }) {
38
40
  if (!recipientAddress) return null;
@@ -65,6 +67,7 @@ export function AnySpendStakeUpsideExactIn({
65
67
  sourceTokenChainId={sourceTokenChainId}
66
68
  destinationToken={token}
67
69
  destinationChainId={base.id}
70
+ destinationTokenAmount={destinationTokenAmount}
68
71
  customExactInConfig={customExactInConfig}
69
72
  header={header}
70
73
  onSuccess={onSuccess}
@@ -498,13 +498,11 @@ export const OrderDetails = memo(function OrderDetails({
498
498
  }
499
499
 
500
500
  const expectedDstAmount =
501
- order.type === "mint_nft" ||
502
- order.type === "join_tournament" ||
503
- order.type === "fund_tournament" ||
504
- order.type === "custom" ||
505
- order.type === "deposit_first"
501
+ order.type === "mint_nft" || order.type === "join_tournament" || order.type === "fund_tournament"
506
502
  ? "0"
507
- : order.payload.expectedDstAmount.toString();
503
+ : order.type === "custom" || order.type === "deposit_first"
504
+ ? order.payload.amount?.toString() || "0"
505
+ : order.payload.expectedDstAmount.toString();
508
506
  const formattedExpectedDstAmount = formatTokenAmount(BigInt(expectedDstAmount), dstToken.decimals);
509
507
 
510
508
  const actualDstAmount = order.settlement?.actualDstAmount;
@@ -57,14 +57,13 @@ export const OrderDetailsCollapsible = memo(function OrderDetailsCollapsible({
57
57
  const setShowOrderDetails = onOpenChange || setInternalOpen;
58
58
 
59
59
  // Calculate expected amount if not provided
60
+ // For custom orders, use payload.amount as the expected destination amount
60
61
  const expectedDstAmount =
61
- order.type === "mint_nft" ||
62
- order.type === "join_tournament" ||
63
- order.type === "fund_tournament" ||
64
- order.type === "custom" ||
65
- order.type === "deposit_first"
62
+ order.type === "mint_nft" || order.type === "join_tournament" || order.type === "fund_tournament"
66
63
  ? "0"
67
- : order.payload.expectedDstAmount.toString();
64
+ : order.type === "custom" || order.type === "deposit_first"
65
+ ? order.payload.amount?.toString() || "0"
66
+ : order.payload.expectedDstAmount.toString();
68
67
 
69
68
  const finalFormattedExpectedDstAmount =
70
69
  formattedExpectedDstAmount || formatTokenAmount(BigInt(expectedDstAmount), dstToken.decimals);
@@ -149,6 +148,8 @@ export const OrderDetailsCollapsible = memo(function OrderDetailsCollapsible({
149
148
  {formatTokenAmount(BigInt(order.payload.expectedDstAmount), dstToken.decimals)} HYPE
150
149
  </div>
151
150
  </div>
151
+ ) : order.type === "custom" || order.type === "custom_exact_in" ? (
152
+ <span className="order-details-amount-text">{`~${finalFormattedExpectedDstAmount} ${dstToken.symbol}`}</span>
152
153
  ) : null}
153
154
 
154
155
  <div className="order-details-chain-info text-as-primary/50 flex items-center gap-2">
@@ -7,6 +7,7 @@ import {
7
7
  useGeoOnrampOptions,
8
8
  } from "@b3dotfun/sdk/anyspend/react";
9
9
  import { anyspendService } from "@b3dotfun/sdk/anyspend/services/anyspend";
10
+ import { normalizeAddress } from "@b3dotfun/sdk/anyspend/utils";
10
11
  import {
11
12
  toast,
12
13
  useAccountWallet,
@@ -18,7 +19,7 @@ import {
18
19
  import { formatTokenAmount, formatUnits } from "@b3dotfun/sdk/shared/utils/number";
19
20
  import { useEffect, useMemo, useState } from "react";
20
21
 
21
- import { parseUnits } from "viem";
22
+ import { encodeFunctionData, parseUnits } from "viem";
22
23
  import { base, mainnet } from "viem/chains";
23
24
  import { components } from "../../types/api";
24
25
  import { GetQuoteRequest } from "../../types/api_req_res";
@@ -40,6 +41,59 @@ export enum PanelView {
40
41
  FEE_DETAIL,
41
42
  }
42
43
 
44
+ export type CustomExactInConfig = {
45
+ functionAbi: string;
46
+ functionName: string;
47
+ functionArgs: string[];
48
+ to: string;
49
+ spenderAddress?: string;
50
+ action?: string;
51
+ };
52
+
53
+ /**
54
+ * Generates encoded function data for custom contract calls.
55
+ * Handles amount placeholder replacement and BigInt conversion.
56
+ */
57
+ export function generateEncodedData(config: CustomExactInConfig | undefined, amountInWei: string): string | undefined {
58
+ if (!config || !config.functionAbi || !config.functionName || !config.functionArgs) {
59
+ console.warn("customExactInConfig missing required fields for encoding:", {
60
+ hasConfig: !!config,
61
+ hasFunctionAbi: !!config?.functionAbi,
62
+ hasFunctionName: !!config?.functionName,
63
+ hasFunctionArgs: !!config?.functionArgs,
64
+ });
65
+ return undefined;
66
+ }
67
+
68
+ try {
69
+ const abi = JSON.parse(config.functionAbi);
70
+ const processedArgs = config.functionArgs.map(arg => {
71
+ // Replace amount placeholders ({{dstAmount}}, {{amount_out}}, etc.)
72
+ if (arg === "{{dstAmount}}" || arg === "{{amount_out}}") {
73
+ return BigInt(amountInWei);
74
+ }
75
+ // Convert numeric strings to BigInt for uint256 args
76
+ if (/^\d+$/.test(arg)) {
77
+ return BigInt(arg);
78
+ }
79
+ return arg;
80
+ });
81
+
82
+ return encodeFunctionData({
83
+ abi,
84
+ functionName: config.functionName,
85
+ args: processedArgs,
86
+ });
87
+ } catch (e) {
88
+ console.error("Failed to encode function data:", e, {
89
+ functionAbi: config.functionAbi,
90
+ functionName: config.functionName,
91
+ functionArgs: config.functionArgs,
92
+ });
93
+ return undefined;
94
+ }
95
+ }
96
+
43
97
  interface UseAnyspendFlowProps {
44
98
  paymentType?: "crypto" | "fiat";
45
99
  recipientAddress?: string;
@@ -54,6 +108,7 @@ interface UseAnyspendFlowProps {
54
108
  slippage?: number;
55
109
  disableUrlParamManagement?: boolean;
56
110
  orderType?: "hype_duel" | "custom_exact_in" | "swap";
111
+ customExactInConfig?: CustomExactInConfig;
57
112
  }
58
113
 
59
114
  // This hook serves for order hype_duel and custom_exact_in
@@ -70,6 +125,7 @@ export function useAnyspendFlow({
70
125
  slippage = 0,
71
126
  disableUrlParamManagement = false,
72
127
  orderType = "hype_duel",
128
+ customExactInConfig,
73
129
  }: UseAnyspendFlowProps) {
74
130
  const searchParams = useSearchParamsSSR();
75
131
  const router = useRouter();
@@ -89,6 +145,8 @@ export function useAnyspendFlow({
89
145
  const [selectedDstToken, setSelectedDstToken] = useState<components["schemas"]["Token"]>(defaultDstToken);
90
146
  const [srcAmount, setSrcAmount] = useState<string>(paymentType === "fiat" ? "5" : "0");
91
147
  const [dstAmount, setDstAmount] = useState<string>("");
148
+ const [dstAmountInput, setDstAmountInput] = useState<string>(""); // User input for destination amount (EXACT_OUTPUT mode)
149
+ const [debouncedDstAmountInput, setDebouncedDstAmountInput] = useState<string>(""); // Debounced version for quote requests
92
150
  const [isSrcInputDirty, setIsSrcInputDirty] = useState(true);
93
151
 
94
152
  // Derive destination chain ID from token or prop (cannot change)
@@ -183,6 +241,20 @@ export function useAnyspendFlow({
183
241
  fetchDestinationToken();
184
242
  }, [destinationTokenAddress, destinationTokenChainId]);
185
243
 
244
+ // Check if destination token is ready (matches the expected address from props)
245
+ // This is important for EXACT_OUTPUT mode where we need correct decimals
246
+ const isDestinationTokenReady =
247
+ !destinationTokenAddress || selectedDstToken.address.toLowerCase() === destinationTokenAddress.toLowerCase();
248
+
249
+ // Debounce destination amount input for quote requests (500ms delay)
250
+ useEffect(() => {
251
+ const timer = setTimeout(() => {
252
+ setDebouncedDstAmountInput(dstAmountInput);
253
+ }, 500);
254
+
255
+ return () => clearTimeout(timer);
256
+ }, [dstAmountInput]);
257
+
186
258
  // Helper function for onramp vendor mapping
187
259
  const getOnrampVendor = (paymentMethod: FiatPaymentMethod): "coinbase" | "stripe" | "stripe-web2" | undefined => {
188
260
  switch (paymentMethod) {
@@ -197,8 +269,18 @@ export function useAnyspendFlow({
197
269
 
198
270
  // Get quote
199
271
  // For fiat payments, always use USDC decimals (6) regardless of selectedSrcToken
200
- const effectiveDecimals = paymentType === "fiat" ? USDC_BASE.decimals : selectedSrcToken.decimals;
201
- const activeInputAmountInWei = parseUnits(srcAmount.replace(/,/g, ""), effectiveDecimals).toString();
272
+ const effectiveSrcDecimals = paymentType === "fiat" ? USDC_BASE.decimals : selectedSrcToken.decimals;
273
+ const activeInputAmountInWei = parseUnits(srcAmount.replace(/,/g, ""), effectiveSrcDecimals).toString();
274
+
275
+ // Calculate output amount in wei for EXACT_OUTPUT mode
276
+ // Only calculate when destination token is ready (has correct decimals)
277
+ // Use debounced value to reduce quote API calls
278
+ const activeOutputAmountInWei = isDestinationTokenReady
279
+ ? parseUnits(debouncedDstAmountInput.replace(/,/g, "") || "0", selectedDstToken.decimals).toString()
280
+ : "0";
281
+
282
+ // Determine trade type based on which input was last edited
283
+ const tradeType = isSrcInputDirty ? "EXACT_INPUT" : "EXACT_OUTPUT";
202
284
 
203
285
  // Build quote request based on order type
204
286
  const quoteRequest: GetQuoteRequest = (() => {
@@ -215,8 +297,8 @@ export function useAnyspendFlow({
215
297
  return {
216
298
  ...baseParams,
217
299
  type: "swap" as const,
218
- tradeType: "EXACT_INPUT" as const,
219
- amount: activeInputAmountInWei,
300
+ tradeType: tradeType as "EXACT_INPUT" | "EXACT_OUTPUT",
301
+ amount: tradeType === "EXACT_INPUT" ? activeInputAmountInWei : activeOutputAmountInWei,
220
302
  };
221
303
  } else if (orderType === "hype_duel") {
222
304
  return {
@@ -225,6 +307,23 @@ export function useAnyspendFlow({
225
307
  amount: activeInputAmountInWei,
226
308
  };
227
309
  } else {
310
+ // custom_exact_in - for EXACT_OUTPUT, use custom type to get the quote
311
+ if (tradeType === "EXACT_OUTPUT") {
312
+ const encodedData = generateEncodedData(customExactInConfig, activeOutputAmountInWei);
313
+
314
+ return {
315
+ ...baseParams,
316
+ type: "custom" as const,
317
+ payload: {
318
+ amount: activeOutputAmountInWei,
319
+ data: encodedData || "",
320
+ to: customExactInConfig ? normalizeAddress(customExactInConfig.to) : "",
321
+ spenderAddress: customExactInConfig?.spenderAddress
322
+ ? normalizeAddress(customExactInConfig.spenderAddress)
323
+ : undefined,
324
+ },
325
+ };
326
+ }
228
327
  return {
229
328
  ...baseParams,
230
329
  type: "custom_exact_in" as const,
@@ -235,26 +334,45 @@ export function useAnyspendFlow({
235
334
 
236
335
  const { anyspendQuote, isLoadingAnyspendQuote, getAnyspendQuoteError } = useAnyspendQuote(quoteRequest);
237
336
 
337
+ // Combined loading state: includes debounce waiting period and actual quote fetching
338
+ // For EXACT_OUTPUT mode, also check if we're waiting for debounce
339
+ const isDebouncingDstAmount = tradeType === "EXACT_OUTPUT" && dstAmountInput !== debouncedDstAmountInput;
340
+ const isQuoteLoading = isLoadingAnyspendQuote || isDebouncingDstAmount;
341
+
238
342
  // Get geo options for fiat
239
343
  const { geoData, coinbaseAvailablePaymentMethods, stripeWeb2Support } = useGeoOnrampOptions(
240
344
  paymentType === "fiat" ? formatUnits(activeInputAmountInWei, USDC_BASE.decimals) : "0",
241
345
  );
242
346
 
243
- // Update destination amount when quote changes
347
+ // Update amounts when quote changes based on trade type
244
348
  useEffect(() => {
245
- if (anyspendQuote?.data?.currencyOut?.amount && anyspendQuote.data.currencyOut.currency?.decimals) {
246
- const amount = anyspendQuote.data.currencyOut.amount;
247
- const decimals = anyspendQuote.data.currencyOut.currency.decimals;
248
-
249
- // Apply slippage (0-100) - reduce amount by slippage percentageFixed slippage value
250
- const amountWithSlippage = (BigInt(amount) * BigInt(100 - slippage)) / BigInt(100);
251
-
252
- const formattedAmount = formatTokenAmount(amountWithSlippage, decimals, 6, false);
253
- setDstAmount(formattedAmount);
349
+ if (isSrcInputDirty) {
350
+ // EXACT_INPUT mode: update destination amount from quote
351
+ if (anyspendQuote?.data?.currencyOut?.amount && anyspendQuote.data.currencyOut.currency?.decimals) {
352
+ const amount = anyspendQuote.data.currencyOut.amount;
353
+ const decimals = anyspendQuote.data.currencyOut.currency.decimals;
354
+
355
+ // Apply slippage (0-100) - reduce amount by slippage percentage
356
+ const amountWithSlippage = (BigInt(amount) * BigInt(100 - slippage)) / BigInt(100);
357
+
358
+ const formattedAmount = formatTokenAmount(amountWithSlippage, decimals, 6, false);
359
+ setDstAmount(formattedAmount);
360
+ } else {
361
+ setDstAmount("");
362
+ }
254
363
  } else {
255
- setDstAmount("");
364
+ // EXACT_OUTPUT mode: update source amount from quote
365
+ if (anyspendQuote?.data?.currencyIn?.amount && anyspendQuote.data.currencyIn.currency?.decimals) {
366
+ const amount = anyspendQuote.data.currencyIn.amount;
367
+ const decimals = anyspendQuote.data.currencyIn.currency.decimals;
368
+
369
+ const formattedAmount = formatTokenAmount(BigInt(amount), decimals, 6, false);
370
+ setSrcAmount(formattedAmount);
371
+ }
372
+ // Also set the display destination amount from the user input
373
+ setDstAmount(dstAmountInput);
256
374
  }
257
- }, [anyspendQuote, slippage]);
375
+ }, [anyspendQuote, slippage, isSrcInputDirty, dstAmountInput]);
258
376
 
259
377
  // Update useEffect for URL parameter to not override loadOrder
260
378
  useEffect(() => {
@@ -344,8 +462,11 @@ export function useAnyspendFlow({
344
462
  setSrcAmount,
345
463
  dstAmount,
346
464
  setDstAmount,
465
+ dstAmountInput,
466
+ setDstAmountInput,
347
467
  isSrcInputDirty,
348
468
  setIsSrcInputDirty,
469
+ tradeType,
349
470
  // Payment methods
350
471
  cryptoPaymentMethod,
351
472
  setCryptoPaymentMethod,
@@ -366,8 +487,10 @@ export function useAnyspendFlow({
366
487
  // Quote data
367
488
  anyspendQuote,
368
489
  isLoadingAnyspendQuote,
490
+ isQuoteLoading, // Combined loading state (includes debounce + quote fetching)
369
491
  getAnyspendQuoteError,
370
492
  activeInputAmountInWei,
493
+ activeOutputAmountInWei, // User's destination amount input in wei (for EXACT_OUTPUT mode)
371
494
  // Geo/onramp data
372
495
  geoData,
373
496
  coinbaseAvailablePaymentMethods,
@@ -62,7 +62,7 @@ export function useRecipientAddressState({
62
62
 
63
63
  // The effective recipient address, derived on each render, respecting priority.
64
64
  const effectiveRecipientAddress =
65
- recipientAddressFromProps || selectedRecipientAddress || walletAddress || globalAddress;
65
+ selectedRecipientAddress || recipientAddressFromProps || walletAddress || globalAddress;
66
66
 
67
67
  // Helper function to reset user's manual selection.
68
68
  const resetRecipientAddress = () => {
@@ -7,7 +7,12 @@ export const getStatusDisplay = (
7
7
  const srcToken = order.metadata?.srcToken;
8
8
  const dstToken = order.metadata?.dstToken;
9
9
  const formattedSrcAmount = srcToken ? formatTokenAmount(BigInt(order.srcAmount), srcToken.decimals) : undefined;
10
- const actualDstAmount = order.settlement?.actualDstAmount;
10
+ // For custom orders, use payload.amount as fallback if actualDstAmount is not available
11
+ const actualDstAmount =
12
+ order.settlement?.actualDstAmount ||
13
+ (order.type === "custom" || order.type === "custom_exact_in" || order.type === "deposit_first"
14
+ ? order.payload.amount?.toString()
15
+ : undefined);
11
16
  const formattedActualDstAmount =
12
17
  actualDstAmount && dstToken ? formatTokenAmount(BigInt(actualDstAmount), dstToken.decimals) : undefined;
13
18
 
@@ -51,6 +56,7 @@ export const getStatusDisplay = (
51
56
  case "executed": {
52
57
  const receivedText =
53
58
  formattedActualDstAmount && dstToken ? `Received ${formattedActualDstAmount} ${dstToken.symbol}` : undefined;
59
+ const actionText = (order.metadata as { action?: string })?.action || "Order";
54
60
  const { text, description } =
55
61
  order.type === "swap"
56
62
  ? { text: receivedText || "Swap Complete", description: "Your swap has been completed successfully." }
@@ -60,7 +66,12 @@ export const getStatusDisplay = (
60
66
  ? { text: "Tournament Joined", description: "You have joined the tournament" }
61
67
  : order.type === "fund_tournament"
62
68
  ? { text: "Tournament Funded", description: "You have funded the tournament" }
63
- : { text: receivedText || "Order Complete", description: "Your order has been completed" };
69
+ : order.type === "custom" || order.type === "custom_exact_in"
70
+ ? {
71
+ text: receivedText || `${actionText} Complete`,
72
+ description: "Your order has been completed successfully.",
73
+ }
74
+ : { text: receivedText || "Order Complete", description: "Your order has been completed" };
64
75
  return { text, status: "success", description };
65
76
  }
66
77
 
@@ -323,6 +323,8 @@ export interface AnySpendDepositUpsideProps extends BaseModalProps {
323
323
  depositContractAddress: string;
324
324
  /** Token to deposit */
325
325
  token: components["schemas"]["Token"];
326
+ /** The exact amount of destination tokens to receive, in wei. This will pre-fill the output amount and switch to an exact output swap. */
327
+ destinationTokenAmount?: string;
326
328
  /** Callback function called when the deposit is successful */
327
329
  onSuccess?: () => void;
328
330
  }