First, you will require installing all Solana stuff and the framework for the backend anchor. You can find the installation guide here, also there are some basic examples and other useful stuff.
The working repository can be found here.
Open the terminal in the folder you prefer to create the program and simply run the next command. It will create the project structure with all the required things.
anchor init crowdfunding-program
cd crowdfunding-program
The main executable code is stored in (all changes need to be done there):
programs/crowdfunding-program/src/lib.rs
First things first you need some account (it is actually like a wallet) for the program.
solana-keygen new -o id.json
Terminal then print the pubkey: <ACCOUNT_ADDRESS>
Change the location of the wallet to "./id.json" in ‘Anchor.toml’ file.
wallet = "./id.json"
Before deployment, you need some amount of Sol to do that. Request some money to your new account.
solana airdrop 2 <ACCOUNT_ADDRESS> --url devnet
Now you can deploy the application.
anchor deploy
You will find new folders in your solution after deployment is completed. Basically, we need to obtain the program address and replace it with the automatically generated address by anchor.
solana address -k ./target/deploy/crowdfunding_program-keypair.json
This address will be the program ID. In the ‘lib.rs’ file and also in ‘Anchor.toml’ replace the program ID that you generated with what is inside these files.
The application will support 3 major functions:
In the body of the create
function I’m simply assigning all parameters to the campaign account.
pub fn create(ctx: Context<Create>, name: String, description: String, target_amount: u64) -> Result<()> {
let campaign = &mut ctx.accounts.campaign;
campaign.name = name;
campaign.description = description;
campaign.amount_donated = 0;
campaign.target_amount = target_amount;
// * - means dereferencing
campaign.owner = *ctx.accounts.user.key;
Ok(())
}
The <Create>
is a structure of the current function context accounts, that must include a campaign account, and a signer who creates the campaign.
#[derive(Accounts)]
pub struct Create<'info> {
// init means to create campaign account
// bump to use unique address for campaign account
#[account(init, payer=user, space=9000, seeds=[b"campaign_demo".as_ref(), user.key().as_ref()], bump)]
pub campaign: Account<'info, Campaign>,
// mut makes it changeble (mutable)
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct Campaign {
pub owner: Pubkey,
pub name: String,
pub description: String,
pub amount_donated: u64,
pub target_amount: u64,
}
The donate functionality requires transferring a specific amount of money to the campaign account. The instruction describes from what account to what account the Sols’ will be transferred. At the I also increasing the counter of donated Sols’ campaign.amount_donated += amount
.
pub fn donate(ctx: Context<Donate>, amount: u64) -> Result<()> {
let instruction = anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.user.key(),
&ctx.accounts.campaign.key(),
amount
);
anchor_lang::solana_program::program::invoke(
&instruction,
&[
ctx.accounts.user.to_account_info(),
ctx.accounts.campaign.to_account_info(),
]
);
let campaign = &mut ctx.accounts.campaign;
campaign.amount_donated += amount;
Ok(())
}
The structure of the <Donate>
accounts context is close to the <Create>
, but here I don’t need to initialize the campaign itself, I just use the campaign that users selected.
#[derive(Accounts)]
pub struct Donate<'info> {
#[account(mut)]
pub campaign: Account<'info, Campaign>,
// mut makes it changeble (mutable)
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
In the withdraw
function besides transferring all funds to the user’s wallet I need to check if the amount of money is sufficient for transferring and if the user is the owner of this particular campaign.
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let campaign = &mut ctx.accounts.campaign;
let user = &mut ctx.accounts.user;
if campaign.owner != *user.key {
return Err(ErrorCode::InvalidOwner.into());
}
// Rent balance depends on data size
let rent_balance = Rent::get()?.minimum_balance(campaign.to_account_info().data_len());
if **campaign.to_account_info().lamports.borrow() - rent_balance < amount {
return Err(ErrorCode::InvalidWithdrawAmount.into());
}
**campaign.to_account_info().try_borrow_mut_lamports()? -= amount;
**user.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub campaign: Account<'info, Campaign>,
// mut makes it changeble (mutable)
#[account(mut)]
pub user: Signer<'info>,
}
As you can see in the withdraw
method, I throw errors if the User is not the owner of the campaign and if the balance is insufficient. The error messages actually represented in an enum called ErrorCode
.
#[error_code]
pub enum ErrorCode {
#[msg("The user is not the owner of the campaign.")]
InvalidOwner,
#[msg("Insufficient amount to withdraw.")]
InvalidWithdrawAmount,
}
If you are done with all changes in your code, you need to build and deploy your program to the network.
anchor build
And deploy the program to the preferred network that you will work this, for example, ‘devnet’. In the root directory, there is file ‘Anchor.toml’, change the value of cluster to ‘devnet’.
cluster = "devnet"
The number 2 is some kind of constraint on the ‘devnet’. And finally, deploy it.
anchor deploy
After creating the main functionality it will be nice to cover it with tests. You can find tests in the repository, they are more or less similar to what you will have on the UI. Then let’s move forward and create the frontend part.
After you have done the backend part you can move to the frontend part and initialize react application via create-react-app
.
npx create-react-app crowdfunding-ui --template typescript
Change directory: cd crowdfunding-ui
And install packages for the UI
npm i @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets @solana/web3.js @project-serum/anchor react-bootstrap
The library react-bootstrap
is for styling only.
But it is not enough to properly build and run the UI application. There are some dependencies in the anchor package and some features in Solana libraries if you use react-scripts.
npm i --save-dev react-app-rewired source-map-loader
The problem is in some modules that don’t maintain the webpack 5. So create the file ‘config-overrides.js’ in the root of your UI application and copy the next code.
/*
* To build solana dependencies properly
*/
const { ProvidePlugin } = require('webpack');
module.exports = function (config, env) {
return {
...config,
module: {
...config.module,
rules: [
...config.module.rules,
{
test: /\.(m?js|ts)$/,
enforce: 'pre',
use: ['source-map-loader'],
},
],
},
plugins: [
...config.plugins,
new ProvidePlugin({
process: 'process/browser',
}),
],
resolve: {
...config.resolve,
fallback: {
assert: require.resolve('assert'),
buffer: require.resolve('buffer'),
stream: require.resolve('stream-browserify'),
crypto: require.resolve('crypto-browserify'),
},
},
ignoreWarnings: [/Failed to parse source map/],
};
};
As in the example of the Solana library, it is better to create a separate ‘wrapper’ or ‘provider’ to connect and work with the wallet.
const supportedWallets = [ new PhantomWalletAdapter() ];
const WalletWrapper: React.FC<WalletWrapperProps> = ({ children, network }) => {
return (
<ConnectionProvider endpoint={network}>
<WalletProvider wallets={supportedWallets} autoConnect>
<WalletModalProvider>
{children}
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
And basically, wrap the whole App in it.
const App: React.FC<AppProps> = () => {
return (
<WalletWrapper network={network}>
<CompaingsView network={network}/>
</WalletWrapper>
);
}
From the Anchor program you created, we need one file → crowdfunding_program.json. Copy it to the UI src folder. This file contains a description of how to communicate with your program, with all types that you defined.
cp ./target/idl/crowdfunding_program.json ../crowdfunding-ui/src/idl.json
To be able to send and receive data, you require to create the Program class, where you put copied Idl
, programId
and provider
. It will be in the CampaignsView component.
const getProgram = () => {
/* create the provider and return it to the caller */
const connection = new Connection(network, opts.preflightCommitment);
const provider = new AnchorProvider(connection, wallet as any, opts);
/* create the program interface combining the idl, program ID, and provider */
const program = new Program(idl as Idl, programId, provider);
return program;
};
const program = getProgram();
This Program class will be responsible to communicate with the crowdfunding program. And to create the first campaign you need some amount of Sols on your wallet and of course wallet itself.
I used a Phantom wallet, so install the extension to the browser if you haven’t done it yet. Change the network to ‘devnet’ in the settings of the wallet, after airdrop to your wallet.
The create functionality is covered by the createCampaign
method. It basically uses values from inputs like: name, description, targetAmount. And signer here is a User’s wallet. At the end CampaignsView will look like this:
import React, { ChangeEvent, useState } from 'react';
import {
AnchorProvider,
BN,
Idl,
Program,
utils,
web3,
} from '@project-serum/anchor';
import { useWallet } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
import idl from '../idl.json';
import { Button, FloatingLabel, Form } from 'react-bootstrap';
import CampaignsTable from '../components/campaigns-table';
const opts: { preflightCommitment: Commitment } = {
preflightCommitment: 'processed',
};
const programId = new PublicKey(idl.metadata.address);
interface CampaignsViewProps {
network: string;
}
export const CampaignsView: React.FC<CampaignsViewProps> = ({ network }) => {
const wallet = useWallet();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [targetAmount, setTargetAmount] = useState<number>(1);
const getProgram = () => {
/* create the provider and return it to the caller */
const connection = new Connection(network, opts.preflightCommitment);
const provider = new AnchorProvider(connection, wallet as any, opts);
/* create the program interface combining the idl, program ID, and provider */
const program = new Program(idl as Idl, programId, provider);
return program;
};
const program = getProgram();
const onNameChange = (e: ChangeEvent<any>) => {
setName(e.target.value);
};
const onDescriptionChange = (e: ChangeEvent<any>) => {
setDescription(e.target.value);
};
const onTargetAmountChange = (e: ChangeEvent<any>) => {
setTargetAmount(e.target.value);
};
const createCampaign = async () => {
const [campaign] = await PublicKey.findProgramAddress(
[
utils.bytes.utf8.encode('campaign_demo'),
wallet.publicKey!.toBuffer(),
],
program.programId,
);
await program.methods
.create(name, description, new BN(targetAmount))
.accounts({
campaign: campaign,
user: wallet.publicKey!,
systemProgram: web3.SystemProgram.programId,
})
.rpc();
};
return (
<div className='campaigns-view p-5'>
{!wallet.connected && <WalletMultiButton />}
<Form>
<Form.Group className='mb-3'>
<FloatingLabel controlId='name' label='Name'>
<Form.Control
type='text'
placeholder='Name of the campaign'
value={name}
onChange={onNameChange}
/>
</FloatingLabel>
</Form.Group>
<Form.Group className='mb-3'>
<FloatingLabel controlId='description' label='Description'>
<Form.Control
as='textarea'
placeholder='Description of the campaign'
style={{ height: '150px' }}
value={description}
onChange={onDescriptionChange}
/>
</FloatingLabel>
</Form.Group>
<Form.Group className='mb-3'>
<FloatingLabel
controlId='targetAmount'
label='Target Amount'
className='mb-3'
>
<Form.Control
as='input'
type='number'
placeholder='Targemt amount that need to be reached'
value={targetAmount}
onChange={onTargetAmountChange}
/>
</FloatingLabel>
</Form.Group>
<Form.Group className='mb-3'>
<Button variant='primary' onClick={createCampaign}>
Create Campaign
</Button>
</Form.Group>
</Form>
{wallet.connected && <CampaignsTable walletKey={wallet.publicKey!} program={program}/>}
</div>
);
};
export default CampaignsView;
At least the user must have the ability to see all campaigns, then add a list with all of the created campaign accounts. In the program, there is no specific functionality for that. It’s because such functionality is inside the Anchor framework. For each account, there is the extension method called ‘all’, you need to call it like program.account.campaign.all()
.
const [campaigns, setCampaigns] = useState<ProgramAccount[]>([]);
const getAllCampaigns = async () => {
const campaigns = await program.account.campaign.all();
setCampaigns(campaigns);
};
The value for donation and withdrawal are hardcoded, but they can be input like for campaign creation. Otherwise donate
and withdraw
functions are using program
to invoke specific methods. For each program method invocation Phantom wallet will ask you to approve or decline the transaction. But the call of the particular function is pretty easy.
import React, { useEffect, useState } from 'react';
import { BN, Program, ProgramAccount, web3 } from '@project-serum/anchor';
import { PublicKey } from '@solana/web3.js';
import { Button, Table } from 'react-bootstrap';
interface CampaignsTableProps {
program: Program;
walletKey: PublicKey;
}
export const CampaignsTable: React.FC<CampaignsTableProps> = ({
program,
walletKey,
}) => {
const [campaigns, setCampaigns] = useState<ProgramAccount[]>([]);
const getAllCampaigns = async () => {
const campaigns = await program.account.campaign.all();
setCampaigns(campaigns);
};
useEffect(() => {
getAllCampaigns();
}, [walletKey]);
const donate = async (campaignKey: PublicKey) => {
try {
await program.methods
.donate(new BN(0.2 * web3.LAMPORTS_PER_SOL))
.accounts({
campaign: campaignKey,
user: walletKey,
systemProgram: web3.SystemProgram.programId,
})
.rpc();
await getAllCampaigns();
} catch (err) {
console.error('Donate transaction error: ', err);
}
};
const withdraw = async (campaignKey: PublicKey) => {
try {
await program.methods
.withdraw(new BN(0.2 * web3.LAMPORTS_PER_SOL))
.accounts({
campaign: campaignKey,
user: walletKey,
})
.rpc();
} catch (err) {
console.error('Withdraw transaction error: ', err);
}
};
const allCampaigns: () => JSX.Element[] = () => {
return campaigns.map((c, i) => {
const key = c.publicKey.toBase58();
return (
<tr key={key}>
<td>{i + 1}</td>
<td>{c.account.name}</td>
<td>{c.account.description}</td>
<td>{c.account.targetAmount.toString()}</td>
<td>{(c.account.amountDonated / web3.LAMPORTS_PER_SOL).toString()}</td>
<td>
<Button
className='m-1'
variant='primary'
onClick={() => donate(c.publicKey)}
>
Donate
</Button>
<Button
disabled={c.account.owner.toBase58() !== walletKey.toBase58()}
className='m-1'
variant='danger'
onClick={() => withdraw(c.publicKey)}
>
Withdraw
</Button>
</td>
</tr>
);
});
};
return (
<>
<div>Campaigns</div>
<Table>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Description</th>
<th>Target Amount</th>
<th>Donated</th>
<th></th>
</tr>
</thead>
<tbody>{allCampaigns()}</tbody>
</Table>
</>
);
};
export default CampaignsTable;
Now you can run your program and enjoy it.
npm start
All data is stored in the blockchain, brilliant.