Skip to content

Commit 9921dba

Browse files
authored
Merge pull request #156 from blockful/refactor/anticapture_client_to_use_checksum_format
Refactor/anticapture client to use checksum format
2 parents 0558a0c + ee1b52b commit 9921dba

9 files changed

Lines changed: 400 additions & 63 deletions

File tree

apps/consumers/jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testMatch: ['**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': ['ts-jest', {
7+
isolatedModules: true
8+
}]
9+
}
10+
};

apps/integrated-tests/src/mocks/graphql-mock-setup.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
11
import { ProposalData } from '../fixtures';
22
import { ProcessedVotingPowerHistory } from '@notification-system/anticapture-client';
3+
import { getAddress, isAddress } from 'viem';
34

45
/**
56
* @notice Setup class for GraphQL API mocking in integration tests
67
* @dev Provides methods to mock different GraphQL endpoints with test data
78
*/
89
export class GraphQLMockSetup {
10+
/**
11+
* @notice Converts an address to checksummed format if valid
12+
* @dev Simulates API behavior of returning checksummed addresses
13+
*/
14+
private static toChecksum(address: string): string {
15+
if (!address || !isAddress(address)) {
16+
return address;
17+
}
18+
try {
19+
return getAddress(address);
20+
} catch {
21+
return address;
22+
}
23+
}
924
/**
1025
* @notice Transforms ProcessedVotingPowerHistory to raw GraphQL format
1126
*/
1227
private static transformToRawGraphQLFormat(votingPowerData: ProcessedVotingPowerHistory[]): any[] {
1328
return votingPowerData.map(vp => ({
14-
accountId: vp.accountId,
29+
accountId: this.toChecksum(vp.accountId),
1530
timestamp: vp.timestamp,
1631
votingPower: vp.votingPower,
1732
delta: vp.delta || null,
1833
daoId: vp.daoId,
1934
transactionHash: vp.transactionHash,
2035
delegation: vp.delegation ? {
21-
delegatorAccountId: vp.delegation.delegatorAccountId,
36+
delegatorAccountId: this.toChecksum(vp.delegation.delegatorAccountId),
2237
delegatedValue: vp.delegation.delegatedValue
2338
} : null,
2439
transfer: vp.transfer ? {
2540
amount: vp.transfer.amount,
26-
fromAccountId: vp.transfer.fromAccountId,
27-
toAccountId: vp.transfer.toAccountId
41+
fromAccountId: this.toChecksum(vp.transfer.fromAccountId),
42+
toAccountId: this.toChecksum(vp.transfer.toAccountId)
2843
} : null
2944
}));
3045
}
@@ -53,17 +68,27 @@ export class GraphQLMockSetup {
5368
if (config?.headers?.['anticapture-dao-id']) {
5469
filtered = filtered.filter(p => p.daoId === config.headers['anticapture-dao-id']);
5570
}
71+
// Convert proposer addresses to checksum format
72+
const checksummedProposals = filtered.map(p => ({
73+
...p,
74+
proposerAccountId: this.toChecksum(p.proposerAccountId)
75+
}));
5676
return Promise.resolve({
57-
data: { data: { proposals: filtered } }
77+
data: { data: { proposals: checksummedProposals } }
5878
});
5979
}
6080

6181
// Handle single proposal
6282
if (data.query?.includes('GetProposalById')) {
6383
const proposalId = data.variables?.id;
6484
const proposal = proposals.find(p => p.id === proposalId);
85+
// Convert proposer address to checksum if proposal exists
86+
const checksummedProposal = proposal ? {
87+
...proposal,
88+
proposerAccountId: this.toChecksum(proposal.proposerAccountId)
89+
} : null;
6590
return Promise.resolve({
66-
data: { data: { proposal: proposal || null } }
91+
data: { data: { proposal: checksummedProposal } }
6792
});
6893
}
6994

@@ -100,10 +125,11 @@ export class GraphQLMockSetup {
100125
}
101126

102127
// Filter by voterAccountId_in if provided
128+
// Now using exact comparison since AnticaptureClient normalizes addresses
103129
if (voterAccountIdIn) {
104130
filtered = filtered.filter((v: any) =>
105131
voterAccountIdIn.some((addr: string) =>
106-
v.voterAccountId.toLowerCase() === addr.toLowerCase()
132+
this.toChecksum(v.voterAccountId) === addr
107133
)
108134
);
109135
}
@@ -115,8 +141,14 @@ export class GraphQLMockSetup {
115141
);
116142
}
117143

144+
// Convert voter addresses to checksum format (simulating real API behavior)
145+
const checksummedVotes = filtered.map((v: any) => ({
146+
...v,
147+
voterAccountId: this.toChecksum(v.voterAccountId)
148+
}));
149+
118150
return Promise.resolve({
119-
data: { data: { votesOnchains: { items: filtered, totalCount: filtered.length } } }
151+
data: { data: { votesOnchains: { items: checksummedVotes, totalCount: checksummedVotes.length } } }
120152
});
121153
}
122154

packages/anticapture-client/dist/anticapture-client.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ type VotesOnchain = NonNullable<ListVotesOnchainsQuery['votesOnchains']['items']
77
export declare class AnticaptureClient {
88
private readonly httpClient;
99
constructor(httpClient: AxiosInstance);
10+
/**
11+
* Recursively normalizes Ethereum addresses to EIP-55 checksum format
12+
* Detects addresses by their format using viem's isAddress validation
13+
* @param obj - Any value to normalize (primitives, objects, arrays, nested structures)
14+
* @returns The normalized value with checksummed addresses
15+
*/
16+
private normalizeAddresses;
1017
private query;
1118
private buildHeaders;
1219
/**

packages/anticapture-client/dist/anticapture-client.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,45 @@
22
Object.defineProperty(exports, "__esModule", { value: true });
33
exports.AnticaptureClient = void 0;
44
const graphql_1 = require("graphql");
5+
const viem_1 = require("viem");
56
const graphql_2 = require("./gql/graphql");
67
const schemas_1 = require("./schemas");
78
class AnticaptureClient {
89
constructor(httpClient) {
910
this.httpClient = httpClient;
1011
}
12+
/**
13+
* Recursively normalizes Ethereum addresses to EIP-55 checksum format
14+
* Detects addresses by their format using viem's isAddress validation
15+
* @param obj - Any value to normalize (primitives, objects, arrays, nested structures)
16+
* @returns The normalized value with checksummed addresses
17+
*/
18+
normalizeAddresses(obj) {
19+
if (obj == null)
20+
return obj;
21+
if (typeof obj === 'string') {
22+
try {
23+
return (0, viem_1.isAddress)(obj) ? (0, viem_1.getAddress)(obj) : obj;
24+
}
25+
catch {
26+
return obj;
27+
}
28+
}
29+
if (Array.isArray(obj)) {
30+
return obj.map(item => this.normalizeAddresses(item));
31+
}
32+
if (typeof obj === 'object') {
33+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, this.normalizeAddresses(v)]));
34+
}
35+
return obj;
36+
}
1137
async query(document, schema, variables, daoId) {
1238
const headers = this.buildHeaders(daoId);
39+
// Normalize addresses in variables to EIP-55 checksum format
40+
const normalizedVariables = variables ? this.normalizeAddresses(variables) : variables;
1341
const response = await this.httpClient.post('', {
1442
query: (0, graphql_1.print)(document),
15-
variables,
43+
variables: normalizedVariables,
1644
}, { headers });
1745
if (response.data.errors) {
1846
throw new Error(JSON.stringify(response.data.errors));

packages/anticapture-client/dist/gql/graphql.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,11 +1365,11 @@ export type Query_ProposalsActivity_Proposals_Items_Proposal = {
13651365
againstVotes: Scalars['String']['output'];
13661366
daoId: Scalars['String']['output'];
13671367
description?: Maybe<Scalars['String']['output']>;
1368-
endBlock: Scalars['String']['output'];
1368+
endBlock: Scalars['Float']['output'];
13691369
forVotes: Scalars['String']['output'];
13701370
id: Scalars['String']['output'];
13711371
proposerAccountId: Scalars['String']['output'];
1372-
startBlock: Scalars['String']['output'];
1372+
startBlock: Scalars['Float']['output'];
13731373
status: Scalars['String']['output'];
13741374
timestamp: Scalars['String']['output'];
13751375
};
@@ -1659,7 +1659,7 @@ export type Transactions_200_Response = {
16591659
};
16601660
export type Transfer = {
16611661
__typename?: 'transfer';
1662-
amount?: Maybe<Scalars['BigInt']['output']>;
1662+
amount: Scalars['BigInt']['output'];
16631663
daoId: Scalars['String']['output'];
16641664
from?: Maybe<Account>;
16651665
fromAccountId: Scalars['String']['output'];
@@ -1668,11 +1668,11 @@ export type Transfer = {
16681668
isLending: Scalars['Boolean']['output'];
16691669
isTotal: Scalars['Boolean']['output'];
16701670
logIndex: Scalars['Int']['output'];
1671-
timestamp?: Maybe<Scalars['BigInt']['output']>;
1671+
timestamp: Scalars['BigInt']['output'];
16721672
to?: Maybe<Account>;
16731673
toAccountId: Scalars['String']['output'];
16741674
token?: Maybe<Token>;
1675-
tokenId?: Maybe<Scalars['String']['output']>;
1675+
tokenId: Scalars['String']['output'];
16761676
transaction?: Maybe<Transaction>;
16771677
transactionHash: Scalars['String']['output'];
16781678
};
@@ -2080,7 +2080,7 @@ export type ListVotingPowerHistorysQuery = {
20802080
} | null;
20812081
transfer?: {
20822082
__typename?: 'transfer';
2083-
amount?: string | null;
2083+
amount: string;
20842084
fromAccountId: string;
20852085
toAccountId: string;
20862086
} | null;

packages/anticapture-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@graphql-typed-document-node/core": "^3.2.0",
3737
"axios": "^1.7.2",
3838
"graphql": "^16.11.0",
39+
"viem": "^2.34.0",
3940
"zod": "^3.22.4"
4041
}
4142
}

packages/anticapture-client/src/anticapture-client.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AxiosInstance } from 'axios';
22
import { print } from 'graphql';
33
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
44
import { z } from 'zod';
5+
import { getAddress, isAddress } from 'viem';
56
import type {
67
GetProposalByIdQuery,
78
GetProposalByIdQueryVariables,
@@ -25,6 +26,36 @@ export class AnticaptureClient {
2526
this.httpClient = httpClient;
2627
}
2728

29+
/**
30+
* Recursively normalizes Ethereum addresses to EIP-55 checksum format
31+
* Detects addresses by their format using viem's isAddress validation
32+
* @param obj - Any value to normalize (primitives, objects, arrays, nested structures)
33+
* @returns The normalized value with checksummed addresses
34+
*/
35+
private normalizeAddresses(obj: any): any {
36+
if (obj == null) return obj;
37+
38+
if (typeof obj === 'string') {
39+
try {
40+
return isAddress(obj) ? getAddress(obj) : obj;
41+
} catch {
42+
return obj;
43+
}
44+
}
45+
46+
if (Array.isArray(obj)) {
47+
return obj.map(item => this.normalizeAddresses(item));
48+
}
49+
50+
if (typeof obj === 'object') {
51+
return Object.fromEntries(
52+
Object.entries(obj).map(([k, v]) => [k, this.normalizeAddresses(v)])
53+
);
54+
}
55+
56+
return obj;
57+
}
58+
2859
private async query<TResult, TVariables, TSchema extends z.ZodSchema<any>>(
2960
document: TypedDocumentNode<TResult, TVariables>,
3061
schema: TSchema,
@@ -33,9 +64,12 @@ export class AnticaptureClient {
3364
): Promise<z.infer<TSchema>> {
3465
const headers = this.buildHeaders(daoId);
3566

67+
// Normalize addresses in variables to EIP-55 checksum format
68+
const normalizedVariables = variables ? this.normalizeAddresses(variables) : variables;
69+
3670
const response = await this.httpClient.post('', {
3771
query: print(document),
38-
variables,
72+
variables: normalizedVariables,
3973
}, { headers });
4074

4175
if (response.data.errors) {

packages/anticapture-client/src/gql/graphql.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,11 +1493,11 @@ export type Query_ProposalsActivity_Proposals_Items_Proposal = {
14931493
againstVotes: Scalars['String']['output'];
14941494
daoId: Scalars['String']['output'];
14951495
description?: Maybe<Scalars['String']['output']>;
1496-
endBlock: Scalars['String']['output'];
1496+
endBlock: Scalars['Float']['output'];
14971497
forVotes: Scalars['String']['output'];
14981498
id: Scalars['String']['output'];
14991499
proposerAccountId: Scalars['String']['output'];
1500-
startBlock: Scalars['String']['output'];
1500+
startBlock: Scalars['Float']['output'];
15011501
status: Scalars['String']['output'];
15021502
timestamp: Scalars['String']['output'];
15031503
};
@@ -1806,7 +1806,7 @@ export type Transactions_200_Response = {
18061806

18071807
export type Transfer = {
18081808
__typename?: 'transfer';
1809-
amount?: Maybe<Scalars['BigInt']['output']>;
1809+
amount: Scalars['BigInt']['output'];
18101810
daoId: Scalars['String']['output'];
18111811
from?: Maybe<Account>;
18121812
fromAccountId: Scalars['String']['output'];
@@ -1815,11 +1815,11 @@ export type Transfer = {
18151815
isLending: Scalars['Boolean']['output'];
18161816
isTotal: Scalars['Boolean']['output'];
18171817
logIndex: Scalars['Int']['output'];
1818-
timestamp?: Maybe<Scalars['BigInt']['output']>;
1818+
timestamp: Scalars['BigInt']['output'];
18191819
to?: Maybe<Account>;
18201820
toAccountId: Scalars['String']['output'];
18211821
token?: Maybe<Token>;
1822-
tokenId?: Maybe<Scalars['String']['output']>;
1822+
tokenId: Scalars['String']['output'];
18231823
transaction?: Maybe<Transaction>;
18241824
transactionHash: Scalars['String']['output'];
18251825
};
@@ -2165,7 +2165,7 @@ export type ListVotingPowerHistorysQueryVariables = Exact<{
21652165
}>;
21662166

21672167

2168-
export type ListVotingPowerHistorysQuery = { __typename?: 'Query', votingPowerHistorys: { __typename?: 'votingPowerHistoryPage', items: Array<{ __typename?: 'votingPowerHistory', accountId: string, timestamp: string, votingPower: string, delta: string, daoId: string, transactionHash: string, delegation?: { __typename?: 'delegation', delegatorAccountId: string, delegatedValue: string } | null, transfer?: { __typename?: 'transfer', amount?: string | null, fromAccountId: string, toAccountId: string } | null }> } };
2168+
export type ListVotingPowerHistorysQuery = { __typename?: 'Query', votingPowerHistorys: { __typename?: 'votingPowerHistoryPage', items: Array<{ __typename?: 'votingPowerHistory', accountId: string, timestamp: string, votingPower: string, delta: string, daoId: string, transactionHash: string, delegation?: { __typename?: 'delegation', delegatorAccountId: string, delegatedValue: string } | null, transfer?: { __typename?: 'transfer', amount: string, fromAccountId: string, toAccountId: string } | null }> } };
21692169

21702170

21712171
export const GetDaOsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDAOs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"daos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"votingDelay"}},{"kind":"Field","name":{"kind":"Name","value":"chainId"}}]}}]}}]}}]} as unknown as DocumentNode<GetDaOsQuery, GetDaOsQueryVariables>;

0 commit comments

Comments
 (0)