Loading...
;
+ }
- if (isLoading) {
+ if (!isSignedIn) {
return (
-
-
-
-
- {[...Array(4)].map((_, i) => (
-
- ))}
-
-
+
+
NEAR Drop
+
+ Create gasless token drops that anyone can claim
+
+
);
}
- return (---
-id: frontend
-title: Frontend Integration
-sidebar_label: Frontend Integration
-description: "Build a complete web interface for the NEAR Drop system, including drop creation, key management, and claiming functionality with React and Next.js."
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-A great user experience is crucial for token distribution systems. In this section, we'll build a complete frontend that makes creating and claiming drops as simple as a few clicks.
-
----
-
-## Project Setup
-
-Let's create a Next.js frontend for our NEAR Drop system:
-
-```bash
-npx create-next-app@latest near-drop-frontend
-cd near-drop-frontend
-
-# Install NEAR dependencies
-npm install near-api-js @near-wallet-selector/core @near-wallet-selector/my-near-wallet
-npm install @near-wallet-selector/modal-ui qrcode react-qr-code
-npm install lucide-react clsx tailwind-merge
-
-# Install development dependencies
-npm install -D @types/qrcode
+ return (
+
+
+ {createdDrop ? (
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
```
-### Environment Configuration
+---
-Create `.env.local`:
+## Deploy Your Frontend
```bash
-NEXT_PUBLIC_NETWORK_ID=testnet
-NEXT_PUBLIC_CONTRACT_ID=drop-contract.testnet
-NEXT_PUBLIC_WALLET_URL=https://testnet.mynearwallet.com
-NEXT_PUBLIC_HELPER_URL=https://helper.testnet.near.org
-NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org
-```
-
----
-
-## Core Components Architecture
+# Build for production
+npm run build
-### Project Structure
+# Deploy to Vercel
+npm i -g vercel
+vercel --prod
-```
-src/
-├── components/
-│ ├── ui/ # Reusable UI components
-│ ├── DropCreation/ # Drop creation components
-│ ├── DropClaiming/ # Drop claiming components
-│ └── Dashboard/ # Dashboard components
-├── hooks/ # Custom React hooks
-├── services/ # NEAR integration services
-├── types/ # TypeScript types
-└── utils/ # Utility functions
+# Or deploy to Netlify
+# Just connect your GitHub repo and it'll auto-deploy
```
---
-## NEAR Integration Layer
-
-### Wallet Connection Service
+## What You've Built
-Create `src/services/near.ts`:
+Awesome! You now have a complete web application with:
-```typescript
-import { connect, ConnectConfig, keyStores, WalletConnection } from 'near-api-js';
-import { setupWalletSelector } from '@near-wallet-selector/core';
-import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
-import { setupModal } from '@near-wallet-selector/modal-ui';
+✅ **Wallet integration** for NEAR accounts
+✅ **Drop creation interface** with cost calculation
+✅ **Key generation and distribution** tools
+✅ **QR code support** for easy sharing
+✅ **Claiming interface** for both new and existing users
+✅ **Mobile-responsive design** that works everywhere
-const config: ConnectConfig = {
- networkId: process.env.NEXT_PUBLIC_NETWORK_ID!,
- keyStore: new keyStores.BrowserLocalStorageKeyStore(),
- nodeUrl: process.env.NEXT_PUBLIC_RPC_URL!,
- walletUrl: process.env.NEXT_PUBLIC_WALLET_URL!,
- helperUrl: process.env.NEXT_PUBLIC_HELPER_URL!,
-};
+Your users can now create and claim token drops with just a few clicks - no technical knowledge required!
-export class NearService {
- near: any;
- wallet: any;
- contract: any;
- selector: any;
- modal: any;
-
- async initialize() {
- // Initialize NEAR connection
- this.near = await connect(config);
-
- // Initialize wallet selector
- this.selector = await setupWalletSelector({
- network: process.env.NEXT_PUBLIC_NETWORK_ID!,
- modules: [
- setupMyNearWallet(),
- ],
- });
-
- // Initialize modal
- this.modal = setupModal(this.selector, {
- contractId: process.env.NEXT_PUBLIC_CONTRACT_ID!,
- });
-
- // Initialize contract
- if (this.selector.isSignedIn()) {
- const wallet = await this.selector.wallet();
- this.contract = new Contract(wallet.account(), process.env.NEXT_PUBLIC_CONTRACT_ID!, {
- viewMethods: [
- 'get_drop',
- 'get_drop_id_by_key',
- 'calculate_near_drop_cost_view',
- 'calculate_ft_drop_cost_view',
- 'calculate_nft_drop_cost_view',
- 'get_nft_drop_details',
- 'get_ft_drop_details',
- ],
- changeMethods: [
- 'create_near_drop',
- 'create_ft_drop',
- 'create_nft_drop',
- 'claim_for',
- 'create_account_and_claim',
- 'create_named_account_and_claim',
- ],
- });
- }
- }
-
- async signIn() {
- this.modal.show();
- }
-
- async signOut() {
- const wallet = await this.selector.wallet();
- await wallet.signOut();
- this.contract = null;
- }
-
- isSignedIn() {
- return this.selector?.isSignedIn() || false;
- }
-
- getAccountId() {
- return this.selector?.store?.getState()?.accounts?.[0]?.accountId || null;
- }
-}
-
-export const nearService = new NearService();
-```
-
-### Contract Interface Types
-
-Create `src/types/contract.ts`:
-
-```typescript
-export interface DropKey {
- public_key: string;
- private_key: string;
-}
-
-export interface NearDrop {
- amount: string;
- counter: number;
-}
-
-export interface FtDrop {
- ft_contract: string;
- amount: string;
- counter: number;
-}
-
-export interface NftDrop {
- nft_contract: string;
- token_id: string;
- counter: number;
-}
-
-export type Drop =
- | { Near: NearDrop }
- | { FungibleToken: FtDrop }
- | { NonFungibleToken: NftDrop };
-
-export interface DropInfo {
- drop_id: number;
- drop: Drop;
- keys: DropKey[];
-}
-
-export interface ClaimableKey {
- private_key: string;
- public_key: string;
- drop_id?: number;
- claim_url: string;
-}
-```
-
----
-
-## Drop Creation Interface
-
-### Drop Creation Form
-
-Create `src/components/DropCreation/DropCreationForm.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Loader2, Plus, Minus } from 'lucide-react';
-import { nearService } from '@/services/near';
-import { generateKeys } from '@/utils/crypto';
-
-interface DropCreationFormProps {
- onDropCreated: (dropInfo: any) => void;
-}
-
-export default function DropCreationForm({ onDropCreated }: DropCreationFormProps) {
- const [isLoading, setIsLoading] = useState(false);
- const [dropType, setDropType] = useState<'near' | 'ft' | 'nft'>('near');
- const [keyCount, setKeyCount] = useState(5);
-
- // NEAR drop form state
- const [nearAmount, setNearAmount] = useState('1');
-
- // FT drop form state
- const [ftContract, setFtContract] = useState('');
- const [ftAmount, setFtAmount] = useState('');
-
- // NFT drop form state
- const [nftContract, setNftContract] = useState('');
- const [nftTokenId, setNftTokenId] = useState('');
-
- const handleCreateDrop = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsLoading(true);
-
- try {
- // Generate keys for the drop
- const keys = generateKeys(keyCount);
- const publicKeys = keys.map(k => k.publicKey);
-
- let dropId: number;
- let cost: string = '0';
-
- switch (dropType) {
- case 'near':
- // Calculate cost first
- cost = await nearService.contract.calculate_near_drop_cost_view({
- num_keys: keyCount,
- amount_per_drop: (parseFloat(nearAmount) * 1e24).toString(),
- });
-
- dropId = await nearService.contract.create_near_drop({
- public_keys: publicKeys,
- amount_per_drop: (parseFloat(nearAmount) * 1e24).toString(),
- }, {
- gas: '100000000000000',
- attachedDeposit: cost,
- });
- break;
-
- case 'ft':
- cost = await nearService.contract.calculate_ft_drop_cost_view({
- num_keys: keyCount,
- });
-
- dropId = await nearService.contract.create_ft_drop({
- public_keys: publicKeys,
- ft_contract: ftContract,
- amount_per_drop: ftAmount,
- }, {
- gas: '150000000000000',
- attachedDeposit: cost,
- });
- break;
-
- case 'nft':
- if (keyCount > 1) {
- throw new Error('NFT drops support only 1 key since each NFT is unique');
- }
-
- cost = await nearService.contract.calculate_nft_drop_cost_view();
-
- dropId = await nearService.contract.create_nft_drop({
- public_key: publicKeys[0],
- nft_contract: nftContract,
- token_id: nftTokenId,
- }, {
- gas: '100000000000000',
- attachedDeposit: cost,
- });
- break;
-
- default:
- throw new Error('Invalid drop type');
- }
-
- // Return drop info with keys
- const dropInfo = {
- dropId,
- dropType,
- keys,
- cost,
- };
-
- onDropCreated(dropInfo);
- } catch (error) {
- console.error('Error creating drop:', error);
- alert('Failed to create drop: ' + error.message);
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
-
- Create Token Drop
-
-
-
-
-
- );
-}
-```
-
-### Key Generation Utility
-
-Create `src/utils/crypto.ts`:
-
-```typescript
-import { KeyPair } from 'near-api-js';
-
-export interface GeneratedKey {
- publicKey: string;
- privateKey: string;
- keyPair: KeyPair;
-}
-
-export function generateKeys(count: number): GeneratedKey[] {
- const keys: GeneratedKey[] = [];
-
- for (let i = 0; i < count; i++) {
- const keyPair = KeyPair.fromRandom('ed25519');
- keys.push({
- publicKey: keyPair.publicKey.toString(),
- privateKey: keyPair.secretKey,
- keyPair,
- });
- }
-
- return keys;
-}
-
-export function generateClaimUrl(privateKey: string, baseUrl: string = window.location.origin): string {
- return `${baseUrl}/claim?key=${encodeURIComponent(privateKey)}`;
-}
-```
-
----
-
-## Drop Display and Management
-
-### Drop Results Component
-
-Create `src/components/DropCreation/DropResults.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Badge } from '@/components/ui/badge';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Copy, Download, QrCode, Share2, ExternalLink } from 'lucide-react';
-import QRCode from 'react-qr-code';
-import { generateClaimUrl } from '@/utils/crypto';
-
-interface DropResultsProps {
- dropInfo: {
- dropId: number;
- dropType: string;
- keys: Array<{ publicKey: string; privateKey: string }>;
- cost: string;
- };
-}
-
-export default function DropResults({ dropInfo }: DropResultsProps) {
- const [selectedKeyIndex, setSelectedKeyIndex] = useState(0);
- const [showQR, setShowQR] = useState(false);
-
- const claimUrls = dropInfo.keys.map(key => generateClaimUrl(key.privateKey));
-
- const copyToClipboard = (text: string) => {
- navigator.clipboard.writeText(text);
- // You might want to add a toast notification here
- };
-
- const downloadKeys = () => {
- const keysData = dropInfo.keys.map((key, index) => ({
- index: index + 1,
- publicKey: key.publicKey,
- privateKey: key.privateKey,
- claimUrl: claimUrls[index],
- }));
-
- const dataStr = JSON.stringify(keysData, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
-
- const exportFileDefaultName = `near-drop-${dropInfo.dropId}-keys.json`;
-
- const linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataUri);
- linkElement.setAttribute('download', exportFileDefaultName);
- linkElement.click();
- };
-
- const downloadQRCodes = async () => {
- // This would generate QR codes as images and download them as a ZIP
- // Implementation depends on additional libraries like JSZip
- console.log('Download QR codes functionality would be implemented here');
- };
-
- return (
-
-
-
- Drop Created Successfully!
- Drop ID: {dropInfo.dropId}
-
-
- Created {dropInfo.keys.length} {dropInfo.dropType.toUpperCase()} drop key(s).
- Total cost: {(parseInt(dropInfo.cost) / 1e24).toFixed(4)} NEAR
-
-
-
-
-
- Keys & Links
- QR Codes
- Sharing Tools
-
-
- {/* Keys and Links Tab */}
-
-
-
Generated Keys
-
-
-
-
-
-
- {dropInfo.keys.map((key, index) => (
-
-
-
- Key {index + 1}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Show Private Key
-
-
- {key.privateKey}
-
-
-
-
- ))}
-
-
-
- {/* QR Codes Tab */}
-
-
-
QR Codes for Claiming
-
-
-
-
-
-
-
-
-
-
- Key {selectedKeyIndex + 1} - Scan to claim
-
-
-
-
-
- {dropInfo.keys.map((_, index) => (
-
setSelectedKeyIndex(index)}
- >
-
-
Key {index + 1}
-
- ))}
-
-
-
- {/* Sharing Tools Tab */}
-
- Sharing & Distribution
-
-
-
- Bulk Share Text
-
- Copy this text to share all claim links at once:
-
-
- {claimUrls.map((url, index) => (
-
- Key {index + 1}: {url}
-
- ))}
-
-
-
-
-
- Social Media Template
-
- 🎁 NEAR Token Drop!
-
- I've created a token drop with {dropInfo.keys.length} claimable key(s).
-
- Click your link to claim: [Paste individual links here]
-
- #NEAR #TokenDrop #Crypto
-
-
-
-
-
-
-
- );
-}
-```
-
----
-
-## Claiming Interface
-
-### Claim Page Component
-
-Create `src/components/DropClaiming/ClaimPage.tsx`:
-
-```tsx
-'use client';
-
-import { useState, useEffect } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Badge } from '@/components/ui/badge';
-import { Alert, AlertDescription } from '@/components/ui/alert';
-import { Loader2, Gift, User, Wallet } from 'lucide-react';
-import { nearService } from '@/services/near';
-import { KeyPair } from 'near-api-js';
-
-export default function ClaimPage() {
- const searchParams = useSearchParams();
- const router = useRouter();
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
- const [success, setSuccess] = useState(false);
-
- // Key and drop info
- const [privateKey, setPrivateKey] = useState('');
- const [dropInfo, setDropInfo] = useState(null);
- const [keyValid, setKeyValid] = useState(false);
-
- // Claiming options
- const [claimMode, setClaimMode] = useState<'existing' | 'new'>('existing');
- const [existingAccount, setExistingAccount] = useState('');
- const [newAccountName, setNewAccountName] = useState('');
-
- useEffect(() => {
- const keyFromUrl = searchParams.get('key');
- if (keyFromUrl) {
- setPrivateKey(keyFromUrl);
- validateKey(keyFromUrl);
- }
- }, [searchParams]);
-
- const validateKey = async (key: string) => {
- try {
- // Parse the key to validate format
- const keyPair = KeyPair.fromString(key);
- const publicKey = keyPair.publicKey.toString();
-
- // Check if drop exists for this key
- const dropId = await nearService.contract.get_drop_id_by_key({
- public_key: publicKey,
- });
-
- if (dropId !== null) {
- const drop = await nearService.contract.get_drop({
- drop_id: dropId,
- });
-
- setDropInfo({ dropId, drop });
- setKeyValid(true);
- } else {
- setError('This key is not associated with any active drop');
- }
- } catch (err) {
- setError('Invalid private key format');
- }
- };
-
- const handleClaim = async () => {
- setIsLoading(true);
- setError(null);
-
- try {
- const keyPair = KeyPair.fromString(privateKey);
-
- // Create a temporary wallet connection with this key
- const tempAccount = {
- accountId: process.env.NEXT_PUBLIC_CONTRACT_ID!,
- keyPair: keyPair,
- };
-
- let result;
-
- if (claimMode === 'existing') {
- // Claim to existing account
- result = await nearService.contract.claim_for({
- account_id: existingAccount,
- }, {
- gas: '150000000000000',
- signerAccount: tempAccount,
- });
- } else {
- // Create new account and claim
- const fullAccountName = `${newAccountName}.${process.env.NEXT_PUBLIC_NETWORK_ID}`;
- result = await nearService.contract.create_named_account_and_claim({
- preferred_name: newAccountName,
- }, {
- gas: '200000000000000',
- signerAccount: tempAccount,
- });
- }
-
- setSuccess(true);
- } catch (err: any) {
- setError(err.message || 'Failed to claim drop');
- } finally {
- setIsLoading(false);
- }
- };
-
- const getDropTypeInfo = (drop: any) => {
- if (drop.Near) {
- return {
- type: 'NEAR',
- amount: `${(parseInt(drop.Near.amount) / 1e24).toFixed(4)} NEAR`,
- remaining: drop.Near.counter,
- };
- } else if (drop.FungibleToken) {
- return {
- type: 'Fungible Token',
- amount: `${drop.FungibleToken.amount} tokens`,
- contract: drop.FungibleToken.ft_contract,
- remaining: drop.FungibleToken.counter,
- };
- } else if (drop.NonFungibleToken) {
- return {
- type: 'NFT',
- tokenId: drop.NonFungibleToken.token_id,
- contract: drop.NonFungibleToken.nft_contract,
- remaining: drop.NonFungibleToken.counter,
- };
- }
- return null;
- };
-
- if (success) {
- return (
-
-
-
-
- Claim Successful!
-
-
-
- Your tokens have been successfully claimed.
-
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- Claim Your Token Drop
-
-
-
- {/* Private Key Input */}
-
-
- {
- setPrivateKey(e.target.value);
- setError(null);
- setKeyValid(false);
- setDropInfo(null);
- }}
- placeholder="ed25519:..."
- className="font-mono text-sm"
- />
- {!keyValid && privateKey && (
-
- )}
-
-
- {/* Error Alert */}
- {error && (
-
- {error}
-
- )}
-
- {/* Drop Information */}
- {keyValid && dropInfo && (
-
-
- Drop Details
- {(() => {
- const info = getDropTypeInfo(dropInfo.drop);
- return info ? (
-
-
- Type:
- {info.type}
-
-
- Amount:
- {info.amount}
-
- {info.contract && (
-
- Contract:
- {info.contract}
-
- )}
- {info.tokenId && (
-
- Token ID:
- {info.tokenId}
-
- )}
-
- Remaining:
- {info.remaining} claim(s)
-
-
- ) : null;
- })()}
-
-
- )}
-
- {/* Claiming Options */}
- {keyValid && (
-
-
- Choose Claiming Method
-
-
- {/* Claim Mode Selection */}
-
-
-
-
-
- {/* Existing Account Option */}
- {claimMode === 'existing' && (
-
-
- setExistingAccount(e.target.value)}
- placeholder="your-account.testnet"
- />
-
- )}
-
- {/* New Account Option */}
- {claimMode === 'new' && (
-
-
-
- setNewAccountName(e.target.value.toLowerCase().replace(/[^a-z0-9\-_]/g, ''))}
- placeholder="my-new-account"
- />
-
- .{process.env.NEXT_PUBLIC_NETWORK_ID}
-
-
-
- A new NEAR account will be created for you
-
-
- )}
-
- {/* Claim Button */}
-
-
-
- )}
-
-
-
- );
-}
-```
-
----
-
-## Dashboard and Management
-
-### Drop Dashboard
-
-Create `src/components/Dashboard/DropDashboard.tsx`:
-
-```tsx
-'use client';
-
-import { useState, useEffect } from 'react';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Eye, Trash2, RefreshCw, TrendingUp, Users, Gift } from 'lucide-react';
-import { nearService } from '@/services/near';
-
-interface Drop {
- dropId: number;
- type: string;
- remaining: number;
- total: number;
- created: Date;
- status: 'active' | 'completed' | 'expired';
-}
-
-export default function DropDashboard() {
- const [drops, setDrops] = useState([]);
- const [stats, setStats] = useState({
- totalDrops: 0,
- activeDrops: 0,
- totalClaimed: 0,
- totalValue: '0',
- });
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- loadDashboardData();
- }, []);
-
- const loadDashboardData = async () => {
- setIsLoading(true);
- try {
- // In a real implementation, you'd have methods to fetch user's drops
- // For now, we'll simulate some data
- const mockDrops: Drop[] = [
- {
- dropId: 1,
- type: 'NEAR',
- remaining: 5,
- total: 10,
- created: new Date('2024-01-15'),
- status: 'active',
- },
- {
- dropId: 2,
- type: 'FT',
- remaining: 0,
- total: 20,
- created: new Date('2024-01-10'),
- status: 'completed',
- },
- ];
-
- setDrops(mockDrops);
- setStats({
- totalDrops: mockDrops.length,
- activeDrops: mockDrops.filter(d => d.status === 'active').length,
- totalClaimed: mockDrops.reduce((acc, d) => acc + (d.total - d.remaining), 0),
- totalValue: '15.5', // Mock value in NEAR
- });
- } catch (error) {
- console.error('Error loading dashboard data:', error);
- } finally {
- setIsLoading(false);
- }
- };
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'active': return 'bg-green-100 text-green-800';
- case 'completed': return 'bg-blue-100 text-blue-800';
- case 'expired': return 'bg-red-100 text-red-800';
- default: return 'bg-gray-100 text-gray-800';
- }
- };
-
- if (isLoading) {
- return (
-
-
-
-
- {[...Array(4)].map((_, i) => (
-
- ))}
-
-
-
- );
- }
-
- return (---
-id: frontend
-title: Frontend Integration
-sidebar_label: Frontend Integration
-description: "Build a complete web interface for the NEAR Drop system, including drop creation, key management, and claiming functionality with React and Next.js."
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-A great user experience is crucial for token distribution systems. In this section, we'll build a complete frontend that makes creating and claiming drops as simple as a few clicks.
-
----
-
-## Project Setup
-
-Let's create a Next.js frontend for our NEAR Drop system:
-
-```bash
-npx create-next-app@latest near-drop-frontend
-cd near-drop-frontend
-
-# Install NEAR dependencies
-npm install near-api-js @near-wallet-selector/core @near-wallet-selector/my-near-wallet
-npm install @near-wallet-selector/modal-ui qrcode react-qr-code
-npm install lucide-react clsx tailwind-merge
-
-# Install development dependencies
-npm install -D @types/qrcode
-```
-
-### Environment Configuration
-
-Create `.env.local`:
-
-```bash
-NEXT_PUBLIC_NETWORK_ID=testnet
-NEXT_PUBLIC_CONTRACT_ID=drop-contract.testnet
-NEXT_PUBLIC_WALLET_URL=https://testnet.mynearwallet.com
-NEXT_PUBLIC_HELPER_URL=https://helper.testnet.near.org
-NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org
-```
-
----
-
-## Core Components Architecture
-
-### Project Structure
-
-```
-src/
-├── components/
-│ ├── ui/ # Reusable UI components
-│ ├── DropCreation/ # Drop creation components
-│ ├── DropClaiming/ # Drop claiming components
-│ └── Dashboard/ # Dashboard components
-├── hooks/ # Custom React hooks
-├── services/ # NEAR integration services
-├── types/ # TypeScript types
-└── utils/ # Utility functions
-```
-
----
-
-## NEAR Integration Layer
-
-### Wallet Connection Service
-
-Create `src/services/near.ts`:
-
-```typescript
-import { connect, ConnectConfig, keyStores, WalletConnection } from 'near-api-js';
-import { setupWalletSelector } from '@near-wallet-selector/core';
-import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
-import { setupModal } from '@near-wallet-selector/modal-ui';
-
-const config: ConnectConfig = {
- networkId: process.env.NEXT_PUBLIC_NETWORK_ID!,
- keyStore: new keyStores.BrowserLocalStorageKeyStore(),
- nodeUrl: process.env.NEXT_PUBLIC_RPC_URL!,
- walletUrl: process.env.NEXT_PUBLIC_WALLET_URL!,
- helperUrl: process.env.NEXT_PUBLIC_HELPER_URL!,
-};
-
-export class NearService {
- near: any;
- wallet: any;
- contract: any;
- selector: any;
- modal: any;
-
- async initialize() {
- // Initialize NEAR connection
- this.near = await connect(config);
-
- // Initialize wallet selector
- this.selector = await setupWalletSelector({
- network: process.env.NEXT_PUBLIC_NETWORK_ID!,
- modules: [
- setupMyNearWallet(),
- ],
- });
-
- // Initialize modal
- this.modal = setupModal(this.selector, {
- contractId: process.env.NEXT_PUBLIC_CONTRACT_ID!,
- });
-
- // Initialize contract
- if (this.selector.isSignedIn()) {
- const wallet = await this.selector.wallet();
- this.contract = new Contract(wallet.account(), process.env.NEXT_PUBLIC_CONTRACT_ID!, {
- viewMethods: [
- 'get_drop',
- 'get_drop_id_by_key',
- 'calculate_near_drop_cost_view',
- 'calculate_ft_drop_cost_view',
- 'calculate_nft_drop_cost_view',
- 'get_nft_drop_details',
- 'get_ft_drop_details',
- ],
- changeMethods: [
- 'create_near_drop',
- 'create_ft_drop',
- 'create_nft_drop',
- 'claim_for',
- 'create_account_and_claim',
- 'create_named_account_and_claim',
- ],
- });
- }
- }
-
- async signIn() {
- this.modal.show();
- }
-
- async signOut() {
- const wallet = await this.selector.wallet();
- await wallet.signOut();
- this.contract = null;
- }
-
- isSignedIn() {
- return this.selector?.isSignedIn() || false;
- }
-
- getAccountId() {
- return this.selector?.store?.getState()?.accounts?.[0]?.accountId || null;
- }
-}
-
-export const nearService = new NearService();
-```
-
-### Contract Interface Types
-
-Create `src/types/contract.ts`:
-
-```typescript
-export interface DropKey {
- public_key: string;
- private_key: string;
-}
-
-export interface NearDrop {
- amount: string;
- counter: number;
-}
-
-export interface FtDrop {
- ft_contract: string;
- amount: string;
- counter: number;
-}
-
-export interface NftDrop {
- nft_contract: string;
- token_id: string;
- counter: number;
-}
-
-export type Drop =
- | { Near: NearDrop }
- | { FungibleToken: FtDrop }
- | { NonFungibleToken: NftDrop };
-
-export interface DropInfo {
- drop_id: number;
- drop: Drop;
- keys: DropKey[];
-}
-
-export interface ClaimableKey {
- private_key: string;
- public_key: string;
- drop_id?: number;
- claim_url: string;
-}
-```
-
----
-
-## Drop Creation Interface
-
-### Drop Creation Form
-
-Create `src/components/DropCreation/DropCreationForm.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Loader2, Plus, Minus } from 'lucide-react';
-import { nearService } from '@/services/near';
-import { generateKeys } from '@/utils/crypto';
-
-interface DropCreationFormProps {
- onDropCreated: (dropInfo: any) => void;
-}
-
-export default function DropCreationForm({ onDropCreated }: DropCreationFormProps) {
- const [isLoading, setIsLoading] = useState(false);
- const [dropType, setDropType] = useState<'near' | 'ft' | 'nft'>('near');
- const [keyCount, setKeyCount] = useState(5);
-
- // NEAR drop form state
- const [nearAmount, setNearAmount] = useState('1');
-
- // FT drop form state
- const [ftContract, setFtContract] = useState('');
- const [ftAmount, setFtAmount] = useState('');
-
- // NFT drop form state
- const [nftContract, setNftContract] = useState('');
- const [nftTokenId, setNftTokenId] = useState('');
-
- const handleCreateDrop = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsLoading(true);
-
- try {
- // Generate keys for the drop
- const keys = generateKeys(keyCount);
- const publicKeys = keys.map(k => k.publicKey);
-
- let dropId: number;
- let cost: string = '0';
-
- switch (dropType) {
- case 'near':
- // Calculate cost first
- cost = await nearService.contract.calculate_near_drop_cost_view({
- num_keys: keyCount,
- amount_per_drop: (parseFloat(nearAmount) * 1e24).toString(),
- });
-
- dropId = await nearService.contract.create_near_drop({
- public_keys: publicKeys,
- amount_per_drop: (parseFloat(nearAmount) * 1e24).toString(),
- }, {
- gas: '100000000000000',
- attachedDeposit: cost,
- });
- break;
-
- case 'ft':
- cost = await nearService.contract.calculate_ft_drop_cost_view({
- num_keys: keyCount,
- });
-
- dropId = await nearService.contract.create_ft_drop({
- public_keys: publicKeys,
- ft_contract: ftContract,
- amount_per_drop: ftAmount,
- }, {
- gas: '150000000000000',
- attachedDeposit: cost,
- });
- break;
-
- case 'nft':
- if (keyCount > 1) {
- throw new Error('NFT drops support only 1 key since each NFT is unique');
- }
-
- cost = await nearService.contract.calculate_nft_drop_cost_view();
-
- dropId = await nearService.contract.create_nft_drop({
- public_key: publicKeys[0],
- nft_contract: nftContract,
- token_id: nftTokenId,
- }, {
- gas: '100000000000000',
- attachedDeposit: cost,
- });
- break;
-
- default:
- throw new Error('Invalid drop type');
- }
-
- // Return drop info with keys
- const dropInfo = {
- dropId,
- dropType,
- keys,
- cost,
- };
-
- onDropCreated(dropInfo);
- } catch (error) {
- console.error('Error creating drop:', error);
- alert('Failed to create drop: ' + error.message);
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
-
- Create Token Drop
-
-
-
-
-
- );
-}
-```
-
-### Key Generation Utility
-
-Create `src/utils/crypto.ts`:
-
-```typescript
-import { KeyPair } from 'near-api-js';
-
-export interface GeneratedKey {
- publicKey: string;
- privateKey: string;
- keyPair: KeyPair;
-}
-
-export function generateKeys(count: number): GeneratedKey[] {
- const keys: GeneratedKey[] = [];
-
- for (let i = 0; i < count; i++) {
- const keyPair = KeyPair.fromRandom('ed25519');
- keys.push({
- publicKey: keyPair.publicKey.toString(),
- privateKey: keyPair.secretKey,
- keyPair,
- });
- }
-
- return keys;
-}
-
-export function generateClaimUrl(privateKey: string, baseUrl: string = window.location.origin): string {
- return `${baseUrl}/claim?key=${encodeURIComponent(privateKey)}`;
-}
-```
-
----
-
-## Drop Display and Management
-
-### Drop Results Component
-
-Create `src/components/DropCreation/DropResults.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Badge } from '@/components/ui/badge';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Copy, Download, QrCode, Share2, ExternalLink } from 'lucide-react';
-import QRCode from 'react-qr-code';
-import { generateClaimUrl } from '@/utils/crypto';
-
-interface DropResultsProps {
- dropInfo: {
- dropId: number;
- dropType: string;
- keys: Array<{ publicKey: string; privateKey: string }>;
- cost: string;
- };
-}
-
-export default function DropResults({ dropInfo }: DropResultsProps) {
- const [selectedKeyIndex, setSelectedKeyIndex] = useState(0);
- const [showQR, setShowQR] = useState(false);
-
- const claimUrls = dropInfo.keys.map(key => generateClaimUrl(key.privateKey));
-
- const copyToClipboard = (text: string) => {
- navigator.clipboard.writeText(text);
- // You might want to add a toast notification here
- };
-
- const downloadKeys = () => {
- const keysData = dropInfo.keys.map((key, index) => ({
- index: index + 1,
- publicKey: key.publicKey,
- privateKey: key.privateKey,
- claimUrl: claimUrls[index],
- }));
-
- const dataStr = JSON.stringify(keysData, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
-
- const exportFileDefaultName = `near-drop-${dropInfo.dropId}-keys.json`;
-
- const linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataUri);
- linkElement.setAttribute('download', exportFileDefaultName);
- linkElement.click();
- };
-
- const downloadQRCodes = async () => {
- // This would generate QR codes as images and download them as a ZIP
- // Implementation depends on additional libraries like JSZip
- console.log('Download QR codes functionality would be implemented here');
- };
-
- return (
-
-
-
Drop Dashboard
-
-
-
- {/* Stats Cards */}
-
-
-
-
-
-
-
Total Drops
-
{stats.totalDrops}
-
-
-
-
-
-
-
-
-
-
-
Active Drops
-
{stats.activeDrops}
-
-
-
-
-
-
-
-
-
-
-
Total Claims
-
{stats.totalClaimed}
-
-
-
-
-
-
-
-
-
- Ⓝ
-
-
-
Total Value
-
{stats.totalValue} NEAR
-
-
-
-
-
-
- {/* Drops Management */}
-
-
- Your Drops
-
-
- {drops.length === 0 ? (
-
-
-
No drops created yet
-
-
- ) : (
-
- {drops.map((drop) => (
-
-
-
-
-
-
- Drop #{drop.dropId}
-
- {drop.status}
-
- {drop.type}
-
-
- Created: {drop.created.toLocaleDateString()}
-
- Progress: {drop.total - drop.remaining}/{drop.total} claimed
-
-
-
-
-
-
-
- {drop.status === 'active' && drop.remaining === 0 && (
-
- )}
-
-
-
- {/* Progress Bar */}
-
-
- Claims Progress
- {Math.round(((drop.total - drop.remaining) / drop.total) * 100)}%
-
-
-
-
-
- ))}
-
- )}
-
-
-
- );
-}
-```
-
----
-
-## Main Application Layout
-
-### App Layout
-
-Create `src/app/layout.tsx`:
-
-```tsx
-import type { Metadata } from 'next';
-import { Inter } from 'next/font/google';
-import './globals.css';
-import { NearProvider } from '@/providers/NearProvider';
-import Navigation from '@/components/Navigation';
-
-const inter = Inter({ subsets: ['latin'] });
-
-export const metadata: Metadata = {
- title: 'NEAR Drop - Token Distribution Made Easy',
- description: 'Create and claim token drops on NEAR Protocol with gasless transactions',
-};
-
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
-
-
-
-
-
-
- {children}
-
-
-
-
-
- );
-}
-```
-
-### NEAR Provider
-
-Create `src/providers/NearProvider.tsx`:
-
-```tsx
-'use client';
-
-import { createContext, useContext, useEffect, useState } from 'react';
-import { nearService } from '@/services/near';
-
-interface NearContextType {
- isSignedIn: boolean;
- accountId: string | null;
- signIn: () => void;
- signOut: () => void;
- contract: any;
- isLoading: boolean;
-}
-
-const NearContext = createContext(undefined);
-
-export function NearProvider({ children }: { children: React.ReactNode }) {
- const [isSignedIn, setIsSignedIn] = useState(false);
- const [accountId, setAccountId] = useState(null);
- const [contract, setContract] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- initializeNear();
- }, []);
-
- const initializeNear = async () => {
- try {
- await nearService.initialize();
-
- const signedIn = nearService.isSignedIn();
- const account = nearService.getAccountId();
-
- setIsSignedIn(signedIn);
- setAccountId(account);
- setContract(nearService.contract);
- } catch (error) {
- console.error('Failed to initialize NEAR:', error);
- } finally {
- setIsLoading(false);
- }
- };
-
- const signIn = async () => {
- await nearService.signIn();
- // The page will reload after sign in
- };
-
- const signOut = async () => {
- await nearService.signOut();
- setIsSignedIn(false);
- setAccountId(null);
- setContract(null);
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useNear() {
- const context = useContext(NearContext);
- if (context === undefined) {
- throw new Error('useNear must be used within a NearProvider');
- }
- return context;
-}
-```
-
-### Navigation Component
-
-Create `src/components/Navigation.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import Link from 'next/link';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Gift, Wallet, User, Menu, X } from 'lucide-react';
-import { useNear } from '@/providers/NearProvider';
-
-export default function Navigation() {
- const { isSignedIn, accountId, signIn, signOut, isLoading } = useNear();
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
-
- return (
-
- );
-}
-```
-
----
-
-## Page Components
-
-### Home Page
-
-Create `src/app/page.tsx`:
-
-```tsx
-'use client';
-
-import { useState } from 'react';
-import { useNear } from '@/providers/NearProvider';
-import DropCreationForm from '@/components/DropCreation/DropCreationForm';
-import DropResults from '@/components/DropCreation/DropResults';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Gift, Wallet, Zap, Shield } from 'lucide-react';
-
-export default function HomePage() {
- const { isSignedIn, signIn } = useNear();
- const [createdDrop, setCreatedDrop] = useState(null);
-
- if (!isSignedIn) {
- return (
-
- {/* Hero Section */}
-
-
- Token Distribution
- Made Simple
-
-
- Create gasless token drops for NEAR, fungible tokens, and NFTs.
- Recipients don't need existing accounts to claim their tokens.
-
-
-
-
- {/* Features */}
-
-
-
-
- Gasless Claims
-
- Recipients don't need NEAR tokens to claim their drops thanks to function-call access keys.
-
-
-
-
-
-
-
- Multiple Token Types
-
- Support for NEAR tokens, fungible tokens (FTs), and non-fungible tokens (NFTs).
-
-
-
-
-
-
-
- Account Creation
-
- New users can create NEAR accounts automatically during the claiming process.
-
-
-
-
-
- {/* How It Works */}
-
-
- How It Works
-
-
-
-
-
- 1
-
-
Create Drop
-
Choose token type and amount, generate access keys
-
-
-
- 2
-
-
Distribute Links
-
Share claim links or QR codes with recipients
-
-
-
- 3
-
-
Gasless Claiming
-
Recipients use private keys to claim without gas fees
-
-
-
- 4
-
-
Account Creation
-
New users get NEAR accounts created automatically
-
-
-
-
-
- );
- }
-
- return (
-
- {createdDrop ? (
-
-
-
-
-
-
- ) : (
-
-
-
- )}
-
- );
-}
-```
-
-### Claim Page
-
-Create `src/app/claim/page.tsx`:
-
-```tsx
-import ClaimPage from '@/components/DropClaiming/ClaimPage';
-
-export default function Claim() {
- return ;
-}
-```
-
-### Dashboard Page
-
-Create `src/app/dashboard/page.tsx`:
-
-```tsx
-'use client';
-
-import { useNear } from '@/providers/NearProvider';
-import DropDashboard from '@/components/Dashboard/DropDashboard';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Wallet } from 'lucide-react';
-
-export default function Dashboard() {
- const { isSignedIn, signIn } = useNear();
-
- if (!isSignedIn) {
- return (
-
-
-
- Sign In Required
-
-
-
- Please connect your wallet to view your drop dashboard.
-
-
-
-
-
- );
- }
-
- return ;
-}
-```
-
----
-
-## Deployment and Configuration
-
-### Build Configuration
-
-Update `next.config.js`:
-
-```javascript
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- experimental: {
- appDir: true,
- },
- webpack: (config) => {
- config.resolve.fallback = {
- ...config.resolve.fallback,
- fs: false,
- net: false,
- tls: false,
- };
- return config;
- },
-};
-
-module.exports = nextConfig;
-```
-
-### Environment Variables for Production
-
-Create `.env.production`:
-
-```bash
-NEXT_PUBLIC_NETWORK_ID=mainnet
-NEXT_PUBLIC_CONTRACT_ID=your-contract.near
-NEXT_PUBLIC_WALLET_URL=https://app.mynearwallet.com
-NEXT_PUBLIC_HELPER_URL=https://helper.near.org
-NEXT_PUBLIC_RPC_URL=https://rpc.near.org
-```
-
----
-
-## Testing the Frontend
-
-### Running the Development Server
-
-```bash
-npm run dev
-```
-
-### Testing Different Scenarios
-
-1. **Wallet Connection**: Test signing in/out with different wallet providers
-2. **Drop Creation**: Create drops with different token types and amounts
-3. **Key Generation**: Verify keys are generated correctly and securely
-4. **Claiming**: Test both existing account and new account claiming flows
-5. **QR Code Generation**: Verify QR codes contain correct claim URLs
-6. **Mobile Responsiveness**: Test on different screen sizes
-
----
+---
## Next Steps
-You now have a complete frontend for the NEAR Drop system featuring:
-
-✅ **Wallet Integration**: Seamless connection with NEAR wallets
-✅ **Drop Creation**: Support for all three token types (NEAR, FT, NFT)
-✅ **Key Management**: Secure key generation and distribution
-✅ **QR Code Support**: Easy sharing via QR codes
-✅ **Claiming Interface**: Simple claiming for both new and existing users
-✅ **Dashboard**: Management interface for created drops
-✅ **Mobile Responsive**: Works on all devices
+Your NEAR Drop system is nearly complete. The final step is to thoroughly test everything and deploy to production.
---
-:::note Frontend Best Practices
-- Always validate user inputs before submitting transactions
-- Use proper error handling and loading states throughout
-- Store sensitive data (private keys) securely and temporarily
-- Implement proper wallet connection state management
-- Test thoroughly on both testnet and mainnet before production use
-:::
+:::tip User Experience
+The frontend makes your powerful token distribution system accessible to everyone. Non-technical users can now create airdrops as easily as sending an email!
+:::
\ No newline at end of file
diff --git a/docs/tutorials/neardrop/ft-drops.md b/docs/tutorials/neardrop/ft-drops.md
index d234916acde..d50b1fcb064 100644
--- a/docs/tutorials/neardrop/ft-drops.md
+++ b/docs/tutorials/neardrop/ft-drops.md
@@ -1,58 +1,48 @@
---
id: ft-drops
title: Fungible Token Drops
-sidebar_label: Fungible Token Drops
-description: "Learn how to implement fungible token (FT) drops using NEP-141 standard tokens. This section covers cross-contract calls, storage registration, and FT transfer patterns."
+sidebar_label: FT Drops
+description: "Add support for NEP-141 fungible tokens with cross-contract calls and automatic user registration."
---
-import {Github} from "@site/src/components/codetabs"
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-Fungible token drops allow you to distribute any NEP-141 compatible token through the NEAR Drop system. This is more complex than NEAR drops because it requires cross-contract calls and proper storage management on the target FT contract.
+Time to level up! Let's add support for fungible token drops. This is where things get interesting because we need to interact with other contracts.
---
-## Understanding FT Drop Requirements
+## Why FT Drops Are Different
+
+Unlike NEAR tokens (which are native), fungible tokens live in separate contracts. This means:
-Fungible token drops involve several additional considerations:
+- **Cross-contract calls** to transfer tokens
+- **User registration** on FT contracts (for storage)
+- **Callback handling** when things go wrong
+- **More complex gas management**
-1. **Cross-Contract Calls**: We need to interact with external FT contracts
-2. **Storage Registration**: Recipients must be registered on the FT contract
-3. **Transfer Patterns**: Using `ft_transfer` for token distribution
-4. **Error Handling**: Managing failures in cross-contract operations
+But don't worry - we'll handle all of this step by step.
---
-## Extending the Drop Types
+## Extend Drop Types
-First, let's extend our drop types to include fungible tokens. Update `src/drop_types.rs`:
+First, let's add FT support to our drop types in `src/drop_types.rs`:
```rust
-use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}};
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub enum Drop {
Near(NearDrop),
- FungibleToken(FtDrop),
+ FungibleToken(FtDrop), // New!
}
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
-pub struct NearDrop {
- pub amount: NearToken,
- pub counter: u64,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub struct FtDrop {
pub ft_contract: AccountId,
- pub amount: String, // Using String to handle large numbers
+ pub amount: String, // String to handle large numbers
pub counter: u64,
}
+```
+Update the helper methods:
+```rust
impl Drop {
pub fn get_counter(&self) -> u64 {
match self {
@@ -63,16 +53,8 @@ impl Drop {
pub fn decrement_counter(&mut self) {
match self {
- Drop::Near(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
- Drop::FungibleToken(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
+ Drop::Near(drop) => drop.counter -= 1,
+ Drop::FungibleToken(drop) => drop.counter -= 1,
}
}
}
@@ -82,80 +64,66 @@ impl Drop {
## Cross-Contract Interface
-Create `src/external.rs` to define the interface for interacting with FT contracts:
+Create `src/external.rs` to define how we talk to FT contracts:
```rust
-use near_sdk::{ext_contract, AccountId, Gas};
+use near_sdk::{ext_contract, AccountId, Gas, NearToken};
// Interface for NEP-141 fungible token contracts
#[ext_contract(ext_ft)]
pub trait FungibleToken {
fn ft_transfer(&mut self, receiver_id: AccountId, amount: String, memo: Option);
- fn storage_deposit(&mut self, account_id: Option, registration_only: Option);
- fn storage_balance_of(&self, account_id: AccountId) -> Option;
+ fn storage_deposit(&mut self, account_id: Option);
}
-// Interface for callbacks to this contract
+// Interface for callbacks to our contract
#[ext_contract(ext_self)]
-pub trait FtDropCallbacks {
- fn ft_transfer_callback(
- &mut self,
- public_key: near_sdk::PublicKey,
- receiver_id: AccountId,
- ft_contract: AccountId,
- amount: String,
- );
-}
-
-#[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)]
-#[serde(crate = "near_sdk::serde")]
-pub struct StorageBalance {
- pub total: String,
- pub available: String,
+pub trait DropCallbacks {
+ fn ft_transfer_callback(&mut self, public_key: PublicKey, receiver_id: AccountId);
}
-// Gas constants for cross-contract calls
+// Gas constants
pub const GAS_FOR_FT_TRANSFER: Gas = Gas(20_000_000_000_000);
pub const GAS_FOR_STORAGE_DEPOSIT: Gas = Gas(30_000_000_000_000);
pub const GAS_FOR_CALLBACK: Gas = Gas(20_000_000_000_000);
-// Storage deposit for FT registration (typical amount)
-pub const STORAGE_DEPOSIT_AMOUNT: NearToken = NearToken::from_millinear(125); // 0.125 NEAR
+// Storage deposit for FT registration
+pub const STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(125); // 0.125 NEAR
```
---
## Creating FT Drops
-Add the FT drop creation function to your main contract in `src/lib.rs`:
+Add this to your main contract in `src/lib.rs`:
```rust
+use crate::external::*;
+
#[near_bindgen]
impl Contract {
- /// Create a new fungible token drop
+ /// Create a fungible token drop
pub fn create_ft_drop(
&mut self,
public_keys: Vec,
ft_contract: AccountId,
amount_per_drop: String,
) -> u64 {
- let deposit = env::attached_deposit();
let num_keys = public_keys.len() as u64;
+ let deposit = env::attached_deposit();
- // Calculate required deposit
- let required_deposit = self.calculate_ft_drop_cost(num_keys);
-
- assert!(
- deposit >= required_deposit,
- "Insufficient deposit. Required: {}, Provided: {}",
- required_deposit.as_yoctonear(),
- deposit.as_yoctonear()
- );
-
- // Validate that the amount is a valid number
+ // Validate amount format
amount_per_drop.parse::()
.expect("Invalid amount format");
+ // Calculate costs
+ let storage_cost = DROP_STORAGE_COST + KEY_STORAGE_COST * num_keys;
+ let gas_cost = ACCESS_KEY_ALLOWANCE * num_keys;
+ let registration_buffer = STORAGE_DEPOSIT * num_keys; // For user registration
+ let total_cost = storage_cost + gas_cost + registration_buffer;
+
+ assert!(deposit >= total_cost, "Need {} NEAR for FT drop", total_cost.as_near());
+
// Create the drop
let drop_id = self.next_drop_id;
self.next_drop_id += 1;
@@ -168,512 +136,257 @@ impl Contract {
self.drop_by_id.insert(&drop_id, &drop);
- // Add access keys and map public keys to drop ID
+ // Add keys
for public_key in public_keys {
- self.add_access_key_for_drop(&public_key);
- self.drop_id_by_key.insert(&public_key, &drop_id);
+ self.add_claim_key(&public_key, drop_id);
}
- env::log_str(&format!(
- "Created FT drop {} with {} {} tokens per claim for {} keys",
- drop_id,
- amount_per_drop,
- ft_contract,
- num_keys
- ));
-
+ env::log_str(&format!("Created FT drop {} with {} {} tokens per claim",
+ drop_id, amount_per_drop, ft_contract));
drop_id
}
-
- /// Calculate the cost of creating an FT drop
- fn calculate_ft_drop_cost(&self, num_keys: u64) -> NearToken {
- let storage_cost = DROP_STORAGE_COST
- .saturating_add(KEY_STORAGE_COST.saturating_mul(num_keys))
- .saturating_add(ACCESS_KEY_STORAGE_COST.saturating_mul(num_keys));
-
- let total_allowance = FUNCTION_CALL_ALLOWANCE.saturating_mul(num_keys);
-
- // Add storage deposit for potential registrations
- let registration_buffer = STORAGE_DEPOSIT_AMOUNT.saturating_mul(num_keys);
-
- storage_cost
- .saturating_add(total_allowance)
- .saturating_add(registration_buffer)
- }
}
```
---
-## Implementing FT Claiming Logic
-
-The FT claiming process is more complex because it involves:
-1. Checking if the recipient is registered on the FT contract
-2. Registering them if necessary
-3. Transferring the tokens
-4. Handling callbacks for error recovery
+## FT Claiming Logic
-Update your `src/claim.rs` file:
+The tricky part! Update your `src/claim.rs`:
```rust
-use crate::external::*;
-use near_sdk::Promise;
-
-#[near_bindgen]
impl Contract {
- /// Internal claiming logic (updated to handle FT drops)
- fn internal_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
+ /// Updated core claiming logic
+ fn process_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
let drop_id = self.drop_id_by_key.get(public_key)
.expect("No drop found for this key");
let mut drop = self.drop_by_id.get(&drop_id)
- .expect("Drop not found");
+ .expect("Drop data not found");
- assert!(drop.get_counter() > 0, "All drops have been claimed");
+ assert!(drop.get_counter() > 0, "No claims remaining");
match &drop {
Drop::Near(near_drop) => {
- // Handle NEAR token drops (as before)
- Promise::new(receiver_id.clone())
- .transfer(near_drop.amount);
-
- env::log_str(&format!(
- "Claimed {} NEAR tokens to {}",
- near_drop.amount.as_yoctonear(),
- receiver_id
- ));
-
- // Clean up immediately for NEAR drops
- self.cleanup_after_claim(public_key, &mut drop, drop_id);
+ // Handle NEAR tokens (same as before)
+ Promise::new(receiver_id.clone()).transfer(near_drop.amount);
+ self.cleanup_claim(public_key, &mut drop, drop_id);
}
Drop::FungibleToken(ft_drop) => {
- // Handle FT drops with cross-contract calls
- self.claim_ft_drop(
+ // Handle FT tokens with cross-contract call
+ self.claim_ft_tokens(
public_key.clone(),
receiver_id.clone(),
ft_drop.ft_contract.clone(),
ft_drop.amount.clone(),
);
-
// Note: cleanup happens in callback for FT drops
- return;
}
}
}
- /// Claim fungible tokens with proper registration handling
- fn claim_ft_drop(
+ /// Claim FT tokens with automatic user registration
+ fn claim_ft_tokens(
&mut self,
public_key: PublicKey,
receiver_id: AccountId,
ft_contract: AccountId,
amount: String,
) {
- // First, check if the receiver is registered on the FT contract
+ // First, register the user on the FT contract
ext_ft::ext(ft_contract.clone())
.with_static_gas(GAS_FOR_STORAGE_DEPOSIT)
- .storage_balance_of(receiver_id.clone())
+ .with_attached_deposit(STORAGE_DEPOSIT)
+ .storage_deposit(Some(receiver_id.clone()))
.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_CALLBACK)
- .handle_storage_check(
- public_key,
- receiver_id,
- ft_contract,
- amount,
- )
+ .ft_registration_callback(public_key, receiver_id, ft_contract, amount)
);
}
- /// Handle the result of storage balance check
- #[private]
- pub fn handle_storage_check(
- &mut self,
- public_key: PublicKey,
- receiver_id: AccountId,
- ft_contract: AccountId,
- amount: String,
- ) {
- let storage_balance: Option = match env::promise_result(0) {
- PromiseResult::Successful(val) => {
- near_sdk::serde_json::from_slice(&val)
- .unwrap_or(None)
- }
- _ => None,
- };
-
- if storage_balance.is_none() {
- // User is not registered, register them first
- env::log_str(&format!("Registering {} on FT contract", receiver_id));
-
- ext_ft::ext(ft_contract.clone())
- .with_static_gas(GAS_FOR_STORAGE_DEPOSIT)
- .with_attached_deposit(STORAGE_DEPOSIT_AMOUNT)
- .storage_deposit(Some(receiver_id.clone()), Some(true))
- .then(
- Self::ext(env::current_account_id())
- .with_static_gas(GAS_FOR_CALLBACK)
- .handle_registration_and_transfer(
- public_key,
- receiver_id,
- ft_contract,
- amount,
- )
- );
- } else {
- // User is already registered, proceed with transfer
- self.execute_ft_transfer(public_key, receiver_id, ft_contract, amount);
- }
- }
-
- /// Handle registration completion and proceed with transfer
+ /// Handle FT registration result
#[private]
- pub fn handle_registration_and_transfer(
- &mut self,
- public_key: PublicKey,
- receiver_id: AccountId,
- ft_contract: AccountId,
- amount: String,
- ) {
- if is_promise_success() {
- env::log_str(&format!("Successfully registered {}", receiver_id));
- self.execute_ft_transfer(public_key, receiver_id, ft_contract, amount);
- } else {
- env::log_str(&format!("Failed to register {} on FT contract", receiver_id));
- // Registration failed - this shouldn't happen in normal circumstances
- // For now, we'll panic, but in production you might want to handle this gracefully
- env::panic_str("Failed to register user on FT contract");
- }
- }
-
- /// Execute the actual FT transfer
- fn execute_ft_transfer(
+ pub fn ft_registration_callback(
&mut self,
public_key: PublicKey,
receiver_id: AccountId,
ft_contract: AccountId,
amount: String,
) {
+ // Registration succeeded or user was already registered
+ // Now transfer the actual tokens
ext_ft::ext(ft_contract.clone())
.with_static_gas(GAS_FOR_FT_TRANSFER)
.ft_transfer(
receiver_id.clone(),
amount.clone(),
- Some(format!("NEAR Drop claim to {}", receiver_id))
+ Some("NEAR Drop claim".to_string())
)
.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_CALLBACK)
- .ft_transfer_callback(
- public_key,
- receiver_id,
- ft_contract,
- amount,
- )
+ .ft_transfer_callback(public_key, receiver_id)
);
}
- /// Handle the result of FT transfer
+ /// Handle FT transfer result
#[private]
- pub fn ft_transfer_callback(
- &mut self,
- public_key: PublicKey,
- receiver_id: AccountId,
- ft_contract: AccountId,
- amount: String,
- ) {
- if is_promise_success() {
- env::log_str(&format!(
- "Successfully transferred {} {} tokens to {}",
- amount,
- ft_contract,
- receiver_id
- ));
-
- // Get drop info for cleanup
- let drop_id = self.drop_id_by_key.get(&public_key)
- .expect("Drop not found during cleanup");
-
- let mut drop = self.drop_by_id.get(&drop_id)
- .expect("Drop data not found during cleanup");
+ pub fn ft_transfer_callback(&mut self, public_key: PublicKey, receiver_id: AccountId) {
+ let success = env::promise_results_count() == 1 &&
+ matches!(env::promise_result(0), PromiseResult::Successful(_));
+
+ if success {
+ env::log_str(&format!("FT tokens transferred to {}", receiver_id));
- // Clean up after successful transfer
- self.cleanup_after_claim(&public_key, &mut drop, drop_id);
+ // Clean up the claim
+ if let Some(drop_id) = self.drop_id_by_key.get(&public_key) {
+ if let Some(mut drop) = self.drop_by_id.get(&drop_id) {
+ self.cleanup_claim(&public_key, &mut drop, drop_id);
+ }
+ }
} else {
- env::log_str(&format!(
- "Failed to transfer {} {} tokens to {}",
- amount,
- ft_contract,
- receiver_id
- ));
-
- // Transfer failed - this could happen if:
- // 1. The drop contract doesn't have enough tokens
- // 2. The FT contract has some issue
- // For now, we'll panic, but you might want to handle this more gracefully
env::panic_str("FT transfer failed");
}
}
- /// Clean up after a successful claim
- fn cleanup_after_claim(&mut self, public_key: &PublicKey, drop: &mut Drop, drop_id: u64) {
- // Decrement counter
+ /// Clean up after successful claim
+ fn cleanup_claim(&mut self, public_key: &PublicKey, drop: &mut Drop, drop_id: u64) {
drop.decrement_counter();
if drop.get_counter() == 0 {
- // All drops claimed, remove the drop entirely
self.drop_by_id.remove(&drop_id);
- env::log_str(&format!("Drop {} fully claimed and removed", drop_id));
} else {
- // Update the drop with decremented counter
- self.drop_by_id.insert(&drop_id, &drop);
+ self.drop_by_id.insert(&drop_id, drop);
}
- // Remove the public key mapping and access key
self.drop_id_by_key.remove(public_key);
-
- // Remove the access key from the account
- Promise::new(env::current_account_id())
- .delete_key(public_key.clone());
+ Promise::new(env::current_account_id()).delete_key(public_key.clone());
}
}
-
-/// Check if the last promise was successful
-fn is_promise_success() -> bool {
- env::promise_results_count() == 1 &&
- matches!(env::promise_result(0), PromiseResult::Successful(_))
-}
```
---
## Testing FT Drops
-### Deploy a Test FT Contract
-
-First, you'll need an FT contract to test with. You can use the [reference FT implementation](https://github.com/near-examples/FT):
+You'll need an FT contract to test with. Let's use a simple one:
```bash
-# Clone and build the FT contract
-git clone https://github.com/near-examples/FT.git
-cd FT
-cargo near build
-
-# Deploy to testnet
+# Deploy a test FT contract (you can use the reference implementation)
near create-account test-ft.testnet --useFaucet
-near deploy test-ft.testnet target/near/fungible_token.wasm
+near deploy test-ft.testnet ft-contract.wasm
# Initialize with your drop contract as owner
near call test-ft.testnet new_default_meta '{
- "owner_id": "drop-contract.testnet",
+ "owner_id": "drop-test.testnet",
"total_supply": "1000000000000000000000000000"
}' --accountId test-ft.testnet
```
-### Create an FT Drop
-
-
-
-
- ```bash
- # Create an FT drop with 1000 tokens per claim
- near call drop-contract.testnet create_ft_drop '{
- "public_keys": [
- "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8",
- "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"
- ],
- "ft_contract": "test-ft.testnet",
- "amount_per_drop": "1000000000000000000000000"
- }' --accountId drop-contract.testnet --deposit 2
- ```
-
-
-
-
- ```bash
- # Create an FT drop with 1000 tokens per claim
- near contract call-function as-transaction drop-contract.testnet create_ft_drop json-args '{
- "public_keys": [
- "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8",
- "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"
- ],
- "ft_contract": "test-ft.testnet",
- "amount_per_drop": "1000000000000000000000000"
- }' prepaid-gas '200.0 Tgas' attached-deposit '2 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send
- ```
-
-
-
-### Transfer FT Tokens to Drop Contract
-
-Before users can claim, the drop contract needs to have the FT tokens:
-
-
-
-
- ```bash
- # First register the drop contract on the FT contract
- near call test-ft.testnet storage_deposit '{
- "account_id": "drop-contract.testnet"
- }' --accountId drop-contract.testnet --deposit 0.25
-
- # Transfer tokens to the drop contract
- near call test-ft.testnet ft_transfer '{
- "receiver_id": "drop-contract.testnet",
- "amount": "2000000000000000000000000"
- }' --accountId drop-contract.testnet --depositYocto 1
- ```
-
-
-
-
- ```bash
- # First register the drop contract on the FT contract
- near contract call-function as-transaction test-ft.testnet storage_deposit json-args '{
- "account_id": "drop-contract.testnet"
- }' prepaid-gas '100.0 Tgas' attached-deposit '0.25 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send
-
- # Transfer tokens to the drop contract
- near contract call-function as-transaction test-ft.testnet ft_transfer json-args '{
- "receiver_id": "drop-contract.testnet",
- "amount": "2000000000000000000000000"
- }' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send
- ```
-
-
-
-### Claim FT Tokens
-
-
-
-
- ```bash
- # Claim FT tokens to an existing account
- near call drop-contract.testnet claim_for '{
- "account_id": "recipient.testnet"
- }' --accountId drop-contract.testnet \
- --keyPair '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "private_key": "ed25519:..."}'
- ```
-
-
-
-
- ```bash
- # Claim FT tokens to an existing account
- near contract call-function as-transaction drop-contract.testnet claim_for json-args '{
- "account_id": "recipient.testnet"
- }' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8 --signer-private-key ed25519:... send
- ```
-
-
+Register your drop contract and transfer some tokens to it:
----
+```bash
+# Register drop contract
+near call test-ft.testnet storage_deposit '{
+ "account_id": "drop-test.testnet"
+}' --accountId drop-test.testnet --deposit 0.25
+
+# Transfer tokens to drop contract
+near call test-ft.testnet ft_transfer '{
+ "receiver_id": "drop-test.testnet",
+ "amount": "10000000000000000000000000"
+}' --accountId drop-test.testnet --depositYocto 1
+```
-## Adding View Methods for FT Drops
+Now create an FT drop:
-Add these helpful view methods to query FT drop information:
+```bash
+# Create FT drop with 1000 tokens per claim
+near call drop-test.testnet create_ft_drop '{
+ "public_keys": [
+ "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8"
+ ],
+ "ft_contract": "test-ft.testnet",
+ "amount_per_drop": "1000000000000000000000000"
+}' --accountId drop-test.testnet --deposit 2
+```
+
+Claim the FT drop:
+
+```bash
+# Claim FT tokens (recipient gets registered automatically)
+near call drop-test.testnet claim_for '{
+ "account_id": "alice.testnet"
+}' --accountId drop-test.testnet \
+ --keyPair
+
+# Check if Alice received the tokens
+near view test-ft.testnet ft_balance_of '{"account_id": "alice.testnet"}'
+```
+
+---
+
+## Add Helper Functions
```rust
#[near_bindgen]
impl Contract {
- /// Calculate FT drop cost (view method)
- pub fn calculate_ft_drop_cost_view(&self, num_keys: u64) -> NearToken {
- self.calculate_ft_drop_cost(num_keys)
+ /// Calculate FT drop cost
+ pub fn estimate_ft_drop_cost(&self, num_keys: u64) -> NearToken {
+ let storage_cost = DROP_STORAGE_COST + KEY_STORAGE_COST * num_keys;
+ let gas_cost = ACCESS_KEY_ALLOWANCE * num_keys;
+ let registration_buffer = STORAGE_DEPOSIT * num_keys;
+ storage_cost + gas_cost + registration_buffer
}
/// Get FT drop details
- pub fn get_ft_drop_details(&self, drop_id: u64) -> Option {
+ pub fn get_ft_drop_info(&self, drop_id: u64) -> Option<(AccountId, String, u64)> {
if let Some(Drop::FungibleToken(ft_drop)) = self.drop_by_id.get(&drop_id) {
- Some(FtDropInfo {
- ft_contract: ft_drop.ft_contract,
- amount_per_drop: ft_drop.amount,
- remaining_claims: ft_drop.counter,
- })
+ Some((ft_drop.ft_contract, ft_drop.amount, ft_drop.counter))
} else {
None
}
}
}
-
-#[derive(near_sdk::serde::Serialize)]
-#[serde(crate = "near_sdk::serde")]
-pub struct FtDropInfo {
- pub ft_contract: AccountId,
- pub amount_per_drop: String,
- pub remaining_claims: u64,
-}
```
---
-## Error Handling for FT Operations
+## Common Issues & Solutions
-Add specific error handling for FT operations:
+**"Storage deposit failed"**
+- The FT contract needs sufficient balance to register users
+- Make sure you attach enough NEAR when creating the drop
-```rust
-// Add these error constants
-const ERR_FT_TRANSFER_FAILED: &str = "Fungible token transfer failed";
-const ERR_FT_REGISTRATION_FAILED: &str = "Failed to register on FT contract";
-const ERR_INVALID_FT_AMOUNT: &str = "Invalid FT amount format";
-
-// Enhanced error handling in create_ft_drop
-pub fn create_ft_drop(
- &mut self,
- public_keys: Vec,
- ft_contract: AccountId,
- amount_per_drop: String,
-) -> u64 {
- // Validate amount format
- amount_per_drop.parse::()
- .unwrap_or_else(|_| env::panic_str(ERR_INVALID_FT_AMOUNT));
-
- // Validate FT contract exists (basic check)
- assert!(
- ft_contract.as_str().len() >= 2 && ft_contract.as_str().contains('.'),
- "Invalid FT contract account ID"
- );
-
- // Rest of implementation...
-}
-```
+**"FT transfer failed"**
+- Check that the drop contract actually owns the FT tokens
+- Verify the FT contract address is correct
----
+**"Gas limit exceeded"**
+- FT operations use more gas than NEAR transfers
+- Our gas constants should work for most cases
-## Gas Optimization Tips
-
-FT drops use more gas due to cross-contract calls. Here are some optimization tips:
-
-1. **Batch Operations**: Group multiple claims when possible
-2. **Gas Estimation**: Monitor gas usage and adjust constants
-3. **Storage Efficiency**: Minimize data stored in contract state
-4. **Error Recovery**: Implement proper rollback mechanisms
+---
-```rust
-// Optimized gas constants based on testing
-pub const GAS_FOR_FT_TRANSFER: Gas = Gas(20_000_000_000_000); // 20 TGas
-pub const GAS_FOR_STORAGE_DEPOSIT: Gas = Gas(30_000_000_000_000); // 30 TGas
-pub const GAS_FOR_CALLBACK: Gas = Gas(20_000_000_000_000); // 20 TGas
-```
+## What You've Accomplished
----
+Great work! You now have:
-## Next Steps
+✅ **FT drop creation** with cost calculation
+✅ **Cross-contract calls** to FT contracts
+✅ **Automatic user registration** on FT contracts
+✅ **Callback handling** for robust error recovery
+✅ **Gas optimization** for complex operations
-You now have a working FT drop system that handles:
-- Cross-contract FT transfers
-- Automatic user registration on FT contracts
-- Proper error handling and callbacks
-- Storage cost management
+FT drops are significantly more complex than NEAR drops because they involve multiple contracts and asynchronous operations. But you've handled it like a pro!
-Next, let's implement NFT drops, which introduce unique token distribution patterns.
+Next up: NFT drops, which have their own unique challenges around uniqueness and ownership.
-[Continue to NFT Drops →](./nft-drops)
+[Continue to NFT Drops →](./nft-drops.md)
---
-:::note FT Drop Considerations
-- Always ensure the drop contract has sufficient FT tokens before creating drops
-- Monitor gas costs as they are higher than NEAR token drops
-- Test with various FT contracts to ensure compatibility
-- Consider implementing deposit refunds for failed operations
+:::tip Pro Tip
+Always test FT drops with small amounts first. The cross-contract call flow has more moving parts, so it's good to verify everything works before creating large drops.
:::
\ No newline at end of file
diff --git a/docs/tutorials/neardrop/introduction.md b/docs/tutorials/neardrop/introduction.md
index 37f94f96640..f49a3199e8d 100644
--- a/docs/tutorials/neardrop/introduction.md
+++ b/docs/tutorials/neardrop/introduction.md
@@ -2,132 +2,81 @@
id: introduction
title: NEAR Drop Tutorial
sidebar_label: Introduction
-description: "Learn to build a token distribution system using NEAR Drop smart contracts. This tutorial covers creating token drops for $NEAR, Fungible Tokens, and NFTs with function-call access keys for seamless user experience."
+description: "Build a token distribution system that lets you airdrop NEAR, FTs, and NFTs to users without them needing gas fees or existing accounts."
---
-In this comprehensive tutorial, you'll learn how to build and deploy a NEAR Drop smart contract that enables seamless token distribution across the NEAR ecosystem. NEAR Drop allows users to create token drops ($NEAR, Fungible Tokens, and Non-Fungible Tokens) and link them to specific private keys, creating a smooth onboarding experience for new users.
+Ever wanted to give tokens to someone who doesn't have a NEAR account? Or send an airdrop without recipients needing gas fees? That's exactly what we're building!
----
-
-## What You'll Build
-
-By the end of this tutorial, you'll have created a fully functional token distribution system that includes:
-
-- **NEAR Token Drops**: Distribute native NEAR tokens to multiple recipients
-- **Fungible Token (FT) Drops**: Create drops for any NEP-141 compatible token
-- **Non-Fungible Token (NFT) Drops**: Distribute unique NFTs to users
-- **Function-Call Access Keys**: Enable gasless claiming for recipients
-- **Account Creation**: Allow users without NEAR accounts to claim drops and create accounts
-
-
+**NEAR Drop** lets you create token distributions that anyone can claim with just a private key - no NEAR account or gas fees required.
---
-## Prerequisites
-
-To complete this tutorial successfully, you'll need:
-
-- [Rust](/smart-contracts/quickstart#prerequisites) installed
-- [A NEAR wallet](https://testnet.mynearwallet.com)
-- [NEAR-CLI](/tools/near-cli#installation)
-- [cargo-near](https://github.com/near/cargo-near)
-- Basic understanding of smart contracts and NEAR Protocol
-
-:::info New to NEAR?
-If you're new to NEAR development, we recommend starting with our [Smart Contract Quickstart](../../smart-contracts/quickstart.md) guide.
-:::
-
----
-
-## How NEAR Drop Works
-
-NEAR Drop leverages NEAR's unique [Function-Call Access Keys](../../protocol/access-keys.md) to create a seamless token distribution experience:
-
-1. **Create Drop**: A user creates a drop specifying recipients, token amounts, and generates public keys
-2. **Add Access Keys**: The contract adds function-call access keys that allow only claiming operations
-3. **Distribute Keys**: Private keys are distributed to recipients (via links, QR codes, etc.)
-4. **Claim Tokens**: Recipients use the private keys to claim their tokens
-5. **Account Creation**: New users can create NEAR accounts during the claiming process
+## What You'll Build
-### Key Benefits
+A complete token distribution system with:
-- **No Gas Fees for Recipients**: Function-call keys handle gas costs
-- **Smooth Onboarding**: New users can claim tokens and create accounts in one step
-- **Multi-Token Support**: Works with NEAR, FTs, and NFTs
-- **Batch Operations**: Create multiple drops efficiently
-- **Secure Distribution**: Private keys control access to specific drops
+- **NEAR Token Drops**: Send native NEAR to multiple people
+- **FT Drops**: Distribute any NEP-141 token (like stablecoins)
+- **NFT Drops**: Give away unique NFTs
+- **Gasless Claims**: Recipients don't pay any fees
+- **Auto Account Creation**: New users get NEAR accounts automatically
---
-## Tutorial Overview
+## How It Works
-This tutorial is divided into several sections that build upon each other:
+1. **Create Drop**: You generate private keys and link them to tokens
+2. **Share Keys**: Send private keys via links, QR codes, etc.
+3. **Gasless Claims**: Recipients use keys to claim without gas fees
+4. **Account Creation**: New users get NEAR accounts created automatically
-| Section | Description |
-|---------|-------------|
-| [Contract Architecture](./contract-architecture) | Understand the smart contract structure and key components |
-| [NEAR Token Drops](./near-drops) | Implement native NEAR token distribution |
-| [Fungible Token Drops](./ft-drops) | Add support for NEP-141 fungible tokens |
-| [NFT Drops](./nft-drops) | Enable NFT distribution with NEP-171 tokens |
-| [Access Key Management](./access-keys) | Learn how function-call keys enable gasless operations |
-| [Account Creation](./account-creation) | Allow new users to create accounts when claiming |
-| [Frontend Integration](./frontend) | Build a web interface for creating and claiming drops |
-| [Testing & Deployment](./testing-deployment) | Test your contract and deploy to testnet/mainnet |
+The magic? **Function-call access keys** - NEAR's unique feature that enables gasless operations.
---
-## Real-World Use Cases
+## Real Examples
-NEAR Drop smart contracts are perfect for:
-
-- **Airdrops**: Distribute tokens to community members
-- **Marketing Campaigns**: Create token-gated experiences
-- **Onboarding**: Introduce new users to your dApp with token gifts
-- **Events**: Distribute commemorative NFTs at conferences
-- **Gaming**: Create in-game item drops and rewards
-- **DAO Operations**: Distribute governance tokens to members
+- **Community Airdrop**: Give 5 NEAR to 100 community members
+- **Event NFTs**: Distribute commemorative NFTs at conferences
+- **Onboarding**: Welcome new users with token gifts
+- **Gaming Rewards**: Drop in-game items to players
---
-## What Makes This Tutorial Special
-
-This tutorial showcases several advanced NEAR concepts:
+## What You Need
-- **Function-Call Access Keys**: Learn to use NEAR's powerful key system
-- **Cross-Contract Calls**: Interact with FT and NFT contracts
-- **Account Creation**: Programmatically create new NEAR accounts
-- **Storage Management**: Handle storage costs efficiently
-- **Batch Operations**: Process multiple operations in single transactions
+- [Rust installed](https://rustup.rs/)
+- [NEAR CLI](../../tools/cli.md#installation)
+- [A NEAR wallet](https://testnet.mynearwallet.com)
+- Basic understanding of smart contracts
---
-## Example Scenario
+## Tutorial Structure
-Throughout this tutorial, we'll use a practical example: **"NEAR Community Airdrop"**
+| Section | What You'll Learn |
+|---------|-------------------|
+| [Contract Architecture](/tutorials/neardrop/contract-architecture) | How the smart contract works |
+| [NEAR Drops](/tutorials/neardrop/near-drops) | Native NEAR token distribution |
+| [FT Drops](/tutorials/neardrop/ft-drops) | Fungible token distribution |
+| [NFT Drops](/tutorials/neardrop/nft-drops) | NFT distribution patterns |
+| [Frontend](/tutorials/neardrop/frontend) | Build a web interface |
-Imagine you're organizing a community event and want to:
-1. Give 5 NEAR tokens to 100 community members
-2. Distribute 1000 community FTs to early adopters
-3. Award special event NFTs to participants
-4. Allow users without NEAR accounts to claim and create accounts
-
-This tutorial will show you how to build exactly this system!
+Each section builds on the previous one, so start from the beginning!
---
-## Next Steps
+## Ready to Start?
-Ready to start building? Let's begin with understanding the contract architecture and core concepts.
+Let's dive into how the contract architecture works and start building your token distribution system.
-[Continue to Contract Architecture →](./contract-architecture)
+[Continue to Contract Architecture →](./contract-architecture.md)
---
-:::note Versioning for this article
-At the time of this writing, this tutorial works with the following versions:
-
-- near-cli: `0.17.0`
-- rustc: `1.82.0`
-- cargo-near: `0.6.2`
-- near-sdk-rs: `5.1.0`
+:::note
+This tutorial uses the latest NEAR SDK features. Make sure you have:
+- near-cli: `0.17.0`+
+- rustc: `1.82.0`+
+- cargo-near: `0.6.2`+
:::
\ No newline at end of file
diff --git a/docs/tutorials/neardrop/near-drops.md b/docs/tutorials/neardrop/near-drops.md
index 63dd72d83ed..dd1e98eb71d 100644
--- a/docs/tutorials/neardrop/near-drops.md
+++ b/docs/tutorials/neardrop/near-drops.md
@@ -1,140 +1,121 @@
---
id: near-drops
title: NEAR Token Drops
-sidebar_label: NEAR Token Drops
-description: "Learn how to implement NEAR token drops, the simplest form of token distribution using native NEAR tokens. This section covers creating drops, managing storage costs, and claiming NEAR tokens."
+sidebar_label: NEAR Token Drops
+description: "Build the foundation: distribute native NEAR tokens using function-call keys for gasless claiming."
---
-import {Github} from "@site/src/components/codetabs"
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-NEAR token drops are the foundation of the NEAR Drop system. They allow you to distribute native NEAR tokens to multiple recipients using a simple and gas-efficient approach. Let's implement this functionality step by step.
+Let's start with the simplest drop type: native NEAR tokens. This will teach you the core concepts before we move to more complex token types.
---
-## Setting Up the Project
+## Project Setup
-First, let's create a new Rust project and set up the basic structure:
+First, create a new Rust project:
```bash
cargo near new near-drop --contract
cd near-drop
```
-Add the necessary dependencies to your `Cargo.toml`:
-
+Update `Cargo.toml`:
```toml
-[package]
-name = "near-drop"
-version = "0.1.0"
-edition = "2021"
-
[dependencies]
near-sdk = { version = "5.1.0", features = ["unstable"] }
serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-
-[profile.release]
-codegen-units = 1
-opt-level = "z"
-lto = true
-debug = false
-panic = "abort"
-overflow-checks = true
```
---
-## Contract Structure
+## Basic Contract Structure
-Let's start by defining the main contract structure in `src/lib.rs`:
-
-
-
-This structure provides the foundation for managing multiple types of drops efficiently.
-
----
-
-## Implementing NEAR Token Drops
-
-### Drop Type Definition
-
-Create `src/drop_types.rs` to define our drop types:
+Let's start with the main contract in `src/lib.rs`:
```rust
-use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}};
+use near_sdk::{
+ env, near_bindgen, AccountId, NearToken, Promise, PublicKey,
+ collections::{LookupMap, UnorderedMap},
+ BorshDeserialize, BorshSerialize,
+};
+
+#[near_bindgen]
+#[derive(BorshDeserialize, BorshSerialize)]
+pub struct Contract {
+ pub top_level_account: AccountId,
+ pub next_drop_id: u64,
+ pub drop_id_by_key: LookupMap,
+ pub drop_by_id: UnorderedMap,
+}
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub enum Drop {
Near(NearDrop),
- // We'll add FT and NFT variants later
+ // We'll add FT and NFT later
}
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub struct NearDrop {
pub amount: NearToken,
pub counter: u64,
}
+```
-impl Drop {
- pub fn get_counter(&self) -> u64 {
- match self {
- Drop::Near(drop) => drop.counter,
- }
+---
+
+## Contract Initialization
+
+```rust
+impl Default for Contract {
+ fn default() -> Self {
+ env::panic_str("Contract must be initialized")
}
-
- pub fn decrement_counter(&mut self) {
- match self {
- Drop::Near(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
+}
+
+#[near_bindgen]
+impl Contract {
+ #[init]
+ pub fn new(top_level_account: AccountId) -> Self {
+ Self {
+ top_level_account,
+ next_drop_id: 0,
+ drop_id_by_key: LookupMap::new(b"k"),
+ drop_by_id: UnorderedMap::new(b"d"),
}
}
}
```
-### Creating NEAR Drops
+---
-Now let's implement the function to create NEAR token drops. Add this to your `src/lib.rs`:
+## Creating NEAR Drops
-```rust
-use near_sdk::{
- env, near_bindgen, AccountId, NearToken, Promise, PublicKey,
- collections::{LookupMap, UnorderedMap},
- BorshDeserialize, BorshSerialize,
-};
+The main function everyone will use:
-// Storage costs (approximate values)
-const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10); // 0.01 NEAR
-const KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // 0.001 NEAR
-const ACCESS_KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // 0.001 NEAR
-const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // 0.005 NEAR
+```rust
+// Storage costs (rough estimates)
+const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10);
+const KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1);
+const ACCESS_KEY_ALLOWANCE: NearToken = NearToken::from_millinear(5);
#[near_bindgen]
impl Contract {
- /// Create a new NEAR token drop
+ /// Create a drop that distributes NEAR tokens
pub fn create_near_drop(
&mut self,
public_keys: Vec,
amount_per_drop: NearToken,
) -> u64 {
- let deposit = env::attached_deposit();
let num_keys = public_keys.len() as u64;
+ let deposit = env::attached_deposit();
- // Calculate required deposit
- let required_deposit = self.calculate_near_drop_cost(num_keys, amount_per_drop);
+ // Calculate total cost
+ let storage_cost = DROP_STORAGE_COST + KEY_STORAGE_COST * num_keys;
+ let token_cost = amount_per_drop * num_keys;
+ let gas_cost = ACCESS_KEY_ALLOWANCE * num_keys;
+ let total_cost = storage_cost + token_cost + gas_cost;
- assert!(
- deposit >= required_deposit,
- "Insufficient deposit. Required: {}, Provided: {}",
- required_deposit.as_yoctonear(),
- deposit.as_yoctonear()
- );
+ assert!(deposit >= total_cost, "Need {} NEAR, got {}",
+ total_cost.as_near(), deposit.as_near());
// Create the drop
let drop_id = self.next_drop_id;
@@ -147,42 +128,26 @@ impl Contract {
self.drop_by_id.insert(&drop_id, &drop);
- // Add access keys and map public keys to drop ID
+ // Add function-call keys
for public_key in public_keys {
- self.add_access_key_for_drop(&public_key);
- self.drop_id_by_key.insert(&public_key, &drop_id);
+ self.add_claim_key(&public_key, drop_id);
}
- env::log_str(&format!(
- "Created NEAR drop {} with {} tokens per claim for {} keys",
- drop_id,
- amount_per_drop.as_yoctonear(),
- num_keys
- ));
-
+ env::log_str(&format!("Created drop {} with {} NEAR per claim",
+ drop_id, amount_per_drop.as_near()));
drop_id
}
- /// Calculate the cost of creating a NEAR drop
- fn calculate_near_drop_cost(&self, num_keys: u64, amount_per_drop: NearToken) -> NearToken {
- let storage_cost = DROP_STORAGE_COST
- .saturating_add(KEY_STORAGE_COST.saturating_mul(num_keys))
- .saturating_add(ACCESS_KEY_STORAGE_COST.saturating_mul(num_keys));
+ /// Add a function-call access key for claiming
+ fn add_claim_key(&mut self, public_key: &PublicKey, drop_id: u64) {
+ // Map key to drop
+ self.drop_id_by_key.insert(public_key, &drop_id);
- let total_token_cost = amount_per_drop.saturating_mul(num_keys);
- let total_allowance = FUNCTION_CALL_ALLOWANCE.saturating_mul(num_keys);
-
- storage_cost
- .saturating_add(total_token_cost)
- .saturating_add(total_allowance)
- }
-
- /// Add a function-call access key for claiming drops
- fn add_access_key_for_drop(&self, public_key: &PublicKey) {
+ // Add limited access key to contract
Promise::new(env::current_account_id())
.add_access_key(
public_key.clone(),
- FUNCTION_CALL_ALLOWANCE,
+ ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim_for,create_account_and_claim".to_string(),
);
@@ -192,304 +157,193 @@ impl Contract {
---
-## Claiming NEAR Tokens
+## Claiming Tokens
-Now let's implement the claiming functionality. Create `src/claim.rs`:
+Now for the claiming logic in `src/claim.rs`:
```rust
use crate::*;
#[near_bindgen]
impl Contract {
- /// Claim a drop to an existing account
+ /// Claim tokens to an existing account
pub fn claim_for(&mut self, account_id: AccountId) {
let public_key = env::signer_account_pk();
- self.internal_claim(&public_key, &account_id);
+ self.process_claim(&public_key, &account_id);
}
- /// Create a new account and claim drop to it
+ /// Create new account and claim tokens to it
pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise {
let public_key = env::signer_account_pk();
- // Validate that this is a valid subaccount creation
- assert!(
- account_id.as_str().ends_with(&format!(".{}", self.top_level_account)),
- "Account must be a subaccount of {}",
- self.top_level_account
- );
+ // Validate account format
+ assert!(account_id.as_str().ends_with(&format!(".{}", self.top_level_account)),
+ "Account must end with .{}", self.top_level_account);
- // Create the account first
- let create_promise = Promise::new(account_id.clone())
+ // Create account with 1 NEAR funding
+ Promise::new(account_id.clone())
.create_account()
- .transfer(NearToken::from_near(1)); // Fund with 1 NEAR for storage
-
- // Then claim the drop
- create_promise.then(
- Self::ext(env::current_account_id())
- .with_static_gas(Gas(30_000_000_000_000))
- .resolve_account_create(public_key, account_id)
- )
+ .transfer(NearToken::from_near(1))
+ .then(
+ Self::ext(env::current_account_id())
+ .with_static_gas(Gas(30_000_000_000_000))
+ .finish_account_creation(public_key, account_id)
+ )
}
- /// Resolve account creation and claim drop
+ /// Handle account creation result and claim
#[private]
- pub fn resolve_account_create(
- &mut self,
- public_key: PublicKey,
- account_id: AccountId,
- ) {
- // Check if account creation was successful
- if is_promise_success() {
- self.internal_claim(&public_key, &account_id);
- } else {
- env::panic_str("Failed to create account");
+ pub fn finish_account_creation(&mut self, public_key: PublicKey, account_id: AccountId) {
+ if env::promise_results_count() == 1 {
+ match env::promise_result(0) {
+ PromiseResult::Successful(_) => {
+ self.process_claim(&public_key, &account_id);
+ }
+ _ => env::panic_str("Account creation failed"),
+ }
}
}
- /// Internal claiming logic
- fn internal_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
- // Get the drop ID from the public key
+ /// Core claiming logic
+ fn process_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
+ // Find the drop
let drop_id = self.drop_id_by_key.get(public_key)
.expect("No drop found for this key");
- // Get the drop data
let mut drop = self.drop_by_id.get(&drop_id)
- .expect("Drop not found");
+ .expect("Drop data not found");
- // Check if drop is still claimable
- assert!(drop.get_counter() > 0, "All drops have been claimed");
+ // Check if claims available
+ let Drop::Near(near_drop) = &drop else {
+ env::panic_str("Wrong drop type");
+ };
- // Process the claim based on drop type
- match &drop {
- Drop::Near(near_drop) => {
- // Transfer NEAR tokens
- Promise::new(receiver_id.clone())
- .transfer(near_drop.amount);
-
- env::log_str(&format!(
- "Claimed {} NEAR tokens to {}",
- near_drop.amount.as_yoctonear(),
- receiver_id
- ));
- }
- }
+ assert!(near_drop.counter > 0, "No claims remaining");
- // Decrement counter and update drop
- drop.decrement_counter();
+ // Send tokens
+ Promise::new(receiver_id.clone()).transfer(near_drop.amount);
- if drop.get_counter() == 0 {
- // All drops claimed, clean up
- self.drop_by_id.remove(&drop_id);
- } else {
- // Update the drop with decremented counter
- self.drop_by_id.insert(&drop_id, &drop);
+ // Update drop counter
+ if let Drop::Near(ref mut near_drop) = drop {
+ near_drop.counter -= 1;
+
+ if near_drop.counter == 0 {
+ // All claimed, clean up
+ self.drop_by_id.remove(&drop_id);
+ } else {
+ // Update remaining counter
+ self.drop_by_id.insert(&drop_id, &drop);
+ }
}
- // Remove the public key mapping and access key
+ // Remove used key
self.drop_id_by_key.remove(public_key);
+ Promise::new(env::current_account_id()).delete_key(public_key.clone());
- Promise::new(env::current_account_id())
- .delete_key(public_key.clone());
+ env::log_str(&format!("Claimed {} NEAR to {}",
+ near_drop.amount.as_near(), receiver_id));
}
}
-
-/// Check if the last promise was successful
-fn is_promise_success() -> bool {
- env::promise_results_count() == 1 &&
- matches!(env::promise_result(0), PromiseResult::Successful(_))
-}
-```
-
----
-
-## Building and Testing
-
-### Build the Contract
-
-```bash
-cargo near build
```
-### Deploy and Initialize
-
-
-
-
- ```bash
- # Create a new account for your contract
- near create-account drop-contract.testnet --useFaucet
-
- # Deploy the contract
- near deploy drop-contract.testnet target/near/near_drop.wasm
-
- # Initialize the contract
- near call drop-contract.testnet new '{"top_level_account": "testnet"}' --accountId drop-contract.testnet
- ```
-
-
-
-
- ```bash
- # Create a new account for your contract
- near account create-account sponsor-by-faucet-service drop-contract.testnet autogenerate-new-keypair save-to-keychain network-config testnet create
-
- # Deploy the contract
- near contract deploy drop-contract.testnet use-file target/near/near_drop.wasm without-init-call network-config testnet sign-with-keychain send
-
- # Initialize the contract
- near contract call-function as-transaction drop-contract.testnet new json-args '{"top_level_account": "testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send
- ```
-
-
-
-### Create a NEAR Drop
-
-To create a NEAR drop, you need to generate public keys and calculate the required deposit:
-
-
-
-
- ```bash
- # Create a drop with 2 NEAR tokens per claim for 2 recipients
- near call drop-contract.testnet create_near_drop '{
- "public_keys": [
- "ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q",
- "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"
- ],
- "amount_per_drop": "2000000000000000000000000"
- }' --accountId drop-contract.testnet --deposit 5
- ```
-
-
-
-
- ```bash
- # Create a drop with 2 NEAR tokens per claim for 2 recipients
- near contract call-function as-transaction drop-contract.testnet create_near_drop json-args '{
- "public_keys": [
- "ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q",
- "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"
- ],
- "amount_per_drop": "2000000000000000000000000"
- }' prepaid-gas '100.0 Tgas' attached-deposit '5 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send
- ```
-
-
-
-### Claim Tokens
-
-Recipients can claim their tokens using the private keys:
-
-
-
-
- ```bash
- # Claim to an existing account
- near call drop-contract.testnet claim_for '{"account_id": "recipient.testnet"}' \
- --accountId drop-contract.testnet \
- --keyPair '{"public_key": "ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q", "private_key": "ed25519:..."}'
- ```
-
-
-
-
- ```bash
- # Claim to an existing account
- near contract call-function as-transaction drop-contract.testnet claim_for json-args '{"account_id": "recipient.testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q --signer-private-key ed25519:... send
- ```
-
-
-
---
-## Adding View Methods
+## Helper Functions
-Let's add some helpful view methods to query drop information:
+Add some useful view functions:
```rust
#[near_bindgen]
impl Contract {
- /// Get drop information by ID
+ /// Get drop information
pub fn get_drop(&self, drop_id: u64) -> Option {
self.drop_by_id.get(&drop_id)
}
- /// Get drop ID by public key
- pub fn get_drop_id_by_key(&self, public_key: PublicKey) -> Option {
+ /// Check what drop a key can claim
+ pub fn get_drop_for_key(&self, public_key: PublicKey) -> Option {
self.drop_id_by_key.get(&public_key)
}
- /// Get the total number of drops created
- pub fn get_next_drop_id(&self) -> u64 {
- self.next_drop_id
- }
-
- /// Calculate the cost of creating a NEAR drop (view method)
- pub fn calculate_near_drop_cost_view(
- &self,
- num_keys: u64,
- amount_per_drop: NearToken
- ) -> NearToken {
- self.calculate_near_drop_cost(num_keys, amount_per_drop)
+ /// Calculate cost for creating a NEAR drop
+ pub fn estimate_near_drop_cost(&self, num_keys: u64, amount_per_drop: NearToken) -> NearToken {
+ let storage_cost = DROP_STORAGE_COST + KEY_STORAGE_COST * num_keys;
+ let token_cost = amount_per_drop * num_keys;
+ let gas_cost = ACCESS_KEY_ALLOWANCE * num_keys;
+ storage_cost + token_cost + gas_cost
}
}
```
---
-## Error Handling and Validation
+## Build and Test
-Add proper error handling throughout your contract:
+```bash
+# Build the contract
+cargo near build
-```rust
-// Add these error messages as constants
-const ERR_NO_DROP_FOUND: &str = "No drop found for this key";
-const ERR_DROP_NOT_FOUND: &str = "Drop not found";
-const ERR_ALL_CLAIMED: &str = "All drops have been claimed";
-const ERR_INSUFFICIENT_DEPOSIT: &str = "Insufficient deposit";
-const ERR_INVALID_ACCOUNT: &str = "Invalid account format";
-
-// Update your claiming function with better error handling
-fn internal_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
- let drop_id = self.drop_id_by_key.get(public_key)
- .unwrap_or_else(|| env::panic_str(ERR_NO_DROP_FOUND));
-
- let mut drop = self.drop_by_id.get(&drop_id)
- .unwrap_or_else(|| env::panic_str(ERR_DROP_NOT_FOUND));
-
- assert!(drop.get_counter() > 0, "{}", ERR_ALL_CLAIMED);
-
- // Rest of implementation...
-}
+# Create test account
+near create-account drop-test.testnet --useFaucet
+
+# Deploy
+near deploy drop-test.testnet target/near/near_drop.wasm
+
+# Initialize
+near call drop-test.testnet new '{"top_level_account": "testnet"}' --accountId drop-test.testnet
```
---
-## Key Takeaways
+## Create Your First Drop
+
+```bash
+# Create a drop with 2 NEAR per claim for 2 recipients
+near call drop-test.testnet create_near_drop '{
+ "public_keys": [
+ "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8",
+ "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"
+ ],
+ "amount_per_drop": "2000000000000000000000000"
+}' --accountId drop-test.testnet --deposit 5
+```
+
+---
-In this section, you've learned:
+## Claim Tokens
-1. **NEAR Drop Basics**: How to create and manage native NEAR token distributions
-2. **Storage Management**: How to calculate and handle storage costs for drops
-3. **Access Keys**: Using function-call keys to enable gasless claiming
-4. **Account Creation**: Allowing new users to create NEAR accounts when claiming
-5. **Security**: Proper validation and error handling for safe operations
+Recipients can now claim using their private keys:
-The NEAR token drop implementation provides the foundation for more complex drop types. The pattern of creating drops, managing access keys, and handling claims will be consistent across all drop types.
+```bash
+# Claim to existing account
+near call drop-test.testnet claim_for '{"account_id": "alice.testnet"}' \
+ --accountId drop-test.testnet \
+ --keyPair
+
+# Or create new account and claim
+near call drop-test.testnet create_account_and_claim '{"account_id": "bob-new.testnet"}' \
+ --accountId drop-test.testnet \
+ --keyPair
+```
---
-## Next Steps
+## What You've Built
+
+Congratulations! You now have:
+
+✅ **NEAR token distribution system**
+✅ **Gasless claiming** with function-call keys
+✅ **Account creation** for new users
+✅ **Automatic cleanup** after claims
+✅ **Cost estimation** for creating drops
-Now that you have a working NEAR token drop system, let's extend it to support fungible token (FT) drops, which will introduce cross-contract calls and additional complexity.
+The foundation is solid. Next, let's add support for fungible tokens, which involves cross-contract calls and is a bit more complex.
-[Continue to Fungible Token Drops →](./ft-drops)
+[Continue to Fungible Token Drops →](./ft-drops.md)
---
-:::note Testing Tips
-- Test with small amounts first to verify functionality
-- Use testnet for all development and testing
-- Keep track of your private keys securely during testing
-- Monitor gas usage to optimize costs
+:::tip Quick Test
+Try creating a small drop and claiming it yourself to make sure everything works before moving on!
:::
\ No newline at end of file
diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md
index 5a9ce29ae8f..f0013deefd3 100644
--- a/docs/tutorials/neardrop/nft-drops.md
+++ b/docs/tutorials/neardrop/nft-drops.md
@@ -1,68 +1,44 @@
---
id: nft-drops
-title: Non-Fungible Token Drops
+title: NFT Drops
sidebar_label: NFT Drops
-description: "Learn how to implement NFT drops using NEP-171 standard tokens. This section covers unique token distribution, cross-contract NFT transfers, and ownership management patterns."
+description: "Distribute unique NFTs with one-time claims and ownership verification."
---
-import {Github} from "@site/src/components/codetabs"
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-NFT drops represent the most unique form of token distribution in the NEAR Drop system. Unlike NEAR or FT drops where multiple recipients can receive the same amount, NFT drops distribute unique, one-of-a-kind tokens. This creates interesting patterns around scarcity, ownership, and distribution mechanics.
+NFT drops are special because each NFT is unique. Unlike NEAR or FT drops where multiple people can get the same amount, each NFT can only be claimed once.
---
-## Understanding NFT Drop Requirements
-
-NFT drops introduce several unique considerations:
+## What Makes NFT Drops Different
-1. **Uniqueness**: Each NFT can only be claimed once
-2. **Cross-Contract Transfers**: We need to interact with NEP-171 NFT contracts
-3. **Ownership Verification**: Ensuring the drop contract owns the NFTs before distribution
-4. **Metadata Preservation**: Maintaining all NFT properties during transfer
-5. **Single-Use Keys**: Each access key can only claim one specific NFT
+- **One NFT = One Key**: Each NFT gets exactly one private key
+- **Ownership Matters**: The contract must own the NFT before creating the drop
+- **No Duplicates**: Once claimed, that specific NFT is gone forever
---
-## Extending Drop Types for NFTs
+## Add NFT Support
-First, let's extend our drop types to include NFTs. Update `src/drop_types.rs`:
+First, extend your drop types in `src/drop_types.rs`:
```rust
-use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}};
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub enum Drop {
Near(NearDrop),
FungibleToken(FtDrop),
- NonFungibleToken(NftDrop),
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
-pub struct NearDrop {
- pub amount: NearToken,
- pub counter: u64,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
-pub struct FtDrop {
- pub ft_contract: AccountId,
- pub amount: String,
- pub counter: u64,
+ NonFungibleToken(NftDrop), // New!
}
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(crate = "near_sdk::serde")]
+#[derive(BorshDeserialize, BorshSerialize, Clone)]
pub struct NftDrop {
pub nft_contract: AccountId,
pub token_id: String,
- pub counter: u64, // Should always be 1 for NFTs
+ pub counter: u64, // Always 1 for NFTs
}
+```
+Update the helper methods:
+```rust
impl Drop {
pub fn get_counter(&self) -> u64 {
match self {
@@ -74,21 +50,9 @@ impl Drop {
pub fn decrement_counter(&mut self) {
match self {
- Drop::Near(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
- Drop::FungibleToken(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
- Drop::NonFungibleToken(drop) => {
- if drop.counter > 0 {
- drop.counter -= 1;
- }
- }
+ Drop::Near(drop) => drop.counter -= 1,
+ Drop::FungibleToken(drop) => drop.counter -= 1,
+ Drop::NonFungibleToken(drop) => drop.counter -= 1,
}
}
}
@@ -96,68 +60,32 @@ impl Drop {
---
-## Cross-Contract NFT Interface
+## NFT Cross-Contract Interface
-Update `src/external.rs` to include NFT contract methods:
+Add NFT methods to `src/external.rs`:
```rust
-use near_sdk::{ext_contract, AccountId, Gas, json_types::U128};
-
-// Existing FT interface...
-
-// Interface for NEP-171 non-fungible token contracts
+// Interface for NEP-171 NFT contracts
#[ext_contract(ext_nft)]
pub trait NonFungibleToken {
fn nft_transfer(
&mut self,
receiver_id: AccountId,
token_id: String,
- approval_id: Option,
memo: Option,
);
fn nft_token(&self, token_id: String) -> Option;
}
-// Interface for NFT callbacks to this contract
-#[ext_contract(ext_nft_self)]
-pub trait NftDropCallbacks {
- fn nft_transfer_callback(
- &mut self,
- public_key: near_sdk::PublicKey,
- receiver_id: AccountId,
- nft_contract: AccountId,
- token_id: String,
- );
-}
-
#[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct JsonToken {
pub token_id: String,
pub owner_id: AccountId,
- pub metadata: Option,
- pub approved_account_ids: Option>,
-}
-
-#[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)]
-#[serde(crate = "near_sdk::serde")]
-pub struct TokenMetadata {
- pub title: Option,
- pub description: Option,
- pub media: Option,
- pub media_hash: Option,
- pub copies: Option,
- pub issued_at: Option,
- pub expires_at: Option,
- pub starts_at: Option,
- pub updated_at: Option,
- pub extra: Option,
- pub reference: Option,
- pub reference_hash: Option,
}
-// Gas constants for NFT operations
+// Gas for NFT operations
pub const GAS_FOR_NFT_TRANSFER: Gas = Gas(30_000_000_000_000);
pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000);
```
@@ -166,12 +94,12 @@ pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000);
## Creating NFT Drops
-Add the NFT drop creation function to your main contract in `src/lib.rs`:
+Add this to your main contract:
```rust
#[near_bindgen]
impl Contract {
- /// Create a new NFT drop
+ /// Create an NFT drop (only 1 key since NFTs are unique)
pub fn create_nft_drop(
&mut self,
public_key: PublicKey,
@@ -180,17 +108,11 @@ impl Contract {
) -> u64 {
let deposit = env::attached_deposit();
- // Calculate required deposit (only one key for NFTs)
- let required_deposit = self.calculate_nft_drop_cost();
+ // Calculate cost (only 1 key for NFTs)
+ let cost = DROP_STORAGE_COST + KEY_STORAGE_COST + ACCESS_KEY_ALLOWANCE;
+ assert!(deposit >= cost, "Need {} NEAR for NFT drop", cost.as_near());
- assert!(
- deposit >= required_deposit,
- "Insufficient deposit. Required: {}, Provided: {}",
- required_deposit.as_yoctonear(),
- deposit.as_yoctonear()
- );
-
- // Validate token_id format
+ // Validate token ID
assert!(!token_id.is_empty(), "Token ID cannot be empty");
assert!(token_id.len() <= 64, "Token ID too long");
@@ -201,73 +123,40 @@ impl Contract {
let drop = Drop::NonFungibleToken(NftDrop {
nft_contract: nft_contract.clone(),
token_id: token_id.clone(),
- counter: 1, // NFTs are unique, so counter is always 1
+ counter: 1, // Always 1 for NFTs
});
self.drop_by_id.insert(&drop_id, &drop);
+ self.add_claim_key(&public_key, drop_id);
- // Add access key and map public key to drop ID
- self.add_access_key_for_drop(&public_key);
- self.drop_id_by_key.insert(&public_key, &drop_id);
-
- env::log_str(&format!(
- "Created NFT drop {} for token {} from contract {}",
- drop_id,
- token_id,
- nft_contract
- ));
-
+ env::log_str(&format!("Created NFT drop {} for token {}", drop_id, token_id));
drop_id
}
- /// Calculate the cost of creating an NFT drop
- fn calculate_nft_drop_cost(&self) -> NearToken {
- // NFT drops only support one key per drop since each NFT is unique
- DROP_STORAGE_COST
- .saturating_add(KEY_STORAGE_COST)
- .saturating_add(ACCESS_KEY_STORAGE_COST)
- .saturating_add(FUNCTION_CALL_ALLOWANCE)
- }
-
- /// Create multiple NFT drops at once for different tokens
+ /// Create multiple NFT drops at once
pub fn create_nft_drops_batch(
&mut self,
nft_drops: Vec,
) -> Vec {
let mut drop_ids = Vec::new();
- let total_drops = nft_drops.len();
-
- let deposit = env::attached_deposit();
- let required_deposit = self.calculate_nft_drop_cost()
- .saturating_mul(total_drops as u64);
+ let total_cost = (DROP_STORAGE_COST + KEY_STORAGE_COST + ACCESS_KEY_ALLOWANCE)
+ * nft_drops.len() as u64;
- assert!(
- deposit >= required_deposit,
- "Insufficient deposit for {} NFT drops. Required: {}, Provided: {}",
- total_drops,
- required_deposit.as_yoctonear(),
- deposit.as_yoctonear()
- );
+ assert!(env::attached_deposit() >= total_cost, "Insufficient deposit for batch");
- for nft_drop in nft_drops {
- let drop_id = self.create_single_nft_drop_internal(
- nft_drop.public_key,
- nft_drop.nft_contract,
- nft_drop.token_id,
+ for config in nft_drops {
+ let drop_id = self.create_single_nft_drop(
+ config.public_key,
+ config.nft_contract,
+ config.token_id,
);
drop_ids.push(drop_id);
}
- env::log_str(&format!(
- "Created {} NFT drops in batch",
- total_drops
- ));
-
drop_ids
}
- /// Internal method for creating a single NFT drop without deposit checks
- fn create_single_nft_drop_internal(
+ fn create_single_nft_drop(
&mut self,
public_key: PublicKey,
nft_contract: AccountId,
@@ -277,15 +166,11 @@ impl Contract {
self.next_drop_id += 1;
let drop = Drop::NonFungibleToken(NftDrop {
- nft_contract,
- token_id,
- counter: 1,
+ nft_contract, token_id, counter: 1,
});
self.drop_by_id.insert(&drop_id, &drop);
- self.add_access_key_for_drop(&public_key);
- self.drop_id_by_key.insert(&public_key, &drop_id);
-
+ self.add_claim_key(&public_key, drop_id);
drop_id
}
}
@@ -301,254 +186,87 @@ pub struct NftDropConfig {
---
-## Implementing NFT Claiming Logic
+## NFT Claiming Logic
-Update your `src/claim.rs` file to handle NFT claims:
+Update your claiming logic in `src/claim.rs`:
```rust
-use crate::external::*;
-use near_sdk::Promise;
-
-#[near_bindgen]
impl Contract {
- /// Internal claiming logic (updated to handle NFT drops)
- fn internal_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
+ fn process_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) {
let drop_id = self.drop_id_by_key.get(public_key)
.expect("No drop found for this key");
let mut drop = self.drop_by_id.get(&drop_id)
- .expect("Drop not found");
+ .expect("Drop data not found");
- assert!(drop.get_counter() > 0, "All drops have been claimed");
+ assert!(drop.get_counter() > 0, "Drop already claimed");
match &drop {
Drop::Near(near_drop) => {
- // Handle NEAR token drops (as before)
- Promise::new(receiver_id.clone())
- .transfer(near_drop.amount);
-
- env::log_str(&format!(
- "Claimed {} NEAR tokens to {}",
- near_drop.amount.as_yoctonear(),
- receiver_id
- ));
-
- self.cleanup_after_claim(public_key, &mut drop, drop_id);
+ Promise::new(receiver_id.clone()).transfer(near_drop.amount);
+ self.cleanup_claim(public_key, &mut drop, drop_id);
}
Drop::FungibleToken(ft_drop) => {
- // Handle FT drops (as before)
- self.claim_ft_drop(
- public_key.clone(),
- receiver_id.clone(),
- ft_drop.ft_contract.clone(),
- ft_drop.amount.clone(),
- );
- return;
+ self.claim_ft_tokens(/* ... */);
}
Drop::NonFungibleToken(nft_drop) => {
- // Handle NFT drops with cross-contract calls
- self.claim_nft_drop(
+ // Transfer NFT with cross-contract call
+ self.claim_nft(
public_key.clone(),
receiver_id.clone(),
nft_drop.nft_contract.clone(),
nft_drop.token_id.clone(),
);
- return;
}
}
}
- /// Claim NFT with proper ownership verification
- fn claim_nft_drop(
+ /// Claim NFT with cross-contract call
+ fn claim_nft(
&mut self,
public_key: PublicKey,
receiver_id: AccountId,
nft_contract: AccountId,
token_id: String,
) {
- // Transfer the NFT to the receiver
ext_nft::ext(nft_contract.clone())
.with_static_gas(GAS_FOR_NFT_TRANSFER)
.nft_transfer(
receiver_id.clone(),
token_id.clone(),
- None, // approval_id
- Some(format!("NEAR Drop claim to {}", receiver_id))
+ Some("NEAR Drop claim".to_string()),
)
.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_NFT_CALLBACK)
- .nft_transfer_callback(
- public_key,
- receiver_id,
- nft_contract,
- token_id,
- )
+ .nft_transfer_callback(public_key, receiver_id, token_id)
);
}
- /// Handle the result of NFT transfer
+ /// Handle NFT transfer result
#[private]
pub fn nft_transfer_callback(
&mut self,
public_key: PublicKey,
receiver_id: AccountId,
- nft_contract: AccountId,
token_id: String,
) {
- if is_promise_success() {
- env::log_str(&format!(
- "Successfully transferred NFT {} from {} to {}",
- token_id,
- nft_contract,
- receiver_id
- ));
-
- // Get drop info for cleanup
- let drop_id = self.drop_id_by_key.get(&public_key)
- .expect("Drop not found during cleanup");
-
- let mut drop = self.drop_by_id.get(&drop_id)
- .expect("Drop data not found during cleanup");
-
- // Clean up after successful transfer
- self.cleanup_after_claim(&public_key, &mut drop, drop_id);
- } else {
- env::log_str(&format!(
- "Failed to transfer NFT {} from {} to {}",
- token_id,
- nft_contract,
- receiver_id
- ));
-
- // NFT transfer failed - this could happen if:
- // 1. The drop contract doesn't own the NFT
- // 2. The NFT contract has some issue
- // 3. The token doesn't exist
- env::panic_str("NFT transfer failed");
- }
- }
-
- /// Verify NFT ownership before creating drop (utility method)
- pub fn verify_nft_ownership(
- &self,
- nft_contract: AccountId,
- token_id: String,
- ) -> Promise {
- ext_nft::ext(nft_contract)
- .with_static_gas(Gas(10_000_000_000_000))
- .nft_token(token_id)
- }
-}
-```
-
----
-
-## NFT Drop Security Considerations
-
-NFT drops require additional security considerations:
-
-### Ownership Verification
-
-Before creating NFT drops, it's crucial to verify that the contract owns the NFTs:
-
-```rust
-#[near_bindgen]
-impl Contract {
- /// Verify and create NFT drop with ownership check
- pub fn create_nft_drop_with_verification(
- &mut self,
- public_key: PublicKey,
- nft_contract: AccountId,
- token_id: String,
- ) -> Promise {
- // First verify ownership
- ext_nft::ext(nft_contract.clone())
- .with_static_gas(Gas(10_000_000_000_000))
- .nft_token(token_id.clone())
- .then(
- Self::ext(env::current_account_id())
- .with_static_gas(Gas(30_000_000_000_000))
- .handle_nft_verification(
- public_key,
- nft_contract,
- token_id,
- env::attached_deposit(),
- )
- )
- }
-
- /// Handle NFT ownership verification result
- #[private]
- pub fn handle_nft_verification(
- &mut self,
- public_key: PublicKey,
- nft_contract: AccountId,
- token_id: String,
- deposit: NearToken,
- ) -> u64 {
- if let PromiseResult::Successful(val) = env::promise_result(0) {
- if let Ok(Some(token_info)) = near_sdk::serde_json::from_slice::