Skip to content

Commit

Permalink
feat: Integrate with moralis for top token holder finding (#94)
Browse files Browse the repository at this point in the history
* feat: add token holder moralis repository

* refactor: substitute goldrush to moralis
  • Loading branch information
yvesfracari authored Oct 29, 2024
1 parent fcfbcbb commit 615821e
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 6 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@
# TENDERLY_PROJECT_NAME=

# ETHPLORER
# ETHPLORER_API_KEY=
# ETHPLORER_API_KEY=

# MORALIS
# MORALIS_API_KEY=
10 changes: 5 additions & 5 deletions apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TokenHolderRepositoryCache,
TokenHolderRepositoryEthplorer,
TokenHolderRepositoryFallback,
TokenHolderRepositoryGoldRush,
TokenHolderRepositoryMoralis,
UsdRepository,
UsdRepositoryCache,
UsdRepositoryCoingecko,
Expand Down Expand Up @@ -112,13 +112,13 @@ function getTokenHolderRepositoryEthplorer(
);
}

function getTokenHolderRepositoryGoldRush(
function getTokenHolderRepositoryMoralis(
cacheRepository: CacheRepository
): TokenHolderRepository {
return new TokenHolderRepositoryCache(
new TokenHolderRepositoryGoldRush(),
new TokenHolderRepositoryMoralis(),
cacheRepository,
'tokenHolderGoldRush',
'tokenHolderMoralis',
DEFAULT_CACHE_VALUE_SECONDS,
DEFAULT_CACHE_NULL_SECONDS
);
Expand All @@ -128,7 +128,7 @@ function getTokenHolderRepository(
cacheRepository: CacheRepository
): TokenHolderRepository {
return new TokenHolderRepositoryFallback([
getTokenHolderRepositoryGoldRush(cacheRepository),
getTokenHolderRepositoryMoralis(cacheRepository),
getTokenHolderRepositoryEthplorer(cacheRepository),
]);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/app/plugins/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const schema = {
ETHPLORER_API_KEY: {
type: 'string',
},
MORALIS_API_KEY: {
type: 'string',
},
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Container } from 'inversify';
import { TokenHolderRepositoryMoralis } from './TokenHolderRepositoryMoralis';
import { SupportedChainId } from '@cowprotocol/shared';
import { WETH, NULL_ADDRESS } from '../../test/mock';
import { MORALIS_API_KEY } from '../datasources/moralis';

describe('TokenHolderRepositoryMoralis', () => {
let tokenHolderRepositoryMoralis: TokenHolderRepositoryMoralis;

beforeAll(() => {
const container = new Container();
container
.bind<TokenHolderRepositoryMoralis>(TokenHolderRepositoryMoralis)
.to(TokenHolderRepositoryMoralis);
tokenHolderRepositoryMoralis = container.get(TokenHolderRepositoryMoralis);
expect(MORALIS_API_KEY).toBeDefined();
});

describe('getTopTokenHolders', () => {
it('should return the top token holders of WETH', async () => {
const tokenHolders =
await tokenHolderRepositoryMoralis.getTopTokenHolders(
SupportedChainId.MAINNET,
WETH
);

expect(tokenHolders?.length).toBeGreaterThan(0);
expect(tokenHolders?.[0].address).toBeDefined();
expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(0);
expect(Number(tokenHolders?.[0].balance)).toBeGreaterThan(
Number(tokenHolders?.[1].balance)
);
}, 100000);

it('should return null for an unknown token', async () => {
const tokenHolders =
await tokenHolderRepositoryMoralis.getTopTokenHolders(
SupportedChainId.MAINNET,
NULL_ADDRESS
);

expect(tokenHolders).toBeNull();
}, 100000);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { injectable } from 'inversify';
import {
TokenHolderPoint,
TokenHolderRepository,
} from './TokenHolderRepository';
import { SupportedChainId } from '@cowprotocol/shared';
import {
MORALIS_API_BASE_URL,
MORALIS_API_KEY,
MORALIS_CLIENT_NETWORK_MAPPING,
} from '../datasources/moralis';

interface MoralisTokenHolderItem {
balance: string;
balance_formated: string;
is_contract: boolean;
owner_address: string;
owner_address_label: string;
entity: string;
entity_logo: string;
usd_value: string;
percentage_relative_to_total_supply: number;
}

interface MoralisTokenHoldersResponse {
result: MoralisTokenHolderItem[];
cursor: string;
page: number;
page_size: number;
}

@injectable()
export class TokenHolderRepositoryMoralis implements TokenHolderRepository {
async getTopTokenHolders(
chainId: SupportedChainId,
tokenAddress: string
): Promise<TokenHolderPoint[] | null> {
const response = (await fetch(
`${MORALIS_API_BASE_URL}/v2.2/erc20/${tokenAddress}/owners?chain=${MORALIS_CLIENT_NETWORK_MAPPING[chainId]}&order=DESC`,
{
method: 'GET',
headers: {
accept: 'application/json',
'X-API-Key': `${MORALIS_API_KEY}`,
},
}
).then((res) => res.json())) as MoralisTokenHoldersResponse;

if (response.result.length === 0) {
return null;
}

return response.result.map((item) => ({
address: item.owner_address,
balance: item.balance,
}));
}
}
12 changes: 12 additions & 0 deletions libs/repositories/src/datasources/moralis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SupportedChainId } from '@cowprotocol/shared';

export const MORALIS_API_KEY = process.env.MORALIS_API_KEY;
export const MORALIS_API_BASE_URL = 'https://deep-index.moralis.io/api';

export const MORALIS_CLIENT_NETWORK_MAPPING: Record<SupportedChainId, string> =
{
[SupportedChainId.MAINNET]: 'eth',
[SupportedChainId.SEPOLIA]: 'sepolia',
[SupportedChainId.GNOSIS_CHAIN]: 'gnosis',
[SupportedChainId.ARBITRUM_ONE]: 'arbitrum',
};
1 change: 1 addition & 0 deletions libs/repositories/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export * from './TokenHolderRepository/TokenHolderRepositoryGoldRush';
export * from './TokenHolderRepository/TokenHolderRepositoryEthplorer';
export * from './TokenHolderRepository/TokenHolderRepositoryCache';
export * from './TokenHolderRepository/TokenHolderRepositoryFallback';
export * from './TokenHolderRepository/TokenHolderRepositoryMoralis';

// Simulation repositories
export * from './SimulationRepository/SimulationRepository';
Expand Down

0 comments on commit 615821e

Please sign in to comment.