diff --git a/.changeset/cyan-taxis-learn.md b/.changeset/cyan-taxis-learn.md new file mode 100644 index 0000000000..1df7261186 --- /dev/null +++ b/.changeset/cyan-taxis-learn.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added Redstone chain. diff --git a/.changeset/sixty-sloths-move.md b/.changeset/sixty-sloths-move.md deleted file mode 100644 index 340eef2641..0000000000 --- a/.changeset/sixty-sloths-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"viem": minor ---- - -**Breaking (Experimental):** Removed EIP-3074 support. diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml deleted file mode 100644 index 23485b6043..0000000000 --- a/.github/workflows/canary.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Release (Canary) -on: - push: - branches: [main] - workflow_dispatch: - -jobs: - canary: - name: Release canary - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Clone repository - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - - name: Install dependencies - uses: ./.github/actions/install-dependencies - - - name: Setup .npmrc file - uses: actions/setup-node@v4 - with: - registry-url: 'https://registry.npmjs.org' - - - name: Set version - run: | - jq --arg prop "workspaces" 'del(.[$prop])' package.json > package.tmp.json && rm package.json && cp package.tmp.json package.json && rm package.tmp.json - cd src - npm --no-git-tag-version version 0.0.0 - npm --no-git-tag-version version $(npm pkg get version | sed 's/"//g')-$(git branch --show-current | tr -cs '[:alnum:]-' '-' | tr '[:upper:]' '[:lower:]' | sed 's/-$//').$(date +'%Y%m%dT%H%M%S') - bun ../scripts/updateVersion.ts - - - name: Build - run: bun run build - - - name: Publish to npm - run: cd src && npm publish --tag $(git branch --show-current | tr -cs '[:alnum:]-' '-' | tr '[:upper:]' '[:lower:]' | sed 's/-$//') - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000000..c2437788eb --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,37 @@ +name: Snapshot +on: + workflow_dispatch: + +jobs: + canary: + name: Release snapshot version + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + timeout-minutes: 5 + + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + + - name: Publish Snapshots + continue-on-error: true + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # https://docs.npmjs.com/generating-provenance-statements + NPM_CONFIG_PROVENANCE: true + run: | + snapshot=$(git branch --show-current | tr -cs '[:alnum:]-' '-' | tr '[:upper:]' '[:lower:]' | sed 's/-$//') + npm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN" + bun run clean + bun run changeset version --no-git-tag --snapshot $snapshot + bun run changeset:prepublish + bun run changeset publish --no-git-tag --snapshot $snapshot --tag $snapshot + diff --git a/bun.lockb b/bun.lockb index 94196cab1b..7f4171f787 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index aaccdeb202..924952d8dd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build:trustedSetups:end": "mv src/node/trustedSetups.ts src/node/trustedSetups_cjs.ts && mv src/node/trustedSetups_esm.ts src/node/trustedSetups.ts", "build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./src/_types --emitDeclarationOnly --declaration --declarationMap", "changeset": "changeset", - "changeset:prepublish": "bun run version:update && bun scripts/prepublishOnly && bun run build", + "changeset:prepublish": "bun run version:update && bun scripts/prepublishOnly.ts && bun run build", "changeset:publish": "bun run changeset:prepublish && changeset publish", "changeset:version": "changeset version && bun install --lockfile-only && bun version:update", "clean": "rimraf src/_esm src/_cjs src/_types", diff --git a/site/pages/docs/compatibility.mdx b/site/pages/docs/compatibility.mdx index eac8f3fc14..c473e581bf 100644 --- a/site/pages/docs/compatibility.mdx +++ b/site/pages/docs/compatibility.mdx @@ -1,8 +1,8 @@ # Platform Compatibility [Platforms compatible with Viem] -**viem supports all modern browsers (Chrome, Edge, Firefox, etc) & runtime environments (Node 18+, Deno, Bun, etc).** +**Viem supports all modern browsers (Chrome, Edge, Firefox, etc) & runtime environments (Node 18+, Deno, Bun, etc).** -viem uses modern EcmaScript features such as: +Viem uses modern EcmaScript features such as: - [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) - [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) diff --git a/site/pages/docs/installation.mdx b/site/pages/docs/installation.mdx new file mode 100644 index 0000000000..8770e4c08a --- /dev/null +++ b/site/pages/docs/installation.mdx @@ -0,0 +1,78 @@ +# Installation + +Install Viem via your package manager, a ` +``` + +## Using Unreleased Commits + +If you can't wait for a new release to test the latest features, you can either install from the `canary` tag (tracks the [`main`](https://github.com/wevm/viem/tree/main) branch). + +:::code-group +```bash [pnpm] +pnpm add viem@canary +``` + +```bash [npm] +npm install viem@canary +``` + +```bash [yarn] +yarn add viem@canary +``` + +```bash [bun] +bun add viem@canary +``` +::: + +Or clone the [Viem repo](https://github.com/wevm/viem) to your local machine, build, and link it yourself. + +```bash +gh repo clone wevm/viem +cd viem +bun install +bun run build +bun link --global +``` + +Then go to the project where you are using Viem and run `bun link --global viem` (or the package manager that you used to link Viem globally). + +## Security + +Ethereum-related projects are often targeted in attacks to steal users' assets. Make sure you follow security best-practices for your project. Some quick things to get started. + +- Pin package versions, upgrade mindfully, and inspect lockfile changes to minimize the risk of [supply-chain attacks](https://nodejs.org/en/guides/security/#supply-chain-attacks). +- Install the [Socket Security](https://socket.dev) [GitHub App](https://github.com/apps/socket-security) to help detect and block supply-chain attacks. +- Add a [Content Security Policy](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) to defend against external scripts running in your app. + diff --git a/site/pages/experimental/erc7115/issuePermissions.mdx b/site/pages/experimental/erc7115/issuePermissions.mdx new file mode 100644 index 0000000000..64264e4416 --- /dev/null +++ b/site/pages/experimental/erc7115/issuePermissions.mdx @@ -0,0 +1,171 @@ +--- +description: Request permissions from a wallet to perform actions on behalf of a user. +--- + +# issuePermissions + +Request permissions from a wallet to perform actions on behalf of a user. + +[Read more.](https://eips.ethereum.org/EIPS/eip-7115) + +:::warning[Warning] +This is an experimental action that is not supported in most wallets. It is recommended to have a fallback mechanism if using this in production. +::: + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { parseEther } from 'viem' +import { account, walletClient } from './config' + +const result = await walletClient.issuePermissions({ // [!code focus:99] + account, + expiry: 1716846083638, + permissions: [ + { + type: 'native-token-limit', + data: { + amount: parseEther('0.5'), + }, + required: true, + }, + ], +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import 'viem/window' +// ---cut--- +import { createWalletClient, custom } from 'viem' +import { mainnet } from 'viem/chains' +import { walletActionsErc7115 } from 'viem/experimental' + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum!), +}).extend(walletActionsErc7115()) + +export const [account] = await walletClient.getAddresses() +``` + +::: + +## Returns + +`IssuePermissionsReturnType` + +Response from the wallet after issuing permissions. + +## Parameters + +### account + +- **Type:** `Account | Address | undefined` + +The Account to scope the permissions to. + +```ts twoslash +import { parseEther } from 'viem' +import { account, walletClient } from './config' + +const result = await walletClient.issuePermissions({ + account, // [!code focus] + expiry: 1716846083638, + permissions: [ + { + type: 'native-token-limit', + data: { + amount: parseEther('0.5'), + }, + required: true, + }, + ], +}) +``` + +### expiry + +- **Type:** `number` + +The timestamp (in seconds) when the permissions will expire. + +```ts twoslash +import { parseEther } from 'viem' +import { account, walletClient } from './config' + +const result = await walletClient.issuePermissions({ + account, + expiry: 1716846083638, // [!code focus] + permissions: [ + { + type: 'native-token-limit', + data: { + amount: parseEther('0.5'), + }, + required: true, + }, + ], +}) +``` + +### permissions + +- **Type:** `Permission[]` + +Set of permissions to grant to the user. + +```ts twoslash +// @noErrors +import { parseEther } from 'viem' +import { account, walletClient } from './config' + +const result = await walletClient.issuePermissions({ + account, + expiry: 1716846083638, + permissions: [ // [!code focus:99] + { + type: 'native-token-limit', + data: { + amount: parseEther('0.5'), + }, + required: true, + }, + { + type: ' +// ^| + } + ], +}) +``` + +### signer + +- **Type:** `Signer | undefined` + +Custom signer type to scope the permissions to. + +```ts twoslash +import { parseEther } from 'viem' +import { account, walletClient } from './config' + +const result = await walletClient.issuePermissions({ + expiry: 1716846083638, + permissions: [ + { + type: 'native-token-limit', + data: { + amount: parseEther('0.5'), + }, + required: true, + }, + ], + signer: { // [!code focus] + type: 'key', // [!code focus] + data: { // [!code focus] + id: '...' // [!code focus] + } // [!code focus] + } // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/sidebar.ts b/site/sidebar.ts index d78a557c2c..053bc47075 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -5,7 +5,8 @@ export const sidebar = { { text: 'Introduction', items: [ - { text: 'Why viem', link: '/docs/introduction' }, + { text: 'Why Viem', link: '/docs/introduction' }, + { text: 'Installation', link: '/docs/installation' }, { text: 'Getting Started', link: '/docs/getting-started' }, { text: 'Platform Compatibility', link: '/docs/compatibility' }, { text: 'FAQ', link: '/docs/faq' }, @@ -1097,6 +1098,20 @@ export const sidebar = { }, ], }, + { + text: 'ERC-7115', + items: [ + { + text: 'Actions', + items: [ + { + text: 'issuePermissions', + link: '/experimental/erc7115/issuePermissions', + }, + ], + }, + ], + }, ], }, '/op-stack': { diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 6db4d3b9ff..1250f84d84 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,5 +1,13 @@ # viem +## 2.13.0 + +### Minor Changes + +- [#2317](https://github.com/wevm/viem/pull/2317) [`3135a0cbd70cd168369fd2d478025d6192d2d852`](https://github.com/wevm/viem/commit/3135a0cbd70cd168369fd2d478025d6192d2d852) Thanks [@jxom](https://github.com/jxom)! - **Experimental:** Added ERC-7115 extension. + +- [#2313](https://github.com/wevm/viem/pull/2313) [`175d0ae2345a36f7923b19676fc8adb5e820e262`](https://github.com/wevm/viem/commit/175d0ae2345a36f7923b19676fc8adb5e820e262) Thanks [@jxom](https://github.com/jxom)! - **Breaking (Experimental):** Removed EIP-3074 support. + ## 2.12.5 ### Patch Changes diff --git a/src/chains/definitions/redstone.ts b/src/chains/definitions/redstone.ts new file mode 100644 index 0000000000..5744066bec --- /dev/null +++ b/src/chains/definitions/redstone.ts @@ -0,0 +1,20 @@ +import { defineChain } from '../../utils/chain/defineChain.js' + +export const redstone = defineChain({ + id: 690, + name: 'Redstone', + nativeCurrency: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + rpcUrls: { + default: { + http: ['https://rpc.redstonechain.com'], + webSocket: ['wss://rpc.redstonechain.com'], + }, + }, + blockExplorers: { + default: { name: 'Explorer', url: ' https://explorer.redstone.xyz' }, + }, +}) diff --git a/src/chains/index.ts b/src/chains/index.ts index 500708ff4e..0392766f3e 100644 --- a/src/chains/index.ts +++ b/src/chains/index.ts @@ -199,6 +199,7 @@ export { pulsechain } from './definitions/pulsechain.js' export { pulsechainV4 } from './definitions/pulsechainV4.js' export { qMainnet } from './definitions/qMainnet.js' export { qTestnet } from './definitions/qTestnet.js' +export { redstone } from './definitions/redstone.js' export { reyaNetwork } from './definitions/reyaNetwork.js' export { rollux } from './definitions/rollux.js' export { rolluxTestnet } from './definitions/rolluxTestnet.js' diff --git a/src/errors/version.ts b/src/errors/version.ts index 6725a3f2ae..e08d440215 100644 --- a/src/errors/version.ts +++ b/src/errors/version.ts @@ -1 +1 @@ -export const version = '2.12.2' +export const version = '2.13.0' diff --git a/src/experimental/erc7115/actions/issuePermissions.test.ts b/src/experimental/erc7115/actions/issuePermissions.test.ts new file mode 100644 index 0000000000..f954ac64b2 --- /dev/null +++ b/src/experimental/erc7115/actions/issuePermissions.test.ts @@ -0,0 +1,263 @@ +import { expect, test } from 'vitest' +import { accounts } from '../../../../test/src/constants.js' +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import { createClient } from '../../../clients/createClient.js' +import { custom } from '../../../clients/transports/custom.js' +import { issuePermissions } from './issuePermissions.js' + +const getClient = ({ onRequest }: { onRequest: (params: any) => void }) => + createClient({ + transport: custom({ + async request({ method, params }) { + onRequest(params) + if (method === 'wallet_issuePermissions') + return { + grantedPermissions: params[0].permissions.map( + (permission: any) => ({ + type: permission.type, + data: permission.data, + }), + ), + expiry: params[0].expiry, + permissionsContext: '0xdeadbeef', + } + return null + }, + }), + }) + +test('default', async () => { + const requests: any[] = [] + const client = getClient({ + onRequest(request) { + requests.push(request) + }, + }) + + expect( + await issuePermissions(client, { + expiry: 1716846083638, + signer: { + type: 'account', + data: { + id: '0x0000000000000000000000000000000000000000', + }, + }, + permissions: [ + { + type: 'contract-call', + data: { + address: '0x0000000000000000000000000000000000000000', + }, + }, + { + type: 'native-token-limit', + data: { + amount: 69420n, + }, + required: true, + }, + ], + }), + ).toMatchInlineSnapshot(` + { + "expiry": 1716846083638, + "grantedPermissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "type": "contract-call", + }, + { + "data": { + "amount": 69420n, + }, + "type": "native-token-limit", + }, + ], + "permissionsContext": "0xdeadbeef", + } + `) + expect(requests).toMatchInlineSnapshot(` + [ + [ + { + "expiry": 1716846083638, + "permissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "required": false, + "type": "contract-call", + }, + { + "data": { + "amount": "0x10f2c", + }, + "required": true, + "type": "native-token-limit", + }, + ], + "signer": { + "data": { + "id": "0x0000000000000000000000000000000000000000", + }, + "type": "account", + }, + }, + ], + ] + `) +}) + +test('args: account as signer', async () => { + const requests: any[] = [] + const client = getClient({ + onRequest(request) { + requests.push(request) + }, + }) + + expect( + await issuePermissions(client, { + account: privateKeyToAccount(accounts[0].privateKey), + expiry: 1716846083638, + permissions: [ + { + type: 'contract-call', + data: { + address: '0x0000000000000000000000000000000000000000', + }, + }, + { + type: 'native-token-limit', + data: { + amount: 69420n, + }, + required: true, + }, + ], + }), + ).toMatchInlineSnapshot(` + { + "expiry": 1716846083638, + "grantedPermissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "type": "contract-call", + }, + { + "data": { + "amount": 69420n, + }, + "type": "native-token-limit", + }, + ], + "permissionsContext": "0xdeadbeef", + } + `) + expect(requests).toMatchInlineSnapshot(` + [ + [ + { + "expiry": 1716846083638, + "permissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "required": false, + "type": "contract-call", + }, + { + "data": { + "amount": "0x10f2c", + }, + "required": true, + "type": "native-token-limit", + }, + ], + }, + ], + ] + `) +}) + +test('args: address as signer', async () => { + const requests: any[] = [] + const client = getClient({ + onRequest(request) { + requests.push(request) + }, + }) + + expect( + await issuePermissions(client, { + account: accounts[0].address, + expiry: 1716846083638, + permissions: [ + { + type: 'contract-call', + data: { + address: '0x0000000000000000000000000000000000000000', + }, + }, + { + type: 'native-token-limit', + data: { + amount: 69420n, + }, + required: true, + }, + ], + }), + ).toMatchInlineSnapshot(` + { + "expiry": 1716846083638, + "grantedPermissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "type": "contract-call", + }, + { + "data": { + "amount": 69420n, + }, + "type": "native-token-limit", + }, + ], + "permissionsContext": "0xdeadbeef", + } + `) + expect(requests).toMatchInlineSnapshot(` + [ + [ + { + "expiry": 1716846083638, + "permissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "required": false, + "type": "contract-call", + }, + { + "data": { + "amount": "0x10f2c", + }, + "required": true, + "type": "native-token-limit", + }, + ], + }, + ], + ] + `) +}) diff --git a/src/experimental/erc7115/actions/issuePermissions.ts b/src/experimental/erc7115/actions/issuePermissions.ts new file mode 100644 index 0000000000..86207b18dd --- /dev/null +++ b/src/experimental/erc7115/actions/issuePermissions.ts @@ -0,0 +1,161 @@ +import type { Address } from 'abitype' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Account } from '../../../types/account.js' +import type { WalletIssuePermissionsReturnType } from '../../../types/eip1193.js' +import type { Hex } from '../../../types/misc.js' +import type { OneOf } from '../../../types/utils.js' +import { numberToHex } from '../../../utils/encoding/toHex.js' +import type { Permission } from '../types/permission.js' +import type { Signer } from '../types/signer.js' + +export type IssuePermissionsParameters = { + /** Timestamp (in seconds) that specifies the time by which this session MUST expire. */ + expiry: number + /** Set of permissions to grant to the user. */ + permissions: readonly Permission[] +} & OneOf< + | { + /** Signer to assign the permissions to. */ + signer?: Signer | undefined + } + | { + /** Account to assign the permissions to. */ + account?: Address | Account | undefined + } +> + +export type IssuePermissionsReturnType = { + /** Timestamp (in seconds) that specifies the time by which this session MUST expire. */ + expiry: number + /** ERC-4337 Factory to deploy smart contract account. */ + factory?: Hex | undefined + /** Calldata to use when calling the ERC-4337 Factory. */ + factoryData?: string | undefined + /** Set of granted permissions. */ + grantedPermissions: Omit[] + /** Permissions identifier. */ + permissionsContext: string + /** Signer attached to the permissions. */ + signerData?: + | { + userOpBuilder?: Hex | undefined + submitToAddress?: Hex | undefined + } + | undefined +} + +/** + * Request permissions from a wallet to perform actions on behalf of a user. + * + * - Docs: https://viem.sh/experimental/erc7115/issuePermissions + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { issuePermissions } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * + * const result = await issuePermissions(client, { + * expiry: 1716846083638, + * permissions: [ + * { + * type: 'contract-call', + * data: { + * address: '0x0000000000000000000000000000000000000000', + * }, + * }, + * { + * type: 'native-token-limit', + * data: { + * amount: 69420n, + * }, + * required: true, + * }, + * ], + * }) + */ +export async function issuePermissions( + client: Client, + parameters: IssuePermissionsParameters, +): Promise { + const { expiry, permissions, signer } = parameters + const result = await client.request({ + method: 'wallet_issuePermissions', + params: [parseParameters({ expiry, permissions, signer })], + }) + return parseResult(result) as IssuePermissionsReturnType +} + +function parseParameters(parameters: IssuePermissionsParameters) { + const { account, expiry, permissions, signer: signer_ } = parameters + + const signer = (() => { + if (!account && !signer_) return undefined + + if (account) { + // Address as signer. + if (typeof account === 'string') + return { + type: 'account', + data: { + id: account, + }, + } + + // Viem Account as signer. + return { + type: 'account', + data: { + id: account.address, + }, + } + } + + // ERC-7115 Signer as signer. + return signer_ + })() + + return { + expiry, + permissions: permissions.map((permission) => ({ + ...permission, + ...(permission.data && typeof permission.data === 'object' + ? { + data: { + ...permission.data, + ...('amount' in permission.data && + typeof permission.data.amount === 'bigint' + ? { amount: numberToHex(permission.data.amount) } + : {}), + }, + } + : {}), + required: permission.required ?? false, + })), + ...(signer ? { signer } : {}), + } +} + +function parseResult(result: WalletIssuePermissionsReturnType) { + return { + expiry: result.expiry, + ...(result.factory ? { factory: result.factory } : {}), + ...(result.factoryData ? { factoryData: result.factoryData } : {}), + grantedPermissions: result.grantedPermissions.map((permission) => ({ + ...permission, + data: { + ...permission.data, + ...('amount' in permission.data + ? { amount: BigInt(permission.data.amount) } + : {}), + }, + })), + permissionsContext: result.permissionsContext, + ...(result.signerData ? { signerData: result.signerData } : {}), + } +} diff --git a/src/experimental/erc7115/decorators/erc7115.test.ts b/src/experimental/erc7115/decorators/erc7115.test.ts new file mode 100644 index 0000000000..43c6b7d4d4 --- /dev/null +++ b/src/experimental/erc7115/decorators/erc7115.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from 'vitest' + +import { createClient } from '../../../clients/createClient.js' +import { custom } from '../../../clients/transports/custom.js' +import { walletActionsErc7115 } from './erc7115.js' + +const client = createClient({ + transport: custom({ + async request({ method, params }) { + if (method === 'wallet_issuePermissions') + return { + grantedPermissions: params[0].permissions.map((permission: any) => ({ + type: permission.type, + data: permission.data, + })), + expiry: params[0].expiry, + permissionsContext: '0xdeadbeef', + } + + return null + }, + }), +}).extend(walletActionsErc7115()) + +test('default', async () => { + expect(walletActionsErc7115()(client)).toMatchInlineSnapshot(` + { + "issuePermissions": [Function], + } + `) +}) + +describe('smoke test', () => { + test('issuePermissions', async () => { + expect( + await client.issuePermissions({ + expiry: 1716846083638, + signer: { + type: 'account', + data: { + id: '0x0000000000000000000000000000000000000000', + }, + }, + permissions: [ + { + type: 'contract-call', + data: { + address: '0x0000000000000000000000000000000000000000', + }, + }, + { + type: 'native-token-limit', + data: { + amount: 69420n, + }, + required: true, + }, + ], + }), + ).toMatchInlineSnapshot(` + { + "expiry": 1716846083638, + "grantedPermissions": [ + { + "data": { + "address": "0x0000000000000000000000000000000000000000", + }, + "type": "contract-call", + }, + { + "data": { + "amount": 69420n, + }, + "type": "native-token-limit", + }, + ], + "permissionsContext": "0xdeadbeef", + } + `) + }) +}) diff --git a/src/experimental/erc7115/decorators/erc7115.ts b/src/experimental/erc7115/decorators/erc7115.ts new file mode 100644 index 0000000000..a9ae24c685 --- /dev/null +++ b/src/experimental/erc7115/decorators/erc7115.ts @@ -0,0 +1,80 @@ +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Account } from '../../../types/account.js' +import type { Chain } from '../../../types/chain.js' +import { + type IssuePermissionsParameters, + type IssuePermissionsReturnType, + issuePermissions, +} from '../actions/issuePermissions.js' + +export type WalletActionsErc7115 = { + /** + * Request permissions from a wallet to perform actions on behalf of a user. + * + * - Docs: https://viem.sh/experimental/erc7115/issuePermissions + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsErc7115 } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(walletActionsErc7115()) + * + * const result = await client.issuePermissions({ + * expiry: 1716846083638, + * permissions: [ + * { + * type: 'contract-call', + * data: { + * address: '0x0000000000000000000000000000000000000000', + * }, + * }, + * { + * type: 'native-token-limit', + * data: { + * amount: 69420n, + * }, + * required: true, + * }, + * ], + * }) + */ + issuePermissions: ( + parameters: IssuePermissionsParameters, + ) => Promise +} + +/** + * A suite of ERC-7115 Wallet Actions. + * + * - Docs: https://viem.sh/experimental + * + * @example + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsErc7115 } from 'viem/experimental' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * }).extend(walletActionsErc7115()) + * + * const result = await walletClient.issuePermissions({...}) + */ +export function walletActionsErc7115() { + return < + transport extends Transport, + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + >( + client: Client, + ): WalletActionsErc7115 => { + return { + issuePermissions: (parameters) => issuePermissions(client, parameters), + } + } +} diff --git a/src/experimental/erc7115/types/permission.ts b/src/experimental/erc7115/types/permission.ts new file mode 100644 index 0000000000..68311fddb0 --- /dev/null +++ b/src/experimental/erc7115/types/permission.ts @@ -0,0 +1,46 @@ +import type { Address } from 'abitype' + +import type { OneOf } from '../../../types/utils.js' + +export type NativeTokenLimitPermission = { + type: 'native-token-limit' + data: { + amount: amount + } +} + +export type Erc20LimitPermission = { + type: 'erc20-limit' + data: { + erc20Address: Address + amount: amount + } +} + +export type GasLimitPermission = { + type: 'gas-limit' + data: { + amount: amount + } +} + +export type ContractCallPermission = { + type: 'contract-call' + data: unknown +} + +export type RateLimitPermission = { + type: 'rate-limit' + data: { + count: number + interval: number + } +} + +export type Permission = OneOf< + | NativeTokenLimitPermission + | Erc20LimitPermission + | GasLimitPermission + | ContractCallPermission + | RateLimitPermission +> & { required?: boolean | undefined } diff --git a/src/experimental/erc7115/types/signer.ts b/src/experimental/erc7115/types/signer.ts new file mode 100644 index 0000000000..346400a7ef --- /dev/null +++ b/src/experimental/erc7115/types/signer.ts @@ -0,0 +1,25 @@ +import type { Address } from 'abitype' +import type { OneOf } from '../../../types/utils.js' + +export type AccountSigner = { + type: 'account' + data: { + id: Address + } +} + +export type KeySigner = { + type: 'key' + data: { + id: string + } +} + +export type MultiKeySigner = { + type: 'keys' + data: { + ids: string[] + } +} + +export type Signer = OneOf diff --git a/src/experimental/index.ts b/src/experimental/index.ts index 2570e7b143..d40fb5449d 100644 --- a/src/experimental/index.ts +++ b/src/experimental/index.ts @@ -52,3 +52,13 @@ export { type SerializeErc6492SignatureReturnType, serializeErc6492Signature, } from './erc6492/serializeErc6492Signature.js' + +export { + type IssuePermissionsParameters, + type IssuePermissionsReturnType, + issuePermissions, +} from './erc7115/actions/issuePermissions.js' +export { + type WalletActionsErc7115, + walletActionsErc7115, +} from './erc7115/decorators/erc7115.js' diff --git a/src/index.ts b/src/index.ts index d3e66726e5..7fb9a8f9d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1006,6 +1006,8 @@ export type { WalletCapabilitiesRecord, WalletCallReceipt, WalletGetCallsStatusReturnType, + WalletIssuePermissionsParameters, + WalletIssuePermissionsReturnType, WalletSendCallsParameters, WalletPermissionCaveat, WalletPermission, diff --git a/src/jsr.json b/src/jsr.json index c5ab69a2a8..eb23106d52 100644 --- a/src/jsr.json +++ b/src/jsr.json @@ -1,6 +1,6 @@ { "name": "@wevm/viem", - "version": "2.12.2", + "version": "2.13.0", "exports": { ".": "./index.ts", "./accounts": "./accounts/index.ts", diff --git a/src/package.json b/src/package.json index 891c55ec82..5bdeb4f871 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,7 @@ { "name": "viem", "description": "TypeScript Interface for Ethereum", - "version": "2.12.5", + "version": "2.13.0", "type": "module", "main": "./_cjs/index.js", "module": "./_esm/index.js", diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 60d3296762..e1d8896319 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -131,6 +131,38 @@ export type WalletCallReceipt = { transactionHash: Hex } +export type WalletIssuePermissionsParameters = { + signer?: + | { + type: string + data: unknown + } + | undefined + permissions: readonly { + type: string + data: unknown + required: boolean + }[] + expiry: number +} + +export type WalletIssuePermissionsReturnType = { + expiry: number + factory?: `0x${string}` | undefined + factoryData?: string | undefined + grantedPermissions: readonly { + type: string + data: any + }[] + permissionsContext: string + signerData?: + | { + userOpBuilder?: `0x${string}` | undefined + submitToAddress?: `0x${string}` | undefined + } + | undefined +} + export type WalletGetCallsStatusReturnType = { status: 'PENDING' | 'CONFIRMED' receipts?: WalletCallReceipt[] | undefined @@ -1368,6 +1400,18 @@ export type WalletRpcSchema = [ Parameters?: undefined ReturnType: WalletPermission[] }, + /** + * @description Requests permissions from a wallet + * @link https://eips.ethereum.org/EIPS/eip-7715 + * @example + * provider.request({ method: 'wallet_issuePermissions', params: [{ ... }] }) + * // => { ... } + */ + { + Method: 'wallet_issuePermissions' + Parameters?: [WalletIssuePermissionsParameters] + ReturnType: Prettify + }, /** * @description Requests the given permissions from the user. * @link https://eips.ethereum.org/EIPS/eip-2255