Skip to content

Commit 44d9630

Browse files
feat: Add provider and signTypedData to ethers5 signer adapter (#2667)
Co-authored-by: Jonas Daniels <jonas.daniels@outlook.com>
1 parent 367576c commit 44d9630

File tree

6 files changed

+262
-65
lines changed

6 files changed

+262
-65
lines changed

.changeset/big-kangaroos-stare.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
API update for ethers5 and ethers5 adapters:
6+
7+
- Now all adapter functions take a singular object
8+
- ethers5: fixed adapted signer not containing a provider by default
9+
- ethers5: added support for sign typed data
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, test, expect } from "vitest";
2+
import { privateKeyAccount } from "../wallets/private-key.js";
3+
import { TEST_CLIENT } from "../../test/src/test-clients.js";
4+
import { toEthersSigner } from "./ethers5.js";
5+
import { ANVIL_CHAIN } from "../../test/src/chains.js";
6+
import * as ethers5 from "ethers5";
7+
8+
const FAKE_PKEY =
9+
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
10+
11+
const account = privateKeyAccount({
12+
privateKey: FAKE_PKEY,
13+
client: TEST_CLIENT,
14+
});
15+
16+
describe("ethers5 adapter", () => {
17+
test("should return an ethers 5 signer", async () => {
18+
const signer = await toEthersSigner(
19+
ethers5,
20+
TEST_CLIENT,
21+
account,
22+
ANVIL_CHAIN,
23+
);
24+
expect(signer).toBeDefined();
25+
expect(signer.signMessage).toBeDefined();
26+
});
27+
28+
test("should sign typed data", async () => {
29+
const signer = await toEthersSigner(
30+
ethers5,
31+
TEST_CLIENT,
32+
account,
33+
ANVIL_CHAIN,
34+
);
35+
expect(signer._signTypedData).toBeDefined();
36+
37+
// All properties on a domain are optional
38+
const domain = {
39+
name: "Ether Mail",
40+
version: "1",
41+
chainId: 1,
42+
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
43+
};
44+
45+
// The named list of all type definitions
46+
const types = {
47+
Person: [
48+
{ name: "name", type: "string" },
49+
{ name: "wallet", type: "address" },
50+
],
51+
Mail: [
52+
{ name: "from", type: "Person" },
53+
{ name: "to", type: "Person" },
54+
{ name: "contents", type: "string" },
55+
],
56+
};
57+
58+
// The data to sign
59+
const value = {
60+
from: {
61+
name: "Cow",
62+
wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
63+
},
64+
to: {
65+
name: "Bob",
66+
wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
67+
},
68+
contents: "Hello, Bob!",
69+
};
70+
71+
const signature = await signer._signTypedData(domain, types, value);
72+
73+
expect(signature).toMatchInlineSnapshot(
74+
`"0x10d3ce8040590e48889801080ad40f3d514c2c3ce03bbbe3e179bbf5ba56c75425951fa15220f637e2ab79fd033b99c4b340339e00e360316547e956c61ffcb01c"`,
75+
);
76+
});
77+
});

packages/thirdweb/src/adapters/ethers5.ts

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import type { Abi } from "abitype";
55
import type { AccessList, Hex, TransactionSerializable } from "viem";
66
import type { ThirdwebClient } from "../client/client.js";
77
import type { Account } from "../wallets/interfaces/wallet.js";
8-
import { defineChain, getRpcUrlForChain } from "../chains/utils.js";
8+
import { getRpcUrlForChain } from "../chains/utils.js";
99
import type { Chain } from "../chains/types.js";
1010
import { getContract, type ThirdwebContract } from "../contract/contract.js";
11-
import { uint8ArrayToHex } from "../utils/encoding/hex.js";
12-
import { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js";
11+
import { toHex, uint8ArrayToHex } from "../utils/encoding/hex.js";
1312
import { waitForReceipt } from "../transaction/actions/wait-for-tx-receipt.js";
1413
import { prepareTransaction } from "../transaction/prepare-transaction.js";
1514
import { sendTransaction } from "../transaction/actions/send-transaction.js";
@@ -47,36 +46,44 @@ function assertEthers5(
4746

4847
export const ethers5Adapter = /* @__PURE__ */ (() => {
4948
const ethers = universalethers;
50-
assertEthers5(ethers);
5149
return {
5250
provider: {
5351
/**
5452
* Converts a Thirdweb client and chain ID into an ethers.js provider.
55-
* @param client - The Thirdweb client.
56-
* @param chain - The chain.
53+
* @param options - The options for converting the Thirdweb client and chain ID into an ethers.js provider.
54+
* @param options.client - The Thirdweb client.
55+
* @param options.chain - The chain.
5756
* @returns The ethers.js provider.
5857
* @example
5958
* ```ts
6059
* import { ethers5Adapter } from "thirdweb/adapters/ethers5";
61-
* const provider = ethers5Adapter.provider.toEthers(client, chainId);
60+
* const provider = ethers5Adapter.provider.toEthers({ client, chainId });
6261
* ```
6362
*/
64-
toEthers: (client: ThirdwebClient, chain: Chain) =>
65-
toEthersProvider(ethers, client, chain),
63+
toEthers: (options: { client: ThirdwebClient; chain: Chain }) => {
64+
assertEthers5(ethers);
65+
return toEthersProvider(ethers, options.client, options.chain);
66+
},
6667
},
6768
contract: {
6869
/**
6970
* Converts a ThirdwebContract to an ethers.js Contract.
70-
* @param twContract - The ThirdwebContract to convert.
71+
* @param options - The options for converting the ThirdwebContract to an ethers.js Contract.
72+
* @param options.thirdwebContract - The ThirdwebContract to convert.
7173
* @returns A Promise that resolves to an ethers.js Contract.
7274
* @example
7375
* ```ts
7476
* import { ethers5Adapter } from "thirdweb/adapters/ethers5";
75-
* const ethersContract = await ethers5Adapter.contract.toEthers(twContract);
77+
* const ethersContract = await ethers5Adapter.contract.toEthers({
78+
* thirdwebContract,
79+
* });
7680
* ```
7781
*/
78-
toEthers: (twContract: ThirdwebContract) =>
79-
toEthersContract(ethers, twContract),
82+
toEthers: (options: { thirdwebContract: ThirdwebContract }) => {
83+
assertEthers5(ethers);
84+
return toEthersContract(ethers, options.thirdwebContract);
85+
},
86+
8087
/**
8188
* Creates a ThirdwebContract instance from an ethers.js contract.
8289
* @param options - The options for creating the ThirdwebContract instance.
@@ -92,35 +99,54 @@ export const ethers5Adapter = /* @__PURE__ */ (() => {
9299
* });
93100
* ```
94101
*/
95-
fromEthers: (options: FromEthersContractOptions) =>
96-
fromEthersContract(options),
102+
fromEthers: (options: FromEthersContractOptions) => {
103+
assertEthers5(ethers);
104+
return fromEthersContract(options);
105+
},
97106
},
98107
signer: {
99108
/**
100109
* Converts an ethers5 Signer into a Wallet object.
101-
* @param signer - The ethers5 Signer object.
110+
* @param options - The options for converting the ethers5 Signer into a Wallet object.
111+
* @param options.signer - The ethers5 Signer object.
102112
* @returns - A Promise that resolves to aa Wallet object.
103113
* @example
104114
* ```ts
105115
* import { ethers5Adapter } from "thirdweb/adapters/ethers5";
106-
* const wallet = await ethers5Adapter.signer.fromEthersSigner(signer);
116+
* const wallet = await ethers5Adapter.signer.fromEthersSigner({ signer });
107117
* ```
108118
*/
109-
fromEthers: (signer: ethers5.Signer) => fromEthersSigner(signer),
119+
fromEthers: (options: { signer: ethers5.Signer }) => {
120+
assertEthers5(ethers);
121+
return fromEthersSigner(options.signer);
122+
},
110123

111124
/**
112125
* Converts a Thirdweb wallet to an ethers.js signer.
113-
* @param client - The thirdweb client.
114-
* @param account - The account.
126+
* @param options - The options for converting the Thirdweb wallet to an ethers.js signer.
127+
* @param options.client - The thirdweb client.
128+
* @param options.chain - The chain.
129+
* @param options.account - The account.
115130
* @returns A promise that resolves to an ethers.js signer.
116131
* @example
117132
* ```ts
118133
* import { ethers5Adapter } from "thirdweb/adapters/ethers5";
119-
* const signer = await ethers5Adapter.signer.toEthers(client, chain, account);
134+
* const signer = await ethers5Adapter.signer.toEthers({ client, chain, account });
120135
* ```
121136
*/
122-
toEthers: (client: ThirdwebClient, account: Account) =>
123-
toEthersSigner(ethers, client, account),
137+
toEthers: (options: {
138+
client: ThirdwebClient;
139+
chain: Chain;
140+
account: Account;
141+
}) => {
142+
assertEthers5(ethers);
143+
return toEthersSigner(
144+
ethers,
145+
options.client,
146+
options.account,
147+
options.chain,
148+
);
149+
},
124150
},
125151
};
126152
})();
@@ -240,18 +266,40 @@ async function fromEthersSigner(signer: ethers5.Signer): Promise<Account> {
240266
return account;
241267
}
242268

243-
async function toEthersSigner(
269+
/**
270+
* @internal
271+
*/
272+
export async function toEthersSigner(
244273
ethers: Ethers5,
245274
client: ThirdwebClient,
246275
account: Account,
276+
chain: Chain,
247277
) {
248278
class ThirdwebAdapterSigner extends ethers.Signer {
279+
/**
280+
* @internal
281+
*/
282+
constructor() {
283+
super();
284+
ethers.utils.defineReadOnly(
285+
this,
286+
"provider",
287+
toEthersProvider(ethers, client, chain),
288+
);
289+
}
290+
291+
/**
292+
* @internal
293+
*/
249294
override getAddress(): Promise<string> {
250295
if (!account) {
251296
throw new Error("Account not found");
252297
}
253298
return Promise.resolve(account.address);
254299
}
300+
/**
301+
* @internal
302+
*/
255303
override signMessage(message: string | Uint8Array): Promise<string> {
256304
if (!account) {
257305
throw new Error("Account not found");
@@ -261,6 +309,9 @@ async function toEthersSigner(
261309
typeof message === "string" ? message : uint8ArrayToHex(message),
262310
});
263311
}
312+
/**
313+
* @internal
314+
*/
264315
override async signTransaction(
265316
transaction: ethers5.ethers.utils.Deferrable<ethers5.ethers.providers.TransactionRequest>,
266317
): Promise<string> {
@@ -276,6 +327,9 @@ async function toEthersSigner(
276327
);
277328
}
278329

330+
/**
331+
* @internal
332+
*/
279333
override async sendTransaction(
280334
transaction: ethers5.ethers.utils.Deferrable<
281335
ethers5.ethers.providers.TransactionRequest & { chainId: number }
@@ -290,8 +344,8 @@ async function toEthersSigner(
290344
const awaitedTx = await ethers.utils.resolveProperties(transaction);
291345
const alignedTx = await alignTxFromEthers(awaitedTx, ethers);
292346
const tx = prepareTransaction({
293-
client,
294-
chain: defineChain(await resolvePromisedValue(transaction.chainId)),
347+
client: client,
348+
chain: chain,
295349
accessList: alignedTx.accessList,
296350
data: alignedTx.data,
297351
gas: alignedTx.gas,
@@ -303,7 +357,10 @@ async function toEthersSigner(
303357
to: alignedTx.to ?? undefined,
304358
value: alignedTx.value,
305359
});
306-
const result = await sendTransaction({ transaction: tx, account });
360+
const result = await sendTransaction({
361+
transaction: tx,
362+
account: account,
363+
});
307364

308365
const response: ethers5.ethers.providers.TransactionResponse = {
309366
chainId: tx.chain.id,
@@ -350,6 +407,39 @@ async function toEthersSigner(
350407
return response;
351408
}
352409

410+
// eslint-disable-next-line jsdoc/require-jsdoc
411+
async _signTypedData(
412+
domain: ethers5.ethers.TypedDataDomain,
413+
types: Record<string, ethers5.ethers.TypedDataField[]>,
414+
value: Record<string, any>,
415+
): Promise<string> {
416+
if (!account) {
417+
throw new Error("Account not found");
418+
}
419+
const typedDataEncoder = new ethers.utils._TypedDataEncoder(types);
420+
421+
const typedData = {
422+
primaryType: typedDataEncoder.primaryType,
423+
domain: {
424+
chainId: domain.chainId
425+
? bigNumberIshToNumber(domain.chainId)
426+
: undefined,
427+
name: domain.name ?? undefined,
428+
429+
salt: domain.salt ? toHex(domain.salt.toString()) : undefined,
430+
verifyingContract: domain.verifyingContract ?? undefined,
431+
version: domain.version ?? undefined,
432+
},
433+
types,
434+
message: value,
435+
};
436+
437+
return account.signTypedData(typedData);
438+
}
439+
440+
/**
441+
* @internal
442+
*/
353443
override connect(): ethers5.ethers.Signer {
354444
return this;
355445
}
@@ -472,3 +562,14 @@ async function alignTxFromEthers(
472562
}
473563
}
474564
}
565+
566+
function bigNumberIshToBigint(value: ethers5.BigNumberish): bigint {
567+
if (typeof value === "bigint") {
568+
return value;
569+
}
570+
return BigInt(value.toString());
571+
}
572+
573+
function bigNumberIshToNumber(value: ethers5.BigNumberish): number {
574+
return Number(bigNumberIshToBigint(value));
575+
}

0 commit comments

Comments
 (0)