diff --git a/src.ts/_tests/test-providers-jsonrpc.ts b/src.ts/_tests/test-providers-jsonrpc.ts index 8dfdb30bc..039ab788c 100644 --- a/src.ts/_tests/test-providers-jsonrpc.ts +++ b/src.ts/_tests/test-providers-jsonrpc.ts @@ -3,7 +3,8 @@ import assert from "assert"; import { id, isError, makeError, toUtf8Bytes, toUtf8String, FetchRequest, - JsonRpcProvider, Transaction, Wallet + JsonRpcProvider, Transaction, Wallet, + hashAuthorization, verifyAuthorization } from "../index.js"; const StatusMessages: Record = { @@ -205,3 +206,47 @@ describe("Ensure Catchable Errors", function() { }); }); +describe("EIP-7702 Authorization Signing (JSON-RPC)", function() { + it("Signs and verifies an authorization via eth_signAuthorization", async function() { + this.timeout(15000); + + const contractAddress = "0x" + "33".repeat(20); + + const provider = createProvider((method, params) => { + switch (method) { + case "eth_getTransactionCount": + return "0x5"; + + case "eth_signAuthorization": { + const [ from, auth ] = <[ string, any ]>params; + assert.strictEqual(from, wallet.address.toLowerCase(), + "signer address sent to node"); + + const signature = wallet.signingKey.sign(hashAuthorization({ + address: auth.address, + chainId: auth.chainId, + nonce: auth.nonce + })); + return signature.serialized; + } + } + return undefined; + }); + + const signer = await provider.getSigner(); + const auth = await signer.authorize({ address: contractAddress }); + + assert.ok(auth, "received an authorization"); + assert.strictEqual(auth.address, contractAddress, "delegate address preserved"); + assert.strictEqual(auth.nonce, 5n, "nonce populated"); + assert.strictEqual(auth.chainId, 0x1337n, "chainId populated"); + assert.ok(auth.signature.r && auth.signature.s, "signature present"); + + // The authorization must verify to the signing account + const authority = verifyAuthorization({ + address: auth.address, nonce: auth.nonce, chainId: auth.chainId + }, auth.signature); + assert.strictEqual(authority, wallet.address, "authority recovered"); + }); +}); + diff --git a/src.ts/providers/provider-jsonrpc.ts b/src.ts/providers/provider-jsonrpc.ts index 301fa2657..6575000e3 100644 --- a/src.ts/providers/provider-jsonrpc.ts +++ b/src.ts/providers/provider-jsonrpc.ts @@ -31,8 +31,8 @@ import { Network } from "./network.js"; import { FilterIdEventSubscriber, FilterIdPendingSubscriber } from "./subscriber-filterid.js"; import { PollingEventSubscriber } from "./subscriber-polling.js"; -import type { TypedDataDomain, TypedDataField } from "../hash/index.js"; -import type { TransactionLike } from "../transaction/index.js"; +import type { AuthorizationRequest, TypedDataDomain, TypedDataField } from "../hash/index.js"; +import type { Authorization, TransactionLike } from "../transaction/index.js"; import type { PerformActionRequest, Subscriber, Subscription } from "./abstract-provider.js"; import type { Networkish } from "./network.js"; @@ -462,6 +462,41 @@ export class JsonRpcSigner extends AbstractSigner { ]); } + /** + * Signs the [[link-eip-7702]] authorization %%req%% using + * ``eth_signAuthorization``. + * + * The %%address%% in %%req%% is the address of the contract code to + * delegate the account to; the account that signs is the address of + * this Signer. + * + * Any missing %%nonce%% and %%chainId%% are populated. + * + * The returned [[Authorization]] can be included in a type-4 + * transaction via [[TransactionRequest-authorizationList]]. + */ + async authorize(_auth: AuthorizationRequest): Promise { + const address = getAddress(await resolveAddress(_auth.address, this.provider)); + + const populated = await this.populateAuthorization( + Object.assign({ }, _auth, { address }) + ); + + const chainId = getBigInt(populated.chainId ?? 0); + const nonce = getBigInt(populated.nonce ?? 0); + + const signature = await this.provider.send("eth_signAuthorization", [ + this.address.toLowerCase(), + { + chainId: toQuantity(chainId), + address, + nonce: toQuantity(nonce) + } + ]); + + return authorizationify({ address, nonce, chainId, signature }); + } + async unlock(password: string): Promise { return this.provider.send("personal_unlockAccount", [ this.address.toLowerCase(), password, null ]);