diff --git a/src/data/modules/m04-querying-data.js b/src/data/modules/m04-querying-data.js index 9a8bff9..0d7ebb3 100644 --- a/src/data/modules/m04-querying-data.js +++ b/src/data/modules/m04-querying-data.js @@ -463,32 +463,35 @@ getAccountInfo("rYourAddressHere");`, ], slides: [ { - title: { es: "Conexión a Xahau", en: "Connecting to Xahau", jp: "Xahauへの接続", ko: "Xahau 연결" }, + title: { es: "Conexión a Xahau", en: "Connecting to Xahau", jp: "Xahauへの接続", ko: "Xahau 연결", zh: "连接到 Xahau" }, content: { es: "Conexión vía WebSocket a nodos públicos\n\n🌐 Mainnet: wss://xahau.network\n🧪 Testnet: wss://xahau-test.net\n\nAPI JSON-RPC para todas las consultas", en: "WebSocket connection to public nodes\n\n🌐 Mainnet: wss://xahau.network\n🧪 Testnet: wss://xahau-test.net\n\nJSON-RPC API for all queries", jp: "パブリックノードへのWebSocket接続\n\n🌐 メインネット:wss://xahau.network\n🧪 テストネット:wss://xahau-test.net\n\nすべての照会にJSON-RPC API", ko: "공용 노드에 WebSocket으로 연결\n\n🌐 Mainnet: wss://xahau.network\n🧪 Testnet: wss://xahau-test.net\n\n모든 조회에 사용하는 JSON-RPC API", + zh: "通过 WebSocket 连接到公共节点\n\n🌐 主网: wss://xahau.network\n🧪 测试网: wss://xahau-test.net\n\n所有查询都通过 JSON-RPC API 完成", }, visual: "🔌", }, { - title: { es: "Comandos principales", en: "Main commands", jp: "主要なコマンド", ko: "주요 명령" }, + title: { es: "Comandos principales", en: "Main commands", jp: "主要なコマンド", ko: "주요 명령", zh: "主要命令" }, content: { es: "• server_info → Estado del nodo\n• account_info → Datos de cuenta\n• account_lines → TrustLines\n• account_objects → Objetos de la cuenta\n• account_tx → Historial de transacciones\n• ledger → Info del ledger", en: "• server_info → Node status\n• account_info → Account data\n• account_lines → TrustLines\n• account_objects → Account objects\n• account_tx → Transaction history\n• ledger → Ledger info", jp: "• server_info → ノードの状態\n• account_info → アカウントデータ\n• account_lines → TrustLine\n• account_objects → アカウントオブジェクト\n• account_tx → トランザクション履歴\n• ledger → レジャー情報", ko: "• server_info → 노드 상태\n• account_info → 계정 데이터\n• account_lines → TrustLine\n• account_objects → 계정 객체\n• account_tx → 트랜잭션 기록\n• ledger → Ledger 정보", + zh: "• server_info → 节点状态\n• account_info → 账户数据\n• account_lines → TrustLines\n• account_objects → 账户对象\n• account_tx → 交易历史\n• ledger → 账本信息", }, visual: "📡", }, { - title: { es: "Buenas prácticas de conexión", en: "Connection best practices", jp: "接続のベストプラクティス", ko: "연결 모범 사례" }, + title: { es: "Buenas prácticas de conexión", en: "Connection best practices", jp: "接続のベストプラクティス", ko: "연결 모범 사례", zh: "连接最佳实践" }, content: { es: "• Envuelve conexiones en try/catch\n• Implementa reconexión automática\n• Escucha el evento 'disconnected'\n• Testnet para desarrollo, Mainnet para producción\n• Configura timeouts razonables\n• Valida respuestas antes de procesar", en: "• Wrap connections in try/catch\n• Implement automatic reconnection\n• Listen for the 'disconnected' event\n• Testnet for development, Mainnet for production\n• Configure reasonable timeouts\n• Validate responses before processing", jp: "• try/catchで接続をラップする\n• 自動再接続を実装する\n• 'disconnected'イベントをリッスンする\n• 開発にはTestnet、本番にはMainnet\n• 適切なタイムアウトを設定する\n• 処理前にレスポンスを検証する", ko: "• 연결 로직을 try/catch로 감싸기\n• 자동 재연결 구현\n• 'disconnected' 이벤트 감지\n• 개발은 Testnet, 운영은 Mainnet 사용\n• 적절한 timeout 설정\n• 처리 전에 응답 검증", + zh: "• 用 try/catch 包裹连接逻辑\n• 实现自动重连\n• 监听 'disconnected' 事件\n• 开发用 Testnet,生产用 Mainnet\n• 设置合理的 timeout\n• 处理前先验证响应", }, visual: "🛡️", }, @@ -501,6 +504,7 @@ getAccountInfo("rYourAddressHere");`, en: "Advanced queries and subscriptions", jp: "高度な照会とサブスクリプション", ko: "고급 조회와 구독", + zh: "高级查询与订阅", }, theory: { es: `Más allá de las consultas básicas, Xahau permite consultar objetos específicos del ledger, el historial de transacciones de una cuenta y suscribirse a eventos en tiempo real. @@ -603,6 +607,31 @@ You can query the details of a specific transaction using its **hash** with the ### 개별 트랜잭션 조회 \`tx\` 명령을 사용하면 **해시**로 특정 트랜잭션의 상세 정보를 조회할 수 있습니다.`, + zh: `除了基础查询外,Xahau 还允许你查询特定账本对象、某个账户的交易历史,并订阅实时事件。 + +### 交易历史 + +\`account_tx\` 命令会返回与某个账户相关的交易。你可以使用 \`marker\` 字段对结果进行分页。 + +### 账户对象 + +\`account_objects\` 命令会返回与某个账户关联的所有账本对象: +- TrustLines +- Offers(DEX 订单) +- URITokens(NFT) +- 已安装的 Hooks +- Hook 状态 + +### 实时订阅 + +通过 \`subscribe\` 命令,你可以在事件发生时收到通知: +- **ledger**:每次有新账本关闭时通知 +- **transactions**:网络中的所有交易 +- **accounts**:影响特定账户的交易 + +### 查询单笔交易 + +你可以使用 \`tx\` 命令,通过交易的 **hash** 查询某一笔交易的详细信息。`, }, codeBlocks: [ { @@ -611,6 +640,7 @@ You can query the details of a specific transaction using its **hash** with the en: "Query an account's transaction history", jp: "アカウントのトランザクション履歴を照会する", ko: "계정 트랜잭션 기록 조회", + zh: "查询账户交易历史", }, language: "javascript", code: { @@ -745,6 +775,39 @@ async function getAccountTransactions(address) { await client.disconnect(); } //예시 주소: rDADDYfnLvVY9FBnS8zFXhwYFHPuU5q2Sk +getAccountTransactions("rYourAddressHere");`, + zh: `const { Client } = require("xahau"); + +async function getAccountTransactions(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const response = await client.request({ + command: "account_tx", + account: address, + ledger_index_min: -1, + ledger_index_max: -1, + limit: 10, + }); + + console.log("=== 最近交易 ==="); + for (const item of response.result.transactions) { + const tx = item.tx; + console.log(\`类型: \${tx.TransactionType}\`); + console.log(\` Hash: \${item.tx.hash}\`); + console.log(\` 日期: \${new Date((tx.date + 946684800) * 1000).toISOString()}\`); + console.log(\` 结果: \${item.meta.TransactionResult}\`); + + if (tx.TransactionType === "Payment") { + console.log(\` 从: \${tx.Account}\`); + console.log(\` 到: \${tx.Destination}\`); + console.log(\` 金额: \${Number(tx.Amount) / 1_000_000} XAH\`); + } + } + + await client.disconnect(); +} +//示例地址: rDADDYfnLvVY9FBnS8zFXhwYFHPuU5q2Sk getAccountTransactions("rYourAddressHere");`, }, }, @@ -754,6 +817,7 @@ getAccountTransactions("rYourAddressHere");`, en: "Query account objects and subscribe to events", jp: "アカウントオブジェクトの照会とイベントのサブスクリプション", ko: "계정 객체 조회와 이벤트 구독", + zh: "查询账户对象并订阅事件", }, language: "javascript", code: { @@ -928,38 +992,84 @@ async function getAccountObjects(address) { setTimeout(() => client.disconnect(), 60000); } //예시 주소: rDADDYfnLvVY9FBnS8zFXhwYFHPuU5q2Sk +getAccountObjects("rYourAddressHere");`, + zh: `const { Client } = require("xahau"); + +async function getAccountObjects(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 查询该账户的所有对象 + const response = await client.request({ + command: "account_objects", + account: address, + ledger_index: "validated", + }); + + console.log("=== 账户对象 ==="); + for (const obj of response.result.account_objects) { + console.log(\`类型: \${obj.LedgerEntryType}\`); + + if (obj.LedgerEntryType === "RippleState") { + console.log(\` 代币: \${obj.Balance.currency}\`); + console.log(\` 余额: \${obj.Balance.value}\`); + } else if (obj.LedgerEntryType === "URIToken") { + console.log(\` URI: \${obj.URI}\`); + } + } + + // 订阅该账户的交易 + console.log("已订阅该账户的交易..."); + await client.request({ + command: "subscribe", + accounts: [address] + }); + + client.on("transaction", (tx) => { + console.log("检测到新交易!"); + console.log("类型:", tx.transaction.TransactionType); + console.log("结果:", tx.meta.TransactionResult); + }); + + // 保持连接 60 秒 + setTimeout(() => client.disconnect(), 60000); +} +//示例地址: rDADDYfnLvVY9FBnS8zFXhwYFHPuU5q2Sk getAccountObjects("rYourAddressHere");`, }, }, ], slides: [ { - title: { es: "Historial de transacciones", en: "Transaction history", jp: "トランザクション履歴", ko: "트랜잭션 기록" }, + title: { es: "Historial de transacciones", en: "Transaction history", jp: "トランザクション履歴", ko: "트랜잭션 기록", zh: "交易历史" }, content: { es: "account_tx → Historial de una cuenta\n\n• Paginar con marker\n• Filtrar por tipo de transacción\n• Ver resultados (éxito/fallo)\n• Consultar metadatos detallados", en: "account_tx → Account history\n\n• Paginate with marker\n• Filter by transaction type\n• View results (success/failure)\n• Query detailed metadata", jp: "account_tx → アカウントの履歴\n\n• markerでページング\n• トランザクションタイプでフィルタリング\n• 結果を確認(成功/失敗)\n• 詳細なメタデータを照会", ko: "account_tx → 계정 기록\n\n• marker로 페이지네이션\n• 트랜잭션 유형별 필터링\n• 결과 확인(성공/실패)\n• 상세 메타데이터 조회", + zh: "account_tx → 账户历史\n\n• 使用 marker 分页\n• 按交易类型过滤\n• 查看结果(成功/失败)\n• 查询详细 metadata", }, visual: "📜", }, { - title: { es: "Tiempo real", en: "Real time", jp: "リアルタイム", ko: "실시간" }, + title: { es: "Tiempo real", en: "Real time", jp: "リアルタイム", ko: "실시간", zh: "实时" }, content: { es: "subscribe → Eventos en tiempo real\n\n• ledger → Cierre de ledgers\n• transactions → Todas las txs\n• accounts → Txs de cuentas específicas\n\nIdeal para monitorizar actividad", en: "subscribe → Real-time events\n\n• ledger → Ledger closings\n• transactions → All txs\n• accounts → Txs for specific accounts\n\nIdeal for monitoring activity", jp: "subscribe → リアルタイムイベント\n\n• ledger → レジャーのクローズ\n• transactions → すべてのtx\n• accounts → 特定アカウントのtx\n\nアクティビティの監視に最適", ko: "subscribe → 실시간 이벤트\n\n• ledger → ledger 닫힘 알림\n• transactions → 모든 tx\n• accounts → 특정 계정 tx\n\n활동 모니터링에 적합", + zh: "subscribe → 实时事件\n\n• ledger → 账本关闭通知\n• transactions → 所有交易\n• accounts → 特定账户的交易\n\n非常适合监控活动", }, visual: "⚡", }, { - title: { es: "Suscripciones en detalle", en: "Subscriptions in detail", jp: "サブスクリプションの詳細", ko: "구독 상세" }, + title: { es: "Suscripciones en detalle", en: "Subscriptions in detail", jp: "サブスクリプションの詳細", ko: "구독 상세", zh: "订阅详解" }, content: { es: "Comando subscribe para eventos en tiempo real:\n\n• Evento ledger → Nuevo ledger cerrado\n• Evento transaction → Tx confirmada\n• Escucha con client.on('transaction')\n• unsubscribe para dejar de escuchar\n• Mantén la conexión WebSocket abierta", en: "subscribe command for real-time events:\n\n• ledger event → New ledger closed\n• transaction event → Tx confirmed\n• Listen with client.on('transaction')\n• unsubscribe to stop listening\n• Keep the WebSocket connection open", jp: "リアルタイムイベントのsubscribeコマンド:\n\n• ledgerイベント → 新しいレジャーがクローズ\n• transactionイベント → txが確認済み\n• client.on('transaction')でリッスン\n• unsubscribeでリッスン停止\n• WebSocket接続を開いたままにする", ko: "실시간 이벤트용 subscribe 명령:\n\n• ledger 이벤트 → 새 ledger 닫힘\n• transaction 이벤트 → tx 확인\n• client.on('transaction')로 수신\n• unsubscribe로 중지\n• WebSocket 연결을 계속 유지", + zh: "用于实时事件的 subscribe 命令:\n\n• ledger 事件 → 新账本关闭\n• transaction 事件 → 交易已确认\n• 用 client.on('transaction') 监听\n• 使用 unsubscribe 停止监听\n• 保持 WebSocket 连接处于打开状态", }, visual: "📡", }, @@ -972,6 +1082,7 @@ getAccountObjects("rYourAddressHere");`, en: "Pagination and error handling", jp: "ページネーションとエラー処理", ko: "페이지네이션과 오류 처리", + zh: "分页与错误处理", }, theory: { es: `Cuando trabajas con la API de Xahau, es fundamental dominar dos aspectos: la **paginación** de resultados grandes y el **manejo de errores** para construir aplicaciones robustas. @@ -1094,6 +1205,36 @@ Many API commands return paginated results. When there is more data than fits in - **연결 끊김 처리**: 클라이언트의 \`disconnected\` 이벤트를 감지하고 자동 재연결합니다 - **Rate limiting 고려**: 공용 노드는 요청을 제한할 수 있으므로 대량 요청 사이에 간격을 둡니다 - **Timeout 설정**: 애플리케이션이 멈추지 않도록 적절한 timeout을 설정합니다`, + zh: `在使用 Xahau API 时,有两个方面尤其重要:处理大结果集时的**分页**,以及构建稳健应用所需的**错误处理**。 + +### marker 系统 + +很多 API 命令返回的是分页结果。当数据量超过单次响应可容纳的范围时,API 会在结果中包含一个 \`marker\` 字段。要获取下一页,你必须发送同一个命令,并带上这个 \`marker\`。 + +- \`limit\` 字段控制每页返回多少结果(最大值视命令而定,通常为 200 到 400) +- 如果响应中包含 \`marker\`,说明后面还有更多页面 +- 如果响应中没有 \`marker\`,说明已经到最后一页 +- \`marker\` 的值是不透明的:不要修改它,直接原样传回即可 + +### 常见 API 错误 + +| Error | 含义 | +|---|---| +| \`actNotFound\` | 查询的账户在账本中不存在 | +| \`lgrNotFound\` | 请求的账本未找到 | +| \`invalidParams\` | 请求参数不正确 | +| \`noCurrent\` | 服务器当前没有可用账本 | +| \`noNetwork\` | 服务器未连接到网络 | +| \`tooBusy\` | 服务器负载过高 | + +### 最佳实践 + +- **始终用 try/catch 包裹请求**:网络错误、timeout 和 API 错误都必须处理 +- **实现重试机制**:对于 \`tooBusy\` 或 timeout 这类瞬时错误,使用指数退避重试 +- **验证响应内容**:在处理数据前先确认 \`result.status === "success"\` +- **处理断线情况**:监听客户端的 \`disconnected\` 事件并自动重连 +- **考虑速率限制**:公共节点可能会限制请求频率,批量请求之间应适当暂停 +- **设置合理 timeout**:防止应用程序长时间卡住`, }, codeBlocks: [ { @@ -1102,6 +1243,7 @@ Many API commands return paginated results. When there is more data than fits in en: "Paginate all account objects using marker", jp: "markerを使用してアカウントのすべてのオブジェクトをページングする", ko: "marker로 계정 객체 전체 페이지네이션", + zh: "使用 marker 分页获取账户全部对象", }, language: "javascript", code: { @@ -1340,38 +1482,100 @@ async function getAllAccountObjects(address) { await client.disconnect(); } //예시 계정: rHh1YJN4kwRdw4Y29Xu1EY9qW8u36vAYLc +getAllAccountObjects("rYourAddressHere");`, + zh: `const { Client } = require("xahau"); + +async function getAllAccountObjects(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + let allObjects = []; + let marker = undefined; + let page = 1; + + console.log("=== 正在获取", address, "的全部对象 ==="); + + do { + const request = { + command: "account_objects", + account: address, + ledger_index: "validated", + limit: 100, + }; + + // 只有 marker 存在时才加入(第一次请求不加) + if (marker) { + request.marker = marker; + } + + const response = await client.request(request); + const objects = response.result.account_objects; + allObjects = allObjects.concat(objects); + + console.log(\`第 \${page} 页: 收到 \${objects.length} 个对象\`); + + // 更新下一页所需的 marker + marker = response.result.marker; + page++; + + // 短暂停顿,避免压垮节点 + if (marker) { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } while (marker); + + console.log(\`总共获取对象数: \${allObjects.length}\`); + + // 按类型分组 + const byType = {}; + for (const obj of allObjects) { + const type = obj.LedgerEntryType; + byType[type] = (byType[type] || 0) + 1; + } + + console.log("按类型汇总:"); + for (const [type, count] of Object.entries(byType)) { + console.log(\` \${type}: \${count}\`); + } + + await client.disconnect(); +} +//示例账户: rHh1YJN4kwRdw4Y29Xu1EY9qW8u36vAYLc getAllAccountObjects("rYourAddressHere");`, }, }, ], slides: [ { - title: { es: "Paginación con marker", en: "Pagination with marker", jp: "markerによるページネーション", ko: "marker 페이지네이션" }, + title: { es: "Paginación con marker", en: "Pagination with marker", jp: "markerによるページネーション", ko: "marker 페이지네이션", zh: "使用 marker 分页" }, content: { es: "Cuando hay muchos resultados, la API pagina:\n\n1. Envía tu consulta con limit\n2. Si la respuesta tiene marker, hay más datos\n3. Reenvía la consulta incluyendo el marker\n4. Repite hasta que no haya marker\n\nNunca modifiques el valor del marker", en: "When there are many results, the API paginates:\n\n1. Send your query with limit\n2. If the response has a marker, there is more data\n3. Resend the query including the marker\n4. Repeat until there is no marker\n\nNever modify the marker value", jp: "多くの結果がある場合、APIはページングします:\n\n1. limitをつけてクエリを送信\n2. レスポンスにmarkerがあればデータが続く\n3. markerを含めてクエリを再送信\n4. markerがなくなるまで繰り返す\n\nmarkerの値を絶対に変更しない", ko: "결과가 많으면 API가 페이지를 나눕니다:\n\n1. limit와 함께 요청 전송\n2. 응답에 marker가 있으면 다음 데이터 존재\n3. marker를 포함해 다시 요청\n4. marker가 없어질 때까지 반복\n\nmarker 값은 절대 수정하지 마세요", + zh: "当结果很多时,API 会分页:\n\n1. 带上 limit 发送请求\n2. 如果响应里有 marker,说明还有更多数据\n3. 带着 marker 再发一次请求\n4. 重复直到 marker 消失\n\n永远不要修改 marker 的值", }, visual: "📄", }, { - title: { es: "Errores comunes", en: "Common errors", jp: "よくあるエラー", ko: "흔한 오류" }, + title: { es: "Errores comunes", en: "Common errors", jp: "よくあるエラー", ko: "흔한 오류", zh: "常见错误" }, content: { es: "• actNotFound → Cuenta no existe\n• lgrNotFound → Ledger no encontrado\n• invalidParams → Parámetros incorrectos\n• noCurrent → Sin ledger actual\n• noNetwork → Sin conexión a la red\n• tooBusy → Servidor sobrecargado", en: "• actNotFound → Account does not exist\n• lgrNotFound → Ledger not found\n• invalidParams → Incorrect parameters\n• noCurrent → No current ledger\n• noNetwork → No network connection\n• tooBusy → Server overloaded", jp: "• actNotFound → アカウントが存在しない\n• lgrNotFound → レジャーが見つからない\n• invalidParams → パラメータが正しくない\n• noCurrent → 現在のレジャーなし\n• noNetwork → ネットワーク接続なし\n• tooBusy → サーバーが過負荷", ko: "• actNotFound → 계정이 존재하지 않음\n• lgrNotFound → ledger를 찾지 못함\n• invalidParams → 잘못된 파라미터\n• noCurrent → 현재 ledger 없음\n• noNetwork → 네트워크 연결 없음\n• tooBusy → 서버 과부하", + zh: "• actNotFound → 账户不存在\n• lgrNotFound → 未找到账本\n• invalidParams → 参数错误\n• noCurrent → 当前没有账本\n• noNetwork → 未连接到网络\n• tooBusy → 服务器过载", }, visual: "⚠️", }, { - title: { es: "Buenas prácticas", en: "Best practices", jp: "ベストプラクティス", ko: "모범 사례" }, + title: { es: "Buenas prácticas", en: "Best practices", jp: "ベストプラクティス", ko: "모범 사례", zh: "最佳实践" }, content: { es: "• Siempre usar try/catch en las peticiones\n• Reintentar con backoff exponencial\n• Validar result.status === 'success'\n• Escuchar evento 'disconnected'\n• Pausar entre peticiones masivas\n• Configurar timeouts razonables", en: "• Always use try/catch for requests\n• Retry with exponential backoff\n• Validate result.status === 'success'\n• Listen for the 'disconnected' event\n• Pause between bulk requests\n• Configure reasonable timeouts", jp: "• リクエストには常にtry/catchを使用\n• 指数バックオフでリトライする\n• result.status === 'success'を検証する\n• 'disconnected'イベントをリッスンする\n• 大量リクエスト間に間隔を設ける\n• 適切なタイムアウトを設定する", ko: "• 요청은 항상 try/catch 사용\n• 지수 백오프로 재시도\n• result.status === 'success' 검증\n• 'disconnected' 이벤트 감지\n• 대량 요청 사이에 잠시 대기\n• 적절한 timeout 설정", + zh: "• 请求始终放在 try/catch 中\n• 用指数退避重试\n• 验证 result.status === 'success'\n• 监听 'disconnected' 事件\n• 批量请求之间稍作暂停\n• 设置合理 timeout", }, visual: "🛡️", }, @@ -1384,6 +1588,7 @@ getAllAccountObjects("rYourAddressHere");`, en: "Working with ledger objects", jp: "レジャーオブジェクトの操作", ko: "레저 객체 다루기", + zh: "处理账本对象", }, theory: { es: `El ledger de Xahau almacena toda la información en forma de **objetos** (ledger entries). Cada objeto tiene un tipo, un índice único (hash) y campos específicos. En esta lección aprenderemos a consultar y trabajar con estos objetos directamente. @@ -1514,6 +1719,38 @@ ledger의 각 객체는 식별 데이터에서 계산되는 SHA-512Half hash 기 - RippleState 인덱스는 두 계정과 통화 정보에서 계산됩니다 이 인덱스는 결정적이므로 입력 데이터를 알고 있으면 언제든지 다시 계산할 수 있습니다.`, + zh: `Xahau 的账本会把所有信息存储为**对象**(ledger entries)。每个对象都有类型、唯一索引(hash)以及专属字段。在这一课里,我们会学习如何直接查询和处理这些对象。 + +### ledger_entry 命令 + +使用 \`ledger_entry\`,你可以通过对象的**索引**(64 个十六进制字符的 hash)查询账本中的某个特定对象。当你已经知道对象的精确标识符时,这个命令非常有用。 + +### 可查询的对象类型 + +| 类型 | 说明 | +|---|---| +| \`AccountRoot\` | 账户的主要数据 | +| \`RippleState\` | 两个账户之间的 trust line | +| \`Offer\` | DEX 上的活跃订单 | +| \`URIToken\` | 非同质化代币(Xahau NFT) | +| \`Hook\` | 已安装 Hook 的定义 | +| \`HookState\` | Hook 存储的状态 | + +### 带类型过滤的 account_objects 命令 + +\`account_objects\` 命令接受 \`type\` 参数,用来只筛选某一类对象。有效值包括: +- \`"state"\` → RippleState(trust lines) +- \`"offer"\` → Offers(DEX 订单) +- \`"uri_token"\` → URITokens +- \`"hook"\` → 已安装 Hooks + +### 理解 ledger 索引 + +账本中的每个对象都拥有一个**唯一索引**,它是根据对象的标识数据通过 SHA-512Half 哈希计算出来的。例如: +- AccountRoot 的索引由账户地址计算得到 +- RippleState 的索引由两个账户和货币共同计算得到 + +这些索引是确定性的:只要你知道输入数据,就可以随时重新计算出来。`, }, codeBlocks: [ @@ -1523,6 +1760,7 @@ ledger의 각 객체는 식별 데이터에서 계산되는 SHA-512Half hash 기 en: "Query account_objects filtered by type", jp: "タイプでフィルタリングされたaccount_objectsを照会する", ko: "type으로 필터링한 account_objects 조회", + zh: "按类型过滤查询 account_objects", }, language: "javascript", code: { @@ -1809,28 +2047,101 @@ getObjectsByType("rDk1xiArDMjDqnrR2yWypwQAKg4mKnQYvs", "state"); // getObjectsByType("rfmPQz4eSmisCVnWJkKj82hHKQdrUPv3Px", "offer"); // URIToken 보기 +// getObjectsByType("rfPMnDQEzb5StPXj3Dkd34oKY4BVAJCwsn", "uri_token");`, + zh: `const { Client } = require("xahau"); + +async function getObjectsByType(address, type) { + const client = new Client("wss://xahau.network"); + await client.connect(); + + let allObjects = []; + let marker = undefined; + + do { + const request = { + command: "account_objects", + account: address, + type: type, + ledger_index: "validated", + limit: 100, + }; + if (marker) request.marker = marker; + + const response = await client.request(request); + allObjects = allObjects.concat(response.result.account_objects); + marker = response.result.marker; + } while (marker); + + console.log(\`=== \${type.toUpperCase()} for \${address} ===\`); + console.log(\`总数: \${allObjects.length}\`); + + for (const obj of allObjects) { + switch (type) { + case "state": // RippleState (trust lines) + const currency = obj.Balance.currency; + const balance = obj.Balance.value; + const peer = obj.HighLimit.issuer === address + ? obj.LowLimit.issuer + : obj.HighLimit.issuer; + console.log(\` \${currency}: 余额 \${balance} (对手方: \${peer})\`); + break; + + case "offer": + const pays = typeof obj.TakerPays === "string" + ? \`\${Number(obj.TakerPays) / 1_000_000} XAH\` + : \`\${obj.TakerPays.value} \${obj.TakerPays.currency}\`; + const gets = typeof obj.TakerGets === "string" + ? \`\${Number(obj.TakerGets) / 1_000_000} XAH\` + : \`\${obj.TakerGets.value} \${obj.TakerGets.currency}\`; + console.log(\` Offer: 支付 \${pays} → 获得 \${gets}\`); + break; + + case "uri_token": + const uri = Buffer.from(obj.URI || "", "hex").toString("utf8"); + console.log(\` URIToken: \${uri}\`); + console.log(\` 索引: \${obj.index}\`); + break; + + default: + console.log(\` \${obj.LedgerEntryType}: \${obj.index}\`); + } + } + + await client.disconnect(); +} + +// 使用示例: +// 查看 trust lines +getObjectsByType("rDk1xiArDMjDqnrR2yWypwQAKg4mKnQYvs", "state"); + +// 查看 DEX 订单 +// getObjectsByType("rfmPQz4eSmisCVnWJkKj82hHKQdrUPv3Px", "offer"); + +// 查看 URITokens // getObjectsByType("rfPMnDQEzb5StPXj3Dkd34oKY4BVAJCwsn", "uri_token");`, }, }, ], slides: [ { - title: { es: "Objetos del ledger", en: "Ledger objects", jp: "レジャーオブジェクト", ko: "레저 객체" }, + title: { es: "Objetos del ledger", en: "Ledger objects", jp: "レジャーオブジェクト", ko: "레저 객체", zh: "账本对象" }, content: { es: "Todo en Xahau se almacena como objetos:\n\n• AccountRoot → Datos de cuenta\n• RippleState → Trust lines\n• Offer → Órdenes DEX\n• URIToken → NFTs\n• Hook → Hooks instalados\n\nCada objeto tiene un índice único (hash)", en: "Everything in Xahau is stored as objects:\n\n• AccountRoot → Account data\n• RippleState → Trust lines\n• Offer → DEX orders\n• URIToken → NFTs\n• Hook → Installed Hooks\n\nEach object has a unique index (hash)", jp: "Xahauのすべてはオブジェクトとして保存される:\n\n• AccountRoot → アカウントデータ\n• RippleState → トラストライン\n• Offer → DEXの注文\n• URIToken → NFT\n• Hook → インストールされたHook\n\n各オブジェクトは一意のインデックス(ハッシュ)を持つ", ko: "Xahau의 모든 것은 객체로 저장됩니다:\n\n• AccountRoot → 계정 데이터\n• RippleState → Trust line\n• Offer → DEX 주문\n• URIToken → NFT\n• Hook → 설치된 Hook\n\n각 객체는 고유 인덱스(hash)를 가집니다", + zh: "Xahau 中的一切都以对象形式存储:\n\n• AccountRoot → 账户数据\n• RippleState → Trust lines\n• Offer → DEX 订单\n• URIToken → NFT\n• Hook → 已安装 Hooks\n\n每个对象都有唯一索引(hash)", }, visual: "🗂️", }, { - title: { es: "Consultas por tipo", en: "Queries by type", jp: "タイプ別照会", ko: "유형별 조회" }, + title: { es: "Consultas por tipo", en: "Queries by type", jp: "タイプ別照会", ko: "유형별 조회", zh: "按类型查询" }, content: { es: "account_objects + type = filtro eficiente\n\n• type: 'state' → Trust lines\n• type: 'offer' → Órdenes DEX\n• type: 'uri_token' → NFTs\n• type: 'hook' → Hooks\n\nCombina con marker para paginar", en: "account_objects + type = efficient filtering\n\n• type: 'state' → Trust lines\n• type: 'offer' → DEX orders\n• type: 'uri_token' → NFTs\n• type: 'hook' → Hooks\n\nCombine with marker to paginate", jp: "account_objects + type = 効率的なフィルタリング\n\n• type: 'state' → トラストライン\n• type: 'offer' → DEXの注文\n• type: 'uri_token' → NFT\n• type: 'hook' → Hook\n\nmarkerと組み合わせてページングする", ko: "account_objects + type = 효율적인 필터링\n\n• type: 'state' → Trust line\n• type: 'offer' → DEX 주문\n• type: 'uri_token' → NFT\n• type: 'hook' → Hook\n\nmarker와 함께 사용해 페이지네이션", + zh: "account_objects + type = 高效过滤\n\n• type: 'state' → Trust lines\n• type: 'offer' → DEX 订单\n• type: 'uri_token' → NFT\n• type: 'hook' → Hooks\n\n可与 marker 结合进行分页", }, visual: "🔎", }, diff --git a/src/data/modules/m05-transaction-anatomy.js b/src/data/modules/m05-transaction-anatomy.js index a19eeaa..4dc9b9a 100644 --- a/src/data/modules/m05-transaction-anatomy.js +++ b/src/data/modules/m05-transaction-anatomy.js @@ -6,6 +6,7 @@ export default { en: "Anatomy of a transaction", jp: "トランザクションの解剖", ko: "트랜잭션 해부", + zh: "交易剖析", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "Lifecycle of a transaction", jp: "トランザクションのライフサイクル", ko: "트랜잭션의 생명주기", + zh: "交易生命周期", }, theory: { es: `Antes de profundizar en tokens, NFTs o smart contracts, es fundamental entender **cómo funciona una transacción de principio a fin** en Xahau. Este conocimiento te ayudará a diagnosticar problemas y construir aplicaciones robustas. @@ -305,6 +307,78 @@ const tx = { - 검증된 ledger에 포함되면 그 트랜잭션은 **최종 결과**입니다 - reorg도, fork도, "확인 대기"도 없습니다 - \`tesSUCCESS\` = 영구적으로 성공`, + zh: `在深入学习代币、NFT 或智能合约之前,先理解 Xahau 中**一笔交易从开始到结束是如何运作的**非常重要。这些知识能帮助你排查问题,并构建更稳健的应用。 + +### 完整流程 + +在 Xahau 中,一笔交易从创建到永久写入账本,会经历 **5 个阶段**: + +1. **构建**:定义交易字段(类型、来源、目标、金额等) +2. **准备(autofill)**:客户端自动补全技术字段(Fee、Sequence、LastLedgerSequence、NetworkID) +3. **签名**:你的私钥生成加密签名,证明交易由你授权 +4. **提交**:将已签名交易发送到网络节点 +5. **验证**:验证者通过共识将其写入账本,结果成为最终结果 + +### 阶段 1:构建 + +你需要定义一个包含交易字段的 JavaScript 对象: + +\`\`\` +const tx = { + TransactionType: "Payment", + Account: "rSource...", + Destination: "rDestination...", + Amount: "1000000", +}; +\`\`\` + +你只需要填写**核心字段**。技术字段会在下一阶段自动补全。 + +### 阶段 2:准备(autofill) + +\`client.autofill(tx)\` 方法会查询节点并补上缺失字段: + +- **Fee**:交易成本(以 drops 计)。根据当前网络负载计算 +- **Sequence**:账户的序列号(每发出一笔交易就递增) +- **LastLedgerSequence**:交易可被纳入的最大账本序号(防止“幽灵交易”) +- **NetworkID**:网络标识符(testnet 与 mainnet) + +### 阶段 3:签名 + +\`wallet.sign(prepared)\` 方法会生成: +- 使用私钥(ed25519 或 secp256k1)的**数字签名** +- **tx_blob**:序列化后的十六进制交易,可直接发送 + +这个签名证明**只有你本人**授权了这笔交易。签名后任何人都无法修改交易内容,否则签名会失效。 + +### 阶段 4:提交 + +已签名交易通过 \`client.submit(tx_blob)\` 或 \`client.submitAndWait(tx_blob)\` 提交到节点: + +- **submit**:立即发送并返回初步结果 +- **submitAndWait**:发送后**等待**交易被验证或拒绝 + +节点会把这笔交易传播给网络中的其他节点。 + +### 阶段 5:验证(共识) + +网络验证者会决定是否将该交易纳入下一个账本: + +1. 交易进入**验证者队列** +2. 验证者提议将其写入下一个账本 +3. 如果至少 **80% 的 UNL** 同意,交易就会被纳入 +4. 账本关闭,结果变为**最终且不可逆** + +### 需要多久? + +从提交到验证通常只需 **3 到 5 秒**,这正是 Xahau 关闭一个账本所需的时间。它不像 Bitcoin 那样有 10 分钟区块,也没有不稳定的确认时间。 + +### Finality:不可逆的结果 + +与采用概率最终性的区块链(Bitcoin、Ethereum)不同,Xahau 的结果是**确定性的**: +- 交易一旦进入已验证账本,就是**最终结果** +- 没有 reorg、没有分叉、也没有“等待确认” +- \`tesSUCCESS\` = 永久成功`, }, codeBlocks: [ { @@ -313,6 +387,7 @@ const tx = { en: "Create .env with your wallet seed", jp: "ウォレットのシードで.envを作成", ko: "지갑 시드로 .env 만들기", + zh: "用钱包种子创建 .env", }, language: "bash", code: { @@ -327,6 +402,9 @@ WALLET_SEED=sYourSeed`, WALLET_SEED=sYourSeed`, ko: `# https://xahau-test.net/wallet 에서 testnet 지갑을 만들고 시드를 받으세요 # 프로젝트 폴더에 ".env" 파일을 만드세요 +WALLET_SEED=sYourSeed`, + zh: `# 访问 https://xahau-test.net/wallet 创建 testnet 钱包并获取种子 +# 在项目文件夹中创建 ".env" 文件 WALLET_SEED=sYourSeed`, }, }, @@ -336,6 +414,7 @@ WALLET_SEED=sYourSeed`, en: "The complete flow step by step", jp: "ステップバイステップの完全フロー", ko: "전체 흐름 단계별 보기", + zh: "完整流程逐步演示", }, language: "javascript", code: { @@ -590,38 +669,104 @@ async function completeFlow() { await client.disconnect(); } +completeFlow().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function completeFlow() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // ============================================= + // 第 1 阶段:构建交易 + // ============================================= + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: "rMXEZJecFdn1dVtE21pZ8duZz2E36KGaCp", + Amount: "5000000", // 5 XAH(单位:drops) + }; + + console.log("1. 交易已构建:"); + console.log(" 类型:", tx.TransactionType); + console.log(" 已定义字段数:", Object.keys(tx).length); + + // ============================================= + // 第 2 阶段:准备(autofill) + // ============================================= + const prepared = await client.autofill(tx); + + console.log("2. 交易已准备完成(autofill):"); + console.log(" Fee:", prepared.Fee, "drops"); + console.log(" Sequence:", prepared.Sequence); + console.log(" LastLedgerSequence:", prepared.LastLedgerSequence); + console.log(" NetworkID:", prepared.NetworkID); + console.log(" 总字段数:", Object.keys(prepared).length); + + // ============================================= + // 第 3 阶段:签名 + // ============================================= + const signed = wallet.sign(prepared); + + console.log("3. 交易已签名:"); + console.log(" Hash:", signed.hash); + console.log(" tx_blob(前 60 个字符):", signed.tx_blob.substring(0, 60) + "..."); + console.log(" blob 长度:", signed.tx_blob.length, "个十六进制字符"); + + // ============================================= + // 第 4 阶段:提交 + // ============================================= + console.log("4. 正在发送到节点..."); + const result = await client.submitAndWait(signed.tx_blob); + + // ============================================= + // 第 5 阶段:验证结果 + // ============================================= + console.log("5. 已验证结果:"); + console.log(" TransactionResult:", result.result.meta.TransactionResult); + console.log(" Ledger:", result.result.ledger_index); + console.log(" 受影响节点数:", result.result.meta.AffectedNodes.length); + + await client.disconnect(); +} + completeFlow().catch(console.error);`, }, }, ], slides: [ { - title: { es: "5 fases de una transacción", en: "5 phases of a transaction", jp: "トランザクションの5フェーズ", ko: "트랜잭션의 5단계" }, + title: { es: "5 fases de una transacción", en: "5 phases of a transaction", jp: "トランザクションの5フェーズ", ko: "트랜잭션의 5단계", zh: "交易的 5 个阶段" }, content: { es: "1. Construir → Definir campos (tipo, origen, destino)\n2. Preparar → autofill (Fee, Sequence, NetworkID)\n3. Firmar → Firma digital con clave privada\n4. Enviar → submit / submitAndWait\n5. Validar → Consenso → Resultado final", en: "1. Build → Define fields (type, source, destination)\n2. Prepare → autofill (Fee, Sequence, NetworkID)\n3. Sign → Digital signature with private key\n4. Submit → submit / submitAndWait\n5. Validate → Consensus → Final result", jp: "1. 構築 → フィールド定義(タイプ、送信元、宛先)\n2. 準備 → autofill(Fee、Sequence、NetworkID)\n3. 署名 → 秘密鍵でデジタル署名\n4. 送信 → submit / submitAndWait\n5. 検証 → コンセンサス → 最終結果", ko: "1. 구성 → 필드 정의 (유형, 출발지, 목적지)\n2. 준비 → autofill (Fee, Sequence, NetworkID)\n3. 서명 → 개인 키로 디지털 서명\n4. 제출 → submit / submitAndWait\n5. 검증 → 합의 → 최종 결과", + zh: "1. 构建 → 定义字段(类型、来源、目标)\n2. 准备 → autofill(Fee、Sequence、NetworkID)\n3. 签名 → 使用私钥生成数字签名\n4. 提交 → submit / submitAndWait\n5. 验证 → 共识 → 最终结果", }, visual: "📋", }, { - title: { es: "Autofill: campos automáticos", en: "Autofill: automatic fields", jp: "Autofill:自動フィールド", ko: "Autofill: 자동 필드" }, + title: { es: "Autofill: campos automáticos", en: "Autofill: automatic fields", jp: "Autofill:自動フィールド", ko: "Autofill: 자동 필드", zh: "Autofill:自动字段" }, content: { es: "client.autofill() rellena por ti:\n\n• Fee → Coste según carga de red\n• Sequence → Número de tx de tu cuenta\n• LastLedgerSequence → Protección anti-fantasma\n• NetworkID → Testnet vs Mainnet", en: "client.autofill() fills in for you:\n\n• Fee → Cost based on network load\n• Sequence → Your account's tx number\n• LastLedgerSequence → Anti-ghost protection\n• NetworkID → Testnet vs Mainnet", jp: "client.autofill()が自動入力:\n\n• Fee → ネットワーク負荷に基づくコスト\n• Sequence → アカウントのトランザクション番号\n• LastLedgerSequence → ゴースト対策保護\n• NetworkID → Testnet対Mainnet", ko: "client.autofill()가 자동으로 채웁니다:\n\n• Fee → 네트워크 부하 기반 비용\n• Sequence → 계정의 tx 번호\n• LastLedgerSequence → 유령 tx 방지\n• NetworkID → Testnet vs Mainnet", + zh: "client.autofill() 会自动补全:\n\n• Fee → 基于网络负载计算的成本\n• Sequence → 你的账户交易序号\n• LastLedgerSequence → 防幽灵交易保护\n• NetworkID → Testnet 与 Mainnet", }, visual: "⚙️", }, { - title: { es: "Finalidad determinista", en: "Deterministic finality", jp: "決定論的ファイナリティ", ko: "결정적 파이널리티" }, + title: { es: "Finalidad determinista", en: "Deterministic finality", jp: "決定論的ファイナリティ", ko: "결정적 파이널리티", zh: "确定性终局性" }, content: { es: "Validación en 3-5 segundos\n\n• Sin reorgs ni forks\n• Sin confirmaciones pendientes\n• tesSUCCESS = éxito para siempre\n• Resultado final e irreversible\n\nDiferente a Bitcoin/Ethereum (probabilístico)", en: "Validation in 3-5 seconds\n\n• No reorgs or forks\n• No pending confirmations\n• tesSUCCESS = success forever\n• Final and irreversible result\n\nDifferent from Bitcoin/Ethereum (probabilistic)", jp: "3〜5秒で検証\n\n• リオーグやフォークなし\n• 保留中の確認なし\n• tesSUCCESS = 永遠の成功\n• 最終的かつ不可逆の結果\n\nビットコイン/イーサリアム(確率的)と異なる", ko: "3~5초 안에 검증\n\n• reorg와 fork 없음\n• 확인 대기 없음\n• tesSUCCESS = 영구적인 성공\n• 최종적이고 되돌릴 수 없는 결과\n\n비트코인/이더리움과 다른 결정적 구조", + zh: "3 到 5 秒内完成验证\n\n• 没有 reorg 或分叉\n• 没有等待确认\n• tesSUCCESS = 永久成功\n• 结果最终且不可逆\n\n这点与 Bitcoin/Ethereum 的概率最终性不同", }, visual: "✅", }, @@ -634,6 +779,7 @@ completeFlow().catch(console.error);`, en: "Transaction fields", jp: "トランザクションのフィールド", ko: "트랜잭션 필드", + zh: "交易字段", }, theory: { es: `Cada transacción en Xahau es un **objeto con campos específicos**. Algunos campos son obligatorios, otros opcionales, y otros los rellena \`autofill()\`. Entender cada campo te dará control total sobre tus transacciones. @@ -960,6 +1106,87 @@ LastLedgerSequence는 트랜잭션의 **만료 시점**입니다: - memos는 **공개 데이터**이며 트랜잭션 데이터로 영구 저장됩니다 - 트랜잭션 로직에 영향은 없고 추가 정보만 저장합니다 - 꼭 필요하지 않다면 블록체인에 불필요한 데이터를 남기지 않도록 사용을 피하는 것이 좋습니다`, + zh: `Xahau 中的每笔交易都是一个**包含特定字段的对象**。有些字段是必填的,有些是可选的,还有一些由 \`autofill()\` 自动补全。理解这些字段后,你就能完全掌控自己的交易。 + +### 所有交易通用的字段 + +这些字段存在于**所有交易类型**中: + +| 字段 | 必填 | 说明 | +|---|---|---| +| **TransactionType** | 是 | 类型:"Payment"、"TrustSet"、"OfferCreate" 等 | +| **Account** | 是 | 你的地址(rXXX...)—— 发起交易的账户 | +| **Fee** | Autofill | 交易成本(单位:drops,1 XAH = 1,000,000 drops) | +| **Sequence** | Autofill | 账户的序列号 | +| **LastLedgerSequence** | Autofill | 交易可被纳入的最大账本号 | +| **NetworkID** | Autofill | 网络 ID(Xahau 主网为 21337) | +| **SigningPubKey** | 自动(签名时) | 你的公钥(签名时加入) | +| **TxnSignature** | 自动(签名时) | 数字签名(签名时加入) | + +### TransactionType:交易类型 + +Xahau 支持很多交易类型。最常见的有: + +- [Payment](https://xahau.network/docs/protocol-reference/transactions/transaction-types/payment/) — 发送 XAH 或代币 +- [TrustSet](https://xahau.network/docs/protocol-reference/transactions/transaction-types/trustset/) — 创建或修改 trust line +- [OfferCreate](https://xahau.network/docs/protocol-reference/transactions/transaction-types/offercreate/) — 在 DEX 上创建报价 +- [OfferCancel](https://xahau.network/docs/protocol-reference/transactions/transaction-types/offercancel/) — 取消 DEX 报价 +- [AccountSet](https://xahau.network/docs/protocol-reference/transactions/transaction-types/accountset/) — 配置账户标志 +- [SetHook](https://xahau.network/docs/protocol-reference/transactions/transaction-types/sethook/) — 安装或管理 Hooks(智能合约) +- [URITokenMint](https://xahau.network/docs/protocol-reference/transactions/transaction-types/uritokenmint/) — 创建 NFT(URIToken) +- [URITokenBuy](https://xahau.network/docs/protocol-reference/transactions/transaction-types/uritokenbuy/) — 购买 URIToken +- [URITokenCreateSellOffer](https://xahau.network/docs/protocol-reference/transactions/transaction-types/uritokencreateselloffer/) — 挂售 URIToken +- [EscrowCreate](https://xahau.network/docs/protocol-reference/transactions/transaction-types/escrowcreate/) — 创建条件支付 +- [EscrowFinish](https://xahau.network/docs/protocol-reference/transactions/transaction-types/escrowfinish/) — 完成 escrow +- [EscrowCancel](https://xahau.network/docs/protocol-reference/transactions/transaction-types/escrowcancel/) — 取消 escrow + +### Fee:交易成本 + +Xahau 的 Fee 与其他区块链不同: + +- 以 **drops** 表示(1 XAH = 1,000,000 drops) +- 基础 fee 为 **12 drops**(0.000012 XAH),非常便宜 +- fee 会被**销毁**,不会给验证者或其他任何人 +- 网络拥堵时,fee 可能会暂时上涨(**fee escalation**) +- \`autofill()\` 会根据当前网络负载计算最优 fee + +### Sequence:交易顺序 + +Sequence 是你账户的**递增计数器**: + +- 从账户激活时分配的编号开始 +- 每笔成功交易后加 1 +- 确保交易按**顺序**处理 +- 如果你发送两笔相同 Sequence 的交易,只会处理其中一笔 +- 如果中间缺少某个 Sequence(例如发送了 5、6、8,没有 7),那么 8 及之后的交易会排队等待 7 解决 + +### LastLedgerSequence:防止幽灵交易 + +LastLedgerSequence 字段是交易的**有效期**: + +- 指定交易可被纳入的**最大账本号** +- 如果当前账本号超过它而交易仍未处理,交易会被丢弃 +- 防止“丢失”的交易在几分钟甚至几小时后才执行 +- \`autofill()\` 会自动设置它(通常是当前账本 + 20) + +### Flags:行为修饰符 + +很多交易类型接受 **Flags** 字段来修改行为: + +- Flags 是通过位运算组合的**数值** +- 例如:在 URITokenMint 中,\`Flags: 1\` 会启用 \`tfBurnable\` +- 例如:在 OfferCreate 中,\`Flags: 131072\` 会启用 \`tfImmediateOrCancel\` +- 你可以把多个 flag 的值相加来组合它们 + +### Memos:附加数据 + +你可以通过 **Memos** 字段给任何交易附加数据: + +- **MemoType**:十六进制表示的 MIME 类型(例如 "text/plain") +- **MemoData**:十六进制内容 +- memo 是**公开的**,会作为交易数据永久保存在链上 +- 它们不会影响交易逻辑,只是用于存储附加信息 +- 如果没有明确需要,建议不要使用这些字段,以免在区块链上保存不必要的数据`, }, codeBlocks: [ { @@ -968,6 +1195,7 @@ LastLedgerSequence는 트랜잭션의 **만료 시점**입니다: en: "Inspect fields before and after autofill", jp: "autofill前後のフィールドを確認", ko: "autofill 전후 필드 확인", + zh: "查看 autofill 前后的字段", }, language: "javascript", code: { @@ -1134,6 +1362,47 @@ async function checkFields() { await client.disconnect(); } +checkFields().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function checkFields() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 只包含核心字段的交易 + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: "rPTkQoZDeKMbwhs8QsRA1wL6gGA9Ee4C4", + Amount: "1000000", // 1 XAH + }; + + console.log("=== autofill 之前 ==="); + console.log("已定义字段:", Object.keys(tx)); + console.log(JSON.stringify(tx, null, 2)); + + // autofill 会补齐技术字段 + const prepared = await client.autofill(tx); + + console.log("=== autofill 之后 ==="); + console.log("总字段:", Object.keys(prepared)); + console.log(JSON.stringify(prepared, null, 2)); + + // 显示 autofill 新增的字段 + const newFields = Object.keys(prepared).filter( + (k) => !Object.keys(tx).includes(k) + ); + console.log("=== autofill 新增的字段 ==="); + for (const field of newFields) { + console.log(" " + field + ":", prepared[field]); + } + + await client.disconnect(); +} + checkFields().catch(console.error);`, }, }, @@ -1143,6 +1412,7 @@ checkFields().catch(console.error);`, en: "Build different transaction types", jp: "異なるトランザクションタイプの構築", ko: "여러 트랜잭션 유형 구성하기", + zh: "构建不同类型的交易", }, language: "javascript", code: { @@ -1382,37 +1652,99 @@ const mint = { console.log("각 유형은 고유한 필드를 가집니다."); console.log("공통 필드: TransactionType, Account, Fee, Sequence.");`, + zh: `// 展示不同交易类型的构建方式。 +// 这里只展示核心字段,其他字段由 autofill() 补齐。 + +// --- Payment:发送 XAH --- +const payment = { + TransactionType: "Payment", + Account: "rOrigin...", + Destination: "rDestination...", + Amount: "5000000", // 5 XAH(单位:drops) +}; + +// --- Payment:发送代币 --- +const tokenPayment = { + TransactionType: "Payment", + Account: "rOrigin...", + Destination: "rDestination...", + Amount: { + currency: "USD", + value: "100", + issuer: "rIssuer...", + }, +}; + +// --- TrustSet:创建 trust line --- +const trustSet = { + TransactionType: "TrustSet", + Account: "rReceiver...", + LimitAmount: { + currency: "USD", + value: "10000", + issuer: "rIssuer...", + }, +}; + +// --- OfferCreate:在 DEX 上创建报价 --- +const offer = { + TransactionType: "OfferCreate", + Account: "rTrader...", + TakerPays: { currency: "USD", value: "50", issuer: "rIssuer..." }, + TakerGets: "100000000", // 100 XAH +}; + +// --- AccountSet:启用标志 --- +const accountSet = { + TransactionType: "AccountSet", + Account: "rMyAccount...", + SetFlag: 8, // asfDefaultRipple +}; + +// --- URITokenMint:创建 NFT --- +const mint = { + TransactionType: "URITokenMint", + Account: "rCreator...", + URI: "68747470733A2F2F...", // 十六进制 URL + Flags: 1, // tfBurnable +}; + +console.log("每种类型都有自己的专属字段。"); +console.log("它们都共享:TransactionType、Account、Fee、Sequence。");`, }, }, ], slides: [ { - title: { es: "Campos comunes", en: "Common fields", jp: "共通フィールド", ko: "공통 필드" }, + title: { es: "Campos comunes", en: "Common fields", jp: "共通フィールド", ko: "공통 필드", zh: "通用字段" }, content: { es: "Toda transacción tiene:\n\n• TransactionType → Tipo de operación\n• Account → Quién envía\n• Fee → Coste (en drops, se quema)\n• Sequence → Orden de txs de la cuenta\n• LastLedgerSequence → Caducidad\n• NetworkID → Red (testnet/mainnet)", en: "Every transaction has:\n\n• TransactionType → Type of operation\n• Account → Who sends it\n• Fee → Cost (in drops, burned)\n• Sequence → Account tx ordering\n• LastLedgerSequence → Expiration\n• NetworkID → Network (testnet/mainnet)", jp: "すべてのトランザクションに含まれる:\n\n• TransactionType → 操作タイプ\n• Account → 送信者\n• Fee → コスト(drops単位、バーン)\n• Sequence → アカウントのトランザクション順序\n• LastLedgerSequence → 有効期限\n• NetworkID → ネットワーク(testnet/mainnet)", ko: "모든 트랜잭션에는 다음이 있습니다:\n\n• TransactionType → 작업 유형\n• Account → 보내는 주체\n• Fee → 비용 (drops, 소각됨)\n• Sequence → 계정 tx 순서\n• LastLedgerSequence → 만료 시점\n• NetworkID → 네트워크 (testnet/mainnet)", + zh: "每笔交易都包含:\n\n• TransactionType → 操作类型\n• Account → 发送方\n• Fee → 成本(单位:drops,会被销毁)\n• Sequence → 账户交易顺序\n• LastLedgerSequence → 过期上限\n• NetworkID → 网络(testnet/mainnet)", }, visual: "📝", }, { - title: { es: "Tipos de transacción", en: "Transaction types", jp: "トランザクションタイプ", ko: "트랜잭션 유형" }, + title: { es: "Tipos de transacción", en: "Transaction types", jp: "トランザクションタイプ", ko: "트랜잭션 유형", zh: "交易类型" }, content: { es: "• Payment → Enviar XAH o tokens\n• TrustSet → Trust lines\n• OfferCreate/Cancel → DEX\n• AccountSet → Configurar cuenta\n• SetHook → Smart contracts\n• URITokenMint/Buy → NFTs\n• EscrowCreate/Finish → Pagos condicionales", en: "• Payment → Send XAH or tokens\n• TrustSet → Trust lines\n• OfferCreate/Cancel → DEX\n• AccountSet → Configure account\n• SetHook → Smart contracts\n• URITokenMint/Buy → NFTs\n• EscrowCreate/Finish → Conditional payments", jp: "• Payment → XAHまたはトークンの送金\n• TrustSet → トラストライン\n• OfferCreate/Cancel → DEX\n• AccountSet → アカウント設定\n• SetHook → スマートコントラクト\n• URITokenMint/Buy → NFT\n• EscrowCreate/Finish → 条件付き支払い", ko: "• Payment → XAH 또는 토큰 전송\n• TrustSet → Trust line\n• OfferCreate/Cancel → DEX\n• AccountSet → 계정 설정\n• SetHook → 스마트 컨트랙트\n• URITokenMint/Buy → NFT\n• EscrowCreate/Finish → 조건부 결제", + zh: "• Payment → 发送 XAH 或代币\n• TrustSet → Trust lines\n• OfferCreate/Cancel → DEX\n• AccountSet → 配置账户\n• SetHook → 智能合约\n• URITokenMint/Buy → NFT\n• EscrowCreate/Finish → 条件支付", }, visual: "📦", }, { - title: { es: "Fee, Sequence y Flags", en: "Fee, Sequence and Flags", jp: "Fee、Sequence、Flags", ko: "Fee, Sequence, Flags" }, + title: { es: "Fee, Sequence y Flags", en: "Fee, Sequence and Flags", jp: "Fee、Sequence、Flags", ko: "Fee, Sequence, Flags", zh: "Fee、Sequence 与 Flags" }, content: { es: "Fee: 12 drops base (~gratis), se quema\n\nSequence: contador incremental\n• Garantiza orden de ejecución\n• Sin huecos: txs quedan en cola\n\nFlags: modifican comportamiento\n• Se combinan sumando valores\n• Cada tipo tiene sus flags propios", en: "Fee: 12 drops base (~free), burned\n\nSequence: incremental counter\n• Ensures execution order\n• No gaps: txs are queued\n\nFlags: modify behavior\n• Combined by adding values\n• Each type has its own flags", jp: "Fee:12 drops基本(ほぼ無料)、バーン\n\nSequence:インクリメンタルカウンター\n• 実行順序を保証\n• 欠番はキューに入る\n\nFlags:動作を変更\n• 値を加算して組み合わせ\n• 各タイプ固有のフラグ", ko: "Fee: 기본 12 drops (~거의 무료), 소각됨\n\nSequence: 증가 카운터\n• 실행 순서를 보장\n• 중간 번호가 비면 tx가 대기함\n\nFlags: 동작을 수정\n• 값을 더해 조합\n• 각 유형마다 고유 flag 보유", + zh: "Fee:基础 12 drops(几乎免费),会被销毁\n\nSequence:递增计数器\n• 保证执行顺序\n• 不能跳号,否则交易会排队\n\nFlags:修改行为\n• 通过数值相加组合\n• 每种交易都有自己的 flags", }, visual: "🔢", }, @@ -1425,6 +1757,7 @@ console.log("공통 필드: TransactionType, Account, Fee, Sequence.");`, en: "Digital signature and serialization", jp: "デジタル署名とシリアライゼーション", ko: "디지털 서명과 직렬화", + zh: "数字签名与序列化", }, theory: { es: `La firma digital es el mecanismo que garantiza que **solo tú puedes autorizar transacciones** desde tu cuenta. Entender cómo funciona te ayudará a comprender la seguridad de Xahau y a depurar problemas de firma. @@ -1727,6 +2060,81 @@ Xahau는 **멀티서명**을 지원합니다. **여러 계정의 서명**이 필 - 각 서명자가 트랜잭션에 개별 서명합니다 - 서명들을 합쳐 함께 전송합니다 - 공동 계정, DAO, 추가 보안에 유용합니다`, + zh: `数字签名是一种机制,用来保证**只有你本人可以从自己的账户授权交易**。理解它的工作方式,有助于你掌握 Xahau 的安全模型,并排查签名相关问题。 + +### 什么是数字签名? + +数字签名是一种数学证明,用来说明: +1. **这笔交易是你创建的**(认证) +2. **签名后没有人修改它**(完整性) +3. **你无法否认自己签过名**(不可抵赖) + +### Xahau 中的签名算法 + +Xahau 支持两种加密算法: + +| 算法 | 种子前缀 | 特点 | +|---|---|---| +| **ed25519** | sEd... | 更快、更现代、推荐使用 | +| **secp256k1** | s...(不带 Ed) | 与 Bitcoin/Ethereum 兼容,较早期 | + +当你使用 \`Wallet.generate()\` 创建钱包时,默认会使用 **ed25519**。以 \`sEd\` 开头的种子就是 ed25519。 + +### 签名过程逐步说明 + +1. **序列化**:交易(JSON 对象)会按照 Xahau 协议转换为**二进制格式**。每个字段都有自己的类型编码和固定顺序。 + +2. **哈希**:序列化后的二进制数据会经过哈希函数(SHA-512 half),得到一个 **32 字节摘要**。 + +3. **签名**:你的私钥会对这个哈希生成加密签名。该签名只能用你的公钥验证。 + +4. **组装**:签名(\`TxnSignature\`)和你的公钥(\`SigningPubKey\`)会被加入已序列化交易中,生成最终的 **tx_blob**。 + +### tx_blob:可直接发送的交易 + +\`tx_blob\` 是一个十六进制字符串,包含了**整笔交易**(字段 + 签名)的二进制表示。这才是真正发送到网络上的内容: + +\`\`\` +wallet.sign(prepared) +// Returns: { tx_blob: "1200002280000000...", hash: "A1B2C3..." } +\`\`\` + +- **tx_blob**:序列化并签名后的交易(十六进制) +- **hash**:交易的唯一标识符(之后可用来查询) + +### 签名验证 + +当节点收到你的 tx_blob 时: + +1. 它会反序列化 blob,提取各个字段 +2. 提取 \`SigningPubKey\` 和 \`TxnSignature\` +3. 验证签名是否与数据和公钥匹配 +4. 验证该公钥是否对应 \`Account\` 地址 +5. 如果全部一致,交易就是有效的 + +如果有人改动 tx_blob 中**哪怕一个 bit**,签名都会失效,交易会被拒绝。 + +### 离线签名 + +你可以在**没有网络连接**的情况下签名交易: + +1. 在联网设备上:用 \`autofill()\` 准备交易 +2. 把准备好的交易复制到离线设备 +3. 在离线设备上:用 \`wallet.sign()\` 完成签名 +4. 把 \`tx_blob\` 再复制回联网设备 +5. 用 \`client.submit(tx_blob)\` 发送 + +这对**冷钱包**非常有用,因为私钥永远不会接触联网设备。 + +### 多重签名(MultiSign) + +Xahau 支持**多重签名**:一笔交易需要**多个账户共同签名**才有效。它通过 \`SignerListSet\` 配置: + +- 你可以定义一个签名人列表(SignerList)及其权重 +- 设置最小法定票数(quorum) +- 每个签名人分别对交易签名 +- 所有签名会组合后一起提交 +- 适用于共享账户、DAO 或额外安全防护`, }, codeBlocks: [ { @@ -1735,6 +2143,7 @@ Xahau는 **멀티서명**을 지원합니다. **여러 계정의 서명**이 필 en: "Signing and verifying the tx_blob", jp: "tx_blobの署名と検証", ko: "tx_blob 서명과 검증", + zh: "tx_blob 的签名与验证", }, language: "javascript", code: { @@ -2001,6 +2410,72 @@ async function detailedSigning() { await client.disconnect(); } +detailedSigning().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function detailedSigning() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + console.log("=== 钱包信息 ==="); + console.log("地址:", wallet.address); + console.log("公钥:", wallet.publicKey); + console.log("算法:", wallet.publicKey.startsWith("ED") ? "ed25519" : "secp256k1"); + + // 构建并准备交易 + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: "1000000", + }; + + const prepared = await client.autofill(tx); + + // 签名 + const signed = wallet.sign(prepared); + + console.log("=== 签名结果 ==="); + console.log("Hash(交易 ID):", signed.hash); + console.log("完整 tx_blob:", signed.tx_blob); + console.log("长度:", signed.tx_blob.length, "个十六进制字符"); + console.log("大小:", signed.tx_blob.length / 2, "bytes"); + + // 验证交易是否有效 + // (节点在收到 submit 时会在内部执行) + console.log("=== 验证 ==="); + + // 尝试按 hash 查询以检查状态 + const decoded = client.request({ + command: "tx", + transaction: signed.hash, + }).catch(() => { + // 交易还未进入账本,这是正常现象 + console.log("该交易尚未提交到网络(当前仅完成签名)。"); + }); + + // 提交 + console.log("正在将 tx_blob 发送到节点..."); + const result = await client.submitAndWait(signed.tx_blob); + console.log("结果:", result.result.meta.TransactionResult); + + // 现在可以通过 hash 查询 + const txInfo = await client.request({ + command: "tx", + transaction: signed.hash, + }); + + console.log("=== 账本中的交易 ==="); + console.log("类型:", txInfo.result.TransactionType); + console.log("SigningPubKey:", txInfo.result.SigningPubKey); + console.log("Ledger:", txInfo.result.ledger_index); + + await client.disconnect(); +} + detailedSigning().catch(console.error);`, }, }, @@ -2010,6 +2485,7 @@ detailedSigning().catch(console.error);`, en: "Offline signing: prepare on one side, sign on another", jp: "オフライン署名:一方で準備し、他方で署名", ko: "오프라인 서명: 한쪽에서 준비, 다른 쪽에서 서명", + zh: "离线签名:一边准备,另一边签名", }, language: "javascript", code: { @@ -2284,38 +2760,109 @@ async function demo() { await sendOnline(signed.tx_blob); } +demo().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +// ============================================= +// 第 1 步:在联网设备上 +// 准备交易(需要联网) +// ============================================= +async function prepareOnline() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const tx = { + TransactionType: "Payment", + Account: "rYourAddressHere", + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: "10000000", // 10 XAH + }; + + const prepared = await client.autofill(tx); + await client.disconnect(); + + // 保存为 JSON,以便传到离线设备 + const txToSign = JSON.stringify(prepared, null, 2); + console.log("=== 把这段 JSON 复制到离线设备 ==="); + console.log(txToSign); + + return prepared; +} + +// ============================================= +// 第 2 步:在离线设备上(无网络) +// 对交易进行签名 +// ============================================= +function signOffline(preparedJSON) { + // 私钥只存在于离线设备 + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + const signed = wallet.sign(preparedJSON); + + console.log("=== 把这个 tx_blob 复制回联网设备 ==="); + console.log("tx_blob:", signed.tx_blob); + console.log("hash:", signed.hash); + + return signed; +} + +// ============================================= +// 第 3 步:在联网设备上 +// 提交已签名交易 +// ============================================= +async function sendOnline(txBlob) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const result = await client.submitAndWait(txBlob); + console.log("结果:", result.result.meta.TransactionResult); + + await client.disconnect(); +} + +// 完整流程演示(为了简单放在一个脚本里) +async function demo() { + const prepared = await prepareOnline(); + const signed = signOffline(prepared); + await sendOnline(signed.tx_blob); +} + demo().catch(console.error);`, }, }, ], slides: [ { - title: { es: "¿Qué es una firma digital?", en: "What is a digital signature?", jp: "デジタル署名とは?", ko: "디지털 서명이란?" }, + title: { es: "¿Qué es una firma digital?", en: "What is a digital signature?", jp: "デジタル署名とは?", ko: "디지털 서명이란?", zh: "什么是数字签名?" }, content: { es: "Prueba matemática de que:\n\n• Tú creaste la transacción (autenticación)\n• Nadie la modificó (integridad)\n• No puedes negar haberla firmado (no repudio)\n\nAlgoritmos: ed25519 (sEd...) o secp256k1 (s...)", en: "Mathematical proof that:\n\n• You created the transaction (authentication)\n• No one modified it (integrity)\n• You cannot deny having signed it (non-repudiation)\n\nAlgorithms: ed25519 (sEd...) or secp256k1 (s...)", jp: "以下を証明する数学的証明:\n\n• あなたがトランザクションを作成(認証)\n• 誰も変更していない(完全性)\n• 署名したことを否定できない(否認不可)\n\nアルゴリズム:ed25519(sEd...)またはsecp256k1(s...)", ko: "다음을 증명하는 수학적 증거:\n\n• 본인이 트랜잭션을 만들었다 (인증)\n• 누구도 수정하지 않았다 (무결성)\n• 서명 사실을 부인할 수 없다 (부인 방지)\n\n알고리즘: ed25519 (sEd...) 또는 secp256k1 (s...)", + zh: "一种数学证明,用来说明:\n\n• 交易是你创建的(认证)\n• 没有人修改过它(完整性)\n• 你无法否认自己签过名(不可抵赖)\n\n算法:ed25519(sEd...)或 secp256k1(s...)", }, visual: "🔏", }, { - title: { es: "El proceso de firma", en: "The signing process", jp: "署名プロセス", ko: "서명 과정" }, + title: { es: "El proceso de firma", en: "The signing process", jp: "署名プロセス", ko: "서명 과정", zh: "签名过程" }, content: { es: "1. Serializar → JSON a binario\n2. Hash → SHA-512 half (32 bytes)\n3. Firmar → Clave privada genera firma\n4. Ensamblar → tx_blob (hex)\n\nwallet.sign(prepared)\n→ { tx_blob: \"1200...\", hash: \"A1B2...\" }", en: "1. Serialize → JSON to binary\n2. Hash → SHA-512 half (32 bytes)\n3. Sign → Private key generates signature\n4. Assemble → tx_blob (hex)\n\nwallet.sign(prepared)\n→ { tx_blob: \"1200...\", hash: \"A1B2...\" }", jp: "1. シリアライズ → JSONをバイナリに\n2. ハッシュ → SHA-512 half(32バイト)\n3. 署名 → 秘密鍵が署名を生成\n4. アセンブリ → tx_blob(16進数)\n\nwallet.sign(prepared)\n→ { tx_blob: \"1200...\", hash: \"A1B2...\" }", ko: "1. 직렬화 → JSON을 바이너리로\n2. 해시 → SHA-512 half (32 bytes)\n3. 서명 → 개인 키로 서명 생성\n4. 조립 → tx_blob (hex)\n\nwallet.sign(prepared)\n→ { tx_blob: \"1200...\", hash: \"A1B2...\" }", + zh: "1. 序列化 → JSON 转为二进制\n2. 哈希 → SHA-512 half(32 bytes)\n3. 签名 → 私钥生成签名\n4. 组装 → tx_blob(hex)\n\nwallet.sign(prepared)\n→ { tx_blob: \"1200...\", hash: \"A1B2...\" }", }, visual: "🔐", }, { - title: { es: "Firma offline y multi-firma", en: "Offline signing and multi-signing", jp: "オフライン署名とマルチ署名", ko: "오프라인 서명과 멀티서명" }, + title: { es: "Firma offline y multi-firma", en: "Offline signing and multi-signing", jp: "オフライン署名とマルチ署名", ko: "오프라인 서명과 멀티서명", zh: "离线签名与多重签名" }, content: { es: "Firma offline (cold wallet):\n• Preparar online → Firmar offline → Enviar online\n• Claves nunca tocan internet\n\nMulti-firma (MultiSign):\n• Múltiples firmantes con pesos\n• Quórum mínimo configurable\n• Ideal para cuentas compartidas", en: "Offline signing (cold wallet):\n• Prepare online → Sign offline → Submit online\n• Keys never touch the internet\n\nMulti-signing (MultiSign):\n• Multiple signers with weights\n• Configurable minimum quorum\n• Ideal for shared accounts", jp: "オフライン署名(コールドウォレット):\n• オンライン準備 → オフライン署名 → オンライン送信\n• 秘密鍵がインターネットに触れない\n\nマルチ署名(MultiSign):\n• 重み付き複数署名者\n• 設定可能な最小クォーラム\n• 共有アカウントに最適", ko: "오프라인 서명 (콜드 월렛):\n• 온라인 준비 → 오프라인 서명 → 온라인 제출\n• 키가 인터넷에 닿지 않음\n\n멀티서명 (MultiSign):\n• 가중치를 가진 여러 서명자\n• 최소 quorum 설정 가능\n• 공동 계정에 적합", + zh: "离线签名(冷钱包):\n• 在线准备 → 离线签名 → 在线提交\n• 私钥永不接触互联网\n\n多重签名(MultiSign):\n• 多个签名人及其权重\n• 最小 quorum 可配置\n• 非常适合共享账户", }, visual: "🧊", }, @@ -2328,6 +2875,7 @@ demo().catch(console.error);`, en: "Submission, validation and results", jp: "送信、検証、結果", ko: "제출, 검증, 결과", + zh: "提交、验证与结果", }, theory: { es: `Una vez firmada la transacción, hay que enviarla a la red y entender los posibles resultados. Xahau tiene un sistema de **códigos de resultado** muy detallado que te indica exactamente qué pasó. @@ -2669,6 +3217,91 @@ result.result.meta.TransactionResult → 코드 (tesSUCCESS 등) result.result.meta.AffectedNodes → ledger에서 무엇이 바뀌었는지 result.result.ledger_index → 어떤 ledger에 포함됐는지 result.result.hash → 고유 트랜잭션 hash +\`\`\``, + zh: `交易签名完成后,下一步就是将它发送到网络,并理解可能出现的结果。Xahau 拥有一套非常详细的**结果代码**系统,可以准确告诉你发生了什么。 + +### submit 与 submitAndWait + +\`xahau\` 库提供两种提交交易的方法: + +**client.submit(tx_blob)**: +- 发送交易后会**立即**返回 +- 返回的是初步结果,只表示节点是否接受了该交易(不代表已验证) +- 你之后还需要通过 \`tx\` 查询最终结果 +- 适合需要快速发送大量交易的场景 + +**client.submitAndWait(tx_blob)**: +- 发送交易后会**等待**它被写入已验证账本 +- 直接返回最终结果 +- 对大多数场景更方便 +- 通常需要 3 到 10 秒(1 到 2 个账本周期) + +### 结果代码分类 + +交易结果会根据其**前缀**分成不同类别: + +### tes:成功 + +\`tesSUCCESS\` 是唯一的成功代码,表示交易已被正确处理,变更已写入账本。 + +### tec:交易已被纳入,但执行失败 + +\`tec\` 代码表示交易**已经被写入账本**(而且 fee 已收取),但实际操作**没有执行成功**: + +| 代码 | 含义 | +|---|---| +| **tecUNFUNDED_PAYMENT** | 余额不足,无法完成支付 | +| **tecNO_LINE** | 该代币不存在 trust line | +| **tecNO_DST** | 目标账户不存在 | +| **tecDST_TAG_NEEDED** | 目标账户要求 DestinationTag | +| **tecNO_PERMISSION** | 你没有执行该操作的权限 | +| **tecINSUFFICIENT_RESERVE** | 没有足够的 XAH 来满足新对象的 reserve | +| **tecPATH_DRY** | 没有找到可行的支付路径 | +| **tecKILLED** | 报价因 tfFillOrKill 标志被取消 | + +**重要**:出现 \`tec\` 错误时,即使操作失败,fee **仍然会被收取**。 + +### tef:处理前被拒绝 + +\`tef\` 代码表示交易在**真正处理之前就被拒绝**。这种情况下 fee **不会被收取**: + +| 代码 | 含义 | +|---|---| +| **tefPAST_SEQ** | Sequence 已被使用(重复交易) | +| **tefMAX_LEDGER** | LastLedgerSequence 已经过期 | +| **tefALREADY** | 该交易已在队列中 | + +### tem:格式错误 + +\`tem\` 代码表示交易**格式不正确**,因此永远不可能有效: + +| 代码 | 含义 | +|---|---| +| **temMALFORMED** | 字段无效或格式错误 | +| **temBAD_AMOUNT** | 金额无效(负数、XAH 为 0 等) | +| **temBAD_FEE** | Fee 无效 | +| **temDISABLED** | 当前网络禁用了该功能 | +| **temINVALID_FLAG** | 此类交易使用了无效 flag | + +### ter:临时错误(可重试) + +\`ter\` 代码表示一种**临时性**错误,稍后重试可能解决: + +| 代码 | 含义 | +|---|---| +| **terPRE_SEQ** | 前一个 Sequence 的交易仍在等待中 | +| **terQUEUED** | 交易正在队列中等待(同时在途交易过多) | +| **terINSUF_FEE_B** | 以当前网络负载来看 fee 不足 | + +### 读取完整结果 + +结果对象中包含了你需要的全部信息: + +\`\`\` +result.result.meta.TransactionResult → 结果代码(tesSUCCESS 等) +result.result.meta.AffectedNodes → 账本中发生了哪些变化 +result.result.ledger_index → 被纳入了哪个账本 +result.result.hash → 交易唯一 hash \`\`\``, }, codeBlocks: [ @@ -2678,6 +3311,7 @@ result.result.hash → 고유 트랜잭션 hash en: "Handle all result types", jp: "すべての結果タイプの処理", ko: "모든 결과 유형 처리하기", + zh: "处理所有结果类型", }, language: "javascript", code: { @@ -2984,6 +3618,82 @@ async function sendChecking() { await client.disconnect(); } +sendChecking().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function sendChecking() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: "1000000", + }; + + try { + const prepared = await client.autofill(tx); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const code = result.result.meta.TransactionResult; + + // 按类别分析结果 + if (code === "tesSUCCESS") { + console.log("成功:交易已被正确处理。"); + console.log("Ledger:", result.result.ledger_index); + console.log("Hash:", signed.hash); + + } else if (code.startsWith("tec")) { + // 交易已被写入账本,但操作失败 + // fee 已收取 + console.log("失败 (tec):", code); + console.log("操作未执行成功,但 fee 已被收取。"); + + // 具体诊断 + switch (code) { + case "tecUNFUNDED_PAYMENT": + console.log("→ 余额不足。"); + break; + case "tecNO_DST": + console.log("→ 目标账户不存在。"); + break; + case "tecDST_TAG_NEEDED": + console.log("→ 缺少 DestinationTag。"); + break; + case "tecINSUFFICIENT_RESERVE": + console.log("→ 没有足够的 XAH 用于 reserve。"); + break; + default: + console.log("→ 请查阅文档了解:", code); + } + + } else if (code.startsWith("tef")) { + console.log("被拒绝 (tef):", code); + console.log("交易在处理前就被拒绝了。"); + console.log("fee 未被收取。"); + + } else if (code.startsWith("tem")) { + console.log("格式错误 (tem):", code); + console.log("交易存在格式问题。"); + console.log("请检查字段和值。"); + + } else if (code.startsWith("ter")) { + console.log("临时错误 (ter):", code); + console.log("你可以几秒后重试。"); + } + + } catch (error) { + console.error("连接或提交错误:", error.message); + } + + await client.disconnect(); +} + sendChecking().catch(console.error);`, }, }, @@ -2991,32 +3701,35 @@ sendChecking().catch(console.error);`, ], slides: [ { - title: { es: "submit vs submitAndWait", en: "submit vs submitAndWait", jp: "submit対submitAndWait", ko: "submit vs submitAndWait" }, + title: { es: "submit vs submitAndWait", en: "submit vs submitAndWait", jp: "submit対submitAndWait", ko: "submit vs submitAndWait", zh: "submit 与 submitAndWait" }, content: { es: "submit():\n• Envía y devuelve inmediatamente\n• Resultado preliminar (no final)\n• Rápido, para enviar muchas txs\n\nsubmitAndWait():\n• Envía y espera validación (3-10s)\n• Resultado final directo\n• Recomendado para la mayoría de casos", en: "submit():\n• Sends and returns immediately\n• Preliminary result (not final)\n• Fast, for sending many txs\n\nsubmitAndWait():\n• Sends and waits for validation (3-10s)\n• Direct final result\n• Recommended for most cases", jp: "submit():\n• 送信して即座に返す\n• 暫定結果(最終でない)\n• 高速、多くのtxを送信する場合\n\nsubmitAndWait():\n• 送信して検証を待つ(3〜10秒)\n• 直接最終結果\n• ほとんどの場合に推奨", ko: "submit():\n• 전송 후 즉시 반환\n• 예비 결과 (최종 아님)\n• 빠르며 많은 tx 전송에 적합\n\nsubmitAndWait():\n• 전송 후 검증까지 대기 (3~10초)\n• 최종 결과 바로 반환\n• 대부분의 경우 권장", + zh: "submit():\n• 发送后立即返回\n• 返回初步结果(不是最终结果)\n• 很快,适合批量发送交易\n\nsubmitAndWait():\n• 发送后等待验证(3 到 10 秒)\n• 直接拿到最终结果\n• 适合大多数场景", }, visual: "📤", }, { - title: { es: "Códigos de resultado", en: "Result codes", jp: "結果コード", ko: "결과 코드" }, + title: { es: "Códigos de resultado", en: "Result codes", jp: "結果コード", ko: "결과 코드", zh: "结果代码" }, content: { es: "• tesSUCCESS → Éxito\n• tec... → Incluida pero falló (fee cobrado)\n• tef... → Rechazada (fee NO cobrado)\n• tem... → Mal formada (error de formato)\n• ter... → Error temporal (reintentar)\n\nSiempre verifica meta.TransactionResult", en: "• tesSUCCESS → Success\n• tec... → Included but failed (fee charged)\n• tef... → Rejected (fee NOT charged)\n• tem... → Malformed (format error)\n• ter... → Temporary error (retry)\n\nAlways check meta.TransactionResult", jp: "• tesSUCCESS → 成功\n• tec... → 含まれたが失敗(Fee徴収)\n• tef... → 拒否(Fee未徴収)\n• tem... → 不正形式(フォームエラー)\n• ter... → 一時的エラー(リトライ)\n\n常にmeta.TransactionResultを確認", ko: "• tesSUCCESS → 성공\n• tec... → 포함됐지만 실패 (fee 청구)\n• tef... → 거부됨 (fee 미청구)\n• tem... → 형식 오류\n• ter... → 일시적 오류 (재시도)\n\n항상 meta.TransactionResult를 확인하세요", + zh: "• tesSUCCESS → 成功\n• tec... → 已纳入账本但执行失败(fee 已收)\n• tef... → 被拒绝(fee 未收)\n• tem... → 格式错误\n• ter... → 临时错误(可重试)\n\n始终检查 meta.TransactionResult", }, visual: "🏷️", }, { - title: { es: "Errores tec más comunes", en: "Most common tec errors", jp: "最も一般的なtecエラー", ko: "가장 흔한 tec 오류" }, + title: { es: "Errores tec más comunes", en: "Most common tec errors", jp: "最も一般的なtecエラー", ko: "가장 흔한 tec 오류", zh: "最常见的 tec 错误" }, content: { es: "• tecUNFUNDED_PAYMENT → Sin balance\n• tecNO_DST → Destino no existe\n• tecDST_TAG_NEEDED → Falta tag\n• tecNO_LINE → Sin trust line\n• tecINSUFFICIENT_RESERVE → Sin reserva\n• tecPATH_DRY → Sin ruta de pago\n\nEl fee SE cobra en errores tec", en: "• tecUNFUNDED_PAYMENT → No balance\n• tecNO_DST → Destination doesn't exist\n• tecDST_TAG_NEEDED → Missing tag\n• tecNO_LINE → No trust line\n• tecINSUFFICIENT_RESERVE → No reserve\n• tecPATH_DRY → No payment path\n\nThe fee IS charged on tec errors", jp: "• tecUNFUNDED_PAYMENT → 残高なし\n• tecNO_DST → 宛先が存在しない\n• tecDST_TAG_NEEDED → タグなし\n• tecNO_LINE → トラストラインなし\n• tecINSUFFICIENT_RESERVE → リザーブなし\n• tecPATH_DRY → 支払いルートなし\n\ntecエラーではFeeが徴収される", ko: "• tecUNFUNDED_PAYMENT → 잔액 부족\n• tecNO_DST → 목적지 없음\n• tecDST_TAG_NEEDED → 태그 누락\n• tecNO_LINE → trust line 없음\n• tecINSUFFICIENT_RESERVE → reserve 부족\n• tecPATH_DRY → 결제 경로 없음\n\ntec 오류에서는 fee가 청구됩니다", + zh: "• tecUNFUNDED_PAYMENT → 余额不足\n• tecNO_DST → 目标不存在\n• tecDST_TAG_NEEDED → 缺少标签\n• tecNO_LINE → 没有 trust line\n• tecINSUFFICIENT_RESERVE → reserve 不足\n• tecPATH_DRY → 没有支付路径\n\n出现 tec 错误时,fee 依然会被收取", }, visual: "⚠️", }, @@ -3029,6 +3742,7 @@ sendChecking().catch(console.error);`, en: "Transactions at the ledger level", jp: "レジャーレベルのトランザクション", ko: "레저 수준의 트랜잭션", + zh: "账本层级的交易", }, theory: { es: `Para entender realmente cómo funcionan las transacciones, necesitas ver lo que ocurre **dentro del ledger** cuando una transacción se procesa. Esto te ayudará a depurar problemas complejos y a entender la metadata. @@ -3467,6 +4181,115 @@ ledger가 닫히면 다음을 요약하는 **hash**가 계산됩니다: - 완전한 ledger 상태 (state tree) 검증자가 UNL의 80%와 다른 hash를 계산하면 그 ledger는 버려집니다. 이것이 네트워크 일관성을 보장합니다.`, + zh: `如果你想真正理解交易是如何工作的,就需要看到交易被处理时**账本内部发生了什么**。这能帮助你调试复杂问题,也能更好理解 metadata。 + +### 一笔交易如何修改账本? + +当一笔交易被成功处理后,它会修改**账本状态**,也就是账本数据库中存储的对象。这些变化会记录在交易的 **metadata** 中。 + +### AffectedNodes:交易留下的痕迹 + +\`meta.AffectedNodes\` 字段是一个数组,用来描述账本中**到底发生了哪些变化**。每个受影响节点都属于以下三种类型之一: + +### CreatedNode:新对象 + +账本中新建了一个对象: + +\`\`\` +{ + "CreatedNode": { + "LedgerEntryType": "RippleState", // 对象类型 + "LedgerIndex": "ABC123...", // 对象唯一 ID + "NewFields": { // 新对象的字段 + "Balance": { "value": "100" }, + "LowLimit": { ... }, + "HighLimit": { ... } + } + } +} +\`\`\` + +例如:新的 trust line、新的 DEX 报价、新的 URIToken。 + +### ModifiedNode:已修改对象 + +一个已有对象被修改: + +\`\`\` +{ + "ModifiedNode": { + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "DEF456...", + "PreviousFields": { // 修改前状态 + "Balance": "100000000" + }, + "FinalFields": { // 修改后状态 + "Balance": "95000000", + "Sequence": 43 + } + } +} +\`\`\` + +\`PreviousFields\` 只展示**发生变化的字段**,而不是对象的全部字段。\`FinalFields\` 展示修改后的完整状态。 + +### DeletedNode:已删除对象 + +一个对象从账本中被移除: + +\`\`\` +{ + "DeletedNode": { + "LedgerEntryType": "Offer", + "LedgerIndex": "GHI789...", + "FinalFields": { // 删除时的状态 + "TakerPays": "0", + "TakerGets": "0" + } + } +} +\`\`\` + +例如:报价已完成或取消、trust line 被删除(余额为 0)、URIToken 被销毁。 + +### Balance changes:追踪资金流动 + +在 Payment 交易中,你可以通过查看 \`AccountRoot\` 类型的 \`ModifiedNode\`,准确追踪资金如何流动: + +- 源账户:\`Balance\` 减少(发送了 XAH) +- 目标账户:\`Balance\` 增加(收到了 XAH) +- 两边余额差额等于 \`Amount\` + \`Fee\` + +如果是代币(IOU),变化会出现在 \`RippleState\` 类型的 \`ModifiedNode\` 中。 + +### Reserves:reserve 系统 + +Xahau 账本使用一套会影响可用余额的 **reserve** 系统: + +- **基础 reserve**:1 XAH —— 账户存在所需的最低值 +- **对象 reserve**:账户每拥有一个对象,就要额外占用 0.2 XAH + +账本中的每个对象(trust line、报价、URIToken、Hook)都会提高 reserve。被占用的 XAH 在删除对象之前无法使用。 + +### 一个账本中的处理顺序 + +在同一个账本内,交易会按**确定性的顺序**处理: + +1. 交易按**规范哈希(canonical hash)**排序,而不是按 Sequence 或发送时间排序 +2. 按该顺序逐笔处理 +3. 每笔交易都会看到前一笔交易执行后的账本状态 +4. 如果两笔交易争夺同一资源,哈希顺序靠前的那笔会获胜 + +这保证了**所有验证者都会计算出完全相同的结果**,无论他们接收交易的顺序如何。 + +### Ledger hash + +一个账本关闭时,会计算一个**hash** 来汇总: +- 上一个账本的 hash(账本链) +- 所有被纳入的交易及其 metadata +- 完整账本状态(state tree) + +如果某个验证者算出的 hash 与 UNL 中 80% 的结果不一致,它的账本就会被丢弃。这就是网络一致性的保障。`, }, codeBlocks: [ { @@ -3475,6 +4298,7 @@ ledger가 닫히면 다음을 요약하는 **hash**가 계산됩니다: en: "Analyze a transaction's AffectedNodes", jp: "トランザクションのAffectedNodesを分析", ko: "트랜잭션의 AffectedNodes 분석", + zh: "分析交易的 AffectedNodes", }, language: "javascript", code: { @@ -3833,6 +4657,95 @@ async function analyzeMetadata() { await client.disconnect(); } +analyzeMetadata().catch(console.error);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function analyzeMetadata() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 发送一笔 payment 以分析其 metadata + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: "5000000", // 5 XAH + }; + + const prepared = await client.autofill(tx); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const meta = result.result.meta; + console.log("=== METADATA 分析 ==="); + console.log("结果:", meta.TransactionResult); + console.log("受影响节点数:", meta.AffectedNodes.length); + + // 对受影响节点分类 + const created = []; + const modified = []; + const deleted = []; + + for (const node of meta.AffectedNodes) { + if (node.CreatedNode) { + created.push(node.CreatedNode); + } else if (node.ModifiedNode) { + modified.push(node.ModifiedNode); + } else if (node.DeletedNode) { + deleted.push(node.DeletedNode); + } + } + + // 显示新建对象 + if (created.length > 0) { + console.log("--- 新建对象 ---"); + for (const n of created) { + console.log(" +", n.LedgerEntryType); + console.log(" Index:", n.LedgerIndex); + } + } + + // 显示修改对象 + if (modified.length > 0) { + console.log("--- 已修改对象 ---"); + for (const n of modified) { + console.log(" ~", n.LedgerEntryType); + if (n.PreviousFields && n.FinalFields) { + // 显示余额变化(AccountRoot) + if (n.PreviousFields.Balance && n.FinalFields.Balance) { + const before = Number(n.PreviousFields.Balance) / 1000000; + const after = Number(n.FinalFields.Balance) / 1000000; + const diff = after - before; + console.log(" 余额:", before, "→", after, "XAH"); + console.log(" 变化:", diff > 0 ? "+" : "", diff.toFixed(6), "XAH"); + } + // 显示 Sequence 变化 + if (n.FinalFields.Sequence) { + console.log(" Sequence:", n.FinalFields.Sequence); + } + } + } + } + + // 显示删除对象 + if (deleted.length > 0) { + console.log("--- 已删除对象 ---"); + for (const n of deleted) { + console.log(" -", n.LedgerEntryType); + } + } + + // 余额摘要 + console.log("--- 摘要 ---"); + console.log("已支付 Fee:", Number(result.result.Fee) / 1000000, "XAH"); + console.log("该 Fee 已被销毁(不会进入任何账户)。"); + + await client.disconnect(); +} + analyzeMetadata().catch(console.error);`, }, }, @@ -3842,6 +4755,7 @@ analyzeMetadata().catch(console.error);`, en: "Query your account's current reserve", jp: "アカウントの現在のリザーブを照会", ko: "계정의 현재 reserve 조회", + zh: "查询你账户当前的 reserve", }, language: "javascript", code: { @@ -4088,38 +5002,102 @@ async function checkReserve(address) { await client.disconnect(); } // 자신의 계정 또는 rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r 를 사용할 수 있습니다 +checkReserve("rYourAccountHere");`, + zh: `require("dotenv").config(); +const { Client } = require("xahau"); + +async function checkReserve(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 获取当前 reserve 的服务器信息 + const serverInfo = await client.request({ command: "server_info" }); + const ledgerInfo = serverInfo.result.info.validated_ledger; + const baseReserve = ledgerInfo.reserve_base_xrp; // 单位:XAH + const ownerReserve = ledgerInfo.reserve_inc_xrp; // 单位:XAH + + console.log("=== 网络 RESERVE ==="); + console.log("基础 reserve(每个账户):", baseReserve, "XAH"); + console.log("对象 reserve(每个对象):", ownerReserve, "XAH"); + + // 获取账户信息 + const accountInfo = await client.request({ + command: "account_info", + account: address, + ledger_index: "validated", + }); + + const account = accountInfo.result.account_data; + const balance = Number(account.Balance) / 1000000; + const ownerCount = account.OwnerCount; + const totalReserve = baseReserve + (ownerCount * ownerReserve); + const available = balance - totalReserve; + + console.log("=== 你的账户 ==="); + console.log("地址:", address); + console.log("总余额:", balance, "XAH"); + console.log("账本中的对象数:", ownerCount); + console.log("总 reserve:", totalReserve, "XAH"); + console.log(" →", baseReserve, "XAH(基础)"); + console.log(" +", ownerCount, "x", ownerReserve, "=", ownerCount * ownerReserve, "XAH(对象)"); + console.log("可用余额:", available, "XAH"); + + // 显示你拥有哪些对象 + const objects = await client.request({ + command: "account_objects", + account: address, + ledger_index: "validated", + }); + + const byType = {}; + for (const obj of objects.result.account_objects) { + const type = obj.LedgerEntryType; + byType[type] = (byType[type] || 0) + 1; + } + + console.log("=== 按类型统计对象 ==="); + for (const [type, count] of Object.entries(byType)) { + console.log(" " + type + ":", count, "(reserve:", count * ownerReserve, "XAH)"); + } + + await client.disconnect(); +} +// 你可以使用自己的账户,或 rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r checkReserve("rYourAccountHere");`, }, }, ], slides: [ { - title: { es: "AffectedNodes", en: "AffectedNodes", jp: "AffectedNodes", ko: "AffectedNodes" }, + title: { es: "AffectedNodes", en: "AffectedNodes", jp: "AffectedNodes", ko: "AffectedNodes", zh: "AffectedNodes" }, content: { es: "Cada transacción registra qué cambió:\n\n• CreatedNode → Nuevo objeto en el ledger\n• ModifiedNode → Objeto existente modificado\n (PreviousFields → FinalFields)\n• DeletedNode → Objeto eliminado\n\nLa huella exacta de la transacción", en: "Each transaction records what changed:\n\n• CreatedNode → New object in the ledger\n• ModifiedNode → Existing object modified\n (PreviousFields → FinalFields)\n• DeletedNode → Object deleted\n\nThe exact footprint of the transaction", jp: "各トランザクションが変化を記録:\n\n• CreatedNode → レジャー内の新しいオブジェクト\n• ModifiedNode → 既存オブジェクトの変更\n (PreviousFields → FinalFields)\n• DeletedNode → 削除されたオブジェクト\n\nトランザクションの正確な足跡", ko: "각 트랜잭션은 바뀐 내용을 기록합니다:\n\n• CreatedNode → ledger의 새 객체\n• ModifiedNode → 기존 객체 수정\n (PreviousFields → FinalFields)\n• DeletedNode → 삭제된 객체\n\n트랜잭션의 정확한 흔적", + zh: "每笔交易都会记录发生了什么变化:\n\n• CreatedNode → 账本中新建对象\n• ModifiedNode → 已有对象被修改\n (PreviousFields → FinalFields)\n• DeletedNode → 对象被删除\n\n这就是交易的精确痕迹", }, visual: "🔍", }, { - title: { es: "Sistema de reservas", en: "Reserve system", jp: "リザーブシステム", ko: "Reserve 시스템" }, + title: { es: "Sistema de reservas", en: "Reserve system", jp: "リザーブシステム", ko: "Reserve 시스템", zh: "Reserve 系统" }, content: { es: "Reserva base: 1 XAH por cuenta\nReserva por objeto: 0.2 XAH cada uno\n\nObjetos que consumen reserva:\n• Trust lines, Ofertas DEX\n• URITokens, Hooks\n\nEliminar objeto = liberar reserva\nDisponible = Balance - Reserva total", en: "Base reserve: 1 XAH per account\nOwner reserve: 0.2 XAH each\n\nObjects that consume reserve:\n• Trust lines, DEX Offers\n• URITokens, Hooks\n\nDelete object = free reserve\nAvailable = Balance - Total reserve", jp: "基本リザーブ:アカウントごと1 XAH\n所有者リザーブ:オブジェクトごと0.2 XAH\n\nリザーブを消費するオブジェクト:\n• トラストライン、DEXオファー\n• URIToken、Hook\n\nオブジェクト削除 = リザーブ解放\n使用可能 = 残高 - 総リザーブ", ko: "기본 reserve: 계정당 1 XAH\n객체 reserve: 객체당 0.2 XAH\n\nreserve를 소비하는 객체:\n• Trust line, DEX 오퍼\n• URIToken, Hook\n\n객체 삭제 = reserve 해제\n사용 가능 = 잔액 - 총 reserve", + zh: "基础 reserve:每个账户 1 XAH\n对象 reserve:每个对象 0.2 XAH\n\n会消耗 reserve 的对象:\n• Trust lines、DEX 报价\n• URITokens、Hooks\n\n删除对象 = 释放 reserve\n可用余额 = Balance - 总 reserve", }, visual: "💰", }, { - title: { es: "Orden y consistencia", en: "Order and consistency", jp: "順序と一貫性", ko: "순서와 일관성" }, + title: { es: "Orden y consistencia", en: "Order and consistency", jp: "順序と一貫性", ko: "순서와 일관성", zh: "顺序与一致性" }, content: { es: "Dentro de un ledger:\n\n• Txs ordenadas por hash canónico\n• Procesadas secuencialmente\n• Mismo resultado en todos los nodos\n\nHash del ledger resume:\n• Ledger anterior + Txs + Estado\n• 80% UNL debe coincidir\n• Garantiza consistencia total", en: "Within a ledger:\n\n• Txs ordered by canonical hash\n• Processed sequentially\n• Same result on all nodes\n\nLedger hash summarizes:\n• Previous ledger + Txs + State\n• 80% UNL must agree\n• Guarantees total consistency", jp: "レジャー内:\n\n• 正規ハッシュで順序付け\n• 順次処理\n• すべてのノードで同じ結果\n\nレジャーハッシュが要約:\n• 前のレジャー + トランザクション + 状態\n• UNLの80%が一致必要\n• 完全な一貫性を保証", ko: "하나의 ledger 안에서:\n\n• tx는 canonical hash 순으로 정렬\n• 순차적으로 처리\n• 모든 노드에서 같은 결과\n\nledger hash는 다음을 요약:\n• 이전 ledger + tx + 상태\n• UNL의 80%가 일치해야 함\n• 완전한 일관성 보장", + zh: "在一个 ledger 内:\n\n• 交易按 canonical hash 排序\n• 按顺序逐笔处理\n• 所有节点得到相同结果\n\nledger hash 汇总了:\n• 前一个 ledger + 交易 + 状态\n• 必须有 80% 的 UNL 一致\n• 这保证了完整一致性", }, visual: "🔗", }, diff --git a/src/data/modules/m06-payments.js b/src/data/modules/m06-payments.js index d40f339..b80b679 100644 --- a/src/data/modules/m06-payments.js +++ b/src/data/modules/m06-payments.js @@ -6,6 +6,7 @@ export default { en: "Creating and using payments", jp: "支払いの作成と使用", ko: "결제 생성 및 사용", + zh: "支付的创建与使用", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "Anatomy of a payment transaction", jp: "支払いトランザクションの解剖", ko: "결제 트랜잭션의 구조", + zh: "支付交易剖析", }, theory: { es: `El **Payment** es la transacción más fundamental de Xahau. Permite enviar XAH (o tokens) de una cuenta a otra. @@ -251,6 +253,63 @@ Payment 트랜잭션에는 여기서 다루는 것보다 훨씬 더 많은 선 - 사용 가능한 플래그 (tfPartialPayment, tfLimitQuality 등) - 오류 코드의 전체 목록 및 원인 - 특수 사례 및 고급 동작`, + zh: `**Payment** 是 Xahau 中最基础的一类交易。它允许你把 XAH(或代币)从一个账户发送到另一个账户。 + +### Payment 交易的字段 + +| 字段 | 说明 | +|---|---| +| \`TransactionType\` | 永远是 \`"Payment"\` | +| \`Account\` | 发送方地址(付款人) | +| \`Destination\` | 接收方地址 | +| \`Amount\` | 要发送的金额(原生 XAH 以 drops 表示) | +| \`Fee\` | 交易费用(单位:drops) | +| \`Sequence\` | 发送账户的序列号 | +| \`NetworkID\` | 网络标识符(Xahau 必需) | + +### Drops 与 XAH + +原生 XAH 的金额使用 **drops** 表示: +- 1 XAH = **1,000,000 drops** +- 原生 XAH 的 \`Amount\` 字段是一个表示 drops 数量的**字符串** +- 例如:\`"10000000"\` = 10 XAH + +### Fees(交易费用) + +Xahau 上的费用非常低且可预测: +- 一笔典型支付只需 **12 drops**(0.000012 XAH) +- 手续费会被**销毁**,不会给任何验证者 +- \`xahau\` 库可以通过 \`autofill()\` 自动计算费用 + +### 发送 IOU(代币)而不是原生 XAH + +发送原生 XAH 时,\`Amount\` 字段是一个表示 drops 数量的**字符串**。但发送 **IOU**(由某个账户发行的代币,例如 USD、EUR 等)时,\`Amount\` 会变成一个包含三个字段的**对象**: + +\`\`\` +{ + "currency": "USD", // 货币代码(3 个字符或 40 字节十六进制) + "issuer": "rIssuerAddress", // 发行该代币的账户 + "value": "100" // 字符串形式的金额 +} +\`\`\` + +**发送 IOU 的前提条件:** +- **发送方必须持有该 IOU 余额**:你的账户必须拥有这种 IOU。你可以通过之前的支付、DEX 交易或直接从代币发行方获得 +- **接收方必须有 TrustLine**:目标账户必须事先为该 IOU 以及相同发行方创建好 TrustLine(\`TrustSet\`)。没有 TrustLine 时,支付会因 \`tecPATH_DRY\` 或 \`tecNO_LINE\` 失败 + +### 为什么 XAH 之外的 IOU 或代币需要这些字段? + +可能会有多个实体发行同一种 IOU。例如,不同银行都可能发行自己的 EUR 或 USD 代币。如果它们共享相同的代币名称,唯一的区分方式就是指定发行方。 + +### 关于 Payment 的更多信息 + +Payment 交易还有很多这里未涉及的可选字段、flags 和潜在错误。完整参考请查看[官方文档](https://xahau.network/docs/protocol-reference/transactions/transaction-types/payment/) + +在文档中你可以找到: +- 所有可选字段(SendMax、DeliverMin、InvoiceID 等) +- 可用 flags(tfPartialPayment、tfLimitQuality 等) +- 完整的错误代码及其原因 +- 特殊情况与高级行为`, }, codeBlocks: [ { @@ -259,6 +318,7 @@ Payment 트랜잭션에는 여기서 다루는 것보다 훨씬 더 많은 선 en: "Send an XAH payment between two accounts", jp: "2つのアカウント間でXAH支払いを送信", ko: "두 계정 간 XAH 결제 전송", + zh: "在两个账户之间发送 XAH 支付", }, language: "javascript", code: { @@ -421,6 +481,46 @@ async function sendPayment() { await client.disconnect(); } +sendPayment();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function sendPayment() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 发送方钱包(使用你的测试网 seed),如果你的 seed 不是 secp256k1,请删除 ", {algorithm: 'secp256k1'}" 这一部分 + const sender = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 构建支付交易 + const payment = { + TransactionType: "Payment", + Account: sender.address, + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: xahToDrops(10), // 10 XAH + }; + + // Autofill 会自动补上 Fee、Sequence、NetworkID + const prepared = await client.autofill(payment); + console.log("已准备交易:", prepared); + + // 对交易签名 + const signed = sender.sign(prepared); + console.log("交易哈希:", signed.hash); + + // 提交并等待验证 + const result = await client.submitAndWait(signed.tx_blob); + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("支付发送成功!"); + } else { + console.log("支付出错"); + } + + await client.disconnect(); +} + sendPayment();`, }, }, @@ -430,6 +530,7 @@ sendPayment();`, en: "Send an IOU (token) payment between two accounts", jp: "2つのアカウント間でIOU(トークン)支払いを送信", ko: "두 계정 간 IOU(토큰) 결제 전송", + zh: "在两个账户之间发送 IOU(代币)支付", }, language: "javascript", code: { @@ -616,38 +717,87 @@ async function sendIOUPayment() { await client.disconnect(); } +sendIOUPayment();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +// 这段代码只有在你持有该 IOU,且目标账户拥有有效 TrustLine 时才能工作。请根据你的测试网配置修改字段。 +async function sendIOUPayment() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 发送方钱包(使用你的测试网 seed),如果你的 seed 不是 secp256k1,请删除 ", {algorithm: 'secp256k1'}" 这一部分 + const sender = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 发送 IOU 时,Amount 是一个包含 currency、issuer 和 value 的对象 + // 前提: + // 1. 发送方必须持有这种 IOU 的余额 + // 2. 目标账户必须拥有该 IOU 的 TrustLine + const payment = { + TransactionType: "Payment", + Account: sender.address, + Destination: "rRecipientAddress", + // 根据你想发送的代币修改 currency、issuer 和 value + Amount: { + currency: "USD", + issuer: "rTokenIssuerAddress", + value: "50", // 50 USD + }, + }; + + const prepared = await client.autofill(payment); + const signed = sender.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("IOU 支付发送成功!"); + } else if (txResult === "tecPATH_DRY") { + console.log("错误:没有支付路径。目标账户有 TrustLine 吗?"); + } else if (txResult === "tecUNFUNDED_PAYMENT") { + console.log("错误:你的 IOU 余额不足。"); + } + + await client.disconnect(); +} + sendIOUPayment();`, }, }, ], slides: [ { - title: { es: "Transacción Payment", en: "Payment Transaction", jp: "Paymentトランザクション", ko: "Payment 트랜잭션" }, + title: { es: "Transacción Payment", en: "Payment Transaction", jp: "Paymentトランザクション", ko: "Payment 트랜잭션", zh: "Payment 交易" }, content: { es: "La transacción más básica de Xahau\n\n• Account → Quien envía\n• Destination → Quien recibe\n• Amount → Cantidad (en drops para XAH)\n• 1 XAH = 1,000,000 drops", en: "The most basic transaction on Xahau\n\n• Account → The sender\n• Destination → The receiver\n• Amount → Quantity (in drops for XAH)\n• 1 XAH = 1,000,000 drops", jp: "Xahauで最も基本的なトランザクション\n\n• Account → 送信者\n• Destination → 受信者\n• Amount → 金額(XAHの場合はdrops単位)\n• 1 XAH = 1,000,000 drops", ko: "Xahau에서 가장 기본적인 트랜잭션\n\n• Account → 발신자\n• Destination → 수신자\n• Amount → 금액 (XAH의 경우 drops 단위)\n• 1 XAH = 1,000,000 drops", + zh: "Xahau 中最基础的交易类型\n\n• Account → 发送方\n• Destination → 接收方\n• Amount → 金额(XAH 以 drops 表示)\n• 1 XAH = 1,000,000 drops", }, visual: "💸", }, { - title: { es: "Envío de IOUs (tokens)", en: "Sending IOUs (tokens)", jp: "IOU(トークン)の送金", ko: "IOU(토큰) 전송" }, + title: { es: "Envío de IOUs (tokens)", en: "Sending IOUs (tokens)", jp: "IOU(トークン)の送金", ko: "IOU(토큰) 전송", zh: "发送 IOU(代币)" }, content: { es: "Amount pasa a ser un objeto:\n\n• currency → Código del token (USD, EUR...)\n• issuer → Cuenta emisora del token\n• value → Cantidad como string\n\nRequisitos:\n• Tener saldo del IOU\n• Destino con TrustLine activa", en: "Amount becomes an object:\n\n• currency → Token code (USD, EUR...)\n• issuer → Token issuer account\n• value → Amount as a string\n\nRequirements:\n• Hold a balance of the IOU\n• Destination with an active TrustLine", jp: "Amountがオブジェクトになります:\n\n• currency → トークンコード(USD、EURなど)\n• issuer → トークン発行アカウント\n• value → 文字列としての金額\n\n要件:\n• IOUの残高を保持\n• アクティブなTrustLineを持つ宛先", ko: "Amount가 객체가 됩니다:\n\n• currency → 토큰 코드 (USD, EUR...)\n• issuer → 토큰 발행 계정\n• value → 문자열로 된 금액\n\n요건:\n• IOU 잔액 보유\n• 활성 TrustLine이 있는 목적지", + zh: "Amount 会变成一个对象:\n\n• currency → 代币代码(USD、EUR...)\n• issuer → 代币发行账户\n• value → 字符串形式的金额\n\n前提条件:\n• 持有该 IOU 余额\n• 目标账户拥有有效 TrustLine", }, visual: "🪙", }, { - title: { es: "Documentación oficial", en: "Official documentation", jp: "公式ドキュメント", ko: "공식 문서" }, + title: { es: "Documentación oficial", en: "Official documentation", jp: "公式ドキュメント", ko: "공식 문서", zh: "官方文档" }, content: { es: "Referencia completa de Payment:\ https://xahau.network/docs/technical/protocol-reference/transactions/transaction-types/payment\n\n• Campos opcionales (SendMax, DeliverMin...)\n• Flags (tfPartialPayment, tfLimitQuality...)\n• Códigos de error completos\n• Casos especiales y avanzados", en: "Complete Payment reference:\ https://xahau.network/docs/technical/protocol-reference/transactions/transaction-types/payment\n\n• Optional fields (SendMax, DeliverMin...)\n• Flags (tfPartialPayment, tfLimitQuality...)\n• Complete error codes\n• Special cases and advanced behaviors", jp: "Paymentの完全リファレンス:\nhttps://xahau.network/docs/...\n\n• オプションフィールド(SendMax、DeliverMinなど)\n• フラグ(tfPartialPayment、tfLimitQualityなど)\n• 完全なエラーコード\n• 特殊ケースと高度な動作", ko: "Payment 전체 참조:\nhttps://xahau.network/docs/technical/protocol-reference/transactions/transaction-types/payment\n\n• 선택적 필드 (SendMax, DeliverMin...)\n• 플래그 (tfPartialPayment, tfLimitQuality...)\n• 전체 오류 코드\n• 특수 사례 및 고급 동작", + zh: "Payment 完整参考:\nhttps://xahau.network/docs/technical/protocol-reference/transactions/transaction-types/payment\n\n• 可选字段(SendMax、DeliverMin...)\n• Flags(tfPartialPayment、tfLimitQuality...)\n• 完整错误代码\n• 特殊情况与高级行为", }, visual: "📖", }, @@ -660,6 +810,7 @@ sendIOUPayment();`, en: "Payments with Destination Tag and memos", jp: "Destination TagとMemoを使った支払い", ko: "Destination Tag와 메모를 사용한 결제", + zh: "带 Destination Tag 和 memos 的支付", }, theory: { es: `Además del pago básico, Xahau soporta campos adicionales que permiten añadir contexto y funcionalidad a los pagos. @@ -778,6 +929,35 @@ Each transaction returns a result code: - \`tecNO_DST\`: 목적지 계정이 존재하지 않음 - \`tecDST_TAG_NEEDED\`: Destination Tag 필요 - \`tecNO_DST_INSUF_XAH\`: 목적지에 예비금을 위한 XAH가 부족`, + zh: `除了基础支付之外,Xahau 还支持一些额外字段,让支付能够携带更多上下文和功能。 + +### Destination Tag + +**Destination Tag** 是一个整数,用来让接收方识别单独的付款。它特别适用于: +- **交易所**:识别一笔充值属于哪个用户 +- **服务平台**:把一笔支付与订单或发票关联起来 +- 如果某个账户启用了 \`RequireDestTag\` 标志,**你就不能在没有 tag 的情况下向它付款** + +系统允许 Destination Tag 使用最多 32 位,也就是说你可以使用最大到 4,294,967,295 的整数。在发送支付之前,务必先和接收方确认正确的 Destination Tag,因为如果向要求 tag 的账户发送无 tag 或错误 tag 的付款,可能会导致资金丢失。 + +另外还有 **Source Tag**,它对发送方起到类似作用。不过在实际使用中,Destination Tag 更常见,也更广泛。 + +### Memos + +**Memos** 允许你在交易中附加任意数据: +- \`MemoType\`:memo 类型(例如 "text/plain"、"application/json") +- \`MemoData\`:memo 内容 +- memos 会被编码为**十六进制** +- 它们是公开的,账本上的任何人都能看到 + +### 交易结果 + +每笔交易都会返回一个结果代码: +- \`tesSUCCESS\`:交易成功 +- \`tecUNFUNDED_PAYMENT\`:余额不足 +- \`tecNO_DST\`:目标账户不存在 +- \`tecDST_TAG_NEEDED\`:必须提供 Destination Tag +- \`tecNO_DST_INSUF_XAH\`:目标账户没有足够的 XAH 用于 reserve`, }, codeBlocks: [ { @@ -786,6 +966,7 @@ Each transaction returns a result code: en: "Payment with Source Tag, Destination Tag, and Memos", jp: "Source Tag、Destination Tag、Memoを使った支払い", ko: "Source Tag, Destination Tag 및 메모를 사용한 결제", + zh: "带 Source Tag、Destination Tag 和 Memos 的支付", }, language: "javascript", code: { @@ -1084,6 +1265,80 @@ async function sendPaymentWithMemo() { await client.disconnect(); } +sendPaymentWithMemo();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +// 把文本转成十六进制的辅助函数 +function toHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); +} +function hexToString(hex) { + if (!hex) return null; + return Buffer.from(hex, "hex").toString("utf8"); +} + +async function sendPaymentWithMemo() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 发送方钱包(使用你的测试网 seed),如果你的 seed 不是 secp256k1,请删除 ", {algorithm: 'secp256k1'}" 这一部分 + const sender = Wallet.fromSeed(process.env.WALLET_SEED, { + algorithm: "secp256k1", + }); + + const payment = { + TransactionType: "Payment", + Account: sender.address, + Destination: "rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r", + Amount: xahToDrops(5), // 5 XAH + SourceTag: 1, // 用于标识付款的发送方标签 + DestinationTag: 12345, // 用于标识付款的目标标签 + Memos: [ + { + Memo: { + MemoType: toHex("text/plain"), + MemoData: toHex("Xahau 课程付款"), + }, + }, + ], + }; + + const prepared = await client.autofill(payment); + const signed = sender.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("带 memo 的支付已发送!"); + console.log("Hash:", signed.hash); + const lookup = await client.request({ + command: "tx", + transaction: signed.hash, + }); + + const tx = lookup.result.tx_json ?? lookup.result; + console.log("Source Tag:", tx.SourceTag); + console.log("Destination Tag:", tx.DestinationTag); + + if (tx.Memos) { + tx.Memos.forEach((memoWrapper, index) => { + const memo = memoWrapper.Memo; + + const memoType = hexToString(memo.MemoType); + const memoData = hexToString(memo.MemoData); + + console.log("MemoType:", memoType); + console.log("MemoData:", memoData); + }); + } + } + + await client.disconnect(); +} + sendPaymentWithMemo();`, }, }, @@ -1093,6 +1348,7 @@ sendPaymentWithMemo();`, en: "Verify a received payment", jp: "受信した支払いを確認", ko: "수신된 결제 확인", + zh: "验证一笔收到的支付", }, language: "javascript", code: { @@ -1243,38 +1499,78 @@ async function verifyPayment(txHash) { await client.disconnect(); } // 트랜잭션 해시 예시: "4B56BD61E7E7F59FF191A779FC0C9ACF68DC25C174930FCB906AC06EB812F38C" +verifyPayment("YOUR_TRANSACTION_HASH_HERE");`, + zh: `const { Client } = require("xahau"); + +async function verifyPayment(txHash) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const response = await client.request({ + command: "tx", + transaction: txHash, + }); + + const tx = response.result; + console.log("=== 支付详情 ==="); + console.log("类型:", tx.TransactionType); + console.log("从:", tx.Account); + console.log("到:", tx.Destination); + console.log("金额:", Number(tx.Amount) / 1_000_000, "XAH"); + console.log("Fee:", Number(tx.Fee) / 1_000_000, "XAH"); + console.log("结果:", tx.meta.TransactionResult); + console.log("Ledger:", tx.ledger_index); + + if (tx.DestinationTag !== undefined) { + console.log("Destination Tag:", tx.DestinationTag); + } + + if (tx.Memos) { + for (const memo of tx.Memos) { + const type = Buffer.from(memo.Memo.MemoType, "hex").toString("utf8"); + const data = Buffer.from(memo.Memo.MemoData, "hex").toString("utf8"); + console.log(\`Memo [\${type}]: \${data}\`); + } + } + + await client.disconnect(); +} +// 示例交易哈希: "4B56BD61E7E7F59FF191A779FC0C9ACF68DC25C174930FCB906AC06EB812F38C" verifyPayment("YOUR_TRANSACTION_HASH_HERE");`, }, }, ], slides: [ { - title: { es: "Destination Tag", en: "Destination Tag", jp: "Destination Tag", ko: "Destination Tag" }, + title: { es: "Destination Tag", en: "Destination Tag", jp: "Destination Tag", ko: "Destination Tag", zh: "Destination Tag" }, content: { es: "Número para identificar pagos individuales\n\n• Usado por exchanges y servicios\n• Asocia pagos con usuarios/pedidos\n• Algunas cuentas lo requieren\n• Es un número entero (uint32)", en: "A number to identify individual payments\n\n• Used by exchanges and services\n• Associates payments with users/orders\n• Some accounts require it\n• It is an integer (uint32)", jp: "個別の支払いを識別する番号\n\n• 取引所やサービスで使用\n• 支払いをユーザー/注文と関連付け\n• 一部のアカウントでは必須\n• 整数(uint32)", ko: "개별 결제를 식별하는 번호\n\n• 거래소 및 서비스에서 사용\n• 결제를 사용자/주문과 연결\n• 일부 계정에서는 필수\n• 정수 (uint32)", + zh: "用于识别单笔付款的编号\n\n• 交易所和服务平台常用\n• 可把付款与用户/订单关联起来\n• 某些账户强制要求它\n• 它是一个整数(uint32)", }, visual: "🏷️", }, { - title: { es: "Memos", en: "Memos", jp: "Memo", ko: "메모" }, + title: { es: "Memos", en: "Memos", jp: "Memo", ko: "메모", zh: "Memos" }, content: { es: "Datos adjuntos a una transacción\n\n• MemoType → Tipo (text/plain, etc.)\n• MemoData → Contenido\n• Codificados en hexadecimal\n• Públicos en el ledger", en: "Data attached to a transaction\n\n• MemoType → Type (text/plain, etc.)\n• MemoData → Content\n• Encoded in hexadecimal\n• Public on the ledger", jp: "トランザクションに添付するデータ\n\n• MemoType → タイプ(text/plainなど)\n• MemoData → コンテンツ\n• 16進数でエンコード\n• レジャー上でパブリック", ko: "트랜잭션에 첨부된 데이터\n\n• MemoType → 유형 (text/plain 등)\n• MemoData → 내용\n• 16진수로 인코딩\n• 레저에서 공개", + zh: "附加在交易上的数据\n\n• MemoType → 类型(text/plain 等)\n• MemoData → 内容\n• 以十六进制编码\n• 在账本上公开可见", }, visual: "📝", }, { - title: { es: "Seguridad del DestinationTag", en: "Destination Tag security", jp: "Destination Tagのセキュリティ", ko: "Destination Tag 보안" }, + title: { es: "Seguridad del DestinationTag", en: "Destination Tag security", jp: "Destination Tagのセキュリティ", ko: "Destination Tag 보안", zh: "Destination Tag 安全" }, content: { es: "• Flag RequireDestTag en la cuenta destino\n• Sin tag → error tecDST_TAG_NEEDED\n• Exchanges exigen tag para depósitos\n• Sin tag correcto = fondos perdidos\n• Siempre valida el tag antes de enviar\n• Maneja errores: tecNO_DST, tecUNFUNDED", en: "• RequireDestTag flag on the destination account\n• No tag → error tecDST_TAG_NEEDED\n• Exchanges require a tag for deposits\n• Wrong or missing tag = lost funds\n• Always validate the tag before sending\n• Handle errors: tecNO_DST, tecUNFUNDED", jp: "• 宛先アカウントのRequireDestTagフラグ\n• タグなし → tecDST_TAG_NEEDEDエラー\n• 取引所は入金にタグを要求\n• タグ誤りや欠落 = 資金消失\n• 送信前にタグを常に確認\n• エラーを処理:tecNO_DST、tecUNFUNDED", ko: "• 목적지 계정의 RequireDestTag 플래그\n• 태그 없음 → tecDST_TAG_NEEDED 오류\n• 거래소는 입금에 태그 필요\n• 잘못되거나 누락된 태그 = 자금 손실\n• 전송 전 항상 태그 확인\n• 오류 처리: tecNO_DST, tecUNFUNDED", + zh: "• 目标账户上的 RequireDestTag 标志\n• 没有 tag → 报错 tecDST_TAG_NEEDED\n• 交易所充值通常要求 tag\n• tag 错误或缺失 = 资金丢失\n• 发送前务必确认 tag\n• 处理错误:tecNO_DST、tecUNFUNDED", }, visual: "🔒", }, @@ -1287,6 +1583,7 @@ verifyPayment("YOUR_TRANSACTION_HASH_HERE");`, en: "Cross-currency payments and pathfinding", jp: "クロスカレンシー支払いとパスファインディング", ko: "크로스 커런시 결제 및 경로 탐색", + zh: "跨币种支付与路径查找", }, theory: { es: `Xahau no solo permite enviar XAH nativo o tokens del mismo tipo: también soporta **pagos cross-currency**, donde el emisor envía una moneda y el receptor recibe otra diferente. Esto es posible gracias al **DEX integrado** y al sistema de **pathfinding**. @@ -1429,38 +1726,76 @@ The \`tfPartialPayment\` flag (value: \`0x00020000\`) allows a payment to delive - 쿼리와 실행 사이에 유동성이 변할 수 있을 때 유용 - \`DeliverMin\`을 사용하여 허용 가능한 최소값 설정 - **중요**: 결제를 받을 때, \`Amount\` 필드가 아니라 메타데이터의 \`delivered_amount\`를 항상 확인하세요. 공격자가 높은 \`Amount\`를 표시하지만 훨씬 적게 전달하는 부분 결제를 보낼 수 있습니다`, + zh: `Xahau 不仅支持发送原生 XAH 或相同类型的代币,还支持 **跨币种支付**。也就是说,发送方可以发送一种货币,而接收方收到另一种货币。这依赖于**内置 DEX**和**路径查找(pathfinding)**系统。 + +### 跨币种支付 + +例如,跨币种支付可以让发送方支付 XAH,而接收方收到 USD。Xahau 会自动通过 DEX 找到最优路径来完成货币转换。 + +### 路径查找系统 + +路径查找是用于寻找货币转换路线的机制: +- Xahau 会通过 trust lines 和 DEX 订单搜索**路径** +- 它可以串联多个中间转换步骤 +- 它始终尝试找到当前可用的**最佳汇率** + +### 跨币种支付中的关键字段 + +| 字段 | 说明 | +|---|---| +| \`Amount\` | 接收方应收到的金额(目标货币) | +| \`SendMax\` | 发送方愿意支付的最大金额(源货币) | +| \`DeliverMin\` | 接收方最少必须收到的金额(部分支付时) | +| \`Paths\` | 由路径查找发现的转换路径 | + +### ripple_path_find 命令 + +在发送跨币种支付之前,可用 \`ripple_path_find\` 来: +- 检查两种货币之间是否存在路径 +- 获取交易所需的 \`Paths\` +- 了解预估成本(\`source_amount\`) + +### 部分支付(tfPartialPayment) + +\`tfPartialPayment\` 标志(值:\`0x00020000\`)允许支付实际交付**少于** \`Amount\` 中指定的金额: +- 当查询与执行之间流动性可能变化时非常有用 +- 使用 \`DeliverMin\` 来设置可接受的最小值 +- **重要**:接收支付时,必须检查 metadata 中的 \`delivered_amount\`,而**不是** \`Amount\` 字段。攻击者可能发起一笔显示较高 \`Amount\`,但实际交付远少于该值的部分支付`, }, codeBlocks: [ ], slides: [ { - title: { es: "Pagos cross-currency", en: "Cross-currency payments", jp: "クロスカレンシー支払い", ko: "크로스 커런시 결제" }, + title: { es: "Pagos cross-currency", en: "Cross-currency payments", jp: "クロスカレンシー支払い", ko: "크로스 커런시 결제", zh: "跨币种支付" }, content: { es: "Envía una moneda, el receptor recibe otra\n\n• El DEX integrado convierte automáticamente\n• Amount = lo que recibe el receptor\n• SendMax = máximo que paga el emisor\n• Paths = rutas de conversión", en: "Send one currency, the receiver gets another\n\n• The built-in DEX converts automatically\n• Amount = what the receiver gets\n• SendMax = maximum the sender pays\n• Paths = conversion routes", jp: "一つの通貨を送り、受信者は別の通貨を受け取る\n\n• 組み込みDEXが自動的に変換\n• Amount = 受信者が受け取る額\n• SendMax = 送信者が支払う最大額\n• Paths = 変換ルート", ko: "한 통화를 보내면 수신자는 다른 통화를 받음\n\n• 내장 DEX가 자동으로 변환\n• Amount = 수신자가 받는 금액\n• SendMax = 발신자가 지불하는 최대 금액\n• Paths = 변환 경로", + zh: "发送一种货币,接收方得到另一种\n\n• 内置 DEX 会自动完成兑换\n• Amount = 接收方实际收到的金额\n• SendMax = 发送方最多支付的金额\n• Paths = 转换路径", }, visual: "🔄", }, { - title: { es: "Pathfinding", en: "Pathfinding", jp: "パスファインディング", ko: "경로 탐색" }, + title: { es: "Pathfinding", en: "Pathfinding", jp: "パスファインディング", ko: "경로 탐색", zh: "路径查找" }, content: { es: "ripple_path_find busca rutas de conversión\n\n1. Indica cuenta origen y destino\n2. Especifica la moneda y cantidad destino\n3. Obtén alternativas con coste estimado\n4. Usa paths_computed en tu Payment", en: "ripple_path_find searches for conversion routes\n\n1. Specify source and destination accounts\n2. Specify the destination currency and amount\n3. Get alternatives with estimated cost\n4. Use paths_computed in your Payment", jp: "ripple_path_findが変換ルートを検索\n\n1. 送信元と宛先アカウントを指定\n2. 宛先通貨と金額を指定\n3. 推定コストの代替案を取得\n4. PaymentでPaths computedを使用", ko: "ripple_path_find가 변환 경로를 탐색\n\n1. 원본 및 목적지 계정 지정\n2. 목적지 통화 및 금액 지정\n3. 예상 비용과 함께 대안 획득\n4. Payment에서 paths_computed 사용", + zh: "ripple_path_find 用于搜索转换路径\n\n1. 指定源账户和目标账户\n2. 指定目标货币与金额\n3. 获取带预估成本的可选路径\n4. 在 Payment 中使用 paths_computed", }, visual: "🗺️", }, { - title: { es: "Pagos parciales", en: "Partial payments", jp: "部分支払い", ko: "부분 결제" }, + title: { es: "Pagos parciales", en: "Partial payments", jp: "部分支払い", ko: "부분 결제", zh: "部分支付" }, content: { es: "Flag tfPartialPayment permite entregar menos\n\n• Útil cuando la liquidez varía\n• DeliverMin = mínimo aceptable\n• SIEMPRE verificar delivered_amount\n• NUNCA confiar en el campo Amount\n\n⚠️ Riesgo de seguridad si no se verifica", en: "tfPartialPayment flag allows delivering less\n\n• Useful when liquidity varies\n• DeliverMin = acceptable minimum\n• ALWAYS verify delivered_amount\n• NEVER trust the Amount field\n\n⚠️ Security risk if not verified", jp: "tfPartialPaymentフラグで少なく配信可能\n\n• 流動性が変わる場合に便利\n• DeliverMin = 許容できる最小額\n• 常にdelivered_amountを確認\n• Amountフィールドは信頼しない\n\n⚠️ 確認しないとセキュリティリスク", ko: "tfPartialPayment 플래그로 적게 전달 가능\n\n• 유동성이 변할 때 유용\n• DeliverMin = 허용 가능한 최솟값\n• 항상 delivered_amount 확인\n• Amount 필드를 절대 신뢰하지 말 것\n\n⚠️ 확인하지 않으면 보안 위험", + zh: "tfPartialPayment 标志允许少量交付\n\n• 在流动性变化时很有用\n• DeliverMin = 可接受的最低金额\n• 一定要检查 delivered_amount\n• 永远不要相信 Amount 字段\n\n⚠️ 不检查会带来安全风险", }, visual: "⚠️", }, diff --git a/src/data/modules/m07-tokens.js b/src/data/modules/m07-tokens.js index 5d8a8c7..5e02554 100644 --- a/src/data/modules/m07-tokens.js +++ b/src/data/modules/m07-tokens.js @@ -6,6 +6,7 @@ export default { en: "Creating and managing your own tokens", jp: "独自トークンの作成と管理", ko: "나만의 토큰 생성 및 관리", + zh: "创建和管理自定义代币", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "TrustLines and the token model in Xahau", jp: "トラストラインとXahauのトークンモデル", ko: "TrustLine과 Xahau의 토큰 모델", + zh: "TrustLine 与 Xahau 的代币模型", }, theory: { es: `En Xahau, los tokens fungibles funcionan de manera diferente a ERC-20 en Ethereum. No necesitas desplegar un smart contract para crear un token. En su lugar, se usa un sistema basado en **TrustLines** (líneas de confianza). @@ -165,6 +167,16 @@ Xahauのトークンシステムの利点の一つは、発行アカウントが - 발행자는 직접 토큰을 “민팅”하기보다 잔액 관계를 생성합니다 Xahau 토큰 모델을 이해하려면 “토큰 컨트랙트”가 아니라 “계정 간 관계”라는 관점이 중요합니다.`, + zh: `Xahau 的发行型代币与 Ethereum 的 ERC-20 不同。用户在接收代币之前,必须先建立 **TrustLine**,它表示发行方与接收方之间的信任关系。 + +### 核心概念 + +- 代币由发行账户定义 +- 接收方先通过 \`TrustSet\` 创建 TrustLine +- TrustLine 包含额度、状态和标志 +- 发行方不是直接“铸造”代币,而是建立余额关系 + +理解 Xahau 的代币模型时,重点不是“代币合约”,而是“账户之间的关系”。`, }, codeBlocks: [ { @@ -173,6 +185,7 @@ Xahau 토큰 모델을 이해하려면 “토큰 컨트랙트”가 아니라 en: "Create a TrustLine toward a token issuer", jp: "トークン発行者へのTrustLineを作成する", ko: "토큰 발행자에 대한 TrustLine 생성", + zh: "为代币发行方创建 TrustLine", }, language: "javascript", code: { @@ -319,6 +332,42 @@ async function createTrustLine() { await client.disconnect(); } +createTrustLine();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function createTrustLine() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 想要接收代币的接收者钱包 + const receiver = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 创建 TrustLine: “我最多信任该发行方 1,000,000 单位” + const trustSet = { + TransactionType: "TrustSet", + Account: receiver.address, + LimitAmount: { + currency: "YourTokenName", + issuer: "YourIssuerAddress", + value: "1000000", // 我愿意接受的最大额度 + }, + }; + + const prepared = await client.autofill(trustSet); + const signed = receiver.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("TrustLine 创建成功!"); + console.log("现在你的账户 " + receiver.address + " 可以接收该发行方的代币了"); + } + + await client.disconnect(); +} + createTrustLine();`, }, }, @@ -328,6 +377,7 @@ createTrustLine();`, en: "Issue (send) tokens to an account with a TrustLine", jp: "TrustLineを持つアカウントへトークンを発行(送信)する", ko: "TrustLine이 있는 계정에 토큰 발행(전송)", + zh: "向已建立 TrustLine 的账户发行代币", }, language: "javascript", code: { @@ -478,38 +528,78 @@ async function issueTokens() { await client.disconnect(); } +issueTokens();`, + zh: `// 如果你并不持有要发送的代币,这段代码会失败 +require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function issueTokens() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 代币发行方钱包 + const issuer = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 向接收方发送 100 USD(对方已拥有 TrustLine) + const payment = { + TransactionType: "Payment", + Account: issuer.address, + Destination: "rRecipientAddress", + Amount: { + currency: "USD", + issuer: issuer.address, + value: "100", // 100 USD + }, + }; + + const prepared = await client.autofill(payment); + const signed = issuer.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("代币发行成功!"); + } + + await client.disconnect(); +} + issueTokens();`, }, }, ], slides: [ { - title: { es: "Modelo de tokens en Xahau", en: "Token model in Xahau", jp: "Xahauのトークンモデル", ko: "Xahau의 토큰 모델" }, + title: { es: "Modelo de tokens en Xahau", en: "Token model in Xahau", jp: "Xahauのトークンモデル", ko: "Xahau의 토큰 모델", zh: "Xahau 的代币模型" }, content: { es: "No necesitas smart contracts para crear tokens\n\n1️⃣ Emisor: Cualquier cuenta\n2️⃣ TrustLine: El receptor opta-in\n3️⃣ Payment: Transferencia nativa\n\nTokens = currency + issuer", en: "No smart contracts needed to create tokens\n\n1️⃣ Issuer: Any account\n2️⃣ TrustLine: Recipient opts-in\n3️⃣ Payment: Native transfer\n\nTokens = currency + issuer", jp: "トークン作成にスマートコントラクト不要\n\n1️⃣ 発行者:どのアカウントでも可\n2️⃣ TrustLine:受取人がオプトイン\n3️⃣ Payment:ネイティブ転送\n\nトークン = currency + issuer", ko: "토큰 생성에 스마트 컨트랙트가 필요 없음\n\n1️⃣ 발행자: 어떤 계정이든 가능\n2️⃣ TrustLine: 수신자가 opt-in\n3️⃣ Payment: 네이티브 전송\n\n토큰 = currency + issuer", + zh: "创建代币不需要智能合约\n\n1️⃣ 发行方:任何账户都可以\n2️⃣ TrustLine:接收方主动选择接收\n3️⃣ Payment:原生转账\n\n代币 = currency + issuer", }, visual: "🪙", }, { - title: { es: "TrustLine = Opt-in", en: "TrustLine = Opt-in", jp: "TrustLine = オプトイン", ko: "TrustLine = Opt-in" }, + title: { es: "TrustLine = Opt-in", en: "TrustLine = Opt-in", jp: "TrustLine = オプトイン", ko: "TrustLine = Opt-in", zh: "TrustLine = 主动加入" }, content: { es: "El receptor ELIGE recibir un token\n\n• Crea una TrustLine hacia el emisor\n• Define el límite máximo\n• Consume reserva de propietario\n• Protege contra spam de tokens", en: "The recipient CHOOSES to receive a token\n\n• Creates a TrustLine toward the issuer\n• Defines the maximum limit\n• Consumes owner reserve\n• Protects against token spam", jp: "受取人がトークンを受け取ることを選択する\n\n• 発行者へのTrustLineを作成\n• 最大限度額を定義\n• オーナーリザーブを消費\n• トークンスパムから保護", ko: "수신자가 토큰 수령을 직접 선택함\n\n• 발행자에 대한 TrustLine 생성\n• 최대 한도 정의\n• owner reserve 소비\n• 토큰 스팸 방지", + zh: "接收方主动选择是否接收代币\n\n• 向发行方创建 TrustLine\n• 定义最大信任额度\n• 会占用 owner reserve\n• 防止代币垃圾信息", }, visual: "🤝", }, { - title: { es: "Sistema de reservas", en: "Reserve system", jp: "リザーブシステム", ko: "Reserve 시스템" }, + title: { es: "Sistema de reservas", en: "Reserve system", jp: "リザーブシステム", ko: "Reserve 시스템", zh: "储备机制" }, content: { es: "Cada TrustLine aumenta la reserva de la cuenta\n\n• Reserva base + reserva por objeto\n• Más TrustLines = más XAH bloqueado\n• Los usuarios deben planificar sus TrustLines\n• Eliminar TrustLine (balance 0) libera reserva\n• Impacto directo en el XAH disponible", en: "Each TrustLine increases the account reserve\n\n• Base reserve + per-object reserve\n• More TrustLines = more XAH locked\n• Users must plan their TrustLines\n• Removing a TrustLine (balance 0) frees reserve\n• Direct impact on available XAH", jp: "各TrustLineはアカウントリザーブを増やします\n\n• ベースリザーブ+オブジェクトごとのリザーブ\n• TrustLineが多いほどXAHがロックされる\n• ユーザーはTrustLineを計画的に\n• TrustLine削除(残高0)でリザーブが解放\n• 利用可能XAHへの直接影響", ko: "각 TrustLine은 계정 reserve를 증가시킴\n\n• 기본 reserve + 객체당 reserve\n• TrustLine이 많을수록 더 많은 XAH가 잠김\n• 사용자는 TrustLine을 계획적으로 만들어야 함\n• TrustLine 제거(잔액 0) 시 reserve 해제\n• 사용 가능한 XAH에 직접 영향", + zh: "每条 TrustLine 都会增加账户储备\n\n• 基础储备 + 每个对象的额外储备\n• TrustLine 越多,锁定的 XAH 越多\n• 用户应规划好自己的 TrustLine\n• 删除 TrustLine(余额为 0)可释放储备\n• 会直接影响可用的 XAH", }, visual: "💎", }, @@ -522,6 +612,7 @@ issueTokens();`, en: "Complete process: create and distribute your own token", jp: "完全なプロセス:独自トークンの作成と配布", ko: "전체 과정: 나만의 토큰 생성 및 배포", + zh: "完整流程:创建并分发你的代币", }, theory: { es: `Ahora que entiendes cómo funcionan las TrustLines, vamos a ver el proceso completo para crear tu propio token y distribuirlo. A diferencia de otras blockchains, en Xahau **no necesitas desplegar ningún contrato**. El proceso se realiza enteramente con transacciones nativas. @@ -678,6 +769,19 @@ console.log(currencyToHex("EURZ")); ### 왜 두 개의 계정을 쓰기도 하나요? 발행자와 운영 계정을 분리하면 보안과 운영 관리가 쉬워집니다. 실무에서는 발행 계정을 더 엄격하게 보호하는 경우가 많습니다.`, + zh: `要创建并分发自己的代币,通常需要几个步骤。它不只是“发行代币”,还包括账户配置与接收方准备。 + +### 常见流程 + +1. 准备发行账户和运营账户 +2. 设置必要标志(如 \`DefaultRipple\`、\`RequireAuth\`) +3. 接收方创建 TrustLine +4. 发行方向外发送代币 +5. 用区块浏览器或 \`account_lines\` 检查状态 + +### 为什么经常使用两个账户? + +把发行账户和运营账户分开,能让安全和日常运营更容易管理。实际项目里,发行账户通常会受到更严格的保护。`, }, codeBlocks: [ { @@ -686,6 +790,7 @@ console.log(currencyToHex("EURZ")); en: "Complete process: configure issuer, create TrustLine, issue and distribute token", jp: "完全なプロセス:発行者の設定、トラストラインの作成、トークンの発行と配布", ko: "전체 과정: 발행자 설정, TrustLine 생성, 토큰 발행 및 배포", + zh: "完整流程:配置发行方、创建 TrustLine、发行并分发代币", }, language: "javascript", code: { @@ -1288,38 +1393,176 @@ async function createAndDistributeToken() { await client.disconnect(); } +createAndDistributeToken();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +// 你需要两个在测试网上有资金的钱包,并在 .env 中定义: +// ISSUER_SEED → 代币发行账户 +// RESERVE_SEED → 储备/分发账户 +// 你可以从水龙头获取测试资金:https://xahau-test.net + +// 如果 token_currency 超过 3 个字符,则转成 40 位十六进制 +function normalizeCurrency(token_currency) { + if (typeof token_currency !== "string") return token_currency; + + const cur = token_currency.trim(); + if (cur.length <= 3) return cur; + + const hex = Buffer.from(cur, "utf8").toString("hex").toUpperCase(); + + if (hex.length > 40) { + throw new Error( + \`token_currency 太长: "\${cur}" -> hex \${hex.length} (>40). UTF-8 最多约 20 bytes。\` + ); + } + + return hex.padEnd(40, "0"); +} + +async function createAndDistributeToken() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // === 账户 === + const issuer = Wallet.fromSeed(process.env.ISSUER_SEED, {algorithm: 'secp256k1'}); + const reserve = Wallet.fromSeed(process.env.RESERVE_SEED, {algorithm: 'secp256k1'}); + + const TOKEN_CURRENCY_INPUT = "YourTokenName"; + const TOTAL_SUPPLY = "1000000"; + const TOKEN_CURRENCY = normalizeCurrency(TOKEN_CURRENCY_INPUT); + + console.log("=== 创建代币 ==="); + console.log("发行方:", issuer.address); + console.log("储备账户:", reserve.address); + console.log("代币:", TOKEN_CURRENCY); + console.log("总供应量:", TOTAL_SUPPLY); + + // === 步骤 1:为发行方开启 DefaultRipple === + console.log("--- 步骤 1:为发行方配置 DefaultRipple ---"); + const accountSet = { + TransactionType: "AccountSet", + Account: issuer.address, + SetFlag: 8, + }; + + const prep1 = await client.autofill(accountSet); + const signed1 = issuer.sign(prep1); + const result1 = await client.submitAndWait(signed1.tx_blob); + console.log("DefaultRipple:", result1.result.meta.TransactionResult); + + if (result1.result.meta.TransactionResult !== "tesSUCCESS") { + console.log("发行方配置失败,终止。"); + await client.disconnect(); + return; + } + + // === 步骤 2:储备账户为发行方创建 TrustLine === + console.log("--- 步骤 2:创建 TrustLine(reserve → issuer)---"); + const trustSet = { + TransactionType: "TrustSet", + Account: reserve.address, + LimitAmount: { + currency: TOKEN_CURRENCY, + issuer: issuer.address, + value: TOTAL_SUPPLY, + }, + }; + + const prep2 = await client.autofill(trustSet); + const signed2 = reserve.sign(prep2); + const result2 = await client.submitAndWait(signed2.tx_blob); + console.log("TrustLine:", result2.result.meta.TransactionResult); + + if (result2.result.meta.TransactionResult !== "tesSUCCESS") { + console.log("创建 TrustLine 失败,终止。"); + await client.disconnect(); + return; + } + + // === 步骤 3:发行方向储备账户发送全部供应量 === + console.log("--- 步骤 3:发行代币(issuer → reserve)---"); + const issuePayment = { + TransactionType: "Payment", + Account: issuer.address, + Destination: reserve.address, + Amount: { + currency: TOKEN_CURRENCY, + issuer: issuer.address, + value: TOTAL_SUPPLY, + }, + }; + + const prep3 = await client.autofill(issuePayment); + const signed3 = issuer.sign(prep3); + const result3 = await client.submitAndWait(signed3.tx_blob); + console.log("发行结果:", result3.result.meta.TransactionResult); + + if (result3.result.meta.TransactionResult !== "tesSUCCESS") { + console.log("代币发行失败,终止。"); + await client.disconnect(); + return; + } + + console.log("代币已创建并分发到储备账户!"); + console.log("总供应量:", TOTAL_SUPPLY, TOKEN_CURRENCY); + + // === 验证 === + console.log("--- 验证 ---"); + const lines = await client.request({ + command: "account_lines", + account: reserve.address, + ledger_index: "validated", + }); + + const tokenLine = lines.result.lines.find( + (l) => l.currency === TOKEN_CURRENCY && l.account === issuer.address + ); + + if (tokenLine) { + console.log("储备余额:", tokenLine.balance, TOKEN_CURRENCY); + console.log("发行方:", tokenLine.account); + console.log("额度:", tokenLine.limit, TOKEN_CURRENCY); + } + + await client.disconnect(); +} + createAndDistributeToken();`, }, }, ], slides: [ { - title: { es: "Proceso de creación de un token", en: "Token creation process", jp: "トークン作成プロセス", ko: "토큰 생성 과정" }, + title: { es: "Proceso de creación de un token", en: "Token creation process", jp: "トークン作成プロセス", ko: "토큰 생성 과정", zh: "代币创建流程" }, content: { es: "No necesitas smart contracts\n\n1️⃣ Configurar emisor (DefaultRipple)\n2️⃣ Crear TrustLine desde cuenta reserva\n3️⃣ Emitir supply (Payment del emisor)\n4️⃣ Distribuir a usuarios finales\n\nTodo con transacciones nativas", en: "No smart contracts needed\n\n1️⃣ Configure issuer (DefaultRipple)\n2️⃣ Create TrustLine from reserve account\n3️⃣ Issue supply (Payment from issuer)\n4️⃣ Distribute to end users\n\nAll with native transactions", jp: "スマートコントラクト不要\n\n1️⃣ 発行者を設定(DefaultRipple)\n2️⃣ リザーブアカウントからTrustLineを作成\n3️⃣ サプライを発行(発行者からPayment)\n4️⃣ エンドユーザーに配布\n\nすべてネイティブトランザクションで", ko: "스마트 컨트랙트가 필요 없음\n\n1️⃣ 발행자 설정 (DefaultRipple)\n2️⃣ reserve 계정에서 TrustLine 생성\n3️⃣ 공급량 발행 (발행자의 Payment)\n4️⃣ 최종 사용자에게 배포\n\n모두 네이티브 트랜잭션으로 처리", + zh: "不需要智能合约\n\n1️⃣ 配置发行方(DefaultRipple)\n2️⃣ 从储备账户创建 TrustLine\n3️⃣ 发行供应量(发行方发起 Payment)\n4️⃣ 分发给最终用户\n\n全部使用原生交易完成", }, visual: "🏭", }, { - title: { es: "Dos cuentas: emisor + reserva", en: "Two accounts: issuer + reserve", jp: "2つのアカウント:発行者 + リザーブ", ko: "두 개의 계정: 발행자 + reserve" }, + title: { es: "Dos cuentas: emisor + reserva", en: "Two accounts: issuer + reserve", jp: "2つのアカウント:発行者 + リザーブ", ko: "두 개의 계정: 발행자 + reserve", zh: "两个账户:发行方 + 储备账户" }, content: { es: "Buena práctica: separar responsabilidades\n\n• Emisor: solo configura y emite\n → Proteger con multi-sign\n → Desactivar clave maestra\n\n• Reserva: opera día a día\n → Distribuye a usuarios\n → Vende en el DEX\n\nSi la reserva se compromete, el emisor puede congelar", en: "Best practice: separate responsibilities\n\n• Issuer: only configures and issues\n -> Protect with multi-sign\n -> Disable master key\n\n• Reserve: day-to-day operations\n -> Distributes to users\n -> Sells on the DEX\n\nIf reserve is compromised, the issuer can freeze", jp: "ベストプラクティス:責任を分離\n\n• 発行者:設定と発行のみ\n -> マルチサインで保護\n -> マスターキーを無効化\n\n• リザーブ:日常業務\n -> ユーザーへの配布\n -> DEXでの販売\n\nリザーブが侵害されたら発行者が凍結可能", ko: "모범 사례: 역할 분리\n\n• 발행자: 설정과 발행만 담당\n → 멀티서명으로 보호\n → 마스터 키 비활성화 가능\n\n• Reserve: 일상 운영 담당\n → 사용자에게 배포\n → DEX에서 판매\n\nreserve가 침해되면 발행자가 동결 가능", + zh: "最佳实践:分离职责\n\n• 发行方:只负责配置与发行\n → 用多重签名保护\n → 可停用主密钥\n\n• 储备账户:负责日常运营\n → 向用户分发\n → 在 DEX 上出售\n\n如果储备账户被攻破,发行方还能冻结代币", }, visual: "🔐", }, { - title: { es: "Resumen de transacciones", en: "Transaction summary", jp: "トランザクションまとめ", ko: "트랜잭션 요약" }, + title: { es: "Resumen de transacciones", en: "Transaction summary", jp: "トランザクションまとめ", ko: "트랜잭션 요약", zh: "交易总结" }, content: { es: "AccountSet → DefaultRipple en emisor\nTrustSet → Reserva confía en emisor\nPayment → Emisor envía supply a reserva\nPayment → Reserva distribuye a usuarios\n\nUsuarios finales necesitan TrustLine\nantes de poder recibir el token", en: "AccountSet -> DefaultRipple on issuer\nTrustSet -> Reserve trusts issuer\nPayment -> Issuer sends supply to reserve\nPayment -> Reserve distributes to users\n\nEnd users need a TrustLine\nbefore they can receive the token", jp: "AccountSet -> 発行者にDefaultRipple\nTrustSet -> リザーブが発行者を信頼\nPayment -> 発行者がリザーブにサプライを送信\nPayment -> リザーブがユーザーに配布\n\nエンドユーザーはトークンを\n受け取る前にTrustLineが必要", ko: "AccountSet → 발행자에 DefaultRipple 설정\nTrustSet → reserve가 발행자를 신뢰\nPayment → 발행자가 reserve에 공급량 전송\nPayment → reserve가 사용자에게 배포\n\n최종 사용자는 토큰을 받기 전에\nTrustLine이 필요함", + zh: "AccountSet → 为发行方开启 DefaultRipple\nTrustSet → 储备账户信任发行方\nPayment → 发行方向储备账户发送供应量\nPayment → 储备账户向用户分发\n\n最终用户在接收代币前\n必须先建立 TrustLine", }, visual: "📋", }, @@ -1332,6 +1575,7 @@ createAndDistributeToken();`, en: "Advanced token management", jp: "高度なトークン管理", ko: "고급 토큰 관리", + zh: "高级代币管理", }, theory: { es: `Una vez creado tu token, puedes gestionar diversos aspectos: consultar balances, configurar la cuenta emisora y transferir tokens entre usuarios. @@ -1412,6 +1656,18 @@ For token names longer than 3 characters, a 40-character hexadecimal code is use ### 운영 관점의 포인트 토큰은 한 번 배포했다고 끝나지 않습니다. 정책, 보안, 규정, 사용자 경험을 고려해 발행자 계정을 지속적으로 관리해야 합니다.`, + zh: `当代币开始运行后,除了发行和分发,还需要处理各种 **管理工作**。这一阶段需要理解发行方权限以及 TrustLine 的状态。 + +### 常见管理内容 + +- 批准或拒绝某条 TrustLine +- 调整发行账户配置 +- 检查持有额度与用户状态 +- 监控代币流向 + +### 运营视角下的重点 + +代币并不是发出去就结束了。你需要持续管理发行账户,同时考虑策略、安全、合规和用户体验。`, }, codeBlocks: [ { @@ -1420,6 +1676,7 @@ For token names longer than 3 characters, a 40-character hexadecimal code is use en: "Query the tokens (TrustLines) of an account", jp: "アカウントのトークン(TrustLine)を照会する", ko: "계정의 토큰(TrustLine) 조회", + zh: "查询账户的代币(TrustLine)", }, language: "javascript", code: { @@ -1542,6 +1799,36 @@ async function getTokenBalances(address) { await client.disconnect(); } +getTokenBalances("rYourAddressHere");`, + zh: `const { Client } = require("xahau"); + +async function getTokenBalances(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const response = await client.request({ + command: "account_lines", + account: address, + ledger_index: "validated", + }); + + console.log("=== 账户代币 ==="); + console.log("地址:", address); + + if (response.result.lines.length === 0) { + console.log("没有找到 TrustLine(代币)。"); + } + + for (const line of response.result.lines) { + console.log(\`代币: \${line.currency}\`); + console.log(\` 发行方: \${line.account}\`); + console.log(\` 余额: \${line.balance}\`); + console.log(\` 限额: \${line.limit}\`); + } + + await client.disconnect(); +} + getTokenBalances("rYourAddressHere");`, }, }, @@ -1549,32 +1836,35 @@ getTokenBalances("rYourAddressHere");`, ], slides: [ { - title: { es: "Consultar tokens", en: "Query tokens", jp: "トークンの照会", ko: "토큰 조회" }, + title: { es: "Consultar tokens", en: "Query tokens", jp: "トークンの照会", ko: "토큰 조회", zh: "查询代币" }, content: { es: "account_lines → TrustLines de una cuenta\n\n• currency → Código del token\n• account → Emisor\n• balance → Balance actual\n• limit → Límite de confianza", en: "account_lines -> TrustLines of an account\n\n• currency -> Token code\n• account -> Issuer\n• balance -> Current balance\n• limit -> Trust limit", jp: "account_lines -> アカウントのTrustLine\n\n• currency -> トークンコード\n• account -> 発行者\n• balance -> 現在の残高\n• limit -> 信頼限度額", ko: "account_lines → 계정의 TrustLine\n\n• currency → 토큰 코드\n• account → 발행자\n• balance → 현재 잔액\n• limit → 신뢰 한도", + zh: "account_lines → 账户的 TrustLine\n\n• currency → 代币代码\n• account → 发行方\n• balance → 当前余额\n• limit → 信任额度", }, visual: "📊", }, { - title: { es: "DefaultRipple", en: "DefaultRipple", jp: "DefaultRipple", ko: "DefaultRipple" }, + title: { es: "DefaultRipple", en: "DefaultRipple", jp: "DefaultRipple", ko: "DefaultRipple", zh: "DefaultRipple" }, content: { es: "Flag esencial para emisores de tokens\n\n• Sin DefaultRipple → Solo ida y vuelta al emisor\n• Con DefaultRipple → Transferible entre terceros\n\nActívalo ANTES de emitir tokens", en: "Essential flag for token issuers\n\n• Without DefaultRipple -> Only back and forth to issuer\n• With DefaultRipple -> Transferable between third parties\n\nActivate it BEFORE issuing tokens", jp: "トークン発行者に不可欠なフラグ\n\n• DefaultRippleなし -> 発行者との間でのみ\n• DefaultRippleあり -> 第三者間で転送可能\n\nトークン発行前に有効化すること", ko: "토큰 발행자에게 필수적인 플래그\n\n• DefaultRipple 없음 → 발행자와의 왕복만 가능\n• DefaultRipple 있음 → 제3자 간 전송 가능\n\n토큰 발행 전에 활성화해야 함", + zh: "代币发行方的重要标志\n\n• 没有 DefaultRipple → 只能与发行方之间往返\n• 有 DefaultRipple → 可在第三方之间转移\n\n必须在发行代币前启用", }, visual: "🔀", }, { - title: { es: "Flags importantes para emisores", en: "Important flags for issuers", jp: "発行者の重要なフラグ", ko: "발행자를 위한 중요한 플래그" }, + title: { es: "Flags importantes para emisores", en: "Important flags for issuers", jp: "発行者の重要なフラグ", ko: "발행자를 위한 중요한 플래그", zh: "发行方的重要标志" }, content: { es: "RequireAuth (asfRequireAuth):\n• El emisor autoriza cada TrustLine\n• Ideal para tokens con KYC\n\nDefaultRipple (asfDefaultRipple):\n• Permite transferencia entre terceros\n\nConfigurar ANTES de emitir tokens\nUsar AccountSet con SetFlag/ClearFlag", en: "RequireAuth (asfRequireAuth):\n• Issuer authorizes each TrustLine\n• Ideal for tokens with KYC\n\nDefaultRipple (asfDefaultRipple):\n• Allows transfer between third parties\n\nConfigure BEFORE issuing tokens\nUse AccountSet with SetFlag/ClearFlag", jp: "RequireAuth(asfRequireAuth):\n• 発行者が各TrustLineを承認\n• KYCトークンに最適\n\nDefaultRipple(asfDefaultRipple):\n• 第三者間の転送を許可\n\nトークン発行前に設定する\nAccountSetにSetFlag/ClearFlagを使用", ko: "RequireAuth (asfRequireAuth):\n• 발행자가 각 TrustLine을 승인\n• KYC가 필요한 토큰에 적합\n\nDefaultRipple (asfDefaultRipple):\n• 제3자 간 전송 허용\n\n토큰 발행 전 설정 필요\nAccountSet의 SetFlag/ClearFlag 사용", + zh: "RequireAuth(asfRequireAuth):\n• 发行方批准每一条 TrustLine\n• 适合需要 KYC 的代币\n\nDefaultRipple(asfDefaultRipple):\n• 允许第三方之间转移\n\n应在发行代币前完成配置\n使用带 SetFlag/ClearFlag 的 AccountSet", }, visual: "🚩", }, @@ -1587,6 +1877,7 @@ getTokenBalances("rYourAddressHere");`, en: "Trading on the native DEX", jp: "ネイティブDEXでのトレーディング", ko: "네이티브 DEX에서 거래하기", + zh: "在原生 DEX 上交易", }, theory: { es: `Xahau incluye un **exchange descentralizado (DEX) nativo** directamente en el protocolo. No necesitas smart contracts ni plataformas externas para intercambiar tokens, todo se hace con transacciones nativas. @@ -1738,6 +2029,20 @@ XahauのDEXはXAHを通じてマルチホップ取引を自動的にルーティ - 가격과 수량 단위를 정확히 이해해야 합니다 - 부분 체결 가능성을 고려해야 합니다 - 유동성이 적으면 원하는 가격으로 체결되지 않을 수 있습니다`, + zh: `Xahau 内置原生 DEX,因此无需额外的智能合约也能进行代币交易。交易基于订单簿和路径查找机制。 + +### 基本组成 + +- \`OfferCreate\`:创建买单或卖单 +- \`OfferCancel\`:取消订单 +- 订单簿:按资产对保存挂单 +- 路径查找:计算经过多个中间资产的兑换路径 + +### 实务中的注意点 + +- 必须准确理解价格和数量单位 +- 要考虑部分成交的可能性 +- 如果流动性不足,可能无法按理想价格成交`, }, codeBlocks: [ { @@ -1746,6 +2051,7 @@ XahauのDEXはXAHを通じてマルチホップ取引を自動的にルーティ en: "Query the order book for a token pair (USD/XAH)", jp: "トークンペアの注文書を照会する(EVR/XAH)", ko: "토큰 쌍의 오더북 조회 (EVR/XAH)", + zh: "查询代币对的订单簿(EVR/XAH)", }, language: "javascript", code: { @@ -1916,6 +2222,48 @@ async function viewOrderBook() { await client.disconnect(); } +viewOrderBook();`, + zh: `const { Client } = require("xahau"); + +async function viewOrderBook() { + // 此示例连接到 Xahau Mainnet,因为那里的 DEX 更可能有活跃订单。 + const client = new Client("wss://xahau.network"); + await client.connect(); + + const issuerAddress = "rEvernodee8dJLaFsujS6q1EiXvZYmHXr8"; + + // 查询挂单:谁在用 EVR 换取 XAH? + const response = await client.request({ + command: "book_offers", + taker_pays: { + currency: "XAH", + }, + taker_gets: { + currency: "EVR", + issuer: issuerAddress, + }, + limit: 10, + }); + + console.log("=== 订单簿: EVR → XAH ==="); + console.log(\`找到的挂单数: \${response.result.offers.length}\`); + + for (const offer of response.result.offers) { + const getsUSD = offer.TakerGets.value || offer.TakerGets; + const paysXAH = + typeof offer.TakerPays === "string" + ? Number(offer.TakerPays) / 1_000_000 + : offer.TakerPays.value; + + console.log(\`账户: \${offer.Account}\`); + console.log(\` 卖出: \${getsUSD} EVR\`); + console.log(\` 想要: \${paysXAH} XAH\`); + console.log(\` Sequence: \${offer.Sequence}\`); + } + + await client.disconnect(); +} + viewOrderBook();`, }, }, @@ -1925,6 +2273,7 @@ viewOrderBook();`, en: "Create an offer on the DEX (sell 100 Tokens for XAH)", jp: "DEXに注文を出す(100トークンをXAHで売る)", ko: "DEX에 오퍼 생성 (100 토큰을 XAH로 판매)", + zh: "在 DEX 上创建订单(卖出 100 个代币换取 XAH)", }, language: "javascript", code: { @@ -2183,6 +2532,68 @@ async function createOffer() { await client.disconnect(); } +createOffer();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +// 如果 token_currency 超过 3 个字符,则转成 40 位十六进制 +function normalizeCurrency(token_currency) { + if (typeof token_currency !== "string") return token_currency; + + const cur = token_currency.trim(); + if (cur.length <= 3) return cur; + + const hex = Buffer.from(cur, "utf8").toString("hex").toUpperCase(); + + if (hex.length > 40) { + throw new Error( + \`token_currency 太长: "\${cur}" -> hex \${hex.length} (>40). UTF-8 最多约 20 bytes。\` + ); + } + + return hex.padEnd(40, "0"); +} + + +async function createOffer() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const trader = Wallet.fromSeed(process.env.RESERVE_SEED, {algorithm: 'secp256k1'}); + const issuerAddress = "rTokenIssuerAddress"; + const tokenCurrencyInput = "YourTokenName"; + + const token_currency = normalizeCurrency(tokenCurrencyInput); + + // 卖出 100 个代币,换取 50 XAH + const offer = { + TransactionType: "OfferCreate", + Account: trader.address, + // 我想收到的:50 XAH + TakerPays: xahToDrops(50), + // 我愿意给出的:100 个代币 + TakerGets: { + currency: token_currency, + issuer: issuerAddress, + value: "100", + }, + }; + + const prepared = await client.autofill(offer); + const signed = trader.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("DEX 订单已创建!"); + console.log(\`卖出 100 个代币,换取 50 XAH(0.5 XAH/代币)\`); + console.log(\`订单 Sequence: \${prepared.Sequence}\`); + } + + await client.disconnect(); +} + createOffer();`, }, }, @@ -2192,6 +2603,7 @@ createOffer();`, en: "Cancel an existing offer on the DEX", jp: "DEXの既存注文をキャンセルする", ko: "DEX의 기존 오퍼 취소", + zh: "取消 DEX 上已有的订单", }, language: "javascript", code: { @@ -2320,38 +2732,72 @@ async function cancelOffer() { await client.disconnect(); } +cancelOffer();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function cancelOffer() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const trader = Wallet.fromSeed(process.env.RESERVE_SEED, {algorithm: 'secp256k1'}); + + // 使用 OfferSequence 取消订单 + const cancel = { + TransactionType: "OfferCancel", + Account: trader.address, + OfferSequence: 12345, // 要取消的订单 Sequence + }; + + const prepared = await client.autofill(cancel); + const signed = trader.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("订单已成功取消!"); + console.log("交易者地址:", trader.address); + } + + await client.disconnect(); +} + cancelOffer();`, }, }, ], slides: [ { - title: { es: "DEX nativo de Xahau", en: "Xahau native DEX", jp: "XahauネイティブDEX", ko: "Xahau 네이티브 DEX" }, + title: { es: "DEX nativo de Xahau", en: "Xahau native DEX", jp: "XahauネイティブDEX", ko: "Xahau 네이티브 DEX", zh: "Xahau 原生 DEX" }, content: { es: "Exchange descentralizado integrado en el protocolo\n\n• Sin smart contracts\n• Sin plataformas externas\n• Liquidación atómica\n• Auto-bridging a través de XAH\n\nTodo con transacciones nativas", en: "Decentralized exchange built into the protocol\n\n• No smart contracts\n• No external platforms\n• Atomic settlement\n• Auto-bridging through XAH\n\nAll with native transactions", jp: "プロトコルに組み込まれた分散型取引所\n\n• スマートコントラクト不要\n• 外部プラットフォーム不要\n• アトミック決済\n• XAHを通じたオートブリッジング\n\nすべてネイティブトランザクションで", ko: "프로토콜에 내장된 탈중앙화 거래소\n\n• 스마트 컨트랙트 불필요\n• 외부 플랫폼 불필요\n• 원자적 결제\n• XAH를 통한 자동 브리징\n\n모두 네이티브 트랜잭션으로 동작", + zh: "集成在协议中的去中心化交易所\n\n• 不需要智能合约\n• 不需要外部平台\n• 原子结算\n• 通过 XAH 自动桥接\n\n全部使用原生交易完成", }, visual: "📈", }, { - title: { es: "OfferCreate: anatomía de una orden", en: "OfferCreate: anatomy of an order", jp: "OfferCreate:注文の構造", ko: "OfferCreate: 주문 구조" }, + title: { es: "OfferCreate: anatomía de una orden", en: "OfferCreate: anatomy of an order", jp: "OfferCreate:注文の構造", ko: "OfferCreate: 주문 구조", zh: "OfferCreate:订单结构" }, content: { es: "TakerPays → Lo que quieres RECIBIR\nTakerGets → Lo que estás dispuesto a DAR\n\nFlags especiales:\n• tfImmediateOrCancel → Ejecutar o cancelar\n• tfPassive → Solo match existente\n• tfFillOrKill → Ejecutar todo o nada\n• tfSell → Recibe tanto como la cantidad de TakerGets\n\nOfferCancel → Cancelar orden abierta", en: "TakerPays -> What you want to RECEIVE\nTakerGets -> What you are willing to GIVE\n\nSpecial flags:\n• tfImmediateOrCancel -> Execute or cancel\n• tfPassive -> Only match existing\n• tfFillOrKill -> Execute all or nothing\n• tfSell -> Receive as much as TakerGets amount\n\nOfferCancel -> Cancel open order", jp: "TakerPays -> 受け取りたいもの\nTakerGets -> 提供する意思があるもの\n\n特殊フラグ:\n• tfImmediateOrCancel -> 実行またはキャンセル\n• tfPassive -> 既存注文にのみマッチ\n• tfFillOrKill -> 全量実行またはキャンセル\n• tfSell -> 可能な限り多くの金額を受け取る\n\nOfferCancel -> 未決注文をキャンセル", ko: "TakerPays → 내가 받고 싶은 것\nTakerGets → 내가 내놓을 것\n\n특수 플래그:\n• tfImmediateOrCancel → 즉시 실행 아니면 취소\n• tfPassive → 기존 주문과만 매칭\n• tfFillOrKill → 전량 체결 아니면 취소\n• tfSell → TakerGets 전량 기준으로 매도\n\nOfferCancel → 열린 주문 취소", + zh: "TakerPays → 你想收到的东西\nTakerGets → 你愿意给出的东西\n\n特殊标志:\n• tfImmediateOrCancel → 立即执行否则取消\n• tfPassive → 只与现有订单撮合\n• tfFillOrKill → 全部成交否则取消\n• tfSell → 以 TakerGets 全量为基准卖出\n\nOfferCancel → 取消未成交订单", }, visual: "🔄", }, { - title: { es: "Auto-bridging y order book", en: "Auto-bridging and order book", jp: "オートブリッジングと注文書", ko: "자동 브리징과 오더북" }, + title: { es: "Auto-bridging y order book", en: "Auto-bridging and order book", jp: "オートブリッジングと注文書", ko: "자동 브리징과 오더북", zh: "自动桥接与订单簿" }, content: { es: "El DEX enruta trades multi-salto vía XAH\n\nEjemplo: USD → XAH → EUR\n\n• book_offers → Ver el libro de órdenes\n• Bids y Asks se cruzan automáticamente\n• Ejecución parcial o total\n• Liquidez compartida entre pares", en: "The DEX routes multi-hop trades via XAH\n\nExample: USD -> XAH -> EUR\n\n• book_offers -> View the order book\n• Bids and Asks cross automatically\n• Partial or full execution\n• Shared liquidity across pairs", jp: "DEXはXAHを経由してマルチホップ取引をルーティング\n\n例:USD -> XAH -> EUR\n\n• book_offers -> 注文書を表示\n• BidsとAsksが自動的に交差\n• 部分または全量実行\n• ペア間で流動性を共有", ko: "DEX는 XAH를 통해 멀티홉 거래를 라우팅함\n\n예: USD → XAH → EUR\n\n• book_offers → 오더북 보기\n• 매수/매도 주문 자동 매칭\n• 부분 또는 전량 체결\n• 거래쌍 간 유동성 공유", + zh: "DEX 会通过 XAH 路由多跳交易\n\n示例:USD → XAH → EUR\n\n• book_offers → 查看订单簿\n• 买单和卖单会自动撮合\n• 可部分成交或完全成交\n• 不同交易对之间共享流动性", }, visual: "🌐", }, @@ -2364,6 +2810,7 @@ cancelOffer();`, en: "Advanced token control: Freeze and Clawback", jp: "高度なトークン制御:FreezeとClawback", ko: "고급 토큰 제어: Freeze와 Clawback", + zh: "高级代币控制:Freeze 与 Clawback", }, theory: { es: `Xahau ofrece a los emisores de tokens herramientas avanzadas de control: **Freeze** (congelación), **Clawback** (recuperación forzada), **Transfer fees** (comisiones de transferencia) y **Authorized TrustLines** (líneas de confianza autorizadas). @@ -2497,6 +2944,17 @@ The \`RequireAuth\` flag (asfRequireAuth) on the issuing account requires the is ### 왜 민감한가? 이 기능들은 강력하지만 사용자 신뢰와 직결됩니다. 발행자는 언제, 왜, 어떤 범위로 사용할지 명확한 정책을 가져야 하며, 사용자도 해당 토큰의 중앙화 수준을 이해해야 합니다.`, + zh: `有些发行方出于合规或运营原因,需要更强的控制能力。Xahau 提供了 **Freeze** 和 **Clawback** 等高级控制功能。 + +### 主要功能 + +- **Freeze**:限制某条 TrustLine 或代币的使用 +- **Global Freeze**:大范围限制整个已发行代币的流动 +- **Clawback**:在特定条件下回收代币 + +### 为什么这很敏感? + +这些功能非常强大,也直接影响用户信任。发行方需要明确说明何时、为何以及在多大范围内使用它们,用户也应了解该代币的中心化程度。`, }, codeBlocks: [ { @@ -2505,6 +2963,7 @@ The \`RequireAuth\` flag (asfRequireAuth) on the issuing account requires the is en: "Create a TrustLine from a holder toward the issuer", jp: "ホルダーから発行者へのTrustLineを作成する", ko: "홀더에서 발행자로 TrustLine 생성", + zh: "从持有人到发行方创建 TrustLine", }, language: "javascript", code: { @@ -2766,6 +3225,64 @@ async function createHolderTrustLine() { await client.disconnect(); } +createHolderTrustLine();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +// 这段代码会从一个账户(持有人)指向代币发行方创建 TrustLine。 +// 这样发行方之后在有需要时就可以冻结这条 TrustLine。 + +function normalizeCurrency(token_currency) { + if (typeof token_currency !== "string") return token_currency; + + const cur = token_currency.trim(); + if (cur.length <= 3) return cur; + + const hex = Buffer.from(cur, "utf8").toString("hex").toUpperCase(); + if (hex.length > 40) { + throw new Error( + \`token_currency 太长: "\${cur}" -> hex \${hex.length} (>40). UTF-8 最多约 20 bytes。\` + ); + } + return hex.padEnd(40, "0"); +} + +async function createHolderTrustLine() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const holder = Wallet.fromSeed(process.env.FROZEN_SEED, {algorithm: 'secp256k1'}); + const issuerAddress = "rIssuerAddress"; + const tokenCurrencyInput = "YourTokenName"; + const token_currency = normalizeCurrency(tokenCurrencyInput); + + const trustSet = { + TransactionType: "TrustSet", + Account: holder.address, + LimitAmount: { + currency: token_currency, + issuer: issuerAddress, + value: "1000000", + }, + }; + + const prepared = await client.autofill(trustSet); + const signed = holder.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("TrustLine 已创建!"); + console.log("持有人:", holder.address); + console.log("发行方:", issuerAddress); + console.log("\\n现在发行方可以向这个账户发送代币。"); + console.log("如有需要,也可以冻结这条 TrustLine。"); + } + + await client.disconnect(); +} + createHolderTrustLine();`, }, }, @@ -2775,6 +3292,7 @@ createHolderTrustLine();`, en: "Freeze a specific user's TrustLine", jp: "特定ユーザーのTrustLineを凍結する", ko: "특정 사용자의 TrustLine 동결", + zh: "冻结某个用户的 TrustLine", }, language: "javascript", code: { @@ -3012,38 +3530,93 @@ async function freezeTrustLine() { await client.disconnect(); } +freezeTrustLine();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +function normalizeCurrency(token_currency) { + if (typeof token_currency !== "string") return token_currency; + const cur = token_currency.trim(); + if (cur.length <= 3) return cur; + const hex = Buffer.from(cur, "utf8").toString("hex").toUpperCase(); + if (hex.length > 40) { + throw new Error( + \`token_currency 太长: "\${cur}" -> hex \${hex.length} (>40). UTF-8 最多约 20 bytes。\` + ); + } + return hex.padEnd(40, "0"); +} + +async function freezeTrustLine() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const issuer = Wallet.fromSeed(process.env.ISSUER_SEED, {algorithm: 'secp256k1'}); + const holderAddress = "rHolderAddress"; + const tokenCurrencyInput = "YourTokenName"; + const token_currency = normalizeCurrency(tokenCurrencyInput); + + // 冻结该持有人的代币 TrustLine + const trustSet = { + TransactionType: "TrustSet", + Account: issuer.address, + LimitAmount: { + currency: token_currency, + issuer: holderAddress, + value: "0", + }, + Flags: 1048576, // tfSetFreeze + }; + + const prepared = await client.autofill(trustSet); + const signed = issuer.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log(\`\${holderAddress} 的代币 TrustLine 已被冻结\`); + console.log("该持有人将无法发送或接收此代币"); + } + + await client.disconnect(); +} + freezeTrustLine();`, }, }, ], slides: [ { - title: { es: "Freeze: congelación de tokens", en: "Freeze: token freezing", jp: "Freeze:トークンの凍結", ko: "Freeze: 토큰 동결" }, + title: { es: "Freeze: congelación de tokens", en: "Freeze: token freezing", jp: "Freeze:トークンの凍結", ko: "Freeze: 토큰 동결", zh: "Freeze:代币冻结" }, content: { es: "El emisor puede congelar transferencias\n\n• Individual Freeze → Una TrustLine específica\n• Global Freeze → TODAS las TrustLines\n• NoFreeze → Renunciar permanentemente\n\nCasos: regulación, seguridad, disputas", en: "The issuer can freeze transfers\n\n• Individual Freeze -> A specific TrustLine\n• Global Freeze -> ALL TrustLines\n• NoFreeze -> Permanently renounce\n\nUse cases: regulation, security, disputes", jp: "発行者は転送を凍結できます\n\n• 個別Freeze -> 特定のTrustLine\n• グローバルFreeze -> すべてのTrustLine\n• NoFreeze -> 恒久的に放棄\n\nユースケース:規制、セキュリティ、紛争", ko: "발행자는 전송을 동결할 수 있습니다\n\n• 개별 Freeze → 특정 TrustLine\n• Global Freeze → 모든 TrustLine\n• NoFreeze → 영구적으로 권한 포기\n\n사례: 규제, 보안, 분쟁", + zh: "发行方可以冻结转账\n\n• Individual Freeze → 某一条特定 TrustLine\n• Global Freeze → 所有 TrustLine\n• NoFreeze → 永久放弃冻结权\n\n使用场景:合规、安全、争议处理", }, visual: "🧊", }, { - title: { es: "Clawback: recuperación forzada", en: "Clawback: forced recovery", jp: "Clawback:強制回収", ko: "Clawback: 강제 회수" }, + title: { es: "Clawback: recuperación forzada", en: "Clawback: forced recovery", jp: "Clawback:強制回収", ko: "Clawback: 강제 회수", zh: "Clawback:强制追回" }, content: { es: "Reclamar tokens de cualquier holder\n\n1️⃣ Activar asfAllowTrustLineClawback\n2️⃣ Usar transacción Clawback\n\n⚠️ Debe activarse ANTES de emitir tokens\n⚠️ Incompatible con NoFreeze", en: "Reclaim tokens from any holder\n\n1️⃣ Activate asfAllowTrustLineClawback\n2️⃣ Use Clawback transaction\n\n⚠️ Must be activated BEFORE issuing tokens\n⚠️ Incompatible with NoFreeze", jp: "任意のホルダーからトークンを回収\n\n1️⃣ asfAllowTrustLineClawbackを有効化\n2️⃣ Clawbackトランザクションを使用\n\n⚠️ トークン発行前に有効化が必要\n⚠️ NoFreezeとは非互換", ko: "어떤 홀더에게서도 토큰 회수 가능\n\n1️⃣ asfAllowTrustLineClawback 활성화\n2️⃣ Clawback 트랜잭션 사용\n\n⚠️ 토큰 발행 전에 활성화해야 함\n⚠️ NoFreeze와 호환되지 않음", + zh: "可以从任何持有人处追回代币\n\n1️⃣ 启用 asfAllowTrustLineClawback\n2️⃣ 使用 Clawback 交易\n\n⚠️ 必须在发行代币前启用\n⚠️ 与 NoFreeze 不兼容", }, visual: "🔙", }, { - title: { es: "Transfer fees y RequireAuth", en: "Transfer fees and RequireAuth", jp: "Transfer feesとRequireAuth", ko: "Transfer fees와 RequireAuth" }, + title: { es: "Transfer fees y RequireAuth", en: "Transfer fees and RequireAuth", jp: "Transfer feesとRequireAuth", ko: "Transfer fees와 RequireAuth", zh: "Transfer fees 与 RequireAuth" }, content: { es: "Transfer fees:\n• TransferRate en AccountSet\n• Porcentaje en cada transferencia entre terceros\n• Ejemplo: 0.1% → 1001000000\n\nRequireAuth:\n• El emisor autoriza cada TrustLine\n• Ideal para tokens con KYC", en: "Transfer fees:\n• TransferRate in AccountSet\n• Percentage on each transfer between third parties\n• Example: 0.1% -> 1001000000\n\nRequireAuth:\n• Issuer authorizes each TrustLine\n• Ideal for tokens with KYC", jp: "Transfer fees:\n• AccountSetのTransferRate\n• 第三者間転送ごとに割合を徴収\n• 例:0.1% -> 1001000000\n\nRequireAuth:\n• 発行者が各TrustLineを承認\n• KYCトークンに最適", ko: "Transfer fees:\n• AccountSet의 TransferRate 사용\n• 제3자 간 전송마다 비율 적용\n• 예: 0.1% → 1001000000\n\nRequireAuth:\n• 발행자가 각 TrustLine 승인\n• KYC가 필요한 토큰에 적합", + zh: "Transfer fees:\n• 在 AccountSet 中使用 TransferRate\n• 对第三方之间的每次转账收取比例费用\n• 示例:0.1% → 1001000000\n\nRequireAuth:\n• 发行方批准每一条 TrustLine\n• 适合需要 KYC 的代币", }, visual: "🔐", }, diff --git a/src/data/modules/m08-nfts.js b/src/data/modules/m08-nfts.js index 836697e..82744ec 100644 --- a/src/data/modules/m08-nfts.js +++ b/src/data/modules/m08-nfts.js @@ -6,6 +6,7 @@ export default { en: "Creating and Using NFTs", jp: "NFTの作成と使用", ko: "NFT 생성 및 사용", + zh: "NFT 的创建与使用", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "URITokens: Native NFTs on Xahau", jp: "URITokens:XahauのネイティブNFT", ko: "URITokens: Xahau의 네이티브 NFT", + zh: "URITokens:Xahau 的原生 NFT", }, theory: { es: `En Xahau, los NFTs se implementan como **URITokens**, objetos nativos del ledger que representan tokens no fungibles con una URI asociada. @@ -149,6 +151,39 @@ URIToken은 레저 상의 **고유한** 객체로 다음 필드를 포함합니 ### URITokenMint 플래그 - **tfBurnable (1)**: 발행자가 더 이상 소유자가 아니더라도 토큰을 소각할 수 있도록 허용`, + zh: `在 Xahau 中,NFT 通过 **URIToken** 实现,它是账本中的原生对象,用来表示带有关联 URI 的非同质化代币。 + +### 什么是 URIToken? + +URIToken 是账本中的一个**唯一**对象,包含: +- **ID**:代币唯一标识符(LedgerIndex) +- **URI**:指向 NFT 元数据或内容的链接(图片、JSON 等) +- **Digest**:URI 指向内容的可选哈希(用于完整性校验) +- **Owner**:当前持有者账户 +- **Issuer**:最初创建它的账户 + +### URIToken vs ERC-721 + +| 特性 | ERC-721 (Ethereum) | URIToken (Xahau) | +|---|---|---| +| 创建集合 | 部署 Solidity 合约 | 不需要 | +| Mint NFT | 合约函数 | \`URITokenMint\` 交易 | +| 转移 | 合约函数 | \`URITokenBuy\` 交易 | +| 元数据 | 合约中的 tokenURI | 对象中的原生 URI | +| 成本 | Gas 昂贵 | 最低手续费(约 12 drops) | +| 验证 | 依赖合约 | 账本中的原生 Digest | + +### 与 URIToken 相关的交易 + +- **URITokenMint**:创建新的 URIToken +- **URITokenBurn**:销毁 URIToken +- **URITokenCreateSellOffer**:将 URIToken 挂牌出售 +- **URITokenCancelSellOffer**:取消卖单 +- **URITokenBuy**:购买正在出售的 URIToken + +### URITokenMint 的标志 + +- **tfBurnable (1)**:即使发行方已经不是持有者,仍允许其销毁该代币`, }, codeBlocks: [ { @@ -157,6 +192,7 @@ URIToken은 레저 상의 **고유한** 객체로 다음 필드를 포함합니 en: "Create (Mint) a URIToken", jp: "URITokenを作成(ミント)する", ko: "URIToken 생성 (민팅)", + zh: "创建(Mint)一个 URIToken", }, language: "javascript", code: { @@ -346,6 +382,53 @@ async function mintURIToken() { await client.disconnect(); } +mintURIToken();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +function toHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); +} + +async function mintURIToken() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const creator = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 使用指向元数据的 URI 创建一个 URIToken + const mint = { + TransactionType: "URITokenMint", + Account: creator.address, + // URI 示例(可以是 IPFS、HTTPS 等)- 例如:ipfs://bafybeieza5w4rkes55paw7jgpo4kzsbyywhw7ildltk3kjx2ttkmt7texa/106.json + URI: toHex("https://example.com/nft/metadata.json"), + Flags: 1, // tfBurnable:发行方可以销毁该代币 + }; + + const prepared = await client.autofill(mint); + const signed = creator.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("URIToken 创建成功!"); + console.log("交易哈希:", signed.hash); + + // 在受影响节点中查找创建出的 URIToken + const created = result.result.meta.AffectedNodes.find( + (n) => n.CreatedNode?.LedgerEntryType === "URIToken" + ); + if (created) { + console.log("URIToken ID:", created.CreatedNode.LedgerIndex); + console.log("地址:", creator.address); + + } + } + + await client.disconnect(); +} + mintURIToken();`, }, }, @@ -355,6 +438,7 @@ mintURIToken();`, en: "Query URITokens for an Account", jp: "アカウントのURITokenを照会する", ko: "계정의 URIToken 조회", + zh: "查询账户的 URIToken", }, language: "javascript", code: { @@ -501,38 +585,77 @@ async function getURITokens(address) { await client.disconnect(); } // 조회할 주소로 교체하세요. 예: r9oB9E7jnRjp88fTrxHzngAietepwCCcqV +getURITokens("rYourAddressHere");`, + zh: `const { Client } = require("xahau"); + +async function getURITokens(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const response = await client.request({ + command: "account_objects", + account: address, + type: "uri_token", + ledger_index: "validated", + }); + + const tokens = response.result.account_objects; + console.log(\`=== \${address} 的 URIToken ===\`); + console.log(\`总数: \${tokens.length}\\n\`); + + for (const token of tokens) { + const uri = Buffer.from(token.URI, "hex").toString("utf8"); + console.log(\`URIToken ID: \${token.index}\`); + console.log(\` URI: \${uri}\`); + console.log(\` 发行方: \${token.Issuer}\`); + console.log(\` 持有者: \${token.Owner}\`); + if (token.Digest) { + console.log(\` Digest: \${token.Digest}\`); + } + if (token.Amount) { + console.log(\` 挂牌价格: \${Number(token.Amount) / 1_000_000} XAH\`); + } + console.log(); + } + + await client.disconnect(); +} +// 替换成你要查询的地址,例如 r9oB9E7jnRjp88fTrxHzngAietepwCCcqV getURITokens("rYourAddressHere");`, }, }, ], slides: [ { - title: { es: "URITokens en Xahau", en: "URITokens on Xahau", jp: "XahauのURIToken", ko: "Xahau의 URIToken" }, + title: { es: "URITokens en Xahau", en: "URITokens on Xahau", jp: "XahauのURIToken", ko: "Xahau의 URIToken", zh: "Xahau 上的 URIToken" }, content: { es: "NFTs nativos del ledger de Xahau\n\n• URI → Enlace a metadatos\n• Digest → Hash de verificación\n• Owner → Propietario actual\n• Issuer → Creador original\n\nSin necesidad de smart contracts", en: "Native NFTs on the Xahau ledger\n\n• URI → Link to metadata\n• Digest → Verification hash\n• Owner → Current owner\n• Issuer → Original creator\n\nNo smart contracts needed", jp: "Xahauレジャーのネイティブ NFT\n\n• URI → メタデータへのリンク\n• Digest → 検証ハッシュ\n• Owner → 現在の所有者\n• Issuer → 元の作成者\n\nスマートコントラクト不要", ko: "Xahau 레저의 네이티브 NFT\n\n• URI → 메타데이터 링크\n• Digest → 검증 해시\n• Owner → 현재 소유자\n• Issuer → 최초 생성자\n\n스마트 컨트랙트 불필요", + zh: "Xahau 账本中的原生 NFT\n\n• URI → 元数据链接\n• Digest → 校验哈希\n• Owner → 当前持有者\n• Issuer → 原始创建者\n\n不需要智能合约", }, visual: "🎨", }, { - title: { es: "Operaciones con URITokens", en: "URIToken Operations", jp: "URITokenの操作", ko: "URIToken 작업" }, + title: { es: "Operaciones con URITokens", en: "URIToken Operations", jp: "URITokenの操作", ko: "URIToken 작업", zh: "URIToken 操作" }, content: { es: "• URITokenMint → Crear NFT\n• URITokenBurn → Destruir NFT\n• URITokenCreateSellOffer → Vender\n• URITokenCancelSellOffer → Cancelar venta\n• URITokenBuy → Comprar", en: "• URITokenMint → Create NFT\n• URITokenBurn → Destroy NFT\n• URITokenCreateSellOffer → Sell\n• URITokenCancelSellOffer → Cancel sale\n• URITokenBuy → Buy", jp: "• URITokenMint → NFTを作成\n• URITokenBurn → NFTを破棄\n• URITokenCreateSellOffer → 売りに出す\n• URITokenCancelSellOffer → 売りをキャンセル\n• URITokenBuy → 購入する", ko: "• URITokenMint → NFT 생성\n• URITokenBurn → NFT 소각\n• URITokenCreateSellOffer → 판매 등록\n• URITokenCancelSellOffer → 판매 취소\n• URITokenBuy → 구매", + zh: "• URITokenMint → 创建 NFT\n• URITokenBurn → 销毁 NFT\n• URITokenCreateSellOffer → 出售\n• URITokenCancelSellOffer → 取消出售\n• URITokenBuy → 购买", }, visual: "🔧", }, { - title: { es: "URIToken vs ERC-721", en: "URIToken vs ERC-721", jp: "URIToken vs ERC-721", ko: "URIToken vs ERC-721" }, + title: { es: "URIToken vs ERC-721", en: "URIToken vs ERC-721", jp: "URIToken vs ERC-721", ko: "URIToken vs ERC-721", zh: "URIToken vs ERC-721" }, content: { es: "URIToken (Xahau):\n• Nativo del ledger, sin contratos\n• Fee mínimo (~12 drops)\n• Digest nativo para verificación\n\nERC-721 (Ethereum):\n• Requiere contrato Solidity\n• Gas costoso y variable\n• Verificación depende del contrato", en: "URIToken (Xahau):\n• Native to the ledger, no contracts\n• Minimal fee (~12 drops)\n• Native Digest for verification\n\nERC-721 (Ethereum):\n• Requires Solidity contract\n• Expensive and variable gas\n• Verification depends on contract", jp: "URIToken(Xahau):\n• レジャーネイティブ、コントラクト不要\n• 最小限のFee(〜12 drops)\n• 検証用のネイティブDigest\n\nERC-721(Ethereum):\n• Solidityコントラクトが必要\n• 高価で変動するガス代\n• 検証はコントラクトに依存", ko: "URIToken (Xahau):\n• 레저 네이티브, 컨트랙트 불필요\n• 최소 수수료 (~12 drops)\n• 검증을 위한 네이티브 Digest\n\nERC-721 (Ethereum):\n• Solidity 컨트랙트 필요\n• 비싸고 가변적인 가스비\n• 검증이 컨트랙트에 의존", + zh: "URIToken(Xahau):\n• 账本原生,无需合约\n• 最低手续费(约 12 drops)\n• 使用原生 Digest 验证\n\nERC-721(Ethereum):\n• 需要 Solidity 合约\n• Gas 昂贵且波动大\n• 验证依赖合约", }, visual: "⚖️", }, @@ -545,6 +668,7 @@ getURITokens("rYourAddressHere");`, en: "Buying and Selling URITokens", jp: "URITokenの売買", ko: "URIToken 매매", + zh: "URIToken 的买卖", }, theory: { es: `Xahau incluye un sistema nativo para la compra-venta de URITokens, sin necesidad de marketplaces externos ni smart contracts. @@ -623,6 +747,25 @@ URIToken을 비용 없이 전송(선물)하려면 \`Amount: "0"\`과 특정 \`De ### URIToken 소각 현재 소유자는 언제든지 \`URITokenBurn\`으로 URIToken을 소각(파괴)할 수 있습니다. \`tfBurnable\` 플래그로 생성된 토큰은 원래 발행자도 소각할 수 있습니다.`, + zh: `Xahau 内置了买卖 URIToken 的原生系统,不需要外部市场或智能合约。 + +### 出售流程 + +1. 持有者通过 \`URITokenCreateSellOffer\` 创建**卖单**,并指定 XAH 或其他货币的价格。 +2. 任何人都可以通过 \`URITokenBuy\` **购买** URIToken,并支付标价。 +3. 持有者可以通过 \`URITokenCancelSellOffer\` **取消**该卖单。 + +### 卖给指定接收者 + +你可以使用 \`Destination\` 字段创建面向特定账户的卖单。只有该账户才能购买这个 URIToken。 + +### 免费转移 + +如果你想免费转移一个 URIToken(例如赠送),可以创建一个 \`Amount: "0"\` 且带有特定 \`Destination\` 的卖单。 + +### 销毁 URIToken + +当前持有者始终可以通过 \`URITokenBurn\` 销毁自己的 URIToken。如果该代币在创建时启用了 \`tfBurnable\` 标志,则最初的发行方也可以销毁它。`, }, codeBlocks: [ { @@ -631,6 +774,7 @@ URIToken을 비용 없이 전송(선물)하려면 \`Amount: "0"\`과 특정 \`De en: "List a URIToken for Sale", jp: "URITokenを売りに出す", ko: "URIToken 판매 등록", + zh: "将 URIToken 挂牌出售", }, language: "javascript", code: { @@ -757,6 +901,37 @@ async function sellURIToken() { await client.disconnect(); } +sellURIToken();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function sellURIToken() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const owner = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 创建一个售价为 5 XAH 的卖单 + const sellOffer = { + TransactionType: "URITokenCreateSellOffer", + Account: owner.address, + URITokenID: "YOUR_URITOKEN_ID_HERE", // 要出售的 URIToken ID + Amount: xahToDrops(5), // 价格:5 XAH + }; + + const prepared = await client.autofill(sellOffer); + const signed = owner.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("URIToken 已以 5 XAH 挂牌出售!"); + } + + await client.disconnect(); +} + sellURIToken();`, }, }, @@ -766,6 +941,7 @@ sellURIToken();`, en: "Buy a URIToken That Is Listed for Sale", jp: "売りに出ているURITokenを購入する", ko: "판매 중인 URIToken 구매", + zh: "购买正在出售的 URIToken", }, language: "javascript", code: { @@ -900,38 +1076,74 @@ async function buyURIToken() { await client.disconnect(); } +buyURIToken();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function buyURIToken() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const buyer = Wallet.fromSeed(process.env.BUYER_SEED, {algorithm: 'secp256k1'}); + + // 按照卖价支付并购买 URIToken + const buy = { + TransactionType: "URITokenBuy", + Account: buyer.address, + URITokenID: "YOUR_URITOKEN_ID_HERE", // 要购买的 URIToken ID + Amount: xahToDrops(5), // 必须与卖价一致 + }; + + const prepared = await client.autofill(buy); + const signed = buyer.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("URIToken 购买成功!"); + console.log("这个 NFT 现在属于你了。"); + console.log("买家地址:", buyer.address); + } + + await client.disconnect(); +} + buyURIToken();`, }, }, ], slides: [ { - title: { es: "Flujo de venta", en: "Sale Flow", jp: "売却フロー", ko: "판매 흐름" }, + title: { es: "Flujo de venta", en: "Sale Flow", jp: "売却フロー", ko: "판매 흐름", zh: "出售流程" }, content: { es: "1️⃣ URITokenCreateSellOffer → Poner precio\n2️⃣ URITokenBuy → Comprador paga\n3️⃣ Transferencia automática\n\nTodo nativo, sin marketplace externo", en: "1️⃣ URITokenCreateSellOffer → Set price\n2️⃣ URITokenBuy → Buyer pays\n3️⃣ Automatic transfer\n\nAll native, no external marketplace", jp: "1️⃣ URITokenCreateSellOffer → 価格を設定\n2️⃣ URITokenBuy → 購入者が支払う\n3️⃣ 自動転送\n\nすべてネイティブ、外部マーケットプレイス不要", ko: "1️⃣ URITokenCreateSellOffer → 가격 설정\n2️⃣ URITokenBuy → 구매자 지불\n3️⃣ 자동 전송\n\n모두 네이티브, 외부 마켓플레이스 불필요", + zh: "1️⃣ URITokenCreateSellOffer → 设置价格\n2️⃣ URITokenBuy → 买家付款\n3️⃣ 自动转移\n\n全部为原生流程,无需外部市场", }, visual: "💰", }, { - title: { es: "Transferir y quemar", en: "Transfer and Burn", jp: "転送とバーン", ko: "전송과 소각" }, + title: { es: "Transferir y quemar", en: "Transfer and Burn", jp: "転送とバーン", ko: "전송과 소각", zh: "转移与销毁" }, content: { es: "Transferir gratis:\n• SellOffer con Amount: 0 + Destination\n\nQuemar (destruir):\n• URITokenBurn por el propietario\n• O por el emisor si tiene flag tfBurnable", en: "Free transfer:\n• SellOffer with Amount: 0 + Destination\n\nBurn (destroy):\n• URITokenBurn by the owner\n• Or by the issuer if tfBurnable flag is set", jp: "無料転送:\n• Amount: 0 + DestinationのSellOffer\n\nバーン(破棄):\n• 所有者によるURITokenBurn\n• またはtfBurnableフラグがあれば発行者も可", ko: "무료 전송:\n• Amount: 0 + Destination의 SellOffer\n\n소각 (파괴):\n• 소유자에 의한 URITokenBurn\n• 또는 tfBurnable 플래그가 있으면 발행자도 가능", + zh: "免费转移:\n• 使用 Amount: 0 + Destination 的 SellOffer\n\n销毁:\n• 持有者执行 URITokenBurn\n• 或者在启用 tfBurnable 时由发行方执行", }, visual: "🔥", }, { - title: { es: "Quemar URITokens en detalle", en: "Burning URITokens in Detail", jp: "URITokenのバーンの詳細", ko: "URIToken 소각 상세" }, + title: { es: "Quemar URITokens en detalle", en: "Burning URITokens in Detail", jp: "URITokenのバーンの詳細", ko: "URIToken 소각 상세", zh: "URIToken 销毁详解" }, content: { es: "Flag tfBurnable (1) al mintear:\n• Permite al emisor quemar el token\n• Incluso si ya no es propietario\n\nSin tfBurnable:\n• Solo el propietario actual puede quemar\n\nUsos: eliminar errores de minteo,\ncontenido expirado, tokens revocables", en: "tfBurnable flag (1) at mint time:\n• Allows the issuer to burn the token\n• Even if they are no longer the owner\n\nWithout tfBurnable:\n• Only the current owner can burn\n\nUse cases: fix minting errors,\nexpired content, revocable tokens", jp: "ミント時のtfBurnableフラグ(1):\n• 発行者がトークンをバーンできる\n• もはや所有者でなくても\n\ntfBurnableなし:\n• 現在の所有者のみバーン可能\n\nユースケース:ミントエラーの修正、\n期限切れコンテンツ、取り消し可能なトークン", ko: "민팅 시 tfBurnable 플래그 (1):\n• 발행자가 토큰을 소각할 수 있음\n• 더 이상 소유자가 아니더라도\n\ntfBurnable 없음:\n• 현재 소유자만 소각 가능\n\n사용 사례: 민팅 오류 수정,\n만료된 콘텐츠, 취소 가능한 토큰", + zh: "Mint 时的 tfBurnable 标志(1):\n• 允许发行方销毁该代币\n• 即使其已不再是持有者\n\n没有 tfBurnable:\n• 只有当前持有者可以销毁\n\n使用场景:修复 mint 错误、\n过期内容、可撤销代币", }, visual: "🗑️", }, @@ -944,6 +1156,7 @@ buyURIToken();`, en: "Metadata and Standards for URITokens", jp: "URITokenのメタデータと標準", ko: "URIToken의 메타데이터와 표준", + zh: "URIToken 的元数据与标准", }, theory: { es: `Los metadatos son la clave para que un NFT sea útil y verificable. En Xahau, los URITokens usan los campos **URI** y **Digest** para enlazar y verificar el contenido asociado. @@ -1154,38 +1367,93 @@ ERC-721과 유사한 표준을 따라, URIToken의 JSON 메타데이터에는 - **프로덕션에는 IPFS를 사용하세요**: 불변성과 분산화가 NFT의 가치를 보호합니다. - **JSON을 일관되게 유지하세요**: 마켓플레이스 및 탐색기와의 호환성을 위해 메타데이터 표준을 따르세요. - **URI에 민감한 데이터를 넣지 마세요**: 레저의 모든 것은 공개 정보입니다.`, + zh: `元数据是让 NFT 变得有用且可验证的关键。在 Xahau 中,URIToken 使用 **URI** 和 **Digest** 字段来链接并验证相关内容。 + +### URI 字段:应填写什么 + +URI 是指向 NFT 内容或元数据的链接,常见选择包括: + +- **IPFS 链接**(\`ipfs://QmXxx...\`):去中心化存储。内容不可变,并通过哈希寻址。这是生产环境中**推荐**的选择 +- **HTTPS 链接**(\`https://my-server.com/metadata/1.json\`):中心化存储。实现简单,但依赖服务器可用性 + +### Digest 字段:完整性验证 + +**Digest** 是 URI 所指向内容的 SHA-256 哈希。它允许任何人验证该内容自 NFT 创建以来是否被篡改。它以 64 个十六进制字符的形式存储在账本中。 + +### JSON 元数据标准 + +遵循与 ERC-721 类似的标准,URIToken 的 JSON 元数据通常包含: + +\`\`\`json +{ + "content": { + "url": "ipfs://bafybeign6w3zkxxqohchtxyv4qot6zrwcrvosmmrz2c6ayijl67h42s3km/106.png" + }, + "details": { + "title": "你的 NFT 名称", + "categories": [ + "0001" + ], + "publisher": { + "name": "你的名字", + "url": "https://www.yourwebsite.com", + "email": "youremail@gmail.com" + }, + "group": { + "title": "你的合集标题" + } + } +} +\`\`\` + +### 存储选项 + +| 选项 | 优点 | 缺点 | +|---|---|---| +| **IPFS** | 去中心化、不可变、哈希寻址 | 需要 pinning 来保证持久性 | +| **中心化服务器** | 简单、快速 | 单点故障、可变 | + +### 最佳实践 + +- **始终设置 Digest**:这样可以随时验证内容完整性 +- **生产环境优先使用 IPFS**:不可变性和去中心化能保护 NFT 的价值 +- **保持 JSON 结构一致**:遵循元数据标准有助于与市场和浏览器兼容 +- **不要把敏感数据放进 URI**:账本上的一切都是公开的`, }, codeBlocks: [ ], slides: [ { - title: { es: "El campo URI: opciones de enlace", en: "The URI Field: Link Options", jp: "URIフィールド:リンクの選択肢", ko: "URI 필드: 링크 옵션" }, + title: { es: "El campo URI: opciones de enlace", en: "The URI Field: Link Options", jp: "URIフィールド:リンクの選択肢", ko: "URI 필드: 링크 옵션", zh: "URI 字段:链接选项" }, content: { es: "¿A dónde apunta tu NFT?\n\n• ipfs://Qm... → Descentralizado e inmutable\n• https://... → Centralizado pero simple\n", en: "Where does your NFT point to?\n\n• ipfs://Qm... → Decentralized and immutable\n• https://... → Centralized but simple\n", jp: "あなたのNFTはどこを指しているか?\n\n• ipfs://Qm... → 分散型かつ不変\n• https://... → 集中型だがシンプル\n", ko: "당신의 NFT는 어디를 가리키나요?\n\n• ipfs://Qm... → 분산형이며 불변\n• https://... → 중앙화되었지만 단순\n", + zh: "你的 NFT 指向哪里?\n\n• ipfs://Qm... → 去中心化且不可变\n• https://... → 中心化但简单\n", }, visual: "🔗", }, { - title: { es: "Digest: verificación de integridad", en: "Digest: Integrity Verification", jp: "Digest:整合性検証", ko: "Digest: 무결성 검증" }, + title: { es: "Digest: verificación de integridad", en: "Digest: Integrity Verification", jp: "Digest:整合性検証", ko: "Digest: 무결성 검증", zh: "Digest:完整性验证" }, content: { es: "SHA-256 del contenido → grabado en el ledger\n\n• Cualquiera puede verificar\n• Detecta alteraciones\n• 64 caracteres hexadecimales\n\nSiempre establece el Digest para proteger tu NFT", en: "SHA-256 of the content → recorded on the ledger\n\n• Anyone can verify\n• Detects tampering\n• 64 hexadecimal characters\n\nAlways set the Digest to protect your NFT", jp: "コンテンツのSHA-256 → レジャーに記録\n\n• 誰でも検証可能\n• 改ざんを検出\n• 64文字の16進数\n\nNFTを守るため常にDigestを設定すること", ko: "콘텐츠의 SHA-256 → 레저에 기록\n\n• 누구나 검증 가능\n• 변조 감지\n• 64자의 16진수 문자\n\nNFT를 보호하기 위해 항상 Digest를 설정하세요", + zh: "内容的 SHA-256 → 记录在账本中\n\n• 任何人都可以验证\n• 能检测篡改\n• 64 个十六进制字符\n\n始终设置 Digest 来保护你的 NFT", }, visual: "🔏", }, { - title: { es: "Estándar de metadatos JSON", en: "JSON Metadata Standard", jp: "JSONメタデータ標準", ko: "JSON 메타데이터 표준" }, + title: { es: "Estándar de metadatos JSON", en: "JSON Metadata Standard", jp: "JSONメタデータ標準", ko: "JSON 메타데이터 표준", zh: "JSON 元数据标准" }, content: { es: "Estructura recomendada (similar a ERC-721):\n\n• name → Nombre del NFT\n• description → Descripción\n• image → Enlace a la imagen\n• attributes → Array de propiedades\n\nConsistencia = compatibilidad con exploradores", en: "Recommended structure (similar to ERC-721):\n\n• name → NFT name\n• description → Description\n• image → Link to image\n• attributes → Array of properties\n\nConsistency = compatibility with explorers", jp: "推奨構造(ERC-721と類似):\n\n• name → NFT名\n• description → 説明\n• image → 画像へのリンク\n• attributes → プロパティの配列\n\n一貫性 = エクスプローラーとの互換性", ko: "권장 구조 (ERC-721과 유사):\n\n• name → NFT 이름\n• description → 설명\n• image → 이미지 링크\n• attributes → 속성 배열\n\n일관성 = 탐색기와의 호환성", + zh: "推荐结构(类似 ERC-721):\n\n• name → NFT 名称\n• description → 描述\n• image → 图片链接\n• attributes → 属性数组\n\n保持一致 = 更好兼容浏览器与市场", }, visual: "📋", }, diff --git a/src/data/modules/m09-smart-contracts.js b/src/data/modules/m09-smart-contracts.js index 3053a47..f671c8c 100644 --- a/src/data/modules/m09-smart-contracts.js +++ b/src/data/modules/m09-smart-contracts.js @@ -6,6 +6,7 @@ export default { en: "Introduction to smart contracts in Non-EVM environments", jp: "Non-EVM環境におけるスマートコントラクト入門", ko: "비 EVM 환경의 스마트 컨트랙트 입문", + zh: "非 EVM 环境中的智能合约入门", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "What are Hooks?", jp: "Hooksとは何か?", ko: "Hooks란 무엇인가?", + zh: "什么是 Hooks?", }, theory: { es: `Los **Hooks** son el sistema de smart contracts nativo de Xahau. A diferencia de Solidity en Ethereum, los Hooks se escriben en **C** y se compilan a **WebAssembly (WASM)**. @@ -167,6 +169,30 @@ Hooksはトランザクションに反応する**フィルター**や**インタ ### Guard 모든 Hook은 무한 루프를 방지하기 위해 \`_g(id, maxiter)\` 를 포함해야 합니다.`, + zh: `**Hook** 是安装在 Xahau 账户上的轻量级智能合约。与 Ethereum 的 Solidity 不同,Hook 使用 **C 语言** 编写,并编译为 **WebAssembly(WASM)**。 + +### Hook 的特点 + +- 以账户为单位安装 +- 可以接受或拒绝交易 +- 可以读写持久状态 +- 在需要时还能自行发出新交易 + +### 核心差异 + +最大的区别在于**执行方式**: + +- 在 Ethereum 中,用户主动调用合约 +- 在 Xahau 中,交易经过安装了 Hook 的账户时,Hook 会自动响应执行 + +### 必要函数 + +- \`hook(uint32_t reserved)\`:必需 +- \`cbak(uint32_t reserved)\`:可选 + +### Guard + +所有 Hook 都必须包含 \`_g(id, maxiter)\`,用于防止无限循环。`, }, codeBlocks: [ { @@ -175,6 +201,7 @@ Hooksはトランザクションに反応する**フィルター**や**インタ en: "Minimal Hook. Accepts all transactions", jp: "最小限のHook。すべてのトランザクションを承認する", ko: "최소 Hook. 모든 트랜잭션 수락", + zh: "最小 Hook:接受所有交易", }, language: "c", code: { @@ -241,6 +268,22 @@ int64_t hook(uint32_t reserved) { // Guard: 여기에는 도달하지 않지만 필수 _g(1, 1); return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: accept_all.c + * 最简单的 Hook 示例。 + * 无条件接受所有交易。 + */ + +int64_t hook(uint32_t reserved) { + // 带消息接受交易 + accept(SBUF("accept_all: 交易已接受。"), __LINE__); + + // Guard:虽然这里不会执行到,但它是必需的 + _g(1, 1); + return 0; }`, }, }, @@ -250,6 +293,7 @@ int64_t hook(uint32_t reserved) { en: "Hook that rejects payments below a minimum", jp: "最低金額未満の支払いを拒否するHook", ko: "최소 금액 미만 결제를 거부하는 Hook", + zh: "拒绝低于最小金额付款的 Hook", }, language: "c", code: { @@ -424,6 +468,48 @@ int64_t hook(uint32_t reserved) { accept(SBUF("min_payment: 결제가 수락되었습니다."), __LINE__); + _g(1, 1); + return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: min_payment.c + * 拒绝低于 10 XAH 的 XAH 付款。 + * 其他交易全部接受。 + */ + +int64_t hook(uint32_t reserved) { + // 读取交易类型 + int64_t tt = otxn_type(); + + // 如果不是 Payment(类型 0),直接接受 + if (tt != 0) { + accept(SBUF("min_payment: 这不是付款交易。"), __LINE__); + } + + // 读取付款金额 + unsigned char amount_buf[48]; + int64_t amount_len = otxn_field(SBUF(amount_buf), sfAmount); + + // 如果不是原生 XAH(8 字节),直接接受 + if (amount_len != 8) { + accept(SBUF("min_payment: 不是原生 XAH。"), __LINE__); + } + + // 转换为 drops 并比较 + int64_t drops = AMOUNT_TO_DROPS(amount_buf); + int64_t min_drops = 10000000; // 10 XAH = 10,000,000 drops + + if (drops < min_drops) { + rollback( + SBUF("min_payment: 付款被拒绝,最低为 10 XAH。"), + __LINE__ + ); + } + + accept(SBUF("min_payment: 付款已接受。"), __LINE__); + _g(1, 1); return 0; }`, @@ -432,32 +518,35 @@ int64_t hook(uint32_t reserved) { ], slides: [ { - title: { es: "Hooks vs Smart Contracts EVM", en: "Hooks vs EVM Smart Contracts", jp: "Hooks vs EVMスマートコントラクト", ko: "Hooks vs EVM 스마트 컨트랙트" }, + title: { es: "Hooks vs Smart Contracts EVM", en: "Hooks vs EVM Smart Contracts", jp: "Hooks vs EVMスマートコントラクト", ko: "Hooks vs EVM 스마트 컨트랙트", zh: "Hooks vs EVM 智能合约" }, content: { es: "Smart contracts nativos de Xahau\n\n• Escritos en C, compilados a WebAssembly\n• Modelo reactivo (no se invocan, reaccionan)\n• Fees fijos y bajos (no gas variable)\n• Estado aislado con namespaces\n• Despliegue con transacción SetHook", en: "Xahau native smart contracts\n\n• Written in C, compiled to WebAssembly\n• Reactive model (not invoked, they react)\n• Fixed low fees (no variable gas)\n• Isolated state with namespaces\n• Deployment with SetHook transaction", jp: "Xahauのネイティブスマートコントラクト\n\n• C言語で記述、WebAssemblyにコンパイル\n• リアクティブモデル(呼び出しではなく反応)\n• 固定の低手数料(可変ガスなし)\n• 名前空間による分離されたステート\n• SetHookトランザクションでデプロイ", ko: "Xahau의 네이티브 스마트 컨트랙트\n\n• C로 작성하고 WebAssembly로 컴파일\n• 호출형이 아닌 반응형 실행 모델\n• 가변 가스 대신 고정되고 낮은 수수료\n• namespace로 분리된 상태\n• SetHook 트랜잭션으로 배포", + zh: "Xahau 的原生智能合约\n\n• 用 C 编写并编译为 WebAssembly\n• 采用响应式执行模型,而不是主动调用\n• 费用固定且较低,不使用可变 Gas\n• 使用 namespace 隔离状态\n• 通过 SetHook 交易部署", }, visual: "🪝", }, { - title: { es: "Modelo reactivo y funciones", en: "Reactive model and functions", jp: "リアクティブモデルと関数", ko: "반응형 모델과 함수" }, + title: { es: "Modelo reactivo y funciones", en: "Reactive model and functions", jp: "リアクティブモデルと関数", ko: "반응형 모델과 함수", zh: "响应式模型与函数" }, content: { es: "EVM: Tú llamas al contrato\nHooks: Se ejecutan automáticamente\n\n• accept() → Aceptar transacción\n• rollback() → Rechazar transacción\n• emit() → Emitir nueva transacción\n• state() / state_set() → Estado persistente\n\nhook() obligatoria | cbak() opcional | _g() guard", en: "EVM: You call the contract\nHooks: Execute automatically\n\n• accept() → Accept transaction\n• rollback() → Reject transaction\n• emit() → Emit new transaction\n• state() / state_set() → Persistent state\n\nhook() mandatory | cbak() optional | _g() guard", jp: "EVM:あなたがコントラクトを呼び出す\nHooks:自動的に実行される\n\n• accept() → トランザクションを承認\n• rollback() → トランザクションを拒否\n• emit() → 新しいトランザクションを発行\n• state() / state_set() → 永続的なステート\n\nhook() 必須 | cbak() 任意 | _g() ガード", ko: "EVM: 사용자가 컨트랙트를 호출\nHooks: 트랜잭션에 반응해 자동 실행\n\n• accept() → 트랜잭션 수락\n• rollback() → 트랜잭션 거부\n• emit() → 새 트랜잭션 발행\n• state() / state_set() → 영속 상태\n\nhook() 필수 | cbak() 선택 | _g() guard", + zh: "EVM:由用户调用合约\nHooks:对交易自动作出响应\n\n• accept() → 接受交易\n• rollback() → 拒绝交易\n• emit() → 发出新交易\n• state() / state_set() → 持久状态\n\nhook() 必需 | cbak() 可选 | _g() 为 guard", }, visual: "⚡", }, { - title: { es: "Datos clave sobre Hooks", en: "Key facts about Hooks", jp: "Hooksの主要な事実", ko: "Hooks 핵심 정보" }, + title: { es: "Datos clave sobre Hooks", en: "Key facts about Hooks", jp: "Hooksの主要な事実", ko: "Hooks 핵심 정보", zh: "Hooks 关键事实" }, content: { es: "• Hasta 10 Hooks por cuenta\n• Cada Hook tiene su propio namespace\n• Puede acceder a namespaces ajenos con permisos\n• WASM deduplicado: mismo codigo = mismo hash\n• Instalar por HookHash sin acceso al codigo fuente", en: "• Up to 10 Hooks per account\n• Each Hook has its own namespace\n• Can access other namespaces with permissions\n• WASM deduplicated: same code = same hash\n• Install by HookHash without source code access", jp: "• アカウントあたり最大10個のHooks\n• 各Hookには独自の名前空間がある\n• 権限があれば他の名前空間にもアクセス可能\n• WASMの重複排除:同じコード = 同じハッシュ\n• ソースコードなしでHookHashによりインストール可能", ko: "• 계정당 최대 10개의 Hook\n• 각 Hook은 자체 namespace를 가짐\n• 권한이 있으면 다른 namespace도 접근 가능\n• 같은 WASM은 같은 해시로 중복 제거됨\n• 소스코드 없이 HookHash로 설치 가능", + zh: "• 每个账户最多可安装 10 个 Hook\n• 每个 Hook 都有自己的 namespace\n• 有权限时也可以访问其他 namespace\n• 相同 WASM 会被去重,对应相同哈希\n• 可通过 HookHash 安装,无需源码", }, visual: "📐", }, @@ -470,6 +559,7 @@ int64_t hook(uint32_t reserved) { en: "Deploying a Hook on Xahau", jp: "XahauへのHookのデプロイ", ko: "Xahau에 Hook 배포하기", + zh: "在 Xahau 上部署 Hook", }, theory: { es: `Una vez que tienes tu Hook escrito en C, necesitas **compilarlo a WebAssembly** y **desplegarlo** en tu cuenta de Xahau mediante una transacción \`SetHook\`. @@ -943,6 +1033,33 @@ Hook: { - 빈 \`CreateCode\` 로 삭제 설정을 잘못하면 Hook이 전혀 실행되지 않거나 원하지 않는 트랜잭션에서 작동할 수 있으므로, 배포 전 필드 확인이 매우 중요합니다.`, + zh: `写好 Hook 之后,需要先将其**编译为 WebAssembly**,再通过 \`SetHook\` 交易**部署**到账户上。 + +### 一般流程 + +1. 编写 Hook 代码 +2. 编译为 WASM +3. 配置 \`HookOn\`、\`HookNamespace\`、\`HookParameters\` 等字段 +4. 提交 \`SetHook\` +5. 通过账户对象或区块浏览器确认是否安装成功 + +### 重要字段 + +- \`CreateCode\`:首次部署时使用的 WASM 二进制 +- \`HookHash\`:复用已经存在的 Hook +- \`HookOn\`:决定哪些交易会触发 Hook +- \`HookCanEmit\`:决定它可以发出哪些交易 +- \`HookNamespace\`:用于隔离状态的命名空间 +- \`Flags\`:覆盖、删除等控制选项 + +### 管理操作 + +- 安装新的 Hook +- 通过已有 HookHash 安装 +- 更新参数或 namespace +- 用空的 \`CreateCode\` 删除 Hook + +如果字段配置错误,Hook 可能完全不会执行,或者会在你不希望的交易上触发,因此部署前务必仔细检查。`, }, codeBlocks: [ @@ -952,6 +1069,7 @@ Hook: { en: "Deploy a Hook from a .wasm file with xahau.js", jp: "xahau.jsで.wasmファイルからHookをデプロイする", ko: "xahau.js로 .wasm 파일에서 Hook 배포하기", + zh: "使用 xahau.js 从 .wasm 文件部署 Hook", }, language: "javascript", code: { @@ -1142,6 +1260,53 @@ async function deployHook() { await client.disconnect(); } +deployHook();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); +const fs = require("fs"); + +async function deployHook() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // Testnet 账户 + const account = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 读取编译后的 Hook WASM 文件 + const wasmBytes = fs.readFileSync("base.wasm"); // 使用你要部署的 .wasm 文件名 + const hookBinary = wasmBytes.toString("hex").toUpperCase(); + + // 构建 SetHook 交易 + const setHook = { + TransactionType: "SetHook", + Account: account.address, + Hooks: [ + { + Hook: { + CreateCode: hookBinary, + HookOn: "0".repeat(64), + HookCanEmit: "0".repeat(64), + HookNamespace: "0".repeat(64), + HookApiVersion: 0, + Flags: 1, + }, + }, + ], + }; + + const prepared = await client.autofill(setHook); + const signed = account.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("Hook 已成功部署到账户上!", account.address); + } + + await client.disconnect(); +} + deployHook();`, }, },{ @@ -1150,6 +1315,7 @@ deployHook();`, en: "Delete a Hook from an account with xahau.js", jp: "xahau.jsでアカウントからHookを削除する", ko: "xahau.js로 계정에서 Hook 삭제하기", + zh: "使用 xahau.js 从账户删除 Hook", }, language: "javascript", code: { @@ -1308,6 +1474,43 @@ async function removeHook() { await client.disconnect(); } +removeHook();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); +const fs = require("fs"); + +async function removeHook() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const account = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + const setHook = { + TransactionType: "SetHook", + Account: account.address, + Hooks: [ + { + Hook: { + CreateCode: "", + Flags: 1, + }, + }, + ], + }; + + const prepared = await client.autofill(setHook); + const signed = account.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("Hook 已成功从账户中删除!", account.address); + } + + await client.disconnect(); +} + removeHook();`, }, }, @@ -1317,6 +1520,7 @@ removeHook();`, en: "Install a Hook by HookHash with xahau.js", jp: "xahau.jsでHookHashによりHookをインストールする", ko: "xahau.js로 HookHash를 이용해 Hook 설치하기", + zh: "使用 xahau.js 通过 HookHash 安装 Hook", }, language: "javascript", code: { @@ -1487,6 +1691,46 @@ async function deployHook() { await client.disconnect(); } +deployHook();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); +const fs = require("fs"); + +async function deployHook() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const account = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + const setHook = { + TransactionType: "SetHook", + Account: account.address, + Hooks: [ + { + Hook: { + HookHash: "66A4FC969ADB5998FD371B7B011F1BC3E506D2171F4729B52E57A6A8BC093227", + HookOn: "0".repeat(64), + HookCanEmit: "0".repeat(64), + HookNamespace: "0".repeat(64), + Flags: 1, + }, + }, + ], + }; + + const prepared = await client.autofill(setHook); + const signed = account.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + console.log("Hook 已成功安装到账户上!", account.address); + } + + await client.disconnect(); +} + deployHook();`, }, }, @@ -1496,6 +1740,7 @@ deployHook();`, en: "Check installed Hooks on an account", jp: "アカウントにインストールされたHooksを確認する", ko: "계정에 설치된 Hook 확인하기", + zh: "检查账户上已安装的 Hook", }, language: "javascript", code: { @@ -1654,38 +1899,80 @@ async function checkHooks(address) { await client.disconnect(); } // Testnet 예시 주소: rHdPUUeSDTcjacxR572aEe7zR9re4mvXJN +checkHooks("rTuDireccionAqui");`, + zh: `const { Client } = require("xahau"); + +async function checkHooks(address) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const response = await client.request({ + command: "account_objects", + account: address, + type: "hook", + ledger_index: "validated", + }); + + const hooks = response.result.account_objects; + console.log(\`=== \${address} 的 Hooks ===\`); + console.log(\`已安装总数: \${hooks.length}\n\`); + + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + + console.log(\`Hook #\${i + 1}:\`); + //console.log(JSON.stringify(hook, null, 2)); // 如需查看完整信息可取消注释 + + if (hook.Hooks && hook.Hooks.length > 0) { + const installedHook = hook.Hooks[0].Hook; + + console.log(\` HookHash: \${installedHook.HookHash}\`); + console.log(\` HookOn: \${installedHook.HookOn}\`); + console.log(\` Namespace: \${installedHook.HookNamespace}\`); + console.log(\` HookCanEmit: \${installedHook.HookCanEmit}\`); + } + + console.log(); + } + + await client.disconnect(); +} +// Testnet 示例地址: rHdPUUeSDTcjacxR572aEe7zR9re4mvXJN checkHooks("rTuDireccionAqui");`, }, }, ], slides: [ { - title: { es: "SetHook: campos principales", en: "SetHook: main fields", jp: "SetHook:主要フィールド", ko: "SetHook: 주요 필드" }, + title: { es: "SetHook: campos principales", en: "SetHook: main fields", jp: "SetHook:主要フィールド", ko: "SetHook: 주요 필드", zh: "SetHook:主要字段" }, content: { es: "Transaccion unica para gestionar Hooks\n\n• CreateCode: WASM en hex\n• HookHash: instalar Hook existente por hash\n• HookOn: filtro de transacciones\n• HookNamespace: aislamiento de estado\n• HookParameters: configuracion sin recompilar\n• HookCanEmit: control de emisiones (seguridad)\n• Flags: hsfOverride | hsfNSDelete | hsfCollect", en: "Single transaction to manage Hooks\n\n• CreateCode: WASM in hex\n• HookHash: install existing Hook by hash\n• HookOn: transaction filter\n• HookNamespace: state isolation\n• HookParameters: configuration without recompiling\n• HookCanEmit: emission control (security)\n• Flags: hsfOverride | hsfNSDelete | hsfCollect", jp: "Hooksを管理する単一トランザクション\n\n• CreateCode: WASMをhex形式で\n• HookHash: 既存HookをHashでインストール\n• HookOn: トランザクションフィルター\n• HookNamespace: ステートの分離\n• HookParameters: 再コンパイルなしで設定\n• HookCanEmit: 発行制御(セキュリティ)\n• Flags: hsfOverride | hsfNSDelete | hsfCollect", ko: "Hook을 관리하는 단일 트랜잭션\n\n• CreateCode: hex 형식의 WASM\n• HookHash: 기존 Hook 해시로 설치\n• HookOn: 트랜잭션 필터\n• HookNamespace: 상태 분리\n• HookParameters: 재컴파일 없는 설정 변경\n• HookCanEmit: 발행 제어\n• Flags: hsfOverride | hsfNSDelete | hsfCollect", + zh: "管理 Hook 的单一交易\n\n• CreateCode:十六进制格式的 WASM\n• HookHash:通过已有 Hook 哈希安装\n• HookOn:交易过滤器\n• HookNamespace:状态隔离\n• HookParameters:无需重新编译即可修改配置\n• HookCanEmit:发交易控制\n• Flags:hsfOverride | hsfNSDelete | hsfCollect", }, visual: "⚙️", }, { - title: { es: "4 fases de gestion de un Hook", en: "4 Hook management phases", jp: "Hookの管理4フェーズ", ko: "Hook 관리의 4단계" }, + title: { es: "4 fases de gestion de un Hook", en: "4 Hook management phases", jp: "Hookの管理4フェーズ", ko: "Hook 관리의 4단계", zh: "Hook 管理的 4 个阶段" }, content: { es: "1. Instalar (CreateCode) → WASM completo\n2. Instalar por HookHash → sin enviar WASM\n3. Actualizar (Update) → modificar namespace,\n parametros o grants sin cambiar codigo\n4. Eliminar (Delete) → CreateCode vacio\n + hsfOverride. hsfNSDelete limpia estado", en: "1. Install (CreateCode) → full WASM\n2. Install by HookHash → without sending WASM\n3. Update → modify namespace,\n parameters or grants without changing code\n4. Delete → empty CreateCode\n + hsfOverride. hsfNSDelete clears state", jp: "1. インストール(CreateCode)→ 完全なWASM\n2. HookHashでインストール → WASMを送信せずに\n3. 更新(Update)→ 名前空間、\n パラメーターやGrantsをコード変更なしで修正\n4. 削除(Delete)→ 空のCreateCode\n + hsfOverride。hsfNSDeleteでステートをクリア", ko: "1. 설치(CreateCode) → 전체 WASM 사용\n2. HookHash로 설치 → WASM 재전송 없음\n3. 업데이트(Update) → 코드 변경 없이 namespace,\n 파라미터, grants 수정\n4. 삭제(Delete) → 빈 CreateCode\n + hsfOverride, 필요 시 hsfNSDelete로 상태 정리", + zh: "1. 安装(CreateCode)→ 使用完整 WASM\n2. 通过 HookHash 安装 → 无需重新发送 WASM\n3. 更新(Update)→ 不改代码,只改 namespace、参数或 grants\n4. 删除(Delete)→ 使用空的 CreateCode\n + hsfOverride,必要时用 hsfNSDelete 清理状态", }, visual: "🔄", }, { - title: { es: "HookOn y HookCanEmit", en: "HookOn and HookCanEmit", jp: "HookOnとHookCanEmit", ko: "HookOn과 HookCanEmit" }, + title: { es: "HookOn y HookCanEmit", en: "HookOn and HookCanEmit", jp: "HookOnとHookCanEmit", ko: "HookOn과 HookCanEmit", zh: "HookOn 与 HookCanEmit" }, content: { es: "HookOn: que transacciones activan el Hook\nHookCanEmit: que transacciones puede emitir\n\n• Ambos usan la misma calculadora\n• Resultado hex sin 0x, en mayusculas\n• Principio de minimo privilegio\n• HookCanEmit opcional pero recomendado", en: "HookOn: which transactions activate the Hook\nHookCanEmit: which transactions it can emit\n\n• Both use the same calculator\n• Hex result without 0x, uppercase\n• Principle of least privilege\n• HookCanEmit optional but recommended", jp: "HookOn: どのトランザクションがHookを起動するか\nHookCanEmit: どのトランザクションを発行できるか\n\n• 両方とも同じ計算機を使用\n• 0xなしhex結果、大文字\n• 最小権限の原則\n• HookCanEmitはオプションだが推奨", ko: "HookOn: 어떤 트랜잭션이 Hook을 실행하는가\nHookCanEmit: 어떤 트랜잭션을 발행할 수 있는가\n\n• 둘 다 같은 계산기 사용\n• 0x 없는 대문자 hex 사용\n• 최소 권한 원칙 적용\n• HookCanEmit은 선택이지만 권장", + zh: "HookOn:哪些交易会触发 Hook\nHookCanEmit:它可以发出哪些交易\n\n• 两者都使用同一个计算器\n• 使用不带 0x 的大写十六进制\n• 应遵循最小权限原则\n• HookCanEmit 可选,但强烈推荐", }, visual: "🎯", }, @@ -1698,6 +1985,7 @@ checkHooks("rTuDireccionAqui");`, en: "Persistent state in Hooks", jp: "Hooksの永続的なステート", ko: "Hooks의 영속 상태", + zh: "Hooks 中的持久状态", }, theory: { es: `Los Hooks pueden almacenar **datos persistentes** entre ejecuciones usando el sistema de estado (\`state\`). Esto permite que un Hook tenga información disponible con la que trabajar en uno o varios \`Namespace\`. @@ -1817,6 +2105,30 @@ Namespaceは16進数の32バイト(256ビット)で識別されます。ス - 내부 잔액이나 누적값 관리 저장 공간과 준비금 비용이 있으므로 상태 구조는 처음부터 간결하게 설계하는 편이 좋습니다.`, + zh: `Hook 可以通过 \`state\` 系统在多次执行之间保存**持久数据**。这让你不仅能写简单的响应式逻辑,还能构建真正依赖状态的逻辑。 + +### 状态结构 + +- 键:32 字节 +- 值:每项最多 256 字节 +- 所有值都存储在特定的 **namespace** 中 + +### 常用函数 + +- \`state()\`:读取值 +- \`state_set()\`:写入值 +- \`state_foreign()\`:读取其他 namespace +- \`state_foreign_set()\`:写入其他 namespace + +### 典型用途 + +- 维护计数器 +- 白名单 / 黑名单 +- 保存动态配置 +- 记录最后一次处理结果 +- 管理内部余额或累计值 + +由于存储空间和储备金都有成本,状态结构最好从一开始就设计得尽量精简。`, }, codeBlocks: [ { @@ -1825,6 +2137,7 @@ Namespaceは16進数の32バイト(256ビット)で識別されます。ス en: "Hook that counts processed payments", jp: "処理された支払いをカウントするHook", ko: "처리한 결제를 세는 Hook", + zh: "统计已处理付款数量的 Hook", }, language: "c", code: { @@ -2006,38 +2319,82 @@ int64_t hook(uint32_t reserved) { accept(SBUF("payment_counter: 결제가 카운트되었습니다."), __LINE__); return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: payment_counter.c + * 统计账户处理过多少笔付款。 + * 计数器保存在 Hook 的状态中。 + */ + +int64_t hook(uint32_t reserved) { + _g(1, 1); + + // 只统计付款(type 0) + int64_t tt = otxn_type(); + if (tt != 0) { + accept(SBUF("payment_counter: 这不是付款。"), __LINE__); + } + + // 计数器状态键 + uint8_t state_key[32] = { 0 }; + state_key[0] = 'C'; + + int64_t counter = 0; + uint8_t counter_buf[8] = { 0 }; + int64_t bytes_read = state(SBUF(counter_buf), SBUF(state_key)); + + if (bytes_read == 8) { + counter = *((int64_t*)counter_buf); + } + + counter++; + + *((int64_t*)counter_buf) = counter; + int64_t result = state_set(SBUF(counter_buf), SBUF(state_key)); + + if (result < 0) { + rollback(SBUF("payment_counter: 保存状态时出错。"), __LINE__); + } + + accept(SBUF("payment_counter: 付款已计数。"), __LINE__); + return 0; }`, }, }, ], slides: [ { - title: { es: "Sistema de estado en Hooks", en: "Hook state system", jp: "Hooksのステートシステム", ko: "Hook 상태 시스템" }, + title: { es: "Sistema de estado en Hooks", en: "Hook state system", jp: "Hooksのステートシステム", ko: "Hook 상태 시스템", zh: "Hook 状态系统" }, content: { es: "Datos persistentes entre ejecuciones\n\n• state() → Leer valor por clave\n• state_set() → Escribir valor\n• state_foreign() → Leer estado de otra cuenta\n\nClave: 32 bytes | Valor: hasta 256 bytes\nCada entrada vive dentro de un namespace", en: "Persistent data between executions\n\n• state() → Read value by key\n• state_set() → Write value\n• state_foreign() → Read state of another account\n\nKey: 32 bytes | Value: up to 256 bytes\nEach entry lives within a namespace", jp: "実行間の永続的なデータ\n\n• state() → キーで値を読み取る\n• state_set() → 値を書き込む\n• state_foreign() → 別のアカウントのステートを読む\n\nキー:32バイト | 値:最大256バイト\n各エントリは名前空間内に存在する", ko: "실행 사이에 유지되는 영속 데이터\n\n• state() → 키로 값 읽기\n• state_set() → 값 쓰기\n• state_foreign() → 다른 계정 상태 읽기\n\n키: 32바이트 | 값: 최대 256바이트\n각 항목은 namespace 안에 저장됨", + zh: "在多次执行之间保留的持久数据\n\n• state() → 通过键读取值\n• state_set() → 写入值\n• state_foreign() → 读取其他账户的状态\n\n键:32 字节 | 值:最多 256 字节\n每一项都位于某个 namespace 内", }, visual: "💾", }, { - title: { es: "Namespace y aislamiento", en: "Namespace and isolation", jp: "名前空間と分離", ko: "Namespace와 분리" }, + title: { es: "Namespace y aislamiento", en: "Namespace and isolation", jp: "名前空間と分離", ko: "Namespace와 분리", zh: "Namespace 与隔离" }, content: { es: "HookNamespace (32 bytes hex):\n\n• Aisla el estado de cada Hook\n• Distinto namespace = estado separado\n• Mismo namespace = estado compartido\n• Se define al instalar con SetHook\n\nstate_foreign() lee estado ajeno (solo lectura)", en: "HookNamespace (32 bytes hex):\n\n• Isolates state of each Hook\n• Different namespace = separate state\n• Same namespace = shared state\n• Defined at install time with SetHook\n\nstate_foreign() reads external state (read-only)", jp: "HookNamespace(32バイトhex):\n\n• 各Hookのステートを分離する\n• 異なる名前空間 = 別のステート\n• 同じ名前空間 = 共有ステート\n• SetHookでインストール時に定義\n\nstate_foreign()は外部ステートを読む(読み取り専用)", ko: "HookNamespace(32바이트 hex):\n\n• 각 Hook의 상태를 분리\n• 다른 namespace = 별도 상태\n• 같은 namespace = 공유 상태\n• SetHook 설치 시 정의\n\nstate_foreign()은 외부 상태를 읽는 용도", + zh: "HookNamespace(32 字节 hex):\n\n• 用于隔离每个 Hook 的状态\n• 不同 namespace = 独立状态\n• 相同 namespace = 共享状态\n• 在 SetHook 安装时定义\n\nstate_foreign() 用于读取外部状态", }, visual: "🔒", }, { - title: { es: "Usos practicos del estado", en: "Practical uses of state", jp: "ステートの実用的な用途", ko: "상태의 실전 활용" }, + title: { es: "Usos practicos del estado", en: "Practical uses of state", jp: "ステートの実用的な用途", ko: "상태의 실전 활용", zh: "状态的实际用途" }, content: { es: "• Contadores de transacciones\n• Listas blancas / negras de direcciones\n• Configuracion dinamica del Hook\n• Tracking: ultima tx, timestamps\n• Acumuladores y balances internos", en: "• Transaction counters\n• Address whitelists / blacklists\n• Dynamic Hook configuration\n• Tracking: last tx, timestamps\n• Accumulators and internal balances", jp: "• トランザクションカウンター\n• アドレスのホワイトリスト/ブラックリスト\n• Hookの動的設定\n• トラッキング:最後のtx、タイムスタンプ\n• アキュムレーターと内部残高", ko: "• 트랜잭션 카운터\n• 주소 화이트리스트 / 블랙리스트\n• 동적 Hook 설정\n• 마지막 tx, timestamp 추적\n• 누적값과 내부 잔액 관리", + zh: "• 交易计数器\n• 地址白名单 / 黑名单\n• 动态 Hook 配置\n• 追踪最后一笔 tx、时间戳等\n• 累积值与内部余额管理", }, visual: "📋", }, @@ -2050,6 +2407,7 @@ int64_t hook(uint32_t reserved) { en: "Emitting transactions from a Hook", jp: "HookからのトランザクションのEmit", ko: "Hook에서 트랜잭션 발행하기", + zh: "从 Hook 中发出交易", }, theory: { es: `Una de las capacidades más poderosas de los Hooks es la posibilidad de **emitir transacciones nuevas** de forma autónoma. Cuando un Hook emite una transacción, esta se ejecuta como si la cuenta del Hook la hubiera enviado. @@ -2232,6 +2590,27 @@ EmitされたトランザクションがEmitしたHookの**完了**(成功ま - 상태 기반 후속 작업 실행 강력한 기능이지만 수수료와 복잡도가 함께 올라가므로, 발행 권한과 실행 흐름을 신중히 설계해야 합니다.`, + zh: `Hook 最强大的能力之一,是可以**自行发出新的交易**。这些交易会被当作安装了 Hook 的账户亲自发送的一样处理。 + +### emit() 的流程 + +1. 用 \`etxn_reserve(N)\` 预留发交易数量 +2. 构造序列化交易缓冲区 +3. 用 \`etxn_details()\` 准备发出细节 +4. 用 \`emit()\` 提交到账本 + +### cbak() 的作用 + +当发出的交易完成后,\`cbak()\` 会被调用,用来检查结果并更新状态。 + +### 常见用途 + +- 自动转发收到付款的一部分 +- 拆分转账到多个账户 +- 对不符合条件的付款自动退款 +- 基于状态触发后续动作 + +这个功能非常强大,但也会带来更高的费用和复杂度,因此必须谨慎设计发交易权限与执行流程。`, }, codeBlocks: [ { @@ -2240,6 +2619,7 @@ EmitされたトランザクションがEmitしたHookの**完了**(成功ま en: "Hook that forwards 10% of each received payment", jp: "受信した各支払いの10%を転送するHook", ko: "받은 결제의 10%를 전달하는 Hook", + zh: "将收到付款的 10% 自动转发的 Hook", }, language: "c", code: { @@ -2561,6 +2941,69 @@ int64_t hook(uint32_t reserved) accept(SBUF("forwarder: 10%가 정상적으로 전달되었습니다"), __LINE__); return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: ten_percent_forwarder.c + * + * 当账户收到 XAH 付款时,会自动将 10% + * 转发到 forward_to[] 中指定的地址。 + */ + +int64_t hook(uint32_t reserved) +{ + // 没有循环,且只发出一笔交易,所以 guard 为 1 次 + _g(1, 1); + etxn_reserve(1); + + // 10% 转发目标地址 + uint8_t forward_to[20] = { + 0x4BU, 0x50U, 0x69U, 0x9EU, 0x25U, 0x3CU, 0x50U, 0x98U, 0xDEU, 0xFEU, 0x3AU, 0x08U, 0x72U, 0xA7U, 0x9DU, 0x12U, 0x91U, 0x72U, 0xF4U, 0x96U + }; + + int64_t tt = otxn_type(); + if (tt != 0) + accept(SBUF("forwarder: 这不是付款"), __LINE__); + + uint8_t account_field[20]; + int32_t account_field_len = otxn_field(SBUF(account_field), sfDestination); + if (account_field_len != 20) + accept(SBUF("forwarder: 无法读取目标地址"), __LINE__); + + unsigned char hook_accid[20]; + hook_account(SBUF(hook_accid)); + + int equal = 0; + BUFFER_EQUAL(equal, hook_accid, account_field, 20); + if (!equal) + accept(SBUF("forwarder: 这是转出付款,忽略"), __LINE__); + + unsigned char amount_buffer[48]; + int64_t amount_len = otxn_field(SBUF(amount_buffer), sfAmount); + if (amount_len != 8) + accept(SBUF("forwarder: 不是原生 XAH"), __LINE__); + + int64_t otxn_drops = AMOUNT_TO_DROPS(amount_buffer); + TRACEVAR(otxn_drops); + + int64_t drops_to_forward = otxn_drops / 10; + TRACEVAR(drops_to_forward); + + if (drops_to_forward < 1) + accept(SBUF("forwarder: 金额太小"), __LINE__); + + unsigned char tx[PREPARE_PAYMENT_SIMPLE_SIZE]; + PREPARE_PAYMENT_SIMPLE(tx, drops_to_forward, forward_to, 0, 0); + + uint8_t emithash[32]; + int64_t emit_result = emit(SBUF(emithash), SBUF(tx)); + + if (emit_result < 0) + rollback(SBUF("forwarder: 发出付款时出错"), __LINE__); + + accept(SBUF("forwarder: 已成功转发 10%"), __LINE__); + return 0; }`, }, }, @@ -2568,32 +3011,35 @@ int64_t hook(uint32_t reserved) ], slides: [ { - title: { es: "emit() — Transacciones autonomas", en: "emit() — Autonomous transactions", jp: "emit() — 自律的なトランザクション", ko: "emit() — 자율 트랜잭션" }, + title: { es: "emit() — Transacciones autonomas", en: "emit() — Autonomous transactions", jp: "emit() — 自律的なトランザクション", ko: "emit() — 자율 트랜잭션", zh: "emit() — 自主交易" }, content: { es: "Los Hooks pueden crear transacciones nuevas\n\n• emit() envia transacciones al ledger\n• Se ejecutan como si la cuenta las enviara\n• Pagos, ofertas, cualquier tipo soportado\n• etxn_reserve(N) obligatorio antes de emitir", en: "Hooks can create new transactions\n\n• emit() sends transactions to the ledger\n• Execute as if the account sent them\n• Payments, offers, any supported type\n• etxn_reserve(N) mandatory before emitting", jp: "Hooksは新しいトランザクションを作成できる\n\n• emit() はレジャーにトランザクションを送信する\n• アカウントが送信したかのように実行される\n• 支払い、オファー、サポートされているあらゆるタイプ\n• etxn_reserve(N) はEmit前に必須", ko: "Hook은 새 트랜잭션을 만들 수 있음\n\n• emit() 으로 레저에 전송\n• 계정이 직접 보낸 것처럼 실행\n• 결제, 오퍼 등 지원되는 모든 타입 가능\n• emit 전에 etxn_reserve(N) 필수", + zh: "Hook 可以创建新的交易\n\n• 使用 emit() 发送到账本\n• 执行方式如同账户亲自发送\n• 可用于付款、挂单等任意支持的类型\n• emit 前必须先调用 etxn_reserve(N)", }, visual: "📤", }, { - title: { es: "Flujo de emision", en: "Emission flow", jp: "Emitのフロー", ko: "Emit 흐름" }, + title: { es: "Flujo de emision", en: "Emission flow", jp: "Emitのフロー", ko: "Emit 흐름", zh: "发出流程" }, content: { es: "1. etxn_reserve(N) → Reservar espacio\n2. Construir tx serializada en buffer\n3. etxn_details() → Preparar detalles\n4. emit() → Enviar al ledger\n\ncbak() se ejecuta cuando la emision\ncompleta (exito o fallo)", en: "1. etxn_reserve(N) → Reserve space\n2. Build serialized tx in buffer\n3. etxn_details() → Prepare details\n4. emit() → Send to ledger\n\ncbak() executes when emission\ncompletes (success or failure)", jp: "1. etxn_reserve(N) → スペースを確保\n2. バッファにシリアライズされたtxを構築\n3. etxn_details() → 詳細を準備\n4. emit() → レジャーに送信\n\ncbak() はEmitが完了したときに実行\n(成功または失敗)", ko: "1. etxn_reserve(N) → 공간 예약\n2. 버퍼에 직렬화된 tx 구성\n3. etxn_details() → 세부정보 준비\n4. emit() → 레저에 전송\n\ncbak() 은 발행 완료 후\n성공/실패 결과를 받음", + zh: "1. etxn_reserve(N) → 预留空间\n2. 在缓冲区中构建序列化交易\n3. etxn_details() → 准备细节\n4. emit() → 发送到账本\n\ncbak() 会在发出完成后收到\n成功或失败结果", }, visual: "📝", }, { - title: { es: "Casos de uso y limitaciones", en: "Use cases and limitations", jp: "ユースケースと制限事項", ko: "활용 사례와 제한사항" }, + title: { es: "Casos de uso y limitaciones", en: "Use cases and limitations", jp: "ユースケースと制限事項", ko: "활용 사례와 제한사항", zh: "使用场景与限制" }, content: { es: "Casos de uso:\n• Auto-forwarding de pagos\n• Splitting entre varias cuentas\n• Refunds automaticos\n• Acciones programadas\n\nLimitaciones:\n• Maximo de emisiones por ejecucion\n• Fees propios por emision\n• _g() previene emisiones infinitas", en: "Use cases:\n• Auto-forwarding of payments\n• Splitting between multiple accounts\n• Automatic refunds\n• Scheduled actions\n\nLimitations:\n• Maximum emissions per execution\n• Own fees per emission\n• _g() prevents infinite emissions", jp: "ユースケース:\n• 支払いの自動転送\n• 複数アカウント間の分割\n• 自動返金\n• スケジュールされたアクション\n\n制限事項:\n• 実行ごとの最大Emit回数\n• Emit固有の手数料\n• _g() は無限Emitを防ぐ", ko: "활용 예시:\n• 결제 자동 전달\n• 여러 계정으로 분할 송금\n• 자동 환불\n• 예약된 후속 작업\n\n제한사항:\n• 실행당 발행 횟수 제한\n• 발행 자체 수수료 발생\n• _g() 가 무한 발행 방지", + zh: "使用场景:\n• 自动转发付款\n• 拆分到多个账户\n• 自动退款\n• 计划好的后续动作\n\n限制:\n• 每次执行可发出的交易数有限\n• 发出的交易本身也会产生费用\n• _g() 可防止无限发出", }, visual: "🔀", }, @@ -2606,6 +3052,7 @@ int64_t hook(uint32_t reserved) en: "Parameters, functions and Hook management", jp: "Hooksのパラメーター、関数と管理", ko: "파라미터, 함수, Hook 관리", + zh: "参数、函数与 Hook 管理", }, theory: { es: `Los Hooks disponen de múltiples funciones con propósitos distintos y de gestión. En esta lección veremos algunos de ellos. @@ -2810,6 +3257,25 @@ Hooks開発の最初のステップで、パラメーターを読みやすい値 - 업데이트와 롤백 전략을 미리 고려 유틸리티 도구를 함께 활용하면 HookOn 계산, hex 변환, 시간 변환, 트랜잭션 생성이 훨씬 쉬워집니다.`, + zh: `在实际 Hook 开发中,除了代码本身,**参数与管理方式**也非常重要。即使是同一个 Hook,也可能因为配置不同而表现完全不同。 + +### \`hook_param()\` 与 \`otxn_param()\` + +- \`hook_param()\`:安装时通过 \`SetHook\` 写入的静态配置 +- \`otxn_param()\`:触发 Hook 的交易附带的动态值 + +### 什么时候使用 + +- 固定阈值、固定地址、模式配置适合用 \`hook_param()\` +- 每次执行都变化的命令、引用值、动作代码适合用 \`otxn_param()\` + +### 运营建议 + +- 优先参数化,而不是硬编码 +- 文档化 namespace 与状态键规则 +- 提前考虑更新与回滚策略 + +如果结合使用实用工具,HookOn 计算、hex 转换、时间转换和交易构建都会轻松很多。`, }, codeBlocks: [ { @@ -2818,6 +3284,7 @@ Hooks開発の最初のステップで、パラメーターを読みやすい値 en: "Hook that reads an otxn_param and displays it with TRACE", jp: "otxn_paramを読み取りTRACEで表示するHook", ko: "otxn_param을 읽어 TRACE로 보여주는 Hook", + zh: "读取 otxn_param 并用 TRACE 显示的 Hook", }, language: "c", code: { @@ -3064,6 +3531,40 @@ int64_t hook(uint32_t reserved) accept(SBUF("otxn_param_demo: 파라미터를 읽고 추적했습니다"), __LINE__); return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: otxn_param_demo.c + * + * 读取触发 Hook 的交易中的 "ACTION" 参数, + * 并通过 trace() 输出到 Debug Stream。 + */ + +int64_t hook(uint32_t reserved) +{ + _g(1, 1); + trace(SBUF("otxn_param_demo: hook() 已启动"), 0, 0, 0); + + uint8_t param_name[] = { 0x41U, 0x43U, 0x54U, 0x49U, 0x4FU, 0x4EU }; + uint8_t param_value[32] = { 0 }; + + int64_t value_len = otxn_param( + SBUF(param_value), + SBUF(param_name) + ); + + TRACEVAR(param_name); + TRACEHEX(param_name); + TRACEVAR(param_value); + TRACEHEX(param_value); + TRACEVAR(value_len); + + trace(SBUF("otxn_param_demo: ACTION 值(文本): "), SBUF(param_value), 0); + trace(SBUF("otxn_param_demo: ACTION 值(hex): "), SBUF(param_value), 1); + + accept(SBUF("otxn_param_demo: 参数已读取并追踪"), __LINE__); + return 0; }`, }, }, @@ -3073,6 +3574,7 @@ int64_t hook(uint32_t reserved) en: "Send a transaction with HookParameters from JavaScript", jp: "JavaScriptからHookParameters付きトランザクションを送信する", ko: "JavaScript에서 HookParameters 포함 트랜잭션 보내기", + zh: "从 JavaScript 发送带 HookParameters 的交易", }, language: "javascript", code: { @@ -3285,38 +3787,90 @@ async function sendParameters() { await client.disconnect(); } +sendParameters();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function sendParameters() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + const HOOK_ACCOUNT = "rAddressOfHookAccount"; + + const paramName = Buffer.from("ACTION").toString("hex").toUpperCase(); + const paramValue = "68656C6C6F"; + + const tx = { + TransactionType: "Payment", + Account: wallet.address, + Destination: HOOK_ACCOUNT, + Amount: "1000000", + HookParameters: [ + { + HookParameter: { + HookParameterName: paramName, + HookParameterValue: paramValue, + }, + }, + ], + }; + + console.log("正在发送带 HookParameters 的 Payment..."); + console.log(" 参数名(hex): ", paramName, " = ACTION"); + console.log(" 参数值(hex): ", paramValue); + + const prepared = await client.autofill(tx); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("TX 已发送,请检查 Hooks Builder 的 Debug Stream"); + console.log("你应该能看到来自该账户参数值的 Hook trace: " + wallet.address); + } + + await client.disconnect(); +} + sendParameters();`, }, }, ], slides: [ { - title: { es: "hook_param vs otxn_param", en: "hook_param vs otxn_param", jp: "hook_param vs otxn_param", ko: "hook_param vs otxn_param" }, + title: { es: "hook_param vs otxn_param", en: "hook_param vs otxn_param", jp: "hook_param vs otxn_param", ko: "hook_param vs otxn_param", zh: "hook_param vs otxn_param" }, content: { es: "Dos sistemas de parámetros distintos:\n\nhook_param() — configuración estática\n• Se define en SetHook al instalar\n• Almacenado junto al Hook en el ledger\n• Cambia solo al actualizar el Hook\n• Ideal para umbrales, direcciones fijas\n\notxn_param() — datos dinámicos\n• Viene en la transacción que activa el Hook\n• Lo envía el emisor de cada tx\n• Cambia en cada ejecución\n• Ideal para instrucciones, modos, referencias", en: "Two different parameter systems:\n\nhook_param() — static configuration\n• Defined in SetHook at installation\n• Stored alongside the Hook in the ledger\n• Changes only when updating the Hook\n• Ideal for thresholds, fixed addresses\n\notxn_param() — dynamic data\n• Comes in the transaction that activates the Hook\n• Sent by the sender of each tx\n• Changes with each execution\n• Ideal for instructions, modes, references", jp: "2つの異なるParameterシステム:\n\nhook_param() — 静的設定\n• インストール時のSetHookで定義\n• レジャーのHookと一緒に保存\n• Hookを更新するときのみ変更\n• 閾値、固定アドレスに最適\n\notxn_param() — 動的データ\n• Hookを起動するトランザクションに含まれる\n• 各txの送信者が送信する\n• 各実行で変わる\n• 命令、モード、参照に最適", ko: "두 가지 파라미터 시스템:\n\nhook_param() — 정적 설정\n• 설치 시 SetHook에서 정의\n• Hook과 함께 레저에 저장\n• Hook 업데이트 때만 변경\n• 임계값, 고정 주소에 적합\n\notxn_param() — 동적 데이터\n• Hook을 실행한 트랜잭션에 포함\n• 각 tx 발신자가 전달\n• 실행마다 달라짐\n• 명령, 모드, 참조값에 적합", + zh: "两种不同的参数系统:\n\nhook_param() — 静态配置\n• 在安装时通过 SetHook 定义\n• 与 Hook 一起存储在账本中\n• 仅在更新 Hook 时变化\n• 适合阈值、固定地址\n\notxn_param() — 动态数据\n• 包含在触发 Hook 的交易中\n• 由每笔 tx 的发送者提供\n• 每次执行都可能变化\n• 适合命令、模式、引用值", }, visual: "🎛️", }, { - title: { es: "otxn_param: firma y retornos", en: "otxn_param: signature and return values", jp: "otxn_param:シグネチャと戻り値", ko: "otxn_param: 시그니처와 반환값" }, + title: { es: "otxn_param: firma y retornos", en: "otxn_param: signature and return values", jp: "otxn_param:シグネチャと戻り値", ko: "otxn_param: 시그니처와 반환값", zh: "otxn_param:签名与返回值" }, content: { es: "int64_t otxn_param(\n write_ptr, write_len, // buffer salida\n read_ptr, read_len // nombre del param\n);\n\nRetornos:\n• > 0 → bytes escritos (encontrado)\n• DOESNT_EXIST → no está en la tx\n• TOO_SMALL → nombre vacío\n• TOO_BIG → nombre > 32 bytes\n• OUT_OF_BOUNDS → punteros inválidos\n\nNombre y valor en HEX en la transacción", en: "int64_t otxn_param(\n write_ptr, write_len, // output buffer\n read_ptr, read_len // param name\n);\n\nReturn values:\n• > 0 → bytes written (found)\n• DOESNT_EXIST → not in the tx\n• TOO_SMALL → empty name\n• TOO_BIG → name > 32 bytes\n• OUT_OF_BOUNDS → invalid pointers\n\nName and value in HEX in the transaction", jp: "int64_t otxn_param(\n write_ptr, write_len, // 出力バッファ\n read_ptr, read_len // パラメーター名\n);\n\n戻り値:\n• > 0 → 書き込まれたバイト数(見つかった)\n• DOESNT_EXIST → txに存在しない\n• TOO_SMALL → 空の名前\n• TOO_BIG → 名前が32バイト超\n• OUT_OF_BOUNDS → 無効なポインター\n\nトランザクション内の名前と値はHEXで", ko: "int64_t otxn_param(\n write_ptr, write_len, // 출력 버퍼\n read_ptr, read_len // 파라미터 이름\n);\n\n반환값:\n• > 0 → 기록된 바이트 수(찾음)\n• DOESNT_EXIST → tx에 없음\n• TOO_SMALL → 빈 이름\n• TOO_BIG → 이름이 32바이트 초과\n• OUT_OF_BOUNDS → 잘못된 포인터\n\n트랜잭션 안의 이름과 값은 HEX 형식", + zh: "int64_t otxn_param(\n write_ptr, write_len, // 输出缓冲区\n read_ptr, read_len // 参数名\n);\n\n返回值:\n• > 0 → 已写入的字节数(找到)\n• DOESNT_EXIST → 交易中不存在\n• TOO_SMALL → 名称为空\n• TOO_BIG → 名称超过 32 字节\n• OUT_OF_BOUNDS → 指针无效\n\n交易中的名称和值都使用 HEX 格式", }, visual: "📨", }, { - title: { es: "Namespace y recursos", en: "Namespace and resources", jp: "Namespaceとリソース", ko: "Namespace와 리소스" }, + title: { es: "Namespace y recursos", en: "Namespace and resources", jp: "Namespaceとリソース", ko: "Namespace와 리소스", zh: "Namespace 与资源" }, content: { es: "HookNamespace (32 bytes hex):\n• Distinto namespace = estado aislado\n• Mismo namespace = estado compartido\n• SHA-256 del nombre → namespace único\n\nRecursos:\n• hooks.services → string ↔ hex\n• HookOn calculator\n• Visualizador tiempo (Ripple Epoch)\n• tx-builder.xahau.tools → C desde JSON", en: "HookNamespace (32 bytes hex):\n• Different namespace = isolated state\n• Same namespace = shared state\n• SHA-256 of name → unique namespace\n\nResources:\n• hooks.services → string ↔ hex\n• HookOn calculator\n• Time visualizer (Ripple Epoch)\n• tx-builder.xahau.tools → C from JSON", jp: "HookNamespace(32バイトhex):\n• 異なる名前空間 = 分離されたステート\n• 同じ名前空間 = 共有ステート\n• 名前のSHA-256 → ユニークな名前空間\n\nリソース:\n• hooks.services → 文字列 ↔ hex\n• HookOn計算機\n• 時間ビジュアライザー(Ripple Epoch)\n• tx-builder.xahau.tools → JSONからC言語", ko: "HookNamespace(32바이트 hex):\n• 다른 namespace = 분리된 상태\n• 같은 namespace = 공유 상태\n• 이름의 SHA-256 → 고유 namespace\n\n리소스:\n• hooks.services → 문자열 ↔ hex\n• HookOn 계산기\n• Ripple Epoch 시간 변환기\n• tx-builder.xahau.tools → JSON을 C로 변환", + zh: "HookNamespace(32 字节 hex):\n• 不同 namespace = 隔离状态\n• 相同 namespace = 共享状态\n• 名称的 SHA-256 → 唯一 namespace\n\n资源:\n• hooks.services → 字符串 ↔ hex\n• HookOn 计算器\n• Ripple Epoch 时间转换器\n• tx-builder.xahau.tools → 从 JSON 生成 C", }, visual: "🔧", }, @@ -3329,6 +3883,7 @@ sendParameters();`, en: "Hook tracing and debugging", jp: "Hooksのトレースとデバッグ", ko: "Hook 추적과 디버깅", + zh: "Hook 的追踪与调试", }, theory: { es: `Cuando un Hook falla o se comporta de forma inesperada, necesitas una forma de **observar su ejecución interna**. El sistema de Hooks proporciona tres funciones de traza que emiten mensajes visibles en el **Debug Stream** de Hooks Builder y en los logs del nodo \`xahaud\`. @@ -3897,6 +4452,30 @@ int64_t cbak(uint32_t reserved) { - \`emit()\` 을 쓴다면 \`cbak()\` 도 함께 추적 디버깅 출력은 학습과 테스트에는 매우 유용하지만, 메인넷 배포 전에는 정리하는 것이 좋습니다.`, + zh: `当 Hook 失败或行为异常时,你需要一种方法来**观察其内部执行过程**。为此,Hooks 提供了多种追踪函数。 + +### 核心追踪函数 + +- \`trace()\`:输出普通文本或十六进制缓冲区 +- \`trace_num()\`:输出标签和整数值 +- \`trace_float()\`:输出标签和 XFL 浮点值 + +### 在哪里查看 + +- Hooks Builder 的 **Debug Stream** +- 本地 \`xahaud\` 节点日志 +- WebSocket 与交易元数据 + +### 调试建议 + +- 在 \`accept()\` / \`rollback()\` 中使用 \`__LINE__\` +- 所有消息统一加上 Hook 名称前缀 +- 关键函数返回值都用 \`trace_num()\` 打印 +- 二进制缓冲区用 hex 模式输出 +- 在每个 if/else 分支入口增加 trace +- 调试 \`emit()\` 时也要给 \`cbak()\` 增加追踪 + +调试输出在学习和测试时非常有用,但在部署到主网前最好清理掉。`, }, codeBlocks: [ { @@ -3905,6 +4484,7 @@ int64_t cbak(uint32_t reserved) { en: "Hook instrumented with all trace functions", jp: "すべてのトレース関数を利用したHook", ko: "모든 trace 함수를 사용하는 Hook", + zh: "使用全部 trace 函数的 Hook", }, language: "c", code: { @@ -4175,38 +4755,90 @@ int64_t hook(uint32_t reserved) trace(SBUF("debug_demo:결제 수락, 종료"), 0, 0, 0); accept(SBUF("debug_demo:ok"), __LINE__); return 0; +}`, + zh: `#include "hookapi.h" + +/** + * Hook: debug_demo.c + * + * 目标: + * - 演示如何使用 trace()、trace_num() 和 trace_float() + * 来实时检查 Hook 的执行。 + * - 只接受原生 XAH 付款。 + */ + +int64_t hook(uint32_t reserved) +{ + _g(1, 1); + + trace(SBUF("debug_demo:hook() 已启动"), 0, 0, 0); + + uint8_t hook_acc[20]; + hook_account(SBUF(hook_acc)); + trace(SBUF("debug_demo:hook_account (20 bytes): "), SBUF(hook_acc), 1); + + int64_t tt = otxn_type(); + trace_num(SBUF("debug_demo:tx 类型 (0=Payment): "), tt); + + if (tt != 0) + { + trace(SBUF("debug_demo:不是付款 - 退出"), 0, 0, 0); + accept(SBUF("debug_demo:ok (no payment)"), __LINE__); + } + + trace(SBUF("debug_demo:进入 payment 分支"), 0, 0, 0); + + unsigned char amount_buf[48]; + int64_t amount_len = otxn_field(SBUF(amount_buf), sfAmount); + trace_num(SBUF("debug_demo:Amount 字节数: "), amount_len); + + if (amount_len != 8) + { + trace(SBUF("debug_demo:Amount 不是原生 XAH"), 0, 0, 0); + rollback(SBUF("debug_demo:仅允许原生 XAH"), __LINE__); + } + + int64_t drops = AMOUNT_TO_DROPS(amount_buf); + trace_num(SBUF("debug_demo:收到的 drops: "), drops); + + trace(SBUF("debug_demo:付款已接受,退出"), 0, 0, 0); + accept(SBUF("debug_demo:ok"), __LINE__); + return 0; }`, }, }, ], slides: [ { - title: { es: "Las tres funciones trace*", en: "The three trace* functions", jp: "3つのtrace*関数", ko: "세 가지 trace* 함수" }, + title: { es: "Las tres funciones trace*", en: "The three trace* functions", jp: "3つのtrace*関数", ko: "세 가지 trace* 함수", zh: "三种 trace* 函数" }, content: { es: "Instrumentar el Hook para ver su ejecución:\n\ntrace(SBUF(\"mensaje\"), 0);\n→ Texto plano en el Debug Stream\n\ntrace(SBUF(buffer), 1);\n→ Contenido del buffer como hex\n\ntrace_num(SBUF(\"label: \"), valor);\n→ Etiqueta + número entero (drops, retornos...)\n\ntrace_float(SBUF(\"label: \"), xfl);\n→ Etiqueta + XFL (coma flotante de Xahau)", en: "Instrument the Hook to see its execution:\n\ntrace(SBUF(\"message\"), 0);\n→ Plain text in Debug Stream\n\ntrace(SBUF(buffer), 1);\n→ Buffer content as hex\n\ntrace_num(SBUF(\"label: \"), value);\n→ Label + integer (drops, returns...)\n\ntrace_float(SBUF(\"label: \"), xfl);\n→ Label + XFL (Xahau floating point)", jp: "Hookの実行を確認するために計装する:\n\ntrace(SBUF(\"メッセージ\"), 0);\n→ Debug Streamにプレーンテキスト\n\ntrace(SBUF(buffer), 1);\n→ バッファの内容をhexとして\n\ntrace_num(SBUF(\"ラベル: \"), 値);\n→ ラベル + 整数(drops、戻り値...)\n\ntrace_float(SBUF(\"ラベル: \"), xfl);\n→ ラベル + XFL(Xahauの浮動小数点)", ko: "Hook 실행을 보기 위한 계측 함수:\n\ntrace(SBUF(\"message\"), 0);\n→ Debug Stream에 일반 텍스트 출력\n\ntrace(SBUF(buffer), 1);\n→ 버퍼를 hex로 출력\n\ntrace_num(SBUF(\"label: \"), value);\n→ 라벨 + 정수값(drops, 반환값 등)\n\ntrace_float(SBUF(\"label: \"), xfl);\n→ 라벨 + XFL 부동소수 표현", + zh: "用于观察 Hook 执行的追踪函数:\n\ntrace(SBUF(\"message\"), 0);\n→ 在 Debug Stream 输出普通文本\n\ntrace(SBUF(buffer), 1);\n→ 以 hex 输出缓冲区\n\ntrace_num(SBUF(\"label: \"), value);\n→ 输出标签 + 整数值(drops、返回值等)\n\ntrace_float(SBUF(\"label: \"), xfl);\n→ 输出标签 + XFL 浮点表示", }, visual: "🔍", }, { - title: { es: "Donde ver las trazas", en: "Where to see traces", jp: "トレースを確認する場所", ko: "trace를 확인하는 곳" }, + title: { es: "Donde ver las trazas", en: "Where to see traces", jp: "トレースを確認する場所", ko: "trace를 확인하는 곳", zh: "在哪里查看 trace" }, content: { es: "Tres formas de leer la salida:\n\n1. Hooks Builder → Debug Stream\n Selecciona la cuenta en el desplegable\n\n2. Logs del nodo xahaud\n En modo debug (desarrollo local)\n\n3. WebSocket desde Node.js\n Suscríbete a la cuenta y lee debug_info\n + HookExecutions en la metadata de la tx", en: "Three ways to read the output:\n\n1. Hooks Builder → Debug Stream\n Select the account from the dropdown\n\n2. xahaud node logs\n In debug mode (local development)\n\n3. WebSocket from Node.js\n Subscribe to the account and read debug_info\n + HookExecutions in tx metadata", jp: "出力を読む3つの方法:\n\n1. Hooks Builder → Debug Stream\n ドロップダウンからアカウントを選択\n\n2. xahaudノードログ\n デバッグモード(ローカル開発)\n\n3. Node.jsからのWebSocket\n アカウントをサブスクライブしてdebug_infoを読む\n + txメタデータのHookExecutions", ko: "출력을 확인하는 세 가지 방법:\n\n1. Hooks Builder → Debug Stream\n 드롭다운에서 계정 선택\n\n2. xahaud 노드 로그\n 로컬 개발의 디버그 모드\n\n3. Node.js WebSocket\n 계정을 구독하고 debug_info 및\n tx 메타데이터의 HookExecutions 확인", + zh: "有三种方式查看输出:\n\n1. Hooks Builder → Debug Stream\n 在下拉菜单中选择账户\n\n2. xahaud 节点日志\n 适用于本地开发调试模式\n\n3. Node.js WebSocket\n 订阅账户并检查 debug_info 与\n 交易元数据中的 HookExecutions", }, visual: "📡", }, { - title: { es: "Trucos clave de debugging", en: "Key debugging tips", jp: "デバッグの重要なヒント", ko: "중요한 디버깅 팁" }, + title: { es: "Trucos clave de debugging", en: "Key debugging tips", jp: "デバッグの重要なヒント", ko: "중요한 디버깅 팁", zh: "关键调试技巧" }, content: { es: "• __LINE__ en accept/rollback → linea exacta de salida\n• Prefijo 'mi_hook:' en cada mensaje\n• trace_num del retorno de CADA funcion critica\n (negativo = error silencioso)\n• trace con hex=1 para buffers binarios\n• Una traza al inicio de cada rama if/else\n• Instrumenta cbak() para debug de emit()\n• Elimina trazas antes de ir a Mainnet", en: "• __LINE__ in accept/rollback → exact exit line\n• Prefix 'my_hook:' in each message\n• trace_num the return of EVERY critical function\n (negative = silent error)\n• trace with hex=1 for binary buffers\n• One trace at the start of each if/else branch\n• Instrument cbak() to debug emit()\n• Remove traces before going to Mainnet", jp: "• __LINE__をaccept/rollbackで使う → 正確な終了行\n• 各メッセージに'my_hook:'プレフィックスを付ける\n• すべての重要な関数の戻り値をtrace_numする\n (負の値 = サイレントエラー)\n• バイナリバッファにはhex=1でtrace\n• 各if/elseブランチの先頭にトレースを置く\n• emit()デバッグのためにcbak()を計装する\n• Mainnetに移行する前にトレースを削除する", ko: "• accept/rollback에 __LINE__ 사용 → 종료 지점 확인\n• 모든 메시지에 'my_hook:' prefix 추가\n• 중요한 함수 반환값은 항상 trace_num\n (음수 = 숨은 오류)\n• 바이너리 버퍼는 hex=1로 출력\n• 각 if/else 시작점에 trace 추가\n• emit() 디버깅을 위해 cbak()도 계측\n• 메인넷 전에는 trace 정리", + zh: "• 在 accept/rollback 中使用 __LINE__ → 快速确认退出位置\n• 所有消息都加上 'my_hook:' 前缀\n• 关键函数返回值都用 trace_num 输出\n (负数通常表示隐藏错误)\n• 二进制缓冲区使用 hex=1 输出\n• 在每个 if/else 起点加 trace\n• 调试 emit() 时也要追踪 cbak()\n• 主网上线前清理 trace", }, visual: "🐛", }, @@ -4219,6 +4851,7 @@ int64_t hook(uint32_t reserved) en: "Hooks Builder: Online development", jp: "Hooks Builder:オンライン開発", ko: "Hooks Builder: 온라인 개발", + zh: "Hooks Builder:在线开发", }, theory: { es: `[Hooks Builder](https://builder.xahau.network) es el entorno de desarrollo online para Hooks en **Xahau Testnet**. Permite escribir, compilar, desplegar y probar Hooks directamente desde el navegador sin necesidad de instalar nada en tu equipo. **Nota:** Recuerda guardar tus avances y seeds antes de cerrar el navegador, puede que no se guarden una vez cerrada la sesión. @@ -4501,36 +5134,55 @@ HookのコンパイルができたらDeploy**タブに戻ります。 - 정상, 실패, 경계, 예외 케이스를 모두 테스트 빠르게 학습하고 실험하기에는 Hooks Builder가 가장 쉬운 진입점이지만, 메인넷 운영이나 자동화에는 한계가 있습니다.`, + zh: `[Hooks Builder](https://builder.xahau.network) 是面向 **Xahau Testnet** 的在线 Hook 开发环境。只用浏览器就能快速完成编写、编译、部署和测试。 + +### 主要标签页 + +- **Develop**:编写与编译 Hook 代码 +- **Deploy**:管理账户并安装 Hook +- **Test**:执行测试交易并查看日志 + +### 实操建议 + +- 至少准备两个测试账户 +- 关闭浏览器前保存好 seed 和当前进度 +- 部署后同时查看 Debug Stream 与结果元数据 +- 覆盖正常、失败、边界和异常场景 + +Hooks Builder 是学习和快速实验最容易的入口,但在主网运维或自动化方面仍然有限制。`, }, codeBlocks: [], slides: [ { - title: { es: "Hooks Builder — Entorno online", en: "Hooks Builder — Online environment", jp: "Hooks Builder — オンライン環境", ko: "Hooks Builder — 온라인 환경" }, + title: { es: "Hooks Builder — Entorno online", en: "Hooks Builder — Online environment", jp: "Hooks Builder — オンライン環境", ko: "Hooks Builder — 온라인 환경", zh: "Hooks Builder — 在线环境" }, content: { es: "builder.xahau.network (solo Testnet)\n\nTres pestanas:\n• Develop: escribir y compilar Hooks en C\n• Deploy: gestionar cuentas y desplegar\n• Test: probar con transacciones reales\n\nGuarda tus seeds antes de cerrar el navegador", en: "builder.xahau.network (Testnet only)\n\nThree tabs:\n• Develop: write and compile Hooks in C\n• Deploy: manage accounts and deploy\n• Test: test with real transactions\n\nSave your seeds before closing the browser", jp: "builder.xahau.network(Testnetのみ)\n\n3つのタブ:\n• Develop:C言語でHooksを記述およびコンパイル\n• Deploy:アカウントを管理してデプロイ\n• Test:実際のトランザクションでテスト\n\nブラウザを閉じる前にシードを保存する", ko: "builder.xahau.network (Testnet 전용)\n\n세 가지 탭:\n• Develop: C로 Hook 작성 및 컴파일\n• Deploy: 계정 관리와 배포\n• Test: 실제 트랜잭션으로 테스트\n\n브라우저를 닫기 전에 seed를 저장", + zh: "builder.xahau.network(仅限 Testnet)\n\n三个标签页:\n• Develop:用 C 编写并编译 Hook\n• Deploy:管理账户并部署\n• Test:用真实交易测试\n\n关闭浏览器前记得保存 seed", }, visual: "🌐", }, { - title: { es: "Deploy: cuentas e instalacion", en: "Deploy: accounts and installation", jp: "Deploy:アカウントとインストール", ko: "Deploy: 계정과 설치" }, + title: { es: "Deploy: cuentas e instalacion", en: "Deploy: accounts and installation", jp: "Deploy:アカウントとインストール", ko: "Deploy: 계정과 설치", zh: "Deploy:账户与安装" }, content: { es: "Cuentas:\n• Generate Account → nueva con faucet\n• Import Account → seed existente de testnet\n• Minimo 2 cuentas (Hook + pruebas)\n\nInstalacion:\n• Seleccionar cuenta + Set Hook\n• Configurar HookOn, Namespace, Parameters\n• Fee → Suggest si hay error de fee", en: "Accounts:\n• Generate Account → new with faucet\n• Import Account → existing testnet seed\n• Minimum 2 accounts (Hook + testing)\n\nInstallation:\n• Select account + Set Hook\n• Configure HookOn, Namespace, Parameters\n• Fee → Suggest if fee error", jp: "アカウント:\n• Generate Account → フォーセットで新規作成\n• Import Account → 既存のTestnetシード\n• 最低2つのアカウント(Hook + テスト用)\n\nインストール:\n• アカウントを選択 + Set Hook\n• HookOn、Namespace、Parametersを設定\n• Fee → 手数料エラーの場合はSuggest", ko: "계정:\n• Generate Account → faucet으로 새 계정 생성\n• Import Account → 기존 testnet seed 가져오기\n• 최소 2개 계정 필요(Hook + 테스트)\n\n설치:\n• 계정 선택 후 Set Hook\n• HookOn, Namespace, Parameters 설정\n• 수수료 오류 시 Suggest 사용", + zh: "账户:\n• Generate Account → 通过 faucet 创建新账户\n• Import Account → 导入已有 testnet seed\n• 至少需要 2 个账户(Hook + 测试)\n\n安装:\n• 选择账户后点击 Set Hook\n• 配置 HookOn、Namespace、Parameters\n• 如遇费用错误可使用 Suggest", }, visual: "🚀", }, { - title: { es: "Test: verificar tu Hook", en: "Test: verify your Hook", jp: "Test:HookをVerify", ko: "Test: Hook 검증" }, + title: { es: "Test: verificar tu Hook", en: "Test: verify your Hook", jp: "Test:HookをVerify", ko: "Test: Hook 검증", zh: "Test:验证你的 Hook" }, content: { es: "• Elegir tipo de tx, cuenta origen, destino\n• Configurar Amount, Flags, Memos\n• Run Test → revisar Development Log\n• Debug Stream: elegir cuenta a monitorear\n\nPruebas recomendadas:\n Positivos | Negativos | Limites | No esperados", en: "• Choose tx type, sender account, destination\n• Configure Amount, Flags, Memos\n• Run Test → check Development Log\n• Debug Stream: choose account to monitor\n\nRecommended tests:\n Positive | Negative | Edge cases | Unexpected", jp: "• txタイプ、送信者アカウント、宛先を選択\n• Amount、Flags、Memosを設定\n• Run Test → Development Logを確認\n• Debug Stream:監視するアカウントを選択\n\n推奨テスト:\n 正常 | 異常 | 境界 | 予期しない", ko: "• tx 타입, 발신 계정, 목적지 선택\n• Amount, Flags, Memos 설정\n• Run Test → Development Log 확인\n• Debug Stream에서 모니터링할 계정 선택\n\n권장 테스트:\n 정상 | 실패 | 경계값 | 예외 케이스", + zh: "• 选择 tx 类型、发送账户和目标地址\n• 配置 Amount、Flags、Memos\n• Run Test → 查看 Development Log\n• 在 Debug Stream 中选择要监控的账户\n\n推荐测试:\n 正常 | 失败 | 边界值 | 异常情况", }, visual: "🧪", }, @@ -4543,6 +5195,7 @@ HookのコンパイルができたらDeploy**タブに戻ります。 en: "Local Hook development with hooks-cli", jp: "hooks-cliによるHooksのローカル開発", ko: "hooks-cli를 사용한 로컬 Hook 개발", + zh: "使用 hooks-cli 进行本地 Hook 开发", }, theory: { es: `Para desarrollo profesional, despliegue en **Xahau Mainnet** o proyectos que requieran mayor control, necesitas un entorno de desarrollo local. La herramienta principal es [hooks-cli](https://github.com/Xahau/hooks-cli), una CLI oficial que permite compilar Hooks en C a WebAssembly desde tu terminal. @@ -4851,38 +5504,59 @@ hooks-cli、高度なコンパイルオプション、完全なHooks APIの詳 5. \`xahau.js\` 로 \`SetHook\` 트랜잭션 배포 로컬 환경은 버전 관리, 반복 테스트, 스크립트 자동화, 메인넷 운영 준비에 훨씬 유리합니다.`, + zh: `如果要做更专业的开发,或者部署到 **Xahau Mainnet**,就需要本地开发环境。核心工具是 [hooks-cli](https://github.com/Xahau/hooks-cli)。 + +### hooks-cli 能做什么 + +- 将 C 代码编译为 WebAssembly +- 提供所需的编译器与头文件 +- 生成基础 Hook 项目结构 +- 适合本地迭代开发与自动化流程 + +### 常见流程 + +1. \`npm install -g hooks-cli\` +2. \`hooks-cli init c my-hook-project\` +3. 在项目目录中执行 \`yarn install\` +4. 使用 \`yarn run build\` 生成 WASM +5. 再用 \`xahau.js\` 部署 \`SetHook\` 交易 + +本地环境在版本管理、反复测试、脚本自动化以及主网准备方面都更有优势。`, }, codeBlocks: [ ], slides: [ { - title: { es: "hooks-cli — Desarrollo local", en: "hooks-cli — Local development", jp: "hooks-cli — ローカル開発", ko: "hooks-cli — 로컬 개발" }, + title: { es: "hooks-cli — Desarrollo local", en: "hooks-cli — Local development", jp: "hooks-cli — ローカル開発", ko: "hooks-cli — 로컬 개발", zh: "hooks-cli — 本地开发" }, content: { es: "CLI oficial para compilar Hooks\n\nnpm install -g hooks-cli\nhooks-cli init c mi-proyecto\ncd mi-proyecto && yarn install\nyarn run build\n\nPara desarrollo profesional y Mainnet", en: "Official CLI to compile Hooks\n\nnpm install -g hooks-cli\nhooks-cli init c my-project\ncd my-project && yarn install\nyarn run build\n\nFor professional development and Mainnet", jp: "HooksをコンパイルするためのCLI\n\nnpm install -g hooks-cli\nhooks-cli init c my-project\ncd my-project && yarn install\nyarn run build\n\nプロフェッショナルな開発とMainnet向け", ko: "Hook 컴파일용 공식 CLI\n\nnpm install -g hooks-cli\nhooks-cli init c my-project\ncd my-project && yarn install\nyarn run build\n\n전문 개발과 Mainnet 배포에 적합", + zh: "用于编译 Hook 的官方 CLI\n\nnpm install -g hooks-cli\nhooks-cli init c my-project\ncd my-project && yarn install\nyarn run build\n\n适合专业开发与 Mainnet 部署", }, visual: "🔨", }, { - title: { es: "Estructura del proyecto", en: "Project structure", jp: "プロジェクト構造", ko: "프로젝트 구조" }, + title: { es: "Estructura del proyecto", en: "Project structure", jp: "プロジェクト構造", ko: "프로젝트 구조", zh: "项目结构" }, content: { es: "hooks-cli init c genera:\n\nmi-proyecto-hook/\n├── contracts/base.c\n├── .env\n├── package.json\n├── tsconfig.json\n└── src/index.ts\n\nCompilar: yarn run build\nAlternativa: hooks-cli compile-c contracts build/", en: "hooks-cli init c generates:\n\nmy-hook-project/\n├── contracts/base.c\n├── .env\n├── package.json\n├── tsconfig.json\n└── src/index.ts\n\nCompile: yarn run build\nAlternative: hooks-cli compile-c contracts build/", jp: "hooks-cli init c が生成するもの:\n\nmy-hook-project/\n├── contracts/base.c\n├── .env\n├── package.json\n├── tsconfig.json\n└── src/index.ts\n\nコンパイル: yarn run build\n代替: hooks-cli compile-c contracts build/", ko: "hooks-cli init c 로 생성되는 구조:\n\nmy-hook-project/\n├── contracts/base.c\n├── .env\n├── package.json\n├── tsconfig.json\n└── src/index.ts\n\n컴파일: yarn run build\n대안: hooks-cli compile-c contracts build/", + zh: "hooks-cli init c 会生成如下结构:\n\nmy-hook-project/\n├── contracts/base.c\n├── .env\n├── package.json\n├── tsconfig.json\n└── src/index.ts\n\n编译:yarn run build\n替代方式:hooks-cli compile-c contracts build/", }, visual: "📁", }, { - title: { es: "Despliegue y referencia", en: "Deployment and reference", jp: "デプロイとリファレンス", ko: "배포와 참고자료" }, + title: { es: "Despliegue y referencia", en: "Deployment and reference", jp: "デプロイとリファレンス", ko: "배포와 참고자료", zh: "部署与参考资料" }, content: { es: "SetHook con xahau.js:\n• Leer .wasm → hex → CreateCode\n• Configurar HookOn, HookCanEmit, Namespace\n• crypto.createHash('sha256') para namespace\n\nReferencia:\n• github.com/Xahau/hooks-cli\n• hooks-toolkit.com", en: "SetHook with xahau.js:\n• Read .wasm → hex → CreateCode\n• Configure HookOn, HookCanEmit, Namespace\n• crypto.createHash('sha256') for namespace\n\nReference:\n• github.com/Xahau/hooks-cli\n• hooks-toolkit.com", jp: "xahau.jsでSetHook:\n• .wasmを読み込む → hex → CreateCode\n• HookOn、HookCanEmit、Namespaceを設定\n• Namespace用にcrypto.createHash('sha256')\n\nリファレンス:\n• github.com/Xahau/hooks-cli\n• hooks-toolkit.com", ko: "xahau.js로 SetHook 배포:\n• .wasm 읽기 → hex → CreateCode\n• HookOn, HookCanEmit, Namespace 설정\n• namespace용 crypto.createHash('sha256') 사용\n\n참고:\n• github.com/Xahau/hooks-cli\n• hooks-toolkit.com", + zh: "使用 xahau.js 部署 SetHook:\n• 读取 .wasm → 转 hex → CreateCode\n• 配置 HookOn、HookCanEmit、Namespace\n• 对 namespace 使用 crypto.createHash('sha256')\n\n参考:\n• github.com/Xahau/hooks-cli\n• hooks-toolkit.com", }, visual: "📚", }, diff --git a/src/data/modules/m10-escrows-checks.js b/src/data/modules/m10-escrows-checks.js index a35247f..591494b 100644 --- a/src/data/modules/m10-escrows-checks.js +++ b/src/data/modules/m10-escrows-checks.js @@ -6,6 +6,7 @@ export default { en: "Other Available Transactions", jp: "その他の利用可能なトランザクション", ko: "기타 사용 가능한 트랜잭션", + zh: "其他可用的交易", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "Escrows: Conditional Payments", jp: "エスクロー:条件付き支払い", ko: "Escrow: 조건부 결제", + zh: "Escrow:条件支付", }, theory: { es: `Un **Escrow** es un mecanismo de pago condicional que bloquea fondos hasta que se cumplan ciertas condiciones. Es como un sobre sellado con dinero que solo se puede abrir bajo circunstancias específicas. Una caja fuerte condicional. @@ -174,6 +176,22 @@ Xahauは**Interledger (ILP)**プロトコルの暗号条件をサポートしま - \`EscrowCancel\`: 취소 가능 시점 이후 취소 시간 조건과 암호 조건을 잘 이해해야 안전하게 사용할 수 있습니다.`, + zh: `**Escrow** 是一种在满足条件之前锁定资金的机制,适合未来付款或条件结算等不适合立即转账的场景。 + +### 常见用途 + +- 预约付款 +- 条件释放资金 +- 基于哈希条件的交换 +- 代币归属期发放 + +### 核心交易 + +- \`EscrowCreate\`:锁定资金 +- \`EscrowFinish\`:条件满足后释放 +- \`EscrowCancel\`:在可取消时间后撤销 + +安全使用 Escrow 的关键是理解时间条件和加密条件。`, }, codeBlocks: [ { @@ -181,6 +199,7 @@ Xahauは**Interledger (ILP)**プロトコルの暗号条件をサポートしま es: "Crear un escrow con bloqueo temporal (FinishAfter = 5 minutos)", en: "Create an escrow with time lock (FinishAfter = 2 minutes)", jp: "タイムロック付きエスクローの作成(FinishAfter = 2分)", + zh: "创建带时间锁的 Escrow(FinishAfter = 2 分钟)", }, language: "javascript", code: { @@ -354,6 +373,63 @@ async function createTimeLockedEscrow() { await client.disconnect(); } +createTimeLockedEscrow();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function createTimeLockedEscrow() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const sender = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // Ripple Epoch:自 2000/01/01 00:00:00 UTC 起的秒数 + // 与 Unix Epoch 相差 946684800 秒 + const RIPPLE_EPOCH_OFFSET = 946684800; + const now = Math.floor(Date.now() / 1000); + + // FinishAfter:2 分钟后 + const finishAfter = now - RIPPLE_EPOCH_OFFSET + 2 * 60; + // CancelAfter:24 小时后(若无人完成,可取消) + const cancelAfter = now - RIPPLE_EPOCH_OFFSET + 24 * 60 * 60; + + const escrowCreate = { + TransactionType: "EscrowCreate", + Account: sender.address, + Destination: "rDestinationAddress", + Amount: xahToDrops(10), // 锁定 10 XAH + FinishAfter: finishAfter, + CancelAfter: cancelAfter, + }; + + const prepared = await client.autofill(escrowCreate); + const signed = sender.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== EscrowCreate ==="); + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("Hash:", signed.hash); + console.log("Sequence:", prepared.Sequence); + console.log( + "FinishAfter:", + new Date((finishAfter + RIPPLE_EPOCH_OFFSET) * 1000).toISOString() + ); + console.log( + "CancelAfter:", + new Date((cancelAfter + RIPPLE_EPOCH_OFFSET) * 1000).toISOString() + ); + console.log("请保存这个 Sequence!EscrowFinish 会用到它。"); + console.log(\`Escrow Sequence: \${prepared.Sequence}\`); + console.log(\`你的地址: \${sender.address}\`); + + } + + await client.disconnect(); +} + createTimeLockedEscrow();`, }, }, @@ -362,6 +438,7 @@ createTimeLockedEscrow();`, es: "Completar (finish) un escrow después del tiempo de bloqueo", en: "Complete (finish) an escrow after the lock period", jp: "ロック期間後にエスクローを完了(finish)する", + zh: "在锁定期结束后完成(finish)Escrow", }, language: "javascript", code: { @@ -598,35 +675,116 @@ async function finishEscrow(ownerAddress, escrowSequence) { } // 作成者のアドレスとEscrowCreateのSequenceを使用 +finishEscrow("rCreatorAddress", 12345);`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function finishEscrow(ownerAddress, escrowSequence) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 任何账户都可以执行 EscrowFinish + const executor = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // 先查询 account_objects,确认 escrow 仍然存在 + const objects = await client.request({ + command: "account_objects", + account: ownerAddress, + type: "escrow", + ledger_index: "validated", + }); + + const escrow = objects.result.account_objects.find( + (obj) => obj.PreviousTxnLgrSeq !== undefined + ); + + if (!escrow) { + console.log("未找到 escrow。它可能已经完成或被取消。"); + await client.disconnect(); + return; + } + + console.log("=== 找到 Escrow ==="); + console.log("Amount:", Number(escrow.Amount) / 1_000_000, "XAH"); + console.log("Destination:", escrow.Destination); + + // 检查是否已经过了 FinishAfter + const RIPPLE_EPOCH_OFFSET = 946684800; + const now = Math.floor(Date.now() / 1000); + const finishAfterUnix = escrow.FinishAfter + RIPPLE_EPOCH_OFFSET; + + if (now < finishAfterUnix) { + const remaining = finishAfterUnix - now; + console.log( + \`现在还不能完成这个 escrow。还需等待 \${remaining} 秒。\` + ); + console.log( + \`可执行时间: \${new Date(finishAfterUnix * 1000).toISOString()}\` + ); + await client.disconnect(); + return; + } + + console.log("锁定时间已过,正在完成 escrow..."); + + const escrowFinish = { + TransactionType: "EscrowFinish", + Account: executor.address, + Owner: ownerAddress, + OfferSequence: escrowSequence, + }; + + const prepared = await client.autofill(escrowFinish); + const signed = executor.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== EscrowFinish ==="); + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("Escrow 已完成,资金已发送给接收方。"); + console.log("Hash:", signed.hash); + } else if (txResult === "tecNO_TARGET") { + console.log("未找到该 escrow,可能已经被取消。"); + } + + await client.disconnect(); +} + +// 使用创建者地址和 EscrowCreate 的 Sequence finishEscrow("rCreatorAddress", 12345);`, }, }, ], slides: [ { - title: { es: "¿Qué es un Escrow?", en: "What is an Escrow?", jp: "エスクローとは?" }, + title: { es: "¿Qué es un Escrow?", en: "What is an Escrow?", jp: "エスクローとは?", zh: "什么是 Escrow?" }, content: { es: "Pago condicional que bloquea fondos\n\n• Bloqueo temporal (FinishAfter)\n• Cancelación automática (CancelAfter)\n• Condición criptográfica (Condition)\n\nUsos: pagos programados, vesting, atomic swaps", en: "Conditional payment that locks funds\n\n• Time lock (FinishAfter)\n• Automatic cancellation (CancelAfter)\n• Cryptographic condition (Condition)\n\nUses: scheduled payments, vesting, atomic swaps", jp: "資金をロックする条件付き支払い\n\n• 時間ロック(FinishAfter)\n• 自動キャンセル(CancelAfter)\n• 暗号条件(Condition)\n\n用途:スケジュール支払い、ベスティング、アトミックスワップ", + zh: "锁定资金的条件支付\n\n• 时间锁(FinishAfter)\n• 自动取消(CancelAfter)\n• 加密条件(Condition)\n\n用途:预约付款、归属期发放、原子交换", }, visual: "🔐", }, { - title: { es: "Ciclo de vida del Escrow", en: "Escrow lifecycle", jp: "エスクローのライフサイクル" }, + title: { es: "Ciclo de vida del Escrow", en: "Escrow lifecycle", jp: "エスクローのライフサイクル", zh: "Escrow 生命周期" }, content: { es: "1. EscrowCreate → Bloquea los fondos\n ↓ (pasa el tiempo)\n2. EscrowFinish → Libera al destinatario\n ó\n2. EscrowCancel → Devuelve al creador\n\n• FinishAfter debe pasar antes de Finish\n• CancelAfter debe pasar antes de Cancel", en: "1. EscrowCreate → Locks the funds\n ↓ (time passes)\n2. EscrowFinish → Releases to recipient\n or\n2. EscrowCancel → Returns to creator\n\n• FinishAfter must pass before Finish\n• CancelAfter must pass before Cancel", jp: "1. EscrowCreate → 資金をロック\n ↓ (時間経過)\n2. EscrowFinish → 受取人にリリース\n または\n2. EscrowCancel → 作成者に返還\n\n• Finish前にFinishAfterが必要\n• Cancel前にCancelAfterが必要", + zh: "1. EscrowCreate → 锁定资金\n ↓(等待时间经过)\n2. EscrowFinish → 释放给接收方\n 或\n2. EscrowCancel → 退还给创建者\n\n• 必须先过 FinishAfter 才能 Finish\n• 必须先过 CancelAfter 才能 Cancel", }, visual: "⏳", }, { - title: { es: "Crypto-condiciones", en: "Crypto-conditions", jp: "暗号条件" }, + title: { es: "Crypto-condiciones", en: "Crypto-conditions", jp: "暗号条件", zh: "加密条件" }, content: { es: "Escrows con prueba criptográfica:\n\n• Condition = hash SHA-256\n• Fulfillment = preimagen secreta\n• Solo quien conozca el secreto puede completar\n• Basado en Interledger Protocol\n\nIdeal para intercambios trustless entre partes", en: "Escrows with cryptographic proof:\n\n• Condition = SHA-256 hash\n• Fulfillment = secret preimage\n• Only those who know the secret can complete\n• Based on Interledger Protocol\n\nIdeal for trustless exchanges between parties", jp: "暗号証明付きエスクロー:\n\n• Condition = SHA-256ハッシュ\n• Fulfillment = 秘密のプリイメージ\n• 秘密を知る者だけが完了可能\n• Interledgerプロトコルに基づく\n\n当事者間のトラストレス交換に最適", + zh: "带加密证明的 Escrow:\n\n• Condition = SHA-256 哈希\n• Fulfillment = 秘密原像\n• 只有知道秘密的人才能完成\n• 基于 Interledger 协议\n\n适合双方互不信任的交换场景", }, visual: "🔑", }, @@ -639,6 +797,7 @@ finishEscrow("rCreatorAddress", 12345);`, en: "Checks: Deferred Payments", jp: "チェック:遅延支払い", ko: "Checks: 지연 결제", + zh: "Checks:延迟支付", }, theory: { es: `Un **Check** (cheque) es similar a un cheque bancario tradicional: el emisor crea un cheque por una cantidad determinada, y el receptor puede cobrarlo cuando lo desee. A diferencia de un pago directo, los fondos **no se transfieren inmediatamente**, el receptor debe ejecutar una acción para cobrar el cheque. @@ -855,6 +1014,22 @@ Either party (sender or recipient) can cancel a check. An expired check can also - \`CheckCancel\` 즉시 결제보다 유연하지만, 만료와 잔액 상태를 함께 관리해야 합니다.`, + zh: `**Check** 类似银行支票:发送方承诺一笔金额,接收方稍后再去兑现。与即时转账不同,兑现时机由接收方决定。 + +### 优点 + +- 接收方可以自行决定兑现时间 +- 支持部分兑现 +- 同时支持 XAH 和 IOU +- 接收方不必当下在线 + +### 相关交易 + +- \`CheckCreate\` +- \`CheckCash\` +- \`CheckCancel\` + +它比即时支付更灵活,但也需要一起管理到期时间和余额状态。`, }, codeBlocks: [ { @@ -862,6 +1037,7 @@ Either party (sender or recipient) can cancel a check. An expired check can also es: "Crear un cheque", en: "Create a check", jp: "チェックの作成", + zh: "创建 Check", }, language: "javascript", code: { @@ -1002,6 +1178,52 @@ async function checkExample() { await client.disconnect(); } +checkExample();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function checkExample() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const sender = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + const receiverAddress = "rReceiverAddress"; // 替换成接收方地址,并把该账户 seed 保存到 .env 的 CASH_SEED,供下个示例使用 + + // === 1. 创建 Check === + const RIPPLE_EPOCH_OFFSET = 946684800; + const expiration = Math.floor(Date.now() / 1000) - RIPPLE_EPOCH_OFFSET + 7 * 24 * 60 * 60; // 7 天后过期 + + const checkCreate = { + TransactionType: "CheckCreate", + Account: sender.address, + Destination: receiverAddress, + SendMax: xahToDrops(50), // 最多 50 XAH + Expiration: expiration, + }; + + const prepared = await client.autofill(checkCreate); + const signed = sender.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + console.log("=== CheckCreate ==="); + console.log("结果:", result.result.meta.TransactionResult); + + if (result.result.meta.TransactionResult === "tesSUCCESS") { + // 在受影响节点中查找 CheckID + const createdNode = result.result.meta.AffectedNodes.find( + (node) => node.CreatedNode && node.CreatedNode.LedgerEntryType === "Check" + ); + + if (createdNode) { + const checkID = createdNode.CreatedNode.LedgerIndex; + console.log("CheckID:", checkID); + console.log("请保存这个 CheckID,之后要用它从你的账户兑现支票: " + sender.address); + } + } + + await client.disconnect(); +} + checkExample();`, }, }, @@ -1010,6 +1232,7 @@ checkExample();`, es: "Cobrar (cash) un cheque recibido", en: "Cash (collect) a received check", jp: "受け取ったチェックを換金(cash)する", + zh: "兑现收到的 Check", }, language: "javascript", code: { @@ -1171,35 +1394,91 @@ async function cashCheck(checkID) { } // チェック作成時に取得したCheckIDを使用 +cashCheck("YOUR_CHECK_ID_HERE");`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function cashCheck(checkID) { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + // 接收方兑现支票 + const receiver = Wallet.fromSeed(process.env.CASH_SEED, {algorithm: 'secp256k1'}); + + // 方案 1:兑现准确金额 + const checkCash = { + TransactionType: "CheckCash", + Account: receiver.address, + CheckID: checkID, + Amount: xahToDrops(50), // 精确兑现 50 XAH + }; + + // 方案 2(可选):至少兑现某个最小金额 + // const checkCash = { + // TransactionType: "CheckCash", + // Account: receiver.address, + // CheckID: checkID, + // DeliverMin: xahToDrops(40), // 至少 40 XAH + // }; + + const prepared = await client.autofill(checkCash); + const signed = receiver.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== CheckCash ==="); + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("支票兑现成功!"); + const delivered = result.result.meta.delivered_amount; + if (typeof delivered === "string") { + console.log("收到金额:", Number(delivered) / 1_000_000, "XAH"); + } else { + console.log("收到金额:", delivered.value, delivered.currency); + } + } else if (txResult === "tecNO_ENTRY") { + console.log("未找到该支票。它可能已被取消或已经兑现。"); + } else if (txResult === "tecUNFUNDED") { + console.log("出票方余额不足。"); + } + + await client.disconnect(); +} + +// 使用创建支票时得到的 CheckID cashCheck("YOUR_CHECK_ID_HERE");`, }, }, ], slides: [ { - title: { es: "¿Qué es un Check?", en: "What is a Check?", jp: "チェックとは?" }, + title: { es: "¿Qué es un Check?", en: "What is a Check?", jp: "チェックとは?", zh: "什么是 Check?" }, content: { es: "Similar a un cheque bancario tradicional\n\n• El emisor crea el cheque (CheckCreate)\n• El receptor lo cobra cuando quiera (CheckCash)\n• Los fondos NO se transfieren al crear\n• Soporta XAH nativo e IOUs\n• Puede tener fecha de expiración", en: "Similar to a traditional bank check\n\n• Sender creates the check (CheckCreate)\n• Recipient cashes it whenever (CheckCash)\n• Funds are NOT transferred at creation\n• Supports native XAH and IOUs\n• Can have an expiration date", jp: "従来の銀行小切手に似ています\n\n• 送信者がチェックを作成(CheckCreate)\n• 受取人がいつでも換金(CheckCash)\n• 作成時に資金は転送されない\n• ネイティブXAHとIOUをサポート\n• 有効期限を設定可能", + zh: "类似传统银行支票\n\n• 发送方创建支票(CheckCreate)\n• 接收方在需要时兑现(CheckCash)\n• 创建时不会立刻转移资金\n• 支持原生 XAH 和 IOU\n• 可以设置过期时间", }, visual: "📝", }, { - title: { es: "Ciclo de vida del Check", en: "Check lifecycle", jp: "チェックのライフサイクル" }, + title: { es: "Ciclo de vida del Check", en: "Check lifecycle", jp: "チェックのライフサイクル", zh: "Check 生命周期" }, content: { es: "1. CheckCreate → Emisor crea el cheque\n ↓ (el receptor decide cuándo)\n2. CheckCash → Receptor cobra el cheque\n ó\n2. CheckCancel → Cualquiera lo cancela\n\n• Amount = cobro exacto\n• DeliverMin = cobro mínimo aceptable\n• Cheques expirados se pueden cancelar", en: "1. CheckCreate → Sender creates the check\n ↓ (recipient decides when)\n2. CheckCash → Recipient cashes the check\n or\n2. CheckCancel → Either party cancels it\n\n• Amount = exact amount to cash\n• DeliverMin = minimum acceptable amount\n• Expired checks can be cancelled", jp: "1. CheckCreate → 送信者がチェックを作成\n ↓ (受取人が決めるまで)\n2. CheckCash → 受取人がチェックを換金\n または\n2. CheckCancel → どちらの当事者もキャンセル可能\n\n• Amount = 換金する正確な金額\n• DeliverMin = 最低許容金額\n• 期限切れのチェックはキャンセル可能", + zh: "1. CheckCreate → 发送方创建支票\n ↓(由接收方决定何时兑现)\n2. CheckCash → 接收方兑现支票\n 或\n2. CheckCancel → 任一方取消支票\n\n• Amount = 精确兑现的金额\n• DeliverMin = 可接受的最小金额\n• 过期支票可以被取消", }, visual: "🔄", }, { - title: { es: "Check vs Payment vs Escrow", en: "Check vs Payment vs Escrow", jp: "チェック vs 支払い vs エスクロー" }, + title: { es: "Check vs Payment vs Escrow", en: "Check vs Payment vs Escrow", jp: "チェック vs 支払い vs エスクロー", zh: "Check vs Payment vs Escrow" }, content: { es: "Payment → Transferencia inmediata\n\nEscrow → Fondos bloqueados con condiciones\n• Tiempo, crypto-condición o ambos\n• Fondos realmente bloqueados\n\nCheck → Promesa de pago diferido\n• Receptor decide cuándo cobrar\n• Fondos NO bloqueados (pueden gastarse)\n• Más flexible, menos garantías", en: "Payment → Immediate transfer\n\nEscrow → Funds locked with conditions\n• Time, crypto-condition or both\n• Funds actually locked\n\nCheck → Deferred payment promise\n• Recipient decides when to cash\n• Funds NOT locked (can be spent)\n• More flexible, fewer guarantees", jp: "Payment → 即時転送\n\nEscrow → 条件付きで資金をロック\n• 時間、暗号条件、または両方\n• 資金は実際にロックされる\n\nCheck → 遅延支払いの約束\n• 受取人が換金タイミングを決める\n• 資金はロックされない(使用可能)\n• より柔軟、保証は少ない", + zh: "Payment → 立即转账\n\nEscrow → 带条件的资金锁定\n• 时间条件、加密条件,或两者都有\n• 资金会真实锁住\n\nCheck → 延迟支付承诺\n• 由接收方决定何时兑现\n• 资金不会被锁定(仍可花费)\n• 更灵活,但保障更少", }, visual: "⚖️", }, @@ -1212,6 +1491,7 @@ cashCheck("YOUR_CHECK_ID_HERE");`, en: "Tickets: Out-of-Order Sequences", jp: "チケット:順序外のシーケンス", ko: "Tickets: 순서와 무관한 시퀀스", + zh: "Tickets:无序序列", }, theory: { es: `Un **Ticket** es un mecanismo que permite enviar transacciones **fuera del orden secuencial** normal. Normalmente, cada transacción en Xahau debe usar el siguiente número de \`Sequence\` de la cuenta. Los Tickets eliminan esa restricción reservando números de secuencia por adelantado. @@ -1373,6 +1653,21 @@ Xahauの各アカウントには、トランザクションごとにインクリ - 실제 거래에서는 \`Sequence\` 대신 \`TicketSequence\` 사용 고급 운영 시나리오에서는 일반 시퀀스보다 훨씬 유연한 도구가 됩니다.`, + zh: `**Ticket** 让你可以脱离账户正常的 \`Sequence\` 流程来准备交易,在需要无顺序依赖地准备多笔交易时非常有用。 + +### Ticket 的优势 + +- 可并行准备交易 +- 适合预签名流程 +- 便于分离多签工作 +- 可以提前准备应急备用交易 + +### 核心结构 + +- 用 \`TicketCreate\` 预留 Ticket +- 实际交易中使用 \`TicketSequence\` 代替 \`Sequence\` + +在高级运营场景中,它比普通序列机制灵活得多。`, }, codeBlocks: [ { @@ -1380,6 +1675,7 @@ Xahauの各アカウントには、トランザクションごとにインクリ es: "Crear Tickets y usarlos para encadenar múltiples pagos", en: "Create Tickets and use them to chain multiple payments", jp: "チケットを作成して複数の支払いに使用する", + zh: "创建 Tickets 并用它们串联多笔支付", }, language: "javascript", code: { @@ -1622,35 +1918,118 @@ async function paymentsWithTickets() { await client.disconnect(); } +paymentsWithTickets();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +async function paymentsWithTickets() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const sender = Wallet.fromSeed(process.env.WALLET_SEED, {algorithm: 'secp256k1'}); + + // === 第 1 步:创建 3 个 Ticket === + console.log("=== 第 1 步:创建 Tickets ==="); + const ticketCreate = { + TransactionType: "TicketCreate", + Account: sender.address, + TicketCount: 3, // 预留 3 个 Ticket + }; + + const prepTicket = await client.autofill(ticketCreate); + const signedTicket = sender.sign(prepTicket); + const resultTicket = await client.submitAndWait(signedTicket.tx_blob); + + console.log("TicketCreate:", resultTicket.result.meta.TransactionResult); + + if (resultTicket.result.meta.TransactionResult !== "tesSUCCESS") { + console.log("创建 Ticket 时出错。"); + await client.disconnect(); + return; + } + + // 从已创建节点中提取 TicketSequence + const ticketSequences = resultTicket.result.meta.AffectedNodes + .filter((n) => n.CreatedNode?.LedgerEntryType === "Ticket") + .map((n) => n.CreatedNode.NewFields.TicketSequence) + .sort((a, b) => a - b); + + console.log("已创建 Tickets:", ticketSequences); + + // === 第 2 步:用 Tickets 发送支付(顺序可任意)=== + console.log("=== 第 2 步:使用 Tickets 发送支付 ==="); + + const destinations = [ + { address: "rDestination1XXXXXXXXXXXXXXXXXXXXX", amount: 5, label: "支付 A" }, + { address: "rDestination2XXXXXXXXXXXXXXXXXXXXX", amount: 10, label: "支付 B" }, + { address: "rDestination3XXXXXXXXXXXXXXXXXXXXX", amount: 15, label: "支付 C" }, + ]; + + // 可以按任意顺序发送,甚至并行发送 + // 这里故意倒序发送,以展示灵活性 + for (let i = destinations.length - 1; i >= 0; i--) { + const dest = destinations[i]; + const ticketSeq = ticketSequences[i]; + + const payment = { + TransactionType: "Payment", + Account: sender.address, + Destination: dest.address, + Amount: xahToDrops(dest.amount), + Sequence: 0, // 不使用普通序列 + TicketSequence: ticketSeq, // 使用预留的 Ticket + }; + + const prepared = await client.autofill(payment); + // autofill 可能会覆盖 Sequence,所以这里强制设回去 + prepared.Sequence = 0; + prepared.TicketSequence = ticketSeq; + + const signed = sender.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log(\`\${dest.label} (Ticket \${ticketSeq}): \${txResult} → \${dest.amount} XAH\`); + } + + console.log("所有支付都已通过 Tickets 发送!"); + console.log("已使用的 Ticket 会被销毁,预留金也会释放。"); + + await client.disconnect(); +} + paymentsWithTickets();`, }, }, ], slides: [ { - title: { es: "¿Qué es un Ticket?", en: "What is a Ticket?", jp: "チケットとは?" }, + title: { es: "¿Qué es un Ticket?", en: "What is a Ticket?", jp: "チケットとは?", zh: "什么是 Ticket?" }, content: { es: "Reserva números de secuencia por adelantado\n\n• Permite transacciones fuera de orden\n• Sequence: 0 + TicketSequence: N\n• Se destruye al usarse\n• Máximo 250 por cuenta\n\nCada Ticket consume reserva de propietario", en: "Reserves sequence numbers in advance\n\n• Allows out-of-order transactions\n• Sequence: 0 + TicketSequence: N\n• Destroyed when used\n• Maximum 250 per account\n\nEach Ticket consumes owner reserve", jp: "シーケンス番号を事前に予約\n\n• 順序外のトランザクションを許可\n• Sequence: 0 + TicketSequence: N\n• 使用時に破棄\n• アカウントあたり最大250\n\n各チケットはオーナーリザーブを消費", + zh: "提前预留序列号\n\n• 允许无序交易\n• Sequence: 0 + TicketSequence: N\n• 使用后会被销毁\n• 每个账户最多 250 个\n\n每个 Ticket 都会占用 owner reserve", }, visual: "🎫", }, { - title: { es: "Casos de uso", en: "Use cases", jp: "ユースケース" }, + title: { es: "Casos de uso", en: "Use cases", jp: "ユースケース", zh: "使用场景" }, content: { es: "• Transacciones paralelas sin bloqueo\n• Pre-firmar txs para enviar después\n• Multi-signing independiente\n• Contingencias y respaldos\n\nTicketCreate → Reservar (1-250)\nUsar → Sequence: 0 + TicketSequence\nCancelar → AccountSet vacío con Ticket", en: "• Parallel transactions without blocking\n• Pre-sign txs to send later\n• Independent multi-signing\n• Contingencies and fallbacks\n\nTicketCreate → Reserve (1-250)\nUse → Sequence: 0 + TicketSequence\nCancel → Empty AccountSet with Ticket", jp: "• ブロックなしの並行トランザクション\n• 後で送信するための事前署名tx\n• 独立したマルチサイニング\n• コンティンジェンシーとバックアップ\n\nTicketCreate → 予約(1〜250)\n使用 → Sequence: 0 + TicketSequence\nキャンセル → チケット付き空のAccountSet", + zh: "• 无阻塞的并行交易\n• 预签名后再发送交易\n• 独立进行多签\n• 应急与备用方案\n\nTicketCreate → 预留(1-250)\n使用 → Sequence: 0 + TicketSequence\n取消 → 带 Ticket 的空 AccountSet", }, visual: "🔀", }, { - title: { es: "Tickets vs Secuencia normal", en: "Tickets vs Normal Sequence", jp: "チケット vs 通常のシーケンス" }, + title: { es: "Tickets vs Secuencia normal", en: "Tickets vs Normal Sequence", jp: "チケット vs 通常のシーケンス", zh: "Tickets vs 普通序列" }, content: { es: "Secuencia normal:\n• Estricto orden: 1, 2, 3, 4...\n• Si falla la 2, la 3 se bloquea\n\nCon Tickets:\n• Cualquier orden: 3, 1, 2...\n• Independientes entre sí\n• Cada uno consume owner reserve\n• Se liberan al usarse o cancelarse", en: "Normal sequence:\n• Strict order: 1, 2, 3, 4...\n• If 2 fails, 3 is blocked\n\nWith Tickets:\n• Any order: 3, 1, 2...\n• Independent from each other\n• Each consumes owner reserve\n• Released when used or cancelled", jp: "通常のシーケンス:\n• 厳格な順序:1, 2, 3, 4...\n• 2が失敗すると3はブロックされる\n\nチケット使用時:\n• 任意の順序:3, 1, 2...\n• 互いに独立\n• 各チケットはオーナーリザーブを消費\n• 使用またはキャンセル時に解放", + zh: "普通序列:\n• 必须严格按顺序:1, 2, 3, 4...\n• 如果 2 失败,3 会被卡住\n\n使用 Tickets:\n• 可以任意顺序:3, 1, 2...\n• 彼此独立\n• 每个都会占用 owner reserve\n• 使用或取消后释放", }, visual: "⚖️", }, @@ -1663,6 +2042,7 @@ paymentsWithTickets();`, en: "ClaimReward: Claiming Network Rewards", jp: "ClaimReward:ネットワーク報酬の請求", ko: "ClaimReward: 네트워크 보상 청구", + zh: "ClaimReward:领取网络奖励", }, theory: { es: `Xahau cuenta con un sistema de **recompensas nativas** que distribuye XAH a las cuentas que participan activamente en la red. La transacción \`ClaimReward\` permite reclamar estas recompensas acumuladas. @@ -1782,6 +2162,21 @@ If for any reason you want to stop participating in the rewards system, you can - 중지하려면 특정 플래그로 비활성화 가능 정확한 운영 정책은 네트워크 규칙에 따라 달라질 수 있으므로 항상 최신 문서를 확인하는 것이 좋습니다.`, + zh: `Xahau 拥有一个向网络参与账户分配 XAH 的**原生奖励系统**。\`ClaimReward\` 用来领取累计奖励。 + +### 工作方式 + +- 奖励会根据账户余额持续累积 +- 需要定期发送 \`ClaimReward\` 才能领取 +- 第一次执行也会启用奖励接收 + +### 特点 + +- 不需要质押或委托 +- 奖励直接计入账户余额 +- 如需停止接收,可通过特定标志关闭 + +具体规则可能会随网络政策变化,因此最好始终查看最新文档。`, }, codeBlocks: [ { @@ -1789,6 +2184,7 @@ If for any reason you want to stop participating in the rewards system, you can es: "Reclamar recompensas de la red", en: "Claim network rewards", jp: "ネットワーク報酬の請求", + zh: "领取网络奖励", }, language: "javascript", code: { @@ -1959,26 +2355,84 @@ async function claimReward() { await client.disconnect(); } +claimReward();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function claimReward() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + // 领取前先查询账户信息 + const accountInfo = await client.request({ + command: "account_info", + account: wallet.address, + ledger_index: "validated", + }); + + const balanceBefore = Number(accountInfo.result.account_data.Balance) / 1_000_000; + console.log("=== 领取前状态 ==="); + console.log("账户:", wallet.address); + console.log("当前余额:", balanceBefore, "XAH"); + + // 发送 ClaimReward + // Issuer:网络的 genesis 账户(testnet 和 mainnet 不同) + const claimReward = { + TransactionType: "ClaimReward", + Account: wallet.address, + Issuer: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", // testnet genesis 账户 + }; + + const prepared = await client.autofill(claimReward); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== ClaimReward ==="); + console.log("结果:", txResult); + console.log("Hash:", signed.hash); + + if (txResult === "tesSUCCESS") { + // 领取后再查询余额 + const accountAfter = await client.request({ + command: "account_info", + account: wallet.address, + ledger_index: "validated", + }); + + const balanceAfter = Number(accountAfter.result.account_data.Balance) / 1_000_000; + console.log("=== 领取后状态 ==="); + console.log("新余额:", balanceAfter, "XAH"); + console.log("获得奖励:", (balanceAfter - balanceBefore).toFixed(6), "XAH"); + } + + await client.disconnect(); +} + claimReward();`, }, }, ], slides: [ { - title: { es: "ClaimReward", en: "ClaimReward", jp: "ClaimReward" }, + title: { es: "ClaimReward", en: "ClaimReward", jp: "ClaimReward", zh: "ClaimReward" }, content: { es: "Recompensas nativas de Xahau\n\n• Se acumulan según tu balance de XAH\n• No requiere staking ni nodos\n• ClaimReward para reclamarlas\n• Se suman directamente a tu balance\n\nReclamar periódicamente (diario/semanal)", en: "Native Xahau rewards\n\n• Accumulated based on your XAH balance\n• No staking or nodes required\n• ClaimReward to collect them\n• Added directly to your balance\n\nClaim periodically (daily/weekly)", jp: "Xahauのネイティブ報酬\n\n• XAHバランスに基づいて累積\n• ステーキングもノードも不要\n• ClaimRewardで請求\n• バランスに直接追加\n\n定期的に請求(毎日・毎週)", + zh: "Xahau 原生奖励\n\n• 根据你的 XAH 余额累积\n• 不需要质押或运行节点\n• 用 ClaimReward 领取\n• 直接加入你的余额\n\n建议定期领取(每日或每周)", }, visual: "🎁", }, { - title: { es: "Cómo reclamar", en: "How to claim", jp: "請求方法" }, + title: { es: "Cómo reclamar", en: "How to claim", jp: "請求方法", zh: "如何领取" }, content: { es: "1ª vez → Activa tu cuenta para recompensas\nSiguientes → Reclama lo acumulado\n\nCampos:\n• Account: tu cuenta\n• Issuer: genesis account de la red\n• Flags: 0 (reclamar) / 1 (desactivar)\n\nFee estándar, compatible con Hooks", en: "1st time → Activates your account for rewards\nSubsequent → Claims accumulated amount\n\nFields:\n• Account: your account\n• Issuer: network genesis account\n• Flags: 0 (claim) / 1 (deactivate)\n\nStandard fee, compatible with Hooks", jp: "1回目 → アカウントを報酬システムに有効化\n以降 → 累積分を請求\n\nフィールド:\n• Account: あなたのアカウント\n• Issuer: ネットワークのジェネシスアカウント\n• Flags: 0(請求)/ 1(無効化)\n\n標準fee、Hooksと互換", + zh: "第一次 → 启用你的奖励账户\n之后 → 领取累计金额\n\n字段:\n• Account: 你的账户\n• Issuer: 网络 genesis 账户\n• Flags: 0(领取)/ 1(停用)\n\n手续费为标准费用,兼容 Hooks", }, visual: "💰", }, @@ -1991,6 +2445,7 @@ claimReward();`, en: "Invoke: Activating Hooks on Demand", jp: "Invoke:オンデマンドでのHooksの実行", ko: "Invoke: 필요 시 Hook 활성화", + zh: "Invoke:按需触发 Hook", }, theory: { es: `La transacción \`Invoke\` es un tipo de transacción exclusivo de Xahau que permite **activar un Hook deliberadamente**, sin necesidad de enviar un pago u otra transacción con efecto económico. Es la forma de "llamar" a un Hook de forma directa. @@ -2103,6 +2558,19 @@ Hook은 보통 계정을 통과하는 트랜잭션에 반응하지만, 때로는 - \`Memos\`나 \`HookParameters\`로 데이터 전달 Destination이 없으면 자기 계정 Hook을, 있으면 대상 계정 Hook을 활성화합니다.`, + zh: `**Invoke** 是 Xahau 专有交易,即使没有经济性支付,也能**主动触发** Hook。 + +### 为什么需要它? + +Hook 通常会对经过账户的交易作出反应,但有时我们需要额外的触发器,这时就可以使用 Invoke。 + +### 常见用途 + +- 手动执行维护型 Hook +- 作为唤醒另一个 Hook 的触发器 +- 通过 \`Memos\` 或 \`HookParameters\` 传递数据 + +没有 Destination 时触发自己的 Hook;有 Destination 时触发目标账户上的 Hook。`, }, codeBlocks: [ { @@ -2110,6 +2578,7 @@ Destination이 없으면 자기 계정 Hook을, 있으면 대상 계정 Hook을 es: "Invocar un Hook en otra cuenta", en: "Invoke a Hook on another account", jp: "別のアカウントのHookをInvokeする", + zh: "调用另一账户上的 Hook", }, language: "javascript", code: { @@ -2211,6 +2680,39 @@ async function invokeHook() { await client.disconnect(); } +invokeHook();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function invokeHook() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + // 对安装了 Hook 的另一个账户发送 Invoke + const invoke = { + TransactionType: "Invoke", + Account: wallet.address, + Destination: "rAccountWithHookInstalled", // 要触发其 Hook 的账户 + }; + + const prepared = await client.autofill(invoke); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== Invoke ==="); + console.log("结果:", txResult); + console.log("Hash:", signed.hash); + + if (txResult === "tesSUCCESS") { + console.log("如果目标账户安装了 Hook,请检查它是否已被正确触发。"); + } + + await client.disconnect(); +} + invokeHook();`, }, }, @@ -2218,20 +2720,22 @@ invokeHook();`, ], slides: [ { - title: { es: "Invoke", en: "Invoke", jp: "Invoke" }, + title: { es: "Invoke", en: "Invoke", jp: "Invoke", zh: "Invoke" }, content: { es: "Activar un Hook directamente\n\n• No transfiere fondos\n• Solo es un trigger para el Hook\n• Sin Destination → tus propios Hooks\n• Con Destination → Hooks de otra cuenta\n\nEl Hook debe tener Invoke en su HookOn", en: "Activate a Hook directly\n\n• Does not transfer funds\n• Just a trigger for the Hook\n• No Destination → your own Hooks\n• With Destination → another account's Hooks\n\nThe Hook must have Invoke enabled in HookOn", jp: "Hookを直接実行\n\n• 資金を転送しない\n• Hookのトリガーのみ\n• Destinationなし → 自身のHooks\n• Destinationあり → 他のアカウントのHooks\n\nHookはHookOnでInvokeが有効になっている必要あり", + zh: "直接触发 Hook\n\n• 不会转移资金\n• 只是 Hook 的触发器\n• 无 Destination → 触发自己的 Hooks\n• 有 Destination → 触发他人账户的 Hooks\n\n目标 Hook 需要在 HookOn 中启用 Invoke", }, visual: "📡", }, { - title: { es: "Casos de uso de Invoke", en: "Invoke use cases", jp: "Invokeのユースケース" }, + title: { es: "Casos de uso de Invoke", en: "Invoke use cases", jp: "Invokeのユースケース", zh: "Invoke 的使用场景" }, content: { es: "• Hook emite un Invoke para activar\n otro Hook distinto\n• Trigger manual: activar lógica de un\n Hook cuando lo necesites\n• Pasar datos al Hook via Memos\n o HookParameters en el Invoke\n\nPara scheduling nativo usa CronSet.\nInvoke sigue siendo útil para casos\npersonalizados o Hooks de otras cuentas", en: "• A Hook emits an Invoke to activate\n another Hook\n• Manual trigger: activate a Hook's logic\n whenever you need it\n• Pass data to the Hook via Memos\n or HookParameters in the Invoke\n\nFor native scheduling use CronSet.\nInvoke is still useful for custom cases\nor activating other accounts' Hooks", jp: "• HookがInvokeを発行して\n 別のHookを実行\n• 手動トリガー:必要なときに\n Hookのロジックを実行\n• InvokeのMemosまたは\n HookParametersでHookにデータを渡す\n\nネイティブスケジューリングにはCronSetを使用。\nInvokeはカスタムケースや\n他のアカウントのHooksに引き続き有効", + zh: "• 一个 Hook 发出 Invoke 去触发\n 另一个 Hook\n• 手动触发:在需要时运行某个 Hook\n 的逻辑\n• 通过 Invoke 中的 Memos\n 或 HookParameters 传递数据\n\n原生定时任务建议用 CronSet。\nInvoke 仍然适合自定义场景\n或触发其他账户上的 Hooks", }, visual: "⚡", }, @@ -2244,6 +2748,7 @@ invokeHook();`, en: "SetRemarks: Metadata on Ledger Objects", jp: "SetRemarks:レジャーオブジェクトへのメタデータ", ko: "SetRemarks: 레저 객체 메타데이터", + zh: "SetRemarks:账本对象元数据", }, theory: { es: `La transacción \`SetRemarks\` permite adjuntar **pares clave-valor** a objetos existentes del ledger de Xahau. No es una forma de enviar mensajes ni de registrar datos en transacciones: es un mecanismo para **anotar objetos del ledger** (cuentas, ofertas, escrows, cheques, URITokens, TrustLines...) con metadata que queda asociada al propio objeto. @@ -2496,6 +3001,25 @@ Remarkを作成するときに\`Flags: 1\`(\`tfImmutable\`)を追加する - 외부 시스템과 객체 매핑 Remarks를 설계할 때는 누가 수정 권한을 가지는지와 값 구조를 명확히 정하는 것이 중요합니다.`, + zh: `**SetRemarks** 是把**键值元数据**附加到账本对象本身的交易。它不是普通消息记录,而更像是在对象层面保存注释或附加信息。 + +### 可应用的对象示例 + +- \`AccountRoot\` +- \`Offer\` +- \`Escrow\` +- \`Ticket\` +- \`Check\` +- \`URIToken\` +- \`RippleState\` + +### 什么时候有用? + +- 关联内部标识符 +- 标记运营状态 +- 将外部系统与链上对象建立映射 + +设计 Remarks 时,关键是先明确谁有修改权限,以及值结构如何定义。`, }, codeBlocks: [ { @@ -2503,6 +3027,7 @@ Remarks를 설계할 때는 누가 수정 권한을 가지는지와 값 구조 es: "Añadir y actualizar Remarks en tu cuenta (AccountRoot)", en: "Add and update Remarks on your account (AccountRoot)", jp: "アカウント(AccountRoot)へのRemarksの追加と更新", + zh: "在你的账户上添加和更新 Remarks(AccountRoot)", }, language: "javascript", code: { @@ -2718,6 +3243,77 @@ async function setAccountRemarks() { await client.disconnect(); } +setAccountRemarks();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +// RemarkName 和 RemarkValue 需要用十六进制表示 +function toHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); +} + +async function setAccountRemarks() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + // 获取 AccountRoot 的 ObjectID(account_info 返回中的 "index" 字段) + const info = await client.request({ + command: "account_info", + account: wallet.address, + ledger_index: "validated", + }); + const objectID = info.result.account_data.index; + + console.log("=== 在 AccountRoot 上执行 SetRemarks ==="); + console.log("账户:", wallet.address); + console.log("ObjectID:", objectID); + + const setRemarks = { + TransactionType: "SetRemarks", + Account: wallet.address, + ObjectID: objectID, + Remarks: [ + { + Remark: { + RemarkName: toHex("name"), + RemarkValue: toHex("Learn Xahau Demo"), + }, + }, + { + Remark: { + RemarkName: toHex("web"), + RemarkValue: toHex("https://learnxahau.inftf.org"), + }, + }, + { + // 不可变 Remark:之后不能再修改或删除 + Remark: { + RemarkName: toHex("created"), + RemarkValue: toHex(new Date().toISOString()), + Flags: 1, // tfImmutable + }, + }, + ], + }; + + const prepared = await client.autofill(setRemarks); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("结果:", txResult); + console.log("Hash:", signed.hash); + + if (txResult === "tesSUCCESS") { + console.log("Remarks 已附加到 AccountRoot。"); + console.log("注意:'created' 这条 Remark 是不可变的,之后无法修改。"); + } + + await client.disconnect(); +} + setAccountRemarks();`, }, }, @@ -2726,6 +3322,7 @@ setAccountRemarks();`, es: "Eliminar una Remark (omitir RemarkValue)", en: "Delete a Remark (omit RemarkValue)", jp: "Remarkの削除(RemarkValueを省略)", + zh: "删除一条 Remark(省略 RemarkValue)", }, language: "javascript", code: { @@ -2911,35 +3508,99 @@ async function deleteRemark() { await client.disconnect(); } +deleteRemark();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +function toHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); +} + +async function deleteRemark() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + // 获取 AccountRoot 的 ObjectID + const info = await client.request({ + command: "account_info", + account: wallet.address, + ledger_index: "validated", + }); + const objectID = info.result.account_data.index; + + // 删除 Remark:只传 RemarkName,不传 RemarkValue + const setRemarks = { + TransactionType: "SetRemarks", + Account: wallet.address, + ObjectID: objectID, + Remarks: [ + { + Remark: { + RemarkName: toHex("web"), // 删除名为 "web" 的 Remark + // 没有 RemarkValue -> 会删除这条记录 + }, + }, + { + Remark: { + RemarkName: toHex("name"), // 更新 "name" 的值 + RemarkValue: toHex("Updated account"), + }, + }, + ], + }; + + const prepared = await client.autofill(setRemarks); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== 删除/更新 Remarks ==="); + console.log("结果:", txResult); + + if (txResult === "tesSUCCESS") { + console.log("Remark 'web' 已删除。"); + console.log("Remark 'name' 已更新。"); + } else if (txResult === "tecIMMUTABLE") { + console.log("无法修改:其中一条 Remark 带有 tfImmutable 标志。"); + } + + await client.disconnect(); +} + deleteRemark();`, }, }, ], slides: [ { - title: { es: "SetRemarks", en: "SetRemarks", jp: "SetRemarks" }, + title: { es: "SetRemarks", en: "SetRemarks", jp: "SetRemarks", zh: "SetRemarks" }, content: { es: "Metadata clave-valor en objetos del ledger\n\n• Adjunta Remarks a: AccountRoot, Offer,\n Escrow, Check, URIToken, TrustLine...\n• RemarkName + RemarkValue (en hex)\n• Solo el propietario/emisor puede modificar\n• Máximo 32 Remarks por objeto\n\nNo es un mensaje: es metadata del objeto", en: "Key-value metadata on ledger objects\n\n• Attach Remarks to: AccountRoot, Offer,\n Escrow, Check, URIToken, TrustLine...\n• RemarkName + RemarkValue (in hex)\n• Only the owner/issuer can modify\n• Maximum 32 Remarks per object\n\nNot a message: it is object metadata", jp: "レジャーオブジェクトへのキーと値のメタデータ\n\n• Remarksの添付先:AccountRoot、Offer、\n Escrow、Check、URIToken、TrustLine...\n• RemarkName + RemarkValue(16進数)\n• 所有者/発行者のみ変更可能\n• オブジェクトあたり最大32 Remarks\n\nメッセージではない:オブジェクトのメタデータです", + zh: "账本对象上的键值元数据\n\n• 可附加到:AccountRoot、Offer、\n Escrow、Check、URIToken、TrustLine...\n• RemarkName + RemarkValue(十六进制)\n• 只有所有者/发行者可以修改\n• 每个对象最多 32 条 Remarks\n\n它不是消息,而是对象元数据", }, visual: "🏷️", }, { - title: { es: "Crear, modificar y eliminar", en: "Create, modify and delete", jp: "作成、変更、削除" }, + title: { es: "Crear, modificar y eliminar", en: "Create, modify and delete", jp: "作成、変更、削除", zh: "创建、修改和删除" }, content: { es: "Crear / actualizar:\n → RemarkName + RemarkValue\n\nEliminar:\n → Solo RemarkName, sin RemarkValue\n\nInmutable (tfImmutable = Flags: 1):\n → No se puede modificar ni eliminar nunca\n\nFee extra: 1 drop por byte de nombre + valor", en: "Create / update:\n → RemarkName + RemarkValue\n\nDelete:\n → RemarkName only, no RemarkValue\n\nImmutable (tfImmutable = Flags: 1):\n → Cannot be modified or deleted ever\n\nExtra fee: 1 drop per byte of name + value", jp: "作成 / 更新:\n → RemarkName + RemarkValue\n\n削除:\n → RemarkNameのみ、RemarkValueなし\n\n不変(tfImmutable = Flags: 1):\n → 今後変更・削除不可\n\n追加fee:名前 + 値のバイトあたり1 drop", + zh: "创建 / 更新:\n → RemarkName + RemarkValue\n\n删除:\n → 只传 RemarkName,不传 RemarkValue\n\n不可变(tfImmutable = Flags: 1):\n → 以后都不能修改或删除\n\n额外手续费:名称 + 值每字节 1 drop", }, visual: "✏️", }, { - title: { es: "ObjectID: ¿qué objeto anotar?", en: "ObjectID: which object to annotate?", jp: "ObjectID:どのオブジェクトに注釈するか?" }, + title: { es: "ObjectID: ¿qué objeto anotar?", en: "ObjectID: which object to annotate?", jp: "ObjectID:どのオブジェクトに注釈するか?", zh: "ObjectID:要标注哪个对象?" }, content: { es: "Cada objeto del ledger tiene un ID único:\n\n• AccountRoot → account_data.index\n• Escrow, Check, Offer → LedgerIndex\n de los AffectedNodes al crear el objeto\n\nSetRemarks necesita ese ID para saber\na qué objeto adjuntar la metadata", en: "Each ledger object has a unique ID:\n\n• AccountRoot → account_data.index\n• Escrow, Check, Offer → LedgerIndex\n from AffectedNodes when creating the object\n\nSetRemarks needs that ID to know\nwhich object to attach the metadata to", jp: "各レジャーオブジェクトには一意のIDがあります:\n\n• AccountRoot → account_data.index\n• Escrow、Check、Offer → オブジェクト作成時の\n AffectedNodesのLedgerIndex\n\nSetRemarksはそのIDを使用して\nどのオブジェクトにメタデータを\n添付するかを識別します", + zh: "每个账本对象都有唯一 ID:\n\n• AccountRoot → account_data.index\n• Escrow、Check、Offer → 创建对象时\n AffectedNodes 中的 LedgerIndex\n\nSetRemarks 需要这个 ID,才能知道\n要把元数据附加到哪个对象", }, visual: "🔍", }, @@ -2952,6 +3613,7 @@ deleteRemark();`, en: "Remit: Multi-function Transaction", jp: "Remit:マルチ機能トランザクション", ko: "Remit: 다기능 트랜잭션", + zh: "Remit:多功能交易", }, theory: { es: `La transacción \`Remit\` es una operación exclusiva de Xahau que combina múltiples acciones en una sola transacción. Puede **activar cuentas**, **enviar pagos** (XAH o IOUs) y realizar **operaciones con URITokens** (transferir o mintear), todo de una vez. Además, **paga todos los fees** de activación de cuenta, TrustLines y reservas de URITokens. @@ -3161,6 +3823,24 @@ Remitは各アクションに関連する追加コストを自動的に支払い - \`Blob\` 복잡한 온보딩 흐름이나 다중 자산 전송에 특히 유용합니다.`, + zh: `**Remit** 是 Xahau 专有的多功能交易。它可以把**账户激活、支付、URIToken 转移或铸造**合并成一次操作。 + +### 优点 + +- 以**原子方式**执行多个动作 +- 比发送多笔独立交易更简洁 +- 连同账户激活相关准备金和费用一起处理 + +### 主要字段 + +- \`Amounts\` +- \`URITokenIDs\` +- \`MintURIToken\` +- \`DestinationTag\` +- \`Inform\` +- \`Blob\` + +它尤其适合复杂的 onboarding 流程或多资产转移。`, }, codeBlocks: [ { @@ -3168,6 +3848,7 @@ Remitは各アクションに関連する追加コストを自動的に支払い es: "Remit: pago + minteo de URIToken en una sola transacción", en: "Remit: payment + URIToken minting in a single transaction", jp: "Remit:単一トランザクションでの支払い + URITokenのミント", + zh: "Remit:在单笔交易中完成支付 + URIToken 铸造", }, language: "javascript", code: { @@ -3332,26 +4013,82 @@ async function sendRemit() { await client.disconnect(); } +sendRemit();`, + zh: `require("dotenv").config(); +const { Client, Wallet, xahToDrops } = require("xahau"); + +function stringToHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); +} + +async function sendRemit() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + // Remit:发送 25 XAH,并为目标账户铸造一个 URIToken + const remit = { + TransactionType: "Remit", + Account: wallet.address, + Destination: "rDestinationAddress", + // 发送 25 XAH + Amounts: [ + { + AmountEntry: { + Amount: xahToDrops(25), + }, + }, + ], + // 直接在目标账户中铸造 URIToken + MintURIToken: { + URI: stringToHex("ipfs://bafybeieza5w4rkes55paw7jgpo4kzsbyywhw7ildltk3kjx2ttkmt7texa/106.json"), + Digest: "A".repeat(64), // 内容的 SHA-256 哈希(64 位十六进制字符) + Flags: 1, // tfBurnable:发行者之后可以销毁该代币 + }, + }; + + const prepared = await client.autofill(remit); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("=== Remit ==="); + console.log("结果:", txResult); + console.log("Hash:", signed.hash); + + if (txResult === "tesSUCCESS") { + console.log("在一笔交易中完成:"); + console.log("- 向目标地址发送了 25 XAH"); + console.log("- 直接在目标账户中铸造了 URIToken"); + console.log("- 相关准备金费用已自动支付"); + } + + await client.disconnect(); +} + sendRemit();`, }, }, ], slides: [ { - title: { es: "Remit — Transacción multi-función", en: "Remit — Multi-function Transaction", jp: "Remit — マルチ機能トランザクション" }, + title: { es: "Remit — Transacción multi-función", en: "Remit — Multi-function Transaction", jp: "Remit — マルチ機能トランザクション", zh: "Remit — 多功能交易" }, content: { es: "Una transacción para todo:\n\n• Activar cuentas nuevas\n• Enviar hasta 32 pagos (XAH + IOUs)\n• Transferir hasta 32 URITokens\n• Mintear un URIToken en el destino\n\nTodo atómico: ocurre junto o no ocurre", en: "One transaction for everything:\n\n• Activate new accounts\n• Send up to 32 payments (XAH + IOUs)\n• Transfer up to 32 URITokens\n• Mint a URIToken at the destination\n\nAll atomic: happens together or not at all", jp: "あらゆることを1つのトランザクションで:\n\n• 新しいアカウントを有効化\n• 最大32件の支払いを送信(XAH + IOU)\n• 最大32個のURITokenを転送\n• 宛先でURITokenをミント\n\nすべてアトミック:一緒に行われるかまったく行われないか", + zh: "一笔交易完成所有操作:\n\n• 激活新账户\n• 最多发送 32 笔付款(XAH + IOU)\n• 最多转移 32 个 URIToken\n• 在目标账户中铸造一个 URIToken\n\n全部原子执行:要么一起成功,要么全部不生效", }, visual: "📦", }, { - title: { es: "Remit paga las reservas", en: "Remit pays the reserves", jp: "Remitはリザーブを支払う" }, + title: { es: "Remit paga las reservas", en: "Remit pays the reserves", jp: "Remitはリザーブを支払う", zh: "Remit 支付准备金" }, content: { es: "El emisor cubre todos los costes:\n\n• Activación de cuenta destino\n• Creación de TrustLines necesarias\n• Reservas de URITokens\n• Fee estándar de la transacción\n\nAhorra fees y garantiza atomicidad\nvs múltiples transacciones separadas", en: "The sender covers all costs:\n\n• Destination account activation\n• Creation of required TrustLines\n• URIToken reserves\n• Standard transaction fee\n\nSaves fees and guarantees atomicity\nvs multiple separate transactions", jp: "送信者がすべてのコストをカバー:\n\n• 宛先アカウントの有効化\n• 必要なTrustLineの作成\n• URITokenのリザーブ\n• 標準トランザクションfee\n\n複数の別々のトランザクションと比較して\nfeeを節約しアトミック性を保証", + zh: "发送方承担所有成本:\n\n• 目标账户激活\n• 创建所需 TrustLines\n• URIToken 准备金\n• 标准交易手续费\n\n相比多笔分开的交易,\n它更省手续费,也能保证原子性", }, visual: "💸", }, @@ -3364,6 +4101,7 @@ sendRemit();`, en: "CronSet: Automatic Hook Execution", jp: "CronSet:Hooksの自動実行", ko: "CronSet: 자동 Hook 실행", + zh: "CronSet:自动执行 Hook", }, theory: { es: `La transacción \`CronSet\` permite programar la **ejecución automática y periódica** de un Hook directamente desde el protocolo de Xahau, sin depender de ningún servicio externo. Es el mecanismo nativo de cron jobs de la red. @@ -3632,6 +4370,20 @@ const cronDelete = { - 계정에 \`asfTshCollect\` 활성화 자동 실행 기능은 강력하지만, 오작동 시 반복적으로 실행될 수 있으므로 테스트넷에서 충분히 검증한 뒤 사용하는 것이 좋습니다.`, + zh: `**CronSet** 是 Xahau 的原生调度功能,可以在没有外部服务器的情况下,按周期**自动执行** Hook。 + +### 优点 + +- 完全链上执行 +- 减少对外部机器人或 cron 服务器的依赖 +- 可以设置开始时间、周期和执行次数 + +### 事前准备 + +- 安装带有 \`hsfCOLLECT\` 标志的 Hook +- 在账户上启用 \`asfTshCollect\` + +自动执行功能很强大,但如果逻辑有误也可能反复运行,所以最好先在测试网充分验证。`, }, codeBlocks: [ { @@ -3639,6 +4391,7 @@ const cronDelete = { es: "Activar TSH Collect y programar un CronSet", en: "Enable TSH Collect and schedule a CronSet", jp: "TSH Collectを有効化してCronSetをスケジュールする", + zh: "启用 TSH Collect 并设置 CronSet", }, language: "javascript", code: { @@ -3839,6 +4592,72 @@ async function setupCron() { await client.disconnect(); } +setupCron();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function setupCron() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + console.log("账户:", wallet.address); + + // === 第 1 步:在账户上启用 TSH Collect === + // 这样网络才能自动执行 Hook + console.log("=== 第 1 步:启用 TSH Collect(asfTshCollect) ==="); + + const accountSet = { + TransactionType: "AccountSet", + Account: wallet.address, + SetFlag: 11, // asfTshCollect + }; + + const prepAccountSet = await client.autofill(accountSet); + const signedAccountSet = wallet.sign(prepAccountSet); + const resultAccountSet = await client.submitAndWait(signedAccountSet.tx_blob); + + console.log("AccountSet 结果:", resultAccountSet.result.meta.TransactionResult); + + if (resultAccountSet.result.meta.TransactionResult !== "tesSUCCESS") { + console.log("启用 TSH Collect 时出错。"); + await client.disconnect(); + return; + } + + // === 第 2 步:创建 CronSet === + // 在此之前,Hook 必须已用 hsfCOLLECT 安装好 + console.log("=== 第 2 步:创建 CronSet ==="); + + // Ripple Epoch:自 2000/01/01 00:00:00 UTC 起的秒数 + const RIPPLE_EPOCH_OFFSET = 946684800; + + const cronSet = { + TransactionType: "CronSet", + Account: wallet.address, + StartTime: 0, // 0 = 从下一个有效账本开始 + DelaySeconds: 3600, // 每 1 小时执行一次 + RepeatCount: 24, // 总共执行 24 次(= 24 小时) + }; + + const prepCron = await client.autofill(cronSet); + const signedCron = wallet.sign(prepCron); + const resultCron = await client.submitAndWait(signedCron.tx_blob); + + const txResult = resultCron.result.meta.TransactionResult; + console.log("CronSet 结果:", txResult); + console.log("Hash:", signedCron.hash); + + if (txResult === "tesSUCCESS") { + console.log("CronSet 创建成功!"); + console.log("该 Hook 将在 24 小时内每小时自动执行一次。"); + console.log("请确认 Hook 已使用 hsfCOLLECT 标志安装。"); + } + + await client.disconnect(); +} + setupCron();`, }, }, @@ -3847,6 +4666,7 @@ setupCron();`, es: "Eliminar un CronSet activo", en: "Delete an active CronSet", jp: "アクティブなCronSetを削除する", + zh: "删除一个活动中的 CronSet", }, language: "javascript", code: { @@ -3963,35 +4783,76 @@ async function deleteCron() { await client.disconnect(); } +deleteCron();`, + zh: `require("dotenv").config(); +const { Client, Wallet } = require("xahau"); + +async function deleteCron() { + const client = new Client("wss://xahau-test.net"); + await client.connect(); + + const wallet = Wallet.fromSeed(process.env.WALLET_SEED, { algorithm: "secp256k1" }); + + console.log("=== 删除活动中的 CronSet ==="); + console.log("账户:", wallet.address); + + // 删除 cron:省略所有调度字段 + // 并添加 Flags: 1(tfCronUnset) + const cronDelete = { + TransactionType: "CronSet", + Account: wallet.address, + Flags: 1, // tfCronUnset — 删除当前活动 cron + }; + + const prepared = await client.autofill(cronDelete); + const signed = wallet.sign(prepared); + const result = await client.submitAndWait(signed.tx_blob); + + const txResult = result.result.meta.TransactionResult; + console.log("结果:", txResult); + console.log("Hash:", signed.hash); + + if (txResult === "tesSUCCESS") { + console.log("CronSet 已删除。该 Hook 将不再自动执行。"); + } else { + console.log("此账户没有找到活动中的 CronSet。"); + } + + await client.disconnect(); +} + deleteCron();`, }, }, ], slides: [ { - title: { es: "¿Qué es CronSet?", en: "What is CronSet?", jp: "CronSetとは?" }, + title: { es: "¿Qué es CronSet?", en: "What is CronSet?", jp: "CronSetとは?", zh: "什么是 CronSet?" }, content: { es: "Ejecución periódica de Hooks on-chain\n\n• Sin servicios externos\n• StartTime: cuándo empieza\n• DelaySeconds: cada cuánto\n• RepeatCount: cuántas veces (máx 256)\n\nRequiere Hook con hsfCOLLECT + TSH Collect activo", en: "Periodic on-chain Hook execution\n\n• No external services\n• StartTime: when it starts\n• DelaySeconds: how often\n• RepeatCount: how many times (max 256)\n\nRequires Hook with hsfCOLLECT + TSH Collect enabled", jp: "オンチェーンでのHookの定期実行\n\n• 外部サービス不要\n• StartTime:いつ開始するか\n• DelaySeconds:どのくらいの間隔で\n• RepeatCount:何回(最大256)\n\nhsfCOLLECT付きのHook + TSH Collect有効化が必要", + zh: "链上周期性执行 Hook\n\n• 不需要外部服务\n• StartTime:何时开始\n• DelaySeconds:间隔多久\n• RepeatCount:执行多少次(最多 256)\n\n需要带 hsfCOLLECT 的 Hook,并启用 TSH Collect", }, visual: "⏱️", }, { - title: { es: "Configurar CronSet", en: "Setting up CronSet", jp: "CronSetの設定" }, + title: { es: "Configurar CronSet", en: "Setting up CronSet", jp: "CronSetの設定", zh: "配置 CronSet" }, content: { es: "Pasos:\n1. Instalar Hook con flag hsfCOLLECT\n2. AccountSet SetFlag: 11 (asfTshCollect)\n3. Enviar CronSet con:\n • StartTime: 0 (inmediato) o Ripple Epoch\n • DelaySeconds: intervalo en segundos\n • RepeatCount: nº de ejecuciones\n\nEliminar: CronSet con Flags: 1 (tfCronUnset)", en: "Steps:\n1. Install Hook with hsfCOLLECT flag\n2. AccountSet SetFlag: 11 (asfTshCollect)\n3. Send CronSet with:\n • StartTime: 0 (immediate) or Ripple Epoch\n • DelaySeconds: interval in seconds\n • RepeatCount: number of executions\n\nDelete: CronSet with Flags: 1 (tfCronUnset)", jp: "手順:\n1. hsfCOLLECTフラグ付きでHookをインストール\n2. AccountSet SetFlag: 11(asfTshCollect)\n3. CronSetを送信:\n • StartTime: 0(即時)またはRipple Epoch\n • DelaySeconds: 秒単位の間隔\n • RepeatCount: 実行回数\n\n削除:Flags: 1(tfCronUnset)付きのCronSet", + zh: "步骤:\n1. 安装带 hsfCOLLECT 标志的 Hook\n2. 用 AccountSet 设置 SetFlag: 11(asfTshCollect)\n3. 发送 CronSet,并设置:\n • StartTime: 0(立即)或 Ripple Epoch\n • DelaySeconds: 间隔秒数\n • RepeatCount: 执行次数\n\n删除:发送带 Flags: 1(tfCronUnset)的 CronSet", }, visual: "🔧", }, { - title: { es: "Invoke vs CronSet", en: "Invoke vs CronSet", jp: "Invoke vs CronSet" }, + title: { es: "Invoke vs CronSet", en: "Invoke vs CronSet", jp: "Invoke vs CronSet", zh: "Invoke vs CronSet" }, content: { es: "Invoke periódico:\n• Trigger externo (script, servidor)\n• Flexible, cualquier intervalo\n• Depende de un servicio activo\n\nCronSet:\n• Completamente on-chain\n• Sin infraestructura extra\n• Máx 256 repeticiones por tx\n• Límite: DelaySeconds ≤ 365 días\n\nCronSet = autonomía total del Hook", en: "Periodic Invoke:\n• External trigger (script, server)\n• Flexible, any interval\n• Depends on an active service\n\nCronSet:\n• Fully on-chain\n• No extra infrastructure\n• Max 256 repetitions per tx\n• Limit: DelaySeconds ≤ 365 days\n\nCronSet = full Hook autonomy", jp: "定期的なInvoke:\n• 外部トリガー(スクリプト、サーバー)\n• 柔軟、任意の間隔\n• アクティブなサービスに依存\n\nCronSet:\n• 完全にオンチェーン\n• 追加インフラ不要\n• 1txあたり最大256回の繰り返し\n• 制限:DelaySeconds ≤ 365日\n\nCronSet = Hookの完全な自律性", + zh: "周期性 Invoke:\n• 依赖外部触发器(脚本、服务器)\n• 更灵活,间隔可任意\n• 需要持续运行的服务\n\nCronSet:\n• 完全链上\n• 不需要额外基础设施\n• 每笔交易最多 256 次重复\n• 限制:DelaySeconds ≤ 365 天\n\nCronSet = Hook 的完全自主调度", }, visual: "⚖️", }, diff --git a/src/data/modules/m11-xaman-sdk.js b/src/data/modules/m11-xaman-sdk.js index 103d9d6..7417962 100644 --- a/src/data/modules/m11-xaman-sdk.js +++ b/src/data/modules/m11-xaman-sdk.js @@ -6,6 +6,7 @@ export default { en: "Xaman Integration (XUMM SDK)", jp: "Xaman連携(XUMM SDK)", ko: "Xaman 통합 (XUMM SDK)", + zh: "Xaman 集成(XUMM SDK)", }, lessons: [ { @@ -15,6 +16,7 @@ export default { en: "The Xaman SDK and developer portal", jp: "Xaman SDKと開発者ポータル", ko: "Xaman SDK와 개발자 포털", + zh: "Xaman SDK 与开发者门户", }, theory: { es: `**Xaman** (anteriormente XUMM) no es solo una wallet: es una plataforma de firma de transacciones que expone una **API REST y SDK** para desarrolladores. Gracias a ella puedes crear aplicaciones web o móviles que piden al usuario que firme transacciones en Xahau sin que nunca tengas acceso a sus claves privadas. @@ -188,6 +190,22 @@ Your app Xaman API Xaman (mobile) - Secret은 **백엔드에서만** 사용 이 구조를 이해하면 프론트엔드 로그인과 결제 요청 플로우를 쉽게 설계할 수 있습니다.`, + zh: `**Xaman** 不只是钱包,它还是一个让开发者构建签名流程的 **API 与 SDK 平台**。应用可以在完全不接触用户私钥的情况下安全地请求交易签名。 + +### XUMM SDK 可以做什么 + +- 基于 SignIn 的登录 +- 为任意交易创建 payload +- 提供二维码或深链接 +- 通过 WebSocket 实时接收批准结果 + +### 开始前准备 + +- 在 \`apps.xaman.dev\` 创建应用 +- 获取 \`API Key\` 和 \`API Secret\` +- Secret **只能放在后端** + +理解这个结构后,就能更轻松地设计前端登录和支付请求流程。`, }, codeBlocks: [ { @@ -196,6 +214,7 @@ Your app Xaman API Xaman (mobile) en: "SDK installation and basic setup", jp: "SDKのインストールと基本設定", ko: "SDK 설치 및 기본 설정", + zh: "SDK 安装与基础配置", }, language: "bash", code: { @@ -230,6 +249,14 @@ npm install xumm npm install xumm # 설치된 버전 확인 +npm list xumm`, + zh: `# 安装 Xaman SDK +npm install xumm + +# React/Vite 项目同样需要 +npm install xumm + +# 查看已安装版本 npm list xumm`, }, }, @@ -239,6 +266,7 @@ npm list xumm`, en: "Initialization: frontend vs backend", jp: "初期化:フロントエンドとバックエンド", ko: "초기화: 프론트엔드 vs 백엔드", + zh: "初始化:前端 vs 后端", }, language: "javascript", code: { @@ -314,6 +342,24 @@ const xummBackend = new Xumm("your-api-key-here", "your-api-secret-here"); const appInfo = await xumm.environment.getAppInfo(); console.log("앱 연결됨:", appInfo?.name); console.log("앱 UUID:", appInfo?.uuidv4);`, + zh: `import { Xumm } from "xumm"; + +// ───────────────────────────────────────────── +// 前端(浏览器)— 仅使用 API Key +// API Key 是公开的,并走安全的 PKCE 流程 +// ───────────────────────────────────────────── +const xumm = new Xumm("your-api-key-here"); + +// ───────────────────────────────────────────── +// 后端(Node.js 服务器)— API Key + Secret +// Secret 绝不能出现在浏览器中 +// ───────────────────────────────────────────── +const xummBackend = new Xumm("your-api-key-here", "your-api-secret-here"); + +// 验证连接是否正常 +const appInfo = await xumm.environment.getAppInfo(); +console.log("应用已连接:", appInfo?.name); +console.log("应用 UUID:", appInfo?.uuidv4);`, }, }, ], @@ -324,12 +370,14 @@ console.log("앱 UUID:", appInfo?.uuidv4);`, en: "What is the XUMM SDK?", jp: "XUMM SDKとは?", ko: "XUMM SDK란?", + zh: "什么是 XUMM SDK?", }, content: { es: "SDK oficial para integrar Xaman en tu app\n\n• Autenticar usuarios con SignIn\n• Crear payloads (solicitudes de firma)\n• Mostrar QR — el usuario escanea con Xaman\n• WebSocket: respuesta en tiempo real\n• El usuario firma, tú nunca ves las claves", en: "Official SDK to integrate Xaman in your app\n\n• Authenticate users with SignIn\n• Create payloads (sign requests)\n• Show QR — user scans with Xaman\n• WebSocket: real-time response\n• User signs, you never see private keys", jp: "アプリにXamanを統合するための公式SDK\n\n• SignInによるユーザー認証\n• ペイロード(署名リクエスト)の作成\n• QR表示 — ユーザーがXamanでスキャン\n• WebSocket:リアルタイムレスポンス\n• ユーザーが署名、秘密鍵は見えない", ko: "앱에 Xaman을 통합하는 공식 SDK\n\n• SignIn으로 사용자 인증\n• payload(서명 요청) 생성\n• QR 표시 — 사용자가 Xaman으로 스캔\n• WebSocket: 실시간 응답\n• 사용자가 서명, 개인키는 절대 노출 안 됨", + zh: "将 Xaman 集成到应用中的官方 SDK\n\n• 使用 SignIn 认证用户\n• 创建 payload(签名请求)\n• 显示二维码,用户用 Xaman 扫描\n• WebSocket:实时响应\n• 用户自己签名,你不会看到私钥", }, visual: "🔑", }, @@ -339,12 +387,14 @@ console.log("앱 UUID:", appInfo?.uuidv4);`, en: "Developer portal", jp: "開発者ポータル", ko: "개발자 포털", + zh: "开发者门户", }, content: { es: "apps.xaman.dev — tu centro de control\n\n• Crear app → obtener API Key + Secret\n• Whitelist de dominios permitidos\n• Configurar webhook URL\n• Ver estadísticas y logs de API\n\ndocs.xumm.dev — documentación completa", en: "apps.xaman.dev — your control center\n\n• Create app → get API Key + Secret\n• Whitelist of allowed domains\n• Configure webhook URL\n• View stats and API logs\n\ndocs.xumm.dev — full documentation", jp: "apps.xaman.dev — あなたのコントロールセンター\n\n• アプリ作成 → APIキー+シークレット取得\n• 許可ドメインのホワイトリスト\n• WebhookURL設定\n• 統計とAPIログの確認\n\ndocs.xumm.dev — 完全なドキュメント", ko: "apps.xaman.dev — 제어 센터\n\n• 앱 생성 → API Key + Secret 발급\n• 허용 도메인 화이트리스트\n• Webhook URL 설정\n• 통계 및 API 로그 확인\n\ndocs.xumm.dev — 전체 문서", + zh: "apps.xaman.dev —— 你的控制中心\n\n• 创建应用 → 获取 API Key + Secret\n• 配置允许域名白名单\n• 设置 Webhook URL\n• 查看统计与 API 日志\n\ndocs.xumm.dev —— 完整文档", }, visual: "🖥️", }, @@ -354,12 +404,14 @@ console.log("앱 UUID:", appInfo?.uuidv4);`, en: "API Key vs API Secret", jp: "APIキー対APIシークレット", ko: "API Key vs API Secret", + zh: "API Key vs API Secret", }, content: { es: "Dos credenciales con roles distintos:\n\nAPI Key (pública)\n• Segura en el navegador\n• Flujo PKCE — no necesita Secret\n• Va en el código React/JS del frontend\n\nAPI Secret (privada)\n• SOLO en el servidor (Node.js)\n• NUNCA en el navegador\n• Permisos de escritura completos", en: "Two credentials with different roles:\n\nAPI Key (public)\n• Safe in the browser\n• PKCE flow — no Secret needed\n• Goes in frontend React/JS code\n\nAPI Secret (private)\n• Server ONLY (Node.js)\n• NEVER in the browser\n• Full write permissions", jp: "異なる役割を持つ2つの認証情報:\n\nAPIキー(公開)\n• ブラウザで安全\n• PKCEフロー — シークレット不要\n• フロントエンドのReact/JSコードに記載\n\nAPIシークレット(非公開)\n• サーバーのみ(Node.js)\n• ブラウザには絶対に記載しない\n• 完全な書き込み権限", ko: "역할이 다른 두 가지 자격증명:\n\nAPI Key (공개)\n• 브라우저에서 안전\n• PKCE 흐름 — Secret 불필요\n• 프론트엔드 React/JS 코드에 사용\n\nAPI Secret (비공개)\n• 서버 전용 (Node.js)\n• 절대 브라우저에 노출 금지\n• 전체 쓰기 권한", + zh: "两种职责不同的凭证:\n\nAPI Key(公开)\n• 可安全放在浏览器中\n• 使用 PKCE 流程,不需要 Secret\n• 放在前端 React/JS 代码中\n\nAPI Secret(私密)\n• 只能放在服务器(Node.js)\n• 绝不能暴露到浏览器\n• 拥有完整写权限", }, visual: "🔐", }, @@ -372,6 +424,7 @@ console.log("앱 UUID:", appInfo?.uuidv4);`, en: "Frontend: authentication with Xaman (QR Login)", jp: "フロントエンド:Xamanによる認証(QRログイン)", ko: "프론트엔드: Xaman 인증 (QR 로그인)", + zh: "前端:使用 Xaman 认证(二维码登录)", }, theory: { es: `La primera integración que construirás es el **login con Xaman**: un flujo en el que el usuario escanea un QR con su app Xaman y queda autenticado en tu aplicación web. Es el equivalente a "Conectar con MetaMask" pero para el ecosistema Xahau. @@ -588,6 +641,23 @@ Viteがプロジェクトを自動生成します。変更が必要なファイ - 딥링크 지원 프론트엔드 통합의 첫 단계로 가장 적합한 시나리오입니다.`, + zh: `Xaman 登录是让用户扫描二维码并在应用中签名,从而登录网页应用的方式。它和 Web3 中常见的“连接钱包”流程非常相似。 + +### 流程概览 + +1. 应用创建 SignIn payload +2. Xaman 返回二维码 URL 与 UUID +3. 用户扫描并在手机上确认 +4. 应用接收地址并完成登录 + +### 优点 + +- 无密码 +- 非托管 +- 可通过签名验证身份 +- 对移动端非常友好 + +这是前端集成中最适合入门的场景。`, }, codeBlocks: [ { @@ -596,6 +666,7 @@ Viteがプロジェクトを自動生成します。変更が必要なファイ en: "SDK installation and project basic setup", jp: "SDKのインストールとプロジェクトの基本設定", ko: "SDK 설치 및 프로젝트 기본 설정", + zh: "SDK 安装与项目基础配置", }, language: "bash", code: { @@ -614,6 +685,11 @@ npm run dev`, cd xaman-login npm install xumm xahau # src/App.jsx 수정 후 실행: +npm run dev`, + zh: `npm create vite@latest xaman-login -- --template react +cd xaman-login +npm install xumm xahau +# 修改 src/App.jsx 后执行: npm run dev`, }, }, @@ -623,6 +699,7 @@ npm run dev`, en: "App.jsx — QR modal login", jp: "App.jsx — QRモーダルログイン", ko: "App.jsx — QR 모달 로그인", + zh: "App.jsx —— 二维码弹窗登录", }, language: "javascript", code: { @@ -1106,6 +1183,72 @@ export default function App() { )} ); +}`, + zh: `// src/App.jsx —— 在页面内用二维码弹窗完成 Xaman 登录 +import { useEffect, useState } from "react"; +import { Xumm } from "xumm"; + +const xumm = new Xumm("YOUR_API_KEY_HERE"); + +function QRModal({ title, qrUrl, deepLink, onCancel }) { + return ( +
+

{title}

+ QR Xaman +

在手机中打开 Xaman

+ +
+ ); +} + +export default function App() { + const [account, setAccount] = useState(null); + const [qrUrl, setQrUrl] = useState(null); + const [deepLink, setDeepLink] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + xumm.on("ready", async () => { + const me = await xumm.me; + if (me?.account) setAccount(me.account); + }); + }, []); + + async function connectWithXaman() { + setLoading(true); + setError(null); + try { + const { created, resolved } = await xumm.payload.createAndSubscribe( + { txjson: { TransactionType: "SignIn", NetworkID: 21338 } }, + (event) => { + if (typeof event.data.signed !== "undefined") return event.data; + } + ); + setQrUrl(created.refs.qr_png); + setDeepLink(created.next.always); + const result = await resolved; + setQrUrl(null); + setDeepLink(null); + if (result.signed) { + const payload = await xumm.payload.get(created.uuid); + setAccount(payload.response.account); + } else { + setError("用户拒绝了登录"); + } + } finally { + setLoading(false); + } + } + + return ( +
+

Xaman 登录 —— 二维码弹窗

+ {account ?

已连接:{account}

: } + {error &&

{error}

} + {qrUrl && setQrUrl(null)} />} +
+ ); }`, ko: `// src/App.jsx — 자신의 페이지에서 QR 모달 로그인 // 실행 전: @@ -1235,12 +1378,14 @@ export default function App() { es: "Flujo de login con Xaman", en: "Xaman login flow", jp: "Xamanログインフロー", + zh: "Xaman 登录流程", ko: "Xaman 로그인 흐름", }, content: { es: "Autenticación sin contraseña:\n\n1. Tu app crea payload SignIn\n2. Muestras el QR al usuario\n3. Usuario escanea con Xaman\n4. Usuario toca 'Sign' (sin fee)\n5. WebSocket te entrega la dirección\n6. El usuario está autenticado ✓", en: "Passwordless authentication:\n\n1. Your app creates SignIn payload\n2. You show the QR to the user\n3. User scans with Xaman\n4. User taps 'Sign' (no fee)\n5. WebSocket delivers the address\n6. User is authenticated ✓", jp: "パスワードレス認証:\n\n1. アプリがSignInペイロードを作成\n2. ユーザーにQRを表示\n3. ユーザーがXamanでスキャン\n4. ユーザーが「Sign」をタップ(手数料なし)\n5. WebSocketがアドレスを配信\n6. ユーザーが認証済み ✓", + zh: "无密码认证流程:\n\n1. 应用创建 SignIn payload\n2. 向用户显示二维码\n3. 用户用 Xaman 扫码\n4. 用户点击“Sign”(无手续费)\n5. WebSocket 返回地址\n6. 用户完成认证 ✓", ko: "비밀번호 없는 인증:\n\n1. 앱이 SignIn payload 생성\n2. 사용자에게 QR 표시\n3. 사용자가 Xaman으로 스캔\n4. 사용자가 'Sign' 탭 (수수료 없음)\n5. WebSocket이 주소 전달\n6. 사용자 인증 완료 ✓", }, visual: "📱", @@ -1250,12 +1395,14 @@ export default function App() { es: "Escritorio vs Móvil", en: "Desktop vs Mobile", jp: "デスクトップ対モバイル", + zh: "桌面端 vs 移动端", ko: "데스크톱 vs 모바일", }, content: { es: "El modal maneja escritorio y móvil:\n\nEscritorio\n• El modal muestra la imagen QR (qr_png)\n• El usuario escanea con su app Xaman\n• El modal se cierra al confirmar la firma\n\nMóvil\n• El modal muestra el deep link (next.always)\n• Pulsa el enlace → abre Xaman automáticamente\n• Sin necesidad de escanear", en: "The modal handles desktop and mobile:\n\nDesktop\n• Modal shows the QR image (qr_png)\n• User scans with their Xaman app\n• Modal closes when signature is confirmed\n\nMobile\n• Modal shows the deep link (next.always)\n• Tap the link → Xaman opens automatically\n• No scanning needed", jp: "モーダルがデスクトップとモバイルを処理:\n\nデスクトップ\n• モーダルがQR画像(qr_png)を表示\n• ユーザーがXamanアプリでスキャン\n• 署名確認後にモーダルが閉じる\n\nモバイル\n• モーダルがディープリンク(next.always)を表示\n• リンクをタップ → Xamanが自動で起動\n• スキャン不要", + zh: "弹窗同时处理桌面端和移动端:\n\n桌面端\n• 弹窗显示二维码图片(qr_png)\n• 用户用 Xaman 应用扫码\n• 签名确认后弹窗关闭\n\n移动端\n• 弹窗显示深链(next.always)\n• 点击链接 → 自动打开 Xaman\n• 无需扫码", ko: "모달이 데스크톱과 모바일 모두 처리:\n\n데스크톱\n• 모달이 QR 이미지(qr_png) 표시\n• 사용자가 Xaman 앱으로 스캔\n• 서명 확인 후 모달 닫힘\n\n모바일\n• 모달이 딥링크(next.always) 표시\n• 링크 탭 → Xaman 자동 실행\n• 스캔 불필요", }, visual: "💻", @@ -1265,12 +1412,14 @@ export default function App() { es: "Eventos del SDK", en: "SDK events", jp: "SDKイベント", + zh: "SDK 事件", ko: "SDK 이벤트", }, content: { es: "payload.createAndSubscribe() desde el browser:\n\n1. La origin http://localhost:5173 está en la whitelist\n2. El browser envía Origin header → Xaman valida el CORS\n3. Devuelve created.refs.qr_png → imagen del QR\n4. Muestra el QR dentro del modal de tu página\n5. WebSocket espera → usuario firma → modal se cierra\n\nNo se abre ninguna ventana externa", en: "payload.createAndSubscribe() from the browser:\n\n1. Origin http://localhost:5173 is in the whitelist\n2. Browser sends Origin header → Xaman validates CORS\n3. Returns created.refs.qr_png → QR image\n4. Shows QR inside your page modal\n5. WebSocket waits → user signs → modal closes\n\nNo external window is opened", jp: "ブラウザからのpayload.createAndSubscribe():\n\n1. http://localhost:5173がホワイトリストにある\n2. ブラウザがOriginヘッダーを送信 → XamanがCORSを検証\n3. created.refs.qr_pngを返す → QR画像\n4. ページのモーダル内にQRを表示\n5. WebSocketが待機 → ユーザーが署名 → モーダルが閉じる\n\n外部ウィンドウは開かない", + zh: "浏览器中的 payload.createAndSubscribe():\n\n1. 将 http://localhost:5173 加入白名单\n2. 浏览器发送 Origin header → Xaman 校验 CORS\n3. 返回 created.refs.qr_png → 二维码图片\n4. 在页面弹窗中显示二维码\n5. WebSocket 等待 → 用户签名 → 弹窗关闭\n\n不会打开任何外部窗口", ko: "브라우저에서 payload.createAndSubscribe():\n\n1. http://localhost:5173이 화이트리스트에 있음\n2. 브라우저가 Origin 헤더 전송 → Xaman이 CORS 검증\n3. created.refs.qr_png 반환 → QR 이미지\n4. 페이지 모달 내에 QR 표시\n5. WebSocket 대기 → 사용자 서명 → 모달 닫힘\n\n외부 창은 열리지 않음", }, visual: "📡", @@ -1283,6 +1432,7 @@ export default function App() { es: "Frontend: construir y firmar un Payment con Xaman", en: "Frontend: build and sign a Payment with Xaman", jp: "フロントエンド:XamanでPaymentを構築・署名", + zh: "前端:使用 Xaman 构建并签署 Payment", ko: "프론트엔드: Xaman으로 Payment 생성 및 서명", }, theory: { @@ -1478,6 +1628,23 @@ const payloadResult = await xumm.payload.get(created.uuid); const status = payloadResult.response.dispatched_result; // "tesSUCCESS" またはエラーコード const txid = result.txid; // トランザクションハッシュ \`\`\``, + zh: `**用户登录后**,就可以通过 Xaman 请求实际的 **Payment 交易签名**。网页应用负责构造交易 JSON,用户在手机上做最终确认。 + +### 典型流程 + +1. 输入收款地址与金额 +2. 创建 \`Payment\` payload +3. 显示新的二维码 +4. 用户在 Xaman 中查看并批准 +5. 应用接收 \`txid\` 和结果 + +### 注意点 + +- 金额始终要转换为 **drops** +- 明确设置当前网络 ID +- 批准后要确认交易是否真的写入了账本 + +把登录和支付分开理解,整个实现会清晰很多。`, ko: `사용자가 로그인한 뒤에는 Xaman을 통해 실제 **Payment 트랜잭션 서명**을 요청할 수 있습니다. 웹앱은 트랜잭션 JSON을 만들고, 사용자는 모바일에서 최종 승인을 합니다. ### 전형적인 흐름 @@ -1502,6 +1669,7 @@ const txid = result.txid; // トランザクシ es: "Instalación y configuración básica del proyecto", en: "SDK installation and project basic setup", jp: "SDKのインストールとプロジェクトの基本設定", + zh: "项目安装与基础配置", ko: "SDK 설치 및 프로젝트 기본 설정", }, language: "bash", @@ -1519,6 +1687,12 @@ npm install xumm xahau # After modifying src/App.jsx run: npm run dev`, jp: ``, + zh: `# 如果上一节已经做过,这一步可以跳过 +npm create vite@latest xaman-login -- --template react +cd xaman-login +npm install xumm xahau +# 修改 src/App.jsx 后执行: +npm run dev`, ko: `# 이전 단계에서 이미 했다면 이 부분은 건너뛰어도 됩니다 npm create vite@latest xaman-login -- --template react cd xaman-login @@ -1532,6 +1706,7 @@ npm run dev`, es: "App.jsx — Login con QR modal + Payment con QR modal", en: "App.jsx — QR modal login + QR modal payment", jp: "App.jsx — QRモーダルログイン+QRモーダルPayment", + zh: "App.jsx —— 二维码弹窗登录 + 二维码弹窗支付", ko: "App.jsx — QR 모달 로그인 + QR 모달 Payment", }, language: "javascript", @@ -2173,6 +2348,93 @@ export default function App() { {qrUrl && } ); +}`, + zh: `// src/App.jsx —— 同一页面内完成登录与 Payment 签名 +import { useEffect, useState } from "react"; +import { Xumm } from "xumm"; + +const xumm = new Xumm("YOUR_API_KEY_HERE"); + +function xahToDrops(xah) { + return String(Math.floor(Number(xah) * 1_000_000)); +} + +export default function App() { + const [account, setAccount] = useState(null); + const [destination, setDestination] = useState(""); + const [amount, setAmount] = useState(""); + const [qrUrl, setQrUrl] = useState(null); + const [deepLink, setDeepLink] = useState(null); + const [txid, setTxid] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + xumm.on("ready", async () => { + const me = await xumm.me; + if (me?.account) setAccount(me.account); + }); + }, []); + + async function connectWithXaman() { + const { created, resolved } = await xumm.payload.createAndSubscribe( + { txjson: { TransactionType: "SignIn", NetworkID: 21338 } }, + (event) => { + if (typeof event.data.signed !== "undefined") return event.data; + } + ); + setQrUrl(created.refs.qr_png); + setDeepLink(created.next.always); + const result = await resolved; + setQrUrl(null); + setDeepLink(null); + if (result.signed) { + const payload = await xumm.payload.get(created.uuid); + setAccount(payload.response.account); + } + } + + async function sendPayment(e) { + e.preventDefault(); + setError(null); + const { created, resolved } = await xumm.payload.createAndSubscribe( + { + txjson: { + TransactionType: "Payment", + NetworkID: 21338, + Account: account, + Destination: destination, + Amount: xahToDrops(amount), + }, + }, + (event) => { + if (typeof event.data.signed !== "undefined") return event.data; + } + ); + setQrUrl(created.refs.qr_png); + setDeepLink(created.next.always); + const result = await resolved; + setQrUrl(null); + setDeepLink(null); + if (result.signed) setTxid(result.txid); + else setError("用户拒绝了交易"); + } + + return ( +
+

Xaman Payment —— 二维码弹窗

+ {!account ? : null} + {account && !qrUrl && !txid ? ( +
+ setDestination(e.target.value)} placeholder="收款地址" /> + setAmount(e.target.value)} placeholder="金额(XAH)" /> + +
+ ) : null} + {qrUrl && 打开 Xaman} + {txid &&

TXID: {txid}

} + {error &&

{error}

} +
+ ); }`, jp: `// src/App.jsx — すべて自分のページで:ログインもPaymentもQRモーダル // 実行前に: @@ -2530,12 +2792,14 @@ export default function App() { es: "Flujo de pago con Xaman", en: "Payment flow with Xaman", jp: "Xamanを使った支払いフロー", + zh: "使用 Xaman 的支付流程", ko: "Xaman을 사용한 결제 흐름", }, content: { es: "El usuario firma dos veces:\n\n1er QR — Login (SignIn, sin fee)\n• Identifica al usuario → obttienes su dirección\n\n2do QR — Pago (Payment, con fee)\n• Muestra destino y cantidad\n• Usuario revisa y aprueba\n• Recibes txid de la tx firmada", en: "The user scans twice:\n\n1st QR — Login (SignIn, no fee)\n• Identifies user → you get their address\n\n2nd QR — Payment (with fee)\n• Shows destination and amount\n• User reviews and approves\n• You receive txid of signed tx", jp: "ユーザーは2回スキャン:\n\n1枚目QR — ログイン(SignIn、手数料なし)\n• ユーザーを識別 → アドレスを取得\n\n2枚目QR — 支払い(手数料あり)\n• 宛先と金額を表示\n• ユーザーが確認・承認\n• 署名済みtxのtxidを受信", + zh: "用户需要扫描两次:\n\n第 1 个二维码 —— 登录(SignIn,无手续费)\n• 识别用户 → 获取其地址\n\n第 2 个二维码 —— 支付(Payment,有手续费)\n• 显示目标地址和金额\n• 用户检查并批准\n• 你收到已签名交易的 txid", ko: "사용자가 두 번 스캔:\n\n1번째 QR — 로그인 (SignIn, 수수료 없음)\n• 사용자 식별 → 주소 획득\n\n2번째 QR — 결제 (수수료 있음)\n• 수신 주소와 금액 표시\n• 사용자가 확인 후 승인\n• 서명된 tx의 txid 수신", }, visual: "💸", @@ -2545,12 +2809,14 @@ export default function App() { es: "Drops: la unidad de XAH", en: "Drops: the XAH unit", jp: "Drops:XAHの単位", + zh: "Drops:XAH 的单位", ko: "Drops: XAH 단위", }, content: { es: "Las cantidades se expresan en drops:\n\n1 XAH = 1,000,000 drops\n0.5 XAH = 500,000 drops\n0.000001 XAH = 1 drop (mínimo)\n\nConversión en código:\ndrops = Math.floor(xah * 1_000_000)\nxah = drops / 1_000_000\n\nSiempre usa strings para Amount en el JSON", en: "Amounts are expressed in drops:\n\n1 XAH = 1,000,000 drops\n0.5 XAH = 500,000 drops\n0.000001 XAH = 1 drop (minimum)\n\nConversion in code:\ndrops = Math.floor(xah * 1_000_000)\nxah = drops / 1_000_000\n\nAlways use strings for Amount in JSON", jp: "金額はdropsで表します:\n\n1 XAH = 1,000,000 drops\n0.5 XAH = 500,000 drops\n0.000001 XAH = 1 drop(最小単位)\n\nコードでの変換:\ndrops = Math.floor(xah × 1,000,000)\nxah = drops / 1,000,000\n\nJSONのAmountには常にstringを使用", + zh: "金额以 drops 表示:\n\n1 XAH = 1,000,000 drops\n0.5 XAH = 500,000 drops\n0.000001 XAH = 1 drop(最小单位)\n\n代码中的换算:\ndrops = Math.floor(xah * 1_000_000)\nxah = drops / 1_000_000\n\nJSON 中的 Amount 一律使用字符串", ko: "금액은 drops로 표현:\n\n1 XAH = 1,000,000 drops\n0.5 XAH = 500,000 drops\n0.000001 XAH = 1 drop (최소)\n\n코드에서 변환:\ndrops = Math.floor(xah * 1_000_000)\nxah = drops / 1_000_000\n\nJSON의 Amount에는 항상 string 사용", }, visual: "🔢", @@ -2560,12 +2826,14 @@ export default function App() { es: "createAndSubscribe: el método clave", en: "createAndSubscribe: the key method", jp: "createAndSubscribe:重要なメソッド", + zh: "createAndSubscribe:关键方法", ko: "createAndSubscribe: 핵심 메서드", }, content: { es: "Un solo método para crear + escuchar:\n\nconst { created, resolved } = await\n xumm.payload.createAndSubscribe(\n { txjson: transaccion },\n (event) => {\n if ('signed' in event.data)\n return event.data\n }\n )\n\ncreated.refs.qr_png → URL del QR\nawait resolved → firma o rechazo", en: "One method to create + listen:\n\nconst { created, resolved } = await\n xumm.payload.createAndSubscribe(\n { txjson: transaction },\n (event) => {\n if ('signed' in event.data)\n return event.data\n }\n )\n\ncreated.refs.qr_png → QR URL\nawait resolved → sign or reject", jp: "作成+リッスンを一つのメソッドで:\n\nconst { created, resolved } = await\n xumm.payload.createAndSubscribe(\n { txjson: transaction },\n (event) => {\n if ('signed' in event.data)\n return event.data\n }\n )\n\ncreated.refs.qr_png → QR URL\nawait resolved → 署名または拒否", + zh: "一个方法同时完成创建 + 监听:\n\nconst { created, resolved } = await\n xumm.payload.createAndSubscribe(\n { txjson: transaction },\n (event) => {\n if ('signed' in event.data)\n return event.data\n }\n )\n\ncreated.refs.qr_png → 二维码 URL\nawait resolved → 签名或拒绝", ko: "생성 + 수신을 한 메서드로:\n\nconst { created, resolved } = await\n xumm.payload.createAndSubscribe(\n { txjson: transaction },\n (event) => {\n if ('signed' in event.data)\n return event.data\n }\n )\n\ncreated.refs.qr_png → QR URL\nawait resolved → 서명 또는 거부", }, visual: "🔄", @@ -2578,6 +2846,7 @@ export default function App() { es: "Backend: servidor Node.js con Express y Xaman", en: "Backend: Node.js server with Express and Xaman", jp: "バックエンド:ExpressとXamanを使ったNode.jsサーバー", + zh: "后端:使用 Express 与 Xaman 的 Node.js 服务器", ko: "백엔드: Express와 Xaman을 사용하는 Node.js 서버", }, theory: { @@ -2809,6 +3078,23 @@ xaman-backend/ │ └── payment.js # 支払いルート └── webhook.js # XamanのWebhookハンドラー \`\`\``, + zh: `虽然也可以在浏览器里直接创建 payload,但加入 **后端** 后,在安全性和业务逻辑上会更有优势。 + +### 后端方式的优点 + +- \`API Secret\` 只保存在服务器 +- 可以在付款前加入校验逻辑 +- 可以保存交易记录与审计日志 +- 更方便接收 Webhook 并对接其他系统 + +### 常见结构 + +- 前端调用 \`/payment\` 之类的接口 +- 服务器创建 Xaman payload +- 服务器返回 QR URL 与 UUID +- 通过 Webhook 或轮询确认签名结果 + +在实际项目里,这通常会是默认架构。`, ko: `브라우저에서 직접 payload를 만드는 방법도 가능하지만, **백엔드**를 두면 보안과 비즈니스 로직 측면에서 훨씬 유리합니다. ### 백엔드 방식의 장점 @@ -2833,6 +3119,7 @@ xaman-backend/ es: "Comandos de instalación", en: "Installation commands", jp: "インストールコマンド", + zh: "安装命令", ko: "설치 명령어", }, language: "bash", @@ -2891,6 +3178,24 @@ printf ".env\\nnode_modules/\\n" > .gitignore # 5. Start in development mode (once you have package.json, server.js and public/index.html) npm run dev # Open http://localhost:3001 in the browser`, + zh: `# 1. 创建项目目录 +mkdir xaman-backend +cd xaman-backend + +# 2. 创建静态前端文件目录 +mkdir public + +# 3. 安装依赖 +npm init -y +npm install express xumm dotenv cors +npm install --save-dev nodemon + +# 4. 创建 .gitignore +printf ".env\\nnode_modules/\\n" > .gitignore + +# 5. 开发模式启动(准备好 package.json、server.js 和 public/index.html 之后) +npm run dev +# 在浏览器中打开 http://localhost:3001`, ko: `# 1. 프로젝트 디렉토리 생성 mkdir xaman-backend cd xaman-backend @@ -2916,6 +3221,7 @@ npm run dev es: "package.json — copia y pega este archivo completo", en: "package.json — copy and paste this complete file", jp: "package.json — このファイルをそのままコピー", + zh: "package.json —— 直接复制整个文件", ko: "package.json — 이 파일 전체를 복사하세요", }, language: "json", @@ -2943,6 +3249,7 @@ npm run dev es: ".env — credenciales (nunca subir a git)", en: ".env — credentials (never push to git)", jp: ".env — 認証情報(gitにpushしない)", + zh: ".env —— 凭证(不要提交到 git)", ko: ".env — 자격증명 (git에 절대 올리지 마세요)", }, language: "bash", @@ -2962,6 +3269,12 @@ PORT=3001`, jp: `# Create the .env file in the root of the xaman-backend/ project # Replace the values with those from your app at apps.xaman.dev +XUMM_API_KEY=your-api-key-here +XUMM_API_SECRET=your-api-secret-here +PORT=3001`, + zh: `# 在 xaman-backend/ 项目根目录创建 .env 文件 +# 用 apps.xaman.dev 中你的应用值替换下面内容 + XUMM_API_KEY=your-api-key-here XUMM_API_SECRET=your-api-secret-here PORT=3001`, @@ -2978,6 +3291,7 @@ PORT=3001`, es: "server.js — Servidor Express completo con Xaman", en: "server.js — Full Express server with Xaman", jp: "server.js — XamanとExpressの完全なサーバー", + zh: "server.js —— 完整的 Express + Xaman 服务端", ko: "server.js — Xaman과 Express 완전한 서버", }, language: "javascript", @@ -3275,6 +3589,75 @@ app.post("/webhook/xaman", (req, res) => { app.listen(PORT, () => { console.log(\`Server running at http://localhost:\${PORT}\`); console.log(\`Open in browser: http://localhost:\${PORT}\`); +});`, + zh: `// server.js —— 使用 Express 与 Xaman 的最小后端示例 +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import { Xumm } from "xumm"; + +const app = express(); +const PORT = process.env.PORT || 3001; +const xumm = new Xumm(process.env.XUMM_API_KEY, process.env.XUMM_API_SECRET); + +app.use(cors()); +app.use(express.json()); +app.use(express.static("public")); + +app.post("/api/login", async (_req, res) => { + const payload = await xumm.payload.create({ + txjson: { TransactionType: "SignIn", NetworkID: 21338 }, + }); + res.json({ + uuid: payload.uuid, + qrUrl: payload.refs.qr_png, + deepLink: payload.next.always, + }); +}); + +app.get("/api/login/:uuid", async (req, res) => { + const payload = await xumm.payload.get(req.params.uuid); + res.json({ + signed: payload.meta.signed, + expired: payload.meta.expired, + account: payload.response?.account ?? null, + }); +}); + +app.post("/api/payment", async (req, res) => { + const { origin, destination, amountXAH } = req.body; + const payload = await xumm.payload.create({ + txjson: { + TransactionType: "Payment", + NetworkID: 21338, + Account: origin, + Destination: destination, + Amount: String(Math.floor(Number(amountXAH) * 1_000_000)), + }, + }); + res.json({ + uuid: payload.uuid, + qrUrl: payload.refs.qr_png, + deepLink: payload.next.always, + }); +}); + +app.get("/api/payment/:uuid", async (req, res) => { + const payload = await xumm.payload.get(req.params.uuid); + res.json({ + signed: payload.meta.signed, + expired: payload.meta.expired, + txid: payload.response?.txid ?? null, + }); +}); + +app.post("/webhook/xaman", (req, res) => { + console.log("Webhook received:", JSON.stringify(req.body, null, 2)); + res.sendStatus(200); +}); + +app.listen(PORT, () => { + console.log(\`Server running at http://localhost:\${PORT}\`); });`, jp: `// server.js import "dotenv/config"; @@ -3425,6 +3808,7 @@ app.listen(PORT, () => { es: "public/index.html — Interfaz completa (pégala en xaman-backend/public/)", en: "public/index.html — Full UI (paste into xaman-backend/public/)", jp: "public/index.html — 完全なUI(xaman-backend/public/に貼り付け)", + zh: "public/index.html —— 完整界面(粘贴到 xaman-backend/public/)", ko: "public/index.html — 전체 UI (xaman-backend/public/에 붙여넣기)", }, language: "html", @@ -3806,6 +4190,48 @@ app.listen(PORT, () => { } +`, + zh: ` + + + + + Xaman Backend Demo + + +

💸 Xaman Backend Demo

+

这个页面通过后端创建登录与支付 payload。

+ +
+ +
+
+ +
+ + + +
+

+
+ + + `, jp: ` @@ -4192,6 +4618,7 @@ app.listen(PORT, () => { es: "src/App.jsx — Frontend React que consume el backend", en: "src/App.jsx — React frontend consuming the backend", jp: "src/App.jsx — バックエンドを使用するReactフロントエンド(ステータスポーリング)", + zh: "src/App.jsx —— 使用后端的 React 前端", ko: "src/App.jsx — 백엔드를 사용하는 React 프론트엔드 (상태 폴링)", }, language: "javascript", @@ -4517,6 +4944,75 @@ export default function App() { )} ); +}`, + zh: `// src/App.jsx —— 通过后端创建 payload 的 React 前端 +import { useState } from "react"; + +const API = "http://localhost:3001/api"; + +async function waitForSignature(uuid, route, intervalMs = 2000) { + return new Promise((resolve) => { + const timer = setInterval(async () => { + const res = await fetch(\`\${API}/\${route}/\${uuid}\`); + const data = await res.json(); + if (data.signed || data.expired) { + clearInterval(timer); + resolve(data); + } + }, intervalMs); + }); +} + +export default function App() { + const [account, setAccount] = useState(null); + const [qrUrl, setQrUrl] = useState(null); + const [deepLink, setDeepLink] = useState(null); + const [destination, setDestination] = useState(""); + const [amount, setAmount] = useState(""); + const [txid, setTxid] = useState(null); + + async function handleLogin() { + const res = await fetch(\`\${API}/login\`, { method: "POST" }); + const data = await res.json(); + setQrUrl(data.qrUrl); + setDeepLink(data.deepLink); + const result = await waitForSignature(data.uuid, "login"); + setQrUrl(null); + setDeepLink(null); + if (result.signed) setAccount(result.account); + } + + async function handlePayment(e) { + e.preventDefault(); + const res = await fetch(\`\${API}/payment\`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ origin: account, destination, amountXAH: Number(amount) }), + }); + const data = await res.json(); + setQrUrl(data.qrUrl); + setDeepLink(data.deepLink); + const result = await waitForSignature(data.uuid, "payment"); + setQrUrl(null); + setDeepLink(null); + if (result.signed) setTxid(result.txid); + } + + return ( +
+

💸 Xahau Payment (Backend)

+ {!account ? :

已连接:{account}

} + {qrUrl ? 在 Xaman 中打开 : null} + {account && !txid ? ( +
+ setDestination(e.target.value)} placeholder="收款地址" /> + setAmount(e.target.value)} placeholder="金额(XAH)" /> + +
+ ) : null} + {txid ?

交易已发送:{txid}

: null} +
+ ); }`, jp: `// src/App.jsx — バックエンドを使用してペイロードを作成するフロントエンド import { useState } from "react"; @@ -4849,12 +5345,14 @@ export default function App() { es: "Frontend vs Backend: cuándo usar cada uno", en: "Frontend vs Backend: when to use each", jp: "フロントエンド対バックエンド:使い分け", + zh: "Frontend vs Backend:什么时候用哪一种", ko: "프론트엔드 vs 백엔드: 언제 무엇을 사용할지", }, content: { es: "Frontend (solo API Key)\n• Apps simples, demos, prototipos\n• Sin lógica de negocio compleja\n• El SDK crea los payloads en el navegador\n\nBackend (API Key + Secret)\n• Aplicaciones de producción\n• Validación y auditoría del servidor\n• Webhooks para notificaciones\n• Integración con base de datos", en: "Frontend (API Key only)\n• Simple apps, demos, prototypes\n• No complex business logic\n• SDK creates payloads in browser\n\nBackend (API Key + Secret)\n• Production applications\n• Server-side validation and audit\n• Webhooks for notifications\n• Database integration", jp: "フロントエンド(APIキーのみ)\n• シンプルなアプリ、デモ、プロトタイプ\n• 複雑なビジネスロジックなし\n• SDKがブラウザでペイロードを作成\n\nバックエンド(APIキー+シークレット)\n• 本番アプリケーション\n• サーバーサイドの検証と監査\n• 通知用Webhook\n• データベース連携", + zh: "Frontend(仅 API Key)\n• 简单应用、演示、原型\n• 没有复杂业务逻辑\n• SDK 在浏览器中创建 payload\n\nBackend(API Key + Secret)\n• 生产环境应用\n• 服务器侧校验与审计\n• 用 Webhook 接收通知\n• 便于集成数据库", ko: "프론트엔드 (API Key만)\n• 간단한 앱, 데모, 프로토타입\n• 복잡한 비즈니스 로직 없음\n• SDK가 브라우저에서 payload 생성\n\n백엔드 (API Key + Secret)\n• 프로덕션 애플리케이션\n• 서버 측 검증 및 감사\n• 알림용 Webhook\n• 데이터베이스 연동", }, visual: "⚖️", @@ -4864,12 +5362,14 @@ export default function App() { es: "Arquitectura: frontend + backend + Xaman", en: "Architecture: frontend + backend + Xaman", jp: "アーキテクチャ:フロントエンド+バックエンド+Xaman", + zh: "架构:前端 + 后端 + Xaman", ko: "아키텍처: 프론트엔드 + 백엔드 + Xaman", }, content: { es: "Flujo de datos completo:\n\n1. React → POST /api/pago → Express\n2. Express → crear payload → Xaman API\n3. Xaman API → uuid + QR → Express\n4. Express → qrUrl → React\n5. React muestra QR al usuario\n6. Usuario firma en Xaman app\n7. Xaman → webhook → Express\n8. Express guarda txid en BD", en: "Complete data flow:\n\n1. React → POST /api/pago → Express\n2. Express → create payload → Xaman API\n3. Xaman API → uuid + QR → Express\n4. Express → qrUrl → React\n5. React shows QR to user\n6. User signs in Xaman app\n7. Xaman → webhook → Express\n8. Express saves txid to DB", jp: "完全なデータフロー:\n\n1. React → POST /api/pago → Express\n2. Express → ペイロード作成 → Xaman API\n3. Xaman API → uuid + QR → Express\n4. Express → qrUrl → React\n5. ReactがユーザーにQRを表示\n6. ユーザーがXamanアプリで署名\n7. Xaman → webhook → Express\n8. ExpressがtxidをDBに保存", + zh: "完整的数据流:\n\n1. React → POST /api/payment → Express\n2. Express → 创建 payload → Xaman API\n3. Xaman API → uuid + QR → Express\n4. Express → qrUrl → React\n5. React 向用户显示二维码\n6. 用户在 Xaman 应用中签名\n7. Xaman → webhook → Express\n8. Express 将 txid 保存到数据库", ko: "완전한 데이터 흐름:\n\n1. React → POST /api/payment → Express\n2. Express → payload 생성 → Xaman API\n3. Xaman API → uuid + QR → Express\n4. Express → qrUrl → React\n5. React가 사용자에게 QR 표시\n6. 사용자가 Xaman 앱에서 서명\n7. Xaman → webhook → Express\n8. Express가 txid를 DB에 저장", }, visual: "🏗️", @@ -4879,12 +5379,14 @@ export default function App() { es: "Webhooks: recibir la firma en el servidor", en: "Webhooks: receive the signature on the server", jp: "Webhook:サーバーで署名を受信", + zh: "Webhook:在服务器端接收签名", ko: "Webhook: 서버에서 서명 수신", }, content: { es: "Configura tu webhook en apps.xaman.dev\n\nXaman llama a tu endpoint cuando:\n• El usuario firma el payload ✅\n• El usuario rechaza el payload ❌\n• El payload expira ⏰\n\nTu servidor debe responder 200 rápido\nProcesa la lógica de forma asíncrona\nUsa ngrok para probar en local", en: "Configure your webhook at apps.xaman.dev\n\nXaman calls your endpoint when:\n• User signs the payload ✅\n• User rejects the payload ❌\n• Payload expires ⏰\n\nYour server must respond 200 quickly\nProcess logic asynchronously\nUse ngrok to test locally", jp: "apps.xaman.devでWebhookを設定\n\nXamanがエンドポイントを呼び出す時:\n• ユーザーがペイロードに署名 ✅\n• ユーザーがペイロードを拒否 ❌\n• ペイロードが期限切れ ⏰\n\nサーバーは素早く200で応答する必要あり\nロジックは非同期で処理\nローカルテストにはngrokを使用", + zh: "在 apps.xaman.dev 中配置你的 webhook\n\nXaman 会在以下情况调用你的端点:\n• 用户签署 payload ✅\n• 用户拒绝 payload ❌\n• payload 过期 ⏰\n\n服务器应尽快返回 200\n业务逻辑异步处理\n本地测试可使用 ngrok", ko: "apps.xaman.dev에서 webhook 설정\n\nXaman이 엔드포인트를 호출하는 경우:\n• 사용자가 payload에 서명 ✅\n• 사용자가 payload를 거부 ❌\n• Payload가 만료 ⏰\n\n서버는 빠르게 200으로 응답해야 함\n로직은 비동기로 처리\n로컬 테스트에는 ngrok 사용", }, visual: "🔔",