From 66e139399fb2caa700fef28b8b1e3450e0a820a9 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:12:08 +0100 Subject: [PATCH 01/23] Add files via upload --- .../neardrop/contract-architecture.md | 232 ++++++ docs/tutorials/neardrop/ft-drops.md | 679 ++++++++++++++++++ docs/tutorials/neardrop/introduction.md | 133 ++++ docs/tutorials/neardrop/near-drops.md | 495 +++++++++++++ 4 files changed, 1539 insertions(+) create mode 100644 docs/tutorials/neardrop/contract-architecture.md create mode 100644 docs/tutorials/neardrop/ft-drops.md create mode 100644 docs/tutorials/neardrop/introduction.md create mode 100644 docs/tutorials/neardrop/near-drops.md diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md new file mode 100644 index 00000000000..04b0faeb1c1 --- /dev/null +++ b/docs/tutorials/neardrop/contract-architecture.md @@ -0,0 +1,232 @@ +--- +id: contract-architecture +title: Contract Architecture +sidebar_label: Contract Architecture +description: "Understand the NEAR Drop smart contract structure, including the core data types, storage patterns, and how different drop types are organized and managed." +--- + +import {Github} from "@site/src/components/codetabs" + +Before diving into implementation, let's understand the architecture of the NEAR Drop smart contract. This foundation will help you understand how the different components work together to create a seamless token distribution system. + +--- + +## Core Contract Structure + +The NEAR Drop contract is organized around several key concepts that work together to manage token distributions efficiently. + +### Main Contract State + +The contract's state is designed to handle multiple types of drops while maintaining efficient storage and lookup patterns: + + + +Let's break down each field: + +- **`top_level_account`**: The account used to create new NEAR accounts (typically `testnet` or `mainnet`) +- **`next_drop_id`**: A simple counter that assigns unique identifiers to each drop +- **`drop_id_by_key`**: Maps public keys to their corresponding drop IDs for efficient lookups +- **`drop_by_id`**: Stores the actual drop data indexed by drop ID + +:::info Storage Efficiency +This dual-mapping approach allows for efficient lookups both by public key (when claiming) and by drop ID (when managing drops), while keeping storage costs reasonable. +::: + +--- + +## Drop Types + +The contract supports three different types of token drops, each represented as an enum variant: + + + +### NEAR Token Drops + +The simplest drop type distributes native NEAR tokens: + + + +Key characteristics: +- **`amount`**: Amount of NEAR tokens to distribute per claim +- **Simple Transfer**: Uses native NEAR transfer functionality +- **No External Dependencies**: Works without additional contracts + +### Fungible Token Drops + +For distributing NEP-141 compatible fungible tokens: + + + +Key characteristics: +- **`ft_contract`**: The contract address of the fungible token +- **`amount`**: Number of tokens to distribute per claim +- **Cross-Contract Calls**: Requires interaction with FT contract +- **Storage Registration**: Recipients must be registered on the FT contract + +### Non-Fungible Token Drops + +For distributing unique NFTs: + + + +Key characteristics: +- **`nft_contract`**: The contract address of the NFT collection +- **`token_id`**: Specific NFT token being distributed +- **Unique Distribution**: Each NFT can only be claimed once +- **Metadata Preservation**: Maintains all NFT properties and metadata + +--- + +## Access Key System + +One of NEAR Drop's most powerful features is its use of function-call access keys to enable gasless claiming. + +### How It Works + +1. **Key Generation**: When creating a drop, the contract generates or accepts public keys +2. **Access Key Addition**: The contract adds these keys as function-call keys to itself +3. **Limited Permissions**: Keys can only call specific claiming functions +4. **Gasless Operations**: Recipients don't need NEAR tokens to claim + +### Key Permissions + +The function-call access keys are configured with specific permissions: + +```rust +// Example of adding a function-call access key +Promise::new(env::current_account_id()) + .add_access_key( + public_key.clone(), + FUNCTION_CALL_ALLOWANCE, + env::current_account_id(), + "claim_for,create_account_and_claim".to_string(), + ) +``` + +This setup allows keys to: +- Call `claim_for` to claim drops to existing accounts +- Call `create_account_and_claim` to create new accounts and claim drops +- Nothing else - providing security through limited permissions + +--- + +## Storage Cost Management + +The contract carefully manages storage costs, which are paid by the drop creator: + +### Cost Components + +1. **Drop Data Storage**: Storing drop information in the contract state +2. **Key-to-Drop Mapping**: Mapping public keys to drop IDs +3. **Access Key Storage**: Adding function-call keys to the contract account + +### Storage Calculation Example + +```rust +// Simplified storage cost calculation +fn calculate_storage_cost(&self, num_keys: u64) -> NearToken { + let drop_storage = DROP_STORAGE_COST; + let key_storage = num_keys * KEY_STORAGE_COST; + let access_key_storage = num_keys * ACCESS_KEY_STORAGE_COST; + + drop_storage + key_storage + access_key_storage +} +``` + +:::tip Storage Optimization +The contract uses efficient data structures and minimal storage patterns to keep costs low while maintaining functionality. +::: + +--- + +## Security Model + +The NEAR Drop contract implements several security measures: + +### Access Control + +- **Drop Creation**: Only the drop creator can modify their drops +- **Function-Call Keys**: Limited to specific claiming functions only +- **Account Validation**: Ensures only valid NEAR accounts can be created + +### Preventing Abuse + +- **One-Time Claims**: Each key can only be used once per drop +- **Key Cleanup**: Used keys are removed to prevent reuse +- **Counter Management**: Drop counters prevent double-claiming + +### Error Handling + +```rust +// Example error handling pattern +if self.drop_id_by_key.get(&public_key).is_none() { + env::panic_str("No drop found for this key"); +} + +if drop.counter == 0 { + env::panic_str("All drops have been claimed"); +} +``` + +--- + +## Cross-Contract Integration + +The contract is designed to work seamlessly with other NEAR standards: + +### Fungible Token Integration + +- Implements NEP-141 interaction patterns +- Handles storage registration automatically +- Manages transfer and callback flows + +### NFT Integration + +- Supports NEP-171 NFT transfers +- Preserves token metadata and properties +- Handles ownership transfers correctly + +### Account Creation Integration + +- Works with the linkdrop contract pattern +- Handles account funding and key management +- Supports both testnet and mainnet account creation + +--- + +## File Organization + +The contract code is organized into logical modules: + +``` +src/ +├── lib.rs # Main contract logic and state +├── drop_types.rs # Drop type definitions +├── near_drop.rs # NEAR token drop implementation +├── ft_drop.rs # Fungible token drop implementation +├── nft_drop.rs # NFT drop implementation +└── claim.rs # Claiming logic for all drop types +``` + +This modular structure makes the code: +- **Easy to Understand**: Each file has a clear purpose +- **Maintainable**: Changes to one drop type don't affect others +- **Extensible**: New drop types can be added easily + +--- + +## Next Steps + +Now that you understand the contract architecture, let's start implementing the core functionality, beginning with NEAR token drops. + +[Continue to NEAR Token Drops →](./near-drops) + +--- + +:::note Key Takeaways +- The contract uses efficient storage patterns with dual mappings +- Three drop types support different token standards (NEAR, FT, NFT) +- Function-call access keys enable gasless claiming operations +- Security is maintained through limited key permissions and proper validation +- Modular architecture makes the contract maintainable and extensible +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/ft-drops.md b/docs/tutorials/neardrop/ft-drops.md new file mode 100644 index 00000000000..d234916acde --- /dev/null +++ b/docs/tutorials/neardrop/ft-drops.md @@ -0,0 +1,679 @@ +--- +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." +--- + +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. + +--- + +## Understanding FT Drop Requirements + +Fungible token drops involve several additional considerations: + +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 + +--- + +## Extending the Drop Types + +First, let's extend our drop types to include fungible tokens. Update `src/drop_types.rs`: + +```rust +use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}}; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum Drop { + Near(NearDrop), + FungibleToken(FtDrop), +} + +#[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, // Using String to handle large numbers + pub counter: u64, +} + +impl Drop { + pub fn get_counter(&self) -> u64 { + match self { + Drop::Near(drop) => drop.counter, + Drop::FungibleToken(drop) => drop.counter, + } + } + + 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; + } + } + } + } +} +``` + +--- + +## Cross-Contract Interface + +Create `src/external.rs` to define the interface for interacting with FT contracts: + +```rust +use near_sdk::{ext_contract, AccountId, Gas}; + +// 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; +} + +// Interface for callbacks to this 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, +} + +// Gas constants for cross-contract calls +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 +``` + +--- + +## Creating FT Drops + +Add the FT drop creation function to your main contract in `src/lib.rs`: + +```rust +#[near_bindgen] +impl Contract { + /// Create a new 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; + + // 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 + amount_per_drop.parse::() + .expect("Invalid amount format"); + + // Create the drop + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + let drop = Drop::FungibleToken(FtDrop { + ft_contract: ft_contract.clone(), + amount: amount_per_drop.clone(), + counter: num_keys, + }); + + self.drop_by_id.insert(&drop_id, &drop); + + // Add access keys and map public keys to drop ID + for public_key in public_keys { + self.add_access_key_for_drop(&public_key); + self.drop_id_by_key.insert(&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 + )); + + 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 + +Update your `src/claim.rs` file: + +```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) { + 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"); + + assert!(drop.get_counter() > 0, "All drops have been 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 + )); + + // Clean up immediately for NEAR drops + self.cleanup_after_claim(public_key, &mut drop, drop_id); + } + Drop::FungibleToken(ft_drop) => { + // Handle FT drops with cross-contract calls + self.claim_ft_drop( + 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( + &mut self, + public_key: PublicKey, + receiver_id: AccountId, + ft_contract: AccountId, + amount: String, + ) { + // First, check if the receiver is registered on the FT contract + ext_ft::ext(ft_contract.clone()) + .with_static_gas(GAS_FOR_STORAGE_DEPOSIT) + .storage_balance_of(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, + ) + ); + } + + /// 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 + #[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( + &mut self, + public_key: PublicKey, + receiver_id: AccountId, + ft_contract: AccountId, + amount: String, + ) { + 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)) + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_CALLBACK) + .ft_transfer_callback( + public_key, + receiver_id, + ft_contract, + amount, + ) + ); + } + + /// Handle the result of FT transfer + #[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"); + + // Clean up after successful transfer + self.cleanup_after_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 + 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); + } + + // 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()); + } +} + +/// 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): + +```bash +# Clone and build the FT contract +git clone https://github.com/near-examples/FT.git +cd FT +cargo near build + +# Deploy to testnet +near create-account test-ft.testnet --useFaucet +near deploy test-ft.testnet target/near/fungible_token.wasm + +# Initialize with your drop contract as owner +near call test-ft.testnet new_default_meta '{ + "owner_id": "drop-contract.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 + ``` + + + +--- + +## Adding View Methods for FT Drops + +Add these helpful view methods to query FT drop information: + +```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) + } + + /// Get FT drop details + pub fn get_ft_drop_details(&self, drop_id: u64) -> Option { + 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, + }) + } 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 + +Add specific error handling for FT operations: + +```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... +} +``` + +--- + +## 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 +``` + +--- + +## Next Steps + +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 + +Next, let's implement NFT drops, which introduce unique token distribution patterns. + +[Continue to NFT Drops →](./nft-drops) + +--- + +:::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 +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/introduction.md b/docs/tutorials/neardrop/introduction.md new file mode 100644 index 00000000000..37f94f96640 --- /dev/null +++ b/docs/tutorials/neardrop/introduction.md @@ -0,0 +1,133 @@ +--- +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." +--- + +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. + +--- + +## 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 Flow](/docs/assets/tutorials/near-drop/near-drop-flow.png) + +--- + +## 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 + +### Key Benefits + +- **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 + +--- + +## Tutorial Overview + +This tutorial is divided into several sections that build upon each other: + +| 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 | + +--- + +## Real-World Use Cases + +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 + +--- + +## What Makes This Tutorial Special + +This tutorial showcases several advanced NEAR concepts: + +- **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 + +--- + +## Example Scenario + +Throughout this tutorial, we'll use a practical example: **"NEAR Community Airdrop"** + +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! + +--- + +## Next Steps + +Ready to start building? Let's begin with understanding the contract architecture and core concepts. + +[Continue to Contract Architecture →](./contract-architecture) + +--- + +:::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` +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/near-drops.md b/docs/tutorials/neardrop/near-drops.md new file mode 100644 index 00000000000..63dd72d83ed --- /dev/null +++ b/docs/tutorials/neardrop/near-drops.md @@ -0,0 +1,495 @@ +--- +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." +--- + +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. + +--- + +## Setting Up the Project + +First, let's create a new Rust project and set up the basic structure: + +```bash +cargo near new near-drop --contract +cd near-drop +``` + +Add the necessary dependencies to your `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 + +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: + +```rust +use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}}; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum Drop { + Near(NearDrop), + // We'll add FT and NFT variants later +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct NearDrop { + pub amount: NearToken, + pub counter: u64, +} + +impl Drop { + pub fn get_counter(&self) -> u64 { + match self { + Drop::Near(drop) => drop.counter, + } + } + + pub fn decrement_counter(&mut self) { + match self { + Drop::Near(drop) => { + if drop.counter > 0 { + drop.counter -= 1; + } + } + } + } +} +``` + +### Creating NEAR Drops + +Now let's implement the function to create NEAR token drops. Add this to your `src/lib.rs`: + +```rust +use near_sdk::{ + env, near_bindgen, AccountId, NearToken, Promise, PublicKey, + collections::{LookupMap, UnorderedMap}, + BorshDeserialize, BorshSerialize, +}; + +// 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 + +#[near_bindgen] +impl Contract { + /// Create a new NEAR token drop + 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; + + // Calculate required deposit + let required_deposit = self.calculate_near_drop_cost(num_keys, amount_per_drop); + + assert!( + deposit >= required_deposit, + "Insufficient deposit. Required: {}, Provided: {}", + required_deposit.as_yoctonear(), + deposit.as_yoctonear() + ); + + // Create the drop + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + let drop = Drop::Near(NearDrop { + amount: amount_per_drop, + counter: num_keys, + }); + + self.drop_by_id.insert(&drop_id, &drop); + + // Add access keys and map public keys to drop ID + for public_key in public_keys { + self.add_access_key_for_drop(&public_key); + self.drop_id_by_key.insert(&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 + )); + + 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)); + + 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) { + Promise::new(env::current_account_id()) + .add_access_key( + public_key.clone(), + FUNCTION_CALL_ALLOWANCE, + env::current_account_id(), + "claim_for,create_account_and_claim".to_string(), + ); + } +} +``` + +--- + +## Claiming NEAR Tokens + +Now let's implement the claiming functionality. Create `src/claim.rs`: + +```rust +use crate::*; + +#[near_bindgen] +impl Contract { + /// Claim a drop 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); + } + + /// Create a new account and claim drop 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 + ); + + // Create the account first + let create_promise = 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) + ) + } + + /// Resolve account creation and claim drop + #[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"); + } + } + + /// Internal claiming logic + fn internal_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) { + // Get the drop ID from the public key + 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"); + + // Check if drop is still claimable + assert!(drop.get_counter() > 0, "All drops have been claimed"); + + // 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 + )); + } + } + + // Decrement counter and update drop + drop.decrement_counter(); + + 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); + } + + // Remove the public key mapping and access key + self.drop_id_by_key.remove(public_key); + + 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(_)) +} +``` + +--- + +## 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 + +Let's add some helpful view methods to query drop information: + +```rust +#[near_bindgen] +impl Contract { + /// Get drop information by ID + 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 { + 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) + } +} +``` + +--- + +## Error Handling and Validation + +Add proper error handling throughout your contract: + +```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... +} +``` + +--- + +## Key Takeaways + +In this section, you've learned: + +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 + +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. + +--- + +## Next Steps + +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. + +[Continue to Fungible Token Drops →](./ft-drops) + +--- + +:::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 +::: \ No newline at end of file From b49061e8e2de455b6da0ab2d8978e2309db25d30 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:27:57 +0100 Subject: [PATCH 02/23] Nft drop smart contract --- docs/tutorials/neardrop/nft-drops.md | 880 +++++++++++++++++++++++++++ 1 file changed, 880 insertions(+) create mode 100644 docs/tutorials/neardrop/nft-drops.md diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md new file mode 100644 index 00000000000..7d85fbea0b3 --- /dev/null +++ b/docs/tutorials/neardrop/nft-drops.md @@ -0,0 +1,880 @@ +--- +id: nft-drops +title: Non-Fungible Token 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." +--- + +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. + +--- + +## Understanding NFT Drop Requirements + +NFT drops introduce several unique considerations: + +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 + +--- + +## Extending Drop Types for NFTs + +First, let's extend our drop types to include NFTs. Update `src/drop_types.rs`: + +```rust +use near_sdk::{AccountId, NearToken, serde::{Deserialize, Serialize}}; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +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, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct NftDrop { + pub nft_contract: AccountId, + pub token_id: String, + pub counter: u64, // Should always be 1 for NFTs +} + +impl Drop { + pub fn get_counter(&self) -> u64 { + match self { + Drop::Near(drop) => drop.counter, + Drop::FungibleToken(drop) => drop.counter, + Drop::NonFungibleToken(drop) => drop.counter, + } + } + + 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; + } + } + } + } +} +``` + +--- + +## Cross-Contract NFT Interface + +Update `src/external.rs` to include NFT contract methods: + +```rust +use near_sdk::{ext_contract, AccountId, Gas, json_types::U128}; + +// Existing FT interface... + +// Interface for NEP-171 non-fungible token 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 +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); +``` + +--- + +## Creating NFT Drops + +Add the NFT drop creation function to your main contract in `src/lib.rs`: + +```rust +#[near_bindgen] +impl Contract { + /// Create a new NFT drop + pub fn create_nft_drop( + &mut self, + public_key: PublicKey, + nft_contract: AccountId, + token_id: String, + ) -> u64 { + let deposit = env::attached_deposit(); + + // Calculate required deposit (only one key for NFTs) + let required_deposit = self.calculate_nft_drop_cost(); + + assert!( + deposit >= required_deposit, + "Insufficient deposit. Required: {}, Provided: {}", + required_deposit.as_yoctonear(), + deposit.as_yoctonear() + ); + + // Validate token_id format + assert!(!token_id.is_empty(), "Token ID cannot be empty"); + assert!(token_id.len() <= 64, "Token ID too long"); + + // Create the drop + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + 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 + }); + + self.drop_by_id.insert(&drop_id, &drop); + + // 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 + )); + + 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 + 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); + + assert!( + deposit >= required_deposit, + "Insufficient deposit for {} NFT drops. Required: {}, Provided: {}", + total_drops, + required_deposit.as_yoctonear(), + deposit.as_yoctonear() + ); + + 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, + ); + 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( + &mut self, + public_key: PublicKey, + nft_contract: AccountId, + token_id: String, + ) -> u64 { + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + let drop = Drop::NonFungibleToken(NftDrop { + 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); + + drop_id + } +} + +#[derive(near_sdk::serde::Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct NftDropConfig { + pub public_key: PublicKey, + pub nft_contract: AccountId, + pub token_id: String, +} +``` + +--- + +## Implementing NFT Claiming Logic + +Update your `src/claim.rs` file to handle NFT claims: + +```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) { + 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"); + + assert!(drop.get_counter() > 0, "All drops have been 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); + } + 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; + } + Drop::NonFungibleToken(nft_drop) => { + // Handle NFT drops with cross-contract calls + self.claim_nft_drop( + 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( + &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)) + ) + .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, + ) + ); + } + + /// Handle the result of NFT transfer + #[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::>(&val) { + // Verify that this contract owns the NFT + assert_eq!( + token_info.owner_id, + env::current_account_id(), + "Contract does not own NFT {} from {}", + token_id, + nft_contract + ); + + // Create the drop with the provided deposit + let required_deposit = self.calculate_nft_drop_cost(); + assert!( + deposit >= required_deposit, + "Insufficient deposit for NFT drop" + ); + + // Proceed with drop creation + return self.create_single_nft_drop_internal( + public_key, + nft_contract, + token_id, + ); + } + } + + env::panic_str("NFT ownership verification failed"); + } +} +``` + +### Preventing Double Claims + +Since NFTs are unique, we need to ensure they can't be claimed twice: + +```rust +impl Contract { + /// Enhanced cleanup that removes NFT drops completely + fn cleanup_after_claim(&mut self, public_key: &PublicKey, drop: &mut Drop, drop_id: u64) { + match drop { + Drop::NonFungibleToken(_) => { + // For NFTs, always remove the drop completely since they're unique + self.drop_by_id.remove(&drop_id); + env::log_str(&format!("NFT drop {} fully claimed and removed", drop_id)); + } + _ => { + // Handle other drop types as before + drop.decrement_counter(); + + if drop.get_counter() == 0 { + self.drop_by_id.remove(&drop_id); + } else { + self.drop_by_id.insert(&drop_id, &drop); + } + } + } + + // Always remove the public key mapping and access key + self.drop_id_by_key.remove(public_key); + Promise::new(env::current_account_id()) + .delete_key(public_key.clone()); + } +} +``` + +--- + +## Testing NFT Drops + +### Deploy a Test NFT Contract + +You'll need an NFT contract for testing. Use the reference implementation: + +```bash +# Clone and build the NFT contract +git clone https://github.com/near-examples/NFT.git +cd NFT +cargo near build + +# Deploy to testnet +near create-account test-nft.testnet --useFaucet +near deploy test-nft.testnet target/near/non_fungible_token.wasm + +# Initialize +near call test-nft.testnet new_default_meta '{ + "owner_id": "drop-contract.testnet" +}' --accountId test-nft.testnet +``` + +### Mint NFTs to the Drop Contract + +```bash +# Mint NFT to drop contract +near call test-nft.testnet nft_mint '{ + "token_id": "unique-drop-token-001", + "metadata": { + "title": "Exclusive Drop NFT", + "description": "A unique NFT distributed via NEAR Drop", + "media": "https://example.com/nft-image.png" + }, + "receiver_id": "drop-contract.testnet" +}' --accountId drop-contract.testnet --deposit 0.1 +``` + +### Create NFT Drop + + + + + ```bash + # Create an NFT drop + near call drop-contract.testnet create_nft_drop '{ + "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", + "nft_contract": "test-nft.testnet", + "token_id": "unique-drop-token-001" + }' --accountId drop-contract.testnet --deposit 0.1 + ``` + + + + + ```bash + # Create an NFT drop + near contract call-function as-transaction drop-contract.testnet create_nft_drop json-args '{ + "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", + "nft_contract": "test-nft.testnet", + "token_id": "unique-drop-token-001" + }' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send + ``` + + + +### Claim NFT + + + + + ```bash + # Claim NFT to an existing account + near call drop-contract.testnet claim_for '{ + "account_id": "nft-collector.testnet" + }' --accountId drop-contract.testnet \ + --keyPair '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "private_key": "ed25519:..."}' + ``` + + + + + ```bash + # Claim NFT to an existing account + near contract call-function as-transaction drop-contract.testnet claim_for json-args '{ + "account_id": "nft-collector.testnet" + }' prepaid-gas '200.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 + ``` + + + +### Verify NFT Transfer + +```bash +# Check NFT ownership after claim +near view test-nft.testnet nft_token '{ + "token_id": "unique-drop-token-001" +}' +``` + +--- + +## Adding NFT-Specific View Methods + +Add helpful view methods for NFT drops: + +```rust +#[near_bindgen] +impl Contract { + /// Get NFT drop details + pub fn get_nft_drop_details(&self, drop_id: u64) -> Option { + if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { + Some(NftDropInfo { + nft_contract: nft_drop.nft_contract, + token_id: nft_drop.token_id, + is_claimed: nft_drop.counter == 0, + }) + } else { + None + } + } + + /// Calculate NFT drop cost (view method) + pub fn calculate_nft_drop_cost_view(&self) -> NearToken { + self.calculate_nft_drop_cost() + } + + /// Check if an NFT drop exists for a specific token + pub fn nft_drop_exists(&self, nft_contract: AccountId, token_id: String) -> bool { + // This is a linear search - in production you might want to optimize this + for drop_id in 0..self.next_drop_id { + if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { + if nft_drop.nft_contract == nft_contract && nft_drop.token_id == token_id { + return nft_drop.counter > 0; + } + } + } + false + } + + /// Get all NFT drops for a specific contract + pub fn get_nft_drops_by_contract(&self, nft_contract: AccountId) -> Vec { + let mut nft_drops = Vec::new(); + + for drop_id in 0..self.next_drop_id { + if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { + if nft_drop.nft_contract == nft_contract { + nft_drops.push(NftDropInfo { + nft_contract: nft_drop.nft_contract, + token_id: nft_drop.token_id, + is_claimed: nft_drop.counter == 0, + }); + } + } + } + + nft_drops + } +} + +#[derive(near_sdk::serde::Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct NftDropInfo { + pub nft_contract: AccountId, + pub token_id: String, + pub is_claimed: bool, +} +``` + +--- + +## Advanced NFT Drop Patterns + +### Rarity-Based Drops + +You can implement rarity-based NFT drops by analyzing metadata: + +```rust +impl Contract { + /// Create NFT drop with rarity verification + pub fn create_rare_nft_drop( + &mut self, + public_key: PublicKey, + nft_contract: AccountId, + token_id: String, + required_rarity: String, + ) -> Promise { + 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_rarity_verification( + public_key, + nft_contract, + token_id, + required_rarity, + env::attached_deposit(), + ) + ) + } + + /// Handle rarity verification + #[private] + pub fn handle_rarity_verification( + &mut self, + public_key: PublicKey, + nft_contract: AccountId, + token_id: String, + required_rarity: 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::>(&val) { + // Verify ownership + assert_eq!(token_info.owner_id, env::current_account_id()); + + // Check rarity in metadata + if let Some(metadata) = token_info.metadata { + if let Some(extra) = metadata.extra { + let extra_data: serde_json::Value = serde_json::from_str(&extra) + .unwrap_or_else(|_| serde_json::Value::Null); + + if let Some(rarity) = extra_data.get("rarity") { + assert_eq!( + rarity.as_str().unwrap_or(""), + required_rarity, + "NFT rarity does not match requirement" + ); + } else { + env::panic_str("NFT does not have rarity metadata"); + } + } + } + + // Create the drop if all validations pass + return self.create_single_nft_drop_internal( + public_key, + nft_contract, + token_id, + ); + } + } + + env::panic_str("Rarity verification failed"); + } +} +``` + +### Collection-Based Drops + +Create drops for entire NFT collections: + +```rust +impl Contract { + /// Create drops for multiple NFTs from the same collection + pub fn create_collection_drop( + &mut self, + nft_contract: AccountId, + token_ids: Vec, + public_keys: Vec, + ) -> Vec { + assert_eq!( + token_ids.len(), + public_keys.len(), + "Token IDs and public keys arrays must have the same length" + ); + + let total_drops = token_ids.len(); + let deposit = env::attached_deposit(); + let required_deposit = self.calculate_nft_drop_cost() + .saturating_mul(total_drops as u64); + + assert!( + deposit >= required_deposit, + "Insufficient deposit for collection drop" + ); + + let mut drop_ids = Vec::new(); + + for (i, token_id) in token_ids.into_iter().enumerate() { + let drop_id = self.create_single_nft_drop_internal( + public_keys[i].clone(), + nft_contract.clone(), + token_id, + ); + drop_ids.push(drop_id); + } + + env::log_str(&format!( + "Created collection drop with {} NFTs from {}", + total_drops, + nft_contract + )); + + drop_ids + } +} +``` + +--- + +## Error Handling for NFT Operations + +Add comprehensive error handling: + +```rust +// Error constants +const ERR_NFT_NOT_FOUND: &str = "NFT not found"; +const ERR_NFT_NOT_OWNED: &str = "Contract does not own this NFT"; +const ERR_NFT_ALREADY_CLAIMED: &str = "This NFT has already been claimed"; +const ERR_INVALID_TOKEN_ID: &str = "Invalid token ID format"; + +impl Contract { + /// Enhanced NFT drop creation with comprehensive validation + pub fn create_nft_drop_safe( + &mut self, + public_key: PublicKey, + nft_contract: AccountId, + token_id: String, + ) -> u64 { + // Validate inputs + self.validate_nft_drop_inputs(&public_key \ No newline at end of file From 5012b5612b6190e32a9e2407e67348de2454dd38 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:10:08 +0100 Subject: [PATCH 03/23] Complete near drop tutorial --- docs/tutorials/neardrop/access-keys.md | 591 +++ docs/tutorials/neardrop/account-creation.md | 781 ++++ docs/tutorials/neardrop/frontend.md | 3643 +++++++++++++++++++ docs/tutorials/neardrop/nft-drops.md | 151 +- 4 files changed, 5165 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/neardrop/access-keys.md create mode 100644 docs/tutorials/neardrop/account-creation.md create mode 100644 docs/tutorials/neardrop/frontend.md diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md new file mode 100644 index 00000000000..0919150df8a --- /dev/null +++ b/docs/tutorials/neardrop/access-keys.md @@ -0,0 +1,591 @@ +--- +id: access-keys +title: Access Key Management +sidebar_label: Access Key Management +description: "Deep dive into NEAR's function-call access keys and how they enable gasless operations in the NEAR Drop system. Learn key generation, management, and security patterns." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Function-call access keys are one of NEAR's most powerful features, enabling gasless operations and seamless user experiences. In the NEAR Drop system, they're the secret sauce that allows recipients to claim tokens without needing NEAR tokens for gas fees. + +--- + +## Understanding NEAR Access Keys + +NEAR supports two types of access keys: + +1. **Full Access Keys**: Complete control over an account (like a master key) +2. **Function-Call Access Keys**: Limited permissions to call specific methods + +![Access Key Types](/docs/assets/tutorials/near-drop/access-key-types.png) + +### Why Function-Call Keys Are Perfect for Drops + +Function-call keys solve a classic blockchain UX problem: new users need tokens to pay for gas, but they need gas to claim tokens. It's a chicken-and-egg problem that function-call keys elegantly solve. + +--- + +## How Access Keys Work in NEAR Drop + +### The Access Key Lifecycle + +1. **Key Generation**: Create a public/private key pair +2. **Key Addition**: Add the public key to the contract with limited permissions +3. **Key Distribution**: Share the private key with recipients +4. **Key Usage**: Recipients use the key to sign claiming transactions +5. **Key Cleanup**: Remove used keys to prevent reuse + +```rust +// Adding a function-call access key +Promise::new(env::current_account_id()) + .add_access_key( + public_key.clone(), + FUNCTION_CALL_ALLOWANCE, // Gas allowance + env::current_account_id(), // Receiver contract + "claim_for,create_account_and_claim".to_string(), // Allowed methods + ) +``` + +### Key Permissions and Restrictions + +Function-call keys in NEAR Drop have very specific limitations: + +```rust +// Key permissions structure +pub struct AccessKeyPermission { + pub allowance: Option, // Gas budget + pub receiver_id: AccountId, // Which contract can be called + pub method_names: Vec, // Which methods are allowed +} +``` + +For NEAR Drop keys: +- **Allowance**: Limited gas budget (e.g., 0.005 NEAR) +- **Receiver**: Only the drop contract itself +- **Methods**: Only `claim_for` and `create_account_and_claim` + +--- + +## Implementing Key Management + +### Key Generation Strategies + +There are several approaches to generating keys for drops: + +#### 1. Contract-Generated Keys (Recommended) + +Let the contract generate keys internally: + +```rust +use near_sdk::env::random_seed; +use ed25519_dalek::{Keypair, PublicKey as Ed25519PublicKey}; + +impl Contract { + /// Generate a new keypair for a drop + pub fn generate_drop_keypair(&self) -> (PublicKey, String) { + let mut rng = near_sdk::env::rng_seed(); + let keypair = Keypair::generate(&mut rng); + + let public_key = PublicKey::ED25519( + keypair.public.to_bytes().try_into().unwrap() + ); + + let private_key = base58::encode(keypair.secret.to_bytes()); + + (public_key, private_key) + } + + /// Create drop with auto-generated keys + pub fn create_near_drop_with_keys( + &mut self, + num_keys: u32, + amount_per_drop: NearToken, + ) -> Vec { + let mut drop_keys = Vec::new(); + let mut public_keys = Vec::new(); + + for _ in 0..num_keys { + let (public_key, private_key) = self.generate_drop_keypair(); + + drop_keys.push(DropKey { + public_key: public_key.clone(), + private_key, + }); + + public_keys.push(public_key); + } + + // Create the drop + let drop_id = self.create_near_drop(public_keys, amount_per_drop); + + // Return keys for distribution + drop_keys + } +} + +#[derive(near_sdk::serde::Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct DropKey { + pub public_key: PublicKey, + pub private_key: String, +} +``` + +#### 2. Client-Generated Keys + +Generate keys on the client side and submit public keys: + +```javascript +// Frontend key generation example +import { KeyPair } from 'near-api-js'; + +function generateDropKeys(count) { + const keys = []; + + for (let i = 0; i < count; i++) { + const keyPair = KeyPair.fromRandom('ed25519'); + keys.push({ + publicKey: keyPair.publicKey.toString(), + privateKey: keyPair.secretKey, + }); + } + + return keys; +} + +// Use with drop creation +const keys = generateDropKeys(10); +const publicKeys = keys.map(k => k.publicKey); + +await contract.create_near_drop({ + public_keys: publicKeys, + amount_per_drop: '1000000000000000000000000', // 1 NEAR +}); +``` + +--- + +## Advanced Key Management Patterns + +### Key Rotation for Security + +Implement key rotation to enhance security: + +```rust +impl Contract { + /// Rotate access keys for enhanced security + pub fn rotate_drop_keys(&mut self, drop_id: u64, new_public_keys: Vec) { + let mut drop = self.drop_by_id.get(&drop_id) + .expect("Drop not found"); + + // Only allow rotation if no claims have been made + assert_eq!( + drop.get_counter(), + new_public_keys.len() as u64, + "Cannot rotate keys after claims have been made" + ); + + // Remove old keys + self.remove_drop_keys(drop_id); + + // Add new keys + for public_key in new_public_keys { + self.add_access_key_for_drop(&public_key); + self.drop_id_by_key.insert(&public_key, &drop_id); + } + + env::log_str(&format!("Rotated keys for drop {}", drop_id)); + } + + /// Remove all keys associated with a drop + fn remove_drop_keys(&mut self, drop_id: u64) { + let keys_to_remove: Vec = self.drop_id_by_key + .iter() + .filter_map(|(key, id)| if id == drop_id { Some(key) } else { None }) + .collect(); + + for key in keys_to_remove { + self.drop_id_by_key.remove(&key); + Promise::new(env::current_account_id()) + .delete_key(key); + } + } +} +``` + +### Time-Limited Keys + +Create keys that expire after a certain time: + +```rust +use near_sdk::Timestamp; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct TimeLimitedDrop { + pub drop: Drop, + pub expires_at: Timestamp, +} + +impl Contract { + /// Create a time-limited drop + pub fn create_time_limited_drop( + &mut self, + public_keys: Vec, + amount_per_drop: NearToken, + duration_seconds: u64, + ) -> u64 { + let expires_at = env::block_timestamp() + (duration_seconds * 1_000_000_000); + + // Create normal drop first + let drop_id = self.create_near_drop(public_keys, amount_per_drop); + + // Convert to time-limited drop + if let Some(drop) = self.drop_by_id.remove(&drop_id) { + let time_limited_drop = TimeLimitedDrop { + drop, + expires_at, + }; + + // Store as time-limited (you'd need to update your storage structure) + self.time_limited_drops.insert(&drop_id, &time_limited_drop); + } + + drop_id + } + + /// Check if a drop has expired + pub fn is_drop_expired(&self, drop_id: u64) -> bool { + if let Some(time_limited_drop) = self.time_limited_drops.get(&drop_id) { + env::block_timestamp() > time_limited_drop.expires_at + } else { + false + } + } + + /// Cleanup expired drops + pub fn cleanup_expired_drops(&mut self) { + let current_time = env::block_timestamp(); + let mut expired_drops = Vec::new(); + + for (drop_id, time_limited_drop) in self.time_limited_drops.iter() { + if current_time > time_limited_drop.expires_at { + expired_drops.push(drop_id); + } + } + + for drop_id in expired_drops { + self.remove_drop_keys(drop_id); + self.time_limited_drops.remove(&drop_id); + env::log_str(&format!("Cleaned up expired drop {}", drop_id)); + } + } +} +``` + +--- + +## Key Security Best Practices + +### 1. Minimal Permissions + +Always use the principle of least privilege: + +```rust +// Good: Minimal gas allowance +const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // 0.005 NEAR + +// Good: Specific method restrictions +let allowed_methods = "claim_for,create_account_and_claim".to_string(); + +// Good: Contract-specific receiver +let receiver_id = env::current_account_id(); +``` + +### 2. Key Lifecycle Management + +Properly manage the entire key lifecycle: + +```rust +impl Contract { + /// Complete key lifecycle management + fn manage_drop_key_lifecycle( + &mut self, + public_key: &PublicKey, + drop_id: u64, + ) { + // 1. Add key with minimal permissions + self.add_access_key_for_drop(public_key); + + // 2. Map key to drop + self.drop_id_by_key.insert(public_key, &drop_id); + + // 3. Log key creation for audit trail + env::log_str(&format!( + "Added access key {} for drop {}", + public_key, + drop_id + )); + + // 4. Set up cleanup (handled in claim functions) + } + + /// Secure key cleanup after use + fn secure_key_cleanup(&mut self, public_key: &PublicKey) { + // 1. Remove from mappings + self.drop_id_by_key.remove(public_key); + + // 2. Delete from account + Promise::new(env::current_account_id()) + .delete_key(public_key.clone()); + + // 3. Log removal for audit trail + env::log_str(&format!( + "Removed access key {} after use", + public_key + )); + } +} +``` + +### 3. Key Validation + +Validate keys before adding them: + +```rust +impl Contract { + /// Validate public key before adding + fn validate_public_key(&self, public_key: &PublicKey) -> Result<(), String> { + match public_key { + PublicKey::ED25519(key_data) => { + if key_data.len() != 32 { + return Err("Invalid ED25519 key length".to_string()); + } + + // Check if key already exists + if self.drop_id_by_key.contains_key(public_key) { + return Err("Key already exists".to_string()); + } + + Ok(()) + } + _ => Err("Only ED25519 keys are supported".to_string()), + } + } + + /// Safe key addition with validation + pub fn add_validated_access_key(&mut self, public_key: PublicKey, drop_id: u64) { + self.validate_public_key(&public_key) + .unwrap_or_else(|err| env::panic_str(&err)); + + self.manage_drop_key_lifecycle(&public_key, drop_id); + } +} +``` + +--- + +## Monitoring and Analytics + +### Key Usage Tracking + +Track how keys are being used: + +```rust +#[derive(BorshDeserialize, BorshSerialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct KeyUsageStats { + pub total_keys_created: u64, + pub keys_claimed: u64, + pub keys_expired: u64, + pub average_claim_time: u64, +} + +impl Contract { + /// Track key usage statistics + pub fn get_key_usage_stats(&self) -> KeyUsageStats { + KeyUsageStats { + total_keys_created: self.total_keys_created, + keys_claimed: self.keys_claimed, + keys_expired: self.keys_expired, + average_claim_time: self.calculate_average_claim_time(), + } + } + + /// Update stats when key is claimed + fn update_claim_stats(&mut self, claim_timestamp: Timestamp) { + self.keys_claimed += 1; + self.total_claim_time += claim_timestamp - self.drop_creation_time; + } +} +``` + +### Gas Usage Analysis + +Monitor gas consumption patterns: + +```rust +impl Contract { + /// Track gas usage for different operations + pub fn track_gas_usage(&mut self, operation: &str, gas_used: Gas) { + let current_stats = self.gas_usage_stats.get(operation) + .unwrap_or_default(); + + let updated_stats = GasUsageStats { + total_calls: current_stats.total_calls + 1, + total_gas: current_stats.total_gas + gas_used.0, + average_gas: (current_stats.total_gas + gas_used.0) / + (current_stats.total_calls + 1), + }; + + self.gas_usage_stats.insert(operation.to_string(), &updated_stats); + } + + /// Get gas usage statistics + pub fn get_gas_stats(&self, operation: String) -> Option { + self.gas_usage_stats.get(&operation) + } +} +``` + +--- + +## Integration Patterns + +### With Web Applications + +```javascript +// Frontend integration example +class NearDropClient { + constructor(contract) { + this.contract = contract; + } + + async createDropWithKeys(numKeys, amountPerDrop) { + // Generate keys locally for better security + const keys = this.generateKeys(numKeys); + const publicKeys = keys.map(k => k.publicKey); + + // Create drop with public keys + const dropId = await this.contract.create_near_drop({ + public_keys: publicKeys, + amount_per_drop: amountPerDrop, + }); + + // Return drop info with private keys for distribution + return { + dropId, + keys: keys.map(k => ({ + publicKey: k.publicKey, + privateKey: k.secretKey, + claimUrl: this.generateClaimUrl(k.secretKey), + })), + }; + } + + generateClaimUrl(privateKey) { + return `https://yourapp.com/claim?key=${privateKey}`; + } +} +``` + +### With QR Codes + +```javascript +// Generate QR codes for drop links +import QRCode from 'qrcode'; + +async function generateDropQRCodes(dropKeys) { + const qrCodes = []; + + for (const key of dropKeys) { + const claimUrl = `https://yourapp.com/claim?key=${key.privateKey}`; + const qrCodeDataUrl = await QRCode.toDataURL(claimUrl); + + qrCodes.push({ + publicKey: key.publicKey, + qrCode: qrCodeDataUrl, + claimUrl, + }); + } + + return qrCodes; +} +``` + +--- + +## Troubleshooting Common Issues + +### Key Permission Errors + +```rust +// Common error: Key doesn't have permission +impl Contract { + /// Diagnose key permission issues + pub fn diagnose_key_permissions(&self, public_key: PublicKey) -> KeyDiagnostic { + let drop_id = self.drop_id_by_key.get(&public_key); + + KeyDiagnostic { + key_exists: drop_id.is_some(), + drop_exists: drop_id.map(|id| self.drop_by_id.contains_key(&id)).unwrap_or(false), + has_claims_remaining: drop_id + .and_then(|id| self.drop_by_id.get(&id)) + .map(|drop| drop.get_counter() > 0) + .unwrap_or(false), + } + } +} + +#[derive(near_sdk::serde::Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct KeyDiagnostic { + pub key_exists: bool, + pub drop_exists: bool, + pub has_claims_remaining: bool, +} +``` + +### Gas Estimation Issues + +```rust +impl Contract { + /// Estimate gas for different claim scenarios + pub fn estimate_claim_gas(&self, drop_type: String) -> Gas { + match drop_type.as_str() { + "near" => Gas(15_000_000_000_000), // 15 TGas + "ft" => Gas(100_000_000_000_000), // 100 TGas (includes cross-contract calls) + "nft" => Gas(50_000_000_000_000), // 50 TGas + _ => Gas(20_000_000_000_000), // Default + } + } + + /// Check if key has sufficient allowance + pub fn check_key_allowance(&self, public_key: PublicKey, operation: String) -> bool { + let required_gas = self.estimate_claim_gas(operation); + required_gas.0 <= FUNCTION_CALL_ALLOWANCE.as_yoctonear() + } +} +``` + +--- + +## Next Steps + +Access keys are fundamental to NEAR Drop's gasless experience. With proper key management, you can create seamless token distribution experiences that don't require recipients to have existing NEAR accounts or tokens. + +Next, let's explore how to create new NEAR accounts during the claiming process, completing the onboarding experience. + +[Continue to Account Creation →](./account-creation) + +--- + +:::note Access Key Best Practices +- Use minimal gas allowances (0.005 NEAR is usually sufficient) +- Restrict methods to only what's necessary for claiming +- Always clean up keys after use to prevent reuse +- Consider time-limited keys for additional security +- Monitor key usage patterns for optimization opportunities +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/account-creation.md b/docs/tutorials/neardrop/account-creation.md new file mode 100644 index 00000000000..5a3bf1a091c --- /dev/null +++ b/docs/tutorials/neardrop/account-creation.md @@ -0,0 +1,781 @@ +--- +id: account-creation +title: Account Creation +sidebar_label: Account Creation +description: "Learn how to create new NEAR accounts during the claiming process, enabling seamless onboarding for users without existing NEAR accounts." +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +One of NEAR Drop's most powerful features is the ability to create new NEAR accounts for users who don't have them yet. This eliminates the biggest barrier to Web3 adoption: requiring users to set up accounts before they can receive tokens. + +--- + +## The Account Creation Challenge + +Traditional blockchain onboarding has a chicken-and-egg problem: +1. Users need an account to receive tokens +2. Users need tokens to pay for account creation +3. Users need gas to claim tokens + +NEAR Drop solves this by: +1. Using function-call keys for gasless operations +2. Creating accounts programmatically during claims +3. Funding new accounts from the drop contract + +![Account Creation Flow](/docs/assets/tutorials/near-drop/account-creation-flow.png) + +--- + +## How Account Creation Works + +### The Two-Phase Process + +Account creation in NEAR Drop happens in two phases: + +1. **Account Creation**: Create the new account and fund it +2. **Token Transfer**: Transfer the claimed tokens to the new account + +```rust +/// Create account and claim in two phases +pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise { + let public_key = env::signer_account_pk(); + + // Phase 1: Create and fund the account + let create_promise = Promise::new(account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)); // Initial funding + + // Phase 2: Resolve creation and claim tokens + 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) + ) +} +``` + +### Account Validation + +Before creating accounts, we need to validate the requested account ID: + +```rust +impl Contract { + /// Validate account ID before creation + fn validate_new_account_id(&self, account_id: &AccountId) -> Result<(), String> { + let account_str = account_id.as_str(); + + // Check length constraints + if account_str.len() < 2 || account_str.len() > 64 { + return Err("Account ID must be between 2 and 64 characters".to_string()); + } + + // Check format for subaccounts + if account_str.contains('.') { + let parts: Vec<&str> = account_str.split('.').collect(); + + // Must end with top-level account + if !account_str.ends_with(&format!(".{}", self.top_level_account)) { + return Err(format!( + "Account must be a subaccount of {}", + self.top_level_account + )); + } + + // Validate each part + for part in &parts[..parts.len()-1] { + if !self.is_valid_account_part(part) { + return Err("Invalid characters in account ID".to_string()); + } + } + } else { + // Top-level account validation + if !self.is_valid_account_part(account_str) { + return Err("Invalid characters in account ID".to_string()); + } + } + + Ok(()) + } + + /// Check if account ID part contains valid characters + fn is_valid_account_part(&self, part: &str) -> bool { + part.chars().all(|c| { + c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' + }) + } +} +``` + +--- + +## Enhanced Account Creation Implementation + +### Complete Account Creation Logic + +```rust +use near_sdk::{Promise, PromiseResult, Gas}; + +#[near_bindgen] +impl Contract { + /// Create account and claim with comprehensive error handling + pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise { + let public_key = env::signer_account_pk(); + + // Validate the account ID + self.validate_new_account_id(&account_id) + .unwrap_or_else(|err| env::panic_str(&err)); + + // Check if this key has a valid drop + let drop_id = self.drop_id_by_key.get(&public_key) + .expect("No drop found for this key"); + + let drop = self.drop_by_id.get(&drop_id) + .expect("Drop not found"); + + assert!(drop.get_counter() > 0, "All drops have been claimed"); + + // Calculate funding amount based on drop type + let initial_funding = self.calculate_initial_funding(&drop); + + env::log_str(&format!( + "Creating account {} and claiming drop {}", + account_id, + drop_id + )); + + // Create account with initial funding + Promise::new(account_id.clone()) + .create_account() + .transfer(initial_funding) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(50_000_000_000_000)) + .resolve_account_create(public_key, account_id, drop_id) + ) + } + + /// Calculate initial funding based on drop type + fn calculate_initial_funding(&self, drop: &Drop) -> NearToken { + match drop { + Drop::Near(_) => { + // Basic storage funding + NearToken::from_millinear(500) // 0.5 NEAR for storage + } + Drop::FungibleToken(_) => { + // Extra for FT registration + NearToken::from_near(1) // 1 NEAR to cover FT storage registration + } + Drop::NonFungibleToken(_) => { + // Standard funding for NFT ownership + NearToken::from_millinear(500) // 0.5 NEAR + } + } + } + + /// Resolve account creation and proceed with claim + #[private] + pub fn resolve_account_create( + &mut self, + public_key: PublicKey, + account_id: AccountId, + drop_id: u64, + ) { + match env::promise_result(0) { + PromiseResult::Successful(_) => { + env::log_str(&format!("Successfully created account {}", account_id)); + + // Account created successfully, now claim the drop + self.internal_claim(&public_key, &account_id); + } + PromiseResult::Failed => { + env::log_str(&format!("Failed to create account {}", account_id)); + + // Account creation failed - could be: + // 1. Account already exists + // 2. Invalid account ID + // 3. Insufficient funds + + // Try to claim anyway in case account already exists + match self.check_account_exists(&account_id) { + Ok(true) => { + env::log_str("Account already exists, proceeding with claim"); + self.internal_claim(&public_key, &account_id); + } + _ => { + env::panic_str("Account creation failed and account does not exist"); + } + } + } + } + } + + /// Check if an account exists + fn check_account_exists(&self, account_id: &AccountId) -> Result { + // This is a simplified check - in practice you might want to make + // a cross-contract call to verify account existence + Ok(account_id.as_str().len() >= 2) + } +} +``` + +--- + +## Advanced Account Creation Patterns + +### Batch Account Creation + +Create multiple accounts efficiently: + +```rust +impl Contract { + /// Create multiple accounts and claim drops in batch + pub fn batch_create_accounts_and_claim( + &mut self, + account_configs: Vec, + ) -> Promise { + assert!( + !account_configs.is_empty() && account_configs.len() <= 10, + "Can create 1-10 accounts per batch" + ); + + let total_funding = account_configs.len() as u128 * + NearToken::from_near(1).as_yoctonear(); + + assert!( + env::account_balance() >= NearToken::from_yoctonear(total_funding), + "Insufficient balance for batch account creation" + ); + + // Create accounts sequentially + let mut promise = Promise::new(account_configs[0].account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)); + + for config in account_configs.iter().skip(1) { + promise = promise.then( + Promise::new(config.account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)) + ); + } + + // Resolve all creations + promise.then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(100_000_000_000_000)) + .resolve_batch_account_creation(account_configs) + ) + } + + /// Resolve batch account creation + #[private] + pub fn resolve_batch_account_creation( + &mut self, + account_configs: Vec, + ) { + let mut successful_accounts = Vec::new(); + let mut failed_accounts = Vec::new(); + + for (i, config) in account_configs.iter().enumerate() { + match env::promise_result(i) { + PromiseResult::Successful(_) => { + successful_accounts.push(config.account_id.clone()); + } + PromiseResult::Failed => { + failed_accounts.push(config.account_id.clone()); + } + } + } + + env::log_str(&format!( + "Created {} accounts successfully, {} failed", + successful_accounts.len(), + failed_accounts.len() + )); + + // Process claims for successful accounts + for config in account_configs { + if successful_accounts.contains(&config.account_id) { + self.internal_claim(&config.public_key, &config.account_id); + } + } + } +} + +#[derive(near_sdk::serde::Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct AccountConfig { + pub account_id: AccountId, + pub public_key: PublicKey, +} +``` + +### Account Creation with Custom Funding + +Allow variable funding amounts based on use case: + +```rust +impl Contract { + /// Create account with custom funding amount + pub fn create_account_with_funding( + &mut self, + account_id: AccountId, + funding_amount: NearToken, + ) -> Promise { + let public_key = env::signer_account_pk(); + + // Validate funding amount + assert!( + funding_amount >= NearToken::from_millinear(100), + "Minimum funding is 0.1 NEAR" + ); + + assert!( + funding_amount <= NearToken::from_near(10), + "Maximum funding is 10 NEAR" + ); + + // Validate we have enough balance + let contract_balance = env::account_balance(); + assert!( + contract_balance >= funding_amount, + "Contract has insufficient balance for funding" + ); + + Promise::new(account_id.clone()) + .create_account() + .transfer(funding_amount) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(30_000_000_000_000)) + .resolve_custom_funding_creation( + public_key, + account_id, + funding_amount, + ) + ) + } + + /// Resolve custom funding account creation + #[private] + pub fn resolve_custom_funding_creation( + &mut self, + public_key: PublicKey, + account_id: AccountId, + funding_amount: NearToken, + ) { + if is_promise_success() { + env::log_str(&format!( + "Created account {} with {} NEAR funding", + account_id, + funding_amount.as_near() + )); + + self.internal_claim(&public_key, &account_id); + } else { + env::panic_str("Custom funding account creation failed"); + } + } +} +``` + +--- + +## Account Naming Strategies + +### Deterministic Account Names + +Generate predictable account names: + +```rust +use near_sdk::env::sha256; + +impl Contract { + /// Generate deterministic account name from public key + pub fn generate_account_name(&self, public_key: &PublicKey) -> AccountId { + let key_bytes = match public_key { + PublicKey::ED25519(bytes) => bytes, + _ => env::panic_str("Unsupported key type"), + }; + + // Create deterministic hash + let hash = sha256(key_bytes); + let hex_string = hex::encode(&hash[..8]); // Use first 8 bytes + + // Create account ID + let account_str = format!("{}.{}", hex_string, self.top_level_account); + + account_str.parse().unwrap_or_else(|_| { + env::panic_str("Failed to generate valid account ID") + }) + } + + /// Create account with deterministic name + pub fn create_deterministic_account_and_claim(&mut self) -> Promise { + let public_key = env::signer_account_pk(); + let account_id = self.generate_account_name(&public_key); + + self.create_account_and_claim(account_id) + } +} +``` + +### Human-Readable Account Names + +Allow users to choose their account names with validation: + +```rust +impl Contract { + /// Create account with user-chosen name + pub fn create_named_account_and_claim( + &mut self, + preferred_name: String, + ) -> Promise { + let public_key = env::signer_account_pk(); + + // Sanitize and validate name + let clean_name = self.sanitize_account_name(&preferred_name); + let account_id = format!("{}.{}", clean_name, self.top_level_account) + .parse::() + .unwrap_or_else(|_| env::panic_str("Invalid account name")); + + self.create_account_and_claim(account_id) + } + + /// Sanitize user input for account names + fn sanitize_account_name(&self, name: &str) -> String { + name.to_lowercase() + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') + .take(32) // Limit length + .collect() + } + + /// Check if an account name is available + pub fn is_account_name_available(&self, name: String) -> Promise { + let clean_name = self.sanitize_account_name(&name); + let account_id = format!("{}.{}", clean_name, self.top_level_account); + + // This would need a cross-contract call to check existence + // For now, return a simple validation + Promise::new(account_id.parse().unwrap()) + .function_call( + "get_account".to_string(), + vec![], + NearToken::from_yoctonear(0), + Gas(5_000_000_000_000), + ) + } +} +``` + +--- + +## Account Recovery Patterns + +### Key Rotation for New Accounts + +Set up key rotation for newly created accounts: + +```rust +impl Contract { + /// Create account with key rotation setup + pub fn create_secure_account_and_claim( + &mut self, + account_id: AccountId, + recovery_key: PublicKey, + ) -> Promise { + let public_key = env::signer_account_pk(); + + Promise::new(account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)) + .add_full_access_key(recovery_key) // Add recovery key + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(50_000_000_000_000)) + .resolve_secure_account_creation( + public_key, + account_id, + ) + ) + } + + /// Set up account security after creation + #[private] + pub fn resolve_secure_account_creation( + &mut self, + public_key: PublicKey, + account_id: AccountId, + ) { + if is_promise_success() { + env::log_str(&format!( + "Created secure account {} with recovery key", + account_id + )); + + // Claim tokens + self.internal_claim(&public_key, &account_id); + + // Optional: Remove the original function-call key after successful claim + Promise::new(account_id) + .delete_key(public_key); + } else { + env::panic_str("Secure account creation failed"); + } + } +} +``` + +--- + +## Integration with Wallets + +### Wallet Integration for Account Creation + +```javascript +// Frontend integration for account creation +class AccountCreationService { + constructor(contract, wallet) { + this.contract = contract; + this.wallet = wallet; + } + + async createAccountAndClaim(privateKey, preferredName) { + try { + // Import the private key temporarily + const keyPair = KeyPair.fromString(privateKey); + + // Sign transaction with the private key + const outcome = await this.wallet.signAndSendTransaction({ + receiverId: this.contract.contractId, + actions: [{ + type: 'FunctionCall', + params: { + methodName: 'create_named_account_and_claim', + args: { + preferred_name: preferredName + }, + gas: '100000000000000', + deposit: '0' + } + }] + }); + + return { + success: true, + transactionHash: outcome.transaction.hash, + newAccountId: `${preferredName}.testnet` + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + async checkAccountAvailability(name) { + try { + const result = await this.contract.is_account_name_available({ + name: name + }); + return { available: true }; + } catch (error) { + return { available: false, reason: error.message }; + } + } +} +``` + +--- + +## Error Handling and Recovery + +### Comprehensive Error Handling + +```rust +impl Contract { + /// Handle account creation errors gracefully + pub fn create_account_with_fallback( + &mut self, + primary_account_id: AccountId, + fallback_account_id: Option, + ) -> Promise { + let public_key = env::signer_account_pk(); + + // Try primary account creation + Promise::new(primary_account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(60_000_000_000_000)) + .resolve_account_creation_with_fallback( + public_key, + primary_account_id, + fallback_account_id, + ) + ) + } + + /// Resolve with fallback options + #[private] + pub fn resolve_account_creation_with_fallback( + &mut self, + public_key: PublicKey, + primary_account_id: AccountId, + fallback_account_id: Option, + ) { + match env::promise_result(0) { + PromiseResult::Successful(_) => { + // Primary account creation succeeded + self.internal_claim(&public_key, &primary_account_id); + } + PromiseResult::Failed => { + if let Some(fallback_id) = fallback_account_id { + env::log_str("Primary account creation failed, trying fallback"); + + // Try fallback account + Promise::new(fallback_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(30_000_000_000_000)) + .resolve_fallback_creation(public_key, fallback_id) + ); + } else { + // No fallback, try to claim with existing account + env::log_str("No fallback available, attempting direct claim"); + self.internal_claim(&public_key, &primary_account_id); + } + } + } + } + + /// Resolve fallback account creation + #[private] + pub fn resolve_fallback_creation( + &mut self, + public_key: PublicKey, + account_id: AccountId, + ) { + if is_promise_success() { + env::log_str("Fallback account creation succeeded"); + self.internal_claim(&public_key, &account_id); + } else { + env::panic_str("Both primary and fallback account creation failed"); + } + } +} +``` + +--- + +## Testing Account Creation + +### CLI Testing Commands + + + + + ```bash + # Create account and claim with deterministic name + near call drop-contract.testnet create_deterministic_account_and_claim \ + --accountId drop-contract.testnet \ + --keyPair '{"public_key": "ed25519:...", "private_key": "ed25519:..."}' + + # Create account with custom name + near call drop-contract.testnet create_named_account_and_claim '{ + "preferred_name": "alice-drop" + }' --accountId drop-contract.testnet \ + --keyPair '{"public_key": "ed25519:...", "private_key": "ed25519:..."}' + + # Check if account was created successfully + near view alice-drop.testnet get_account + ``` + + + + + ```bash + # Create account and claim with deterministic name + near contract call-function as-transaction drop-contract.testnet create_deterministic_account_and_claim json-args '{}' prepaid-gas '150.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:... --signer-private-key ed25519:... send + + # Create account with custom name + near contract call-function as-transaction drop-contract.testnet create_named_account_and_claim json-args '{ + "preferred_name": "alice-drop" + }' prepaid-gas '150.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:... --signer-private-key ed25519:... send + + # Check if account was created successfully + near contract call-function as-read-only alice-drop.testnet state json-args '{}' network-config testnet now + ``` + + + +--- + +## Performance Considerations + +### Optimizing Account Creation + +```rust +// Constants for account creation optimization +const MAX_ACCOUNTS_PER_BATCH: usize = 5; +const MIN_ACCOUNT_FUNDING: NearToken = NearToken::from_millinear(100); +const MAX_ACCOUNT_FUNDING: NearToken = NearToken::from_near(5); + +impl Contract { + /// Optimized account creation with resource management + pub fn create_account_optimized(&mut self, account_id: AccountId) -> Promise { + // Check contract balance before creation + let available_balance = env::account_balance(); + let required_funding = NearToken::from_near(1); + + assert!( + available_balance >= required_funding.saturating_mul(2), + "Insufficient contract balance for account creation" + ); + + // Create with minimal required funding + Promise::new(account_id.clone()) + .create_account() + .transfer(required_funding) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(40_000_000_000_000)) + .resolve_optimized_creation( + env::signer_account_pk(), + account_id, + ) + ) + } +} +``` + +--- + +## Next Steps + +Account creation is the final piece of the onboarding puzzle. Users can now: +1. Receive private keys for drops +2. Create NEAR accounts without existing tokens +3. Claim their tokens to new accounts +4. Start using NEAR immediately + +Next, let's build a frontend that ties everything together into a seamless user experience. + +[Continue to Frontend Integration →](./frontend) + +--- + +:::note Account Creation Best Practices +- Always validate account IDs before creation attempts +- Provide adequate initial funding based on intended use +- Implement fallback strategies for failed creations +- Consider deterministic naming for better UX +- Monitor contract balance to ensure sufficient funds for creation +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md new file mode 100644 index 00000000000..9618906a9d0 --- /dev/null +++ b/docs/tutorials/neardrop/frontend.md @@ -0,0 +1,3643 @@ +--- +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 + + +
+ {/* Drop Type Selection */} + setDropType(value as any)}> + + NEAR Tokens + Fungible Tokens + NFT + + + {/* Key Count Configuration */} +
+ +
+ + setKeyCount(parseInt(e.target.value) || 1)} + className="w-20 text-center" + min="1" + max={dropType === 'nft' ? 1 : 100} + disabled={dropType === 'nft'} + /> + +
+
+ + {/* NEAR Drop Configuration */} + +
+ + setNearAmount(e.target.value)} + placeholder="1.0" + required + /> +

+ Each recipient will receive {nearAmount} NEAR tokens +

+
+
+ + {/* FT Drop Configuration */} + +
+ + setFtContract(e.target.value)} + placeholder="token.testnet" + required + /> +
+
+ + setFtAmount(e.target.value)} + placeholder="1000000000000000000000000" + required + /> +

+ Amount in smallest token units (including decimals) +

+
+
+ + {/* NFT Drop Configuration */} + +
+ + setNftContract(e.target.value)} + placeholder="nft.testnet" + required + /> +
+
+ + setNftTokenId(e.target.value)} + placeholder="unique-token-123" + required + /> +
+

+ ⚠️ NFT drops support only 1 key since each NFT is unique +

+
+
+ + {/* Submit Button */} + +
+
+
+ ); +} +``` + +### 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 + + +
+ {/* Drop Type Selection */} + setDropType(value as any)}> + + NEAR Tokens + Fungible Tokens + NFT + + + {/* Key Count Configuration */} +
+ +
+ + setKeyCount(parseInt(e.target.value) || 1)} + className="w-20 text-center" + min="1" + max={dropType === 'nft' ? 1 : 100} + disabled={dropType === 'nft'} + /> + +
+
+ + {/* NEAR Drop Configuration */} + +
+ + setNearAmount(e.target.value)} + placeholder="1.0" + required + /> +

+ Each recipient will receive {nearAmount} NEAR tokens +

+
+
+ + {/* FT Drop Configuration */} + +
+ + setFtContract(e.target.value)} + placeholder="token.testnet" + required + /> +
+
+ + setFtAmount(e.target.value)} + placeholder="1000000000000000000000000" + required + /> +

+ Amount in smallest token units (including decimals) +

+
+
+ + {/* NFT Drop Configuration */} + +
+ + setNftContract(e.target.value)} + placeholder="nft.testnet" + required + /> +
+
+ + setNftTokenId(e.target.value)} + placeholder="unique-token-123" + required + /> +
+

+ ⚠️ NFT drops support only 1 key since each NFT is unique +

+
+
+ + {/* Submit Button */} + +
+
+
+ ); +} +``` + +### 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 + + +
+ {/* Drop Type Selection */} + setDropType(value as any)}> + + NEAR Tokens + Fungible Tokens + NFT + + + {/* Key Count Configuration */} +
+ +
+ + setKeyCount(parseInt(e.target.value) || 1)} + className="w-20 text-center" + min="1" + max={dropType === 'nft' ? 1 : 100} + disabled={dropType === 'nft'} + /> + +
+
+ + {/* NEAR Drop Configuration */} + +
+ + setNearAmount(e.target.value)} + placeholder="1.0" + required + /> +

+ Each recipient will receive {nearAmount} NEAR tokens +

+
+
+ + {/* FT Drop Configuration */} + +
+ + setFtContract(e.target.value)} + placeholder="token.testnet" + required + /> +
+
+ + setFtAmount(e.target.value)} + placeholder="1000000000000000000000000" + required + /> +

+ Amount in smallest token units (including decimals) +

+
+
+ + {/* NFT Drop Configuration */} + +
+ + setNftContract(e.target.value)} + placeholder="nft.testnet" + required + /> +
+
+ + setNftTokenId(e.target.value)} + placeholder="unique-token-123" + required + /> +
+

+ ⚠️ NFT drops support only 1 key since each NFT is unique +

+
+
+ + {/* Submit Button */} + +
+
+
+ ); +} +``` + +### 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 + +--- + +:::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 +::: diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md index 7d85fbea0b3..5a9ce29ae8f 100644 --- a/docs/tutorials/neardrop/nft-drops.md +++ b/docs/tutorials/neardrop/nft-drops.md @@ -877,4 +877,153 @@ impl Contract { token_id: String, ) -> u64 { // Validate inputs - self.validate_nft_drop_inputs(&public_key \ No newline at end of file + self.validate_nft_drop_inputs(&public_key, &nft_contract, &token_id); + + // Check if drop already exists for this NFT + assert!( + !self.nft_drop_exists(nft_contract.clone(), token_id.clone()), + "{}", + ERR_NFT_ALREADY_CLAIMED + ); + + // Create the drop + self.create_nft_drop(public_key, nft_contract, token_id) + } + + /// Validate NFT drop inputs + fn validate_nft_drop_inputs( + &self, + public_key: &PublicKey, + nft_contract: &AccountId, + token_id: &String, + ) { + // Validate public key format + assert!( + matches!(public_key, PublicKey::ED25519(_)), + "Only ED25519 keys are supported" + ); + + // Validate NFT contract account ID + assert!( + nft_contract.as_str().len() >= 2 && nft_contract.as_str().contains('.'), + "Invalid NFT contract account ID" + ); + + // Validate token ID + assert!(!token_id.is_empty(), "{}", ERR_INVALID_TOKEN_ID); + assert!(token_id.len() <= 64, "Token ID too long (max 64 characters)"); + + // Check for reserved characters + assert!( + token_id.chars().all(|c| c.is_alphanumeric() || "-_.".contains(c)), + "Token ID contains invalid characters" + ); + } +} +``` + +--- + +## Gas Optimization for NFT Operations + +NFT drops can be gas-intensive due to cross-contract calls. Here are optimization strategies: + +```rust +// Optimized gas constants based on testing +pub const GAS_FOR_NFT_TRANSFER: Gas = Gas(30_000_000_000_000); // 30 TGas +pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000); // 20 TGas +pub const GAS_FOR_NFT_VERIFICATION: Gas = Gas(10_000_000_000_000); // 10 TGas + +impl Contract { + /// Optimized NFT claiming with gas monitoring + fn claim_nft_drop_optimized( + &mut self, + public_key: PublicKey, + receiver_id: AccountId, + nft_contract: AccountId, + token_id: String, + ) { + let initial_gas = env::used_gas(); + + // Transfer the NFT with optimized gas allocation + ext_nft::ext(nft_contract.clone()) + .with_static_gas(GAS_FOR_NFT_TRANSFER) + .nft_transfer( + receiver_id.clone(), + token_id.clone(), + None, + Some("NEAR Drop claim".to_string()) // Shorter memo to save gas + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_NFT_CALLBACK) + .nft_transfer_callback_optimized( + public_key, + receiver_id, + nft_contract, + token_id, + initial_gas, + ) + ); + } + + /// Optimized callback with gas usage reporting + #[private] + pub fn nft_transfer_callback_optimized( + &mut self, + public_key: PublicKey, + receiver_id: AccountId, + nft_contract: AccountId, + token_id: String, + initial_gas: Gas, + ) { + let gas_used = env::used_gas() - initial_gas; + + if is_promise_success() { + env::log_str(&format!( + "NFT {} transferred to {} using {} gas", + token_id, + receiver_id, + gas_used.0 + )); + + // Efficient cleanup + if let Some(drop_id) = self.drop_id_by_key.get(&public_key) { + self.drop_by_id.remove(&drop_id); + self.drop_id_by_key.remove(&public_key); + + // Remove access key + Promise::new(env::current_account_id()) + .delete_key(public_key); + } + } else { + env::panic_str("NFT transfer failed"); + } + } +} +``` + +--- + +## Next Steps + +You now have a complete NFT drop system that handles: +- Unique token distribution patterns +- Cross-contract NFT transfers with proper callbacks +- Ownership verification and security measures +- Advanced patterns like rarity-based and collection drops +- Comprehensive error handling and gas optimization + +The NFT drop implementation completes the core token distribution functionality. Next, let's explore how function-call access keys work in detail to understand the gasless claiming mechanism. + +[Continue to Access Key Management →](./access-keys) + +--- + +:::note NFT Drop Considerations +- Always verify NFT ownership before creating drops +- NFT drops are inherently single-use (counter always equals 1) +- Test with various NFT contracts to ensure NEP-171 compatibility +- Monitor gas costs as they can be higher than NEAR/FT drops +- Consider implementing batch operations for multiple NFT drops +::: \ No newline at end of file From 971046e3b360d8eda38e513c75e96fa6a65f910d Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:05:53 +0100 Subject: [PATCH 04/23] Add files via upload --- docs/tutorials/neardrop/access-keys.md | 626 +-- docs/tutorials/neardrop/account-creation.md | 854 +--- .../neardrop/contract-architecture.md | 264 +- docs/tutorials/neardrop/frontend.md | 3860 ++--------------- docs/tutorials/neardrop/ft-drops.md | 617 +-- docs/tutorials/neardrop/introduction.md | 137 +- docs/tutorials/neardrop/near-drops.md | 518 +-- docs/tutorials/neardrop/nft-drops.md | 897 +--- 8 files changed, 1454 insertions(+), 6319 deletions(-) diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md index 0919150df8a..9dec373fb78 100644 --- a/docs/tutorials/neardrop/access-keys.md +++ b/docs/tutorials/neardrop/access-keys.md @@ -1,517 +1,222 @@ --- id: access-keys title: Access Key Management -sidebar_label: Access Key Management -description: "Deep dive into NEAR's function-call access keys and how they enable gasless operations in the NEAR Drop system. Learn key generation, management, and security patterns." +sidebar_label: Access Key Management +description: "Understand how function-call access keys enable gasless operations in NEAR Drop." --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Function-call access keys are one of NEAR's most powerful features, enabling gasless operations and seamless user experiences. In the NEAR Drop system, they're the secret sauce that allows recipients to claim tokens without needing NEAR tokens for gas fees. +This is where NEAR gets really cool. Function-call access keys are what make gasless claiming possible - let's understand how they work! --- -## Understanding NEAR Access Keys +## The Problem NEAR Solves -NEAR supports two types of access keys: +Traditional blockchains have a chicken-and-egg problem: +- You need tokens to pay gas fees +- But you need gas to receive tokens +- New users are stuck! -1. **Full Access Keys**: Complete control over an account (like a master key) -2. **Function-Call Access Keys**: Limited permissions to call specific methods +NEAR's solution: **Function-call access keys** that let you call specific functions without owning the account. -![Access Key Types](/docs/assets/tutorials/near-drop/access-key-types.png) +--- -### Why Function-Call Keys Are Perfect for Drops +## How Access Keys Work -Function-call keys solve a classic blockchain UX problem: new users need tokens to pay for gas, but they need gas to claim tokens. It's a chicken-and-egg problem that function-call keys elegantly solve. +NEAR has two types of keys: ---- +**Full Access Keys** 🔑 +- Complete control over an account +- Can do anything: transfer tokens, deploy contracts, etc. +- Like having admin access -## How Access Keys Work in NEAR Drop +**Function-Call Keys** 🎫 +- Limited permissions +- Can only call specific functions +- Like having a concert ticket - gets you in, but only to your seat + +--- -### The Access Key Lifecycle +## NEAR Drop's Key Magic -1. **Key Generation**: Create a public/private key pair -2. **Key Addition**: Add the public key to the contract with limited permissions -3. **Key Distribution**: Share the private key with recipients -4. **Key Usage**: Recipients use the key to sign claiming transactions -5. **Key Cleanup**: Remove used keys to prevent reuse +Here's what happens when you create a drop: ```rust -// Adding a function-call access key -Promise::new(env::current_account_id()) +// 1. Generate public/private key pairs +let keypair = KeyPair::fromRandom('ed25519'); + +// 2. Add public key to contract with limited permissions +Promise::new(contract_account) .add_access_key( - public_key.clone(), - FUNCTION_CALL_ALLOWANCE, // Gas allowance - env::current_account_id(), // Receiver contract - "claim_for,create_account_and_claim".to_string(), // Allowed methods + public_key, + NearToken::from_millinear(5), // 0.005 NEAR gas budget + contract_account, // Can only call this contract + "claim_for,create_account_and_claim" // Only these methods ) -``` - -### Key Permissions and Restrictions -Function-call keys in NEAR Drop have very specific limitations: - -```rust -// Key permissions structure -pub struct AccessKeyPermission { - pub allowance: Option, // Gas budget - pub receiver_id: AccountId, // Which contract can be called - pub method_names: Vec, // Which methods are allowed -} +// 3. Give private key to recipient +// 4. Recipient signs transactions using the CONTRACT'S account (gasless!) ``` -For NEAR Drop keys: -- **Allowance**: Limited gas budget (e.g., 0.005 NEAR) -- **Receiver**: Only the drop contract itself -- **Methods**: Only `claim_for` and `create_account_and_claim` +**The result**: Recipients can claim tokens without having NEAR accounts or paying gas! --- -## Implementing Key Management - -### Key Generation Strategies - -There are several approaches to generating keys for drops: - -#### 1. Contract-Generated Keys (Recommended) +## Key Permissions Breakdown -Let the contract generate keys internally: +Function-call keys in NEAR Drop have strict limits: ```rust -use near_sdk::env::random_seed; -use ed25519_dalek::{Keypair, PublicKey as Ed25519PublicKey}; - -impl Contract { - /// Generate a new keypair for a drop - pub fn generate_drop_keypair(&self) -> (PublicKey, String) { - let mut rng = near_sdk::env::rng_seed(); - let keypair = Keypair::generate(&mut rng); - - let public_key = PublicKey::ED25519( - keypair.public.to_bytes().try_into().unwrap() - ); - - let private_key = base58::encode(keypair.secret.to_bytes()); - - (public_key, private_key) - } - - /// Create drop with auto-generated keys - pub fn create_near_drop_with_keys( - &mut self, - num_keys: u32, - amount_per_drop: NearToken, - ) -> Vec { - let mut drop_keys = Vec::new(); - let mut public_keys = Vec::new(); - - for _ in 0..num_keys { - let (public_key, private_key) = self.generate_drop_keypair(); - - drop_keys.push(DropKey { - public_key: public_key.clone(), - private_key, - }); - - public_keys.push(public_key); - } - - // Create the drop - let drop_id = self.create_near_drop(public_keys, amount_per_drop); - - // Return keys for distribution - drop_keys - } -} - -#[derive(near_sdk::serde::Serialize)] -#[serde(crate = "near_sdk::serde")] -pub struct DropKey { - pub public_key: PublicKey, - pub private_key: String, +pub struct AccessKeyPermission { + allowance: NearToken::from_millinear(5), // Gas budget: 0.005 NEAR + receiver_id: "drop-contract.testnet", // Can only call this contract + method_names: ["claim_for", "create_account_and_claim"] // Only these methods } ``` -#### 2. Client-Generated Keys - -Generate keys on the client side and submit public keys: - -```javascript -// Frontend key generation example -import { KeyPair } from 'near-api-js'; - -function generateDropKeys(count) { - const keys = []; - - for (let i = 0; i < count; i++) { - const keyPair = KeyPair.fromRandom('ed25519'); - keys.push({ - publicKey: keyPair.publicKey.toString(), - privateKey: keyPair.secretKey, - }); - } - - return keys; -} - -// Use with drop creation -const keys = generateDropKeys(10); -const publicKeys = keys.map(k => k.publicKey); +**What keys CAN do:** +- Call `claim_for` to claim to existing accounts +- Call `create_account_and_claim` to create new accounts +- Use up to 0.005 NEAR worth of gas -await contract.create_near_drop({ - public_keys: publicKeys, - amount_per_drop: '1000000000000000000000000', // 1 NEAR -}); -``` +**What keys CANNOT do:** +- Transfer tokens from the contract +- Call any other functions +- Deploy contracts or change state maliciously +- Exceed their gas allowance --- -## Advanced Key Management Patterns +## Key Lifecycle -### Key Rotation for Security +The lifecycle is simple and secure: -Implement key rotation to enhance security: +``` +1. CREATE → Add key with limited permissions +2. SHARE → Give private key to recipient +3. CLAIM → Recipient uses key to claim tokens +4. CLEANUP → Remove key after use (prevents reuse) +``` +Here's the cleanup code: ```rust -impl Contract { - /// Rotate access keys for enhanced security - pub fn rotate_drop_keys(&mut self, drop_id: u64, new_public_keys: Vec) { - let mut drop = self.drop_by_id.get(&drop_id) - .expect("Drop not found"); - - // Only allow rotation if no claims have been made - assert_eq!( - drop.get_counter(), - new_public_keys.len() as u64, - "Cannot rotate keys after claims have been made" - ); - - // Remove old keys - self.remove_drop_keys(drop_id); - - // Add new keys - for public_key in new_public_keys { - self.add_access_key_for_drop(&public_key); - self.drop_id_by_key.insert(&public_key, &drop_id); - } - - env::log_str(&format!("Rotated keys for drop {}", drop_id)); - } +fn cleanup_after_claim(&mut self, public_key: &PublicKey) { + // Remove mapping + self.drop_id_by_key.remove(public_key); - /// Remove all keys associated with a drop - fn remove_drop_keys(&mut self, drop_id: u64) { - let keys_to_remove: Vec = self.drop_id_by_key - .iter() - .filter_map(|(key, id)| if id == drop_id { Some(key) } else { None }) - .collect(); + // Delete the access key + Promise::new(env::current_account_id()) + .delete_key(public_key.clone()); - for key in keys_to_remove { - self.drop_id_by_key.remove(&key); - Promise::new(env::current_account_id()) - .delete_key(key); - } - } + env::log_str("Key cleaned up after claim"); } ``` +--- + +## Advanced Key Patterns + ### Time-Limited Keys -Create keys that expire after a certain time: +You can make keys that expire: ```rust -use near_sdk::Timestamp; - -#[derive(Serialize, Deserialize, Clone)] -#[serde(crate = "near_sdk::serde")] pub struct TimeLimitedDrop { - pub drop: Drop, - pub expires_at: Timestamp, + drop: Drop, + expires_at: Timestamp, } impl Contract { - /// Create a time-limited drop - pub fn create_time_limited_drop( - &mut self, - public_keys: Vec, - amount_per_drop: NearToken, - duration_seconds: u64, - ) -> u64 { - let expires_at = env::block_timestamp() + (duration_seconds * 1_000_000_000); - - // Create normal drop first - let drop_id = self.create_near_drop(public_keys, amount_per_drop); - - // Convert to time-limited drop - if let Some(drop) = self.drop_by_id.remove(&drop_id) { - let time_limited_drop = TimeLimitedDrop { - drop, - expires_at, - }; - - // Store as time-limited (you'd need to update your storage structure) - self.time_limited_drops.insert(&drop_id, &time_limited_drop); - } - - drop_id - } - - /// Check if a drop has expired - pub fn is_drop_expired(&self, drop_id: u64) -> bool { - if let Some(time_limited_drop) = self.time_limited_drops.get(&drop_id) { - env::block_timestamp() > time_limited_drop.expires_at - } else { - false - } - } - - /// Cleanup expired drops - pub fn cleanup_expired_drops(&mut self) { - let current_time = env::block_timestamp(); - let mut expired_drops = Vec::new(); + pub fn cleanup_expired_keys(&mut self) { + let now = env::block_timestamp(); - for (drop_id, time_limited_drop) in self.time_limited_drops.iter() { - if current_time > time_limited_drop.expires_at { - expired_drops.push(drop_id); + // Find and remove expired drops + for (drop_id, drop) in self.time_limited_drops.iter() { + if now > drop.expires_at { + self.remove_all_keys_for_drop(drop_id); } } - - for drop_id in expired_drops { - self.remove_drop_keys(drop_id); - self.time_limited_drops.remove(&drop_id); - env::log_str(&format!("Cleaned up expired drop {}", drop_id)); - } } } ``` ---- - -## Key Security Best Practices - -### 1. Minimal Permissions - -Always use the principle of least privilege: - -```rust -// Good: Minimal gas allowance -const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // 0.005 NEAR - -// Good: Specific method restrictions -let allowed_methods = "claim_for,create_account_and_claim".to_string(); - -// Good: Contract-specific receiver -let receiver_id = env::current_account_id(); -``` - -### 2. Key Lifecycle Management +### Key Rotation -Properly manage the entire key lifecycle: +For extra security, you can rotate keys: ```rust impl Contract { - /// Complete key lifecycle management - fn manage_drop_key_lifecycle( - &mut self, - public_key: &PublicKey, - drop_id: u64, - ) { - // 1. Add key with minimal permissions - self.add_access_key_for_drop(public_key); - - // 2. Map key to drop - self.drop_id_by_key.insert(public_key, &drop_id); - - // 3. Log key creation for audit trail - env::log_str(&format!( - "Added access key {} for drop {}", - public_key, - drop_id - )); - - // 4. Set up cleanup (handled in claim functions) - } - - /// Secure key cleanup after use - fn secure_key_cleanup(&mut self, public_key: &PublicKey) { - // 1. Remove from mappings - self.drop_id_by_key.remove(public_key); - - // 2. Delete from account - Promise::new(env::current_account_id()) - .delete_key(public_key.clone()); + pub fn rotate_drop_keys(&mut self, drop_id: u64, new_keys: Vec) { + // Remove old keys + self.remove_old_keys(drop_id); - // 3. Log removal for audit trail - env::log_str(&format!( - "Removed access key {} after use", - public_key - )); - } -} -``` - -### 3. Key Validation - -Validate keys before adding them: - -```rust -impl Contract { - /// Validate public key before adding - fn validate_public_key(&self, public_key: &PublicKey) -> Result<(), String> { - match public_key { - PublicKey::ED25519(key_data) => { - if key_data.len() != 32 { - return Err("Invalid ED25519 key length".to_string()); - } - - // Check if key already exists - if self.drop_id_by_key.contains_key(public_key) { - return Err("Key already exists".to_string()); - } - - Ok(()) - } - _ => Err("Only ED25519 keys are supported".to_string()), + // Add new keys + for key in new_keys { + self.add_claim_key(&key, drop_id); } } - - /// Safe key addition with validation - pub fn add_validated_access_key(&mut self, public_key: PublicKey, drop_id: u64) { - self.validate_public_key(&public_key) - .unwrap_or_else(|err| env::panic_str(&err)); - - self.manage_drop_key_lifecycle(&public_key, drop_id); - } } ``` --- -## Monitoring and Analytics +## Security Best Practices -### Key Usage Tracking +**✅ DO:** +- Use minimal gas allowances (0.005 NEAR is plenty) +- Remove keys immediately after use +- Validate key formats before adding +- Monitor key usage patterns -Track how keys are being used: - -```rust -#[derive(BorshDeserialize, BorshSerialize, Serialize)] -#[serde(crate = "near_sdk::serde")] -pub struct KeyUsageStats { - pub total_keys_created: u64, - pub keys_claimed: u64, - pub keys_expired: u64, - pub average_claim_time: u64, -} +**❌ DON'T:** +- Give keys excessive gas allowances +- Reuse keys for multiple drops +- Skip cleanup after claims +- Log private keys anywhere -impl Contract { - /// Track key usage statistics - pub fn get_key_usage_stats(&self) -> KeyUsageStats { - KeyUsageStats { - total_keys_created: self.total_keys_created, - keys_claimed: self.keys_claimed, - keys_expired: self.keys_expired, - average_claim_time: self.calculate_average_claim_time(), - } - } - - /// Update stats when key is claimed - fn update_claim_stats(&mut self, claim_timestamp: Timestamp) { - self.keys_claimed += 1; - self.total_claim_time += claim_timestamp - self.drop_creation_time; - } -} -``` +--- -### Gas Usage Analysis +## Gas Usage Monitoring -Monitor gas consumption patterns: +Track how much gas your keys use: ```rust impl Contract { - /// Track gas usage for different operations - pub fn track_gas_usage(&mut self, operation: &str, gas_used: Gas) { - let current_stats = self.gas_usage_stats.get(operation) - .unwrap_or_default(); + pub fn track_key_usage(&mut self, operation: &str) { + let gas_used = env::used_gas(); - let updated_stats = GasUsageStats { - total_calls: current_stats.total_calls + 1, - total_gas: current_stats.total_gas + gas_used.0, - average_gas: (current_stats.total_gas + gas_used.0) / - (current_stats.total_calls + 1), - }; + // Log for monitoring + env::log_str(&format!("{} used {} gas", operation, gas_used.0)); - self.gas_usage_stats.insert(operation.to_string(), &updated_stats); - } - - /// Get gas usage statistics - pub fn get_gas_stats(&self, operation: String) -> Option { - self.gas_usage_stats.get(&operation) + // Could store in state for analytics + self.gas_usage_stats.insert(operation, gas_used); } } ``` --- -## Integration Patterns +## Integration with Frontend -### With Web Applications +Your frontend can generate keys securely: ```javascript -// Frontend integration example -class NearDropClient { - constructor(contract) { - this.contract = contract; - } - - async createDropWithKeys(numKeys, amountPerDrop) { - // Generate keys locally for better security - const keys = this.generateKeys(numKeys); - const publicKeys = keys.map(k => k.publicKey); - - // Create drop with public keys - const dropId = await this.contract.create_near_drop({ - public_keys: publicKeys, - amount_per_drop: amountPerDrop, - }); - - // Return drop info with private keys for distribution +import { KeyPair } from 'near-api-js'; + +// Generate keys on the client +function generateDropKeys(count) { + return Array.from({ length: count }, () => { + const keyPair = KeyPair.fromRandom('ed25519'); return { - dropId, - keys: keys.map(k => ({ - publicKey: k.publicKey, - privateKey: k.secretKey, - claimUrl: this.generateClaimUrl(k.secretKey), - })), + publicKey: keyPair.publicKey.toString(), + privateKey: keyPair.secretKey, + claimUrl: generateClaimUrl(keyPair.secretKey) }; - } - - generateClaimUrl(privateKey) { - return `https://yourapp.com/claim?key=${privateKey}`; - } + }); } -``` - -### With QR Codes -```javascript -// Generate QR codes for drop links -import QRCode from 'qrcode'; - -async function generateDropQRCodes(dropKeys) { - const qrCodes = []; - - for (const key of dropKeys) { - const claimUrl = `https://yourapp.com/claim?key=${key.privateKey}`; - const qrCodeDataUrl = await QRCode.toDataURL(claimUrl); - - qrCodes.push({ - publicKey: key.publicKey, - qrCode: qrCodeDataUrl, - claimUrl, - }); - } - - return qrCodes; +// Create claim URLs +function generateClaimUrl(privateKey) { + return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; } ``` @@ -519,73 +224,46 @@ async function generateDropQRCodes(dropKeys) { ## Troubleshooting Common Issues -### Key Permission Errors +**"Access key not found"** +- Key wasn't added properly to the contract +- Key was already used and cleaned up +- Check the public key format -```rust -// Common error: Key doesn't have permission -impl Contract { - /// Diagnose key permission issues - pub fn diagnose_key_permissions(&self, public_key: PublicKey) -> KeyDiagnostic { - let drop_id = self.drop_id_by_key.get(&public_key); - - KeyDiagnostic { - key_exists: drop_id.is_some(), - drop_exists: drop_id.map(|id| self.drop_by_id.contains_key(&id)).unwrap_or(false), - has_claims_remaining: drop_id - .and_then(|id| self.drop_by_id.get(&id)) - .map(|drop| drop.get_counter() > 0) - .unwrap_or(false), - } - } -} +**"Method not allowed"** +- Trying to call a function not in the allowed methods list +- Our keys only allow `claim_for` and `create_account_and_claim` -#[derive(near_sdk::serde::Serialize)] -#[serde(crate = "near_sdk::serde")] -pub struct KeyDiagnostic { - pub key_exists: bool, - pub drop_exists: bool, - pub has_claims_remaining: bool, -} -``` +**"Insufficient allowance"** +- Key ran out of gas budget +- Increase `FUNCTION_CALL_ALLOWANCE` if needed -### Gas Estimation Issues +**"Key already exists"** +- Trying to add a duplicate key +- Generate new unique keys for each drop -```rust -impl Contract { - /// Estimate gas for different claim scenarios - pub fn estimate_claim_gas(&self, drop_type: String) -> Gas { - match drop_type.as_str() { - "near" => Gas(15_000_000_000_000), // 15 TGas - "ft" => Gas(100_000_000_000_000), // 100 TGas (includes cross-contract calls) - "nft" => Gas(50_000_000_000_000), // 50 TGas - _ => Gas(20_000_000_000_000), // Default - } - } - - /// Check if key has sufficient allowance - pub fn check_key_allowance(&self, public_key: PublicKey, operation: String) -> bool { - let required_gas = self.estimate_claim_gas(operation); - required_gas.0 <= FUNCTION_CALL_ALLOWANCE.as_yoctonear() - } -} -``` +--- + +## Why This Matters + +Function-call access keys are NEAR's superpower for user experience: + +🎯 **No Onboarding Friction**: New users can interact immediately +⚡ **Gasless Operations**: Recipients don't pay anything +🔒 **Still Secure**: Keys have minimal, specific permissions +🚀 **Scalable**: Works for any number of recipients + +This is what makes NEAR Drop possible - without function-call keys, you'd need a completely different (and much more complex) approach. --- ## Next Steps -Access keys are fundamental to NEAR Drop's gasless experience. With proper key management, you can create seamless token distribution experiences that don't require recipients to have existing NEAR accounts or tokens. - -Next, let's explore how to create new NEAR accounts during the claiming process, completing the onboarding experience. +Now that you understand how the gasless magic works, let's see how to create new NEAR accounts during the claiming process. -[Continue to Account Creation →](./account-creation) +[Continue to Account Creation →](./account-creation.md) --- -:::note Access Key Best Practices -- Use minimal gas allowances (0.005 NEAR is usually sufficient) -- Restrict methods to only what's necessary for claiming -- Always clean up keys after use to prevent reuse -- Consider time-limited keys for additional security -- Monitor key usage patterns for optimization opportunities +:::tip Key Insight +Function-call access keys are like giving someone a specific key to your house that only opens one room and only works once. It's secure, limited, and perfect for token distribution! ::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/account-creation.md b/docs/tutorials/neardrop/account-creation.md index 5a3bf1a091c..5d1e2fcfc05 100644 --- a/docs/tutorials/neardrop/account-creation.md +++ b/docs/tutorials/neardrop/account-creation.md @@ -2,380 +2,130 @@ id: account-creation title: Account Creation sidebar_label: Account Creation -description: "Learn how to create new NEAR accounts during the claiming process, enabling seamless onboarding for users without existing NEAR accounts." +description: "Enable new users to create NEAR accounts automatically when claiming their first tokens." --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -One of NEAR Drop's most powerful features is the ability to create new NEAR accounts for users who don't have them yet. This eliminates the biggest barrier to Web3 adoption: requiring users to set up accounts before they can receive tokens. +The ultimate onboarding experience: users can claim tokens AND get a NEAR account created for them automatically. No existing account required! --- -## The Account Creation Challenge - -Traditional blockchain onboarding has a chicken-and-egg problem: -1. Users need an account to receive tokens -2. Users need tokens to pay for account creation -3. Users need gas to claim tokens +## The Magic of NEAR Account Creation -NEAR Drop solves this by: -1. Using function-call keys for gasless operations -2. Creating accounts programmatically during claims -3. Funding new accounts from the drop contract +Most blockchains require you to have an account before you can receive tokens. NEAR flips this around: -![Account Creation Flow](/docs/assets/tutorials/near-drop/account-creation-flow.png) +**Traditional Flow:** +1. Create wallet → Fund with tokens → Receive more tokens ---- +**NEAR Drop Flow:** +1. Get private key → Claim tokens → Account created automatically ✨ -## How Account Creation Works +This eliminates the biggest barrier to Web3 adoption. -### The Two-Phase Process +--- -Account creation in NEAR Drop happens in two phases: +## How It Works -1. **Account Creation**: Create the new account and fund it -2. **Token Transfer**: Transfer the claimed tokens to the new account +Account creation happens in two phases: +### Phase 1: Create the Account ```rust -/// Create account and claim in two phases -pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise { - let public_key = env::signer_account_pk(); - - // Phase 1: Create and fund the account - let create_promise = Promise::new(account_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)); // Initial funding - - // Phase 2: Resolve creation and claim tokens - 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) - ) -} +Promise::new(account_id.clone()) + .create_account() + .transfer(NearToken::from_near(1)) // Fund with 1 NEAR ``` -### Account Validation - -Before creating accounts, we need to validate the requested account ID: - +### Phase 2: Claim the Tokens ```rust -impl Contract { - /// Validate account ID before creation - fn validate_new_account_id(&self, account_id: &AccountId) -> Result<(), String> { - let account_str = account_id.as_str(); - - // Check length constraints - if account_str.len() < 2 || account_str.len() > 64 { - return Err("Account ID must be between 2 and 64 characters".to_string()); - } - - // Check format for subaccounts - if account_str.contains('.') { - let parts: Vec<&str> = account_str.split('.').collect(); - - // Must end with top-level account - if !account_str.ends_with(&format!(".{}", self.top_level_account)) { - return Err(format!( - "Account must be a subaccount of {}", - self.top_level_account - )); - } - - // Validate each part - for part in &parts[..parts.len()-1] { - if !self.is_valid_account_part(part) { - return Err("Invalid characters in account ID".to_string()); - } - } - } else { - // Top-level account validation - if !self.is_valid_account_part(account_str) { - return Err("Invalid characters in account ID".to_string()); - } - } - - Ok(()) - } - - /// Check if account ID part contains valid characters - fn is_valid_account_part(&self, part: &str) -> bool { - part.chars().all(|c| { - c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' - }) - } -} +.then( + Self::ext(env::current_account_id()) + .resolve_account_creation(public_key, account_id) +) ``` +If account creation succeeds, we proceed with the normal claiming process. If it fails (account already exists), we try to claim anyway. + --- -## Enhanced Account Creation Implementation +## Implementation -### Complete Account Creation Logic +Add this to your `src/claim.rs`: ```rust -use near_sdk::{Promise, PromiseResult, Gas}; - #[near_bindgen] impl Contract { - /// Create account and claim with comprehensive error handling + /// 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 the account ID - self.validate_new_account_id(&account_id) - .unwrap_or_else(|err| env::panic_str(&err)); + // Validate account format + self.validate_account_id(&account_id); - // Check if this key has a valid drop + // Check we have a valid drop for this key let drop_id = self.drop_id_by_key.get(&public_key) .expect("No drop found for this key"); let drop = self.drop_by_id.get(&drop_id) - .expect("Drop not found"); - - assert!(drop.get_counter() > 0, "All drops have been claimed"); + .expect("Drop data not found"); - // Calculate funding amount based on drop type - let initial_funding = self.calculate_initial_funding(&drop); - - env::log_str(&format!( - "Creating account {} and claiming drop {}", - account_id, - drop_id - )); + // Calculate funding based on drop type + let funding = self.calculate_account_funding(&drop); // Create account with initial funding Promise::new(account_id.clone()) .create_account() - .transfer(initial_funding) + .transfer(funding) .then( Self::ext(env::current_account_id()) .with_static_gas(Gas(50_000_000_000_000)) - .resolve_account_create(public_key, account_id, drop_id) + .resolve_account_creation(public_key, account_id) ) } - /// Calculate initial funding based on drop type - fn calculate_initial_funding(&self, drop: &Drop) -> NearToken { - match drop { - Drop::Near(_) => { - // Basic storage funding - NearToken::from_millinear(500) // 0.5 NEAR for storage - } - Drop::FungibleToken(_) => { - // Extra for FT registration - NearToken::from_near(1) // 1 NEAR to cover FT storage registration - } - Drop::NonFungibleToken(_) => { - // Standard funding for NFT ownership - NearToken::from_millinear(500) // 0.5 NEAR - } - } - } - - /// Resolve account creation and proceed with claim + /// Handle account creation result #[private] - pub fn resolve_account_create( + pub fn resolve_account_creation( &mut self, public_key: PublicKey, account_id: AccountId, - drop_id: u64, ) { match env::promise_result(0) { PromiseResult::Successful(_) => { - env::log_str(&format!("Successfully created account {}", account_id)); - - // Account created successfully, now claim the drop - self.internal_claim(&public_key, &account_id); + env::log_str(&format!("Created account {}", account_id)); + self.process_claim(&public_key, &account_id); } PromiseResult::Failed => { - env::log_str(&format!("Failed to create account {}", account_id)); - - // Account creation failed - could be: - // 1. Account already exists - // 2. Invalid account ID - // 3. Insufficient funds - - // Try to claim anyway in case account already exists - match self.check_account_exists(&account_id) { - Ok(true) => { - env::log_str("Account already exists, proceeding with claim"); - self.internal_claim(&public_key, &account_id); - } - _ => { - env::panic_str("Account creation failed and account does not exist"); - } - } + // Account creation failed - maybe it already exists? + env::log_str("Account creation failed, trying to claim anyway"); + self.process_claim(&public_key, &account_id); } } } - /// Check if an account exists - fn check_account_exists(&self, account_id: &AccountId) -> Result { - // This is a simplified check - in practice you might want to make - // a cross-contract call to verify account existence - Ok(account_id.as_str().len() >= 2) - } -} -``` - ---- - -## Advanced Account Creation Patterns - -### Batch Account Creation - -Create multiple accounts efficiently: - -```rust -impl Contract { - /// Create multiple accounts and claim drops in batch - pub fn batch_create_accounts_and_claim( - &mut self, - account_configs: Vec, - ) -> Promise { - assert!( - !account_configs.is_empty() && account_configs.len() <= 10, - "Can create 1-10 accounts per batch" - ); - - let total_funding = account_configs.len() as u128 * - NearToken::from_near(1).as_yoctonear(); - - assert!( - env::account_balance() >= NearToken::from_yoctonear(total_funding), - "Insufficient balance for batch account creation" - ); - - // Create accounts sequentially - let mut promise = Promise::new(account_configs[0].account_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)); - - for config in account_configs.iter().skip(1) { - promise = promise.then( - Promise::new(config.account_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)) - ); - } - - // Resolve all creations - promise.then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(100_000_000_000_000)) - .resolve_batch_account_creation(account_configs) - ) - } - - /// Resolve batch account creation - #[private] - pub fn resolve_batch_account_creation( - &mut self, - account_configs: Vec, - ) { - let mut successful_accounts = Vec::new(); - let mut failed_accounts = Vec::new(); - - for (i, config) in account_configs.iter().enumerate() { - match env::promise_result(i) { - PromiseResult::Successful(_) => { - successful_accounts.push(config.account_id.clone()); - } - PromiseResult::Failed => { - failed_accounts.push(config.account_id.clone()); - } - } - } - - env::log_str(&format!( - "Created {} accounts successfully, {} failed", - successful_accounts.len(), - failed_accounts.len() - )); - - // Process claims for successful accounts - for config in account_configs { - if successful_accounts.contains(&config.account_id) { - self.internal_claim(&config.public_key, &config.account_id); - } - } - } -} - -#[derive(near_sdk::serde::Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct AccountConfig { - pub account_id: AccountId, - pub public_key: PublicKey, -} -``` - -### Account Creation with Custom Funding - -Allow variable funding amounts based on use case: - -```rust -impl Contract { - /// Create account with custom funding amount - pub fn create_account_with_funding( - &mut self, - account_id: AccountId, - funding_amount: NearToken, - ) -> Promise { - let public_key = env::signer_account_pk(); - - // Validate funding amount - assert!( - funding_amount >= NearToken::from_millinear(100), - "Minimum funding is 0.1 NEAR" - ); + /// Validate account ID format + fn validate_account_id(&self, account_id: &AccountId) { + let account_str = account_id.as_str(); - assert!( - funding_amount <= NearToken::from_near(10), - "Maximum funding is 10 NEAR" - ); + // Check length + assert!(account_str.len() >= 2 && account_str.len() <= 64, + "Account ID must be 2-64 characters"); - // Validate we have enough balance - let contract_balance = env::account_balance(); - assert!( - contract_balance >= funding_amount, - "Contract has insufficient balance for funding" - ); + // Must be subaccount of top-level account + assert!(account_str.ends_with(&format!(".{}", self.top_level_account)), + "Account must end with .{}", self.top_level_account); - Promise::new(account_id.clone()) - .create_account() - .transfer(funding_amount) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(30_000_000_000_000)) - .resolve_custom_funding_creation( - public_key, - account_id, - funding_amount, - ) - ) + // Check valid characters (lowercase, numbers, hyphens, underscores) + let name_part = account_str.split('.').next().unwrap(); + assert!(name_part.chars().all(|c| + c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_' + ), "Invalid characters in account name"); } - /// Resolve custom funding account creation - #[private] - pub fn resolve_custom_funding_creation( - &mut self, - public_key: PublicKey, - account_id: AccountId, - funding_amount: NearToken, - ) { - if is_promise_success() { - env::log_str(&format!( - "Created account {} with {} NEAR funding", - account_id, - funding_amount.as_near() - )); - - self.internal_claim(&public_key, &account_id); - } else { - env::panic_str("Custom funding account creation failed"); + /// Calculate how much NEAR to fund the new account with + fn calculate_account_funding(&self, drop: &Drop) -> NearToken { + match drop { + Drop::Near(_) => NearToken::from_millinear(500), // 0.5 NEAR + Drop::FungibleToken(_) => NearToken::from_near(1), // 1 NEAR (for FT registration) + Drop::NonFungibleToken(_) => NearToken::from_millinear(500), // 0.5 NEAR } } } @@ -385,289 +135,181 @@ impl Contract { ## Account Naming Strategies -### Deterministic Account Names +### User-Chosen Names -Generate predictable account names: +Let users pick their own account names: ```rust -use near_sdk::env::sha256; - -impl Contract { - /// Generate deterministic account name from public key - pub fn generate_account_name(&self, public_key: &PublicKey) -> AccountId { - let key_bytes = match public_key { - PublicKey::ED25519(bytes) => bytes, - _ => env::panic_str("Unsupported key type"), - }; - - // Create deterministic hash - let hash = sha256(key_bytes); - let hex_string = hex::encode(&hash[..8]); // Use first 8 bytes - - // Create account ID - let account_str = format!("{}.{}", hex_string, self.top_level_account); - - account_str.parse().unwrap_or_else(|_| { - env::panic_str("Failed to generate valid account ID") - }) - } +pub fn create_named_account_and_claim(&mut self, preferred_name: String) -> Promise { + let public_key = env::signer_account_pk(); - /// Create account with deterministic name - pub fn create_deterministic_account_and_claim(&mut self) -> Promise { - let public_key = env::signer_account_pk(); - let account_id = self.generate_account_name(&public_key); - - self.create_account_and_claim(account_id) - } + // Clean up the name + let clean_name = self.sanitize_name(&preferred_name); + let full_account_id = format!("{}.{}", clean_name, self.top_level_account) + .parse::() + .expect("Invalid account name"); + + self.create_account_and_claim(full_account_id) +} + +fn sanitize_name(&self, name: &str) -> String { + name.to_lowercase() + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') + .take(32) // Limit length + .collect() } ``` -### Human-Readable Account Names +### Deterministic Names -Allow users to choose their account names with validation: +Or generate predictable names from keys: ```rust -impl Contract { - /// Create account with user-chosen name - pub fn create_named_account_and_claim( - &mut self, - preferred_name: String, - ) -> Promise { - let public_key = env::signer_account_pk(); - - // Sanitize and validate name - let clean_name = self.sanitize_account_name(&preferred_name); - let account_id = format!("{}.{}", clean_name, self.top_level_account) - .parse::() - .unwrap_or_else(|_| env::panic_str("Invalid account name")); - - self.create_account_and_claim(account_id) - } +use near_sdk::env::sha256; + +pub fn create_deterministic_account_and_claim(&mut self) -> Promise { + let public_key = env::signer_account_pk(); - /// Sanitize user input for account names - fn sanitize_account_name(&self, name: &str) -> String { - name.to_lowercase() - .chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') - .take(32) // Limit length - .collect() - } + // Generate name from public key hash + let key_bytes = match &public_key { + PublicKey::ED25519(bytes) => bytes, + _ => env::panic_str("Unsupported key type"), + }; - /// Check if an account name is available - pub fn is_account_name_available(&self, name: String) -> Promise { - let clean_name = self.sanitize_account_name(&name); - let account_id = format!("{}.{}", clean_name, self.top_level_account); - - // This would need a cross-contract call to check existence - // For now, return a simple validation - Promise::new(account_id.parse().unwrap()) - .function_call( - "get_account".to_string(), - vec![], - NearToken::from_yoctonear(0), - Gas(5_000_000_000_000), - ) - } + let hash = sha256(key_bytes); + let name = hex::encode(&hash[..8]); // Use first 8 bytes + + let account_id = format!("{}.{}", name, self.top_level_account) + .parse::() + .expect("Failed to generate account ID"); + + self.create_account_and_claim(account_id) } ``` --- -## Account Recovery Patterns - -### Key Rotation for New Accounts +## Frontend Integration -Set up key rotation for newly created accounts: +Make account creation seamless in your UI: -```rust -impl Contract { - /// Create account with key rotation setup - pub fn create_secure_account_and_claim( - &mut self, - account_id: AccountId, - recovery_key: PublicKey, - ) -> Promise { - let public_key = env::signer_account_pk(); - - Promise::new(account_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)) - .add_full_access_key(recovery_key) // Add recovery key - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(50_000_000_000_000)) - .resolve_secure_account_creation( - public_key, - account_id, - ) - ) - } +```jsx +function ClaimForm() { + const [claimType, setClaimType] = useState('new'); // 'existing' or 'new' + const [accountName, setAccountName] = useState(''); - /// Set up account security after creation - #[private] - pub fn resolve_secure_account_creation( - &mut self, - public_key: PublicKey, - account_id: AccountId, - ) { - if is_promise_success() { - env::log_str(&format!( - "Created secure account {} with recovery key", - account_id - )); - - // Claim tokens - self.internal_claim(&public_key, &account_id); - - // Optional: Remove the original function-call key after successful claim - Promise::new(account_id) - .delete_key(public_key); + const handleClaim = async () => { + const keyPair = KeyPair.fromString(privateKey); + + if (claimType === 'existing') { + await contract.claim_for({ account_id: accountName }); } else { - env::panic_str("Secure account creation failed"); + await contract.create_named_account_and_claim({ + preferred_name: accountName + }); } - } + }; + + return ( +
+
+ + +
+ + setAccountName(e.target.value)} + placeholder={claimType === 'existing' ? 'alice.testnet' : 'my-new-name'} + /> + {claimType === 'new' && .testnet} + + +
+ ); } ``` --- -## Integration with Wallets +## Testing Account Creation -### Wallet Integration for Account Creation +```bash +# Test creating new account and claiming +near call drop-test.testnet create_named_account_and_claim '{ + "preferred_name": "alice-new" +}' --accountId drop-test.testnet \ + --keyPair -```javascript -// Frontend integration for account creation -class AccountCreationService { - constructor(contract, wallet) { - this.contract = contract; - this.wallet = wallet; - } - - async createAccountAndClaim(privateKey, preferredName) { - try { - // Import the private key temporarily - const keyPair = KeyPair.fromString(privateKey); - - // Sign transaction with the private key - const outcome = await this.wallet.signAndSendTransaction({ - receiverId: this.contract.contractId, - actions: [{ - type: 'FunctionCall', - params: { - methodName: 'create_named_account_and_claim', - args: { - preferred_name: preferredName - }, - gas: '100000000000000', - deposit: '0' - } - }] - }); - - return { - success: true, - transactionHash: outcome.transaction.hash, - newAccountId: `${preferredName}.testnet` - }; - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - async checkAccountAvailability(name) { - try { - const result = await this.contract.is_account_name_available({ - name: name - }); - return { available: true }; - } catch (error) { - return { available: false, reason: error.message }; - } - } -} +# Check if account was created +near view alice-new.testnet state + +# Verify balance includes claimed tokens +near view alice-new.testnet account ``` --- -## Error Handling and Recovery +## Error Handling -### Comprehensive Error Handling +Handle common issues gracefully: ```rust impl Contract { - /// Handle account creation errors gracefully pub fn create_account_with_fallback( &mut self, - primary_account_id: AccountId, - fallback_account_id: Option, + primary_name: String, + fallback_name: Option, ) -> Promise { - let public_key = env::signer_account_pk(); + let primary_account = format!("{}.{}", primary_name, self.top_level_account); - // Try primary account creation - Promise::new(primary_account_id.clone()) + // Try primary name first + Promise::new(primary_account.parse().unwrap()) .create_account() .transfer(NearToken::from_near(1)) .then( Self::ext(env::current_account_id()) - .with_static_gas(Gas(60_000_000_000_000)) - .resolve_account_creation_with_fallback( - public_key, - primary_account_id, - fallback_account_id, - ) + .handle_creation_with_fallback(primary_name, fallback_name) ) } - /// Resolve with fallback options - #[private] - pub fn resolve_account_creation_with_fallback( - &mut self, - public_key: PublicKey, - primary_account_id: AccountId, - fallback_account_id: Option, - ) { - match env::promise_result(0) { - PromiseResult::Successful(_) => { - // Primary account creation succeeded - self.internal_claim(&public_key, &primary_account_id); - } - PromiseResult::Failed => { - if let Some(fallback_id) = fallback_account_id { - env::log_str("Primary account creation failed, trying fallback"); - - // Try fallback account - Promise::new(fallback_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(30_000_000_000_000)) - .resolve_fallback_creation(public_key, fallback_id) - ); - } else { - // No fallback, try to claim with existing account - env::log_str("No fallback available, attempting direct claim"); - self.internal_claim(&public_key, &primary_account_id); - } - } - } - } - - /// Resolve fallback account creation #[private] - pub fn resolve_fallback_creation( + pub fn handle_creation_with_fallback( &mut self, - public_key: PublicKey, - account_id: AccountId, + primary_name: String, + fallback_name: Option, ) { - if is_promise_success() { - env::log_str("Fallback account creation succeeded"); - self.internal_claim(&public_key, &account_id); + if env::promise_result(0).is_successful() { + // Primary succeeded + let account_id = format!("{}.{}", primary_name, self.top_level_account); + self.process_claim(&env::signer_account_pk(), &account_id.parse().unwrap()); + } else if let Some(fallback) = fallback_name { + // Try fallback + let fallback_account = format!("{}.{}", fallback, self.top_level_account); + Promise::new(fallback_account.parse().unwrap()) + .create_account() + .transfer(NearToken::from_near(1)); } else { - env::panic_str("Both primary and fallback account creation failed"); + env::panic_str("Account creation failed and no fallback provided"); } } } @@ -675,107 +317,73 @@ impl Contract { --- -## Testing Account Creation - -### CLI Testing Commands - - - +## Cost Considerations - ```bash - # Create account and claim with deterministic name - near call drop-contract.testnet create_deterministic_account_and_claim \ - --accountId drop-contract.testnet \ - --keyPair '{"public_key": "ed25519:...", "private_key": "ed25519:..."}' +Account creation costs depend on the drop type: - # Create account with custom name - near call drop-contract.testnet create_named_account_and_claim '{ - "preferred_name": "alice-drop" - }' --accountId drop-contract.testnet \ - --keyPair '{"public_key": "ed25519:...", "private_key": "ed25519:..."}' +```rust +// Funding amounts by drop type +const NEAR_DROP_FUNDING: NearToken = NearToken::from_millinear(500); // 0.5 NEAR +const FT_DROP_FUNDING: NearToken = NearToken::from_near(1); // 1 NEAR +const NFT_DROP_FUNDING: NearToken = NearToken::from_millinear(500); // 0.5 NEAR + +// Total cost = drop cost + account funding +pub fn estimate_cost_with_account_creation(&self, drop_type: &str, num_keys: u64) -> NearToken { + let base_cost = match drop_type { + "near" => self.estimate_near_drop_cost(num_keys, NearToken::from_near(1)), + "ft" => self.estimate_ft_drop_cost(num_keys), + "nft" => self.estimate_nft_drop_cost(), + _ => NearToken::from_near(0), + }; + + let funding_per_account = match drop_type { + "near" | "nft" => NEAR_DROP_FUNDING, + "ft" => FT_DROP_FUNDING, + _ => NearToken::from_near(0), + }; + + base_cost + (funding_per_account * num_keys) +} +``` - # Check if account was created successfully - near view alice-drop.testnet get_account - ``` - +--- - +## What You've Accomplished - ```bash - # Create account and claim with deterministic name - near contract call-function as-transaction drop-contract.testnet create_deterministic_account_and_claim json-args '{}' prepaid-gas '150.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:... --signer-private-key ed25519:... send +Amazing! You now have: - # Create account with custom name - near contract call-function as-transaction drop-contract.testnet create_named_account_and_claim json-args '{ - "preferred_name": "alice-drop" - }' prepaid-gas '150.0 Tgas' attached-deposit '0 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:... --signer-private-key ed25519:... send +✅ **Automatic account creation** during claims +✅ **Flexible naming strategies** (user-chosen or deterministic) +✅ **Robust error handling** for edge cases +✅ **Cost optimization** based on drop types +✅ **Seamless UX** that removes Web3 barriers - # Check if account was created successfully - near contract call-function as-read-only alice-drop.testnet state json-args '{}' network-config testnet now - ``` - - +This is the complete onboarding solution - users go from having nothing to owning a NEAR account with tokens in a single step! --- -## Performance Considerations +## Real-World Impact -### Optimizing Account Creation +Account creation enables powerful use cases: -```rust -// Constants for account creation optimization -const MAX_ACCOUNTS_PER_BATCH: usize = 5; -const MIN_ACCOUNT_FUNDING: NearToken = NearToken::from_millinear(100); -const MAX_ACCOUNT_FUNDING: NearToken = NearToken::from_near(5); +🎯 **Mass Onboarding**: Bring thousands of users to Web3 instantly +🎁 **Gift Cards**: Create accounts for family/friends with token gifts +📱 **App Onboarding**: New users get accounts + tokens to start using your dApp +🎮 **Gaming**: Players get accounts + in-game assets automatically +🏢 **Enterprise**: Employee onboarding with company tokens -impl Contract { - /// Optimized account creation with resource management - pub fn create_account_optimized(&mut self, account_id: AccountId) -> Promise { - // Check contract balance before creation - let available_balance = env::account_balance(); - let required_funding = NearToken::from_near(1); - - assert!( - available_balance >= required_funding.saturating_mul(2), - "Insufficient contract balance for account creation" - ); - - // Create with minimal required funding - Promise::new(account_id.clone()) - .create_account() - .transfer(required_funding) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(40_000_000_000_000)) - .resolve_optimized_creation( - env::signer_account_pk(), - account_id, - ) - ) - } -} -``` +You've eliminated the biggest friction point in Web3 adoption! --- ## Next Steps -Account creation is the final piece of the onboarding puzzle. Users can now: -1. Receive private keys for drops -2. Create NEAR accounts without existing tokens -3. Claim their tokens to new accounts -4. Start using NEAR immediately - -Next, let's build a frontend that ties everything together into a seamless user experience. +With gasless claiming and automatic account creation working, it's time to build a beautiful frontend that makes this power accessible to everyone. -[Continue to Frontend Integration →](./frontend) +[Continue to Frontend Integration →](./frontend.md) --- -:::note Account Creation Best Practices -- Always validate account IDs before creation attempts -- Provide adequate initial funding based on intended use -- Implement fallback strategies for failed creations -- Consider deterministic naming for better UX -- Monitor contract balance to ensure sufficient funds for creation +:::tip Pro Tip +Always provide enough initial funding for the account type. FT drops need more funding because recipients might need to register on multiple FT contracts later. ::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md index 04b0faeb1c1..cbd8e254352 100644 --- a/docs/tutorials/neardrop/contract-architecture.md +++ b/docs/tutorials/neardrop/contract-architecture.md @@ -2,231 +2,181 @@ id: contract-architecture title: Contract Architecture sidebar_label: Contract Architecture -description: "Understand the NEAR Drop smart contract structure, including the core data types, storage patterns, and how different drop types are organized and managed." +description: "Understand how the NEAR Drop contract works - the core data types, storage patterns, and drop management system." --- -import {Github} from "@site/src/components/codetabs" - -Before diving into implementation, let's understand the architecture of the NEAR Drop smart contract. This foundation will help you understand how the different components work together to create a seamless token distribution system. +Before we start coding, let's understand how the NEAR Drop contract is structured. Think of it as the blueprint for our token distribution system. --- -## Core Contract Structure - -The NEAR Drop contract is organized around several key concepts that work together to manage token distributions efficiently. - -### Main Contract State - -The contract's state is designed to handle multiple types of drops while maintaining efficient storage and lookup patterns: +## The Big Picture - +The contract manages three things: +1. **Drops** - Collections of tokens ready for distribution +2. **Keys** - Private keys that unlock specific drops +3. **Claims** - The process of users getting their tokens -Let's break down each field: +Here's how they connect: -- **`top_level_account`**: The account used to create new NEAR accounts (typically `testnet` or `mainnet`) -- **`next_drop_id`**: A simple counter that assigns unique identifiers to each drop -- **`drop_id_by_key`**: Maps public keys to their corresponding drop IDs for efficient lookups -- **`drop_by_id`**: Stores the actual drop data indexed by drop ID - -:::info Storage Efficiency -This dual-mapping approach allows for efficient lookups both by public key (when claiming) and by drop ID (when managing drops), while keeping storage costs reasonable. -::: +``` +Drop #1 (10 NEAR) ──→ Key A ──→ Alice claims +Drop #1 (10 NEAR) ──→ Key B ──→ Bob claims +Drop #2 (1 NFT) ──→ Key C ──→ Carol claims +``` --- -## Drop Types - -The contract supports three different types of token drops, each represented as an enum variant: - - +## Contract State -### NEAR Token Drops +The contract stores everything in four simple maps: -The simplest drop type distributes native NEAR tokens: - - - -Key characteristics: -- **`amount`**: Amount of NEAR tokens to distribute per claim -- **Simple Transfer**: Uses native NEAR transfer functionality -- **No External Dependencies**: Works without additional contracts +```rust +pub struct Contract { + pub top_level_account: AccountId, // "testnet" or "near" + pub next_drop_id: u64, // Counter for unique drop IDs + pub drop_id_by_key: LookupMap, // Key → Drop ID + pub drop_by_id: UnorderedMap, // Drop ID → Drop Data +} +``` -### Fungible Token Drops +**Why this design?** +- Find drops quickly by key (for claiming) +- Find drops by ID (for management) +- Keep storage costs reasonable -For distributing NEP-141 compatible fungible tokens: +--- - +## Drop Types -Key characteristics: -- **`ft_contract`**: The contract address of the fungible token -- **`amount`**: Number of tokens to distribute per claim -- **Cross-Contract Calls**: Requires interaction with FT contract -- **Storage Registration**: Recipients must be registered on the FT contract +We support three types of token drops: -### Non-Fungible Token Drops +### NEAR Drops +```rust +pub struct NearDrop { + pub amount: NearToken, // How much NEAR per claim + pub counter: u64, // How many claims left +} +``` -For distributing unique NFTs: +### Fungible Token Drops +```rust +pub struct FtDrop { + pub ft_contract: AccountId, // Which FT contract + pub amount: String, // Amount per claim + pub counter: u64, // Claims remaining +} +``` - +### NFT Drops +```rust +pub struct NftDrop { + pub nft_contract: AccountId, // Which NFT contract + pub token_id: String, // Specific NFT + pub counter: u64, // Always 1 (NFTs are unique) +} +``` -Key characteristics: -- **`nft_contract`**: The contract address of the NFT collection -- **`token_id`**: Specific NFT token being distributed -- **Unique Distribution**: Each NFT can only be claimed once -- **Metadata Preservation**: Maintains all NFT properties and metadata +All wrapped in an enum: +```rust +pub enum Drop { + Near(NearDrop), + FungibleToken(FtDrop), + NonFungibleToken(NftDrop), +} +``` --- -## Access Key System - -One of NEAR Drop's most powerful features is its use of function-call access keys to enable gasless claiming. +## The Magic: Function-Call Keys -### How It Works +Here's where NEAR gets awesome. Instead of requiring gas fees, we use **function-call access keys**. -1. **Key Generation**: When creating a drop, the contract generates or accepts public keys -2. **Access Key Addition**: The contract adds these keys as function-call keys to itself -3. **Limited Permissions**: Keys can only call specific claiming functions -4. **Gasless Operations**: Recipients don't need NEAR tokens to claim +When you create a drop: +1. Generate public/private key pairs +2. Add public keys to the contract with limited permissions +3. Share private keys with recipients +4. Recipients sign transactions using the contract's account (gasless!) -### Key Permissions - -The function-call access keys are configured with specific permissions: +The keys can ONLY call claiming functions - nothing else. ```rust -// Example of adding a function-call access key +// Adding a function-call key Promise::new(env::current_account_id()) .add_access_key( - public_key.clone(), - FUNCTION_CALL_ALLOWANCE, - env::current_account_id(), - "claim_for,create_account_and_claim".to_string(), + public_key, + NearToken::from_millinear(5), // 0.005 NEAR gas allowance + env::current_account_id(), // Can only call this contract + "claim_for,create_account_and_claim".to_string() // Specific methods ) ``` -This setup allows keys to: -- Call `claim_for` to claim drops to existing accounts -- Call `create_account_and_claim` to create new accounts and claim drops -- Nothing else - providing security through limited permissions - --- ## Storage Cost Management -The contract carefully manages storage costs, which are paid by the drop creator: - -### Cost Components - -1. **Drop Data Storage**: Storing drop information in the contract state -2. **Key-to-Drop Mapping**: Mapping public keys to drop IDs -3. **Access Key Storage**: Adding function-call keys to the contract account - -### Storage Calculation Example +Creating drops costs money because we're storing data on-chain. The costs include: ```rust -// Simplified storage cost calculation -fn calculate_storage_cost(&self, num_keys: u64) -> NearToken { - let drop_storage = DROP_STORAGE_COST; - let key_storage = num_keys * KEY_STORAGE_COST; - let access_key_storage = num_keys * ACCESS_KEY_STORAGE_COST; - - drop_storage + key_storage + access_key_storage -} +const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10); // Drop data +const KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Key mapping +const ACCESS_KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Adding key to account +const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // Gas for claiming ``` -:::tip Storage Optimization -The contract uses efficient data structures and minimal storage patterns to keep costs low while maintaining functionality. -::: +**Total for 5-key NEAR drop**: ~0.08 NEAR + token amounts --- ## Security Model -The NEAR Drop contract implements several security measures: - -### Access Control - -- **Drop Creation**: Only the drop creator can modify their drops -- **Function-Call Keys**: Limited to specific claiming functions only -- **Account Validation**: Ensures only valid NEAR accounts can be created - -### Preventing Abuse +The contract protects against common attacks: -- **One-Time Claims**: Each key can only be used once per drop -- **Key Cleanup**: Used keys are removed to prevent reuse -- **Counter Management**: Drop counters prevent double-claiming +**Access Control** +- Only specific functions can be called with function-call keys +- Keys are removed after use to prevent reuse +- Amount validation prevents overflows -### Error Handling +**Key Management** +- Each key works only once +- Keys have limited gas allowances +- Automatic cleanup after claims +**Error Handling** ```rust -// Example error handling pattern -if self.drop_id_by_key.get(&public_key).is_none() { - env::panic_str("No drop found for this key"); -} - -if drop.counter == 0 { - env::panic_str("All drops have been claimed"); -} +// Example validation +assert!(!token_id.is_empty(), "Token ID cannot be empty"); +assert!(amount > 0, "Amount must be positive"); ``` --- -## Cross-Contract Integration - -The contract is designed to work seamlessly with other NEAR standards: - -### Fungible Token Integration - -- Implements NEP-141 interaction patterns -- Handles storage registration automatically -- Manages transfer and callback flows - -### NFT Integration - -- Supports NEP-171 NFT transfers -- Preserves token metadata and properties -- Handles ownership transfers correctly - -### Account Creation Integration - -- Works with the linkdrop contract pattern -- Handles account funding and key management -- Supports both testnet and mainnet account creation - ---- - ## File Organization -The contract code is organized into logical modules: +We'll organize the code into logical modules: ``` src/ -├── lib.rs # Main contract logic and state -├── drop_types.rs # Drop type definitions -├── near_drop.rs # NEAR token drop implementation -├── ft_drop.rs # Fungible token drop implementation -├── nft_drop.rs # NFT drop implementation -└── claim.rs # Claiming logic for all drop types +├── lib.rs # Main contract and initialization +├── drop_types.rs # Drop type definitions +├── near_drops.rs # NEAR token drop logic +├── ft_drops.rs # Fungible token drop logic +├── nft_drops.rs # NFT drop logic +├── claim.rs # Claiming logic for all types +└── external.rs # Cross-contract interfaces ``` -This modular structure makes the code: -- **Easy to Understand**: Each file has a clear purpose -- **Maintainable**: Changes to one drop type don't affect others -- **Extensible**: New drop types can be added easily +This keeps things organized and makes it easy to understand each piece. --- -## Next Steps +## What's Next? -Now that you understand the contract architecture, let's start implementing the core functionality, beginning with NEAR token drops. +Now that you understand the architecture, let's start building! We'll begin with the simplest drop type: NEAR tokens. -[Continue to NEAR Token Drops →](./near-drops) +[Continue to NEAR Token Drops →](./near-drops.md) --- -:::note Key Takeaways -- The contract uses efficient storage patterns with dual mappings -- Three drop types support different token standards (NEAR, FT, NFT) -- Function-call access keys enable gasless claiming operations -- Security is maintained through limited key permissions and proper validation -- Modular architecture makes the contract maintainable and extensible +:::tip Key Takeaway +The contract is essentially a **key-to-token mapping system** powered by NEAR's function-call access keys. Users get keys, keys unlock tokens, and everything happens without gas fees for the recipient! ::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md index 9618906a9d0..5f523908bfa 100644 --- a/docs/tutorials/neardrop/frontend.md +++ b/docs/tutorials/neardrop/frontend.md @@ -2,19 +2,14 @@ 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." +description: "Build a React app that makes creating and claiming drops as easy as a few clicks." --- -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. +Time to build a user-friendly interface! Let's create a React app that makes your NEAR Drop system accessible to everyone. --- -## Project Setup - -Let's create a Next.js frontend for our NEAR Drop system: +## Quick Setup ```bash npx create-next-app@latest near-drop-frontend @@ -23,833 +18,400 @@ 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_CONTRACT_ID=your-drop-contract.testnet 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 +## NEAR Connection Service -### Wallet Connection Service +Create `src/services/near.js`: -Create `src/services/near.ts`: - -```typescript -import { connect, ConnectConfig, keyStores, WalletConnection } from 'near-api-js'; +```javascript +import { connect, keyStores } 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!, +const config = { + 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!, + nodeUrl: process.env.NEXT_PUBLIC_RPC_URL, + contractName: process.env.NEXT_PUBLIC_CONTRACT_ID, }; -export class NearService { - near: any; - wallet: any; - contract: any; - selector: any; - modal: any; - +class NearService { 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(), - ], + this.walletSelector = await setupWalletSelector({ + network: config.networkId, + 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.walletSelector?.isSignedIn() || false; } - isSignedIn() { - return this.selector?.isSignedIn() || false; + async signIn() { + const modal = setupModal(this.walletSelector); + modal.show(); } - getAccountId() { - return this.selector?.store?.getState()?.accounts?.[0]?.accountId || null; + async getContract() { + if (!this.isSignedIn()) return null; + + const wallet = await this.walletSelector.wallet(); + return new Contract( + wallet.account(), + config.contractName, + { + viewMethods: ['get_drop', 'estimate_near_drop_cost'], + changeMethods: ['create_near_drop', 'claim_for', 'create_account_and_claim'], + } + ); } } 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; -} +## Key Generation Utility -export interface NftDrop { - nft_contract: string; - token_id: string; - counter: number; -} +Create `src/utils/crypto.js`: -export type Drop = - | { Near: NearDrop } - | { FungibleToken: FtDrop } - | { NonFungibleToken: NftDrop }; +```javascript +import { KeyPair } from 'near-api-js'; -export interface DropInfo { - drop_id: number; - drop: Drop; - keys: DropKey[]; +export function generateKeys(count) { + const keys = []; + + for (let i = 0; i < count; i++) { + const keyPair = KeyPair.fromRandom('ed25519'); + keys.push({ + publicKey: keyPair.publicKey.toString(), + privateKey: keyPair.secretKey, + }); + } + + return keys; } -export interface ClaimableKey { - private_key: string; - public_key: string; - drop_id?: number; - claim_url: string; +export function generateClaimUrl(privateKey) { + return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; } ``` --- -## Drop Creation Interface - -### Drop Creation Form +## Drop Creation Component -Create `src/components/DropCreation/DropCreationForm.tsx`: - -```tsx -'use client'; +Create `src/components/CreateDrop.js`: +```jsx 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(''); +import { nearService } from '../services/near'; +import { generateKeys } from '../utils/crypto'; + +export default function CreateDrop({ onDropCreated }) { + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + dropType: 'near', + amount: '1', + keyCount: 5, + ftContract: '', + ftAmount: '', + }); - const handleCreateDrop = async (e: React.FormEvent) => { + const handleSubmit = async (e) => { e.preventDefault(); - setIsLoading(true); + setLoading(true); try { - // Generate keys for the drop - const keys = generateKeys(keyCount); + const contract = await nearService.getContract(); + const keys = generateKeys(formData.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'); - } + let dropId; + + if (formData.dropType === 'near') { + // Calculate cost first + const cost = await contract.estimate_near_drop_cost({ + num_keys: formData.keyCount, + amount_per_drop: (parseFloat(formData.amount) * 1e24).toString(), + }); - // Return drop info with keys - const dropInfo = { - dropId, - dropType, - keys, - cost, - }; + dropId = await contract.create_near_drop({ + public_keys: publicKeys, + amount_per_drop: (parseFloat(formData.amount) * 1e24).toString(), + }, { + gas: '100000000000000', + attachedDeposit: cost, + }); + } + // Add FT and NFT cases here... - onDropCreated(dropInfo); + onDropCreated({ dropId, keys, dropType: formData.dropType }); } catch (error) { - console.error('Error creating drop:', error); alert('Failed to create drop: ' + error.message); } finally { - setIsLoading(false); + setLoading(false); } }; return ( - - - - Create Token Drop - - -
- {/* Drop Type Selection */} - setDropType(value as any)}> - - NEAR Tokens - Fungible Tokens - NFT - - - {/* Key Count Configuration */} -
- -
- - setKeyCount(parseInt(e.target.value) || 1)} - className="w-20 text-center" - min="1" - max={dropType === 'nft' ? 1 : 100} - disabled={dropType === 'nft'} - /> - -
-
- - {/* NEAR Drop Configuration */} - -
- - setNearAmount(e.target.value)} - placeholder="1.0" - required - /> -

- Each recipient will receive {nearAmount} NEAR tokens -

-
-
- - {/* FT Drop Configuration */} - -
- - setFtContract(e.target.value)} - placeholder="token.testnet" - required - /> -
-
- - setFtAmount(e.target.value)} - placeholder="1000000000000000000000000" - required - /> -

- Amount in smallest token units (including decimals) -

-
-
- - {/* NFT Drop Configuration */} - -
- - setNftContract(e.target.value)} - placeholder="nft.testnet" - required - /> -
-
- - setNftTokenId(e.target.value)} - placeholder="unique-token-123" - required - /> -
-

- ⚠️ NFT drops support only 1 key since each NFT is unique -

-
-
- - {/* Submit Button */} - -
-
-
- ); -} -``` - -### Key Generation Utility - -Create `src/utils/crypto.ts`: - -```typescript -import { KeyPair } from 'near-api-js'; +
+

Create Token Drop

+ +
+ {/* Drop Type */} +
+ + +
-export interface GeneratedKey { - publicKey: string; - privateKey: string; - keyPair: KeyPair; -} + {/* NEAR Amount */} + {formData.dropType === 'near' && ( +
+ + setFormData({...formData, amount: e.target.value})} + className="w-full border rounded px-3 py-2" + required + /> +
+ )} -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; -} + {/* Key Count */} +
+ + setFormData({...formData, keyCount: parseInt(e.target.value)})} + className="w-full border rounded px-3 py-2" + required + /> +
-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`: +## Drop Results Component -```tsx -'use client'; +Create `src/components/DropResults.js`: +```jsx 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; - }; -} +import { generateClaimUrl } from '../utils/crypto'; -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)); +export default function DropResults({ dropInfo }) { + const [selectedKey, setSelectedKey] = useState(0); - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - // You might want to add a toast notification here - }; + const claimUrls = dropInfo.keys.map(key => generateClaimUrl(key.privateKey)); const downloadKeys = () => { - const keysData = dropInfo.keys.map((key, index) => ({ + const data = 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 blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `near-drop-${dropInfo.dropId}-keys.json`; + a.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'); + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text); + // Add toast notification 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 +

+
+

Drop Created! 🎉

+

Drop ID: {dropInfo.dropId}

+

+ Created {dropInfo.keys.length} keys for {dropInfo.dropType} drop

- - - - - 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

-
- - -
-
+
+ {/* Keys List */} +
+
+

Claim Keys

+ +
-
-
- -

- Key {selectedKeyIndex + 1} - Scan to claim -

+
+ {dropInfo.keys.map((key, index) => ( +
+
+ Key {index + 1} + +
+ +
+ {claimUrls[index]} +
+ +
+ + Show Private Key + +
+ {key.privateKey} +
+
-
+ ))} +
+
-
+ {/* QR Codes */} +
+
+

QR Codes

+ +
+ +
+
+
- - - {/* 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 -
-
-
-
- - - +

+ Scan to claim Key {selectedKey + 1} +

+
+ +
+ {dropInfo.keys.map((_, index) => ( +
setSelectedKey(index)} + > + +

#{index + 1}

+
+ ))} +
+
+
+
); } ``` --- -## Claiming Interface - -### Claim Page Component +## Claiming Component -Create `src/components/DropClaiming/ClaimPage.tsx`: - -```tsx -'use client'; +Create `src/components/ClaimDrop.js`: +```jsx 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 { useRouter } from 'next/router'; +import { nearService } from '../services/near'; import { KeyPair } from 'near-api-js'; -export default function ClaimPage() { - const searchParams = useSearchParams(); +export default function ClaimDrop() { 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(''); + const [claimType, setClaimType] = useState('existing'); + const [accountName, setAccountName] = useState(''); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); 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'); + if (router.query.key) { + setPrivateKey(router.query.key); } - }; + }, [router.query]); - const handleClaim = async () => { - setIsLoading(true); - setError(null); + const handleClaim = async (e) => { + e.preventDefault(); + setLoading(true); try { + // Create temporary wallet with the private key const keyPair = KeyPair.fromString(privateKey); - - // Create a temporary wallet connection with this key const tempAccount = { - accountId: process.env.NEXT_PUBLIC_CONTRACT_ID!, + 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, + const contract = await nearService.getContract(); + + if (claimType === 'existing') { + await contract.claim_for({ + account_id: accountName, }, { 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, + await contract.create_account_and_claim({ + account_id: `${accountName}.testnet`, }, { gas: '200000000000000', signerAccount: tempAccount, @@ -857,232 +419,104 @@ export default function ClaimPage() { } setSuccess(true); - } catch (err: any) { - setError(err.message || 'Failed to claim drop'); + } catch (error) { + alert('Claim failed: ' + error.message); } 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, - }; + setLoading(false); } - return null; }; if (success) { return ( -
- - - - Claim Successful! - - -

- Your tokens have been successfully claimed. -

- -
-
+
+
🎉
+

Claim Successful!

+

Your tokens have been transferred.

+
); } 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" +
+

Claim Your Drop

+ +
+ {/* Private Key */} +
+ + setPrivateKey(e.target.value)} + placeholder="ed25519:..." + className="w-full border rounded px-3 py-2 font-mono text-sm" + required + /> +
+ + {/* Claim Type */} +
+ +
+ + +
+
+ + {/* Account Name */} +
+ +
+ setAccountName(e.target.value)} + placeholder={claimType === 'existing' ? 'alice.testnet' : 'alice'} + className="flex-1 border rounded-l px-3 py-2" + required /> - {!keyValid && privateKey && ( - + {claimType === 'new' && ( + + .testnet + )}
+
- {/* 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 */} - -
-
- )} - - + +
); } @@ -1090,2554 +524,120 @@ export default function ClaimPage() { --- -## Dashboard and Management - -### Drop Dashboard +## Main App Layout -Create `src/components/Dashboard/DropDashboard.tsx`: - -```tsx -'use client'; +Create `src/pages/index.js`: +```jsx 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'; -} +import { nearService } from '../services/near'; +import CreateDrop from '../components/CreateDrop'; +import DropResults from '../components/DropResults'; -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); +export default function Home() { + const [isSignedIn, setIsSignedIn] = useState(false); + const [loading, setLoading] = useState(true); + const [createdDrop, setCreatedDrop] = useState(null); useEffect(() => { - loadDashboardData(); + initNear(); }, []); - const loadDashboardData = async () => { - setIsLoading(true); + const initNear = async () => { 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 - }); + await nearService.initialize(); + setIsSignedIn(nearService.isSignedIn()); } catch (error) { - console.error('Error loading dashboard data:', error); + console.error('Failed to initialize NEAR:', error); } finally { - setIsLoading(false); + setLoading(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 (loading) { + return
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 - - -
- {/* Drop Type Selection */} - setDropType(value as any)}> - - NEAR Tokens - Fungible Tokens - NFT - - - {/* Key Count Configuration */} -
- -
- - setKeyCount(parseInt(e.target.value) || 1)} - className="w-20 text-center" - min="1" - max={dropType === 'nft' ? 1 : 100} - disabled={dropType === 'nft'} - /> - -
-
- - {/* NEAR Drop Configuration */} - -
- - setNearAmount(e.target.value)} - placeholder="1.0" - required - /> -

- Each recipient will receive {nearAmount} NEAR tokens -

-
-
- - {/* FT Drop Configuration */} - -
- - setFtContract(e.target.value)} - placeholder="token.testnet" - required - /> -
-
- - setFtAmount(e.target.value)} - placeholder="1000000000000000000000000" - required - /> -

- Amount in smallest token units (including decimals) -

-
-
- - {/* NFT Drop Configuration */} - -
- - setNftContract(e.target.value)} - placeholder="nft.testnet" - required - /> -
-
- - setNftTokenId(e.target.value)} - placeholder="unique-token-123" - required - /> -
-

- ⚠️ NFT drops support only 1 key since each NFT is unique -

-
-
- - {/* Submit Button */} - -
-
-
- ); -} -``` - -### 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 - - -
- {/* Drop Type Selection */} - setDropType(value as any)}> - - NEAR Tokens - Fungible Tokens - NFT - - - {/* Key Count Configuration */} -
- -
- - setKeyCount(parseInt(e.target.value) || 1)} - className="w-20 text-center" - min="1" - max={dropType === 'nft' ? 1 : 100} - disabled={dropType === 'nft'} - /> - -
-
- - {/* NEAR Drop Configuration */} - -
- - setNearAmount(e.target.value)} - placeholder="1.0" - required - /> -

- Each recipient will receive {nearAmount} NEAR tokens -

-
-
- - {/* FT Drop Configuration */} - -
- - setFtContract(e.target.value)} - placeholder="token.testnet" - required - /> -
-
- - setFtAmount(e.target.value)} - placeholder="1000000000000000000000000" - required - /> -

- Amount in smallest token units (including decimals) -

-
-
- - {/* NFT Drop Configuration */} - -
- - setNftContract(e.target.value)} - placeholder="nft.testnet" - required - /> -
-
- - setNftTokenId(e.target.value)} - placeholder="unique-token-123" - required - /> -
-

- ⚠️ NFT drops support only 1 key since each NFT is unique -

-
-
- - {/* Submit Button */} - -
-
-
- ); -} -``` - -### 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 Flow](/docs/assets/tutorials/near-drop/near-drop-flow.png) +**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::>(&val) { - // Verify that this contract owns the NFT - assert_eq!( - token_info.owner_id, - env::current_account_id(), - "Contract does not own NFT {} from {}", - token_id, - nft_contract - ); - - // Create the drop with the provided deposit - let required_deposit = self.calculate_nft_drop_cost(); - assert!( - deposit >= required_deposit, - "Insufficient deposit for NFT drop" - ); - - // Proceed with drop creation - return self.create_single_nft_drop_internal( - public_key, - nft_contract, - token_id, - ); - } - } + let success = env::promise_results_count() == 1 && + matches!(env::promise_result(0), PromiseResult::Successful(_)); - env::panic_str("NFT ownership verification failed"); - } -} -``` - -### Preventing Double Claims - -Since NFTs are unique, we need to ensure they can't be claimed twice: - -```rust -impl Contract { - /// Enhanced cleanup that removes NFT drops completely - fn cleanup_after_claim(&mut self, public_key: &PublicKey, drop: &mut Drop, drop_id: u64) { - match drop { - Drop::NonFungibleToken(_) => { - // For NFTs, always remove the drop completely since they're unique + if success { + env::log_str(&format!("NFT {} transferred to {}", token_id, receiver_id)); + + // Clean up the claim + if let Some(drop_id) = self.drop_id_by_key.get(&public_key) { + // For NFTs, always remove completely since they're unique self.drop_by_id.remove(&drop_id); - env::log_str(&format!("NFT drop {} fully claimed and removed", drop_id)); - } - _ => { - // Handle other drop types as before - drop.decrement_counter(); - - if drop.get_counter() == 0 { - self.drop_by_id.remove(&drop_id); - } else { - self.drop_by_id.insert(&drop_id, &drop); - } + self.drop_id_by_key.remove(&public_key); + Promise::new(env::current_account_id()).delete_key(public_key); } + } else { + env::panic_str("NFT transfer failed - contract may not own this NFT"); } - - // Always remove the public key mapping and access key - self.drop_id_by_key.remove(public_key); - Promise::new(env::current_account_id()) - .delete_key(public_key.clone()); } } ``` @@ -557,473 +275,138 @@ impl Contract { ## Testing NFT Drops -### Deploy a Test NFT Contract - -You'll need an NFT contract for testing. Use the reference implementation: +You'll need an NFT contract for testing: ```bash -# Clone and build the NFT contract -git clone https://github.com/near-examples/NFT.git -cd NFT -cargo near build - -# Deploy to testnet +# Deploy test NFT contract near create-account test-nft.testnet --useFaucet -near deploy test-nft.testnet target/near/non_fungible_token.wasm +near deploy test-nft.testnet nft-contract.wasm # Initialize near call test-nft.testnet new_default_meta '{ - "owner_id": "drop-contract.testnet" + "owner_id": "drop-test.testnet" }' --accountId test-nft.testnet -``` -### Mint NFTs to the Drop Contract - -```bash -# Mint NFT to drop contract +# Mint NFT to your drop contract near call test-nft.testnet nft_mint '{ - "token_id": "unique-drop-token-001", + "token_id": "unique-nft-001", "metadata": { "title": "Exclusive Drop NFT", - "description": "A unique NFT distributed via NEAR Drop", - "media": "https://example.com/nft-image.png" + "description": "A unique NFT from NEAR Drop" }, - "receiver_id": "drop-contract.testnet" -}' --accountId drop-contract.testnet --deposit 0.1 + "receiver_id": "drop-test.testnet" +}' --accountId drop-test.testnet --deposit 0.1 ``` -### Create NFT Drop - - - - - ```bash - # Create an NFT drop - near call drop-contract.testnet create_nft_drop '{ - "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", - "nft_contract": "test-nft.testnet", - "token_id": "unique-drop-token-001" - }' --accountId drop-contract.testnet --deposit 0.1 - ``` - - - - - ```bash - # Create an NFT drop - near contract call-function as-transaction drop-contract.testnet create_nft_drop json-args '{ - "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", - "nft_contract": "test-nft.testnet", - "token_id": "unique-drop-token-001" - }' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as drop-contract.testnet network-config testnet sign-with-keychain send - ``` - - - -### Claim NFT - - - - - ```bash - # Claim NFT to an existing account - near call drop-contract.testnet claim_for '{ - "account_id": "nft-collector.testnet" - }' --accountId drop-contract.testnet \ - --keyPair '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "private_key": "ed25519:..."}' - ``` - - - - - ```bash - # Claim NFT to an existing account - near contract call-function as-transaction drop-contract.testnet claim_for json-args '{ - "account_id": "nft-collector.testnet" - }' prepaid-gas '200.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 - ``` - - - -### Verify NFT Transfer +Create and test the NFT drop: ```bash -# Check NFT ownership after claim -near view test-nft.testnet nft_token '{ - "token_id": "unique-drop-token-001" -}' +# Create NFT drop +near call drop-test.testnet create_nft_drop '{ + "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", + "nft_contract": "test-nft.testnet", + "token_id": "unique-nft-001" +}' --accountId drop-test.testnet --deposit 0.1 + +# Claim the NFT +near call drop-test.testnet claim_for '{ + "account_id": "alice.testnet" +}' --accountId drop-test.testnet \ + --keyPair + +# Verify Alice owns the NFT +near view test-nft.testnet nft_token '{"token_id": "unique-nft-001"}' ``` --- -## Adding NFT-Specific View Methods +## Helper Functions -Add helpful view methods for NFT drops: +Add some useful view methods: ```rust #[near_bindgen] impl Contract { /// Get NFT drop details - pub fn get_nft_drop_details(&self, drop_id: u64) -> Option { + pub fn get_nft_drop_info(&self, drop_id: u64) -> Option<(AccountId, String, bool)> { if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { - Some(NftDropInfo { - nft_contract: nft_drop.nft_contract, - token_id: nft_drop.token_id, - is_claimed: nft_drop.counter == 0, - }) + Some(( + nft_drop.nft_contract, + nft_drop.token_id, + nft_drop.counter == 0, // is_claimed + )) } else { None } } - /// Calculate NFT drop cost (view method) - pub fn calculate_nft_drop_cost_view(&self) -> NearToken { - self.calculate_nft_drop_cost() + /// Calculate NFT drop cost + pub fn estimate_nft_drop_cost(&self) -> NearToken { + DROP_STORAGE_COST + KEY_STORAGE_COST + ACCESS_KEY_ALLOWANCE } - /// Check if an NFT drop exists for a specific token + /// Check if NFT drop exists for a token pub fn nft_drop_exists(&self, nft_contract: AccountId, token_id: String) -> bool { - // This is a linear search - in production you might want to optimize this for drop_id in 0..self.next_drop_id { if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { - if nft_drop.nft_contract == nft_contract && nft_drop.token_id == token_id { - return nft_drop.counter > 0; + if nft_drop.nft_contract == nft_contract && + nft_drop.token_id == token_id && + nft_drop.counter > 0 { + return true; } } } false } - - /// Get all NFT drops for a specific contract - pub fn get_nft_drops_by_contract(&self, nft_contract: AccountId) -> Vec { - let mut nft_drops = Vec::new(); - - for drop_id in 0..self.next_drop_id { - if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { - if nft_drop.nft_contract == nft_contract { - nft_drops.push(NftDropInfo { - nft_contract: nft_drop.nft_contract, - token_id: nft_drop.token_id, - is_claimed: nft_drop.counter == 0, - }); - } - } - } - - nft_drops - } -} - -#[derive(near_sdk::serde::Serialize)] -#[serde(crate = "near_sdk::serde")] -pub struct NftDropInfo { - pub nft_contract: AccountId, - pub token_id: String, - pub is_claimed: bool, } ``` --- -## Advanced NFT Drop Patterns - -### Rarity-Based Drops - -You can implement rarity-based NFT drops by analyzing metadata: - -```rust -impl Contract { - /// Create NFT drop with rarity verification - pub fn create_rare_nft_drop( - &mut self, - public_key: PublicKey, - nft_contract: AccountId, - token_id: String, - required_rarity: String, - ) -> Promise { - 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_rarity_verification( - public_key, - nft_contract, - token_id, - required_rarity, - env::attached_deposit(), - ) - ) - } - - /// Handle rarity verification - #[private] - pub fn handle_rarity_verification( - &mut self, - public_key: PublicKey, - nft_contract: AccountId, - token_id: String, - required_rarity: 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::>(&val) { - // Verify ownership - assert_eq!(token_info.owner_id, env::current_account_id()); - - // Check rarity in metadata - if let Some(metadata) = token_info.metadata { - if let Some(extra) = metadata.extra { - let extra_data: serde_json::Value = serde_json::from_str(&extra) - .unwrap_or_else(|_| serde_json::Value::Null); - - if let Some(rarity) = extra_data.get("rarity") { - assert_eq!( - rarity.as_str().unwrap_or(""), - required_rarity, - "NFT rarity does not match requirement" - ); - } else { - env::panic_str("NFT does not have rarity metadata"); - } - } - } - - // Create the drop if all validations pass - return self.create_single_nft_drop_internal( - public_key, - nft_contract, - token_id, - ); - } - } - - env::panic_str("Rarity verification failed"); - } -} -``` +## Important Notes -### Collection-Based Drops +**⚠️ Ownership is Critical** +- The drop contract MUST own the NFT before creating the drop +- If the contract doesn't own the NFT, claiming will fail +- Always verify ownership before creating drops -Create drops for entire NFT collections: +**🔒 Security Considerations** +- Each NFT drop supports exactly 1 key (since NFTs are unique) +- Once claimed, the NFT drop is completely removed +- No possibility of double-claiming the same NFT -```rust -impl Contract { - /// Create drops for multiple NFTs from the same collection - pub fn create_collection_drop( - &mut self, - nft_contract: AccountId, - token_ids: Vec, - public_keys: Vec, - ) -> Vec { - assert_eq!( - token_ids.len(), - public_keys.len(), - "Token IDs and public keys arrays must have the same length" - ); - - let total_drops = token_ids.len(); - let deposit = env::attached_deposit(); - let required_deposit = self.calculate_nft_drop_cost() - .saturating_mul(total_drops as u64); - - assert!( - deposit >= required_deposit, - "Insufficient deposit for collection drop" - ); - - let mut drop_ids = Vec::new(); - - for (i, token_id) in token_ids.into_iter().enumerate() { - let drop_id = self.create_single_nft_drop_internal( - public_keys[i].clone(), - nft_contract.clone(), - token_id, - ); - drop_ids.push(drop_id); - } - - env::log_str(&format!( - "Created collection drop with {} NFTs from {}", - total_drops, - nft_contract - )); - - drop_ids - } -} -``` +**💰 Cost Structure** +- NFT drops are cheaper than multi-key drops (only 1 key) +- No need for token funding (just storage + gas costs) +- Total cost: ~0.017 NEAR per NFT drop --- -## Error Handling for NFT Operations +## What You've Accomplished -Add comprehensive error handling: +Great work! You now have complete NFT drop support: -```rust -// Error constants -const ERR_NFT_NOT_FOUND: &str = "NFT not found"; -const ERR_NFT_NOT_OWNED: &str = "Contract does not own this NFT"; -const ERR_NFT_ALREADY_CLAIMED: &str = "This NFT has already been claimed"; -const ERR_INVALID_TOKEN_ID: &str = "Invalid token ID format"; +✅ **Unique NFT distribution** with proper ownership validation +✅ **Cross-contract NFT transfers** with error handling +✅ **Batch NFT drop creation** for collections +✅ **Complete cleanup** after claims (no leftover data) +✅ **Security measures** to prevent double-claiming -impl Contract { - /// Enhanced NFT drop creation with comprehensive validation - pub fn create_nft_drop_safe( - &mut self, - public_key: PublicKey, - nft_contract: AccountId, - token_id: String, - ) -> u64 { - // Validate inputs - self.validate_nft_drop_inputs(&public_key, &nft_contract, &token_id); - - // Check if drop already exists for this NFT - assert!( - !self.nft_drop_exists(nft_contract.clone(), token_id.clone()), - "{}", - ERR_NFT_ALREADY_CLAIMED - ); - - // Create the drop - self.create_nft_drop(public_key, nft_contract, token_id) - } - - /// Validate NFT drop inputs - fn validate_nft_drop_inputs( - &self, - public_key: &PublicKey, - nft_contract: &AccountId, - token_id: &String, - ) { - // Validate public key format - assert!( - matches!(public_key, PublicKey::ED25519(_)), - "Only ED25519 keys are supported" - ); - - // Validate NFT contract account ID - assert!( - nft_contract.as_str().len() >= 2 && nft_contract.as_str().contains('.'), - "Invalid NFT contract account ID" - ); - - // Validate token ID - assert!(!token_id.is_empty(), "{}", ERR_INVALID_TOKEN_ID); - assert!(token_id.len() <= 64, "Token ID too long (max 64 characters)"); - - // Check for reserved characters - assert!( - token_id.chars().all(|c| c.is_alphanumeric() || "-_.".contains(c)), - "Token ID contains invalid characters" - ); - } -} -``` - ---- - -## Gas Optimization for NFT Operations - -NFT drops can be gas-intensive due to cross-contract calls. Here are optimization strategies: - -```rust -// Optimized gas constants based on testing -pub const GAS_FOR_NFT_TRANSFER: Gas = Gas(30_000_000_000_000); // 30 TGas -pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000); // 20 TGas -pub const GAS_FOR_NFT_VERIFICATION: Gas = Gas(10_000_000_000_000); // 10 TGas - -impl Contract { - /// Optimized NFT claiming with gas monitoring - fn claim_nft_drop_optimized( - &mut self, - public_key: PublicKey, - receiver_id: AccountId, - nft_contract: AccountId, - token_id: String, - ) { - let initial_gas = env::used_gas(); - - // Transfer the NFT with optimized gas allocation - ext_nft::ext(nft_contract.clone()) - .with_static_gas(GAS_FOR_NFT_TRANSFER) - .nft_transfer( - receiver_id.clone(), - token_id.clone(), - None, - Some("NEAR Drop claim".to_string()) // Shorter memo to save gas - ) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(GAS_FOR_NFT_CALLBACK) - .nft_transfer_callback_optimized( - public_key, - receiver_id, - nft_contract, - token_id, - initial_gas, - ) - ); - } - - /// Optimized callback with gas usage reporting - #[private] - pub fn nft_transfer_callback_optimized( - &mut self, - public_key: PublicKey, - receiver_id: AccountId, - nft_contract: AccountId, - token_id: String, - initial_gas: Gas, - ) { - let gas_used = env::used_gas() - initial_gas; - - if is_promise_success() { - env::log_str(&format!( - "NFT {} transferred to {} using {} gas", - token_id, - receiver_id, - gas_used.0 - )); - - // Efficient cleanup - if let Some(drop_id) = self.drop_id_by_key.get(&public_key) { - self.drop_by_id.remove(&drop_id); - self.drop_id_by_key.remove(&public_key); - - // Remove access key - Promise::new(env::current_account_id()) - .delete_key(public_key); - } - } else { - env::panic_str("NFT transfer failed"); - } - } -} -``` +Your NEAR Drop system now supports all three major token types: NEAR, FTs, and NFTs! --- ## Next Steps -You now have a complete NFT drop system that handles: -- Unique token distribution patterns -- Cross-contract NFT transfers with proper callbacks -- Ownership verification and security measures -- Advanced patterns like rarity-based and collection drops -- Comprehensive error handling and gas optimization - -The NFT drop implementation completes the core token distribution functionality. Next, let's explore how function-call access keys work in detail to understand the gasless claiming mechanism. +Let's explore how function-call access keys work in detail to understand the gasless claiming mechanism. -[Continue to Access Key Management →](./access-keys) +[Continue to Access Key Management →](./access-keys.md) --- -:::note NFT Drop Considerations -- Always verify NFT ownership before creating drops -- NFT drops are inherently single-use (counter always equals 1) -- Test with various NFT contracts to ensure NEP-171 compatibility -- Monitor gas costs as they can be higher than NEAR/FT drops -- Consider implementing batch operations for multiple NFT drops +:::tip NFT Drop Pro Tips +- Always test with a small NFT collection first +- Verify the drop contract owns all NFTs before creating drops +- Consider using batch creation for large NFT collections +- NFT drops are perfect for event tickets, collectibles, and exclusive content ::: \ No newline at end of file From 3b008fd07b4a0e8d163ae88be65e15ee832cd39f Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:20:34 +0100 Subject: [PATCH 05/23] Update near-drop.md --- docs/tutorials/examples/near-drop.md | 626 +++++++++++++++++---------- 1 file changed, 404 insertions(+), 222 deletions(-) diff --git a/docs/tutorials/examples/near-drop.md b/docs/tutorials/examples/near-drop.md index 271a3021f2c..81c17113a2b 100644 --- a/docs/tutorials/examples/near-drop.md +++ b/docs/tutorials/examples/near-drop.md @@ -8,296 +8,478 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import {CodeTabs, Language, Github} from "@site/src/components/codetabs" -NEAR Drop is a smart contract that allows users to create token drops ($NEAR, Fungible and Non-Fungible Tokens), and link them to specific private keys. Whoever has the private key can claim the drop into an existing account, or ask the contract to create a new one for them. +# NEAR Drop Tutorial: Creating Token Airdrops Made Simple -Particularly, it shows: +Ever wanted to send tokens to someone who doesn't have a NEAR account yet? Or maybe you want to distribute tokens to a group of people in a seamless way? That's exactly what NEAR Drop contracts are for! -1. How to create a token drops (NEAR, FT and NFT) -2. How to leverage Function Call keys for enabling amazing UX +## What Are Drops? -:::tip +Think of a drop as a digital gift card that you can send to anyone. Here's how it works: -This example showcases a simplified version of the contract that both [Keypom](https://keypom.xyz/) and the [Token Drop Utility](https://dev.near.org/tools?tab=linkdrops) use to distribute tokens to users +**Traditional way**: "Hey Bob, create a NEAR account first, then I'll send you some tokens" +**With drops**: "Hey Bob, here's a link. Click it and you'll get tokens AND a new account automatically" -::: +A drop is essentially a smart contract that holds tokens (NEAR, fungible tokens, or NFTs) and links them to a special private key. Anyone with that private key can claim the tokens - either into an existing account or by creating a brand new account on the spot. ---- +### Real-World Example -## Contract Overview +Imagine Alice wants to onboard her friend Bob to NEAR: -The contract exposes 3 methods to create drops of NEAR tokens, FT, and NFT. To claim the tokens, the contract exposes two methods, one to claim in an existing account, and another that will create a new account and claim the tokens into it. +1. **Alice creates a drop**: She puts 5 NEAR tokens into a drop and gets a special private key +2. **Alice shares the key**: She sends Bob the private key (usually as a link) +3. **Bob claims the drop**: Bob uses the key to either: + - Claim tokens into his existing NEAR account, or + - Create a new NEAR account and receive the tokens there -This contract leverages NEAR unique feature of [FunctionCall keys](../../protocol/access-keys.md), which allows the contract to create new accounts and claim tokens on behalf of the user. +The magic happens because of NEAR's unique **Function Call Keys** - the contract can actually create accounts on behalf of users! -Imagine Alice want to drop some NEAR to Bob: +## Types of Drops -1. Alice will call `create_near_drop` passing some NEAR amount, and a **Public** Access Key -2. The Contract will check if Alice attached enough tokens and create the drop -3. The Contract will add the `PublicKey` as a `FunctionCall Key` to itself, that **only allow to call the claim methods** -4. Alice will give the `Private Key` to Bob -5. Bob will use the Key to sign a transaction calling the `claim_for` method -6. The Contract will check if the key is linked to a drop, and if it is, it will send the drop +There are three types of drops you can create: -It is important to notice that, in step (5), Bob will be using the Contract's account to sign the transaction, and not his own account. Remember that in step (3) the contract added the key to itself, meaning that anyone with the key can call the claim methods in the name of the contract. +- **NEAR Drops**: Drop native NEAR tokens +- **FT Drops**: Drop fungible tokens (like stablecoins) +- **NFT Drops**: Drop non-fungible tokens (like collectibles) -
+## Building Your Own Drop Contract -Contract's interface +Let's walk through creating a drop contract step by step. -#### `create_near_drop(public_keys, amount_per_drop)` -Creates `#public_keys` drops, each with `amount_per_drop` NEAR tokens on them +### 1. Setting Up the Contract Structure -#### `create_ft_drop(public_keys, ft_contract, amount_per_drop)` -Creates `#public_keys` drops, each with `amount_per_drop` FT tokens, corresponding to the `ft_contract` +First, let's understand what our contract needs to track: -#### `create_nft_drop(public_key, nft_contract)` -Creates a drop with an NFT token, which will come from the `nft_contract` + -#### `claim_for(account_id)` -Claims a drop, which will be sent to the existing `account_id` +```rust +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize)] +pub struct NearDropContract { + /// The account used to create new accounts (usually "testnet" or "mainnet") + pub top_level_account: AccountId, + + /// Counter for assigning unique IDs to drops + pub next_drop_id: u64, + + /// Maps public keys to their corresponding drop IDs + pub drop_id_by_key: UnorderedMap, + + /// Maps drop IDs to the actual drop data + pub drop_by_id: UnorderedMap, +} +``` -#### `create_account_and_claim(account_id)` -Creates the `account_id`, and then drops the tokens into it + -
+### 2. Defining Drop Types ---- +We need to handle three different types of drops: -## Contract's State + -We can see in the contract's state that the contract keeps track of different `PublicKeys`, and links them to a specific `DropId`, which is simply an identifier for a `Drop` (see below). +```rust +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub enum Drop { + Near(NearDrop), + FungibleToken(FtDrop), + NonFungibleToken(NftDrop), +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct NearDrop { + pub amount_per_drop: U128, + pub counter: u64, +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct FtDrop { + pub contract_id: AccountId, + pub amount_per_drop: U128, + pub counter: u64, +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct NftDrop { + pub contract_id: AccountId, + pub counter: u64, +} +``` -- `top_level_account`: The account that will be used to create new accounts, generally it will be `testnet` or `mainnet` -- `next_drop_id`: A simple counter used to assign unique identifiers to each drop -- `drop_id_by_key`: A `Map` between `PublicKey` and `DropId`, which allows the contract to know what drops are claimable by a given key -- `drop_by_id`: A simple `Map` that links each `DropId` with the actual `Drop` data. + - +### 3. Creating NEAR Drops ---- +Here's how to implement NEAR token drops: -## Drop Types + -There are 3 types of drops, which differ in what the user will receive when they claims the corresponding drop - NEAR, fungible tokens (FTs) or non-fungible tokens (NFTs). +```rust +#[payable] +pub fn create_near_drop( + &mut self, + public_keys: Vec, + amount_per_drop: U128, +) -> bool { + let attached_deposit = env::attached_deposit(); + let amount_per_drop: u128 = amount_per_drop.into(); + + // Calculate required deposit + let required_deposit = (public_keys.len() as u128) * amount_per_drop; + + // Check if user attached enough NEAR + require!( + attached_deposit >= required_deposit, + "Not enough deposit attached" + ); + + // Create the drop + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + let drop = Drop::Near(NearDrop { + amount_per_drop: amount_per_drop.into(), + counter: public_keys.len() as u64, + }); + + // Store the drop + self.drop_by_id.insert(&drop_id, &drop); + + // Add each public key to the contract and map it to the drop + for public_key in public_keys { + // Add key to contract as a function call key + self.add_function_call_key(public_key.clone()); + + // Map the key to this drop + self.drop_id_by_key.insert(&public_key, &drop_id); + } + + true +} + +fn add_function_call_key(&self, public_key: PublicKey) { + let promise = Promise::new(env::current_account_id()).add_access_key( + public_key, + ACCESS_KEY_ALLOWANCE, + env::current_account_id(), + "claim_for,create_account_and_claim".to_string(), + ); + promise.as_return(); +} +``` - - - - - -:::info +### 4. Creating FT Drops -Notice that in this example implementation users cannot mix drops. This is, you can either drop NEAR tokens, or FT, or NFTs, but not a mixture of them (i.e. you cannot drop 1 NEAR token and 1 FT token in the same drop) +For fungible token drops, the process is similar but we need to handle token transfers: -::: + ---- +```rust +pub fn create_ft_drop( + &mut self, + public_keys: Vec, + ft_contract: AccountId, + amount_per_drop: U128, +) -> Promise { + let drop_id = self.next_drop_id; + self.next_drop_id += 1; + + let drop = Drop::FungibleToken(FtDrop { + contract_id: ft_contract.clone(), + amount_per_drop, + counter: public_keys.len() as u64, + }); + + self.drop_by_id.insert(&drop_id, &drop); + + for public_key in public_keys { + self.add_function_call_key(public_key.clone()); + self.drop_id_by_key.insert(&public_key, &drop_id); + } + + // Transfer FT tokens to the contract + let total_amount: u128 = amount_per_drop.0 * (drop.counter as u128); + + ext_ft_contract::ext(ft_contract) + .with_attached_deposit(1) + .ft_transfer_call( + env::current_account_id(), + total_amount.into(), + None, + "".to_string(), + ) +} +``` -## Create a drop - -All `create` start by checking that the user deposited enough funds to create the drop, and then proceed to add the access keys to the contract's account as [FunctionCall Keys](../../protocol/access-keys.md). - - - - - - - - - - - - - - - - - - - - - - + -
+### 5. Claiming Drops -### Storage Costs +Users can claim drops in two ways: -While we will not go into the details of how the storage costs are calculated, it is important to know what is being taken into account: +#### Claim to Existing Account -1. The cost of storing each Drop, which will include storing all bytes associated with the `Drop` struct -2. The cost of storing each `PublicKey -> DropId` relation in the maps -3. Cost of storing each `PublicKey` in the account + -Notice that (3) is not the cost of storing the byte representation of the `PublicKey` on the state, but the cost of adding the key to the contract's account as a FunctionCall key. +```rust +pub fn claim_for(&mut self, account_id: AccountId) -> Promise { + let public_key = env::signer_account_pk(); + self.internal_claim(account_id, public_key) +} + +fn internal_claim(&mut self, account_id: AccountId, public_key: PublicKey) -> Promise { + // Get the drop ID for this key + 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"); + + // Decrease counter + match &mut drop { + Drop::Near(near_drop) => { + near_drop.counter -= 1; + let amount = near_drop.amount_per_drop.0; + + // Transfer NEAR tokens + Promise::new(account_id).transfer(amount) + } + Drop::FungibleToken(ft_drop) => { + ft_drop.counter -= 1; + let amount = ft_drop.amount_per_drop; + + // Transfer FT tokens + ext_ft_contract::ext(ft_drop.contract_id.clone()) + .with_attached_deposit(1) + .ft_transfer(account_id, amount, None) + } + Drop::NonFungibleToken(nft_drop) => { + nft_drop.counter -= 1; + + // Transfer NFT + ext_nft_contract::ext(nft_drop.contract_id.clone()) + .with_attached_deposit(1) + .nft_transfer(account_id, "token_id".to_string(), None, None) + } + } + + // Update or remove the drop + if drop.get_counter() == 0 { + self.drop_by_id.remove(&drop_id); + self.drop_id_by_key.remove(&public_key); + } else { + self.drop_by_id.insert(&drop_id, &drop); + } +} +``` ---- + -## Claim a drop - -In order to claim drop, a user needs to sign a transaction using the `Private Key`, which is the counterpart of the `Public Key` that was added to the contract. - -All `Drops` have a `counter` which decreases by 1 each time a drop is claimed. This way, when all drops are claimed (`counter` == 0), we can remove all information from the Drop. - -There are two ways to claim a drop: claim for an existing account and claim for a new account. The main difference between them is that the first one will send the tokens to an existing account, while the second one will create a new account and send the tokens to it. - -
- - - - - - - - - - - - - - - - +#### Claim to New Account ---- + -### Testing the Contract +```rust +pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise { + let public_key = env::signer_account_pk(); + + // Create the new account first + Promise::new(account_id.clone()) + .create_account() + .add_full_access_key(public_key.clone()) + .transfer(NEW_ACCOUNT_BALANCE) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(30_000_000_000_000)) + .resolve_account_create(account_id, public_key) + ) +} + +#[private] +pub fn resolve_account_create( + &mut self, + account_id: AccountId, + public_key: PublicKey, +) -> Promise { + match env::promise_result(0) { + PromiseResult::Successful(_) => { + // Account created successfully, now claim the drop + self.internal_claim(account_id, public_key) + } + _ => { + env::panic_str("Failed to create account"); + } + } +} +``` -The contract readily includes a sandbox testing to validate its functionality. To execute the tests, run the following command: + - - - - ```bash - cargo test - ``` +### 6. Deployment and Usage - - +#### Deploy the Contract -:::tip -The `integration tests` use a sandbox to create NEAR users and simulate interactions with the contract. -::: + + ---- +```bash +# Build the contract +cargo near build -### Deploying the Contract to the NEAR network +# Deploy with initialization +cargo near deploy .testnet with-init-call new json-args '{"top_level_account": "testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +``` -In order to deploy the contract you will need to create a NEAR account. + + + +```bash +# Build the contract +cargo near build + +# Deploy with initialization +cargo near deploy .testnet \ + with-init-call new \ + json-args '{"top_level_account": "testnet"}' \ + prepaid-gas '100.0 Tgas' \ + attached-deposit '0 NEAR' \ + network-config testnet \ + sign-with-keychain send +``` + + + + +#### Create a Drop - + - ```bash - # Create a new account pre-funded by a faucet - near create-account --useFaucet - ``` - +```bash +# Create a NEAR drop +near call .testnet create_near_drop '{"public_keys": ["ed25519:YourPublicKeyHere"], "amount_per_drop": "1000000000000000000000000"}' --accountId .testnet --deposit 2 --gas 100000000000000 +``` - + + - ```bash - # Create a new account pre-funded by a faucet - near account create-account sponsor-by-faucet-service .testnet autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - +```bash +# Create a NEAR drop +near contract call-function as-transaction .testnet create_near_drop json-args '{"public_keys": ["ed25519:YourPublicKeyHere"], "amount_per_drop": "1000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '2 NEAR' sign-as .testnet network-config testnet sign-with-keychain send +``` + + -Then build and deploy the contract: +#### Claim a Drop + + + ```bash -cargo near build +# Claim to existing account +near call .testnet claim_for '{"account_id": ".testnet"}' --accountId .testnet --gas 30000000000000 --useLedgerKey "ed25519:YourPrivateKeyHere" -cargo near deploy with-init-call new json-args '{"top_level_account": "testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +# Claim to new account +near call .testnet create_account_and_claim '{"account_id": ".testnet"}' --accountId .testnet --gas 100000000000000 --useLedgerKey "ed25519:YourPrivateKeyHere" ``` ---- + + + +```bash +# Claim to existing account +near contract call-function as-transaction .testnet claim_for json-args '{"account_id": ".testnet"}' prepaid-gas '30.0 Tgas' attached-deposit '0 NEAR' sign-as .testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:YourPublicKeyHere --signer-private-key ed25519:YourPrivateKeyHere send -### CLI: Interacting with the Contract +# Claim to new account +near contract call-function as-transaction .testnet create_account_and_claim json-args '{"account_id": ".testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as .testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:YourPublicKeyHere --signer-private-key ed25519:YourPrivateKeyHere send +``` -To interact with the contract through the console, you can use the following commands: + + - - - - ```bash - # create a NEAR drop - near call create_near_drop '{"public_keys": ["ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q", "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"], "amount_per_drop": "10000000000000000000000"}' --accountId --deposit 1 --gas 100000000000000 - - # create a FT drop - near call create_ft_drop '{"public_keys": ["ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"], "amount_per_drop": "1", "ft_contract": ""}' --accountId --gas 100000000000000 - - # create a NFT drop - near call create_nft_drop '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "nft_contract": ""}' --accountId --gas 100000000000000 - - # claim to an existing account - # see the full version - - # claim to a new account - # see the full version - ``` - - - - - ```bash - # create a NEAR drop - near contract call-function as-transaction create_near_drop json-args '{"public_keys": ["ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q", "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"], "amount_per_drop": "10000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as network-config testnet sign-with-keychain send - - # create a FT drop - near contract call-function as-transaction create_ft_drop json-args '{"public_keys": ["ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"], "amount_per_drop": "1", "ft_contract": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send - - # create a NFT drop - near contract call-function as-transaction create_nft_drop json-args '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "nft_contract": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send - - # claim to an existing account - near contract call-function as-transaction claim_for json-args '{"account_id": ""}' prepaid-gas '30.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q --signer-private-key ed25519:3yVFxYtyk7ZKEMshioC3BofK8zu2q6Y5hhMKHcV41p5QchFdQRzHYUugsoLtqV3Lj4zURGYnHqMqt7zhZZ2QhdgB send - - # claim to a new account - near contract call-function as-transaction create_account_and_claim json-args '{"account_id": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4 --signer-private-key ed25519:2xZcegrZvP52VrhehvApnx4McL85hcSBq1JETJrjuESC6v6TwTcr4VVdzxaCReyMCJvx9V4X1ppv8cFFeQZ6hJzU send - ``` - +### 7. Testing Your Contract + + + + +Create integration tests to verify functionality: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use near_sdk::test_utils::{accounts, VMContextBuilder}; + use near_sdk::{testing_env, MockedBlockchain}; + + #[test] + fn test_create_near_drop() { + let context = VMContextBuilder::new() + .signer_account_id(accounts(0)) + .attached_deposit(1000000000000000000000000) // 1 NEAR + .build(); + testing_env!(context); + + let mut contract = NearDropContract::new(accounts(0)); + + let public_keys = vec![ + "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse().unwrap() + ]; + + let result = contract.create_near_drop( + public_keys, + U128(500000000000000000000000) // 0.5 NEAR per drop + ); + + assert!(result); + } + + #[test] + fn test_claim_drop() { + // Set up contract and create drop + // Then test claiming functionality + } +} +``` + +Run the tests: + +```bash +cargo test +``` + + -:::note Versioning for this article +### Key Points to Remember + +1. **Function Call Keys**: The contract adds public keys as function call keys to itself, allowing holders of the private keys to call claim methods +2. **Storage Costs**: Account for storage costs when calculating required deposits +3. **Security**: Only specific methods can be called with the function call keys +4. **Cleanup**: Remove drops and keys when all tokens are claimed to save storage +5. **Error Handling**: Always validate inputs and handle edge cases + +This contract provides a foundation for token distribution systems and can be extended with additional features like: +- Time-based expiration +- Multiple token types in a single drop +- Whitelist functionality +- Custom claim conditions + +The beauty of this system is that it dramatically improves user onboarding - users can receive tokens and create accounts in a single step, removing traditional barriers to blockchain adoption. + +## Why This Matters + +Drop contracts solve a real problem in blockchain adoption. Instead of the usual friction of "create an account first, then I'll send you tokens," drops allow you to onboard users seamlessly. They get tokens AND an account in one smooth experience. + +This is particularly powerful for: -At the time of this writing, this example works with the following versions: +- **Airdrops**: Distribute tokens to a large audience +- **Onboarding**: Get new users into your ecosystem +- **Gifts**: Send crypto gifts to friends and family +- **Marketing**: Create engaging distribution campaigns -- near-cli: `0.17.0` -- rustc: `1.82.0` +The NEAR Drop contract leverages NEAR's unique Function Call Keys to create this seamless experience. It's a perfect example of how thoughtful protocol design can enable better user experiences. -::: \ No newline at end of file +Want to see this in action? The contract powers tools like [Keypom](https://keypom.xyz/) and NEAR's Token Drop Utility, making token distribution accessible to everyone. \ No newline at end of file From 372af250c27640a12af32646989835416a5f4891 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:37:38 +0100 Subject: [PATCH 06/23] Update near-drop.md --- docs/tutorials/examples/near-drop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorials/examples/near-drop.md b/docs/tutorials/examples/near-drop.md index 81c17113a2b..9e65e7c23c4 100644 --- a/docs/tutorials/examples/near-drop.md +++ b/docs/tutorials/examples/near-drop.md @@ -12,6 +12,8 @@ import {CodeTabs, Language, Github} from "@site/src/components/codetabs" Ever wanted to send tokens to someone who doesn't have a NEAR account yet? Or maybe you want to distribute tokens to a group of people in a seamless way? That's exactly what NEAR Drop contracts are for! +Get more in-depth understanding [Here](../../tutorials/near drop/introduction.md) + ## What Are Drops? Think of a drop as a digital gift card that you can send to anyone. Here's how it works: From 3ad43617c40f1dcd3e9c015e0e0beebd08a4cf3b Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:38:09 +0100 Subject: [PATCH 07/23] Update near-drop.md --- docs/tutorials/examples/near-drop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/examples/near-drop.md b/docs/tutorials/examples/near-drop.md index 9e65e7c23c4..f8ec82cb66e 100644 --- a/docs/tutorials/examples/near-drop.md +++ b/docs/tutorials/examples/near-drop.md @@ -12,7 +12,7 @@ import {CodeTabs, Language, Github} from "@site/src/components/codetabs" Ever wanted to send tokens to someone who doesn't have a NEAR account yet? Or maybe you want to distribute tokens to a group of people in a seamless way? That's exactly what NEAR Drop contracts are for! -Get more in-depth understanding [Here](../../tutorials/near drop/introduction.md) +Get step by step usage [Here](../../tutorials/near drop/introduction.md) ## What Are Drops? From 05809353671a6a2b1c23d7f5365db6b8ec2a61e9 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:42:21 +0100 Subject: [PATCH 08/23] Add files via upload --- docs/tutorials/neardrop/access-keys.md | 17 ++++++++++++++ docs/tutorials/neardrop/account-creation.md | 19 +++++++++++++++ .../neardrop/contract-architecture.md | 23 +++++++++++++++++-- docs/tutorials/neardrop/frontend.md | 22 ++++++++++++++---- docs/tutorials/neardrop/ft-drops.md | 15 ++++++++++++ docs/tutorials/neardrop/near-drops.md | 14 ++++++++++- docs/tutorials/neardrop/nft-drops.md | 15 +++++++++++- 7 files changed, 116 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md index 9dec373fb78..08fed52337f 100644 --- a/docs/tutorials/neardrop/access-keys.md +++ b/docs/tutorials/neardrop/access-keys.md @@ -4,6 +4,8 @@ title: Access Key Management sidebar_label: Access Key Management description: "Understand how function-call access keys enable gasless operations in NEAR Drop." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; This is where NEAR gets really cool. Function-call access keys are what make gasless claiming possible - let's understand how they work! @@ -40,6 +42,7 @@ NEAR has two types of keys: Here's what happens when you create a drop: + ```rust // 1. Generate public/private key pairs let keypair = KeyPair::fromRandom('ed25519'); @@ -56,6 +59,7 @@ Promise::new(contract_account) // 3. Give private key to recipient // 4. Recipient signs transactions using the CONTRACT'S account (gasless!) ``` + **The result**: Recipients can claim tokens without having NEAR accounts or paying gas! @@ -65,6 +69,7 @@ Promise::new(contract_account) Function-call keys in NEAR Drop have strict limits: + ```rust pub struct AccessKeyPermission { allowance: NearToken::from_millinear(5), // Gas budget: 0.005 NEAR @@ -72,6 +77,7 @@ pub struct AccessKeyPermission { method_names: ["claim_for", "create_account_and_claim"] // Only these methods } ``` + **What keys CAN do:** - Call `claim_for` to claim to existing accounts @@ -98,6 +104,8 @@ The lifecycle is simple and secure: ``` Here's the cleanup code: + + ```rust fn cleanup_after_claim(&mut self, public_key: &PublicKey) { // Remove mapping @@ -110,6 +118,7 @@ fn cleanup_after_claim(&mut self, public_key: &PublicKey) { env::log_str("Key cleaned up after claim"); } ``` + --- @@ -119,6 +128,7 @@ fn cleanup_after_claim(&mut self, public_key: &PublicKey) { You can make keys that expire: + ```rust pub struct TimeLimitedDrop { drop: Drop, @@ -138,11 +148,13 @@ impl Contract { } } ``` + ### Key Rotation For extra security, you can rotate keys: + ```rust impl Contract { pub fn rotate_drop_keys(&mut self, drop_id: u64, new_keys: Vec) { @@ -156,6 +168,7 @@ impl Contract { } } ``` + --- @@ -179,6 +192,7 @@ impl Contract { Track how much gas your keys use: + ```rust impl Contract { pub fn track_key_usage(&mut self, operation: &str) { @@ -192,6 +206,7 @@ impl Contract { } } ``` + --- @@ -199,6 +214,7 @@ impl Contract { Your frontend can generate keys securely: + ```javascript import { KeyPair } from 'near-api-js'; @@ -219,6 +235,7 @@ function generateClaimUrl(privateKey) { return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; } ``` + --- diff --git a/docs/tutorials/neardrop/account-creation.md b/docs/tutorials/neardrop/account-creation.md index 5d1e2fcfc05..e363227678b 100644 --- a/docs/tutorials/neardrop/account-creation.md +++ b/docs/tutorials/neardrop/account-creation.md @@ -5,6 +5,9 @@ sidebar_label: Account Creation description: "Enable new users to create NEAR accounts automatically when claiming their first tokens." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + The ultimate onboarding experience: users can claim tokens AND get a NEAR account created for them automatically. No existing account required! --- @@ -28,19 +31,23 @@ This eliminates the biggest barrier to Web3 adoption. Account creation happens in two phases: ### Phase 1: Create the Account + ```rust Promise::new(account_id.clone()) .create_account() .transfer(NearToken::from_near(1)) // Fund with 1 NEAR + ``` ### Phase 2: Claim the Tokens + ```rust .then( Self::ext(env::current_account_id()) .resolve_account_creation(public_key, account_id) ) ``` + If account creation succeeds, we proceed with the normal claiming process. If it fails (account already exists), we try to claim anyway. @@ -50,6 +57,7 @@ If account creation succeeds, we proceed with the normal claiming process. If it Add this to your `src/claim.rs`: + ```rust #[near_bindgen] impl Contract { @@ -130,6 +138,7 @@ impl Contract { } } ``` + --- @@ -139,6 +148,7 @@ impl Contract { Let users pick their own account names: + ```rust pub fn create_named_account_and_claim(&mut self, preferred_name: String) -> Promise { let public_key = env::signer_account_pk(); @@ -160,11 +170,13 @@ fn sanitize_name(&self, name: &str) -> String { .collect() } ``` + ### Deterministic Names Or generate predictable names from keys: + ```rust use near_sdk::env::sha256; @@ -187,6 +199,7 @@ pub fn create_deterministic_account_and_claim(&mut self) -> Promise { self.create_account_and_claim(account_id) } ``` + --- @@ -194,6 +207,7 @@ pub fn create_deterministic_account_and_claim(&mut self) -> Promise { Make account creation seamless in your UI: + ```jsx function ClaimForm() { const [claimType, setClaimType] = useState('new'); // 'existing' or 'new' @@ -248,6 +262,7 @@ function ClaimForm() { ); } ``` + --- @@ -273,6 +288,7 @@ near view alice-new.testnet account Handle common issues gracefully: + ```rust impl Contract { pub fn create_account_with_fallback( @@ -314,6 +330,7 @@ impl Contract { } } ``` + --- @@ -321,6 +338,7 @@ impl Contract { Account creation costs depend on the drop type: + ```rust // Funding amounts by drop type const NEAR_DROP_FUNDING: NearToken = NearToken::from_millinear(500); // 0.5 NEAR @@ -345,6 +363,7 @@ pub fn estimate_cost_with_account_creation(&self, drop_type: &str, num_keys: u64 base_cost + (funding_per_account * num_keys) } ``` + --- diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md index cbd8e254352..5c37e1e83fe 100644 --- a/docs/tutorials/neardrop/contract-architecture.md +++ b/docs/tutorials/neardrop/contract-architecture.md @@ -4,6 +4,8 @@ title: Contract Architecture sidebar_label: Contract Architecture description: "Understand how the NEAR Drop contract works - the core data types, storage patterns, and drop management system." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; Before we start coding, let's understand how the NEAR Drop contract is structured. Think of it as the blueprint for our token distribution system. @@ -30,6 +32,7 @@ Drop #2 (1 NFT) ──→ Key C ──→ Carol claims The contract stores everything in four simple maps: + ```rust pub struct Contract { pub top_level_account: AccountId, // "testnet" or "near" @@ -38,6 +41,7 @@ pub struct Contract { pub drop_by_id: UnorderedMap, // Drop ID → Drop Data } ``` + **Why this design?** - Find drops quickly by key (for claiming) @@ -51,14 +55,17 @@ pub struct Contract { We support three types of token drops: ### NEAR Drops + ```rust pub struct NearDrop { pub amount: NearToken, // How much NEAR per claim pub counter: u64, // How many claims left } ``` + -### Fungible Token Drops +### Fungible Token Drops + ```rust pub struct FtDrop { pub ft_contract: AccountId, // Which FT contract @@ -66,8 +73,10 @@ pub struct FtDrop { pub counter: u64, // Claims remaining } ``` + ### NFT Drops + ```rust pub struct NftDrop { pub nft_contract: AccountId, // Which NFT contract @@ -75,8 +84,10 @@ pub struct NftDrop { pub counter: u64, // Always 1 (NFTs are unique) } ``` + All wrapped in an enum: + ```rust pub enum Drop { Near(NearDrop), @@ -84,6 +95,7 @@ pub enum Drop { NonFungibleToken(NftDrop), } ``` + --- @@ -98,7 +110,7 @@ When you create a drop: 4. Recipients sign transactions using the contract's account (gasless!) The keys can ONLY call claiming functions - nothing else. - + ```rust // Adding a function-call key Promise::new(env::current_account_id()) @@ -109,6 +121,7 @@ Promise::new(env::current_account_id()) "claim_for,create_account_and_claim".to_string() // Specific methods ) ``` + --- @@ -116,12 +129,14 @@ Promise::new(env::current_account_id()) Creating drops costs money because we're storing data on-chain. The costs include: + ```rust const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10); // Drop data const KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Key mapping const ACCESS_KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Adding key to account const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // Gas for claiming ``` + **Total for 5-key NEAR drop**: ~0.08 NEAR + token amounts @@ -142,11 +157,13 @@ The contract protects against common attacks: - Automatic cleanup after claims **Error Handling** + ```rust // Example validation assert!(!token_id.is_empty(), "Token ID cannot be empty"); assert!(amount > 0, "Amount must be positive"); ``` + --- @@ -154,6 +171,7 @@ assert!(amount > 0, "Amount must be positive"); We'll organize the code into logical modules: + ``` src/ ├── lib.rs # Main contract and initialization @@ -164,6 +182,7 @@ src/ ├── claim.rs # Claiming logic for all types └── external.rs # Cross-contract interfaces ``` + This keeps things organized and makes it easy to understand each piece. diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md index 5f523908bfa..94ac5b0a9e1 100644 --- a/docs/tutorials/neardrop/frontend.md +++ b/docs/tutorials/neardrop/frontend.md @@ -4,7 +4,11 @@ title: Frontend Integration sidebar_label: Frontend Integration description: "Build a React app that makes creating and claiming drops as easy as a few clicks." --- - +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + Time to build a user-friendly interface! Let's create a React app that makes your NEAR Drop system accessible to everyone. --- @@ -32,7 +36,7 @@ NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org ## NEAR Connection Service Create `src/services/near.js`: - + ```javascript import { connect, keyStores } from 'near-api-js'; import { setupWalletSelector } from '@near-wallet-selector/core'; @@ -80,6 +84,7 @@ class NearService { export const nearService = new NearService(); ``` + --- @@ -87,6 +92,7 @@ export const nearService = new NearService(); Create `src/utils/crypto.js`: + ```javascript import { KeyPair } from 'near-api-js'; @@ -108,6 +114,7 @@ export function generateClaimUrl(privateKey) { return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; } ``` + --- @@ -115,6 +122,7 @@ export function generateClaimUrl(privateKey) { Create `src/components/CreateDrop.js`: + ```jsx import { useState } from 'react'; import { nearService } from '../services/near'; @@ -226,13 +234,14 @@ export default function CreateDrop({ onDropCreated }) { ); } ``` + --- ## Drop Results Component Create `src/components/DropResults.js`: - + ```jsx import { useState } from 'react'; import QRCode from 'react-qr-code'; @@ -361,13 +370,14 @@ export default function DropResults({ dropInfo }) { ); } ``` + --- ## Claiming Component Create `src/components/ClaimDrop.js`: - + ```jsx import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; @@ -521,13 +531,14 @@ export default function ClaimDrop() { ); } ``` + --- ## Main App Layout Create `src/pages/index.js`: - + ```jsx import { useState, useEffect } from 'react'; import { nearService } from '../services/near'; @@ -598,6 +609,7 @@ export default function Home() { ); } ``` + --- diff --git a/docs/tutorials/neardrop/ft-drops.md b/docs/tutorials/neardrop/ft-drops.md index d50b1fcb064..191a35e1f83 100644 --- a/docs/tutorials/neardrop/ft-drops.md +++ b/docs/tutorials/neardrop/ft-drops.md @@ -5,6 +5,9 @@ sidebar_label: FT Drops description: "Add support for NEP-141 fungible tokens with cross-contract calls and automatic user registration." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + 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. --- @@ -26,6 +29,7 @@ But don't worry - we'll handle all of this step by step. First, let's add FT support to our drop types in `src/drop_types.rs`: + ```rust #[derive(BorshDeserialize, BorshSerialize, Clone)] pub enum Drop { @@ -40,8 +44,10 @@ pub struct FtDrop { pub counter: u64, } ``` + Update the helper methods: + ```rust impl Drop { pub fn get_counter(&self) -> u64 { @@ -59,6 +65,7 @@ impl Drop { } } ``` + --- @@ -66,6 +73,7 @@ impl Drop { Create `src/external.rs` to define how we talk to FT contracts: + ```rust use near_sdk::{ext_contract, AccountId, Gas, NearToken}; @@ -90,6 +98,7 @@ pub const GAS_FOR_CALLBACK: Gas = Gas(20_000_000_000_000); // Storage deposit for FT registration pub const STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(125); // 0.125 NEAR ``` + --- @@ -97,6 +106,7 @@ pub const STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(125); // 0.125 Add this to your main contract in `src/lib.rs`: + ```rust use crate::external::*; @@ -147,6 +157,7 @@ impl Contract { } } ``` + --- @@ -154,6 +165,7 @@ impl Contract { The tricky part! Update your `src/claim.rs`: + ```rust impl Contract { /// Updated core claiming logic @@ -265,6 +277,7 @@ impl Contract { } } ``` + --- @@ -329,6 +342,7 @@ near view test-ft.testnet ft_balance_of '{"account_id": "alice.testnet"}' ## Add Helper Functions + ```rust #[near_bindgen] impl Contract { @@ -350,6 +364,7 @@ impl Contract { } } ``` + --- diff --git a/docs/tutorials/neardrop/near-drops.md b/docs/tutorials/neardrop/near-drops.md index dd1e98eb71d..ac3da131571 100644 --- a/docs/tutorials/neardrop/near-drops.md +++ b/docs/tutorials/neardrop/near-drops.md @@ -5,6 +5,9 @@ sidebar_label: NEAR Token Drops description: "Build the foundation: distribute native NEAR tokens using function-call keys for gasless claiming." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + 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. --- @@ -31,6 +34,7 @@ serde = { version = "1.0", features = ["derive"] } Let's start with the main contract in `src/lib.rs`: + ```rust use near_sdk::{ env, near_bindgen, AccountId, NearToken, Promise, PublicKey, @@ -59,11 +63,13 @@ pub struct NearDrop { pub counter: u64, } ``` + --- ## Contract Initialization + ```rust impl Default for Contract { fn default() -> Self { @@ -84,13 +90,14 @@ impl Contract { } } ``` + --- ## Creating NEAR Drops The main function everyone will use: - + ```rust // Storage costs (rough estimates) const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10); @@ -154,6 +161,7 @@ impl Contract { } } ``` + --- @@ -161,6 +169,7 @@ impl Contract { Now for the claiming logic in `src/claim.rs`: + ```rust use crate::*; @@ -245,6 +254,7 @@ impl Contract { } } ``` + --- @@ -252,6 +262,7 @@ impl Contract { Add some useful view functions: + ```rust #[near_bindgen] impl Contract { @@ -274,6 +285,7 @@ impl Contract { } } ``` + --- diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md index f0013deefd3..04a0131fe9c 100644 --- a/docs/tutorials/neardrop/nft-drops.md +++ b/docs/tutorials/neardrop/nft-drops.md @@ -4,6 +4,8 @@ title: NFT Drops sidebar_label: NFT Drops description: "Distribute unique NFTs with one-time claims and ownership verification." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; 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. @@ -20,7 +22,7 @@ NFT drops are special because each NFT is unique. Unlike NEAR or FT drops where ## Add NFT Support First, extend your drop types in `src/drop_types.rs`: - + ```rust #[derive(BorshDeserialize, BorshSerialize, Clone)] pub enum Drop { @@ -36,8 +38,10 @@ pub struct NftDrop { pub counter: u64, // Always 1 for NFTs } ``` + Update the helper methods: + ```rust impl Drop { pub fn get_counter(&self) -> u64 { @@ -57,6 +61,7 @@ impl Drop { } } ``` + --- @@ -64,6 +69,7 @@ impl Drop { Add NFT methods to `src/external.rs`: + ```rust // Interface for NEP-171 NFT contracts #[ext_contract(ext_nft)] @@ -89,6 +95,7 @@ pub struct JsonToken { 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); ``` + --- @@ -96,6 +103,7 @@ pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000); Add this to your main contract: + ```rust #[near_bindgen] impl Contract { @@ -183,6 +191,7 @@ pub struct NftDropConfig { pub token_id: String, } ``` + --- @@ -190,6 +199,7 @@ pub struct NftDropConfig { Update your claiming logic in `src/claim.rs`: + ```rust impl Contract { fn process_claim(&mut self, public_key: &PublicKey, receiver_id: &AccountId) { @@ -270,6 +280,7 @@ impl Contract { } } ``` + --- @@ -324,6 +335,7 @@ near view test-nft.testnet nft_token '{"token_id": "unique-nft-001"}' Add some useful view methods: + ```rust #[near_bindgen] impl Contract { @@ -360,6 +372,7 @@ impl Contract { } } ``` + --- From 031716c8a5ee809f2f57b2ee8e4e2d54c8910ed0 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:32:25 +0100 Subject: [PATCH 09/23] Update access-keys.md --- docs/tutorials/neardrop/access-keys.md | 144 +++++-------------------- 1 file changed, 29 insertions(+), 115 deletions(-) diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md index 08fed52337f..47eb42cfa7c 100644 --- a/docs/tutorials/neardrop/access-keys.md +++ b/docs/tutorials/neardrop/access-keys.md @@ -6,6 +6,7 @@ description: "Understand how function-call access keys enable gasless operations --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" This is where NEAR gets really cool. Function-call access keys are what make gasless claiming possible - let's understand how they work! @@ -42,24 +43,9 @@ NEAR has two types of keys: Here's what happens when you create a drop: - -```rust -// 1. Generate public/private key pairs -let keypair = KeyPair::fromRandom('ed25519'); - -// 2. Add public key to contract with limited permissions -Promise::new(contract_account) - .add_access_key( - public_key, - NearToken::from_millinear(5), // 0.005 NEAR gas budget - contract_account, // Can only call this contract - "claim_for,create_account_and_claim" // Only these methods - ) - -// 3. Give private key to recipient -// 4. Recipient signs transactions using the CONTRACT'S account (gasless!) -``` - + **The result**: Recipients can claim tokens without having NEAR accounts or paying gas! @@ -69,15 +55,9 @@ Promise::new(contract_account) Function-call keys in NEAR Drop have strict limits: - -```rust -pub struct AccessKeyPermission { - allowance: NearToken::from_millinear(5), // Gas budget: 0.005 NEAR - receiver_id: "drop-contract.testnet", // Can only call this contract - method_names: ["claim_for", "create_account_and_claim"] // Only these methods -} -``` - + **What keys CAN do:** - Call `claim_for` to claim to existing accounts @@ -105,20 +85,9 @@ The lifecycle is simple and secure: Here's the cleanup code: - -```rust -fn cleanup_after_claim(&mut self, public_key: &PublicKey) { - // Remove mapping - self.drop_id_by_key.remove(public_key); - - // Delete the access key - Promise::new(env::current_account_id()) - .delete_key(public_key.clone()); - - env::log_str("Key cleaned up after claim"); -} -``` - + --- @@ -128,47 +97,17 @@ fn cleanup_after_claim(&mut self, public_key: &PublicKey) { You can make keys that expire: - -```rust -pub struct TimeLimitedDrop { - drop: Drop, - expires_at: Timestamp, -} - -impl Contract { - pub fn cleanup_expired_keys(&mut self) { - let now = env::block_timestamp(); - - // Find and remove expired drops - for (drop_id, drop) in self.time_limited_drops.iter() { - if now > drop.expires_at { - self.remove_all_keys_for_drop(drop_id); - } - } - } -} -``` - + ### Key Rotation For extra security, you can rotate keys: - -```rust -impl Contract { - pub fn rotate_drop_keys(&mut self, drop_id: u64, new_keys: Vec) { - // Remove old keys - self.remove_old_keys(drop_id); - - // Add new keys - for key in new_keys { - self.add_claim_key(&key, drop_id); - } - } -} -``` - + --- @@ -192,21 +131,9 @@ impl Contract { Track how much gas your keys use: - -```rust -impl Contract { - pub fn track_key_usage(&mut self, operation: &str) { - let gas_used = env::used_gas(); - - // Log for monitoring - env::log_str(&format!("{} used {} gas", operation, gas_used.0)); - - // Could store in state for analytics - self.gas_usage_stats.insert(operation, gas_used); - } -} -``` - + --- @@ -214,28 +141,15 @@ impl Contract { Your frontend can generate keys securely: - -```javascript -import { KeyPair } from 'near-api-js'; - -// Generate keys on the client -function generateDropKeys(count) { - return Array.from({ length: count }, () => { - const keyPair = KeyPair.fromRandom('ed25519'); - return { - publicKey: keyPair.publicKey.toString(), - privateKey: keyPair.secretKey, - claimUrl: generateClaimUrl(keyPair.secretKey) - }; - }); -} - -// Create claim URLs -function generateClaimUrl(privateKey) { - return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; -} -``` - + + +Create claim URLs: + + --- @@ -283,4 +197,4 @@ Now that you understand how the gasless magic works, let's see how to create new :::tip Key Insight Function-call access keys are like giving someone a specific key to your house that only opens one room and only works once. It's secure, limited, and perfect for token distribution! -::: \ No newline at end of file +::: From 3d5e0ba001d584d3b8d4b4611ba7cda9608d172b Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:37:40 +0100 Subject: [PATCH 10/23] Update contract-architecture.md --- .../neardrop/contract-architecture.md | 94 +++++-------------- 1 file changed, 24 insertions(+), 70 deletions(-) diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md index 5c37e1e83fe..19d14f6e96b 100644 --- a/docs/tutorials/neardrop/contract-architecture.md +++ b/docs/tutorials/neardrop/contract-architecture.md @@ -6,6 +6,7 @@ description: "Understand how the NEAR Drop contract works - the core data types, --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" Before we start coding, let's understand how the NEAR Drop contract is structured. Think of it as the blueprint for our token distribution system. @@ -32,16 +33,9 @@ Drop #2 (1 NFT) ──→ Key C ──→ Carol claims The contract stores everything in four simple maps: - -```rust -pub struct Contract { - pub top_level_account: AccountId, // "testnet" or "near" - pub next_drop_id: u64, // Counter for unique drop IDs - pub drop_id_by_key: LookupMap, // Key → Drop ID - pub drop_by_id: UnorderedMap, // Drop ID → Drop Data -} -``` - + **Why this design?** - Find drops quickly by key (for claiming) @@ -55,47 +49,24 @@ pub struct Contract { We support three types of token drops: ### NEAR Drops - -```rust -pub struct NearDrop { - pub amount: NearToken, // How much NEAR per claim - pub counter: u64, // How many claims left -} -``` - + ### Fungible Token Drops - -```rust -pub struct FtDrop { - pub ft_contract: AccountId, // Which FT contract - pub amount: String, // Amount per claim - pub counter: u64, // Claims remaining -} -``` - + ### NFT Drops - -```rust -pub struct NftDrop { - pub nft_contract: AccountId, // Which NFT contract - pub token_id: String, // Specific NFT - pub counter: u64, // Always 1 (NFTs are unique) -} -``` - + All wrapped in an enum: - -```rust -pub enum Drop { - Near(NearDrop), - FungibleToken(FtDrop), - NonFungibleToken(NftDrop), -} -``` - + --- @@ -110,18 +81,10 @@ When you create a drop: 4. Recipients sign transactions using the contract's account (gasless!) The keys can ONLY call claiming functions - nothing else. - -```rust -// Adding a function-call key -Promise::new(env::current_account_id()) - .add_access_key( - public_key, - NearToken::from_millinear(5), // 0.005 NEAR gas allowance - env::current_account_id(), // Can only call this contract - "claim_for,create_account_and_claim".to_string() // Specific methods - ) -``` - + + --- @@ -129,14 +92,9 @@ Promise::new(env::current_account_id()) Creating drops costs money because we're storing data on-chain. The costs include: - -```rust -const DROP_STORAGE_COST: NearToken = NearToken::from_millinear(10); // Drop data -const KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Key mapping -const ACCESS_KEY_STORAGE_COST: NearToken = NearToken::from_millinear(1); // Adding key to account -const FUNCTION_CALL_ALLOWANCE: NearToken = NearToken::from_millinear(5); // Gas for claiming -``` - + **Total for 5-key NEAR drop**: ~0.08 NEAR + token amounts @@ -157,13 +115,11 @@ The contract protects against common attacks: - Automatic cleanup after claims **Error Handling** - ```rust // Example validation assert!(!token_id.is_empty(), "Token ID cannot be empty"); assert!(amount > 0, "Amount must be positive"); ``` - --- @@ -171,7 +127,6 @@ assert!(amount > 0, "Amount must be positive"); We'll organize the code into logical modules: - ``` src/ ├── lib.rs # Main contract and initialization @@ -182,7 +137,6 @@ src/ ├── claim.rs # Claiming logic for all types └── external.rs # Cross-contract interfaces ``` - This keeps things organized and makes it easy to understand each piece. @@ -198,4 +152,4 @@ Now that you understand the architecture, let's start building! We'll begin with :::tip Key Takeaway The contract is essentially a **key-to-token mapping system** powered by NEAR's function-call access keys. Users get keys, keys unlock tokens, and everything happens without gas fees for the recipient! -::: \ No newline at end of file +::: From c084b283f002bf110fa3b216c73c37a4d43d9765 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:42:36 +0100 Subject: [PATCH 11/23] Update account-creation.md --- docs/tutorials/neardrop/account-creation.md | 318 +++----------------- 1 file changed, 48 insertions(+), 270 deletions(-) diff --git a/docs/tutorials/neardrop/account-creation.md b/docs/tutorials/neardrop/account-creation.md index e363227678b..dcede4b9a97 100644 --- a/docs/tutorials/neardrop/account-creation.md +++ b/docs/tutorials/neardrop/account-creation.md @@ -7,6 +7,7 @@ description: "Enable new users to create NEAR accounts automatically when claimi import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" The ultimate onboarding experience: users can claim tokens AND get a NEAR account created for them automatically. No existing account required! @@ -31,23 +32,14 @@ This eliminates the biggest barrier to Web3 adoption. Account creation happens in two phases: ### Phase 1: Create the Account - -```rust -Promise::new(account_id.clone()) - .create_account() - .transfer(NearToken::from_near(1)) // Fund with 1 NEAR - -``` + ### Phase 2: Claim the Tokens - -```rust -.then( - Self::ext(env::current_account_id()) - .resolve_account_creation(public_key, account_id) -) -``` - + If account creation succeeds, we proceed with the normal claiming process. If it fails (account already exists), we try to claim anyway. @@ -57,88 +49,21 @@ If account creation succeeds, we proceed with the normal claiming process. If it Add this to your `src/claim.rs`: - -```rust -#[near_bindgen] -impl Contract { - /// 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 account format - self.validate_account_id(&account_id); - - // Check we have a valid drop for this key - let drop_id = self.drop_id_by_key.get(&public_key) - .expect("No drop found for this key"); - - let drop = self.drop_by_id.get(&drop_id) - .expect("Drop data not found"); - - // Calculate funding based on drop type - let funding = self.calculate_account_funding(&drop); - - // Create account with initial funding - Promise::new(account_id.clone()) - .create_account() - .transfer(funding) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(50_000_000_000_000)) - .resolve_account_creation(public_key, account_id) - ) - } - - /// Handle account creation result - #[private] - pub fn resolve_account_creation( - &mut self, - public_key: PublicKey, - account_id: AccountId, - ) { - match env::promise_result(0) { - PromiseResult::Successful(_) => { - env::log_str(&format!("Created account {}", account_id)); - self.process_claim(&public_key, &account_id); - } - PromiseResult::Failed => { - // Account creation failed - maybe it already exists? - env::log_str("Account creation failed, trying to claim anyway"); - self.process_claim(&public_key, &account_id); - } - } - } - - /// Validate account ID format - fn validate_account_id(&self, account_id: &AccountId) { - let account_str = account_id.as_str(); - - // Check length - assert!(account_str.len() >= 2 && account_str.len() <= 64, - "Account ID must be 2-64 characters"); - - // Must be subaccount of top-level account - assert!(account_str.ends_with(&format!(".{}", self.top_level_account)), - "Account must end with .{}", self.top_level_account); - - // Check valid characters (lowercase, numbers, hyphens, underscores) - let name_part = account_str.split('.').next().unwrap(); - assert!(name_part.chars().all(|c| - c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_' - ), "Invalid characters in account name"); - } - - /// Calculate how much NEAR to fund the new account with - fn calculate_account_funding(&self, drop: &Drop) -> NearToken { - match drop { - Drop::Near(_) => NearToken::from_millinear(500), // 0.5 NEAR - Drop::FungibleToken(_) => NearToken::from_near(1), // 1 NEAR (for FT registration) - Drop::NonFungibleToken(_) => NearToken::from_millinear(500), // 0.5 NEAR - } - } -} -``` - + + +Validate account ID format: + + + +Calculate funding based on drop type: + + --- @@ -148,58 +73,17 @@ impl Contract { Let users pick their own account names: - -```rust -pub fn create_named_account_and_claim(&mut self, preferred_name: String) -> Promise { - let public_key = env::signer_account_pk(); - - // Clean up the name - let clean_name = self.sanitize_name(&preferred_name); - let full_account_id = format!("{}.{}", clean_name, self.top_level_account) - .parse::() - .expect("Invalid account name"); - - self.create_account_and_claim(full_account_id) -} - -fn sanitize_name(&self, name: &str) -> String { - name.to_lowercase() - .chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') - .take(32) // Limit length - .collect() -} -``` - + ### Deterministic Names Or generate predictable names from keys: - -```rust -use near_sdk::env::sha256; - -pub fn create_deterministic_account_and_claim(&mut self) -> Promise { - let public_key = env::signer_account_pk(); - - // Generate name from public key hash - let key_bytes = match &public_key { - PublicKey::ED25519(bytes) => bytes, - _ => env::panic_str("Unsupported key type"), - }; - - let hash = sha256(key_bytes); - let name = hex::encode(&hash[..8]); // Use first 8 bytes - - let account_id = format!("{}.{}", name, self.top_level_account) - .parse::() - .expect("Failed to generate account ID"); - - self.create_account_and_claim(account_id) -} -``` - + --- @@ -207,62 +91,9 @@ pub fn create_deterministic_account_and_claim(&mut self) -> Promise { Make account creation seamless in your UI: - -```jsx -function ClaimForm() { - const [claimType, setClaimType] = useState('new'); // 'existing' or 'new' - const [accountName, setAccountName] = useState(''); - - const handleClaim = async () => { - const keyPair = KeyPair.fromString(privateKey); - - if (claimType === 'existing') { - await contract.claim_for({ account_id: accountName }); - } else { - await contract.create_named_account_and_claim({ - preferred_name: accountName - }); - } - }; - - return ( -
-
- - -
- - setAccountName(e.target.value)} - placeholder={claimType === 'existing' ? 'alice.testnet' : 'my-new-name'} - /> - {claimType === 'new' && .testnet} - - -
- ); -} -``` -
+ --- @@ -288,49 +119,9 @@ near view alice-new.testnet account Handle common issues gracefully: - -```rust -impl Contract { - pub fn create_account_with_fallback( - &mut self, - primary_name: String, - fallback_name: Option, - ) -> Promise { - let primary_account = format!("{}.{}", primary_name, self.top_level_account); - - // Try primary name first - Promise::new(primary_account.parse().unwrap()) - .create_account() - .transfer(NearToken::from_near(1)) - .then( - Self::ext(env::current_account_id()) - .handle_creation_with_fallback(primary_name, fallback_name) - ) - } - - #[private] - pub fn handle_creation_with_fallback( - &mut self, - primary_name: String, - fallback_name: Option, - ) { - if env::promise_result(0).is_successful() { - // Primary succeeded - let account_id = format!("{}.{}", primary_name, self.top_level_account); - self.process_claim(&env::signer_account_pk(), &account_id.parse().unwrap()); - } else if let Some(fallback) = fallback_name { - // Try fallback - let fallback_account = format!("{}.{}", fallback, self.top_level_account); - Promise::new(fallback_account.parse().unwrap()) - .create_account() - .transfer(NearToken::from_near(1)); - } else { - env::panic_str("Account creation failed and no fallback provided"); - } - } -} -``` - + --- @@ -338,32 +129,19 @@ impl Contract { Account creation costs depend on the drop type: - -```rust -// Funding amounts by drop type -const NEAR_DROP_FUNDING: NearToken = NearToken::from_millinear(500); // 0.5 NEAR -const FT_DROP_FUNDING: NearToken = NearToken::from_near(1); // 1 NEAR -const NFT_DROP_FUNDING: NearToken = NearToken::from_millinear(500); // 0.5 NEAR - -// Total cost = drop cost + account funding -pub fn estimate_cost_with_account_creation(&self, drop_type: &str, num_keys: u64) -> NearToken { - let base_cost = match drop_type { - "near" => self.estimate_near_drop_cost(num_keys, NearToken::from_near(1)), - "ft" => self.estimate_ft_drop_cost(num_keys), - "nft" => self.estimate_nft_drop_cost(), - _ => NearToken::from_near(0), - }; - - let funding_per_account = match drop_type { - "near" | "nft" => NEAR_DROP_FUNDING, - "ft" => FT_DROP_FUNDING, - _ => NearToken::from_near(0), - }; - - base_cost + (funding_per_account * num_keys) -} -``` - + + +--- + +## Frontend Account Creation Flow + +Add account creation options to your claiming interface: + + --- @@ -405,4 +183,4 @@ With gasless claiming and automatic account creation working, it's time to build :::tip Pro Tip Always provide enough initial funding for the account type. FT drops need more funding because recipients might need to register on multiple FT contracts later. -::: \ No newline at end of file +::: From 6834a1a5deb8b2d232d647aadb3883f9963349f4 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:45:20 +0100 Subject: [PATCH 12/23] Update frontend.md --- docs/tutorials/neardrop/frontend.md | 659 +++++----------------------- 1 file changed, 112 insertions(+), 547 deletions(-) diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md index 94ac5b0a9e1..bd759ec2489 100644 --- a/docs/tutorials/neardrop/frontend.md +++ b/docs/tutorials/neardrop/frontend.md @@ -6,9 +6,8 @@ description: "Build a React app that makes creating and claiming drops as easy a --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - - - +import {Github} from "@site/src/components/codetabs" + Time to build a user-friendly interface! Let's create a React app that makes your NEAR Drop system accessible to everyone. --- @@ -36,55 +35,10 @@ NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org ## NEAR Connection Service Create `src/services/near.js`: - -```javascript -import { connect, keyStores } from 'near-api-js'; -import { setupWalletSelector } from '@near-wallet-selector/core'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; - -const config = { - networkId: process.env.NEXT_PUBLIC_NETWORK_ID, - keyStore: new keyStores.BrowserLocalStorageKeyStore(), - nodeUrl: process.env.NEXT_PUBLIC_RPC_URL, - contractName: process.env.NEXT_PUBLIC_CONTRACT_ID, -}; - -class NearService { - async initialize() { - this.near = await connect(config); - this.walletSelector = await setupWalletSelector({ - network: config.networkId, - modules: [setupMyNearWallet()], - }); - } - - isSignedIn() { - return this.walletSelector?.isSignedIn() || false; - } - - async signIn() { - const modal = setupModal(this.walletSelector); - modal.show(); - } - - async getContract() { - if (!this.isSignedIn()) return null; - - const wallet = await this.walletSelector.wallet(); - return new Contract( - wallet.account(), - config.contractName, - { - viewMethods: ['get_drop', 'estimate_near_drop_cost'], - changeMethods: ['create_near_drop', 'claim_for', 'create_account_and_claim'], - } - ); - } -} - -export const nearService = new NearService(); -``` - + + --- @@ -92,29 +46,9 @@ export const nearService = new NearService(); Create `src/utils/crypto.js`: - -```javascript -import { KeyPair } from 'near-api-js'; - -export function generateKeys(count) { - const keys = []; - - for (let i = 0; i < count; i++) { - const keyPair = KeyPair.fromRandom('ed25519'); - keys.push({ - publicKey: keyPair.publicKey.toString(), - privateKey: keyPair.secretKey, - }); - } - - return keys; -} - -export function generateClaimUrl(privateKey) { - return `${window.location.origin}/claim?key=${encodeURIComponent(privateKey)}`; -} -``` - + --- @@ -122,494 +56,59 @@ export function generateClaimUrl(privateKey) { Create `src/components/CreateDrop.js`: - -```jsx -import { useState } from 'react'; -import { nearService } from '../services/near'; -import { generateKeys } from '../utils/crypto'; - -export default function CreateDrop({ onDropCreated }) { - const [loading, setLoading] = useState(false); - const [formData, setFormData] = useState({ - dropType: 'near', - amount: '1', - keyCount: 5, - ftContract: '', - ftAmount: '', - }); - - const handleSubmit = async (e) => { - e.preventDefault(); - setLoading(true); - - try { - const contract = await nearService.getContract(); - const keys = generateKeys(formData.keyCount); - const publicKeys = keys.map(k => k.publicKey); - - let dropId; - - if (formData.dropType === 'near') { - // Calculate cost first - const cost = await contract.estimate_near_drop_cost({ - num_keys: formData.keyCount, - amount_per_drop: (parseFloat(formData.amount) * 1e24).toString(), - }); - - dropId = await contract.create_near_drop({ - public_keys: publicKeys, - amount_per_drop: (parseFloat(formData.amount) * 1e24).toString(), - }, { - gas: '100000000000000', - attachedDeposit: cost, - }); - } - // Add FT and NFT cases here... - - onDropCreated({ dropId, keys, dropType: formData.dropType }); - } catch (error) { - alert('Failed to create drop: ' + error.message); - } finally { - setLoading(false); - } - }; - - return ( -
-

Create Token Drop

- -
- {/* Drop Type */} -
- - -
- - {/* NEAR Amount */} - {formData.dropType === 'near' && ( -
- - setFormData({...formData, amount: e.target.value})} - className="w-full border rounded px-3 py-2" - required - /> -
- )} - - {/* Key Count */} -
- - setFormData({...formData, keyCount: parseInt(e.target.value)})} - className="w-full border rounded px-3 py-2" - required - /> -
- - -
-
- ); -} -``` -
+ --- ## Drop Results Component Create `src/components/DropResults.js`: - -```jsx -import { useState } from 'react'; -import QRCode from 'react-qr-code'; -import { generateClaimUrl } from '../utils/crypto'; - -export default function DropResults({ dropInfo }) { - const [selectedKey, setSelectedKey] = useState(0); - - const claimUrls = dropInfo.keys.map(key => generateClaimUrl(key.privateKey)); - - const downloadKeys = () => { - const data = dropInfo.keys.map((key, index) => ({ - index: index + 1, - publicKey: key.publicKey, - privateKey: key.privateKey, - claimUrl: claimUrls[index], - })); - - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `near-drop-${dropInfo.dropId}-keys.json`; - a.click(); - }; - - const copyToClipboard = (text) => { - navigator.clipboard.writeText(text); - // Add toast notification here - }; - - return ( -
-
-

Drop Created! 🎉

-

Drop ID: {dropInfo.dropId}

-

- Created {dropInfo.keys.length} keys for {dropInfo.dropType} drop -

-
- -
- {/* Keys List */} -
-
-

Claim Keys

- -
- -
- {dropInfo.keys.map((key, index) => ( -
-
- Key {index + 1} - -
- -
- {claimUrls[index]} -
- -
- - Show Private Key - -
- {key.privateKey} -
-
-
- ))} -
-
- - {/* QR Codes */} -
-
-

QR Codes

- -
- -
-
- -
-

- Scan to claim Key {selectedKey + 1} -

-
- -
- {dropInfo.keys.map((_, index) => ( -
setSelectedKey(index)} - > - -

#{index + 1}

-
- ))} -
-
-
-
- ); -} -``` -
+ + --- ## Claiming Component Create `src/components/ClaimDrop.js`: - -```jsx -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import { nearService } from '../services/near'; -import { KeyPair } from 'near-api-js'; - -export default function ClaimDrop() { - const router = useRouter(); - const [privateKey, setPrivateKey] = useState(''); - const [claimType, setClaimType] = useState('existing'); - const [accountName, setAccountName] = useState(''); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - - useEffect(() => { - if (router.query.key) { - setPrivateKey(router.query.key); - } - }, [router.query]); - - const handleClaim = async (e) => { - e.preventDefault(); - setLoading(true); - - try { - // Create temporary wallet with the private key - const keyPair = KeyPair.fromString(privateKey); - const tempAccount = { - accountId: process.env.NEXT_PUBLIC_CONTRACT_ID, - keyPair: keyPair, - }; - - const contract = await nearService.getContract(); - - if (claimType === 'existing') { - await contract.claim_for({ - account_id: accountName, - }, { - gas: '150000000000000', - signerAccount: tempAccount, - }); - } else { - await contract.create_account_and_claim({ - account_id: `${accountName}.testnet`, - }, { - gas: '200000000000000', - signerAccount: tempAccount, - }); - } - - setSuccess(true); - } catch (error) { - alert('Claim failed: ' + error.message); - } finally { - setLoading(false); - } - }; - - if (success) { - return ( -
-
🎉
-

Claim Successful!

-

Your tokens have been transferred.

- -
- ); - } - - return ( -
-

Claim Your Drop

- -
- {/* Private Key */} -
- - setPrivateKey(e.target.value)} - placeholder="ed25519:..." - className="w-full border rounded px-3 py-2 font-mono text-sm" - required - /> -
- - {/* Claim Type */} -
- -
- - -
-
- - {/* Account Name */} -
- -
- setAccountName(e.target.value)} - placeholder={claimType === 'existing' ? 'alice.testnet' : 'alice'} - className="flex-1 border rounded-l px-3 py-2" - required - /> - {claimType === 'new' && ( - - .testnet - - )} -
-
- - -
-
- ); -} -``` -
+ + --- ## Main App Layout Create `src/pages/index.js`: - -```jsx -import { useState, useEffect } from 'react'; -import { nearService } from '../services/near'; -import CreateDrop from '../components/CreateDrop'; -import DropResults from '../components/DropResults'; - -export default function Home() { - const [isSignedIn, setIsSignedIn] = useState(false); - const [loading, setLoading] = useState(true); - const [createdDrop, setCreatedDrop] = useState(null); - - useEffect(() => { - initNear(); - }, []); - - const initNear = async () => { - try { - await nearService.initialize(); - setIsSignedIn(nearService.isSignedIn()); - } catch (error) { - console.error('Failed to initialize NEAR:', error); - } finally { - setLoading(false); - } - }; - - if (loading) { - return
Loading...
; - } - - if (!isSignedIn) { - return ( -
-

NEAR Drop

-

- Create gasless token drops that anyone can claim -

- -
- ); - } - - return ( -
-
- {createdDrop ? ( -
- -
- -
-
- ) : ( - - )} -
-
- ); -} -``` -
+ + + +--- + +## QR Code Generation + +Add QR code generation for easy sharing: + + + +--- + +## Mobile-First Design + +Ensure your CSS is mobile-responsive: + + --- @@ -627,6 +126,31 @@ vercel --prod # Just connect your GitHub repo and it'll auto-deploy ``` +Add environment variables in your deployment platform: +- `NEXT_PUBLIC_NETWORK_ID=testnet` +- `NEXT_PUBLIC_CONTRACT_ID=your-contract.testnet` +- `NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org` + +--- + +## Advanced Features + +### Batch Key Generation + +For large drops, add batch processing: + + + +### Drop Analytics + +Track drop performance: + + + --- ## What You've Built @@ -639,17 +163,58 @@ Awesome! You now have a complete web application with: ✅ **QR code support** for easy sharing ✅ **Claiming interface** for both new and existing users ✅ **Mobile-responsive design** that works everywhere +✅ **Batch processing** for large drops +✅ **Analytics dashboard** for tracking performance Your users can now create and claim token drops with just a few clicks - no technical knowledge required! --- +## Testing Your Frontend + +1. **Local Development**: + ```bash + npm run dev + ``` + +2. **Connect Wallet**: Test wallet connection with testnet +3. **Create Small Drop**: Try creating a 1-key NEAR drop +4. **Test Claiming**: Use the generated private key to claim +5. **Mobile Testing**: Verify responsive design on mobile devices + +--- + +## Production Considerations + +**Security**: +- Never log private keys in production +- Validate all user inputs +- Use HTTPS for all requests + +**Performance**: +- Implement proper loading states +- Cache contract calls where possible +- Optimize images and assets + +**User Experience**: +- Add helpful error messages +- Provide clear instructions +- Support keyboard navigation + +--- + ## Next Steps -Your NEAR Drop system is nearly complete. The final step is to thoroughly test everything and deploy to production. +Your NEAR Drop system is complete! Consider adding: + +- **Social sharing** for claim links +- **Email notifications** for drop creators +- **Advanced analytics** with charts +- **Multi-language support** +- **Custom themes** and branding --- :::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 +::: From 663c8b51b0f2af62706e235a22f9f82786cb7393 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:46:49 +0100 Subject: [PATCH 13/23] Update ft-drops.md --- docs/tutorials/neardrop/ft-drops.md | 276 +++------------------------- 1 file changed, 26 insertions(+), 250 deletions(-) diff --git a/docs/tutorials/neardrop/ft-drops.md b/docs/tutorials/neardrop/ft-drops.md index 191a35e1f83..75b2b5283cc 100644 --- a/docs/tutorials/neardrop/ft-drops.md +++ b/docs/tutorials/neardrop/ft-drops.md @@ -7,6 +7,7 @@ description: "Add support for NEP-141 fungible tokens with cross-contract calls import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" 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. @@ -29,43 +30,14 @@ But don't worry - we'll handle all of this step by step. First, let's add FT support to our drop types in `src/drop_types.rs`: - -```rust -#[derive(BorshDeserialize, BorshSerialize, Clone)] -pub enum Drop { - Near(NearDrop), - FungibleToken(FtDrop), // New! -} - -#[derive(BorshDeserialize, BorshSerialize, Clone)] -pub struct FtDrop { - pub ft_contract: AccountId, - 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 { - Drop::Near(drop) => drop.counter, - Drop::FungibleToken(drop) => drop.counter, - } - } - - pub fn decrement_counter(&mut self) { - match self { - Drop::Near(drop) => drop.counter -= 1, - Drop::FungibleToken(drop) => drop.counter -= 1, - } - } -} -``` - + --- @@ -73,32 +45,9 @@ impl Drop { Create `src/external.rs` to define how we talk to FT contracts: - -```rust -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); -} - -// Interface for callbacks to our contract -#[ext_contract(ext_self)] -pub trait DropCallbacks { - fn ft_transfer_callback(&mut self, public_key: PublicKey, receiver_id: AccountId); -} - -// 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 -pub const STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(125); // 0.125 NEAR -``` - + --- @@ -106,58 +55,9 @@ pub const STORAGE_DEPOSIT: NearToken = NearToken::from_millinear(125); // 0.125 Add this to your main contract in `src/lib.rs`: - -```rust -use crate::external::*; - -#[near_bindgen] -impl Contract { - /// Create a fungible token drop - pub fn create_ft_drop( - &mut self, - public_keys: Vec, - ft_contract: AccountId, - amount_per_drop: String, - ) -> u64 { - let num_keys = public_keys.len() as u64; - let deposit = env::attached_deposit(); - - // 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; - - let drop = Drop::FungibleToken(FtDrop { - ft_contract: ft_contract.clone(), - amount: amount_per_drop.clone(), - counter: num_keys, - }); - - self.drop_by_id.insert(&drop_id, &drop); - - // Add keys - for public_key in public_keys { - self.add_claim_key(&public_key, drop_id); - } - - env::log_str(&format!("Created FT drop {} with {} {} tokens per claim", - drop_id, amount_per_drop, ft_contract)); - drop_id - } -} -``` - + --- @@ -165,119 +65,15 @@ impl Contract { The tricky part! Update your `src/claim.rs`: - -```rust -impl Contract { - /// 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 data not found"); - - assert!(drop.get_counter() > 0, "No claims remaining"); - - match &drop { - Drop::Near(near_drop) => { - // 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 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 - } - } - } - - /// 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, register the user on the FT contract - ext_ft::ext(ft_contract.clone()) - .with_static_gas(GAS_FOR_STORAGE_DEPOSIT) - .with_attached_deposit(STORAGE_DEPOSIT) - .storage_deposit(Some(receiver_id.clone())) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(GAS_FOR_CALLBACK) - .ft_registration_callback(public_key, receiver_id, ft_contract, amount) - ); - } - - /// Handle FT registration result - #[private] - 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("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) - ); - } - - /// Handle FT transfer result - #[private] - 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 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::panic_str("FT transfer failed"); - } - } - - /// 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 { - self.drop_by_id.remove(&drop_id); - } else { - self.drop_by_id.insert(&drop_id, drop); - } - - self.drop_id_by_key.remove(public_key); - Promise::new(env::current_account_id()).delete_key(public_key.clone()); - } -} -``` - + + +FT claiming with automatic user registration: + + --- @@ -342,29 +138,9 @@ near view test-ft.testnet ft_balance_of '{"account_id": "alice.testnet"}' ## Add Helper Functions - -```rust -#[near_bindgen] -impl Contract { - /// 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_info(&self, drop_id: u64) -> Option<(AccountId, String, u64)> { - if let Some(Drop::FungibleToken(ft_drop)) = self.drop_by_id.get(&drop_id) { - Some((ft_drop.ft_contract, ft_drop.amount, ft_drop.counter)) - } else { - None - } - } -} -``` - + --- @@ -404,4 +180,4 @@ Next up: NFT drops, which have their own unique challenges around uniqueness and :::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 +::: From 00497d058405fceeb1c69a1c817cad0b2a2a3964 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:48:41 +0100 Subject: [PATCH 14/23] Update introduction.md --- docs/tutorials/neardrop/introduction.md | 55 ++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/neardrop/introduction.md b/docs/tutorials/neardrop/introduction.md index f49a3199e8d..13fc1686604 100644 --- a/docs/tutorials/neardrop/introduction.md +++ b/docs/tutorials/neardrop/introduction.md @@ -5,6 +5,8 @@ sidebar_label: Introduction description: "Build a token distribution system that lets you airdrop NEAR, FTs, and NFTs to users without them needing gas fees or existing accounts." --- +import {Github} from "@site/src/components/codetabs" + 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! **NEAR Drop** lets you create token distributions that anyone can claim with just a private key - no NEAR account or gas fees required. @@ -34,6 +36,22 @@ The magic? **Function-call access keys** - NEAR's unique feature that enables ga --- +## Project Structure + +Your completed project will look like this: + + + +The frontend structure: + + + +--- + ## Real Examples - **Community Airdrop**: Give 5 NEAR to 100 community members @@ -60,12 +78,44 @@ The magic? **Function-call access keys** - NEAR's unique feature that enables ga | [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 | +| [Access Keys](/tutorials/neardrop/access-keys) | Understanding gasless operations | +| [Account Creation](/tutorials/neardrop/account-creation) | Auto account creation | | [Frontend](/tutorials/neardrop/frontend) | Build a web interface | Each section builds on the previous one, so start from the beginning! --- +## Repository Links + +- **Smart Contract**: [github.com/Festivemena/Near-drop](https://github.com/Festivemena/Near-drop) +- **Frontend**: [github.com/Festivemena/Drop](https://github.com/Festivemena/Drop) + +--- + +## Quick Start + +If you want to jump straight into the code: + +```bash +# Clone the smart contract +git clone https://github.com/Festivemena/Near-drop.git +cd Near-drop + +# Build and deploy +cd contract +cargo near build +near deploy .testnet target/near/near_drop.wasm + +# Clone the frontend +git clone https://github.com/Festivemena/Drop.git +cd Drop +npm install +npm run dev +``` + +--- + ## Ready to Start? Let's dive into how the contract architecture works and start building your token distribution system. @@ -74,9 +124,10 @@ Let's dive into how the contract architecture works and start building your toke --- -:::note +:::note Version Requirements 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 +- node: `18.0.0`+ +::: From e49566e19feddb989406a174e9ba40f11779323c Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:51:33 +0100 Subject: [PATCH 15/23] Update near-drops.md --- docs/tutorials/neardrop/near-drops.md | 252 +++----------------------- 1 file changed, 24 insertions(+), 228 deletions(-) diff --git a/docs/tutorials/neardrop/near-drops.md b/docs/tutorials/neardrop/near-drops.md index ac3da131571..5a5d7ffe116 100644 --- a/docs/tutorials/neardrop/near-drops.md +++ b/docs/tutorials/neardrop/near-drops.md @@ -7,6 +7,7 @@ description: "Build the foundation: distribute native NEAR tokens using function import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" 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. @@ -34,134 +35,27 @@ serde = { version = "1.0", features = ["derive"] } Let's start with the main contract in `src/lib.rs`: - -```rust -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(BorshDeserialize, BorshSerialize, Clone)] -pub enum Drop { - Near(NearDrop), - // We'll add FT and NFT later -} - -#[derive(BorshDeserialize, BorshSerialize, Clone)] -pub struct NearDrop { - pub amount: NearToken, - pub counter: u64, -} -``` - + --- ## Contract Initialization - -```rust -impl Default for Contract { - fn default() -> Self { - env::panic_str("Contract must be initialized") - } -} - -#[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 The main function everyone will use: - -```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 drop that distributes NEAR tokens - pub fn create_near_drop( - &mut self, - public_keys: Vec, - amount_per_drop: NearToken, - ) -> u64 { - let num_keys = public_keys.len() as u64; - let deposit = env::attached_deposit(); - - // 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 >= total_cost, "Need {} NEAR, got {}", - total_cost.as_near(), deposit.as_near()); - - // Create the drop - let drop_id = self.next_drop_id; - self.next_drop_id += 1; - - let drop = Drop::Near(NearDrop { - amount: amount_per_drop, - counter: num_keys, - }); - - self.drop_by_id.insert(&drop_id, &drop); - - // Add function-call keys - for public_key in public_keys { - self.add_claim_key(&public_key, drop_id); - } - - env::log_str(&format!("Created drop {} with {} NEAR per claim", - drop_id, amount_per_drop.as_near())); - drop_id - } - - /// 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); - - // Add limited access key to contract - Promise::new(env::current_account_id()) - .add_access_key( - public_key.clone(), - ACCESS_KEY_ALLOWANCE, - env::current_account_id(), - "claim_for,create_account_and_claim".to_string(), - ); - } -} -``` - + + --- @@ -169,92 +63,15 @@ impl Contract { Now for the claiming logic in `src/claim.rs`: - -```rust -use crate::*; - -#[near_bindgen] -impl Contract { - /// Claim tokens to an existing account - pub fn claim_for(&mut self, account_id: AccountId) { - let public_key = env::signer_account_pk(); - self.process_claim(&public_key, &account_id); - } - - /// 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 account format - assert!(account_id.as_str().ends_with(&format!(".{}", self.top_level_account)), - "Account must end with .{}", self.top_level_account); - - // Create account with 1 NEAR funding - Promise::new(account_id.clone()) - .create_account() - .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) - ) - } - - /// Handle account creation result and claim - #[private] - 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"), - } - } - } - - /// 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"); - - let mut drop = self.drop_by_id.get(&drop_id) - .expect("Drop data not found"); - - // Check if claims available - let Drop::Near(near_drop) = &drop else { - env::panic_str("Wrong drop type"); - }; - - assert!(near_drop.counter > 0, "No claims remaining"); - - // Send tokens - Promise::new(receiver_id.clone()).transfer(near_drop.amount); - - // 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 used key - self.drop_id_by_key.remove(public_key); - 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)); - } -} -``` - + + +Core claiming logic: + + --- @@ -262,30 +79,9 @@ impl Contract { Add some useful view functions: - -```rust -#[near_bindgen] -impl Contract { - /// Get drop information - pub fn get_drop(&self, drop_id: u64) -> Option { - self.drop_by_id.get(&drop_id) - } - - /// 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) - } - - /// 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 - } -} -``` - + --- @@ -358,4 +154,4 @@ The foundation is solid. Next, let's add support for fungible tokens, which invo :::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 +::: From 160d294f2848b2ea2f7d7370cf915bab0256ad7b Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:52:47 +0100 Subject: [PATCH 16/23] Update nft-drops.md --- docs/tutorials/neardrop/nft-drops.md | 301 +++------------------------ 1 file changed, 26 insertions(+), 275 deletions(-) diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md index 04a0131fe9c..ebf27167741 100644 --- a/docs/tutorials/neardrop/nft-drops.md +++ b/docs/tutorials/neardrop/nft-drops.md @@ -6,6 +6,7 @@ description: "Distribute unique NFTs with one-time claims and ownership verifica --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import {Github} from "@site/src/components/codetabs" 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. @@ -22,46 +23,14 @@ NFT drops are special because each NFT is unique. Unlike NEAR or FT drops where ## Add NFT Support First, extend your drop types in `src/drop_types.rs`: - -```rust -#[derive(BorshDeserialize, BorshSerialize, Clone)] -pub enum Drop { - Near(NearDrop), - FungibleToken(FtDrop), - NonFungibleToken(NftDrop), // New! -} - -#[derive(BorshDeserialize, BorshSerialize, Clone)] -pub struct NftDrop { - pub nft_contract: AccountId, - pub token_id: String, - pub counter: u64, // Always 1 for NFTs -} -``` - + Update the helper methods: - -```rust -impl Drop { - pub fn get_counter(&self) -> u64 { - match self { - Drop::Near(drop) => drop.counter, - Drop::FungibleToken(drop) => drop.counter, - Drop::NonFungibleToken(drop) => drop.counter, - } - } - - pub fn decrement_counter(&mut self) { - match self { - Drop::Near(drop) => drop.counter -= 1, - Drop::FungibleToken(drop) => drop.counter -= 1, - Drop::NonFungibleToken(drop) => drop.counter -= 1, - } - } -} -``` - + --- @@ -69,33 +38,9 @@ impl Drop { Add NFT methods to `src/external.rs`: - -```rust -// Interface for NEP-171 NFT contracts -#[ext_contract(ext_nft)] -pub trait NonFungibleToken { - fn nft_transfer( - &mut self, - receiver_id: AccountId, - token_id: String, - memo: Option, - ); - - fn nft_token(&self, token_id: String) -> Option; -} - -#[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, -} - -// 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); -``` - + --- @@ -103,95 +48,15 @@ pub const GAS_FOR_NFT_CALLBACK: Gas = Gas(20_000_000_000_000); Add this to your main contract: - -```rust -#[near_bindgen] -impl Contract { - /// Create an NFT drop (only 1 key since NFTs are unique) - pub fn create_nft_drop( - &mut self, - public_key: PublicKey, - nft_contract: AccountId, - token_id: String, - ) -> u64 { - let deposit = env::attached_deposit(); - - // 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()); - - // Validate token ID - assert!(!token_id.is_empty(), "Token ID cannot be empty"); - assert!(token_id.len() <= 64, "Token ID too long"); - - // Create the drop - let drop_id = self.next_drop_id; - self.next_drop_id += 1; - - let drop = Drop::NonFungibleToken(NftDrop { - nft_contract: nft_contract.clone(), - token_id: token_id.clone(), - counter: 1, // Always 1 for NFTs - }); - - self.drop_by_id.insert(&drop_id, &drop); - self.add_claim_key(&public_key, drop_id); - - env::log_str(&format!("Created NFT drop {} for token {}", drop_id, token_id)); - drop_id - } - - /// 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_cost = (DROP_STORAGE_COST + KEY_STORAGE_COST + ACCESS_KEY_ALLOWANCE) - * nft_drops.len() as u64; - - assert!(env::attached_deposit() >= total_cost, "Insufficient deposit for batch"); - - 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); - } - - drop_ids - } - - fn create_single_nft_drop( - &mut self, - public_key: PublicKey, - nft_contract: AccountId, - token_id: String, - ) -> u64 { - let drop_id = self.next_drop_id; - self.next_drop_id += 1; - - let drop = Drop::NonFungibleToken(NftDrop { - nft_contract, token_id, counter: 1, - }); - - self.drop_by_id.insert(&drop_id, &drop); - self.add_claim_key(&public_key, drop_id); - drop_id - } -} - -#[derive(near_sdk::serde::Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct NftDropConfig { - pub public_key: PublicKey, - pub nft_contract: AccountId, - pub token_id: String, -} -``` - + + +Batch NFT creation: + + --- @@ -199,88 +64,9 @@ pub struct NftDropConfig { Update your claiming logic in `src/claim.rs`: - -```rust -impl Contract { - 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 data not found"); - - assert!(drop.get_counter() > 0, "Drop already claimed"); - - match &drop { - Drop::Near(near_drop) => { - Promise::new(receiver_id.clone()).transfer(near_drop.amount); - self.cleanup_claim(public_key, &mut drop, drop_id); - } - Drop::FungibleToken(ft_drop) => { - self.claim_ft_tokens(/* ... */); - } - Drop::NonFungibleToken(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(), - ); - } - } - } - - /// Claim NFT with cross-contract call - fn claim_nft( - &mut self, - public_key: PublicKey, - receiver_id: AccountId, - nft_contract: AccountId, - token_id: String, - ) { - ext_nft::ext(nft_contract.clone()) - .with_static_gas(GAS_FOR_NFT_TRANSFER) - .nft_transfer( - receiver_id.clone(), - token_id.clone(), - 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, token_id) - ); - } - - /// Handle NFT transfer result - #[private] - pub fn nft_transfer_callback( - &mut self, - public_key: PublicKey, - receiver_id: AccountId, - token_id: String, - ) { - let success = env::promise_results_count() == 1 && - matches!(env::promise_result(0), PromiseResult::Successful(_)); - - if success { - env::log_str(&format!("NFT {} transferred to {}", token_id, receiver_id)); - - // Clean up the claim - if let Some(drop_id) = self.drop_id_by_key.get(&public_key) { - // For NFTs, always remove completely since they're unique - self.drop_by_id.remove(&drop_id); - self.drop_id_by_key.remove(&public_key); - Promise::new(env::current_account_id()).delete_key(public_key); - } - } else { - env::panic_str("NFT transfer failed - contract may not own this NFT"); - } - } -} -``` - + --- @@ -335,44 +121,9 @@ near view test-nft.testnet nft_token '{"token_id": "unique-nft-001"}' Add some useful view methods: - -```rust -#[near_bindgen] -impl Contract { - /// Get NFT drop details - pub fn get_nft_drop_info(&self, drop_id: u64) -> Option<(AccountId, String, bool)> { - if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { - Some(( - nft_drop.nft_contract, - nft_drop.token_id, - nft_drop.counter == 0, // is_claimed - )) - } else { - None - } - } - - /// Calculate NFT drop cost - pub fn estimate_nft_drop_cost(&self) -> NearToken { - DROP_STORAGE_COST + KEY_STORAGE_COST + ACCESS_KEY_ALLOWANCE - } - - /// Check if NFT drop exists for a token - pub fn nft_drop_exists(&self, nft_contract: AccountId, token_id: String) -> bool { - for drop_id in 0..self.next_drop_id { - if let Some(Drop::NonFungibleToken(nft_drop)) = self.drop_by_id.get(&drop_id) { - if nft_drop.nft_contract == nft_contract && - nft_drop.token_id == token_id && - nft_drop.counter > 0 { - return true; - } - } - } - false - } -} -``` - + --- @@ -422,4 +173,4 @@ Let's explore how function-call access keys work in detail to understand the gas - Verify the drop contract owns all NFTs before creating drops - Consider using batch creation for large NFT collections - NFT drops are perfect for event tickets, collectibles, and exclusive content -::: \ No newline at end of file +::: From cfaeb66250d2c2f55627f08de3c90969955aa026 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:16:44 +0100 Subject: [PATCH 17/23] Update frontend.md --- docs/tutorials/neardrop/frontend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md index bd759ec2489..499fc943da8 100644 --- a/docs/tutorials/neardrop/frontend.md +++ b/docs/tutorials/neardrop/frontend.md @@ -34,7 +34,7 @@ NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org ## NEAR Connection Service -Create `src/services/near.js`: +Create `src/utils/near.js`: Date: Tue, 19 Aug 2025 00:20:11 +0100 Subject: [PATCH 18/23] Update frontend.md --- docs/tutorials/neardrop/frontend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md index 499fc943da8..6889d1f0832 100644 --- a/docs/tutorials/neardrop/frontend.md +++ b/docs/tutorials/neardrop/frontend.md @@ -37,7 +37,7 @@ NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org Create `src/utils/near.js`: --- From 70c9f874762699faf22997dc58b15da995556d57 Mon Sep 17 00:00:00 2001 From: Festivemena Date: Sat, 23 Aug 2025 16:41:05 +0100 Subject: [PATCH 19/23] Fixed [FEEDBACK] issue #2734 --- docs/integrations/create-transactions.md | 64 ++++++++++++------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/integrations/create-transactions.md b/docs/integrations/create-transactions.md index 6df7d156708..1afce2468e8 100644 --- a/docs/integrations/create-transactions.md +++ b/docs/integrations/create-transactions.md @@ -43,14 +43,14 @@ In [`send-tokens-easy.js`](https://github.com/near-examples/transaction-examples 2. [`dotenv`](https://www.npmjs.com/package/dotenv) (used to load environment variables for private key) ```js -const nearAPI = require("near-api-js"); -const { connect, KeyPair, keyStores, utils } = nearAPI; +const { Near, Account, KeyPair, keyStores, utils } = require("near-api-js"); require("dotenv").config(); ``` -The second line above deconstructs several utilities from nearAPI that you will use to interact with the blockchain. +The destructured utilities from near-api-js that you will use to interact with the blockchain: -- `connect` - create a connection to NEAR passing configuration variables +- `Near` - create a connection to NEAR passing configuration variables +- `Account` - creates an account object for interacting with the blockchain - `KeyPair` - creates a keyPair from the private key you'll provide in an `.env` file - `keyStores` - stores the keyPair that you will create from the private key and used to sign Transactions - `utils` - used to format NEAR amounts @@ -72,7 +72,7 @@ When sending NEAR tokens (Ⓝ) during a transaction, the amount needs to be conv - To perform this you will use the [`near-api-js`](https://github.com/near/near-api-js) method [`parseNearAmount()`](https://github.com/near/near-api-js/blob/d4d4cf1ac3182fa998b1e004e6782219325a641b/src/utils/format.ts#L53-L63) (located in `utils/format`) ```js -const amount = nearAPI.utils.format.parseNearAmount("1.5"); +const amount = utils.format.parseNearAmount("1.5"); ``` ### Create a Key Store @@ -110,9 +110,9 @@ const config = { }; // connect to NEAR! :) -const near = await connect(config); +const near = new Near(config); // create a NEAR account object -const senderAccount = await near.account(sender); +const senderAccount = new Account(near.connection, sender); ``` You'll notice the last line uses your NEAR connection to create a `senderAccount` object that you'll use to perform the transaction. @@ -152,7 +152,7 @@ In [`send-tokens-deconstructed.js`](https://github.com/near-examples/transaction 3. [`dotenv`](https://www.npmjs.com/package/dotenv) (used to load environment variables) ```js -const nearAPI = require("near-api-js"); +const { providers, KeyPair, utils, transactions } = require("near-api-js"); const sha256 = require("js-sha256"); require("dotenv").config(); ``` @@ -178,7 +178,7 @@ When sending NEAR tokens (Ⓝ) during a transaction, the amount needs to be conv - To perform this you will use the [`near-api-js`](https://github.com/near/near-api-js) method [`parseNearAmount()`](https://github.com/near/near-api-js/blob/d4d4cf1ac3182fa998b1e004e6782219325a641b/src/utils/format.ts#L53-L63) (located in `utils/format`) ```js -const amount = nearAPI.utils.format.parseNearAmount("1.5"); +const amount = utils.format.parseNearAmount("1.5"); ``` --- @@ -188,9 +188,9 @@ const amount = nearAPI.utils.format.parseNearAmount("1.5"); In this example, we will create a NEAR RPC `provider` that allows us to interact with the chain via [RPC endpoints](/api/rpc/introduction). ```js -const provider = new nearAPI.providers.JsonRpcProvider( - `https://rpc.${networkId}.near.org` -); +const provider = new providers.JsonRpcProvider({ + url: `https://rpc.${networkId}.near.org` +}); ``` --- @@ -209,7 +209,7 @@ Once you have access to the private key of the sender's account, create an envir ```js const privateKey = process.env.SENDER_PRIVATE_KEY; -const keyPair = nearAPI.KeyPair.fromString(privateKey); +const keyPair = KeyPair.fromString(privateKey); ``` --- @@ -293,10 +293,12 @@ const publicKey = keyPair.getPublicKey(); - Current nonce can be retrieved using the `provider` we [created earlier](#setting-up-a-connection-to-near). ```js -const accessKey = await provider.query( - `access_key/${sender}/${publicKey.toString()}`, - "" -); +const accessKey = await provider.query({ + request_type: "view_access_key", + finality: "final", + account_id: sender, + public_key: publicKey.toString(), +}); ``` - now we can create a unique number for our transaction by incrementing the current `nonce`. @@ -309,10 +311,10 @@ const nonce = ++accessKey.nonce; - There are currently eight supported `Action` types. [[see here]](/protocol/transaction-anatomy#actions) - For this example, we are using `Transfer` -- This transfer action can be created using the [imported `nearAPI` object](#imports) and the [formatted Ⓝ amount](#formatting-token-amounts) created earlier. +- This transfer action can be created using the [imported `transactions` object](#imports) and the [formatted Ⓝ amount](#formatting-token-amounts) created earlier. ```js -const actions = [nearAPI.transactions.transfer(amount)]; +const actions = [transactions.transfer(amount)]; ``` [[click here]](https://github.com/near/near-api-js/blob/d4d4cf1ac3182fa998b1e004e6782219325a641b/src/transaction.ts#L70-L72) to view source for `transfer()`. @@ -320,10 +322,10 @@ const actions = [nearAPI.transactions.transfer(amount)]; ### 6 `blockHash` - Each transaction requires a current block hash (within 24hrs) to prove that the transaction was created recently. -- Hash must be converted to an array of bytes using the `base_decode` method found in [`nearAPI`](#imports). +- Hash must be converted to an array of bytes using the `base_decode` method found in [`utils`](#imports). ```js -const recentBlockHash = nearAPI.utils.serialize.base_decode( +const recentBlockHash = utils.serialize.base_decode( accessKey.block_hash ); ``` @@ -336,10 +338,10 @@ const recentBlockHash = nearAPI.utils.serialize.base_decode( With all of our [required arguments](#transaction-requirements), we can construct the transaction. -- Using [`nearAPI`](#imports), we call on `createTransaction()` to perform this task. +- Using [`transactions`](#imports), we call on `createTransaction()` to perform this task. ```js -const transaction = nearAPI.transactions.createTransaction( +const transaction = transactions.createTransaction( sender, publicKey, receiver, @@ -357,11 +359,11 @@ const transaction = nearAPI.transactions.createTransaction( Now that the transaction is created, we sign it before sending it to the NEAR blockchain. At the lowest level, there are four steps to this process. -1. Using [`nearAPI`](#imports), we call on `serialize()` to serialize the transaction in [Borsh](https://borsh.io/). +1. Using [`utils`](#imports), we call on `serialize()` to serialize the transaction in [Borsh](https://borsh.io/). ```js -const serializedTx = nearAPI.utils.serialize.serialize( - nearAPI.transactions.SCHEMA.Transaction, +const serializedTx = utils.serialize.serialize( + transactions.SCHEMA.Transaction, transaction ); ``` @@ -378,12 +380,12 @@ const serializedTxHash = new Uint8Array(sha256.sha256.array(serializedTx)); const signature = keyPair.sign(serializedTxHash); ``` -4. Construct the signed transaction using `near-api-js` [SignedTransaction class](https://github.com/near/near-api-js/blob/d4d4cf1ac3182fa998b1e004e6782219325a641b/src/transaction.ts#L112-L123). +4. Construct the signed transaction using near-api-js [SignedTransaction class](https://github.com/near/near-api-js/blob/d4d4cf1ac3182fa998b1e004e6782219325a641b/src/transaction.ts#L112-L123). ```js -const signedTransaction = new nearAPI.transactions.SignedTransaction({ +const signedTransaction = new transactions.SignedTransaction({ transaction, - signature: new nearAPI.transactions.Signature({ + signature: new transactions.Signature({ keyType: transaction.publicKey.keyType, data: signature.signature, }), @@ -395,7 +397,7 @@ const signedTransaction = new nearAPI.transactions.SignedTransaction({ Final step is to encode and send the transaction. - First we serialize transaction into [Borsh](https://borsh.io/), and store the result as `signedSerializedTx`. _(required for all transactions)_ -- Then we send the transaction via [RPC call](/api/rpc/introduction) using the `sendJsonRpc()` method nested inside [`near`](#setting-up-a-connection-to-near). +- Then we send the transaction via [RPC call](/api/rpc/introduction) using the `sendJsonRpc()` method nested inside [`provider`](#setting-up-a-connection-to-near). ```js // encodes transaction to serialized Borsh (required for all transactions) @@ -475,4 +477,4 @@ const transactionLink = `https://${prefix}nearblocks.io/txns/${result.transactio Ask it on StackOverflow! ::: -Happy Coding! 🚀 +Happy Coding! 🚀 \ No newline at end of file From cc93f267abab68d59a71a1cdde2019ed4c02a5da Mon Sep 17 00:00:00 2001 From: Festivemena Date: Sat, 23 Aug 2025 16:47:23 +0100 Subject: [PATCH 20/23] Fix: Update create-transactions docs to remove deprecated near-api-js functions (fixes #2734) --- docs/integrations/create-transactions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/create-transactions.md b/docs/integrations/create-transactions.md index 1afce2468e8..cf543c5448a 100644 --- a/docs/integrations/create-transactions.md +++ b/docs/integrations/create-transactions.md @@ -477,4 +477,4 @@ const transactionLink = `https://${prefix}nearblocks.io/txns/${result.transactio Ask it on StackOverflow! ::: -Happy Coding! 🚀 \ No newline at end of file +Happy Coding! 🚀 \ No newline at end of file From ecba6312440e2e9915831cb57fe86004ba24da59 Mon Sep 17 00:00:00 2001 From: Festivemena Date: Sat, 23 Aug 2025 17:09:42 +0100 Subject: [PATCH 21/23] Fix: Update create-transactions docs to remove deprecated near-api-js functions (fixes #2734) --- docs/integrations/create-transactions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/create-transactions.md b/docs/integrations/create-transactions.md index cf543c5448a..c741c47fecb 100644 --- a/docs/integrations/create-transactions.md +++ b/docs/integrations/create-transactions.md @@ -477,4 +477,4 @@ const transactionLink = `https://${prefix}nearblocks.io/txns/${result.transactio Ask it on StackOverflow! ::: -Happy Coding! 🚀 \ No newline at end of file +Happy Coding! 🚀 \ No newline at end of file From 40fee8dc4e05acd7629bdc499a4bc6c849600af3 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Sat, 23 Aug 2025 17:14:49 +0100 Subject: [PATCH 22/23] Delete docs/tutorials/neardrop directory --- docs/tutorials/neardrop/access-keys.md | 200 ---------------- docs/tutorials/neardrop/account-creation.md | 186 --------------- .../neardrop/contract-architecture.md | 155 ------------ docs/tutorials/neardrop/frontend.md | 220 ------------------ docs/tutorials/neardrop/ft-drops.md | 183 --------------- docs/tutorials/neardrop/introduction.md | 133 ----------- docs/tutorials/neardrop/near-drops.md | 157 ------------- docs/tutorials/neardrop/nft-drops.md | 176 -------------- 8 files changed, 1410 deletions(-) delete mode 100644 docs/tutorials/neardrop/access-keys.md delete mode 100644 docs/tutorials/neardrop/account-creation.md delete mode 100644 docs/tutorials/neardrop/contract-architecture.md delete mode 100644 docs/tutorials/neardrop/frontend.md delete mode 100644 docs/tutorials/neardrop/ft-drops.md delete mode 100644 docs/tutorials/neardrop/introduction.md delete mode 100644 docs/tutorials/neardrop/near-drops.md delete mode 100644 docs/tutorials/neardrop/nft-drops.md diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md deleted file mode 100644 index 47eb42cfa7c..00000000000 --- a/docs/tutorials/neardrop/access-keys.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -id: access-keys -title: Access Key Management -sidebar_label: Access Key Management -description: "Understand how function-call access keys enable gasless operations in NEAR Drop." ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -This is where NEAR gets really cool. Function-call access keys are what make gasless claiming possible - let's understand how they work! - ---- - -## The Problem NEAR Solves - -Traditional blockchains have a chicken-and-egg problem: -- You need tokens to pay gas fees -- But you need gas to receive tokens -- New users are stuck! - -NEAR's solution: **Function-call access keys** that let you call specific functions without owning the account. - ---- - -## How Access Keys Work - -NEAR has two types of keys: - -**Full Access Keys** 🔑 -- Complete control over an account -- Can do anything: transfer tokens, deploy contracts, etc. -- Like having admin access - -**Function-Call Keys** 🎫 -- Limited permissions -- Can only call specific functions -- Like having a concert ticket - gets you in, but only to your seat - ---- - -## NEAR Drop's Key Magic - -Here's what happens when you create a drop: - - - -**The result**: Recipients can claim tokens without having NEAR accounts or paying gas! - ---- - -## Key Permissions Breakdown - -Function-call keys in NEAR Drop have strict limits: - - - -**What keys CAN do:** -- Call `claim_for` to claim to existing accounts -- Call `create_account_and_claim` to create new accounts -- Use up to 0.005 NEAR worth of gas - -**What keys CANNOT do:** -- Transfer tokens from the contract -- Call any other functions -- Deploy contracts or change state maliciously -- Exceed their gas allowance - ---- - -## Key Lifecycle - -The lifecycle is simple and secure: - -``` -1. CREATE → Add key with limited permissions -2. SHARE → Give private key to recipient -3. CLAIM → Recipient uses key to claim tokens -4. CLEANUP → Remove key after use (prevents reuse) -``` - -Here's the cleanup code: - - - ---- - -## Advanced Key Patterns - -### Time-Limited Keys - -You can make keys that expire: - - - -### Key Rotation - -For extra security, you can rotate keys: - - - ---- - -## Security Best Practices - -**✅ DO:** -- Use minimal gas allowances (0.005 NEAR is plenty) -- Remove keys immediately after use -- Validate key formats before adding -- Monitor key usage patterns - -**❌ DON'T:** -- Give keys excessive gas allowances -- Reuse keys for multiple drops -- Skip cleanup after claims -- Log private keys anywhere - ---- - -## Gas Usage Monitoring - -Track how much gas your keys use: - - - ---- - -## Integration with Frontend - -Your frontend can generate keys securely: - - - -Create claim URLs: - - - ---- - -## Troubleshooting Common Issues - -**"Access key not found"** -- Key wasn't added properly to the contract -- Key was already used and cleaned up -- Check the public key format - -**"Method not allowed"** -- Trying to call a function not in the allowed methods list -- Our keys only allow `claim_for` and `create_account_and_claim` - -**"Insufficient allowance"** -- Key ran out of gas budget -- Increase `FUNCTION_CALL_ALLOWANCE` if needed - -**"Key already exists"** -- Trying to add a duplicate key -- Generate new unique keys for each drop - ---- - -## Why This Matters - -Function-call access keys are NEAR's superpower for user experience: - -🎯 **No Onboarding Friction**: New users can interact immediately -⚡ **Gasless Operations**: Recipients don't pay anything -🔒 **Still Secure**: Keys have minimal, specific permissions -🚀 **Scalable**: Works for any number of recipients - -This is what makes NEAR Drop possible - without function-call keys, you'd need a completely different (and much more complex) approach. - ---- - -## Next Steps - -Now that you understand how the gasless magic works, let's see how to create new NEAR accounts during the claiming process. - -[Continue to Account Creation →](./account-creation.md) - ---- - -:::tip Key Insight -Function-call access keys are like giving someone a specific key to your house that only opens one room and only works once. It's secure, limited, and perfect for token distribution! -::: diff --git a/docs/tutorials/neardrop/account-creation.md b/docs/tutorials/neardrop/account-creation.md deleted file mode 100644 index dcede4b9a97..00000000000 --- a/docs/tutorials/neardrop/account-creation.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -id: account-creation -title: Account Creation -sidebar_label: Account Creation -description: "Enable new users to create NEAR accounts automatically when claiming their first tokens." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -The ultimate onboarding experience: users can claim tokens AND get a NEAR account created for them automatically. No existing account required! - ---- - -## The Magic of NEAR Account Creation - -Most blockchains require you to have an account before you can receive tokens. NEAR flips this around: - -**Traditional Flow:** -1. Create wallet → Fund with tokens → Receive more tokens - -**NEAR Drop Flow:** -1. Get private key → Claim tokens → Account created automatically ✨ - -This eliminates the biggest barrier to Web3 adoption. - ---- - -## How It Works - -Account creation happens in two phases: - -### Phase 1: Create the Account - - -### Phase 2: Claim the Tokens - - -If account creation succeeds, we proceed with the normal claiming process. If it fails (account already exists), we try to claim anyway. - ---- - -## Implementation - -Add this to your `src/claim.rs`: - - - -Validate account ID format: - - - -Calculate funding based on drop type: - - - ---- - -## Account Naming Strategies - -### User-Chosen Names - -Let users pick their own account names: - - - -### Deterministic Names - -Or generate predictable names from keys: - - - ---- - -## Frontend Integration - -Make account creation seamless in your UI: - - - ---- - -## Testing Account Creation - -```bash -# Test creating new account and claiming -near call drop-test.testnet create_named_account_and_claim '{ - "preferred_name": "alice-new" -}' --accountId drop-test.testnet \ - --keyPair - -# Check if account was created -near view alice-new.testnet state - -# Verify balance includes claimed tokens -near view alice-new.testnet account -``` - ---- - -## Error Handling - -Handle common issues gracefully: - - - ---- - -## Cost Considerations - -Account creation costs depend on the drop type: - - - ---- - -## Frontend Account Creation Flow - -Add account creation options to your claiming interface: - - - ---- - -## What You've Accomplished - -Amazing! You now have: - -✅ **Automatic account creation** during claims -✅ **Flexible naming strategies** (user-chosen or deterministic) -✅ **Robust error handling** for edge cases -✅ **Cost optimization** based on drop types -✅ **Seamless UX** that removes Web3 barriers - -This is the complete onboarding solution - users go from having nothing to owning a NEAR account with tokens in a single step! - ---- - -## Real-World Impact - -Account creation enables powerful use cases: - -🎯 **Mass Onboarding**: Bring thousands of users to Web3 instantly -🎁 **Gift Cards**: Create accounts for family/friends with token gifts -📱 **App Onboarding**: New users get accounts + tokens to start using your dApp -🎮 **Gaming**: Players get accounts + in-game assets automatically -🏢 **Enterprise**: Employee onboarding with company tokens - -You've eliminated the biggest friction point in Web3 adoption! - ---- - -## Next Steps - -With gasless claiming and automatic account creation working, it's time to build a beautiful frontend that makes this power accessible to everyone. - -[Continue to Frontend Integration →](./frontend.md) - ---- - -:::tip Pro Tip -Always provide enough initial funding for the account type. FT drops need more funding because recipients might need to register on multiple FT contracts later. -::: diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md deleted file mode 100644 index 19d14f6e96b..00000000000 --- a/docs/tutorials/neardrop/contract-architecture.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -id: contract-architecture -title: Contract Architecture -sidebar_label: Contract Architecture -description: "Understand how the NEAR Drop contract works - the core data types, storage patterns, and drop management system." ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -Before we start coding, let's understand how the NEAR Drop contract is structured. Think of it as the blueprint for our token distribution system. - ---- - -## The Big Picture - -The contract manages three things: -1. **Drops** - Collections of tokens ready for distribution -2. **Keys** - Private keys that unlock specific drops -3. **Claims** - The process of users getting their tokens - -Here's how they connect: - -``` -Drop #1 (10 NEAR) ──→ Key A ──→ Alice claims -Drop #1 (10 NEAR) ──→ Key B ──→ Bob claims -Drop #2 (1 NFT) ──→ Key C ──→ Carol claims -``` - ---- - -## Contract State - -The contract stores everything in four simple maps: - - - -**Why this design?** -- Find drops quickly by key (for claiming) -- Find drops by ID (for management) -- Keep storage costs reasonable - ---- - -## Drop Types - -We support three types of token drops: - -### NEAR Drops - - -### Fungible Token Drops - - -### NFT Drops - - -All wrapped in an enum: - - ---- - -## The Magic: Function-Call Keys - -Here's where NEAR gets awesome. Instead of requiring gas fees, we use **function-call access keys**. - -When you create a drop: -1. Generate public/private key pairs -2. Add public keys to the contract with limited permissions -3. Share private keys with recipients -4. Recipients sign transactions using the contract's account (gasless!) - -The keys can ONLY call claiming functions - nothing else. - - - ---- - -## Storage Cost Management - -Creating drops costs money because we're storing data on-chain. The costs include: - - - -**Total for 5-key NEAR drop**: ~0.08 NEAR + token amounts - ---- - -## Security Model - -The contract protects against common attacks: - -**Access Control** -- Only specific functions can be called with function-call keys -- Keys are removed after use to prevent reuse -- Amount validation prevents overflows - -**Key Management** -- Each key works only once -- Keys have limited gas allowances -- Automatic cleanup after claims - -**Error Handling** -```rust -// Example validation -assert!(!token_id.is_empty(), "Token ID cannot be empty"); -assert!(amount > 0, "Amount must be positive"); -``` - ---- - -## File Organization - -We'll organize the code into logical modules: - -``` -src/ -├── lib.rs # Main contract and initialization -├── drop_types.rs # Drop type definitions -├── near_drops.rs # NEAR token drop logic -├── ft_drops.rs # Fungible token drop logic -├── nft_drops.rs # NFT drop logic -├── claim.rs # Claiming logic for all types -└── external.rs # Cross-contract interfaces -``` - -This keeps things organized and makes it easy to understand each piece. - ---- - -## What's Next? - -Now that you understand the architecture, let's start building! We'll begin with the simplest drop type: NEAR tokens. - -[Continue to NEAR Token Drops →](./near-drops.md) - ---- - -:::tip Key Takeaway -The contract is essentially a **key-to-token mapping system** powered by NEAR's function-call access keys. Users get keys, keys unlock tokens, and everything happens without gas fees for the recipient! -::: diff --git a/docs/tutorials/neardrop/frontend.md b/docs/tutorials/neardrop/frontend.md deleted file mode 100644 index 6889d1f0832..00000000000 --- a/docs/tutorials/neardrop/frontend.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -id: frontend -title: Frontend Integration -sidebar_label: Frontend Integration -description: "Build a React app that makes creating and claiming drops as easy as a few clicks." ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -Time to build a user-friendly interface! Let's create a React app that makes your NEAR Drop system accessible to everyone. - ---- - -## Quick Setup - -```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 -``` - -Create `.env.local`: -```bash -NEXT_PUBLIC_NETWORK_ID=testnet -NEXT_PUBLIC_CONTRACT_ID=your-drop-contract.testnet -NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org -``` - ---- - -## NEAR Connection Service - -Create `src/utils/near.js`: - - - ---- - -## Key Generation Utility - -Create `src/utils/crypto.js`: - - - ---- - -## Drop Creation Component - -Create `src/components/CreateDrop.js`: - - - ---- - -## Drop Results Component - -Create `src/components/DropResults.js`: - - - ---- - -## Claiming Component - -Create `src/components/ClaimDrop.js`: - - - ---- - -## Main App Layout - -Create `src/pages/index.js`: - - - ---- - -## QR Code Generation - -Add QR code generation for easy sharing: - - - ---- - -## Mobile-First Design - -Ensure your CSS is mobile-responsive: - - - ---- - -## Deploy Your Frontend - -```bash -# Build for production -npm run build - -# Deploy to Vercel -npm i -g vercel -vercel --prod - -# Or deploy to Netlify -# Just connect your GitHub repo and it'll auto-deploy -``` - -Add environment variables in your deployment platform: -- `NEXT_PUBLIC_NETWORK_ID=testnet` -- `NEXT_PUBLIC_CONTRACT_ID=your-contract.testnet` -- `NEXT_PUBLIC_RPC_URL=https://rpc.testnet.near.org` - ---- - -## Advanced Features - -### Batch Key Generation - -For large drops, add batch processing: - - - -### Drop Analytics - -Track drop performance: - - - ---- - -## What You've Built - -Awesome! You now have a complete web application with: - -✅ **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 -✅ **Batch processing** for large drops -✅ **Analytics dashboard** for tracking performance - -Your users can now create and claim token drops with just a few clicks - no technical knowledge required! - ---- - -## Testing Your Frontend - -1. **Local Development**: - ```bash - npm run dev - ``` - -2. **Connect Wallet**: Test wallet connection with testnet -3. **Create Small Drop**: Try creating a 1-key NEAR drop -4. **Test Claiming**: Use the generated private key to claim -5. **Mobile Testing**: Verify responsive design on mobile devices - ---- - -## Production Considerations - -**Security**: -- Never log private keys in production -- Validate all user inputs -- Use HTTPS for all requests - -**Performance**: -- Implement proper loading states -- Cache contract calls where possible -- Optimize images and assets - -**User Experience**: -- Add helpful error messages -- Provide clear instructions -- Support keyboard navigation - ---- - -## Next Steps - -Your NEAR Drop system is complete! Consider adding: - -- **Social sharing** for claim links -- **Email notifications** for drop creators -- **Advanced analytics** with charts -- **Multi-language support** -- **Custom themes** and branding - ---- - -:::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! -::: diff --git a/docs/tutorials/neardrop/ft-drops.md b/docs/tutorials/neardrop/ft-drops.md deleted file mode 100644 index 75b2b5283cc..00000000000 --- a/docs/tutorials/neardrop/ft-drops.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -id: ft-drops -title: Fungible Token Drops -sidebar_label: FT Drops -description: "Add support for NEP-141 fungible tokens with cross-contract calls and automatic user registration." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -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. - ---- - -## Why FT Drops Are Different - -Unlike NEAR tokens (which are native), fungible tokens live in separate contracts. This means: - -- **Cross-contract calls** to transfer tokens -- **User registration** on FT contracts (for storage) -- **Callback handling** when things go wrong -- **More complex gas management** - -But don't worry - we'll handle all of this step by step. - ---- - -## Extend Drop Types - -First, let's add FT support to our drop types in `src/drop_types.rs`: - - - -Update the helper methods: - - ---- - -## Cross-Contract Interface - -Create `src/external.rs` to define how we talk to FT contracts: - - - ---- - -## Creating FT Drops - -Add this to your main contract in `src/lib.rs`: - - - ---- - -## FT Claiming Logic - -The tricky part! Update your `src/claim.rs`: - - - -FT claiming with automatic user registration: - - - ---- - -## Testing FT Drops - -You'll need an FT contract to test with. Let's use a simple one: - -```bash -# Deploy a test FT contract (you can use the reference implementation) -near create-account test-ft.testnet --useFaucet -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-test.testnet", - "total_supply": "1000000000000000000000000000" -}' --accountId test-ft.testnet -``` - -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 -``` - -Now create an FT drop: - -```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 - - - ---- - -## Common Issues & Solutions - -**"Storage deposit failed"** -- The FT contract needs sufficient balance to register users -- Make sure you attach enough NEAR when creating the drop - -**"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 - ---- - -## What You've Accomplished - -Great work! You now have: - -✅ **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 - -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 up: NFT drops, which have their own unique challenges around uniqueness and ownership. - -[Continue to NFT Drops →](./nft-drops.md) - ---- - -:::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. -::: diff --git a/docs/tutorials/neardrop/introduction.md b/docs/tutorials/neardrop/introduction.md deleted file mode 100644 index 13fc1686604..00000000000 --- a/docs/tutorials/neardrop/introduction.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -id: introduction -title: NEAR Drop Tutorial -sidebar_label: Introduction -description: "Build a token distribution system that lets you airdrop NEAR, FTs, and NFTs to users without them needing gas fees or existing accounts." ---- - -import {Github} from "@site/src/components/codetabs" - -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! - -**NEAR Drop** lets you create token distributions that anyone can claim with just a private key - no NEAR account or gas fees required. - ---- - -## What You'll Build - -A complete token distribution system with: - -- **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 - ---- - -## How It Works - -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 - -The magic? **Function-call access keys** - NEAR's unique feature that enables gasless operations. - ---- - -## Project Structure - -Your completed project will look like this: - - - -The frontend structure: - - - ---- - -## Real Examples - -- **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 You Need - -- [Rust installed](https://rustup.rs/) -- [NEAR CLI](../../tools/cli.md#installation) -- [A NEAR wallet](https://testnet.mynearwallet.com) -- Basic understanding of smart contracts - ---- - -## Tutorial Structure - -| 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 | -| [Access Keys](/tutorials/neardrop/access-keys) | Understanding gasless operations | -| [Account Creation](/tutorials/neardrop/account-creation) | Auto account creation | -| [Frontend](/tutorials/neardrop/frontend) | Build a web interface | - -Each section builds on the previous one, so start from the beginning! - ---- - -## Repository Links - -- **Smart Contract**: [github.com/Festivemena/Near-drop](https://github.com/Festivemena/Near-drop) -- **Frontend**: [github.com/Festivemena/Drop](https://github.com/Festivemena/Drop) - ---- - -## Quick Start - -If you want to jump straight into the code: - -```bash -# Clone the smart contract -git clone https://github.com/Festivemena/Near-drop.git -cd Near-drop - -# Build and deploy -cd contract -cargo near build -near deploy .testnet target/near/near_drop.wasm - -# Clone the frontend -git clone https://github.com/Festivemena/Drop.git -cd Drop -npm install -npm run dev -``` - ---- - -## Ready to Start? - -Let's dive into how the contract architecture works and start building your token distribution system. - -[Continue to Contract Architecture →](./contract-architecture.md) - ---- - -:::note Version Requirements -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`+ -- node: `18.0.0`+ -::: diff --git a/docs/tutorials/neardrop/near-drops.md b/docs/tutorials/neardrop/near-drops.md deleted file mode 100644 index 5a5d7ffe116..00000000000 --- a/docs/tutorials/neardrop/near-drops.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -id: near-drops -title: NEAR Token Drops -sidebar_label: NEAR Token Drops -description: "Build the foundation: distribute native NEAR tokens using function-call keys for gasless claiming." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -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. - ---- - -## Project Setup - -First, create a new Rust project: - -```bash -cargo near new near-drop --contract -cd near-drop -``` - -Update `Cargo.toml`: -```toml -[dependencies] -near-sdk = { version = "5.1.0", features = ["unstable"] } -serde = { version = "1.0", features = ["derive"] } -``` - ---- - -## Basic Contract Structure - -Let's start with the main contract in `src/lib.rs`: - - - ---- - -## Contract Initialization - - - ---- - -## Creating NEAR Drops - -The main function everyone will use: - - - ---- - -## Claiming Tokens - -Now for the claiming logic in `src/claim.rs`: - - - -Core claiming logic: - - - ---- - -## Helper Functions - -Add some useful view functions: - - - ---- - -## Build and Test - -```bash -# Build the contract -cargo near build - -# 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 -``` - ---- - -## 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 -``` - ---- - -## Claim Tokens - -Recipients can now claim using their private keys: - -```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 -``` - ---- - -## 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 - -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.md) - ---- - -:::tip Quick Test -Try creating a small drop and claiming it yourself to make sure everything works before moving on! -::: diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md deleted file mode 100644 index ebf27167741..00000000000 --- a/docs/tutorials/neardrop/nft-drops.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -id: nft-drops -title: NFT Drops -sidebar_label: NFT Drops -description: "Distribute unique NFTs with one-time claims and ownership verification." ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {Github} from "@site/src/components/codetabs" - -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. - ---- - -## What Makes NFT Drops Different - -- **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 - ---- - -## Add NFT Support - -First, extend your drop types in `src/drop_types.rs`: - - -Update the helper methods: - - ---- - -## NFT Cross-Contract Interface - -Add NFT methods to `src/external.rs`: - - - ---- - -## Creating NFT Drops - -Add this to your main contract: - - - -Batch NFT creation: - - - ---- - -## NFT Claiming Logic - -Update your claiming logic in `src/claim.rs`: - - - ---- - -## Testing NFT Drops - -You'll need an NFT contract for testing: - -```bash -# Deploy test NFT contract -near create-account test-nft.testnet --useFaucet -near deploy test-nft.testnet nft-contract.wasm - -# Initialize -near call test-nft.testnet new_default_meta '{ - "owner_id": "drop-test.testnet" -}' --accountId test-nft.testnet - -# Mint NFT to your drop contract -near call test-nft.testnet nft_mint '{ - "token_id": "unique-nft-001", - "metadata": { - "title": "Exclusive Drop NFT", - "description": "A unique NFT from NEAR Drop" - }, - "receiver_id": "drop-test.testnet" -}' --accountId drop-test.testnet --deposit 0.1 -``` - -Create and test the NFT drop: - -```bash -# Create NFT drop -near call drop-test.testnet create_nft_drop '{ - "public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", - "nft_contract": "test-nft.testnet", - "token_id": "unique-nft-001" -}' --accountId drop-test.testnet --deposit 0.1 - -# Claim the NFT -near call drop-test.testnet claim_for '{ - "account_id": "alice.testnet" -}' --accountId drop-test.testnet \ - --keyPair - -# Verify Alice owns the NFT -near view test-nft.testnet nft_token '{"token_id": "unique-nft-001"}' -``` - ---- - -## Helper Functions - -Add some useful view methods: - - - ---- - -## Important Notes - -**⚠️ Ownership is Critical** -- The drop contract MUST own the NFT before creating the drop -- If the contract doesn't own the NFT, claiming will fail -- Always verify ownership before creating drops - -**🔒 Security Considerations** -- Each NFT drop supports exactly 1 key (since NFTs are unique) -- Once claimed, the NFT drop is completely removed -- No possibility of double-claiming the same NFT - -**💰 Cost Structure** -- NFT drops are cheaper than multi-key drops (only 1 key) -- No need for token funding (just storage + gas costs) -- Total cost: ~0.017 NEAR per NFT drop - ---- - -## What You've Accomplished - -Great work! You now have complete NFT drop support: - -✅ **Unique NFT distribution** with proper ownership validation -✅ **Cross-contract NFT transfers** with error handling -✅ **Batch NFT drop creation** for collections -✅ **Complete cleanup** after claims (no leftover data) -✅ **Security measures** to prevent double-claiming - -Your NEAR Drop system now supports all three major token types: NEAR, FTs, and NFTs! - ---- - -## Next Steps - -Let's explore how function-call access keys work in detail to understand the gasless claiming mechanism. - -[Continue to Access Key Management →](./access-keys.md) - ---- - -:::tip NFT Drop Pro Tips -- Always test with a small NFT collection first -- Verify the drop contract owns all NFTs before creating drops -- Consider using batch creation for large NFT collections -- NFT drops are perfect for event tickets, collectibles, and exclusive content -::: From b1f811037b12c9e585f1c317661bd4f7a73f30d2 Mon Sep 17 00:00:00 2001 From: Efemena <88979259+Festivemena@users.noreply.github.com> Date: Sat, 23 Aug 2025 17:19:59 +0100 Subject: [PATCH 23/23] feat: add description tag to front matter (#2642) * feat: add description tag to front matter * feat: add more description and delete description duplicate * feat: add more description --- docs/tutorials/examples/near-drop.md | 628 ++++++++++----------------- 1 file changed, 222 insertions(+), 406 deletions(-) diff --git a/docs/tutorials/examples/near-drop.md b/docs/tutorials/examples/near-drop.md index f8ec82cb66e..e47f9b08f3f 100644 --- a/docs/tutorials/examples/near-drop.md +++ b/docs/tutorials/examples/near-drop.md @@ -8,480 +8,296 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import {CodeTabs, Language, Github} from "@site/src/components/codetabs" -# NEAR Drop Tutorial: Creating Token Airdrops Made Simple +NEAR Drop is a smart contract that allows users to create token drops ($NEAR, Fungible and Non-Fungible Tokens), and link them to specific private keys. Whoever has the private key can claim the drop into an existing account, or ask the contract to create a new one for them. -Ever wanted to send tokens to someone who doesn't have a NEAR account yet? Or maybe you want to distribute tokens to a group of people in a seamless way? That's exactly what NEAR Drop contracts are for! +Particularly, it shows: -Get step by step usage [Here](../../tutorials/near drop/introduction.md) +1. How to create a token drops (NEAR, FT and NFT) +2. How to leverage Function Call keys for enabling amazing UX -## What Are Drops? +:::tip -Think of a drop as a digital gift card that you can send to anyone. Here's how it works: +This example showcases a simplified version of the contract that both [Keypom](https://keypom.xyz/) and the [Token Drop Utility](https://dev.near.org/tools?tab=linkdrops) use to distribute tokens to users -**Traditional way**: "Hey Bob, create a NEAR account first, then I'll send you some tokens" -**With drops**: "Hey Bob, here's a link. Click it and you'll get tokens AND a new account automatically" +::: -A drop is essentially a smart contract that holds tokens (NEAR, fungible tokens, or NFTs) and links them to a special private key. Anyone with that private key can claim the tokens - either into an existing account or by creating a brand new account on the spot. - -### Real-World Example - -Imagine Alice wants to onboard her friend Bob to NEAR: - -1. **Alice creates a drop**: She puts 5 NEAR tokens into a drop and gets a special private key -2. **Alice shares the key**: She sends Bob the private key (usually as a link) -3. **Bob claims the drop**: Bob uses the key to either: - - Claim tokens into his existing NEAR account, or - - Create a new NEAR account and receive the tokens there - -The magic happens because of NEAR's unique **Function Call Keys** - the contract can actually create accounts on behalf of users! - -## Types of Drops - -There are three types of drops you can create: - -- **NEAR Drops**: Drop native NEAR tokens -- **FT Drops**: Drop fungible tokens (like stablecoins) -- **NFT Drops**: Drop non-fungible tokens (like collectibles) - -## Building Your Own Drop Contract - -Let's walk through creating a drop contract step by step. +--- -### 1. Setting Up the Contract Structure +## Contract Overview -First, let's understand what our contract needs to track: +The contract exposes 3 methods to create drops of NEAR tokens, FT, and NFT. To claim the tokens, the contract exposes two methods, one to claim in an existing account, and another that will create a new account and claim the tokens into it. - +This contract leverages NEAR unique feature of [FunctionCall keys](../../protocol/access-keys.md), which allows the contract to create new accounts and claim tokens on behalf of the user. -```rust -#[near_bindgen] -#[derive(BorshDeserialize, BorshSerialize)] -pub struct NearDropContract { - /// The account used to create new accounts (usually "testnet" or "mainnet") - pub top_level_account: AccountId, - - /// Counter for assigning unique IDs to drops - pub next_drop_id: u64, - - /// Maps public keys to their corresponding drop IDs - pub drop_id_by_key: UnorderedMap, - - /// Maps drop IDs to the actual drop data - pub drop_by_id: UnorderedMap, -} -``` +Imagine Alice want to drop some NEAR to Bob: - +1. Alice will call `create_near_drop` passing some NEAR amount, and a **Public** Access Key +2. The Contract will check if Alice attached enough tokens and create the drop +3. The Contract will add the `PublicKey` as a `FunctionCall Key` to itself, that **only allow to call the claim methods** +4. Alice will give the `Private Key` to Bob +5. Bob will use the Key to sign a transaction calling the `claim_for` method +6. The Contract will check if the key is linked to a drop, and if it is, it will send the drop -### 2. Defining Drop Types +It is important to notice that, in step (5), Bob will be using the Contract's account to sign the transaction, and not his own account. Remember that in step (3) the contract added the key to itself, meaning that anyone with the key can call the claim methods in the name of the contract. -We need to handle three different types of drops: +
- +Contract's interface -```rust -#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] -pub enum Drop { - Near(NearDrop), - FungibleToken(FtDrop), - NonFungibleToken(NftDrop), -} - -#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] -pub struct NearDrop { - pub amount_per_drop: U128, - pub counter: u64, -} - -#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] -pub struct FtDrop { - pub contract_id: AccountId, - pub amount_per_drop: U128, - pub counter: u64, -} - -#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] -pub struct NftDrop { - pub contract_id: AccountId, - pub counter: u64, -} -``` +#### `create_near_drop(public_keys, amount_per_drop)` +Creates `#public_keys` drops, each with `amount_per_drop` NEAR tokens on them - +#### `create_ft_drop(public_keys, ft_contract, amount_per_drop)` +Creates `#public_keys` drops, each with `amount_per_drop` FT tokens, corresponding to the `ft_contract` -### 3. Creating NEAR Drops +#### `create_nft_drop(public_key, nft_contract)` +Creates a drop with an NFT token, which will come from the `nft_contract` -Here's how to implement NEAR token drops: +#### `claim_for(account_id)` +Claims a drop, which will be sent to the existing `account_id` - +#### `create_account_and_claim(account_id)` +Creates the `account_id`, and then drops the tokens into it -```rust -#[payable] -pub fn create_near_drop( - &mut self, - public_keys: Vec, - amount_per_drop: U128, -) -> bool { - let attached_deposit = env::attached_deposit(); - let amount_per_drop: u128 = amount_per_drop.into(); - - // Calculate required deposit - let required_deposit = (public_keys.len() as u128) * amount_per_drop; - - // Check if user attached enough NEAR - require!( - attached_deposit >= required_deposit, - "Not enough deposit attached" - ); - - // Create the drop - let drop_id = self.next_drop_id; - self.next_drop_id += 1; - - let drop = Drop::Near(NearDrop { - amount_per_drop: amount_per_drop.into(), - counter: public_keys.len() as u64, - }); - - // Store the drop - self.drop_by_id.insert(&drop_id, &drop); - - // Add each public key to the contract and map it to the drop - for public_key in public_keys { - // Add key to contract as a function call key - self.add_function_call_key(public_key.clone()); - - // Map the key to this drop - self.drop_id_by_key.insert(&public_key, &drop_id); - } - - true -} - -fn add_function_call_key(&self, public_key: PublicKey) { - let promise = Promise::new(env::current_account_id()).add_access_key( - public_key, - ACCESS_KEY_ALLOWANCE, - env::current_account_id(), - "claim_for,create_account_and_claim".to_string(), - ); - promise.as_return(); -} -``` +
-
+--- -### 4. Creating FT Drops +## Contract's State -For fungible token drops, the process is similar but we need to handle token transfers: +We can see in the contract's state that the contract keeps track of different `PublicKeys`, and links them to a specific `DropId`, which is simply an identifier for a `Drop` (see below). - +- `top_level_account`: The account that will be used to create new accounts, generally it will be `testnet` or `mainnet` +- `next_drop_id`: A simple counter used to assign unique identifiers to each drop +- `drop_id_by_key`: A `Map` between `PublicKey` and `DropId`, which allows the contract to know what drops are claimable by a given key +- `drop_by_id`: A simple `Map` that links each `DropId` with the actual `Drop` data. -```rust -pub fn create_ft_drop( - &mut self, - public_keys: Vec, - ft_contract: AccountId, - amount_per_drop: U128, -) -> Promise { - let drop_id = self.next_drop_id; - self.next_drop_id += 1; - - let drop = Drop::FungibleToken(FtDrop { - contract_id: ft_contract.clone(), - amount_per_drop, - counter: public_keys.len() as u64, - }); - - self.drop_by_id.insert(&drop_id, &drop); - - for public_key in public_keys { - self.add_function_call_key(public_key.clone()); - self.drop_id_by_key.insert(&public_key, &drop_id); - } - - // Transfer FT tokens to the contract - let total_amount: u128 = amount_per_drop.0 * (drop.counter as u128); - - ext_ft_contract::ext(ft_contract) - .with_attached_deposit(1) - .ft_transfer_call( - env::current_account_id(), - total_amount.into(), - None, - "".to_string(), - ) -} -``` + - - -### 5. Claiming Drops +--- -Users can claim drops in two ways: +## Drop Types -#### Claim to Existing Account +There are 3 types of drops, which differ in what the user will receive when they claims the corresponding drop - NEAR, fungible tokens (FTs) or non-fungible tokens (NFTs). - -```rust -pub fn claim_for(&mut self, account_id: AccountId) -> Promise { - let public_key = env::signer_account_pk(); - self.internal_claim(account_id, public_key) -} - -fn internal_claim(&mut self, account_id: AccountId, public_key: PublicKey) -> Promise { - // Get the drop ID for this key - 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"); - - // Decrease counter - match &mut drop { - Drop::Near(near_drop) => { - near_drop.counter -= 1; - let amount = near_drop.amount_per_drop.0; - - // Transfer NEAR tokens - Promise::new(account_id).transfer(amount) - } - Drop::FungibleToken(ft_drop) => { - ft_drop.counter -= 1; - let amount = ft_drop.amount_per_drop; - - // Transfer FT tokens - ext_ft_contract::ext(ft_drop.contract_id.clone()) - .with_attached_deposit(1) - .ft_transfer(account_id, amount, None) - } - Drop::NonFungibleToken(nft_drop) => { - nft_drop.counter -= 1; - - // Transfer NFT - ext_nft_contract::ext(nft_drop.contract_id.clone()) - .with_attached_deposit(1) - .nft_transfer(account_id, "token_id".to_string(), None, None) - } - } - - // Update or remove the drop - if drop.get_counter() == 0 { - self.drop_by_id.remove(&drop_id); - self.drop_id_by_key.remove(&public_key); - } else { - self.drop_by_id.insert(&drop_id, &drop); - } -} -``` - + + + + -#### Claim to New Account - - +:::info -```rust -pub fn create_account_and_claim(&mut self, account_id: AccountId) -> Promise { - let public_key = env::signer_account_pk(); - - // Create the new account first - Promise::new(account_id.clone()) - .create_account() - .add_full_access_key(public_key.clone()) - .transfer(NEW_ACCOUNT_BALANCE) - .then( - Self::ext(env::current_account_id()) - .with_static_gas(Gas(30_000_000_000_000)) - .resolve_account_create(account_id, public_key) - ) -} - -#[private] -pub fn resolve_account_create( - &mut self, - account_id: AccountId, - public_key: PublicKey, -) -> Promise { - match env::promise_result(0) { - PromiseResult::Successful(_) => { - // Account created successfully, now claim the drop - self.internal_claim(account_id, public_key) - } - _ => { - env::panic_str("Failed to create account"); - } - } -} -``` +Notice that in this example implementation users cannot mix drops. This is, you can either drop NEAR tokens, or FT, or NFTs, but not a mixture of them (i.e. you cannot drop 1 NEAR token and 1 FT token in the same drop) - +::: -### 6. Deployment and Usage +--- -#### Deploy the Contract +## Create a drop + +All `create` start by checking that the user deposited enough funds to create the drop, and then proceed to add the access keys to the contract's account as [FunctionCall Keys](../../protocol/access-keys.md). + + + + + + + + + + + + + + + + + + + + + + - - +
-```bash -# Build the contract -cargo near build +### Storage Costs -# Deploy with initialization -cargo near deploy .testnet with-init-call new json-args '{"top_level_account": "testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` +While we will not go into the details of how the storage costs are calculated, it is important to know what is being taken into account: -
- +1. The cost of storing each Drop, which will include storing all bytes associated with the `Drop` struct +2. The cost of storing each `PublicKey -> DropId` relation in the maps +3. Cost of storing each `PublicKey` in the account -```bash -# Build the contract -cargo near build +Notice that (3) is not the cost of storing the byte representation of the `PublicKey` on the state, but the cost of adding the key to the contract's account as a FunctionCall key. -# Deploy with initialization -cargo near deploy .testnet \ - with-init-call new \ - json-args '{"top_level_account": "testnet"}' \ - prepaid-gas '100.0 Tgas' \ - attached-deposit '0 NEAR' \ - network-config testnet \ - sign-with-keychain send -``` +--- - +## Claim a drop + +In order to claim drop, a user needs to sign a transaction using the `Private Key`, which is the counterpart of the `Public Key` that was added to the contract. + +All `Drops` have a `counter` which decreases by 1 each time a drop is claimed. This way, when all drops are claimed (`counter` == 0), we can remove all information from the Drop. + +There are two ways to claim a drop: claim for an existing account and claim for a new account. The main difference between them is that the first one will send the tokens to an existing account, while the second one will create a new account and send the tokens to it. + +
+ + + + + + + + + + + + + + + -#### Create a Drop - - - +--- -```bash -# Create a NEAR drop -near call .testnet create_near_drop '{"public_keys": ["ed25519:YourPublicKeyHere"], "amount_per_drop": "1000000000000000000000000"}' --accountId .testnet --deposit 2 --gas 100000000000000 -``` +### Testing the Contract - - +The contract readily includes a sandbox testing to validate its functionality. To execute the tests, run the following command: -```bash -# Create a NEAR drop -near contract call-function as-transaction .testnet create_near_drop json-args '{"public_keys": ["ed25519:YourPublicKeyHere"], "amount_per_drop": "1000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '2 NEAR' sign-as .testnet network-config testnet sign-with-keychain send -``` + + + + ```bash + cargo test + ``` - + -#### Claim a Drop +:::tip +The `integration tests` use a sandbox to create NEAR users and simulate interactions with the contract. +::: - - +--- -```bash -# Claim to existing account -near call .testnet claim_for '{"account_id": ".testnet"}' --accountId .testnet --gas 30000000000000 --useLedgerKey "ed25519:YourPrivateKeyHere" +### Deploying the Contract to the NEAR network -# Claim to new account -near call .testnet create_account_and_claim '{"account_id": ".testnet"}' --accountId .testnet --gas 100000000000000 --useLedgerKey "ed25519:YourPrivateKeyHere" -``` +In order to deploy the contract you will need to create a NEAR account. - - + + -```bash -# Claim to existing account -near contract call-function as-transaction .testnet claim_for json-args '{"account_id": ".testnet"}' prepaid-gas '30.0 Tgas' attached-deposit '0 NEAR' sign-as .testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:YourPublicKeyHere --signer-private-key ed25519:YourPrivateKeyHere send + ```bash + # Create a new account pre-funded by a faucet + near create-account --useFaucet + ``` + -# Claim to new account -near contract call-function as-transaction .testnet create_account_and_claim json-args '{"account_id": ".testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as .testnet network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:YourPublicKeyHere --signer-private-key ed25519:YourPrivateKeyHere send -``` + - + ```bash + # Create a new account pre-funded by a faucet + near account create-account sponsor-by-faucet-service .testnet autogenerate-new-keypair save-to-keychain network-config testnet create + ``` + -### 7. Testing Your Contract - - - - -Create integration tests to verify functionality: - -```rust -#[cfg(test)] -mod tests { - use super::*; - use near_sdk::test_utils::{accounts, VMContextBuilder}; - use near_sdk::{testing_env, MockedBlockchain}; - - #[test] - fn test_create_near_drop() { - let context = VMContextBuilder::new() - .signer_account_id(accounts(0)) - .attached_deposit(1000000000000000000000000) // 1 NEAR - .build(); - testing_env!(context); - - let mut contract = NearDropContract::new(accounts(0)); - - let public_keys = vec![ - "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse().unwrap() - ]; - - let result = contract.create_near_drop( - public_keys, - U128(500000000000000000000000) // 0.5 NEAR per drop - ); - - assert!(result); - } - - #[test] - fn test_claim_drop() { - // Set up contract and create drop - // Then test claiming functionality - } -} -``` - -Run the tests: +Then build and deploy the contract: ```bash -cargo test -``` - - - - -### Key Points to Remember +cargo near build -1. **Function Call Keys**: The contract adds public keys as function call keys to itself, allowing holders of the private keys to call claim methods -2. **Storage Costs**: Account for storage costs when calculating required deposits -3. **Security**: Only specific methods can be called with the function call keys -4. **Cleanup**: Remove drops and keys when all tokens are claimed to save storage -5. **Error Handling**: Always validate inputs and handle edge cases +cargo near deploy with-init-call new json-args '{"top_level_account": "testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +``` -This contract provides a foundation for token distribution systems and can be extended with additional features like: -- Time-based expiration -- Multiple token types in a single drop -- Whitelist functionality -- Custom claim conditions +--- -The beauty of this system is that it dramatically improves user onboarding - users can receive tokens and create accounts in a single step, removing traditional barriers to blockchain adoption. +### CLI: Interacting with the Contract -## Why This Matters +To interact with the contract through the console, you can use the following commands: -Drop contracts solve a real problem in blockchain adoption. Instead of the usual friction of "create an account first, then I'll send you tokens," drops allow you to onboard users seamlessly. They get tokens AND an account in one smooth experience. + + + + ```bash + # create a NEAR drop + near call create_near_drop '{"public_keys": ["ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q", "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"], "amount_per_drop": "10000000000000000000000"}' --accountId --deposit 1 --gas 100000000000000 + + # create a FT drop + near call create_ft_drop '{"public_keys": ["ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"], "amount_per_drop": "1", "ft_contract": ""}' --accountId --gas 100000000000000 + + # create a NFT drop + near call create_nft_drop '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "nft_contract": ""}' --accountId --gas 100000000000000 + + # claim to an existing account + # see the full version + + # claim to a new account + # see the full version + ``` + + + + + ```bash + # create a NEAR drop + near contract call-function as-transaction create_near_drop json-args '{"public_keys": ["ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q", "ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4"], "amount_per_drop": "10000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as network-config testnet sign-with-keychain send + + # create a FT drop + near contract call-function as-transaction create_ft_drop json-args '{"public_keys": ["ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "ed25519:5oN7Yk7FKQMKpuP4aroWgNoFfVDLnY3zmRnqYk9fuEvR"], "amount_per_drop": "1", "ft_contract": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send + + # create a NFT drop + near contract call-function as-transaction create_nft_drop json-args '{"public_key": "ed25519:HcwvxZXSCX341Pe4vo9FLTzoRab9N8MWGZ2isxZjk1b8", "nft_contract": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send + + # claim to an existing account + near contract call-function as-transaction claim_for json-args '{"account_id": ""}' prepaid-gas '30.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:AvBVZDQrg8pCpEDFUpgeLYLRGUW8s5h57NGhb1Tc4H5q --signer-private-key ed25519:3yVFxYtyk7ZKEMshioC3BofK8zu2q6Y5hhMKHcV41p5QchFdQRzHYUugsoLtqV3Lj4zURGYnHqMqt7zhZZ2QhdgB send + + # claim to a new account + near contract call-function as-transaction create_account_and_claim json-args '{"account_id": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-plaintext-private-key --signer-public-key ed25519:4FMNvbvU4epP3HL9mRRefsJ2tMECvNLfAYDa9h8eUEa4 --signer-private-key ed25519:2xZcegrZvP52VrhehvApnx4McL85hcSBq1JETJrjuESC6v6TwTcr4VVdzxaCReyMCJvx9V4X1ppv8cFFeQZ6hJzU send + ``` + + -This is particularly powerful for: +:::note Versioning for this article -- **Airdrops**: Distribute tokens to a large audience -- **Onboarding**: Get new users into your ecosystem -- **Gifts**: Send crypto gifts to friends and family -- **Marketing**: Create engaging distribution campaigns +At the time of this writing, this example works with the following versions: -The NEAR Drop contract leverages NEAR's unique Function Call Keys to create this seamless experience. It's a perfect example of how thoughtful protocol design can enable better user experiences. +- near-cli: `0.17.0` +- rustc: `1.82.0` -Want to see this in action? The contract powers tools like [Keypom](https://keypom.xyz/) and NEAR's Token Drop Utility, making token distribution accessible to everyone. \ No newline at end of file +:::