Skip to content

Commit 1db8852

Browse files
Merge branch 'dev' into feat/proposal-finished-notification
2 parents d410510 + ea8ea74 commit 1db8852

5 files changed

Lines changed: 188 additions & 6 deletions

File tree

.github/workflows/deploy.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches: [dev, main]
6+
7+
jobs:
8+
changes:
9+
runs-on: ubuntu-latest
10+
outputs:
11+
triggers: ${{ steps.filter.outputs.triggers }}
12+
dispatcher: ${{ steps.filter.outputs.dispatcher }}
13+
subscriptions: ${{ steps.filter.outputs.subscriptions }}
14+
consumers: ${{ steps.filter.outputs.consumers }}
15+
steps:
16+
- uses: actions/checkout@v3
17+
- uses: dorny/paths-filter@v2
18+
id: filter
19+
with:
20+
base: ${{ github.ref_name }}
21+
filters: |
22+
triggers:
23+
- 'apps/logic-system/src/**/*.ts'
24+
- 'packages/**/*.ts'
25+
dispatcher:
26+
- 'apps/dispatcher/src/**/*.ts'
27+
- 'packages/**/*.ts'
28+
subscriptions:
29+
- 'apps/subscription-server/src/**/*.ts'
30+
consumers:
31+
- 'apps/consumers/src/**/*.ts'
32+
- 'packages/**/*.ts'
33+
34+
deploy:
35+
runs-on: ubuntu-latest
36+
needs: changes
37+
container: ghcr.io/railwayapp/cli:latest
38+
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'dev' }}
39+
env:
40+
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
41+
steps:
42+
- uses: actions/checkout@v3
43+
- name: Determine environment
44+
id: env
45+
run: |
46+
if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.base_ref }}" == "main" ]]; then
47+
echo "environment=production" >> $GITHUB_OUTPUT
48+
else
49+
echo "environment=dev" >> $GITHUB_OUTPUT
50+
fi
51+
- name: Deploy triggers
52+
if: ${{ needs.changes.outputs.triggers == 'true' }}
53+
run: railway up -s logic-system -e ${{ steps.env.outputs.environment }} --ci
54+
- name: Deploy subscriptions
55+
if: ${{ needs.changes.outputs.subscriptions == 'true' }}
56+
run: railway up -s subscription-server -e ${{ steps.env.outputs.environment }} --ci
57+
- name: Deploy dispatcher
58+
if: ${{ needs.changes.outputs.dispatcher == 'true' || needs.changes.outputs.subscriptions == 'true' }}
59+
run: railway up -s dispatcher -e ${{ steps.env.outputs.environment }} --ci
60+
- name: Deploy consumers
61+
if: ${{ needs.changes.outputs.consumers == 'true' }}
62+
run: railway up -s consumer-telegram -e ${{ steps.env.outputs.environment }} --ci

apps/consumers/src/services/dao.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class DAOService {
1717
private daoEmojis = new Map<string, string>([
1818
['UNI', '🦄'],
1919
['ENS', '🔷'],
20-
['OP', '🔴']
20+
['OP', '🔴'],
2121
]);
2222

2323
constructor(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { formatTokenAmount } from './number-formatter';
2+
3+
describe('formatTokenAmount', () => {
4+
describe('with 18 decimals (default)', () => {
5+
it('should format zero correctly', () => {
6+
expect(formatTokenAmount('0')).toBe('0');
7+
expect(formatTokenAmount(0)).toBe('0');
8+
});
9+
10+
it('should format very small amounts as <0.1', () => {
11+
expect(formatTokenAmount('1')).toBe('<0.1'); // 1 wei
12+
expect(formatTokenAmount('50000000000000000')).toBe('<0.1'); // 0.05 token
13+
expect(formatTokenAmount('99999999999999999')).toBe('0.1'); // 0.099... token (rounds to 0.1)
14+
});
15+
16+
it('should format small amounts with 1 decimal', () => {
17+
expect(formatTokenAmount('100000000000000000')).toBe('0.1'); // 0.1 token
18+
expect(formatTokenAmount('100000000000000001')).toBe('0.1'); // 0.1+ token (rounded)
19+
expect(formatTokenAmount('500000000000000000')).toBe('0.5'); // 0.5 token
20+
expect(formatTokenAmount('1000000000000000000')).toBe('1.0'); // 1 token
21+
expect(formatTokenAmount('1500000000000000000')).toBe('1.5'); // 1.5 tokens
22+
expect(formatTokenAmount('999000000000000000000')).toBe('999.0'); // 999 tokens
23+
});
24+
25+
it('should format thousands with K suffix', () => {
26+
expect(formatTokenAmount('1000000000000000000000')).toBe('1.0K'); // 1,000 tokens
27+
expect(formatTokenAmount('1234000000000000000000')).toBe('1.2K'); // 1,234 tokens
28+
expect(formatTokenAmount('12345000000000000000000')).toBe('12.3K'); // 12,345 tokens
29+
expect(formatTokenAmount('999999000000000000000000')).toBe('1.0M'); // 999,999 tokens (rounded up to 1M)
30+
});
31+
32+
it('should format millions with M suffix', () => {
33+
expect(formatTokenAmount('1000000000000000000000000')).toBe('1.0M'); // 1 million tokens
34+
expect(formatTokenAmount('1234567000000000000000000')).toBe('1.2M'); // 1.234567 million tokens
35+
expect(formatTokenAmount('999999999000000000000000000')).toBe('1.0B'); // ~1 billion tokens (rounded up to 1B)
36+
});
37+
38+
it('should format billions with B suffix', () => {
39+
expect(formatTokenAmount('1000000000000000000000000000')).toBe('1.0B'); // 1 billion tokens
40+
expect(formatTokenAmount('1234567890000000000000000000')).toBe('1.2B'); // 1.23... billion tokens
41+
});
42+
43+
it('should use exponential notation for very large numbers', () => {
44+
expect(formatTokenAmount('1000000000000000000000000000000')).toBe('1.0e+12'); // trillion (uses exponential)
45+
});
46+
47+
it('should promote units when rounding results in 1000+', () => {
48+
// Test promotion from K to M (999.95K rounds to 1000.0K -> 1.0M)
49+
expect(formatTokenAmount('999950000000000000000000')).toBe('1.0M');
50+
51+
// Test promotion from M to B (999.95M rounds to 1000.0M -> 1.0B)
52+
expect(formatTokenAmount('999950000000000000000000000')).toBe('1.0B');
53+
});
54+
});
55+
56+
describe('with custom decimals', () => {
57+
it('should handle 6 decimals (like USDC)', () => {
58+
expect(formatTokenAmount('1000000', 6)).toBe('1.0'); // 1 USDC
59+
expect(formatTokenAmount('1500000', 6)).toBe('1.5'); // 1.5 USDC
60+
expect(formatTokenAmount('1000000000', 6)).toBe('1.0K'); // 1,000 USDC
61+
});
62+
63+
it('should handle 8 decimals (like WBTC)', () => {
64+
expect(formatTokenAmount('100000000', 8)).toBe('1.0'); // 1 WBTC
65+
expect(formatTokenAmount('50000000', 8)).toBe('0.5'); // 0.5 WBTC
66+
expect(formatTokenAmount('100000000000', 8)).toBe('1.0K'); // 1,000 WBTC
67+
});
68+
});
69+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Utility functions for formatting numbers in human-readable notation
3+
*/
4+
5+
/**
6+
* Formats token amounts from wei to human-readable format
7+
* @param amount The amount in wei (as string or number)
8+
* @param decimals The number of decimals for the token (default: 18)
9+
* @returns Formatted string with appropriate suffix (K, M, B, etc)
10+
*/
11+
export function formatTokenAmount(amount: string | number, decimals: number = 18): string {
12+
// Convert wei string to number, handling 18 decimal places
13+
const divisor = Math.pow(10, decimals);
14+
const actualAmount = Number(amount) / divisor;
15+
16+
// Handle zero
17+
if (actualAmount === 0) return '0';
18+
19+
// Handle very small amounts (less than 0.1)
20+
if (actualAmount < 0.1) return '<0.1';
21+
22+
// For amounts less than 1000, show with 1 decimal place
23+
if (actualAmount < 1000) {
24+
return actualAmount.toFixed(1);
25+
}
26+
27+
// For larger amounts, use K, M, B notation
28+
const units = ['', 'K', 'M', 'B'];
29+
let unitIndex = Math.floor(Math.log10(actualAmount) / 3);
30+
31+
if (unitIndex >= units.length) {
32+
return actualAmount.toExponential(1);
33+
}
34+
35+
let scaledAmount = actualAmount / Math.pow(1000, unitIndex);
36+
let formatted = scaledAmount.toFixed(1);
37+
38+
// If rounded result is >= 1000, promote to next unit
39+
if (parseFloat(formatted) >= 1000) {
40+
unitIndex++;
41+
if (unitIndex >= units.length) {
42+
return actualAmount.toExponential(1);
43+
}
44+
scaledAmount = actualAmount / Math.pow(1000, unitIndex);
45+
formatted = scaledAmount.toFixed(1);
46+
}
47+
48+
return formatted + units[unitIndex];
49+
}

apps/dispatcher/src/services/triggers/voting-power-trigger.service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DispatcherMessage, MessageProcessingResult } from "../../interfaces/dis
22
import { ISubscriptionClient, User } from "../../interfaces/subscription-client.interface";
33
import { NotificationClientFactory } from "../notification/notification-factory.service";
44
import { BaseTriggerHandler } from "./base-trigger.service";
5+
import { formatTokenAmount } from "../../lib/number-formatter";
56
import crypto from 'crypto';
67

78
/**
@@ -62,23 +63,24 @@ export class VotingPowerTriggerHandler extends BaseTriggerHandler {
6263
let notificationMessage = '';
6364

6465
const deltaValue = delta ? parseInt(delta) : 0;
66+
const formattedDelta = formatTokenAmount(Math.abs(deltaValue));
6567

6668
if (changeType === 'delegation') {
6769
if (deltaValue >= 0) {
68-
notificationMessage = `🥳 You've received a new delegation in ${daoId}!\n${sourceAccountId} delegated to you, increasing your voting power by ${deltaValue}.`;
70+
notificationMessage = `🥳 You've received a new delegation in ${daoId}!\n${sourceAccountId} delegated to you, increasing your voting power by ${formattedDelta}.`;
6971
} else if (deltaValue < 0) {
70-
notificationMessage = `🥺 A delegator just undelegated in ${daoId}!\n${sourceAccountId} removed their delegation, reducing your voting power by ${deltaValue}.`;
72+
notificationMessage = `🥺 A delegator just undelegated in ${daoId}!\n${sourceAccountId} removed their delegation, reducing your voting power by ${formattedDelta}.`;
7173
}
7274
} else if (changeType === 'transfer') {
7375
if (deltaValue >= 0) {
74-
notificationMessage = `📈 Your voting power increased in ${daoId}!\nYou gained ${deltaValue} voting power from token transfer activity.`;
76+
notificationMessage = `📈 Your voting power increased in ${daoId}!\nYou gained ${formattedDelta} voting power from token transfer activity.`;
7577
} else if (deltaValue < 0) {
76-
notificationMessage = `📉 Your voting power decreased in ${daoId}!\nYou lost ${Math.abs(deltaValue)} voting power from token transfer activity.`;
78+
notificationMessage = `📉 Your voting power decreased in ${daoId}!\nYou lost ${formattedDelta} voting power from token transfer activity.`;
7779
}
7880
} else {
7981
// Generic voting power change
8082
if (deltaValue !== 0) {
81-
notificationMessage = `⚡ Your voting power has changed in ${daoId}!\nVoting power updated by ${deltaValue}.`;
83+
notificationMessage = `⚡ Your voting power has changed in ${daoId}!\nVoting power updated by ${formattedDelta}.`;
8284
} else {
8385
notificationMessage = `⚡ Your voting power has changed in ${daoId}!\nVoting power activity detected.`;
8486
}

0 commit comments

Comments
 (0)