Skip to content

Commit 1ec29b6

Browse files
author
Jade Hamilton
committed
Revert "Revert "feat: solana nft demo (#1578)""
This reverts commit 9b84edc.
1 parent 9b84edc commit 1ec29b6

File tree

12 files changed

+522
-4
lines changed

12 files changed

+522
-4
lines changed

account-kit/react/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export * from "./useSignTypedData.js";
2222
export * from "./useSmartAccountClient.js";
2323
export * from "./useSolanaSigner.js";
2424
export * from "./useSolanaTransaction.js";
25+
export * from "./useSolanaConnection.js";
2526
export * from "./useUiConfig.js";
2627
export * from "./useUser.js";
2728
export * from "./useWaitForUserOperationTransaction.js";
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import * as solanaNetwork from "../solanaNetwork.js";
4+
import { SolanaSigner } from "@account-kit/signer";
5+
import { useSolanaSigner } from "./useSolanaSigner.js";
6+
import { getSolanaConnection, watchSolanaConnection } from "@account-kit/core";
7+
import { useSyncExternalStore } from "react";
8+
import { useAlchemyAccountContext } from "./useAlchemyAccountContext.js";
9+
10+
/**
11+
* Returned from the solana connection.
12+
*/
13+
export interface SolanaConnection {
14+
/** The solana signer used to send the transaction */
15+
readonly signer: SolanaSigner | null;
16+
/** The solana connection used to send the transaction */
17+
readonly connection: solanaNetwork.Connection | null;
18+
}
19+
20+
/**
21+
* The parameters for the SolanaConnectionHookParams hook.
22+
*/
23+
export type SolanaConnectionHookParams = {
24+
signer?: SolanaSigner;
25+
};
26+
27+
/**
28+
* This hook is used for establishing a connection to Solana and returns the connection object and the signer object.
29+
*
30+
* @example
31+
* ```ts
32+
const {connection} = useSolanaConnection();
33+
34+
* ```
35+
* @param {SolanaConnectionHookParams} opts Options for the hook to get setup
36+
* @returns {SolanaConnection} The transaction hook.
37+
*/
38+
export function useSolanaConnection(
39+
opts: SolanaConnectionHookParams = {}
40+
): SolanaConnection {
41+
const { config } = useAlchemyAccountContext();
42+
const fallbackSigner: null | SolanaSigner = useSolanaSigner();
43+
const connection =
44+
useSyncExternalStore(
45+
watchSolanaConnection(config),
46+
() => getSolanaConnection(config),
47+
() => getSolanaConnection(config)
48+
)?.connection || null;
49+
const signer: null | SolanaSigner = opts?.signer || fallbackSigner;
50+
51+
return {
52+
connection,
53+
signer,
54+
};
55+
}

account-kit/react/src/hooks/useSolanaTransaction.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
PublicKey,
99
SystemProgram,
1010
TransactionInstruction,
11+
VersionedTransaction,
1112
} from "@solana/web3.js";
1213
import { useSolanaSigner } from "./useSolanaSigner.js";
1314
import { getSolanaConnection, watchSolanaConnection } from "@account-kit/core";
@@ -20,9 +21,15 @@ export type SolanaTransactionParams =
2021
amount: number;
2122
toAddress: string;
2223
};
24+
preSend?: (
25+
transaction: VersionedTransaction
26+
) => Promise<VersionedTransaction>;
2327
}
2428
| {
2529
instructions: TransactionInstruction[];
30+
preSend?: (
31+
transaction: VersionedTransaction
32+
) => Promise<VersionedTransaction>;
2633
};
2734
/**
2835
* We wanted to make sure that this will be using the same useMutation that the
@@ -112,10 +119,14 @@ export function useSolanaTransaction(
112119
];
113120
const policyId =
114121
"policyId" in opts ? opts.policyId : backupConnection?.policyId;
115-
const transaction = policyId
122+
let transaction = policyId
116123
? await signer.addSponsorship(instructions, connection, policyId)
117124
: await signer.createTransfer(instructions, connection);
118125

126+
if (params.preSend) {
127+
transaction = await params.preSend(transaction);
128+
}
129+
119130
await signer.addSignature(transaction);
120131

121132
const hash = await solanaNetwork.broadcast(connection, transaction);

account-kit/react/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,9 @@ export {
6868
useSolanaTransaction,
6969
type SolanaTransaction,
7070
} from "./hooks/useSolanaTransaction.js";
71+
export {
72+
useSolanaConnection,
73+
type SolanaConnection,
74+
} from "./hooks/useSolanaConnection.js";
7175

7276
export { useSolanaSignMessage } from "./hooks/useSolanaSignMessage.js";
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
# This file is autogenerated
3+
title: useSolanaConnection
4+
description: Overview of the useSolanaConnection method
5+
slug: wallets/reference/account-kit/react/hooks/useSolanaConnection
6+
---
7+
8+
This is the hook that will be used to send to the solana connection.
9+
10+
## Import
11+
12+
```ts
13+
import { useSolanaConnection } from "@account-kit/react";
14+
```
15+
16+
## Usage
17+
18+
```ts
19+
const { connection } = useSolanaConnection();
20+
```
21+
22+
## Parameters
23+
24+
### opts
25+
26+
`SolanaConnectionHookParams`
27+
Options for the hook to get setup
28+
29+
## Returns
30+
31+
`SolanaConnection`
32+
The transaction hook.

examples/ui-demo/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"@radix-ui/react-switch": "^1.0.3",
2222
"@radix-ui/react-toast": "^1.2.1",
2323
"@radix-ui/react-tooltip": "^1.1.2",
24+
"@solana/spl-token": "^0.4.13",
25+
"@solana/spl-token-metadata": "^0.1.6",
2426
"@solana/web3.js": "^1.98.0",
2527
"@t3-oss/env-core": "^0.7.1",
2628
"@t3-oss/env-nextjs": "^0.7.1",
1.5 MB
Loading

examples/ui-demo/src/app/config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const alchemyConfig = () => {
115115
enablePopupOauth: true,
116116
solana: {
117117
connection: solanaConnection,
118-
// policyId: process.env.NEXT_PUBLIC_PAYMASTER_POLICY_ID,
118+
policyId: process.env.NEXT_PUBLIC_SOLANA_POLICY_ID,
119119
},
120120
},
121121
{
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { useToast } from "@/hooks/useToast";
2+
import { useSolanaTransaction } from "@account-kit/react";
3+
import {
4+
Keypair,
5+
PublicKey,
6+
SystemProgram,
7+
VersionedTransaction,
8+
} from "@solana/web3.js";
9+
10+
import {
11+
ExtensionType,
12+
LENGTH_SIZE,
13+
TOKEN_2022_PROGRAM_ID,
14+
TYPE_SIZE,
15+
createInitializeInstruction,
16+
createInitializeMetadataPointerInstruction,
17+
createInitializeMintInstruction,
18+
createUpdateFieldInstruction,
19+
getMintLen,
20+
} from "@solana/spl-token";
21+
import { pack, TokenMetadata } from "@solana/spl-token-metadata";
22+
import { LoadingIcon } from "../icons/loading";
23+
import { Button } from "./Button";
24+
import { Card } from "./Card";
25+
import Image from "next/image";
26+
import { Badge } from "./Badge";
27+
import { CheckCircleFilledIcon } from "../icons/check-circle-filled";
28+
import { useState } from "react";
29+
30+
type TransactionState = "idle" | "signing" | "sponsoring" | "complete";
31+
32+
export const SolanaNftCard = () => {
33+
const { setToast } = useToast();
34+
const {
35+
sendTransactionAsync,
36+
isPending,
37+
connection,
38+
signer: solanaSigner,
39+
} = useSolanaTransaction();
40+
const [transactionState, setTransactionState] =
41+
useState<TransactionState>("idle");
42+
43+
const handleCollectNFT = async () => {
44+
try {
45+
if (!solanaSigner) throw new Error("No signer found");
46+
if (!connection) throw new Error("No connection found");
47+
setTransactionState("signing");
48+
setTransactionState("sponsoring");
49+
const stakeAccount = Keypair.generate();
50+
const publicKey = new PublicKey(solanaSigner.address);
51+
const metaData: (readonly [string, string])[] = [
52+
["Background", "Blue"],
53+
["WrongData", "DeleteMe!"],
54+
["Points", "0"],
55+
];
56+
const tokenMetadata: TokenMetadata = {
57+
updateAuthority: publicKey,
58+
mint: stakeAccount.publicKey,
59+
name: "Alchemy Duck",
60+
symbol: "ALCHDUCK",
61+
uri: "https://bafybeigtvzjqalevyw67xdhr7am5r3jxe5kjbg4pi2jv3nxvhelptwksoe.ipfs.dweb.link?filename=duckImage.png",
62+
additionalMetadata: metaData,
63+
};
64+
const decimals = 6;
65+
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
66+
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(tokenMetadata).length;
67+
const mintLamports = await connection.getMinimumBalanceForRentExemption(
68+
mintLen + metadataLen
69+
);
70+
71+
const mint = stakeAccount.publicKey;
72+
const tx = await sendTransactionAsync({
73+
preSend: async (transaction: VersionedTransaction) => {
74+
transaction.sign([stakeAccount]);
75+
return transaction;
76+
},
77+
instructions: [
78+
SystemProgram.createAccount({
79+
fromPubkey: publicKey,
80+
newAccountPubkey: mint,
81+
space: mintLen,
82+
lamports: mintLamports,
83+
programId: TOKEN_2022_PROGRAM_ID,
84+
}),
85+
createInitializeMetadataPointerInstruction(
86+
mint,
87+
publicKey,
88+
mint,
89+
TOKEN_2022_PROGRAM_ID
90+
),
91+
createInitializeMintInstruction(
92+
mint,
93+
decimals,
94+
publicKey,
95+
null,
96+
TOKEN_2022_PROGRAM_ID
97+
),
98+
createInitializeInstruction({
99+
programId: TOKEN_2022_PROGRAM_ID,
100+
metadata: mint,
101+
updateAuthority: publicKey,
102+
mint: mint,
103+
mintAuthority: publicKey,
104+
name: tokenMetadata.name,
105+
symbol: tokenMetadata.symbol,
106+
uri: tokenMetadata.uri,
107+
}),
108+
...metaData.map(([key, value]) =>
109+
createUpdateFieldInstruction({
110+
programId: TOKEN_2022_PROGRAM_ID,
111+
metadata: mint,
112+
updateAuthority: publicKey,
113+
field: key,
114+
value: value,
115+
})
116+
),
117+
],
118+
});
119+
120+
console.log(`Created transaction: ${tx.hash}
121+
https://explorer.solana.com/tx/${tx.hash}?cluster=devnet
122+
https://explorer.solana.com/address/${mint.toBase58()}?cluster=devnet
123+
`);
124+
125+
setTransactionState("complete");
126+
} catch (error) {
127+
console.log(error);
128+
setToast({
129+
type: "error",
130+
text: "Error sending transaction",
131+
open: true,
132+
});
133+
}
134+
};
135+
136+
const imageSlot = (
137+
<div className="w-full h-full bg-[#DCFCE7] flex justify-center items-center relative">
138+
<Image
139+
src="/images/duckImage.png"
140+
alt="Solana Duck NFT"
141+
width={300}
142+
height={300}
143+
className="w-full h-full object-cover object-top"
144+
/>
145+
</div>
146+
);
147+
148+
const states = [
149+
{
150+
state: "signing",
151+
text: "Signing transaction...",
152+
isCompleteStates: ["complete", "sponsoring"],
153+
},
154+
{
155+
state: "sponsoring",
156+
text: "Sponsoring gas & minting NFT...",
157+
isCompleteStates: ["complete"],
158+
},
159+
];
160+
161+
const renderTransactionStates = (
162+
<div className="flex flex-col gap-3">
163+
{states.map(({ state, text, isCompleteStates }) => {
164+
const isComplete = isCompleteStates.includes(transactionState);
165+
return (
166+
<div key={state} className="flex items-center gap-2">
167+
{isComplete && (
168+
<CheckCircleFilledIcon className="h-4 w-4 fill-demo-surface-success" />
169+
)}
170+
{!isComplete && <LoadingIcon className="h-4 w-4" />}
171+
<p className="text-sm text-fg-secondary">{text}</p>
172+
</div>
173+
);
174+
})}
175+
</div>
176+
);
177+
const content = (
178+
<>
179+
{transactionState === "idle" || transactionState === "complete" ? (
180+
<>
181+
<p className="text-fg-primary text-sm mb-3">
182+
Transact with one click using gas sponsorship and background
183+
signing.
184+
</p>
185+
<div className="flex justify-between items-center">
186+
<p className="text-fg-secondary text-sm">Gas Fee</p>
187+
<p>
188+
<span className="line-through mr-1 text-sm text-fg-primary">
189+
$0.02
190+
</span>
191+
<span className="text-sm bg-gradient-to-r from-[#FF9C27] to-[#FD48CE] bg-clip-text text-transparent">
192+
Free
193+
</span>
194+
</p>
195+
</div>
196+
</>
197+
) : (
198+
renderTransactionStates
199+
)}
200+
</>
201+
);
202+
203+
return (
204+
<Card
205+
badgeSlot={<Badge text="New!" className="text-[#F3F3FF] bg-[#16A34A]" />}
206+
imageSlot={imageSlot}
207+
heading="Solana Gasless Transactions"
208+
content={content}
209+
buttons={
210+
<>
211+
<Button
212+
className="mt-auto w-full"
213+
onClick={handleCollectNFT}
214+
disabled={!solanaSigner || isPending}
215+
>
216+
{transactionState === "idle"
217+
? "Collect NFT"
218+
: transactionState === "complete"
219+
? "Re-collect NFT"
220+
: "Collecting NFT..."}
221+
</Button>
222+
</>
223+
}
224+
/>
225+
);
226+
};

0 commit comments

Comments
 (0)