- Published on
Building a Crowdfunding dApp with ink! Smart Contracts and PAPI/ReactiveDOT
- Reading time
- 21 min read
- Authors

- Name
- Wasiu Ajao
- @truthixifi
Building a Crowdfunding dApp with ink! Smart Contracts and PAPI/ReactiveDOT
Hey there! Ready to build something awesome? In this tutorial, we'll create a decentralized crowdfunding platform from scratch using ink! smart contracts and a modern React frontend. Think of this as me and you coding together. I'll walk you through each step, explain the important decisions, and make sure you understand both the how and the why.
We'll be working with the InkFundMe smart contract, integrating it with a sleek frontend using PAPI (Polkadot API) and ReactiveDOT. By the end, you'll have a fully functional dApp where users can create campaigns, make contributions, and manage their fundraising goals. You can grab the complete code here: InkFundMe Repository.
Project Overview
We're building InkFundMe, a crowdfunding dApp that combines the power of ink! smart contracts with a modern React frontend.
Here's what we're working with:
- ink! smart contracts: We have two contracts which are an ERC20/PSP22 token and the main crowdfunding logic
- React frontend: Built with Vite, TypeScript, and Tailwind CSS
- PAPI integration: Using Polkadot API for seamless blockchain interaction
- ReactiveDOT: A reactive library for building Substrate front-ends
The flow is simple: users create campaigns with funding goals and deadlines, contributors can donate using our custom ERC20/PSP22 tokens, and the smart contract handles all the logic for successful funding or refunds.
Prerequisites
Before we dive in, make sure you have these tools ready:
- Rust & ink! – Install Rust and ink! CLI
- Node.js (v18+) – Download from nodejs.org
- npm – comes with Node.js
- cargo-contract – ink! contract CLI tool
- Make (optional) – Install Make or use the full cargo commands
- Polkadot.js extension – Browser extension
- Git – Install Git
If you're missing any of these, install them before continuing. The ink! documentation has great setup guides.
Smart Contract Deep Dive
Contract Location in Repository
The smart contracts are located in the contracts/ directory:
contracts/
├── src/
│ ├── token/ # ERC20/PSP22 token contract
│ │ ├── lib.rs # Token implementation
│ │ └── Cargo.toml # Token dependencies
│ └── inkfundme/ # Main crowdfunding contract
│ ├── lib.rs # InkFundMe implementation
│ └── Cargo.toml # InkFundMe dependencies
├── Makefile # Build and deployment scripts
└── target/ # Compiled contracts (generated)
└── ink/
├── token/
│ └── token.contract # Compiled token contract
└── inkfundme/
└── inkfundme.contract # Compiled InkFundMe contract
Contract Architecture
Our system uses two smart contracts working together:
- Token Contract (
contracts/src/token/lib.rs): A custom ERC20/PSP22 token with minting capabilities - InkFundMe Contract (
contracts/src/inkfundme/lib.rs): The main crowdfunding logic
Token Contract Features
The token contract is straightforward but powerful:
#[ink(storage)]
#[derive(Default)]
pub struct Token {
total_supply: U256,
balances: Mapping<Address, U256>,
allowances: Mapping<(Address, Address), U256>,
name: String,
symbol: String,
decimals: u8,
}
Key Functions:
- Standard ERC20/PSP22 operations:
transfer,transfer_from,approve,allowance - Minting capability:
mintfunction for creating new tokens - Faucet functionality: Free tokens for testing and onboarding
InkFundMe Contract Structure
The main contract handles all crowdfunding logic:
#[derive(Clone, Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub struct Campaign {
pub id: u32, // Unique identifier
pub title: String, // Campaign name
pub description: String, // Campaign details
pub goal: U256, // Funding target
pub deadline: u64, // End timestamp
pub owner: Address, // Creator address
pub raised: U256, // Current funding
pub completed: bool, // Finalization status
}
#[ink(storage)]
pub struct InkFundMe {
token_contract: TokenRef, // ERC20/PSP22 token reference
campaigns: StorageVec<Campaign>, // All campaigns
contributions: Mapping<(u32, Address), U256>, // Contribution tracking
next_campaign_id: u32, // ID counter
}
Design Decisions:
U256for token amounts → ensures compatibility and handles large numbersStorageVecfor campaign storage → provides ordered, append-only data with efficient iteration.Mappingfor contributions → enables constant-time lookups for campaign data.- Clear separation of concerns → token contract handles ERC-20 logic, crowdfunding contract handles campaign logic.
Core Contract Functions
Campaign Creation
#[ink(message)]
pub fn create_campaign(
&mut self,
title: String,
description: String,
goal: U256,
deadline: u64,
) -> Result<u32> {
// Validation
if goal == U256::zero() || deadline <= self.env().block_timestamp() {
return Err(Error::InvalidParameters);
}
let campaign_id = self.next_campaign_id;
let campaign = Campaign {
id: campaign_id,
title,
description,
goal,
deadline,
owner: self.env().caller(),
raised: U256::zero(),
completed: false,
};
self.campaigns.push(&campaign);
self.next_campaign_id += 1;
self.env().emit_event(CampaignCreated {
id: campaign_id,
owner: campaign.owner,
goal,
deadline,
});
Ok(campaign_id)
}
Contributing to Campaigns
#[ink(message)]
pub fn contribute(&mut self, campaign_id: u32, amount: U256) -> Result<()> {
let campaign = self.get_campaign_mut(campaign_id)?;
let caller = self.env().caller();
// Validation checks
if campaign.completed {
return Err(Error::CampaignCompleted);
}
if self.env().block_timestamp() > campaign.deadline {
return Err(Error::DeadlineReached);
}
// Transfer tokens from contributor to contract
self.token_contract.transfer_from(caller, self.env().account_id(), amount)?;
// Update campaign and contribution tracking
campaign.raised = campaign.raised.checked_add(amount).ok_or(Error::Overflow)?;
let existing = self.contributions.get((campaign_id, caller)).unwrap_or(U256::zero());
self.contributions.insert((campaign_id, caller), &(existing + amount));
self.env().emit_event(ContributionMade {
campaign_id,
contributor: caller,
amount,
});
Ok(())
}
Finalizing Campaigns (The Smart Decision Maker)
Here's where things get interesting. The finalize function is like the judge that decides what happens to all the money once a campaign ends. Think of it as the moment of truth, did we reach our goal or not?
#[ink(message)]
pub fn finalize(&mut self, campaign_id: u32) -> Result<()> {
let mut campaign = self.get_campaign_mut(campaign_id)?;
let caller = self.env().caller();
// Only campaign owner can finalize
if caller != campaign.owner {
return Err(Error::OnlyOwner);
}
// Must wait until deadline passes
if self.env().block_timestamp() <= campaign.deadline {
return Err(Error::DeadlineNotReached);
}
// Prevent double finalization
if campaign.completed {
return Err(Error::CampaignCompleted);
}
let success = campaign.raised >= campaign.goal;
campaign.completed = true;
if success {
// 🎉 Success! Transfer all funds to campaign owner
self.token_contract.transfer(campaign.owner, campaign.raised)?;
}
// 😞 Failed? Funds stay locked in contract for contributors to claim refunds
self.env().emit_event(CampaignFinalized { campaign_id, success });
Ok(())
}
What's happening here? The contract automatically decides the fate of the funds:
- Campaign succeeded (raised ≥ goal): All the money goes straight to the campaign creator. Contributors get nothing back, but they supported a successful project!
- Campaign failed (raised < goal): The money stays locked in the contract, and each contributor can claim their full refund. Nobody loses money on a failed project.
The smart contracts get to decide how the fund will be distributed.
Claiming Refunds (Getting Your Money Back For a Failed Campaign)
When a campaign doesn't reach its goal, contributors shouldn't lose their money. That's where claim_refund comes to the rescue. It's like we have an automatic insurance policy built into the contract.
#[ink(message)]
pub fn claim_refund(&mut self, campaign_id: u32) -> Result<()> {
let campaign = self.get_campaign(campaign_id)?;
let contributor = self.env().caller();
// Campaign must be completed and failed
if !campaign.completed {
return Err(Error::CampaignCompleted);
}
if campaign.raised >= campaign.goal {
return Err(Error::GoalNotMet);
}
// Get contributor's amount
let contribution = self.contributions.get((campaign_id, contributor)).unwrap_or_default();
if contribution == U256::zero() {
return Err(Error::NoContribution);
}
// Clear contribution and refund
self.contributions.remove((campaign_id, contributor));
self.token_contract.transfer(contributor, contribution)?;
self.env().emit_event(RefundClaimed { campaign_id, contributor, amount: contribution });
Ok(())
}
The refund process is bulletproof:
- Double-check the campaign failed - No refunds for successful campaigns (that would be taking contributions from the creator!)
- Verify you actually contributed - Can't claim money you never put in
- One refund per person - Once you claim, your contribution record gets wiped to prevent double-dipping
- Instant transfer - Your tokens come back immediately, no waiting periods
This creates a win-win situation where creators get funded if they hit their goals, and contributors never lose money on failed projects.
Query Functions - Your Window Into the Contract
These are the "read-only" functions that let you peek inside the contract without changing anything. Think of them as your dashboard for checking what's happening:
get_campaign(campaign_id: u32)- This gives us everything related to a specific campaign (title, description, goal, how much is raised, deadline, and whether it's completed).get_all_campaigns()- This returns all the campaigns in the contract.get_contribution(campaign_id: u32, contributor: Address)- With this function, we can get how much someone contributed to a particular campaign.get_token_address()- Returns the address of the ERC20/PSP22 token contract. The frontend needs this to interact with the token (like checking balances or approving transfers).get_campaign_count()-This returns a simple counter of how many campaigns exist.
These functions are free to call (no gas costs) and never change the blockchain state, they're just for reading data.
Security Patterns (How We Keep Your Funds Safe)
Building a crowdfunding platform means handling real money, so security isn't optional, it's everything. Here are the key security patterns we've baked into the contract to protect everyone involved:
Checks-Effects-Interactions Pattern: We always validate everything first, update our internal state second, and only then interact with external contracts. This prevents sneaky reentrancy attacks where malicious contracts try to drain funds.
Safe Math Operations: We use checked arithmetic that panics on overflow instead of wrapping around. No more "I contributed 1 token but somehow it became 18 quintillion tokens" bugs.
Comprehensive Input Validation: Every function checks its inputs thoroughly. Zero goals? Nope. Past deadlines? Not happening. We catch these issues before they can cause problems.
Event Emission for Transparency: Every important action emits an event. This creates an immutable audit trail that frontends can use to show real-time updates and users can verify on-chain.
Owner-Only Functions: Only campaign creators can finalize their campaigns. This prevents random people from prematurely ending campaigns.
Proper State Validation for Refunds: Multiple layers of checks ensure refunds only happen for failed campaigns, only to actual contributors, and only once per person.
These aren't just theoretical concepts, they're battle-tested patterns that protect millions of dollars in DeFi protocols every day.
Setting Up the Development Environment
1. Clone and Setup
git clone https://github.com/truthixify/inkfundme-tutorial.git
cd inkfundme-tutorial
2. Contract Development Setup
cd contracts
# Build both contracts (if you have Make installed)
make build
# OR use the full cargo commands:
cargo contract build --manifest-path ./src/token/Cargo.toml
cargo contract build --manifest-path ./src/inkfundme/Cargo.toml
3. Contract Deployment
The project includes a Makefile for easy deployment. First, configure your environment for Passet Hub testnet:
# Create .env file in contracts directory
echo 'ACCOUNT_URI=your-seed-phrase-or-private-key' > .env
echo 'CHAIN=wss://testnet-passet-hub.polkadot.io' >> .env
Deploy the contracts:
# Deploy token contract first (with Make)
make instantiate-token
# OR use the full cargo command:
cargo contract instantiate \
--constructor new \
--args \"INKFUNDME\" \"IFM\" 0 1000 \
--suri "your-seed-phrase" \
--url wss://testnet-passet-hub.polkadot.io \
--manifest-path ./src/token/Cargo.toml \
--execute
# Note the deployed address, then update TOKEN_ADDRESS in Makefile or use it directly
# Deploy InkFundMe contract (with Make)
make instantiate-inkfundme
# OR use the full cargo command:
cargo contract instantiate \
--constructor new \
--args 0x1f92b64f3e344dfd020ccd287e39a660ba329262 \
--suri "your-seed-phrase" \
--url wss://testnet-passet-hub.polkadot.io \
--manifest-path ./src/inkfundme/Cargo.toml \
--execute
Important: After deployment, update the contract addresses in:
contracts/Makefile(TOKEN_ADDRESS variable) if using Makefrontend/src/lib/constants.ts(TOKEN_ADDRESS and INK_FUND_ME_ADDRESS)
Frontend Integration with PAPI and ReactiveDOT
Project Structure
frontend/
├── src/
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── lib/
│ │ ├── contracts.ts # Contract definitions
│ │ └── constants.ts # Contract addresses and config
│ ├── pages/ # Route components
│ └── main.tsx # App entry point
├── .papi/ # Generated PAPI files
└── package.json
1. Install Dependencies
cd frontend
npm install
Key dependencies:
@polkadot-api/descriptors- Generated contract descriptors@polkadot-api/sdk-ink- ink! contract SDK@reactive-dot/react- Integrating PAPI into a React applicationpolkadot-api- Core Polkadot API
2. Generate Contract Descriptors
This is where the magic happens, we generate TypeScript bindings from our compiled contracts:
# Add the Passet Hub testnet
npm run postinstall
# This runs: npx papi add passetHub -w wss://testnet-passet-hub.polkadot.io
# Generate contract descriptors from compiled contracts
npm run codegen
# This runs:
# npx papi ink add ../contracts/target/ink/token/token.contract
# npx papi ink add ../contracts/target/ink/inkfundme/inkfundme.contract
What happens here:
- PAPI reads your compiled
.contractfiles - Generates TypeScript types and descriptors
- Creates files in
.papi/descriptors/and.papi/contracts/ - Updates
polkadot-api.jsonconfiguration
3. Configure Contract References
After running codegen, you can import the generated descriptors:
// frontend/src/lib/contracts.ts
import { contracts } from "@polkadot-api/descriptors"
export const token = {
contract: contracts.token,
}
export const inkFundMe = {
contract: contracts.inkfundme,
}
4. Set Contract Addresses
Update the deployed contract addresses:
// frontend/src/lib/constants.ts
export const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
export const FAUCET_URLS: { [key: string]: string } = {
pop: "https://learn.onpop.io/contracts/guides/bridge-tokens-to-pop-network",
passethub: "https://faucet.polkadot.io/?parachain=1111",
dev: "https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/accounts",
}
// Replace these with your deployed contract addresses
export const TOKEN_ADDRESS = "0x1f92b64f3e344dfd020ccd287e39a660ba329262"
export const INK_FUND_ME_ADDRESS = "0xa89c351e2ddd04cdc65aaed75b4dea5f1c0be279"
Getting Contract Addresses: When you run make instantiate-token and make instantiate-inkfundme, the output will show the deployed addresses. Look for lines like:
Contract 0xca1b92ff69afe515131c5a4ce8c4066db2f418db
Building the Frontend Components
1. Setting Up ReactiveDOT Provider
// frontend/src/main.tsx
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
// Pages
import HomePage from "./pages/home"
import CampaignPage from "./pages/campaign"
import { createBrowserRouter, RouterProvider } from "react-router-dom"
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ index: true, element: <HomePage /> },
{ path: "campaign/:id", element: <CampaignPage /> },
],
},
])
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
2. Contract Interaction Hooks
The project includes several custom hooks for seamless contract interactions. Here are the key ones:
- Account Mapping Hook (
use-is-mapped.tsx) - Signer Hook (
use-signer-and-address.tsx)
Note: These hooks handle the complex logic of wallet connection, address conversion, and account mapping status. Check the implementations in
frontend/src/hooks/.
3. Campaign Card Component
The project includes a CampaignCard component that displays individual campaigns. Here's a simplified version:
// frontend/src/components/campaign-card.tsx (simplified)
import { formatDistanceToNowStrict } from "date-fns"
import { Link } from "react-router-dom"
import type { FixedSizeArray, FixedSizeBinary } from "polkadot-api"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
type Campaign = {
id: number
title: string
description: string
goal: FixedSizeArray<4, bigint>
deadline: bigint
owner: FixedSizeBinary<20>
raised: FixedSizeArray<4, bigint>
completed: boolean
}
export function CampaignCard({ campaign, tokenInfo }: {
campaign: Campaign,
tokenInfo: TokenInfo
}) {
const progress = Number(campaign.raised[0]) / Number(campaign.goal[0]) * 100
const isExpired = Date.now() / 1000 > Number(campaign.deadline)
return (
<Card className="h-full">
<CardHeader>
<CardTitle className="line-clamp-2">{campaign.title}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground line-clamp-3">
{campaign.description}
</p>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Progress</span>
<span>{progress.toFixed(1)}%</span>
</div>
<Progress value={progress} className="h-2" />
<div className="flex justify-between text-xs text-muted-foreground">
<span>{campaign.raised[0].toString()} {tokenInfo?.symbol}</span>
<span>Goal: {campaign.goal[0].toString()}</span>
</div>
</div>
{/* More UI elements... */}
</CardContent>
</Card>
)
}
Note: This is a simplified version. Check the full implementation in
frontend/src/components/campaign-card.tsxfor complete functionality including contribution handling, status badges, and responsive design.
4. Advanced Features in the dApp
The project includes several advanced features that make it production-ready:
Smart Contract Integration:
- Token Faucet: Users can mint free test tokens directly from the dApp
- Account Mapping: Seamless SS58 to EVM address mapping
- Real-time Updates: Live campaign data refreshing after transactions
User Experience Enhancements:
- Loading States: Comprehensive loading indicators for all operations
- Error Handling: User-friendly error messages with transaction cancellation support
- Toast Notifications: Real-time feedback for all user actions
- Responsive Design: Works perfectly on desktop and mobile devices
Developer-Friendly Features:
- TypeScript Integration: Full type safety with generated contract types
- Modular Components: Reusable UI components with shadcn/ui
- Custom Hooks: Clean separation of blockchain logic from UI components
- Error Boundaries: Graceful error handling throughout the application
Key Components You'll Find:
- Campaign Cards (
campaign-card.tsx) - Display individual campaigns with progress bars - Create Campaign Form (
create-campaign-form.tsx) - Full form with validation - Campaign Detail Page (
campaign.tsx) - Complete campaign management interface - Map Account Button (
map-account-button.tsx) - Handles account mapping UX
Note: Each component includes comprehensive error handling, loading states, and user feedback. Check the full implementations in
frontend/src/components/andfrontend/src/pages/to see the complete feature set.
Key Integration Patterns
1. U256 Handling
ink! contracts use U256 for large numbers, but JavaScript uses BigInt. Here's how to handle the conversion:
// Convert JavaScript number to U256 format
const amountU256 = [BigInt(amount), 0n, 0n, 0n]
// Convert U256 back to JavaScript
const jsAmount = Number(u256Value[0])
2. Address Handling and Account Mapping
Before diving into contract interactions, there's an important step you need to understand: account mapping. This is a unique requirement when working with ink! contracts on Substrate chains.
What is Account Mapping?
Think of account mapping as registering your Substrate address (SS58 format) with the contract runtime so it can interact with EVM-style contracts. It's like getting a passport to enter a different country, you need it stamped before you can do business there.
The Mapping Process
Every account must be mapped before it can interact with contracts (even for read-only queries). Here's how the project handles this:
1. Check if Mapped (useIsMapped hook):
// frontend/src/hooks/use-is-mapped.tsx
export function useIsMapped() {
const api = useTypedApi()
const { signerAddress } = useSignerAndAddress()
const [isMapped, setIsMapped] = useState<boolean>()
const updateIsMapped = useCallback(async () => {
if (!api || !signerAddress) {
setIsMapped(undefined)
return
}
const evmSignerAddress = ss58ToEthereum(signerAddress)
const isMapped = !!(await api.query.Revive.OriginalAccount.getValue(evmSignerAddress))
setIsMapped(isMapped)
}, [api, signerAddress])
return isMapped
}
2. Map Account Button: The project includes a handy MapAccountButton component (frontend/src/components/map-account-button.tsx) that automatically appears when an account needs mapping:
// Simplified version of the mapping process
const handleMapAccount = async () => {
const tx = api.tx.Revive.map_account()
.signAndSubmit(signer)
// Once successful, the account can interact with contracts
}
Why This Matters:
- First-time users will see a "Map" button before they can contribute or create campaigns
- Returning users won't see this button if they're already mapped
- Failed transactions often happen because users forgot to map their account first
This is all handled automatically by the UI, but understanding it helps you debug issues and explain to users why they might see an extra step before using your dApp.
3. Error Handling
Wrap contract calls in try-catch blocks:
try {
const result = await contract.send('function_name', { ... })
// Handle success
} catch (error) {
if (error.message.includes('Cancelled')) {
// Handle user canceled error
} else {
// Handle other errors
}
}
Testing and Development
1. Testnet Setup
For Passet Hub testnet development:
- Get testnet tokens from the faucet
- Update your
.envwith testnet configuration - Deploy contracts to testnet using the
make instantiate-*commands - Update frontend constants with deployed addresses
Start the frontend:
cd frontend
npm run dev
2. Testing Contract Functions
Use the faucet button on each campaign page to mint tokens
Advanced Features and Next Steps
1. Enhanced UI/UX
- Add loading states and error boundaries
- Implement pagination for large campaign lists
- Add search and filtering capabilities
- Create detailed campaign pages with progress charts
2. Smart Contract Improvements
- Add campaign categories and tags
- Implement milestone-based funding
- Add campaign update functionality
- Create governance mechanisms for dispute resolution
- Use assets on Asset Hub instead of custom ERC20/PSP22 tokens
3. Integration Enhancements
- Add IPFS integration for campaign media
- Implement push notifications for campaign updates
- Add social sharing features
- Create analytics dashboard
4. Security and Optimization
- Add comprehensive error handling
- Implement gas optimization strategies
- Add contract upgrade mechanisms
- Perform security audits
Deployment to Production
1. Make It Yours (Customize the UI)
Before deploying, this is your chance to make the dApp truly yours! The current UI is functional, but you can make it shine:
Styling Tweaks You Can Make:
- Update colors in
tailwind.config.jsto match your brand - Modify the campaign cards in
frontend/src/components/campaign-card.tsx, maybe add images or different layouts - Customize the create campaign form styling in
frontend/src/components/create-campaign-form.tsx - Add your own logo and branding to the header
- Change fonts, spacing, or add animations to make it more engaging
Feature Ideas to Add:
- Campaign categories (charity, tech, art, etc.)
- Image uploads for campaigns (using IPFS)
- Social sharing buttons
- Progress animations
- Campaign search and filtering
- User profiles showing contribution history
Take some time to browse through the components and make them reflect your vision. The beauty of having the full source code is that you can customize everything!
2. Deploy to Vercel (Share Your Creation)
Once you're happy with your customizations, let's get your dApp live for the world to see:
Step 1: Prepare for Deployment
cd frontend
# Make sure everything builds correctly
npm run build
# Test the production build locally (optional)
npm run preview
If you encounter TypeScript errors during build:
If you still encounter issues:
- Make sure you've run
npm run codegenafter building contracts - Check that your contract addresses in
constants.tsare correct - Verify your
.papi/directory contains the generated descriptors
Step 2: Deploy to Vercel
Option A: Deploy via Vercel CLI (Recommended)
# Install Vercel CLI if you haven't already
npm install -g vercel
# IMPORTANT: Navigate to the frontend directory first!
cd frontend
vercel --prod
# Follow the prompts:
# - Link to existing project? No (for first deployment)
# - What's your project name? inkfundme-yourname
# - In which directory is your code located? ./ (just dot slash, since you're already in frontend)
# - Want to override settings? No
⚠️ Common Deployment Issue: If you get an error like "files should NOT have more than 15000 items", it means you're trying to deploy from the wrong directory. Make sure you're in the frontend folder before running vercel, not the root project folder!
Option B: Deploy via Vercel Dashboard
- Push your code to GitHub
- Go to vercel.com and sign in
- Click "New Project" and import your GitHub repository
- Set the root directory to
frontend - Vercel will auto-detect it's a Vite project
- Click "Deploy"
Step 3: Configure Environment Variables (if needed)
If you have any environment variables (like API keys), add them in your Vercel dashboard:
- Go to your project settings
- Navigate to "Environment Variables"
- Add any variables your app needs
Step 4: Custom Domain (Optional)
Want a custom domain like onchaincrowdfunding.com?
- Go to your Vercel project settings
- Click "Domains"
- Add your custom domain and follow the DNS setup instructions
3. Share Your Success
Once deployed, you'll get a URL like https://inkfundme-yourname.vercel.app. Now you can:
- Share it with friends and family
- Post it on social media
- Add it to your portfolio
- Use it as a real crowdfunding platform!
Pro Tips for Going Live:
- Test all functionality on the live site before sharing widely
- Make sure your contract addresses in
constants.tsare correct for Passet Hub testnet - Consider adding analytics (like Google Analytics) to track usage
- Set up error monitoring (like Sentry) to catch any issues
4. Contract Deployment Considerations
Your contracts are already deployed on Passet Hub testnet, which is perfect for demonstration and testing. For a production mainnet deployment:
- Thoroughly test everything on testnets first
- Get a security audit - never deploy unaudited contracts with real money
- Deploy with proper access controls - consider multi-sig wallets for admin functions
- Set up monitoring - track contract events and unusual activity
- Have an emergency plan - know how to pause or upgrade if needed
But for now, the testnet deployment is perfect for showcasing your skills and letting people try out your dApp risk-free!
Troubleshooting Common Issues
1. Contract Call Failures
- Ensure wallet is connected and has sufficient balance
- Check contract addresses are correct
- Verify function parameters match contract expectations
2. Type Errors
- Ensure U256 values are properly formatted as arrays
- Check address format conversions
- Verify TypeScript types match contract definitions
3. Transaction Failures
- Check gas limits and fees
- Ensure proper token approvals for transfers
- Verify contract state allows the operation
Resources and Further Reading
- ink! Documentation
- Polkadot API (PAPI) Documentation
- ReactiveDOT Documentation
- Substrate Contracts Node
- Source Code Repository
- Live dApp
Conclusion
Congratulations! You've built a complete decentralized crowdfunding platform using ink! smart contracts and modern frontend technologies. You now understand how to:
- Design and implement ink! smart contracts
- Generate TypeScript bindings with PAPI
- Build reactive library for the front-ends with ReactiveDOT (This is built on top of PAPI)
- Handle complex blockchain interactions
- Deploy and test dApps
This is just the beginning. The patterns and techniques you've learned here can be applied to build all kinds of decentralized applications. Keep experimenting, keep building, and most importantly, keep learning!