This commit is contained in:
zramsay 2025-03-21 15:50:02 -04:00
parent cb0d83d107
commit 87c2fdb8b3
37 changed files with 4612 additions and 17 deletions

1085
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"@cosmjs/cosmwasm-stargate": "^0.32.2",
"@cosmjs/proto-signing": "^0.32.2",
"@fal-ai/client": "^1.2.1",
"@google-cloud/vision": "^4.3.2",
"@google/generative-ai": "^0.21.0",
"@sei-js/core": "^3.2.0",
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.4",
"@supabase/supabase-js": "^2.49.1",

View File

@ -38,9 +38,31 @@ const AboutPage = () => {
<p>
Users earn a point for each submission. These points are tracked onchain and may be redeemable in the future.
</p>
<p>
Ranger is currently in development; location data is randomly assigned to each image. In the future, we will incorporate verifiable location data in a compliant and privacy preserving manner.
</p>
<h3 className="text-xl font-semibold text-emerald-300 mt-8 mb-4">Sei Blockchain Integration</h3>
<p>
Connect your Sei wallet to receive token rewards for your wildlife contributions! Each verified wildlife sighting earns you WILD tokens sent directly to your connected wallet on the Atlantic-1 testnet.
</p>
<p>
To get started:
</p>
<ol className="list-decimal pl-5 space-y-2">
<li>Install the <a href="https://www.keplr.app/download" className="text-emerald-400 hover:underline">Keplr</a> or <a href="https://www.leapwallet.io/" className="text-emerald-400 hover:underline">Leap</a> wallet extension</li>
<li>Configure your wallet for the Sei Atlantic-1 testnet</li>
<li>Click "Connect Wallet" in the navigation bar</li>
<li>Upload wildlife photos to earn both points and WILD tokens</li>
</ol>
<p className="mt-4">
Rare wildlife sightings earn bonus tokens! Our Wildlife Token (WILD) smart contract on the Sei blockchain ensures transparent and automatic token distribution based on species rarity.
</p>
</div>
</div>

View File

@ -217,11 +217,12 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
: "🌿 No wildlife detected in this image. Try uploading a photo of an animal!"
}`
// Prepare user response
const userResponse = NextResponse.json({
// Prepare user response with extra fields for token data we'll add later
const responseData = {
description: responseMessage,
isAnimal: visionResult.isAnimal
})
isAnimal: visionResult.isAnimal,
tokenReward: null
};
// Background processing for animal images
if (visionResult.isAnimal) {
@ -275,6 +276,37 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
visionResult.mainObject
);
console.log('Points award result:', pointsResult);
// Check for Sei wallet connection and award tokens
try {
// Import dynamically to avoid server-side issues with window object
const { awardTokensForSighting } = await import('../../../services/blockchain/tokenRewardService');
// Get species from vision result for token amount calculation
const species = visionResult.mainObject || 'animal';
// Attempt to award tokens for this wildlife sighting
const tokenResult = await awardTokensForSighting(species);
// Add token reward info to response if successful
if (tokenResult.success) {
responseData.tokenReward = {
amount: tokenResult.tokenAmount,
txHash: tokenResult.txHash
};
console.log('Token reward successful:', tokenResult);
} else if (tokenResult.walletConnected) {
// Wallet was connected but award failed
console.error('Token reward failed:', tokenResult.error);
} else {
// No wallet connected - this is not an error, just informational
console.log('No wallet connected, skipping token reward');
}
} catch (tokenError) {
console.error('Error processing token reward:', tokenError);
// Continue without token rewards if there's an error
}
}
} catch (err) {
console.error('Failed to award points for image:', err);
@ -296,6 +328,9 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
console.error('Error in image processing or points awarding:', error);
}
}
// Create the final response with all data
const userResponse = NextResponse.json(responseData);
return userResponse

44
src/app/earn/page.tsx Normal file
View File

@ -0,0 +1,44 @@
'use client'
import React from 'react'
import Navigation from '../../components/Navigation'
import WildlifeIdentifier from '../../components/WildlifeIdentifier'
import { APP_CONFIG, getThemeColors } from '../../config/appConfig'
const EarnPage: React.FC = (): React.ReactElement => {
const theme = getThemeColors(APP_CONFIG.theme)
return (
<div className={`min-h-screen w-full flex flex-col items-center bg-gradient-to-b ${theme.gradient}`}>
<div className="container max-w-7xl mx-auto px-4 py-8">
<Navigation />
<div className="text-center mb-8 pt-24 sm:pt-20">
<h1 className={`text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-${theme.primary}-400 to-${theme.secondary}-300`}>
Earn WILD Tokens
</h1>
<p className={`text-${theme.text}-200 text-lg mb-8`}>
Identify wildlife species and earn WILD tokens on the Sei blockchain
</p>
</div>
<div className="max-w-2xl mx-auto relative">
{/* Decorative elements */}
<div className="absolute -top-4 -left-4 w-8 h-8 bg-emerald-500/10 rounded-full blur-lg" />
<div className="absolute -bottom-4 -right-4 w-8 h-8 bg-teal-500/10 rounded-full blur-lg" />
<WildlifeIdentifier />
</div>
{/* Info Section */}
<div className="mt-12 text-center text-emerald-300/60">
<p className="text-sm">
Built by <a href="https://mito.systems">Mito Systems</a> - Powered by <a href="https://sei.io">Sei Network</a>
</p>
</div>
</div>
</div>
)
}
export default EarnPage

View File

@ -2,14 +2,25 @@
'use client'
import React, { useState, useRef } from 'react'
import { Leaf } from 'lucide-react'
import { Leaf, Coins } from 'lucide-react'
import { APP_CONFIG } from '../config/appConfig'
interface TokenReward {
amount?: number;
txHash?: string;
}
interface AnalysisResult {
description?: string;
error?: string;
isAnimal?: boolean;
tokenReward?: TokenReward | null;
}
interface ImageAnalysisCardProps {
title: string
description: string
onAnalyze: (file: File) => Promise<{ description?: string, error?: string }>
onAnalyze: (file: File) => Promise<AnalysisResult>
}
interface AnalysisState {
@ -17,6 +28,7 @@ interface AnalysisState {
imageUrl: string | null
description: string | null
error: string | null
tokenReward: TokenReward | null
}
const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
@ -30,6 +42,7 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
imageUrl: null,
description: null,
error: null,
tokenReward: null
})
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -40,7 +53,8 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
...analysisState,
imageUrl,
description: null,
error: null
error: null,
tokenReward: null
})
}
}
@ -54,6 +68,7 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
...analysisState,
loading: true,
error: null,
tokenReward: null
})
try {
@ -74,6 +89,7 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
imageUrl: analysisState.imageUrl,
description: result.description,
error: null,
tokenReward: result.tokenReward || null
})
} else {
throw new Error('No analysis received')
@ -83,6 +99,7 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
...analysisState,
loading: false,
error: error instanceof Error ? error.message : 'Analysis failed',
tokenReward: null
})
}
}
@ -155,6 +172,23 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
{analysisState.description && (
<div className="mt-4 bg-green-900/30 border border-emerald-800/50 rounded-xl p-4">
<p className="text-emerald-200 whitespace-pre-wrap">{analysisState.description}</p>
{/* Token reward notification */}
{analysisState.tokenReward && analysisState.tokenReward.amount && (
<div className="mt-4 bg-amber-900/30 border border-amber-500/30 rounded-lg p-3 flex items-center gap-2">
<Coins className="text-amber-400 w-5 h-5" />
<div>
<p className="font-medium text-amber-300">
{analysisState.tokenReward.amount} WILD tokens awarded!
</p>
{analysisState.tokenReward.txHash && (
<p className="text-xs text-amber-400/70 mt-1">
Transaction: {analysisState.tokenReward.txHash.substring(0, 10)}...
</p>
)}
</div>
</div>
)}
</div>
)}
</div>

View File

@ -5,6 +5,7 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useSession } from 'next-auth/react'
import AuthButton from './AuthButton'
import WalletButton from './wallet/WalletButton'
const Navigation = () => {
const pathname = usePathname()
@ -15,6 +16,7 @@ const Navigation = () => {
const baseLinks = [
{ href: '/', label: 'Home' },
{ href: '/sightings', label: 'Sightings' },
{ href: '/earn', label: 'Earn Tokens' },
{ href: '/about', label: 'About' },
]
@ -51,8 +53,13 @@ const Navigation = () => {
</ul>
</nav>
<div className="border border-emerald-400/30 px-3 py-1 rounded ml-4 flex-shrink-0">
<AuthButton />
<div className="flex items-center gap-3">
<div className="relative">
<WalletButton />
</div>
<div className="border border-emerald-400/30 px-3 py-1 rounded flex-shrink-0">
<AuthButton />
</div>
</div>
</div>
)

View File

@ -0,0 +1,278 @@
'use client'
import React, { useState, useRef } from 'react'
import { Leaf, Coins, AlertCircle } from 'lucide-react'
import { APP_CONFIG } from '../config/appConfig'
import { isWalletConnected, getWalletAddress } from '../services/blockchain/seiService'
import { awardTokensForSighting } from '../services/blockchain/tokenRewardService'
// Wildlife API endpoint
const API_URL = process.env.NEXT_PUBLIC_WILDLIFE_API_URL || '/api';
interface AnalysisResult {
success: boolean;
message: string;
species?: string;
rewardAmount?: string;
txHash?: string;
error?: string;
}
const WildlifeIdentifier: React.FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [result, setResult] = useState<AnalysisResult | null>(null);
const [error, setError] = useState<string | null>(null);
// Handle image selection
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// Check if the file is an image
if (!file.type.startsWith('image/')) {
setError('Please select an image file');
return;
}
// Set the selected file
setSelectedImage(file);
// Create a preview URL
const imageUrl = URL.createObjectURL(file);
setPreviewUrl(imageUrl);
// Reset previous results and errors
setResult(null);
setError(null);
}
};
// Process the image and get token rewards
const handleAnalyze = async () => {
if (!selectedImage) {
setError('Please select an image first');
return;
}
// Check if wallet is connected
if (!isWalletConnected()) {
setError('Please connect your wallet first to receive rewards');
return;
}
setIsProcessing(true);
setError(null);
try {
// Convert image to base64
const reader = new FileReader();
reader.readAsDataURL(selectedImage);
reader.onloadend = async () => {
const base64Image = reader.result as string;
const userAddress = getWalletAddress();
if (!userAddress) {
setError('Wallet connection issue. Please reconnect your wallet.');
setIsProcessing(false);
return;
}
try {
// Option 1: Using the backend API
if (process.env.NEXT_PUBLIC_USE_WILDLIFE_API === 'true') {
// Send to the backend API
const response = await fetch(`${API_URL}/process-image`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageData: base64Image,
userWalletAddress: userAddress,
}),
});
const responseData = await response.json();
setResult(responseData);
}
// Option 2: Direct integration with the blockchain
else {
// Use our existing image analysis code
// This is a simplified example - you should replace this with your actual analysis code
const species = await simulateSpeciesDetection(selectedImage);
if (species) {
// Award tokens through the Sei blockchain
const rewardResult = await awardTokensForSighting(species);
if (rewardResult.success) {
setResult({
success: true,
message: `You've been awarded WILD tokens for identifying a ${species}!`,
species: species,
rewardAmount: rewardResult.tokenAmount?.toString(),
txHash: rewardResult.txHash
});
} else {
throw new Error(rewardResult.error || 'Failed to award tokens');
}
} else {
setResult({
success: false,
message: "No wildlife identified in this image."
});
}
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Error processing image');
} finally {
setIsProcessing(false);
}
};
reader.onerror = () => {
setError('Error reading the image file');
setIsProcessing(false);
};
} catch (err) {
setError(err instanceof Error ? err.message : 'Error processing image');
setIsProcessing(false);
}
};
// Format token amount (from microWILD to WILD)
const formatTokenAmount = (amount?: string): string => {
if (!amount) return '0';
return (parseInt(amount) / 1_000_000).toFixed(6);
};
// This simulates species detection - replace with your actual image analysis
const simulateSpeciesDetection = async (image: File): Promise<string | null> => {
// In a real app, you would use your actual image analysis service here
// This is just a simulation for demonstration purposes
const species = ['elephant', 'tiger', 'rhino', 'panda', 'gorilla', 'whale', 'dolphin', 'snow leopard', 'eagle'];
await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate processing time
// Randomly determine if we detected wildlife (80% chance)
if (Math.random() < 0.8) {
// Return a random species
return species[Math.floor(Math.random() * species.length)];
}
return null; // No wildlife detected
};
return (
<div className="w-full bg-emerald-900/20 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 mb-8 hover:shadow-emerald-500/20 transition-all duration-300">
<div className="p-6">
<div className="mb-4">
<div className="flex items-center gap-2">
<Leaf className="w-6 h-6 text-emerald-400" />
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
Wildlife Identifier
</h2>
</div>
<p className="text-emerald-200 mt-2">
Identify wildlife species and earn WILD tokens as rewards for conservation
</p>
</div>
<div className="space-y-4">
{/* Image Upload Area */}
<div
className="relative border-2 border-dashed border-emerald-800/50 rounded-xl p-4 text-center
hover:border-emerald-500/50 transition-colors duration-200
bg-emerald-950/30"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer
disabled:cursor-not-allowed"
disabled={isProcessing}
/>
<div className="space-y-2">
<div className="text-emerald-300">
{previewUrl ? (
<img
src={previewUrl}
alt="Selected wildlife"
className="max-h-64 mx-auto rounded-lg"
/>
) : (
<p>Upload a photo of wildlife to identify and earn rewards</p>
)}
</div>
</div>
</div>
<button
onClick={handleAnalyze}
disabled={isProcessing || !selectedImage || !isWalletConnected()}
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600
hover:to-teal-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-emerald-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{isProcessing ? 'Processing...' : 'Identify Wildlife & Earn Tokens'}
</button>
{!isWalletConnected() && (
<div className="bg-amber-900/20 border border-amber-500/20 text-amber-400 px-4 py-3 rounded-xl text-center flex items-center justify-center gap-2">
<AlertCircle className="w-5 h-5" />
<span>Please connect your wallet to earn token rewards</span>
</div>
)}
</div>
{error && (
<div className="mt-4 px-4 py-3 rounded-xl text-center bg-red-900/20 border border-red-500/20 text-red-400">
{error}
</div>
)}
{result && (
<div className={`mt-4 ${result.success ? 'bg-green-900/30 border border-emerald-800/50' : 'bg-amber-900/20 border border-amber-500/20'} rounded-xl p-4`}>
<p className={`${result.success ? 'text-emerald-200' : 'text-amber-400'} whitespace-pre-wrap`}>
{result.message}
</p>
{/* Token reward notification */}
{result.success && result.species && (
<div className="mt-4 bg-amber-900/30 border border-amber-500/30 rounded-lg p-3 flex items-center gap-2">
<Coins className="text-amber-400 w-5 h-5" />
<div>
<p className="font-medium text-amber-300">
{result.rewardAmount
? `${formatTokenAmount(result.rewardAmount)} WILD tokens awarded!`
: 'WILD tokens awarded!'}
</p>
{result.txHash && (
<p className="text-xs text-amber-400/70 mt-1">
Transaction: <a
href={`https://www.seiscan.app/atlantic-2/txs/${result.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-amber-300"
>
{result.txHash.substring(0, 10)}...
</a>
</p>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default WildlifeIdentifier;

View File

@ -0,0 +1,118 @@
// src/components/wallet/WalletButton.tsx
'use client'
import { useState, useEffect } from 'react'
import {
connectSeiWallet,
disconnectSeiWallet,
isWalletConnected,
getWalletAddress,
WALLET_EVENTS
} from '../../services/blockchain/seiService'
const WalletButton: React.FC = () => {
const [address, setAddress] = useState<string | null>(null)
const [isConnecting, setIsConnecting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Check if wallet is already connected on component mount
if (isWalletConnected()) {
setAddress(getWalletAddress())
}
// Listen for wallet connection events
const handleWalletConnected = (event: CustomEvent) => {
setAddress(event.detail.address)
setError(null)
setIsConnecting(false)
}
const handleWalletDisconnected = () => {
setAddress(null)
setIsConnecting(false)
}
const handleWalletError = (event: CustomEvent) => {
setError(typeof event.detail === 'string'
? event.detail
: 'Failed to connect wallet')
setIsConnecting(false)
}
// Add event listeners
window.addEventListener(WALLET_EVENTS.CONNECTED, handleWalletConnected as EventListener)
window.addEventListener(WALLET_EVENTS.DISCONNECTED, handleWalletDisconnected)
window.addEventListener(WALLET_EVENTS.ERROR, handleWalletError as EventListener)
// Clean up event listeners
return () => {
window.removeEventListener(WALLET_EVENTS.CONNECTED, handleWalletConnected as EventListener)
window.removeEventListener(WALLET_EVENTS.DISCONNECTED, handleWalletDisconnected)
window.removeEventListener(WALLET_EVENTS.ERROR, handleWalletError as EventListener)
}
}, [])
const handleConnect = async () => {
setIsConnecting(true)
setError(null)
try {
const result = await connectSeiWallet()
if (!result) {
setError('No wallet extension found. Please install Keplr or Leap wallet.')
}
} catch (err) {
console.error('Wallet connection error:', err)
setError('Failed to connect wallet')
} finally {
setIsConnecting(false)
}
}
const handleDisconnect = () => {
disconnectSeiWallet()
setAddress(null)
}
// Format address for display
const formatAddress = (addr: string) => {
if (!addr) return ''
return `${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}`
}
return (
<div className="flex items-center">
{address ? (
<button
onClick={handleDisconnect}
className="text-xs md:text-sm px-3 py-1 rounded-full bg-amber-500/20 text-amber-300 hover:bg-amber-500/30 transition-colors flex items-center gap-2"
>
<span className="hidden md:inline">{formatAddress(address)}</span>
<span className="inline md:hidden">Wallet</span>
<span className="w-2 h-2 rounded-full bg-green-400"></span>
</button>
) : (
<button
onClick={handleConnect}
disabled={isConnecting}
className={`text-xs md:text-sm px-3 py-1 rounded-full ${
isConnecting
? 'bg-gray-500/20 text-gray-300 cursor-not-allowed'
: 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/30'
} transition-colors`}
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
)}
{error && (
<div className="text-red-500 text-xs mt-1 absolute -bottom-6 left-0 right-0 text-center">
{error}
</div>
)}
</div>
)
}
export default WalletButton

View File

@ -0,0 +1,185 @@
// src/services/blockchain/seiService.ts
import { SeiWallet } from '@sei-js/core';
import { AccountData, OfflineSigner } from '@cosmjs/proto-signing';
import { getSigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
// Sei network configurations
const NETWORKS = {
testnet: {
chainId: 'atlantic-1',
rpcUrl: 'https://rpc.atlantic-1.sei.io',
restUrl: 'https://rest.atlantic-1.sei.io',
// Smart contract addresses
tokenFactoryAddress: process.env.NEXT_PUBLIC_SEI_TOKEN_FACTORY_ADDRESS || '',
},
};
// Track connected wallet state
let currentWallet: SeiWallet | null = null;
let currentAddress: string | null = null;
let cosmWasmClient: any = null;
// Custom event for wallet status changes
export const WALLET_EVENTS = {
CONNECTED: 'wallet_connected',
DISCONNECTED: 'wallet_disconnected',
ERROR: 'wallet_error',
};
// Dispatch custom events for wallet state changes
const dispatchWalletEvent = (eventName: string, detail?: any) => {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent(eventName, { detail }));
}
};
/**
* Connect to Sei wallet via Keplr, Leap, or other compatible wallets
*/
export const connectSeiWallet = async (): Promise<string | null> => {
try {
// Check for Keplr or other Sei compatible wallets
const isBrowserWalletAvailable = typeof window !== 'undefined' &&
(window.keplr !== undefined || window.leap !== undefined);
if (!isBrowserWalletAvailable) {
console.error('No compatible wallet extension found');
dispatchWalletEvent(WALLET_EVENTS.ERROR, 'No compatible wallet extension found');
return null;
}
// Initialize SeiWallet from @sei-js/core
const wallet = await SeiWallet.connectWallet();
if (!wallet) {
throw new Error('Failed to connect wallet');
}
// Get the connected wallet's address
const [account] = await wallet.getAccounts();
const address = account.address;
// Store wallet and address
currentWallet = wallet;
currentAddress = address;
// Initialize CosmWasm client for contract interactions
const network = NETWORKS.testnet;
cosmWasmClient = await getSigningCosmWasmClient(
network.rpcUrl,
wallet as unknown as OfflineSigner
);
console.log('Sei wallet connected:', address);
dispatchWalletEvent(WALLET_EVENTS.CONNECTED, { address });
return address;
} catch (error) {
console.error('Error connecting Sei wallet:', error);
dispatchWalletEvent(WALLET_EVENTS.ERROR, error);
return null;
}
};
/**
* Disconnect the currently connected wallet
*/
export const disconnectSeiWallet = () => {
currentWallet = null;
currentAddress = null;
cosmWasmClient = null;
dispatchWalletEvent(WALLET_EVENTS.DISCONNECTED);
console.log('Sei wallet disconnected');
return true;
};
/**
* Check if wallet is connected
*/
export const isWalletConnected = (): boolean => {
return currentWallet !== null && currentAddress !== null;
};
/**
* Get the connected wallet's address
*/
export const getWalletAddress = (): string | null => {
return currentAddress;
};
/**
* Award WILD tokens to a user for wildlife sighting
* This interacts with the Wildlife Token smart contract on Sei
*/
export const awardTokensForWildlife = async (
species: string,
amount: number = 10
): Promise<{ success: boolean; txHash?: string; error?: string }> => {
try {
if (!isWalletConnected() || !currentAddress || !cosmWasmClient) {
return {
success: false,
error: 'Wallet not connected'
};
}
// WILD token contract address from environment or config
const tokenAddress = NETWORKS.testnet.tokenFactoryAddress;
if (!tokenAddress) {
return {
success: false,
error: 'WILD token contract address not configured'
};
}
// Execute CosmWasm contract message to award WILD tokens based on species
// The contract determines the actual amount based on species rarity
const result = await cosmWasmClient.execute(
currentAddress,
tokenAddress,
{ award_tokens: { recipient: currentAddress, species: species.toLowerCase() } },
'auto',
undefined, // memo
[] // funds - no funds sent with this execution
);
console.log('WILD tokens awarded successfully:', result);
return {
success: true,
txHash: result.transactionHash
};
} catch (error) {
console.error('Error awarding WILD tokens:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during token award'
};
}
};
/**
* Get WILD token balance for connected wallet
*/
export const getTokenBalance = async (): Promise<number> => {
try {
if (!isWalletConnected() || !currentAddress || !cosmWasmClient) {
return 0;
}
const tokenAddress = NETWORKS.testnet.tokenFactoryAddress;
if (!tokenAddress) {
return 0;
}
// Query WILD token balance from CosmWasm contract
const balance = await cosmWasmClient.queryContractSmart(
tokenAddress,
{ balance: { address: currentAddress } }
);
// Parse the balance - CosmWasm contracts often return strings for big numbers
return parseInt(balance.balance) || 0;
} catch (error) {
console.error('Error getting WILD token balance:', error);
return 0;
}
};

View File

@ -0,0 +1,89 @@
// src/services/blockchain/tokenRewardService.ts
import { awardTokensForWildlife } from './seiService';
import { isWalletConnected, getWalletAddress } from './seiService';
// Note: These reward amounts are for display purposes only
// The actual reward amounts are determined by the WILD token contract on Sei
export const TOKEN_REWARDS = {
WILDLIFE_SIGHTING: 10, // Base WILD token reward for any wildlife sighting
RARE_SPECIES: 25, // Additional WILD tokens for rare species
};
// List of species considered rare for bonus rewards
const RARE_SPECIES = [
'elephant',
'tiger',
'rhino',
'panda',
'gorilla',
'whale',
'dolphin',
'snow leopard',
'eagle',
// Add more rare species as needed
];
/**
* Determine if a species is considered rare
* @param species The detected animal species
* @returns True if the species is considered rare
*/
export const isRareSpecies = (species: string): boolean => {
if (!species) return false;
const normalizedSpecies = species.toLowerCase().trim();
return RARE_SPECIES.some(rareSpecies =>
normalizedSpecies.includes(rareSpecies)
);
};
/**
* Award tokens for a wildlife sighting if wallet is connected
* @param species The detected animal species
* @returns Object containing success status and transaction info
*/
export const awardTokensForSighting = async (species: string): Promise<{
success: boolean;
tokenAmount?: number;
txHash?: string;
error?: string;
walletConnected: boolean;
}> => {
try {
// Check if wallet is connected
if (!isWalletConnected()) {
return {
success: false,
error: 'No wallet connected',
walletConnected: false
};
}
// Calculate token reward amount
const baseReward = TOKEN_REWARDS.WILDLIFE_SIGHTING;
const isRare = isRareSpecies(species);
const rareBonus = isRare ? TOKEN_REWARDS.RARE_SPECIES : 0;
const totalReward = baseReward + rareBonus;
// Award WILD tokens through Sei contract based on species
const result = await awardTokensForWildlife(species, totalReward);
if (result.success) {
console.log(`Awarded ${totalReward} tokens for ${species} sighting to wallet ${getWalletAddress()}`);
return {
success: true,
tokenAmount: totalReward,
txHash: result.txHash,
walletConnected: true
};
} else {
throw new Error(result.error || 'Failed to award tokens');
}
} catch (error) {
console.error('Error in token reward service:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error processing token reward',
walletConnected: true
};
}
};

View File

@ -0,0 +1,14 @@
# Configuration for Sei blockchain integration
# Copy this file to .env and replace with your actual values
# Sei testnet configuration - Atlantic-1
NEXT_PUBLIC_SEI_NETWORK="atlantic-1"
NEXT_PUBLIC_SEI_RPC_URL="https://rpc.atlantic-1.sei.io"
NEXT_PUBLIC_SEI_REST_URL="https://rest.atlantic-1.sei.io"
# WILD token contract address
# Replace with your deployed contract address after deployment
NEXT_PUBLIC_SEI_TOKEN_FACTORY_ADDRESS="sei1..."
# Optional: Wallet for automated tests (use only for testing)
# SEI_TEST_MNEMONIC="your twelve word mnemonic here only for testing"

View File

@ -0,0 +1,55 @@
# Wildlife Token Smart Contract
This directory contains the smart contract code for the Wildlife Token (WILD) used in the Wildlife Sightings application.
## Overview
The `WildlifeToken.sol` contract is an ERC-20 token designed to be deployed on the Sei EVM. It provides functionality for:
- Minting tokens as rewards for wildlife documentation
- Setting different reward amounts for various wildlife species
- Managing authorized distributors who can award tokens
## Deployment Instructions
To deploy this contract on the Sei EVM:
1. Install Hardhat or Truffle as your Ethereum development framework
2. Configure your deployment script to target Sei EVM
3. Deploy with an RPC endpoint for Sei testnet (atlantic-1):
- RPC URL: https://rpc.atlantic-1.sei.io
- Chain ID: 1
## Contract Configuration
Before deployment, you may want to customize:
- The token name and symbol
- Maximum token supply
- Initial token distribution
- Default reward amounts for different species
## Authorizing the Backend
After deployment, you'll need to:
1. Call `setDistributor(backendAddress, true)` to authorize your backend server to distribute tokens
2. Configure the backend with the deployed contract address
3. Update the `.env` file with:
```
NEXT_PUBLIC_SEI_TOKEN_FACTORY_ADDRESS=0x... (your deployed contract address)
```
## Security Considerations
- Only authorized distributors can mint new tokens
- The contract has a capped supply to prevent inflation
- The owner can update reward amounts for different species
## Testing
Before mainnet deployment, thoroughly test the contract on Sei testnet:
1. Test token minting and distribution
2. Verify reward calculations for different species
3. Check authorization controls

View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title WildlifeToken
* @dev ERC20 token for rewarding wildlife documentation contributions
* To be deployed on Sei Atlantic-1 testnet EVM
*/
contract WildlifeToken is ERC20Capped, Ownable {
// Events
event TokensAwarded(address indexed recipient, uint256 amount, string species);
// Reward amounts for different wildlife types
mapping(string => uint256) public speciesRewards;
// List of authorized reward distributors
mapping(address => bool) public authorizedDistributors;
// Maximum token supply: 100 million tokens
uint256 private constant MAX_SUPPLY = 100_000_000 * 10**18;
/**
* @dev Constructor that sets up the Wildlife Token
*/
constructor()
ERC20("Wildlife Conservation Token", "WILD")
ERC20Capped(MAX_SUPPLY)
Ownable(msg.sender)
{
// Initialize with base reward values (in tokens, will be multiplied by decimals)
speciesRewards["default"] = 10 * 10**18; // Default reward
// Rare species receive higher rewards
speciesRewards["elephant"] = 25 * 10**18;
speciesRewards["tiger"] = 25 * 10**18;
speciesRewards["rhino"] = 30 * 10**18;
speciesRewards["panda"] = 25 * 10**18;
speciesRewards["gorilla"] = 25 * 10**18;
speciesRewards["whale"] = 25 * 10**18;
speciesRewards["dolphin"] = 15 * 10**18;
speciesRewards["snow leopard"] = 30 * 10**18;
speciesRewards["eagle"] = 15 * 10**18;
// Set contract creator as an authorized distributor
authorizedDistributors[msg.sender] = true;
// Mint initial supply for rewards pool (10% of total supply)
_mint(msg.sender, MAX_SUPPLY / 10);
}
/**
* @dev Add or update a distributor authorization
* @param distributor Address to authorize or deauthorize
* @param authorized True to authorize, false to deauthorize
*/
function setDistributor(address distributor, bool authorized) external onlyOwner {
authorizedDistributors[distributor] = authorized;
}
/**
* @dev Set the reward amount for a specific species
* @param species Name of the wildlife species
* @param amount Reward amount in tokens
*/
function setSpeciesReward(string calldata species, uint256 amount) external onlyOwner {
speciesRewards[species] = amount;
}
/**
* @dev Award tokens to a user for documenting wildlife
* @param recipient Address of the recipient
* @param species Name of the wildlife species documented
* @return Amount of tokens awarded
*/
function awardTokens(address recipient, string calldata species) external returns (uint256) {
// Check if caller is an authorized distributor
require(authorizedDistributors[msg.sender], "Not authorized to distribute tokens");
// Get reward amount for the species (or default if not set)
uint256 rewardAmount = speciesRewards[species];
if (rewardAmount == 0) {
rewardAmount = speciesRewards["default"];
}
// Mint tokens to the recipient
_mint(recipient, rewardAmount);
// Emit event for tracking
emit TokensAwarded(recipient, rewardAmount, species);
return rewardAmount;
}
/**
* @dev Override _mint to respect the cap
*/
function _mint(address account, uint256 amount) internal override(ERC20, ERC20Capped) {
ERC20Capped._mint(account, amount);
}
}

View File

@ -0,0 +1,75 @@
# Wildlife Token CosmWasm Contract
This directory contains the CosmWasm smart contract code for the Wildlife Token (WILD) used in the Wildlife Sightings application.
## Overview
The `wildlife_token.rs` contract is a CW20-compatible token designed for the Sei blockchain. It builds on the standard CW20 token implementation and adds functionality for:
- Minting tokens as rewards for wildlife documentation
- Setting different reward amounts for various wildlife species
- Managing authorized distributors who can award tokens
## Deployment Instructions
To deploy this contract on the Sei Atlantic-1 testnet:
1. Set up a Rust development environment with CosmWasm
2. Compile the contract to Wasm
3. Deploy the compiled contract using a Sei wallet with:
```
sei-cli tx wasm store wildlife_token.wasm --from <your_wallet> --chain-id atlantic-1 --gas-prices 0.01usei
```
4. Instantiate the deployed contract:
```
sei-cli tx wasm instantiate <code_id> '{"name":"Wildlife Conservation Token","symbol":"WILD","decimals":6}' --from <your_wallet> --label "Wildlife Token" --chain-id atlantic-1
```
## Contract Configuration
The contract is pre-configured with:
- Token name: "Wildlife Conservation Token"
- Token symbol: "WILD"
- Decimals: 6
- Initial supply: 10% of max supply (10,000,000 WILD)
- Default reward: 10 WILD per sighting
- Rare species rewards: 15-30 WILD depending on rarity
## Authorizing the Backend
After deployment, you'll need to:
1. Call the `add_distributor` function to authorize your backend server to distribute tokens
2. Configure the backend with the deployed contract address
3. Update the `.env` file with:
```
NEXT_PUBLIC_SEI_TOKEN_FACTORY_ADDRESS=sei1... (your deployed contract address)
```
## Testing
Before using on the testnet, thoroughly test the contract using:
1. Unit tests for contract functionality
2. Test token minting and distribution on the Sei testnet
3. Verify reward calculations for different species
## Contract Messages
The contract supports the following messages:
### Execute Messages
- Standard CW20 messages for transfers, allowances, etc.
- `set_species_reward`: Set the reward amount for a specific species
- `award_tokens`: Mint and send tokens to a user who documented wildlife
- `add_distributor`: Authorize an address to award tokens
- `remove_distributor`: Remove distributor authorization
### Query Messages
- Standard CW20 balance and token info queries
- `species_reward`: Get the reward amount for a specific species
- `is_distributor`: Check if an address is authorized to award tokens
- `all_species_rewards`: List all configured species and their reward amounts

View File

@ -0,0 +1,352 @@
// wildlife_token.rs - CosmWasm contract for Wildlife Token (WILD)
// This is a simplified example of a CosmWasm contract for the Sei blockchain
// To be deployed on Atlantic-1 testnet
use cosmwasm_std::{
entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
Addr,
};
use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg};
use cw20_base::contract::{execute as cw20_execute, query as cw20_query};
use cw20_base::msg::{ExecuteMsg as Cw20ExecuteMsg, QueryMsg as Cw20QueryMsg};
use cw20_base::state::{MinterData, TokenInfo, TOKEN_INFO};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// Custom messages for our Wildlife Token contract
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
// Standard CW20 messages
Transfer { recipient: String, amount: Uint128 },
Burn { amount: Uint128 },
Send { contract: String, amount: Uint128, msg: Binary },
IncreaseAllowance { spender: String, amount: Uint128, expires: Option<u64> },
DecreaseAllowance { spender: String, amount: Uint128, expires: Option<u64> },
TransferFrom { owner: String, recipient: String, amount: Uint128 },
BurnFrom { owner: String, amount: Uint128 },
SendFrom { owner: String, contract: String, amount: Uint128, msg: Binary },
// Custom Wildlife Token messages
SetSpeciesReward { species: String, amount: Uint128 },
AwardTokens { recipient: String, species: String },
AddDistributor { address: String },
RemoveDistributor { address: String },
}
// State for tracking distributors and species rewards
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct WildlifeState {
pub distributors: Vec<Addr>,
pub species_rewards: HashMap<String, Uint128>,
}
// Initialize the contract
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// Initialize CW20 token
let token_info = TokenInfo {
name: "Wildlife Conservation Token".to_string(),
symbol: "WILD".to_string(),
decimals: 6,
total_supply: Uint128::zero(),
mint: Some(MinterData {
minter: info.sender.clone(),
cap: Some(Uint128::from(100_000_000_000_000u128)), // 100 million tokens with 6 decimals
}),
};
TOKEN_INFO.save(deps.storage, &token_info)?;
// Initialize wildlife-specific state
let mut species_rewards = HashMap::new();
// Set default reward
species_rewards.insert("default".to_string(), Uint128::from(10_000_000u128)); // 10 WILD
// Set rewards for rare species
species_rewards.insert("elephant".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("tiger".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("rhino".to_string(), Uint128::from(30_000_000u128));
species_rewards.insert("panda".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("gorilla".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("whale".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("dolphin".to_string(), Uint128::from(15_000_000u128));
species_rewards.insert("snow leopard".to_string(), Uint128::from(30_000_000u128));
species_rewards.insert("eagle".to_string(), Uint128::from(15_000_000u128));
let wildlife_state = WildlifeState {
distributors: vec![info.sender.clone()],
species_rewards,
};
WILDLIFE_STATE.save(deps.storage, &wildlife_state)?;
// Mint initial supply for rewards pool (10% of max supply)
let initial_supply = Uint128::from(10_000_000_000_000u128);
cw20_execute(
deps,
_env,
info,
Cw20ExecuteMsg::Mint {
recipient: info.sender.to_string(),
amount: initial_supply,
},
)?;
Ok(Response::default()
.add_attribute("method", "instantiate")
.add_attribute("token_name", "Wildlife Conservation Token")
.add_attribute("token_symbol", "WILD")
.add_attribute("initial_supply", initial_supply))
}
// Execute function for handling messages
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
match msg {
// For standard CW20 operations, use the base implementation
ExecuteMsg::Transfer { recipient, amount } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::Transfer { recipient, amount })
}
ExecuteMsg::Burn { amount } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::Burn { amount })
}
ExecuteMsg::Send { contract, amount, msg } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::Send { contract, amount, msg })
}
ExecuteMsg::IncreaseAllowance { spender, amount, expires } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::IncreaseAllowance { spender, amount, expires })
}
ExecuteMsg::DecreaseAllowance { spender, amount, expires } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::DecreaseAllowance { spender, amount, expires })
}
ExecuteMsg::TransferFrom { owner, recipient, amount } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::TransferFrom { owner, recipient, amount })
}
ExecuteMsg::BurnFrom { owner, amount } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::BurnFrom { owner, amount })
}
ExecuteMsg::SendFrom { owner, contract, amount, msg } => {
cw20_execute(deps, env, info, Cw20ExecuteMsg::SendFrom { owner, contract, amount, msg })
}
// Custom wildlife token operations
ExecuteMsg::SetSpeciesReward { species, amount } => {
// Only contract owner can set rewards
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(cosmwasm_std::StdError::generic_err("Unauthorized"));
}
} else {
return Err(cosmwasm_std::StdError::generic_err("No minter set"));
}
let mut state = WILDLIFE_STATE.load(deps.storage)?;
state.species_rewards.insert(species.clone(), amount);
WILDLIFE_STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "set_species_reward")
.add_attribute("species", species)
.add_attribute("amount", amount.to_string()))
}
ExecuteMsg::AwardTokens { recipient, species } => {
// Check if caller is an authorized distributor
let state = WILDLIFE_STATE.load(deps.storage)?;
if !state.distributors.contains(&info.sender) {
return Err(cosmwasm_std::StdError::generic_err("Not authorized to distribute tokens"));
}
// Get reward amount for the species (or default if not set)
let species_lower = species.to_lowercase();
let reward_amount = match state.species_rewards.get(&species_lower) {
Some(amount) => *amount,
None => match state.species_rewards.get("default") {
Some(amount) => *amount,
None => return Err(cosmwasm_std::StdError::generic_err("No default reward set")),
},
};
// Get recipient address
let recipient_addr = deps.api.addr_validate(&recipient)?;
// Mint tokens to the recipient
cw20_execute(
deps,
env,
info,
Cw20ExecuteMsg::Mint {
recipient: recipient_addr.to_string(),
amount: reward_amount,
},
)?;
Ok(Response::new()
.add_attribute("action", "award_tokens")
.add_attribute("recipient", recipient)
.add_attribute("species", species)
.add_attribute("amount", reward_amount.to_string()))
}
ExecuteMsg::AddDistributor { address } => {
// Only contract owner can add distributors
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(cosmwasm_std::StdError::generic_err("Unauthorized"));
}
} else {
return Err(cosmwasm_std::StdError::generic_err("No minter set"));
}
let addr = deps.api.addr_validate(&address)?;
let mut state = WILDLIFE_STATE.load(deps.storage)?;
if !state.distributors.contains(&addr) {
state.distributors.push(addr.clone());
WILDLIFE_STATE.save(deps.storage, &state)?;
}
Ok(Response::new()
.add_attribute("action", "add_distributor")
.add_attribute("address", address))
}
ExecuteMsg::RemoveDistributor { address } => {
// Only contract owner can remove distributors
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(cosmwasm_std::StdError::generic_err("Unauthorized"));
}
} else {
return Err(cosmwasm_std::StdError::generic_err("No minter set"));
}
let addr = deps.api.addr_validate(&address)?;
let mut state = WILDLIFE_STATE.load(deps.storage)?;
state.distributors.retain(|x| x != &addr);
WILDLIFE_STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "remove_distributor")
.add_attribute("address", address))
}
}
}
// Query function for reading contract state
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
// For standard CW20 queries, use the base implementation
QueryMsg::Balance { address } => cw20_query(deps, _env, Cw20QueryMsg::Balance { address }),
QueryMsg::TokenInfo {} => cw20_query(deps, _env, Cw20QueryMsg::TokenInfo {}),
QueryMsg::Minter {} => cw20_query(deps, _env, Cw20QueryMsg::Minter {}),
QueryMsg::Allowance { owner, spender } => {
cw20_query(deps, _env, Cw20QueryMsg::Allowance { owner, spender })
}
QueryMsg::AllAllowances { owner, start_after, limit } => {
cw20_query(deps, _env, Cw20QueryMsg::AllAllowances { owner, start_after, limit })
}
QueryMsg::AllAccounts { start_after, limit } => {
cw20_query(deps, _env, Cw20QueryMsg::AllAccounts { start_after, limit })
}
// Custom wildlife token queries
QueryMsg::SpeciesReward { species } => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let species_lower = species.to_lowercase();
let reward = match state.species_rewards.get(&species_lower) {
Some(amount) => *amount,
None => match state.species_rewards.get("default") {
Some(amount) => *amount,
None => Uint128::zero(),
},
};
to_binary(&SpeciesRewardResponse { species, reward })
}
QueryMsg::IsDistributor { address } => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let addr = deps.api.addr_validate(&address)?;
let is_distributor = state.distributors.contains(&addr);
to_binary(&IsDistributorResponse { address, is_distributor })
}
QueryMsg::AllSpeciesRewards {} => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let rewards: Vec<SpeciesReward> = state.species_rewards
.iter()
.map(|(species, amount)| SpeciesReward {
species: species.clone(),
reward: *amount,
})
.collect();
to_binary(&AllSpeciesRewardsResponse { rewards })
}
}
}
// Custom query messages
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
// Standard CW20 queries
Balance { address: String },
TokenInfo {},
Minter {},
Allowance { owner: String, spender: String },
AllAllowances { owner: String, start_after: Option<String>, limit: Option<u32> },
AllAccounts { start_after: Option<String>, limit: Option<u32> },
// Custom Wildlife Token queries
SpeciesReward { species: String },
IsDistributor { address: String },
AllSpeciesRewards {},
}
// Query response types
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SpeciesRewardResponse {
pub species: String,
pub reward: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct IsDistributorResponse {
pub address: String,
pub is_distributor: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SpeciesReward {
pub species: String,
pub reward: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AllSpeciesRewardsResponse {
pub rewards: Vec<SpeciesReward>,
}
// Instantiate message
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub name: String,
pub symbol: String,
pub decimals: u8,
}
// State variable for wildlife-specific data
pub const WILDLIFE_STATE: Item<WildlifeState> = Item::new("wildlife_state");

View File

@ -0,0 +1,5 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema"
integration-test = "test --lib integration_tests"

View File

@ -0,0 +1,61 @@
version: 2.1
executors:
builder:
docker:
- image: buildpack-deps:trusty
jobs:
docker-image:
executor: builder
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run:
name: Build Docker artifact
command: docker build --pull -t "cosmwasm/cw-gitpod-base:${CIRCLE_SHA1}" .
- run:
name: Push application Docker image to docker hub
command: |
if [ "${CIRCLE_BRANCH}" = "master" ]; then
docker tag "cosmwasm/cw-gitpod-base:${CIRCLE_SHA1}" cosmwasm/cw-gitpod-base:latest
docker login --password-stdin -u "$DOCKER_USER" \<<<"$DOCKER_PASS"
docker push cosmwasm/cw-gitpod-base:latest
docker logout
fi
docker-tagged:
executor: builder
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run:
name: Push application Docker image to docker hub
command: |
docker tag "cosmwasm/cw-gitpod-base:${CIRCLE_SHA1}" "cosmwasm/cw-gitpod-base:${CIRCLE_TAG}"
docker login --password-stdin -u "$DOCKER_USER" \<<<"$DOCKER_PASS"
docker push
docker logout
workflows:
version: 2
test-suite:
jobs:
# this is now a slow process... let's only run on master
- docker-image:
filters:
branches:
only:
- master
- docker-tagged:
filters:
tags:
only:
- /^v.*/
branches:
ignore:
- /.*/
requires:
- docker-image

View File

@ -0,0 +1,11 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.rs]
indent_size = 4

View File

@ -0,0 +1,75 @@
# Based on https://github.com/actions-rs/example/blob/master/.github/workflows/quickstart.yml
on: [push, pull_request]
name: Basic
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
target: wasm32-unknown-unknown
override: true
- name: Run unit tests
uses: actions-rs/cargo@v1
with:
command: unit-test
args: --locked
env:
RUST_BACKTRACE: 1
- name: Compile WASM contract
uses: actions-rs/cargo@v1
with:
command: wasm
args: --locked
env:
RUSTFLAGS: "-C link-arg=-s"
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
override: true
components: rustfmt, clippy
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- name: Generate Schema
uses: actions-rs/cargo@v1
with:
command: schema
args: --locked
- name: Schema Changes
# fails if any changes not committed
run: git diff --exit-code schema

View File

@ -0,0 +1,35 @@
name: release wasm
on:
release:
types: [created]
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install cargo-run-script
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-run-script
- name: Run cargo optimize
uses: actions-rs/cargo@v1
with:
command: run-script
args: optimize
- name: Get release ID
id: get_release
uses: bruceadams/get-release@v1.2.3
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload optimized wasm
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/*.wasm
tag: ${{ github.ref }}
overwrite: true
file_glob: true

View File

@ -0,0 +1,16 @@
# Build results
/target
/schema
# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
.cargo-ok
# Text file backups
**/*.rs.bk
# macOS
.DS_Store
# IDEs
*.iml
.idea

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
[package]
name = "wildlife_token"
version = "0.1.0"
authors = ["zramsay <zach@bluecollarcoding.ca>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
# Use this to enable backtraces in WASM
backtraces = ["cosmwasm-std/backtraces"]
[package.metadata.scripts]
optimize = """docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.10
"""
[dependencies]
cosmwasm-schema = "1.1.9"
cosmwasm-std = { version = "1.1.9", default-features = false }
cw-storage-plus = "0.16.0"
cw2 = "0.16.0"
cw20 = "0.16.0"
cw20-base = { version = "0.16.0", features = ["library"] }
schemars = "0.8.12"
serde = { version = "1.0.152", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.38" }
[dev-dependencies]
cw-multi-test = "0.16.0"

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,13 @@
Copyright 2025 zramsay <zach@bluecollarcoding.ca>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,137 @@
# Wildlife Token (WILD) - CosmWasm Contract
This is a CosmWasm implementation of the Wildlife Token (WILD) for the Sei blockchain. The token rewards users for identifying wildlife in images, with different species having different reward amounts.
## Overview
The Wildlife Token is a CW20-compliant token with additional features for managing species-specific rewards and authorized distributors.
Key features:
- Standard CW20 token functionality (transfer, allowances, etc.)
- Species-specific reward amounts
- Authorized distributor management
- Minting rewards based on verified wildlife identification
## Contract Components
1. **Token Info**: Standard CW20 token with 6 decimals
2. **Species Rewards**: Configurable reward amounts for different wildlife species
3. **Distributors**: Authorized addresses that can award tokens
## Building and Testing
```sh
# Build the contract
cargo build
# Run tests
cargo test
# Generate schema
cargo schema
# Create optimized WASM for deployment
# Use an appropriate rust-optimizer version compatible with Atlantic-2
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.6
```
## Deployment to Atlantic-2 Testnet
To deploy this contract to the Sei Atlantic-2 testnet:
1. **Install Sei Daemon**:
```sh
# Install sei daemon based on your OS
# See: https://docs.sei.io/develop/deploy-contracts/deploy-on-sei
```
2. **Store WASM on Chain**:
```sh
# Set up your wallet
sei-daemon keys add my-wallet
# Store the WASM on-chain (after optimization)
sei-daemon tx wasm store artifacts/wildlife_token.wasm \
--from my-wallet \
--chain-id atlantic-2 \
--gas auto \
--gas-adjustment 1.3 \
--node https://rpc.atlantic-2.seinetwork.io:443 \
--yes
```
3. **Instantiate the Contract**:
```sh
# Get the code ID from the store transaction
sei-daemon tx wasm instantiate [CODE_ID] \
'{"name":"Wildlife Conservation Token","symbol":"WILD","decimals":6}' \
--label "wildlife-token" \
--from my-wallet \
--chain-id atlantic-2 \
--gas auto \
--gas-adjustment 1.3 \
--node https://rpc.atlantic-2.seinetwork.io:443 \
--yes
```
4. **Contract Interaction Examples**:
```sh
# Query token info
sei-daemon query wasm contract-state smart [CONTRACT_ADDRESS] '{"token_info":{}}'
# Add a distributor
sei-daemon tx wasm execute [CONTRACT_ADDRESS] \
'{"add_distributor":{"address":"sei..."}}' \
--from my-wallet \
--chain-id atlantic-2 \
--gas auto \
--gas-adjustment 1.3 \
--node https://rpc.atlantic-2.seinetwork.io:443 \
--yes
# Award tokens for a wildlife identification
sei-daemon tx wasm execute [CONTRACT_ADDRESS] \
'{"award_tokens":{"recipient":"sei...","species":"elephant"}}' \
--from my-wallet \
--chain-id atlantic-2 \
--gas auto \
--gas-adjustment 1.3 \
--node https://rpc.atlantic-2.seinetwork.io:443 \
--yes
```
## Sei Dashboard Deployment
You can also deploy the contract using the Sei Dashboard available at https://app.atlantic-2.seinetwork.io/:
1. Navigate to the Contracts section
2. Click "Upload Contract"
3. Select your optimized WASM file
4. After upload, instantiate the contract with the parameters:
```json
{
"name": "Wildlife Conservation Token",
"symbol": "WILD",
"decimals": 6
}
```
5. Use the dashboard to interact with your contract
## License
Copyright 2023 Wildlife Token Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1 @@
1d9a6b82b201c4f2f691fa03b879266cfb641e0482c223915f684de4431a2c6a wildlife_token.wasm

View File

@ -0,0 +1 @@
6db20ed197f7a0f1fb402f7d72f2934e2dab9337f979aee4ba0c964802cd04ba /target/wasm32-unknown-unknown/release/wildlife_token.wasm

View File

@ -0,0 +1,11 @@
use cosmwasm_schema::write_api;
use wildlife_token::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ExecuteMsg,
query: QueryMsg,
}
}

View File

@ -0,0 +1,309 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128};
use cw2::set_contract_version;
use cw20_base::contract::{execute as cw20_execute, query as cw20_query};
use cw20_base::msg::{ExecuteMsg as Cw20ExecuteMsg, QueryMsg as Cw20QueryMsg};
use cw20_base::state::{MinterData, TokenInfo, TOKEN_INFO};
use std::collections::HashMap;
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, SpeciesRewardResponse, IsDistributorResponse, AllSpeciesRewardsResponse, SpeciesReward};
use crate::state::{WildlifeState, WILDLIFE_STATE};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:wildlife_token";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// Initialize CW20 token
let token_info = TokenInfo {
name: msg.name.clone(),
symbol: msg.symbol.clone(),
decimals: msg.decimals,
total_supply: Uint128::zero(),
mint: Some(MinterData {
minter: info.sender.clone(),
cap: Some(Uint128::from(100_000_000_000_000u128)), // 100 million tokens with 6 decimals
}),
};
TOKEN_INFO.save(deps.storage, &token_info)?;
// Initialize wildlife-specific state
let mut species_rewards = HashMap::new();
// Set default reward
species_rewards.insert("default".to_string(), Uint128::from(10_000_000u128)); // 10 WILD
// Set rewards for rare species
species_rewards.insert("elephant".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("tiger".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("rhino".to_string(), Uint128::from(30_000_000u128));
species_rewards.insert("panda".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("gorilla".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("whale".to_string(), Uint128::from(25_000_000u128));
species_rewards.insert("dolphin".to_string(), Uint128::from(15_000_000u128));
species_rewards.insert("snow leopard".to_string(), Uint128::from(30_000_000u128));
species_rewards.insert("eagle".to_string(), Uint128::from(15_000_000u128));
let wildlife_state = WildlifeState {
distributors: vec![info.sender.clone()],
species_rewards,
};
WILDLIFE_STATE.save(deps.storage, &wildlife_state)?;
// Mint initial supply for rewards pool (10% of max supply)
let initial_supply = Uint128::from(10_000_000_000_000u128);
cw20_execute(
deps,
_env,
info.clone(),
Cw20ExecuteMsg::Mint {
recipient: info.sender.to_string(),
amount: initial_supply,
},
)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("token_name", msg.name)
.add_attribute("token_symbol", msg.symbol)
.add_attribute("initial_supply", initial_supply.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
// For standard CW20 operations, use the base implementation
ExecuteMsg::Transfer { recipient, amount } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::Transfer { recipient, amount })?)
}
ExecuteMsg::Burn { amount } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::Burn { amount })?)
}
ExecuteMsg::Send { contract, amount, msg } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::Send { contract, amount, msg })?)
}
ExecuteMsg::IncreaseAllowance { spender, amount, expires } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::IncreaseAllowance { spender, amount, expires })?)
}
ExecuteMsg::DecreaseAllowance { spender, amount, expires } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::DecreaseAllowance { spender, amount, expires })?)
}
ExecuteMsg::TransferFrom { owner, recipient, amount } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::TransferFrom { owner, recipient, amount })?)
}
ExecuteMsg::BurnFrom { owner, amount } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::BurnFrom { owner, amount })?)
}
ExecuteMsg::SendFrom { owner, contract, amount, msg } => {
Ok(cw20_execute(deps, env, info, Cw20ExecuteMsg::SendFrom { owner, contract, amount, msg })?)
}
// Custom wildlife token operations
ExecuteMsg::SetSpeciesReward { species, amount } => {
// Only contract owner can set rewards
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(ContractError::Unauthorized {});
}
} else {
return Err(ContractError::NoMinterSet {});
}
let mut state = WILDLIFE_STATE.load(deps.storage)?;
state.species_rewards.insert(species.clone(), amount);
WILDLIFE_STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "set_species_reward")
.add_attribute("species", species)
.add_attribute("amount", amount.to_string()))
}
ExecuteMsg::AwardTokens { recipient, species } => {
// Check if caller is an authorized distributor
let state = WILDLIFE_STATE.load(deps.storage)?;
if !state.distributors.contains(&info.sender) {
return Err(ContractError::NotAuthorizedDistributor {});
}
// Get reward amount for the species (or default if not set)
let species_lower = species.to_lowercase();
let reward_amount = match state.species_rewards.get(&species_lower) {
Some(amount) => *amount,
None => match state.species_rewards.get("default") {
Some(amount) => *amount,
None => return Err(ContractError::NoDefaultReward {}),
},
};
// Get recipient address
let recipient_addr = deps.api.addr_validate(&recipient)?;
// Mint tokens to the recipient
cw20_execute(
deps,
env,
info,
Cw20ExecuteMsg::Mint {
recipient: recipient_addr.to_string(),
amount: reward_amount,
},
)?;
Ok(Response::new()
.add_attribute("action", "award_tokens")
.add_attribute("recipient", recipient)
.add_attribute("species", species)
.add_attribute("amount", reward_amount.to_string()))
}
ExecuteMsg::AddDistributor { address } => {
// Only contract owner can add distributors
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(ContractError::Unauthorized {});
}
} else {
return Err(ContractError::NoMinterSet {});
}
let addr = deps.api.addr_validate(&address)?;
let mut state = WILDLIFE_STATE.load(deps.storage)?;
if !state.distributors.contains(&addr) {
state.distributors.push(addr.clone());
WILDLIFE_STATE.save(deps.storage, &state)?;
}
Ok(Response::new()
.add_attribute("action", "add_distributor")
.add_attribute("address", address))
}
ExecuteMsg::RemoveDistributor { address } => {
// Only contract owner can remove distributors
let token_info = TOKEN_INFO.load(deps.storage)?;
if let Some(mint) = token_info.mint {
if mint.minter != info.sender {
return Err(ContractError::Unauthorized {});
}
} else {
return Err(ContractError::NoMinterSet {});
}
let addr = deps.api.addr_validate(&address)?;
let mut state = WILDLIFE_STATE.load(deps.storage)?;
state.distributors.retain(|x| x != &addr);
WILDLIFE_STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "remove_distributor")
.add_attribute("address", address))
}
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
// For standard CW20 queries, use the base implementation
QueryMsg::Balance { address } => cw20_query(deps, env, Cw20QueryMsg::Balance { address }),
QueryMsg::TokenInfo {} => cw20_query(deps, env, Cw20QueryMsg::TokenInfo {}),
QueryMsg::Minter {} => cw20_query(deps, env, Cw20QueryMsg::Minter {}),
QueryMsg::Allowance { owner, spender } => {
cw20_query(deps, env, Cw20QueryMsg::Allowance { owner, spender })
}
QueryMsg::AllAllowances { owner, start_after, limit } => {
cw20_query(deps, env, Cw20QueryMsg::AllAllowances { owner, start_after, limit })
}
QueryMsg::AllAccounts { start_after, limit } => {
cw20_query(deps, env, Cw20QueryMsg::AllAccounts { start_after, limit })
}
// Custom wildlife token queries
QueryMsg::SpeciesReward { species } => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let species_lower = species.to_lowercase();
let reward = match state.species_rewards.get(&species_lower) {
Some(amount) => *amount,
None => match state.species_rewards.get("default") {
Some(amount) => *amount,
None => Uint128::zero(),
},
};
to_binary(&SpeciesRewardResponse { species, reward })
}
QueryMsg::IsDistributor { address } => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let addr = deps.api.addr_validate(&address)?;
let is_distributor = state.distributors.contains(&addr);
to_binary(&IsDistributorResponse { address, is_distributor })
}
QueryMsg::AllSpeciesRewards {} => {
let state = WILDLIFE_STATE.load(deps.storage)?;
let rewards: Vec<SpeciesReward> = state.species_rewards
.iter()
.map(|(species, amount)| SpeciesReward {
species: species.clone(),
reward: *amount,
})
.collect();
to_binary(&AllSpeciesRewardsResponse { rewards })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary};
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {
name: "Wildlife Conservation Token".to_string(),
symbol: "WILD".to_string(),
decimals: 6,
};
let info = mock_info("creator", &coins(1000, "earth"));
// we can just call .unwrap() to assert this was a success
let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
assert_eq!(0, res.messages.len());
// Check token info
let res = query(deps.as_ref(), mock_env(), QueryMsg::TokenInfo {}).unwrap();
let token_info: cw20::TokenInfoResponse = from_binary(&res).unwrap();
assert_eq!("Wildlife Conservation Token", token_info.name);
assert_eq!("WILD", token_info.symbol);
assert_eq!(6, token_info.decimals);
assert_eq!(Uint128::from(10_000_000_000_000u128), token_info.total_supply);
// Check creator is a distributor
let res = query(deps.as_ref(), mock_env(), QueryMsg::IsDistributor { address: "creator".to_string() }).unwrap();
let distributor: IsDistributorResponse = from_binary(&res).unwrap();
assert!(distributor.is_distributor);
}
}

View File

@ -0,0 +1,23 @@
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{0}")]
Cw20Error(#[from] cw20_base::ContractError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Not authorized to distribute tokens")]
NotAuthorizedDistributor {},
#[error("No default reward set")]
NoDefaultReward {},
#[error("No minter set")]
NoMinterSet {},
}

View File

@ -0,0 +1,27 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg};
use crate::msg::ExecuteMsg;
/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers
/// for working with this.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct CwTemplateContract(pub Addr);
impl CwTemplateContract {
pub fn addr(&self) -> Addr {
self.0.clone()
}
pub fn call<T: Into<ExecuteMsg>>(&self, msg: T) -> StdResult<CosmosMsg> {
let msg = to_binary(&msg.into())?;
Ok(WasmMsg::Execute {
contract_addr: self.addr().into(),
msg,
funds: vec![],
}
.into())
}
}

View File

@ -0,0 +1,7 @@
pub mod contract;
mod error;
pub mod helpers;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;

View File

@ -0,0 +1,79 @@
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Binary, Uint128};
use cw20::Expiration;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cw_serde]
pub struct InstantiateMsg {
pub name: String,
pub symbol: String,
pub decimals: u8,
}
#[cw_serde]
pub enum ExecuteMsg {
// Standard CW20 messages
Transfer { recipient: String, amount: Uint128 },
Burn { amount: Uint128 },
Send { contract: String, amount: Uint128, msg: Binary },
IncreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
DecreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
TransferFrom { owner: String, recipient: String, amount: Uint128 },
BurnFrom { owner: String, amount: Uint128 },
SendFrom { owner: String, contract: String, amount: Uint128, msg: Binary },
// Custom Wildlife Token messages
SetSpeciesReward { species: String, amount: Uint128 },
AwardTokens { recipient: String, species: String },
AddDistributor { address: String },
RemoveDistributor { address: String },
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
// Standard CW20 queries
#[returns(cw20::BalanceResponse)]
Balance { address: String },
#[returns(cw20::TokenInfoResponse)]
TokenInfo {},
#[returns(Option<cw20::MinterResponse>)]
Minter {},
#[returns(cw20::AllowanceResponse)]
Allowance { owner: String, spender: String },
#[returns(cw20::AllAllowancesResponse)]
AllAllowances { owner: String, start_after: Option<String>, limit: Option<u32> },
#[returns(cw20::AllAccountsResponse)]
AllAccounts { start_after: Option<String>, limit: Option<u32> },
// Custom Wildlife Token queries
#[returns(SpeciesRewardResponse)]
SpeciesReward { species: String },
#[returns(IsDistributorResponse)]
IsDistributor { address: String },
#[returns(AllSpeciesRewardsResponse)]
AllSpeciesRewards {},
}
// Query response types
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SpeciesRewardResponse {
pub species: String,
pub reward: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct IsDistributorResponse {
pub address: String,
pub is_distributor: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SpeciesReward {
pub species: String,
pub reward: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AllSpeciesRewardsResponse {
pub rewards: Vec<SpeciesReward>,
}

View File

@ -0,0 +1,15 @@
use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// State for tracking distributors and species rewards
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct WildlifeState {
pub distributors: Vec<Addr>,
pub species_rewards: HashMap<String, Uint128>,
}
// State variable for wildlife-specific data
pub const WILDLIFE_STATE: Item<WildlifeState> = Item::new("wildlife_state");