mirror of
https://github.com/mito-systems/ranger-app.git
synced 2026-04-28 04:34:08 +00:00
oi
This commit is contained in:
parent
cb0d83d107
commit
87c2fdb8b3
1085
package-lock.json
generated
1085
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
44
src/app/earn/page.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
278
src/components/WildlifeIdentifier.tsx
Normal file
278
src/components/WildlifeIdentifier.tsx
Normal 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;
|
||||
118
src/components/wallet/WalletButton.tsx
Normal file
118
src/components/wallet/WalletButton.tsx
Normal 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
|
||||
185
src/services/blockchain/seiService.ts
Normal file
185
src/services/blockchain/seiService.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
89
src/services/blockchain/tokenRewardService.ts
Normal file
89
src/services/blockchain/tokenRewardService.ts
Normal 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
|
||||
};
|
||||
}
|
||||
};
|
||||
14
src/smart-contracts/.env.example
Normal file
14
src/smart-contracts/.env.example
Normal 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"
|
||||
55
src/smart-contracts/README.md
Normal file
55
src/smart-contracts/README.md
Normal 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
|
||||
104
src/smart-contracts/WildlifeToken.sol
Normal file
104
src/smart-contracts/WildlifeToken.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
75
src/smart-contracts/cosmwasm/README.md
Normal file
75
src/smart-contracts/cosmwasm/README.md
Normal 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
|
||||
352
src/smart-contracts/cosmwasm/wildlife_token.rs
Normal file
352
src/smart-contracts/cosmwasm/wildlife_token.rs
Normal 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");
|
||||
5
wildlife_token_new/wildlife_token/.cargo/config.toml
Normal file
5
wildlife_token_new/wildlife_token/.cargo/config.toml
Normal 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"
|
||||
61
wildlife_token_new/wildlife_token/.circleci/config.yml
Normal file
61
wildlife_token_new/wildlife_token/.circleci/config.yml
Normal 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
|
||||
11
wildlife_token_new/wildlife_token/.editorconfig
Normal file
11
wildlife_token_new/wildlife_token/.editorconfig
Normal 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
|
||||
75
wildlife_token_new/wildlife_token/.github/workflows/Basic.yml
vendored
Normal file
75
wildlife_token_new/wildlife_token/.github/workflows/Basic.yml
vendored
Normal 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
|
||||
35
wildlife_token_new/wildlife_token/.github/workflows/Release.yml
vendored
Normal file
35
wildlife_token_new/wildlife_token/.github/workflows/Release.yml
vendored
Normal 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
|
||||
16
wildlife_token_new/wildlife_token/.gitignore
vendored
Normal file
16
wildlife_token_new/wildlife_token/.gitignore
vendored
Normal 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
|
||||
1035
wildlife_token_new/wildlife_token/Cargo.lock
generated
Normal file
1035
wildlife_token_new/wildlife_token/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
wildlife_token_new/wildlife_token/Cargo.toml
Normal file
48
wildlife_token_new/wildlife_token/Cargo.toml
Normal 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"
|
||||
202
wildlife_token_new/wildlife_token/LICENSE
Normal file
202
wildlife_token_new/wildlife_token/LICENSE
Normal 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.
|
||||
13
wildlife_token_new/wildlife_token/NOTICE
Normal file
13
wildlife_token_new/wildlife_token/NOTICE
Normal 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.
|
||||
137
wildlife_token_new/wildlife_token/README.md
Normal file
137
wildlife_token_new/wildlife_token/README.md
Normal 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.
|
||||
@ -0,0 +1 @@
|
||||
1d9a6b82b201c4f2f691fa03b879266cfb641e0482c223915f684de4431a2c6a wildlife_token.wasm
|
||||
@ -0,0 +1 @@
|
||||
6db20ed197f7a0f1fb402f7d72f2934e2dab9337f979aee4ba0c964802cd04ba /target/wasm32-unknown-unknown/release/wildlife_token.wasm
|
||||
BIN
wildlife_token_new/wildlife_token/artifacts/wildlife_token.wasm
Normal file
BIN
wildlife_token_new/wildlife_token/artifacts/wildlife_token.wasm
Normal file
Binary file not shown.
11
wildlife_token_new/wildlife_token/src/bin/schema.rs
Normal file
11
wildlife_token_new/wildlife_token/src/bin/schema.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
309
wildlife_token_new/wildlife_token/src/contract.rs
Normal file
309
wildlife_token_new/wildlife_token/src/contract.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
wildlife_token_new/wildlife_token/src/error.rs
Normal file
23
wildlife_token_new/wildlife_token/src/error.rs
Normal 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 {},
|
||||
}
|
||||
27
wildlife_token_new/wildlife_token/src/helpers.rs
Normal file
27
wildlife_token_new/wildlife_token/src/helpers.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
7
wildlife_token_new/wildlife_token/src/lib.rs
Normal file
7
wildlife_token_new/wildlife_token/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod contract;
|
||||
mod error;
|
||||
pub mod helpers;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
pub use crate::error::ContractError;
|
||||
79
wildlife_token_new/wildlife_token/src/msg.rs
Normal file
79
wildlife_token_new/wildlife_token/src/msg.rs
Normal 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>,
|
||||
}
|
||||
15
wildlife_token_new/wildlife_token/src/state.rs
Normal file
15
wildlife_token_new/wildlife_token/src/state.rs
Normal 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");
|
||||
Loading…
Reference in New Issue
Block a user