Skip to content

Commit 52e3d51

Browse files
committed
feat: show wallet credits in auth status
1 parent bcff00f commit 52e3d51

5 files changed

Lines changed: 35 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ file mode 0600 on POSIX); they are not encrypted. On Windows, storage inherits
6363
the user's directory ACLs. `HYPER3D_CONFIG_DIR` can change that directory.
6464
`auth status` (alias `auth info`) verifies credentials by calling the existing
6565
`POST /api/user/get_info` HTTP API. It displays the username, user UUID, and the
66-
balance/frozen credits of the authorized billing workspace. For team grants it
66+
separate regular, subscription and frozen credit balances of an authorized personal
67+
workspace. Team workspaces display their separate regular and frozen balances. For team grants it
6768
also calls `POST /api/group/group_info` with the group UUID returned by the server.
6869
It does not call MCP tools or query wallet usage. Expired stored access tokens
6970
are refreshed once; environment tokens are used as supplied. Without credentials

‎packages/cli/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ The backend redirects to `https://hyper3d.ai/workspace/oauth/device?interaction=
3535
the CLI always opens the verification URL returned by the server.
3636
Client identity is managed internally; no client configuration or localhost
3737
listener is required. `hyper3d auth status` (alias `hyper3d auth info`) displays
38-
your username, user UUID and the authorized personal/team wallet's balance and
39-
frozen credits using the existing HTTP APIs. No MCP tool or usage query is needed.
38+
your username, user UUID and the authorized wallet's separate credit balances.
39+
Personal wallets show subscription, regular and frozen credits; team wallets show
40+
regular and frozen credits. No MCP tool or usage query is needed.
4041
Account lookup requires backend `account:read` support and the updated official
4142
client metadata. Run `hyper3d auth login` again to authorize it for an older login.
4243
Stored credentials refresh automatically after access-token expiry; network and

‎packages/cli/src/account.js‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {auth} from '@modelcontextprotocol/sdk/client/auth.js';
22
import {loadCredentials, createProvider, secureUrl} from './auth.js';
33

4+
const WALLET_UNITS_PER_CREDIT = 10;
5+
46
export async function accountInfo(endpoint, {token = process.env.HYPER3D_ACCESS_TOKEN, fetchFn = fetch} = {}) {
57
const base = secureUrl(endpoint);
68
if (!base.pathname.replace(/\/$/, '').endsWith('/mcp'))
@@ -44,7 +46,16 @@ export async function accountInfo(endpoint, {token = process.env.HYPER3D_ACCESS_
4446
throw new Error('Invalid user information returned by server.');
4547
let wallet;
4648
if (workspace?.type === 'personal') {
47-
wallet = {type: 'personal', balance: meta.balance, frozen: meta.frozen};
49+
const activeSubscriptions = meta.subscriptions?.active_subscriptions;
50+
if (!Array.isArray(activeSubscriptions))
51+
throw new Error('Server did not return valid subscription balances for the authorized wallet.');
52+
const subscriptionBalance = activeSubscriptions.reduce((total, subscription) => {
53+
if (!Number.isFinite(subscription?.wallet_balance))
54+
throw new Error('Server did not return valid subscription balances for the authorized wallet.');
55+
return total + subscription.wallet_balance;
56+
}, 0);
57+
wallet = {type: 'personal', balance: meta.balance,
58+
subscription_balance: subscriptionBalance, frozen: meta.frozen};
4859
} else if (workspace?.type === 'group' && typeof workspace.group_uuid === 'string' && workspace.group_uuid) {
4960
const result = await post('group/group_info', {group_uuid: workspace.group_uuid});
5061
if (result.group_meta?.uuid !== workspace.group_uuid)
@@ -56,5 +67,13 @@ export async function accountInfo(endpoint, {token = process.env.HYPER3D_ACCESS_
5667
}
5768
if (!Number.isFinite(wallet.balance) || !Number.isFinite(wallet.frozen))
5869
throw new Error('Server did not return a valid balance for the authorized wallet.');
59-
return {authenticated: true, user: {uuid: meta.user_uuid, username: meta.username}, wallet};
70+
const displayedWallet = {
71+
...wallet,
72+
balance: wallet.balance / WALLET_UNITS_PER_CREDIT,
73+
frozen: wallet.frozen / WALLET_UNITS_PER_CREDIT,
74+
...(wallet.type === 'personal' ? {
75+
subscription_balance: wallet.subscription_balance / WALLET_UNITS_PER_CREDIT,
76+
} : {}),
77+
};
78+
return {authenticated: true, user: {uuid: meta.user_uuid, username: meta.username}, wallet: displayedWallet};
6079
}

‎packages/cli/src/index.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ authentication.command('login')
2828
.option('--no-browser', 'Print the verification link and code without opening a browser')
2929
.action(async options => { await login(cli.opts().endpoint, {browser: options.browser}); print({authenticated: true}); });
3030
authentication.command('logout').action(async () => { await logout(cli.opts().endpoint); print({localCredentialsRemoved: true}); });
31-
authentication.command('status').alias('info').description('Show your account and authorized wallet balance')
31+
authentication.command('status').alias('info').description('Show your account and authorized wallet credit balances')
3232
.action(async () => print(await accountInfo(cli.opts().endpoint)));
3333
const operation = action => {
3434
const contract = selectedContract();

‎test/account.test.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {accountInfo} from '../packages/cli/src/account.js';
1111
import {createProvider, loadCredentials, CLI_CLIENT_ID} from '../packages/cli/src/auth.js';
1212

1313
const endpoint = 'https://api.example.com/api/mcp';
14-
const user = {user_uuid: 'user-1', username: 'fixture', balance: 125, frozen: 5};
14+
const user = {user_uuid: 'user-1', username: 'fixture', balance: 125, frozen: 5,
15+
subscriptions: {active_subscriptions: [{wallet_balance: 40}, {wallet_balance: 60}]}};
1516
const json = (body, status = 200) => new Response(JSON.stringify(body), {
1617
status, headers: {'Content-Type': 'application/json'},
1718
});
@@ -44,7 +45,7 @@ test('personal account status uses the account HTTP API and its authorized walle
4445
}});
4546
assert.deepEqual(calls, ['https://api.example.com/api/user/get_info']);
4647
assert.deepEqual(result, {authenticated: true, user: {uuid: 'user-1', username: 'fixture'},
47-
wallet: {type: 'personal', balance: 125, frozen: 5}});
48+
wallet: {type: 'personal', balance: 12.5, subscription_balance: 10, frozen: 0.5}});
4849
});
4950

5051
test('team account status requests only the team bound to the grant', async () => {
@@ -56,7 +57,7 @@ test('team account status requests only the team bound to the grant', async () =
5657
: json({group_meta: {uuid: 'team-1', name: 'Fixture team', frozen: 9}, balance: 800});
5758
}});
5859
assert.deepEqual(calls, [['/api/user/get_info', {}], ['/api/group/group_info', {group_uuid: 'team-1'}]]);
59-
assert.deepEqual(result.wallet, {type: 'group', group_uuid: 'team-1', name: 'Fixture team', balance: 800, frozen: 9});
60+
assert.deepEqual(result.wallet, {type: 'group', group_uuid: 'team-1', name: 'Fixture team', balance: 80, frozen: 0.9});
6061
});
6162

6263
test('account status reports HTTP, server and network errors without treating them as logout', async () => {
@@ -74,6 +75,8 @@ test('account status rejects missing workspace, invalid identity and invalid wal
7475
[{meta: user}, /authorized billing workspace/],
7576
[{meta: {...user, user_uuid: ''}, billing_workspace: {type: 'personal'}}, /Invalid user information/],
7677
[{meta: {...user, balance: '125'}, billing_workspace: {type: 'personal'}}, /valid balance/],
78+
[{meta: {...user, subscriptions: null}, billing_workspace: {type: 'personal'}}, /valid subscription balances/],
79+
[{meta: {...user, subscriptions: {active_subscriptions: [{wallet_balance: null}]}}, billing_workspace: {type: 'personal'}}, /valid subscription balances/],
7780
[{meta: {...user, frozen: null}, billing_workspace: {type: 'personal'}}, /valid balance/],
7881
]) await assert.rejects(accountInfo(endpoint, {token: 'fixture-token', fetchFn: async () => json(body)}), message);
7982
let calls = 0;
@@ -127,7 +130,8 @@ test('auth status and info alias return JSON over real HTTP; authorization error
127130
try {
128131
for (const command of ['status', 'info']) {
129132
const result = await run(command, 'fixture-token');
130-
assert.equal(JSON.parse(result.stdout).wallet.balance, 125);
133+
assert.deepEqual(JSON.parse(result.stdout).wallet, {
134+
type: 'personal', balance: 12.5, subscription_balance: 10, frozen: 0.5});
131135
assert.equal(result.stderr, '');
132136
}
133137
assert.equal(JSON.parse((await run('status', '')).stdout).authenticated, false);

0 commit comments

Comments
 (0)