钱包与交易所
本页面向集成 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)
- 构造不含
sig的对象。省略不用的可选字段(不要设为null)。 - 序列化为规范 JSON:键排序,无空白。
- 用钱包密钥对 UTF-8 字节签名。
sig= 十六进制签名。 - 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 } |
| 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.
- 在你自己的节点上,用
chain.head和chain.block逐块跟踪区块链。 - 只处理转入你地址的
transfer交易。资产为token,没有则为 VEL。 - 区块达到 30 个确认后入账。
- 保存每个已处理区块的哈希。如果下次轮询时哈希变了,说明发生了重组:回滚并从该区块重新处理。
- 在合适的时候把资金归集到冷钱包。低于 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 需要:
- 派生 ed25519 密钥。 使用
snap_getBip32Entropy,curve: "ed25519"(SLIP-10),路径为m/44'/7420'/0'/0'/0',与 SDK 相同。7420 尚待在 SLIP-44 中注册。 - 生成地址:
vel+ 公钥的 base58 编码。 - 用
snap_dialog请用户确认后,用 ed25519 对交易的规范 JSON 签名。 - 通过 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 计的价值,以及商户实际收到的金额。
- 为每个订单生成一个地址(派生
m/44'/7420'/0'/0'/N'),或使用固定地址并在memo中填写订单号。 - 展示包含地址、金额和代币的二维码(例如
{{quote}})。 - 通过
hub?role=watch或你的节点监听区块,达到 6 个确认后把订单标记为已支付。 - 如果用代币收款、想结算为 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)。