WaaS项目
WaaS 30 分钟接入实战
通过一条完整的实战链路快速跑通 WaaS:创建地址、接收回调、发起提币与终态确认。
本指南帮助您在 30 分钟内跑通一条完整的 WaaS 链路:创建充值地址 → 接收充值回调 → 发起提币 → 接收提币终态回调。
验收目标
完成本文后,您应满足以下 4 个验收条件:
- 能成功调用
/api/v1/address/create创建地址 - 回调服务能验证
sign并返回success - 能成功调用
/api/v1/payout发起提币并拿到cid - 能基于回调或
/api/v1/payout/query判定终态(status=2/4/6/7)
准备参数(约 5 分钟)
请先准备以下信息:
BASE_URL:项目专属 Base URL(请在 Cregis 客户端 > WaaS项目 > 项目设置页面 > Base URL 复制)PID:WaaS 项目 IDAPI_KEY:项目 API KeyCHAIN_ID:链 ID(如195)CURRENCY:代币标识(如195@195)CALLBACK_URL:可公网访问的回调地址
相关文档:
第 1 步:先启动一个最小回调服务(约 8 分钟)
下面示例提供 Node.js / Python / PHP 三种最小实现,完成两件事:验签 + 幂等。
重点:回调处理成功后必须返回 HTTP 200 且响应体为纯文本 cid/txid。import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const API_KEY = process.env.CREGIS_API_KEY;
const processed = new Set(); // 生产环境请改为数据库唯一键
function buildSign(apiKey, payload) {
const canonical = Object.keys(payload)
.filter((k) => k !== 'sign' && payload[k] !== '' && payload[k] !== null && payload[k] !== undefined)
.sort()
.map((k) => `${k}${payload[k]}`)
.join('');
return crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
}
app.post('/webhook/waas', (req, res) => {
const body = req.body || {};
if (buildSign(API_KEY, body) !== body.sign) {
return res.status(401).send('invalid sign');
}
const uniqueKey = `${body.cid || 'no-cid'}:${body.txid || 'no-txid'}:${body.status || 'no-status'}`;
if (processed.has(uniqueKey)) {
return res.status(200).send('success');
}
processed.add(uniqueKey);
// TODO: 在这里写入你的业务逻辑(入账、出账终态更新、告警等)
return res.status(200).send('success');
});
app.listen(8080, () => console.log('WaaS webhook server running on :8080'));import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
API_KEY = os.environ['CREGIS_API_KEY']
processed = set() # Use a database unique key in production
def build_sign(api_key, payload):
canonical = ''.join(
f'{key}{payload[key]}'
for key in sorted(payload)
if key != 'sign' and payload[key] not in ('', None)
)
return hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
@app.post('/webhook/waas')
def waas_webhook():
body = request.get_json(silent=True) or {}
if build_sign(API_KEY, body) != body.get('sign'):
return 'invalid sign', 401
unique_key = f"{body.get('cid', 'no-cid')}:{body.get('txid', 'no-txid')}:{body.get('status', 'no-status')}"
if unique_key in processed:
return 'success', 200
processed.add(unique_key)
# TODO: 在这里写入你的业务逻辑(入账、出账终态更新、告警等)
return 'success', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)<?php
$apiKey = getenv('CREGIS_API_KEY');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
function buildSign(string $apiKey, array $payload): string
{
$sorted = $payload;
unset($sorted['sign']);
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
return strtolower(md5($apiKey . $canonical));
}
if (buildSign($apiKey, $body) !== ($body['sign'] ?? null)) {
http_response_code(401);
echo 'invalid sign';
exit;
}
$uniqueKey = ($body['cid'] ?? 'no-cid') . ':' . ($body['txid'] ?? 'no-txid') . ':' . ($body['status'] ?? 'no-status');
// Production: store $uniqueKey in a database/Redis unique index for idempotency.
// TODO: 在这里写入你的业务逻辑(入账、出账终态更新、告警等)
http_response_code(200);
echo 'success';第 2 步:创建充值地址(约 5 分钟)
调用 /api/v1/address/create,为用户分配地址并绑定回调地址。
TON、XRP不支持请求API创建地址,请在客户端手动创建,详情参考创建子地址。
curl -X POST "${BASE_URL}/api/v1/address/create" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"chain_id": "195",
"alias": "user_10001",
"callback_url": "https://your-domain.com/webhook/waas",
"nonce": "n6k2q1",
"timestamp": 1776391200000,
"sign": "<按签名算法计算>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"chain_id": "195",
"alias": "user_10001",
"callback_url": "https://your-domain.com/webhook/waas"
});
const response = await fetch(`${BASE_URL}/api/v1/address/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'chain_id': '195',
'alias': 'user_10001',
'callback_url': 'https://your-domain.com/webhook/waas',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v1/address/create', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'chain_id' => '195',
'alias' => 'user_10001',
'callback_url' => 'https://your-domain.com/webhook/waas',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v1/address/create', false, $context);成功后会返回地址信息。将该地址分配给对应用户,后续用户充值将触发回调。
第 3 步:验证充值回调(约 5 分钟)
当用户向该地址转账后,WaaS 会向 callback_url 推送通知。典型字段包含:
cid:Cregis 业务流水号txid:链上交易哈希amount:到账金额status:充值回调通常为1(成功)
请务必做到:
- 先验签,再做账
- 以
cid/txid做幂等 - 成功后立即响应
success
第 4 步:发起提币(约 5 分钟)
调用 /api/v1/payout 发起出款。third_party_id 建议用你系统的唯一业务单号,作为幂等键和对账键。
curl -X POST "${BASE_URL}/api/v1/payout" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"currency": "195@195",
"address": "TXsmKpEuW7qWnXzJLGP9eDLvWPR2GRn1FS",
"amount": "1.1",
"third_party_id": "withdraw_20260417_0001",
"callback_url": "https://your-domain.com/webhook/waas",
"remark": "user withdrawal",
"nonce": "p8x9m2",
"timestamp": 1776391300000,
"sign": "<按签名算法计算>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"currency": "195@195",
"address": "TXsmKpEuW7qWnXzJLGP9eDLvWPR2GRn1FS",
"amount": "1.1",
"third_party_id": "withdraw_20260417_0001",
"callback_url": "https://your-domain.com/webhook/waas",
"remark": "user withdrawal"
});
const response = await fetch(`${BASE_URL}/api/v1/payout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'currency': '195@195',
'address': 'TXsmKpEuW7qWnXzJLGP9eDLvWPR2GRn1FS',
'amount': '1.1',
'third_party_id': 'withdraw_20260417_0001',
'callback_url': 'https://your-domain.com/webhook/waas',
'remark': 'user withdrawal',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v1/payout', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'currency' => '195@195',
'address' => 'TXsmKpEuW7qWnXzJLGP9eDLvWPR2GRn1FS',
'amount' => '1.1',
'third_party_id' => 'withdraw_20260417_0001',
'callback_url' => 'https://your-domain.com/webhook/waas',
'remark' => 'user withdrawal',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v1/payout', false, $context);接口成功返回后请存储 cid,后续状态追踪都依赖它。
第 5 步:确认提币终态(约 2 分钟)
提币终态建议“回调优先 + 主动查询兜底”:
- 回调终态:
status=2/4/6/7(互斥,单笔只会有一个终态) - 查询兜底:调用
/api/v1/payout/query按cid查询
curl -X POST "${BASE_URL}/api/v1/payout/query" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"cid": 1391751691788288,
"nonce": "a1b2c3",
"timestamp": 1776391400000,
"sign": "<按签名算法计算>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"cid": 1391751691788288
});
const response = await fetch(`${BASE_URL}/api/v1/payout/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'cid': 1391751691788288,
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v1/payout/query', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'cid' => 1391751691788288,
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v1/payout/query', false, $context);上线前检查清单
- 已区分开发/生产环境凭证,避免混用
- 回调接口具备验签、幂等、告警监控
- 资金动作(入账/出账)均有唯一业务键防重
- 关键日志可按
cid、third_party_id、txid检索 - 对终态
2/4/6/7都已定义明确的业务处理分支
常见问题(快速排障)
1) 为什么我收不到回调?
优先检查:callback_url 是否公网可达、接口是否返回了纯文本 cid/txid、是否被网关拦截。
2) 为什么回调重复?
如果收到已处理过的回调内容,请按幂等逻辑处理。建议用 success 做幂等,避免重复入账。
3) 签名经常校验失败怎么办?
重点核对:排序规则、空值剔除、是否排除了 sign 字段、MD5 后是否转小写。
---
如果你已经跑通本篇,下一步建议继续阅读: