Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
/packages/delegation-core @MetaMask/delegation
/packages/delegation-deployments @MetaMask/delegation
/packages/smart-accounts-kit @MetaMask/delegation
/packages/smart-accounts-kit-x402 @MetaMask/delegation
/packages/7715-permission-types @MetaMask/delegation
14 changes: 14 additions & 0 deletions packages/smart-accounts-kit-x402/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- New @metamask/smart-accounts-kit-x402 package providing plugins to @x402 packages ([#236](https://github.com/MetaMask/smart-accounts-kit/pull/236))

[Unreleased]: https://github.com/metamask/smart-accounts-kit/
3 changes: 3 additions & 0 deletions packages/smart-accounts-kit-x402/LICENSE.APACHE2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
1 change: 1 addition & 0 deletions packages/smart-accounts-kit-x402/LICENSE.MIT0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MIT No Attribution License (MIT-0)
21 changes: 21 additions & 0 deletions packages/smart-accounts-kit-x402/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @metamask/smart-accounts-kit-x402

x402 adapters for ERC-7710 payment requirement publishing and payload creation.

## Installation

```bash
yarn add @metamask/smart-accounts-kit-x402
npm install @metamask/smart-accounts-kit-x402
```

## Exports

- `x402Erc7710Client`
- `x402Erc7710Server`
- `x402ExactEvmErc7710ServerScheme`

## Notes

This package intentionally does not depend on `@metamask/smart-accounts-kit`.
Consumers provide delegation payloads via `x402DelegationProvider`.
52 changes: 52 additions & 0 deletions packages/smart-accounts-kit-x402/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// eslint-disable-next-line
import baseConfig from '../../shared/config/base.eslint.mjs';

const withX402NamingExceptions = baseConfig.map((entry) => {
const namingConventionRule =
entry.rules?.['@typescript-eslint/naming-convention'];

if (!Array.isArray(namingConventionRule)) {
return entry;
}

const [level, ...conventions] = namingConventionRule;

return {
...entry,
rules: {
...entry.rules,
'@typescript-eslint/naming-convention': [
level,
{
selector: ['class', 'typeAlias'],
filter: {
regex: '^x402[A-Z].*$',
match: true,
},
format: null,
},
...conventions,
],
},
};
});

const config = [
...withX402NamingExceptions,
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'new-cap': [
'error',
{
newIsCap: true,
newIsCapExceptionPattern: '^x402[A-Z]',
capIsNew: true,
properties: true,
},
],
},
},
];

export default config;
74 changes: 74 additions & 0 deletions packages/smart-accounts-kit-x402/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"name": "@metamask/smart-accounts-kit-x402",
"version": "0.0.0",
"description": "x402 adapters for MetaMask smart accounts and ERC-7710",
"license": "(MIT-0 OR Apache-2.0)",
"type": "module",
"keywords": [
"MetaMask",
"Ethereum"
],
"homepage": "https://github.com/metamask/smart-accounts-kit/tree/main/packages/smart-accounts-kit-x402#readme",
"bugs": {
"url": "https://github.com/metamask/smart-accounts-kit/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/metamask/smart-accounts-kit.git"
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**",
"dist/"
],
"exports": {
".": {
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
},
"./package.json": "./package.json"
},
"engines": {
"node": "^18.18 || >=20"
},
"sideEffects": false,
"scripts": {
"build": "yarn typecheck && tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run --coverage",
"test:watch": "vitest watch",
"lint": "yarn lint:eslint",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix",
"changelog:update": "../../scripts/update-changelog.sh @metamask/smart-accounts-kit-x402",
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/smart-accounts-kit-x402"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"peerDependencies": {
"@x402/core": "^2.12.0",
"@x402/evm": "^2.12.0",
"viem": "^2.31.4"
},
"devDependencies": {
"@metamask/auto-changelog": "^5.0.2",
"@x402/core": "^2.12.0",
"@x402/evm": "^2.12.0",
"eslint": "^9.39.2",
"prettier": "^3.5.3",
"tsup": "^8.5.0",
"typescript": "5.5.4",
"viem": "2.31.4",
"vitest": "^3.2.4"
}
}
11 changes: 11 additions & 0 deletions packages/smart-accounts-kit-x402/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export {
x402Erc7710Client,
type x402DelegationProvider,
type x402DelegationPaymentPayload,
type x402PaymentRequirements,
type x402PaymentPayloadResult,
type x402SchemeNetworkClientLike,
type x402Erc7710ClientConfig,
} from './x402Client';
export { x402Erc7710Server, type x402Erc7710ServerConfig } from './x402Server';
export { x402ExactEvmErc7710ServerScheme } from './x402ExactEvmErc7710ServerScheme';
116 changes: 116 additions & 0 deletions packages/smart-accounts-kit-x402/src/x402Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type Hex, getAddress, isHex } from 'viem';

export type x402PaymentRequirements = {
scheme: string;
network: string;
asset: string;
amount: string;
payTo: string;
maxTimeoutSeconds: number;
extra?: Record<string, unknown>;
};

export type x402PaymentPayloadResult = {
x402Version: number;
payload: Record<string, unknown>;
extensions?: Record<string, unknown>;
};

export type x402DelegationPaymentPayload = {
delegationManager: Hex;
permissionContext: Hex;
delegator: Hex;
};

export type x402DelegationProvider = (
paymentRequirements: x402PaymentRequirements,
) => Promise<x402DelegationPaymentPayload>;

export type x402SchemeNetworkClientLike = {
readonly scheme: string;
createPaymentPayload: (
x402Version: number,
paymentRequirements: x402PaymentRequirements,
context?: Record<string, unknown>,
) => Promise<x402PaymentPayloadResult>;
};

export type x402Erc7710ClientConfig = {
delegationProvider: x402DelegationProvider;
fallbackClient?: x402SchemeNetworkClientLike;
};

/**
* Normalize and validate a delegation payload before publishing it.
*
* @param payload - Delegation payload returned by the configured provider.
* @returns The normalized payload with checksum addresses.
*/
function normalizeDelegationPayload(
payload: x402DelegationPaymentPayload,
): x402DelegationPaymentPayload {
if (!isHex(payload.permissionContext) || payload.permissionContext === '0x') {
throw new Error(
'Invalid delegation payload: permissionContext must be non-empty hex data',
);
}

return {
delegationManager: getAddress(payload.delegationManager),
permissionContext: payload.permissionContext,
delegator: getAddress(payload.delegator),
};
}

/**
* x402 `SchemeNetworkClient`-compatible implementation for ERC-7710 payments.
*
* This class uses structural typing and intentionally does not import x402 types,
* so it can be consumed without adding a direct dependency on x402 packages.
*/
export class x402Erc7710Client {
readonly scheme = 'exact';

readonly #delegationProvider: x402DelegationProvider;

readonly #fallbackClient?: x402SchemeNetworkClientLike;

constructor(config: x402Erc7710ClientConfig) {
this.#delegationProvider = config.delegationProvider;
this.#fallbackClient = config.fallbackClient;
}

async createPaymentPayload(
x402Version: number,
paymentRequirements: x402PaymentRequirements,
context?: Record<string, unknown>,
): Promise<x402PaymentPayloadResult> {
const assetTransferMethod = paymentRequirements.extra?.assetTransferMethod;

if (assetTransferMethod !== 'erc7710') {
if (this.#fallbackClient) {
return this.#fallbackClient.createPaymentPayload(
x402Version,
paymentRequirements,
context,
);
}

const invalidAssetTransferMethod =
typeof assetTransferMethod === 'string'
? `"${assetTransferMethod}"`
: JSON.stringify(assetTransferMethod);

throw new Error(
`x402Erc7710Client can only process assetTransferMethod "erc7710". Received: ${invalidAssetTransferMethod}$`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stray $ character in error message string

Low Severity

The error message template literal has a trailing $ after the ${invalidAssetTransferMethod} interpolation, producing messages like Received: "eip3009"$ instead of Received: "eip3009". The tests don't catch this because Vitest's .toThrow(string) performs a substring match, so the expected string without the $ still matches.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 66cc06a. Configure here.

);
Comment thread
jeffsmale90 marked this conversation as resolved.
}

const delegation = await this.#delegationProvider(paymentRequirements);

return {
x402Version,
payload: normalizeDelegationPayload(delegation),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Network, PaymentRequirements } from '@x402/core/types';
import { ExactEvmScheme } from '@x402/evm/exact/server';

import { x402Erc7710Server } from './x402Server';

/**
* Exact EVM server scheme that injects ERC-7710 payment requirement fields.
*/
export class x402ExactEvmErc7710ServerScheme extends ExactEvmScheme {
readonly #erc7710Server = new x402Erc7710Server();

async enhancePaymentRequirements(
paymentRequirements: PaymentRequirements,
supportedKind: {
x402Version: number;
scheme: string;
network: Network;
extra?: Record<string, unknown>;
},
facilitatorExtensions: string[],
): Promise<PaymentRequirements> {
const baseRequirements = await super.enhancePaymentRequirements(
paymentRequirements,
supportedKind,
facilitatorExtensions,
);

if (baseRequirements.extra?.assetTransferMethod !== 'erc7710') {
return baseRequirements;
}

const enhancedRequirements =
await this.#erc7710Server.enhancePaymentRequirements(
baseRequirements,
supportedKind,
);

return enhancedRequirements as PaymentRequirements;
}
}
Loading
Loading