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
4 changes: 4 additions & 0 deletions packages/x402/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Use `@metamask/utils` for Ethereum address checksum validation and drop `viem` as a package dependency. ([#246](https://github.com/MetaMask/smart-accounts-kit/pull/246))

## [0.1.0]

### Added
Expand Down
7 changes: 4 additions & 3 deletions packages/x402/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@metamask/utils": "^11.4.0"
},
"peerDependencies": {
"@x402/core": "^2.12.0",
"@x402/evm": "^2.12.0",
"viem": "^2.31.4"
"@x402/evm": "^2.12.0"
},
"devDependencies": {
"@metamask/auto-changelog": "^5.0.2",
Expand All @@ -68,7 +70,6 @@
"prettier": "^3.5.3",
"tsup": "^8.5.0",
"typescript": "5.5.4",
"viem": "2.31.4",
"vitest": "^3.2.4"
}
}
31 changes: 31 additions & 0 deletions packages/x402/src/ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
getChecksumAddress,
isHexChecksumAddress,
type Hex,
} from '@metamask/utils';

/**
* Validate and normalize an Ethereum address to its EIP-55 checksum form.
*
* @param value - Address string to normalize.
* @returns The checksummed address.
*/
export function getAddress(value: string): Hex {
if (!isHexChecksumAddress(value)) {
throw new Error('Invalid Ethereum address');
}

const lowerAddress = value.toLowerCase();
const upperAddress = `0x${value.slice(2).toUpperCase()}`;
const checksummedAddress = getChecksumAddress(value);

if (
value !== lowerAddress &&
value !== upperAddress &&
value !== checksummedAddress
Comment thread
cursor[bot] marked this conversation as resolved.
) {
throw new Error('Invalid Ethereum address checksum');
}

return checksummedAddress;
}
6 changes: 4 additions & 2 deletions packages/x402/src/x402Client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type Hex, getAddress, isHex } from 'viem';
import { isStrictHexString, type Hex } from '@metamask/utils';

import { getAddress } from './ethereum';

export type x402PaymentRequirements = {
scheme: string;
Expand Down Expand Up @@ -49,7 +51,7 @@ export type x402Erc7710ClientConfig = {
function normalizeDelegationPayload(
payload: x402DelegationPaymentPayload,
): x402DelegationPaymentPayload {
if (!isHex(payload.permissionContext) || payload.permissionContext === '0x') {
if (!isStrictHexString(payload.permissionContext)) {
throw new Error(
'Invalid delegation payload: permissionContext must be non-empty hex data',
);
Expand Down
7 changes: 4 additions & 3 deletions packages/x402/src/x402Server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Address, getAddress } from 'viem';
import type { Hex } from '@metamask/utils';

import { getAddress } from './ethereum';
import type { x402PaymentRequirements } from './x402Client';

export type x402Erc7710ServerConfig = {
Expand All @@ -14,7 +15,7 @@ export type x402Erc7710ServerConfig = {
*/
function validateFacilitatorAddresses(
publishedAddresses: unknown,
): Address[] | undefined {
): Hex[] | undefined {
if (publishedAddresses === undefined) {
return undefined;
}
Expand All @@ -31,7 +32,7 @@ function validateFacilitatorAddresses(
);
}

const normalizedAddresses: Address[] = [];
const normalizedAddresses: Hex[] = [];
const validationErrors: string[] = [];

publishedAddresses.forEach((address, index) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/x402/test/x402Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ describe('x402Erc7710Server', () => {
]);
});

it('normalizes all-uppercase facilitatorAddresses', async () => {
const server = new x402Erc7710Server();

const result = await server.enhancePaymentRequirements(baseRequirements, {
extra: {
facilitatorAddresses: ['0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
},
});

expect(result.extra?.facilitatorAddresses).toEqual([
'0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa',
]);
});

it('throws when facilitatorAddresses is not an array', async () => {
const server = new x402Erc7710Server();

Expand Down Expand Up @@ -156,4 +170,18 @@ describe('x402Erc7710Server', () => {
'Invalid facilitatorAddresses specified: facilitatorAddresses[0] must be a string; facilitatorAddresses[1] is not a valid address: "not-an-address"',
);
});

it('throws for invalid mixed-case facilitatorAddress checksums', async () => {
const server = new x402Erc7710Server();

await expect(
server.enhancePaymentRequirements(baseRequirements, {
extra: {
facilitatorAddresses: ['0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAB'],
},
}),
).rejects.toThrow(
'Invalid facilitatorAddresses specified: facilitatorAddresses[0] is not a valid address: "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAB"',
);
});
});
3 changes: 1 addition & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -862,18 +862,17 @@ __metadata:
resolution: "@metamask/x402@workspace:packages/x402"
dependencies:
"@metamask/auto-changelog": "npm:^5.0.2"
"@metamask/utils": "npm:^11.4.0"
"@x402/core": "npm:^2.12.0"
"@x402/evm": "npm:^2.12.0"
eslint: "npm:^9.39.2"
prettier: "npm:^3.5.3"
tsup: "npm:^8.5.0"
typescript: "npm:5.5.4"
viem: "npm:2.31.4"
vitest: "npm:^3.2.4"
peerDependencies:
"@x402/core": ^2.12.0
"@x402/evm": ^2.12.0
viem: ^2.31.4
languageName: unknown
linkType: soft

Expand Down
Loading