diff --git a/.gitignore b/.gitignore index 9707d0204ec..315b1d302c1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ website/i18n/* website/.docusaurus/* website/package-lock.json website/yarn.lock +website/pnpm-lock.yaml neardev .idea .docz diff --git a/docs/tutorials/advanced-xcc/0-introduction.md b/docs/tutorials/advanced-xcc/0-introduction.md new file mode 100644 index 00000000000..f90c807acc0 --- /dev/null +++ b/docs/tutorials/advanced-xcc/0-introduction.md @@ -0,0 +1,82 @@ +--- +id: introduction +title: Advanced Cross-Contract Calls on NEAR +sidebar_label: Introduction +description: "Master complex cross-contract call patterns in NEAR Protocol, including batch operations, parallel execution, and advanced error handling." +--- + +Cross-contract calls are one of NEAR's most powerful features, enabling smart contracts to interact with each other seamlessly. While basic cross-contract calls allow simple interactions, advanced patterns unlock the full potential of NEAR's composable architecture. + +## What You'll Build + +By the end of this tutorial, you'll master how to: + +- **Batch multiple actions** to the same contract with atomic rollback +- **Execute parallel calls** to different contracts simultaneously +- **Handle complex responses** from multiple contract interactions +- **Manage gas efficiently** across multiple contract calls +- **Implement robust error handling** for multi-contract scenarios + +## Why Advanced Cross-Contract Calls Matter + +These patterns are essential for: + +- **DeFi protocols** that interact with multiple token contracts and AMMs +- **Gaming platforms** that manage assets across different contract systems +- **DAO governance** systems that execute proposals across multiple contracts +- **NFT marketplaces** that coordinate with various collection contracts + +:::info Source Code + +The complete source code is available at [near-examples/cross-contract-calls](https://github.com/near-examples/cross-contract-calls). + +Test contracts are deployed on testnet: +- `hello.near-examples.testnet` +- `counter.near-examples.testnet` +- `guestbook.near-examples.testnet` + +::: + +## How It Works + +Advanced cross-contract calls leverage NEAR's promise-based architecture: + +```mermaid +graph TD + A[Your Contract] --> B[Batch Actions] + A --> C[Parallel Calls] + + B --> D[Same Contract - Action 1] + B --> E[Same Contract - Action 2] + B --> F[Same Contract - Action 3] + + C --> G[Contract A] + C --> H[Contract B] + C --> I[Contract C] + + D --> J[Single Result] + E --> J + F --> J + + G --> K[Array of Results] + H --> K + I --> K +``` + +Key concepts: + +- **Atomicity**: Batch actions either all succeed or all fail +- **Parallelism**: Multiple contracts execute simultaneously +- **Complex responses**: Handle arrays of results with individual success/failure states +- **Gas optimization**: Efficient gas distribution across multiple calls + +## What You Will Learn + +This tutorial is organized into focused chapters: + +1. **[Project Setup](1-setup.md)** - Get the example project running locally +2. **[Batch Actions](2-batch-actions.md)** - Learn atomic multi-action patterns +3. **[Parallel Execution](3-parallel-execution.md)** - Execute multiple contracts simultaneously +5. **[Testing & Deployment](4-testing-deployment.md)** - Test and deploy your contracts + +Let's [get started with the project setup](1-setup.md)! \ No newline at end of file diff --git a/docs/tutorials/advanced-xcc/1-setup.md b/docs/tutorials/advanced-xcc/1-setup.md new file mode 100644 index 00000000000..60d7fcc956e --- /dev/null +++ b/docs/tutorials/advanced-xcc/1-setup.md @@ -0,0 +1,189 @@ +--- +id: setup +title: Setting up the Advanced Cross-Contract Calls Project +sidebar_label: Project Setup +description: "Get the advanced cross-contract calls example project running locally with all necessary dependencies and test contracts." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Let's set up the project environment with the main contract and test contracts. + +## Obtaining the Project + +```bash +git clone https://github.com/near-examples/cross-contract-calls +cd cross-contract-calls +``` + +Choose your language: + + + + +```bash +cd contract-advanced-ts +npm install +``` + + + + +```bash +cd contract-advanced-rs +cargo check +``` + + + + +## Project Structure + +``` +├── tests/ # Test environment +│ └── external-contracts/ # Pre-compiled contracts +│ ├── counter.wasm # Simple counter +│ ├── guest-book.wasm # Message storage +│ └── hello-near.wasm # Greeting contract +├── src/ # Main contract code +│ ├── batch_actions.* # Batch operations +│ ├── multiple_contracts.* # Parallel execution +│ └── similar_contracts.* # Same-type calls +└── README.md +``` + +## Test Contracts Overview + +### Hello NEAR Contract +- `get_greeting()` - Returns current greeting +- `set_greeting(message: string)` - Updates greeting + +### Counter Contract +- `get_num()` - Returns current count +- `increment()` - Increases by 1 +- `decrement()` - Decreases by 1 + +### Guest Book Contract +- `add_message(text: string)` - Adds message +- `get_messages()` - Returns all messages + +## Building the Contract + + + + +```bash +npm run build +# Output: build/cross_contract.wasm +``` + + + + +```bash +cargo near build +# Output: target/wasm32-unknown-unknown/release/cross_contract.wasm +``` + + + + +## Running Tests + +Verify everything works: + + + + +```bash +npm test +``` + + + + +```bash +cargo test +``` + + + + +Expected output: +``` +✓ Test batch actions +✓ Test multiple contracts +✓ Test similar contracts +``` + +## Contract Initialization + +The main contract needs external contract addresses: + + + + +```typescript +@NearBindgen({}) +export class CrossContractCall { + hello_account: AccountId; + counter_account: AccountId; + guestbook_account: AccountId; + + @initialize({}) + init({ + hello_account, + counter_account, + guestbook_account, + }: { + hello_account: AccountId; + counter_account: AccountId; + guestbook_account: AccountId; + }) { + this.hello_account = hello_account; + this.counter_account = counter_account; + this.guestbook_account = guestbook_account; + } +} +``` + + + + +```rust +#[near_bindgen] +impl Contract { + #[init] + pub fn init( + hello_account: AccountId, + counter_account: AccountId, + guestbook_account: AccountId, + ) -> Self { + Self { + hello_account, + counter_account, + guestbook_account, + } + } +} +``` + + + + +## Deploy Your Contract + +```bash +# Create your account +near account create-account sponsor-by-faucet-service xcc.YOUR_NAME.testnet autogenerate-new-keypair save-to-keychain network-config testnet create + +# Deploy with initialization +near contract deploy xcc.YOUR_NAME.testnet use-file ./build/cross_contract.wasm with-init-call init json-args '{ + "hello_account":"hello.near-examples.testnet", + "counter_account":"counter.near-examples.testnet", + "guestbook_account":"guestbook.near-examples.testnet" +}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +``` + +Now let's explore [batch actions](2-batch-actions.md) in the next chapter! \ No newline at end of file diff --git a/docs/tutorials/advanced-xcc/2-batch-actions.md b/docs/tutorials/advanced-xcc/2-batch-actions.md new file mode 100644 index 00000000000..b3429575473 --- /dev/null +++ b/docs/tutorials/advanced-xcc/2-batch-actions.md @@ -0,0 +1,214 @@ +--- +id: batch-actions +title: Implementing Batch Actions with Atomic Rollback +sidebar_label: Batch Actions +description: "Learn to batch multiple function calls to the same contract with atomic rollback - if one fails, they all get reverted." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Batch actions execute multiple function calls to the same contract sequentially with **atomic rollback** - if any action fails, all actions are reverted. + +## Understanding Batch Actions + +Key characteristics: +1. **Sequential execution** - Actions run one after another +2. **Atomic rollback** - All succeed or all fail +3. **Single gas payment** - Shared gas allocation +4. **Last result access** - Callback receives final action's result + +## Implementation + + + + + + + + + + + + + + +## Handling Batch Responses + +The callback receives only the **last action's result**: + + + + + + + + + + + + + + +## Real-World Example: DeFi Operations + + + + +```typescript +@call({}) +batch_defi_operations({ + token_contract, + amount, + recipient +}: { + token_contract: AccountId; + amount: string; + recipient: AccountId; +}) { + return NearPromise.new(token_contract) + // 1. Approve spending + .functionCall( + "ft_approve", + JSON.stringify({ + spender_id: env.current_account_id(), + amount + }), + 1n, + 10_000_000_000_000n + ) + // 2. Transfer tokens + .functionCall( + "ft_transfer", + JSON.stringify({ + receiver_id: recipient, + amount + }), + 1n, + 15_000_000_000_000n + ) + // 3. Get balance + .functionCall( + "ft_balance_of", + JSON.stringify({ + account_id: recipient + }), + 0n, + 5_000_000_000_000n + ) + .then( + NearPromise.new(env.current_account_id()) + .functionCall( + "defi_callback", + JSON.stringify({amount}), + 0n, + 10_000_000_000_000n + ) + ); +} +``` + + + + +```rust +pub fn batch_defi_operations( + &mut self, + token_contract: AccountId, + amount: U128, + recipient: AccountId, +) -> Promise { + Promise::new(token_contract.clone()) + // 1. Approve spending + .function_call( + "ft_approve".to_owned(), + json!({ + "spender_id": env::current_account_id(), + "amount": amount + }).to_string().into_bytes(), + 1, + Gas::from_tgas(10), + ) + // 2. Transfer tokens + .function_call( + "ft_transfer".to_owned(), + json!({ + "receiver_id": recipient, + "amount": amount + }).to_string().into_bytes(), + 1, + Gas::from_tgas(15), + ) + // 3. Get balance + .function_call( + "ft_balance_of".to_owned(), + json!({ + "account_id": recipient + }).to_string().into_bytes(), + 0, + Gas::from_tgas(5), + ) + .then( + Promise::new(env::current_account_id()) + .function_call( + "defi_callback".to_owned(), + json!({"amount": amount}).to_string().into_bytes(), + 0, + Gas::from_tgas(10), + ) + ) +} +``` + + + + +## Testing Batch Actions + +```bash +# Test batch execution +near contract call-function as-transaction xcc.YOUR_NAME.testnet batch_actions json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as YOUR_ACCOUNT.testnet network-config testnet sign-with-keychain send +``` + +## Best Practices + +### ✅ Gas Planning +```typescript +// Allocate appropriate gas per action +.functionCall("simple", args, 0n, 5_000_000_000_000n) // 5 TGas +.functionCall("complex", args, 0n, 15_000_000_000_000n) // 15 TGas +``` + +### ✅ Error Handling +```typescript +if (result.success) { + return `Success: ${result.value}`; +} else { + // All actions rolled back + return "Batch failed - all reverted"; +} +``` + +### ❌ Common Mistakes +- Insufficient gas allocation +- Wrong action order for dependencies +- Not handling callback failures + +## When to Use Batch Actions + +Perfect for: +- **Atomic operations** across multiple calls +- **Sequential execution** with dependencies +- **Single contract** multiple operations +- **All-or-nothing** execution requirements + +Next, let's explore [parallel execution](3-parallel-execution.md) for calling multiple contracts simultaneously! \ No newline at end of file diff --git a/docs/tutorials/advanced-xcc/3-parallel-execution.md b/docs/tutorials/advanced-xcc/3-parallel-execution.md new file mode 100644 index 00000000000..4acf8dc7a1c --- /dev/null +++ b/docs/tutorials/advanced-xcc/3-parallel-execution.md @@ -0,0 +1,239 @@ +--- +id: parallel-execution +title: Parallel Contract Execution for Maximum Efficiency +sidebar_label: Parallel Execution +description: "Execute multiple contracts simultaneously for maximum efficiency. Unlike batch actions, parallel calls don't rollback if one fails." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Parallel execution calls multiple contracts simultaneously. Each call executes independently - if one fails, others continue. + +## Understanding Parallel Execution + +Key characteristics: +1. **Simultaneous execution** - All contracts called at once +2. **Independent results** - Each succeeds/fails independently +3. **Faster completion** - Limited by slowest call +4. **Array of results** - Callback receives all results + +## Implementation + + + + + + + + + + + + + + +## Handling Multiple Responses + +Process an array of results: + + + + + + + + + + + + + + +## Real-World Example: DeFi Portfolio + + + + +```typescript +@call({}) +get_portfolio_data({ + user, + token_contract, + lending_contract, + staking_contract +}: { + user: AccountId; + token_contract: AccountId; + lending_contract: AccountId; + staking_contract: AccountId; +}) { + // Get balance + const balance_promise = NearPromise.new(token_contract) + .functionCall( + "ft_balance_of", + JSON.stringify({account_id: user}), + 0n, + 5_000_000_000_000n + ); + + // Get lending position + const lending_promise = NearPromise.new(lending_contract) + .functionCall( + "get_position", + JSON.stringify({account_id: user}), + 0n, + 10_000_000_000_000n + ); + + // Get staking rewards + const staking_promise = NearPromise.new(staking_contract) + .functionCall( + "get_rewards", + JSON.stringify({account_id: user}), + 0n, + 5_000_000_000_000n + ); + + // Execute all in parallel + return balance_promise + .and(lending_promise) + .and(staking_promise) + .then( + NearPromise.new(env.current_account_id()) + .functionCall( + "portfolio_callback", + JSON.stringify({user}), + 0n, + 15_000_000_000_000n + ) + ); +} + +@call({privateFunction: true}) +portfolio_callback({user}: {user: AccountId}) { + const results = []; + + for (let i = 0; i < 3; i++) { + const result = getValueFromPromise(i); + if (result.success) { + results.push(JSON.parse(result.value)); + } else { + results.push(null); // Handle failure + } + } + + return { + user, + balance: results[0], + lending: results[1], + staking: results[2] + }; +} +``` + + + + +```rust +pub fn get_portfolio_data( + &mut self, + user: AccountId, + token_contract: AccountId, + lending_contract: AccountId, + staking_contract: AccountId, +) -> Promise { + // Get balance + let balance_promise = Promise::new(token_contract) + .function_call( + "ft_balance_of".to_owned(), + json!({"account_id": user}).to_string().into_bytes(), + 0, + Gas::from_tgas(5), + ); + + // Get lending position + let lending_promise = Promise::new(lending_contract) + .function_call( + "get_position".to_owned(), + json!({"account_id": user}).to_string().into_bytes(), + 0, + Gas::from_tgas(10), + ); + + // Get staking rewards + let staking_promise = Promise::new(staking_contract) + .function_call( + "get_rewards".to_owned(), + json!({"account_id": user}).to_string().into_bytes(), + 0, + Gas::from_tgas(5), + ); + + // Execute all in parallel + balance_promise + .and(lending_promise) + .and(staking_promise) + .then( + Promise::new(env::current_account_id()) + .function_call( + "portfolio_callback".to_owned(), + json!({"user": user}).to_string().into_bytes(), + 0, + Gas::from_tgas(15), + ) + ) +} +``` + + + + +## Performance Benefits + +``` +Sequential: ~300ms (100ms × 3) +Parallel: ~100ms (limited by slowest) +``` + +## Error Handling + +```typescript +// Graceful degradation +const results = { + balance: "0", + rewards: "0" +}; + +if (balance_result.success) { + results.balance = balance_result.value; +} +// Use defaults for failures +``` + +## Testing Parallel Execution + +```bash +# Test parallel calls +near contract call-function as-transaction xcc.YOUR_NAME.testnet multiple_contracts json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as YOUR_ACCOUNT.testnet network-config testnet sign-with-keychain send +``` + +## When to Use + +| Use Parallel When: | Use Batch When: | +|-------------------|-----------------| +| Different contracts | Same contract | +| Independent operations | Sequential dependencies | +| Can handle partial failures | Need atomic rollback | +| Performance critical | Consistency critical | + +Finally, let's cover [testing and deployment](4-testing-deployment.md)! \ No newline at end of file diff --git a/docs/tutorials/advanced-xcc/4-testing-deployment.md b/docs/tutorials/advanced-xcc/4-testing-deployment.md new file mode 100644 index 00000000000..b8c2f49e0fc --- /dev/null +++ b/docs/tutorials/advanced-xcc/4-testing-deployment.md @@ -0,0 +1,224 @@ +--- +id: testing-deployment +title: Testing and Deploying Advanced Cross-Contract Calls +sidebar_label: Testing & Deployment +description: "Test your advanced cross-contract patterns and deploy them to NEAR testnet for production use." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Let's test our advanced patterns and deploy to NEAR testnet. + +## Testing with Sandbox + + + + +```javascript +import { Worker } from 'near-workspaces'; +import test from 'ava'; + +test('batch actions execute atomically', async (t) => { + const worker = await Worker.init(); + const root = worker.rootAccount; + + // Deploy contracts + const main = await root.createSubAccount('main'); + await main.deploy('./build/cross_contract.wasm'); + + const hello = await root.createSubAccount('hello'); + await hello.deploy('./tests/external-contracts/hello-near.wasm'); + + // Initialize main contract + await main.call('init', { + hello_account: hello.accountId, + counter_account: counter.accountId, + guestbook_account: guestbook.accountId + }); + + // Test batch actions + const result = await main.call('batch_actions', {}); + t.truthy(result); + + await worker.tearDown(); +}); + +test('parallel execution handles failures', async (t) => { + // Test with one contract failing + const result = await main.call('multiple_contracts', {}); + + // Should contain partial results + t.true(result.includes('Hello:')); + t.true(result.includes('failed')); +}); +``` + + + + + + + + + +## Running Tests + + + + +```bash +npm test + +# Output: +✓ batch actions execute atomically (5s) +✓ parallel execution handles failures (3s) +✓ callbacks process correctly (4s) +``` + + + + +```bash +cargo test + +# Output: +test test_batch_actions ... ok +test test_multiple_contracts ... ok +test test_error_handling ... ok +``` + + + + +## Deployment Steps + +### 1. Create Account + +```bash +near account create-account sponsor-by-faucet-service advanced-xcc.YOUR_NAME.testnet autogenerate-new-keypair save-to-keychain network-config testnet create +``` + +### 2. Build Contract + + + + +```bash +npm run build +``` + + + + +```bash +cargo near build +``` + + + + +### 3. Deploy with Initialization + +```bash +near contract deploy advanced-xcc.YOUR_NAME.testnet use-file ./build/cross_contract.wasm with-init-call init json-args '{ + "hello_account": "hello.near-examples.testnet", + "counter_account": "counter.near-examples.testnet", + "guestbook_account": "guestbook.near-examples.testnet" +}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +``` + +## Testing on Testnet + +```bash +# Batch actions +near contract call-function as-transaction advanced-xcc.YOUR_NAME.testnet batch_actions json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as YOUR_ACCOUNT.testnet network-config testnet sign-with-keychain send + +# Parallel execution +near contract call-function as-transaction advanced-xcc.YOUR_NAME.testnet multiple_contracts json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as YOUR_ACCOUNT.testnet network-config testnet sign-with-keychain send +``` + +## Production Checklist + +### Security +- [ ] Validate external contract addresses +- [ ] Implement access controls +- [ ] Add callback validation +- [ ] Test failure scenarios + +### Gas Optimization +- [ ] Profile gas usage +- [ ] Set appropriate limits +- [ ] Add gas monitoring + +### Error Handling +- [ ] Handle partial failures +- [ ] Implement retry logic +- [ ] Add comprehensive logging +- [ ] Create fallback mechanisms + +## Common Issues + +### Gas Exhaustion +``` +Error: Exceeded the prepaid gas +``` +**Solution**: Increase gas allocation: +```typescript +.functionCall("callback", args, 0n, 50_000_000_000_000n) // 50 TGas +``` + +### Callback Failures +``` +Error: Promise result not found +``` +**Solution**: Match index to promise order: +```typescript +const result0 = getValueFromPromise(0); // First promise +const result1 = getValueFromPromise(1); // Second promise +``` + +## Performance Benchmarks + +| Pattern | Gas Usage | Time | +|---------|-----------|------| +| Batch (3 calls) | ~60 TGas | 2-3 blocks | +| Parallel (3 contracts) | ~45 TGas | 1-2 blocks | +| Complex callback | ~15 TGas | 1 block | + +## Monitoring + +```bash +# View transactions +near account view-account-summary advanced-xcc.YOUR_NAME.testnet network-config testnet now + +# Check state +near contract view-state advanced-xcc.YOUR_NAME.testnet view-state all network-config testnet now +``` + +## Summary + +You've mastered: +- ✅ Batch actions with atomic rollback +- ✅ Parallel execution for efficiency +- ✅ Complex response handling +- ✅ Testing with sandbox +- ✅ Deploying to testnet + +## Next Steps + +- Build complex DApps combining patterns +- Optimize gas usage +- Implement circuit breakers +- Create reusable libraries + +## Resources + +- [NEAR SDK Docs](https://docs.near.org/sdk) +- [Example Repository](https://github.com/near-examples/cross-contract-calls) +- [NEAR Discord](https://near.chat) + +Congratulations! You can now build sophisticated multi-contract applications on NEAR! \ No newline at end of file diff --git a/docs/tutorials/auction/4-factory.md b/docs/tutorials/auction/4-factory.md index 253b7d78dab..074d291007d 100644 --- a/docs/tutorials/auction/4-factory.md +++ b/docs/tutorials/auction/4-factory.md @@ -9,11 +9,11 @@ import {Github, Language} from "@site/src/components/codetabs" Since an auction contract hosts a single auction, each time you would like to host a new auction you will need to deploy a new contract. Rather than finding the compiled WASM file, creating a new account, deploying the contract, and then initializing it each time, you can use a factory contract to do this for you. -Luckily for us, there is already a [factory contract example](https://github.com/near-examples/factory-rust)! We will fork this example and slightly modify it to suit our use case. If you would like to learn more about how the factory contract works, you can take a look at the [associated documentation](/tutorials/examples/factory#generic-factory). +Luckily for us, there is already a [factory contract example](https://github.com/near-examples/factory-rust)! We will fork this example and slightly modify it to suit our use case. The factory example only comes in rust since, currently, the JavaScript SDK does not allow you to embed the WASM file in the contract. This is a limitation of the SDK and not the blockchain itself. ---- +--- ## Changing the default contract diff --git a/docs/tutorials/coin-flip/0-introduction.md b/docs/tutorials/coin-flip/0-introduction.md new file mode 100644 index 00000000000..4d901f34a07 --- /dev/null +++ b/docs/tutorials/coin-flip/0-introduction.md @@ -0,0 +1,50 @@ +--- +id: introduction +title: On-Chain Randomness with NEAR +sidebar_label: Introduction +description: "Learn how to implement secure and verifiable randomness in NEAR smart contracts through a practical coin flip game." +--- + +Generating random numbers on a blockchain is fundamentally different from traditional programming. While you might use `Math.random()` in JavaScript or `/dev/urandom` in Linux, blockchains require every node to agree on the same "random" value - making true randomness impossible. This tutorial will teach you how to implement secure, verifiable randomness in NEAR smart contracts. + +## The Challenge of Blockchain Randomness + +In a blockchain environment, randomness faces unique constraints: + +- **Determinism**: All validators must compute identical results +- **Unpredictability**: Users shouldn't be able to predict outcomes before transactions +- **Manipulation Resistance**: Miners or validators shouldn't influence results + +NEAR solves this through a Verifiable Random Function (VRF) that provides cryptographically secure randomness available directly in your smart contracts. + +## How NEAR's Randomness Works + +NEAR provides randomness through: +- **Rust**: `env::random_seed()` +- **JavaScript**: `near.randomSeed()` + +Both return a 32-byte array derived from: +- Block producer's cryptographic signature +- Previous epoch's random value +- Block height and timestamp +- Network-specific constants + +This ensures randomness that is unpredictable for users but deterministic for validators. + +:::info +The complete source code for this tutorial is available in the [near-examples/coin-flip-examples](https://github.com/near-examples/coin-flip-examples) repository. + +A live version is deployed on testnet at `coinflip.near-examples.testnet` for testing. +::: + +## What You Will Build + +You'll create a coin flip game that demonstrates: + +- [Understanding randomness challenges](1-randomness-basics.md) on blockchain and NEAR's solution +- [Building a simple coin flip contract](2-basic-contract.md) with secure randomness +- [Testing randomness](3-testing-randomness.md) to ensure fairness and distribution +- [Advanced randomness patterns](4-advanced-patterns.md) for complex applications +- [Deploying and interacting](5-deployment.md) with your random contract + +Let's start by understanding the fundamentals of [on-chain randomness](1-randomness-basics.md). \ No newline at end of file diff --git a/docs/tutorials/coin-flip/1-randomness-basics.md b/docs/tutorials/coin-flip/1-randomness-basics.md new file mode 100644 index 00000000000..7504995d91f --- /dev/null +++ b/docs/tutorials/coin-flip/1-randomness-basics.md @@ -0,0 +1,114 @@ +--- +id: randomness-basics +title: Understanding On-Chain Randomness +sidebar_label: Randomness Fundamentals +description: Understand the basics of On-Chain Randomness and why this approach works +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Before diving into code, let's understand why traditional randomness approaches fail on blockchains and how NEAR solves this problem. + +## Why Traditional Methods Don't Work + + + + +```javascript +// Using timestamp - Predictable and manipulable +const outcome = Date.now() % 2 ? 'heads' : 'tails'; + +// Using block data - Miners can influence +const result = block.timestamp % 100; + +// Using previous transaction - Public and predictable +const random = previousTx.hash % 6 + 1; +``` + + + + +```plain +These methods fail because: +- **Timestamps** are predictable and can be slightly manipulated by block producers +- **Block hashes** are known before user transactions are included +- **Transaction data** is public before execution +- **External input** breaks consensus as nodes can't agree +``` + + + +## NEAR's VRF Solution + +NEAR uses a Verifiable Random Function (VRF) that provides: + + + +The random seed is: +- **Unpredictable**: Cannot be known before the block is produced +- **Verifiable**: Can be cryptographically verified as authentic +- **Consistent**: Same for all transactions in a block + +## Accessing Randomness in Your Contract + + + + + + + + + + + + + + +## Important Limitations + +### Block-Level Consistency +All calls to `random_seed()` within the same block return the same value: + +```javascript +// Both calls return the same seed if in the same block +const seed1 = near.randomSeed(); +const seed2 = near.randomSeed(); +// seed1 === seed2 +``` + +### Generating Multiple Values +To get different random values in one transaction, combine the seed with unique data: + +```javascript +// To get different random values in one transaction +function multipleRandomValues(count) { + const seed = near.randomSeed(); + const values = []; + + for (let i = 0; i < count; i++) { + // Mix seed with index for uniqueness + const mixed = new Uint8Array([...seed, i]); + values.push(mixed[0] % 100); + } + + return values; +} +``` + +### Security Considerations + +:::warning +NEAR's randomness is suitable for: +- ✅ Games and lotteries +- ✅ NFT trait generation +- ✅ Random selection processes + +But NOT for: +- ❌ Cryptographic key generation +- ❌ Security-critical random values +- ❌ High-value financial applications without additional safeguards +::: + +Now that you understand the fundamentals, let's [build a coin flip](2-basic-contract.md) contract that properly uses NEAR's randomness. \ No newline at end of file diff --git a/docs/tutorials/coin-flip/2-basic-contract.md b/docs/tutorials/coin-flip/2-basic-contract.md new file mode 100644 index 00000000000..a6c19e706d2 --- /dev/null +++ b/docs/tutorials/coin-flip/2-basic-contract.md @@ -0,0 +1,147 @@ +--- +id: basic-contract +title: Building a Coin Flip Contract +sidebar_label: Building the Contract +description: Learn how to build the Coin Flip Contract +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Let's build a coin flip game where players guess "heads" or "tails" and earn points for correct guesses. + +## Obtaining the Project + +You have two options to start with the Coin Flip tutorial: + +| GitHub Codespaces | Clone Locally | +|------------------|---------------| +| [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/near-examples/coin-flip-examples?quickstart=1) | 🌐 `https://github.com/near-examples/coin-flip-examples` | + + + + +Click the "Open in GitHub Codespaces" button above to get a fully configured development environment in your browser. + + + + +```bash +# Clone the repository +git clone https://github.com/near-examples/coin-flip-examples +cd coin-flip-examples + +# Choose your preferred language +cd contract-ts # for TypeScript +# or +cd contract-rs # for Rust +``` + + + + +## Contract Structure + +Our contract needs: +- A method to flip the coin using randomness +- Storage for player points +- View methods to check scores + + + + + + + + + + + + + + +## Implementing the Coin Flip + +The core logic generates a random outcome and compares it with the player's guess: + + + + + + + + + + + + + + +## Key Implementation Details + +### 1. Random Seed Usage +We use only the first byte of the 32-byte seed for simplicity: +```javascript +randomSeed[0] % 2 === 0 ? 'heads' : 'tails' +``` + +### 2. State Management +Points are stored in an `UnorderedMap` for efficient access: +- JavaScript: `UnorderedMap` +- Rust: `UnorderedMap` + +### 3. Input Validation +Always validate user input: +```javascript +if (!['heads', 'tails'].includes(player_guess)) { + throw new Error('Invalid guess'); +} +``` + +## View Methods + +Add methods to check player scores: + + + + + + + + + + + + + + +## Building the Contract + + + + +```bash +# Install dependencies +npm install + +# Build the contract +npm run build + +# The compiled contract will be in build/coin_flip.wasm +``` + + + + +```bash +# Build the contract +cargo near build + +# The compiled contract will be in target/wasm32-unknown-unknown/release/ +``` + + + + +Now that we have a working contract, let's test it to ensure the [randomness](3-testing-randomness.md) behaves correctly. \ No newline at end of file diff --git a/docs/tutorials/coin-flip/3-testing-randomness.md b/docs/tutorials/coin-flip/3-testing-randomness.md new file mode 100644 index 00000000000..1c9d048cdaa --- /dev/null +++ b/docs/tutorials/coin-flip/3-testing-randomness.md @@ -0,0 +1,140 @@ +--- +id: testing-randomness +title: Testing On-Chain Randomness +sidebar_label: Testing Randomness +description: Learn how to Test you Coin Flip Contract and understand practically how On-Chain Randomness works +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs"; + +Testing randomness requires special attention to ensure fair distribution and proper behavior. + +## Setting Up Tests + + + + + + + + + + + + + + +## Testing Initial State + +Verify players start with zero points: + + + + + + + + + + + + + + +## Testing Randomness Distribution + +Verify both outcomes occur with reasonable frequency: + + + + + + + + + + + + + + +## Running Tests + + + + +```bash +# Run all tests +npm test + +# Run with verbose output +npm test -- --verbose + +# Run specific test file +npm test sandbox-test/main.ava.js +``` + + + + +```bash +# Run all tests +cargo test + +# Run with output +cargo test -- --nocapture + +# Run specific test +cargo test test_points_are_correctly_computed +``` + + + + +## Test Best Practices + +### 1. Test Edge Cases +```javascript +test('handles minimum points correctly', async (t) => { + // Ensure points don't go below 0 + const points = await contract.view('points_of', { + player: account.accountId + }); + t.true(points >= 0); +}); +``` + +### 2. Mock Randomness for Unit Tests +For deterministic testing, consider mocking: +```rust +#[cfg(test)] +fn mock_random_seed() -> [u8; 32] { + [42; 32] // Fixed seed for testing +} +``` + +### 3. Integration Testing +Test the full flow with multiple accounts: + +```javascript +test('multiple players maintain separate scores', async (t) => { + const { root, contract } = t.context.accounts; + const alice = await root.createSubAccount('alice'); + + // Play as root + await root.call(contract, 'flip_coin', { player_guess: 'heads' }); + + // Play as alice + await alice.call(contract, 'flip_coin', { player_guess: 'tails' }); + + // Check separate scores + const rootPoints = await contract.view('points_of', { player: root.accountId }); + const alicePoints = await contract.view('points_of', { player: alice.accountId }); + + t.not(rootPoints, alicePoints); // Different players, different scores +}); +``` + +With tests confirming our randomness works correctly, let's explore [advanced patterns](4-advanced-patterns.md) for more complex use cases. \ No newline at end of file diff --git a/docs/tutorials/coin-flip/4-advanced-patterns.md b/docs/tutorials/coin-flip/4-advanced-patterns.md new file mode 100644 index 00000000000..69c7b1c4217 --- /dev/null +++ b/docs/tutorials/coin-flip/4-advanced-patterns.md @@ -0,0 +1,176 @@ +--- +id: advanced-patterns +title: Advanced Randomness Patterns +sidebar_label: Advanced Patterns +description: Learn a thing or two on Advanced Randomness Patterns +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from "@site/src/components/codetabs" + +Beyond simple coin flips, many applications need sophisticated randomness patterns. Here are production-ready implementations. + +## Random Numbers in a Range + +Generate random numbers within specific bounds: + + + + +```javascript +function randomInRange(min, max) { + const seed = near.randomSeed(); + // Use 4 bytes for better distribution + const value = new DataView(seed.buffer).getUint32(0, true); + const range = max - min; + return min + (value % range); +} + +// Example: Roll a dice (1-6) +const diceRoll = randomInRange(1, 7); +``` + + + + + + + + + +## Weighted Random Selection + +For loot boxes or rarity systems: + + + +Usage example: +```rust +// 70% common, 20% rare, 10% legendary +let weights = vec![70, 20, 10]; +let selected_index = self.weighted_selection(weights); +``` + +## Avoiding Modulo Bias + +For cryptographically fair distribution: + +```rust +pub fn unbiased_range(&self, min: u32, max: u32) -> u32 { + let range = max - min; + let max_valid = u32::MAX - (u32::MAX % range); + let seed = env::random_seed(); + + let mut value = u32::from_le_bytes([ + seed[0], seed[1], seed[2], seed[3] + ]); + + // Rejection sampling + while value >= max_valid { + // Use next 4 bytes + value = u32::from_le_bytes([ + seed[4], seed[5], seed[6], seed[7] + ]); + } + + min + (value % range) +} +``` + +## Shuffling Arrays + +For card games or random ordering: + + + + +```javascript +function shuffle(array) { + const seed = near.randomSeed(); + const shuffled = [...array]; + + for (let i = shuffled.length - 1; i > 0; i--) { + const j = seed[i % 32] % (i + 1); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + return shuffled; +} +``` + + + + +```rust +pub fn shuffle(&self, mut items: Vec) -> Vec { + let seed = env::random_seed(); + let len = items.len(); + + for i in (1..len).rev() { + let j = (seed[i % 32] as usize) % (i + 1); + items.swap(i, j); + } + + items +} +``` + + + + +## Generating Unique Random Values + +When you need multiple different random values: + +```javascript +function multipleRandomValues(count) { + const seed = near.randomSeed(); + const values = []; + + for (let i = 0; i < count; i++) { + // Mix seed with index for uniqueness + const hash = near.sha256( + new Uint8Array([...seed, i]) + ); + values.push(hash[0] % 100); + } + + return values; +} +``` + +## Random with Commit-Reveal + +For high-stakes randomness where users shouldn't know outcomes immediately: + +```rust +#[near_bindgen] +impl Contract { + pub fn commit_guess(&mut self, commitment: String) { + let caller = env::predecessor_account_id(); + self.commitments.insert(&caller, &commitment); + } + + pub fn reveal_and_play(&mut self, guess: String, nonce: String) -> bool { + let caller = env::predecessor_account_id(); + let commitment = self.commitments.get(&caller).unwrap(); + + // Verify commitment + let hash = env::sha256(format!("{}{}", guess, nonce).as_bytes()); + assert_eq!(commitment, base64::encode(hash)); + + // Now use randomness + let outcome = self.simulate_coin_flip(); + guess == outcome + } +} +``` + +## Monitoring Randomness Health + +Track distribution for quality assurance: + + + +Now let's [Deploy and interact](5-deployment.md) with your randomness-powered contract \ No newline at end of file diff --git a/docs/tutorials/coin-flip/5-deployment.md b/docs/tutorials/coin-flip/5-deployment.md new file mode 100644 index 00000000000..06d8bff1f37 --- /dev/null +++ b/docs/tutorials/coin-flip/5-deployment.md @@ -0,0 +1,223 @@ +--- +id: deployment +title: Deploying and Interacting with Your Contract +sidebar_label: Deployment & Interaction +description: Now is the time to Deploy and Interact with your Smart Contract +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Let's deploy your randomness contract and interact with it on NEAR testnet. + +## Prerequisites + +Ensure you have: +- NEAR CLI installed: `npm install -g near-cli-rs` +- A testnet account: create one at [mynearwallet.com](https://testnet.mynearwallet.com) +- Your contract compiled (`.wasm` file) + +## Creating a Dev Account + +The easiest way to deploy for testing: + +```bash +# Deploy to a dev account (creates new account automatically) +near dev-deploy build/coin_flip.wasm + +# Note the deployed account ID (e.g., dev-1234567890123-12345678901234) +export CONTRACT_ID=dev-1234567890123-12345678901234 +``` + +## Deploying to a Named Account + +For production or persistent testing: + +```bash +# Create a subaccount +near account create-account sponsor-by-faucet-service coin-flip.YOUR_ACCOUNT.testnet autogenerate-new-keypair save-to-keychain network-config testnet create + +# Deploy the contract +near contract deploy coin-flip.YOUR_ACCOUNT.testnet use-file ./build/coin_flip.wasm without-init-call network-config testnet sign-with-keychain send +``` + +## Interacting with the Contract + +### Playing the Coin Flip Game + + + + +```bash +# Flip the coin (guess heads) +near contract call-function as-transaction $CONTRACT_ID flip_coin json-args '{"player_guess":"heads"}' prepaid-gas '30 TeraGas' attached-deposit '0 NEAR' sign-as YOUR_ACCOUNT.testnet network-config testnet sign-with-keychain send + +# Check your points +near contract call-function as-read-only $CONTRACT_ID points_of json-args '{"player":"YOUR_ACCOUNT.testnet"}' network-config testnet now +``` + + + + +```javascript +const { connect, keyStores, utils } = require("near-api-js"); + +// Setup +const keyStore = new keyStores.UnencryptedFileSystemKeyStore( + `${process.env.HOME}/.near-credentials` +); + +const config = { + networkId: "testnet", + keyStore, + nodeUrl: "https://rpc.testnet.near.org", +}; + +const near = await connect(config); +const account = await near.account("YOUR_ACCOUNT.testnet"); + +// Play the game +const result = await account.functionCall({ + contractId: "coinflip.near-examples.testnet", + methodName: "flip_coin", + args: { player_guess: "heads" }, + gas: "30000000000000", +}); + +console.log("Result:", result); + +// Check points +const points = await account.viewFunction({ + contractId: "coinflip.near-examples.testnet", + methodName: "points_of", + args: { player: "YOUR_ACCOUNT.testnet" }, +}); + +console.log("Your points:", points); +``` + + + + +## Building a Simple Frontend + +Create a basic web interface: + +```html + + + + NEAR Coin Flip + + + +

Coin Flip Game

+ + +
+
+ + + + +``` + +## Using the Example Frontend + +The repository includes a complete Next.js frontend: + +```bash +# Navigate to frontend directory +cd frontend + +# Install dependencies +npm install + +# Run development server +npm run dev + +# Open http://localhost:3000 +``` + +The frontend demonstrates: +- Wallet integration with multiple wallet providers +- Real-time game interactions +- Score tracking +- Animated coin flips + +## Monitoring Your Contract + +View contract activity: + +```bash +# Check recent transactions +near account view-account-summary $CONTRACT_ID network-config testnet now + +# View contract state size +near contract view-state $CONTRACT_ID view-state-all network-config testnet now +``` + +## Best Practices for Production + +1. **Set up monitoring** for randomness distribution +2. **Implement rate limiting** to prevent spam +3. **Add events** for important actions +4. **Consider upgradability** patterns +5. **Audit your randomness** usage regularly + +## Next Steps + +- Build more complex games using randomness patterns +- Add social features like leaderboards +- Implement different game modes +- Study other randomness use cases in DeFi and NFTs + +Congratulations! You've learned how to implement secure on-chain randomness in NEAR smart contracts. \ No newline at end of file diff --git a/docs/tutorials/donation/0-introduction.md b/docs/tutorials/donation/0-introduction.md new file mode 100644 index 00000000000..af2bf11b1cf --- /dev/null +++ b/docs/tutorials/donation/0-introduction.md @@ -0,0 +1,69 @@ +--- +id: introduction +title: Building a Donation Smart Contract +sidebar_label: Introduction +description: "This tutorial will guide you through building a donation smart contract that handles NEAR token transfers, tracks donations, and manages beneficiaries." +--- + +Learn how to build a smart contract that accepts NEAR token donations, tracks contributors, and automatically forwards funds to beneficiaries. This tutorial covers the essential patterns for handling token transfers in NEAR Protocol smart contracts. + +## How It Works + +The donation contract demonstrates key concepts in NEAR token handling: + +- **Payable Methods**: Functions that can receive NEAR tokens with transactions +- **Token Transfers**: Automatically forwarding received tokens to beneficiaries +- **Storage Management**: Tracking donation history while managing storage costs +- **View Methods**: Querying donation data without gas fees + +:::info + +The complete source code for this tutorial is available in the [GitHub repository](https://github.com/near-examples/donation-examples). + +The repository contains: +- **contract-rs/**: Rust implementation of the donation contract +- **contract-ts/**: TypeScript implementation of the donation contract +- **frontend/**: Next.js frontend application + +A deployed contract is available on testnet at `donation.near-examples.testnet` for testing. + +::: + +## What You Will Learn + +In this tutorial, you will learn how to: + +- [Set up the donation contract structure](1-setup.md) with proper initialization +- [Handle token transfers](2-contract.md) using payable methods and storage management +- [Query donation data](3-queries.md) with efficient view methods +- [Deploy and test](4-testing.md) your contract on NEAR testnet +- [Build a frontend](5-frontend.md) to interact with your donation contract + +## Prerequisites + +Before starting, make sure you have: + +- Basic understanding of smart contracts +- [NEAR CLI](https://docs.near.org/tools/near-cli) installed +- A NEAR testnet account +- Node.js 18+ (for TypeScript examples) +- Rust toolchain (for Rust examples) + +## Contract Overview + +The donation contract includes these core features: + +```rust +pub struct Contract { + pub beneficiary: AccountId, // Who receives donations + pub donations: IterableMap, // Track donor contributions +} +``` + +**Key Methods:** +- `donate()` - Payable method to accept NEAR tokens +- `get_beneficiary()` - View current beneficiary +- `get_donations()` - List all donations with pagination +- `number_of_donors()` - Count total unique donors + +This tutorial will walk you through implementing each component step-by-step, with examples in both Rust and TypeScript. \ No newline at end of file diff --git a/docs/tutorials/donation/1-setup.md b/docs/tutorials/donation/1-setup.md new file mode 100644 index 00000000000..ce0cfa9696a --- /dev/null +++ b/docs/tutorials/donation/1-setup.md @@ -0,0 +1,220 @@ +--- +id: setup +title: Setting up the Donation Contract +sidebar_label: Contract Setup +description: Learn the Rudiments of Setting Up your Donation Smart Contract +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from '@site/src/components/codetabs'; + +The first step is setting up the basic structure of our donation contract. We'll define the contract state, initialization method, and basic beneficiary management. + +## Contract State Structure + +The donation contract needs to track two key pieces of data: who receives the donations (beneficiary) and how much each donor has contributed. + + + + +```rust +use near_sdk::store::IterableMap; +use near_sdk::{near, AccountId, NearToken, PanicOnDefault}; + +#[near(contract_state)] +#[derive(PanicOnDefault)] +pub struct Contract { + pub beneficiary: AccountId, + pub donations: IterableMap, +} +``` + + + + +```ts +import { NearBindgen, near, call, view, initialize, UnorderedMap } from 'near-sdk-js' + +@NearBindgen({ requireInit: true }) +class DonationContract { + beneficiary: string = ""; + donations = new UnorderedMap('uid-1'); + + static schema = { + beneficiary: "string", + donations: { class: UnorderedMap, value: "bigint" } + } +} +``` + + + + +## Contract Initialization + +Every NEAR contract needs an initialization method to set up the initial state. Our contract requires a beneficiary to be specified during deployment. + + + + +```rust +#[near] +impl Contract { + #[init] + #[private] // only callable by the contract's account + pub fn init(beneficiary: AccountId) -> Self { + Self { + beneficiary, + donations: IterableMap::new(b"d"), + } + } +} +``` + + + + +```ts +@initialize({ privateFunction: true }) +init({ beneficiary }: { beneficiary: string }) { + this.beneficiary = beneficiary +} +``` + + + + +:::tip +The `#[private]` decorator in Rust and `privateFunction: true` in TypeScript ensure only the contract account can call the initialization method. +::: + +## Beneficiary Management + +Add methods to view and update the beneficiary. The update method should be restricted to the contract owner for security. + + + + +```rust +impl Contract { + pub fn get_beneficiary(&self) -> &AccountId { + &self.beneficiary + } + + #[private] // only callable by the contract's account + pub fn change_beneficiary(&mut self, new_beneficiary: AccountId) { + self.beneficiary = new_beneficiary; + } +} +``` + + + + +```ts +@view({}) +get_beneficiary(): string { + return this.beneficiary +} + +@call({ privateFunction: true }) +change_beneficiary(beneficiary: string) { + this.beneficiary = beneficiary; +} +``` + + + + +## Building the Contract + +Now let's build the contract to ensure our setup is correct. + + + + +First, create your `Cargo.toml`: + + + +Build the contract: +```bash +cargo near build +``` + + + + +Create your `package.json`: + + + +Build the contract: +```bash +npm run build +``` + + + + +## Basic Unit Test + +Let's add a simple test to verify our initialization works correctly. + + + + +```rust +#[cfg(test)] +mod tests { + use super::*; + + const BENEFICIARY: &str = "beneficiary.testnet"; + + #[test] + fn initializes() { + let contract = Contract::init(BENEFICIARY.parse().unwrap()); + assert_eq!(contract.beneficiary, BENEFICIARY.parse::().unwrap()); + } +} +``` + +Run the test: +```bash +cargo test +``` + + + + +```ts +// In your test file with NEAR Workspaces +import { Worker, NEAR } from 'near-workspaces'; + +test.beforeEach(async (t) => { + const worker = t.context.worker = await Worker.init(); + const root = worker.rootAccount; + + const beneficiary = await root.createSubAccount("beneficiary"); + const contract = await root.createSubAccount("contract"); + + await contract.deploy(process.argv[2]); + await contract.call(contract, "init", { beneficiary: beneficiary.accountId }); + + t.context.accounts = { contract, beneficiary }; +}); +``` + + + + +## Next Steps + +With the basic contract structure in place, you're ready to implement the core donation functionality. The next chapter will cover how to handle token transfers using payable methods. + +Continue to [Handle Donations](2-contract.md) to learn about accepting NEAR token payments. \ No newline at end of file diff --git a/docs/tutorials/donation/2-contract.md b/docs/tutorials/donation/2-contract.md new file mode 100644 index 00000000000..9d385c760e8 --- /dev/null +++ b/docs/tutorials/donation/2-contract.md @@ -0,0 +1,146 @@ +--- +id: contract +title: Handling Token Donations +sidebar_label: Handle Donations +description: Learn how to Handle Token Donations and implement core Functionalities +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from '@site/src/components/codetabs'; + +Now we'll implement the core donation functionality. This involves creating payable methods that can receive NEAR tokens, managing storage costs, and automatically forwarding funds to the beneficiary. + +## Understanding Storage Costs + +NEAR charges for data storage in smart contracts. Each new donor entry requires storage, so we need to account for this cost in our donation logic. + + + + + + + + + + + + + + +:::tip +Storage costs are one-time fees paid when first storing data. Subsequent updates to existing data don't require additional storage fees. +::: + +## The Donation Method + +The `donate` method is the heart of our contract. It must be marked as `payable` to accept NEAR tokens, handle storage costs, track donations, and forward funds. + + + + + + + + + + + + + + +## Key Concepts Explained + +**Payable Functions**: The `#[payable]` decorator (Rust) or `payableFunction: true` (TypeScript) allows methods to receive NEAR tokens. + +**Storage Management**: We charge new donors a storage fee but not returning donors, since their data already exists. + +**Promise Transfers**: We use `Promise::new().transfer()` in Rust or `promiseBatchCreate` + `promiseBatchActionTransfer` in TypeScript to send tokens to the beneficiary. + +**Deposit Handling**: `env::attached_deposit()` (Rust) or `near.attachedDeposit()` (TypeScript) gets the amount of NEAR sent with the transaction. + +## Testing the Donation Logic + +Let's examine how the donation handling is tested to understand the expected behavior. + + + + + + + + + + + + + + +## Advanced Testing with Workspaces + +For integration testing, both implementations use NEAR Workspaces to simulate real blockchain interactions: + + + + + + + + + + + + + + +## Running Tests + +Test your donation logic to ensure everything works correctly: + + + + +```bash +# Unit tests +cargo test + +# Integration tests with workspaces +cargo test --test workspaces +``` + + + + +```bash +# Build and test +npm run build +npm run test +``` + + + + +## Key Takeaways + +- **Payable methods** can receive NEAR tokens with transactions +- **Storage costs** must be handled for new data entries +- **Promise transfers** allow contracts to send tokens to other accounts +- **Testing** verifies both donation recording and fund forwarding work correctly + +Continue to [Query Donation Data](3-queries.md) to learn about implementing view methods for retrieving donation information. \ No newline at end of file diff --git a/docs/tutorials/donation/3-queries.md b/docs/tutorials/donation/3-queries.md new file mode 100644 index 00000000000..9ec49b8651e --- /dev/null +++ b/docs/tutorials/donation/3-queries.md @@ -0,0 +1,190 @@ +--- +id: queries +title: Querying Donation Data +sidebar_label: Query Donation Data +description: Learn how to query your Donation Contracts for various datas +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from '@site/src/components/codetabs'; + +Now that we can accept donations, we need ways to query the data. View methods allow anyone to read contract state without paying gas fees, making them perfect for displaying donation information. + +## Individual Donation Queries + +First, let's implement a method to get donation information for a specific account: + + + + + + + + + + + + + + +## Donation Data Structure + +The methods return structured data that's easy for frontends to consume: + + + + + + + + + + + + + + +## Counting Donors + +A simple method to get the total number of unique donors: + + + + + + + + + + + + + + +## Paginated Donation Lists + +For displaying all donations, we need pagination to handle large datasets efficiently: + + + + + + + + + + + + + + +## Beneficiary Management + +Remember the beneficiary methods from our setup? They're view methods too: + + + + + + + + + + + + + + +## Testing Query Methods + +Let's verify our query methods work correctly in the integration tests: + + + + + + + + + + + + + + +## Using View Methods via CLI + +Once deployed, you can query your contract using NEAR CLI: + +```bash +# Get beneficiary +near view donation.near-examples.testnet get_beneficiary + +# Get number of donors +near view donation.near-examples.testnet number_of_donors + +# Get donations with pagination +near view donation.near-examples.testnet get_donations \ + '{"from_index": 0, "limit": 10}' + +# Get specific donation +near view donation.near-examples.testnet get_donation_for_account \ + '{"account_id": "alice.testnet"}' +``` + +## Key Concepts + +**View Methods**: Functions marked with `#[view]` (Rust) or `@view({})` (TypeScript) are read-only and don't cost gas to call. + +**Pagination**: Large datasets should be paginated to avoid hitting gas limits and provide better user experience. + +**Data Serialization**: NEAR automatically serializes return values to JSON, making them easy to consume from frontends. + +**Public Access**: View methods can be called by anyone, even accounts without NEAR tokens. + +## Query Performance Tips + +1. **Use pagination** for methods that could return large datasets +2. **Consider caching** frequently accessed data in your frontend +3. **Batch queries** when possible to reduce RPC calls +4. **Index important data** for efficient lookups + +## Error Handling + +View methods can still fail if they access non-existent data or exceed computation limits: + +```rust +// Always handle missing data gracefully +let donated_amount = self + .donations + .get(&account_id) + .cloned() + .unwrap_or(NearToken::from_near(0)); // Default to 0 if not found +``` + +Continue to [Deploy and Test](4-testing.md) to learn how to deploy your contract and test it on NEAR testnet. \ No newline at end of file diff --git a/docs/tutorials/donation/4-testing.md b/docs/tutorials/donation/4-testing.md new file mode 100644 index 00000000000..5ba93bc6bdc --- /dev/null +++ b/docs/tutorials/donation/4-testing.md @@ -0,0 +1,271 @@ +--- +id: testing +title: Deploy and Test Your Contract +sidebar_label: Deploy and Test +Description: Learn how to deploy and test your smart contract +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Now that we have a complete donation contract, let's deploy it to NEAR testnet and test all functionality through the command line. + +## Create a NEAR Account + +First, you need a NEAR testnet account to deploy the contract: + + + + +```bash +# Create account with funding from faucet +near create-account your-contract.testnet --useFaucet + +# Login to your account +near login +``` + + + + +```bash +# Create a new account pre-funded by faucet +near account create-account sponsor-by-faucet-service \ + your-contract.testnet autogenerate-new-keypair \ + save-to-keychain network-config testnet create +``` + + + + +## Deploy the Contract + + + + +```bash +# Build the contract +cargo near build + +# Deploy with initialization +near deploy your-contract.testnet \ + --wasmFile target/near/contract.wasm \ + --initFunction init \ + --initArgs '{"beneficiary": "beneficiary.testnet"}' +``` + + + + +```bash +# Build the contract +npm run build + +# Deploy the WASM file +near deploy your-contract.testnet ./build/donation.wasm + +# Initialize the contract +near call your-contract.testnet init \ + '{"beneficiary": "beneficiary.testnet"}' \ + --accountId your-contract.testnet +``` + + + + +## Test View Methods + +Start by testing the read-only methods to verify deployment: + + + + +```bash +# Check the beneficiary +near view your-contract.testnet get_beneficiary + +# Check number of donors (should be 0) +near view your-contract.testnet number_of_donors + +# Check donations list (should be empty) +near view your-contract.testnet get_donations \ + '{"from_index": 0, "limit": 10}' +``` + + + + +```bash +# Check beneficiary +near contract call-function as-read-only \ + your-contract.testnet get_beneficiary json-args '{}' \ + network-config testnet now + +# Check number of donors +near contract call-function as-read-only \ + your-contract.testnet number_of_donors json-args '{}' \ + network-config testnet now +``` + + + + +## Test Donations + +Now let's test the donation functionality: + + + + +```bash +# Make a donation +near call your-contract.testnet donate \ + --accountId donor.testnet \ + --deposit 1 + +# Check your donation was recorded +near view your-contract.testnet get_donation_for_account \ + '{"account_id": "donor.testnet"}' + +# Verify donor count increased +near view your-contract.testnet number_of_donors +``` + + + + +```bash +# Make a donation +near contract call-function as-transaction \ + your-contract.testnet donate json-args '{}' \ + prepaid-gas '30.0 Tgas' attached-deposit '1 NEAR' \ + sign-as donor.testnet network-config testnet \ + sign-with-keychain send + +# Check donation was recorded +near contract call-function as-read-only \ + your-contract.testnet get_donation_for_account \ + json-args '{"account_id": "donor.testnet"}' \ + network-config testnet now +``` + + + + +## Test Multiple Donations + +Test the storage cost logic with multiple donations: + +```bash +# First donation (pays storage cost) +near call your-contract.testnet donate \ + --accountId alice.testnet --deposit 0.1 + +# Second donation from same account (no storage cost) +near call your-contract.testnet donate \ + --accountId alice.testnet --deposit 0.2 + +# Different donor +near call your-contract.testnet donate \ + --accountId bob.testnet --deposit 0.5 + +# Check total donors +near view your-contract.testnet number_of_donors + +# Get paginated donations +near view your-contract.testnet get_donations \ + '{"from_index": 0, "limit": 10}' +``` + +## Verify Fund Transfer + +Check that donations were forwarded to the beneficiary: + +```bash +# Get beneficiary balance before +near state beneficiary.testnet + +# Make donation +near call your-contract.testnet donate \ + --accountId test.testnet --deposit 1 + +# Check beneficiary balance after (should increase by ~0.999 NEAR) +near state beneficiary.testnet +``` + +:::tip +The beneficiary receives slightly less than the donation amount due to the storage cost deduction for first-time donors. +::: + +## Test Error Cases + +Verify error handling works correctly: + +```bash +# Try donating too little to cover storage +near call your-contract.testnet donate \ + --accountId new-donor.testnet --deposit 0.0001 +# Should fail with storage cost error + +# Try unauthorized beneficiary change +near call your-contract.testnet change_beneficiary \ + '{"new_beneficiary": "hacker.testnet"}' \ + --accountId not-owner.testnet +# Should fail with access error +``` + +## Monitor Contract Activity + +Track your contract's activity through NEAR explorers: + +- **Testnet Explorer**: https://testnet.nearblocks.io/address/your-contract.testnet +- **NEAR Explorer**: https://explorer.testnet.near.org/accounts/your-contract.testnet + +Look for: +- Donation transactions +- Transfer calls to beneficiary +- Contract method calls +- Token flow + +## Performance Testing + +For production contracts, test with larger datasets: + +```bash +# Create many donations to test pagination +for i in {1..20}; do + near call your-contract.testnet donate \ + --accountId "test-$i.testnet" --deposit 0.1 +done + +# Test pagination limits +near view your-contract.testnet get_donations \ + '{"from_index": 0, "limit": 5}' + +near view your-contract.testnet get_donations \ + '{"from_index": 5, "limit": 5}' +``` + +## Debugging Tips + +If transactions fail: + +1. **Check gas limits**: Complex operations may need more gas +2. **Verify deposits**: Ensure sufficient NEAR for storage costs +3. **Review logs**: Use `near tx-status TRANSACTION_HASH` to see detailed logs +4. **Test locally first**: Use unit tests before testnet deployment + +## Contract Upgrade + +When you need to update your contract: + +```bash +# Deploy new version (keeps state) +near deploy your-contract.testnet new-contract.wasm + +# Or redeploy with state reset +near delete your-contract.testnet beneficiary.testnet +# Then redeploy fresh +``` + +Continue to [Build Frontend](5-frontend.md) to learn how to create a web interface for your donation contract. \ No newline at end of file diff --git a/docs/tutorials/donation/5-frontend.md b/docs/tutorials/donation/5-frontend.md new file mode 100644 index 00000000000..b80e0cf3801 --- /dev/null +++ b/docs/tutorials/donation/5-frontend.md @@ -0,0 +1,186 @@ +--- +id: frontend +title: Building a Frontend Interface +sidebar_label: Build Frontend +description: Build the frontend to better understand how users interact with the smart contract +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import {Github} from '@site/src/components/codetabs'; + +Now let's build a web interface for users to interact with our donation contract. The frontend example uses Next.js with NEAR Wallet Selector to create a complete donation experience. + +## Project Structure + +The frontend directory contains a complete Next.js application: + + + +## Configuration + +Configure your contract address and network settings: + + + +## Wallet Integration + +The app supports multiple wallet types through NEAR Wallet Selector: + + + +Complete wallet configuration with all supported wallet types: + + + +## Donation Form Component + +The donation form allows users to select preset amounts or enter custom donations: + + + +The form includes USD-to-NEAR conversion using CoinGecko API: + + + +## Handling Donation Transactions + +The submit handler processes donations and updates the UI: + + + +## Displaying Donation History + +The donations table shows all contributions with pagination: + + + +The component handles pagination and fetches donation data: + + + +## User's Personal Donations + +Track and display the current user's total donations: + + + +## Navigation Component + +Provide wallet connection functionality: + + + +## Main Page Layout + +The home page combines all components: + + + +## Donation Box Component + +The donation box handles wallet connection states: + + + +## Running the Frontend + +Start the development server: + +```bash +cd frontend +yarn install +yarn dev +``` + +Visit `http://localhost:3000` to see your donation interface. + +## Key Frontend Features + +**Multi-Wallet Support**: Supports NEAR native wallets and Ethereum wallets through Chain Abstraction. + +**Real-time Price Conversion**: Converts USD amounts to NEAR using live price feeds. + +**Transaction Handling**: Graceful error handling and transaction state management. + +**Responsive Design**: Mobile-friendly interface using Bootstrap. + +**Pagination**: Efficient handling of large donation lists. + +## Production Deployment + +The frontend is configured for GitHub Pages deployment: + + + +Deploy with GitHub Actions: + + + +## Styling + +The application uses Bootstrap for consistent styling: + + + +## Testing the Interface + +Test the complete user flow: + +1. **Wallet Connection**: Try different wallet types from the selector +2. **Donation Flow**: Test preset amounts and custom donations +3. **Price Conversion**: Verify USD-to-NEAR conversion accuracy +4. **Transaction States**: Test loading, success, and error states +5. **Data Updates**: Confirm donation tables update after transactions +6. **Mobile Experience**: Test responsive design on various devices + +## Live Demo + +The frontend is deployed and available at: +- **Live Demo**: https://near-examples.github.io/donation-examples/ +- **Contract**: `donation.near-examples.testnet` + +## Next Steps + +With your complete donation application, you can extend it with: + +- **Donation Goals**: Set funding targets and progress tracking +- **Donor Recognition**: Leaderboards and achievement systems +- **Recurring Donations**: Subscription-based giving +- **Multi-Currency**: Support for NEAR fungible tokens +- **Analytics Dashboard**: Detailed donation metrics and insights +- **Mobile App**: React Native version for mobile users + +This donation pattern serves as a foundation for crowdfunding platforms, tip jars, charity applications, and many other token-handling use cases on NEAR. \ No newline at end of file diff --git a/docs/tutorials/examples/advanced-xcc.md b/docs/tutorials/examples/advanced-xcc.md index 78676034766..8f4aaf75315 100644 --- a/docs/tutorials/examples/advanced-xcc.md +++ b/docs/tutorials/examples/advanced-xcc.md @@ -7,8 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import {CodeTabs, Language, Github} from "@site/src/components/codetabs" -This example presents 3 instances of complex cross-contract calls on the NEAR blockchain, showcasing how to batch multiple function calls to a same contract, call multiple contracts in parallel, and handle responses in the callback. It includes both the smart contract and the frontend components. - +This guide explores advanced cross-contract call patterns in NEAR, demonstrating how to orchestrate complex multi-contract interactions. You'll learn to batch function calls, execute contracts in parallel, and handle sophisticated callback scenarios. :::info Simple Cross-Contract Calls @@ -16,11 +15,32 @@ Check the tutorial on how to use [simple cross-contract calls](xcc.md) ::: +## Understanding NEAR's Asynchronous Architecture + +Before diving into complex patterns, it's crucial to understand why NEAR handles cross-contract calls differently from other blockchains. + +NEAR's sharded architecture makes all cross-contract interactions asynchronous and independent. This design choice enables massive scalability but requires a different mental model: + +- **Independent execution**: Each contract runs in its own environment +- **Asynchronous results**: You cannot get immediate responses from external calls +- **Promise-based coordination**: Use Promises to schedule and chain operations + +Think of it like coordinating multiple teams across different time zones—you send instructions, continue with other work, and process responses as they arrive. + +### Why This Design is Powerful + +NEAR's asynchronous approach provides significant advantages: + +- **Scalability**: Sharded execution means calls don't compete for resources +- **Flexibility**: Design sophisticated workflows impossible in synchronous systems +- **Reliability**: Failed calls in one contract don't cascade to others +- **Performance**: Parallel execution enables faster overall processing + --- ## Obtaining the Cross Contract Call Example -You have two options to start the Donation Example: +You have two options to start the Cross Contract Call Example: 1. You can use the app through `Github Codespaces`, which will open a web-based interactive environment. 2. Clone the repository locally and use it from your computer. @@ -86,13 +106,13 @@ The smart contract is available in two flavors: Rust and JavaScript --- -## Smart Contract +## Smart Contract Patterns -### Batch Actions +### Pattern 1: Batch Actions -You can aggregate multiple actions directed towards one same contract into a batched transaction. -Methods called this way are executed sequentially, with the added benefit that, if one fails then -they **all get reverted**. +Batch actions let you aggregate multiple function calls to the same contract into a single atomic transaction. This is perfect when you need sequential operations that must all succeed or all fail together. + +**Use case**: Multi-step operations that require consistency, such as complex DeFi transactions or multi-stage data updates. @@ -110,10 +130,9 @@ they **all get reverted**. -#### Getting the Last Response +#### Handling Batch Responses -In this case, the callback has access to the value returned by the **last -action** from the chain. +With batch actions, your callback receives the result from the **last action** in the sequence. This design makes sense because if any earlier action failed, the entire batch would have reverted. @@ -136,10 +155,11 @@ action** from the chain. --- -### Calling Multiple Contracts +### Pattern 2: Calling Multiple Contracts in Parallel + +When you need to interact with multiple contracts simultaneously, NEAR's parallel execution shines. Each call executes independently—if one fails, the others continue unaffected. -A contract can call multiple other contracts. This creates multiple transactions that execute -all in parallel. If one of them fails the rest **ARE NOT REVERTED**. +**Use case**: Gathering data from multiple sources, executing independent operations, or building resilient multi-protocol interactions. @@ -157,10 +177,9 @@ all in parallel. If one of them fails the rest **ARE NOT REVERTED**. -#### Getting All Responses +#### Processing Multiple Responses -In this case, the callback has access to an **array of responses**, which have either the -value returned by each call, or an error message. +With parallel calls, your callback receives an **array of responses**. Each response either contains the returned value or an error message, allowing you to handle partial failures gracefully. @@ -183,12 +202,11 @@ value returned by each call, or an error message. --- -### Multiple Calls - Same Result Type +### Pattern 3: Multiple Calls with Uniform Response Types -This example is a particular case of the previous one ([Calling Multiple Contracts](#calling-multiple-contracts)). -It simply showcases a different way to check the results by directly accessing the `promise_result` array. +This pattern is particularly useful when calling multiple instances of similar contracts or the same method across different contracts. It demonstrates a clean way to handle uniform response types. -In this case, we call multiple contracts that will return the same type: +**Use case**: Polling multiple data sources, aggregating results from similar contracts, or implementing multi-oracle patterns. @@ -206,10 +224,9 @@ In this case, we call multiple contracts that will return the same type: -#### Getting All Responses +#### Iterating Through Uniform Responses -In this case, the callback again has access to an **array of responses**, which we can iterate checking the -results. +When all external contracts return the same data type, you can process responses more elegantly: @@ -232,9 +249,55 @@ results. --- -### Testing the Contract +## Production Considerations + +### Critical Callback Behavior + +Understanding callback execution is essential for building reliable applications: + +- **Callbacks always execute**: Whether external calls succeed or fail, your callback will run +- **Manual rollbacks required**: Failed external calls don't automatically revert your contract's state changes +- **Token handling**: Failed calls return attached NEAR tokens to your contract, not the original caller + +### Error Handling Strategy + +Implement comprehensive error handling in your callbacks: + +```javascript +@call({ privateFunction: true }) +robust_callback({ user_data, transaction_id }) { + const result = near.promiseResult(0); + + if (result.length === 0) { + // External call failed - implement cleanup + this.revert_user_changes(user_data); + this.refund_if_needed(user_data.amount); + this.log_failure(transaction_id); + return { success: false, error: "External operation failed" }; + } + + try { + const response = JSON.parse(result); + return this.process_success(response, user_data); + } catch (error) { + // Invalid response format + this.handle_parse_error(transaction_id); + return { success: false, error: "Invalid response format" }; + } +} +``` + +### Gas Management Tips + +- **Allocate sufficient gas**: Cross-contract calls consume more gas than single-contract operations +- **Account for callback execution**: Reserve gas for your callback function +- **Handle gas estimation failures**: Implement fallbacks when gas estimates are insufficient + +--- + +## Testing the Contract -The contract readily includes a set of unit and sandbox testing to validate its functionality. To execute the tests, run the following commands: +The contract includes comprehensive testing to validate complex interaction patterns. Run the following commands to execute tests: @@ -257,8 +320,8 @@ The contract readily includes a set of unit and sandbox testing to validate its -:::tip -The `integration tests` use a sandbox to create NEAR users and simulate interactions with the contract. +:::tip Testing Cross-Contract Logic +The integration tests use a sandbox environment to simulate multi-contract interactions. This is essential for validating that your callback logic handles both success and failure scenarios correctly. :::
@@ -311,22 +374,19 @@ Go into the directory containing the smart contract (`cd contract-advanced-ts` o ### CLI: Interacting with the Contract -To interact with the contract through the console, you can use the following commands: +Test the different cross-contract patterns using these commands: ```bash - # Execute contracts sequentially - # Replace with your account ID + # Execute contracts sequentially (batch pattern) near call batch_actions --accountId --gas 300000000000000 - # Execute contracts in parallel - # Replace with your account ID - near call multiple_contracts --accountId --gas 300000000000000 + # Execute contracts in parallel (multiple contracts pattern) + near call multiple_contracts --accountId --gas 300000000000000 - # Execute multiple instances of the same contract in parallel - # Replace with your account ID + # Execute multiple instances with same return type near call similar_contracts --accountId --gas 300000000000000 ``` @@ -334,32 +394,137 @@ To interact with the contract through the console, you can use the following com ```bash - # Execute contracts sequentially - # Replace with your account ID + # Execute contracts sequentially (batch pattern) near contract call-function as-transaction batch_actions json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send - # Execute contracts in parallel - # Replace with your account ID + # Execute contracts in parallel (multiple contracts pattern) near contract call-function as-transaction multiple_contracts json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send - # Execute multiple instances of the same contract in parallel - # Replace with your account ID + # Execute multiple instances with same return type near contract call-function as-transaction similar_contracts json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as network-config testnet sign-with-keychain send ``` +--- + +## Advanced Implementation Patterns + +### Coordinating Complex Multi-Contract Workflows + +For sophisticated applications, you might need to coordinate between multiple contracts with interdependent operations: + +```javascript +// Example: Coordinated DeFi operation across multiple protocols +contract_a = CrossContract(this.dex_contract); +promise_a = contract_a.call("get_price", { token: "USDC" }); + +contract_b = CrossContract(this.lending_contract); +promise_b = contract_b.call("get_collateral_ratio", { user: user_id }); + +combined_promise = promise_a.join( + [promise_b], + "execute_leveraged_trade", + contract_ids=[this.dex_contract, this.lending_contract] +); + +return combined_promise.value(); +``` + +### Error Recovery Strategies + +Implement robust error handling for production applications: + +```javascript +@call({ privateFunction: true }) +complex_operation_callback({ user_id, operation_data, original_state }) { + const results = this.get_all_promise_results(); + + // Check if any critical operations failed + const critical_failures = results.filter((result, index) => + !result.success && operation_data.critical_operations.includes(index) + ); + + if (critical_failures.length > 0) { + // Rollback strategy for critical failures + this.restore_user_state(user_id, original_state); + this.refund_user_funds(user_id, operation_data.total_amount); + return { + success: false, + error: "Critical operations failed", + failed_operations: critical_failures.length + }; + } + + // Process partial success scenarios + return this.handle_partial_success(results, user_id); +} +``` + +--- + +## Best Practices for Complex Cross-Contract Calls + +### 1. Design for Partial Failures + +Always assume some external calls might fail and design your application to handle partial success gracefully. + +### 2. Implement Comprehensive Logging + +Add detailed logging to track cross-contract call outcomes: + +```javascript +near.log(`Cross-contract call initiated: ${contract_id}.${method_name}`); +near.log(`Callback executed with status: ${result.success ? 'SUCCESS' : 'FAILED'}`); +``` + +### 3. Optimize Gas Usage -:::info -If at some point you get an "Exceeded the prepaid gas" error, try to increase the gas amount used within the functions when calling other contracts +Cross-contract calls consume significant gas. Profile your operations and optimize: + +- Use appropriate gas allocations for each external call +- Consider the gas cost of your callback processing +- Implement gas estimation for complex workflows + +### 4. State Management Strategy + +Plan your state changes carefully: + +- Save original state before making external calls +- Implement clear rollback procedures +- Use consistent patterns across your application + +--- + +## Troubleshooting Common Issues + +:::warning Gas Limitations +If you encounter "Exceeded the prepaid gas" errors, increase the gas amount in your external contract calls. Complex multi-contract operations require substantial gas allocation. ::: -:::note Versioning for this article +:::info Callback Debugging +Use NEAR's sandbox testing environment to debug callback logic. The sandbox lets you simulate various failure scenarios and validate your error handling. +::: +:::note Version Compatibility At the time of this writing, this example works with the following versions: - near-cli: `4.0.13` -- node: `18.19.1` +- node: `18.19.1` - rustc: `1.77.0` - ::: + +--- + +## Taking Your Skills Further + +Mastering these complex cross-contract patterns opens up possibilities for building sophisticated applications: + +- **DeFi protocols** that coordinate across multiple markets +- **Multi-step workflows** that span several specialized contracts +- **Resilient systems** that gracefully handle partial failures +- **High-performance applications** that leverage parallel execution + +The key is understanding that NEAR's asynchronous nature isn't a constraint—it's a powerful feature that enables building applications that would be impossible on synchronous blockchains. + +Start with these patterns, experiment with combining them, and you'll discover new ways to architect complex blockchain applications that take full advantage of NEAR's unique capabilities. diff --git a/docs/tutorials/examples/coin-flip.md b/docs/tutorials/examples/coin-flip.md index e05f6ca7302..62d2b562453 100644 --- a/docs/tutorials/examples/coin-flip.md +++ b/docs/tutorials/examples/coin-flip.md @@ -1,135 +1,538 @@ + --- id: coin-flip -title: Coin Flip -description: "Learn to build a coin flip game smart contract with randomness, betting mechanics, and reward distribution on NEAR Protocol." +title: "Coin Flip on NEAR: Mastering Randomness in Smart Contracts" +description: "Learn how to implement secure and fair randomness in NEAR smart contracts through a practical coin flip game, with beginner-friendly examples and advanced techniques." --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import {CodeTabs, Language, Github} from "@site/src/components/codetabs" -This example demonstrates a simple coin flip game on the NEAR blockchain, where players can guess the outcome of a coin flip and earn points. It includes both the smart contract and the frontend components. +# Coin Flip on NEAR: Mastering Randomness in Smart Contracts -![img](/docs/assets/examples/coin-flip.png) +Randomness is a cornerstone of many Web3 applications, from games like lotteries and coin flips to DeFi protocols and NFT minting. However, generating randomness on a blockchain is tricky because all nodes must agree on the same result. In this beginner-friendly tutorial, we'll build a Coin Flip game on NEAR Protocol to learn how to handle randomness securely and fairly. We'll use both JavaScript and Rust, include testing, and explore advanced patterns for more complex applications—all while keeping things clear and approachable. ---- -## Starting the Game -Coin Flip is a game where the player tries to guess the outcome of a coin flip. It is one of the simplest contracts implementing random numbers. +## Why Randomness on Chain Is Hard + +In traditional programming, you might use Math.random() or /dev/urandom for randomness. On a blockchain like NEAR, things are different because: -You have two options to start the example: -1. **Recommended:** use the app through Gitpod (a web-based interactive environment) -2. Clone the project locally. +**Consensus Requirement**: Every node in the network must compute the same result to maintain agreement. If your coin flip gives "heads" on one node and "tails" on another, the blockchain breaks. -| Gitpod | Clone locally | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| Open in Gitpod | `https://github.com/near-examples/coin-flip-examples.git` | +**Predictability Risks**: Using predictable inputs like block timestamps can be manipulated by miners or malicious actors, leading to unfair outcomes. +For example, this won't work on a blockchain: -If you choose Gitpod, a new browser window will open automatically with the code. Give it a minute, and the front-end will pop up (ensure the pop-up window is not blocked). +```javascript +// ❌ BAD: Predictable and manipulable +const outcome = (block.timestamp % 2) ? 'heads' : 'tails'; +``` -If you are running the app locally, you should build and deploy a contract (JavaScript or Rust version) and a client manually. +## NEAR's Elegant Solution ---- +NEAR Protocol solves this with a Verifiable Random Function (VRF), providing a secure and unpredictable random seed through: -## Interacting With the Counter -Go ahead and log in with your NEAR account. If you don't have one, you can create one on the fly. Once logged in, use the `tails` and `heads` buttons to try to guess the next coin flip outcome. +- **Rust**: env::random_seed() (returns a 32-byte array) +- **JavaScript**: near.randomSeed() (returns a 32-byte array) -![img](/docs/assets/examples/coin-flip.png) -*Frontend of the Game* +This randomness is: ---- +**Secure**: Based on cryptographic VRF, making it resistant to manipulation. +**Consistent**: All nodes in a block get the same random seed, ensuring consensus. +**Immediate**: Available within your contract call, no external oracles needed. + +The seed is derived from block-level data (e.g., block producer signatures, block height, and previous randomness), ensuring unpredictability for users but determinism for validators. + +:::tip +Think of NEAR's randomness like flipping a coin where every observer sees the same result, but no one can predict it before the flip! +::: + +## Building the Coin Flip Contract + +Let's build a simple coin flip game where players guess "heads" or "tails," earn points for correct guesses, and lose points for wrong ones. We'll provide implementations in both JavaScript and Rust, followed by tests. + +### JavaScript Implementation + +This contract uses NEAR's JavaScript SDK to create a coin flip game. + +```javascript +import { NearBindgen, near, call, view, UnorderedMap } from 'near-sdk-js'; +import { AccountId } from 'near-sdk-js/lib/types'; + +type Side = 'heads' | 'tails'; + +function simulateCoinFlip(): Side { + // Get 32-byte random seed from NEAR's VRF + const randomSeed = near.randomSeed(); + // Use first byte for a simple 50/50 chance + return randomSeed[0] % 2 === 0 ? 'heads' : 'tails'; +} + +@NearBindgen({}) +class CoinFlip { + points: UnorderedMap = new UnorderedMap("points"); + + static schema = { + points: { class: UnorderedMap, value: 'number' } + }; + + @call({}) + flip_coin({ player_guess }: { player_guess: Side }): Side { + // Validate input + if (!['heads', 'tails'].includes(player_guess)) { + throw new Error('Invalid guess: must be heads or tails'); + } + + const player: AccountId = near.predecessorAccountId(); + near.log(`${player} guessed ${player_guess}`); + + // Generate secure randomness + const outcome = simulateCoinFlip(); + + // Update player points + let player_points: number = this.points.get(player, { defaultValue: 0 }); + if (player_guess === outcome) { + near.log(`Result: ${outcome} - You won!`); + player_points += 1; + } else { + near.log(`Result: ${outcome} - You lost`); + player_points = Math.max(0, player_points - 1); + } + + this.points.set(player, player_points); + return outcome; + } + + @view({}) + points_of({ player }: { player: AccountId }): number { + return this.points.get(player, { defaultValue: 0 }); + } +} +``` + +### Rust Implementation + +This contract uses NEAR's Rust SDK for the same coin flip game. + +```rust +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::UnorderedMap; +use near_sdk::{env, near_bindgen, AccountId}; + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum Side { + Heads, + Tails, +} + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize)] +pub struct CoinFlip { + points: UnorderedMap, +} + +impl Default for CoinFlip { + fn default() -> Self { + Self { + points: UnorderedMap::new(b"p"), + } + } +} + +#[near_bindgen] +impl CoinFlip { + #[private] + fn simulate_coin_flip(&self) -> Side { + let random_seed = env::random_seed(); + if random_seed[0] % 2 == 0 { + Side::Heads + } else { + Side::Tails + } + } + + pub fn flip_coin(&mut self, player_guess: Side) -> Side { + let player = env::predecessor_account_id(); + let outcome = self.simulate_coin_flip(); + + // Update points + let current_points = self.points.get(&player).unwrap_or(0); + let new_points = match (&player_guess, &outcome) { + (Side::Heads, Side::Heads) | (Side::Tails, Side::Tails) => { + env::log_str("You won!"); + current_points + 1 + } + _ => { + env::log_str("You lost!"); + current_points.saturating_sub(1) + } + }; + + self.points.insert(&player, &new_points); + outcome + } + + pub fn points_of(&self, player: AccountId) -> u32 { + self.points.get(&player).unwrap_or(0) + } +} +``` + +### How It Works + +**Randomness**: Both implementations use NEAR's VRF-based random_seed() to generate a 32-byte array. We use the first byte (random_seed[0]) to decide heads (even) or tails (odd). + +**Game Logic**: Players guess "heads" or "tails." A correct guess adds 1 point; a wrong guess subtracts 1 (minimum 0). + +**State**: Points are stored in an UnorderedMap, persisting player scores across calls. + +## Testing Your Contract + +Testing ensures your contract works as expected. Below are tests for both implementations using NEAR's testing frameworks. + +### JavaScript Tests (AVA + near-workspaces) + +```javascript +import anyTest from 'ava'; +import { Worker } from 'near-workspaces'; + +const test = anyTest; + +test.beforeEach(async (t) => { + const worker = t.context.worker = await Worker.init(); + const root = worker.rootAccount; + const contract = await root.createSubAccount('contract'); + await contract.deploy(process.argv[2]); + + t.context.accounts = { root, contract }; +}); + +test.afterEach.always(async (t) => { + await t.context.worker.tearDown(); +}); + +test('by default the user has no points', async (t) => { + const { root, contract } = t.context.accounts; + const points = await contract.view('points_of', { player: root.accountId }); + t.is(points, 0); +}); + +test('points are correctly computed', async (t) => { + const { root, contract } = t.context.accounts; + + let counter = { 'heads': 0, 'tails': 0 }; + let expected_points = 0; + + for (let i = 0; i < 10; i++) { + const res = await root.call(contract, 'flip_coin', { player_guess: 'heads' }); + counter[res] += 1; + expected_points += res === 'heads' ? 1 : -1; + expected_points = Math.max(expected_points, 0); + } + + // Basic randomness check: both outcomes should appear + t.true(counter['heads'] >= 2, 'Expected at least 2 heads'); + t.true(counter['tails'] >= 2, 'Expected at least 2 tails'); -## Structure of a dApp + const points = await contract.view('points_of', { player: root.accountId }); + t.is(points, expected_points); +}); +``` + +### Rust Tests (near-workspaces) + +```rust +use near_workspaces::{types::NearToken, Account, Contract}; +use serde_json::json; + +#[tokio::test] +async fn main() -> Result<(), Box> { + let worker = near_workspaces::sandbox().await?; + let contract_wasm = near_workspaces::compile_project("./").await?; + let contract = worker.dev_deploy(&contract_wasm).await?; + + let alice = worker.dev_create_account().await? + .create_subaccount("alice") + .initial_balance(NearToken::from_near(30)) + .transact().await?.into_result()?; + + test_user_has_no_points(&alice, &contract).await?; + test_points_are_correctly_computed(&alice, &contract).await?; + Ok(()) +} + +async fn test_user_has_no_points( + user: &Account, + contract: &Contract, +) -> Result<(), Box> { + let points: u32 = user.call(contract.id(), "points_of") + .args_json(json!({ "player": user.id()})) + .transact().await?.json()?; + assert_eq!(points, 0); + Ok(()) +} + +async fn test_points_are_correctly_computed( + user: &Account, + contract: &Contract, +) -> Result<(), Box> { + let mut tails_counter = 0; + let mut heads_counter = 0; + let mut expected_points = 0; + + for _ in 0..10 { + let outcome: String = user.call(contract.id(), "flip_coin") + .args_json(json!({"player_guess": "Tails"})) + .transact().await?.json()?; + + if outcome.eq("Tails") { + tails_counter += 1; + expected_points += 1; + } else { + heads_counter += 1; + expected_points = expected_points.saturating_sub(1); + } + } + + assert!(heads_counter >= 2, "Expected at least 2 heads"); + assert!(tails_counter >= 2, "Expected at least 2 tails"); + + let points: u32 = user.call(contract.id(), "points_of") + .args_json(json!({ "player": user.id()})) + .transact().await?.json()?; + + assert_eq!(points, expected_points); + Ok(()) +} +``` + +### What These Tests Verify + +**Initial State**: Players start with zero points. +**Logic**: Points increase for wins and decrease for losses, with a minimum of 0. +**Randomness**: Both heads and tails appear in a sample of 10 flips, ensuring basic randomness quality. +**State Persistence**: Points are correctly stored and retrieved. + +## Understanding NEAR's Randomness -Now that you understand what the dApp does, let us take a closer look to its structure: +### How It Works -1. The frontend code lives in the `/frontend` folder. -2. The smart contract code in Rust is in the `/contract-rs` folder. -3. The smart contract code in JavaScript is in the `/contract-ts` folder. +NEAR's randomness is generated using a Verifiable Random Function (VRF), which combines: + +- Block producer's cryptographic signature +- Previous epoch's random value +- Block height and timestamp +- Network-specific constants + +This produces a 32-byte random seed that's: + +**Unpredictable**: Users can't guess it before the block is produced. +**Deterministic**: All nodes compute the same seed for consensus. +**Cryptographically Secure**: Suitable for gaming and most DApps, though not for generating cryptographic keys due to block-level determinism. :::note -Both Rust and JavaScript versions of the contract implement the same functionality. +The random seed is the same for all transactions in a single block. If you need different random values within one transaction, combine the seed with other inputs like block height or user IDs (see advanced patterns below). ::: -### Contract -The contract presents 2 methods: `flip_coin`, and `points_of`. - - - - - - - - - - -### Running the Frontend - -To start the frontend you will need to install the dependencies and start the server. - -```bash -cd frontend -yarn -yarn dev +### Limitations + +**Block-Level Consistency**: All calls to random_seed() in the same block return the same value. This ensures consensus but means you can't generate multiple unique random numbers in one transaction without additional logic. + +**Validator Trust**: While VRF is secure, validators could theoretically influence future blocks' randomness (though this requires compromising NEAR's consensus, which is highly unlikely). + +**Not for Key Generation**: The seed is secure for DApps but not suitable for cryptographic key generation due to its block-level scope. + +## Advanced Randomness Patterns + +For simple apps like our coin flip, using one byte of the random seed is fine. But for more complex applications, you'll need advanced techniques. Here are a few, explained simply: + +### 1. Random Numbers in a Range + +Suppose you want a random number between min and max (e.g., 1 to 6 for a die roll). Here's how to do it safely in Rust: + +```rust +pub fn random_range(&self, min: u32, max: u32) -> u32 { + assert!(min < max, "min must be less than max"); + let seed = env::random_seed(); + let random_u32 = u32::from_le_bytes([seed[0], seed[1], seed[2], seed[3]]); + let range_size = max - min; + min + (random_u32 % range_size) +} ``` -
+:::warning +Using modulo (%) can introduce slight bias because not all numbers divide evenly into u32::MAX. For high-security apps, use rejection sampling (see below). +::: -### Understanding the Frontend +### 2. Avoiding Modulo Bias (Advanced) + +Modulo can skew results if the range doesn't evenly divide u32::MAX. Here's a bias-free version using rejection sampling: + +```rust +pub fn unbiased_range(&self, min: u32, max: u32) -> u32 { + let range = max - min; + let max_valid = u32::MAX - (u32::MAX % range); + let seed = env::random_seed(); + let mut candidate = u32::from_le_bytes([seed[0], seed[1], seed[2], seed[3]]); + let mut byte_index = 4; + + while candidate >= max_valid && byte_index < 32 { + candidate = u32::from_le_bytes([ + seed[byte_index % 32], + seed[(byte_index + 1) % 32], + seed[(byte_index + 2) % 32], + seed[(byte_index + 3) % 32] + ]); + byte_index += 4; + } + + min + (candidate % range) +} +``` -The frontend is a [Next.JS](https://nextjs.org/) project generated by [create-near-app](https://github.com/near/create-near-app). Check `_app.js` and `index.js` to understand how components are displayed and interacting with the contract. +This ensures every number in the range has an equal chance, which is critical for fair lotteries or NFT drops. - - - - +### 3. Weighted Random Selection ---- +For scenarios where outcomes have different probabilities (e.g., 70% chance of "common" item, 20% "rare," 10% "epic"): -## Testing +```rust +pub fn weighted_selection(&self, weights: Vec) -> usize { + let total_weight: u32 = weights.iter().sum(); + assert!(total_weight > 0, "No valid weights provided"); -When writing smart contracts, it is very important to test all methods exhaustively. In this -project you have integration tests. Before digging into them, go ahead and perform the tests present in the dApp through the command `yarn test` for the JavaScript version, or `./test.sh` for the Rust version. + let random_value = self.unbiased_range(0, total_weight); + let mut cumulative_weight = 0; -### Integration test + for (index, &weight) in weights.iter().enumerate() { + cumulative_weight += weight; + if random_value < cumulative_weight { + return index; + } + } -Integration tests can be written in both Rust and JavaScript. They automatically deploy a new -contract and execute methods on it. In this way, integration tests simulate interactions -from users in a realistic scenario. You will find the integration tests for the `coin-flip` -in `contract-ts/sandbox-ts` (for the JavaScript contract) and `contract-rs/tests` (for the Rust contract). + weights.len() - 1 // Fallback +} +``` - - - - - - - - +Example: weights = [70, 20, 10] for common, rare, epic items. ---- +### 4. Handling Edge Cases + +Sometimes, the random seed might be invalid (e.g., all zeros, though rare). Here's a safe fallback: + +```rust +pub fn safe_random_byte(&self) -> u8 { + let seed = env::random_seed(); + if seed.iter().all(|&x| x == 0) { + // Use XOR of multiple bytes as fallback + seed[0] ^ seed[1] ^ seed[2] ^ seed[3] + } else { + seed[0] + } +} +``` + +## Monitoring Randomness Quality + +For production apps, track randomness to ensure it's fair. Here's a simple way to monitor distribution in Rust: + +```rust +#[derive(BorshSerialize, BorshDeserialize, Default)] +pub struct RandomnessStats { + pub total_calls: u64, + pub distribution: [u32; 256], +} + +impl CoinFlip { + pub fn record_random_usage(&mut self, value: u8) { + self.stats.total_calls += 1; + self.stats.distribution[value as usize] += 1; + } + + pub fn get_randomness_health(&self) -> f64 { + if self.stats.total_calls == 0 { + return 1.0; + } + let expected_per_bucket = self.stats.total_calls as f64 / 256.0; + let mut chi_square = 0.0; + + for &observed in &self.stats.distribution { + let diff = observed as f64 - expected_per_bucket; + chi_square += (diff * diff) / expected_per_bucket; + } + + 1.0 / (1.0 + chi_square / 1000.0) // 1.0 = perfect, 0.0 = biased + } +} +``` + +This tracks how evenly random bytes are distributed, helping you catch issues like bias or errors. + +## Real-World Applications + +Randomness powers many Web3 use cases: + +**Gaming**: Dice rolls, card shuffles, loot drops (e.g., our coin flip game). +**DeFi**: Lottery systems, random reward distributions. +**NFTs**: Random trait assignment during minting, mystery box reveals. +**Governance**: Random jury selection, tie-breaking in votes. + +## Best Practices + +1. **Use Multiple Bytes**: For better distribution, use several bytes from the seed: + +```javascript +function betterRandom(max) { + const seed = near.randomSeed(); + let value = 0; + for (let i = 0; i < 4; i++) { + value += seed[i] * Math.pow(256, i); + } + return value % max; +} +``` + +2. **Validate Inputs**: Always check user inputs: + +```javascript +if (!['heads', 'tails'].includes(guess)) { + throw new Error('Invalid guess: must be heads or tails'); +} +``` + +3. **Handle Block-Level Consistency**: Combine the seed with unique inputs for different random values: + +```rust +let seed = env::random_seed(); +let variant_1 = (seed[0] as u32 * 7 + env::block_height()) % 100; +let variant_2 = (seed[1] as u32 * 13 + env::block_timestamp()) % 100; +``` + +4. **Test Thoroughly**: Use statistical tests (like the chi-square test above) to verify randomness quality. + +5. **Monitor in Production**: Track distribution to detect issues early. + +## Why NEAR for Randomness? -## A Note On Randomness +NEAR's randomness is ideal because it's: -Randomness in the blockchain is a complex subject. We recommend you to read and investigate about it. -You can start with our [security page on it](../../smart-contracts/security/random.md). +**Simple**: No external oracles or multi-step schemes. +**Fast**: Available instantly in your contract. +**Cost-Effective**: No extra fees. +**Secure**: VRF-based and consensus-protected. +**Developer-Friendly**: Easy APIs in Rust and JavaScript. -:::note Versioning for this article +## Key Takeaways -At the time of this writing, this example works with the following versions: +- Blockchain randomness requires consensus, making traditional methods like Math.random() unusable. +- NEAR's env::random_seed() and near.randomSeed() provide VRF-based, secure randomness. +- Start with simple patterns (like our coin flip) and use advanced techniques (e.g., unbiased ranges, weighted selection) for complex apps. +- Test and monitor randomness to ensure fairness and reliability. +- NEAR's approach is perfect for gaming, DeFi, NFTs, and governance applications. -- near-cli: `4.0.13` -- node: `18.19.1` -- rustc: `1.77.0` +Ready to build something random? Deploy this coin flip contract or experiment with advanced patterns in your next NEAR project! +:::note Development Setup +NEAR CLI: 4.0.13 +Node.js: 18.19.1 +Rust: 1.77.0 +near-sdk: 4.1.0 ::: diff --git a/docs/tutorials/examples/factory.md b/docs/tutorials/examples/factory.md index aaf88ff38e8..bfa97dc46a5 100644 --- a/docs/tutorials/examples/factory.md +++ b/docs/tutorials/examples/factory.md @@ -1,25 +1,63 @@ --- id: factory -title: Factory -description: "A factory is a smart contract that stores a compiled contract, and automatizes deploying the stored contract onto new sub-accounts." +title: How to Deploy Contracts from Contracts +description: "Learn how to implement the factory pattern on NEAR to programmatically deploy smart contracts from within other smart contracts." --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import {CodeTabs, Language, Github} from "@site/src/components/codetabs" -A factory is a smart contract that stores a compiled contract, and automatizes deploying the stored contract onto new sub-accounts. +# How to Deploy Contracts from Contracts -We have a [**A Generic Factory**](https://github.com/near-examples/factory-rust) that deploys the [donation contract](./donation.md). This donation contract can be changed for whichever compiled contract you like (e.g. a fungible token or DAO). +The factory pattern is a powerful design pattern that allows one smart contract to deploy and manage other smart contracts programmatically. This tutorial will teach you how to build a factory contract that can store compiled contract code and deploy it to new sub-accounts automatically. + +## What is a Factory Contract? + +A factory contract is a smart contract that acts as a template deployer. Instead of manually deploying each instance of a contract, the factory automates this process by: + +- Storing compiled contract bytecode +- Creating new sub-accounts +- Deploying the stored contract to those sub-accounts +- Managing and updating the stored contract code + +This pattern is particularly useful when you need to deploy many instances of the same contract type, such as creating multiple DAOs, token contracts, or any standardized smart contract. + +## Why Use the Factory Pattern? + +**Cost Efficiency**: Deploy once, reuse many times without re-uploading contract code. + +**Standardization**: Ensure all deployed contracts follow the same tested pattern. + +**Automation**: Programmatically create contracts without manual intervention. + +**Upgradability**: Update the stored contract template for future deployments. + +**Access Control**: Implement permissions for who can deploy new instances. --- -## Overview {#generic-factory} +## Understanding NEAR Account Limitations + +Before implementing a factory, it's crucial to understand NEAR's account creation rules: + +### What Factories Can Do +- Create sub-accounts of themselves (e.g., `factory.near` can create `instance1.factory.near`) +- Deploy contracts to their own sub-accounts +- Manage the stored contract bytecode + +### What Factories Cannot Do +- Create sub-accounts for other accounts +- Deploy contracts to accounts they don't own +- Control sub-accounts after creation (they become independent) + +This means your factory at `factory.testnet` can create `dao1.factory.testnet` and `dao2.factory.testnet`, but cannot create `dao1.alice.testnet`. + +--- -The factory is a smart contract that: +## Building Your Factory Contract -1. Creates sub-accounts of itself and deploys its contract on them (`create_factory_subaccount_and_deploy`). -2. Can change the stored contract using the `update_stored_contract` method. +Let's examine the core components of a factory contract: @@ -32,123 +70,165 @@ The factory is a smart contract that: ---- +### Core Factory Methods -## Quickstart +The factory implements two essential methods: -1. Make sure you have installed [rust](https://www.rust-lang.org/). -2. Install the [`NEAR CLI`](/tools/near-cli#installation) +**`create_factory_subaccount_and_deploy`**: Creates a new sub-account and deploys the stored contract to it. -
+**`update_stored_contract`**: Updates the contract bytecode that will be deployed to future instances. -### Build and Deploy the Factory +--- -You can automatically compile and deploy the contract in the NEAR testnet by running: +## Implementing Contract Deployment -```bash -./deploy.sh -``` +When you call `create_factory_subaccount_and_deploy`, the factory: -Once finished, check the `neardev/dev-account` file to find the address in which the contract was deployed: +1. **Creates the sub-account** using NEAR's account creation APIs +2. **Transfers the required deposit** for account creation and storage +3. **Deploys the stored contract** bytecode to the new account +4. **Initializes the contract** with the provided parameters ```bash -cat ./neardev/dev-account -# e.g. dev-1659899566943-21539992274727 +near call create_factory_subaccount_and_deploy '{ "name": "sub", "beneficiary": ""}' --deposit 1.24 --accountId --gas 300000000000000 ``` -
+The deposit covers: +- Account creation costs (~0.1 NEAR) +- Storage costs for the contract code +- Initial balance for the new contract -### Deploy the Stored Contract Into a Sub-Account +--- -`create_factory_subaccount_and_deploy` will create a sub-account of the factory and deploy the -stored contract on it. +## Managing Contract Updates -```bash -near call create_factory_subaccount_and_deploy '{ "name": "sub", "beneficiary": ""}' --deposit 1.24 --accountId --gas 300000000000000 +One of the factory pattern's key advantages is the ability to update the stored contract for future deployments: + +### The Update Method Implementation + +```rust +#[private] +pub fn update_stored_contract(&mut self) { + self.code = env::input().expect("Error: No input").to_vec(); +} ``` -This will create the `sub.`, which will have a `donation` contract deployed on it: +This method uses a clever optimization: instead of deserializing the input parameters (which would consume excessive gas for large files), it reads the raw input directly using `env::input()`. + +### Why This Optimization Matters + +Standard parameter deserialization would: +1. Parse the entire WASM file from JSON +2. Validate the input format +3. Convert it to the appropriate data type + +For large contract files, this process consumes the entire gas limit. The direct input approach bypasses this overhead. + +### Updating Your Stored Contract ```bash -near view sub. get_beneficiary -# expected response is: +# Convert your contract to base64 +export BYTES=`cat ./path/to/new-contract.wasm | base64` + +# Update the factory's stored contract +near call update_stored_contract "$BYTES" --base64 --accountId --gas 30000000000000 ``` -
+--- + +## Testing Your Factory -### Update the Stored Contract +### 1. Deploy the Factory -`update_stored_contract` enables to change the compiled contract that the factory stores. +```bash +./deploy.sh +``` -The method is interesting because it has no declared parameters, and yet it takes -an input: the new contract to store as a stream of bytes. +Check the deployment: +```bash +cat ./neardev/dev-account +# Returns: dev-1659899566943-21539992274727 +``` -To use it, we need to transform the contract we want to store into its `base64` -representation, and pass the result as input to the method: +### 2. Create Your First Instance ```bash -# Use near-cli to update stored contract -export BYTES=`cat ./src/to/new-contract/contract.wasm | base64` -near call update_stored_contract "$BYTES" --base64 --accountId --gas 30000000000000 +near call create_factory_subaccount_and_deploy '{ "name": "test-instance", "beneficiary": "alice.testnet"}' --deposit 1.24 --accountId --gas 300000000000000 ``` -> This works because the arguments of a call can be either a `JSON` object or a `String Buffer` +### 3. Verify the Deployment ---- +```bash +near view test-instance. get_beneficiary +# Expected: alice.testnet +``` -## Factories - Concepts & Limitations +--- -Factories are an interesting concept, here we further explain some of their implementation aspects, -as well as their limitations. +## Best Practices and Considerations -
+### Gas Management +- Contract deployment requires significant gas (200-300 TGas) +- Always specify sufficient gas limits +- Test gas requirements with smaller contracts first -### Automatically Creating Accounts +### Storage Costs +- Factor in storage costs for both factory and deployed contracts +- Require sufficient deposit to cover all costs +- Consider implementing deposit refund mechanisms -NEAR accounts can only create sub-accounts of itself, therefore, the `factory` can only create and -deploy contracts on its own sub-accounts. +### Security Considerations +- Implement access controls for who can deploy contracts +- Validate initialization parameters before deployment +- Consider implementing factory ownership and permissions -This means that the factory: +### Contract Versioning +- Implement version tracking for stored contracts +- Consider allowing multiple contract versions +- Document breaking changes between versions -1. **Can** create `sub.factory.testnet` and deploy a contract on it. -2. **Cannot** create sub-accounts of the `predecessor`. -3. **Can** create new accounts (e.g. `account.testnet`), but **cannot** deploy contracts on them. +--- -It is important to remember that, while `factory.testnet` can create `sub.factory.testnet`, it has -no control over it after its creation. +## Alternative Approaches -
+### Direct Account Creation +Instead of using a factory, you could create accounts directly, but this requires: +- Manual contract deployment for each instance +- Separate storage costs for each deployment +- More complex coordination between deployments -### The Update Method +### Proxy Pattern +For upgradeable contracts, consider the proxy pattern where: +- A proxy contract delegates calls to an implementation contract +- Updates change the implementation address +- All instances can be upgraded simultaneously -The `update_stored_contracts` has a very short implementation: +### When to Use Factories +Factories work best when: +- You need many instances of similar contracts +- Instances should be independent after creation +- You want to standardize deployment parameters +- Cost optimization is important for multiple deployments -```rust -#[private] -pub fn update_stored_contract(&mut self) { - self.code = env::input().expect("Error: No input").to_vec(); -} -``` +--- -On first sight it looks like the method takes no input parameters, but we can see that its only -line of code reads from `env::input()`. What is happening here is that `update_stored_contract` -**bypasses** the step of **deserializing the input**. +## Common Pitfalls to Avoid -You could implement `update_stored_contract(&mut self, new_code: Vec)`, -which takes the compiled code to store as a `Vec`, but that would trigger the contract to: +**Insufficient Deposits**: Always calculate the minimum required deposit including account creation, storage, and initialization costs. -1. Deserialize the `new_code` variable from the input. -2. Sanitize it, making sure it is correctly built. +**Gas Limit Errors**: Contract deployment is gas-intensive. Start with higher limits and optimize down. -When dealing with big streams of input data (as is the compiled `wasm` file to be stored), this process -of deserializing/checking the input ends up **consuming the whole GAS** for the transaction. +**Access Control**: Implement proper permissions to prevent unauthorized contract deployments. -:::note Versioning for this article +**Storage Management**: Monitor storage costs as they accumulate with each stored contract and deployment. -At the time of this writing, this example works with the following versions: +**Sub-account Naming**: Plan your naming convention carefully as sub-accounts cannot be renamed after creation. +:::note Development Environment +This tutorial works with: - near-cli: `4.0.13` -- node: `18.19.1` +- node: `18.19.1` - rustc: `1.77.0` - ::: + +The factory pattern provides a powerful way to scale smart contract deployment on NEAR. By understanding the account limitations, gas considerations, and implementation details, you can build efficient factory contracts that automate and standardize your deployment process. diff --git a/docs/tutorials/examples/near-drop.md b/docs/tutorials/examples/near-drop.md index e47f9b08f3f..266730a3940 100644 --- a/docs/tutorials/examples/near-drop.md +++ b/docs/tutorials/examples/near-drop.md @@ -8,296 +8,480 @@ 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 +Get step by step usage [Here](../../tutorials/near drop/introduction.md) -:::tip +## What Are Drops? -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 +Think of a drop as a digital gift card that you can send to anyone. Here's how it works: -::: +**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" ---- - -## Contract Overview +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. -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. +### Real-World Example -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. +Imagine Alice wants to onboard her friend Bob to NEAR: -Imagine Alice want to drop some NEAR to Bob: +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 -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 +The magic happens because of NEAR's unique **Function Call Keys** - the contract can actually create accounts on behalf of users! -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. +## Types of Drops -
+There are three types of drops you can create: -Contract's interface +- **NEAR Drops**: Drop native NEAR tokens +- **FT Drops**: Drop fungible tokens (like stablecoins) +- **NFT Drops**: Drop non-fungible tokens (like collectibles) -#### `create_near_drop(public_keys, amount_per_drop)` -Creates `#public_keys` drops, each with `amount_per_drop` NEAR tokens on them +## Building Your Own Drop Contract -#### `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` +Let's walk through creating a drop contract step by step. -#### `create_nft_drop(public_key, nft_contract)` -Creates a drop with an NFT token, which will come from the `nft_contract` +### 1. Setting Up the Contract Structure -#### `claim_for(account_id)` -Claims a drop, which will be sent to the existing `account_id` +First, let's understand what our contract needs to track: -#### `create_account_and_claim(account_id)` -Creates the `account_id`, and then drops the tokens into it + -
+```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, +} +``` ---- +
-## Contract's State +### 2. Defining Drop Types -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). +We need to handle three different types of drops: -- `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 +#[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, +} +``` ---- + -## Drop Types +### 3. Creating NEAR Drops -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). +Here's how to implement NEAR token drops: - - - - + +```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 + +# 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 +``` -### Deploying the Contract to the NEAR network + + -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 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 create-account --useFaucet - ``` - + + - +```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 - # 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 - ``` - + -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" ``` ---- + + -### CLI: Interacting with the Contract +```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 -To interact with the contract through the console, you can use the following commands: +# 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 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 - ``` - + -:::note Versioning for this article +### 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 +``` + + + + +### 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. -::: +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. diff --git a/docs/tutorials/factory/0-introduction.md b/docs/tutorials/factory/0-introduction.md new file mode 100644 index 00000000000..9ec7f46fd9e --- /dev/null +++ b/docs/tutorials/factory/0-introduction.md @@ -0,0 +1,59 @@ +--- +id: introduction +title: Deploy Contracts from Contracts +sidebar_label: Introduction +description: "Learn how to implement the factory pattern on NEAR to programmatically deploy smart contracts from within other smart contracts." +--- + +The factory pattern is one of the most powerful design patterns in smart contract development. It allows you to deploy and manage multiple contract instances programmatically, automating what would otherwise be a manual process. + +## How It Works + +A factory contract stores compiled bytecode and can create new sub-accounts, then deploy that stored code to those accounts. This enables you to: + +- Deploy many instances of the same contract type +- Standardize contract deployment parameters +- Reduce gas costs through code reuse +- Automate contract creation workflows + +:::info + +The complete source code for this tutorial is available in the [GitHub repository](https://github.com/near-examples/factory-rust). + +You can also interact with a deployed factory at `factory-example.testnet` on testnet. + +::: + +## What You Will Learn + +In this tutorial, you will learn how to: + +- [Build a factory contract](1-factory-contract.md) that stores and deploys contract code +- [Deploy your factory](2-deploy-factory.md) and upload initial contract bytecode +- [Create contract instances](3-create-instances.md) using the factory +- [Update the stored contract](4-update-contract.md) for future deployments + +## Prerequisites + +- Basic understanding of NEAR smart contracts +- Rust development environment set up +- NEAR CLI installed and configured +- A NEAR testnet account with some balance + +## Account Limitations + +Before we begin, it's important to understand NEAR's account creation rules: + +**What factories can do:** +- Create sub-accounts of themselves (e.g., `factory.testnet` → `instance1.factory.testnet`) +- Deploy contracts to their own sub-accounts +- Manage stored contract bytecode + +**What factories cannot do:** +- Create sub-accounts for other accounts +- Deploy contracts to accounts they don't own +- Control sub-accounts after creation (they become independent) + +This means your factory at `factory.testnet` can create `dao1.factory.testnet` but cannot create `dao1.alice.testnet`. + +Ready to get started? Let's [build your first factory contract](1-factory-contract.md)! \ No newline at end of file diff --git a/docs/tutorials/factory/1-factory-contract.md b/docs/tutorials/factory/1-factory-contract.md new file mode 100644 index 00000000000..8a83f8fc943 --- /dev/null +++ b/docs/tutorials/factory/1-factory-contract.md @@ -0,0 +1,97 @@ +--- +id: factory-contract +title: Building a Factory Contract +sidebar_label: Build Factory Contract +description: "Create a smart contract that can store bytecode and deploy it to new sub-accounts." +--- + +import {Github} from "@site/src/components/codetabs" + +A factory contract needs two core capabilities: storing contract bytecode and deploying it to new sub-accounts. Let's build this step by step. + +## Contract Structure + +First, let's look at the basic structure of our factory contract: + + + +The factory contract stores the compiled bytecode in a `Vec` field. This bytecode will be deployed to each new sub-account we create. + +## Core Deployment Method + +The heart of our factory is the deployment method. This function creates a new sub-account and deploys our stored contract to it: + + + +Let's break down what this method does: + +1. **Creates a sub-account** using the provided name +2. **Transfers the required deposit** to cover account creation and storage costs +3. **Deploys the stored contract code** to the new account +4. **Calls the initialization method** on the deployed contract + +## Account Creation Promise + +The `Promise::new()` chain handles the complex process of account creation and contract deployment: + + + +This promise chain: +- Creates the account with the specified initial balance +- Deploys the bytecode stored in `self.code` +- Calls the contract's initialization method + +## Contract Management + +We also need a way to update the stored contract code for future deployments: + + + +The `#[private]` annotation ensures only the factory contract itself can update the stored bytecode. + +## Why Direct Input Reading? + +Notice the `env::input()` approach instead of regular parameter deserialization. This is a gas optimization: + +- Standard deserialization would parse the entire WASM file from JSON +- For large contracts, this consumes the entire gas limit +- Direct input reading bypasses this overhead + +## Initialization Method + +Finally, we need an initialization method for when the factory is first deployed: + + + +This sets up the factory with an initial contract to deploy. The factory owner can later update this stored contract. + +## Cargo.toml Dependencies + +Make sure your `Cargo.toml` includes the necessary dependencies: + +```toml +[dependencies] +near-sdk = "4.1.1" +``` + +## Building the Contract + +Compile your factory contract: + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +The compiled WASM file will be in `target/wasm32-unknown-unknown/release/`. + +Now that we have our factory contract built, let's [deploy it to testnet](2-deploy-factory.md) and upload our first contract template. \ No newline at end of file diff --git a/docs/tutorials/factory/2-deploy-factory.md b/docs/tutorials/factory/2-deploy-factory.md new file mode 100644 index 00000000000..2c21fdee496 --- /dev/null +++ b/docs/tutorials/factory/2-deploy-factory.md @@ -0,0 +1,179 @@ +--- +id: deploy-factory +title: Deploying Your Factory Contract +sidebar_label: Deploy Factory +description: "Deploy the factory contract to NEAR testnet and upload the initial contract template." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Now that we have our factory contract built, let's deploy it to NEAR testnet and upload our first contract template. + +## Deploy the Factory + +First, let's deploy our factory contract to a testnet account: + + + + +```bash +# Create a new account for your factory (optional) +near account create-account fund-myself my-factory.testnet + +# Deploy the factory contract +near contract deploy my-factory.testnet use-file target/wasm32-unknown-unknown/release/factory.wasm with-init-call new json-args '{"code": ""}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send +``` + + + + +Create a `deploy.sh` script: + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" + +# Build the factory contract +cargo build --target wasm32-unknown-unknown --release + +# Deploy factory +near contract deploy $FACTORY_ACCOUNT \ + use-file target/wasm32-unknown-unknown/release/factory.wasm \ + with-init-call new json-args '{"code": ""}' \ + prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' \ + network-config testnet sign-with-keychain send + +echo "Factory deployed to: $FACTORY_ACCOUNT" +``` + + + + +## Prepare Your Template Contract + +For this tutorial, we'll use a simple "Hello World" contract as our template. Here's a minimal example: + +```rust +use near_sdk::near_bindgen; +use near_sdk::{env, AccountId, PanicOnDefault}; + +#[near_bindgen] +#[derive(PanicOnDefault)] +pub struct HelloWorld { + pub beneficiary: AccountId, +} + +#[near_bindgen] +impl HelloWorld { + #[init] + pub fn new(beneficiary: AccountId) -> Self { + Self { beneficiary } + } + + pub fn get_beneficiary(&self) -> AccountId { + self.beneficiary.clone() + } + + pub fn say_hello(&self) -> String { + format!("Hello from {}", env::current_account_id()) + } +} +``` + +Build this contract: + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +## Upload Contract Template + +Now we need to upload our template contract to the factory. This requires converting the WASM file to base64: + + + + +```bash +# Convert contract to base64 +export TEMPLATE_BYTES=$(cat target/wasm32-unknown-unknown/release/hello_world.wasm | base64 -w 0) + +# Upload to factory +near call my-factory.testnet update_stored_contract "$TEMPLATE_BYTES" \ + --base64 --accountId my-factory.testnet \ + --gas 300000000000000 --networkId testnet +``` + + + + +Create an `upload_template.sh` script: + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" +TEMPLATE_PATH="target/wasm32-unknown-unknown/release/hello_world.wasm" + +# Convert to base64 +TEMPLATE_BYTES=$(cat $TEMPLATE_PATH | base64 -w 0) + +# Upload template +near call $FACTORY_ACCOUNT update_stored_contract "$TEMPLATE_BYTES" \ + --base64 --accountId $FACTORY_ACCOUNT \ + --gas 300000000000000 --networkId testnet + +echo "Template contract uploaded successfully!" +``` + + + + +## Verify Deployment + +Let's verify that our factory is deployed and ready: + +```bash +# Check factory state (should show stored contract size) +near view my-factory.testnet get_info --networkId testnet +``` + +## Understanding the Costs + +When uploading contract templates, consider these costs: + +**Storage Costs:** +- Each byte of stored WASM code costs storage +- Larger contracts require more NEAR tokens locked for storage +- Storage costs are paid by the factory account + +**Gas Costs:** +- Uploading large contracts consumes significant gas +- Use `--gas 300000000000000` (300 TGas) for safety +- Failed uploads still consume gas + +**Deployment Costs (for later steps):** +- Creating sub-accounts: ~0.1 NEAR minimum +- Contract deployment: Gas + storage for the new account +- Initialization calls: Additional gas costs + +## Common Issues + +**"Smart contract panicked: The contract is not initialized"** +- Make sure you called the `new` method during deployment +- Verify the initialization parameters are correct + +**"Exceeded the prepaid gas"** +- Increase gas limit for large contract uploads +- Try with `--gas 300000000000000` + +**"Not enough balance"** +- Ensure your factory account has enough NEAR for storage costs +- Add more funds if needed + +## Next Steps + +With your factory deployed and template uploaded, you're ready to [create your first contract instances](3-create-instances.md)! \ No newline at end of file diff --git a/docs/tutorials/factory/3-create-instances.md b/docs/tutorials/factory/3-create-instances.md new file mode 100644 index 00000000000..14dd046e377 --- /dev/null +++ b/docs/tutorials/factory/3-create-instances.md @@ -0,0 +1,232 @@ +--- +id: create-instances +title: Creating Contract Instances +sidebar_label: Create Instances +description: "Use your factory to deploy multiple contract instances to sub-accounts." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Now that your factory is deployed and has a contract template stored, let's create some contract instances! + +## Understanding Instance Creation + +When you call the factory's deployment method, it: + +1. **Creates a new sub-account** under your factory +2. **Transfers the required deposit** for account creation and storage +3. **Deploys the stored contract** to the new sub-account +4. **Initializes the contract** with your specified parameters + +The new contract becomes completely independent after creation. + +## Create Your First Instance + +Let's deploy our first contract instance: + + + + +```bash +near call my-factory.testnet create_factory_subaccount_and_deploy \ + '{ + "name": "hello1", + "beneficiary": "alice.testnet" + }' \ + --deposit 1.5 --accountId your-account.testnet \ + --gas 300000000000000 --networkId testnet +``` + + + + +Create a `create_instance.sh` script: + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" +INSTANCE_NAME=$1 +BENEFICIARY=$2 + +if [ -z "$INSTANCE_NAME" ] || [ -z "$BENEFICIARY" ]; then + echo "Usage: ./create_instance.sh " + exit 1 +fi + +near call $FACTORY_ACCOUNT create_factory_subaccount_and_deploy \ + "{ + \"name\": \"$INSTANCE_NAME\", + \"beneficiary\": \"$BENEFICIARY\" + }" \ + --deposit 1.5 --accountId $FACTORY_ACCOUNT \ + --gas 300000000000000 --networkId testnet + +echo "Instance created: $INSTANCE_NAME.$FACTORY_ACCOUNT" +``` + +Usage: `./create_instance.sh hello1 alice.testnet` + + + + +This creates a new contract at `hello1.my-factory.testnet` initialized with `alice.testnet` as the beneficiary. + +## Verify Instance Creation + +Let's confirm our instance was created and works correctly: + +```bash +# Check the beneficiary +near view hello1.my-factory.testnet get_beneficiary --networkId testnet +# Expected output: alice.testnet + +# Test the contract functionality +near view hello1.my-factory.testnet say_hello --networkId testnet +# Expected output: "Hello from hello1.my-factory.testnet" +``` + +## Create Multiple Instances + +One of the factory pattern's main benefits is creating multiple instances easily: + +```bash +# Create several instances with different beneficiaries +near call my-factory.testnet create_factory_subaccount_and_deploy \ + '{"name": "dao1", "beneficiary": "dao-member1.testnet"}' \ + --deposit 1.5 --accountId your-account.testnet \ + --gas 300000000000000 --networkId testnet + +near call my-factory.testnet create_factory_subaccount_and_deploy \ + '{"name": "dao2", "beneficiary": "dao-member2.testnet"}' \ + --deposit 1.5 --accountId your-account.testnet \ + --gas 300000000000000 --networkId testnet + +near call my-factory.testnet create_factory_subaccount_and_deploy \ + '{"name": "game1", "beneficiary": "game-admin.testnet"}' \ + --deposit 1.5 --accountId your-account.testnet \ + --gas 300000000000000 --networkId testnet +``` + +## Understanding Deposit Requirements + +The `--deposit` parameter covers several costs: + +**Account Creation:** ~0.1 NEAR minimum for new account creation + +**Storage Costs:** Contract code storage on the new account (~0.1-1 NEAR depending on contract size) + +**Initial Balance:** Remaining deposit becomes the new account's initial balance + +**Recommended Deposits:** +- Small contracts (< 100KB): 1.0 NEAR +- Medium contracts (100-500KB): 1.5 NEAR +- Large contracts (> 500KB): 2.0+ NEAR + +## Batch Creation Script + +For creating many instances, use a batch script: + + + + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" + +# Array of instances to create +declare -a INSTANCES=( + "instance1:alice.testnet" + "instance2:bob.testnet" + "instance3:charlie.testnet" + "dao1:dao-admin.testnet" + "game1:game-master.testnet" +) + +for instance in "${INSTANCES[@]}"; do + IFS=':' read -r name beneficiary <<< "$instance" + + echo "Creating instance: $name with beneficiary: $beneficiary" + + near call $FACTORY_ACCOUNT create_factory_subaccount_and_deploy \ + "{\"name\": \"$name\", \"beneficiary\": \"$beneficiary\"}" \ + --deposit 1.5 --accountId $FACTORY_ACCOUNT \ + --gas 300000000000000 --networkId testnet + + echo "Created: $name.$FACTORY_ACCOUNT" + echo "---" +done + +echo "All instances created successfully!" +``` + + + + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" + +# Verify all instances +declare -a INSTANCES=("instance1" "instance2" "instance3" "dao1" "game1") + +for name in "${INSTANCES[@]}"; do + echo "Checking $name.$FACTORY_ACCOUNT:" + + # Check if account exists and get beneficiary + near view $name.$FACTORY_ACCOUNT get_beneficiary --networkId testnet 2>/dev/null && \ + echo "✅ $name.$FACTORY_ACCOUNT is working" || \ + echo "❌ $name.$FACTORY_ACCOUNT failed" + + echo "---" +done +``` + + + + +## Monitoring Deployment + +You can monitor your deployments by checking transaction receipts: + +```bash +# Get recent transactions for your factory +near tx-status --accountId my-factory.testnet --networkId testnet +``` + +Or view them in [NEAR Explorer](https://testnet.nearblocks.io/) by searching for your factory account. + +## Common Issues + +**"Sub-account already exists"** +- Each sub-account name must be unique +- Try a different name or add numbers/timestamps + +**"Not enough deposit"** +- Increase the `--deposit` amount +- Large contracts need more storage deposit + +**"Exceeded the prepaid gas"** +- Use `--gas 300000000000000` for safety +- Contract deployment is gas-intensive + +**"Account doesn't exist after creation"** +- Check the transaction receipt for errors +- Verify the factory has enough balance for deposits + +## Instance Independence + +Remember that once created, instances are completely independent: + +- They have their own balance and storage +- The factory cannot control them after creation +- They can be updated independently of the factory +- Each instance can have different owners/admins + +Ready to learn how to update your factory's contract template? Continue to [updating stored contracts](4-update-contract.md). \ No newline at end of file diff --git a/docs/tutorials/factory/4-update-contract.md b/docs/tutorials/factory/4-update-contract.md new file mode 100644 index 00000000000..40ed65162dc --- /dev/null +++ b/docs/tutorials/factory/4-update-contract.md @@ -0,0 +1,292 @@ +--- +id: update-contract +title: Updating Stored Contracts +sidebar_label: Update Contract Template +description: "Learn how to update the contract template stored in your factory for future deployments." +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +One of the factory pattern's most powerful features is the ability to update the stored contract template. This means you can improve your contract and all future instances will use the new version. + +## Why Update Contract Templates? + +**Bug Fixes:** Deploy corrected versions to prevent issues in new instances + +**Feature Additions:** Add new functionality to future deployments + +**Performance Improvements:** Optimize gas usage and execution speed + +**Security Enhancements:** Address security vulnerabilities + +**API Changes:** Update contract interfaces and methods + +:::info +Updating the stored contract only affects **future** deployments. Existing instances remain unchanged and independent. +::: + +## Prepare Your Updated Contract + +Let's create an improved version of our Hello World contract with additional features: + +```rust +use near_sdk::near_bindgen; +use near_sdk::{env, AccountId, PanicOnDefault, Timestamp}; + +#[near_bindgen] +#[derive(PanicOnDefault)] +pub struct HelloWorldV2 { + pub beneficiary: AccountId, + pub created_at: Timestamp, + pub greeting_count: u64, +} + +#[near_bindgen] +impl HelloWorldV2 { + #[init] + pub fn new(beneficiary: AccountId) -> Self { + Self { + beneficiary, + created_at: env::block_timestamp(), + greeting_count: 0, + } + } + + pub fn get_beneficiary(&self) -> AccountId { + self.beneficiary.clone() + } + + pub fn get_created_at(&self) -> Timestamp { + self.created_at + } + + pub fn say_hello(&mut self) -> String { + self.greeting_count += 1; + format!( + "Hello #{} from {} (created at {})", + self.greeting_count, + env::current_account_id(), + self.created_at + ) + } + + pub fn get_greeting_count(&self) -> u64 { + self.greeting_count + } +} +``` + +Build the updated contract: + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +## Update the Factory Template + +Now let's update our factory with the new contract template: + + + + +```bash +# Convert new contract to base64 +export NEW_TEMPLATE_BYTES=$(cat target/wasm32-unknown-unknown/release/hello_world_v2.wasm | base64 -w 0) + +# Update factory's stored contract +near call my-factory.testnet update_stored_contract "$NEW_TEMPLATE_BYTES" \ + --base64 --accountId my-factory.testnet \ + --gas 300000000000000 --networkId testnet +``` + + + + +Create an `update_template.sh` script: + +```bash +#!/bin/bash +set -e + +FACTORY_ACCOUNT="my-factory.testnet" +NEW_CONTRACT_PATH="target/wasm32-unknown-unknown/release/hello_world_v2.wasm" + +echo "Updating factory template with: $NEW_CONTRACT_PATH" + +# Convert to base64 +NEW_TEMPLATE_BYTES=$(cat $NEW_CONTRACT_PATH | base64 -w 0) + +# Update the stored contract +near call $FACTORY_ACCOUNT update_stored_contract "$NEW_TEMPLATE_BYTES" \ + --base64 --accountId $FACTORY_ACCOUNT \ + --gas 300000000000000 --networkId testnet + +echo "Factory template updated successfully!" +echo "New instances will use the updated contract." +``` + + + + +## Test the Updated Template + +Create a new instance to verify the template was updated: + +```bash +# Create instance with new template +near call my-factory.testnet create_factory_subaccount_and_deploy \ + '{"name": "hello-v2", "beneficiary": "alice.testnet"}' \ + --deposit 1.5 --accountId your-account.testnet \ + --gas 300000000000000 --networkId testnet + +# Test new features +near view hello-v2.my-factory.testnet get_created_at --networkId testnet +near view hello-v2.my-factory.testnet get_greeting_count --networkId testnet + +# Test the enhanced say_hello method +near call hello-v2.my-factory.testnet say_hello \ + --accountId your-account.testnet --networkId testnet +``` + +## Version Comparison + +Let's compare old and new instances: + + + + +```bash +# Old instance (hello1.my-factory.testnet) +near view hello1.my-factory.testnet say_hello --networkId testnet +# Output: "Hello from hello1.my-factory.testnet" + +# This method doesn't exist in v1 +near view hello1.my-factory.testnet get_created_at --networkId testnet +# Error: MethodNotFound +``` + + + + +```bash +# New instance (hello-v2.my-factory.testnet) +near call hello-v2.my-factory.testnet say_hello \ + --accountId your-account.testnet --networkId testnet +# Output: "Hello #1 from hello-v2.my-factory.testnet (created at 1699123456789)" + +# New methods work +near view hello-v2.my-factory.testnet get_created_at --networkId testnet +near view hello-v2.my-factory.testnet get_greeting_count --networkId testnet +``` + + + + +## Managing Multiple Versions + +For production systems, consider implementing version tracking: + +```rust +#[near_bindgen] +#[derive(PanicOnDefault)] +pub struct VersionedFactory { + pub current_version: String, + pub contracts: UnorderedMap>, // version -> bytecode +} + +#[near_bindgen] +impl VersionedFactory { + pub fn update_contract_version(&mut self, version: String) { + self.contracts.insert(&version, env::input().unwrap().to_vec()); + self.current_version = version; + } + + pub fn deploy_specific_version(&mut self, name: String, version: String, args: String) { + let code = self.contracts.get(&version).expect("Version not found"); + // Deploy specific version... + } +} +``` + +## Update Best Practices + +**Test Thoroughly:** Always test updated contracts on testnet first + +**Version Documentation:** Keep track of changes between versions + +**Backward Compatibility:** Consider how changes affect existing instances + +**Storage Costs:** Larger contracts increase deployment costs + +**Breaking Changes:** Document any API changes for instance users + +**Rollback Plan:** Keep previous versions available if needed + +## Gas and Storage Considerations + +**Update Costs:** +- Gas: 200-300 TGas for large contracts +- Storage: Additional costs for larger bytecode +- No cost for smaller/optimized contracts + +**Deployment Impact:** +- Future deployments use new storage costs +- Existing instances unaffected +- Factor new costs into deposit calculations + +## Automation and CI/CD + +For production factories, consider automation: + +```bash +#!/bin/bash +# Automated deployment pipeline + +# Build latest contract +cargo build --target wasm32-unknown-unknown --release + +# Run tests +cargo test + +# Update factory if tests pass +if [ $? -eq 0 ]; then + ./update_template.sh + echo "Factory updated with latest version" +else + echo "Tests failed, factory not updated" + exit 1 +fi +``` + +## Monitoring Updates + +Track your factory's evolution: + +```bash +# Check factory state +near view my-factory.testnet get_info --networkId testnet + +# Monitor storage usage +near state my-factory.testnet --networkId testnet +``` + +## Final Thoughts + +You've now learned the complete factory pattern workflow: + +✅ **Built** a factory contract that stores and deploys bytecode +✅ **Deployed** your factory to testnet +✅ **Created** multiple contract instances +✅ **Updated** the stored contract template + +The factory pattern enables powerful automation and standardization for smart contract deployment on NEAR. Use it when you need to deploy many similar contracts while maintaining consistency and reducing costs. + +## Next Steps + +Consider exploring: +- **Advanced Factory Patterns:** Multi-template factories, versioning systems +- **Access Control:** Permission systems for who can deploy instances +- **Factory Upgrades:** Upgrading the factory contract itself +- **Cross-Contract Calls:** Factories that interact with their instances +- **Integration:** Using factories in larger application architectures \ No newline at end of file diff --git a/docs/tutorials/neardrop/access-keys.md b/docs/tutorials/neardrop/access-keys.md new file mode 100644 index 00000000000..b5b8b0e7f0a --- /dev/null +++ b/docs/tutorials/neardrop/access-keys.md @@ -0,0 +1,200 @@ +--- +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! +::: \ 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..a7338d92f64 --- /dev/null +++ b/docs/tutorials/neardrop/account-creation.md @@ -0,0 +1,186 @@ +--- +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. +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/contract-architecture.md b/docs/tutorials/neardrop/contract-architecture.md new file mode 100644 index 00000000000..6871caba482 --- /dev/null +++ b/docs/tutorials/neardrop/contract-architecture.md @@ -0,0 +1,155 @@ +--- +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! +::: \ 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..6a7f0432168 --- /dev/null +++ b/docs/tutorials/neardrop/frontend.md @@ -0,0 +1,220 @@ +--- +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! +::: \ 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..b01b3662de0 --- /dev/null +++ b/docs/tutorials/neardrop/ft-drops.md @@ -0,0 +1,183 @@ +--- +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. +::: \ 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..0daed731f1a --- /dev/null +++ b/docs/tutorials/neardrop/introduction.md @@ -0,0 +1,133 @@ +--- +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`+ +::: \ 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..1dc13055af5 --- /dev/null +++ b/docs/tutorials/neardrop/near-drops.md @@ -0,0 +1,157 @@ +--- +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! +::: \ No newline at end of file diff --git a/docs/tutorials/neardrop/nft-drops.md b/docs/tutorials/neardrop/nft-drops.md new file mode 100644 index 00000000000..f29b7c73cb0 --- /dev/null +++ b/docs/tutorials/neardrop/nft-drops.md @@ -0,0 +1,176 @@ +--- +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 +::: \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index 7e41c7e0595..11f9e4bbace 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -265,12 +265,56 @@ const sidebar = { "Tutorials": [ 'tutorials/examples/count-near', 'tutorials/examples/guest-book', - 'tutorials/examples/donation', - 'tutorials/examples/coin-flip', + // 'tutorials/examples/donation', + // 'tutorials/examples/coin-flip', 'tutorials/examples/factory', - 'tutorials/examples/near-drop', + { + "LinkDrops": [ + 'tutorials/neardrop/introduction', + 'tutorials/neardrop/contract-architecture', + 'tutorials/neardrop/near-drops', + 'tutorials/neardrop/ft-drops', + 'tutorials/neardrop/nft-drops', + 'tutorials/neardrop/access-keys', + 'tutorials/neardrop/account-creation', + 'tutorials/neardrop/frontend' + ]}, + { + "Advanced Cross-Contract Call": [ + 'tutorials/advanced-xcc/introduction', + 'tutorials/advanced-xcc/setup', + 'tutorials/advanced-xcc/batch-actions', + 'tutorials/advanced-xcc/parallel-execution', + 'tutorials/advanced-xcc/testing-deployment' + ]}, + // { + // "Factory": [ + // 'tutorials/factory/0-introduction', + // 'tutorials/factory/1-factory-contract', + // 'tutorials/factory/2-deploy-factory', + // 'tutorials/factory/3-create-instances', + // 'tutorials/factory/4-update-contract', + // ]}, + { + "Coin Flip": [ + 'tutorials/coin-flip/introduction', + 'tutorials/coin-flip/randomness-basics', + 'tutorials/coin-flip/basic-contract', + 'tutorials/coin-flip/testing-randomness', + 'tutorials/coin-flip/advanced-patterns', + 'tutorials/coin-flip/deployment', + ]}, + { + "Donation": [ + 'tutorials/donation/introduction', + 'tutorials/donation/setup', + 'tutorials/donation/contract', + 'tutorials/donation/queries', + 'tutorials/donation/testing', + 'tutorials/donation/frontend' + ]}, 'tutorials/examples/xcc', - 'tutorials/examples/advanced-xcc', + // 'tutorials/examples/advanced-xcc', 'tutorials/examples/global-contracts', 'tutorials/examples/update-contract-migrate-state', {