Skip to content

Commit 743e758

Browse files
boosik-solcalintje
andauthored
[TKN-740, TKN-724] Improve tx-sender browser wallet compatibility and add Next.js swap example (#1076)
* draft: swap example * add comments * tailwind * build fix * changeset * lint * Fix build. Format. * Update swap example (#1109) * Update project README * Base * Enable env var * Improve how tx-sender works with browser wallets and add tx-sender to example * Fix tests * Add changeset for tx-sender * Format * Replace custom TxMessage type with Solana-native CompilableTransactionMessage across all transaction building utilities. Narrow feePayer type from TransactionSigner to KeyPairSigner | NoopSigner and use built-in isKeyPairSigner helper. Conditionally apply maxRetries only when resendOnPoll is enabled to optimize RPC usage. * Format * Fix test * Fmt. Fix yarn.lock. * Reduced deps in nextjs example --------- Co-authored-by: calintje <[email protected]> Co-authored-by: Calin <[email protected]>
1 parent ebd184d commit 743e758

30 files changed

+1187
-166
lines changed

.changeset/free-crews-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@orca-so/whirlpools-example-ts-next": patch
3+
---
4+
5+
Added Next.js example demonstrating browser wallet integration with swap functionality.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@orca-so/tx-sender": major
3+
---
4+
5+
Improve browser wallet compatibility and add configurable transaction sending strategy
6+
7+
**Browser Wallet Support:**
8+
- `buildTransaction` now accepts `TransactionSigner` (KeyPairSigner or NoopSigner) and automatically detects signer type
9+
- Performs partial signing for NoopSigner (browser wallets), full signing for KeyPairSigner (Node.js)
10+
- `sendTransaction` now accepts `(FullySignedTransaction | Transaction) & TransactionWithLifetime` to support both workflows
11+
12+
**Configurable RPC Usage:**
13+
- Added `pollIntervalMs` (default: 0) and `resendOnPoll` (default: true) options to `setRpc()`
14+
- Allows control over confirmation polling frequency and transaction resending behavior
15+
- Default settings optimized for premium RPCs; public RPC users can configure conservative settings
16+
17+
**Breaking Changes:**
18+
- `buildTransaction` no longer accepts `Address` string parameter. Must pass `TransactionSigner` instance to ensure same object is used for both instruction building and transaction building (required by Solana's `@solana/kit` identity checks).
19+
20+
**Migration:**
21+
```typescript
22+
// Before
23+
await buildTransaction(instructions, "7Td...zzc");
24+
25+
// After
26+
const noopSigner = createNoopSigner(address("7Td...zzc"));
27+
const { instructions } = await swapInstructions(rpc, params, pool, 100, noopSigner);
28+
await buildTransaction(instructions, noopSigner);
29+
```
30+

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/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NEXT_PUBLIC_RPC_URL=

examples/ts-sdk/next/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11
# Whirlpools Next.js Example
22

3-
This example demonstrates how to use the Whirlpools SDK in a Next.js application. Since the Orca SDK suite uses WebAssembly, the `experiments.asyncWebAssembly` feature of webpack must be enabled and `@orca-so/whirlpools-core` needs to be added as a `serverExternalPackages`. See `next.config.js` for more details.
3+
This example demonstrates how to use the Orca Whirlpools SDK in a Next.js application to build a token swap interface with wallet integration.
44

5+
## Features
6+
7+
- Wallet connection using Wallet Standard
8+
- Token swaps (SOL ↔ USDC) with Orca's concentrated liquidity pools
9+
- Real-time transaction status tracking
10+
11+
## Getting Started
12+
13+
After [building the repository](../../../README.md#getting-started), start the development server:
14+
15+
```bash
16+
yarn workspace @orca-so/whirlpools-example-ts-next start
17+
```
18+
19+
Navigate to [http://localhost:3000](http://localhost:3000).
20+
21+
## Using as a Standalone Project
22+
23+
This example references the local version of the Whirlpools SDK via the monorepo workspace (`"@orca-so/whirlpools": "*"`).
24+
25+
To use this as a standalone project outside the monorepo:
26+
27+
1. Copy this directory to your desired location
28+
2. Update `package.json` to use the published package version:
29+
```json
30+
"@orca-so/whirlpools": "^{current version}"
31+
```
32+
3. Run `npm install` or `yarn install`
33+
4. Start the development server with `npm run start` or `yarn start`
34+
35+
## WebAssembly Configuration
36+
37+
The Orca SDK uses WebAssembly for performance-critical operations. See `next.config.js` for the required webpack configuration that enables `experiments.asyncWebAssembly` and adds `@orca-so/whirlpools-core` to `serverExternalPackages`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useWallets } from "@wallet-standard/react";
5+
import { useWallet } from "../contexts/WalletContext";
6+
import { WalletListModal } from "./WalletListModal";
7+
8+
export function ConnectWalletButton() {
9+
const [isModalOpen, setIsModalOpen] = useState(false);
10+
const wallets = useWallets();
11+
const { account, isConnected } = useWallet();
12+
13+
const solanaWallets = wallets.filter((wallet) =>
14+
wallet.chains.some((chain) => chain.startsWith("solana:")),
15+
);
16+
17+
if (isConnected && account) {
18+
return (
19+
<div className="flex items-center gap-2 px-3 py-2 bg-green-50 text-green-700 rounded-lg border border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800">
20+
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
21+
<span className="font-mono text-sm">
22+
{account.address.slice(0, 4)}...{account.address.slice(-4)}
23+
</span>
24+
</div>
25+
);
26+
}
27+
28+
return (
29+
<>
30+
<button
31+
onClick={() => setIsModalOpen(true)}
32+
className="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 bg-blue-600 text-white hover:bg-blue-700 h-10 px-4 py-2"
33+
>
34+
Connect Wallet
35+
</button>
36+
<WalletListModal
37+
isOpen={isModalOpen}
38+
onClose={() => setIsModalOpen(false)}
39+
wallets={solanaWallets}
40+
/>
41+
</>
42+
);
43+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"use client";
2+
3+
import {
4+
useConnect,
5+
useDisconnect,
6+
type UiWallet,
7+
} from "@wallet-standard/react";
8+
import { useEffect } from "react";
9+
import { useWallet } from "../contexts/WalletContext";
10+
11+
interface WalletListModalProps {
12+
isOpen: boolean;
13+
onClose: () => void;
14+
wallets: UiWallet[];
15+
}
16+
17+
export function WalletListModal({
18+
isOpen,
19+
onClose,
20+
wallets,
21+
}: WalletListModalProps) {
22+
useEffect(() => {
23+
if (isOpen) {
24+
document.body.style.overflow = "hidden";
25+
} else {
26+
document.body.style.overflow = "unset";
27+
}
28+
return () => {
29+
document.body.style.overflow = "unset";
30+
};
31+
}, [isOpen]);
32+
33+
if (!isOpen) return null;
34+
35+
return (
36+
<div className="fixed inset-0 z-50 flex items-center justify-center">
37+
{/* Backdrop */}
38+
<div className="fixed inset-0 bg-black/80" onClick={onClose} />
39+
40+
{/* Modal Content */}
41+
<div
42+
className="relative z-50 w-full max-w-lg m-4 bg-white p-6 shadow-lg duration-200 border border-gray-200 rounded-lg animate-in"
43+
onClick={(e) => e.stopPropagation()}
44+
>
45+
{/* Close Button */}
46+
<button
47+
onClick={onClose}
48+
className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
49+
>
50+
<svg
51+
width="15"
52+
height="15"
53+
viewBox="0 0 15 15"
54+
fill="none"
55+
xmlns="http://www.w3.org/2000/svg"
56+
>
57+
<path
58+
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
59+
fill="currentColor"
60+
fillRule="evenodd"
61+
clipRule="evenodd"
62+
/>
63+
</svg>
64+
<span className="sr-only">Close</span>
65+
</button>
66+
67+
{/* Header */}
68+
<div className="flex flex-col space-y-1.5 text-center sm:text-left mb-4">
69+
<h3 className="text-lg font-semibold leading-none tracking-tight text-gray-900">
70+
Connect Wallet
71+
</h3>
72+
<p className="text-sm text-gray-600">
73+
Select a wallet to connect to.
74+
</p>
75+
</div>
76+
77+
{/* Wallet List */}
78+
<div className="grid gap-2">
79+
{wallets.map((wallet) => (
80+
<WalletListItem
81+
key={wallet.name}
82+
wallet={wallet}
83+
onConnect={onClose}
84+
/>
85+
))}
86+
</div>
87+
</div>
88+
</div>
89+
);
90+
}
91+
92+
interface WalletItemProps {
93+
wallet: UiWallet;
94+
onConnect?: () => void;
95+
}
96+
97+
export const WalletListItem = ({ wallet, onConnect }: WalletItemProps) => {
98+
const [isConnecting, connect] = useConnect(wallet);
99+
const [isDisconnecting, disconnect] = useDisconnect(wallet);
100+
const { setConnectedWallet, isConnected } = useWallet();
101+
102+
useEffect(() => {
103+
if (isDisconnecting) {
104+
setConnectedWallet(null);
105+
}
106+
}, [isDisconnecting, setConnectedWallet]);
107+
108+
const handleConnect = async () => {
109+
try {
110+
const connectedAccount = await connect();
111+
if (!connectedAccount.length) {
112+
console.warn(`Connect to ${wallet.name} but there are no accounts.`);
113+
return connectedAccount;
114+
}
115+
116+
const first = connectedAccount[0];
117+
setConnectedWallet({ account: first, wallet });
118+
onConnect?.(); // Close modal after successful connection
119+
return connectedAccount;
120+
} catch (error) {
121+
console.error("Failed to connect wallet:", error);
122+
return [];
123+
}
124+
};
125+
126+
return (
127+
<button
128+
className="flex items-center justify-between w-full p-3 rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-blue-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
129+
onClick={isConnected ? disconnect : handleConnect}
130+
disabled={isConnecting}
131+
>
132+
<div className="flex items-center gap-3">
133+
{wallet.icon ? (
134+
<img
135+
src={wallet.icon}
136+
alt={wallet.name}
137+
className="w-8 h-8 rounded-full"
138+
/>
139+
) : (
140+
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center">
141+
<span className="text-sm font-medium text-gray-600">
142+
{wallet.name.charAt(0).toUpperCase()}
143+
</span>
144+
</div>
145+
)}
146+
<div className="text-left">
147+
<div className="font-medium text-gray-900">
148+
{isConnecting ? "Connecting..." : wallet.name}
149+
</div>
150+
<div className="text-sm text-gray-500">
151+
{isConnecting ? "Please wait..." : "Click to connect"}
152+
</div>
153+
</div>
154+
</div>
155+
{isConnecting && (
156+
<div className="w-4 h-4 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin" />
157+
)}
158+
</button>
159+
);
160+
};

0 commit comments

Comments
 (0)