Skip to content

Commit 6f9f260

Browse files
committed
Merge branch 'add_logic' into add_user_stats
2 parents 1ee5e45 + b78932b commit 6f9f260

File tree

21 files changed

+349
-124
lines changed

21 files changed

+349
-124
lines changed

.github/workflows/indexer-build-and-push-dev-staging.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on: # yamllint disable-line rule:truthy
88
- 'release/indexer/v[0-9]+.x' # e.g. release/indexer/v1.x
99
# TODO(DEC-837): Customize github build and push to ECR by service with paths
1010
- add_user_stats
11+
- 'add_logic'
1112

1213
jobs:
1314
# Build and push to dev

.github/workflows/protocol-build-and-push-snapshot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on: # yamllint disable-line rule:truthy
77
- 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x
88
- 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x
99
- add_user_stats
10+
- 'add_logic'
1011

1112
jobs:
1213
build-and-push-snapshot-dev:

.github/workflows/protocol-build-and-push.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on: # yamllint disable-line rule:truthy
77
- 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x
88
- 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x
99
- add_user_stats
10+
- 'add_logic'
1011

1112
jobs:
1213
build-and-push-dev:

indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,22 @@ export interface EpochStats_UserWithStatsSDKType {
105105
user: string;
106106
stats?: UserStatsSDKType;
107107
}
108-
/** GlobalStats stores global stats */
108+
/** GlobalStats stores global stats for the rolling window (default 30d). */
109109

110110
export interface GlobalStats {
111111
/** Notional USDC traded in quantums */
112112
notionalTraded: Long;
113113
}
114-
/** GlobalStats stores global stats */
114+
/** GlobalStats stores global stats for the rolling window (default 30d). */
115115

116116
export interface GlobalStatsSDKType {
117117
/** Notional USDC traded in quantums */
118118
notional_traded: Long;
119119
}
120-
/** UserStats stores stats for a User */
120+
/**
121+
* UserStats stores stats for a User. This is the sum of all stats for a user in
122+
* the rolling window (default 30d).
123+
*/
121124

122125
export interface UserStats {
123126
/** Taker USDC in quantums */
@@ -129,7 +132,10 @@ export interface UserStats {
129132

130133
affiliateRevenueGeneratedQuantums: Long;
131134
}
132-
/** UserStats stores stats for a User */
135+
/**
136+
* UserStats stores stats for a User. This is the sum of all stats for a user in
137+
* the rolling window (default 30d).
138+
*/
133139

134140
export interface UserStatsSDKType {
135141
/** Taker USDC in quantums */

indexer/services/comlink/src/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ export const configSchema = {
107107
SOLANA_SPONSOR_PUBLIC_KEY: parseString({ default: '' }),
108108
SKIP_SLIPPAGE_TOLERANCE_PERCENTAGE: parseString({ default: '0' }),
109109
TURNKEY_EMAIL_SENDER_ADDRESS: parseString({ default: 'notifications@mail.dydx.trade' }),
110-
TURNKEY_EMAIL_SENDER_NAME: parseString({ default: 'dydx Notifications' }),
110+
TURNKEY_EMAIL_SENDER_NAME: parseString({ default: 'dYdX Notifications' }),
111111
// Alchemy auth token for the skip bridge.
112112
ALCHEMY_AUTH_TOKEN: parseString({ default: '' }),
113+
ALCHEMY_API_KEY: parseString({ default: '' }),
113114
ALCHEMY_WEBHOOK_UPDATE_URL: parseString({ default: 'https://dashboard.alchemy.com/api/update-webhook-addresses' }),
114115
// ZeroDev RPC for skip bridge.
115116
ZERODEV_API_KEY: parseString({ default: '' }),
@@ -120,6 +121,8 @@ export const configSchema = {
120121
APPROVAL_SIGNER_PUBLIC_ADDRESS: parseString({ default: '0x3FC11ff27e5373c88EA142d2EdF5492d0839980B' }),
121122
// if policy approvals are enabled.
122123
APPROVAL_ENABLED: parseBoolean({ default: true }),
124+
// largest amount we will tolerate to swap in usdc.
125+
MAXIMUM_BRIDGE_AMOUNT_USDC: parseInteger({ default: 99_900 }),
123126

124127
// webhook ids, defaults to the production webhook id.
125128
ETHEREUM_WEBHOOK_ID: parseString({ default: 'wh_ctbkt6y9hez91xr2' }),

indexer/services/comlink/src/controllers/api/v4/skip-bridge-controller.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
createHash,
3+
} from 'crypto';
4+
15
import { logger, stats } from '@dydxprotocol-indexer/base';
26
import {
37
BridgeInformationTable,
@@ -45,6 +49,7 @@ import {
4549
import {
4650
getSvmSigner, getSkipCallData, getKernelAccount,
4751
buildUserAddresses,
52+
limitAmount,
4853
} from '../../../helpers/skip-helper';
4954
import { trackTurnkeyDepositSubmitted } from '../../../lib/amplitude-helpers';
5055
import { handleControllerError } from '../../../lib/helpers';
@@ -257,14 +262,16 @@ class BridgeController extends Controller {
257262
throw new Error('Failed to derive dYdX address');
258263
}
259264

265+
const amountIn = await limitAmount(chainId, amount, sourceAssetDenom);
266+
260267
const path = await route({
261268
goFast: true,
262269
allowUnsafe: false,
263270
smartRelay: true, // skip recommended to enable for better routes and less faults.
264271
smartSwapOptions: {
265272
splitRoutes: true,
266273
},
267-
amountIn: amount,
274+
amountIn,
268275
sourceAssetDenom,
269276
sourceAssetChainId: chainId,
270277
destAssetChainId: dydxChainId,
@@ -319,7 +326,7 @@ class BridgeController extends Controller {
319326
logger.info({
320327
message: `Broadcasted on ${c}: ${txHash}`,
321328
from: fromAddress,
322-
amount,
329+
amount: amountIn,
323330
sourceAssetDenom,
324331
chainId,
325332
toAddress: fromAddress,
@@ -339,7 +346,7 @@ class BridgeController extends Controller {
339346
message: 'Bridge transaction completed',
340347
fromAddress,
341348
chainId: c,
342-
amount,
349+
amount: amountIn,
343350
sourceAssetDenom,
344351
transactionHash: txHash,
345352
status,
@@ -357,18 +364,20 @@ class BridgeController extends Controller {
357364
const bridgeRecord = {
358365
from_address: fromAddress,
359366
chain_id: c,
360-
amount,
367+
amount: amountIn,
361368
transaction_hash: txHash,
362369
created_at: new Date().toISOString(),
363370
};
364371

365372
await BridgeInformationTable.create(bridgeRecord);
366-
373+
const email = record.email?.trim().toLowerCase();
374+
// sha256 hash email
375+
const emailHash = email ? createHash('sha256').update(email).digest('hex') : record.evm_address;
367376
// Track TurnKey deposit confirmation event in Amplitude
368377
await trackTurnkeyDepositSubmitted(
369-
dydxAddress,
378+
emailHash,
370379
c,
371-
amount,
380+
amountIn,
372381
txHash,
373382
sourceAssetDenom,
374383
);
@@ -377,7 +386,7 @@ class BridgeController extends Controller {
377386
message: 'Bridge transaction tracked',
378387
fromAddress,
379388
chainId: c,
380-
amount,
389+
amount: amountIn,
381390
sourceAssetDenom,
382391
transactionHash: txHash,
383392
explorerLink,
@@ -389,7 +398,7 @@ class BridgeController extends Controller {
389398
message: 'Failed to create bridge information record on tracked',
390399
fromAddress,
391400
chainId: c,
392-
amount,
401+
amount: amountIn,
393402
error: error.message || error,
394403
});
395404
// Don't throw error to avoid breaking the bridge flow
@@ -457,13 +466,15 @@ class BridgeController extends Controller {
457466
if (!record || !record.dydx_address) {
458467
throw new Error('Failed to derive dYdX address');
459468
}
469+
// we cannot bridge more than the max amount allowed through the bridge.
470+
const amountToUse = await limitAmount(chainId, amount, sourceAssetDenom);
460471
let callData: Parameters<SmartAccountImplementation['encodeCalls']>[0] = [];
461472
try {
462473
callData = await getSkipCallData(
463474
srcAddress,
464475
sourceAssetDenom,
465476
record.dydx_address,
466-
amount,
477+
amountToUse,
467478
chainId,
468479
);
469480
} catch (error) {
@@ -534,7 +545,7 @@ class BridgeController extends Controller {
534545
const bridgeRecord: BridgeInformationCreateObject = {
535546
from_address: fromAddress,
536547
chain_id: chainId,
537-
amount,
548+
amount: amountToUse,
538549
transaction_hash: receipt.transactionHash,
539550
created_at: new Date().toISOString(),
540551
};
@@ -543,12 +554,22 @@ class BridgeController extends Controller {
543554
);
544555

545556
// Track TurnKey deposit confirmation event in Amplitude
546-
const dydxAddress = await getDydxAddress(fromAddress, chainId);
547-
if (dydxAddress) {
557+
if (record.email) {
558+
const email = record.email.trim().toLowerCase();
559+
// sha256 hash email
560+
const emailHash = createHash('sha256').update(email).digest('hex');
561+
await trackTurnkeyDepositSubmitted(
562+
emailHash,
563+
chainId,
564+
amountToUse,
565+
receipt.transactionHash,
566+
sourceAssetDenom,
567+
);
568+
} else {
548569
await trackTurnkeyDepositSubmitted(
549-
dydxAddress,
570+
record.evm_address,
550571
chainId,
551-
amount,
572+
amountToUse,
552573
receipt.transactionHash,
553574
sourceAssetDenom,
554575
);

indexer/services/comlink/src/helpers/alchemy-helpers.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { findByEvmAddress, findBySmartAccountAddress, findBySvmAddress } from '@
33
import { TurnkeyUserFromDatabase } from '@dydxprotocol-indexer/postgres/build/src/types';
44
import { getKernelAddressFromECDSA } from '@zerodev/ecdsa-validator';
55
import { getEntryPoint, KERNEL_V3_1 } from '@zerodev/sdk/constants';
6+
import { Alchemy } from 'alchemy-sdk';
67
import { decode, encode } from 'bech32';
78
import express from 'express';
89
import {
@@ -54,6 +55,10 @@ export const publicClients = Object.keys(chains).reduce((acc, chainId) => {
5455
return acc;
5556
}, {} as Record<string, PublicClient>);
5657

58+
const alchemy = new Alchemy({
59+
apiKey: config.ALCHEMY_API_KEY,
60+
});
61+
5762
export async function addAddressesToAlchemyWebhook(evm?: string, svm?: string): Promise<void> {
5863
const errors: string[] = [];
5964

@@ -300,3 +305,38 @@ export function verifyAlchemyWebhook(
300305
}
301306
return next();
302307
}
308+
309+
export async function getETHPrice(): Promise<number> {
310+
try {
311+
const price = await alchemy.prices.getTokenPriceBySymbol(['ETH']);
312+
313+
// Check if we have valid price data
314+
if (!price.data || price.data.length === 0) {
315+
throw new Error('No price data returned from Alchemy API');
316+
}
317+
318+
const priceData = price.data[0];
319+
if (!priceData.prices || priceData.prices.length === 0) {
320+
if (priceData.error) {
321+
throw new Error(`Alchemy API error: ${priceData.error.message}`);
322+
}
323+
throw new Error('No price data available for ETH');
324+
}
325+
326+
return parseFloat(priceData.prices[0].value);
327+
} catch (error) {
328+
// Properly serialize error for logging
329+
const errorDetails = {
330+
message: error instanceof Error ? error.message : String(error),
331+
name: error instanceof Error ? error.name : 'Unknown',
332+
stack: error instanceof Error ? error.stack : undefined,
333+
};
334+
335+
logger.error({
336+
at: 'alchemy-helpers#getETHPrice',
337+
message: 'Failed to get ETH price',
338+
error: errorDetails,
339+
});
340+
throw error;
341+
}
342+
}

0 commit comments

Comments
 (0)