Skip to content

Commit 971e824

Browse files
committed
Update swap example
1 parent ba63e74 commit 971e824

File tree

8 files changed

+274
-629
lines changed

8 files changed

+274
-629
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/rust/Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/rust-sdk/whirlpool_repositioning_bot/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/ts-sdk/next/app/page.tsx

Lines changed: 260 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,271 @@
11
"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";
62

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;
1775
}
18-
return createSolanaRpc(mainnet(process.env.NEXT_PUBLIC_RPC_URL));
19-
}, [process.env.NEXT_PUBLIC_RPC_URL]);
2076

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,
2589
);
26-
setPositions(positions);
27-
}, [owner]);
2890

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();
33223

34224
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>
54257
</div>
55258
);
56259
}
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

Comments
 (0)