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