|
1 | 1 | "use client"; |
2 | | -import { fetchPositionsForOwner, PositionOrBundle } from "@orca-so/whirlpools"; |
3 | | -import { tickIndexToSqrtPrice } from "@orca-so/whirlpools-core"; |
4 | | -import { useCallback, useMemo, useState } from "react"; |
5 | | -import { createSolanaRpc, mainnet, address, devnet } from "@solana/kit"; |
6 | 2 |
|
7 | | -export default function Page() { |
8 | | - const [positions, setPositions] = useState<PositionOrBundle[]>([]); |
9 | | - const [owner, setOwner] = useState<string>(""); |
10 | | - const [tickIndex, setTickIndex] = useState<string>(""); |
11 | | - const [sqrtPrice, setSqrtPrice] = useState<bigint>(); |
12 | | - |
13 | | - const rpc = useMemo(() => { |
14 | | - if (!process.env.NEXT_PUBLIC_RPC_URL) { |
15 | | - console.error("NEXT_PUBLIC_RPC_URL is not set"); |
16 | | - return createSolanaRpc(devnet("https://api.devnet.solana.com")); |
| 3 | +import { swapInstructions, setWhirlpoolsConfig } from "@orca-so/whirlpools"; |
| 4 | +import { useWallet } from "./contexts/WalletContext"; |
| 5 | +import { useState, useEffect, useMemo } from "react"; |
| 6 | +import { WalletProvider } from "./contexts/WalletContext"; |
| 7 | +import { ConnectWalletButton } from "./components/ConnectWalletButton"; |
| 8 | +import { cn } from "@/lib/utils"; |
| 9 | +import { |
| 10 | + createSolanaRpc, |
| 11 | + address, |
| 12 | + Address, |
| 13 | + pipe, |
| 14 | + createTransactionMessage, |
| 15 | + setTransactionMessageFeePayerSigner, |
| 16 | + setTransactionMessageLifetimeUsingBlockhash, |
| 17 | + appendTransactionMessageInstructions, |
| 18 | + signAndSendTransactionMessageWithSigners, |
| 19 | + getBase58Decoder, |
| 20 | + Signature, |
| 21 | +} from "@solana/kit"; |
| 22 | + |
| 23 | +const SOL_MINT: Address = address( |
| 24 | + "So11111111111111111111111111111111111111112", |
| 25 | +); |
| 26 | +const POOL_ADDRESS: Address = address( |
| 27 | + "Bz7wxD47Y1pDQNAmT6SejSETj6o8SneWMUaFXERDB1fr", |
| 28 | +); |
| 29 | + |
| 30 | +interface SwapPageProps { |
| 31 | + account: NonNullable<ReturnType<typeof useWallet>["account"]>; |
| 32 | +} |
| 33 | + |
| 34 | +async function awaitTxConfirmation( |
| 35 | + rpcClient: any, |
| 36 | + signature: Signature, |
| 37 | + options?: { maxTimeMs?: number; pollIntervalMs?: number }, |
| 38 | +): Promise<boolean> { |
| 39 | + const maxTimeMs = options?.maxTimeMs ?? 90_000; |
| 40 | + const pollIntervalMs = options?.pollIntervalMs ?? 500; |
| 41 | + const startTime = Date.now(); |
| 42 | + while (Date.now() - startTime < maxTimeMs) { |
| 43 | + const startLoopTime = Date.now(); |
| 44 | + const status = await rpcClient.getSignatureStatuses([signature]).send(); |
| 45 | + const info = status.value[0]; |
| 46 | + if (info && info.err === null && info.confirmationStatus === "finalized") { |
| 47 | + return true; |
| 48 | + } |
| 49 | + const elapsedTime = Date.now() - startLoopTime; |
| 50 | + const remainingTime = pollIntervalMs - elapsedTime; |
| 51 | + if (remainingTime > 0) { |
| 52 | + await new Promise((resolve) => setTimeout(resolve, remainingTime)); |
| 53 | + } else { |
| 54 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 55 | + } |
| 56 | + } |
| 57 | + return false; |
| 58 | +} |
| 59 | + |
| 60 | +function SwapPage({ account }: SwapPageProps) { |
| 61 | + const { signer } = useWallet(); |
| 62 | + const [transactionStatus, setTransactionStatus] = useState<string>(""); |
| 63 | + const [isSwapping, setIsSwapping] = useState(false); |
| 64 | + const [solscanLink, setSolscanLink] = useState<string | null>(null); |
| 65 | + |
| 66 | + const rpc = useMemo( |
| 67 | + () => createSolanaRpc("https://api.devnet.solana.com"), |
| 68 | + [], |
| 69 | + ); |
| 70 | + |
| 71 | + const handleSwap = async () => { |
| 72 | + if (!account || !signer) { |
| 73 | + alert("Please connect wallet"); |
| 74 | + return; |
17 | 75 | } |
18 | | - return createSolanaRpc(mainnet(process.env.NEXT_PUBLIC_RPC_URL)); |
19 | | - }, [process.env.NEXT_PUBLIC_RPC_URL]); |
20 | 76 |
|
21 | | - const fetchPositions = useCallback(async () => { |
22 | | - const positions = await fetchPositionsForOwner( |
23 | | - rpc as any, |
24 | | - address(owner) as any, |
| 77 | + setIsSwapping(true); |
| 78 | + setSolscanLink(null); |
| 79 | + setTransactionStatus("Creating swap transaction..."); |
| 80 | + const { instructions } = await swapInstructions( |
| 81 | + rpc, |
| 82 | + { |
| 83 | + inputAmount: 100_000_000n, |
| 84 | + mint: SOL_MINT, |
| 85 | + }, |
| 86 | + POOL_ADDRESS, |
| 87 | + 100, |
| 88 | + signer, |
25 | 89 | ); |
26 | | - setPositions(positions); |
27 | | - }, [owner]); |
28 | 90 |
|
29 | | - const convertTickIndex = useCallback(() => { |
30 | | - const index = parseInt(tickIndex); |
31 | | - setSqrtPrice(tickIndexToSqrtPrice(index)); |
32 | | - }, [tickIndex]); |
| 91 | + try { |
| 92 | + const { value: latestBlockhash } = await rpc |
| 93 | + .getLatestBlockhash({ commitment: "confirmed" }) |
| 94 | + .send(); |
| 95 | + |
| 96 | + const message = pipe( |
| 97 | + createTransactionMessage({ version: 0 }), |
| 98 | + (m) => setTransactionMessageFeePayerSigner(signer, m), |
| 99 | + (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), |
| 100 | + (m) => appendTransactionMessageInstructions(instructions, m), |
| 101 | + ); |
| 102 | + |
| 103 | + setTransactionStatus("Signing and sending transaction..."); |
| 104 | + |
| 105 | + const signatureBytes = |
| 106 | + await signAndSendTransactionMessageWithSigners(message); |
| 107 | + |
| 108 | + const signature = getBase58Decoder().decode(signatureBytes) as Signature; |
| 109 | + |
| 110 | + setTransactionStatus(`Awaiting transaction confirmation...`); |
| 111 | + (async () => { |
| 112 | + const isFinalized = await awaitTxConfirmation(rpc, signature); |
| 113 | + if (isFinalized) { |
| 114 | + setTransactionStatus("finalized"); |
| 115 | + setSolscanLink(`https://solscan.io/tx/${signature}?cluster=devnet`); |
| 116 | + } else { |
| 117 | + setTransactionStatus(`Transaction failed: timed out.`); |
| 118 | + } |
| 119 | + })(); |
| 120 | + } catch (error) { |
| 121 | + console.error("Swap failed:", error); |
| 122 | + setTransactionStatus( |
| 123 | + `Transaction failed: ${error instanceof Error ? error.message : "Unknown error"}`, |
| 124 | + ); |
| 125 | + } finally { |
| 126 | + setIsSwapping(false); |
| 127 | + } |
| 128 | + }; |
| 129 | + |
| 130 | + return ( |
| 131 | + <div |
| 132 | + style={{ |
| 133 | + maxWidth: "600px", |
| 134 | + margin: "0 auto", |
| 135 | + padding: "24px", |
| 136 | + display: "flex", |
| 137 | + flexDirection: "column", |
| 138 | + gap: "24px", |
| 139 | + }} |
| 140 | + > |
| 141 | + <div style={{ textAlign: "center" }}> |
| 142 | + <h1 |
| 143 | + style={{ |
| 144 | + fontSize: "28px", |
| 145 | + fontWeight: "700", |
| 146 | + color: "#111827", |
| 147 | + margin: "0 0 8px 0", |
| 148 | + }} |
| 149 | + > |
| 150 | + Buy devUSDC |
| 151 | + </h1> |
| 152 | + <p style={{ color: "#6b7280", margin: 0 }}> |
| 153 | + Executes a single swap of 0.1 SOL → devUSDC |
| 154 | + </p> |
| 155 | + </div> |
| 156 | + |
| 157 | + <div |
| 158 | + style={{ |
| 159 | + backgroundColor: "white", |
| 160 | + borderRadius: "12px", |
| 161 | + padding: "24px", |
| 162 | + border: "1px solid #e5e7eb", |
| 163 | + boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)", |
| 164 | + }} |
| 165 | + > |
| 166 | + <button |
| 167 | + onClick={handleSwap} |
| 168 | + disabled={!account || isSwapping} |
| 169 | + className={cn( |
| 170 | + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", |
| 171 | + "bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400", |
| 172 | + "h-11 rounded-md px-8 w-full", |
| 173 | + )} |
| 174 | + > |
| 175 | + {isSwapping |
| 176 | + ? "Swapping..." |
| 177 | + : !account |
| 178 | + ? "Connect Wallet" |
| 179 | + : "Buy devUSDC for 0.1 SOL"} |
| 180 | + </button> |
| 181 | + |
| 182 | + {transactionStatus && ( |
| 183 | + <div |
| 184 | + style={{ |
| 185 | + marginTop: "16px", |
| 186 | + padding: "16px", |
| 187 | + borderRadius: "8px", |
| 188 | + backgroundColor: transactionStatus.includes("failed") |
| 189 | + ? "#fef2f2" |
| 190 | + : "#eff6ff", |
| 191 | + border: transactionStatus.includes("failed") |
| 192 | + ? "1px solid #fca5a5" |
| 193 | + : "1px solid #93c5fd", |
| 194 | + color: transactionStatus.includes("failed") |
| 195 | + ? "#b91c1c" |
| 196 | + : "#1d4ed8", |
| 197 | + }} |
| 198 | + > |
| 199 | + {transactionStatus === "finalized" && solscanLink ? ( |
| 200 | + <span> |
| 201 | + Confirmed! View details on{" "} |
| 202 | + <a |
| 203 | + href={solscanLink} |
| 204 | + target="_blank" |
| 205 | + rel="noreferrer" |
| 206 | + style={{ textDecoration: "underline" }} |
| 207 | + > |
| 208 | + Solscan |
| 209 | + </a> |
| 210 | + </span> |
| 211 | + ) : ( |
| 212 | + transactionStatus |
| 213 | + )} |
| 214 | + </div> |
| 215 | + )} |
| 216 | + </div> |
| 217 | + </div> |
| 218 | + ); |
| 219 | +} |
| 220 | + |
| 221 | +function PageContent() { |
| 222 | + const { account } = useWallet(); |
33 | 223 |
|
34 | 224 | return ( |
35 | | - <div> |
36 | | - <p> |
37 | | - <input |
38 | | - type="number" |
39 | | - value={tickIndex} |
40 | | - onChange={(e) => setTickIndex(e.target.value)} |
41 | | - />{" "} |
42 | | - <button onClick={() => convertTickIndex()}>Convert</button>{" "} |
43 | | - {sqrtPrice !== undefined && <>Sqrt Price: {sqrtPrice.toString()}</>} |
44 | | - </p> |
45 | | - <p> |
46 | | - <input |
47 | | - type="text" |
48 | | - value={owner} |
49 | | - onChange={(e) => setOwner(e.target.value)} |
50 | | - />{" "} |
51 | | - <button onClick={() => fetchPositions()}>Fetch Positions</button>{" "} |
52 | | - {positions.length > 0 && <>{positions.length} positions found</>} |
53 | | - </p> |
| 225 | + <div |
| 226 | + style={{ |
| 227 | + minHeight: "100vh", |
| 228 | + backgroundColor: "#f9fafb", |
| 229 | + padding: "32px 24px", |
| 230 | + }} |
| 231 | + > |
| 232 | + <div |
| 233 | + style={{ |
| 234 | + maxWidth: "800px", |
| 235 | + margin: "0 auto", |
| 236 | + }} |
| 237 | + > |
| 238 | + <div |
| 239 | + style={{ |
| 240 | + display: "flex", |
| 241 | + justifyContent: "center", |
| 242 | + marginBottom: "32px", |
| 243 | + }} |
| 244 | + > |
| 245 | + <ConnectWalletButton /> |
| 246 | + </div> |
| 247 | + {account ? ( |
| 248 | + <SwapPage account={account} /> |
| 249 | + ) : ( |
| 250 | + <div style={{ textAlign: "center", marginTop: "48px" }}> |
| 251 | + <p style={{ color: "#6b7280", fontSize: "18px" }}> |
| 252 | + Please connect your wallet to start trading |
| 253 | + </p> |
| 254 | + </div> |
| 255 | + )} |
| 256 | + </div> |
54 | 257 | </div> |
55 | 258 | ); |
56 | 259 | } |
| 260 | + |
| 261 | +export default function Page() { |
| 262 | + useEffect(() => { |
| 263 | + setWhirlpoolsConfig("solanaDevnet"); |
| 264 | + }, []); |
| 265 | + |
| 266 | + return ( |
| 267 | + <WalletProvider> |
| 268 | + <PageContent /> |
| 269 | + </WalletProvider> |
| 270 | + ); |
| 271 | +} |
0 commit comments