Skip to content

Commit

Permalink
Added top 100 wallets table to the stats page, and fixed liquid suppl…
Browse files Browse the repository at this point in the history
…y concentration to correctly remove community wallets from the calculations
  • Loading branch information
hemulin authored and minaxolone committed May 27, 2024
1 parent 914f650 commit 0349305
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 44 deletions.
2 changes: 1 addition & 1 deletion api/src/ol/ol.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,6 @@ for (const role of roles) {
...workers,
],
controllers: [OlController],
exports: [OlService, TransformerService],
exports: [OlService, TransformerService, CommunityWalletsResolver],
})
export class OlModule {}
171 changes: 128 additions & 43 deletions api/src/stats/stats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { ClickhouseService } from "../clickhouse/clickhouse.service.js";
import { OlService } from "../ol/ol.service.js";
import _ from "lodash";
import { CommunityWalletsResolver } from "../ol/community-wallets.resolver.js";

@Injectable()
export class StatsService {
Expand All @@ -23,6 +24,7 @@ export class StatsService {
public constructor(
private readonly clickhouseService: ClickhouseService,
private readonly olService: OlService,
private readonly communityWalletsResolver: CommunityWalletsResolver,
config: ConfigService,
) {
this.dataApiHost = config.get("dataApiHost")!;
Expand Down Expand Up @@ -86,6 +88,10 @@ export class StatsService {
await this.calculateLiquidityConcentrationLocked();
console.timeEnd("calculateLiquidityConcentrationLocked");

console.time("getTopUnlockedBalanceWallets");
const topAccounts = await this.getTopUnlockedBalanceWallets(100, supplyStats.circulatingSupply);
console.timeEnd("getTopUnlockedBalanceWallets");

// calculate KPIS
// circulating
const circulatingSupply = {
Expand Down Expand Up @@ -153,6 +159,7 @@ export class StatsService {
clearingBidoverTime: pofValues.clearingBidOverTime, // net rewards? also available on the pofValues object
liquidSupplyConcentration: liquidSupplyConcentration,
lockedSupplyConcentration: lockedSupplyConcentration,
topAccounts,

// kpis
circulatingSupply,
Expand Down Expand Up @@ -1034,15 +1041,17 @@ export class StatsService {
slowWalletsUnlockedBalances: { address: string; unlockedBalance: number }[],
): Promise<{ address: string; balance: number }[]> {
try {
// Convert community wallets to a format suitable for the SQL query
const communityWalletsFormatted = communityWallets
.map((wallet) => `'${wallet}'`)
.join(",");
function getLast15Chars(address: string): string {
return address.slice(-15).toUpperCase();
}

// Convert community wallets to a Set for easy lookup
const communityAddresses = new Set(communityWallets.map(wallet => wallet.toUpperCase()));

// Convert slow wallets to a map for easy access
const slowWalletsMap = new Map(
slowWalletsUnlockedBalances.map((wallet) => [
wallet.address,
wallet.address.toUpperCase(),
wallet.unlockedBalance,
]),
);
Expand All @@ -1055,7 +1064,6 @@ export class StatsService {
max(version) AS latest_version
FROM coin_balance
WHERE coin_module = 'libra_coin'
AND NOT has([${communityWalletsFormatted}], hex(address))
GROUP BY address
`;

Expand All @@ -1074,56 +1082,40 @@ export class StatsService {
return [];
}

// Extract versions and convert them to timestamps
const versions = rows.map((row) => row.latest_version);
const chunkSize = 1000; // Adjust chunk size as necessary
const versionChunks = this.chunkArray<number>(versions, chunkSize);

const allTimestampMappings = (
await Promise.all(
versionChunks.map((chunk) => this.mapVersionsToTimestamps(chunk)),
)
).flat();

// Use a Map for faster version-to-timestamp lookup
const versionToTimestampMap = new Map<number, number>(
allTimestampMappings.map(({ version, timestamp }) => [
version,
timestamp,
]),
);

// Map addresses to their balances and corresponding timestamps
const addressBalanceMap = new Map<
string,
{ balance: number; timestamp: number }
>();
rows.forEach((row) => {
const version = row.latest_version;
const timestamp = versionToTimestampMap.get(version) ?? 0;
addressBalanceMap.set(row.address, {
balance: row.balance,
timestamp,
});
// Create a map of address to latest balance, excluding community wallets
const addressBalanceMap = new Map<string, number>();
rows.forEach(row => {
const address = row.address.toUpperCase();
if (!communityAddresses.has(address)) {
addressBalanceMap.set(address, row.balance);
}
});

// Adjust balances for slow wallets
const result = rows.map((row) => {
const unlockedBalance = slowWalletsMap.get(row.address);
// Check if there's an unlocked balance, if not, use the original balance
if (unlockedBalance !== undefined) {
return { address: row.address, balance: unlockedBalance };
slowWalletsUnlockedBalances.forEach(row => {
const address = getLast15Chars(row.address);
for (const [key, value] of addressBalanceMap.entries()) {
if (getLast15Chars(key) === address) {
addressBalanceMap.set(key, row.unlockedBalance);
break;
}
}
return { address: row.address, balance: row.balance };
});

// Convert the map to an array
const result = Array.from(addressBalanceMap.entries()).map(([address, balance]) => ({
address,
balance,
}));

return result;
} catch (error) {
console.error("Error in getAllWalletsBalances:", error);
throw error;
}
}


private binGenericBalances(
balances: BalanceItem[],
ranges: { min: number; max: number }[],
Expand Down Expand Up @@ -1282,4 +1274,97 @@ export class StatsService {
};
});
}

private async getTopUnlockedBalanceWallets(limit: number, circulatingSupply: number): Promise<{ address: string; unlockedBalance: number; percentOfCirculating: number }[]> {
try {
function toHexString(decimalString: string): string {
return BigInt(decimalString).toString(16).toUpperCase();
}

function getLast15Chars(address: string): string {
return address.slice(-15).toUpperCase();
}

// Get the list of community wallets
const communityWallets = await this.communityWalletsResolver.communityWallets();
const communityAddresses = new Set(communityWallets.map(wallet => wallet.address.toString('hex').toUpperCase()));

// Query to get the latest balances and versions from coin_balance
const coinBalanceQuery = `
SELECT
address,
argMax(balance, version) AS latest_balance
FROM coin_balance
WHERE coin_module = 'libra_coin'
GROUP BY address
`;

const coinBalanceResultSet = await this.clickhouseService.client.query({
query: coinBalanceQuery,
format: "JSONEachRow",
});

const coinBalanceRows: Array<{
address: string;
latest_balance: number;
}> = await coinBalanceResultSet.json();

if (!coinBalanceRows.length) {
return [];
}

// Create a map of address to latest balance
const addressBalanceMap = new Map<string, number>();
coinBalanceRows.forEach(row => {
const address = toHexString(row.address).toUpperCase();
if (!communityAddresses.has(address)) {
addressBalanceMap.set(address, row.latest_balance / 1e6);
}
});

// Query to get the latest unlocked balances from slow_wallet
const slowWalletQuery = `
SELECT
hex(address) AS address,
argMax(unlocked, version) / 1e6 AS unlocked_balance
FROM slow_wallet
GROUP BY address
`;

const slowWalletResultSet = await this.clickhouseService.client.query({
query: slowWalletQuery,
format: "JSONEachRow",
});

const slowWalletRows: Array<{
address: string;
unlocked_balance: number;
}> = await slowWalletResultSet.json();

// Adjust balances: replace balance with unlocked balance for slow wallets
slowWalletRows.forEach(row => {
const address = getLast15Chars(row.address);
for (const [key, value] of addressBalanceMap.entries()) {
if (getLast15Chars(key) === address) {
addressBalanceMap.set(key, row.unlocked_balance);
break;
}
}
});

// Convert the map to an array and calculate percentOfCirculating
const result = Array.from(addressBalanceMap.entries()).map(([address, unlockedBalance]) => ({
address,
unlockedBalance,
percentOfCirculating: (unlockedBalance / circulatingSupply) * 100,
}));

// Sort by unlockedBalance and take the top N
result.sort((a, b) => b.unlockedBalance - a.unlockedBalance);
return result.slice(0, limit);
} catch (error) {
console.error("Error in getTopUnlockedBalanceWallets:", error);
throw error;
}
}
}
3 changes: 3 additions & 0 deletions api/src/stats/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface Stats {
accountsLocked: BinRange[];
avgTotalVestingTime: BinRange[];
};
topAccounts: Array<{address: string;
unlockedBalance: number;
percentOfCirculating: number;}>
circulatingSupply: RelativeValue;
totalBurned: RelativeValue;
communityWalletsBalance: RelativeValue;
Expand Down
51 changes: 51 additions & 0 deletions web-app/src/modules/core/routes/Stats/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,57 @@ const Coinstats = () => {
</div>
</dl>
</div>
<div className="mt-5">
<h3 className="text-base font-semibold text-gray-900 mb-5">Top 100 Liquid accounts</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Address
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Liquid Balance
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
% of Circulating Supply
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.topAccounts.map((account: { address: string; unlockedBalance: number; percentOfCirculating: number }, index: number) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<a
href={`https://0l.fyi/accounts/${account.address}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700"
>
{account.address}
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<Money>{account.unlockedBalance}</Money>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{account.percentOfCirculating.toFixed(4)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)}
</Page>
Expand Down

0 comments on commit 0349305

Please sign in to comment.