钱包与交易所

本页面向集成 Velum 的开发者:钱包、交易所、支付网关和各类应用。Velum 与 EVM 不兼容:它有自己的地址格式、交易格式和签名方式。所需的一切都在这里。

技术概要

参数 值
链 {{chain}}(每笔交易的 chain 字段)
原生货币 VEL,6 位小数(1 VEL = 1,000,000 µVEL)
金额 以代币最小单位表示的整数
出块时间 目标 2 秒(实际节奏在浏览器中测量)· 纪元:1,800 个区块(约 1 小时)30 个区块(约 60 秒)
曲线与签名 对规范 JSON 做 ed25519 签名
地址 vel + base58(32 字节 ed25519 公钥)
钱包派生 BIP-39(24 个单词)+ 路径 m/44'/7420'/账户'/0'/索引' 上的 SLIP-10 ed25519
转账手续费 价值低于 1,000 欧元为 0(附工作量证明);1,000 欧元起为 0.01%
价格 自有预言机:{{pair}} 的 1 小时 TWAP
交易顺序 每个钱包一个 nonce,从 0 开始
交易 ID SHA-256(含 sig 的规范 JSON)的前 48 个十六进制字符
建议确认数 6(支付),30(交易所充值)
节点 基于 WebSocket 的 JSON RPC
浏览器 /api 下的只读 REST API

地址 {#direcciones}

地址由前缀 vel 加上 ed25519 公钥(32 字节)的 base58 编码(比特币字母表)组成,长度为 46 到 47 个字符。

import { ed25519 } from "@noble/curves/ed25519.js";
import { base58 } from "@scure/base";

// A Velum address is "vel" + base58(32-byte ed25519 public key). No checksum byte:
// validate by decoding and checking the length is exactly 32.
const address = (pub: Uint8Array) => "vel" + base58.encode(pub);
const isAddress = (a: string) =>
  a.startsWith("vel") && !a.startsWith("vel_") && (() => {
    try { return base58.decode(a.slice(3)).length === 32; } catch { return false; }
  })();
  • 没有校验和。 务必通过解码并确认恰好得到 32 字节来验证。对于手动输入的地址,建议复制粘贴或扫描二维码。
  • 以 vel_ 开头的是没有私钥的特殊账户(vel_pool、vel_reserve、vel_escrow)。不要接受向这些地址的提现。
  • 用户可以分享别名(@lucia)而不是地址。务必在链上解析别名,并在发送前显示解析出的地址。

密钥派生 {#claves}

一组 24 个单词的 BIP-39 助记词通过 SLIP-10 ed25519 生成钱包,所有层级均为强化派生:

项目 值
路径 m/44'/7420'/账户'/0'/索引'
官方应用的钱包 m/44'/7420'/0'/0'/0'
SLIP-44 币种编号 7420(待注册)

即时通讯的身份密钥和信箱密钥通过 HKDF-SHA512(velum/v2/identity、velum/v2/mailbox)从同一个种子派生,不是钱包密钥:交易所或钱包都不需要它们。

import { mnemonicToSeedSync } from "@scure/bip39";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha512 } from "@noble/hashes/sha2.js";
import { walletFromPhrase, walletPath } from "@velum/sdk";

// Wallet keys: SLIP-10 ed25519, every level hardened: m/44'/7420'/account'/0'/index'
const hot = walletFromPhrase(phrase, 0, 0);       // m/44'/7420'/0'/0'/0' (the official app's wallet)
const user42 = walletFromPhrase(phrase, 0, 42);   // one deposit address per customer
walletPath(0, 42);                                // "m/44'/7420'/0'/0'/42'"

// Messaging keys are NOT wallet keys: they come from the same seed with HKDF-SHA512.
const seed = mnemonicToSeedSync(phrase);
const identity = hkdf(sha512, seed, undefined, new TextEncoder().encode("velum/v2/identity"), 32);

使用 SDK:

import { walletFromPhrase, isAddress } from "@velum/sdk";

const { address, secret } = walletFromPhrase(phrase, 0, 0);
isAddress(address);                                // true: "vel" + base58(32-byte ed25519 public key)

SDK @velum/sdk、硬件钱包(Trezor、Ledger)和 MetaMask Snap 使用的都是这条路径。参见生态。

交易 {#transacciones}

所有交易都包含以下字段:

字段 类型 含义
type string 交易类型
chain string {{chain}}。不匹配则拒绝。
from 地址 签名的钱包
nonce 整数 from 之前的交易数(0, 1, 2…)
ts 整数(毫秒) 客户端时间。仅供参考:链使用区块时间。
sig hex(128) 钱包的 ed25519 签名

签名与规范 JSON

// Canonical JSON: keys sorted, no whitespace, undefined fields dropped. This is what gets signed and hashed.
export function canonical(v: unknown): string {
  if (v === null || typeof v !== "object") return JSON.stringify(v);
  if (Array.isArray(v)) return "[" + v.map(canonical).join(",") + "]";
  const o = v as Record<string, unknown>;
  return "{" + Object.keys(o).filter((k) => o[k] !== undefined).sort()
    .map((k) => JSON.stringify(k) + ":" + canonical(o[k])).join(",") + "}";
}

// sig = hex(ed25519.sign(utf8(canonical(tx without "sig")), walletSecret))
// txId = hex(sha256(utf8(canonical(tx with "sig")))).slice(0, 48)
  1. 构造不含 sig 的对象。省略不用的可选字段(不要设为 null)。
  2. 序列化为规范 JSON:键排序,无空白。
  3. 用钱包密钥对 UTF-8 字节签名。sig = 十六进制签名。
  4. ID 为 sha256(含 sig 的规范 JSON) 的十六进制,截取前 48 个字符。

转账(transfer)

{
  "type": "transfer",
  "chain": "{{chain}}",
  "from": "vel8Kx3vQ…",
  "to": "vel4Jt9mR…",
  "amount": 12500000,
  "fee": 0,
  "token": "tEUR",
  "memo": "inv-20481",
  "nonce": 17,
  "ts": 1793577600000,
  "sig": "9f2c…(128 hex)"
}
  • amount 以代币最小单位计(没有 token 时为 µVEL),必须大于 0。
  • fee 遵循 v3 规则:按网络预言机价格计算,转账价值低于 1,000 欧元时为 0;否则为金额的 0.01%。fee 为 0 的转账附带一个工作量印章 pow(SHA-512,在手机上约 50 毫秒),它属于签名内容;@velum/sdk 的 buildTransfer 会计算它。确切格式见 PROTOCOL.md 和 spec/vectors/。
  • token 可选:代币符号。不填则转 VEL。
  • memo 可选,最多 128 个字符。它是公开的:只用于付款参考号,切勿写入个人数据。

节点注册(register)

{
  "type": "register", "chain": "{{chain}}", "from": "vel…node", "nonce": 0, "ts": 1793577600000,
  "name": "atico-madrid", "payout": "vel…wallet", "version": "2.0.0",
  "kind": "full", "endpoint": "wss://node.example.org:7420/ws",
  "sig": "…"
}

免费。kind 为 full(电脑,参与出块)或 light(手机)。payout 是收款钱包。

代币(token.create、token.mint、pool.fund)

{ "type": "token.create", "chain": "{{chain}}", "from": "vel…", "nonce": 3, "ts": 1793577600000,
  "symbol": "CAFE", "name": "Café de barrio", "decimals": 2,
  "maxSupply": 100000000, "initial": 2500000, "mintable": true, "fee": 100000000, "sig": "…" }

{ "type": "token.mint", "chain": "{{chain}}", "from": "vel…issuer", "nonce": 4, "ts": 1793577660000,
  "symbol": "CAFE", "to": "vel…", "amount": 50000, "fee": 0, "sig": "…" }

{ "type": "pool.fund", "chain": "{{chain}}", "from": "vel…issuer", "nonce": 5, "ts": 1793577720000,
  "token": "CAFE", "amount": 1440000, "epochs": 1440, "fee": 0, "sig": "…" }

各字段规则见自定义代币。

市场(order.place、order.cancel)

{ "type": "order.place", "chain": "{{chain}}", "from": "vel…", "nonce": 9, "ts": 1793577600000,
  "pair": "VEL/tEUR", "side": "buy", "price": 12500, "amount": 250000000, "ttl": 1440, "sig": "…" }

{ "type": "order.cancel", "chain": "{{chain}}", "from": "vel…", "nonce": 10, "ts": 1793577660000,
  "id": "4c1f0a…", "sig": "…" }

price 为每 1 VEL 对应的代币最小单位数;amount 以 µVEL 计(最低 1 VEL)。见交易市场。

设备认证(device.attest)

{ "type": "device.attest", "chain": "{{chain}}", "from": "vel…", "nonce": 0, "ts": 1793577600000,
  "device": "e3b0c442…(64 hex = sha256 of the device public key)",
  "keyId": "base64…", "attestation": "base64 CBOR…", "sig": "…" }

由 iOS 应用发送,用于登记它是运行在真实 iPhone 上的官方应用。集成方无需发送。

Nonce

  • 钱包的第一个 nonce 是 0;每被接受一笔交易就加 1。
  • 下一个 nonce 用 account.get → nonce + pending(pending = 该钱包在节点内存池中等待的交易数)。
  • 使用已用过的 nonce 的交易会被拒绝。重新广播同一笔已签名交易是安全的:不会被扣两次。

常见错误

错误 原因
Cadena incorrecta chain 不是 {{chain}}
Firma no válida 签名的 JSON 不是规范格式,或包含了 sig
Comisión incorrecta fee 与 1,000 欧元规则及预言机价格不符
Nonce no válido 为负数、非整数或重复
Nota demasiado larga memo 超过 128 个字符

最终性与确认数 {#finalidad}

目前最终性是概率性的(见共识)。重组都很短,通常只涉及一两个区块。

场景 确认数 时间
聊天、打赏 1 一个区块
商户、支付网关 6 几秒钟
交易所充值 30 按目标节奏约 1 分钟
大额充值 一个纪元 约 1 小时约 1 分钟

当一笔交易所在区块的高度为 h、链头位于 h + n − 1 时,这笔交易有 n 个确认。

节点 RPC(WebSocket) {#rpc}

应用和服务通过 WebSocket 上的 JSON 帧与节点通信:

  • 经由公共网关:wss://velum.consultorabim.com/hub?role=client(备用:wss://velum.consultora-bim.workers.dev/hub)。
  • 直接连接节点:ws://<host>:7420/ws?role=client。对交易所而言,建议运行自己的节点。
// Client → gateway (wss://velum.consultorabim.com/hub?role=client) or a node (ws://host:7420/ws?role=client)
type ClientToHub =
  | { t: "rpc"; node?: string; id: string; method: RpcMethod; params: any }
  | { t: "ping" };

type HubToClient =
  | { t: "welcome"; client: string; nodes: PeerInfo[] }
  | { t: "nodes"; nodes: PeerInfo[] }
  | { t: "rpc_res"; id: string; result?: any; error?: string; node?: string }
  | { t: "push"; node: string; event: "shard" | "head"; data: any }
  | { t: "pong" };

// Read-only live feed for explorers (wss://…/hub?role=watch)
type HubToWatch =
  | { t: "snapshot"; head: { height: number; hash: string } | null; nodes: PeerInfo[]; stats: Record<string, number> }
  | { t: "block"; block: Block }
  | { t: "nodes"; nodes: PeerInfo[] }
  | { t: "flow"; kind: "shard" | "receipt" | "tx"; id: string; from?: string; to?: string; ts: number };

经由网关时,node 可选:不填的话,网关会选择一个节点,并在响应中告诉你是哪个。

方法

方法 参数 结果
chain.head {} { height, hash, ts, node }
chain.block { height? , hash? } Block 或 null
chain.blocks { from, to } Block[]
account.get { address } { balance, nonce, pending }(VEL 以 µVEL 计)
account.history { address, limit? } [{ height, ts, tx }]
tx.submit { tx } { id }
| `faucet` | `{ address }` | `{ id, amount }`(仅限测试网) |

| shard.put | { shard } | { ok, carrier } | | mailbox.open | { mailboxes: MailboxOpen[] } | { shards, courier },之后推送 push 事件 | | receipt.submit | { receipts } | { accepted } | | node.info | {} | 节点信息 | | node.list | {} | 已知节点 | | stats | {} | 节点计数器 |

不使用 SDK 的示例:

const ws = new WebSocket("wss://velum.consultorabim.com/hub?role=client");
let seq = 0;
const pending = new Map<string, (r: any) => void>();
const rpc = (method: string, params: object) =>
  new Promise((resolve, reject) => {
    const id = String(++seq);
    pending.set(id, (m) => (m.error ? reject(new Error(m.error)) : resolve(m.result)));
    ws.send(JSON.stringify({ t: "rpc", id, method, params }));
  });
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);
  if (m.t === "rpc_res") pending.get(m.id)?.(m), pending.delete(m.id);
};
ws.onopen = async () => {
  const head = await rpc("chain.head", {});               // { height, hash, ts, node }
  const acc = await rpc("account.get", { address: "vel…" }); // { balance, nonce, pending }
};

浏览器 REST API {#rest}

浏览器在 https://velum.consultorabim.com/api 提供只读 API,开放 CORS。响应为 JSON;错误为 { "error": "…" } 并带相应的 HTTP 状态码。

路径 返回
GET /api/stats 高度、哈希、发行量、网络储备、供应量、节点、等级与参数
GET /api/oracle 网络价格:{ eur, usd, usdt, ts, source }(没有价格时为 null)
GET /api/blocks?limit=&before= 规范链区块,从新到旧(最多 100 个)
GET /api/block/:高度或哈希 区块详情:转账、积分、奖励
GET /api/block/:id?raw=1 完整的已签名区块(最近 5,000 个)
GET /api/tx/:id 交易、确认数和已签名 JSON
GET /api/txs?type=transfer|register|all&limit=&before= 最新交易
GET /api/fragments?limit=&before= 各区块的投递回执
GET /api/nodes 节点及其等级、投递量、收入和状态
GET /api/address/:地址 余额、nonce、记录和奖励
GET /api/node/:地址 同上,用于节点
GET /api/search?q= 搜索高度、哈希、交易、地址或节点名称
curl -s https://velum.consultorabim.com/api/stats
curl -s "https://velum.consultorabim.com/api/blocks?limit=25&before=120000"
curl -s https://velum.consultorabim.com/api/block/118204
curl -s "https://velum.consultorabim.com/api/block/118204?raw=1"   # signed block JSON
curl -s https://velum.consultorabim.com/api/tx/4c1f0a9e…
curl -s "https://velum.consultorabim.com/api/txs?type=all&limit=50"
curl -s https://velum.consultorabim.com/api/address/vel8Kx3vQ…
curl -s "https://velum.consultorabim.com/api/search?q=118204"

如需实时数据流,连接 wss://velum.consultorabim.com/hub?role=watch:你会收到 snapshot、每个新的 block 以及节点列表 nodes。

浏览器 API 适合查询,但不能作为转移资金的可信依据。交易所必须用自己的节点核验。

交易所的充值与提现 {#exchanges}

账户模型

与其他账户型链一样,有两种方式:

  • 每个用户一个地址(推荐)。从冷助记词为每位客户派生 m/44'/7420'/0'/0'/N'。不依赖用户正确填写 memo。
  • 共享地址 + memo。 只用一个充值地址,用 memo 作为客户参考号。memo 缺失或错误的充值将转入人工审核。

检测充值

import { CHAIN_ID } from "@velum/core";

const CONFIRMATIONS = 6;          // ≈ 12 s; see "Finality" above
let next = loadCursor();          // last processed height + 1

async function poll() {
  const head = (await rpc("chain.head", {})) as { height: number };
  while (next <= head.height - CONFIRMATIONS) {
    const block = (await rpc("chain.block", { height: next })) as Block;
    if (block.header.chain !== CHAIN_ID) throw new Error("wrong chain");
    for (const tx of block.txs) {
      if (tx.type !== "transfer" || !isOurDepositAddress(tx.to)) continue;
      const asset = tx.token ?? "VEL";
      credit({ txid: txId(tx), to: tx.to, asset, amount: tx.amount, memo: tx.memo, height: next });
    }
    saveCursor(++next);
  }
}
// Re-check the hash of the last credited blocks on every poll: if it changed, a short reorg happened.
  1. 在你自己的节点上,用 chain.head 和 chain.block 逐块跟踪区块链。
  2. 只处理转入你地址的 transfer 交易。资产为 token,没有则为 VEL。
  3. 区块达到 30 个确认后入账。
  4. 保存每个已处理区块的哈希。如果下次轮询时哈希变了,说明发生了重组:回滚并从该区块重新处理。
  5. 在合适的时候把资金归集到冷钱包。低于 1,000 欧元的转移不收手续费;更大的转移收取 0.01%。

提现

import { makeTransfer, txId } from "@velum/core";

const acc = (await rpc("account.get", { address: hot.address })) as { nonce: number; pending: number };
const tx = makeTransfer(hot.secret, hot.address, userAddress, amountMicro, acc.nonce + acc.pending, { memo: withdrawalId });
const { id } = (await rpc("tx.submit", { tx })) as { id: string };   // id === txId(tx)
// Track id until it is CONFIRMATIONS blocks deep. Re-broadcasting the same signed tx is safe (same nonce).
  • 在隔离的服务中签名;节点只接收已签名的交易。
  • 用 account.get(nonce + pending)获取 nonce;如果连续发送多笔,请自行维护计数器。
  • 检查目标地址有效,且不是 vel_… 特殊账户。
  • 如果一笔提现几个区块后仍未上链,请重新广播同一笔已签名交易。在确认第一笔未上链之前,不要用另一个 nonce 签发新交易。

MetaMask Snaps {#snaps}

MetaMask 使用以太坊协议,但 Snaps 允许添加非 EVM 链。一个 Velum Snap 需要:

  1. 派生 ed25519 密钥。 使用 snap_getBip32Entropy,curve: "ed25519"(SLIP-10),路径为 m/44'/7420'/0'/0'/0',与 SDK 相同。7420 尚待在 SLIP-44 中注册。
  2. 生成地址:vel + 公钥的 base58 编码。
  3. 用 snap_dialog 请用户确认后,用 ed25519 对交易的规范 JSON 签名。
  4. 通过 WebSocket 把交易发送到节点(endowment:network-access),或把签好的交易返回给 dapp。
{
  "version": "0.1.0",
  "proposedName": "Velum",
  "source": { "shasum": "…", "location": { "npm": { "filePath": "dist/bundle.js", "packageName": "velum-snap", "registry": "https://registry.npmjs.org/" } } },
  "initialPermissions": {
    "snap_getBip32Entropy": [{ "path": ["m", "44'", "7420'"], "curve": "ed25519" }],
    "endowment:rpc": { "dapps": true, "snaps": false },
    "snap_dialog": {},
    "endowment:network-access": {}
  },
  "manifestVersion": "0.1"
}
import type { OnRpcRequestHandler } from "@metamask/snaps-sdk";
import { confirmUi } from "./ui";                                   // renders the transfer for the user
import { ed25519 } from "@noble/curves/ed25519.js";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
import { base58 } from "@scure/base";
import { canonical, CHAIN_ID } from "@velum/core";

// SLIP-10 ed25519 at m/44'/7420'/0'/0'/0' — the same path the Velum SDK and hardware wallets use.
async function walletKey() {
  const node = await snap.request({
    method: "snap_getBip32Entropy",
    params: { path: ["m", "44'", "7420'", "0'", "0'", "0'"], curve: "ed25519" },
  });
  const secret = hexToBytes(node.privateKey!.slice(2));   // SLIP-10 ed25519 key, "0x…"
  const pub = ed25519.getPublicKey(secret);
  return { secret, address: "vel" + base58.encode(pub) };
}

export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
  const k = await walletKey();
  if (request.method === "velum_getAddress") return k.address;
  if (request.method === "velum_signTransfer") {
    const { to, amount, nonce, token, memo, fee } = request.params as any;
    // fee: 0 below 1,000 € at the network oracle price (plus a small proof of work, see PROTOCOL.md), else 0.01 %
    const body = { type: "transfer", chain: CHAIN_ID, from: k.address, to, amount, fee, nonce, ts: Date.now(), token, memo };
    const ok = await snap.request({ method: "snap_dialog", params: { type: "confirmation", content: confirmUi(body) } });
    if (!ok) throw new Error("rejected");
    const sig = bytesToHex(ed25519.sign(new TextEncoder().encode(canonical(body)), k.secret));
    return { ...body, sig };
  }
  throw new Error("method not found");
};

注意:

  • 路径相同,但助记词是 MetaMask 的:除非你在两边使用同一组助记词,否则 Snap 的账户与 Velum 应用中的账户并不一致。
  • 发布前,Snap 必须通过 MetaMask 的审计和收录流程。

在交易所上线 VEL 的清单 {#listado}

大型交易所(Binance、Kraken、Coinbase……)接入一条新链时通常要求的内容,以及各项所在位置:

要求 状态
交易所可运行的节点 搭建节点:无依赖的二进制,Linux x64/arm64
稳定且有文档的 RPC 节点 RPC
公开的区块浏览器 浏览器 和 REST API
交易格式与签名规范 交易
最终性规则 最终性
节点开源 公开仓库 velum-node(筹备中)
外部安全审计 待完成,主网上线前进行
主网 velum-mainnet-1,没有水龙头,也没有测试代币
法律框架(欧盟 MiCA) 正与法律顾问研究

支付网关 {#pasarelas}

Velum 可用于实时收款:几秒钟出块,低于 1,000 欧元的转账手续费为 0。始终按网络预言机价格显示以 EUR、USD 或 USDT 计的价值,以及商户实际收到的金额。

  1. 为每个订单生成一个地址(派生 m/44'/7420'/0'/0'/N'),或使用固定地址并在 memo 中填写订单号。
  2. 展示包含地址、金额和代币的二维码(例如 {{quote}})。
  3. 通过 hub?role=watch 或你的节点监听区块,达到 6 个确认后把订单标记为已支付。
  4. 如果用代币收款、想结算为 VEL(或反之),可使用市场。

SDK @velum/core {#sdk}

与应用、节点和本网站使用的是同一个核心。TypeScript 编写,没有原生依赖:可在 Bun、Node、浏览器、Workers 和 React Native 中运行。

连接

import { VelumClient, keysFromPhrase, generatePhrase, createDevice, createAccount } from "@velum/core";

const phrase = generatePhrase("en");                   // 24 words — store it offline
const keys = keysFromPhrase(phrase);
const account = createAccount(keys, createDevice("server-1"));

const client = new VelumClient({ account });           // defaults to the public gateways
client.on("status", (s) => console.log("network:", s));
client.connect();
await client.faucet();                                 // testnet only: 25 VEL

发送消息

client.on("message", (m) => console.log(m.payload.from, m.payload.body));
client.setContact(theirDeviceList);                    // signed DeviceList received out of band / via QR
const progress = await client.send(theirIdentity, { kind: "text", body: "Hola" });
client.on("progress", (p) => console.log(`${p.accepted}/${p.total} shards accepted`));

转账

import { parseAmount } from "@velum/core";

const { id } = await client.pay("vel4Jt9mR…", parseAmount("12,5")!);            // VEL (µVEL integer)
await client.pay("vel4Jt9mR…", 2_000_000, { token: "tEUR", memo: "order-981" }); // 2 tEUR, fee in VEL
const { balance, nonce } = await client.balance();

挂单

// Buy 250 VEL at 0.0125 tEUR each. price = smallest token units per 1 VEL (tEUR has 6 decimals).
await client.placeOrder({ token: "tEUR", side: "buy", price: 12_500, amount: 250_000_000, ttl: 1_440 });

创建代币

await client.createToken({ symbol: "CAFE", name: "Café de barrio", decimals: 2, maxSupply: 100_000_000, initial: 2_500_000, mintable: true });
// Costs 100 VEL. Then reward nodes in CAFE for a day:
// makePoolFund(wallet.secret, wallet.address, nonce, "CAFE", 1_440_000, 1_440)

SDK 还导出交易构造函数(makeTransfer、makeRegister、makeTokenCreate、makeTokenMint、makePoolFund、makeOrder、makeCancel)、状态读取函数(supplyInfo、difficulty、bookOf、tradesOf)以及金额格式化函数(formatAmount、parseAmount)。