From b6939375ba3a0469f0a9cc0f27802898c720b86d Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Fri, 16 Feb 2024 14:15:29 -0800 Subject: [PATCH] add signer.toEthers for ethersv6 adapter --- packages/thirdweb/src/adapters/ethers6.ts | 213 +++++++++++++++++- .../src/wallets/utils/normalizeChainId.ts | 5 +- 2 files changed, 211 insertions(+), 7 deletions(-) diff --git a/packages/thirdweb/src/adapters/ethers6.ts b/packages/thirdweb/src/adapters/ethers6.ts index b772d8460d3..532b7633d9f 100644 --- a/packages/thirdweb/src/adapters/ethers6.ts +++ b/packages/thirdweb/src/adapters/ethers6.ts @@ -2,7 +2,7 @@ import type * as ethers5 from "ethers5"; import type * as ethers6 from "ethers6"; import * as universalethers from "ethers"; import type { Abi } from "abitype"; -import type { Hex, TransactionSerializable } from "viem"; +import type { AccessList, Hex, TransactionSerializable } from "viem"; import type { ThirdwebClient } from "../client/client.js"; import { getRpcUrlForChain, @@ -11,6 +11,9 @@ import { } from "../chain/index.js"; import { getContract, type ThirdwebContract } from "../contract/index.js"; import type { Account } from "../wallets/interfaces/wallet.js"; +import { normalizeChainId } from "../wallets/utils/normalizeChainId.js"; +import { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js"; +import { uint8ArrayToHex } from "../utils/uint8-array.js"; type Ethers6 = typeof ethers6; @@ -105,6 +108,21 @@ export const ethers6Adapter = /* @__PURE__ */ (() => { * ``` */ fromEthers: (signer: ethers6.Signer) => fromEthersSigner(signer), + + /** + * Converts a Thirdweb account to an ethers.js signer. + * @param client - The Thirdweb client. + * @param chain - The blockchain chain. + * @param account - The Thirdweb account. + * @returns A promise that resolves to an ethers.js signer. + * @example + * ```ts + * import { ethers6Adapter } from "@thirdweb/adapters/erthers6"; + * const signer = await ethers6Adapter.signer.toEthers(client, chain, account); + * ``` + */ + toEthers: (client: ThirdwebClient, chain: Chain, account: Account) => + toEthersSigner(ethers, client, chain, account), }, }; })(); @@ -208,11 +226,12 @@ async function fromEthersSigner(signer: ethers6.Signer): Promise { ) as Promise; }, signTransaction: async (tx) => { - return signer.signTransaction(alignTx(tx)) as Promise; + return signer.signTransaction(alignTxToEthers(tx)) as Promise; }, sendTransaction: async (tx) => { - const transactionHash = (await signer.sendTransaction(alignTx(tx))) - .hash as Hex; + const transactionHash = ( + await signer.sendTransaction(alignTxToEthers(tx)) + ).hash as Hex; return { transactionHash, }; @@ -230,12 +249,106 @@ async function fromEthersSigner(signer: ethers6.Signer): Promise { } /** - * Aligns a transaction object to fit the format expected by ethers5 library. + * Converts a Thirdweb account to an ethers.js signer. + * @param ethers - The ethers.js library. + * @param client - The Thirdweb client. + * @param chain - The blockchain chain. + * @param account - The Thirdweb account. + * @returns A promise that resolves to an ethers.js signer. + * @internal + */ +async function toEthersSigner( + ethers: Ethers6, + client: ThirdwebClient, + chain: Chain, + account: Account, +): Promise { + class ThirdwebAdapterSigner extends ethers.AbstractSigner { + address: string; + override provider: ethers6.ethers.JsonRpcProvider; + constructor(provider: ethers6.JsonRpcProvider, address: string) { + super(provider); + this.address = address; + this.provider = provider; + } + + override async getAddress(): Promise { + // needs to be a promise because ethers6 returns a promise + return this.address; + } + override connect( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _provider: ethers6.ethers.Provider | null, + ): ethers6.ethers.Signer { + return this; + } + override async sendTransaction( + tx: ethers6.ethers.TransactionRequest & { chainId: number }, + ): Promise { + const alignedTx = await alignTxFromEthers(tx); + const result = await account.sendTransaction({ + ...alignedTx, + chainId: tx.chainId, + }); + + const txResponseParams: ethers6.TransactionResponseParams = { + ...alignedTx, + blockHash: null, + from: this.address, + hash: result.transactionHash as string, + blockNumber: null, + index: 0, + gasLimit: alignedTx.gas as bigint, + // @ts-expect-error - we don't have this reliably so we'll just not include it + signature: null, + }; + + return new ethers.TransactionResponse(txResponseParams, this.provider); + } + override async signTransaction( + tx: ethers6.ethers.TransactionRequest, + ): Promise { + if (!account.signTransaction) { + throw new Error("Account does not support signing transactions"); + } + const viemTx = await alignTxFromEthers(tx); + + return account.signTransaction(viemTx); + } + override signMessage(message: string | Uint8Array): Promise { + return account.signMessage({ + message: + typeof message === "string" ? message : uint8ArrayToHex(message), + }); + } + override signTypedData( + domain: ethers6.ethers.TypedDataDomain, + types: Record, + value: Record, + ): Promise { + return account.signTypedData({ + // @ts-expect-error - types don't fully align here but works fine? + domain: domain ?? undefined, + types: types ?? undefined, + message: value, + }); + } + } + return new ThirdwebAdapterSigner( + toEthersProvider(ethers, client, chain), + account.address, + ); +} + +/** + * Aligns a transaction object to fit the format expected by ethers6 library. * @param tx - The transaction object to align. * @returns The aligned transaction object. * @internal */ -function alignTx(tx: TransactionSerializable): ethers6.TransactionRequest { +function alignTxToEthers( + tx: TransactionSerializable, +): ethers6.TransactionRequest { const { type: viemType, ...rest } = tx; // massage "type" to fit ethers @@ -260,3 +373,91 @@ function alignTx(tx: TransactionSerializable): ethers6.TransactionRequest { } return { ...rest, type }; } + +async function alignTxFromEthers( + tx: ethers6.TransactionRequest, +): Promise { + const { + type: ethersType, + accessList, + chainId: ethersChainId, + to: ethersTo, + // unused here on purpose + // eslint-disable-next-line @typescript-eslint/no-unused-vars + from, + data, + nonce, + value, + gasPrice, + gasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + ...rest + } = tx; + let chainId: number | undefined; + if (ethersChainId) { + chainId = normalizeChainId(ethersChainId); + } + + // massage "type" to fit ethers + let type: string; + switch (ethersType) { + case 0: { + type = "legacy"; + break; + } + case 1: { + type = "eip2930"; + break; + } + case 2: { + type = "eip1559"; + break; + } + default: { + // fall back to legacy + type = "legacy"; + break; + } + } + + const to = await resolveEthers6Address(ethersTo); + + return { + ...rest, + // access list is the same values just strictly typed + accessList: accessList as AccessList, + chainId, + type, + to, + data: (data ?? undefined) as Hex | undefined, + nonce: nonce ?? undefined, + value: value ? bigNumberIshToBigint(value) : undefined, + gasPrice: gasPrice ? bigNumberIshToBigint(gasPrice) : undefined, + gas: gasLimit ? bigNumberIshToBigint(gasLimit) : undefined, + maxFeePerGas: maxFeePerGas ? bigNumberIshToBigint(maxFeePerGas) : undefined, + maxPriorityFeePerGas: maxPriorityFeePerGas + ? bigNumberIshToBigint(maxPriorityFeePerGas) + : undefined, + }; +} + +async function resolveEthers6Address( + address: ethers6.AddressLike | null | undefined, +): Promise { + if (!address) { + return address; + } + address = await resolvePromisedValue(address); + if (typeof address === "string") { + return address; + } + return address.getAddress(); +} + +function bigNumberIshToBigint(value: ethers6.BigNumberish): bigint { + if (typeof value === "bigint") { + return value; + } + return BigInt(value); +} diff --git a/packages/thirdweb/src/wallets/utils/normalizeChainId.ts b/packages/thirdweb/src/wallets/utils/normalizeChainId.ts index a4fadd5b820..cc83d4a73a9 100644 --- a/packages/thirdweb/src/wallets/utils/normalizeChainId.ts +++ b/packages/thirdweb/src/wallets/utils/normalizeChainId.ts @@ -3,12 +3,15 @@ import { hexToNumber, isHex } from "viem"; /** * @internal */ -export function normalizeChainId(chainId: string | number): number { +export function normalizeChainId(chainId: string | number | bigint): number { if (typeof chainId === "number") { return chainId; } if (isHex(chainId)) { return hexToNumber(chainId); } + if (typeof chainId === "bigint") { + return Number(chainId); + } return parseInt(chainId, 10); }