Compile the Solana program to generate the idl.json file and integrate it into the React project. Additionally, create a types.ts file to define TypeScript types for the project, as TypeScript is required.
npx create-next-app solana-dapp
npm i @coral-xyz/anchor@0.29.0 @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/web3.js
Note: we need to use this version of this package to avoid conflict in IDL file:
"@coral-xyz/anchor": "^0.29.0",
We need two files:
idl.json file:
{
"version": "0.1.0",
"name": "example",
"instructions": [
{
"name": "initialize",
"accounts": [
{
"name": "payer",
"isMut": true,
"isSigner": true
},
{
"name": "counter",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "increment",
"accounts": [
{
"name": "counter",
"isMut": true,
"isSigner": false
},
{
"name": "authority",
"isMut": true,
"isSigner": true
}
],
"args": []
}
],
"accounts": [
{
"name": "Counter",
"type": {
"kind": "struct",
"fields": [
{
"name": "count",
"type": "u64"
},
{
"name": "authority",
"type": "publicKey"
}
]
}
}
]
}
types.ts file:
export type example = {
"version": "0.1.0",
"name": "example",
"instructions": [
{
"name": "initialize",
"accounts": [
{
"name": "payer",
"isMut": true,
"isSigner": true
},
{
"name": "counter",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "increment",
"accounts": [
{
"name": "counter",
"isMut": true,
"isSigner": false
},
{
"name": "authority",
"isMut": true,
"isSigner": true
}
],
"args": []
}
],
"accounts": [
{
"name": "Counter",
"type": {
"kind": "struct",
"fields": [
{
"name": "count",
"type": "u64"
},
{
"name": "authority",
"type": "publicKey"
}
]
}
}
]
}
To interact with an Anchor program using @coral-xyz/anchor, you'll need to create a Program instance using the program's IDL file.
Creating an instance of the Program requires the program's IDL and an AnchorProvider. An AnchorProvider is an abstraction that combines two things:
Connection: the connection to a Solana cluster (i.e. localhost, devnet, mainnet)Wallet: a default wallet used to pay for and sign transactionsThe following diagram illustrates the relationships between these concepts:
+-----------------+
| Cluster(Solana) |
+-----------------+
|
v
+-----------------+ +-----------------+
| Connection | | Wallet |
+-----------------+ +-----------------+
| |
v v
+---------------------------------------+ +-----------------+
| Provider | | IDL (anchor) |
+---------------------------------------+ +-----------------+
| |
v v
+--------------------------------------------------------------------+
| Program |
+--------------------------------------------------------------------+
To implement with our React app, we have:
const Content: React.FC = () => {
const { connection } = useConnection();
const wallet = useAnchorWallet();
console.log("wallet:", wallet?.publicKey.toBase58());
return <WalletMultiButton />;
}
const Context: React.FC<{ children: ReactNode }> = ({ children }) => {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter(),],[]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
const Home: React.FC = () => {
return (
<Context>
<Content />
</Context>
);
}
export default Home;
In this section, we implement two key functions:
findPDAforAuthority: Generates the PDA for the Counter account based on the user’s wallet and the program’s ID.fetchAccount: Retrieves the Counter account’s data (e.g., the current count) from the Solana blockchain using the PDA.These functions allow the React front-end to locate and display the Counter account’s data, which is critical for showing the current counter value to the user.
const Content: React.FC = () => {
const connection = useConnection();
const wallet = useAnchorWallet();
const [program, setProgram] = useState<anchor.Program<example>>(null!);
const [account, setAccount] = useState<{publicKey: anchor.web3.PublicKey, dataset: any}>(null!);
const [setValue, setSetValue] = useState<number>(0);
console.log('account is: ', account);
const findPDAforAuthority = async (_program?: anchor.Program<example>): Promise<anchor.web3.PublicKey> => {
const [pda, _bump] = await anchor.web3.PublicKey.findProgramAddressSync(
[new TextEncoder().encode('account'), wallet!.publicKey.toBytes()],
(typeof(_program) !== 'undefined' ? _program.programId : program.programId)
);
console.log('pda is: ', pda);
return pda;
}
const fetchAccount = async (_program?: anchor.Program<example>) => {
if(typeof(_program) !== 'undefined'){
const pda = await findPDAforAuthority(_program);
return await _program.account.counter.fetch(pda);
} else{
const pda = await findPDAforAuthority();
return await program.account.counter.fetch(pda);
}
}
useEffect(() => {
if(wallet && connection){
const anchorConnection = new anchor.web3.Connection(
connection.connection.rpcEndpoint,
connection.connection.commitment
);
const anchorProvider = new anchor.AnchorProvider(
anchorConnection,
wallet,
{"preflightCommitment" : connection.connection.commitment}
);
const _program = new anchor.Program<example>(
JSON.parse(JSON.stringify(idl)),
"2ZhxsERLkcxYuuUnHWA9K7btyzT4SyWAAcr1XZuAHaRc",
anchorProvider
);
console.log('program is: ', _program);
setProgram(_program);
/*** search for existing MyAccount account */
fetchAccount(_program)
.then((result) => {
if(result){
findPDAforAuthority(_program)
.then((pda) => {
setAccount({publicKey: pda, dataset: result});
})
}
})
.catch((reason) => console.log(reason));
}
}, [wallet, connection]);
return(
<>
{/* <WalletMultiButton /> */}
</>
);
}
We need to create a new Counter storage account for our current wallet address. Next, we can increment the value stored in the Counter account. After initialization, the Solana program returns the following response. The publicKey is the PDA and the authority is the wallet address.
[
{
"publicKey": "EHKGEgJemPfykeY4QTaX5Wr6FYjuxsH1szuADfGQobRq",
"account": {
"count": "0",
"authority": "AFM2Lr7WUFVVydyDfijsMengB2CThY7J7fxEwp2GS9os"
}
}
]
const Content: React.FC = () => {
const connection = useConnection();
const wallet = useAnchorWallet();
console.log('wallet is: ', wallet);
const [program, setProgram] = useState<anchor.Program<example>>(null!);
const [account, setAccount] = useState<{publicKey: anchor.web3.PublicKey, dataset: any}>(null!);
const [setValue, setSetValue] = useState<number>(0);
console.log('account is: ', account);
const findPDAforAuthority = async (_program?: anchor.Program<example>): Promise<anchor.web3.PublicKey> => {
const [pda, _bump] = await anchor.web3.PublicKey.findProgramAddressSync(
[new TextEncoder().encode('counter'), wallet!.publicKey.toBytes()],
(typeof(_program) !== 'undefined' ? _program.programId : program.programId)
);
console.log('pda is: ', pda);
return pda;
}
const fetchAccount = async (_program?: anchor.Program<example>) => {
if(typeof(_program) !== 'undefined'){
const pda = await findPDAforAuthority(_program);
return await _program.account.counter.fetch(pda);
} else{
const pda = await findPDAforAuthority();
return await program.account.counter.fetch(pda);
}
}
const initAccount = async () => {
try {
const pda = await findPDAforAuthority();
const tx = await program.methods
.initialize()
.accounts({
counter: pda,
payer: wallet!.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([])
.rpc();
setAccount({
publicKey: pda,
dataset: await program.account.counter.fetch(pda)
});
} catch (e) {
console.log('Initialization Error: ', e);
}
};
useEffect(() => {
if(wallet && connection){
const anchorConnection = new anchor.web3.Connection(
connection.connection.rpcEndpoint,
connection.connection.commitment
);
const anchorProvider = new anchor.AnchorProvider(
anchorConnection,
wallet,
{"preflightCommitment" : connection.connection.commitment}
);
const _program = new anchor.Program<example>(
JSON.parse(JSON.stringify(idl)),
"2ZhxsERLkcxYuuUnHWA9K7btyzT4SyWAAcr1XZuAHaRc",
anchorProvider
);
console.log('program is: ', _program);
setProgram(_program);
/*** search for existing MyAccount account */
fetchAccount(_program)
.then((result) => {
if(result){
findPDAforAuthority(_program)
.then((pda) => {
setAccount({publicKey: pda, dataset: result});
})
}
})
.catch((reason) => console.log(reason));
}
}, [wallet, connection]);
return(
<>
{wallet && connection
?
account
?
`Counter: ${account.dataset.count.toString()}`
:
<><button onClick={initAccount}>Initialize</button><br/></>
:
''
}
<WalletMultiButton />
</>
);
}
The has_one = authority constraint in the Counter account struct prevents unauthorized wallets from updating the counter value.
The result of Solana blockchain after increasing:
[
{
"publicKey": "EHKGEgJemPfykeY4QTaX5Wr6FYjuxsH1szuADfGQobRq",
"account": {
"count": "1",
"authority": "AFM2Lr7WUFVVydyDfijsMengB2CThY7J7fxEwp2GS9os"
}
}
]
const increaseAccountCounter = async () => {
await program.methods.increment()
.accounts({
counter: account!.publicKey,
authority: wallet!.publicKey
})
.rpc();
setAccount({
publicKey: account.publicKey,
dataset: await fetchAccount()
});
}
import React, {useEffect, useMemo, useState, type ReactNode} from 'react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { ConnectionProvider, WalletProvider, useAnchorWallet, useConnection } from '@solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
import * as anchor from '@coral-xyz/anchor';
import { clusterApiUrl } from '@solana/web3.js';
import { type example } from "../assets/types";
import idl from "../assets/idl.json";
require('@solana/wallet-adapter-react-ui/styles.css');
const Content: React.FC = () => {
const connection = useConnection();
const wallet = useAnchorWallet();
console.log('wallet is: ', wallet);
const [program, setProgram] = useState<anchor.Program<example>>(null!);
const [account, setAccount] = useState<{publicKey: anchor.web3.PublicKey, dataset: any}>(null!);
const [setValue, setSetValue] = useState<number>(0);
console.log('account is: ', account);
const findPDAforAuthority = async (_program?: anchor.Program<example>): Promise<anchor.web3.PublicKey> => {
const [pda, _bump] = await anchor.web3.PublicKey.findProgramAddressSync(
[new TextEncoder().encode('counter'), wallet!.publicKey.toBytes()],
(typeof(_program) !== 'undefined' ? _program.programId : program.programId)
);
console.log('pda is: ', pda);
return pda;
}
const fetchAccount = async (_program?: anchor.Program<example>) => {
if(typeof(_program) !== 'undefined'){
const pda = await findPDAforAuthority(_program);
return await _program.account.counter.fetch(pda);
} else{
const pda = await findPDAforAuthority();
return await program.account.counter.fetch(pda);
}
}
const initAccount = async () => {
try {
const pda = await findPDAforAuthority();
const tx = await program.methods
.initialize()
.accounts({
counter: pda,
payer: wallet!.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([])
.rpc();
setAccount({
publicKey: pda,
dataset: await program.account.counter.fetch(pda)
});
} catch (e) {
console.log('Initialization Error: ', e);
}
};
const increaseAccountCounter = async () => {
await program.methods.increment()
.accounts({
counter: account!.publicKey,
authority: wallet!.publicKey
})
.rpc();
setAccount({
publicKey: account.publicKey,
dataset: await fetchAccount()
});
}
useEffect(() => {
if(wallet && connection){
const anchorConnection = new anchor.web3.Connection(
connection.connection.rpcEndpoint,
connection.connection.commitment
);
const anchorProvider = new anchor.AnchorProvider(
anchorConnection,
wallet,
{"preflightCommitment" : connection.connection.commitment}
);
const _program = new anchor.Program<example>(
JSON.parse(JSON.stringify(idl)),
"2ZhxsERLkcxYuuUnHWA9K7btyzT4SyWAAcr1XZuAHaRc",
anchorProvider
);
console.log('program is: ', _program);
setProgram(_program);
/*** search for existing MyAccount account */
fetchAccount(_program)
.then((result) => {
if(result){
findPDAforAuthority(_program)
.then((pda) => {
setAccount({publicKey: pda, dataset: result});
})
}
})
.catch((reason) => console.log(reason));
}
}, [wallet, connection]);
return (
<>
{wallet && connection
?
(
account
?
(
<>
{`Counter: ${account.dataset.count.toString()}`} <br />
<>
<button onClick={increaseAccountCounter}>Increase</button>
<br />
</>
</>
)
:
(
<>
<button onClick={initAccount}>Initialize</button>
<br />
</>
)
)
:
null
}
<WalletMultiButton />
</>
);
}
const Context: React.FC<{ children: ReactNode }> = ({ children }) => {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter(),],[]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
const Home: React.FC = () => {
return (
<Context>
<Content />
</Context>
);
}
export default Home;