diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8476c59..4559adb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -52,15 +52,33 @@ function sidebarGuide() { { text: "Introduction", collapsible: false, - items: [{ text: "Getting Started", link: "/getting-started" }] + items: [ + { text: "Getting Started", link: "/getting-started" }, + { text: "Installation", link: "/installation" }, + { text: "Core Concepts", link: "/core-concepts" } + ] + }, + { + text: "Tutorial", + collapsible: false, + items: [ + { text: "Build Your First dApp", link: "/tutorial" } + ] }, { text: "Basic Usage", collapsible: false, items: [ - { text: "Wallet interaction", link: "/wallet-interaction" }, - { text: "Transaction building", link: "/transaction-building" }, - { text: "Token burning", link: "/token-burning" } + { text: "Wallet Interaction", link: "/wallet-interaction" }, + { text: "Transaction Building", link: "/transaction-building" }, + { text: "Token Burning", link: "/token-burning" } + ] + }, + { + text: "Advanced", + collapsible: false, + items: [ + { text: "Contract Signing", link: "/contract-signing" } ] } ]; diff --git a/docs/contract-signing.md b/docs/contract-signing.md new file mode 100644 index 0000000..93383d7 --- /dev/null +++ b/docs/contract-signing.md @@ -0,0 +1,356 @@ +# Contract Signing + +When working with smart contracts on Ergo, you'll encounter situations where the boxes you want to spend are protected by contracts, not just simple P2PK addresses. This guide covers how to handle transaction signatures for these cases. + +## Understanding Contract-Protected Boxes + +### P2PK vs Contract Boxes + +| Type | Protection | Signing | +|------|------------|---------| +| **P2PK** | Public key (wallet address) | Wallet can sign directly | +| **Contract** | ErgoScript conditions | Requires context/proof satisfaction | + +```typescript +// P2PK box - protected by an address +const p2pkBox = { + ergoTree: "0008cd03...", // Starts with 0008cd = P2PK + value: "1000000000" +}; + +// Contract box - protected by a script +const contractBox = { + ergoTree: "100604...", // Complex ErgoTree + value: "1000000000" +}; +``` + +## The Challenge + +When input boxes are protected by contracts (not in your wallet), the standard wallet signing won't work because: + +1. Wallet only holds keys for your addresses +2. Contract boxes require satisfying specific conditions +3. Some contracts need external context data + +## Signing Strategies + +### Strategy 1: Using Context Extension + +For contracts that require specific context values: + +```typescript +import { TransactionBuilder, OutputBuilder } from "@fleet-sdk/core"; + +// Contract expects values in context extension +const unsignedTx = new TransactionBuilder(height) + .from(contractBoxes) + .to(/* outputs */) + .configure((settings) => { + settings.setContextExtension({ + 0: "0e2001020304", // Extension index 0 with some value + 1: "04c801" // Extension index 1 with another value + }); + }) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); +``` + +### Strategy 2: Multi-Signature Transactions + +For contracts requiring multiple signatures: + +```typescript +import { TransactionBuilder, OutputBuilder } from "@fleet-sdk/core"; +import { Prover } from "@fleet-sdk/wallet"; + +// Build the transaction +const unsignedTx = new TransactionBuilder(height) + .from(multiSigBoxes) + .to( + new OutputBuilder(SAFE_MIN_BOX_VALUE, recipientAddress) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + +// First signer signs +const partiallySignedTx = await firstProver.sign(unsignedTx); + +// Second signer completes the signature +const fullySignedTx = await secondProver.sign(partiallySignedTx); +``` + +### Strategy 3: Combining Wallet and Contract Boxes + +When mixing P2PK boxes (from wallet) with contract boxes: + +```typescript +import { TransactionBuilder, OutputBuilder } from "@fleet-sdk/core"; + +// Get wallet boxes +const walletBoxes = await ergo.get_utxos(); + +// Contract box (from explorer/dApp state) +const contractBox = { + boxId: "abc123...", + value: "1000000000", + ergoTree: "100604...", // Contract script + // ... other properties +}; + +// Build transaction with both types +const unsignedTx = new TransactionBuilder(height) + .from([...walletBoxes, contractBox]) + .to(/* outputs */) + .sendChangeTo(changeAddress) + .payMinFee() + .build() + .toEIP12Object(); + +// Wallet signs what it can +const signedTx = await ergo.sign_tx(unsignedTx); +``` + +### Strategy 4: Prover for Local Signing + +Using `@fleet-sdk/wallet` for full control: + +```typescript +import { Prover, BIP32Path } from "@fleet-sdk/wallet"; +import { Mnemonic } from "@fleet-sdk/crypto"; + +// Derive signing key +const mnemonic = Mnemonic.fromPhrase("your mnemonic phrase..."); +const rootKey = mnemonic.toHDKey(); +const signingKey = rootKey.derive(BIP32Path.fromString("m/44'/429'/0'/0/0")); + +// Create prover with the key +const prover = new Prover(signingKey); + +// Sign the transaction +const signedTx = prover.sign(unsignedTx, { + inputBoxes: allInputBoxes, + dataInputBoxes: dataBoxes +}); +``` + +## Common Contract Patterns + +### Pattern 1: Time-Locked Boxes + +Boxes that become spendable after a certain height: + +```typescript +// Contract: HEIGHT > unlockHeight && proveDlog(pk) +const timeLockBox = await fetchTimeLockBox(); + +// Only works if current height > unlockHeight +const unsignedTx = new TransactionBuilder(currentHeight) + .from([timeLockBox, ...walletBoxes]) + .to(/* outputs */) + .build(); + +// Wallet can sign the proveDlog part +const signedTx = await ergo.sign_tx(unsignedTx.toEIP12Object()); +``` + +### Pattern 2: Oracle Data Contracts + +Contracts that reference oracle data: + +```typescript +// Contract: CONTEXT.dataInputs(0).R4[Long].get == expectedValue + +const oracleBox = await fetchOracleBox(); + +const unsignedTx = new TransactionBuilder(height) + .from(contractBoxes) + .withDataFrom([oracleBox]) // Oracle as data input + .to(/* outputs */) + .build(); +``` + +### Pattern 3: Threshold Signatures + +Multi-party contracts (e.g., 2-of-3): + +```typescript +import { TransactionBuilder } from "@fleet-sdk/core"; +import { Prover } from "@fleet-sdk/wallet"; + +// Build transaction +const unsignedTx = new TransactionBuilder(height) + .from(thresholdBox) + .to(/* outputs */) + .build(); + +// Collect required number of signatures +const hints = []; + +// Party 1 signs and creates hint +const party1Signed = await party1Prover.signWithHints(unsignedTx); +hints.push(party1Signed.hint); + +// Party 2 uses hint and completes +const fullySignedTx = await party2Prover.signWithHints( + unsignedTx, + { previousHints: hints } +); +``` + +## Using dApp Connector with Contract Boxes + +### Partial Signing + +Some wallets support partial signing for contract boxes: + +```typescript +// Tell wallet which inputs belong to it +const unsignedTx = new TransactionBuilder(height) + .from([ + { ...walletBox, inputIndex: 0 }, + { ...contractBox, inputIndex: 1 } + ]) + .to(/* outputs */) + .build() + .toEIP12Object(); + +// Request partial signing for wallet-owned inputs only +const partiallySignedTx = await ergo.sign_tx(unsignedTx); + +// Contract inputs remain unsigned - handle separately +``` + +### Sign Input Data + +Provide signing hints through the EIP-12 API: + +```typescript +const signOptions = { + // Specify which inputs to sign + inputIndices: [0, 1], + // Provide context for contract evaluation + dataInputs: [oracleBox] +}; + +const signedTx = await ergo.sign_tx(unsignedTx, signOptions); +``` + +## Practical Example: DEX Swap + +Implementing a DEX swap where you interact with a liquidity pool contract: + +```typescript +import { + TransactionBuilder, + OutputBuilder, + SAFE_MIN_BOX_VALUE +} from "@fleet-sdk/core"; + +async function swapErgForToken( + poolBox: Box, // DEX pool contract box + ergAmount: bigint, // ERG to swap + minTokenOut: bigint, // Minimum tokens expected + userAddress: string +): Promise { + // Get wallet boxes for the ERG we're swapping + const walletBoxes = await ergo.get_utxos(); + const height = await ergo.get_current_height(); + + // Calculate new pool state after swap + const poolErgReserve = BigInt(poolBox.value); + const poolTokenReserve = BigInt(poolBox.assets[0].amount); + + // AMM formula: dy = y * dx / (x + dx) + const tokenOut = (poolTokenReserve * ergAmount) / (poolErgReserve + ergAmount); + + if (tokenOut < minTokenOut) { + throw new Error("Slippage too high"); + } + + // Build the swap transaction + const unsignedTx = new TransactionBuilder(height) + .from([poolBox, ...walletBoxes]) + .to([ + // New pool box with updated reserves + new OutputBuilder( + (poolErgReserve + ergAmount).toString(), + poolBox.ergoTree // Same contract + ).addTokens({ + tokenId: poolBox.assets[0].tokenId, + amount: (poolTokenReserve - tokenOut).toString() + }).setAdditionalRegisters(poolBox.additionalRegisters), + + // User receives tokens + new OutputBuilder(SAFE_MIN_BOX_VALUE, userAddress) + .addTokens({ + tokenId: poolBox.assets[0].tokenId, + amount: tokenOut.toString() + }) + ]) + .sendChangeTo(userAddress) + .payMinFee() + .build() + .toEIP12Object(); + + // Sign - wallet handles its inputs, pool contract validates conditions + const signedTx = await ergo.sign_tx(unsignedTx); + + return signedTx; +} +``` + +## Troubleshooting + +### Error: "Script verification failed" + +The contract conditions weren't satisfied: + +```typescript +// Check contract requirements: +// 1. Are all required context values provided? +// 2. Is the HEIGHT condition met? +// 3. Are outputs correctly formatted? +// 4. Are all required data-inputs included? +``` + +### Error: "Input not signed" + +Wallet can't sign contract-protected inputs: + +```typescript +// Solution: Ensure contract allows spending with provided conditions +// Some contracts are not designed to be spent by external parties +``` + +### Error: "Missing context extension" + +Contract expects context data: + +```typescript +// Add required context extension +.configure((settings) => { + settings.setContextExtension({ + // Add required context values + }); +}) +``` + +## Summary + +| Scenario | Solution | +|----------|----------| +| Time-locked boxes | Wait for unlock height, then sign normally | +| Multi-sig | Collect signatures from required parties | +| Contract + wallet boxes | Wallet signs its inputs; contract validates conditions | +| Oracle-dependent | Include oracle as data-input | +| Complex contracts | Use context extension and proper output formatting | + +## Further Reading + +- [ErgoScript Documentation](https://docs.ergoplatform.com/dev/scs/) +- [EIP-12 dApp Connector](https://github.com/ergoplatform/eips/pull/23) +- [Fleet SDK Wallet Package](https://github.com/fleet-sdk/fleet/tree/master/packages/wallet) +- [Sigma Protocols](https://docs.ergoplatform.com/dev/scs/sigma/) diff --git a/docs/core-concepts.md b/docs/core-concepts.md new file mode 100644 index 0000000..ee713a0 --- /dev/null +++ b/docs/core-concepts.md @@ -0,0 +1,357 @@ +# Core Concepts + +Before diving into Fleet SDK, it's essential to understand the fundamental concepts of the Ergo blockchain. This chapter covers the core building blocks that make Ergo unique. + +## The UTXO Model + +Ergo uses an **Extended UTXO (eUTXO)** model, which is an evolution of Bitcoin's original UTXO design. Understanding this model is crucial for building applications on Ergo. + +### What is UTXO? + +**UTXO** stands for **Unspent Transaction Output**. Think of UTXOs like physical cash: + +- When you receive money, you get specific "bills" (UTXOs) +- When you spend money, you use entire bills and get change back +- You can't spend half a bill - you spend the whole thing + +``` +┌─────────────────────────────────────────────────────────┐ +│ UTXO Model │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Input UTXOs Transaction Outputs │ +│ ┌─────────┐ ┌────────┐ │ +│ │ 5 ERG │──┐ ┌──│ 3 ERG │ │ +│ └─────────┘ │ ┌──────────────┐ │ └────────┘ │ +│ ├────►│ Transaction │─────┤ │ +│ ┌─────────┐ │ └──────────────┘ │ ┌────────┐ │ +│ │ 2 ERG │──┘ └──│ 4 ERG │ │ +│ └─────────┘ └────────┘ │ +│ (change) │ +│ Total: 7 ERG Total: 7 ERG │ +│ (minus fee) │ +└─────────────────────────────────────────────────────────┘ +``` + +### UTXO vs Account Model + +| Feature | UTXO (Ergo, Bitcoin) | Account (Ethereum) | +|---------|---------------------|-------------------| +| State | Distributed across UTXOs | Stored in accounts | +| Parallelism | Excellent - no global state | Limited - sequential nonces | +| Privacy | Better - new addresses per tx | Worse - reused addresses | +| Complexity | Transaction-focused | State-focused | +| Verification | Stateless - inputs contain proof | Stateful - global state needed | + +## What is a Box? + +In Ergo, UTXOs are called **Boxes**. A Box is an extended UTXO that contains much more than just a value: + +```typescript +type Box = { + boxId: string; // Unique identifier (32 bytes, hex) + value: bigint; // Amount in nanoErg (1 ERG = 10^9 nanoErg) + ergoTree: string; // The protecting script (contract) + creationHeight: number; // Block height when created + assets: Token[]; // List of tokens in this box + additionalRegisters: { // R4-R9 for custom data + R4?: string; + R5?: string; + R6?: string; + R7?: string; + R8?: string; + R9?: string; + }; + transactionId: string; // ID of creating transaction + index: number; // Output index in creating transaction +}; +``` + +### Box Components Explained + +#### 1. Value (ERG Amount) + +Every box must contain a minimum amount of ERG (currently ~0.001 ERG) to exist on the blockchain: + +```typescript +import { SAFE_MIN_BOX_VALUE } from "@fleet-sdk/core"; + +// SAFE_MIN_BOX_VALUE = 1000000n (0.001 ERG) +const output = new OutputBuilder(SAFE_MIN_BOX_VALUE, recipientAddress); +``` + +#### 2. ErgoTree (Contract) + +The ErgoTree is a serialized script that defines who can spend this box. Common patterns: + +```typescript +// Simple P2PK (Pay to Public Key) - most common +// Anyone with the private key for this address can spend +const p2pkErgoTree = "0008cd03a621f820dbed198b42a2dca799a571911f2dabbd2e4d441c9aad558da63f084d"; + +// The ErgoTree encodes spending conditions: +// - Who can spend this box? +// - Under what conditions? +// - What data must be provided? +``` + +#### 3. Tokens (Native Assets) + +Boxes can hold multiple tokens alongside ERG: + +```typescript +type Token = { + tokenId: string; // 32-byte hex identifier + amount: bigint; // Token amount +}; + +// Example: Box holding ERG + SigUSD + NFT +const box = { + value: 1000000000n, // 1 ERG + assets: [ + { + tokenId: "03faf2cb329f2e90d6d23b58d91bbb6c046aa143261cc21f52fbe2824bfcbf04", + amount: 100n // 100 SigUSD cents + }, + { + tokenId: "unique-nft-token-id-here", + amount: 1n // NFT (quantity = 1) + } + ] +}; +``` + +#### 4. Registers (R4-R9) + +Boxes have 10 registers (R0-R9). R0-R3 are reserved: + +| Register | Purpose | Access | +|----------|---------|--------| +| R0 | Value (nanoErg) | Implicit | +| R1 | Guard script (ErgoTree) | Implicit | +| R2 | Tokens | Implicit | +| R3 | Creation info | Implicit | +| R4-R9 | User data | Custom | + +```typescript +import { OutputBuilder } from "@fleet-sdk/core"; +import { SInt, SColl, SByte } from "@fleet-sdk/serializer"; + +// Store custom data in registers +new OutputBuilder(SAFE_MIN_BOX_VALUE, address) + .setAdditionalRegisters({ + R4: SInt(42), // Integer + R5: SColl(SByte, [0x01, 0x02, 0x03]), // Byte array + R6: "0e0b48656c6c6f20576f726c64" // Raw hex (string) + }); +``` + +## Transactions + +An Ergo transaction transforms input boxes into output boxes: + +``` +┌────────────────────────────────────────────────────────────────┐ +│ ERGO TRANSACTION │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ INPUTS (boxes being spent) │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Box 1: 5 ERG + 100 SigUSD │ │ +│ │ Box 2: 2 ERG │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ DATA-INPUTS (read-only boxes, not spent) │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Oracle Box: Current ERG/USD price │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ OUTPUTS (new boxes created) │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Box A: 3 ERG (to recipient) │ │ +│ │ Box B: 50 SigUSD (to recipient) │ │ +│ │ Box C: 3.999 ERG + 50 SigUSD (change to sender) │ │ +│ │ Box D: 0.001 ERG (miner fee) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Transaction Rules + +1. **Value Conservation**: Total input value = Total output value (minus fee) +2. **Token Conservation**: Tokens in = Tokens out (unless minting/burning) +3. **Minimum Box Value**: Every output must have at least min box value +4. **Valid Signatures**: All input boxes must be properly unlocked + +### Building Transactions with Fleet + +```typescript +import { TransactionBuilder, OutputBuilder } from "@fleet-sdk/core"; + +const unsignedTx = new TransactionBuilder(creationHeight) + .from(inputBoxes) // Boxes to spend + .withDataFrom(dataInputs) // Read-only reference boxes + .to( // Create new boxes + new OutputBuilder("1000000000", recipientAddress) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); +``` + +## Token System + +Ergo has a native token system without requiring smart contracts. + +### Token Properties + +| Property | Description | +|----------|-------------| +| Token ID | First input's box ID of minting transaction | +| Amount | Number of tokens (max: 2^63 - 1) | +| Decimals | Display decimals (stored in R4) | +| Name | Human-readable name (R4) | +| Description | Token description (R5) | + +### Token Standards (EIPs) + +| Standard | Purpose | Key Features | +|----------|---------|--------------| +| [EIP-4](https://github.com/ergoplatform/eips/blob/master/eip-0004.md) | Asset Standard | Name, description, decimals | +| [EIP-24](https://github.com/ergoplatform/eips/blob/master/eip-0024.md) | Artwork Standard | NFT metadata | +| [EIP-34](https://github.com/ergoplatform/eips/blob/master/eip-0034.md) | Token Minting | Enhanced minting | + +### Minting Tokens with Fleet + +```typescript +import { TransactionBuilder, OutputBuilder } from "@fleet-sdk/core"; + +new TransactionBuilder(height) + .from(inputs) + .to( + new OutputBuilder(SAFE_MIN_BOX_VALUE, myAddress) + .mintToken({ + amount: "1000000", + name: "MyToken", + decimals: 2, + description: "My first token on Ergo" + }) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); +``` + +## Addresses + +Ergo addresses encode the spending conditions (ErgoTree) in a human-readable format. + +### Address Types + +| Prefix | Network | Type | Example Start | +|--------|---------|------|---------------| +| 9 | Mainnet | P2PK | `9f4QF...` | +| 3 | Testnet | P2PK | `3WwbY...` | +| 8 | Mainnet | P2S | `8UApt...` | +| ? | Testnet | P2S | Various | + +### Address Components + +```typescript +import { ErgoAddress } from "@fleet-sdk/core"; + +const address = ErgoAddress.fromBase58( + "9gNvAv97W71Wm33GoXgSQBFJxinFubKvE6wh2dEhFTSgYEe783j" +); + +// Get the ErgoTree (contract) from address +const ergoTree = address.ergoTree; + +// Check network +const isMainnet = address.network === "mainnet"; +``` + +## ErgoScript Basics + +ErgoScript is the smart contract language for Ergo. While Fleet SDK is for off-chain code, understanding ErgoScript helps you work with contracts. + +### Simple Contract Examples + +```scala +// Anyone can spend (not recommended!) +{ true } + +// Only owner can spend (P2PK) +{ proveDlog(ownerPubKey) } + +// Multi-signature (2 of 3) +{ atLeast(2, Coll(pk1, pk2, pk3)) } + +// Time-locked +{ HEIGHT > 1000000 && proveDlog(ownerPubKey) } + +// Value threshold +{ OUTPUTS(0).value >= 1000000000L } +``` + +### Working with Contracts in Fleet + +```typescript +import { compile } from "@fleet-sdk/compiler"; +import { OutputBuilder } from "@fleet-sdk/core"; + +// Compile ErgoScript to ErgoTree +const ergoTree = compile("{ sigmaProp(true) }"); + +// Create output with custom contract +const output = new OutputBuilder(SAFE_MIN_BOX_VALUE, ergoTree); +``` + +## Block and Network Concepts + +### Block Height + +Block height is the number of blocks since genesis. Used for: + +- Transaction creation height (required) +- Time-locking conditions +- Context reference + +```typescript +// Get current height from wallet +const height = await ergo.get_current_height(); + +// Use in transaction +new TransactionBuilder(height) + .from(inputs) + // ... +``` + +### Network Types + +| Network | Purpose | Explorer | +|---------|---------|----------| +| Mainnet | Production | [explorer.ergoplatform.com](https://explorer.ergoplatform.com) | +| Testnet | Development | [testnet.ergoplatform.com](https://testnet.ergoplatform.com) | + +## Summary + +| Concept | Description | Fleet SDK Usage | +|---------|-------------|-----------------| +| Box | Extended UTXO with value, tokens, registers | `OutputBuilder` creates boxes | +| Transaction | Transforms inputs to outputs | `TransactionBuilder` | +| Token | Native asset in boxes | `addTokens()`, `mintToken()` | +| ErgoTree | Spending conditions/contract | Encoded in addresses | +| Registers | Custom data storage (R4-R9) | `setAdditionalRegisters()` | +| Height | Block number | Required for `TransactionBuilder` | + +## Next Steps + +Now that you understand the core concepts: + +1. **[Transaction Building](/transaction-building)** - Create your first transaction +2. **[Wallet Interaction](/wallet-interaction)** - Connect to user wallets +3. **[Token Operations](/token-burning)** - Work with tokens diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..25b0dd6 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,348 @@ +# Installation Guide + +This comprehensive guide will take you from a fresh installation to running your first Fleet SDK example scripts. + +## Prerequisites + +Before installing Fleet SDK, ensure you have the following: + +### Node.js + +Fleet SDK requires **Node.js 18.0.0 or higher**. Check your version: + +```bash +node --version +``` + +If you don't have Node.js installed or need to upgrade: + +- **macOS/Linux**: Use [nvm](https://github.com/nvm-sh/nvm) (recommended) + ```bash + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash + nvm install 20 + nvm use 20 + ``` + +- **Windows**: Download from [nodejs.org](https://nodejs.org/) or use [nvm-windows](https://github.com/coreybutler/nvm-windows) + +- **Using Volta** (cross-platform): + ```bash + curl https://get.volta.sh | bash + volta install node@20 + ``` + +### Package Manager + +You can use any of these package managers: + +| Package Manager | Version | Install Command | +|-----------------|---------|-----------------| +| npm | 8.0.0+ | Included with Node.js | +| yarn | 1.22.0+ | `npm install -g yarn` | +| pnpm | 8.0.0+ | `npm install -g pnpm` | +| bun | 1.0.0+ | `curl -fsSL https://bun.sh/install \| bash` | + +## Installation Methods + +### Method 1: Quick Start (Recommended for beginners) + +Create a new project and install Fleet SDK in one go: + +::: code-group + +```bash [npm] +mkdir my-ergo-dapp +cd my-ergo-dapp +npm init -y +npm install @fleet-sdk/core +``` + +```bash [yarn] +mkdir my-ergo-dapp +cd my-ergo-dapp +yarn init -y +yarn add @fleet-sdk/core +``` + +```bash [pnpm] +mkdir my-ergo-dapp +cd my-ergo-dapp +pnpm init +pnpm add @fleet-sdk/core +``` + +```bash [bun] +mkdir my-ergo-dapp +cd my-ergo-dapp +bun init +bun add @fleet-sdk/core +``` + +::: + +### Method 2: Add to Existing Project + +If you already have a project, simply add Fleet SDK: + +::: code-group + +```bash [npm] +npm install @fleet-sdk/core +``` + +```bash [yarn] +yarn add @fleet-sdk/core +``` + +```bash [pnpm] +pnpm add @fleet-sdk/core +``` + +```bash [bun] +bun add @fleet-sdk/core +``` + +::: + +### Method 3: Full Fleet SDK Installation + +For advanced use cases, install all Fleet SDK packages: + +::: code-group + +```bash [npm] +npm install @fleet-sdk/core @fleet-sdk/wallet @fleet-sdk/common @fleet-sdk/crypto @fleet-sdk/serializer @fleet-sdk/blockchain-providers +``` + +```bash [yarn] +yarn add @fleet-sdk/core @fleet-sdk/wallet @fleet-sdk/common @fleet-sdk/crypto @fleet-sdk/serializer @fleet-sdk/blockchain-providers +``` + +```bash [pnpm] +pnpm add @fleet-sdk/core @fleet-sdk/wallet @fleet-sdk/common @fleet-sdk/crypto @fleet-sdk/serializer @fleet-sdk/blockchain-providers +``` + +::: + +## Fleet SDK Packages Overview + +| Package | Description | When to Use | +|---------|-------------|-------------| +| `@fleet-sdk/core` | Transaction building, outputs, inputs | Always - core functionality | +| `@fleet-sdk/wallet` | Wallet operations, signing | Local wallet/signing needs | +| `@fleet-sdk/common` | Types, constants, utilities | Shared types across packages | +| `@fleet-sdk/crypto` | Cryptographic operations | Hashing, key derivation | +| `@fleet-sdk/serializer` | Sigma serialization | Box/register encoding | +| `@fleet-sdk/blockchain-providers` | Blockchain API integration | Fetching chain data | +| `@fleet-sdk/compiler` | ErgoScript compilation | Smart contract development | +| `@fleet-sdk/mock-chain` | Testing utilities | Unit/integration testing | + +## TypeScript Configuration + +For TypeScript projects, configure your `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "lib": ["ES2020", "DOM"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Your First Script + +### Step 1: Create the script file + +Create a new file called `first-transaction.ts`: + +```typescript +import { + TransactionBuilder, + OutputBuilder, + SAFE_MIN_BOX_VALUE +} from "@fleet-sdk/core"; + +// Example: Build a simple transaction structure +function buildSimpleTransaction() { + // Sample input box (in real apps, fetch from wallet or explorer) + const mockInput = { + boxId: "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e", + value: "1000000000", // 1 ERG in nanoErg + assets: [], + ergoTree: "0008cd03a621f820dbed198b42a2dca799a571911f2dabbd2e4d441c9aad558da63f084d", + creationHeight: 1000000, + additionalRegisters: {}, + index: 0, + transactionId: "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9" + }; + + const recipientAddress = "9gNvAv97W71Wm33GoXgSQBFJxinFubKvE6wh2dEhFTSgYEe783j"; + const changeAddress = "9gNvAv97W71Wm33GoXgSQBFJxinFubKvE6wh2dEhFTSgYEe783j"; + + // Build the transaction + const unsignedTx = new TransactionBuilder(1000000) + .from([mockInput]) + .to( + new OutputBuilder(SAFE_MIN_BOX_VALUE, recipientAddress) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + + console.log("✅ Transaction built successfully!"); + console.log("Inputs:", unsignedTx.inputs.length); + console.log("Outputs:", unsignedTx.outputs.length); + + return unsignedTx; +} + +// Run it! +buildSimpleTransaction(); +``` + +### Step 2: Run with ts-node + +Install ts-node and run: + +```bash +npm install -D ts-node typescript @types/node +npx ts-node first-transaction.ts +``` + +### Step 3: Expected output + +``` +✅ Transaction built successfully! +Inputs: 1 +Outputs: 2 +``` + +## Browser Integration + +Fleet SDK works in browsers too! Here's a minimal HTML setup: + +```html + + + + Fleet SDK Demo + + +

Fleet SDK Browser Demo

+ +
+ + + + +``` + +## Framework Integration + +### React / Next.js + +```bash +npx create-next-app@latest my-ergo-app --typescript +cd my-ergo-app +npm install @fleet-sdk/core +``` + +### Vite + +```bash +npm create vite@latest my-ergo-app -- --template vanilla-ts +cd my-ergo-app +npm install @fleet-sdk/core +``` + +### Node.js Backend + +```bash +mkdir ergo-backend +cd ergo-backend +npm init -y +npm install @fleet-sdk/core @fleet-sdk/blockchain-providers +npm install -D typescript ts-node @types/node +``` + +## Troubleshooting + +### Common Issues + +**1. "Cannot find module '@fleet-sdk/core'"** +```bash +# Ensure you're in the right directory +pwd +# Reinstall dependencies +rm -rf node_modules package-lock.json +npm install +``` + +**2. TypeScript errors with BigInt** +Add to `tsconfig.json`: +```json +{ + "compilerOptions": { + "lib": ["ES2020", "DOM"] + } +} +``` + +**3. ESM/CommonJS conflicts** +Add to `package.json`: +```json +{ + "type": "module" +} +``` + +**4. Browser wallet not detected** +- Ensure Nautilus or SAFEW extension is installed +- Check the page is served via HTTP (not file://) +- Wallet extensions need time to inject - use `window.onload` + +## Next Steps + +Now that you have Fleet SDK installed: + +1. **[Getting Started](/getting-started)** - Learn the basics +2. **[Core Concepts](/core-concepts)** - Understand Ergo's UTXO model +3. **[Transaction Building](/transaction-building)** - Build your first real transaction +4. **[Wallet Interaction](/wallet-interaction)** - Connect to user wallets + +## Version Compatibility + +| Fleet SDK | Node.js | TypeScript | +|-----------|---------|------------| +| 0.6.x | ≥18.0.0 | ≥4.7.0 | +| 0.5.x | ≥16.0.0 | ≥4.5.0 | +| 0.4.x | ≥14.0.0 | ≥4.3.0 | diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..52930b6 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,812 @@ +# Tutorial: Building Your First Ergo dApp + +This step-by-step tutorial will guide you through building a complete Ergo dApp using Fleet SDK. By the end, you'll have a working application that can: + +- Connect to a wallet +- Display balances +- Build and send transactions +- Mint tokens + +## Prerequisites + +- Node.js 18+ installed ([Installation Guide](/installation)) +- A browser wallet (Nautilus recommended) +- Basic TypeScript knowledge +- Testnet ERG for testing (get from [faucet](https://testnet-faucet.ergoplatform.com)) + +## Part 1: Project Setup + +### Step 1: Create the project + +```bash +mkdir ergo-dapp-tutorial +cd ergo-dapp-tutorial +npm init -y +npm install @fleet-sdk/core @fleet-sdk/common +npm install -D typescript vite +``` + +### Step 2: Configure TypeScript + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} +``` + +### Step 3: Create project structure + +```bash +mkdir src +touch src/main.ts src/wallet.ts src/transactions.ts +touch index.html +``` + +## Part 2: Wallet Connection + +### Step 1: Create the wallet module + +Create `src/wallet.ts`: + +```typescript +// Types for the EIP-12 wallet API +declare global { + interface Window { + ergoConnector: { + nautilus: { + connect: () => Promise; + isConnected: () => Promise; + }; + }; + ergo: { + get_balance: (tokenId?: string) => Promise; + get_change_address: () => Promise; + get_used_addresses: () => Promise; + get_utxos: (params?: any) => Promise; + get_current_height: () => Promise; + sign_tx: (tx: any) => Promise; + submit_tx: (tx: any) => Promise; + }; + } +} + +export interface Box { + boxId: string; + value: string; + assets: { tokenId: string; amount: string }[]; + ergoTree: string; + creationHeight: number; + additionalRegisters: Record; + transactionId: string; + index: number; +} + +export interface WalletState { + connected: boolean; + balance: string; + address: string; + height: number; +} + +export class WalletManager { + private state: WalletState = { + connected: false, + balance: "0", + address: "", + height: 0 + }; + + async connect(): Promise { + // Check if wallet is available + if (typeof window.ergoConnector === "undefined") { + throw new Error("No wallet found. Please install Nautilus Wallet."); + } + + if (!window.ergoConnector.nautilus) { + throw new Error("Nautilus Wallet not found."); + } + + // Request connection + const connected = await window.ergoConnector.nautilus.connect(); + + if (connected) { + this.state.connected = true; + await this.refreshState(); + } + + return connected; + } + + async refreshState(): Promise { + if (!this.state.connected) { + throw new Error("Wallet not connected"); + } + + // Fetch all wallet state in parallel + const [balance, address, height] = await Promise.all([ + window.ergo.get_balance("ERG"), + window.ergo.get_change_address(), + window.ergo.get_current_height() + ]); + + this.state = { + connected: true, + balance, + address, + height + }; + + return this.state; + } + + async getBoxes(amount?: string): Promise { + if (!this.state.connected) { + throw new Error("Wallet not connected"); + } + + const boxes = await window.ergo.get_utxos( + amount ? { amount } : undefined + ); + + return boxes; + } + + async signAndSubmit(unsignedTx: any): Promise { + // Sign the transaction + const signedTx = await window.ergo.sign_tx(unsignedTx); + + // Submit to network + const txId = await window.ergo.submit_tx(signedTx); + + return txId; + } + + getState(): WalletState { + return { ...this.state }; + } + + formatErg(nanoErg: string): string { + const erg = Number(nanoErg) / 1_000_000_000; + return erg.toFixed(4); + } +} +``` + +## Part 3: Transaction Building + +### Step 1: Create the transactions module + +Create `src/transactions.ts`: + +```typescript +import { + TransactionBuilder, + OutputBuilder, + SAFE_MIN_BOX_VALUE +} from "@fleet-sdk/core"; +import type { Box } from "./wallet"; + +export interface TransactionResult { + unsignedTx: any; + fee: string; + inputs: number; + outputs: number; +} + +/** + * Build a simple ERG transfer transaction + */ +export function buildTransfer( + inputs: Box[], + recipientAddress: string, + amountNanoErg: string, + changeAddress: string, + height: number +): TransactionResult { + const tx = new TransactionBuilder(height) + .from(inputs) + .to( + new OutputBuilder(amountNanoErg, recipientAddress) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + + return { + unsignedTx: tx.toEIP12Object(), + fee: "1100000", + inputs: tx.inputs.length, + outputs: tx.outputs.length + }; +} + +/** + * Build a token transfer transaction + */ +export function buildTokenTransfer( + inputs: Box[], + recipientAddress: string, + tokenId: string, + tokenAmount: string, + changeAddress: string, + height: number +): TransactionResult { + const tx = new TransactionBuilder(height) + .from(inputs) + .to( + new OutputBuilder(SAFE_MIN_BOX_VALUE, recipientAddress) + .addTokens({ tokenId, amount: tokenAmount }) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + + return { + unsignedTx: tx.toEIP12Object(), + fee: "1100000", + inputs: tx.inputs.length, + outputs: tx.outputs.length + }; +} + +/** + * Build a token minting transaction + */ +export function buildMintToken( + inputs: Box[], + mintToAddress: string, + tokenName: string, + tokenDescription: string, + tokenAmount: string, + decimals: number, + changeAddress: string, + height: number +): TransactionResult { + const tx = new TransactionBuilder(height) + .from(inputs) + .to( + new OutputBuilder(SAFE_MIN_BOX_VALUE, mintToAddress) + .mintToken({ + name: tokenName, + description: tokenDescription, + amount: tokenAmount, + decimals + }) + ) + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + + return { + unsignedTx: tx.toEIP12Object(), + fee: "1100000", + inputs: tx.inputs.length, + outputs: tx.outputs.length + }; +} + +/** + * Build a multi-output transaction (send to multiple recipients) + */ +export function buildMultiTransfer( + inputs: Box[], + recipients: { address: string; amount: string }[], + changeAddress: string, + height: number +): TransactionResult { + let builder = new TransactionBuilder(height).from(inputs); + + // Add each recipient as an output + for (const recipient of recipients) { + builder = builder.to( + new OutputBuilder(recipient.amount, recipient.address) + ); + } + + const tx = builder + .sendChangeTo(changeAddress) + .payMinFee() + .build(); + + return { + unsignedTx: tx.toEIP12Object(), + fee: "1100000", + inputs: tx.inputs.length, + outputs: tx.outputs.length + }; +} +``` + +## Part 4: Building the UI + +### Step 1: Create the HTML + +Create `index.html`: + +```html + + + + + + Ergo dApp Tutorial + + + +

🚀 Ergo dApp Tutorial

+ + +
+

Connect Wallet

+ + +
+ + + + + + + + + + + + + +``` + +### Step 2: Create the main application + +Create `src/main.ts`: + +```typescript +import { WalletManager } from "./wallet"; +import { buildTransfer, buildMintToken } from "./transactions"; + +// Initialize wallet manager +const wallet = new WalletManager(); + +// DOM Elements +const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement; +const connectError = document.getElementById("connect-error") as HTMLParagraphElement; +const connectSection = document.getElementById("connect-section") as HTMLDivElement; +const walletSection = document.getElementById("wallet-section") as HTMLDivElement; +const sendSection = document.getElementById("send-section") as HTMLDivElement; +const mintSection = document.getElementById("mint-section") as HTMLDivElement; + +const balanceEl = document.getElementById("balance") as HTMLParagraphElement; +const addressEl = document.getElementById("address") as HTMLParagraphElement; +const heightEl = document.getElementById("height") as HTMLSpanElement; +const refreshBtn = document.getElementById("refresh-btn") as HTMLButtonElement; + +const recipientInput = document.getElementById("recipient") as HTMLInputElement; +const amountInput = document.getElementById("amount") as HTMLInputElement; +const sendBtn = document.getElementById("send-btn") as HTMLButtonElement; +const sendResult = document.getElementById("send-result") as HTMLParagraphElement; + +const tokenNameInput = document.getElementById("token-name") as HTMLInputElement; +const tokenDescInput = document.getElementById("token-desc") as HTMLInputElement; +const tokenAmountInput = document.getElementById("token-amount") as HTMLInputElement; +const tokenDecimalsInput = document.getElementById("token-decimals") as HTMLInputElement; +const mintBtn = document.getElementById("mint-btn") as HTMLButtonElement; +const mintResult = document.getElementById("mint-result") as HTMLParagraphElement; + +// Update UI with wallet state +function updateUI(): void { + const state = wallet.getState(); + + if (state.connected) { + connectSection.classList.add("hidden"); + walletSection.classList.remove("hidden"); + sendSection.classList.remove("hidden"); + mintSection.classList.remove("hidden"); + + balanceEl.textContent = `${wallet.formatErg(state.balance)} ERG`; + addressEl.textContent = state.address; + heightEl.textContent = state.height.toString(); + } +} + +// Connect wallet +connectBtn.addEventListener("click", async () => { + try { + connectBtn.disabled = true; + connectBtn.textContent = "Connecting..."; + connectError.classList.add("hidden"); + + await wallet.connect(); + updateUI(); + } catch (error) { + connectError.textContent = (error as Error).message; + connectError.classList.remove("hidden"); + } finally { + connectBtn.disabled = false; + connectBtn.textContent = "Connect Nautilus"; + } +}); + +// Refresh wallet state +refreshBtn.addEventListener("click", async () => { + try { + refreshBtn.disabled = true; + await wallet.refreshState(); + updateUI(); + } finally { + refreshBtn.disabled = false; + } +}); + +// Send ERG +sendBtn.addEventListener("click", async () => { + try { + sendBtn.disabled = true; + sendBtn.textContent = "Building..."; + sendResult.classList.add("hidden"); + + const recipient = recipientInput.value.trim(); + const amountErg = parseFloat(amountInput.value); + + if (!recipient || !amountErg) { + throw new Error("Please fill in all fields"); + } + + // Convert ERG to nanoErg + const amountNanoErg = Math.floor(amountErg * 1_000_000_000).toString(); + + // Get current state + const state = wallet.getState(); + + // Get input boxes + const boxes = await wallet.getBoxes(amountNanoErg); + + if (boxes.length === 0) { + throw new Error("Insufficient funds"); + } + + // Build transaction + const result = buildTransfer( + boxes, + recipient, + amountNanoErg, + state.address, + state.height + ); + + sendBtn.textContent = "Signing..."; + + // Sign and submit + const txId = await wallet.signAndSubmit(result.unsignedTx); + + sendResult.className = "success"; + sendResult.innerHTML = `✅ Transaction submitted!
+ View on Explorer`; + sendResult.classList.remove("hidden"); + + // Refresh balance + await wallet.refreshState(); + updateUI(); + + } catch (error) { + sendResult.className = "error"; + sendResult.textContent = `❌ ${(error as Error).message}`; + sendResult.classList.remove("hidden"); + } finally { + sendBtn.disabled = false; + sendBtn.textContent = "Send ERG"; + } +}); + +// Mint Token +mintBtn.addEventListener("click", async () => { + try { + mintBtn.disabled = true; + mintBtn.textContent = "Building..."; + mintResult.classList.add("hidden"); + + const name = tokenNameInput.value.trim(); + const description = tokenDescInput.value.trim(); + const amount = tokenAmountInput.value; + const decimals = parseInt(tokenDecimalsInput.value) || 0; + + if (!name || !amount) { + throw new Error("Please fill in token name and amount"); + } + + // Get current state + const state = wallet.getState(); + + // Get input boxes (need some ERG for the transaction) + const boxes = await wallet.getBoxes(); + + if (boxes.length === 0) { + throw new Error("Insufficient funds"); + } + + // Build transaction + const result = buildMintToken( + boxes, + state.address, // Mint to own address + name, + description, + amount, + decimals, + state.address, + state.height + ); + + mintBtn.textContent = "Signing..."; + + // Sign and submit + const txId = await wallet.signAndSubmit(result.unsignedTx); + + mintResult.className = "success"; + mintResult.innerHTML = `✅ Token minted!
+ View on Explorer`; + mintResult.classList.remove("hidden"); + + // Refresh balance + await wallet.refreshState(); + updateUI(); + + } catch (error) { + mintResult.className = "error"; + mintResult.textContent = `❌ ${(error as Error).message}`; + mintResult.classList.remove("hidden"); + } finally { + mintBtn.disabled = false; + mintBtn.textContent = "Mint Token"; + } +}); + +// Check if already connected on page load +window.addEventListener("load", async () => { + if (typeof window.ergoConnector !== "undefined" && + window.ergoConnector.nautilus) { + const connected = await window.ergoConnector.nautilus.isConnected(); + if (connected) { + await wallet.connect(); + updateUI(); + } + } +}); +``` + +## Part 5: Running the Application + +### Step 1: Start the development server + +```bash +npx vite +``` + +### Step 2: Open in browser + +Navigate to `http://localhost:5173` and: + +1. Click "Connect Nautilus" +2. Approve the connection in your wallet +3. Try sending ERG or minting a token! + +## Part 6: Advanced Patterns + +### Pattern 1: Handling Transaction Errors + +```typescript +async function safeTransactionSubmit(unsignedTx: any): Promise { + try { + const signedTx = await window.ergo.sign_tx(unsignedTx); + const txId = await window.ergo.submit_tx(signedTx); + return txId; + } catch (error: any) { + // User rejected signing + if (error.code === 1) { + console.log("User cancelled transaction"); + return null; + } + // Insufficient funds + if (error.message?.includes("insufficient")) { + throw new Error("Insufficient funds for this transaction"); + } + // Re-throw unknown errors + throw error; + } +} +``` + +### Pattern 2: Waiting for Confirmation + +```typescript +async function waitForConfirmation( + txId: string, + timeout: number = 60000 +): Promise { + const startTime = Date.now(); + const explorerApi = "https://api.ergoplatform.com/api/v1"; + + while (Date.now() - startTime < timeout) { + try { + const response = await fetch( + `${explorerApi}/transactions/${txId}` + ); + + if (response.ok) { + const data = await response.json(); + if (data.numConfirmations > 0) { + return true; + } + } + } catch { + // Ignore fetch errors, keep polling + } + + // Wait 5 seconds before next check + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + return false; +} +``` + +### Pattern 3: Box Selection Strategy + +```typescript +import { + TransactionBuilder, + OutputBuilder, + BoxSelector +} from "@fleet-sdk/core"; + +// Use box selector for precise control +const selector = new BoxSelector(inputBoxes); +const selectedBoxes = selector.select({ + target: { + nanoErgs: BigInt(amountToSend) + BigInt(fee) + } +}); + +new TransactionBuilder(height) + .from(selectedBoxes) + .to(/* outputs */) + .build(); +``` + +## Exercises + +To reinforce your learning, try these exercises: + +### Exercise 1: Add Token Balance Display +Modify the UI to display all tokens the wallet holds, not just ERG. + +### Exercise 2: Multi-Recipient Transaction +Implement a form that allows sending ERG to multiple addresses at once. + +### Exercise 3: Transaction History +Use the Explorer API to display recent transactions for the connected wallet. + +### Exercise 4: Token Transfer +Add functionality to send existing tokens (not just mint new ones). + +## Summary + +In this tutorial, you learned how to: + +✅ Set up a Fleet SDK project +✅ Connect to a browser wallet +✅ Read wallet state (balance, address, UTXOs) +✅ Build transfer transactions +✅ Mint new tokens +✅ Sign and submit transactions +✅ Handle errors gracefully + +## Next Steps + +- **[Transaction Building](/transaction-building)** - Deep dive into transaction building +- **[Wallet Interaction](/wallet-interaction)** - Complete EIP-12 reference +- **[Token Operations](/token-burning)** - Learn about burning tokens +- **[Core Concepts](/core-concepts)** - Understand Ergo's architecture