Towns 백서

Towns

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 17쪽 중 3쪽

allet** (`bot.viem.account`) - Signs and pays for transactions * **App address** (`bot.appAddress`) - Treasury wallet (SimpleAccount) <Note> Your gas wallet needs Base ETH for transaction fees. See [Getting Started](/build/bots/getting-started#understanding-your-bots-wallet-architecture) for details. </Note> ## Configuration ### Base RPC URL For reliable blockchain interactions, configure a custom RPC endpoint. The default public RPC has strict rate limits. ```typescript theme={null} const bot = await makeTownsBot( process.env.APP_PRIVATE_DATA!, process.env.JWT_SECRET!, { baseRpcUrl: process.env.BASE_RPC_URL, commands } ) ``` Add to `.env`: ```bash theme={null} BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY ``` We recommend getting a RPC URL from a reputable provider, like [Alchemy](https://www.alchemy.com/). ## Bot Contract Interactions ### Reading from Contracts Read contract state without gas costs: ```ts theme={null} import { readContract } from 'viem/actions' import simpleAppAbi from '@towns-protocol/bot/simpleAppAbi' const owner = await readContract(bot.viem, { address: bot.appAddress, abi: simpleAppAbi, functionName: 'moduleOwner', args: [] }) ``` ### Writing to Contracts Use `execute` from ERC-7821 for any onchain interaction: ```ts theme={null} import { execute } from 'viem/experimental/erc7821' import { parseEther } from 'viem' import { waitForTransactionReceipt } from 'viem/actions' const hash = await execute(bot.viem, { address: bot.appAddress, account: bot.viem.account, calls: [{ to: tokenAddress, abi: erc20Abi, functionName: 'transfer', args: [recipientAddress, parseEther('1.0')] }] }) await waitForTransactionReceipt(bot.viem, { hash }) ``` ### Batch Transactions Execute multiple operations atomically: ```ts theme={null} const hash = await execute(bot.viem, { address: bot.appAddress, account: bot.viem.account, calls: [ { to: tokenAddress, abi: erc20Abi, functionName: 'approve', args: [dexAddress, amount] }, { to: dexAddress, abi: dexAbi, functionName: 'swap', args: [tokenIn, tokenOut, amount] } ] }) ``` All operations succeed or all fail. <Note> To request users to sign transactions or messages, see [Interactions](/build/bots/interactions). This page focuses on bot-initiated blockchain operations. </Note> ## Utility Functions ### getSmartAccountFromUserId Get a user's smart account address: ```ts theme={null} import { getSmartAccountFromUserId } from '@towns-protocol/bot' const wallet = await getSmartAccountFromUserId(bot, { userId: event.userId }) if (wallet) { // Send tokens, check balances, etc. } ``` Returns `null` if no smart account exists. **Use cases:** * Send tokens/NFTs to users * Airdrop rewards * Check balances * Verify ownership ## Method Selection Guide | Task | Method | Learn More | | ------------------------------ | -------------------------------------- | ---------------------------------------- | | Read contract state | `readContract` | - | | Bot sends transaction | `execute` | - | | Bot sends batch transactions | `execute` with multiple calls | - | | User sends transaction | `sendInteractionRequest` (transaction) | [Interactions](/build/bots/interactions) | | User signs message | `sendInteractionRequest` (signature) | [Interactions](/build/bots/interactions) | | User clicks button/form | `sendInteractionRequest` (form) | [Interactions](/build/bots/interactions) | | Bot's SimpleAccount operations | `writeContract` | - | ## Next Steps * Create [interactions](/build/bots/interactions) with buttons and forms * Integrate [external services](/build/bots/external-interactions) via webhooks * Learn about [slash commands](/build/bots/slash-commands) * Explore [event handlers](/build/bots/events) # Roles Source: https://docs.towns.com/build/bots/roles Bots can create and manage roles in Towns spaces. Roles control what users can do - from reading messages to banning members. You can also create token-gated roles that require users to hold specific NFTs or tokens. <Note> Role management requires your bot to have admin permissions in the space. All role operations are onchain transactions that require gas. </Note> ## Creating Roles ```ts theme={null} import { Permission } from '@towns-protocol/web3' const { roleId } = await bot.createRole(spaceId, { name: 'Moderator', permissions: [Permission.Read, Permission.Write, Permission.ModifyBanning], users: ['0x...', '0x...'] // optional: only assign to specific users }) ``` ### CreateRoleParams | Parameter | Type | Description | | ------------- | -------------- | -------------------------------------------- | | `name` | `string` | Display name for the role | | `permissions` | `Permission[]` | Array of permissions to grant | | `users` | `string[]` | (Optional) User addresses to assign the role | | `rule` | `Operation` | (Optional) Token-gating rule (see below) | ## Token-Gated Roles Create roles that require users to hold specific tokens using the `Rules` API: ```ts theme={null} import { Permission, Rules } from '@towns-protocol/web3' const townsHolderRule = Rules.checkErc20({ chainId: 8453n, contractAddress: '0x00000000A22C618fd6b4D7E9A335C4B96B189a38', threshold: 1n }) const role = await bot.createRole(spaceId, { name: 'Towns Holder', permissions: [Permission.Read, Permission.Write], rule: townsHolderRule }) ``` You can apply the role to a channel by calling `bot.addRoleToChannel(channelId, roleId)`. This will gate the channel to only users who hold the required tokens. ```ts theme={null} await bot.addRoleToChannel(channelId, role.roleId) ``` ### Available Rule Types | Rule | Description | | ---------------------------------------------------------------------- | --------------------------------------------- | | `Rules.checkErc721({ chainId, contractAddress, threshold })` | Check ERC721 NFT balance | | `Rules.checkErc20({ chainId, contractAddress, threshold })` | Check ERC20 token balance | | `Rules.checkErc1155({ chainId, contractAddress, tokenId, threshold })` | Check ERC1155 balance for a specific token ID | | `Rules.checkEthBalance({ threshold })` | Check native ETH balance | | `Rules.checkIsEntitled({ chainId, contractAddress, params? })` | Check cross-chain entitlement | ### Combining Rules ```ts theme={null} import { Rules } from '@towns-protocol/web3' import { parseEther } from 'viem' // AND: both conditions must pass const andRule = Rules.and( Rules.checkErc721({ chainId: 8453n, contractAddress: nftContract, threshold: 1n }), Rules.checkErc20({ chainId: 8453n, contractAddress: tokenContract, threshold: parseEther('100') }) ) // OR: either condition must pass const orRule = Rules.or( Rules.checkErc721({ chainId: 8453n, contractAddress: nftContract, threshold: 1n }), Rules.checkEthBalance({ threshold: parseEther('0.1') }) ) // All rules must pass. Does not allow failure. Can be nested. const allRule = Rules.every(ruleA, ruleB, ruleC) // Any rule can pass, allowing failure. Can be nested. const anyRule = Rules.some(ruleA, ruleB, ruleC) ``` ## Managing Roles ```ts theme={null} // Get all roles in a space // Returns: Array<{ id, name, permissions, disabled }> const roles = await bot.getAllRoles(spaceId) // Get full details of a specific role // Returns: { id, name, permissions, users, ruleData, disabled } | null const role = await bot.getRole(spaceId, roleId) // Update a role. Fields are optional and will only update the fields that are provided. // Returns: Transaction hash await bot.updateRole(spaceId, roleId, { name: 'New Name', users: ['0x...', '0x...'], permissions: [Permission.Read, Permission.Write] }) // Delete a role // Returns: Transaction hash await bot.deleteRole(spaceId, roleId) // Add role to a channel, applying the role's rules to the channel // Returns: Transaction hash await bot.addRoleToChannel(channelId, roleId) ``` ## Available Permissions See [Roles & Entitlements](/towns-smart-contracts/roles-entitlements) for the full list of available permissions. # Slash Commands Source: https://docs.towns.com/build/bots/slash-commands Slash commands are special message types that: * Start with `/` (e.g., `/help`, `/poll`, `/weather`) * Show up in autocomplete when users type `/` in Towns * Display descriptions to help users understand what they do * Are handled separately from regular messages (don't trigger `onMessage`) ### Example Commands ``` /help → Show bot commands /ban @john → Ban a user /weather San Francisco → Get weather for a location /remind 5m Check the oven → Set a reminder ``` ## Creating commands Commands are defined in a TypeScript file (typically `src/commands.ts`): ```ts src/commands.ts theme={null} import type { BotCommand } from '@towns-protocol/bot' export const commands = [ { name: 'help', description: 'Show available bot commands' }, { name: 'ping', description: 'Check if bot is responding' }, { name: 'greet', description: 'Greet a user' }, { name: 'weather', description: 'Get current weather for a location' } ] as const satisfies BotCommand[] export default commands ``` ## Handling Commands You can use `bot.onSlashCommand()` to handle each slash command: ```ts src/index.ts theme={null} import { makeTownsBot } from '@towns-protocol/bot' import commands from './commands' const bot = await makeTownsBot(appPrivateData, jwtSecret, { commands }) // Handle /help command bot.onSlashCommand('help', async (handler, event) => { const commandList = commands .map(cmd => `• \`/${cmd.name}\` - ${cmd.description}`) .join('\n') await handler.sendMessage( event.channelId, `**Available Commands**\n${commandList}` ) }) // Handle /ping command bot.onSlashCommand('ping', async (handler, event) => { const latency = Date.now() - event.createdAt.getTime() await handler.sendMessage(event.channelId, `Pong! 🏓 (${latency}ms)`) }) ``` ### Working with Arguments Slash commands can receive arguments after the command name. You can get them from the `args` property of the event. ```ts theme={null} bot.onSlashCommand('weather', async (handler, { args, channelId }) => { // /weather San Francisco // args: ['San', 'Francisco'] const location = args.join(' ') if (!location) { await handler.sendMessage( channelId, 'Usage: /weather <location>\nExample: /weather San Francisco' ) return } const weather = await callWeatherAPI(location) await handler.sendMessage( channelId, `Weather in ${location}: ${weather.temp}°F, ${weather.conditions}` ) }) ``` ### Working with Mentions Users can mention other users or the bot in slash commands. You can get the mentions from the `mentions` property of the event. ```ts theme={null} // /greet @john @jane bot.onSlashCommand('greet', async (handler, { channelId, mentions}) => { if (mentions.length === 0) { await handler.sendMessage( channelId, 'Usage: /greet @user1 @user2 ...' ) return } const greetings = mentions .map(m => `Hello <@${m.userId}>!`) .join('\n') await handler.sendMessage(channelId, greetings, { mentions }) }) ``` ### Checking Permissions You may want to check if the user has the necessary permissions to use a command. You can use the `hasAdminPermission` method to check if the user is an admin. This is a shortcut for checking if the user has the `ModifyBanning` permission. If you want to check for a specific permission, you can use the `checkPermission` method. ```ts theme={null} bot.onSlashCommand('ban', async (handler, { userId, spaceId, channelId, mentions }) => { // Check if user is admin const isAdmin = await handler.hasAdminPermission(userId, spaceId) if (!isAdmin) { await handler.sendMessage( channelId, '❌ Only admins can use this command') return } const userToBan = mentions[0]?.userId if (!userToBan) { await handler.sendMessage( channelId, 'Usage: /ban @user') return } try { await handler.ban(userToBan, spaceId) await handler.sendMessage( channelId, `✓ Banned <@${userToBan}>` ) } catch { await handler.sendMessage( channelId, '❌ Failed to ban user') } }) ``` ## Paid Commands You can create commands that require USDC payments in any network. Payments are processed through the [x402 protocol](https://x402.org). ### Defining Paid Commands Add a `paid` property to your command definition with a price in USDC: ```ts src/commands.ts theme={null} import type { BotCommand } from '@towns-protocol/bot' export const commands = [ { name: 'generate', description: 'Generate AI content', paid: { price: '$0.20' } } ] as const satisfies BotCommand[] export default commands ``` ### How it works When a user invokes a paid command: 1. Bot sends a signature request for USDC transfer authorization 2. User signs the request in their wallet 3. Payment is verified and settled via the x402 facilitator 4. Your command handler executes after successful payment ```ts src/commands.ts theme={null} export const commands = [ { name: 'generate', description: 'Generate AI content', paid: { price: '$0.20' } } ] ``` ```ts theme={null} // Handler only runs after payment succeeds bot.onSlashCommand('generate', async (handler, event) => { // Payment already processed at this point await handler.sendMessage( event.channelId, 'Thank you for your purchase! Here is your generated content...' ) }) ``` ## Updating Commands Commands are automatically synced with Towns when your bot starts. To update your bot's slash commands: 1. Modify your command definitions in `src/commands.ts` 2. Restart your bot The new commands will be automatically registered and appear in Towns' autocomplete. ## Next Steps * Learn about [handling other events](/build/bots/events) * Explore [message formatting and attachments](/build/bots/events#sending-attachments) * See [permission-based features](/build/bots/events#permissions) # Troubleshooting Source: https://docs.towns.com/build/bots/troubleshooting Common issues and solutions for Towns bots. ## Bot Doesn't Respond **Check:** 1. ✅ `APP_PRIVATE_DATA` and `JWT_SECRET` are correct 2. ✅ Webhook URL is accessible: `https://your-bot.com/webhook` 3. ✅ Bot is installed in the space (Space Settings → Bots) 4. ✅ Forwarding setting is correct: * **"All Messages"** - Receives everything * **"Mentions, Commands, Replies & Reactions"** (Default) - Only @mentions, commands, replies * **"No Messages"** - Receives nothing **Test webhook:** ```bash theme={null} curl https://your-bot.com/health # Should respond ``` ## Lost Context Between Events <Warning> The bot framework is stateless. Each event is isolated - no access to previous messages or conversation history. </Warning> **Solution:** Store context externally: ```ts theme={null} const messageCache = new Map() bot.onMessage(async (handler, event) => { messageCache.set(event.eventId, event.message) if (event.replyId) { const original = messageCache.get(event.replyId) // Now you have the context } }) ``` For production, use a database. See [Storing State](/build/bots/external-interactions#storing-state). ## Transaction Errors ``` Error: insufficient funds for gas ``` **Problem:** Your bot treasury wallet (`bot.appAddress`) needs ETH for gas. **Solution:** ```ts theme={null} import { formatEther } from 'viem' // 1. Check balance const balance = await bot.viem.getBalance({ address: bot.appAddress }) console.log(`Balance: ${formatEther(balance)} ETH`) console.log(`Fund this address: ${bot.appAddress}`) // 2. Send ETH to bot.appAddress from any wallet // 3. Verify const newBalance = await bot.viem.getBalance({ address: bot.appAddress }) ``` <Note> Fund `bot.appAddress` (the app contract), not `bot.botId` (the signer). See [Wallet Architecture](/build/bots/getting-started#understanding-your-bots-wallet-architecture). </Note> ## Can't Mention Users ```ts theme={null} // ❌ Wrong await handler.sendMessage(channelId, "@username hello") // ✅ Correct await handler.sendMessage(channelId, "Hello <@0x1234...>", { mentions: [{ userId: "0x1234...", displayName: "username" }] }) ``` ## Slash Commands Not Showing **Fix:** ```ts theme={null} // Pass commands to makeTownsBot import commands from './commands' const bot = await makeTownsBot(privateData, jwtSecret, { commands }) // Restart bot after changing commands ``` ## Rate Limiting **Use a better RPC:** ```ts theme={null} const bot = await makeTownsBot(privateData, jwtSecret, { baseRpcUrl: 'https://base-mainnet.g.alchemy.com/v2/YOUR_KEY' }) ``` Get free RPC from [Alchemy](https://alchemy.com) or [Infura](https://infura.io). ## Bot Crashes **Add error handling:** ```ts theme={null} bot.onMessage(async (handler, event) => { try { await someAsyncOperation() } catch (error) { console.error('Error:', error) await handler.sendMessage(event.channelId, 'Something went wrong') } }) ``` ## Next Steps * Review [bot architecture](/build/bots/introduction) * Learn about [stateless design](/build/bots/events#understanding-bot-state-and-context) * Set up [storage](/build/bots/external-interactions#storing-state) # Delegate Key Signing Source: https://docs.towns.com/build/delegate-key-signing ### Delegate Key Signing All events in the Towns Protocol are signed by an [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) wallet key pair. Users and nodes can both send events using a process referred to as delegate signing that allows for events to be signed by a separate device linked to the primary wallet. Delegate signing therefore opens the protocol to allow events from linked devices the user has custody of. Towns Stream Nodes validate the signature prior to processing an event that is for example being added to a stream using `addEvent` RPC method. <Note>Why ECDSA ? First, [ECDSA](https://blog.cloudflare.com/ecdsa-the-digital-signature-algorithm-of-a-better-internet) elliptic curve derived keys have been proven to be less susceptible to breaking than earlier algorithms such as RSA. Secondly, ECDSA keys are smaller at 256-bytes and less cpu intensive to generate signatures than asymmetric or RSA keys. Since Towns Stream Nodes validate and store signatures in storage for each event, using ECDSA signatures saves on cpu and disk space. </Note> The rules and construction of the delegate key signature are the topic of this page. ### Delegate Signature Protocol Rules The logic dictating how a Towns Stream Node should process delegate signatures on the critical path of a request is described in `protocol.proto` and defined in `delegate.go`. Events sent over RPC to a Towns Stream Node are sent with a message `Envelope` that includes several fields used to validate the sender. <img alt="Wire Payload" /> * **signature** : For the event to be valid, the signature on the `Envelope` must match the `Event` creator\_address or be signed by the same address implied by `Event` delegate\_sig. * **creator\_address** : This is the wallet address of the creator of the event, which can be a user or a Towns Stream Node. * **delegate\_sig** : Optional field that allows events to be signed by a device keypair linked to the user's primary wallet. <Note>If a delegate signature is present on an event, the event is only valid if either the creator\_address matches the delegate\_sig's signer public key or if the delegate\_sig signing public key matches the key implied by Envelope.signature.</Note> ### Delegate Signature Validation Each event on a Towns Stream Node is parsed and validated as follows. <Steps> <Step title="Unmarshal Bytes"> Events are stored as bytes in Towns Stream Node storage and transmitted as bytes over the wire. All events are unmarshalled first and then parsed to run a series of validity checks prior to storing those events. ```go theme={null} // stream_view.go example unpacking and parsing of events var env Envelope // e is [][]byte err := proto.Unmarshal(e, &env) if err != nil { return nil, err } parsed, err := ParseEvent(&env) if err != nil { return nil, err } ``` </Step> <St
프로젝트로 돌아가기