Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/lib/aoconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import {
createDataItemSigner as createDataItemSignerNode,
} from "@permaweb/aoconnect/node";

const CU_URL = process.env.CU_URL || "https://cu.ao-testnet.xyz";
const OUR_CU_URL = "https://gateway.ar";

export const aoInstance = connect({ MODE: "legacy" });
export const customAoInstance = connect({ MODE: "legacy", CU_URL });
export const ourAoInstance = connect({ MODE: "legacy", CU_URL: OUR_CU_URL });

export const createDataItemSigner = createDataItemSignerNode;
148 changes: 109 additions & 39 deletions src/lib/tier.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { retryWithDelay } from "@/utils/retry.utils";
import { customAoInstance, ourAoInstance } from "./aoconnect";
import { ourAoInstance } from "./aoconnect";
import { redis } from "./redis";
import { isArweaveAddress } from "@/utils/address.utils";

type TierWallet = {
interface TierWalletBase {
balance: string;
rank: number | "";
tier: number;
progress: number;
}

type TierWallet = TierWalletBase & {
snapshotTimestamp: number;
totalHolders: number;
};

type TierWalletHB = TierWalletBase & {
"snapshot-timestamp": number;
"total-holders": number;
};

type WalletsTierInfo = Record<string, TierWallet>;

type CachedWalletsTierInfo = {
Expand Down Expand Up @@ -78,6 +87,10 @@ const Tiers = [
},
];

const TIER_PROCESS_ID = "rkAezEIgacJZ_dVuZHOKJR8WKpSDqLGfgPJrs_Es7CA";

const ONE_DAY_MS = 24 * 60 * 60 * 1000; // 1 day in milliseconds

function getTierThresholds(totalWallets: number) {
const tierThresholds = [];

Expand Down Expand Up @@ -135,53 +148,110 @@ function getWalletTier(walletRank: number, totalWallets: number): number {
}

async function getWalletsTierInfoFromAo() {
const { wallets, snapshotTimestamp } =
await retryWithDelay<WalletsTierInfoFromAo>(async (attempt) => {
const instance = attempt % 2 === 0 ? customAoInstance : ourAoInstance;
try {
const response = await fetch(
`https://hyperbeam.ar/${TIER_PROCESS_ID}~process@1.0/now/wallets-tier-info/~json@1.0/serialize/?bundle`
);
if (!response.ok) {
throw new Error("Failed to fetch wallets tier info from HB");
}
const data = (await response.json()) as Record<string, TierWalletHB>;

let firstWallet: TierWallet | null = null;
const walletsTierInfo: Record<string, TierWallet> = {};

for (const [addr, wallet] of Object.entries(data)) {
if (isArweaveAddress(addr)) {
const transformedWallet: TierWallet = {
balance: wallet.balance,
rank: wallet.rank,
tier: wallet.tier,
progress: wallet.progress,
snapshotTimestamp: wallet["snapshot-timestamp"],
totalHolders: wallet["total-holders"],
};
if (!firstWallet) {
firstWallet = transformedWallet;
}
walletsTierInfo[addr] = transformedWallet;
}
}

const result = await instance.dryrun({
process: "rkAezEIgacJZ_dVuZHOKJR8WKpSDqLGfgPJrs_Es7CA",
tags: [{ name: "Action", value: "Get-Wallets" }],
});
if (!firstWallet) {
throw new Error("No valid wallet data found");
}

const data = result?.Messages?.[0]?.Data;
if (!data) {
throw new Error("No data returned from AO");
}
const snapshotTimestamp = firstWallet.snapshotTimestamp;
const totalWallets = firstWallet.totalHolders;
const actualTotalWallets = Object.keys(walletsTierInfo).length;

const parsedData = JSON.parse(data);
if (!parsedData?.wallets || !parsedData?.snapshotTimestamp) {
throw new Error("Invalid response from AO");
}
if (!snapshotTimestamp || !totalWallets) {
throw new Error("Invalid response from HB");
}

return parsedData;
});
if (actualTotalWallets !== totalWallets) {
throw new Error("Total wallets mismatch");
}

const totalWallets = wallets.length;
// Ensure snapshot is not older than 1 day
const timestampDiff = Date.now() - snapshotTimestamp;
if (timestampDiff > ONE_DAY_MS) {
throw new Error("Snapshot data is too old - needs refresh");
}

const walletsTierInfo = wallets.reduce(
(acc: Record<string, TierWallet>, wallet, index) => {
const walletRank = index + 1;
const tier = getWalletTier(walletRank, totalWallets);
const progress = calculateTierProgressPercent(walletRank, totalWallets);
return {
walletsTierInfo,
snapshotTimestamp,
totalWallets,
};
} catch (error) {
console.error("Fallback to dryrun due to HB fetch error: ", error);
const { wallets, snapshotTimestamp } =
await retryWithDelay<WalletsTierInfoFromAo>(async () => {
const result = await ourAoInstance.dryrun({
process: TIER_PROCESS_ID,
tags: [{ name: "Action", value: "Get-Wallets" }],
});

const data = result?.Messages?.[0]?.Data;
if (!data) {
throw new Error("No data returned from AO");
}

const parsedData = JSON.parse(data);
if (!parsedData?.wallets || !parsedData?.snapshotTimestamp) {
throw new Error("Invalid response from AO");
}

return parsedData;
});

const walletData = {
balance: wallet.balance,
rank: walletRank,
tier,
progress,
snapshotTimestamp,
totalHolders: totalWallets,
};
const totalWallets = wallets.length;

acc[wallet.address] = walletData;
const walletsTierInfo = wallets.reduce(
(acc: Record<string, TierWallet>, wallet, index) => {
const walletRank = index + 1;
const tier = getWalletTier(walletRank, totalWallets);
const progress = calculateTierProgressPercent(walletRank, totalWallets);

return acc;
},
{}
);
const walletData = {
balance: wallet.balance,
rank: walletRank,
tier,
progress,
snapshotTimestamp,
totalHolders: totalWallets,
};

acc[wallet.address] = walletData;

return { walletsTierInfo, snapshotTimestamp, totalWallets };
return acc;
},
{}
);

return { walletsTierInfo, snapshotTimestamp, totalWallets };
}
}

export async function getWalletsTierInfo(addresses: string[]) {
Expand Down