mirror of
https://github.com/INFTF/xahau-course.git
synced 2026-07-29 01:50:51 +00:00
CH part 2
This commit is contained in:
@@ -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: "🔎",
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: "⚠️",
|
||||
},
|
||||
|
||||
@@ -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: "🔐",
|
||||
},
|
||||
|
||||
@@ -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: "📋",
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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 (
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
<img src={qrUrl} alt="QR Xaman" width={220} />
|
||||
<p><a href={deepLink}>在手机中打开 Xaman</a></p>
|
||||
<button onClick={onCancel}>取消</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<h1>Xaman 登录 —— 二维码弹窗</h1>
|
||||
{account ? <p>已连接:{account}</p> : <button onClick={connectWithXaman} disabled={loading}>{loading ? "正在生成二维码..." : "连接 Xaman"}</button>}
|
||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||
{qrUrl && <QRModal title="使用 Xaman 登录" qrUrl={qrUrl} deepLink={deepLink} onCancel={() => setQrUrl(null)} />}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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 && <QRModal title={qrTitle} qrUrl={qrUrl} deepLink={deepLink} onCancel={cancel} />}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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 (
|
||||
<div>
|
||||
<h1>Xaman Payment —— 二维码弹窗</h1>
|
||||
{!account ? <button onClick={connectWithXaman}>连接 Xaman</button> : null}
|
||||
{account && !qrUrl && !txid ? (
|
||||
<form onSubmit={sendPayment}>
|
||||
<input value={destination} onChange={(e) => setDestination(e.target.value)} placeholder="收款地址" />
|
||||
<input value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="金额(XAH)" />
|
||||
<button type="submit">发送 Payment</button>
|
||||
</form>
|
||||
) : null}
|
||||
{qrUrl && <a href={deepLink}>打开 Xaman</a>}
|
||||
{txid && <p>TXID: {txid}</p>}
|
||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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, () => {
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
zh: `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Xaman Backend Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>💸 Xaman Backend Demo</h1>
|
||||
<p>这个页面通过后端创建登录与支付 payload。</p>
|
||||
|
||||
<section>
|
||||
<button id="btnLogin">连接 Xaman</button>
|
||||
<div id="loginQR"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<input id="inputDestination" placeholder="收款地址" />
|
||||
<input id="inputAmount" placeholder="金额(XAH)" />
|
||||
<button id="btnPayment">发送 Payment</button>
|
||||
<div id="paymentQR"></div>
|
||||
<p id="result"></p>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const API = "/api";
|
||||
|
||||
async function waitForStatus(uuid, route) {
|
||||
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);
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
jp: `<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
@@ -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() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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 (
|
||||
<div>
|
||||
<h1>💸 Xahau Payment (Backend)</h1>
|
||||
{!account ? <button onClick={handleLogin}>连接 Xaman</button> : <p>已连接:{account}</p>}
|
||||
{qrUrl ? <a href={deepLink}>在 Xaman 中打开</a> : null}
|
||||
{account && !txid ? (
|
||||
<form onSubmit={handlePayment}>
|
||||
<input value={destination} onChange={(e) => setDestination(e.target.value)} placeholder="收款地址" />
|
||||
<input value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="金额(XAH)" />
|
||||
<button type="submit">发送</button>
|
||||
</form>
|
||||
) : null}
|
||||
{txid ? <p>交易已发送:{txid}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
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: "🔔",
|
||||
|
||||
Reference in New Issue
Block a user