Skip to content

Commit a475f99

Browse files
committed
feat: expand venue support and refine desktop UX
- Add Aster and GRVT venue support across account storage, symbol sync, API contracts, trading flows, and automated coverage so both exchanges work end to end in the desktop app. - Split workspace tab logic into dedicated market and trading partial view-model files to make the new venue integrations easier to maintain and review. - Improve account management UX by clarifying each venue's supported authentication methods, normalizing placeholder copy, tightening venue-specific validation, and fixing the auth mode ComboBox rendering issue when switching venues or accounts. - Rework desktop shutdown into a single asynchronous path that blocks the UI with a shutting-down overlay, hides auxiliary windows, and prevents duplicate cleanup errors while background disposal completes. - Expand MCP-facing regression coverage and refresh the English and Traditional Chinese README/API documentation to reflect the newly supported venues and desktop behavior in this update.
1 parent e28c3c0 commit a475f99

34 files changed

+6415
-1507
lines changed

API.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Path params:
5353
### `POST /api/v1/accounts`
5454
### `PUT /api/v1/accounts/{accountId}`
5555
Body: `ApiAccountUpsertRequest`
56-
- `venueId` (`string`, required): `BitMEX` or `Hyperliquid`
56+
- `venueId` (`string`, required): `BitMEX`, `Hyperliquid`, or `Aster`
5757
- `displayName` (`string`, required)
5858
- `environment` (`string`, required): `mainnet` or `testnet`
5959
- `summary` (`string`, required)
@@ -64,6 +64,15 @@ Body: `ApiAccountUpsertRequest`
6464
- `privateKey` (`string`, optional)
6565
- `isEnabled` (`boolean`, optional, update only)
6666

67+
### Aster account notes
68+
- Aster private endpoints use the V3 signer model.
69+
- For authenticated operations on `Aster`, set:
70+
- `accountAddress`: main account wallet address (`user`)
71+
- `walletAddress`: API wallet address (`signer`)
72+
- `privateKey`: private key of the API wallet (`signer`)
73+
- If these are missing, Aster can still use public market data but trading/account-state endpoints will fail.
74+
- The app enforces one-way position mode for Aster trading. If your account is in hedge mode and cannot switch to one-way mode, order requests are rejected.
75+
6776
### `DELETE /api/v1/accounts/{accountId}`
6877
Starts an async delete operation.
6978

API_zh.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Path 參數:
5353
### `POST /api/v1/accounts`
5454
### `PUT /api/v1/accounts/{accountId}`
5555
Body:`ApiAccountUpsertRequest`
56-
- `venueId``string`,必填):`BitMEX``Hyperliquid`
56+
- `venueId``string`,必填):`BitMEX``Hyperliquid``Aster`
5757
- `displayName``string`,必填)
5858
- `environment``string`,必填):`mainnet``testnet`
5959
- `summary``string`,必填)
@@ -64,6 +64,15 @@ Body:`ApiAccountUpsertRequest`
6464
- `privateKey``string`,可選)
6565
- `isEnabled``boolean`,可選,更新時可用)
6666

67+
### Aster 帳號說明
68+
- Aster 私有端點使用 V3 signer 驗證模型。
69+
- 針對 `Aster` 的已驗證操作,請填入:
70+
- `accountAddress`:主帳戶錢包地址(`user`
71+
- `walletAddress`:API 錢包地址(`signer`
72+
- `privateKey`:API 錢包(`signer`)私鑰
73+
- 若未提供上述資料,Aster 仍可使用公開行情,但交易/帳務端點會失敗。
74+
- 本軟體會強制 Aster 使用單向持倉模式。若帳戶為雙向模式且無法切換為單向,則下單請求會被拒絕。
75+
6776
### `DELETE /api/v1/accounts/{accountId}`
6877
啟動非同步刪除作業。
6978

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
10+
<PackageReference Include="xunit" Version="2.9.3" />
11+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
12+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
13+
<PrivateAssets>all</PrivateAssets>
14+
</PackageReference>
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="../AiyoPerps/AiyoPerps.csproj" />
19+
</ItemGroup>
20+
</Project>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using AiyoPerps.Models;
2+
using AiyoPerps.Services;
3+
using Microsoft.Data.Sqlite;
4+
using System;
5+
using System.IO;
6+
using System.Security.Cryptography;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
namespace AiyoPerps.Test;
12+
13+
public sealed class AsterApiLiveTests
14+
{
15+
private static readonly string DbPath = Environment.GetEnvironmentVariable("AIYOPERPS_DB_PATH")
16+
?? "/mnt/e/work/AiyoPerps/AiyoPerps/AiyoPerps/bin/Debug/net10.0/db/AiyoPerps.main.db";
17+
18+
[Fact]
19+
public async Task ValidateConnection_MainnetAster_FromStoredWallet_ShouldSucceed()
20+
{
21+
if (!ShouldRunLiveTests())
22+
{
23+
return;
24+
}
25+
26+
var (environment, credentials) = LoadAsterCredentialsFromDb(DbPath);
27+
var logger = new AppLogger();
28+
await using var venue = new AsterVenueAdapter(environment, credentials, logger);
29+
30+
var result = await venue.ValidateConnectionAsync();
31+
32+
Assert.True(result.IsSuccess, $"Aster ValidateConnection failed: {result.Message}");
33+
}
34+
35+
[Fact]
36+
public async Task GetAccountSnapshot_MainnetAster_FromStoredWallet_ShouldReturnBalances()
37+
{
38+
if (!ShouldRunLiveTests())
39+
{
40+
return;
41+
}
42+
43+
var (environment, credentials) = LoadAsterCredentialsFromDb(DbPath);
44+
var logger = new AppLogger();
45+
await using var venue = new AsterVenueAdapter(environment, credentials, logger);
46+
47+
var snapshot = await venue.GetAccountSnapshotAsync();
48+
49+
Assert.NotNull(snapshot);
50+
Assert.True(snapshot.Balances.Count > 0, "Aster balances are empty.");
51+
}
52+
53+
private static bool ShouldRunLiveTests()
54+
{
55+
var flag = Environment.GetEnvironmentVariable("AIYOPERPS_RUN_ASTER_LIVE_TESTS");
56+
return string.Equals(flag, "1", StringComparison.OrdinalIgnoreCase)
57+
|| string.Equals(flag, "true", StringComparison.OrdinalIgnoreCase);
58+
}
59+
60+
private static (string Environment, AccountCredentials Credentials) LoadAsterCredentialsFromDb(string dbPath)
61+
{
62+
var dbFile = new FileInfo(dbPath);
63+
if (!dbFile.Exists)
64+
{
65+
throw new FileNotFoundException($"AiyoPerps DB not found: {dbPath}");
66+
}
67+
68+
var dbDir = dbFile.Directory ?? throw new InvalidOperationException("DB directory missing.");
69+
var keyPath = Path.Combine(dbDir.FullName, "secrets.key");
70+
if (!File.Exists(keyPath))
71+
{
72+
throw new FileNotFoundException($"secrets.key not found: {keyPath}");
73+
}
74+
75+
using var conn = new SqliteConnection($"Data Source={dbPath}");
76+
conn.Open();
77+
78+
using var cmd = conn.CreateCommand();
79+
cmd.CommandText = @"
80+
SELECT Environment, AccountAddress, WalletAddress, ApiKeyEncrypted, ApiSecretEncrypted, PrivateKeyEncrypted
81+
FROM Accounts
82+
WHERE VenueId='Aster' AND IsEnabled=1
83+
ORDER BY CASE WHEN Environment='mainnet' THEN 0 ELSE 1 END, CreatedAt DESC
84+
LIMIT 1";
85+
86+
using var reader = cmd.ExecuteReader();
87+
if (!reader.Read())
88+
{
89+
throw new InvalidOperationException("No enabled Aster account found in DB.");
90+
}
91+
92+
var environment = reader.GetString(0);
93+
var accountAddress = reader.IsDBNull(1) ? null : reader.GetString(1);
94+
var walletAddress = reader.IsDBNull(2) ? null : reader.GetString(2);
95+
var apiKeyEnc = reader.IsDBNull(3) ? null : reader.GetString(3);
96+
var apiSecretEnc = reader.IsDBNull(4) ? null : reader.GetString(4);
97+
var privateKeyEnc = reader.IsDBNull(5) ? null : reader.GetString(5);
98+
99+
var key = File.ReadAllBytes(keyPath);
100+
101+
var credentials = new AccountCredentials
102+
{
103+
AccountAddress = accountAddress,
104+
WalletAddress = walletAddress,
105+
ApiKey = DecryptOrNull(apiKeyEnc, key),
106+
ApiSecret = DecryptOrNull(apiSecretEnc, key),
107+
PrivateKey = DecryptOrNull(privateKeyEnc, key)
108+
};
109+
110+
if (string.IsNullOrWhiteSpace(credentials.AccountAddress) ||
111+
string.IsNullOrWhiteSpace(credentials.WalletAddress) ||
112+
string.IsNullOrWhiteSpace(credentials.PrivateKey))
113+
{
114+
throw new InvalidOperationException("Aster credentials are incomplete in DB.");
115+
}
116+
117+
return (environment, credentials);
118+
}
119+
120+
private static string? DecryptOrNull(string? cipher, byte[] key)
121+
{
122+
if (string.IsNullOrWhiteSpace(cipher))
123+
{
124+
return null;
125+
}
126+
127+
var payload = Convert.FromBase64String(cipher);
128+
using var aes = Aes.Create();
129+
aes.Key = key;
130+
131+
var ivLen = aes.BlockSize / 8;
132+
var iv = payload[..ivLen];
133+
var data = payload[ivLen..];
134+
135+
using var decryptor = aes.CreateDecryptor(aes.Key, iv);
136+
var plain = decryptor.TransformFinalBlock(data, 0, data.Length);
137+
return Encoding.UTF8.GetString(plain);
138+
}
139+
}

0 commit comments

Comments
 (0)