380 lines
9.4 KiB
TypeScript
380 lines
9.4 KiB
TypeScript
/**
|
|
* Account Service HTTP Client
|
|
*
|
|
* 用于 Service-Party-App 调用 Account 服务的 HTTP API
|
|
* 主要用于创建/查询 keygen 和 sign 会话
|
|
*/
|
|
|
|
// =============================================================================
|
|
// 类型定义
|
|
// =============================================================================
|
|
|
|
// Keygen 会话相关
|
|
export interface CreateKeygenSessionRequest {
|
|
wallet_name: string;
|
|
threshold_t: number;
|
|
threshold_n: number;
|
|
initiator_party_id: string;
|
|
initiator_name?: string;
|
|
persistent_count: number;
|
|
external_count: number;
|
|
expires_in_seconds?: number;
|
|
}
|
|
|
|
export interface CreateKeygenSessionResponse {
|
|
session_id: string;
|
|
invite_code: string;
|
|
wallet_name: string;
|
|
threshold_n: number;
|
|
threshold_t: number;
|
|
selected_server_parties: string[];
|
|
join_tokens: Record<string, string>;
|
|
join_token?: string; // Wildcard token for backward compatibility
|
|
expires_at: number;
|
|
}
|
|
|
|
export interface JoinSessionRequest {
|
|
party_id: string;
|
|
join_token: string;
|
|
device_type?: string;
|
|
device_id?: string;
|
|
}
|
|
|
|
export interface PartyInfo {
|
|
party_id: string;
|
|
party_index: number;
|
|
}
|
|
|
|
export interface SessionInfo {
|
|
session_id: string;
|
|
session_type: string;
|
|
threshold_n: number;
|
|
threshold_t: number;
|
|
status: string;
|
|
wallet_name: string;
|
|
invite_code: string;
|
|
keygen_session_id?: string;
|
|
}
|
|
|
|
export interface JoinSessionResponse {
|
|
success: boolean;
|
|
party_index: number;
|
|
session_info: SessionInfo;
|
|
other_parties: PartyInfo[];
|
|
}
|
|
|
|
// Participant status information with party_index
|
|
export interface ParticipantStatusInfo {
|
|
party_id: string;
|
|
party_index: number;
|
|
status: string;
|
|
}
|
|
|
|
export interface GetSessionStatusResponse {
|
|
session_id: string;
|
|
status: string;
|
|
threshold_t: number; // Minimum parties needed to sign (e.g., 2 in 2-of-3)
|
|
threshold_n: number; // Total number of parties required (e.g., 3 in 2-of-3)
|
|
completed_parties: number;
|
|
total_parties: number;
|
|
session_type: string;
|
|
public_key?: string;
|
|
signature?: string;
|
|
has_delegate: boolean;
|
|
// participants contains detailed participant information including party_index
|
|
// Used for co_managed_keygen sessions to build correct participant list
|
|
participants?: ParticipantStatusInfo[];
|
|
}
|
|
|
|
export interface GetSessionByInviteCodeResponse {
|
|
session_id: string;
|
|
wallet_name: string;
|
|
threshold_n: number;
|
|
threshold_t: number;
|
|
status: string;
|
|
invite_code: string;
|
|
expires_at: number;
|
|
joined_parties: number;
|
|
completed_parties?: number;
|
|
total_parties?: number;
|
|
join_token?: string;
|
|
}
|
|
|
|
// Sign 会话相关
|
|
export interface SignPartyInfo {
|
|
party_id: string;
|
|
party_index: number;
|
|
}
|
|
|
|
export interface CreateSignSessionRequest {
|
|
keygen_session_id: string;
|
|
wallet_name: string;
|
|
message_hash: string;
|
|
parties: SignPartyInfo[];
|
|
threshold_t: number;
|
|
initiator_name?: string;
|
|
}
|
|
|
|
export interface CreateSignSessionResponse {
|
|
session_id: string;
|
|
invite_code: string;
|
|
keygen_session_id: string;
|
|
wallet_name: string;
|
|
threshold_t: number;
|
|
selected_parties: string[];
|
|
expires_at: number;
|
|
join_token?: string; // Backward compatible: wildcard token (may be empty)
|
|
join_tokens: Record<string, string>; // New: all join tokens (map[partyID]token)
|
|
}
|
|
|
|
export interface GetSignSessionByInviteCodeResponse {
|
|
session_id: string;
|
|
keygen_session_id: string;
|
|
wallet_name: string;
|
|
message_hash: string;
|
|
threshold_t: number;
|
|
threshold_n: number;
|
|
status: string;
|
|
invite_code: string;
|
|
expires_at: number;
|
|
parties: SignPartyInfo[];
|
|
joined_count: number;
|
|
join_token?: string;
|
|
}
|
|
|
|
export interface GetSignSessionStatusResponse {
|
|
session_id: string;
|
|
status: string;
|
|
session_type: string;
|
|
threshold_t: number;
|
|
threshold_n: number;
|
|
completed_parties: number;
|
|
total_parties: number;
|
|
joined_count?: number;
|
|
parties?: SignPartyInfo[];
|
|
participants?: Array<{ party_id: string; party_index: number; status: string }>;
|
|
message_hash?: string;
|
|
signature?: string;
|
|
}
|
|
|
|
// 错误响应
|
|
export interface ErrorResponse {
|
|
error: string;
|
|
message?: string;
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTTP 客户端类
|
|
// =============================================================================
|
|
|
|
export class AccountClient {
|
|
private baseUrl: string;
|
|
private timeout: number;
|
|
|
|
/**
|
|
* 构造函数
|
|
* @param baseUrl Account 服务的基础 URL (例如: https://api.szaiai.com 或 http://localhost:8080)
|
|
* @param timeout 请求超时时间 (毫秒)
|
|
*/
|
|
constructor(baseUrl: string, timeout: number = 30000) {
|
|
// 移除末尾的斜杠
|
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
this.timeout = timeout;
|
|
}
|
|
|
|
/**
|
|
* 更新基础 URL
|
|
*/
|
|
setBaseUrl(baseUrl: string): void {
|
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
}
|
|
|
|
/**
|
|
* 获取当前基础 URL
|
|
*/
|
|
getBaseUrl(): string {
|
|
return this.baseUrl;
|
|
}
|
|
|
|
/**
|
|
* 发送 HTTP 请求
|
|
*/
|
|
private async request<T>(
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
path: string,
|
|
body?: unknown
|
|
): Promise<T> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
|
|
try {
|
|
const options: RequestInit = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
signal: controller.signal,
|
|
};
|
|
|
|
if (body) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
console.log(`[AccountClient] ${method} ${url}`, body ? JSON.stringify(body) : '');
|
|
|
|
const response = await fetch(url, options);
|
|
|
|
const text = await response.text();
|
|
let data: T | ErrorResponse;
|
|
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
throw new Error(`Invalid JSON response: ${text}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = data as ErrorResponse;
|
|
throw new Error(errorData.message || errorData.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
console.log(`[AccountClient] Response:`, data);
|
|
return data as T;
|
|
} catch (error) {
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
throw new Error(`Request timeout after ${this.timeout}ms`);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Keygen 会话 API
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* 创建 Keygen 会话
|
|
*/
|
|
async createKeygenSession(
|
|
params: CreateKeygenSessionRequest
|
|
): Promise<CreateKeygenSessionResponse> {
|
|
return this.request<CreateKeygenSessionResponse>(
|
|
'POST',
|
|
'/api/v1/co-managed/sessions',
|
|
params
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 加入会话
|
|
*/
|
|
async joinSession(
|
|
sessionId: string,
|
|
params: JoinSessionRequest
|
|
): Promise<JoinSessionResponse> {
|
|
return this.request<JoinSessionResponse>(
|
|
'POST',
|
|
`/api/v1/co-managed/sessions/${sessionId}/join`,
|
|
params
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 获取会话状态
|
|
*/
|
|
async getSessionStatus(sessionId: string): Promise<GetSessionStatusResponse> {
|
|
return this.request<GetSessionStatusResponse>(
|
|
'GET',
|
|
`/api/v1/co-managed/sessions/${sessionId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 通过邀请码查询 Keygen 会话
|
|
*/
|
|
async getSessionByInviteCode(inviteCode: string): Promise<GetSessionByInviteCodeResponse> {
|
|
return this.request<GetSessionByInviteCodeResponse>(
|
|
'GET',
|
|
`/api/v1/co-managed/sessions/by-invite-code/${inviteCode}`
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Sign 会话 API
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* 创建 Sign 会话
|
|
*/
|
|
async createSignSession(
|
|
params: CreateSignSessionRequest
|
|
): Promise<CreateSignSessionResponse> {
|
|
return this.request<CreateSignSessionResponse>(
|
|
'POST',
|
|
'/api/v1/co-managed/sign',
|
|
params
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 通过邀请码查询 Sign 会话
|
|
*/
|
|
async getSignSessionByInviteCode(inviteCode: string): Promise<GetSignSessionByInviteCodeResponse> {
|
|
return this.request<GetSignSessionByInviteCodeResponse>(
|
|
'GET',
|
|
`/api/v1/co-managed/sign/by-invite-code/${inviteCode}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 获取 Sign 会话状态
|
|
*/
|
|
async getSignSessionStatus(sessionId: string): Promise<GetSignSessionStatusResponse> {
|
|
return this.request<GetSignSessionStatusResponse>(
|
|
'GET',
|
|
`/api/v1/co-managed/sign/${sessionId}`
|
|
);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 健康检查
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* 健康检查
|
|
*/
|
|
async healthCheck(): Promise<{ status: string; service: string }> {
|
|
return this.request<{ status: string; service: string }>(
|
|
'GET',
|
|
'/health'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 测试连接
|
|
*/
|
|
async testConnection(): Promise<boolean> {
|
|
try {
|
|
const result = await this.healthCheck();
|
|
return result.status === 'healthy';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 默认实例
|
|
// =============================================================================
|
|
|
|
// 默认使用生产环境地址
|
|
const DEFAULT_ACCOUNT_SERVICE_URL = 'https://rwaapi.szaiai.com';
|
|
|
|
// 创建默认客户端实例
|
|
export const accountClient = new AccountClient(DEFAULT_ACCOUNT_SERVICE_URL);
|