Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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}$`,
Comment thread
jeffsmale90 marked this conversation as resolved.
Outdated
);
}

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