Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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": {
"@noble/hashes": "^1.8.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"
}
}
67 changes: 67 additions & 0 deletions packages/x402/src/ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';

export type Address = `0x${string}`;
export type Hex = `0x${string}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also remove the Hex and Address types and use the Hex type exported from @metamask/utils in all places where we reference these types (Hex and Address are functionally identical, so I don't think there's value in distinguishing unless there's an actual difference in the type).


const ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/u;
const HEX_REGEX = /^0x[0-9a-fA-F]*$/u;

/**
* Check whether a value is a 0x-prefixed hexadecimal string.
*
* @param value - Value to inspect.
* @returns True when the value is hex data.
*/
export function isHex(value: unknown): value is Hex {
return typeof value === 'string' && HEX_REGEX.test(value);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the isHex util and HEX_REGEX from here, and use the isStrictHexString function exported from @metamask/utils in all cases where we presently call isHex

It ensures non-zero length '0x' prefixed hex string.


/**
* 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): Address {
if (!ADDRESS_REGEX.test(value)) {
throw new Error('Invalid Ethereum address');
}

const address = value.slice(2);
const lowerAddress = address.toLowerCase();
const upperAddress = address.toUpperCase();
const checksummedAddress = checksumAddress(lowerAddress);

if (
address !== lowerAddress &&
address !== upperAddress &&
`0x${address}` !== checksummedAddress

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using string interpolation here, we can just compare value !== checksummedAddress

Suggested change
`0x${address}` !== checksummedAddress
value !== checksummedAddress

) {
throw new Error('Invalid Ethereum address checksum');
}

return checksummedAddress;
}

/**
* Convert a lowercase Ethereum address body to EIP-55 checksum form.
*
* @param lowerAddress - 40-character lowercase hex address without 0x prefix.
* @returns The checksummed address with 0x prefix.
*/
function checksumAddress(lowerAddress: string): Address {
const addressHash = bytesToHex(keccak256(utf8ToBytes(lowerAddress)));
let checksummed = '0x';

for (let index = 0; index < lowerAddress.length; index += 1) {
const character = lowerAddress[index] as string;
const hashNibble = addressHash[index] as string;
checksummed +=
Number.parseInt(hashNibble, 16) >= 8
? character.toUpperCase()
: character;
}

return checksummed as Address;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to add a dependency to @metamask/utils instead of @noble/hashes directly, and use the getChecksumAddress function instead of implementing it ourselves here in checksumAddress.

We could also drop the regex validation as this is implemented in getChecksumAddress in @metamask/utils - it throws Invalid hex address..

The checksum validation in getAddress looks good though.

2 changes: 1 addition & 1 deletion packages/x402/src/x402Client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Hex, getAddress, isHex } from 'viem';
import { type Hex, getAddress, isHex } from './ethereum';

export type x402PaymentRequirements = {
scheme: string;
Expand Down
3 changes: 1 addition & 2 deletions packages/x402/src/x402Server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type Address, getAddress } from 'viem';

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

export type x402Erc7710ServerConfig = {
Expand Down
14 changes: 14 additions & 0 deletions packages/x402/test/x402Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,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"
"@noble/hashes": "npm:^1.8.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