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
10 changes: 5 additions & 5 deletions src/access/access.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ export class AccessService {
private readonly contracts?: ContractClient,
private readonly onDiscrepancy?: (payload: DiscrepancyHookPayload) => void | Promise<void>,
private readonly verifySignedResponses = false,
private readonly trustedSignerAddress?: string
private readonly trustedSignerAddress?: string,
private readonly strictAddressChecksum = false,
) {}

public async checkAccess(params: AccessCheckParams): Promise<AccessCheckResult>;
public async checkAccess(params: AccessCheckParams, options: RequestOptions & { includeMeta: true }): Promise<{ data: AccessCheckResult; meta: ResponseMetadata }>;
public async checkAccess(params: AccessCheckParams, options?: RequestOptions): Promise<AccessCheckResult | { data: AccessCheckResult; meta: ResponseMetadata}> {
const { walletAddress, guildId, resourceId } = params;

validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.strictAddressChecksum });
validateGuildId(guildId);
validateResourceId(resourceId);

Expand Down Expand Up @@ -188,7 +189,7 @@ export class AccessService {
): Promise<AccessCheckBatchResult[] | AccessCheckBatchByResourceResult> {
if (!Array.isArray(input)) {
const { walletAddress, guildId, resourceIds } = input;
validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.strictAddressChecksum });
validateGuildId(guildId);
if (!Array.isArray(resourceIds) || resourceIds.length === 0) {
throw new GuildPassConfigError('resourceIds array must not be empty', GuildPassErrorCode.INVALID_INPUT);
Expand Down Expand Up @@ -281,7 +282,7 @@ export class AccessService {
// GuildPass SDK: Local block-scoped constant reference.
const { walletAddress, guildId, roleId } = params;

validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.strictAddressChecksum });
validateGuildId(guildId);
validateRoleId(roleId);

Expand Down Expand Up @@ -315,4 +316,3 @@ export class AccessService {
}
// GuildPass SDK: End of logic containment structure block.
}

10 changes: 6 additions & 4 deletions src/client/GuildPassClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class GuildPassClient {
);

const validateResponses = this.config.validateResponses ?? false;
const strictAddressChecksum = this.config.strictAddressChecksum ?? false;

// IMPORTANT: Instantiate Contracts first so we can pass it to Access
const rawContracts = new ContractClient(this.config, this.http);
Expand All @@ -131,10 +132,11 @@ export class GuildPassClient {
rawContracts,
this.config.hooks?.onDiscrepancy,
verifySignedResponses,
trustedSignerAddress
trustedSignerAddress,
strictAddressChecksum,
);
const rawMembership = new MembershipService(this.http, validateResponses);
const rawRoles = new RolesService(this.http, validateResponses, rawAccess);
const rawMembership = new MembershipService(this.http, validateResponses, strictAddressChecksum);
const rawRoles = new RolesService(this.http, validateResponses, rawAccess, strictAddressChecksum);
const rawGuilds = new GuildsService(
this.http,
validateResponses,
Expand Down Expand Up @@ -191,7 +193,7 @@ export class GuildPassClient {
* Useful when a wallet's on-chain state has changed (e.g., token transfer).
*/
public async invalidateWalletCache(walletAddress: string): Promise<void> {
validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });
if (!this.cache) return;
const wallet = normaliseAddress(walletAddress);
try {
Expand Down
1 change: 1 addition & 0 deletions src/config/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export const DEFAULT_CONFIG: Partial<GuildPassClientConfig> = {
chainId: 1, // Ethereum Mainnet
timeoutMs: 10000, // 10 seconds
validateResponses: false,
strictAddressChecksum: false,
// GuildPass SDK: End of logic containment structure block.
};
20 changes: 13 additions & 7 deletions src/config/sdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type GuildPassClientConfig = {
fetch?: FetchLike;
rateLimit?: RateLimitConfig;
validateResponses?: boolean;
/** Enforces EIP-55 checksums for all addresses accepted by the SDK. @default false */
strictAddressChecksum?: boolean;
cache?: CacheAdapter;
cacheTtl?: number;
/**
Expand Down Expand Up @@ -169,6 +171,10 @@ export function validateConfig(config: GuildPassClientConfig): void {
throwConfigError('deduplication must be a boolean', 'deduplication', 'invalid_type', config.deduplication);
}

if (config.strictAddressChecksum !== undefined && typeof config.strictAddressChecksum !== 'boolean') {
throwConfigError('strictAddressChecksum must be a boolean', 'strictAddressChecksum', 'invalid_type', config.strictAddressChecksum);
}

if (config.contractProvider !== undefined) {
const provider = config.contractProvider;
const required = ['ethCall', 'batchEthCall'] as const;
Expand All @@ -185,7 +191,7 @@ export function validateConfig(config: GuildPassClientConfig): void {

if (config.multicallAddress !== undefined) {
try {
validateAddress(config.multicallAddress);
validateAddress(config.multicallAddress, { strict: config.strictAddressChecksum });
} catch {
throw new GuildPassConfigError(
'Invalid multicallAddress: expected a valid EVM address',
Expand Down Expand Up @@ -215,7 +221,7 @@ export function validateConfig(config: GuildPassClientConfig): void {

if (config.trustedSignerAddress !== undefined) {
try {
validateAddress(config.trustedSignerAddress);
validateAddress(config.trustedSignerAddress, { strict: config.strictAddressChecksum });
} catch {
throw new GuildPassConfigError(
'Invalid trustedSignerAddress: expected a valid EVM address',
Expand Down Expand Up @@ -321,15 +327,15 @@ export function validateConfig(config: GuildPassClientConfig): void {

validateContractReadConsensus(config.contractReadConsensus);

validateChainsConfig(config.chains);
validateChainsConfig(config.chains, config.strictAddressChecksum);

const transport = config.fetch ?? globalThis.fetch;
if (typeof transport !== 'function') {
throwConfigError('A fetch-compatible transport is required.', 'fetch', 'required', null);
}
}

function validateChainsConfig(chains?: Record<number, ChainConfig>): void {
function validateChainsConfig(chains?: Record<number, ChainConfig>, strictAddressChecksum = false): void {
if (!chains) return;

for (const [chainIdKey, chainConfig] of Object.entries(chains)) {
Expand Down Expand Up @@ -374,7 +380,7 @@ function validateChainsConfig(chains?: Record<number, ChainConfig>): void {

if (chainConfig.contractAddress !== undefined) {
try {
validateAddress(chainConfig.contractAddress);
validateAddress(chainConfig.contractAddress, { strict: strictAddressChecksum });
} catch (err: any) {
throw new GuildPassConfigError(
`Invalid chains[${chainIdKey}].contractAddress: expected a valid EVM address`,
Expand All @@ -392,7 +398,7 @@ function validateChainsConfig(chains?: Record<number, ChainConfig>): void {

if (chainConfig.multicallAddress !== undefined) {
try {
validateAddress(chainConfig.multicallAddress);
validateAddress(chainConfig.multicallAddress, { strict: strictAddressChecksum });
} catch (err: any) {
throw new GuildPassConfigError(
`Invalid chains[${chainIdKey}].multicallAddress: expected a valid EVM address`,
Expand Down Expand Up @@ -545,4 +551,4 @@ export function resolveChainConfig(config: GuildPassClientConfig, chainId: numbe
contractAddress: config.contractAddress,
multicallAddress: config.multicallAddress,
};
}
}
32 changes: 16 additions & 16 deletions src/contracts/contractClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export class ContractClient {
const chainConfig = this.getChainConfig(chainId);
const contractAddress = params.contractAddress ?? chainConfig.contractAddress;

validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });

if (!contractAddress) {
throw new GuildPassConfigError(
Expand All @@ -432,7 +432,7 @@ export class ContractClient {
);
}

validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const data = `${BALANCE_OF_SELECTOR}${encodeAddressArgument(walletAddress)}`;
const result = await this.resolveSingleEthCall(
Expand Down Expand Up @@ -466,7 +466,7 @@ export class ContractClient {
GuildPassErrorCode.INVALID_CONFIG,
);
}
validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const result = await provider.ethCall({ to: contractAddress, data: DECIMALS_SELECTOR }, options);

Expand Down Expand Up @@ -518,7 +518,7 @@ export class ContractClient {
options?: RequestOptions,
): Promise<MembershipTokenBalancesResult> {
const { walletAddress, contractAddress } = params;
validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });

// Collect every chain ID we should query.
const chainIds: number[] = [];
Expand Down Expand Up @@ -578,8 +578,8 @@ export class ContractClient {
const { walletAddress, chainId, contractAddress } = params;
const chainConfig = this.getChainConfig(chainId);

validateAddress(walletAddress);
validateAddress(contractAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const data = `${BALANCE_OF_SELECTOR}${encodeAddressArgument(walletAddress)}`;
const result = await this.resolveSingleEthCall(
Expand Down Expand Up @@ -608,8 +608,8 @@ export class ContractClient {
const { walletAddress, tokenId, chainId, contractAddress } = params;
const chainConfig = this.getChainConfig(chainId);

validateAddress(walletAddress);
validateAddress(contractAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const data = `${ERC721_OWNER_OF_SELECTOR}${encodeUint256Argument(tokenId, 'tokenId')}`;
const result = await this.resolveSingleEthCall(
Expand Down Expand Up @@ -638,8 +638,8 @@ export class ContractClient {
const { walletAddress, tokenId, chainId, contractAddress } = params;
const chainConfig = this.getChainConfig(chainId);

validateAddress(walletAddress);
validateAddress(contractAddress);
validateAddress(walletAddress, { strict: this.config.strictAddressChecksum });
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const data = `${ERC1155_BALANCE_OF_SELECTOR}${encodeAddressArgument(walletAddress)}${encodeUint256Argument(tokenId, 'tokenId')}`;
const result = await this.resolveSingleEthCall(
Expand Down Expand Up @@ -682,7 +682,7 @@ export class ContractClient {
);
}

validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

const signature = buildFunctionSignature(abi);
const selector = getFunctionSelector(signature);
Expand Down Expand Up @@ -728,7 +728,7 @@ export class ContractClient {
);
}

validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });
const data = `${GET_GUILD_OWNER_SELECTOR}${encodeGuildId(guildId)}`;

const result = await this.resolveSingleEthCall(
Expand Down Expand Up @@ -1213,7 +1213,7 @@ export class ContractClient {
GuildPassErrorCode.INVALID_INPUT,
);
}
validateAddress(call.to);
validateAddress(call.to, { strict: this.config.strictAddressChecksum });
}

// Resolve precedence at the leaf path so chunked recursion also routes
Expand Down Expand Up @@ -1261,7 +1261,7 @@ export class ContractClient {

// Validate all addresses upfront
for (const addr of walletAddresses) {
validateAddress(addr);
validateAddress(addr, { strict: this.config.strictAddressChecksum });
}

const chainConfig = this.getChainConfig(chainId);
Expand All @@ -1285,7 +1285,7 @@ export class ContractClient {
);
}

validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

// Build the batch calls
const calls: BatchEthCallItem[] = walletAddresses.map((addr) => ({
Expand Down Expand Up @@ -1370,7 +1370,7 @@ export class ContractClient {
);
}

validateAddress(contractAddress);
validateAddress(contractAddress, { strict: this.config.strictAddressChecksum });

// Build the batch calls
const calls: BatchEthCallItem[] = guildIds.map((gid) => ({
Expand Down
3 changes: 2 additions & 1 deletion src/membership/membership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class MembershipService {
constructor(
private readonly http: HttpClient,
private readonly validateResponses = false,
private readonly strictAddressChecksum = false,
) {}

/**
Expand All @@ -36,7 +37,7 @@ export class MembershipService {
// GuildPass SDK: Local block-scoped constant reference.
const { walletAddress, guildId } = params;

validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.strictAddressChecksum });
validateGuildId(guildId);

// GuildPass SDK: Terminate function block execution and return.
Expand Down
3 changes: 2 additions & 1 deletion src/roles/roles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class RolesService {
private readonly http: HttpClient,
private readonly validateResponses = false,
private readonly access?: AccessService,
private readonly strictAddressChecksum = false,
) {}

/**
Expand Down Expand Up @@ -59,7 +60,7 @@ export class RolesService {
public async getUserRoles(params: GetUserRolesParams, options?: RequestOptions): Promise<any> {
const { walletAddress, guildId, cursor, limit } = params;

validateAddress(walletAddress);
validateAddress(walletAddress, { strict: this.strictAddressChecksum });
validateGuildId(guildId);

const path = `/guilds/${encodePathSegment(guildId)}/members/${encodePathSegment(normaliseAddress(walletAddress))}/roles`;
Expand Down
45 changes: 45 additions & 0 deletions tests/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,3 +969,48 @@ describe('Service Modules', () => {
});
});
});

describe('strictAddressChecksum', () => {
const nonChecksummedAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';

it('rejects non-checksummed addresses in AccessService', async () => {
const client = new GuildPassClient({
apiUrl: 'https://api.test.com',
strictAddressChecksum: true,
});

await expect(client.access.checkAccess({
walletAddress: nonChecksummedAddress,
guildId: 'guild_1',
resourceId: 'res_1',
})).rejects.toMatchObject({ code: GuildPassErrorCode.INVALID_ADDRESS });
});

it('rejects non-checksummed addresses in MembershipService', async () => {
const client = new GuildPassClient({
apiUrl: 'https://api.test.com',
strictAddressChecksum: true,
});

await expect(client.membership.getMembership({
walletAddress: nonChecksummedAddress,
guildId: 'guild_1',
})).rejects.toMatchObject({ code: GuildPassErrorCode.INVALID_ADDRESS });
});

it('accepts non-checksummed addresses by default', async () => {
const fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ hasAccess: true }),
headers: new Headers(),
});
const client = new GuildPassClient({ apiUrl: 'https://api.test.com', fetch });

await expect(client.access.checkAccess({
walletAddress: nonChecksummedAddress,
guildId: 'guild_1',
resourceId: 'res_1',
})).resolves.toEqual({ hasAccess: true });
});
});