Skip to content
Merged
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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.12.4] - 2026-05-08

- Improve efficiency when fetching ENS data.
- Upgrade dependencies to keep maintenance with latest releases.

## [3.12.3] - 2025-12-12

- Apply security patches to mitigate published CVE-2025-55183 and CVE-2025-55184
Expand Down Expand Up @@ -455,7 +460,8 @@ Staking Pools

- First release

[unreleased]: https://github.com/cartesi/explorer/compare/v3.12.3...HEAD
[unreleased]: https://github.com/cartesi/explorer/compare/v3.12.4...HEAD
[3.12.4]: https://github.com/cartesi/explorer/compare/v3.12.4...v3.12.3
[3.12.3]: https://github.com/cartesi/explorer/compare/v3.12.3...v3.12.2
[3.12.2]: https://github.com/cartesi/explorer/compare/v3.12.2...v3.12.1
[3.12.1]: https://github.com/cartesi/explorer/compare/v3.12.1...v3.12.0
Expand Down
45 changes: 21 additions & 24 deletions __tests__/services/server/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { ObservableQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { GetEnsDomainsQuery } from '../../../src/graphql/queries/ensDomains';
import {
GetAliasedEnsDomainsQuery,
getDomainAlias,
} from '../../../src/graphql/queries/ensDomains';
import client from '../../../src/services/apolloENSClient';
import {
getENSData,
Expand Down Expand Up @@ -56,19 +59,24 @@ const createAvatarUrlResolver = (url: string) => {

const buildENSResponse = () => [
{
createdAt: 1740987259,
id: '1',
name: 'my-pool',
name: 'my-pool.eth',
labelName: 'my-pool',
resolvedAddress: {
id: validAddress,
},
},
];

const setENSQueryReturn = (list: QueriedDomain[]) => {
const response = list.reduce((acc, curr) => {
const alias = getDomainAlias(curr.resolvedAddress?.id ?? '');
acc[alias] = [curr];
return acc;
}, {} as GetAliasedEnsDomainsQuery);

clientMock.query.mockResolvedValue({
data: { domains: list },
} as ObservableQuery.Result<GetEnsDomainsQuery>);
data: response,
} as ObservableQuery.Result<GetAliasedEnsDomainsQuery>);
};

const validAddress = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6';
Expand Down Expand Up @@ -108,21 +116,12 @@ describe('ENS Functions', () => {
});

it('should set the name when returned from ENS service', async () => {
setENSQueryReturn([
{
createdAt: 1740987259,
id: '1',
name: 'my-pool',
resolvedAddress: {
id: validAddress,
},
},
]);
setENSQueryReturn(buildENSResponse());
const {
data: [expectedData],
} = await getENSData([entry]);

expect(expectedData).toHaveProperty('name', 'my-pool');
expect(expectedData).toHaveProperty('name', 'my-pool.eth');
expect(expectedData).toHaveProperty('hasEns', true);
expect(expectedData).toHaveProperty('avatarUrl', '');
});
Expand All @@ -138,7 +137,7 @@ describe('ENS Functions', () => {
data: [expectedData],
} = await getENSData([entry]);

expect(expectedData).toHaveProperty('name', 'my-pool');
expect(expectedData).toHaveProperty('name', 'my-pool.eth');
expect(expectedData).toHaveProperty('hasEns', true);
expect(expectedData).toHaveProperty(
'avatarUrl',
Expand Down Expand Up @@ -202,12 +201,12 @@ describe('ENS Functions', () => {
address: validAddress,
avatarUrl: null,
hasEns: true,
name: 'my-pool',
name: 'my-pool.eth',
});

expect(errorLog).toHaveBeenCalledTimes(1);
expect(errorLog.mock.calls[0][0]).toEqual(
'GET_AVATAR_URL: (my-pool) => Fail to get avatar.\nReason: no name found!'
'GET_AVATAR_URL: (my-pool.eth) => Fail to get avatar.\nReason: no name found!'
);
});
});
Expand All @@ -216,9 +215,7 @@ describe('ENS Functions', () => {
it('should partition request when entries are above the limit', async () => {
setENSQueryReturn([
{
createdAt: 1740987259,
id: '1',
name: 'my-pool',
name: 'my-pool.eth',
resolvedAddress: {
id: validAddress,
},
Expand All @@ -241,7 +238,7 @@ describe('ENS Functions', () => {
...entry,
avatarUrl: 'http://host.com/avatar.png',
hasEns: true,
name: 'my-pool',
name: 'my-pool.eth',
};

expect(expectedPayloads.length).toEqual(2);
Expand Down
8 changes: 8 additions & 0 deletions additional.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ declare namespace NodeJS {
*/
HTTP_MAINNET_NODE_RPC: string;

/**
* @summary Maximum number of concurrent calls when fetching ENS record data using the RPC node.
* @description This is to avoid overwhelming the RPC node with too many requests at once,
* which can lead to rate limiting or timeouts.
* @default 8
*/
ENS_RESOLVER_RPC_CONCURRENT_CALLS: string;

/**
* Maximum number of entries per request when fetching ENS information. It is configurable but default and maximum is 900 entries.
* Therefore, the number here dictates the relation between entries-limit and concurrent calls to be created when this limit is exceeded.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@libsql/client": "^0.17.2",
"@safe-global/safe-apps-provider": "^0.18.6",
"@safe-global/safe-apps-sdk": "^9.1.0",
"@supercharge/promise-pool": "3.3.0",
"@vercel/speed-insights": "^2.0.0",
"@web3-onboard/coinbase": "^2.4.2",
"@web3-onboard/common": "^2.4.2",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 59 additions & 33 deletions src/graphql/queries/ensDomains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,64 @@

import { gql } from '@apollo/client';

export const DOMAINS = gql`
query getDomains(
$where: Domain_filter
$orderBy: Domain_orderBy
$orderDirection: OrderDirection
$first: Int
) {
domains(
where: $where
orderBy: $orderBy
orderDirection: $orderDirection
first: $first
) {
id
name
labelName
createdAt
resolvedAddress {
id
}
export type EnsDomain = {
__typename?: 'Domain';
name?: string | null;
labelName?: string | null;
resolvedAddress?: { __typename?: 'Account'; id: string } | null;
};

export type DomainAlias = `addr${string}`;

export type GetAliasedEnsDomainsQuery = Record<DomainAlias, EnsDomain[]>;

/**
*
* @summary Generates an alias for a given address to be used in the aliased GraphQL query.
* @description
* The alias is generated by prefixing the address with "addr" and replacing any non-alphanumeric characters with underscores.
* This ensures that the alias is a valid GraphQL field name.
* @param address
* @returns
*/
export const getDomainAlias = (address: string): DomainAlias =>
`addr${address.toLowerCase().replace(/[^a-z0-9_]/g, '_')}`;

/**
* @summary Dynamically generates aliased GraphQL query to fetch the latest ENS domain per Address passed.
* @description One address may have multiple domain entries through time.
* Due to constraints in response size it may cause domains to not be included in the response.
* Nonetheless, we want only the latest domain name.
* By aliasing the query there is a perceptible improvement in response time as the database query
* is more efficient due to not using the "in" operator and instead using multiple "where" clauses with equality checks.
*
* @param addresses
* @returns
*/
export const buildAliasedEnsDomainsQuery = (addresses: string[]) => {
const fields = addresses
.map((address) => {
const alias = getDomainAlias(address);
return `
${alias}: domains(
first: 1
where: { resolvedAddress: "${address}" }
orderBy: createdAt
orderDirection: desc
) {
labelName
name
resolvedAddress {
id
}
}
`;
})
.join('\n');

return gql`
query getDomains {
${fields}
}
}
`;

export type GetEnsDomainsQuery = {
__typename?: 'Query';
domains: Array<{
__typename?: 'Domain';
id: string;
name?: string | null;
labelName?: string | null;
createdAt: any;
resolvedAddress?: { __typename?: 'Account'; id: string } | null;
}>;
`;
};
Loading
Loading