Skip to content

Commit 8201409

Browse files
committed
feat: add wrapSol/unwrapSol helpers for wSOL operations
Add helper functions to easily wrap native SOL into Wrapped SOL (wSOL) and unwrap it back: @solana/client: - createWsolHelper(runtime) - Factory function to create wSOL helpers - WsolHelper.sendWrap({ amount, authority }) - Wrap SOL to wSOL - WsolHelper.sendUnwrap({ authority }) - Unwrap wSOL back to SOL - WsolHelper.fetchWsolBalance(owner) - Get wSOL balance - WsolHelper.deriveWsolAddress(owner) - Derive the wSOL ATA address - WRAPPED_SOL_MINT - The wSOL mint address constant - createWsolController() - Controller for React integration @solana/react-hooks: - useWrapSol() - Hook for wrapping/unwrapping SOL with status tracking Also adds WrapSolPanel example component to vite-react demo. Closes #116
1 parent cf3f247 commit 8201409

File tree

11 files changed

+1016
-0
lines changed

11 files changed

+1016
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@solana/client": minor
3+
"@solana/react-hooks": minor
4+
---
5+
6+
Add wrapSol/unwrapSol helpers for wSOL operations
7+
8+
Adds helper functions to easily wrap native SOL into Wrapped SOL (wSOL) and unwrap it back:
9+
10+
**@solana/client:**
11+
- `createWsolHelper(runtime)` - Factory function to create wSOL helpers
12+
- `WsolHelper.sendWrap({ amount, authority })` - Wrap SOL to wSOL
13+
- `WsolHelper.sendUnwrap({ authority })` - Unwrap wSOL back to SOL (closes the account)
14+
- `WsolHelper.fetchWsolBalance(owner)` - Get wSOL balance
15+
- `WsolHelper.deriveWsolAddress(owner)` - Derive the wSOL ATA address
16+
- `WRAPPED_SOL_MINT` - The wSOL mint address constant
17+
- `createWsolController()` - Controller for React integration
18+
19+
**@solana/react-hooks:**
20+
- `useWrapSol()` - Hook for wrapping/unwrapping SOL with status tracking
21+
22+
Example usage:
23+
```ts
24+
// Using the client helper
25+
const wsol = client.wsol;
26+
await wsol.sendWrap({ amount: 1_000_000_000n, authority: session });
27+
await wsol.sendUnwrap({ authority: session });
28+
29+
// Using the React hook
30+
const { wrap, unwrap, balance, isWrapping, isUnwrapping } = useWrapSol();
31+
await wrap({ amount: 1_000_000_000n });
32+
await unwrap({});
33+
```

examples/vite-react/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { StoreInspectorCard } from './components/StoreInspectorCard.tsx';
1818
import { TransactionPoolPanel } from './components/TransactionPoolPanel.tsx';
1919
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs.tsx';
2020
import { WalletControls } from './components/WalletControls.tsx';
21+
import { WrapSolPanel } from './components/WrapSolPanel.tsx';
2122

2223
const walletConnectors = [...phantom(), ...solflare(), ...backpack(), ...metamask(), ...autoDiscover()];
2324
const client = createClient({
@@ -82,6 +83,7 @@ function DemoApp() {
8283
<div className="grid gap-6 lg:grid-cols-2">
8384
<SolTransferForm />
8485
<SendTransactionCard />
86+
<WrapSolPanel />
8587
<SplTokenPanel />
8688
<StakePanel />
8789
<TransactionPoolPanel />
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { WRAPPED_SOL_MINT } from '@solana/client';
2+
import { useWalletSession, useWrapSol } from '@solana/react-hooks';
3+
import { type FormEvent, useState } from 'react';
4+
5+
import { Button } from './ui/button';
6+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
7+
import { Input } from './ui/input';
8+
9+
function formatError(error: unknown): string {
10+
if (error instanceof Error) {
11+
return error.message;
12+
}
13+
if (typeof error === 'string') {
14+
return error;
15+
}
16+
return JSON.stringify(error);
17+
}
18+
19+
function formatWsolBalance(balance: { amount: bigint; exists: boolean } | null, owner: string | null): string {
20+
if (!owner) {
21+
return 'Connect a wallet to see your wSOL balance.';
22+
}
23+
if (!balance) {
24+
return 'Loading balance...';
25+
}
26+
if (!balance.exists) {
27+
return '0 wSOL (no token account)';
28+
}
29+
const solAmount = Number(balance.amount) / 1_000_000_000;
30+
return `${solAmount.toFixed(9)} wSOL (${balance.amount.toString()} lamports)`;
31+
}
32+
33+
export function WrapSolPanel() {
34+
const session = useWalletSession();
35+
const [wrapAmount, setWrapAmount] = useState('0.1');
36+
const {
37+
balance,
38+
error,
39+
isFetching,
40+
isUnwrapping,
41+
isWrapping,
42+
owner,
43+
refresh,
44+
refreshing,
45+
resetUnwrap,
46+
resetWrap,
47+
unwrap,
48+
unwrapError,
49+
unwrapSignature,
50+
unwrapStatus,
51+
wrap,
52+
wrapError,
53+
wrapSignature,
54+
wrapStatus,
55+
status,
56+
} = useWrapSol();
57+
58+
const handleWrap = async (event: FormEvent<HTMLFormElement>) => {
59+
event.preventDefault();
60+
if (!session) {
61+
return;
62+
}
63+
const amountStr = wrapAmount.trim();
64+
if (!amountStr) {
65+
return;
66+
}
67+
const solAmount = parseFloat(amountStr);
68+
if (Number.isNaN(solAmount) || solAmount <= 0) {
69+
return;
70+
}
71+
// Convert SOL to lamports
72+
const lamports = BigInt(Math.floor(solAmount * 1_000_000_000));
73+
await wrap({ amount: lamports });
74+
await refresh();
75+
};
76+
77+
const handleUnwrap = async () => {
78+
if (!session) {
79+
return;
80+
}
81+
await unwrap({});
82+
await refresh();
83+
};
84+
85+
const isWalletConnected = Boolean(owner);
86+
const hasWsolBalance = balance?.exists && balance.amount > 0n;
87+
88+
const getWrapStatus = (): string => {
89+
if (!owner) {
90+
return 'Connect a wallet to wrap SOL.';
91+
}
92+
if (isWrapping || wrapStatus === 'loading') {
93+
return 'Wrapping SOL...';
94+
}
95+
if (wrapStatus === 'success' && wrapSignature) {
96+
return `Wrap successful! Signature: ${String(wrapSignature)}`;
97+
}
98+
if (wrapStatus === 'error' && wrapError) {
99+
return `Wrap failed: ${formatError(wrapError)}`;
100+
}
101+
return 'Enter an amount and click Wrap to convert SOL to wSOL.';
102+
};
103+
104+
const getUnwrapStatus = (): string => {
105+
if (!owner) {
106+
return '';
107+
}
108+
if (isUnwrapping || unwrapStatus === 'loading') {
109+
return 'Unwrapping wSOL...';
110+
}
111+
if (unwrapStatus === 'success' && unwrapSignature) {
112+
return `Unwrap successful! Signature: ${String(unwrapSignature)}`;
113+
}
114+
if (unwrapStatus === 'error' && unwrapError) {
115+
return `Unwrap failed: ${formatError(unwrapError)}`;
116+
}
117+
if (!hasWsolBalance) {
118+
return 'No wSOL to unwrap.';
119+
}
120+
return 'Click Unwrap to convert all wSOL back to SOL.';
121+
};
122+
123+
return (
124+
<Card aria-disabled={!isWalletConnected}>
125+
<CardHeader>
126+
<div className="space-y-1.5">
127+
<CardTitle>Wrapped SOL (wSOL)</CardTitle>
128+
<CardDescription>
129+
Wrap native SOL into wSOL and unwrap it back using the <code>useWrapSol</code> hook.
130+
</CardDescription>
131+
</div>
132+
</CardHeader>
133+
<CardContent className="space-y-4">
134+
<div className="grid gap-2 text-sm text-muted-foreground">
135+
<div>
136+
<span className="font-medium text-foreground">Mint:</span>{' '}
137+
<code className="break-all">{WRAPPED_SOL_MINT}</code>
138+
</div>
139+
<div>
140+
<span className="font-medium text-foreground">Owner:</span>{' '}
141+
{owner ? <code className="break-all">{owner}</code> : 'Connect a wallet'}
142+
</div>
143+
</div>
144+
145+
{/* Balance Section */}
146+
<div className="space-y-2">
147+
<div className="flex flex-wrap gap-2">
148+
<Button
149+
disabled={!isWalletConnected || refreshing || isFetching}
150+
onClick={() => void refresh()}
151+
type="button"
152+
variant="secondary"
153+
>
154+
{refreshing || isFetching ? 'Refreshing...' : 'Refresh Balance'}
155+
</Button>
156+
</div>
157+
<div aria-live="polite" className="log-panel">
158+
{status === 'error' && error
159+
? `Error: ${formatError(error)}`
160+
: formatWsolBalance(balance, owner)}
161+
</div>
162+
</div>
163+
164+
{/* Wrap Form */}
165+
<form className="grid gap-4" onSubmit={handleWrap}>
166+
<fieldset className="grid gap-4" disabled={!isWalletConnected}>
167+
<div className="space-y-2">
168+
<label htmlFor="wrap-amount">Amount (SOL)</label>
169+
<Input
170+
autoComplete="off"
171+
disabled={!owner}
172+
id="wrap-amount"
173+
min="0"
174+
onChange={(event) => setWrapAmount(event.target.value)}
175+
placeholder="0.1"
176+
step="0.000000001"
177+
type="number"
178+
value={wrapAmount}
179+
/>
180+
</div>
181+
<div className="flex flex-wrap gap-2">
182+
<Button disabled={!owner || isWrapping} type="submit">
183+
{isWrapping ? 'Wrapping...' : 'Wrap SOL'}
184+
</Button>
185+
<Button disabled={wrapStatus === 'idle'} onClick={resetWrap} type="button" variant="ghost">
186+
Reset
187+
</Button>
188+
</div>
189+
</fieldset>
190+
</form>
191+
<div aria-live="polite" className="log-panel">
192+
{getWrapStatus()}
193+
</div>
194+
195+
{/* Unwrap Section */}
196+
<div className="space-y-2 border-t pt-4">
197+
<h4 className="font-medium">Unwrap wSOL</h4>
198+
<p className="text-sm text-muted-foreground">
199+
Unwrapping closes your wSOL token account and returns all SOL to your wallet.
200+
</p>
201+
<div className="flex flex-wrap gap-2">
202+
<Button
203+
disabled={!owner || isUnwrapping || !hasWsolBalance}
204+
onClick={() => void handleUnwrap()}
205+
type="button"
206+
variant="destructive"
207+
>
208+
{isUnwrapping ? 'Unwrapping...' : 'Unwrap All wSOL'}
209+
</Button>
210+
<Button disabled={unwrapStatus === 'idle'} onClick={resetUnwrap} type="button" variant="ghost">
211+
Reset
212+
</Button>
213+
</div>
214+
</div>
215+
</CardContent>
216+
<CardFooter>
217+
<div aria-live="polite" className="log-panel w-full">
218+
{getUnwrapStatus()}
219+
</div>
220+
</CardFooter>
221+
</Card>
222+
);
223+
}

packages/client/src/client/createClient.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export function createClient(config: SolanaClientConfig): SolanaClient {
100100
get transaction() {
101101
return helpers.transaction;
102102
},
103+
get wsol() {
104+
return helpers.wsol;
105+
},
103106
prepareTransaction: helpers.prepareTransaction,
104107
watchers,
105108
};

packages/client/src/client/createClientHelpers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createSolTransferHelper, type SolTransferHelper } from '../features/sol
44
import { createSplTokenHelper, type SplTokenHelper, type SplTokenHelperConfig } from '../features/spl';
55
import { createStakeHelper, type StakeHelper } from '../features/stake';
66
import { createTransactionHelper, type TransactionHelper } from '../features/transactions';
7+
import { createWsolHelper, type WsolHelper } from '../features/wsol';
78
import type { SolanaClientRuntime } from '../rpc/types';
89
import {
910
type PrepareTransactionMessage,
@@ -72,6 +73,19 @@ function wrapStakeHelper(helper: StakeHelper, getFallback: () => Commitment): St
7273
};
7374
}
7475

76+
function wrapWsolHelper(helper: WsolHelper, getFallback: () => Commitment): WsolHelper {
77+
return {
78+
deriveWsolAddress: helper.deriveWsolAddress,
79+
fetchWsolBalance: (owner, commitment) => helper.fetchWsolBalance(owner, commitment ?? getFallback()),
80+
prepareWrap: (config) => helper.prepareWrap(withDefaultCommitment(config, getFallback)),
81+
prepareUnwrap: (config) => helper.prepareUnwrap(withDefaultCommitment(config, getFallback)),
82+
sendPreparedWrap: helper.sendPreparedWrap,
83+
sendPreparedUnwrap: helper.sendPreparedUnwrap,
84+
sendWrap: (config, options) => helper.sendWrap(withDefaultCommitment(config, getFallback), options),
85+
sendUnwrap: (config, options) => helper.sendUnwrap(withDefaultCommitment(config, getFallback), options),
86+
};
87+
}
88+
7589
function normaliseConfigValue(value: unknown): string | undefined {
7690
if (value === null || value === undefined) {
7791
return undefined;
@@ -101,6 +115,7 @@ export function createClientHelpers(runtime: SolanaClientRuntime, store: ClientS
101115
let solTransfer: SolTransferHelper | undefined;
102116
let stake: StakeHelper | undefined;
103117
let transaction: TransactionHelper | undefined;
118+
let wsol: WsolHelper | undefined;
104119

105120
const getSolTransfer = () => {
106121
if (!solTransfer) {
@@ -123,6 +138,13 @@ export function createClientHelpers(runtime: SolanaClientRuntime, store: ClientS
123138
return transaction;
124139
};
125140

141+
const getWsol = () => {
142+
if (!wsol) {
143+
wsol = wrapWsolHelper(createWsolHelper(runtime), getFallbackCommitment);
144+
}
145+
return wsol;
146+
};
147+
126148
function getSplTokenHelper(config: SplTokenHelperConfig): SplTokenHelper {
127149
const cacheKey = serialiseSplConfig(config);
128150
const cached = splTokenCache.get(cacheKey);
@@ -157,6 +179,9 @@ export function createClientHelpers(runtime: SolanaClientRuntime, store: ClientS
157179
get transaction() {
158180
return getTransaction();
159181
},
182+
get wsol() {
183+
return getWsol();
184+
},
160185
prepareTransaction: prepareTransactionWithRuntime,
161186
});
162187
}

0 commit comments

Comments
 (0)