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
47 changes: 46 additions & 1 deletion src.ts/_tests/test-providers-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
Expand Down Expand Up @@ -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");
});
});

39 changes: 37 additions & 2 deletions src.ts/providers/provider-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -462,6 +462,41 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
]);
}

/**
* 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<Authorization> {
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<boolean> {
return this.provider.send("personal_unlockAccount", [
this.address.toLowerCase(), password, null ]);
Expand Down