diff --git a/apps/anyspend-demo-nextjs/next.config.js b/apps/anyspend-demo-nextjs/next.config.js index 71caa0c4..a906ba73 100644 --- a/apps/anyspend-demo-nextjs/next.config.js +++ b/apps/anyspend-demo-nextjs/next.config.js @@ -5,10 +5,12 @@ const nextConfig = { env: { NEXT_PUBLIC_THIRDWEB_CLIENT_ID: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID || "eb17a5ec4314526d42fc567821aeb9a6", NEXT_PUBLIC_GLOBAL_ACCOUNTS_PARTNER_ID: - process.env.NEXT_PUBLIC_GLOBAL_ACCOUNTS_PARTNER_ID || "ceba2f84-45ff-4717-b3e9-0acf0d062abd", + process.env.NEXT_PUBLIC_GLOBAL_ACCOUNTS_PARTNER_ID || "b9aac999-efef-4625-96d6-8043f20ec615", NEXT_PUBLIC_THIRDWEB_ECOSYSTEM_ID: process.env.NEXT_PUBLIC_THIRDWEB_ECOSYSTEM_ID || "ecosystem.b3dotfun", NEXT_PUBLIC_B3_API: process.env.NEXT_PUBLIC_B3_API || "https://b3-api-development.up.railway.app", NEXT_PUBLIC_ANYSPEND_BASE_URL: process.env.NEXT_PUBLIC_ANYSPEND_BASE_URL || "https://mainnet.anyspend.com", + NEXT_PUBLIC_DEVMODE_SHARED_SECRET: + process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET || "k1c4Ep6agmoejiBinKE70B6bzb8vSdm8", }, }; diff --git a/apps/anyspend-demo-nextjs/src/app/components/AnySpendOrderlyDepositButton.tsx b/apps/anyspend-demo-nextjs/src/app/components/AnySpendOrderlyDepositButton.tsx new file mode 100644 index 00000000..b5d4685d --- /dev/null +++ b/apps/anyspend-demo-nextjs/src/app/components/AnySpendOrderlyDepositButton.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { AnySpendOrderlyDeposit } from "@b3dotfun/sdk/anyspend/react"; +import { useAccountWallet } from "@b3dotfun/sdk/global-account/react"; +import { useState } from "react"; + +// Demo broker ID - replace with your actual broker ID from Orderly +const DEMO_BROKER_ID = "volt"; + +export function AnySpendOrderlyDepositButton() { + const { address } = useAccountWallet(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleOpenModal = () => { + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + const handleSuccess = (amount: string) => { + console.log("Orderly deposit successful! Amount:", amount); + }; + + return ( + <> + + + {/* Modal */} + {isModalOpen && ( +
+
+ {/* AnySpend Orderly Deposit Component */} + {address ? ( + + ) : ( +
+

Connect wallet to deposit

+ +
+ )} +
+
+ )} + + ); +} diff --git a/apps/anyspend-demo-nextjs/src/app/components/OrderlyDepositButton.tsx b/apps/anyspend-demo-nextjs/src/app/components/OrderlyDepositButton.tsx new file mode 100644 index 00000000..4b166bec --- /dev/null +++ b/apps/anyspend-demo-nextjs/src/app/components/OrderlyDepositButton.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { OrderlyDeposit } from "@b3dotfun/sdk/anyspend/react"; +import { useAccountWallet } from "@b3dotfun/sdk/global-account/react"; +import { useState } from "react"; + +// Demo broker ID - replace with your actual broker ID from Orderly +const DEMO_BROKER_ID = "volt"; + +// Available chains for the demo +const DEMO_CHAINS = [ + { id: 42161, name: "Arbitrum" }, + { id: 10, name: "Optimism" }, + { id: 8453, name: "Base" }, + { id: 1, name: "Ethereum" }, + { id: 137, name: "Polygon" }, +]; + +export function OrderlyDepositButton() { + const { address } = useAccountWallet(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedChainId, setSelectedChainId] = useState(42161); + const [amount, setAmount] = useState("10"); + + const handleOpenModal = () => { + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + const handleSuccess = (txHash: string) => { + console.log("Orderly deposit successful!", txHash); + // Optionally close modal after success + // setIsModalOpen(false); + }; + + const handleError = (error: Error) => { + console.error("Orderly deposit failed:", error); + }; + + return ( + <> + + + {/* Modal */} + {isModalOpen && ( +
+
+ {/* Header */} +
+

Orderly Deposit

+ +
+ + {/* Chain selector */} +
+ + +
+ + {/* Amount input */} +
+ + setAmount(e.target.value)} + placeholder="Enter amount" + className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + /> +
+ {["10", "50", "100", "500"].map(val => ( + + ))} +
+
+ + {/* Orderly Deposit Component */} + {address ? ( + + ) : ( +
+ Connect wallet to deposit +
+ )} +
+
+ )} + + ); +} diff --git a/apps/anyspend-demo-nextjs/src/app/page.tsx b/apps/anyspend-demo-nextjs/src/app/page.tsx index 8e2d7256..cf4bcb62 100644 --- a/apps/anyspend-demo-nextjs/src/app/page.tsx +++ b/apps/anyspend-demo-nextjs/src/app/page.tsx @@ -11,6 +11,8 @@ import { DepositHypeButton } from "./components/DepositHypeButton"; import { GetB3TokenButton } from "./components/GetB3TokenButton"; import { MintNftButton } from "./components/MintNftButton"; import { OrderDetailsButton } from "./components/OrderDetailsButton"; +import { OrderlyDepositButton } from "./components/OrderlyDepositButton"; +import { AnySpendOrderlyDepositButton } from "./components/AnySpendOrderlyDepositButton"; import { SignInButton } from "./components/SignInButton"; import { SignatureMintButton } from "./components/SignatureMintButton"; import { SignatureMintModal } from "./components/SignatureMintModal"; @@ -60,6 +62,8 @@ export default function Home() { + + diff --git a/packages/sdk/src/anyspend/constants/orderly.ts b/packages/sdk/src/anyspend/constants/orderly.ts new file mode 100644 index 00000000..084b8a85 --- /dev/null +++ b/packages/sdk/src/anyspend/constants/orderly.ts @@ -0,0 +1,369 @@ +import { keccak256, toBytes, encodeAbiParameters, parseAbiParameters } from "viem"; +import { components } from "../types/api"; + +/** + * Orderly Network Omnichain Configuration + * Supports deposits from any chain where Orderly has a vault + * + * @see https://orderly.network/docs/build-on-omnichain/addresses + */ + +export interface OrderlyChainConfig { + /** Chain ID */ + chainId: number; + /** Display name */ + name: string; + /** Vault contract address */ + vaultAddress: `0x${string}`; + /** USDC token address (or USDC.e for some chains) */ + usdcAddress: `0x${string}`; + /** USDC decimals (usually 6, but some chains may differ) */ + usdcDecimals: number; + /** Token symbol (USDC or USDC.e) */ + usdcSymbol: string; + /** Whether USDT is also supported */ + supportsUsdt?: boolean; + /** USDT address if supported */ + usdtAddress?: `0x${string}`; + /** Public RPC URL */ + rpcUrl: string; + /** Block explorer URL */ + explorerUrl: string; + /** Chain logo URI */ + logoUri?: string; +} + +/** + * All Orderly supported chains with their contract addresses + * Mainnet addresses only + */ +export const ORDERLY_CHAINS: Record = { + // Arbitrum One + 42161: { + chainId: 42161, + name: "Arbitrum", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + usdcDecimals: 6, + usdcSymbol: "USDC", + supportsUsdt: true, + usdtAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + rpcUrl: "https://arbitrum-one-rpc.publicnode.com", + explorerUrl: "https://arbiscan.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg", + }, + + // Optimism + 10: { + chainId: 10, + name: "Optimism", + vaultAddress: "0x816f722424b49cf1275cc86da9840fbd5a6167e9", + usdcAddress: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://optimism-rpc.publicnode.com", + explorerUrl: "https://optimistic.etherscan.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg", + }, + + // Base + 8453: { + chainId: 8453, + name: "Base", + vaultAddress: "0x816f722424b49cf1275cc86da9840fbd5a6167e9", + usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://base-rpc.publicnode.com", + explorerUrl: "https://basescan.org", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_base.jpg", + }, + + // Ethereum Mainnet + 1: { + chainId: 1, + name: "Ethereum", + vaultAddress: "0x816f722424b49cf1275cc86da9840fbd5a6167e9", + usdcAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + usdcDecimals: 6, + usdcSymbol: "USDC", + supportsUsdt: true, + usdtAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + rpcUrl: "https://ethereum-rpc.publicnode.com", + explorerUrl: "https://etherscan.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_ethereum.jpg", + }, + + // Mantle + 5000: { + chainId: 5000, + name: "Mantle", + vaultAddress: "0x816f722424b49cf1275cc86da9840fbd5a6167e9", + usdcAddress: "0x09bc4e0d864854c6afb6eb9a9cdf58ac190d0df9", + usdcDecimals: 6, + usdcSymbol: "USDC.e", + rpcUrl: "https://rpc.mantle.xyz", + explorerUrl: "https://mantlescan.xyz", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_mantle.jpg", + }, + + // Avalanche C-Chain + 43114: { + chainId: 43114, + name: "Avalanche", + vaultAddress: "0x816f722424b49cf1275cc86da9840fbd5a6167e9", + usdcAddress: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://avalanche-c-chain-rpc.publicnode.com", + explorerUrl: "https://snowtrace.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_avalanche.jpg", + }, + + // BNB Smart Chain + 56: { + chainId: 56, + name: "BNB Chain", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + usdcDecimals: 18, // BSC USDC has 18 decimals + usdcSymbol: "USDC", + supportsUsdt: true, + usdtAddress: "0x55d398326f99059fF775485246999027B3197955", + rpcUrl: "https://bsc-rpc.publicnode.com", + explorerUrl: "https://bscscan.com", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_binance.jpg", + }, + + // Polygon PoS + 137: { + chainId: 137, + name: "Polygon", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // Native USDC on Polygon + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://polygon-bor-rpc.publicnode.com", + explorerUrl: "https://polygonscan.com", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_polygon.jpg", + }, + + // SEI + 1329: { + chainId: 1329, + name: "SEI", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://evm-rpc.sei-apis.com", + explorerUrl: "https://seitrace.com", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg", + }, + + // Mode + 34443: { + chainId: 34443, + name: "Mode", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0xd988097fb8612cc24eeC14542bC03424c656005f", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://mainnet.mode.network", + explorerUrl: "https://modescan.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_mode.jpg", + }, + + // Abstract + 2741: { + chainId: 2741, + name: "Abstract", + vaultAddress: "0xE80F2396A266e898FBbD251b89CFE65B3e41fD18", // Different vault address! + usdcAddress: "0x84A71ccD554Cc1b02749b35d22F684CC8ec987e1", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://api.mainnet.abs.xyz", + explorerUrl: "https://abscan.org", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_abstract.jpg", + }, + + // Morph + 2818: { + chainId: 2818, + name: "Morph", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0xe34c91815d7fc18A9e2148bcD4241d0a5848b693", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://rpc.morphl2.io", + explorerUrl: "https://explorer.morphl2.io", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_morph.jpg", + }, + + // Sonic (formerly Fantom Sonic) + 146: { + chainId: 146, + name: "Sonic", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0x29219dd037d542be3f1a41f28cdafee7a38f5894", // Fixed truncated address + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://rpc.soniclabs.com", + explorerUrl: "https://sonicscan.org", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg", + }, + + // Berachain + 80094: { + chainId: 80094, + name: "Berachain", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0x549943e04f40284185054145c6e4e9568c1d3241", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://rpc.berachain.com", + explorerUrl: "https://berascan.com", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_berachain.jpg", + }, + + // Story + 1516: { + chainId: 1516, + name: "Story", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0xF1815bd50389c46847f0Bda824eC8da914045D14", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://mainnet.storyrpc.io", + explorerUrl: "https://storyscan.xyz", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_story.jpg", + }, + + // Plume + 98865: { + chainId: 98865, + name: "Plume", + vaultAddress: "0x816f722424B49Cf1275cc86DA9840Fbd5a6167e9", + usdcAddress: "0x78adD880A697070c1e765Ac44D65323a0DcCE913", + usdcDecimals: 6, + usdcSymbol: "USDC", + rpcUrl: "https://rpc.plume.org", + explorerUrl: "https://explorer.plume.org", + logoUri: "https://icons.llamao.fi/icons/chains/rsz_plume.jpg", + }, +} as const; + +/** + * Get list of all supported Orderly chain IDs + */ +export const ORDERLY_SUPPORTED_CHAIN_IDS = Object.keys(ORDERLY_CHAINS).map(Number); + +/** + * Primary/recommended chains for deposits (most liquid) + */ +export const ORDERLY_PRIMARY_CHAINS = [42161, 10, 8453, 1, 137, 56] as const; + +/** + * Default chain for Orderly deposits + */ +export const ORDERLY_DEFAULT_CHAIN_ID = 42161; // Arbitrum + +// Pre-computed hashes for efficiency +export const ORDERLY_HASHES = { + USDC_TOKEN_HASH: keccak256(toBytes("USDC")), + USDT_TOKEN_HASH: keccak256(toBytes("USDT")), +} as const; + +// Deposit fee buffer (5% like SDK) +export const ORDERLY_DEPOSIT_FEE_BUFFER = 105n; + +// Vault ABI for deposit and getDepositFee functions +export const ORDERLY_VAULT_ABI = [ + { + name: "deposit", + type: "function", + stateMutability: "payable", + inputs: [ + { + name: "depositData", + type: "tuple", + components: [ + { name: "accountId", type: "bytes32" }, + { name: "brokerHash", type: "bytes32" }, + { name: "tokenHash", type: "bytes32" }, + { name: "tokenAmount", type: "uint128" }, + ], + }, + ], + outputs: [], + }, + { + name: "getDepositFee", + type: "function", + stateMutability: "view", + inputs: [ + { name: "user", type: "address" }, + { + name: "depositData", + type: "tuple", + components: [ + { name: "accountId", type: "bytes32" }, + { name: "brokerHash", type: "bytes32" }, + { name: "tokenHash", type: "bytes32" }, + { name: "tokenAmount", type: "uint128" }, + ], + }, + ], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; + +/** + * Get Orderly chain config by chain ID + */ +export function getOrderlyChainConfig(chainId: number): OrderlyChainConfig | undefined { + return ORDERLY_CHAINS[chainId]; +} + +/** + * Check if a chain is supported by Orderly + */ +export function isOrderlyChainSupported(chainId: number): boolean { + return chainId in ORDERLY_CHAINS; +} + +/** + * Get USDC token definition for AnySpend by chain ID + */ +export function getOrderlyUsdcToken(chainId: number): components["schemas"]["Token"] | undefined { + const config = ORDERLY_CHAINS[chainId]; + if (!config) return undefined; + + return { + chainId: config.chainId, + address: config.usdcAddress, + symbol: config.usdcSymbol, + name: config.usdcSymbol === "USDC.e" ? "Bridged USD Coin" : "USD Coin", + decimals: config.usdcDecimals, + metadata: { + logoURI: + "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + }, + }; +} + +/** + * Compute Orderly accountId from wallet address and broker ID + * Formula: keccak256(abi.encode(address, keccak256(brokerId))) + */ +export function computeOrderlyAccountId(walletAddress: `0x${string}`, brokerId: string): `0x${string}` { + const brokerIdHash = keccak256(toBytes(brokerId)); + return keccak256(encodeAbiParameters(parseAbiParameters("address, bytes32"), [walletAddress, brokerIdHash])); +} + +/** + * Compute broker hash from broker ID + */ +export function computeBrokerHash(brokerId: string): `0x${string}` { + return keccak256(toBytes(brokerId)); +} diff --git a/packages/sdk/src/anyspend/index.ts b/packages/sdk/src/anyspend/index.ts index 91b8f24e..826666dd 100644 --- a/packages/sdk/src/anyspend/index.ts +++ b/packages/sdk/src/anyspend/index.ts @@ -14,6 +14,7 @@ export * from "./utils/validation"; // Constants export * from "./constants"; +export * from "./constants/orderly"; // Services export * from "./services/gas"; diff --git a/packages/sdk/src/anyspend/react/components/AnySpendCustomExactIn.tsx b/packages/sdk/src/anyspend/react/components/AnySpendCustomExactIn.tsx index 396f265c..299ca292 100644 --- a/packages/sdk/src/anyspend/react/components/AnySpendCustomExactIn.tsx +++ b/packages/sdk/src/anyspend/react/components/AnySpendCustomExactIn.tsx @@ -42,6 +42,8 @@ type CustomExactInConfig = { to: string; spenderAddress?: string; action?: string; + /** Native token value to send with the transaction (in wei as string) */ + value?: string; }; export interface AnySpendCustomExactInProps { @@ -232,6 +234,7 @@ function AnySpendCustomExactInInner({ ? normalizeAddress(customExactInConfig.spenderAddress) : undefined, action: customExactInConfig.action, + value: customExactInConfig.value, }; }; diff --git a/packages/sdk/src/anyspend/react/components/AnySpendDeposit.tsx b/packages/sdk/src/anyspend/react/components/AnySpendDeposit.tsx index d8f019d6..ab4383d3 100644 --- a/packages/sdk/src/anyspend/react/components/AnySpendDeposit.tsx +++ b/packages/sdk/src/anyspend/react/components/AnySpendDeposit.tsx @@ -33,6 +33,8 @@ export interface DepositContractConfig { spenderAddress?: string; /** Custom action label */ action?: string; + /** Native token value to send with the transaction (in wei as string) */ + value?: string; } export interface ChainConfig { diff --git a/packages/sdk/src/anyspend/react/components/AnySpendOrderlyDeposit.tsx b/packages/sdk/src/anyspend/react/components/AnySpendOrderlyDeposit.tsx new file mode 100644 index 00000000..911f6660 --- /dev/null +++ b/packages/sdk/src/anyspend/react/components/AnySpendOrderlyDeposit.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useMemo } from "react"; +import { useAccount } from "wagmi"; + +import { cn } from "@b3dotfun/sdk/shared/utils/cn"; + +import { + ORDERLY_HASHES, + getOrderlyChainConfig, + getOrderlyUsdcToken, + computeOrderlyAccountId, + computeBrokerHash, +} from "../../constants/orderly"; +import { useOrderlyDepositFee } from "../hooks/useOrderlyDepositFee"; +import { AnySpendDeposit, type DepositContractConfig } from "./AnySpendDeposit"; + +// Hardcoded to Arbitrum +const ORDERLY_CHAIN_ID = 42161; + +// Orderly vault depositTo function ABI - allows depositing on behalf of another user +// This is the key: depositTo validates accountId against the `receiver` param, not msg.sender +const ORDERLY_DEPOSIT_TO_ABI = JSON.stringify([ + { + name: "depositTo", + type: "function", + stateMutability: "payable", + inputs: [ + { name: "receiver", type: "address" }, + { + name: "data", + type: "tuple", + components: [ + { name: "accountId", type: "bytes32" }, + { name: "brokerHash", type: "bytes32" }, + { name: "tokenHash", type: "bytes32" }, + { name: "tokenAmount", type: "uint128" }, + ], + }, + ], + outputs: [], + }, +]); + +export interface AnySpendOrderlyDepositProps { + /** The broker ID for Orderly Network */ + brokerId: string; + /** The beneficiary wallet address (defaults to connected wallet) */ + beneficiaryAddress?: `0x${string}`; + /** Callback when the deposit succeeds */ + onSuccess?: (amount: string) => void; + /** Callback when user closes the modal */ + onClose?: () => void; + /** Display mode */ + mode?: "modal" | "page"; + /** Custom class name for the container */ + className?: string; + /** Minimum destination amount in USDC */ + minAmount?: number; + /** Payment type - crypto or fiat */ + paymentType?: "crypto" | "fiat"; + /** Custom header component */ + header?: React.ReactNode; +} + +/** + * AnySpendOrderlyDeposit - Deposit any token from any chain to Orderly Network on Arbitrum + * + * Uses AnySpend to swap any token to USDC and deposit directly to Orderly vault + * in a single transaction flow. Currently hardcoded to Arbitrum. + * + * @example + * ```tsx + * console.log("Deposited!", amount)} + * /> + * ``` + */ +export function AnySpendOrderlyDeposit({ + brokerId, + beneficiaryAddress, + onSuccess, + onClose, + mode = "modal", + className, + minAmount = 1, + paymentType, + header, +}: AnySpendOrderlyDepositProps) { + const { address: connectedAddress } = useAccount(); + + // Use beneficiary address or connected wallet + const effectiveAddress = beneficiaryAddress ?? connectedAddress; + + // Get Arbitrum chain config + const chainConfig = useMemo(() => getOrderlyChainConfig(ORDERLY_CHAIN_ID), []); + + // Get USDC token for Arbitrum + const destinationToken = useMemo(() => getOrderlyUsdcToken(ORDERLY_CHAIN_ID), []); + + // Compute Orderly identifiers for the deposit + const accountId = useMemo(() => { + if (!effectiveAddress) return undefined; + return computeOrderlyAccountId(effectiveAddress, brokerId); + }, [effectiveAddress, brokerId]); + + const brokerHash = useMemo(() => computeBrokerHash(brokerId), [brokerId]); + + // Fetch deposit fee from Orderly vault contract using minAmount as estimate + const { feeWithBufferWei } = useOrderlyDepositFee({ + walletAddress: effectiveAddress, + brokerId, + chainId: ORDERLY_CHAIN_ID, + amount: minAmount.toString(), // Use minAmount for fee estimation + }); + + // Build the deposit contract config for AnySpend + const depositContractConfig: DepositContractConfig | undefined = useMemo(() => { + if (!chainConfig || !effectiveAddress || !accountId || !brokerHash || !feeWithBufferWei) return undefined; + + // For tuple arguments, pass as a JSON object representing the tuple fields + // The tuple is: (bytes32 accountId, bytes32 brokerHash, bytes32 tokenHash, uint128 tokenAmount) + const tupleArg = JSON.stringify({ + accountId: accountId, + brokerHash: brokerHash, + tokenHash: ORDERLY_HASHES.USDC_TOKEN_HASH, + tokenAmount: "{{amount_out}}", // Will be replaced with actual swapped amount + }); + + return { + functionAbi: ORDERLY_DEPOSIT_TO_ABI, + functionName: "depositTo", + functionArgs: [effectiveAddress, tupleArg], // receiver address first, then tuple data + to: chainConfig.vaultAddress, + spenderAddress: chainConfig.vaultAddress, // USDC approval goes to vault + action: "Deposit to Orderly", + value: feeWithBufferWei.toString(), // Native token fee for LayerZero cross-chain messaging + }; + }, [chainConfig, effectiveAddress, accountId, brokerHash, feeWithBufferWei]); + + // Validation + if (!chainConfig) { + return
Failed to load Arbitrum chain configuration
; + } + + if (!destinationToken) { + return
Could not find USDC token for Arbitrum
; + } + + if (!effectiveAddress) { + return
Connect wallet to deposit
; + } + + if (!depositContractConfig) { + return
Loading deposit configuration...
; + } + + // Custom header for Orderly deposit + const defaultHeader = () => ( +
+
+

Deposit to Orderly

+

Swap any token and deposit to Arbitrum

+
+
+ ); + + return ( +
+ <>{header} : defaultHeader} + /> +
+ ); +} + +export default AnySpendOrderlyDeposit; diff --git a/packages/sdk/src/anyspend/react/components/OrderlyDeposit.tsx b/packages/sdk/src/anyspend/react/components/OrderlyDeposit.tsx new file mode 100644 index 00000000..f233392f --- /dev/null +++ b/packages/sdk/src/anyspend/react/components/OrderlyDeposit.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useAccount, useChainId, useSendTransaction, useSwitchChain, useWaitForTransactionReceipt } from "wagmi"; +import { encodeFunctionData, erc20Abi, parseUnits } from "viem"; +import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; + +import { cn } from "@b3dotfun/sdk/shared/utils/cn"; +import { ShinyButton, StyleRoot } from "@b3dotfun/sdk/global-account/react"; + +import { + ORDERLY_VAULT_ABI, + ORDERLY_HASHES, + computeOrderlyAccountId, + computeBrokerHash, + getOrderlyChainConfig, + isOrderlyChainSupported, +} from "../../constants/orderly"; +import { useOrderlyDepositFee } from "../hooks/useOrderlyDepositFee"; + +export interface OrderlyDepositProps { + /** The broker ID for Orderly Network */ + brokerId: string; + /** The chain ID to deposit on */ + chainId: number; + /** The amount to deposit in USDC (e.g., "100" for $100) */ + amount: string; + /** The beneficiary wallet address (defaults to connected wallet) */ + beneficiaryAddress?: `0x${string}`; + /** Callback when deposit succeeds */ + onSuccess?: (txHash: string) => void; + /** Callback when deposit fails */ + onError?: (error: Error) => void; + /** Custom button text */ + buttonText?: string; + /** Whether to show the fee breakdown */ + showFeeBreakdown?: boolean; + /** Custom class name for the container */ + className?: string; +} + +type DepositState = "idle" | "switching-chain" | "approving" | "depositing" | "success" | "error"; + +/** + * OrderlyDeposit - Single-screen deposit component for Orderly Network + * + * Receives chain and amount as props, fetches deposit fee automatically, + * and executes the deposit in one click. + * + * @example + * ```tsx + * console.log("Deposited!", txHash)} + * /> + * ``` + */ +export function OrderlyDeposit({ + brokerId, + chainId, + amount, + beneficiaryAddress, + onSuccess, + onError, + buttonText, + showFeeBreakdown = true, + className, +}: OrderlyDepositProps) { + const { address: connectedAddress } = useAccount(); + const currentChainId = useChainId(); + const { switchChainAsync } = useSwitchChain(); + + // Use beneficiary address or connected wallet + const effectiveAddress = beneficiaryAddress ?? connectedAddress; + + // State + const [depositState, setDepositState] = useState("idle"); + const [txHash, setTxHash] = useState<`0x${string}` | undefined>(); + const [errorMessage, setErrorMessage] = useState(); + + // Get chain config + const chainConfig = useMemo(() => getOrderlyChainConfig(chainId), [chainId]); + const isChainSupported = isOrderlyChainSupported(chainId); + const isOnCorrectChain = currentChainId === chainId; + + // Compute Orderly identifiers + const accountId = useMemo(() => { + if (!effectiveAddress) return undefined; + return computeOrderlyAccountId(effectiveAddress, brokerId); + }, [effectiveAddress, brokerId]); + + const brokerHash = useMemo(() => computeBrokerHash(brokerId), [brokerId]); + + // Fetch deposit fee + const { + feeWithBufferWei, + feeFormatted, + isLoading: isFeeLoading, + error: feeError, + } = useOrderlyDepositFee({ + walletAddress: effectiveAddress, + brokerId, + chainId, + amount, + }); + + // Wagmi hooks + const { sendTransactionAsync } = useSendTransaction(); + const { isSuccess: isTxConfirmed } = useWaitForTransactionReceipt({ + hash: txHash, + }); + + // Handle tx confirmation + useEffect(() => { + if (isTxConfirmed && txHash) { + setDepositState("success"); + onSuccess?.(txHash); + } + }, [isTxConfirmed, txHash, onSuccess]); + + // Validation + const isValidAmount = useMemo(() => { + const numAmount = parseFloat(amount); + return !isNaN(numAmount) && numAmount > 0; + }, [amount]); + + const canDeposit = useMemo(() => { + return ( + effectiveAddress && + accountId && + brokerHash && + chainConfig && + isValidAmount && + feeWithBufferWei && + !isFeeLoading && + depositState === "idle" + ); + }, [ + effectiveAddress, + accountId, + brokerHash, + chainConfig, + isValidAmount, + feeWithBufferWei, + isFeeLoading, + depositState, + ]); + + // Execute deposit + const executeDeposit = useCallback(async () => { + if (!effectiveAddress || !accountId || !brokerHash || !chainConfig || !feeWithBufferWei) { + return; + } + + setErrorMessage(undefined); + + try { + // Switch chain if needed + if (!isOnCorrectChain) { + setDepositState("switching-chain"); + await switchChainAsync({ chainId }); + } + + // Approve USDC + setDepositState("approving"); + const amountInUnits = parseUnits(amount, chainConfig.usdcDecimals); + + await sendTransactionAsync({ + to: chainConfig.usdcAddress, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [chainConfig.vaultAddress, amountInUnits], + }), + }); + + // Small delay to ensure approval is mined + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Deposit to vault + setDepositState("depositing"); + const depositTxHash = await sendTransactionAsync({ + to: chainConfig.vaultAddress, + value: feeWithBufferWei, + data: encodeFunctionData({ + abi: ORDERLY_VAULT_ABI, + functionName: "deposit", + args: [ + { + accountId: accountId, + brokerHash: brokerHash, + tokenHash: ORDERLY_HASHES.USDC_TOKEN_HASH, + tokenAmount: amountInUnits, + }, + ], + }), + }); + + setTxHash(depositTxHash); + } catch (err: any) { + console.error("Deposit error:", err); + setDepositState("error"); + setErrorMessage(err.shortMessage || err.message || "Deposit failed"); + onError?.(err); + } + }, [ + effectiveAddress, + accountId, + brokerHash, + chainConfig, + feeWithBufferWei, + isOnCorrectChain, + switchChainAsync, + chainId, + amount, + sendTransactionAsync, + onError, + ]); + + // Reset state + const reset = useCallback(() => { + setDepositState("idle"); + setTxHash(undefined); + setErrorMessage(undefined); + }, []); + + // Button text based on state + const getButtonText = () => { + if (buttonText && depositState === "idle") return buttonText; + + switch (depositState) { + case "switching-chain": + return `Switching to ${chainConfig?.name}...`; + case "approving": + return "Approving USDC..."; + case "depositing": + return "Depositing..."; + case "success": + return "Deposit Successful!"; + case "error": + return "Try Again"; + default: + if (!isOnCorrectChain) { + return `Switch to ${chainConfig?.name} & Deposit`; + } + return `Deposit ${amount} ${chainConfig?.usdcSymbol || "USDC"}`; + } + }; + + // Error states + if (!isChainSupported) { + return
Chain {chainId} is not supported by Orderly
; + } + + if (!effectiveAddress) { + return
Connect wallet to deposit
; + } + + const isProcessing = depositState !== "idle" && depositState !== "success" && depositState !== "error"; + + return ( + +
+ {/* Fee breakdown */} + {showFeeBreakdown && ( +
+
+
+ Amount + + {amount} {chainConfig?.usdcSymbol} + +
+
+ Network +
+ {chainConfig?.logoUri && } + {chainConfig?.name} +
+
+
+ Deposit Fee + + {isFeeLoading ? ( + + ) : feeFormatted ? ( + `~${parseFloat(feeFormatted).toFixed(6)} ETH` + ) : feeError ? ( + Error + ) : ( + "..." + )} + +
+
+
+ )} + + {/* Success state */} + {depositState === "success" && txHash && ( +
+ +
+

Deposit successful!

+ + View transaction + +
+
+ )} + + {/* Error state */} + {depositState === "error" && errorMessage && ( +
+ +

{errorMessage}

+
+ )} + + {/* Deposit button */} + +
+ {isProcessing && } + {depositState === "success" && } + {getButtonText()} +
+
+
+
+ ); +} + +export default OrderlyDeposit; diff --git a/packages/sdk/src/anyspend/react/components/index.ts b/packages/sdk/src/anyspend/react/components/index.ts index 3741bee3..3fa68529 100644 --- a/packages/sdk/src/anyspend/react/components/index.ts +++ b/packages/sdk/src/anyspend/react/components/index.ts @@ -24,6 +24,10 @@ export type { RecipientSelectionClasses, } from "./types/classes"; export { AnySpendDepositHype, HYPE_TOKEN_DETAILS } from "./AnyspendDepositHype"; +export { OrderlyDeposit } from "./OrderlyDeposit"; +export type { OrderlyDepositProps } from "./OrderlyDeposit"; +export { AnySpendOrderlyDeposit } from "./AnySpendOrderlyDeposit"; +export type { AnySpendOrderlyDepositProps } from "./AnySpendOrderlyDeposit"; export * from "./AnySpendFingerprintWrapper"; export { AnySpendNFT } from "./AnySpendNFT"; export { AnyspendSignatureMint } from "./AnyspendSignatureMint"; diff --git a/packages/sdk/src/anyspend/react/hooks/index.ts b/packages/sdk/src/anyspend/react/hooks/index.ts index dd3104ee..2737d9a7 100644 --- a/packages/sdk/src/anyspend/react/hooks/index.ts +++ b/packages/sdk/src/anyspend/react/hooks/index.ts @@ -15,4 +15,5 @@ export * from "./useRecipientAddressState"; export * from "./useSigMint"; export * from "./useStripeClientSecret"; export * from "./useStripeSupport"; +export * from "./useOrderlyDepositFee"; export * from "./useWatchTransfer"; diff --git a/packages/sdk/src/anyspend/react/hooks/useCreateDepositFirstOrder.ts b/packages/sdk/src/anyspend/react/hooks/useCreateDepositFirstOrder.ts index 73091c6e..68b3d8d9 100644 --- a/packages/sdk/src/anyspend/react/hooks/useCreateDepositFirstOrder.ts +++ b/packages/sdk/src/anyspend/react/hooks/useCreateDepositFirstOrder.ts @@ -50,6 +50,7 @@ export function useCreateDepositFirstOrder({ onSuccess, onError }: UseCreateDepo to: normalizeAddress(contractConfig.to), spenderAddress: contractConfig.spenderAddress ? normalizeAddress(contractConfig.spenderAddress) : undefined, action: contractConfig.action, + value: contractConfig.value, } : {}; diff --git a/packages/sdk/src/anyspend/react/hooks/useOrderlyDepositFee.ts b/packages/sdk/src/anyspend/react/hooks/useOrderlyDepositFee.ts new file mode 100644 index 00000000..0974f520 --- /dev/null +++ b/packages/sdk/src/anyspend/react/hooks/useOrderlyDepositFee.ts @@ -0,0 +1,231 @@ +"use client"; + +import { useCallback, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { createPublicClient, http, parseUnits, formatUnits } from "viem"; +import { + ORDERLY_VAULT_ABI, + ORDERLY_HASHES, + ORDERLY_DEPOSIT_FEE_BUFFER, + ORDERLY_DEFAULT_CHAIN_ID, + computeOrderlyAccountId, + computeBrokerHash, + getOrderlyChainConfig, + isOrderlyChainSupported, + type OrderlyChainConfig, +} from "../../constants/orderly"; + +/** + * Create a public client for a specific chain + */ +function createChainClient(chainConfig: OrderlyChainConfig) { + return createPublicClient({ + transport: http(chainConfig.rpcUrl), + }); +} + +export interface OrderlyDepositFeeParams { + /** The wallet address of the depositor */ + walletAddress?: `0x${string}`; + /** The broker ID for Orderly */ + brokerId: string; + /** The chain ID to deposit on (defaults to Arbitrum) */ + chainId?: number; + /** The amount to deposit (in USDC, e.g., "100" for $100) */ + amount?: string; + /** Whether to apply the 5% buffer (default: true) */ + applyBuffer?: boolean; +} + +export interface OrderlyDepositFeeResult { + /** The raw deposit fee in wei */ + feeWei: bigint | undefined; + /** The deposit fee with buffer applied (in wei) */ + feeWithBufferWei: bigint | undefined; + /** The deposit fee formatted in native token (ETH, AVAX, etc.) */ + feeFormatted: string | undefined; + /** The deposit fee with buffer formatted */ + feeWithBufferFormatted: string | undefined; + /** Whether the fee is loading */ + isLoading: boolean; + /** Error if fee fetch failed */ + error: Error | null; + /** The computed accountId for this wallet/broker combination */ + accountId: `0x${string}` | undefined; + /** The broker hash */ + brokerHash: `0x${string}` | undefined; + /** The chain config being used */ + chainConfig: OrderlyChainConfig | undefined; + /** Whether the chain is supported */ + isChainSupported: boolean; + /** Refetch the deposit fee */ + refetch: () => void; + /** Fetch fee for a specific amount (useful for dynamic calculations) */ + fetchFeeForAmount: ( + amountUsdc: string, + targetChainId?: number, + ) => Promise<{ + feeWei: bigint; + feeWithBufferWei: bigint; + }>; +} + +/** + * Hook to fetch the Orderly vault deposit fee for any supported chain + * + * @example + * ```tsx + * const { feeFormatted, feeWithBufferWei, isLoading, chainConfig } = useOrderlyDepositFee({ + * walletAddress: address, + * brokerId: "my_broker_id", + * chainId: 42161, // Arbitrum + * amount: "100", // $100 USDC + * }); + * + * // Display the fee + * console.log(`Deposit fee: ${feeFormatted} ${chainConfig?.name === 'Avalanche' ? 'AVAX' : 'ETH'}`); + * ``` + */ +export function useOrderlyDepositFee({ + walletAddress, + brokerId, + chainId = ORDERLY_DEFAULT_CHAIN_ID, + amount, + applyBuffer = true, +}: OrderlyDepositFeeParams): OrderlyDepositFeeResult { + // Get chain config + const chainConfig = useMemo(() => getOrderlyChainConfig(chainId), [chainId]); + const isChainSupported = useMemo(() => isOrderlyChainSupported(chainId), [chainId]); + + // Compute accountId and brokerHash + const accountId = useMemo(() => { + if (!walletAddress) return undefined; + return computeOrderlyAccountId(walletAddress, brokerId); + }, [walletAddress, brokerId]); + + const brokerHash = useMemo(() => { + return computeBrokerHash(brokerId); + }, [brokerId]); + + // Convert amount to USDC smallest units (using chain-specific decimals) + const amountInUnits = useMemo(() => { + if (!amount || parseFloat(amount) <= 0 || !chainConfig) return undefined; + try { + return parseUnits(amount, chainConfig.usdcDecimals); + } catch { + return undefined; + } + }, [amount, chainConfig]); + + // Fetch deposit fee from contract + const fetchDepositFee = useCallback(async (): Promise => { + if (!walletAddress || !accountId || !brokerHash || !amountInUnits || !chainConfig) { + throw new Error("Missing required parameters for fee calculation"); + } + + const client = createChainClient(chainConfig); + + const depositData = { + accountId: accountId, + brokerHash: brokerHash, + tokenHash: ORDERLY_HASHES.USDC_TOKEN_HASH, + tokenAmount: amountInUnits, + }; + + const fee = await client.readContract({ + address: chainConfig.vaultAddress, + abi: ORDERLY_VAULT_ABI, + functionName: "getDepositFee", + args: [walletAddress, depositData], + }); + + return fee as bigint; + }, [walletAddress, accountId, brokerHash, amountInUnits, chainConfig]); + + // Use react-query for caching and refetching + const { + data: feeWei, + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ["orderly-deposit-fee", walletAddress, brokerId, chainId, amount], + queryFn: fetchDepositFee, + enabled: !!walletAddress && !!accountId && !!amountInUnits && !!chainConfig, + staleTime: 30_000, // 30 seconds + refetchInterval: 60_000, // Refetch every minute + structuralSharing: false, // BigInt can't be serialized, disable structural sharing + }); + + // Calculate fee with buffer + const feeWithBufferWei = useMemo(() => { + if (!feeWei) return undefined; + return applyBuffer ? (feeWei * ORDERLY_DEPOSIT_FEE_BUFFER) / 100n : feeWei; + }, [feeWei, applyBuffer]); + + // Format fees for display + const feeFormatted = useMemo(() => { + if (!feeWei) return undefined; + return formatUnits(feeWei, 18); + }, [feeWei]); + + const feeWithBufferFormatted = useMemo(() => { + if (!feeWithBufferWei) return undefined; + return formatUnits(feeWithBufferWei, 18); + }, [feeWithBufferWei]); + + // Function to fetch fee for a specific amount on any chain + const fetchFeeForAmount = useCallback( + async (amountUsdc: string, targetChainId?: number): Promise<{ feeWei: bigint; feeWithBufferWei: bigint }> => { + const effectiveChainId = targetChainId ?? chainId; + const targetChainConfig = getOrderlyChainConfig(effectiveChainId); + + if (!walletAddress || !accountId || !brokerHash || !targetChainConfig) { + throw new Error("Wallet address, broker ID, and valid chain are required"); + } + + const client = createChainClient(targetChainConfig); + const units = parseUnits(amountUsdc, targetChainConfig.usdcDecimals); + + const depositData = { + accountId: accountId, + brokerHash: brokerHash, + tokenHash: ORDERLY_HASHES.USDC_TOKEN_HASH, + tokenAmount: units, + }; + + const fee = await client.readContract({ + address: targetChainConfig.vaultAddress, + abi: ORDERLY_VAULT_ABI, + functionName: "getDepositFee", + args: [walletAddress, depositData], + }); + + const feeValue = fee as bigint; + const feeWithBuffer = (feeValue * ORDERLY_DEPOSIT_FEE_BUFFER) / 100n; + + return { + feeWei: feeValue, + feeWithBufferWei: feeWithBuffer, + }; + }, + [walletAddress, accountId, brokerHash, chainId], + ); + + return { + feeWei, + feeWithBufferWei, + feeFormatted, + feeWithBufferFormatted, + isLoading, + error: error as Error | null, + accountId, + brokerHash, + chainConfig, + isChainSupported, + refetch, + fetchFeeForAmount, + }; +} + +export default useOrderlyDepositFee; diff --git a/packages/sdk/src/anyspend/types/api.ts b/packages/sdk/src/anyspend/types/api.ts index 79bb98d0..e6f51ea5 100644 --- a/packages/sdk/src/anyspend/types/api.ts +++ b/packages/sdk/src/anyspend/types/api.ts @@ -1905,6 +1905,11 @@ export interface components { * @example 0x58241893EF1f86C9fBd8109Cd44Ea961fDb474e1 */ spenderAddress?: string; + /** + * @description Native token value to send with the transaction (in wei) + * @example 1000000000000000000 + */ + value?: string; /** * @description Optional action identifier used for display purposes * @example stake B3