Skip to content

Conversation

@flopez7
Copy link
Contributor

@flopez7 flopez7 commented Oct 24, 2025

Issue tracking

Close #3618, #3560, #3619, #3633

Context behind the change

Reputation Oracle:

  • Added ExchangeApiKeysModule for managing exchange API keys.
  • Added validation for exchange names using ExchangeNameValidator.
  • Integrated AES encryption for storing API keys securely.
  • Implemented exchange-specific clients (GateExchangeClient, MexcExchangeClient) for interacting with respective APIs.
  • Enhanced AuthService to check staking eligibility based on exchange balances.
  • Added configuration services for encryption and staking parameters.
  • Created migration for the exchange_api_keys table in the database.

Human App:

  • Added new stake_allowed property to JWT
  • Check in all the necessary controllers if we're allowed to interact based on this stake property.

How has this been tested?

In progress

Release plan

Add new AES_ENCRYPTION_KEY and STAKING_ELIGIBILITY_ENABLED in Reputation Oracle

Potential risks; What to monitor; Rollback plan

Not yet defined

- Added ExchangeApiKeysModule for managing exchange API keys.
- Added validation for exchange names using ExchangeNameValidator.
- Integrated AES encryption for storing API keys securely.
- Implemented exchange-specific services (GateExchangeService, MexcExchangeService) for interacting with respective APIs.
- Enhanced AuthService to check staking eligibility based on exchange balances.
- Added configuration services for encryption and staking parameters.
- Created migration for the exchange_api_keys table in the database.
@flopez7 flopez7 requested review from Dzeranov and dnechay October 24, 2025 14:40
@flopez7 flopez7 self-assigned this Oct 24, 2025
@vercel
Copy link

vercel bot commented Oct 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

5 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
faucet-frontend Ignored Ignored Preview Oct 31, 2025 3:11pm
faucet-server Ignored Ignored Preview Oct 31, 2025 3:11pm
human-app Skipped Skipped Oct 31, 2025 3:11pm
human-dashboard-frontend Skipped Skipped Oct 31, 2025 3:11pm
staking-dashboard Skipped Skipped Oct 31, 2025 3:11pm

@vercel vercel bot temporarily deployed to Preview – human-app October 24, 2025 16:52 Inactive
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 24, 2025 16:52 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 24, 2025 16:52 Inactive
@flopez7 flopez7 marked this pull request as draft October 27, 2025 09:48
Copy link
Collaborator

@dnechay dnechay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a though: it can probably make sense to have just exchanges module where you have all the logic of working with api keys and exchange APIs to avoid circular dependencies and simplify how you instantiate exchange client with api keys of specific user

Review summary
Implementation-wise (i.e. not taking into account code-style/refactoring comments) I have a question regarding of UX of this feature.

Right now in case of errors from exchange (no matter what error: access error or network error) we always fallback to 0 balance. It can lead to situations when some request had a short "hiccup" (dumb network error) and as a result user has "not eligible stake" for the whole JWT lifetime, and for the user it will look like they just don't have any oracles/jobs even if they have necessary staked amount, so that would be very confusing and only thing on how they can understand that is to reach our support, we go to logs, see error and etc. Another case is that they staked after the sing in: they will have to wait until token refresh.

Would be nice to discuss some options of how we can improve this (if we need to, because maybe it's fine to just let them know about "delays"). Maybe instead of having stake_eligible: boolean we can have some sort of stake_status: 'eligible' | 'error' | 'not_eligible' that will help UI to distinguish between different states and to show some hints + ability to trigger refresh. That might be an overhead though, so let's discuss it internally

- Introduced ExchangeClientFactory to handle exchange client creation for different exchanges.
- Implemented GateExchangeClient and MexcExchangeClient for API interactions.
- Removed deprecated ExchangeRouterService and related services.
- Updated error handling to include ExchangeApiClientError for better clarity.
@vercel vercel bot temporarily deployed to Preview – human-app October 28, 2025 14:58 Inactive
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 28, 2025 14:58 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 28, 2025 14:58 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 28, 2025 15:19 Inactive
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 28, 2025 15:19 Inactive
@vercel vercel bot temporarily deployed to Preview – human-app October 28, 2025 15:19 Inactive
Copy link
Collaborator

@dnechay dnechay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall

  1. Please change back volume for services as mentioned in the earlier comment
  2. Let's wait for UI/UX to make sure they are aligned
  3. Can add unit tests where necessary (e.g. for auth.service)
  4. Let's try to DRY send request logic in each of exchange clients (i.e. separately for each exchange, not one logic shared across all of them)

…geClient, MexcExchangeClient and exchangeApiKeyService
@flopez7 flopez7 added the do-not-merge PR shouldn't be merged until this label is removed label Oct 29, 2025
…ndling utility for improved error handling and code consistency
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 30, 2025 09:11 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 30, 2025 09:12 Inactive
@vercel vercel bot temporarily deployed to Preview – human-app October 30, 2025 09:12 Inactive
@vercel vercel bot temporarily deployed to Preview – human-app October 30, 2025 10:53 Inactive
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 30, 2025 10:53 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 30, 2025 10:53 Inactive
@flopez7 flopez7 requested a review from dnechay October 30, 2025 10:55
@flopez7 flopez7 marked this pull request as ready for review October 30, 2025 10:55
Copy link
Collaborator

@dnechay dnechay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIce tests coverage!

LGTM overall. Some comments on tests improvements + need to wait for final UI/UX

…tructure; add signature generation for API requests
@vercel vercel bot temporarily deployed to Preview – human-app October 31, 2025 15:11 Inactive
@vercel vercel bot temporarily deployed to Preview – staking-dashboard October 31, 2025 15:11 Inactive
@vercel vercel bot temporarily deployed to Preview – human-dashboard-frontend October 31, 2025 15:11 Inactive
@flopez7 flopez7 requested a review from dnechay October 31, 2025 15:22
Comment on lines +66 to +80
const timestamp = faker.number.int();
const client = new MexcExchangeClient({ apiKey, secretKey });

jest.spyOn(Date, 'now').mockReturnValue(timestamp);
const result = client['getSignedQuery']();
expect(result).toHaveProperty('query');
expect(result).toHaveProperty('signature');
expect(result.query).toBe(`timestamp=${timestamp}&recvWindow=5000`);

const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(result.query)
.digest('hex');
expect(result.signature).toBe(expectedSignature);
jest.restoreAllMocks();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's correctly mock timers where needed

Suggested change
const timestamp = faker.number.int();
const client = new MexcExchangeClient({ apiKey, secretKey });
jest.spyOn(Date, 'now').mockReturnValue(timestamp);
const result = client['getSignedQuery']();
expect(result).toHaveProperty('query');
expect(result).toHaveProperty('signature');
expect(result.query).toBe(`timestamp=${timestamp}&recvWindow=5000`);
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(result.query)
.digest('hex');
expect(result.signature).toBe(expectedSignature);
jest.restoreAllMocks();
const client = new MexcExchangeClient({ apiKey, secretKey });
const now = Date.now();
jest.useFakeTimers({ now });
const result = client['getSignedQuery']();
jest.useRealTimers();
expect(result).toHaveProperty('query');
expect(result).toHaveProperty('signature');
expect(result.query).toBe(`timestamp=${now}&recvWindow=5000`);
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(result.query)
.digest('hex');
expect(result.signature).toBe(expectedSignature);

Comment on lines 115 to 117
await expect(client.checkRequiredAccess()).rejects.toBeInstanceOf(
ExchangeApiClientError,
);
Copy link
Collaborator

@dnechay dnechay Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to wrap this call to try..catch and check that thrownError is what you expect later like

let thrownError;
try {
  await client.checkRequiredAccess()
} catch (error) {
  thrownError = error;
}

scope.done();

expect(thrownError).toBeInstanceOf(ExchangeApiClientError);
expect(thrownError.message).toBe('message');

otherwise in case this test fails and scope.done is not called - it can affect other test runs

.get(path)
.query(true)
.replyWithError('network error');
await expect(client.getAccountBalance(asset)).rejects.toBeInstanceOf(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: must be wrapped into try-catch

.get(path)
.query(true)
.replyWithError('network error');
await expect(client.checkRequiredAccess()).rejects.toBeInstanceOf(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be in try-catch

const path = '/spot/accounts';
const query = `currency=${encodeURIComponent(asset)}`;
const body = '';
const ts = String(Math.floor(Date.now() / 1000));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to move this part with timestamp generation to signGateRequest and return both signature and timestamp for request from it

const secretKey = faker.string.sample();
const client = new GateExchangeClient(
{ apiKey, secretKey },
{ timeoutMs: 1234 },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
{ timeoutMs: 1234 },
{ timeoutMs: faker.number.int() },

.get(path)
.query(true)
.replyWithError('network error');
await expect(client.getAccountBalance(asset)).rejects.toBeInstanceOf(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be in try catch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge PR shouldn't be merged until this label is removed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Human App] Require staking for accessing jobs on Human App

3 participants