身份验证 (Authentication)
ZTDX API 使用 EIP-712 结构化签名进行身份验证。所有需要认证的接口都需要使用用户的以太坊私钥对 EIP-712 类型化数据进行签名。
EIP-712 签名标准
ZTDX 使用 EIP-712 (Ethereum Typed Structured Data Hashing and Signing) 标准进行签名验证。这是一种更加安全和用户友好的签名方式,钱包会清晰地显示签名的内容。
Domain Configuration
所有 EIP-712 签名使用以下 Domain 配置:
{
"name": "ZTDX",
"version": "1",
"chainId": <network_chain_id>,
"verifyingContract": "<vault_contract_address>"
}
参数说明:
chainId: 网络链 ID(如 Arbitrum Sepolia: 421614)verifyingContract: ZTDX Vault 合约地址
重要
不同的网络环境(测试网/主网)使用不同的 chainId 和 verifyingContract。请从服务器配置或前端环境变量中获取正确的值。
登录流程 (Login Flow)
登录流程分为两步:1) 获取 Nonce,2) 签名登录
1. 获取 Nonce
在登录前,必须先获取用户的当前 nonce 值和用于签名的 EIP-712 typed data。
请求
- Method:
GET - Path:
/api/v1/auth/nonce/:address - Authentication: 不需要
路径参数
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
| address | string | 是 | 以太坊地址(0x开头,42字符) |
响应示例
{
"nonce": 1,
"typed_data": {
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Login": [
{ "name": "wallet", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "timestamp", "type": "uint256" }
]
},
"primaryType": "Login",
"domain": {
"name": "ZTDX",
"version": "1",
"chainId": 421614,
"verifyingContract": "0xYourVaultContractAddress"
},
"message": {
"wallet": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"nonce": "1",
"timestamp": "1704067200"
}
}
}
响应字段
| 字段 | 类型 | 描述 |
|---|---|---|
| nonce | number | 当前用户的 nonce 值(每次成功登录后自增) |
| typed_data | object | EIP-712 类型化数据,可直接用于钱包签名 |
Nonce 机制说明
- Nonce 在用户首次访问时创建,初始值为 1
- 每次成功登录后,nonce 会自动加 1
- 多次调用 nonce 接口,在未登录前返回值保持不变
- Nonce 用于防止重放攻击
2. 签名并登录
使用获取到的 typed_data 进行 EIP-712 签名,然后发送登录请求。
请求
- Method:
POST - Path:
/api/v1/auth/login - Content-Type:
application/json - Authentication: 不需要
请求参数
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
| address | string | 是 | 以太坊地址(小写) |
| signature | string | 是 | EIP-712 签名(0x开头) |
| timestamp | number | 是 | Unix 时间戳(秒),需在 5 分钟内有效 |
时间戳格式
timestamp 使用秒级时间戳,而不是毫秒。请确保与 typed_data 中的 timestamp 保持一致。
请求示例
{
"address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"signature": "0xabcdef123456...",
"timestamp": 1704067200
}
前端签名代码示例 (ethers.js v6)
import { BrowserProvider } from 'ethers';
async function loginToZTDX(address) {
// 1. 获取 nonce 和 typed data
const response = await fetch(`/api/v1/auth/nonce/${address}`);
const { nonce, typed_data } = await response.json();
// 2. 使用钱包进行 EIP-712 签名
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// ethers v6 使用 signTypedData
const signature = await signer.signTypedData(
typed_data.domain,
{ Login: typed_data.types.Login },
typed_data.message
);
// 3. 发送登录请求
const loginResponse = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: address.toLowerCase(),
signature,
timestamp: parseInt(typed_data.message.timestamp)
})
});
const { token, expires_at } = await loginResponse.json();
// 保存 token
localStorage.setItem('jwt_token', token);
return { token, expires_at };
}
Python 签名代码示例
from eth_account import Account
from eth_account.messages import encode_typed_data
import requests
import time
def login_to_ztdx(privat_key: str, address: str) -> dict:
# 1. 获取 nonce 和 typed data
response = requests.get(f"/api/v1/auth/nonce/{address}")
data = response.json()
typed_data = data["typed_data"]
# 2. 使用 EIP-712 签名
signable_message = encode_typed_data(full_message=typed_data)
signed = Account.sign_message(signable_message, private_key=private_key)
signature = signed.signature.hex()
# 3. 发送登录请求
login_response = requests.post(
"/api/v1/auth/login",
json={
"address": address.lower(),
"signature": signature,
"timestamp": int(typed_data["message"]["timestamp"])
}
)
return login_response.json()
响应示例
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": 1704153600
}
响应字段
| 字段 | 类型 | 描述 |
|---|---|---|
| token | string | JWT 认证令牌 |
| expires_at | number | 令牌过期时间(Unix 时间戳,秒) |