diff --git a/.changeset/long-toys-act.md b/.changeset/long-toys-act.md new file mode 100644 index 0000000000..4ca3635e0e --- /dev/null +++ b/.changeset/long-toys-act.md @@ -0,0 +1,5 @@ +--- +"@talismn/chain-connector": patch +--- + +Improved error handling in Websocket connector diff --git a/.changeset/moody-moons-teach.md b/.changeset/moody-moons-teach.md new file mode 100644 index 0000000000..2e4e77e27f --- /dev/null +++ b/.changeset/moody-moons-teach.md @@ -0,0 +1,8 @@ +--- +"@talismn/balances-evm-native": patch +"@talismn/chain-connector-evm": patch +"@talismn/balances-evm-erc20": patch +"@talismn/util": patch +--- + +replace ethers by viem diff --git a/.changeset/quiet-papayas-cheat.md b/.changeset/quiet-papayas-cheat.md new file mode 100644 index 0000000000..92db0c3a8e --- /dev/null +++ b/.changeset/quiet-papayas-cheat.md @@ -0,0 +1,6 @@ +--- +"@talismn/chain-connector": patch +"@talismn/util": patch +--- + +Error handling improvements diff --git a/.changeset/witty-spiders-rhyme.md b/.changeset/witty-spiders-rhyme.md new file mode 100644 index 0000000000..7d09406d5e --- /dev/null +++ b/.changeset/witty-spiders-rhyme.md @@ -0,0 +1,6 @@ +--- +"@talismn/chain-connector-evm": minor +"@talismn/on-chain-id": minor +--- + +replace ethers by viem diff --git a/apps/extension/package.json b/apps/extension/package.json index 06057519ee..e27768d7c7 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -9,12 +9,13 @@ "@babel/eslint-parser": "^7.19.1", "@dnd-kit/core": "6.0.7", "@dnd-kit/sortable": "7.0.2", + "@eth-optimism/contracts-ts": "^0.17.0", "@ethereumjs/util": "8.0.3", "@floating-ui/react": "0.24.3", "@floating-ui/react-dom": "2.0.1", "@headlessui/react": "1.7.13", "@hookform/resolvers": "2.9.11", - "@ledgerhq/hw-app-eth": "6.33.3", + "@ledgerhq/hw-app-eth": "6.34.8", "@ledgerhq/hw-transport-webusb": "6.27.14", "@metamask/browser-passworder": "4.1.0", "@metamask/eth-sig-util": "5.1.0", @@ -92,7 +93,6 @@ "dotenv-webpack": "^7.1.1", "downshift": "^6.1.12", "eth-phishing-detect": "latest", - "ethers": "5.7.2", "fork-ts-checker-notifier-webpack-plugin": "^6.0.0", "fork-ts-checker-webpack-plugin": "^7.2.14", "framer-motion": "10.12.18", @@ -129,6 +129,7 @@ "scale-codec": "^0.11.0", "semver": "^7.5.4", "style-loader": "3.3.3", + "subshape": "^0.14.0", "talisman-ui": "workspace:*", "terser-webpack-plugin": "5.3.9", "toml": "^3.0.0", @@ -137,6 +138,7 @@ "typescript": "^5.2.2", "url-join": "^5.0.0", "uuid": "^8.3.2", + "viem": "^1.18.9", "webextension-polyfill": "0.8.0", "webpack": "^5.88.1", "webpack-cli": "^4.10.0", diff --git a/apps/extension/src/@talisman/util/formatEthValue.ts b/apps/extension/src/@talisman/util/formatEthValue.ts index 158d1f94a0..94e4daeb24 100644 --- a/apps/extension/src/@talisman/util/formatEthValue.ts +++ b/apps/extension/src/@talisman/util/formatEthValue.ts @@ -1,9 +1,7 @@ -import { formatDecimals } from "@talismn/util" -import { BigNumber, BigNumberish } from "ethers" -import { formatUnits } from "ethers/lib/utils" +import { formatDecimals, planckToTokens } from "@talismn/util" -export const formatEtherValue = (value: BigNumberish, decimals: number, symbol?: string) => { - return `${formatDecimals(formatUnits(BigNumber.from(value), decimals))}${ +export const formatEthValue = (value: bigint, decimals: number, symbol?: string) => { + return `${formatDecimals(planckToTokens(value.toString(), decimals))}${ symbol ? ` ${symbol}` : "" }` } diff --git a/apps/extension/src/core/config/sentry.ts b/apps/extension/src/core/config/sentry.ts index 9f3e7d0582..33c883b3d1 100644 --- a/apps/extension/src/core/config/sentry.ts +++ b/apps/extension/src/core/config/sentry.ts @@ -22,6 +22,10 @@ export const initSentry = (sentry: typeof SentryBrowser | typeof SentryReact) => release: process.env.RELEASE, sampleRate: 1, maxBreadcrumbs: 20, + ignoreErrors: [ + /(No window with id: )(\d+).?/, + /(disconnected from wss)[(]?:\/\/[\w./:-]+: \d+:: Normal Closure[)]?/, + ], // prevents sending the event if user has disabled error tracking beforeSend: async (event) => ((await firstValueFrom(useErrorTracking)) ? event : null), // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/apps/extension/src/core/domains/accounts/handler.ts b/apps/extension/src/core/domains/accounts/handler.ts index 92f73c137b..ec55326b2b 100644 --- a/apps/extension/src/core/domains/accounts/handler.ts +++ b/apps/extension/src/core/domains/accounts/handler.ts @@ -437,11 +437,11 @@ export default class AccountsHandler extends ExtensionHandler { const { err, val } = await getPairForAddressSafely(address, async (pair) => { assert(pair.type === "ethereum", "Private key cannot be exported for this account type") - const pk = getPrivateKey(pair, pw as string) + const pk = getPrivateKey(pair, pw as string, "hex") talismanAnalytics.capture("account export", { type: pair.type, mode: "pk" }) - return pk.toString("hex") + return pk }) if (err) throw new Error(val as string) diff --git a/apps/extension/src/core/domains/encrypt/handler.ts b/apps/extension/src/core/domains/encrypt/handler.ts index eba69f8a12..b78c6ba0e5 100644 --- a/apps/extension/src/core/domains/encrypt/handler.ts +++ b/apps/extension/src/core/domains/encrypt/handler.ts @@ -27,7 +27,7 @@ export default class EncryptHandler extends ExtensionHandler { const pw = this.stores.password.getPassword() assert(pw, "Unable to retreive password from store.") - const pk = getPrivateKey(pair, pw) + const pk = getPrivateKey(pair, pw, "u8a") const kp = { publicKey: pair.publicKey, secretKey: u8aToU8a(pk) } as Keypair assert(kp.secretKey.length === 64, "Talisman secretKey is incorrect length") @@ -66,7 +66,7 @@ export default class EncryptHandler extends ExtensionHandler { const pw = this.stores.password.getPassword() assert(pw, "Unable to retreive password from store.") - const pk = getPrivateKey(pair, pw) + const pk = getPrivateKey(pair, pw, "u8a") assert(pk.length === 64, "Talisman secretKey is incorrect length") diff --git a/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts b/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts index dbb189e3bb..cb1d061b1b 100644 --- a/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts +++ b/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers" +import { parseGwei } from "viem" import { getEthDerivationPath, @@ -8,78 +8,71 @@ import { isSafeImageUrl, } from "../helpers" -const baseFeePerGas = ethers.utils.parseUnits("2", "gwei") -const maxPriorityFeePerGas = ethers.utils.parseUnits("8", "gwei") +const baseFeePerGas = parseGwei("2") +const maxPriorityFeePerGas = parseGwei("8") describe("Test ethereum helpers", () => { test("getMaxFeePerGas 0 block", async () => { - const result = getMaxFeePerGas(baseFeePerGas, maxPriorityFeePerGas, 0).toString() - const expected = ethers.utils.parseUnits("10", "gwei").toString() + const result = getMaxFeePerGas(baseFeePerGas, maxPriorityFeePerGas, 0) + const expected = parseGwei("10") expect(result).toEqual(expected) }) test("getMaxFeePerGas 8 block", async () => { - const result = getMaxFeePerGas(baseFeePerGas, maxPriorityFeePerGas, 8).toString() - const expected = ethers.utils.parseUnits("13131569026", "wei").toString() + const result = getMaxFeePerGas(baseFeePerGas, maxPriorityFeePerGas, 8) - expect(result).toEqual(expected) + expect(result).toEqual(13131569026n) }) test("getTotalFeesFromGasSettings - EIP1559 maxFee lower than baseFee", () => { const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( { - type: 2, - maxFeePerGas: ethers.utils.parseUnits("1.5", "gwei"), - maxPriorityFeePerGas: ethers.utils.parseUnits("0.5", "gwei"), - gasLimit: 22000, + type: "eip1559", + maxFeePerGas: parseGwei("1.5"), + maxPriorityFeePerGas: parseGwei("0.5"), + gas: 22000n, }, - 21000, - baseFeePerGas - ) // - - const expectedEstimatedFee = ethers.utils.parseUnits("42000000000000", "wei").toString() - const expectedMaxFee = ethers.utils.parseUnits("44000000000000", "wei").toString() + 21000n, + baseFeePerGas, + 0n + ) - expect(estimatedFee.toString()).toEqual(expectedEstimatedFee) - expect(maxFee.toString()).toEqual(expectedMaxFee) + expect(estimatedFee).toEqual(42000000000000n) + expect(maxFee).toEqual(44000000000000n) }) test("getTotalFeesFromGasSettings - EIP1559 classic", () => { const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( { - type: 2, - maxFeePerGas: ethers.utils.parseUnits("3.5", "gwei"), - maxPriorityFeePerGas: ethers.utils.parseUnits("0.5", "gwei"), - gasLimit: 22000, + type: "eip1559", + maxFeePerGas: parseGwei("3.5"), + maxPriorityFeePerGas: parseGwei("0.5"), + gas: 22000n, }, - 21000, - baseFeePerGas + 21000n, + baseFeePerGas, + 0n ) - const expectedEstimatedFee = ethers.utils.parseUnits("52500000000000", "wei").toString() - const expectedMaxFee = ethers.utils.parseUnits("88000000000000", "wei").toString() - - expect(estimatedFee.toString()).toEqual(expectedEstimatedFee) - expect(maxFee.toString()).toEqual(expectedMaxFee) + expect(estimatedFee).toEqual(52500000000000n) + expect(maxFee).toEqual(88000000000000n) }) test("getTotalFeesFromGasSettings - Legacy", () => { const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( { - type: 0, - gasPrice: baseFeePerGas.add(maxPriorityFeePerGas), - gasLimit: 22000, + type: "legacy", + gasPrice: baseFeePerGas + maxPriorityFeePerGas, + gas: 22000n, }, - 21000, - baseFeePerGas + 21000n, + baseFeePerGas, + 0n ) - const expectedEstimatedFee = ethers.utils.parseUnits("210000", "gwei").toString() - const expectedMaxFee = ethers.utils.parseUnits("220000", "gwei").toString() - - expect(estimatedFee.toString()).toEqual(expectedEstimatedFee) - expect(maxFee.toString()).toEqual(expectedMaxFee) + expect(estimatedFee).toEqual(parseGwei("210000")) + expect(maxFee).toEqual(parseGwei("220000")) }) test("getEthDerivationPath", () => { diff --git a/apps/extension/src/core/domains/ethereum/errors.ts b/apps/extension/src/core/domains/ethereum/errors.ts index a7f7e1a3c4..20da94b185 100644 --- a/apps/extension/src/core/domains/ethereum/errors.ts +++ b/apps/extension/src/core/domains/ethereum/errors.ts @@ -1,118 +1,69 @@ -import { ethers } from "ethers" +import { log } from "@core/log" -export const getEthersErrorLabelFromCode = (code?: string | number) => { - if (typeof code === "string") { - switch (code) { - // operational errors - case ethers.errors.BUFFER_OVERRUN: - return "Buffer overrun" - case ethers.errors.NUMERIC_FAULT: - return "Numeric fault" - - // argument errors - case ethers.errors.UNEXPECTED_ARGUMENT: - return "Too many arguments" - case ethers.errors.MISSING_ARGUMENT: - return "Missing argument" - case ethers.errors.INVALID_ARGUMENT: - return "Invalid argument" - case ethers.errors.MISSING_NEW: - return "Missing constructor" - - // interactions errors - case ethers.errors.ACTION_REJECTED: - return "Action rejected" - - // blockchain errors - case ethers.errors.CALL_EXCEPTION: - return "Contract method failed to execute" - case ethers.errors.INSUFFICIENT_FUNDS: - return "Insufficient balance" - case ethers.errors.NONCE_EXPIRED: - return "Nonce expired" - case ethers.errors.UNPREDICTABLE_GAS_LIMIT: - // TODO could be gas limit to low, parameters making the operation impossible, balance to low to pay for gas, or anything else that makes the operation impossible to succeed. - // TODO need a better copy to explain this, but gas limit issue has to be stated as it can be solved by the user. - return "Transaction may fail because of insufficient balance, incorrect parameters or may require higher gas limit" - case ethers.errors.TRANSACTION_REPLACED: - return "Transaction was replaced with another one with higher gas price" - case ethers.errors.REPLACEMENT_UNDERPRICED: - return "Replacement fee is too low, try again with higher gas price" - - // generic errors - case ethers.errors.NETWORK_ERROR: - return "Network error" - case ethers.errors.UNSUPPORTED_OPERATION: - return "Unsupported operation" - case ethers.errors.NOT_IMPLEMENTED: - return "Not implemented" - case ethers.errors.TIMEOUT: - return "Timeout exceeded" - case ethers.errors.SERVER_ERROR: - return "Server error" - case ethers.errors.UNKNOWN_ERROR: - default: - return "Unknown error" - } - } +import { AnyEvmError } from "./types" +export const getErrorLabelFromCode = (code: number) => { // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md - if (typeof code === "number") { - switch (code) { - case -32700: - return "Parse error. Invalid JSON was received by the server" - case -32600: - return "Invalid request, it could not be understood by the server" - case -32601: - return "Method does not exist" - case -32602: - return "Invalid method parameters" - case -32603: - return "Internal JSON-RPC error" - case -32000: - return "Missing or invalid parameters" - case -32001: - return "Requested resource not found" - case -32002: - return "Requested resource is not available" - case -32003: - return "Transaction rejected" - case -32004: - return "Method is not implemented" - case -32005: - return "Request exceeds defined limit" - case -32006: - return "Transaction not yet known" - default: - return "Unknown error" + switch (code) { + case -32700: + return "Parse error. Invalid JSON was received by the server" + case -32600: + return "Invalid request, it could not be understood by the server" + case -32601: + return "Method does not exist" + case -32602: + return "Invalid method parameters" + case -32603: + return "Internal JSON-RPC error" + case -32000: + return "Missing or invalid parameters" + case -32001: + return "Requested resource not found" + case -32002: + return "Requested resource is not available" + case -32003: + return "Transaction rejected" + case -32004: + return "Method is not implemented" + case -32005: + return "Request exceeds defined limit" + case -32006: + return "Transaction not yet known" + default: { + log.warn("Unknown error code", { code }) + return "Unknown error" } } +} - return undefined +export const getEvmErrorCause = (err: unknown): AnyEvmError => { + const error = err as AnyEvmError + return error?.cause ? getEvmErrorCause(error.cause) : error } // turns errors into short and human readable message. // main use case is teling the user why a transaction failed without going into details and clutter the UI export const getHumanReadableErrorMessage = (error: unknown) => { - const { - code, - reason, - error: serverError, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } = error as { code?: string; reason?: string; error?: any } + if (!error) return undefined - if (serverError) { - const message = serverError.error?.message ?? serverError.reason ?? serverError.message - return message - .replace("VM Exception while processing transaction: reverted with reason string ", "") - .replace("VM Exception while processing transaction: revert", "") - .replace("VM Exception while processing transaction:", "") - .trim() - } + const { message, shortMessage, details, code } = error as AnyEvmError + + if (details) return details + + if (shortMessage) return shortMessage - if (reason === "processing response error") return "Invalid transaction" + if (code) return getErrorLabelFromCode(code) - if (reason) return reason + if (message) return message + + return undefined +} - return getEthersErrorLabelFromCode(code) +export const cleanupEvmErrorMessage = (message: string) => { + if (!message) return "Unknown error" + return message + .replace("VM Exception while processing transaction: reverted with reason string ", "") + .replace("VM Exception while processing transaction: revert", "") + .replace("VM Exception while processing transaction:", "") + .trim() } diff --git a/apps/extension/src/core/domains/ethereum/handler.extension.ts b/apps/extension/src/core/domains/ethereum/handler.extension.ts index 4fde24fda3..d281c6a65f 100644 --- a/apps/extension/src/core/domains/ethereum/handler.extension.ts +++ b/apps/extension/src/core/domains/ethereum/handler.extension.ts @@ -10,7 +10,6 @@ import { import { talismanAnalytics } from "@core/libs/Analytics" import { ExtensionHandler } from "@core/libs/Handler" import { requestStore } from "@core/libs/requests/store" -import { log } from "@core/log" import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" import { chaindataProvider } from "@core/rpcs/chaindata" import { MessageHandler, MessageTypes, RequestTypes, ResponseType } from "@core/types" @@ -22,12 +21,12 @@ import { assert } from "@polkadot/util" import { HexString } from "@polkadot/util/types" import { evmNativeTokenId } from "@talismn/balances" import { CustomEvmNetwork, githubUnknownTokenLogoUrl } from "@talismn/chaindata-provider" -import { ethers } from "ethers" +import { isEthereumAddress } from "@talismn/util" +import { privateKeyToAccount } from "viem/accounts" import { getHostName } from "../app/helpers" import { getHumanReadableErrorMessage } from "./errors" -import { rebuildTransactionRequestNumbers } from "./helpers" -import { getProviderForEvmNetworkId } from "./rpcProviders" +import { parseTransactionRequest } from "./helpers" import { getTransactionCount, incrementTransactionCount } from "./transactionCountManager" export class EthHandler extends ExtensionHandler { @@ -44,17 +43,19 @@ export class EthHandler extends ExtensionHandler { account: { address: accountAddress }, } = queued - const provider = await getProviderForEvmNetworkId(ethChainId) - assert(provider, "Unable to find provider for chain " + ethChainId) + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(ethChainId) + assert(client, "Unable to find client for chain " + ethChainId) - const { chainId, hash, from } = await provider.sendTransaction(signedPayload) + const hash = await client.sendRawTransaction({ + serializedTransaction: signedPayload, + }) - watchEthereumTransaction(chainId.toString(), hash, unsigned, { + watchEthereumTransaction(ethChainId, hash, unsigned, { siteUrl: queued.url, notifications: true, }) - incrementTransactionCount(from, chainId.toString()) + if (unsigned.from) incrementTransactionCount(unsigned.from, ethChainId) resolve(hash) @@ -65,7 +66,7 @@ export class EthHandler extends ExtensionHandler { method, hostName: ok ? host : null, dapp: queued.url, - chain: chainId, + chain: Number(ethChainId), networkType: "ethereum", hardwareType: account?.meta.hardwareType, }) @@ -85,27 +86,30 @@ export class EthHandler extends ExtensionHandler { assert(queued, "Unable to find request") const { resolve, reject, ethChainId, account, url } = queued - const provider = await getProviderForEvmNetworkId(ethChainId) - assert(provider, "Unable to find provider for chain " + ethChainId) + assert(isEthereumAddress(account.address), "Invalid ethereum address") - // rebuild BigNumber property values (converted to json when serialized) - const tx = rebuildTransactionRequestNumbers(transaction) - tx.nonce = await getTransactionCount(account.address, ethChainId) + const tx = parseTransactionRequest(transaction) + if (tx.nonce === undefined) tx.nonce = await getTransactionCount(account.address, ethChainId) const result = await getPairForAddressSafely(account.address, async (pair) => { + const client = await chainConnectorEvm.getWalletClientForEvmNetwork(ethChainId) + assert(client, "Missing client for chain " + ethChainId) + const password = this.stores.password.getPassword() assert(password, "Unauthorised") - const privateKey = getPrivateKey(pair, password) - const signer = new ethers.Wallet(privateKey, provider) - const { hash } = await signer.sendTransaction(tx) + const privateKey = getPrivateKey(pair, password, "hex") + const account = privateKeyToAccount(privateKey) - return hash + return await client.sendTransaction({ + chain: client.chain, + account, + ...tx, + }) }) if (result.ok) { - // long running operation, we do not want this inside getPairForAddressSafely - watchEthereumTransaction(ethChainId, result.val, tx, { + watchEthereumTransaction(ethChainId, result.val, transaction, { siteUrl: queued.url, notifications: true, }) @@ -119,7 +123,7 @@ export class EthHandler extends ExtensionHandler { type: "evm sign and send", hostName: ok ? host : null, dapp: url, - chain: ethChainId, + chain: Number(ethChainId), networkType: "ethereum", }) @@ -135,31 +139,27 @@ export class EthHandler extends ExtensionHandler { } private sendSigned: MessageHandler<"pri(eth.signing.sendSigned)"> = async ({ + evmNetworkId, unsigned, signed, transferInfo, }) => { - assert(unsigned.chainId, "chainId is not defined") - const evmNetworkId = unsigned.chainId.toString() - - const provider = await getProviderForEvmNetworkId(evmNetworkId) - assert(provider, `Unable to find provider for chain ${unsigned.chainId}`) + assert(evmNetworkId, "chainId is not defined") - // rebuild BigNumber property values (converted to json when serialized) - const tx = rebuildTransactionRequestNumbers(unsigned) + const client = await chainConnectorEvm.getWalletClientForEvmNetwork(evmNetworkId) + assert(client, "Missing client for chain " + evmNetworkId) try { - const { hash } = await provider.sendTransaction(signed) + const hash = await client.sendRawTransaction({ serializedTransaction: signed }) - // long running operation, we do not want this inside getPairForAddressSafely - watchEthereumTransaction(evmNetworkId, hash, tx, { + watchEthereumTransaction(evmNetworkId, hash, unsigned, { notifications: true, transferInfo, }) talismanAnalytics.captureDelayed("send transaction", { type: "evm send signed", - chain: evmNetworkId, + chain: Number(evmNetworkId), networkType: "ethereum", }) @@ -170,40 +170,40 @@ export class EthHandler extends ExtensionHandler { } private signAndSend: MessageHandler<"pri(eth.signing.signAndSend)"> = async ({ + evmNetworkId, unsigned, transferInfo, }) => { - assert(unsigned.chainId, "chainId is not defined") + assert(evmNetworkId, "chainId is not defined") assert(unsigned.from, "from is not defined") - const evmNetworkId = unsigned.chainId.toString() - - const provider = await getProviderForEvmNetworkId(evmNetworkId) - assert(provider, `Unable to find provider for chain ${unsigned.chainId}`) - - // rebuild BigNumber property values (converted to json when serialized) - const tx = rebuildTransactionRequestNumbers(unsigned) const result = await getPairForAddressSafely(unsigned.from, async (pair) => { + const client = await chainConnectorEvm.getWalletClientForEvmNetwork(evmNetworkId) + assert(client, "Missing client for chain " + evmNetworkId) + const password = this.stores.password.getPassword() assert(password, "Unauthorised") - const privateKey = getPrivateKey(pair, password) - const signer = new ethers.Wallet(privateKey, provider) + const privateKey = getPrivateKey(pair, password, "hex") + const account = privateKeyToAccount(privateKey) - const { hash } = await signer.sendTransaction(tx) + const tx = parseTransactionRequest(unsigned) - return hash as HexString + return await client.sendTransaction({ + chain: client.chain, + account, + ...tx, + }) }) if (result.ok) { - // long running operation, we do not want this inside getPairForAddressSafely - watchEthereumTransaction(evmNetworkId, result.val, tx, { + watchEthereumTransaction(evmNetworkId, result.val, unsigned, { notifications: true, transferInfo, }) talismanAnalytics.captureDelayed("send transaction", { type: "evm sign and send", - chain: evmNetworkId, + chain: Number(evmNetworkId), networkType: "ethereum", }) @@ -241,7 +241,7 @@ export class EthHandler extends ExtensionHandler { isHardware: true, hostName: ok ? host : null, dapp: url, - chain: queued.ethChainId, + chain: Number(queued.ethChainId), networkType: "ethereum", hardwareType: account?.meta.hardwareType, }) @@ -259,7 +259,7 @@ export class EthHandler extends ExtensionHandler { const { val, ok } = await getPairForAddressSafely(queued.account.address, async (pair) => { const pw = this.stores.password.getPassword() assert(pw, "Unauthorised") - const privateKey = getPrivateKey(pair, pw) + const privateKey = getPrivateKey(pair, pw, "buffer") let signature: string if (method === "personal_sign") { @@ -295,7 +295,7 @@ export class EthHandler extends ExtensionHandler { isHardware: true, hostName: ok ? host : null, dapp: queued.url, - chain: queued.ethChainId, + chain: Number(queued.ethChainId), networkType: "ethereum", }) @@ -325,7 +325,7 @@ export class EthHandler extends ExtensionHandler { talismanAnalytics.captureDelayed("sign reject", { method: queued.method, dapp: queued.url, - chain: queued.ethChainId, + chain: Number(queued.ethChainId), }) return true @@ -529,18 +529,13 @@ export class EthHandler extends ExtensionHandler { } private ethRequest: MessageHandler<"pri(eth.request)"> = async ({ chainId, method, params }) => { - const provider = await getProviderForEvmNetworkId(chainId, { batch: true }) - assert(provider, `No healthy RPCs available for chain ${chainId}`) - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await provider.send(method, params as unknown as any[]) - } catch (err) { - log.error("[ethRequest]", { err }) - // errors raised from batches are raw (number code), errors raised from ethers JsonProvider are wrapped by ethers (text code) - // throw error as-is so frontend can figure it out on it's own it, while keeping access to underlying error message - // any component interested in knowing what the error is about should use @core/domains/ethereum/errors helpers - throw err - } + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(chainId) + assert(client, `No client for chain ${chainId}`) + + return client.request({ + method: method as never, + params: params as never, + }) } public async handle( diff --git a/apps/extension/src/core/domains/ethereum/handler.tabs.ts b/apps/extension/src/core/domains/ethereum/handler.tabs.ts index 30e154cb20..56e1a049b3 100644 --- a/apps/extension/src/core/domains/ethereum/handler.tabs.ts +++ b/apps/extension/src/core/domains/ethereum/handler.tabs.ts @@ -10,6 +10,7 @@ import { import { CustomErc20Token } from "@core/domains/tokens/types" import i18next from "@core/i18nConfig" import { + ETH_ERROR_EIP1474_INTERNAL_ERROR, ETH_ERROR_EIP1474_INVALID_INPUT, ETH_ERROR_EIP1474_INVALID_PARAMS, ETH_ERROR_EIP1474_RESOURCE_UNAVAILABLE, @@ -20,21 +21,14 @@ import { ETH_ERROR_UNKNOWN_CHAIN_NOT_CONFIGURED, EthProviderRpcError, } from "@core/injectEth/EthProviderRpcError" -import { - AnyEthRequest, - EthProviderMessage, - EthRequestArguments, - EthRequestSignArguments, - EthRequestSignatures, -} from "@core/injectEth/types" +import { AnyEthRequest } from "@core/injectEth/types" import { TabsHandler } from "@core/libs/Handler" import { log } from "@core/log" +import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" import { chaindataProvider } from "@core/rpcs/chaindata" import type { RequestSignatures, RequestTypes, ResponseType } from "@core/types" import { Port } from "@core/types/base" -import { getErc20TokenInfo } from "@core/util/getErc20TokenInfo" import { urlToDomain } from "@core/util/urlToDomain" -import { recoverPersonalSignature } from "@metamask/eth-sig-util" import keyring from "@polkadot/ui-keyring" import { accounts as accountsObservable } from "@polkadot/ui-keyring/observable/accounts" import { assert } from "@polkadot/util" @@ -42,12 +36,23 @@ import { isEthereumAddress } from "@polkadot/util-crypto" import { convertAddress } from "@talisman/util/convertAddress" import { githubUnknownTokenLogoUrl } from "@talismn/chaindata-provider" import { throwAfter } from "@talismn/util" -import { ethers, providers } from "ethers" - +import { + PublicClient, + RpcError, + createClient, + getAddress, + http, + recoverMessageAddress, + toHex, +} from "viem" +import { hexToNumber, isHex } from "viem/utils" + +import { getErc20TokenInfo } from "../../util/getErc20TokenInfo" import { ERROR_DUPLICATE_AUTH_REQUEST_MESSAGE, requestAuthoriseSite, } from "../sitesAuthorised/requests" +import { getEvmErrorCause } from "./errors" import { getErc20TokenId, isValidAddEthereumRequestParam, @@ -56,8 +61,16 @@ import { sanitizeWatchAssetRequestParam, } from "./helpers" import { requestAddNetwork, requestWatchAsset } from "./requests" -import { getProviderForEthereumNetwork, getProviderForEvmNetworkId } from "./rpcProviders" -import { Web3WalletPermission, Web3WalletPermissionTarget } from "./types" +import { + AnyEvmError, + EthProviderMessage, + EthRequestArgs, + EthRequestArguments, + EthRequestResult, + EthRequestSignArguments, + Web3WalletPermission, + Web3WalletPermissionTarget, +} from "./types" interface EthAuthorizedSite extends AuthorizedSite { ethChainId: number @@ -73,7 +86,10 @@ export class EthTabsHandler extends TabsHandler { } } - async getSiteDetails(url: string, authorisedAddress?: string): Promise { + private async getSiteDetails( + url: string, + authorisedAddress?: string + ): Promise { let site try { @@ -90,14 +106,14 @@ export class EthTabsHandler extends TabsHandler { return site as EthAuthorizedSite } - async getProvider(url: string, authorisedAddress?: string): Promise { + private async getPublicClient(url: string, authorisedAddress?: string): Promise { const site = await this.getSiteDetails(url, authorisedAddress) const ethereumNetwork = await chaindataProvider.getEvmNetwork(site.ethChainId.toString()) if (!ethereumNetwork) throw new EthProviderRpcError("Network not supported", ETH_ERROR_EIP1993_CHAIN_DISCONNECTED) - const provider = await getProviderForEthereumNetwork(ethereumNetwork, { batch: true }) + const provider = await chainConnectorEvm.getPublicClientForEvmNetwork(ethereumNetwork.id) if (!provider) throw new EthProviderRpcError( `No provider for network ${ethereumNetwork.id} (${ethereumNetwork.name})`, @@ -156,7 +172,7 @@ export class EthTabsHandler extends TabsHandler { ) .filter(({ type }) => type === "ethereum") // send as - .map(({ address }) => ethers.utils.getAddress(address).toLowerCase()) + .map(({ address }) => getAddress(address).toLowerCase()) ) } @@ -190,10 +206,7 @@ export class EthTabsHandler extends TabsHandler { if (!site) return siteId = site.id if (site.ethChainId && site.ethAddresses?.length) { - chainId = - typeof site?.ethChainId !== "undefined" - ? ethers.utils.hexValue(site.ethChainId) - : undefined + chainId = site?.ethChainId !== undefined ? toHex(site.ethChainId) : undefined accounts = site.ethAddresses ?? [] // check that the network is still registered before broadcasting @@ -228,10 +241,7 @@ export class EthTabsHandler extends TabsHandler { try { // new state for this dapp - chainId = - typeof site?.ethChainId !== "undefined" - ? ethers.utils.hexValue(site.ethChainId) - : undefined + chainId = site?.ethChainId !== undefined ? toHex(site.ethChainId) : undefined //TODO check eth addresses still exist accounts = site?.ethAddresses ?? [] connected = !!accounts.length @@ -259,7 +269,7 @@ export class EthTabsHandler extends TabsHandler { if (connected && chainId && prevAccounts?.join() !== accounts.join()) { sendToClient({ type: "accountsChanged", - data: accounts.map((ac) => ethers.utils.getAddress(ac).toLowerCase()), + data: accounts.map((ac) => getAddress(ac).toLowerCase()), }) } } catch (err) { @@ -293,7 +303,7 @@ export class EthTabsHandler extends TabsHandler { url: string, request: EthRequestArguments<"wallet_addEthereumChain">, port: Port - ) => { + ): Promise> => { const { params: [network], } = request @@ -322,14 +332,15 @@ export class EthTabsHandler extends TabsHandler { await Promise.all( network.rpcUrls.map(async (rpcUrl) => { try { - const provider = new providers.JsonRpcProvider(rpcUrl) - const providerChainIdHex: string = await Promise.race([ - provider.send("eth_chainId", []), + const client = createClient({ transport: http(rpcUrl, { retryCount: 1 }) }) + const rpcChainIdHex = await Promise.race([ + client.request({ method: "eth_chainId" }), throwAfter(10_000, "timeout"), // 10 sec timeout ]) - const providerChainId = parseInt(providerChainIdHex, 16) + assert(!!rpcChainIdHex, `No chainId returned for ${rpcUrl}`) + const rpcChainId = hexToNumber(rpcChainIdHex) - assert(providerChainId === chainId, "chainId mismatch") + assert(rpcChainId === chainId, "chainId mismatch") } catch (err) { log.error({ err }) throw new EthProviderRpcError("Invalid rpc " + rpcUrl, ETH_ERROR_EIP1474_INVALID_PARAMS) @@ -353,7 +364,7 @@ export class EthTabsHandler extends TabsHandler { private switchEthereumChain = async ( url: string, request: EthRequestArguments<"wallet_switchEthereumChain"> - ) => { + ): Promise> => { const { params: [{ chainId: hexChainId }], } = request @@ -368,7 +379,7 @@ export class EthTabsHandler extends TabsHandler { ETH_ERROR_UNKNOWN_CHAIN_NOT_CONFIGURED ) - const provider = await getProviderForEthereumNetwork(ethereumNetwork, { batch: true }) + const provider = await chainConnectorEvm.getPublicClientForEvmNetwork(ethereumNetwork.id) if (!provider) throw new EthProviderRpcError( `Failed to connect to network ${ethChainId}`, @@ -393,35 +404,29 @@ export class EthTabsHandler extends TabsHandler { return site?.ethChainId ?? DEFAULT_ETH_CHAIN_ID } - private async getFallbackRequest( - url: string, - request: EthRequestArguments - ): Promise { + private async getFallbackRequest(url: string, request: AnyEthRequest): Promise { // obtain the chain id without checking auth. // note: this method is only called if method doesn't require auth, or if auth is already checked const chainId = await this.getChainId(url) + const publicClient = await chainConnectorEvm.getPublicClientForEvmNetwork(chainId.toString()) - const ethereumNetwork = await chaindataProvider.getEvmNetwork(chainId.toString()) - if (!ethereumNetwork) + if (!publicClient) throw new EthProviderRpcError( `Unknown network ${chainId}`, ETH_ERROR_UNKNOWN_CHAIN_NOT_CONFIGURED ) - const provider = await getProviderForEthereumNetwork(ethereumNetwork, { batch: true }) - if (!provider) - throw new EthProviderRpcError( - `Failed to connect to network ${chainId}`, - ETH_ERROR_EIP1993_CHAIN_DISCONNECTED - ) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return provider.send(request.method, request.params as unknown as any[]) + return publicClient.request({ + method: request.method as never, + params: request.params as never, + }) } - private signMessage = async (url: string, request: EthRequestSignArguments, port: Port) => { - const { params, method } = request as EthRequestSignArguments - + private signMessage = async ( + url: string, + { params, method }: EthRequestSignArguments, + port: Port + ) => { // eth_signTypedData requires a non-empty array of parameters, else throw (uniswap will then call v4) if (method === "eth_signTypedData") { if (!Array.isArray(params[0])) @@ -432,12 +437,17 @@ export class EthTabsHandler extends TabsHandler { method ) // on https://astar.network, params are in reverse order - if (isMessageFirst && isEthereumAddress(params[0]) && !isEthereumAddress(params[1])) + if ( + typeof params[0] === "string" && + isMessageFirst && + isEthereumAddress(params[0]) && + !isEthereumAddress(params[1]) + ) isMessageFirst = false const [uncheckedMessage, from] = isMessageFirst - ? [params[0], ethers.utils.getAddress(params[1])] - : [params[1], ethers.utils.getAddress(params[0])] + ? [params[0], getAddress(params[1])] + : [params[1], getAddress(params[0] as string)] // message is either a raw string or a hex string or an object (signTypedData_v1) const message = @@ -448,7 +458,7 @@ export class EthTabsHandler extends TabsHandler { const address = site.ethAddresses[0] const pair = keyring.getPair(address) - if (!address || !pair || ethers.utils.getAddress(address) !== ethers.utils.getAddress(from)) { + if (!address || !pair || getAddress(address) !== getAddress(from)) { throw new EthProviderRpcError( `No account available for ${url}`, ETH_ERROR_EIP1993_UNAUTHORIZED @@ -461,7 +471,7 @@ export class EthTabsHandler extends TabsHandler { message, site.ethChainId.toString(), { - address: ethers.utils.getAddress(address), + address: getAddress(address), ...pair.meta, }, port @@ -472,7 +482,7 @@ export class EthTabsHandler extends TabsHandler { url: string, request: EthRequestArguments<"wallet_watchAsset">, port: Port - ) => { + ): Promise> => { if (!isValidWatchAssetRequestParam(request.params)) throw new EthProviderRpcError("Invalid parameter", ETH_ERROR_EIP1474_INVALID_PARAMS) @@ -491,8 +501,8 @@ export class EthTabsHandler extends TabsHandler { if (existing) throw new EthProviderRpcError("Asset already exists", ETH_ERROR_EIP1474_INVALID_PARAMS) - const provider = await getProviderForEvmNetworkId(ethChainId.toString()) - if (!provider) + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(ethChainId.toString()) + if (!client) throw new EthProviderRpcError( "Network not supported", ETH_ERROR_EIP1993_CHAIN_DISCONNECTED @@ -500,7 +510,7 @@ export class EthTabsHandler extends TabsHandler { try { // eslint-disable-next-line no-var - var tokenInfo = await getErc20TokenInfo(provider, ethChainId.toString(), address) + var tokenInfo = await getErc20TokenInfo(client, ethChainId.toString(), address) } catch (err) { throw new EthProviderRpcError("Asset not found", ETH_ERROR_EIP1474_INVALID_PARAMS) } @@ -561,25 +571,27 @@ export class EthTabsHandler extends TabsHandler { private async sendTransaction( url: string, - request: EthRequestArguments<"eth_sendTransaction">, + { params: [txRequest] }: EthRequestArguments<"eth_sendTransaction">, port: Port ) { - const { - params: [txRequest], - } = request as EthRequestArguments<"eth_sendTransaction"> - const site = await this.getSiteDetails(url, txRequest.from) - // ensure chainId isn't an hex (ex: Zerion) - if (typeof txRequest.chainId === "string" && (txRequest.chainId as string).startsWith("0x")) - txRequest.chainId = parseInt(txRequest.chainId, 16) + { + // eventhough not standard, some transactions specify a chainId in the request + // throw an error if it's not the current tab's chainId + let specifiedChainId = (txRequest as unknown as { chainId?: string | number }).chainId + + // ensure chainId isn't an hex (ex: Zerion) + if (isHex(specifiedChainId)) specifiedChainId = hexToNumber(specifiedChainId) - // checks that the request targets currently selected network - if (txRequest.chainId && site.ethChainId !== txRequest.chainId) - throw new EthProviderRpcError("Wrong network", ETH_ERROR_EIP1474_INVALID_PARAMS) + // checks that the request targets currently selected network + if (specifiedChainId && Number(site.ethChainId) !== Number(specifiedChainId)) + throw new EthProviderRpcError("Wrong network", ETH_ERROR_EIP1474_INVALID_PARAMS) + } try { - await this.getProvider(url, txRequest.from) + // ensure that we have a valid provider for the current network + await this.getPublicClient(url, txRequest.from) } catch (error) { throw new EthProviderRpcError("Network not supported", ETH_ERROR_EIP1993_CHAIN_DISCONNECTED) } @@ -588,7 +600,7 @@ export class EthTabsHandler extends TabsHandler { // allow only the currently selected account in "from" field if (txRequest.from?.toLowerCase() !== address.toLowerCase()) - throw new EthProviderRpcError("Unknown from account", ETH_ERROR_EIP1474_INVALID_INPUT) + throw new EthProviderRpcError("Invalid from account", ETH_ERROR_EIP1474_INVALID_INPUT) const pair = keyring.getPair(address) @@ -601,11 +613,7 @@ export class EthTabsHandler extends TabsHandler { return signAndSendEth( url, - { - // locks the chainId in case the dapp's chainId changes after signing request creation - chainId: site.ethChainId, - ...txRequest, - }, + txRequest, site.ethChainId.toString(), { address, @@ -621,7 +629,7 @@ export class EthTabsHandler extends TabsHandler { // url validation carried out inside stores.sites.getSiteFromUrl site = await this.stores.sites.getSiteFromUrl(url) } catch (error) { - //no-op + // no-op } return site?.ethPermissions @@ -637,7 +645,7 @@ export class EthTabsHandler extends TabsHandler { url: string, request: EthRequestArguments<"wallet_requestPermissions">, port: Port - ): Promise { + ): Promise> { if (request.params.length !== 1) throw new EthProviderRpcError( "This method expects an array with only 1 entry", @@ -689,19 +697,19 @@ export class EthTabsHandler extends TabsHandler { return this.getPermissions(url) } - private async ethRequest( + private async ethRequest( id: string, url: string, - request: EthRequestArguments, + request: EthRequestArgs, port: Port ): Promise { if ( ![ "eth_requestAccounts", "eth_accounts", - "eth_chainId", - "eth_blockNumber", - "net_version", + "eth_chainId", // TODO check if necessary ? + "eth_blockNumber", // TODO check if necessary ? + "net_version", // TODO check if necessary ? "wallet_switchEthereumChain", "wallet_addEthereumChain", "wallet_watchAsset", @@ -710,6 +718,7 @@ export class EthTabsHandler extends TabsHandler { ) await this.checkAccountAuthorised(url) + // TODO typecheck return types against rpc schema switch (request.method) { case "eth_requestAccounts": await this.requestPermissions( @@ -733,74 +742,42 @@ export class EthTabsHandler extends TabsHandler { case "eth_chainId": // public method, no need to auth (returns undefined if not authorized yet) - return ethers.utils.hexValue(await this.getChainId(url)) + return toHex(await this.getChainId(url)) case "net_version": // public method, no need to auth (returns undefined if not authorized yet) // legacy, but still used by etherscan prior calling eth_watchAsset return (await this.getChainId(url)).toString() - case "estimateGas": { - const { params } = request as EthRequestArguments<"estimateGas"> - if (params[1] && params[1] !== "latest") { - throw new EthProviderRpcError( - "estimateGas does not support blockTag", - ETH_ERROR_EIP1474_INVALID_PARAMS - ) - } - - await this.checkAccountAuthorised(url, params[0].from) - - const req = ethers.providers.JsonRpcProvider.hexlifyTransaction(params[0]) - const provider = await this.getProvider(url) - const result = await provider.estimateGas(req) - return result.toHexString() - } - case "personal_sign": case "eth_signTypedData": case "eth_signTypedData_v1": case "eth_signTypedData_v3": case "eth_signTypedData_v4": { - return this.signMessage(url, request as EthRequestSignArguments, port) + return this.signMessage(url, request, port) } case "personal_ecRecover": { const { - params: [data, signature], - } = request as EthRequestArguments<"personal_ecRecover"> - return recoverPersonalSignature({ data, signature }) + params: [message, signature], + } = request + return recoverMessageAddress({ message, signature }) } case "eth_sendTransaction": - return this.sendTransaction( - url, - request as EthRequestArguments<"eth_sendTransaction">, - port - ) + return this.sendTransaction(url, request, port) case "wallet_watchAsset": //auth-less test dapp : rsksmart.github.io/metamask-rsk-custom-network/ - return this.addWatchAssetRequest( - url, - request as EthRequestArguments<"wallet_watchAsset">, - port - ) + return this.addWatchAssetRequest(url, request, port) case "wallet_addEthereumChain": //auth-less test dapp : rsksmart.github.io/metamask-rsk-custom-network/ - return this.addEthereumChain( - url, - request as EthRequestArguments<"wallet_addEthereumChain">, - port - ) + return this.addEthereumChain(url, request, port) case "wallet_switchEthereumChain": //auth-less test dapp : rsksmart.github.io/metamask-rsk-custom-network/ - return this.switchEthereumChain( - url, - request as EthRequestArguments<"wallet_switchEthereumChain"> - ) + return this.switchEthereumChain(url, request) // https://docs.metamask.io/guide/rpc-api.html#wallet-getpermissions case "wallet_getPermissions": @@ -808,18 +785,14 @@ export class EthTabsHandler extends TabsHandler { // https://docs.metamask.io/guide/rpc-api.html#wallet-requestpermissions case "wallet_requestPermissions": - return this.requestPermissions( - url, - request as EthRequestArguments<"wallet_requestPermissions">, - port - ) + return this.requestPermissions(url, request, port) default: return this.getFallbackRequest(url, request) } } - handle( + async handle( id: string, type: TMessageType, request: RequestTypes[TMessageType], @@ -830,9 +803,26 @@ export class EthTabsHandler extends TabsHandler { case "pub(eth.subscribe)": return this.ethSubscribe(id, url, port) - case "pub(eth.request)": - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return this.ethRequest(id, url, request as AnyEthRequest, port) as any + case "pub(eth.request)": { + try { + return await this.ethRequest(id, url, request as EthRequestArgs, port) + } catch (err) { + // error may already be formatted by our handler + if (err instanceof EthProviderRpcError) throw err + + const { code, message, shortMessage, details } = err as RpcError + const cause = getEvmErrorCause(err as AnyEvmError) + + const myError = new EthProviderRpcError( + shortMessage ?? message ?? "Internal error", + code ?? ETH_ERROR_EIP1474_INTERNAL_ERROR, + // assume if data property is present, it's an EVM revert => dapp expects that underlying error object + cause.data ? cause : details + ) + + throw myError + } + } default: throw new Error(`Unable to handle message of type ${type}`) diff --git a/apps/extension/src/core/domains/ethereum/helpers.ts b/apps/extension/src/core/domains/ethereum/helpers.ts index 284eefbd08..c86cfcc19a 100644 --- a/apps/extension/src/core/domains/ethereum/helpers.ts +++ b/apps/extension/src/core/domains/ethereum/helpers.ts @@ -2,14 +2,25 @@ import { ChainId } from "@core/domains/chains/types" import { EthGasSettings, EthGasSettingsEip1559, + EvmAddress, EvmNetworkId, LedgerEthDerivationPathType, } from "@core/domains/ethereum/types" import { Token } from "@core/domains/tokens/types" import { assert } from "@polkadot/util" -import { isEthereumAddress } from "@polkadot/util-crypto" import { erc20Abi } from "@talismn/balances" -import { BigNumber, BigNumberish, ethers } from "ethers" +import { isBigInt, isEthereumAddress } from "@talismn/util" +import { + Hex, + TransactionRequest, + TransactionRequestBase, + encodeFunctionData, + getAddress, + hexToBigInt, + hexToNumber, + isAddress, + isHex, +} from "viem" import * as yup from "yup" const DERIVATION_PATHS_PATTERNS = { @@ -32,34 +43,36 @@ export const getEthLedgerDerivationPath = (type: LedgerEthDerivationPathType, in export const getEthTransferTransactionBase = async ( evmNetworkId: EvmNetworkId, - from: string, - to: string, + from: EvmAddress, + to: EvmAddress, token: Token, - planck: string -): Promise => { + planck: bigint +) => { assert(evmNetworkId, "evmNetworkId is required") assert(token, "token is required") - assert(planck, "planck is required") - assert(isEthereumAddress(from), "from address is required") - assert(isEthereumAddress(to), "to address is required") - - let tx: ethers.providers.TransactionRequest + assert(isBigInt(planck), "planck is required") + assert(isAddress(from), "from address is required") + assert(isAddress(to), "to address is required") if (token.type === "evm-native") { - tx = { - value: ethers.BigNumber.from(planck), - to: ethers.utils.getAddress(to), + return { + from, + value: planck, + to: getAddress(to), } } else if (token.type === "evm-erc20") { - const contract = new ethers.Contract(token.contractAddress, erc20Abi) - tx = await contract.populateTransaction["transfer"](to, ethers.BigNumber.from(planck)) - } else throw new Error(`Invalid token type ${token.type} - token ${token.id}`) + const data = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [to, planck], + }) - return { - chainId: parseInt(evmNetworkId, 10), - from, - ...tx, - } + return { + from, + to: getAddress(token.contractAddress), + data, + } + } else throw new Error(`Invalid token type ${token.type} - token ${token.id}`) } export const getErc20TokenId = ( @@ -67,82 +80,138 @@ export const getErc20TokenId = ( contractAddress: string ) => `${chainOrNetworkId}-evm-erc20-${contractAddress}`.toLowerCase() -const safeBigNumberish = (value?: BigNumberish) => - BigNumber.isBigNumber(value) ? value.toString() : value +export const serializeTransactionRequest = ( + tx: TransactionRequest +): TransactionRequest => { + const serialized: TransactionRequest = { from: tx.from } + + if (tx.to !== undefined) serialized.to = tx.to + if (tx.data !== undefined) serialized.data = tx.data + if (tx.accessList !== undefined) serialized.accessList = tx.accessList + if (tx.type !== undefined) serialized.type = tx.type + if (tx.nonce !== undefined) serialized.nonce = tx.nonce + + // bigint fields need to be serialized + if (tx.value !== undefined) serialized.value = tx.value.toString() + if (tx.gas !== undefined) serialized.gas = tx.gas.toString() + if (tx.gasPrice !== undefined) serialized.gasPrice = tx.gasPrice.toString() + if (tx.maxFeePerGas !== undefined) serialized.maxFeePerGas = tx.maxFeePerGas.toString() + if (tx.maxPriorityFeePerGas !== undefined) + serialized.maxPriorityFeePerGas = tx.maxPriorityFeePerGas.toString() + + return serialized +} -export const serializeTransactionRequestBigNumbers = ( - transaction: ethers.providers.TransactionRequest -) => { - const tx = { ...transaction } +export const serializeTransactionRequestBase = ( + txb: TransactionRequestBase +): TransactionRequestBase => { + const serialized: TransactionRequestBase = { + from: txb.from, + } - if (tx.gasLimit) tx.gasLimit = safeBigNumberish(tx.gasLimit) - if (tx.gasPrice) tx.gasPrice = safeBigNumberish(tx.gasPrice) - if (tx.maxFeePerGas) tx.maxFeePerGas = safeBigNumberish(tx.maxFeePerGas) - if (tx.maxPriorityFeePerGas) tx.maxPriorityFeePerGas = safeBigNumberish(tx.maxPriorityFeePerGas) - if (tx.value) tx.value = safeBigNumberish(tx.value) - if (tx.nonce) tx.nonce = safeBigNumberish(tx.nonce) + if (txb.data !== undefined) serialized.data = txb.data + if (txb.to !== undefined) serialized.to = txb.to + if (txb.value !== undefined) serialized.value = txb.value.toString() + if (txb.gas !== undefined) serialized.gas = txb.gas.toString() + if (txb.nonce !== undefined) serialized.nonce = txb.nonce - return tx + return serialized } -// BigNumbers need to be reconstructed if they are serialized then deserialized -export const rebuildTransactionRequestNumbers = ( - transaction: ethers.providers.TransactionRequest -) => { - const tx = structuredClone(transaction) - - if (tx.gasLimit) tx.gasLimit = BigNumber.from(tx.gasLimit) - if (tx.gasPrice) tx.gasPrice = BigNumber.from(tx.gasPrice) - if (tx.maxFeePerGas) tx.maxFeePerGas = BigNumber.from(tx.maxFeePerGas) - if (tx.maxPriorityFeePerGas) tx.maxPriorityFeePerGas = BigNumber.from(tx.maxPriorityFeePerGas) - if (tx.value) tx.value = BigNumber.from(tx.value) - if (tx.nonce) tx.nonce = BigNumber.from(tx.nonce) +export const parseGasSettings = (gasSettings: EthGasSettings): EthGasSettings => { + return gasSettings.type === "eip1559" + ? { + type: "eip1559", + gas: BigInt(gasSettings.gas), + maxFeePerGas: BigInt(gasSettings.maxFeePerGas), + maxPriorityFeePerGas: BigInt(gasSettings.maxPriorityFeePerGas), + } + : { + type: gasSettings.type, + gas: BigInt(gasSettings.gas), + gasPrice: BigInt(gasSettings.gasPrice), + } +} - return tx +export const serializeGasSettings = ( + gasSettings: EthGasSettings +): EthGasSettings => { + return gasSettings.type === "eip1559" + ? { + type: "eip1559", + gas: gasSettings.gas.toString(), + maxFeePerGas: gasSettings.maxFeePerGas.toString(), + maxPriorityFeePerGas: gasSettings.maxPriorityFeePerGas.toString(), + } + : { + type: gasSettings.type, + gas: gasSettings.gas.toString(), + gasPrice: gasSettings.gasPrice.toString(), + } } -export const rebuildGasSettings = (gasSettings: EthGasSettings) => { - const gs = structuredClone(gasSettings) +// BigNumbers need to be reconstructed if they are serialized then deserialized +export const parseTransactionRequest = ( + tx: TransactionRequest +): TransactionRequest => { + const parsed: TransactionRequest = { from: tx.from } + + if (tx.to !== undefined) parsed.to = tx.to + if (tx.data !== undefined) parsed.data = tx.data + if (tx.accessList !== undefined) parsed.accessList = tx.accessList + if (tx.type !== undefined) parsed.type = tx.type + if (tx.nonce !== undefined) parsed.nonce = tx.nonce + + // bigint fields need to be parsed + if (typeof tx.value === "string") parsed.value = BigInt(tx.value) + if (typeof tx.gas === "string") parsed.gas = BigInt(tx.gas) + if (typeof tx.gasPrice === "string") parsed.gasPrice = BigInt(tx.gasPrice) + if (typeof tx.maxFeePerGas === "string") parsed.maxFeePerGas = BigInt(tx.maxFeePerGas) + if (typeof tx.maxPriorityFeePerGas === "string") + parsed.maxPriorityFeePerGas = BigInt(tx.maxPriorityFeePerGas) + + return parsed +} - gs.gasLimit = BigNumber.from(gs.gasLimit) +export const parseRpcTransactionRequestBase = ( + rtx: TransactionRequestBase +): TransactionRequestBase => { + const txBase: TransactionRequestBase = { from: rtx.from } - if (gs.type === 2) { - gs.maxFeePerGas = BigNumber.from(gs.maxFeePerGas) - gs.maxPriorityFeePerGas = BigNumber.from(gs.maxPriorityFeePerGas) - } else if (gs.type === 0) { - gs.gasPrice = BigNumber.from(gs.gasPrice) - } else throw new Error("Unexpected gas settings type") + if (isHex(rtx.to)) txBase.to = rtx.to + if (isHex(rtx.data)) txBase.data = rtx.data + if (isHex(rtx.value)) txBase.value = hexToBigInt(rtx.value) + if (isHex(rtx.gas)) txBase.gas = hexToBigInt(rtx.gas) + if (isHex(rtx.nonce)) txBase.nonce = hexToNumber(rtx.nonce) - return gs + return txBase } -const TX_GAS_LIMIT_DEFAULT = BigNumber.from("250000") -const TX_GAS_LIMIT_MIN = BigNumber.from("21000") -const TX_GAS_LIMIT_SAFETY_RATIO = 2 +const TX_GAS_LIMIT_DEFAULT = 250000n +const TX_GAS_LIMIT_MIN = 21000n +const TX_GAS_LIMIT_SAFETY_RATIO = 2n export const getGasLimit = ( - blockGasLimit: BigNumberish, - estimatedGas: BigNumberish, - tx: ethers.providers.TransactionRequest | undefined, + blockGasLimit: bigint, + estimatedGas: bigint, + tx: TransactionRequestBase | undefined, isContractCall?: boolean ) => { - // some dapps use legacy gas field instead of gasLimit // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bnSuggestedGasLimit = BigNumber.from(tx?.gasLimit ?? (tx as any)?.gas ?? 0) - const bnEstimatedGas = BigNumber.from(estimatedGas) + const suggestedGasLimit = tx?.gas ?? 0n // for contract calls, gas cost can evolve overtime : add a safety margin - const bnSafeGasLimit = isContractCall - ? bnEstimatedGas.mul(100 + TX_GAS_LIMIT_SAFETY_RATIO).div(100) - : bnEstimatedGas + const safeGasLimit = isContractCall + ? (estimatedGas * (100n + TX_GAS_LIMIT_SAFETY_RATIO)) / 100n + : estimatedGas // RPC estimated gas may be too low (reliable ex: https://portal.zksync.io/bridge), // so if dapp suggests higher gas limit as the estimate, use that - const highestLimit = bnSafeGasLimit.gt(bnSuggestedGasLimit) ? bnSafeGasLimit : bnSuggestedGasLimit + const highestLimit = safeGasLimit > suggestedGasLimit ? safeGasLimit : suggestedGasLimit - let gasLimit = BigNumber.from(highestLimit) - if (gasLimit.gt(blockGasLimit)) { + let gasLimit = highestLimit + if (gasLimit > blockGasLimit) { // probably bad formatting or error from the dapp, fallback to default value gasLimit = TX_GAS_LIMIT_DEFAULT - } else if (gasLimit.lt(TX_GAS_LIMIT_MIN)) { + } else if (gasLimit < TX_GAS_LIMIT_MIN) { // invalid, all chains use 21000 as minimum, fallback to default value gasLimit = TX_GAS_LIMIT_DEFAULT } @@ -154,114 +223,94 @@ export const getGasLimit = ( const FEE_MAX_RAISE_RATIO_PER_BLOCK = 0.125 export const getMaxFeePerGas = ( - baseFeePerGas: BigNumberish, - maxPriorityFeePerGas: BigNumberish, + baseFeePerGas: bigint, + maxPriorityFeePerGas: bigint, maxBlocksWait = 8, increase = true ) => { - let base = BigNumber.from(baseFeePerGas) + let base = baseFeePerGas //baseFeePerGas can augment 12.5% per block for (let i = 0; i < maxBlocksWait; i++) - base = base.mul((1 + (increase ? 1 : -1) * FEE_MAX_RAISE_RATIO_PER_BLOCK) * 1000).div(1000) + base = (base * BigInt((1 + (increase ? 1 : -1) * FEE_MAX_RAISE_RATIO_PER_BLOCK) * 1000)) / 1000n - return base.add(maxPriorityFeePerGas) + return base + maxPriorityFeePerGas } export const getGasSettingsEip1559 = ( - baseFee: BigNumber, - maxPriorityFeePerGas: BigNumber, - gasLimit: BigNumber, + baseFee: bigint, + maxPriorityFeePerGas: bigint, + gas: bigint, maxBlocksWait?: number ): EthGasSettingsEip1559 => ({ - type: 2, + type: "eip1559", maxPriorityFeePerGas, maxFeePerGas: getMaxFeePerGas(baseFee, maxPriorityFeePerGas, maxBlocksWait), - gasLimit, + gas, }) export const getTotalFeesFromGasSettings = ( gasSettings: EthGasSettings, - estimatedGas: BigNumberish, - baseFeePerGas?: BigNumberish | null + estimatedGas: bigint, + baseFeePerGas: bigint | null | undefined, + l1Fee: bigint ) => { - if (gasSettings.type === 2) { - if (baseFeePerGas === undefined) + // L1 fee needs to be included in estimatedFee and maxFee to keep the same UX behavior whether or not the chain is a L2 + const estimatedL1DataFee = l1Fee > 0n ? l1Fee : null + + // OP Stack docs : Spikes in Ethereum gas prices may result in users paying a higher or lower than estimated L1 data fee, by up to 25% + // https://community.optimism.io/docs/developers/build/transaction-fees/#the-l1-data-fee + const maxL1Fee = (l1Fee * 125n) / 100n + + if (gasSettings.type === "eip1559") { + if (!isBigInt(baseFeePerGas)) throw new Error("baseFeePerGas argument is required for type 2 fee computation") return { - estimatedFee: BigNumber.from( - BigNumber.from(baseFeePerGas).lt(gasSettings.maxFeePerGas) - ? baseFeePerGas - : gasSettings.maxFeePerGas - ) - .add(gasSettings.maxPriorityFeePerGas) - .mul( - BigNumber.from(estimatedGas).lt(gasSettings.gasLimit) - ? estimatedGas - : gasSettings.gasLimit - ), - maxFee: BigNumber.from(gasSettings.maxFeePerGas) - .add(gasSettings.maxPriorityFeePerGas) - .mul(gasSettings.gasLimit), + estimatedL1DataFee, + estimatedFee: + (gasSettings.maxPriorityFeePerGas + + (baseFeePerGas < gasSettings.maxFeePerGas ? baseFeePerGas : gasSettings.maxFeePerGas)) * + (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas) + + l1Fee, + maxFee: + (gasSettings.maxFeePerGas + gasSettings.maxPriorityFeePerGas) * gasSettings.gas + maxL1Fee, } } else { return { - estimatedFee: BigNumber.from(gasSettings.gasPrice).mul( - BigNumber.from(estimatedGas).lt(gasSettings.gasLimit) ? estimatedGas : gasSettings.gasLimit - ), - maxFee: BigNumber.from(gasSettings.gasPrice).mul(gasSettings.gasLimit), + estimatedL1DataFee, + estimatedFee: + gasSettings.gasPrice * (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas) + + l1Fee, + maxFee: gasSettings.gasPrice * gasSettings.gas + maxL1Fee, } } } -export const getMaxTransactionCost = (transaction: ethers.providers.TransactionRequest) => { - if (transaction.gasLimit === undefined) - throw new Error("gasLimit is required for fee computation") +export const getMaxTransactionCost = (transaction: TransactionRequest) => { + if (transaction.gas === undefined) throw new Error("gasLimit is required for fee computation") - const value = BigNumber.from(transaction.value ?? 0) + const value = transaction.value ?? 0n - if (transaction.type === 2) { + if (transaction.type === "eip1559") { if (transaction.maxFeePerGas === undefined) throw new Error("maxFeePerGas is required for type 2 fee computation") - return BigNumber.from(transaction.maxFeePerGas).mul(transaction.gasLimit).add(value) + return transaction.maxFeePerGas * transaction.gas + value } else { if (transaction.gasPrice === undefined) throw new Error("gasPrice is required for legacy fee computation") - return BigNumber.from(transaction.gasPrice).mul(transaction.gasLimit).add(value) + return transaction.gasPrice * transaction.gas + value } } export const prepareTransaction = ( - tx: ethers.providers.TransactionRequest, + txBase: TransactionRequestBase, gasSettings: EthGasSettings, nonce: number -) => { - const { - chainId, - data, - from, - to, - value = BigNumber.from(0), - accessList, - ccipReadEnabled, - customData, - } = tx - - const transaction: ethers.providers.TransactionRequest = { - chainId, - from, - to, - value, - nonce: BigNumber.from(nonce), - data, - ...gasSettings, - } - if (accessList) transaction.accessList = accessList - if (customData) transaction.customData = customData - if (ccipReadEnabled !== undefined) transaction.ccipReadEnabled = ccipReadEnabled - - return transaction -} +): TransactionRequest => ({ + ...txBase, + ...gasSettings, + nonce, +}) const testNoScriptTag = (text?: string) => !text?.toLowerCase().includes(" => { - return chainConnectorEvm.getProviderForEvmNetwork(ethereumNetwork, { batch }) -} - -// TODO: Refactor any code which uses this function to directly -// call methods on `chainConnectorEvm` instead! -export const getProviderForEvmNetworkId = async ( - ethereumNetworkId: EvmNetworkId, - { batch }: GetProviderOptions = {} -): Promise => { - return await chainConnectorEvm.getProviderForEvmNetworkId(ethereumNetworkId, { batch }) -} diff --git a/apps/extension/src/core/domains/ethereum/transactionCountManager.ts b/apps/extension/src/core/domains/ethereum/transactionCountManager.ts index e3c77de052..914f53a769 100644 --- a/apps/extension/src/core/domains/ethereum/transactionCountManager.ts +++ b/apps/extension/src/core/domains/ethereum/transactionCountManager.ts @@ -1,4 +1,5 @@ -import { getProviderForEvmNetworkId } from "./rpcProviders" +import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" + import { EvmNetworkId } from "./types" const dicTransactionCount = new Map() @@ -9,13 +10,13 @@ const getKey = (address: string, evmNetworkId: EvmNetworkId) => /* To be called to set a valid nonce for a transaction */ -export const getTransactionCount = async (address: string, evmNetworkId: EvmNetworkId) => { +export const getTransactionCount = async (address: `0x${string}`, evmNetworkId: EvmNetworkId) => { const key = getKey(address, evmNetworkId) - const provider = await getProviderForEvmNetworkId(evmNetworkId) + const provider = await chainConnectorEvm.getPublicClientForEvmNetwork(evmNetworkId) if (!provider) throw new Error(`Could not find provider for EVM chain ${evmNetworkId}`) - const transactionCount = await provider.getTransactionCount(address) + const transactionCount = await provider.getTransactionCount({ address }) if (!dicTransactionCount.has(key)) { // initial value @@ -41,3 +42,8 @@ export const incrementTransactionCount = (address: string, evmNetworkId: EvmNetw dicTransactionCount.set(key, count + 1) } + +export const resetTransactionCount = (address: string, evmNetworkId: EvmNetworkId) => { + const key = getKey(address, evmNetworkId) + dicTransactionCount.delete(key) +} diff --git a/apps/extension/src/core/domains/ethereum/types.ts b/apps/extension/src/core/domains/ethereum/types.ts index eff4f2a69c..c1ea2e1a9a 100644 --- a/apps/extension/src/core/domains/ethereum/types.ts +++ b/apps/extension/src/core/domains/ethereum/types.ts @@ -1,13 +1,31 @@ import type { ETH_SEND, ETH_SIGN, KnownSigningRequestIdOnly } from "@core/domains/signing/types" import type { CustomErc20Token } from "@core/domains/tokens/types" -import { AnyEthRequest, EthProviderMessage, EthResponseTypes } from "@core/injectEth/types" +import { AnyEthRequest } from "@core/injectEth/types" import { BaseRequest, BaseRequestId, RequestIdOnly } from "@core/types/base" import { HexString } from "@polkadot/util/types" import { EvmNetworkId } from "@talismn/chaindata-provider" -import { BigNumberish, ethers } from "ethers" +import type { + AddEthereumChainParameter, + EIP1193Parameters, + Address as EvmAddress, + Chain as EvmChain, + TransactionRequest, +} from "viem" +import { PublicRpcSchema, RpcSchema, WalletRpcSchema } from "viem" import { WalletTransactionTransferInfo } from "../transactions" +export type { EvmAddress, EvmChain } + +export type AnyEvmError = { + message?: string + shortMessage?: string + details?: string + code?: number + cause?: AnyEvmError + data?: unknown +} + export type { EvmNetwork, CustomEvmNetwork, @@ -16,34 +34,86 @@ export type { EthereumRpc, } from "@talismn/chaindata-provider" -export type AddEthereumChainParameter = { - /** A 0x-prefixed hexadecimal string */ - chainId: string - chainName: string - nativeCurrency: { - name: string - /** 2-6 characters long */ - symbol: string - decimals: 18 +// define here the rpc methods that do not exist in viem or that need to be overriden +type TalismanRpcSchema = [ + { + Method: "personal_ecRecover" + Parameters: [signedData: `0x${string}`, signature: `0x${string}`] + ReturnType: EvmAddress + }, + { + Method: "eth_signTypedData" + Parameters: [message: unknown[], from: EvmAddress] + ReturnType: `0x${string}` + }, + { + Method: "eth_signTypedData_v1" + Parameters: [message: unknown[], from: EvmAddress] + ReturnType: `0x${string}` + }, + { + Method: "eth_signTypedData_v3" + Parameters: [from: EvmAddress, message: string] + ReturnType: `0x${string}` + }, + { + // TODO see if we can remove this one + // override for now because of result type mismatch + Method: "wallet_requestPermissions" + Parameters: [permissions: { eth_accounts: Record }] + ReturnType: Web3WalletPermission[] } - rpcUrls: string[] - blockExplorerUrls?: string[] - /** Currently ignored by metamask */ - iconUrls?: string[] +] + +export type FullRpcSchema = [...PublicRpcSchema, ...WalletRpcSchema, ...TalismanRpcSchema] + +type EthRequestSignaturesMap = { + [K in TRpcSchema[number]["Method"]]: [ + Extract["Parameters"], + Extract["ReturnType"] + ] +} + +export type EthRequestSignatures = EthRequestSignaturesMap + +export type EthRequestMethod = keyof EthRequestSignatures +export type EthRequestParams = EthRequestSignatures[T][0] +export type EthRequestResult = EthRequestSignatures[T][1] + +export type EthRequestArguments = { + readonly method: T + readonly params: EthRequestParams +} + +// TODO yeet ? +export type EthRequestArgs = EIP1193Parameters +export type EthRequestSignArguments = EthRequestArguments< + | "personal_sign" + | "eth_signTypedData" + | "eth_signTypedData_v1" + | "eth_signTypedData_v3" + | "eth_signTypedData_v4" +> + +export interface EthProviderMessage { + readonly type: string + readonly data: unknown } export type EthTxSignAndSend = { - unsigned: ethers.providers.TransactionRequest + evmNetworkId: EvmNetworkId + unsigned: TransactionRequest transferInfo?: WalletTransactionTransferInfo } export type EthTxSendSigned = { - unsigned: ethers.providers.TransactionRequest + evmNetworkId: EvmNetworkId + unsigned: TransactionRequest signed: `0x${string}` transferInfo?: WalletTransactionTransferInfo } export declare type EthApproveSignAndSend = KnownSigningRequestIdOnly & { - transaction: ethers.providers.TransactionRequest + transaction: TransactionRequest } export type EthRequestSigningApproveSignature = KnownSigningRequestIdOnly & { @@ -51,7 +121,7 @@ export type EthRequestSigningApproveSignature = KnownSigningRequestIdOnly & { - unsigned: ethers.providers.TransactionRequest + unsigned: TransactionRequest signedPayload: `0x${string}` } @@ -60,7 +130,7 @@ export interface AnyEthRequestChainId extends AnyEthRequest { } export type EthNonceRequest = { - address: string + address: `0x${string}` evmNetworkId: EvmNetworkId } @@ -97,11 +167,11 @@ export type RequestUpsertCustomEvmNetwork = { export interface EthMessages { // all ethereum calls - "pub(eth.request)": [AnyEthRequest, EthResponseTypes] + "pub(eth.request)": [AnyEthRequest, unknown] "pub(eth.subscribe)": [null, boolean, EthProviderMessage] "pub(eth.mimicMetaMask)": [null, boolean] // eth signing message signatures - "pri(eth.request)": [AnyEthRequestChainId, EthResponseTypes] + "pri(eth.request)": [AnyEthRequestChainId, unknown] "pri(eth.transactions.count)": [EthNonceRequest, number] "pri(eth.signing.signAndSend)": [EthTxSignAndSend, HexString] "pri(eth.signing.sendSigned)": [EthTxSendSigned, HexString] @@ -126,18 +196,20 @@ export interface EthMessages { "pri(eth.networks.upsert)": [RequestUpsertCustomEvmNetwork, boolean] } -export type EthGasSettingsLegacy = { - type: 0 - gasLimit: BigNumberish - gasPrice: BigNumberish +export type EthGasSettingsLegacy = { + type: "legacy" | "eip2930" + gas: TQuantity + gasPrice: TQuantity } -export type EthGasSettingsEip1559 = { - type: 2 - gasLimit: BigNumberish - maxFeePerGas: BigNumberish - maxPriorityFeePerGas: BigNumberish +export type EthGasSettingsEip1559 = { + type: "eip1559" + gas: TQuantity + maxFeePerGas: TQuantity + maxPriorityFeePerGas: TQuantity } -export type EthGasSettings = EthGasSettingsLegacy | EthGasSettingsEip1559 +export type EthGasSettings = + | EthGasSettingsLegacy + | EthGasSettingsEip1559 export type LedgerEthDerivationPathType = "LedgerLive" | "Legacy" | "BIP44" diff --git a/apps/extension/src/core/domains/signing/requests.ts b/apps/extension/src/core/domains/signing/requests.ts index 2a909bf6b3..73f7369a95 100644 --- a/apps/extension/src/core/domains/signing/requests.ts +++ b/apps/extension/src/core/domains/signing/requests.ts @@ -3,11 +3,11 @@ import { EvmNetworkId } from "@core/domains/ethereum/types" import type { EthSignRequest, SubstrateSigningRequest } from "@core/domains/signing/types" import { requestStore } from "@core/libs/requests/store" import type { Port } from "@core/types/base" -import type { TransactionRequest } from "@ethersproject/providers" +import { RpcTransactionRequest } from "viem" export const signAndSendEth = ( url: string, - request: TransactionRequest, + request: RpcTransactionRequest, ethChainId: EvmNetworkId, account: AccountJson, port: Port diff --git a/apps/extension/src/core/domains/signing/types.ts b/apps/extension/src/core/domains/signing/types.ts index ecd6dd8387..ed0a910f3c 100644 --- a/apps/extension/src/core/domains/signing/types.ts +++ b/apps/extension/src/core/domains/signing/types.ts @@ -5,14 +5,13 @@ import { EvmNetworkId, } from "@core/domains/ethereum/types" import { BaseRequest, BaseRequestId } from "@core/types/base" -import type { TransactionRequest as EthTransactionRequest } from "@ethersproject/abstract-provider" import { RequestSigningApproveSignature as PolkadotRequestSigningApproveSignature, RequestSign, ResponseSigning, } from "@polkadot/extension-base/background/types" import type { SignerPayloadJSON, SignerPayloadRaw } from "@polkadot/types/types" -import { BigNumberish } from "ethers" +import { RpcTransactionRequest } from "viem" export type { ResponseSigning, SignerPayloadJSON, SignerPayloadRaw } // Make this available elsewhere also @@ -55,7 +54,7 @@ export interface SubstrateSigningRequest extends BaseSigningRequest extends BaseSigningRequest { ethChainId: EvmNetworkId account: AccountJsonAny - request: string | EthTransactionRequest + request: string | RpcTransactionRequest } export type ETH_SIGN = "eth-sign" @@ -84,7 +83,7 @@ export interface EthSignRequest extends EthBaseSignRequest { } export interface EthSignAndSendRequest extends EthBaseSignRequest { - request: EthTransactionRequest + request: RpcTransactionRequest ethChainId: EvmNetworkId method: "eth_sendTransaction" } @@ -152,11 +151,13 @@ export type GasSettingsByPriority = GasSettingsByPriorityEip1559 | GasSettingsBy export type EthBaseFeeTrend = "idle" | "decreasing" | "increasing" | "toTheMoon" export type EthTransactionDetails = { - estimatedGas: BigNumberish - gasPrice: BigNumberish - estimatedFee: BigNumberish - maxFee: BigNumberish // TODO yeet ! - baseFeePerGas?: BigNumberish | null + evmNetworkId: EvmNetworkId + estimatedGas: bigint + gasPrice: bigint + estimatedFee: bigint + estimatedL1DataFee: bigint | null + maxFee: bigint + baseFeePerGas?: bigint | null baseFeeTrend?: EthBaseFeeTrend } diff --git a/apps/extension/src/core/domains/transactions/helpers.ts b/apps/extension/src/core/domains/transactions/helpers.ts index 7a716a3014..d6280c9285 100644 --- a/apps/extension/src/core/domains/transactions/helpers.ts +++ b/apps/extension/src/core/domains/transactions/helpers.ts @@ -4,10 +4,10 @@ import { TypeRegistry } from "@polkadot/types" import { HexString } from "@polkadot/util/types" import { SignerPayloadJSON } from "@substrate/txwrapper-core" import { Address } from "@talismn/balances" -import { ethers } from "ethers" +import { EvmNetworkId } from "@talismn/chaindata-provider" import merge from "lodash/merge" +import { Hex, TransactionRequest } from "viem" -import { serializeTransactionRequestBigNumbers } from "../ethereum/helpers" import { TransactionStatus } from "./types" type AddTransactionOptions = { @@ -23,23 +23,24 @@ const DEFAULT_OPTIONS: AddTransactionOptions = { } export const addEvmTransaction = async ( - hash: string, - unsigned: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + hash: Hex, + unsigned: TransactionRequest, options: AddTransactionOptions = {} ) => { const { siteUrl, label, tokenId, value, to } = merge(structuredClone(DEFAULT_OPTIONS), options) try { - if (!unsigned.chainId || !unsigned.from || unsigned.nonce === undefined) + if (!evmNetworkId || !unsigned.from || unsigned.nonce === undefined) throw new Error("Invalid transaction") - const evmNetworkId = String(unsigned.chainId) - const nonce = ethers.BigNumber.from(unsigned.nonce).toNumber() const isReplacement = (await db.transactions .filter( (row) => - row.networkType === "evm" && row.evmNetworkId === evmNetworkId && row.nonce === nonce + row.networkType === "evm" && + row.evmNetworkId === evmNetworkId && + row.nonce === unsigned.nonce ) .count()) > 0 @@ -48,9 +49,9 @@ export const addEvmTransaction = async ( networkType: "evm", evmNetworkId, account: unsigned.from, - nonce, + nonce: unsigned.nonce, isReplacement, - unsigned: serializeTransactionRequestBigNumbers(unsigned), + unsigned, status: "pending", siteUrl, label, @@ -103,7 +104,7 @@ export const addSubstrateTransaction = async ( export const updateTransactionStatus = async ( hash: string, status: TransactionStatus, - blockNumber?: number + blockNumber?: bigint | number ) => { try { await db.transactions.update(hash, { status, blockNumber: blockNumber?.toString() }) diff --git a/apps/extension/src/core/domains/transactions/types.ts b/apps/extension/src/core/domains/transactions/types.ts index f30ac611a9..4d3f660896 100644 --- a/apps/extension/src/core/domains/transactions/types.ts +++ b/apps/extension/src/core/domains/transactions/types.ts @@ -1,7 +1,7 @@ import { SignerPayloadJSON } from "@core/domains/signing/types" import { Address } from "@talismn/balances" import { EvmNetworkId, TokenId } from "@talismn/chaindata-provider" -import { ethers } from "ethers" +import { TransactionRequest } from "viem" // unknown for substrate txs from dapps export type TransactionStatus = "unknown" | "pending" | "success" | "error" | "replaced" @@ -33,7 +33,7 @@ export type WalletTransactionBase = WalletTransactionTransferInfo & { export type EvmWalletTransaction = WalletTransactionBase & { networkType: "evm" evmNetworkId: EvmNetworkId - unsigned: ethers.providers.TransactionRequest + unsigned: TransactionRequest } export type SubWalletTransaction = WalletTransactionBase & { diff --git a/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts b/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts index 4e2c4de16f..8454a2bba4 100644 --- a/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts +++ b/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts @@ -2,53 +2,72 @@ import { settingsStore } from "@core/domains/app" import { addEvmTransaction, updateTransactionStatus } from "@core/domains/transactions/helpers" import { log } from "@core/log" import { createNotification } from "@core/notifications" +import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" import { chaindataProvider } from "@core/rpcs/chaindata" +import { assert } from "@polkadot/util" import * as Sentry from "@sentry/browser" import { EvmNetworkId } from "@talismn/chaindata-provider" -import { ethers } from "ethers" +import { sleep, throwAfter } from "@talismn/util" import { nanoid } from "nanoid" import urlJoin from "url-join" +import { Hex, TransactionReceipt, TransactionRequest } from "viem" -import { getProviderForEthereumNetwork } from "../ethereum/rpcProviders" +import { resetTransactionCount } from "../ethereum/transactionCountManager" import { WatchTransactionOptions } from "./types" export const watchEthereumTransaction = async ( - ethChainId: EvmNetworkId, - txHash: string, - unsigned: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + hash: `0x${string}`, + unsigned: TransactionRequest, options: WatchTransactionOptions = {} ) => { try { const { siteUrl, notifications, transferInfo = {} } = options const withNotifications = !!(notifications && (await settingsStore.get("allowNotifications"))) - const ethereumNetwork = await chaindataProvider.getEvmNetwork(ethChainId) - if (!ethereumNetwork) throw new Error(`Could not find ethereum network ${ethChainId}`) + const ethereumNetwork = await chaindataProvider.getEvmNetwork(evmNetworkId) + if (!ethereumNetwork) throw new Error(`Could not find ethereum network ${evmNetworkId}`) - const provider = await getProviderForEthereumNetwork(ethereumNetwork, { batch: true }) - if (!provider) - throw new Error(`No provider for network ${ethChainId} (${ethereumNetwork.name})`) + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(evmNetworkId) + if (!client) throw new Error(`No client for network ${evmNetworkId} (${ethereumNetwork.name})`) const networkName = ethereumNetwork.name ?? "unknown network" const txUrl = ethereumNetwork.explorerUrl - ? urlJoin(ethereumNetwork.explorerUrl, "tx", txHash) + ? urlJoin(ethereumNetwork.explorerUrl, "tx", hash) : nanoid() // PENDING if (withNotifications) await createNotification("submitted", networkName, txUrl) try { - await addEvmTransaction(txHash, unsigned, { siteUrl, ...transferInfo }) + await addEvmTransaction(evmNetworkId, hash, unsigned, { siteUrl, ...transferInfo }) - const receipt = await provider.waitForTransaction(txHash) + // Observed on polygon network (tried multiple rpcs) that waitForTransactionReceipt throws TransactionNotFoundError & BlockNotFoundError randomly + // so we retry as long as we don't get a receipt, with a timeout on our side + const getTransactionReceipt = async (hash: Hex): Promise => { + try { + return await client.waitForTransactionReceipt({ hash }) + } catch (err) { + await sleep(4000) + return getTransactionReceipt(hash) + } + } - // to test failing transactions, swap on busy AMM pools with a 0.05% slippage limit - // status 0 = error - // status 1 = ok - // TODO are there other statuses ? - if (receipt.status === undefined || ![0, 1].includes(receipt.status)) - log.warn("Unknown evm tx status", receipt) - updateTransactionStatus(txHash, receipt.status ? "success" : "error", receipt.blockNumber) + const receipt = await Promise.race([ + getTransactionReceipt(hash), + throwAfter(5 * 60_000, "Transaction not found"), + ]) + + assert(receipt, "Transaction to watch not found") + // check hash which may be incorrect for cancelled tx, in which case receipt includes the replacement tx hash + if (receipt.transactionHash === hash) { + // to test failing transactions, swap on busy AMM pools with a 0.05% slippage limit + updateTransactionStatus( + hash, + receipt.status === "success" ? "success" : "error", + receipt.blockNumber + ) + } // success if associated to a block number if (withNotifications) @@ -58,13 +77,17 @@ export const watchEthereumTransaction = async ( txUrl ) } catch (err) { - updateTransactionStatus(txHash, "unknown") + log.error("watchEthereumTransaction error: ", { err }) + updateTransactionStatus(hash, "error") + + // observed on polygon, some submitted transactions are not found, in which case we must reset the nonce counter to avoid being stuck + resetTransactionCount(unsigned.from, evmNetworkId) if (withNotifications) await createNotification("error", networkName, txUrl, err as Error) // eslint-disable-next-line no-console else console.error("Failed to watch transaction", { err }) } } catch (err) { - Sentry.captureException(err, { tags: { ethChainId } }) + Sentry.captureException(err, { tags: { ethChainId: evmNetworkId } }) } } diff --git a/apps/extension/src/core/domains/transfers/handler.ts b/apps/extension/src/core/domains/transfers/handler.ts index d8d67f8f7b..04ffc6be0a 100644 --- a/apps/extension/src/core/domains/transfers/handler.ts +++ b/apps/extension/src/core/domains/transfers/handler.ts @@ -1,6 +1,8 @@ -import { DEBUG } from "@core/constants" -import { getEthTransferTransactionBase, rebuildGasSettings } from "@core/domains/ethereum/helpers" -import { getProviderForEvmNetworkId } from "@core/domains/ethereum/rpcProviders" +import { + getEthTransferTransactionBase, + parseGasSettings, + prepareTransaction, +} from "@core/domains/ethereum/helpers" import { getTransactionCount, incrementTransactionCount, @@ -17,18 +19,18 @@ import { import { getPairForAddressSafely, getPairFromAddress } from "@core/handlers/helpers" import { ExtensionHandler } from "@core/libs/Handler" import { log } from "@core/log" +import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" import { chaindataProvider } from "@core/rpcs/chaindata" import type { RequestSignatures, RequestTypes, ResponseType } from "@core/types" import { Port } from "@core/types/base" import { getPrivateKey } from "@core/util/getPrivateKey" import { validateHexString } from "@core/util/validateHexString" -import { TransactionRequest } from "@ethersproject/abstract-provider" import { assert } from "@polkadot/util" -import { HexString } from "@polkadot/util/types" import * as Sentry from "@sentry/browser" -import { planckToTokens } from "@talismn/util" -import { Wallet, ethers } from "ethers" +import { isEthereumAddress, planckToTokens } from "@talismn/util" +import { privateKeyToAccount } from "viem/accounts" +import { serializeTransactionRequest } from "../ethereum/helpers" import { transferAnalytics } from "./helpers" export default class AssetTransferHandler extends ExtensionHandler { @@ -151,14 +153,20 @@ export default class AssetTransferHandler extends ExtensionHandler { signedTransaction, }: RequestAssetTransferEthHardware): Promise { try { - const provider = await getProviderForEvmNetworkId(evmNetworkId) - if (!provider) throw new Error(`Could not find provider for network ${evmNetworkId}`) + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(evmNetworkId) + if (!client) throw new Error(`Could not find provider for network ${evmNetworkId}`) const token = await chaindataProvider.getToken(tokenId) if (!token) throw new Error(`Invalid tokenId ${tokenId}`) - const { from, to, hash } = await provider.sendTransaction(signedTransaction) - if (!to) throw new Error("Unable to transfer - no recipient address given") + const { from, to } = unsigned + if (!from) throw new Error("Unable to transfer - no from address specified") + if (!to) throw new Error("Unable to transfer - no recipient address specified") + + const hash = await client?.sendRawTransaction({ + serializedTransaction: signedTransaction, + }) + if (!hash) throw new Error("Failed to submit - no hash returned") watchEthereumTransaction(evmNetworkId, hash, unsigned, { transferInfo: { tokenId: token.id, value: amount, to }, @@ -174,11 +182,10 @@ export default class AssetTransferHandler extends ExtensionHandler { incrementTransactionCount(from, evmNetworkId) - return { hash } as { hash: HexString } + return { hash } } catch (err) { const error = err as Error & { reason?: string; error?: Error } - // eslint-disable-next-line no-console - DEBUG && console.error(error.message, { err }) + log.error(error.message, { err }) Sentry.captureException(err, { extra: { tokenId, evmNetworkId } }) throw new Error(error?.error?.message ?? error.reason ?? "Failed to send transaction") } @@ -195,39 +202,47 @@ export default class AssetTransferHandler extends ExtensionHandler { const token = await chaindataProvider.getToken(tokenId) if (!token) throw new Error(`Invalid tokenId ${tokenId}`) - const provider = await getProviderForEvmNetworkId(evmNetworkId) - if (!provider) throw new Error(`Could not find provider for network ${evmNetworkId}`) + assert(isEthereumAddress(fromAddress), "Invalid from address") + assert(isEthereumAddress(toAddress), "Invalid to address") const transfer = await getEthTransferTransactionBase( evmNetworkId, - ethers.utils.getAddress(fromAddress), - ethers.utils.getAddress(toAddress), + fromAddress, + toAddress, token, - amount + BigInt(amount ?? 0) ) - const transaction: TransactionRequest = { - nonce: await getTransactionCount(fromAddress, evmNetworkId), - ...rebuildGasSettings(gasSettings), - ...transfer, - } + const parsedGasSettings = parseGasSettings(gasSettings) + const nonce = await getTransactionCount(fromAddress, evmNetworkId) + + const transaction = prepareTransaction(transfer, parsedGasSettings, nonce) + const unsigned = serializeTransactionRequest(transaction) const result = await getPairForAddressSafely(fromAddress, async (pair) => { + const client = await chainConnectorEvm.getWalletClientForEvmNetwork(evmNetworkId) + assert(client, "Missing client for chain " + evmNetworkId) + const password = this.stores.password.getPassword() assert(password, "Unauthorised") - const privateKey = getPrivateKey(pair, password) - const wallet = new Wallet(privateKey, provider) + const privateKey = getPrivateKey(pair, password, "hex") + const account = privateKeyToAccount(privateKey) - const { hash } = await wallet.sendTransaction(transaction) + const hash = await client.sendTransaction({ + chain: client.chain, + account, + ...transaction, + }) incrementTransactionCount(fromAddress, evmNetworkId) - return { hash } as { hash: HexString } + return { hash } }) if (result.ok) { - watchEthereumTransaction(evmNetworkId, result.val.hash, transaction, { + // TODO test this + watchEthereumTransaction(evmNetworkId, result.val.hash, unsigned, { transferInfo: { tokenId: token.id, value: amount, to: toAddress }, }) diff --git a/apps/extension/src/core/domains/transfers/types.ts b/apps/extension/src/core/domains/transfers/types.ts index 4821181152..d90a800a1f 100644 --- a/apps/extension/src/core/domains/transfers/types.ts +++ b/apps/extension/src/core/domains/transfers/types.ts @@ -3,7 +3,7 @@ import { SignerPayloadJSON } from "@core/domains/signing/types" import { TokenId } from "@core/domains/tokens/types" import { HexString } from "@polkadot/util/types" import { Address } from "@talismn/balances" -import { ethers } from "ethers" +import { TransactionRequest } from "viem" import { EthGasSettings, EvmNetworkId } from "../ethereum/types" import { WalletTransactionTransferInfo } from "../transactions" @@ -25,15 +25,15 @@ export interface RequestAssetTransferEth { fromAddress: string toAddress: string amount: string - gasSettings: EthGasSettings + gasSettings: EthGasSettings } export interface RequestAssetTransferEthHardware { evmNetworkId: EvmNetworkId tokenId: TokenId amount: string to: Address - unsigned: ethers.providers.TransactionRequest - signedTransaction: string + unsigned: TransactionRequest + signedTransaction: `0x${string}` } export interface RequestAssetTransferApproveSign { diff --git a/apps/extension/src/core/handlers/index.ts b/apps/extension/src/core/handlers/index.ts index 40299a235b..e456f5ac36 100644 --- a/apps/extension/src/core/handlers/index.ts +++ b/apps/extension/src/core/handlers/index.ts @@ -1,4 +1,5 @@ import { PORT_EXTENSION } from "@core/constants" +import { cleanupEvmErrorMessage, getEvmErrorCause } from "@core/domains/ethereum/errors" import { AnyEthRequest } from "@core/injectEth/types" import { log } from "@core/log" import { assert } from "@polkadot/util" @@ -104,15 +105,19 @@ const talismanHandler = ( // only send message back to port if it's still connected, unfortunately this check is not reliable in all browsers if (port) { try { - if (["pub(eth.request)", "pri(eth.request)"].includes(message)) + if (["pub(eth.request)", "pri(eth.request)"].includes(message)) { + const evmError = getEvmErrorCause(error) port.postMessage({ id, - error: error.message, + error: cleanupEvmErrorMessage( + (message === "pri(eth.request)" && evmError.details) || + (evmError.shortMessage ?? evmError.message ?? "Unknown error") + ), code: error.code, - data: error.data, + rpcData: evmError.data, // don't use "data" as property name or viem will interpret it differently isEthProviderRpcError: true, }) - else port.postMessage({ id, error: error.message }) + } else port.postMessage({ id, error: error.message }) } catch (caughtError) { /** * no-op diff --git a/apps/extension/src/core/inject/injectSubstrate.ts b/apps/extension/src/core/inject/injectSubstrate.ts index 98f45a09f4..aaf82b2f6a 100644 --- a/apps/extension/src/core/inject/injectSubstrate.ts +++ b/apps/extension/src/core/inject/injectSubstrate.ts @@ -1,8 +1,8 @@ import { log } from "@core/log" import type { ResponseType, SendRequest } from "@core/types" import type { ProviderInterfaceCallback } from "@polkadot/rpc-provider/types" -import { HexString } from "@polkadot/util/types" -import { CustomChain, CustomEvmNetwork, Token } from "@talismn/chaindata-provider" +import type { HexString } from "@polkadot/util/types" +import type { CustomChain, CustomEvmNetwork, Token } from "@talismn/chaindata-provider" type TalismanWindow = typeof globalThis & { talismanSub?: ReturnType & ReturnType diff --git a/apps/extension/src/core/injectEth/EthProviderRpcError.ts b/apps/extension/src/core/injectEth/EthProviderRpcError.ts index 00456e2b7d..1e61607838 100644 --- a/apps/extension/src/core/injectEth/EthProviderRpcError.ts +++ b/apps/extension/src/core/injectEth/EthProviderRpcError.ts @@ -46,3 +46,23 @@ export class EthProviderRpcError extends Error { Object.setPrototypeOf(this, EthProviderRpcError.prototype) } } + +/** + * Wrapped error so viem doesn't see the "data" property + */ +export class WrappedEthProviderRpcError extends Error { + code: number + message: string + rpcData?: unknown //hex encoded error or underlying error object + + constructor(message: string, code: number, rpcData?: unknown) { + super(message) + + this.code = code + this.message = message + this.rpcData = rpcData + + // Set the prototype explicitly. + Object.setPrototypeOf(this, WrappedEthProviderRpcError.prototype) + } +} diff --git a/apps/extension/src/core/injectEth/getInjectableEvmProvider.ts b/apps/extension/src/core/injectEth/getInjectableEvmProvider.ts index 22c9e0a6a2..191aa8f8e9 100644 --- a/apps/extension/src/core/injectEth/getInjectableEvmProvider.ts +++ b/apps/extension/src/core/injectEth/getInjectableEvmProvider.ts @@ -8,19 +8,19 @@ import { ETH_ERROR_EIP1474_INTERNAL_ERROR, ETH_ERROR_EIP1993_USER_REJECTED, EthProviderRpcError, + WrappedEthProviderRpcError, } from "./EthProviderRpcError" -import type { - EthRequestArguments, - EthRequestSignatures, - EthRequestTypes, - EthResponseType, -} from "./types" + +interface RequestArguments { + readonly method: string + readonly params?: readonly unknown[] | object +} interface JsonRpcRequest { id: string | undefined jsonrpc: "2.0" method: string - params?: Array + params?: Array } interface JsonRpcResponse { @@ -81,14 +81,8 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { log.debug("Talisman provider initializing") const [resChainId, resAccounts] = await Promise.all([ - sendRequest("pub(eth.request)", { - method: "eth_chainId", - params: null, - }), - sendRequest("pub(eth.request)", { - method: "eth_accounts", - params: null, - }), + sendRequest("pub(eth.request)", { method: "eth_chainId" }), + sendRequest("pub(eth.request)", { method: "eth_accounts" }), ]) const chainId = resChainId as string @@ -146,9 +140,7 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { const waitReady = initialize() - const request = async ( - args: EthRequestArguments - ): Promise> => { + const request = async (args: RequestArguments): Promise => { try { log.debug("[talismanEth.request] request %s", args.method, args.params) await waitReady @@ -159,7 +151,7 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { } catch (err) { log.debug("[talismanEth.request] error on %s", args.method, { err }) - const { code, message, data } = err as EthProviderRpcError + const { code, message, rpcData } = err as WrappedEthProviderRpcError if (code > 0) { // standard wallet error (user rejected, etc.) @@ -173,7 +165,7 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { throw new EthProviderRpcError( "Internal JSON-RPC error.", ETH_ERROR_EIP1474_INTERNAL_ERROR, - { code, message, data } + rpcData ) } } @@ -184,8 +176,8 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { if (typeof methodOrPayload === "string") return request({ - method: methodOrPayload as keyof EthRequestSignatures, - params: paramsOrCallback as any, + method: methodOrPayload, + params: paramsOrCallback, }) else { return request(methodOrPayload).then(paramsOrCallback) @@ -197,10 +189,7 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { const { method, params, ...rest } = payload try { - const result = await request({ - method: method as EthRequestTypes, - params: params as any, - }) + const result = await request({ method, params }) callback(null, { ...rest, method, result }) } catch (err) { const error = err as Error @@ -213,7 +202,7 @@ export const getInjectableEvmProvider = (sendRequest: SendRequest) => { log.debug("[talismanEth.enable]") // some frameworks such as web3modal requires this method to exist - return request({ method: "eth_requestAccounts", params: null }) + return request({ method: "eth_requestAccounts" }) } provider.isConnected = isConnected diff --git a/apps/extension/src/core/injectEth/types.ts b/apps/extension/src/core/injectEth/types.ts index 17770c4d4c..51e9f599f2 100644 --- a/apps/extension/src/core/injectEth/types.ts +++ b/apps/extension/src/core/injectEth/types.ts @@ -1,157 +1,8 @@ import EventEmitter from "events" -import type { - AddEthereumChainParameter, - RequestedPermissions, - Web3WalletPermission, -} from "@core/domains/ethereum/types" -import type { WatchAssetBase } from "@core/domains/ethereum/types" -import { BlockWithTransactions } from "@ethersproject/abstract-provider" -import { BigNumberish } from "@ethersproject/bignumber" -import type { - Block, - BlockTag, - TransactionReceipt, - TransactionRequest, - TransactionResponse, -} from "@ethersproject/providers" -// Compliant with https://eips.ethereum.org/EIPS/eip-1193 -import type { InjectedAccount } from "@polkadot/extension-inject/types" - -type Promisify = T | Promise - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PromisifyArray> = { - /* The inbuilt ethers provider methods take arguments which can - * be a value or the promise of a value, this utility type converts a normal - * object type into one where all the values may be promises - */ - [K in keyof T]: Promisify -} - -export type EthRequestGetBalance = PromisifyArray<[string, BlockTag]> - -export type EthRequestGetStorage = PromisifyArray<[string, BigNumberish, BlockTag]> - -export type EthRequestGetTxCount = PromisifyArray<[string, BlockTag]> - -export type EthRequestBlockTagOnly = PromisifyArray<[BlockTag]> - -export type EthRequestSendRawTx = PromisifyArray<[string]> - -export type EthRequestCall = [TransactionRequest, Promise] - -export type EthRequestEstimateGas = [TransactionRequest, string] - -export type EthRequestGetBlock = PromisifyArray<[BlockTag, boolean]> - -export type EthRequestTxHashOnly = PromisifyArray<[string]> - -export type EthRequestSign = [string, string] -export type EthRequestRecoverAddress = [string, string] - -export type EthRequestSendTx = [TransactionRequest] - -export type EthRequestAddEthereumChain = [AddEthereumChainParameter] - -export type EthRequestSwitchEthereumChain = [{ chainId: string }] - -export interface EthRequestSignatures { - eth_requestAccounts: [null, InjectedAccount[]] - eth_gasPrice: [null, string] - eth_accounts: [null, string] - eth_blockNumber: [null, number] - eth_chainId: [null, string] - eth_coinbase: [null, string] - net_version: [null, string] - eth_getBalance: [EthRequestGetBalance, string] - eth_getStorageAt: [EthRequestGetStorage, string] - eth_getTransactionCount: [EthRequestGetTxCount, string] - eth_getBlockTransactionCountByHash: [EthRequestBlockTagOnly, string] - eth_getBlockTransactionCountByNumber: [EthRequestBlockTagOnly, string] - eth_getCode: [EthRequestBlockTagOnly, Block] - eth_sendRawTransaction: [EthRequestSendRawTx, TransactionResponse] - eth_call: [EthRequestCall, string] - estimateGas: [EthRequestEstimateGas, string] - eth_getBlockByHash: [EthRequestGetBlock, Block | BlockWithTransactions] - eth_getBlockByNumber: [EthRequestGetBlock, Block | BlockWithTransactions] - eth_getTransactionByHash: [EthRequestTxHashOnly, TransactionResponse] - eth_getTransactionReceipt: [EthRequestTxHashOnly, TransactionReceipt] - personal_sign: [EthRequestSign, string] - eth_signTypedData: [EthRequestSign, string] - eth_signTypedData_v1: [EthRequestSign, string] - eth_signTypedData_v3: [EthRequestSign, string] - eth_signTypedData_v4: [EthRequestSign, string] - eth_sendTransaction: [EthRequestSendTx, string] - personal_ecRecover: [EthRequestRecoverAddress, string] - - // EIP 747 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-747.md - wallet_watchAsset: [WatchAssetBase, string] - - // pending EIP https://eips.ethereum.org/EIPS/eip-3085, defined by metamask to let dapp add chains. - // returns `null` if the request was successful, otherwise throws an error. - // metamask will automatically reject this when: - // - the rpc endpoint doesn't respond to rpc calls - // - the rpc endpoint returns a different chain id than the one specified - // - the chain id corresponds to any of the default metamask chains - wallet_addEthereumChain: [EthRequestAddEthereumChain, null] - - // pending EIP https://eips.ethereum.org/EIPS/eip-3326, defined by metamask to let dapp change chain. - // returns `null` if the request was successful, otherwise throws an error. - // if the `error.code` is `4902` then the dapp is more likely to call `wallet_addEthereumChain`. - // metamask will automatically reject this when: - // - the chain id is malformed - // - the chain with the specified id has not been added to metamask - wallet_switchEthereumChain: [EthRequestSwitchEthereumChain, null] - - // https://docs.metamask.io/guide/rpc-api.html#wallet-getpermissions - wallet_getPermissions: [null, Web3WalletPermission[]] - - // https://docs.metamask.io/guide/rpc-api.html#wallet-requestpermissions - wallet_requestPermissions: [[RequestedPermissions], Web3WalletPermission[]] -} - -export type EthRequestTypes = keyof EthRequestSignatures -export type EthResponseTypes = EthRequestSignatures[keyof EthRequestSignatures][1] -export type EthResponseType = EthRequestSignatures[T][1] -export type EthRequestParams = EthRequestSignatures[keyof EthRequestSignatures][0] -export interface EthRequestArguments { - readonly method: T - readonly params: EthRequestSignatures[T][0] -} - -export type EthRequestSignArguments = EthRequestArguments< - | "personal_sign" - | "eth_signTypedData" - | "eth_signTypedData_v1" - | "eth_signTypedData_v3" - | "eth_signTypedData_v4" -> - export interface AnyEthRequest { - readonly method: EthRequestTypes - readonly params: EthRequestSignatures[EthRequestTypes][0] -} - -export interface EthProviderMessage { - readonly type: string - readonly data: unknown -} - -export type EthSubscriptionId = string - -export interface EthSubscriptionData { - readonly subscription: EthSubscriptionId - readonly result: unknown -} - -export interface EthSubscriptionMessage extends EthProviderMessage { - readonly type: "eth_subscription" - readonly data: EthSubscriptionData -} - -export interface ProviderConnectInfo { - readonly chainId: string + readonly method: string + readonly params?: readonly unknown[] | object } export interface EthProvider extends EventEmitter { diff --git a/apps/extension/src/core/libs/MessageService.ts b/apps/extension/src/core/libs/MessageService.ts index ecfebc67ba..9de843bb83 100644 --- a/apps/extension/src/core/libs/MessageService.ts +++ b/apps/extension/src/core/libs/MessageService.ts @@ -5,7 +5,7 @@ import { ETH_ERROR_EIP1474_INTERNAL_ERROR, - EthProviderRpcError, + WrappedEthProviderRpcError, } from "@core/injectEth/EthProviderRpcError" import { log } from "@core/log" import type { @@ -132,7 +132,7 @@ export default class MessageService { data: TransportResponseMessage & { subscription?: string code?: number - data?: unknown + rpcData?: unknown isEthProviderRpcError?: boolean } ): void { @@ -159,10 +159,10 @@ export default class MessageService { else if (data.error) { if (data.isEthProviderRpcError) { handler.reject( - new EthProviderRpcError( + new WrappedEthProviderRpcError( data.error, data.code ?? ETH_ERROR_EIP1474_INTERNAL_ERROR, - data.data + data.rpcData ) ) } else handler.reject(new Error(data.error)) diff --git a/apps/extension/src/core/log/index.ts b/apps/extension/src/core/log/index.ts index 88987aee58..cae061a560 100644 --- a/apps/extension/src/core/log/index.ts +++ b/apps/extension/src/core/log/index.ts @@ -10,4 +10,15 @@ export const log = { warn: (message: any, ...args: any[]) => DEBUG && console.warn(message, ...args), log: (message: any, ...args: any[]) => DEBUG && console.log(message, ...args), debug: (message: any, ...args: any[]) => DEBUG && console.debug(message, ...args), + + timer: (label: string) => { + if (!DEBUG) return () => {} + + const timeKey = `${label} (${crypto.randomUUID()})` + console.time(timeKey) + + return () => { + console.timeEnd(timeKey) + } + }, } diff --git a/apps/extension/src/core/notifications/createNotification.ts b/apps/extension/src/core/notifications/createNotification.ts index d2e2223a98..7054d4abc0 100644 --- a/apps/extension/src/core/notifications/createNotification.ts +++ b/apps/extension/src/core/notifications/createNotification.ts @@ -8,7 +8,7 @@ export type NotificationType = "submitted" | "success" | "error" const getNotificationOptions = ( type: NotificationType, networkName: string, - error?: Error & { reason?: string } + error?: Error & { shortMessage?: string; reason?: string } ): Browser.Notifications.CreateNotificationOptions => { switch (type) { case "submitted": @@ -29,7 +29,11 @@ const getNotificationOptions = ( return { type: "basic", title: "Transaction failed", - message: error?.reason ?? error?.message ?? `Failed transaction on ${networkName}.`, + message: + error?.shortMessage ?? + error?.reason ?? + error?.message ?? + `Failed transaction on ${networkName}.`, iconUrl: "/images/tx-nok.png", } } diff --git a/apps/extension/src/core/rpcs/chain-connector-evm.ts b/apps/extension/src/core/rpcs/chain-connector-evm.ts index 9ffbdb1d13..09894c5aa6 100644 --- a/apps/extension/src/core/rpcs/chain-connector-evm.ts +++ b/apps/extension/src/core/rpcs/chain-connector-evm.ts @@ -1,4 +1,4 @@ import { chaindataProvider } from "@core/rpcs/chaindata" import { ChainConnectorEvm } from "@talismn/chain-connector-evm" -export const chainConnectorEvm = new ChainConnectorEvm(chaindataProvider) +export const chainConnectorEvm = new ChainConnectorEvm(chaindataProvider, chaindataProvider) diff --git a/apps/extension/src/core/util/abi/abiErc20.ts b/apps/extension/src/core/util/abi/abiErc20.ts index e2cc8bd5d7..1324f12799 100644 --- a/apps/extension/src/core/util/abi/abiErc20.ts +++ b/apps/extension/src/core/util/abi/abiErc20.ts @@ -9,4 +9,4 @@ export const abiErc20 = [ "function allowance(address owner, address spender) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", "function transferFrom(address from, address to, uint256 amount) returns (bool)", -] +] as const diff --git a/apps/extension/src/core/util/abi/abiErc721.ts b/apps/extension/src/core/util/abi/abiErc721.ts index 77ac47000d..d5513244e0 100644 --- a/apps/extension/src/core/util/abi/abiErc721.ts +++ b/apps/extension/src/core/util/abi/abiErc721.ts @@ -1,4 +1,8 @@ export const abiErc721 = [ + "function symbol() view returns (string)", + "function name() view returns (string)", + "function tokenURI(uint256 tokenId) view returns (string)", + "function supportsInterface(bytes4 interfaceId) external view returns (bool)", "function balanceOf(address owner) external view returns (uint256)", "function ownerOf(uint256 tokenId) external view returns (address)", diff --git a/apps/extension/src/core/util/decodeEvmTransaction.ts b/apps/extension/src/core/util/decodeEvmTransaction.ts new file mode 100644 index 0000000000..3723f25b98 --- /dev/null +++ b/apps/extension/src/core/util/decodeEvmTransaction.ts @@ -0,0 +1,160 @@ +import { + PublicClient, + TransactionRequestBase, + decodeFunctionData, + getAbiItem, + getContract, + parseAbi, +} from "viem" + +import { abiErc1155, abiErc20, abiErc721, abiMoonStaking } from "./abi" +import { abiMoonConvictionVoting } from "./abi/abiMoonConvictionVoting" +import { abiMoonXTokens } from "./abi/abiMoonXTokens" +import { isContractAddress } from "./isContractAddress" + +const MOON_CHAIN_PRECOMPILES = [ + { + address: "0x0000000000000000000000000000000000000800", + contractType: "MoonStaking", + abi: abiMoonStaking, + }, + { + address: "0x0000000000000000000000000000000000000812", + contractType: "MoonConvictionVoting", + abi: abiMoonConvictionVoting, + }, + { + address: "0x0000000000000000000000000000000000000804", + contractType: "MoonXTokens", + abi: abiMoonXTokens, + }, +] as const + +const STANDARD_CONTRACTS = [ + { + contractType: "ERC20", + abi: parseAbi(abiErc20), + }, + { + contractType: "ERC721", + abi: parseAbi(abiErc721), + }, + { + contractType: "ERC1155", + abi: parseAbi(abiErc1155), + }, +] as const + +export const decodeEvmTransaction = async ( + publicClient: PublicClient, + tx: TransactionRequestBase +) => { + // transactions that provision a contract have an empty 'to' field + const { to: targetAddress, value, data } = tx + + const isContractCall = targetAddress + ? await isContractAddress(publicClient, targetAddress) + : false + + if (isContractCall && data && targetAddress) { + // moon chains precompiles + if (publicClient.chain?.id && [1284, 1285, 1287].includes(publicClient.chain.id)) { + for (const { address, contractType, abi } of MOON_CHAIN_PRECOMPILES) { + if (address === targetAddress) { + //const { contractType, abi } = precompile + const contractCall = decodeFunctionData({ abi, data }) + return { + contractType, + contractCall, + targetAddress, + isContractCall: true, + value, + abi, + } + } + } + } + + // common contracts + for (const { contractType, abi } of STANDARD_CONTRACTS) { + try { + if (contractType === "ERC20") { + const contractCall = decodeFunctionData({ abi, data }) + + const contract = getContract({ + address: targetAddress, + abi, + publicClient, + }) + + const [name, symbol, decimals] = await Promise.all([ + contract.read.name(), + contract.read.symbol(), + contract.read.decimals(), + ]) + + return { + contractType, + contractCall, + abi, + targetAddress, + isContractCall: true, + value, + asset: { name, symbol, decimals }, + } + } + if (contractType === "ERC721") { + const contractCall = decodeFunctionData({ abi, data }) + + const abiItem = getAbiItem({ + abi, + args: contractCall.args, + name: contractCall.functionName, + }) + const tokenIdIndex = abiItem.inputs.findIndex((input) => input.name === "tokenId") + const tokenId = + tokenIdIndex > -1 ? (contractCall.args?.[tokenIdIndex] as bigint) : undefined + + const contract = getContract({ + address: targetAddress, + abi, + publicClient, + }) + + // some calls may fail as not all NFTs implement the metadata functions + const [name, symbol, tokenURI] = await Promise.allSettled([ + contract.read.name(), + contract.read.symbol(), + tokenId ? contract.read.tokenURI([tokenId]) : undefined, + ]) + + const asset = [name.status, symbol.status, tokenURI].includes("fulfilled") + ? { + name: name.status === "fulfilled" ? name.value : undefined, + symbol: symbol.status === "fulfilled" ? symbol.value : undefined, + tokenId, + tokenURI: tokenURI.status === "fulfilled" ? tokenURI.value : undefined, + decimals: 1, + } + : undefined + + return { + contractType, + contractCall, + abi, + targetAddress, + isContractCall: true, + value, + asset, + } + } + } catch { + // ignore + } + } + } + + return { contractType: "unknown", targetAddress, isContractCall, value } +} + +export type DecodedEvmTransaction = Awaited> diff --git a/apps/extension/src/core/util/getErc20ContractData.ts b/apps/extension/src/core/util/getErc20ContractData.ts index a3ea3ac729..9ee6690e4c 100644 --- a/apps/extension/src/core/util/getErc20ContractData.ts +++ b/apps/extension/src/core/util/getErc20ContractData.ts @@ -1,9 +1,11 @@ -import { ethers } from "ethers" +import { Client, getContract, parseAbi } from "viem" const ABI_ERC20 = [ "function symbol() view returns (string)", "function decimals() view returns (uint8)", -] +] as const + +const PARSED_ABI_ERC20 = parseAbi(ABI_ERC20) export type Erc20ContractData = { symbol: string @@ -11,10 +13,14 @@ export type Erc20ContractData = { } export const getErc20ContractData = async ( - provider: ethers.providers.JsonRpcProvider, - contractAddress: string + client: Client, + contractAddress: `0x${string}` ): Promise => { - const erc20 = new ethers.Contract(contractAddress, ABI_ERC20, provider) - const [symbol, decimals] = await Promise.all([erc20.symbol(), erc20.decimals()]) + const contract = getContract({ + address: contractAddress, + abi: PARSED_ABI_ERC20, + publicClient: client, + }) + const [symbol, decimals] = await Promise.all([contract.read.symbol(), contract.read.decimals()]) return { symbol, decimals } } diff --git a/apps/extension/src/core/util/getErc20TokenInfo.ts b/apps/extension/src/core/util/getErc20TokenInfo.ts index 37268d9200..9ed556a224 100644 --- a/apps/extension/src/core/util/getErc20TokenInfo.ts +++ b/apps/extension/src/core/util/getErc20TokenInfo.ts @@ -1,17 +1,17 @@ -import { EvmNetworkId } from "@core/domains/ethereum/types" +import { EvmAddress, EvmNetworkId } from "@core/domains/ethereum/types" import { CustomErc20TokenCreate } from "@core/domains/tokens/types" -import { ethers } from "ethers" +import { Client } from "viem" import { getCoinGeckoErc20Coin } from "./coingecko/getCoinGeckoErc20Coin" import { getErc20ContractData } from "./getErc20ContractData" export const getErc20TokenInfo = async ( - provider: ethers.providers.JsonRpcProvider, + client: Client, evmNetworkId: EvmNetworkId, - contractAddress: string + contractAddress: EvmAddress ): Promise => { const [{ decimals, symbol }, coinGeckoData] = await Promise.all([ - getErc20ContractData(provider, contractAddress), + getErc20ContractData(client, contractAddress), getCoinGeckoErc20Coin(evmNetworkId, contractAddress), ]) diff --git a/apps/extension/src/core/util/getEthTransactionInfo.ts b/apps/extension/src/core/util/getEthTransactionInfo.ts deleted file mode 100644 index 22d6d64bfc..0000000000 --- a/apps/extension/src/core/util/getEthTransactionInfo.ts +++ /dev/null @@ -1,164 +0,0 @@ -import * as Sentry from "@sentry/browser" -import { getContractCallArg } from "@ui/domains/Sign/Ethereum/getContractCallArg" -import { BigNumber, ethers } from "ethers" - -import { abiErc1155, abiErc20, abiErc721, abiErc721Metadata, abiMoonStaking } from "./abi" -import { abiMoonConvictionVoting } from "./abi/abiMoonConvictionVoting" -import { abiMoonXTokens } from "./abi/abiMoonXTokens" -import { isContractAddress } from "./isContractAddress" - -export type ContractType = - | "ERC20" - | "ERC721" - | "ERC1155" - | "MoonXTokens" - | "MoonStaking" - | "MoonConvictionVoting" - | "unknown" - -const MOON_CHAIN_PRECOMPILE_ADDRESSES: Record< - string, - { contractType: ContractType; abi: unknown } -> = { - "0x0000000000000000000000000000000000000800": { - contractType: "MoonStaking", - abi: abiMoonStaking, - }, - "0x0000000000000000000000000000000000000812": { - contractType: "MoonConvictionVoting", - abi: abiMoonConvictionVoting, - }, - "0x0000000000000000000000000000000000000804": { - contractType: "MoonXTokens", - abi: abiMoonXTokens, - }, -} - -// note : order may be important here as some contracts may inherit from others -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const knownContracts: { contractType: ContractType; abi: any }[] = [ - { - contractType: "ERC20", - abi: abiErc20, - }, - { - contractType: "ERC721", - abi: abiErc721, - }, - { - contractType: "ERC1155", - abi: abiErc1155, - }, -] - -export type TransactionInfo = { - targetAddress?: string - isContractCall: boolean - value?: BigNumber - contractType?: ContractType - contractCall?: ethers.utils.TransactionDescription - asset?: { - name: string - symbol: string - decimals: number - image?: string - tokenId?: BigNumber - tokenURI?: string - } -} -export type KnownTransactionInfo = Required - -export const getEthTransactionInfo = async ( - provider: ethers.providers.Provider, - tx: ethers.providers.TransactionRequest -): Promise => { - // transactions that provision a contract have an empty 'to' field - const targetAddress = tx.to ? ethers.utils.getAddress(tx.to) : undefined - - const isContractCall = targetAddress ? await isContractAddress(provider, targetAddress) : false - - const result: TransactionInfo = { - targetAddress, - isContractCall, - contractType: isContractCall ? "unknown" : undefined, - value: tx.value ? BigNumber.from(tx.value) : undefined, - } - - // moon chains precompiles - if ( - tx.data && - tx.to && - tx.chainId && - [1284, 1285, 1287].includes(tx.chainId) && - !!MOON_CHAIN_PRECOMPILE_ADDRESSES[tx.to] - ) { - const { contractType, abi } = MOON_CHAIN_PRECOMPILE_ADDRESSES[tx.to] - try { - const data = ethers.utils.hexlify(tx.data) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const contractInterface = new ethers.utils.Interface(abi as any) - // error will be thrown here if contract doesn't match the abi - const contractCall = contractInterface.parseTransaction({ data, value: tx.value }) - result.contractType = contractType - result.contractCall = contractCall - - return result - } catch (err) { - Sentry.captureException(err, { extra: { to: tx.to, chainId: tx.chainId } }) - } - } - - if (targetAddress && tx.data) { - const data = ethers.utils.hexlify(tx.data) - - for (const { contractType, abi } of knownContracts) { - try { - const contractInterface = new ethers.utils.Interface(abi) - - // error will be thrown here if contract doesn't match the abi - const contractCall = contractInterface.parseTransaction({ data, value: tx.value }) - result.contractType = contractType - result.contractCall = contractCall - - if (contractType === "ERC20") { - const contract = new ethers.Contract(targetAddress, contractInterface, provider) - const [name, symbol, decimals] = await Promise.all([ - contract.name(), - contract.symbol(), - contract.decimals(), - ]) - - result.asset = { name, symbol, decimals } - } else if (contractType === "ERC721") { - const tokenId = getContractCallArg(contractCall, "tokenId") - - try { - const contract = new ethers.Contract(targetAddress, abiErc721Metadata, provider) - const [name, symbol, tokenURI] = await Promise.all([ - contract.name(), - contract.symbol(), - tokenId ? contract.tokenURI(tokenId) : undefined, - ]) - - result.asset = { - name, - symbol, - tokenId, - tokenURI, - decimals: 1, - } - } catch (err) { - // some NFTs don't implement the metadata functions - } - } - - return result - } catch (err) { - // transaction doesn't match this contract interface - } - } - } - - return result -} diff --git a/apps/extension/src/core/util/getFeeHistoryAnalysis.ts b/apps/extension/src/core/util/getFeeHistoryAnalysis.ts index 0d757cad62..2dac1adff6 100644 --- a/apps/extension/src/core/util/getFeeHistoryAnalysis.ts +++ b/apps/extension/src/core/util/getFeeHistoryAnalysis.ts @@ -1,75 +1,51 @@ import { EthBaseFeeTrend } from "@core/domains/signing/types" +import { log } from "@core/log" import * as Sentry from "@sentry/browser" -import { BigNumber, ethers } from "ethers" -import { parseUnits } from "ethers/lib/utils" +import { PublicClient, formatGwei, parseGwei } from "viem" -const BLOCKS_HISTORY_LENGTH = 4 +const BLOCKS_HISTORY_LENGTH = 5 const REWARD_PERCENTILES = [10, 20, 30] +const LIVE_DEBUG = false -type EthBasePriorityOptionsEip1559 = Record<"low" | "medium" | "high", BigNumber> +type EthBasePriorityOptionsEip1559 = Record<"low" | "medium" | "high", bigint> -export const DEFAULT_ETH_PRIORITY_OPTIONS: EthBasePriorityOptionsEip1559 = { - low: parseUnits("1.5", "gwei"), - medium: parseUnits("1.6", "gwei"), - high: parseUnits("1.7", "gwei"), -} - -type FeeHistory = { - oldestBlock: number - baseFeePerGas: BigNumber[] - gasUsedRatio: (number | null)[] // can have null values (ex astar) - reward?: BigNumber[][] // TODO find network that doesn't return this property, for testing +const DEFAULT_ETH_PRIORITY_OPTIONS: EthBasePriorityOptionsEip1559 = { + low: parseGwei("1.5"), + medium: parseGwei("1.6"), + high: parseGwei("1.7"), } export type FeeHistoryAnalysis = { maxPriorityPerGasOptions: EthBasePriorityOptionsEip1559 - avgGasUsedRatio: number | null + avgGasUsedRatio: number isValid: boolean - avgBaseFeePerGas: BigNumber + avgBaseFeePerGas: bigint isBaseFeeIdle: boolean - nextBaseFee: BigNumber + nextBaseFee: bigint baseFeeTrend: EthBaseFeeTrend } export const getFeeHistoryAnalysis = async ( - provider: ethers.providers.JsonRpcProvider + publicClient: PublicClient ): Promise => { try { - const rawHistoryFee = await provider.send("eth_feeHistory", [ - ethers.utils.hexValue(BLOCKS_HISTORY_LENGTH), - "latest", - REWARD_PERCENTILES, - ]) - - // instrument for information - remove asap - if (!rawHistoryFee.reward) - Sentry.captureMessage(`No reward on fee history`, { extra: { chain: provider.network.name } }) - - // parse hex values - const feeHistory: FeeHistory = { - oldestBlock: parseInt(rawHistoryFee.oldestBlock, 16), - baseFeePerGas: rawHistoryFee.baseFeePerGas.map((fee: string) => BigNumber.from(fee)), - gasUsedRatio: rawHistoryFee.gasUsedRatio as (number | null)[], - reward: rawHistoryFee.reward - ? rawHistoryFee.reward.map((reward: string[]) => reward.map((r) => BigNumber.from(r))) - : null, - } + const feeHistory = await publicClient.getFeeHistory({ + blockCount: BLOCKS_HISTORY_LENGTH, + rewardPercentiles: REWARD_PERCENTILES, + }) - // how busy the network is over this period - // values can be null (ex astar) - const avgGasUsedRatio = feeHistory.gasUsedRatio.includes(null) - ? null - : (feeHistory.gasUsedRatio as number[]).reduce((prev, curr) => prev + curr, 0) / - feeHistory.gasUsedRatio.length + const avgGasUsedRatio = + (feeHistory.gasUsedRatio as number[]).reduce((prev, curr) => prev + curr, 0) / + feeHistory.gasUsedRatio.length // lookup the max priority fee per gas based on our percentiles options // use a median to exclude extremes, to limits edge cases in low network activity conditions - const medMaxPriorityFeePerGas: BigNumber[] = [] + const medMaxPriorityFeePerGas: bigint[] = [] if (feeHistory.reward) { const percentilesCount = REWARD_PERCENTILES.length for (let i = 0; i < percentilesCount; i++) { - const values = feeHistory.reward.map((arr) => BigNumber.from(arr[i])) - const sorted = values.sort((a, b) => (a.eq(b) ? 0 : a.gt(b) ? 1 : -1)) + const values = feeHistory.reward.map((arr) => arr[i]) + const sorted = values.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) const median = sorted[Math.floor((sorted.length - 1) / 2)] medMaxPriorityFeePerGas.push(median) } @@ -81,27 +57,27 @@ export const getFeeHistoryAnalysis = async ( ) // last entry of the array is the base fee for next block, exclude it from further averages - const nextBaseFee = feeHistory.baseFeePerGas.pop() as BigNumber + const nextBaseFee = feeHistory.baseFeePerGas.pop() as bigint - const isBaseFeeIdle = feeHistory.baseFeePerGas.every((fee) => fee.eq(nextBaseFee)) + const isBaseFeeIdle = feeHistory.baseFeePerGas.every((fee) => fee === nextBaseFee) - const avgBaseFeePerGas = feeHistory.baseFeePerGas - .reduce((prev, curr) => prev.add(curr), BigNumber.from(0)) - .div(feeHistory.baseFeePerGas.length) + const avgBaseFeePerGas = + feeHistory.baseFeePerGas.reduce((prev, curr) => prev + curr, 0n) / + BigInt(feeHistory.baseFeePerGas.length) const baseFeeTrend = isBaseFeeIdle ? "idle" - : nextBaseFee.lt(avgBaseFeePerGas) + : nextBaseFee < avgBaseFeePerGas ? "decreasing" : !avgGasUsedRatio || avgGasUsedRatio < 0.9 ? "increasing" : "toTheMoon" - return { + const result: FeeHistoryAnalysis = { maxPriorityPerGasOptions: { low: medMaxPriorityFeePerGas[0], - medium: medMaxPriorityFeePerGas[1], - high: medMaxPriorityFeePerGas[2], + medium: (medMaxPriorityFeePerGas[1] * 102n) / 100n, + high: (medMaxPriorityFeePerGas[2] * 104n) / 100n, }, avgGasUsedRatio: avgGasUsedRatio, isValid: !feeHistory.gasUsedRatio.includes(0), // if a 0 is found, not all blocks contained a transaction @@ -110,6 +86,28 @@ export const getFeeHistoryAnalysis = async ( nextBaseFee, baseFeeTrend, } + + if (LIVE_DEBUG) { + log.log( + "rewards", + feeHistory.reward?.map((arr) => arr.map((reward) => `${formatGwei(reward)} GWEI`)) + ) + log.log("baseFee", `${formatGwei(result.nextBaseFee)} GWEI`) + log.log( + "medMaxPriorityFeePerGas", + medMaxPriorityFeePerGas.map((fee) => `${formatGwei(fee)} GWEI`) + ) + log.log( + "maxPriorityPerGasOptions", + [ + result.maxPriorityPerGasOptions.low, + result.maxPriorityPerGasOptions.medium, + result.maxPriorityPerGasOptions.high, + ].map((fee) => `${formatGwei(fee)} GWEI`) + ) + log.log("=========================================") + } + return result } catch (err) { Sentry.captureException(err) throw new Error("Failed to load fee history", { cause: err as Error }) diff --git a/apps/extension/src/core/util/getPrivateKey.ts b/apps/extension/src/core/util/getPrivateKey.ts index ee66390549..b2ff71cbc9 100644 --- a/apps/extension/src/core/util/getPrivateKey.ts +++ b/apps/extension/src/core/util/getPrivateKey.ts @@ -12,7 +12,7 @@ const SEED_LENGTH = 32 const SEED_OFFSET = PKCS8_HEADER.length // built from reverse engineering polkadot keyring -export const getPrivateKey = (pair: KeyringPair, password: string) => { +const getU8aPrivateKey = (pair: KeyringPair, password: string) => { if (pair.isLocked) pair.unlock(password) const json = pair.toJson(password) @@ -34,5 +34,30 @@ export const getPrivateKey = (pair: KeyringPair, password: string) => { if (!u8aEq(divider, PKCS8_DIVIDER)) throw new Error("Invalid Pkcs8 divider found in body") } - return u8aToBuffer(privateKey) + return privateKey +} + +type PrivateKeyFormat = "u8a" | "hex" | "buffer" +type PrivateKeyOutput = T extends "u8a" + ? Uint8Array + : T extends "hex" + ? `0x${string}` + : Buffer + +export const getPrivateKey = ( + pair: KeyringPair, + password: string, + format: F +): PrivateKeyOutput => { + const privateKey = getU8aPrivateKey(pair, password) + + switch (format) { + case "hex": + return `0x${Buffer.from(privateKey).toString("hex")}` as PrivateKeyOutput + case "u8a": + return u8aToBuffer(privateKey) as PrivateKeyOutput + case "buffer": + default: + return privateKey as PrivateKeyOutput + } } diff --git a/apps/extension/src/core/util/isContractAddress.ts b/apps/extension/src/core/util/isContractAddress.ts index 10384df8dd..282e56731a 100644 --- a/apps/extension/src/core/util/isContractAddress.ts +++ b/apps/extension/src/core/util/isContractAddress.ts @@ -1,8 +1,9 @@ -import { ethers } from "ethers" +import { EvmAddress } from "@core/domains/ethereum/types" +import { PublicClient } from "viem" -export const isContractAddress = async (provider: ethers.providers.Provider, address: string) => { +export const isContractAddress = async (client: PublicClient, address: EvmAddress) => { try { - const code = await provider.getCode(address) + const code = await client.getBytecode({ address }) return code !== "0x" } catch (error) { // not a contract diff --git a/apps/extension/src/ui/api/api.ts b/apps/extension/src/ui/api/api.ts index 3d523ccd87..2f1465c2da 100644 --- a/apps/extension/src/ui/api/api.ts +++ b/apps/extension/src/ui/api/api.ts @@ -260,10 +260,19 @@ export const api: MessageTypes = { }), // eth related messages - ethSignAndSend: (unsigned, transferInfo) => - messageService.sendMessage("pri(eth.signing.signAndSend)", { unsigned, transferInfo }), - ethSendSigned: (unsigned, signed, transferInfo) => - messageService.sendMessage("pri(eth.signing.sendSigned)", { unsigned, signed, transferInfo }), + ethSignAndSend: (evmNetworkId, unsigned, transferInfo) => + messageService.sendMessage("pri(eth.signing.signAndSend)", { + evmNetworkId, + unsigned, + transferInfo, + }), + ethSendSigned: (evmNetworkId, unsigned, signed, transferInfo) => + messageService.sendMessage("pri(eth.signing.sendSigned)", { + evmNetworkId, + unsigned, + signed, + transferInfo, + }), ethApproveSign: (id) => messageService.sendMessage("pri(eth.signing.approveSign)", { id, diff --git a/apps/extension/src/ui/api/types.ts b/apps/extension/src/ui/api/types.ts index 92e3c96066..033103507d 100644 --- a/apps/extension/src/ui/api/types.ts +++ b/apps/extension/src/ui/api/types.ts @@ -26,7 +26,7 @@ import { } from "@core/domains/balances/types" import { ChainId, RequestUpsertCustomChain } from "@core/domains/chains/types" import type { DecryptRequestId, EncryptRequestId } from "@core/domains/encrypt/types" -import { AddEthereumChainRequestId } from "@core/domains/ethereum/types" +import { AddEthereumChainRequestId, EvmAddress } from "@core/domains/ethereum/types" import { AddEthereumChainRequest, AnyEthRequestChainId, @@ -58,15 +58,13 @@ import { ResponseAssetTransferFeeQuery, } from "@core/domains/transfers/types" import { MetadataDef } from "@core/inject/types" -import { EthResponseType } from "@core/injectEth/types" import { ValidRequests } from "@core/libs/requests/types" import { UnsubscribeFn } from "@core/types" import { AddressesByChain } from "@core/types/base" import type { KeyringPair$Json } from "@polkadot/keyring/types" import { KeypairType } from "@polkadot/util-crypto/types" import type { HexString } from "@polkadot/util/types" -import { Address } from "@talismn/balances" -import { ethers } from "ethers" +import { TransactionRequest } from "viem" export default interface MessageTypes { unsubscribe: (id: string) => Promise @@ -226,17 +224,17 @@ export default interface MessageTypes { assetTransferEth: ( evmNetworkId: EvmNetworkId, tokenId: TokenId, - fromAddress: string, - toAddress: string, + fromAddress: EvmAddress, + toAddress: EvmAddress, amount: string, - gasSettings: EthGasSettings + gasSettings: EthGasSettings ) => Promise assetTransferEthHardware: ( evmNetworkId: EvmNetworkId, tokenId: TokenId, amount: string, - to: Address, - unsigned: ethers.providers.TransactionRequest, + to: EvmAddress, + unsigned: TransactionRequest, signedTransaction: HexString ) => Promise assetTransferCheckFees: ( @@ -256,11 +254,13 @@ export default interface MessageTypes { // eth related messages ethSignAndSend: ( - unsigned: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + unsigned: TransactionRequest, transferInfo?: WalletTransactionTransferInfo ) => Promise ethSendSigned: ( - unsigned: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + unsigned: TransactionRequest, signed: HexString, transferInfo?: WalletTransactionTransferInfo ) => Promise @@ -271,16 +271,16 @@ export default interface MessageTypes { ) => Promise ethApproveSignAndSend: ( id: SigningRequestID<"eth-send">, - transaction: ethers.providers.TransactionRequest + transaction: TransactionRequest ) => Promise ethApproveSignAndSendHardware: ( id: SigningRequestID<"eth-send">, - unsigned: ethers.providers.TransactionRequest, + unsigned: TransactionRequest, signedTransaction: HexString ) => Promise ethCancelSign: (id: SigningRequestID<"eth-sign" | "eth-send">) => Promise - ethRequest: (request: T) => Promise> - ethGetTransactionsCount: (address: string, evmNetworkId: EvmNetworkId) => Promise + ethRequest: (request: AnyEthRequestChainId) => Promise + ethGetTransactionsCount: (address: EvmAddress, evmNetworkId: EvmNetworkId) => Promise ethNetworkAddGetRequests: () => Promise ethNetworkAddApprove: (id: AddEthereumChainRequestId) => Promise ethNetworkAddCancel: (is: AddEthereumChainRequestId) => Promise diff --git a/apps/extension/src/ui/apps/onboard/routes/Success.tsx b/apps/extension/src/ui/apps/onboard/routes/Success.tsx index 485889445d..6365ef54f7 100644 --- a/apps/extension/src/ui/apps/onboard/routes/Success.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/Success.tsx @@ -1,5 +1,6 @@ import imgHandOrb from "@talisman/theme/images/onboard_hand_orb.png" import { AnalyticsPage } from "@ui/api/analytics" +import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView" import { Button } from "talisman-ui" import { useOnboard } from "../context" @@ -13,6 +14,7 @@ const SUCCESS_PAGE: AnalyticsPage = { } export const SuccessPage = () => { + useAnalyticsPageView(SUCCESS_PAGE) const { completeOnboarding } = useOnboard() return ( diff --git a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Message.tsx b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Message.tsx index e9df5b571e..15abc4c5b8 100644 --- a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Message.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Message.tsx @@ -71,12 +71,7 @@ export const EthSignMessageRequest = () => { - diff --git a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx index 4963ef7729..bdad2cd6c9 100644 --- a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx @@ -1,6 +1,8 @@ +import { EvmAddress } from "@core/domains/ethereum/types" import { EthPriorityOptionName } from "@core/domains/signing/types" import { AppPill } from "@talisman/components/AppPill" import { WithTooltip } from "@talisman/components/Tooltip" +import { EvmNetworkId, TokenId } from "@talismn/chaindata-provider" import { InfoIcon } from "@talismn/icons" import { PopupContent, @@ -11,7 +13,7 @@ import { import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import { EthFeeSelect } from "@ui/domains/Ethereum/GasSettings/EthFeeSelect" import { useEthBalance } from "@ui/domains/Ethereum/useEthBalance" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { EthSignBody } from "@ui/domains/Sign/Ethereum/EthSignBody" import { SignAlertMessage } from "@ui/domains/Sign/SignAlertMessage" import { SignHardwareEthereum } from "@ui/domains/Sign/SignHardwareEthereum" @@ -22,9 +24,9 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" import { SignAccountAvatar } from "../SignAccountAvatar" -const useEvmBalance = (address: string, evmNetworkId: string | undefined) => { - const provider = useEthereumProvider(evmNetworkId) - return useEthBalance(provider, address) +const useEvmBalance = (address: EvmAddress, evmNetworkId: EvmNetworkId | undefined) => { + const publicClient = usePublicClient(evmNetworkId) + return useEthBalance(publicClient, address) } const FeeTooltip = ({ @@ -33,10 +35,10 @@ const FeeTooltip = ({ tokenId, balance, }: { - estimatedFee: string | bigint | undefined - maxFee: string | bigint | undefined - tokenId: string | undefined - balance: string | bigint | null | undefined + estimatedFee: bigint | undefined + maxFee: bigint | undefined + tokenId: TokenId | undefined + balance: bigint | null | undefined }) => { const { t } = useTranslation("request") @@ -101,14 +103,14 @@ export const EthSignTransactionRequest = () => { approveHardware, isPayloadLocked, setIsPayloadLocked, - transactionInfo, + decodedTx, gasSettingsByPriority, setCustomSettings, setReady, isValid, networkUsage, } = useEthSignTransactionRequest() - const { balance } = useEvmBalance(account?.address, network?.id) + const { balance } = useEvmBalance(account?.address as EvmAddress, network?.id) const { processing, errorMessage } = useMemo(() => { return { @@ -137,7 +139,7 @@ export const EthSignTransactionRequest = () => {
- +
{!isLoading && ( @@ -164,14 +166,14 @@ export const EthSignTransactionRequest = () => { -
{transaction?.type === 2 && t("Priority")}
+
{transaction?.type === "eip1559" && t("Priority")}
@@ -182,7 +184,7 @@ export const EthSignTransactionRequest = () => {
{ ) : null} {account && request && account.isHardware ? ( { {t("Cancel")} -
- -
{ - setHasHovered(true) - onMouseEnter && onMouseEnter(e) - }} - > -
- {mnemonic.split(" ").map((word, i) => ( - - {word} - - ))} -
-
-
-
-
- - ) -} diff --git a/apps/extension/src/ui/domains/CopyAddress/useCopyAddressWizard.ts b/apps/extension/src/ui/domains/CopyAddress/useCopyAddressWizard.ts index b878b1f51b..c16008949d 100644 --- a/apps/extension/src/ui/domains/CopyAddress/useCopyAddressWizard.ts +++ b/apps/extension/src/ui/domains/CopyAddress/useCopyAddressWizard.ts @@ -16,8 +16,8 @@ import useToken from "@ui/hooks/useToken" import useTokens from "@ui/hooks/useTokens" import { copyAddress } from "@ui/util/copyAddress" import { isEvmToken } from "@ui/util/isEvmToken" -import { ethers } from "ethers" import { useCallback, useEffect, useMemo, useState } from "react" +import { getAddress } from "viem" import { CopyAddressWizardInputs } from "./types" import { useCopyAddressModal } from "./useCopyAddressModal" @@ -91,7 +91,7 @@ const getNextRoute = (inputs: CopyAddressWizardInputs): CopyAddressWizardPage => const getFormattedAddress = (address?: Address, chain?: Chain) => { if (address) { try { - if (isEthereumAddress(address)) return ethers.utils.getAddress(address) // enforces format for checksum + if (isEthereumAddress(address)) return getAddress(address) // enforces format for checksum return convertAddress(address, chain?.prefix ?? null) } catch (err) { diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormEip1559.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormEip1559.tsx index 1a4f2b9d7a..8467a3ee12 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormEip1559.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormEip1559.tsx @@ -1,26 +1,28 @@ +import { getHumanReadableErrorMessage } from "@core/domains/ethereum/errors" import { getMaxFeePerGas } from "@core/domains/ethereum/helpers" -import { EthGasSettingsEip1559 } from "@core/domains/ethereum/types" +import { EthGasSettingsEip1559, EvmNetworkId } from "@core/domains/ethereum/types" import { EthTransactionDetails, GasSettingsByPriorityEip1559 } from "@core/domains/signing/types" import { log } from "@core/log" import { yupResolver } from "@hookform/resolvers/yup" import { notify } from "@talisman/components/Notifications" import { WithTooltip } from "@talisman/components/Tooltip" +import { TokenId } from "@talismn/chaindata-provider" import { ArrowRightIcon, InfoIcon, LoaderIcon } from "@talismn/icons" import { formatDecimals } from "@talismn/util" import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { BigNumber, ethers } from "ethers" import { FC, FormEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { useDebounce } from "react-use" -import { IconButton } from "talisman-ui" +import { IconButton, Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" import { Button, FormFieldContainer, FormFieldInputText } from "talisman-ui" +import { TransactionRequest, formatGwei, parseGwei } from "viem" import * as yup from "yup" import { NetworkUsage } from "../NetworkUsage" -import { useEthereumProvider } from "../useEthereumProvider" import { useIsValidEthTransaction } from "../useIsValidEthTransaction" +import { usePublicClient } from "../usePublicClient" import { Indicator, MessageRow } from "./common" const INPUT_PROPS = { @@ -34,12 +36,12 @@ type FormData = { } const gasSettingsFromFormData = (formData: FormData): EthGasSettingsEip1559 => ({ - type: 2, - maxFeePerGas: BigNumber.from( + type: "eip1559", + maxFeePerGas: BigInt( Math.round((formData.maxBaseFee + formData.maxPriorityFee) * Math.pow(10, 9)) ), - maxPriorityFeePerGas: BigNumber.from(Math.round(formData.maxPriorityFee * Math.pow(10, 9))), - gasLimit: BigNumber.from(formData.gasLimit), + maxPriorityFeePerGas: BigInt(Math.round(formData.maxPriorityFee * Math.pow(10, 9))), + gas: BigInt(formData.gasLimit), }) const schema = yup @@ -51,7 +53,8 @@ const schema = yup .required() const useIsValidGasSettings = ( - tx: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + tx: TransactionRequest, maxBaseFee: number, maxPriorityFee: number, gasLimit: number @@ -79,7 +82,7 @@ const useIsValidGasSettings = ( [maxBaseFee, maxPriorityFee, gasLimit] ) - const provider = useEthereumProvider(tx.chainId?.toString()) + const publicClient = usePublicClient(evmNetworkId) const txPrepared = useMemo(() => { try { @@ -87,7 +90,7 @@ const useIsValidGasSettings = ( return { ...tx, ...gasSettingsFromFormData(debouncedFormData), - } as ethers.providers.TransactionRequest + } as TransactionRequest } catch (err) { // any bad input throws here, ignore return undefined @@ -95,7 +98,7 @@ const useIsValidGasSettings = ( }, [debouncedFormData, tx]) const { isLoading: isValidationLoading, ...rest } = useIsValidEthTransaction( - provider, + publicClient, txPrepared, "custom" ) @@ -107,8 +110,8 @@ const useIsValidGasSettings = ( } type CustomGasSettingsFormEip1559Props = { - tx: ethers.providers.TransactionRequest - tokenId: string + tx: TransactionRequest + tokenId: TokenId txDetails: EthTransactionDetails gasSettingsByPriority: GasSettingsByPriorityEip1559 onConfirm: (gasSettings: EthGasSettingsEip1559) => void @@ -128,10 +131,10 @@ export const CustomGasSettingsFormEip1559: FC useEffect(() => { genericEvent("open custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettingsByPriority?.type, }) - }, [gasSettingsByPriority?.type, genericEvent, tx.chainId]) + }, [gasSettingsByPriority?.type, genericEvent, txDetails.evmNetworkId]) const { customSettings, highSettings } = useMemo( () => ({ @@ -143,34 +146,29 @@ export const CustomGasSettingsFormEip1559: FC const baseFee = useMemo( () => - formatDecimals( - ethers.utils.formatUnits(txDetails.baseFeePerGas as string, "gwei"), - undefined, - { notation: "standard" } - ), - [txDetails.baseFeePerGas] + txDetails.baseFeePerGas + ? formatDecimals(formatGwei(txDetails.baseFeePerGas), undefined, { + notation: "standard", + }) + : t("N/A"), + [t, txDetails.baseFeePerGas] ) const defaultValues: FormData = useMemo( () => ({ maxBaseFee: Number( formatDecimals( - ethers.utils.formatUnits( - BigNumber.from(customSettings.maxFeePerGas).sub(customSettings.maxPriorityFeePerGas), - "gwei" - ), + formatGwei(customSettings.maxFeePerGas - customSettings.maxPriorityFeePerGas), undefined, { notation: "standard" } ) ), maxPriorityFee: Number( - formatDecimals( - ethers.utils.formatUnits(customSettings.maxPriorityFeePerGas, "gwei"), - undefined, - { notation: "standard" } - ) + formatDecimals(formatGwei(customSettings.maxPriorityFeePerGas), undefined, { + notation: "standard", + }) ), - gasLimit: BigNumber.from(customSettings.gasLimit).toNumber(), + gasLimit: Number(customSettings.gas), }), [customSettings] ) @@ -205,9 +203,7 @@ export const CustomGasSettingsFormEip1559: FC const totalMaxFee = useMemo(() => { try { - return BigNumber.from(ethers.utils.parseUnits(String(maxBaseFee), "gwei")) - .add(ethers.utils.parseUnits(String(maxPriorityFee), "gwei")) - .mul(gasLimit) + return (parseGwei(String(maxBaseFee)) + parseGwei(String(maxPriorityFee))) * BigInt(gasLimit) } catch (err) { return null } @@ -223,31 +219,25 @@ export const CustomGasSettingsFormEip1559: FC else if ( maxBaseFee && txDetails.baseFeePerGas && - BigNumber.from(ethers.utils.parseUnits(String(maxBaseFee), "gwei")).lt( - getMaxFeePerGas(txDetails.baseFeePerGas, 0, 20, false) - ) + parseGwei(String(maxBaseFee)) < getMaxFeePerGas(txDetails.baseFeePerGas, 0n, 20, false) ) warningFee = t("Max Base Fee seems too low for current network conditions") // if higher than highest possible fee after 20 blocks else if ( txDetails.baseFeePerGas && maxBaseFee && - BigNumber.from(ethers.utils.parseUnits(String(maxBaseFee), "gwei")).gt( - getMaxFeePerGas(txDetails.baseFeePerGas, 0, 20) - ) + parseGwei(String(maxBaseFee)) > getMaxFeePerGas(txDetails.baseFeePerGas, 0n, 20) ) warningFee = t("Max Base Fee seems higher than required") else if ( maxPriorityFee && - BigNumber.from(ethers.utils.parseUnits(String(maxPriorityFee), "gwei")).gt( - BigNumber.from(2).mul(highSettings?.maxPriorityFeePerGas) - ) + parseGwei(String(maxPriorityFee)) > 2n * highSettings.maxPriorityFeePerGas ) warningFee = t("Max Priority Fee seems higher than required") if (errors.gasLimit?.type === "min") errorGasLimit = t("Gas Limit minimum value is 21000") else if (errors.gasLimit) errorGasLimit = t("Gas Limit is invalid") - else if (BigNumber.from(txDetails.estimatedGas).gt(gasLimit)) + else if (txDetails.estimatedGas > gasLimit) errorGasLimit = t("Gas Limit too low, transaction likely to fail") return { @@ -273,7 +263,7 @@ export const CustomGasSettingsFormEip1559: FC const gasSettings = gasSettingsFromFormData(formData) genericEvent("set custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettings.type, }) @@ -283,11 +273,14 @@ export const CustomGasSettingsFormEip1559: FC notify({ title: "Error", subtitle: (err as Error).message, type: "error" }) } }, - [genericEvent, onConfirm, tx.chainId] + [genericEvent, onConfirm, txDetails.evmNetworkId] ) - const { isValid: isGasSettingsValid, isLoading: isLoadingGasSettingsValid } = - useIsValidGasSettings(tx, maxBaseFee, maxPriorityFee, gasLimit) + const { + isValid: isGasSettingsValid, + isLoading: isLoadingGasSettingsValid, + error: gasSettingsError, + } = useIsValidGasSettings(txDetails.evmNetworkId, tx, maxBaseFee, maxPriorityFee, gasLimit) const showMaxFeeTotal = isFormValid && isGasSettingsValid && !isLoadingGasSettingsValid @@ -414,7 +407,14 @@ export const CustomGasSettingsFormEip1559: FC ) : isLoadingGasSettingsValid ? ( ) : ( - {t("Invalid settings")} + + + {t("Invalid transaction")} + + {!!gasSettingsError && ( + {getHumanReadableErrorMessage(gasSettingsError)} + )} + )}
diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx index c4fd1cc04f..b820c54168 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx @@ -1,4 +1,5 @@ -import { EthGasSettingsLegacy } from "@core/domains/ethereum/types" +import { getHumanReadableErrorMessage } from "@core/domains/ethereum/errors" +import { EthGasSettingsLegacy, EvmNetworkId } from "@core/domains/ethereum/types" import { EthTransactionDetails, GasSettingsByPriorityLegacy } from "@core/domains/signing/types" import { log } from "@core/log" import { yupResolver } from "@hookform/resolvers/yup" @@ -8,17 +9,17 @@ import { ArrowRightIcon, InfoIcon, LoaderIcon } from "@talismn/icons" import { formatDecimals } from "@talismn/util" import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { BigNumber, ethers } from "ethers" import { FC, FormEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { useDebounce } from "react-use" -import { IconButton } from "talisman-ui" +import { IconButton, Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" import { Button, FormFieldContainer, FormFieldInputText } from "talisman-ui" +import { TransactionRequest, formatGwei, parseGwei } from "viem" import * as yup from "yup" -import { useEthereumProvider } from "../useEthereumProvider" import { useIsValidEthTransaction } from "../useIsValidEthTransaction" +import { usePublicClient } from "../usePublicClient" import { Indicator, MessageRow } from "./common" const INPUT_PROPS = { @@ -31,9 +32,9 @@ type FormData = { } const gasSettingsFromFormData = (formData: FormData): EthGasSettingsLegacy => ({ - type: 0, - gasPrice: BigNumber.from(Math.round(formData.gasPrice * Math.pow(10, 9))), - gasLimit: BigNumber.from(formData.gasLimit), + type: "legacy", + gasPrice: BigInt(Math.round(formData.gasPrice * Math.pow(10, 9))), + gas: BigInt(formData.gasLimit), }) const schema = yup @@ -44,7 +45,8 @@ const schema = yup .required() const useIsValidGasSettings = ( - tx: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + tx: TransactionRequest, gasPrice: number, gasLimit: number ) => { @@ -70,7 +72,7 @@ const useIsValidGasSettings = ( [gasPrice, gasLimit] ) - const provider = useEthereumProvider(tx.chainId?.toString()) + const provider = usePublicClient(evmNetworkId) const txPrepared = useMemo(() => { try { @@ -78,7 +80,7 @@ const useIsValidGasSettings = ( return { ...tx, ...gasSettingsFromFormData(debouncedFormData), - } as ethers.providers.TransactionRequest + } as TransactionRequest } catch (err) { // any bad input throws here, ignore return undefined @@ -99,7 +101,7 @@ const useIsValidGasSettings = ( type CustomGasSettingsFormLegacyProps = { networkUsage?: number - tx: ethers.providers.TransactionRequest + tx: TransactionRequest tokenId: string txDetails: EthTransactionDetails gasSettingsByPriority: GasSettingsByPriorityLegacy @@ -121,16 +123,16 @@ export const CustomGasSettingsFormLegacy: FC = useEffect(() => { genericEvent("open custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettingsByPriority?.type, }) - }, [gasSettingsByPriority?.type, genericEvent, tx.chainId]) + }, [gasSettingsByPriority?.type, genericEvent, txDetails.evmNetworkId]) const customSettings = gasSettingsByPriority.custom const networkGasPrice = useMemo( () => - formatDecimals(ethers.utils.formatUnits(txDetails.gasPrice as string, "gwei"), undefined, { + formatDecimals(formatGwei(txDetails.gasPrice), undefined, { notation: "standard", }), [txDetails.gasPrice] @@ -139,13 +141,13 @@ export const CustomGasSettingsFormLegacy: FC = const defaultValues: FormData = useMemo( () => ({ gasPrice: Number( - formatDecimals(ethers.utils.formatUnits(customSettings.gasPrice, "gwei"), undefined, { + formatDecimals(formatGwei(customSettings.gasPrice), undefined, { notation: "standard", }) ), - gasLimit: BigNumber.from(customSettings.gasLimit).toNumber(), + gasLimit: Number(customSettings.gas), }), - [customSettings.gasLimit, customSettings.gasPrice] + [customSettings.gas, customSettings.gasPrice] ) const { @@ -174,7 +176,7 @@ export const CustomGasSettingsFormLegacy: FC = const totalMaxFee = useMemo(() => { try { - return BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).mul(gasLimit) + return parseGwei(String(gasPrice)) * BigInt(gasLimit) } catch (err) { return null } @@ -185,22 +187,14 @@ export const CustomGasSettingsFormLegacy: FC = let errorGasLimit = "" if (errors.gasPrice) warningFee = t("Gas price is invalid") - else if ( - gasPrice && - BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).lt(txDetails.gasPrice) - ) + else if (gasPrice && parseGwei(String(gasPrice)) < txDetails.gasPrice) warningFee = t("Gas price seems too low for current network conditions") - else if ( - gasPrice && - BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).gt( - BigNumber.from(txDetails.gasPrice).mul(2) - ) - ) + else if (gasPrice && parseGwei(String(gasPrice)) > txDetails.gasPrice * 2n) warningFee = t("Gas price seems higher than required") if (errors.gasLimit?.type === "min") errorGasLimit = t("Gas Limit minimum value is 21000") else if (errors.gasLimit) errorGasLimit = t("Gas Limit is invalid") - else if (BigNumber.from(txDetails.estimatedGas).gt(gasLimit)) + else if (txDetails.estimatedGas > BigInt(gasLimit)) errorGasLimit = t("Gas Limit too low, transaction likely to fail") return { @@ -223,7 +217,7 @@ export const CustomGasSettingsFormLegacy: FC = const gasSettings = gasSettingsFromFormData(formData) genericEvent("set custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettings.type, }) @@ -233,11 +227,14 @@ export const CustomGasSettingsFormLegacy: FC = notify({ title: "Error", subtitle: (err as Error).message, type: "error" }) } }, - [genericEvent, onConfirm, tx.chainId] + [genericEvent, onConfirm, txDetails.evmNetworkId] ) - const { isValid: isGasSettingsValid, isLoading: isLoadingGasSettingsValid } = - useIsValidGasSettings(tx, gasPrice, gasLimit) + const { + isValid: isGasSettingsValid, + isLoading: isLoadingGasSettingsValid, + error: gasSettingsError, + } = useIsValidGasSettings(txDetails.evmNetworkId, tx, gasPrice, gasLimit) const showMaxFeeTotal = isFormValid && isGasSettingsValid && !isLoadingGasSettingsValid @@ -344,7 +341,14 @@ export const CustomGasSettingsFormLegacy: FC = ) : isLoadingGasSettingsValid ? ( ) : ( - {t("Invalid settings")} + + + {t("Invalid transaction")} + + {!!gasSettingsError && ( + {getHumanReadableErrorMessage(gasSettingsError)} + )} + )} diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx index 825f6c552b..aedea16437 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx @@ -8,9 +8,9 @@ import { TokenId } from "@core/domains/tokens/types" import { useOpenClose } from "@talisman/hooks/useOpenClose" import { classNames } from "@talismn/util" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { ethers } from "ethers" import { FC, useCallback, useEffect, useState } from "react" import { Drawer, PillButton } from "talisman-ui" +import { TransactionRequest } from "viem" import { useFeePriorityOptionsUI } from "./common" import { CustomGasSettingsFormEip1559 } from "./CustomGasSettingsFormEip1559" @@ -28,7 +28,7 @@ const OpenFeeSelectTracker = () => { } type EthFeeSelectProps = { - tx: ethers.providers.TransactionRequest + tx: TransactionRequest tokenId: TokenId disabled?: boolean txDetails: EthTransactionDetails diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx index 49cfa02a59..d69e624a23 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx @@ -7,11 +7,11 @@ import { GasSettingsByPriority, } from "@core/domains/signing/types" import { BalanceFormatter } from "@talismn/balances" +import { TokenId } from "@talismn/chaindata-provider" import { ChevronRightIcon } from "@talismn/icons" import { classNames } from "@talismn/util" import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import useToken from "@ui/hooks/useToken" -import { BigNumber } from "ethers" import { FC, useCallback, useMemo } from "react" import { Trans, useTranslation } from "react-i18next" import { Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" @@ -34,9 +34,9 @@ const getGasSettings = ( } const Eip1559FeeTooltip: FC<{ - estimatedFee: BigNumber - maxFee: BigNumber - tokenId: string + estimatedFee: bigint + maxFee: bigint + tokenId: TokenId }> = ({ estimatedFee, maxFee, tokenId }) => { const { t } = useTranslation("request") const token = useToken(tokenId) @@ -100,8 +100,19 @@ const PriorityOption = ({ }: PriorityOptionProps) => { const { estimatedFee, maxFee } = useMemo(() => { const gasSettings = getGasSettings(gasSettingsByPriority, priority) - return getTotalFeesFromGasSettings(gasSettings, txDetails.estimatedGas, txDetails.baseFeePerGas) - }, [gasSettingsByPriority, priority, txDetails.baseFeePerGas, txDetails.estimatedGas]) + return getTotalFeesFromGasSettings( + gasSettings, + txDetails.estimatedGas, + txDetails.baseFeePerGas, + txDetails.estimatedL1DataFee ?? 0n + ) + }, [ + gasSettingsByPriority, + priority, + txDetails.baseFeePerGas, + txDetails.estimatedGas, + txDetails.estimatedL1DataFee, + ]) const options = useFeePriorityOptionsUI() diff --git a/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx b/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx index dd93999a94..d59129d091 100644 --- a/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx +++ b/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx @@ -1,8 +1,8 @@ -import { AddEthereumChainParameter } from "@core/domains/ethereum/types" import { useOpenClose } from "@talisman/hooks/useOpenClose" import { FC, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { Button, Drawer, PillButton } from "talisman-ui" +import { AddEthereumChainParameter } from "viem" import { ViewDetailsField } from "../Sign/ViewDetails/ViewDetailsField" @@ -25,11 +25,11 @@ export const NetworksDetailsButton: FC<{ const { name, rpcs, chainId, tokenSymbol, blockExplorers } = useMemo(() => { return { - name: network?.chainName || "N/A", - rpcs: network?.rpcUrls?.join("\n") || "N/A", - chainId: tryParseIntFromHex(network?.chainId), - tokenSymbol: network?.nativeCurrency?.symbol || "N/A", - blockExplorers: network?.blockExplorerUrls?.join("\n"), + name: network.chainName || "N/A", + rpcs: network.rpcUrls?.join("\n") || "N/A", + chainId: tryParseIntFromHex(network.chainId), + tokenSymbol: network.nativeCurrency?.symbol || "N/A", + blockExplorers: network.blockExplorerUrls?.join("\n"), } }, [network, tryParseIntFromHex]) diff --git a/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts b/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts deleted file mode 100644 index 9ad98d2048..0000000000 --- a/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - ETH_ERROR_EIP1474_INTERNAL_ERROR, - EthProviderRpcError, -} from "@core/injectEth/EthProviderRpcError" -import { EthRequestSignatures, EthRequestTypes } from "@core/injectEth/types" -import { log } from "@core/log" -import { EvmNetworkId } from "@talismn/chaindata-provider" -import { api } from "@ui/api" -import { ethers } from "ethers" - -const ethereumRequest = - (chainId: EvmNetworkId): ethers.providers.JsonRpcFetchFunc => - async (method: string, params?: unknown[]) => { - try { - return await api.ethRequest({ - chainId, - method: method as keyof EthRequestSignatures, - params: params as EthRequestSignatures[EthRequestTypes][0], - }) - } catch (err) { - log.error("[provider.request] error on %s", method, { err }) - - const { message, code, data } = err as EthProviderRpcError - throw new EthProviderRpcError(message, code ?? ETH_ERROR_EIP1474_INTERNAL_ERROR, data) - } - } - -export const getExtensionEthereumProvider = (evmNetworkId: EvmNetworkId) => { - return new ethers.providers.Web3Provider(ethereumRequest(evmNetworkId)) -} diff --git a/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts b/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts index 7881876b16..fe1d583b43 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts @@ -1,21 +1,23 @@ +import { EvmAddress } from "@core/domains/ethereum/types" +import { isEthereumAddress } from "@talismn/util" import { useQuery } from "@tanstack/react-query" -import { ethers } from "ethers" +import { PublicClient } from "viem" export const useEthBalance = ( - provider: ethers.providers.JsonRpcProvider | undefined, - address: string | undefined + publicClient: PublicClient | undefined, + address: EvmAddress | undefined ) => { const { data: balance, ...rest } = useQuery({ - queryKey: ["useEthBalance", provider?.network?.chainId, address], + queryKey: ["useEthBalance", publicClient?.chain?.id, address], queryFn: () => { - if (!provider || !address) return null - return provider.getBalance(address) + if (!publicClient || !isEthereumAddress(address)) return null + return publicClient.getBalance({ address }) }, refetchInterval: 12_000, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - enabled: !!provider?.network && !!address, + enabled: !!publicClient?.chain?.id && isEthereumAddress(address), }) return { balance, ...rest } diff --git a/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts b/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts new file mode 100644 index 0000000000..7276a28240 --- /dev/null +++ b/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts @@ -0,0 +1,58 @@ +import { log } from "@core/log" +import { gasPriceOracleABI, gasPriceOracleAddress } from "@eth-optimism/contracts-ts" +import { useQuery } from "@tanstack/react-query" +import { useMemo } from "react" +import { Hex, PublicClient, TransactionRequest, getContract, serializeTransaction } from "viem" + +const OP_STACK_EVM_NETWORK_IDS = [ + 10, // OP Mainnet, + 420, // OP Goerli + 7777777, // Zora Mainnet + 999, // Zora Goerli + 8453, // Base Mainnet + 84531, // Base Goerli +] + +const getEthL1DataFee = async (publicClient: PublicClient, serializedTx: Hex): Promise => { + try { + const contract = getContract({ + address: gasPriceOracleAddress[420], + abi: gasPriceOracleABI, + publicClient, + }) + return await contract.read.getL1Fee([serializedTx]) + } catch (err) { + log.error(err) + throw new Error("Failed to get L1 data fee", { cause: err }) + } +} + +export const useEthEstimateL1DataFee = ( + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined +) => { + const serialized = useMemo( + () => + tx && publicClient?.chain?.id + ? serializeTransaction({ chainId: publicClient.chain.id, ...tx }) + : null, + [publicClient?.chain?.id, tx] + ) + + return useQuery({ + queryKey: ["useEthEstimateL1DataFee", publicClient?.chain?.id, serialized], + queryFn: () => { + if (!publicClient?.chain?.id || !serialized) return null + + return OP_STACK_EVM_NETWORK_IDS.includes(publicClient.chain.id) + ? getEthL1DataFee(publicClient, serialized) + : 0n + }, + keepPreviousData: true, + refetchInterval: 6_000, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + enabled: !!publicClient?.chain?.id && !!serialized, + }) +} diff --git a/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts index 59bb4ab4da..bb9cfa05bc 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts @@ -1,32 +1,40 @@ -import { rebuildTransactionRequestNumbers } from "@core/domains/ethereum/helpers" -import { ethers } from "ethers" +import { parseTransactionRequest } from "@core/domains/ethereum/helpers" +import { EvmNetworkId } from "@talismn/chaindata-provider" import { useMemo } from "react" +import { TransactionRequest } from "viem" import { TxReplaceType } from "../Transactions" import { useEthTransaction } from "./useEthTransaction" export const useEthReplaceTransaction = ( - tx: ethers.providers.TransactionRequest, + txToReplace: TransactionRequest, + evmNetworkId: EvmNetworkId, type: TxReplaceType, lock?: boolean ) => { - const transaction: ethers.providers.TransactionRequest = useMemo( - () => - rebuildTransactionRequestNumbers({ - chainId: tx.chainId, - from: tx.from, - to: type === "cancel" ? tx.from : tx.to, - value: type === "cancel" ? "0" : tx.value, - data: type === "cancel" ? undefined : tx.data, - nonce: tx.nonce, + const replaceTx = useMemo(() => { + const parsed = parseTransactionRequest(txToReplace) - // pass previous tx gas data - type: tx.type, - gasPrice: tx.gasPrice, - maxPriorityFeePerGas: tx.maxPriorityFeePerGas, - }), - [tx, type] - ) + const newTx: TransactionRequest = + txToReplace.type === "eip1559" + ? { + type: "eip1559", + from: parsed.from, + maxPriorityFeePerGas: parsed.maxPriorityFeePerGas, + } + : { + type: "legacy", + from: parsed.from, + gasPrice: parsed.gasPrice, + } - return useEthTransaction(transaction, lock, true) + newTx.nonce = parsed.nonce + newTx.to = type === "cancel" ? parsed.from : parsed.to + newTx.value = type === "cancel" ? 0n : parsed.value + if (type === "speed-up") newTx.data = parsed.data + + return newTx + }, [txToReplace, type]) + + return useEthTransaction(replaceTx, evmNetworkId, lock, true) } diff --git a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts index 1fd35054b3..b00cc4b913 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts @@ -1,9 +1,10 @@ -import { getEthersErrorLabelFromCode } from "@core/domains/ethereum/errors" +import { getHumanReadableErrorMessage } from "@core/domains/ethereum/errors" import { getGasLimit, getGasSettingsEip1559, getTotalFeesFromGasSettings, prepareTransaction, + serializeTransactionRequest, } from "@core/domains/ethereum/helpers" import { EthGasSettings, @@ -19,27 +20,29 @@ import { GasSettingsByPriority, } from "@core/domains/signing/types" import { ETH_ERROR_EIP1474_METHOD_NOT_FOUND } from "@core/injectEth/EthProviderRpcError" -import { getEthTransactionInfo } from "@core/util/getEthTransactionInfo" +import { decodeEvmTransaction } from "@core/util/decodeEvmTransaction" import { FeeHistoryAnalysis, getFeeHistoryAnalysis } from "@core/util/getFeeHistoryAnalysis" +import { isBigInt } from "@talismn/util" import { useQuery } from "@tanstack/react-query" import { api } from "@ui/api" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" -import { BigNumber, ethers } from "ethers" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { PublicClient, TransactionRequest } from "viem" +import { useEthEstimateL1DataFee } from "./useEthEstimateL1DataFee" import { useIsValidEthTransaction } from "./useIsValidEthTransaction" // gasPrice isn't reliable on polygon & mumbai, see https://github.com/ethers-io/ethers.js/issues/2828#issuecomment-1283014250 const UNRELIABLE_GASPRICE_NETWORK_IDS = [137, 80001] const useNonce = ( - address: string | undefined, + address: `0x${string}` | undefined, evmNetworkId: EvmNetworkId | undefined, forcedValue?: number ) => { const { data, ...rest } = useQuery({ - queryKey: ["nonce", address, evmNetworkId, forcedValue], + queryKey: ["useNonce", address, evmNetworkId, forcedValue], queryFn: () => { if (forcedValue !== undefined) return forcedValue return address && evmNetworkId ? api.ethGetTransactionsCount(address, evmNetworkId) : null @@ -50,21 +53,20 @@ const useNonce = ( } // TODO : could be skipped for networks that we know already support it, but need to keep checking for legacy network in case they upgrade -const useHasEip1559Support = (provider: ethers.providers.JsonRpcProvider | undefined) => { +const useHasEip1559Support = (publicClient: PublicClient | undefined) => { const { data, ...rest } = useQuery({ - queryKey: ["hasEip1559Support", provider?.network?.chainId], + queryKey: ["useHasEip1559Support", publicClient?.chain?.id], queryFn: async () => { - if (!provider) return null + if (!publicClient) return null try { - const [{ baseFeePerGas }] = await Promise.all([ - // check that block has a baseFeePerGas - provider.send("eth_getBlockByNumber", ["latest", false]), - // check that method eth_feeHistory exists. This will throw with code -32601 if it doesn't. - provider.send("eth_feeHistory", [ethers.utils.hexValue(1), "latest", [10]]), - ]) - return baseFeePerGas !== undefined + publicClient.chain?.fees?.defaultPriorityFee + const { + baseFeePerGas: [baseFee], + } = await publicClient.getFeeHistory({ blockCount: 1, rewardPercentiles: [] }) + return baseFee > 0n } catch (err) { + // TODO check that feeHistory returns -32601 when method doesn't exist const error = err as Error & { code?: number } if (error.code === ETH_ERROR_EIP1474_METHOD_NOT_FOUND) return false @@ -75,58 +77,49 @@ const useHasEip1559Support = (provider: ethers.providers.JsonRpcProvider | undef refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - enabled: !!provider, + enabled: !!publicClient, }) return { hasEip1559Support: data ?? undefined, ...rest } } const useBlockFeeData = ( - provider: ethers.providers.JsonRpcProvider | undefined, - tx: ethers.providers.TransactionRequest | undefined, + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined, withFeeOptions: boolean | undefined ) => { const { data, ...rest } = useQuery({ - queryKey: ["block", provider?.network?.chainId, tx, withFeeOptions], + queryKey: [ + "useBlockFeeData", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), + withFeeOptions, + ], queryFn: async () => { - if (!provider || !tx) return null - - // estimate gas without any gas setting, will be used as our gasLimit - // spread to keep only valid properties (exclude the one called "gas" and any undefined ones) - const { chainId, from, to, value = BigNumber.from("0"), data } = tx - const txForEstimate: ethers.providers.TransactionRequest = { - chainId, - from, - to, - value, - data, - } - if (tx.accessList !== undefined) txForEstimate.accessList = tx.accessList - if (tx.customData !== undefined) txForEstimate.customData = tx.customData - if (tx.ccipReadEnabled !== undefined) txForEstimate.ccipReadEnabled = tx.ccipReadEnabled + if (!publicClient?.chain?.id || !tx) return null + + // estimate gas without any gas or nonce setting to prevent rpc from validating these + const { from: account, to, value, data } = tx const [ gasPrice, - { gasLimit: blockGasLimit, baseFeePerGas, gasUsed, number: blockNumber }, + { gasLimit: blockGasLimit, baseFeePerGas, gasUsed }, feeHistoryAnalysis, estimatedGas, ] = await Promise.all([ - provider.getGasPrice(), - provider.getBlock("latest"), - withFeeOptions ? getFeeHistoryAnalysis(provider) : undefined, + publicClient.getGasPrice(), + publicClient.getBlock(), + withFeeOptions ? getFeeHistoryAnalysis(publicClient) : undefined, // estimate gas may change over time for contract calls, so we need to refresh it every time we prepare the tx to prevent an invalid transaction - provider.estimateGas(txForEstimate), + publicClient.estimateGas({ account, to, value, data }), ]) - if ( - feeHistoryAnalysis && - !UNRELIABLE_GASPRICE_NETWORK_IDS.includes(provider.network.chainId) - ) { + if (feeHistoryAnalysis && !UNRELIABLE_GASPRICE_NETWORK_IDS.includes(publicClient.chain.id)) { // minimum maxPriorityPerGas value required to be considered valid into next block is equal to `gasPrice - baseFee` - let minimumMaxPriorityFeePerGas = gasPrice.sub(baseFeePerGas ?? 0) - if (minimumMaxPriorityFeePerGas.lt(0)) { + let minimumMaxPriorityFeePerGas = gasPrice - feeHistoryAnalysis.nextBaseFee + if (minimumMaxPriorityFeePerGas < 0n) { // on a busy network, when there is a sudden lowering of amount of transactions, it can happen that baseFeePerGas is higher than gPrice - minimumMaxPriorityFeePerGas = BigNumber.from("0") + minimumMaxPriorityFeePerGas = 0n } // if feeHistory is invalid (network is inactive), use minimumMaxPriorityFeePerGas for all options. @@ -140,29 +133,24 @@ const useBlockFeeData = ( feeHistoryAnalysis.avgGasUsedRatio !== null && feeHistoryAnalysis.avgGasUsedRatio < 0.8 ) - feeHistoryAnalysis.maxPriorityPerGasOptions.low = minimumMaxPriorityFeePerGas.lt( - feeHistoryAnalysis.maxPriorityPerGasOptions.low - ) - ? minimumMaxPriorityFeePerGas - : feeHistoryAnalysis.maxPriorityPerGasOptions.low + feeHistoryAnalysis.maxPriorityPerGasOptions.low = + minimumMaxPriorityFeePerGas < feeHistoryAnalysis.maxPriorityPerGasOptions.low + ? minimumMaxPriorityFeePerGas + : feeHistoryAnalysis.maxPriorityPerGasOptions.low } - const networkUsage = - !gasUsed || !blockGasLimit - ? undefined - : gasUsed.mul(100).div(blockGasLimit).toNumber() / 100 + const networkUsage = Number((gasUsed * 100n) / blockGasLimit) / 100 return { estimatedGas, gasPrice, - baseFeePerGas, + baseFeePerGas: feeHistoryAnalysis?.nextBaseFee ?? baseFeePerGas, blockGasLimit, networkUsage, feeHistoryAnalysis, - blockNumber, } }, - enabled: !!tx && !!provider && withFeeOptions !== undefined, + enabled: !!tx && !!publicClient && withFeeOptions !== undefined, refetchInterval: 6_000, retry: false, }) @@ -183,50 +171,55 @@ const useBlockFeeData = ( } } -const useTransactionInfo = ( - provider: ethers.providers.JsonRpcProvider | undefined, - tx: ethers.providers.TransactionRequest | undefined +const useDecodeEvmTransaction = ( + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined ) => { const { data, ...rest } = useQuery({ // check tx as boolean as it's not pure - queryKey: ["transactionInfo", provider?.network?.chainId, tx], + queryKey: [ + "useDecodeEvmTransaction", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), + ], queryFn: async () => { - if (!provider || !tx) return null - return await getEthTransactionInfo(provider, tx) + if (!publicClient || !tx) return null + return await decodeEvmTransaction(publicClient, tx) }, refetchInterval: false, refetchOnWindowFocus: false, // prevents error to be cleared when window gets focus - enabled: !!provider && !!tx, + enabled: !!publicClient && !!tx, }) - return { transactionInfo: data ?? undefined, ...rest } + return { decodedTx: data, ...rest } } const getEthGasSettingsFromTransaction = ( - tx: ethers.providers.TransactionRequest | undefined, + tx: TransactionRequest | undefined, hasEip1559Support: boolean | undefined, - estimatedGas: BigNumber | undefined, - blockGasLimit: BigNumber | undefined, + estimatedGas: bigint | undefined, + blockGasLimit: bigint | undefined, isContractCall: boolean | undefined = true // default to worse scenario ) => { - if (!tx || hasEip1559Support === undefined || !blockGasLimit || !estimatedGas) return undefined + if (!tx || hasEip1559Support === undefined || !isBigInt(blockGasLimit) || !isBigInt(estimatedGas)) + return undefined const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = tx - const gasLimit = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) + const gas = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) - if (hasEip1559Support && gasLimit && maxFeePerGas && maxPriorityFeePerGas) { + if (hasEip1559Support && gas && maxFeePerGas && maxPriorityFeePerGas) { return { - type: 2, - gasLimit, + type: "eip1559", + gas, maxFeePerGas, maxPriorityFeePerGas, } as EthGasSettingsEip1559 } - if (!hasEip1559Support && gasLimit && gasPrice) { + if (!hasEip1559Support && gas && gasPrice) { return { - type: 0, - gasLimit, + type: "legacy", + gas, gasPrice, } as EthGasSettingsLegacy } @@ -247,22 +240,29 @@ const useGasSettings = ({ isContractCall, }: { hasEip1559Support: boolean | undefined - baseFeePerGas: BigNumber | null | undefined - estimatedGas: BigNumber | null | undefined - gasPrice: BigNumber | null | undefined - blockGasLimit: BigNumber | null | undefined + baseFeePerGas: bigint | null | undefined + estimatedGas: bigint | null | undefined + gasPrice: bigint | null | undefined + blockGasLimit: bigint | null | undefined feeHistoryAnalysis: FeeHistoryAnalysis | null | undefined priority: EthPriorityOptionName | undefined - tx: ethers.providers.TransactionRequest | undefined + tx: TransactionRequest | undefined isReplacement: boolean | undefined isContractCall: boolean | undefined }) => { const [customSettings, setCustomSettings] = useState() const gasSettingsByPriority: GasSettingsByPriority | undefined = useMemo(() => { - if (hasEip1559Support === undefined || !estimatedGas || !gasPrice || !blockGasLimit || !tx) + if ( + hasEip1559Support === undefined || + !isBigInt(estimatedGas) || + !isBigInt(gasPrice) || + !isBigInt(blockGasLimit) || + !tx + ) return undefined - const gasLimit = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) + + const gas = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) const suggestedSettings = getEthGasSettingsFromTransaction( tx, hasEip1559Support, @@ -272,31 +272,29 @@ const useGasSettings = ({ ) if (hasEip1559Support) { - if (!feeHistoryAnalysis || !baseFeePerGas) return undefined + if (!feeHistoryAnalysis || !isBigInt(baseFeePerGas)) return undefined const mapMaxPriority = feeHistoryAnalysis.maxPriorityPerGasOptions - if (isReplacement) { + if (isReplacement && tx.maxPriorityFeePerGas !== undefined) { // for replacement transactions, ensure that maxPriorityFeePerGas is at least 10% higher than original tx - const minimumMaxPriorityFeePerGas = ethers.BigNumber.from(tx.maxPriorityFeePerGas) - .mul(110) - .div(100) - if (mapMaxPriority.low.lt(minimumMaxPriorityFeePerGas)) + const minimumMaxPriorityFeePerGas = (tx.maxPriorityFeePerGas * 110n) / 100n + if (mapMaxPriority.low < minimumMaxPriorityFeePerGas) mapMaxPriority.low = minimumMaxPriorityFeePerGas - if (mapMaxPriority.medium.lt(minimumMaxPriorityFeePerGas)) + if (mapMaxPriority.medium < minimumMaxPriorityFeePerGas) mapMaxPriority.medium = minimumMaxPriorityFeePerGas - if (mapMaxPriority.high.lt(minimumMaxPriorityFeePerGas)) + if (mapMaxPriority.high < minimumMaxPriorityFeePerGas) mapMaxPriority.high = minimumMaxPriorityFeePerGas } - const low = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.low, gasLimit) - const medium = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.medium, gasLimit) - const high = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.high, gasLimit) + const low = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.low, gas) + const medium = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.medium, gas) + const high = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.high, gas) const custom: EthGasSettingsEip1559 = - customSettings?.type === 2 + customSettings?.type === "eip1559" ? customSettings - : suggestedSettings?.type === 2 + : suggestedSettings?.type === "eip1559" ? suggestedSettings : { ...low, @@ -314,24 +312,23 @@ const useGasSettings = ({ } const recommendedSettings: EthGasSettingsLegacy = { - type: 0, - gasLimit, + type: "legacy", + gas, // in some cases (ex: claiming bridged tokens on Polygon zkEVM), // 0 is provided by the dapp and has to be used for the tx to succeed - gasPrice: tx.gasPrice && BigNumber.from(tx.gasPrice).isZero() ? BigNumber.from(0) : gasPrice, + gasPrice: tx.gasPrice === 0n ? 0n : gasPrice, } - if (isReplacement) { + if (isReplacement && tx.gasPrice !== undefined) { // for replacement transactions, ensure that maxPriorityFeePerGas is at least 10% higher than original tx - const minimumGasPrice = ethers.BigNumber.from(tx.gasPrice).mul(110).div(100) - if (ethers.BigNumber.from(gasPrice).lt(minimumGasPrice)) - recommendedSettings.gasPrice = minimumGasPrice + const minimumGasPrice = (tx.gasPrice * 110n) / 100n + if (gasPrice < minimumGasPrice) recommendedSettings.gasPrice = minimumGasPrice } const custom: EthGasSettingsLegacy = - customSettings?.type === 0 + customSettings?.type === "legacy" ? customSettings - : suggestedSettings?.type === 0 + : suggestedSettings?.type === "legacy" ? suggestedSettings : recommendedSettings @@ -369,19 +366,19 @@ const useGasSettings = ({ } export const useEthTransaction = ( - tx: ethers.providers.TransactionRequest | undefined, + tx: TransactionRequest | undefined, + evmNetworkId: EvmNetworkId | undefined, lockTransaction = false, isReplacement = false ) => { - const provider = useEthereumProvider(tx?.chainId?.toString()) - const { transactionInfo, error: errorTransactionInfo } = useTransactionInfo(provider, tx) - const { hasEip1559Support, error: errorEip1559Support } = useHasEip1559Support(provider) + const publicClient = usePublicClient(evmNetworkId) + const { decodedTx, isLoading: isDecoding } = useDecodeEvmTransaction(publicClient, tx) + const { hasEip1559Support, error: errorEip1559Support } = useHasEip1559Support(publicClient) const { nonce, error: nonceError } = useNonce( - tx?.from, - tx?.chainId?.toString(), - isReplacement && tx?.nonce ? BigNumber.from(tx.nonce).toNumber() : undefined + tx?.from as `0x${string}` | undefined, + evmNetworkId, + isReplacement && tx?.nonce ? tx.nonce : undefined ) - const { gasPrice, networkUsage, @@ -390,7 +387,7 @@ export const useEthTransaction = ( feeHistoryAnalysis, estimatedGas, error: blockFeeDataError, - } = useBlockFeeData(provider, tx, hasEip1559Support) + } = useBlockFeeData(publicClient, tx, hasEip1559Support) const [priority, setPriority] = useState() @@ -398,7 +395,7 @@ export const useEthTransaction = ( // ex: from send funds when switching from BSC (legacy) to mainnet (eip1559) useEffect(() => { setPriority(undefined) - }, [tx?.chainId]) + }, [evmNetworkId]) // set default priority based on EIP1559 support useEffect(() => { @@ -407,7 +404,7 @@ export const useEthTransaction = ( }, [hasEip1559Support, isReplacement, priority]) const { gasSettings, setCustomSettings, gasSettingsByPriority } = useGasSettings({ - tx, + tx: tx, priority, hasEip1559Support, baseFeePerGas, @@ -416,13 +413,13 @@ export const useEthTransaction = ( blockGasLimit, feeHistoryAnalysis, isReplacement, - isContractCall: transactionInfo?.isContractCall, + isContractCall: decodedTx?.isContractCall, }) const liveUpdatingTransaction = useMemo(() => { - if (!provider || !tx || !gasSettings || nonce === undefined) return undefined + if (!publicClient || !tx || !gasSettings || nonce === undefined) return undefined return prepareTransaction(tx, gasSettings, nonce) - }, [gasSettings, provider, tx, nonce]) + }, [gasSettings, publicClient, tx, nonce]) // transaction may be locked once sent to hardware device for signing const [transaction, setTransaction] = useState(liveUpdatingTransaction) @@ -431,61 +428,87 @@ export const useEthTransaction = ( if (!lockTransaction) setTransaction(liveUpdatingTransaction) }, [liveUpdatingTransaction, lockTransaction]) + const { data: estimatedL1DataFee, error: l1FeeError } = useEthEstimateL1DataFee( + publicClient, + transaction + ) + // TODO replace this wierd object name with something else... gasInfo ? - const txDetails: EthTransactionDetails | undefined = useMemo(() => { - if (!gasPrice || !estimatedGas || !transaction || !gasSettings) return undefined + const txDetails = useMemo(() => { + if ( + !evmNetworkId || + !isBigInt(gasPrice) || + !isBigInt(estimatedGas) || + !isBigInt(estimatedL1DataFee) || + !transaction || + !gasSettings + ) + return undefined - // if type 2 transaction, wait for baseFee to be available - if (gasSettings?.type === 2 && !baseFeePerGas) return undefined + // if eip1559 transaction, wait for baseFee to be available + if (gasSettings?.type === "eip1559" && !isBigInt(baseFeePerGas)) return undefined const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( gasSettings, estimatedGas, - baseFeePerGas + baseFeePerGas, + estimatedL1DataFee ) return { + evmNetworkId, estimatedGas, gasPrice, baseFeePerGas, estimatedFee, + estimatedL1DataFee, maxFee, baseFeeTrend: feeHistoryAnalysis?.baseFeeTrend, } - }, [baseFeePerGas, estimatedGas, feeHistoryAnalysis, gasPrice, gasSettings, transaction]) + }, [ + baseFeePerGas, + estimatedGas, + evmNetworkId, + feeHistoryAnalysis?.baseFeeTrend, + gasPrice, + gasSettings, + estimatedL1DataFee, + transaction, + ]) // use staleIsValid to prevent disabling approve button each time there is a new block (triggers gas check) - const { isValid, error: isValidError } = useIsValidEthTransaction(provider, transaction, priority) + const { isValid, error: isValidError } = useIsValidEthTransaction( + publicClient, + transaction, + priority + ) const { t } = useTranslation("request") const { error, errorDetails } = useMemo(() => { const anyError = (errorEip1559Support ?? nonceError ?? blockFeeDataError ?? - errorTransactionInfo ?? - isValidError) as Error & { code?: string; error?: Error } - - const userFriendlyError = getEthersErrorLabelFromCode(anyError?.code) + l1FeeError ?? + isValidError) as Error - // if ethers.js error, display underlying error that shows the RPC's error message - const errorToDisplay = anyError?.error ?? anyError + const userFriendlyError = getHumanReadableErrorMessage(anyError) - if (errorToDisplay) + if (anyError) return { error: userFriendlyError ?? t("Failed to prepare transaction"), - errorDetails: errorToDisplay.message, + errorDetails: anyError.message, } return { error: undefined, errorDetails: undefined } - }, [blockFeeDataError, isValidError, errorEip1559Support, errorTransactionInfo, nonceError, t]) + }, [errorEip1559Support, nonceError, blockFeeDataError, l1FeeError, isValidError, t]) const isLoading = useMemo( - () => tx && !transactionInfo && !txDetails && !error, - [tx, transactionInfo, txDetails, error] + () => tx && isDecoding && !txDetails && !error, + [tx, isDecoding, txDetails, error] ) return { - transactionInfo, + decodedTx, transaction, txDetails, gasSettings, diff --git a/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts b/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts deleted file mode 100644 index 778dfb1358..0000000000 --- a/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { EvmNetworkId } from "@core/domains/ethereum/types" -import { getExtensionEthereumProvider } from "@ui/domains/Ethereum/getExtensionEthereumProvider" -import { ethers } from "ethers" -import { useMemo } from "react" - -export const useEthereumProvider = ( - evmNetworkId?: EvmNetworkId -): ethers.providers.JsonRpcProvider | undefined => { - const provider = useMemo(() => { - if (!evmNetworkId) return undefined - return getExtensionEthereumProvider(evmNetworkId) - }, [evmNetworkId]) - - return provider -} diff --git a/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts index 9db9cb4e0e..95bccd2591 100644 --- a/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts @@ -1,52 +1,55 @@ -import { getMaxTransactionCost } from "@core/domains/ethereum/helpers" +import { getMaxTransactionCost, serializeTransactionRequest } from "@core/domains/ethereum/helpers" import { EthPriorityOptionName } from "@core/domains/signing/types" import { useQuery } from "@tanstack/react-query" import { useAccountByAddress } from "@ui/hooks/useAccountByAddress" -import { ethers } from "ethers" import { useTranslation } from "react-i18next" +import { PublicClient, TransactionRequest } from "viem" import { useEthBalance } from "./useEthBalance" export const useIsValidEthTransaction = ( - provider: ethers.providers.JsonRpcProvider | undefined, - transaction: ethers.providers.TransactionRequest | undefined, + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined, priority: EthPriorityOptionName | undefined ) => { const { t } = useTranslation("request") - const account = useAccountByAddress(transaction?.from) - const { balance } = useEthBalance(provider, transaction?.from) + const account = useAccountByAddress(tx?.from) + const { balance } = useEthBalance(publicClient, tx?.from) const { data, error, isLoading } = useQuery({ queryKey: [ - "useCheckTransaction", - provider?.network?.chainId, - transaction, + "useIsValidEthTransaction", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), account?.address, priority, ], queryFn: async () => { - if (!provider || !transaction || !account || balance === undefined) return null + if (!publicClient || !tx || !account || balance === undefined) return null if (account.origin === "WATCHED") throw new Error(t("Cannot sign transactions with a watched account")) // balance checks - const value = ethers.BigNumber.from(transaction.value ?? 0) - const maxTransactionCost = getMaxTransactionCost(transaction) - if (!balance || value.gt(balance)) throw new Error(t("Insufficient balance")) - if (!balance || maxTransactionCost.gt(balance)) + const value = tx.value ?? 0n + const maxTransactionCost = getMaxTransactionCost(tx) + if (!balance || value > balance) throw new Error(t("Insufficient balance")) + if (!balance || maxTransactionCost > balance) throw new Error(t("Insufficient balance to pay for fee")) // dry runs the transaction, if it fails we can't know for sure what the issue really is // there should be helpful message in the error though. - const estimatedGas = await provider.estimateGas(transaction) - return estimatedGas?.gt(0) + const estimatedGas = await publicClient.estimateGas({ + account: tx.from, + ...tx, + }) + return estimatedGas > 0n }, refetchInterval: false, refetchOnWindowFocus: false, retry: 0, keepPreviousData: true, - enabled: !!provider && !!transaction && !!account && balance !== undefined, + enabled: !!publicClient && !!tx && !!account && balance !== undefined, }) return { isValid: !!data, error, isLoading } diff --git a/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts b/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts new file mode 100644 index 0000000000..3e183bc063 --- /dev/null +++ b/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts @@ -0,0 +1,67 @@ +import { EvmNetwork, EvmNetworkId } from "@core/domains/ethereum/types" +import { log } from "@core/log" +import { EvmNativeToken } from "@talismn/balances" +import { api } from "@ui/api" +import { useEvmNetwork } from "@ui/hooks/useEvmNetwork" +import useToken from "@ui/hooks/useToken" +import { useMemo } from "react" +import { PublicClient, createPublicClient, custom } from "viem" + +type ViemRequest = (arg: { method: string; params?: unknown[] }) => Promise + +const viemRequest = + (chainId: EvmNetworkId): ViemRequest => + async ({ method, params }) => { + try { + return await api.ethRequest({ chainId, method, params }) + } catch (err) { + log.error("publicClient request error : %s", method, { err }) + throw err + } + } + +export const getExtensionPublicClient = ( + evmNetwork: EvmNetwork, + nativeToken: EvmNativeToken +): PublicClient => { + const name = evmNetwork.name ?? `EVM Chain ${evmNetwork.id}` + + return createPublicClient({ + chain: { + id: Number(evmNetwork.id), + name: name, + network: name, + nativeCurrency: { + symbol: nativeToken.symbol, + decimals: nativeToken.decimals, + name: nativeToken.symbol, + }, + rpcUrls: { + // rpcs are a typescript requirement, but won't be used by the custom transport + public: { http: [] }, + default: { http: [] }, + }, + }, + transport: custom( + { + request: viemRequest(evmNetwork.id), + }, + { + // backend will retry, at it's own transport level + retryCount: 0, + } + ), + }) +} + +export const usePublicClient = (evmNetworkId?: EvmNetworkId): PublicClient | undefined => { + const evmNetwork = useEvmNetwork(evmNetworkId) + const nativeToken = useToken(evmNetwork?.nativeToken?.id) + + const publicClient = useMemo(() => { + if (!evmNetwork || nativeToken?.type !== "evm-native") return undefined + return getExtensionPublicClient(evmNetwork, nativeToken) + }, [evmNetwork, nativeToken]) + + return publicClient +} diff --git a/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx b/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx index 72451bbf0a..67e8bc961b 100644 --- a/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx +++ b/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx @@ -75,7 +75,7 @@ export const Mnemonic: FC = ({ onReveal, mnemonic, topRight }) => mnemonic.split(" ").map((word, i) => ( {i + 1}. - {word} + {word} ))} ) : ( @@ -216,16 +259,7 @@ const SignLedgerEthereum: FC = ({ {t("Cancel")} )} - {error && ( - - {/* Shouldn't be a LedgerSigningStatus, just an error message */} - - - )} + ) } diff --git a/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts b/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts index 4081bff50f..0ebf994ee5 100644 --- a/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts +++ b/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts @@ -1,4 +1,7 @@ -import { rebuildTransactionRequestNumbers } from "@core/domains/ethereum/helpers" +import { + parseRpcTransactionRequestBase, + serializeTransactionRequest, +} from "@core/domains/ethereum/helpers" import { KnownSigningRequestIdOnly } from "@core/domains/signing/types" import { log } from "@core/log" import { HexString } from "@polkadot/util/types" @@ -15,8 +18,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< const signingRequest = useRequest(id) const network = useEvmNetwork(signingRequest?.ethChainId) - const transactionRequest = useMemo( - () => (signingRequest ? rebuildTransactionRequestNumbers(signingRequest.request) : undefined), + const txBase = useMemo( + () => (signingRequest ? parseRpcTransactionRequestBase(signingRequest.request) : undefined), [signingRequest] ) @@ -24,8 +27,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< const [isPayloadLocked, setIsPayloadLocked] = useState(false) const { + decodedTx, transaction, - transactionInfo, txDetails, priority, setPriority, @@ -36,7 +39,7 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< gasSettingsByPriority, setCustomSettings, isValid, - } = useEthTransaction(transactionRequest, isPayloadLocked) + } = useEthTransaction(txBase, signingRequest?.ethChainId, isPayloadLocked) const baseRequest = useAnySigningRequest({ currentRequest: signingRequest, @@ -45,7 +48,10 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< }) const approve = useCallback(() => { - return baseRequest && baseRequest.approve(transaction) + if (!baseRequest) throw new Error("Missing base request") + if (!transaction) throw new Error("Missing transaction") + const serialized = serializeTransactionRequest(transaction) + return baseRequest && baseRequest.approve(serialized) }, [baseRequest, transaction]) const approveHardware = useCallback( @@ -53,11 +59,13 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< if (!baseRequest || !transaction || !baseRequest.id) return baseRequest.setStatus.processing("Approving request") try { - await api.ethApproveSignAndSendHardware(baseRequest.id, transaction, signature) + const serialized = serializeTransactionRequest(transaction) + await api.ethApproveSignAndSendHardware(baseRequest.id, serialized, signature) baseRequest.setStatus.success("Approved") } catch (err) { log.error("failed to approve hardware", { err }) baseRequest.setStatus.error((err as Error).message) + setIsPayloadLocked(false) } }, [baseRequest, transaction] @@ -73,8 +81,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< errorDetails, network, networkUsage, + decodedTx, transaction, - transactionInfo, approve, approveHardware, isPayloadLocked, diff --git a/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx b/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx index f9502445de..fea9ec3bbe 100644 --- a/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx +++ b/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx @@ -27,8 +27,12 @@ const getComponentFromTxDetails = (extrinsic: GenericExtrinsic | null | undefine return SubSignConvictionVotingUndelegate case "convictionVoting.vote": return SubSignConvictionVotingVote + case "xcmPallet.reserveTransferAssets": case "xcmPallet.limitedReserveTransferAssets": + case "xcmPallet.limitedTeleportAssets": + case "polkadotXcm.reserveTransferAssets": case "polkadotXcm.limitedReserveTransferAssets": + case "polkadotXcm.limitedTeleportAssets": return SubSignXcmTransferAssets case "xTokens.transfer": return SubSignXTokensTransfer diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts new file mode 100644 index 0000000000..5d3bcf0a26 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts @@ -0,0 +1,653 @@ +import * as $ from "subshape" + +// Type 379 - bounded_collections::weak_bounded_vec::WeakBoundedVec +// Param T : 2 +type BoundedCollectionsWeakBoundedVecWeakBoundedVec379 = Uint8Array +const $boundedCollectionsWeakBoundedVecWeakBoundedVec379: $.Shape = + $.uint8Array + +// Type 378 - xcm::v2::NetworkId +type XcmV2NetworkId = + | { type: "Any" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 } + | { type: "Polkadot" } + | { type: "Kusama" } +const $xcmV2NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Any"), + 1: $.variant("Named", $.field("value", $.uint8Array)), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), +}) + +// Type 380 - xcm::v2::BodyId +type XcmV2BodyId = + | { type: "Unit" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV2BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Named", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec379)), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 381 - xcm::v2::BodyPart +type XcmV2BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV2BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 377 - xcm::v2::junction::Junction +type XcmV2Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: XcmV2NetworkId; id: Uint8Array } + | { type: "AccountIndex64"; network: XcmV2NetworkId; index: bigint } + | { type: "AccountKey20"; network: XcmV2NetworkId; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { + type: "GeneralKey" + value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 + } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV2BodyId; part: XcmV2BodyPart } +const $xcmV2Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $xcmV2NetworkId), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec379)), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV2BodyId), $.field("part", $xcmV2BodyPart)), +}) + +// Type 376 - xcm::v2::multilocation::Junctions +type XcmV2Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV2Junction } + | { type: "X2"; value: [XcmV2Junction, XcmV2Junction] } + | { type: "X3"; value: [XcmV2Junction, XcmV2Junction, XcmV2Junction] } + | { + type: "X4" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X5" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X6" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X7" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X8" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } +const $xcmV2Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV2Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), +}) + +// Type 375 - xcm::v2::multilocation::MultiLocation +type XcmV2MultiLocation = { parents: number; interior: XcmV2Junctions } +const $xcmV2MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV2Junctions) +) + +// Type 389 - xcm::v2::multiasset::AssetId +type XcmV2AssetId = + | { type: "Concrete"; value: XcmV2MultiLocation } + | { type: "Abstract"; value: Uint8Array } +const $xcmV2AssetId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Concrete", $.field("value", $xcmV2MultiLocation)), + 1: $.variant("Abstract", $.field("value", $.uint8Array)), +}) + +// Type 391 - xcm::v2::multiasset::AssetInstance +type XcmV2AssetInstance = + | { type: "Undefined" } + | { type: "Index"; value: bigint } + | { type: "Array4"; value: Uint8Array } + | { type: "Array8"; value: Uint8Array } + | { type: "Array16"; value: Uint8Array } + | { type: "Array32"; value: Uint8Array } + | { type: "Blob"; value: Uint8Array } +const $xcmV2AssetInstance: $.Shape = $.taggedUnion("type", { + 0: $.variant("Undefined"), + 1: $.variant("Index", $.field("value", $.compact($.u128))), + 2: $.variant("Array4", $.field("value", $.sizedUint8Array(4))), + 3: $.variant("Array8", $.field("value", $.sizedUint8Array(8))), + 4: $.variant("Array16", $.field("value", $.sizedUint8Array(16))), + 5: $.variant("Array32", $.field("value", $.sizedUint8Array(32))), + 6: $.variant("Blob", $.field("value", $.uint8Array)), +}) + +// Type 390 - xcm::v2::multiasset::Fungibility +type XcmV2Fungibility = + | { type: "Fungible"; value: bigint } + | { type: "NonFungible"; value: XcmV2AssetInstance } +const $xcmV2Fungibility: $.Shape = $.taggedUnion("type", { + 0: $.variant("Fungible", $.field("value", $.compact($.u128))), + 1: $.variant("NonFungible", $.field("value", $xcmV2AssetInstance)), +}) + +// Type 388 - xcm::v2::multiasset::MultiAsset +type XcmV2MultiAsset = { id: XcmV2AssetId; fun: XcmV2Fungibility } +const $xcmV2MultiAsset: $.Shape = $.object( + $.field("id", $xcmV2AssetId), + $.field("fun", $xcmV2Fungibility) +) + +// Type 386 - xcm::v2::multiasset::MultiAssets +type XcmV2MultiAssets = Array +const $xcmV2MultiAssets: $.Shape = $.array($xcmV2MultiAsset) + +// Type 168 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 167 - Option +// Param T : 168 +type Option167 = XcmV3NetworkId | undefined +const $option167: $.Shape = $.option($xcmV3NetworkId) + +// Type 169 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 170 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 166 - xcm::v3::junction::Junction +type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option167; id: Uint8Array } + | { type: "AccountIndex64"; network: Option167; index: bigint } + | { type: "AccountKey20"; network: Option167; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } +const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option167) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option167) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) + +// Type 165 - xcm::v3::junctions::Junctions +type XcmV3Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV3Junction } + | { type: "X2"; value: [XcmV3Junction, XcmV3Junction] } + | { type: "X3"; value: [XcmV3Junction, XcmV3Junction, XcmV3Junction] } + | { + type: "X4" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X5" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X6" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X7" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X8" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } +const $xcmV3Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV3Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), +}) + +// Type 164 - xcm::v3::multilocation::MultiLocation +type XcmV3MultiLocation = { parents: number; interior: XcmV3Junctions } +const $xcmV3MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV3Junctions) +) + +// Type 408 - xcm::v3::multiasset::AssetId +type XcmV3AssetId = + | { type: "Concrete"; value: XcmV3MultiLocation } + | { type: "Abstract"; value: Uint8Array } +const $xcmV3AssetId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Concrete", $.field("value", $xcmV3MultiLocation)), + 1: $.variant("Abstract", $.field("value", $.sizedUint8Array(32))), +}) + +// Type 410 - xcm::v3::multiasset::AssetInstance +type XcmV3AssetInstance = + | { type: "Undefined" } + | { type: "Index"; value: bigint } + | { type: "Array4"; value: Uint8Array } + | { type: "Array8"; value: Uint8Array } + | { type: "Array16"; value: Uint8Array } + | { type: "Array32"; value: Uint8Array } +const $xcmV3AssetInstance: $.Shape = $.taggedUnion("type", { + 0: $.variant("Undefined"), + 1: $.variant("Index", $.field("value", $.compact($.u128))), + 2: $.variant("Array4", $.field("value", $.sizedUint8Array(4))), + 3: $.variant("Array8", $.field("value", $.sizedUint8Array(8))), + 4: $.variant("Array16", $.field("value", $.sizedUint8Array(16))), + 5: $.variant("Array32", $.field("value", $.sizedUint8Array(32))), +}) + +// Type 409 - xcm::v3::multiasset::Fungibility +type XcmV3Fungibility = + | { type: "Fungible"; value: bigint } + | { type: "NonFungible"; value: XcmV3AssetInstance } +const $xcmV3Fungibility: $.Shape = $.taggedUnion("type", { + 0: $.variant("Fungible", $.field("value", $.compact($.u128))), + 1: $.variant("NonFungible", $.field("value", $xcmV3AssetInstance)), +}) + +// Type 407 - xcm::v3::multiasset::MultiAsset +type XcmV3MultiAsset = { id: XcmV3AssetId; fun: XcmV3Fungibility } +const $xcmV3MultiAsset: $.Shape = $.object( + $.field("id", $xcmV3AssetId), + $.field("fun", $xcmV3Fungibility) +) + +// Type 405 - xcm::v3::multiasset::MultiAssets +type XcmV3MultiAssets = Array +const $xcmV3MultiAssets: $.Shape = $.array($xcmV3MultiAsset) + +// Type 427 - xcm::VersionedMultiAssets +export type VersionedMultiAssets = + | { type: "V2"; value: XcmV2MultiAssets } + | { type: "V3"; value: XcmV3MultiAssets } + +export const $versionedMultiAssets: $.Shape = $.taggedUnion("type", { + 1: $.variant("V2", $.field("value", $xcmV2MultiAssets)), + 3: $.variant("V3", $.field("value", $xcmV3MultiAssets)), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts new file mode 100644 index 0000000000..4720a6c1b0 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts @@ -0,0 +1,559 @@ +import * as $ from "subshape" + +// Type 119 - bounded_collections::weak_bounded_vec::WeakBoundedVec +// Param T : 2 +type BoundedCollectionsWeakBoundedVecWeakBoundedVec119 = Uint8Array +const $boundedCollectionsWeakBoundedVecWeakBoundedVec119: $.Shape = + $.uint8Array + +// Type 118 - xcm::v2::NetworkId +type XcmV2NetworkId = + | { type: "Any" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 } + | { type: "Polkadot" } + | { type: "Kusama" } +const $xcmV2NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Any"), + 1: $.variant("Named", $.field("value", $.uint8Array)), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), +}) + +// Type 120 - xcm::v2::BodyId +type XcmV2BodyId = + | { type: "Unit" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV2BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Named", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec119)), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 121 - xcm::v2::BodyPart +type XcmV2BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV2BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 117 - xcm::v2::junction::Junction +type XcmV2Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: XcmV2NetworkId; id: Uint8Array } + | { type: "AccountIndex64"; network: XcmV2NetworkId; index: bigint } + | { type: "AccountKey20"; network: XcmV2NetworkId; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { + type: "GeneralKey" + value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 + } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV2BodyId; part: XcmV2BodyPart } +const $xcmV2Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $xcmV2NetworkId), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec119)), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV2BodyId), $.field("part", $xcmV2BodyPart)), +}) + +// Type 116 - xcm::v2::multilocation::Junctions +type XcmV2Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV2Junction } + | { type: "X2"; value: [XcmV2Junction, XcmV2Junction] } + | { type: "X3"; value: [XcmV2Junction, XcmV2Junction, XcmV2Junction] } + | { + type: "X4" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X5" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X6" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X7" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X8" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } +const $xcmV2Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV2Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), +}) + +// Type 115 - xcm::v2::multilocation::MultiLocation +type XcmV2MultiLocation = { parents: number; interior: XcmV2Junctions } +const $xcmV2MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV2Junctions) +) + +// Type 79 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 78 - Option +// Param T : 79 +type Option78 = XcmV3NetworkId | undefined +const $option78: $.Shape = $.option($xcmV3NetworkId) + +// Type 80 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 81 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 76 - xcm::v3::junction::Junction +type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option78; id: Uint8Array } + | { type: "AccountIndex64"; network: Option78; index: bigint } + | { type: "AccountKey20"; network: Option78; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } +const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) + +// Type 75 - xcm::v3::junctions::Junctions +type XcmV3Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV3Junction } + | { type: "X2"; value: [XcmV3Junction, XcmV3Junction] } + | { type: "X3"; value: [XcmV3Junction, XcmV3Junction, XcmV3Junction] } + | { + type: "X4" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X5" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X6" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X7" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X8" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } +const $xcmV3Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV3Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), +}) + +// Type 74 - xcm::v3::multilocation::MultiLocation +type XcmV3MultiLocation = { parents: number; interior: XcmV3Junctions } +const $xcmV3MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV3Junctions) +) + +// Type 124 - xcm::VersionedMultiLocation +export type VersionedMultiLocation = + | { type: "V2"; value: XcmV2MultiLocation } + | { type: "V3"; value: XcmV3MultiLocation } + +export const $versionedMultiLocation: $.Shape = $.taggedUnion("type", { + 1: $.variant("V2", $.field("value", $xcmV2MultiLocation)), + 3: $.variant("V3", $.field("value", $xcmV3MultiLocation)), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts new file mode 100644 index 0000000000..f7d2145770 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts @@ -0,0 +1,133 @@ +import * as $ from "subshape" + +// Type 79 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 78 - Option +// Param T : 79 +type Option78 = XcmV3NetworkId | undefined +const $option78: $.Shape = $.option($xcmV3NetworkId) + +// Type 80 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 81 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 76 - xcm::v3::junction::Junction +export type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option78; id: Uint8Array } + | { type: "AccountIndex64"; network: Option78; index: bigint } + | { type: "AccountKey20"; network: Option78; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } + +export const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx b/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx index fdf457bd33..295ca0b8ae 100644 --- a/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx +++ b/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx @@ -1,6 +1,5 @@ import { log } from "@core/log" import { Codec } from "@polkadot/types-codec/types" -import { JunctionV1, VersionedMultiLocation } from "@polkadot/types/interfaces/xcm" import { assert } from "@polkadot/util" import { Address } from "@talismn/balances" import { Chain, Token } from "@talismn/chaindata-provider" @@ -9,13 +8,37 @@ import useChains from "@ui/hooks/useChains" import { useExtrinsic } from "@ui/hooks/useExtrinsic" import { useTokenRatesMap } from "@ui/hooks/useTokenRatesMap" import useTokens from "@ui/hooks/useTokens" +import isEqual from "lodash/isEqual" import { useMemo } from "react" import { useTranslation } from "react-i18next" +import * as $ from "subshape" import { SignContainer } from "../../SignContainer" import { usePolkadotSigningRequest } from "../../SignRequestContext" +import { SignViewBodyShimmer } from "../../Views/SignViewBodyShimmer" import { SignViewIconHeader } from "../../Views/SignViewIconHeader" import { SignViewXTokensTransfer } from "../../Views/transfer/SignViewCrossChainTransfer" +import { $versionedMultiLocation, VersionedMultiLocation } from "../shapes/VersionedMultiLocation" +import { XcmV3Junction } from "../shapes/XcmV3Junction" + +const normalizeTokenId = (tokenId: unknown) => { + if (typeof tokenId === "string" && tokenId.startsWith("{") && tokenId.endsWith("}")) + tokenId = JSON.parse(tokenId) + if (typeof tokenId === "object") { + // some property names don't have the same case in chaindata. ex: vsKSM + return Object.entries(tokenId as Record).reduce((acc, [key, value]) => { + acc[key.toLowerCase()] = typeof value === "string" ? value.toLowerCase() : value + return acc + }, {} as Record) + } + return tokenId +} + +const isSameTokenId = (tokenId1: unknown, tokenId2: unknown) => { + tokenId1 = normalizeTokenId(tokenId1) + tokenId2 = normalizeTokenId(tokenId2) + return isEqual(tokenId1, tokenId2) +} const getTokenFromCurrency = (currency: Codec, chain: Chain, tokens: Token[]): Token => { // ex: HDX @@ -27,42 +50,45 @@ const getTokenFromCurrency = (currency: Codec, chain: Chain, tokens: Token[]): T log.warn("unknown currencyId %d on chain %s", currencyId, chain.id) } + const jsonCurrency = currency.toJSON() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const unsafeCurrency = currency as any - if (unsafeCurrency.isToken) { - const token = tokens.find( - (t) => - // ex: ACA - (t.type === "substrate-native" && - t.symbol.toLowerCase() === unsafeCurrency.asToken.type.toLowerCase()) || - // ex: aUSD - (t.type === "substrate-tokens" && - t.onChainId?.toString()?.toLowerCase() === unsafeCurrency.toString().toLowerCase()) - ) - if (token) return token - } + const lsymbol = (unsafeCurrency.isToken ? unsafeCurrency.asToken.type : "").toLowerCase() + + const token = tokens.find( + (t) => + // ex: ACA + (t.type === "substrate-native" && t.symbol.toLowerCase() === lsymbol) || + (t.type === "substrate-tokens" && + // ex: vsKSM + (isSameTokenId(t.onChainId, jsonCurrency) || + // ex: aUSD + t.onChainId?.toString()?.toLowerCase() === jsonCurrency?.toString().toLowerCase())) + ) + if (token) return token // throw an error so the sign popup fallbacks to default view log.warn("unknown on chain %s", chain.id, currency.toHuman()) throw new Error("Token not found") } -const getTargetFromInteriorV1 = ( - interior: JunctionV1, +const getTargetFromInterior = ( + interior: XcmV3Junction, chain: Chain, chains: Chain[] ): { chain?: Chain; address?: Address } => { - if (interior.isParachain) { - const paraId = interior.asParachain.toNumber() + if (interior.type === "Parachain") { + const paraId = interior.value const relayId = chain.relay ? chain.relay.id : chain.id const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) if (targetChain) return { chain: targetChain } } - if (interior.isAccountKey20) return { address: interior.asAccountKey20.key.toString() } - if (interior.isAccountId32) return { address: interior.asAccountId32.id.toString() } + if (interior.type === "AccountKey20") return { address: encodeAnyAddress(interior.key) } + if (interior.type === "AccountId32") return { address: encodeAnyAddress(interior.id) } // throw an error so the sign popup fallbacks to default view - log.warn("Unsupported interior", interior.toHuman()) + //log.warn("Unsupported interior", interior.toHuman()) throw new Error("Unknown interior") } @@ -72,24 +98,31 @@ const getTarget = ( chains: Chain[], address: Address ): { chain?: Chain; address?: Address } => { - if (multiLocation?.isV1) { + if (multiLocation?.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && chain) return { chain, address } - - if (interior.isX1 && chain && interior.asX1.isParachain) { - const paraId = interior.asX1.asParachain.toNumber() - const relayId = chain.relay ? chain.relay.id : chain.id - const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) - if (targetChain) return { chain: targetChain, address } + const interior = multiLocation.value.interior + if (interior.type === "Here" && chain) return { chain, address } + + if (interior.type === "X1") { + if (interior.value.type === "Parachain") { + const paraId = interior.value.value + const relayId = chain.relay ? chain.relay.id : chain.id + const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) + if (targetChain) return { chain: targetChain, address } + } + + const targetChain = + multiLocation.value.parents === 1 ? chains.find((c) => c.id === chain.relay?.id) : chain + if (interior.value.type === "AccountKey20") + return { chain: targetChain, address: encodeAnyAddress(interior.value.key) } + if (interior.value.type === "AccountId32") + return { chain: targetChain, address: encodeAnyAddress(interior.value.id) } } - if (interior.isX1 && chain && interior.asX1.isAccountKey20) - return { chain, address: interior.asX1.asAccountKey20.key.toString() } - if (interior.isX2) { - const interiorX2 = interior.asX2 - const res0 = getTargetFromInteriorV1(interiorX2[0], chain, chains) - const res1 = getTargetFromInteriorV1(interiorX2[1], chain, chains) + if (interior.type === "X2") { + const interiorX2 = interior.value + const res0 = getTargetFromInterior(interiorX2[0], chain, chains) + const res1 = getTargetFromInterior(interiorX2[1], chain, chains) const resChain = res0.chain || res1.chain const resAddress = res0.address || res1.address if (!resChain || !resAddress) throw new Error("Unknown multi location") @@ -101,7 +134,7 @@ const getTarget = ( } // throw an error so the sign popup fallbacks to default view - log.warn("Unsupported multi location", multiLocation?.toHuman()) + //log.warn("Unsupported multi location", multiLocation?.toHuman()) throw new Error("Unknown multi location") } @@ -114,12 +147,15 @@ export const SubSignXTokensTransfer = () => { const { chains } = useChains("all") const props = useMemo(() => { + // wait for tokens to be loaded + if (!tokens.length) return null assert(extrinsic, "No extrinsic") assert(chain, "No chain") - const currency = extrinsic.method?.args[0] // as any - const value = extrinsic.registry.createType("u128", extrinsic.method?.args[1]).toBigInt() - const dest = extrinsic.registry.createType("VersionedMultiLocation", extrinsic.method?.args[2]) + // CurrencyId - currency ids are chain specific, can't use subshape easily + const currency = extrinsic.method.args[0] // as any + const value = $.u128.decode(extrinsic.method.args[1].toU8a()) + const dest = $versionedMultiLocation.decode(extrinsic.method.args[2].toU8a()) const token = getTokenFromCurrency( currency, @@ -129,7 +165,6 @@ export const SubSignXTokensTransfer = () => { const target = getTarget(dest, chain, chains, account.address) assert(target.chain && target.address, "Unknown target") - return { value, tokenDecimals: token.decimals, @@ -143,6 +178,8 @@ export const SubSignXTokensTransfer = () => { } }, [extrinsic, chain, tokens, chains, account.address, tokenRates, payload.address]) + if (!props) return + return ( { - if (multiAsset?.isV1) { + if (multiAsset?.type === "V3") { // our view only support displaying one asset - if (multiAsset.asV1.length === 1) { - const asset = multiAsset.asV1[0] - const fungible = asset.getAtIndex(1) as FungibilityV1 // property fungibility isn't mapped properly on pjs typings + if (multiAsset.value.length === 1) { + const asset = multiAsset.value[0] + + if (asset?.id.type === "Concrete" && asset.fun.type === "Fungible") { + const value = asset.fun.value + const interior = asset.id.value.interior + if (interior.type === "Here" && chain?.nativeToken?.id) { + return { tokenId: chain.nativeToken.id, value } + } + if (interior.type === "X2") { + if ( + interior.value[0].type === "PalletInstance" && + interior.value[0].value === 50 && + interior.value[1].type === "GeneralIndex" + ) { + // Assets pallet + const assetId = interior.value[1].value + // at this stage we don't know the symbol but we know the start of the id + const search = `${chain?.id}-substrate-assets-${assetId}` + const tokenId = Object.keys(tokens).find((id) => id.startsWith(search)) - if (asset?.id.isConcrete && fungible.isFungible) { - if (asset.id.asConcrete.interior.isHere && chain?.nativeToken?.id) { - return { tokenId: chain.nativeToken.id, value: fungible.asFungible.toBigInt() } + if (!tokenId) throw new Error("Unknown multi asset") + + return { tokenId, value } + } } } } } // throw an error so the sign popup fallbacks to default view + log.warn("Unknown multi asset", { multiAsset, chain }) throw new Error("Unknown multi asset") } -const getTargetChainId = ( - multiLocation: VersionedMultiLocation | undefined, +const getTargetChain = ( + multiLocation: VersionedMultiLocation, chain: Chain | null | undefined, chains: Chain[] ): Chain => { - if (multiLocation?.isV1) { + if (multiLocation.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && chain) return chain - if (interior.isX1 && chain && interior.asX1.isParachain) { - const paraId = interior.asX1.asParachain.toNumber() + const interior = multiLocation.value.interior + if (interior.type === "Here" && chain) return chain + if (interior.type === "X1" && chain && interior.value.type === "Parachain") { + const paraId = interior.value.value const relayId = chain.relay ? chain.relay.id : chain.id const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) if (targetChain) return targetChain @@ -60,7 +78,7 @@ const getTargetChainId = ( } // throw an error so the sign popup fallbacks to default view - log.warn("Unknown multi location", multiLocation?.toHuman()) + log.warn("Unknown multi location", multiLocation) throw new Error("Unknown multi location") } @@ -68,17 +86,17 @@ const getTargetAccount = ( multiLocation: VersionedMultiLocation | undefined, account: AccountJsonAny ): Address => { - if (multiLocation?.isV1) { + if (multiLocation?.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && account) return account.address - if (interior.isX1) { - if (interior.asX1.isAccountKey20) return interior.asX1.asAccountKey20.key.toString() - if (interior.asX1.isAccountId32) return interior.asX1.asAccountId32.id.toString() + const interior = multiLocation.value.interior + if (interior.type === "Here" && account) return account.address + if (interior.type === "X1") { + if (interior.value.type === "AccountKey20") return encodeAnyAddress(interior.value.key) + if (interior.value.type === "AccountId32") return encodeAnyAddress(interior.value.id) } } // throw an error so the sign popup fallbacks to default view - log.warn("Unknown multi location", multiLocation?.toHuman()) + log.warn("Unknown multi location", multiLocation) throw new Error("Unknown multi location") } @@ -91,22 +109,19 @@ export const SubSignXcmTransferAssets = () => { const { chains } = useChains("all") const props = useMemo(() => { + if (Object.keys(tokensMap).length === 0) return null if (!chain) throw new Error("Unknown chain") if (!extrinsic) throw new Error("Unknown extrinsic") - // Note: breaks here fore statemine assets. hoping that next version of @polkadot/api will fix it - const dest = extrinsic.registry.createType("VersionedMultiLocation", extrinsic.method.args[0]) - const beneficiary = extrinsic.registry.createType( - "VersionedMultiLocation", - extrinsic.method.args[1] - ) - const assets = extrinsic.registry.createType("VersionedMultiAssets", extrinsic.method.args[2]) + const dest = $versionedMultiLocation.decode(extrinsic.method.args[0].toU8a()) + const beneficiary = $versionedMultiLocation.decode(extrinsic.method.args[1].toU8a()) + const assets = $versionedMultiAssets.decode(extrinsic.method.args[2].toU8a()) - const { tokenId, value } = getMultiAssetTokenId(assets, chain) + const { tokenId, value } = getMultiAssetTokenId(assets, chain, tokensMap) const token = tokensMap[tokenId] if (!token) throw new Error("Unknown token") - const toNetwork = getTargetChainId(dest, chain, chains) + const toNetwork = getTargetChain(dest, chain, chains) const toAddress = getTargetAccount(beneficiary, account) return { @@ -122,6 +137,8 @@ export const SubSignXcmTransferAssets = () => { } }, [chain, extrinsic, tokensMap, chains, account, tokenRates, payload.address]) + if (!props) return + return ( void } -const Gwei: FC<{ value: BigNumberish | null | undefined }> = ({ value }) => { +const Gwei: FC<{ value: bigint | null | undefined }> = ({ value }) => { const { t } = useTranslation("request") return ( <> - {value - ? t("{{value}} GWEI", { value: formatDecimals(formatUnits(value, "gwei")) }) + {value !== null && value !== undefined + ? t("{{value}} GWEI", { value: formatDecimals(formatGwei(value)) }) : t("N/A")} ) @@ -57,20 +56,15 @@ const ViewDetailsContent: FC = ({ onClose }) => { txDetails, priority, transaction, - transactionInfo, + decodedTx, error, errorDetails, } = useEthSignTransactionRequest() const { genericEvent } = useAnalytics() - const txInfo = useMemo(() => { - if (transactionInfo && transactionInfo.contractType !== "unknown") return transactionInfo - return undefined - }, [transactionInfo]) - const nativeToken = useToken(network?.nativeToken?.id) const formatEthValue = useCallback( - (value?: BigNumberish) => { + (value: bigint = 0n) => { return value ? `${formatEther(value)} ${nativeToken?.symbol ?? ""}` : null }, [nativeToken?.symbol] @@ -82,22 +76,28 @@ const ViewDetailsContent: FC = ({ onClose }) => { genericEvent("open sign transaction view details", { type: "ethereum" }) }, [genericEvent]) - const [estimatedFee, maximumFee] = useMemo( + const [estimatedFee, maximumFee, estimatedL1DataFee, estimatedL2Fee] = useMemo( () => txDetails && nativeToken ? [ - new BalanceFormatter( - BigNumber.from(txDetails?.estimatedFee).toString(), - nativeToken?.decimals, - nativeTokenRates - ), - new BalanceFormatter( - BigNumber.from(txDetails?.maxFee).toString(), - nativeToken?.decimals, - nativeTokenRates - ), + new BalanceFormatter(txDetails.estimatedFee, nativeToken?.decimals, nativeTokenRates), + new BalanceFormatter(txDetails.maxFee, nativeToken?.decimals, nativeTokenRates), + txDetails.estimatedL1DataFee + ? new BalanceFormatter( + txDetails.estimatedL1DataFee, + nativeToken.decimals, + nativeTokenRates + ) + : null, + txDetails.estimatedL1DataFee + ? new BalanceFormatter( + txDetails.estimatedFee - txDetails.estimatedL1DataFee, + nativeToken.decimals, + nativeTokenRates + ) + : null, ] - : [null, null], + : [null, null, null, null], [nativeToken, nativeTokenRates, txDetails] ) @@ -126,10 +126,10 @@ const ViewDetailsContent: FC = ({ onClose }) => {
{t("Details")}
- {!!txInfo?.isContractCall && ( + {!!decodedTx?.isContractCall && ( - {txInfo?.contractType - ? `${txInfo?.contractType} : ${txInfo?.contractCall?.name ?? t("N/A")}` + {decodedTx?.contractType + ? `${decodedTx?.contractType} : ${decodedTx?.contractCall?.functionName ?? t("N/A")}` : t("Unknown")} )} @@ -140,11 +140,11 @@ const ViewDetailsContent: FC = ({ onClose }) => { /> - {formatEthValue(request.value)} + {formatEthValue(transaction?.value)} @@ -158,8 +158,13 @@ const ViewDetailsContent: FC = ({ onClose }) => { typeof networkUsage === "number" ? `${Math.round(networkUsage * 100)}%` : t("N/A") } /> - {transaction?.type === 2 && ( + + {transaction?.type === "eip1559" && ( <> + } + /> } @@ -181,7 +186,7 @@ const ViewDetailsContent: FC = ({ onClose }) => { {transaction ? ( - {transaction?.type === 2 ? ( + {transaction?.type === "eip1559" ? ( <> @@ -205,11 +210,7 @@ const ViewDetailsContent: FC = ({ onClose }) => { )} ) : ( @@ -217,6 +218,48 @@ const ViewDetailsContent: FC = ({ onClose }) => { )} )} + {estimatedL1DataFee && estimatedL2Fee && nativeToken && ( + + + + + {estimatedL1DataFee && nativeTokenRates ? ( + <> + {" "} + / + + ) : null} + + } + /> + + + {estimatedL2Fee && nativeTokenRates ? ( + <> + {" "} + / + + ) : null} + + } + /> + + + )} {transaction ? ( diff --git a/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx b/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx index 0950b6d3ce..fab056444d 100644 --- a/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx +++ b/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx @@ -24,7 +24,6 @@ import { useTokenRates } from "@ui/hooks/useTokenRates" import { getTransactionHistoryUrl } from "@ui/util/getTransactionHistoryUrl" import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict" import { useLiveQuery } from "dexie-react-hooks" -import { BigNumber } from "ethers" import sortBy from "lodash/sortBy" import { FC, PropsWithChildren, forwardRef, useCallback, useEffect, useMemo, useState } from "react" import { Trans, useTranslation } from "react-i18next" @@ -348,10 +347,7 @@ const TransactionRowEvm: FC = ({ const [isCtxMenuOpen, setIsCtxMenuOpen] = useState(false) const amount = useMemo( - () => - token && value - ? new BalanceFormatter(BigNumber.from(value).toBigInt(), token.decimals, tokenRates) - : null, + () => (token && value ? new BalanceFormatter(value, token.decimals, tokenRates) : null), [token, tokenRates, value] ) diff --git a/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx b/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx index db7c2d2f0f..16b4a0a434 100644 --- a/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx +++ b/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx @@ -1,3 +1,4 @@ +import { serializeTransactionRequest } from "@core/domains/ethereum/helpers" import { EthTransactionDetails } from "@core/domains/signing/types" import { EvmWalletTransaction, WalletTransaction } from "@core/domains/transactions/types" import { HexString } from "@polkadot/util/types" @@ -11,8 +12,6 @@ import { useAccountByAddress } from "@ui/hooks/useAccountByAddress" import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView" import { useBalance } from "@ui/hooks/useBalance" import { useEvmNetwork } from "@ui/hooks/useEvmNetwork" -import { BigNumber } from "ethers" -import { ethers } from "ethers" import { FC, useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Button, Drawer, useOpenCloseWithData } from "talisman-ui" @@ -56,21 +55,13 @@ export const EvmEstimatedFeeTooltip: FC<{
{t("Estimated fee:")}
- +
{!!txDetails?.maxFee && ( <>
{t("Max. fee:")}
- +
)} @@ -125,7 +116,7 @@ const EvmDrawerContent: FC<{ networkUsage, isLoading, isValid, - } = useEthReplaceTransaction(tx.unsigned, type, isLocked) + } = useEthReplaceTransaction(tx.unsigned, tx.evmNetworkId, type, isLocked) const account = useAccountByAddress(tx.account) @@ -136,11 +127,12 @@ const EvmDrawerContent: FC<{ setIsProcessing(true) try { const transferInfo = getTransferInfo(tx) - const newHash = await api.ethSignAndSend(transaction, transferInfo) + const serialized = serializeTransactionRequest(transaction) + const newHash = await api.ethSignAndSend(tx.evmNetworkId, serialized, transferInfo) api.analyticsCapture({ eventName: `transaction ${type}`, options: { - chainId: transaction.chainId, + chainId: Number(tx.evmNetworkId), networkType: "ethereum", }, }) @@ -166,11 +158,17 @@ const EvmDrawerContent: FC<{ setIsProcessing(true) try { const transferInfo = getTransferInfo(tx) - const newHash = await api.ethSendSigned(transaction, signature, transferInfo) + const serialized = serializeTransactionRequest(transaction) + const newHash = await api.ethSendSigned( + tx.evmNetworkId, + serialized, + signature, + transferInfo + ) api.analyticsCapture({ eventName: `transaction ${type}`, options: { - chainId: transaction.chainId, + chainId: Number(tx.evmNetworkId), networkType: "ethereum", }, }) @@ -259,7 +257,7 @@ const EvmDrawerContent: FC<{
{txDetails?.estimatedFee ? ( ) : null} diff --git a/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts b/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts index 7cbc40910d..b3ad363599 100644 --- a/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts +++ b/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts @@ -53,6 +53,7 @@ export const useLedgerEthereum = (persist = false) => { ledger.getAddress(getEthLedgerDerivationPath("LedgerLive")), throwAfter(5_000, "Timeout"), ]) + const { version } = await ledger.getAppConfiguration() if (!gte(version, LEDGER_ETHEREUM_MIN_VERSION)) throw new LedgerError("Unsupported version", "UnsupportedVersion") diff --git a/apps/extension/src/ui/hooks/useErc20TokenInfo.ts b/apps/extension/src/ui/hooks/useErc20TokenInfo.ts index 42954cc471..cbdb9ef8fc 100644 --- a/apps/extension/src/ui/hooks/useErc20TokenInfo.ts +++ b/apps/extension/src/ui/hooks/useErc20TokenInfo.ts @@ -1,26 +1,27 @@ +import { EvmAddress } from "@core/domains/ethereum/types" import { CustomErc20TokenCreate } from "@core/domains/tokens/types" import { getErc20TokenInfo } from "@core/util/getErc20TokenInfo" import { EvmNetworkId } from "@talismn/chaindata-provider" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { useEffect, useState } from "react" -export const useErc20TokenInfo = (evmNetworkId?: EvmNetworkId, contractAddress?: string) => { +export const useErc20TokenInfo = (evmNetworkId?: EvmNetworkId, contractAddress?: EvmAddress) => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() const [token, setToken] = useState() - const provider = useEthereumProvider(evmNetworkId) + const publicClient = usePublicClient(evmNetworkId) useEffect(() => { setError(undefined) setToken(undefined) - if (!evmNetworkId || !provider || !contractAddress) return + if (!evmNetworkId || !publicClient || !contractAddress) return setIsLoading(true) - getErc20TokenInfo(provider, evmNetworkId, contractAddress) + getErc20TokenInfo(publicClient, evmNetworkId, contractAddress) .then(setToken) .catch(setError) .finally(() => setIsLoading(false)) - }, [contractAddress, evmNetworkId, provider]) + }, [contractAddress, evmNetworkId, publicClient]) return { isLoading, error, token } } diff --git a/apps/extension/src/ui/hooks/useOnChainId.ts b/apps/extension/src/ui/hooks/useOnChainId.ts index 9258c7d489..0ee4db5922 100644 --- a/apps/extension/src/ui/hooks/useOnChainId.ts +++ b/apps/extension/src/ui/hooks/useOnChainId.ts @@ -12,6 +12,10 @@ export const useOnChainId = (address?: string) => { }, enabled: !!address, cacheTime: Infinity, + refetchInterval: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, initialData: () => address && onChainIdsCache.get(address)?.onChainId, onSuccess: (onChainId) => { if (!address) return diff --git a/apps/extension/webpack/webpack.common.js b/apps/extension/webpack/webpack.common.js index 540ab8cacf..869f680793 100644 --- a/apps/extension/webpack/webpack.common.js +++ b/apps/extension/webpack/webpack.common.js @@ -28,10 +28,6 @@ const config = (env) => ({ "@substrate/txwrapper-core", "@talismn/chaindata-provider-extension", "@metamask/eth-sig-util", - "@acala-network/types", - "@acala-network/eth-providers", - "@acala-network/eth-transactions", - "@acala-network/api-derive", ], // Wallet injected scripts diff --git a/apps/playground/package.json b/apps/playground/package.json index 757e635f5a..115a1200d6 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -17,7 +17,6 @@ "@metamask/eth-sig-util": "5.1.0", "@talismn/wagmi-connector": "^0.2.0", "buffer": "^6.0.3", - "ethers": "^5.7.2", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "^4.0.4", @@ -25,7 +24,8 @@ "react-router-dom": "6.14.1", "react-use": "^17.4.0", "talisman-ui": "workspace:*", - "wagmi": "^0.12.12" + "viem": "^1.18.9", + "wagmi": "^1.4.5" }, "devDependencies": { "@openzeppelin/contracts": "^4.9.2", @@ -39,7 +39,7 @@ "hardhat": "^2.16.1", "postcss": "^8.4.20", "tailwindcss": "^3.3.2", - "typescript": "4.9.4", + "typescript": "5.2.2", "vite": "^4.4.3", "vite-plugin-svgr": "^2.2.1" }, diff --git a/apps/playground/src/components/Ethereum/NavEthereum.tsx b/apps/playground/src/components/Ethereum/NavEthereum.tsx index 9b48332708..bb57c7b8f9 100644 --- a/apps/playground/src/components/Ethereum/NavEthereum.tsx +++ b/apps/playground/src/components/Ethereum/NavEthereum.tsx @@ -36,12 +36,6 @@ export const NavEthereum = () => { > Sign - - Behavior -
) } diff --git a/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx b/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx deleted file mode 100644 index 124f28b067..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { EthereumLayout } from "../shared/EthereumLayout" -import { NetworkSwitch } from "./NetworkSwitch" - -export const BehaviorPage = () => { - return ( - - - - ) -} diff --git a/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts b/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts deleted file mode 100644 index 95e71f6e7e..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts +++ /dev/null @@ -1,368 +0,0 @@ -export const CrestAbi = [ - { - inputs: [ - { internalType: "address", name: "_store", type: "address" }, - { internalType: "address", name: "_auctionHouse", type: "address" }, - { internalType: "address", name: "_founders", type: "address" }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "owner", type: "address" }, - { indexed: true, internalType: "address", name: "approved", type: "address" }, - { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "Approval", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "owner", type: "address" }, - { indexed: true, internalType: "address", name: "operator", type: "address" }, - { indexed: false, internalType: "bool", name: "approved", type: "bool" }, - ], - name: "ApprovalForAll", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "delegator", type: "address" }, - { indexed: true, internalType: "address", name: "fromDelegate", type: "address" }, - { indexed: true, internalType: "address", name: "toDelegate", type: "address" }, - ], - name: "DelegateChanged", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "delegate", type: "address" }, - { indexed: false, internalType: "uint256", name: "previousBalance", type: "uint256" }, - { indexed: false, internalType: "uint256", name: "newBalance", type: "uint256" }, - ], - name: "DelegateVotesChanged", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, - { indexed: true, internalType: "address", name: "newOwner", type: "address" }, - ], - name: "OwnershipTransferred", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "from", type: "address" }, - { indexed: true, internalType: "address", name: "to", type: "address" }, - { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "Transfer", - type: "event", - }, - { - inputs: [], - name: "DOMAIN_SEPARATOR", - outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "approve", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "auctionHouse", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "owner", type: "address" }], - name: "balanceOf", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "burn", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "contractURI", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "delegatee", type: "address" }], - name: "delegate", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "delegatee", type: "address" }, - { internalType: "uint256", name: "nonce", type: "uint256" }, - { internalType: "uint256", name: "expiry", type: "uint256" }, - { internalType: "uint8", name: "v", type: "uint8" }, - { internalType: "bytes32", name: "r", type: "bytes32" }, - { internalType: "bytes32", name: "s", type: "bytes32" }, - ], - name: "delegateBySig", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "account", type: "address" }], - name: "delegates", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "", type: "uint256" }], - name: "dnaMap", - outputs: [{ internalType: "uint96", name: "", type: "uint96" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "founders", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "getApproved", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "blockNumber", type: "uint256" }], - name: "getPastTotalSupply", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "account", type: "address" }, - { internalType: "uint256", name: "blockNumber", type: "uint256" }, - ], - name: "getPastVotes", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "account", type: "address" }], - name: "getVotes", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "owner", type: "address" }, - { internalType: "address", name: "operator", type: "address" }, - ], - name: "isApprovedForAll", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function", - }, - { inputs: [], name: "mint", outputs: [], stateMutability: "nonpayable", type: "function" }, - { - inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint96", name: "dna", type: "uint96" }, - ], - name: "mintSpecific", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "name", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "owner", type: "address" }], - name: "nonces", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "owner", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "ownerOf", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "safeTransferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - { internalType: "bytes", name: "data", type: "bytes" }, - ], - name: "safeTransferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "operator", type: "address" }, - { internalType: "bool", name: "approved", type: "bool" }, - ], - name: "setApprovalForAll", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "_auctionHouse", type: "address" }], - name: "setAuctionHouse", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "string", name: "_uri", type: "string" }], - name: "setContractURI", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "_founders", type: "address" }], - name: "setFounders", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "store", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], - name: "supportsInterface", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "symbol", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], - name: "tokenByIndex", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "owner", type: "address" }, - { internalType: "uint256", name: "index", type: "uint256" }, - ], - name: "tokenOfOwnerByIndex", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "tokenURI", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "totalSupply", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "transferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "newOwner", type: "address" }], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const diff --git a/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx b/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx deleted file mode 100644 index a5eb7393a7..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { ethers } from "ethers" -import { useCallback, useState } from "react" -import { Button } from "talisman-ui" -import { useAccount } from "wagmi" - -import { Section } from "../../shared/Section" -import { CrestAbi } from "./CrestAbi" - -export const NetworkSwitch = () => { - const [output, setOutput] = useState("") - - const { connector } = useAccount() - - const handleTestClick = useCallback(async () => { - setOutput("") - - const appendOutput = (text: string) => { - setOutput((prev) => prev + text + "\n") - } - - try { - const injectedProvider = await connector?.getProvider() - - const switchToMoonbeam = async () => { - appendOutput(`Switching to moonbeam `) - const res = await injectedProvider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: "0x504" }], - }) - appendOutput(`Switched to moonbeam : ${res}`) - } - - const switchToAstar = async () => { - appendOutput(`Switching to astar`) - const res = await injectedProvider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: "0x250" }], - }) - appendOutput(`Switched to astar : ${res}`) - } - - const callNftTokenUri = async () => { - appendOutput(`fetching NFT info`) - const ci = ethers.Contract.getInterface(CrestAbi) - const data = ci.encodeFunctionData("tokenURI", [1]) - - const res = await injectedProvider.request({ - method: "eth_call", - params: [ - { - to: "0x8417F77904a86436223942a516f00F8aDF933B70", - data, - }, - ], - }) - - const decoded = ci.decodeFunctionResult("tokenURI", res) - appendOutput(`fetched NFT info ${decoded.toString().slice(0, 40)}...`) - } - - await switchToMoonbeam() - const prom = callNftTokenUri() - await switchToAstar() - await prom - } catch (err) { - appendOutput(`Error : ${err?.toString()}`) - } - }, [connector]) - - return ( -
-
- This will : -
    -
  • - switch to Moonbeam
  • -
  • - asynchronously make an expensive read contract call
  • -
  • - switch to Astar
  • -
-
-
- -
-
-        {output}
-      
-
- ) -} diff --git a/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx b/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx index 54b26ec79d..4d6db4d3c0 100644 --- a/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx +++ b/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx @@ -1,5 +1,4 @@ -import { ethers } from "ethers" -import { useCallback, useMemo } from "react" +import { useCallback } from "react" import { useForm } from "react-hook-form" import { Button } from "talisman-ui" import { @@ -8,6 +7,7 @@ import { useContractWrite, useNetwork, usePrepareContractWrite, + useWalletClient, } from "wagmi" import { TestBasics, useDeployment } from "../../../contracts" @@ -31,10 +31,12 @@ export const ContractTestBasics = () => { } const ContractTestBasicsInner = () => { - const { isConnected, address: from, connector } = useAccount() + const { isConnected } = useAccount() const { chain } = useNetwork() const { address } = useDeployment("TestBasics", chain?.id) + const { data: walletClient } = useWalletClient() + const { data: readData, isLoading: readIsLoading } = useContractRead({ address, abi: TestBasics.abi, @@ -73,43 +75,15 @@ const ContractTestBasicsInner = () => { // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(TestBasics.abi) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "setValue" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [newValue]) - - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from, - to: address, - data, - }, - ], + if (!walletClient || !address) return + + await walletClient.writeContract({ + abi: TestBasics.abi, + address, + functionName: "setValue", + args: [newValue], }) - }, [address, connector, from, newValue]) - - const parsedError = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorData = (error as any)?.data?.data - if (!errorData) return undefined - - try { - const contract = new ethers.utils.Interface(TestBasics.abi) - return contract.parseError(errorData) - } catch (err) { - // eslint-disable-next-line no-console - console.error("Failed to parse error") - return null - } - }, [error]) + }, [address, newValue, walletClient]) if (!isConnected) return null @@ -138,12 +112,7 @@ const ContractTestBasicsInner = () => {
{!isLoading && newValue && error && ( -
-                    {JSON.stringify(error, undefined, 2)}
-
-                    {parsedError &&
-                      `\n\ndecoded error : ${JSON.stringify(parsedError, undefined, 2)}`}
-                  
+
{error.message}
)}
diff --git a/apps/playground/src/components/Ethereum/contracts/index.ts b/apps/playground/src/components/Ethereum/contracts/index.ts index ca23cf08a6..146ae5102c 100644 --- a/apps/playground/src/components/Ethereum/contracts/index.ts +++ b/apps/playground/src/components/Ethereum/contracts/index.ts @@ -1,7 +1,5 @@ import ContractsJson from "./ContractsAddresses.json" import GreeterJson from "./Greeter.json" -export * from "./types" -export * from "./types/common" export const GreeterAbi = GreeterJson diff --git a/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts b/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts deleted file mode 100644 index 371d645268..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { FunctionFragment, Result } from "@ethersproject/abi" -import type { Listener, Provider } from "@ethersproject/providers" -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { - BaseContract, - BigNumber, - BytesLike, - CallOverrides, - ContractTransaction, - Overrides, - PopulatedTransaction, - Signer, - utils, -} from "ethers" - -import type { OnEvent, TypedEvent, TypedEventFilter, TypedListener } from "./common" - -export interface GreeterInterface extends utils.Interface { - functions: { - "greet()": FunctionFragment - "setGreeting(string)": FunctionFragment - } - - getFunction(nameOrSignatureOrTopic: "greet" | "setGreeting"): FunctionFragment - - encodeFunctionData(functionFragment: "greet", values?: undefined): string - encodeFunctionData(functionFragment: "setGreeting", values: [string]): string - - decodeFunctionResult(functionFragment: "greet", data: BytesLike): Result - decodeFunctionResult(functionFragment: "setGreeting", data: BytesLike): Result - - events: {} -} - -export interface Greeter extends BaseContract { - connect(signerOrProvider: Signer | Provider | string): this - attach(addressOrName: string): this - deployed(): Promise - - interface: GreeterInterface - - queryFilter( - event: TypedEventFilter, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise> - - listeners( - eventFilter?: TypedEventFilter - ): Array> - listeners(eventName?: string): Array - removeAllListeners(eventFilter: TypedEventFilter): this - removeAllListeners(eventName?: string): this - off: OnEvent - on: OnEvent - once: OnEvent - removeListener: OnEvent - - functions: { - greet(overrides?: CallOverrides): Promise<[string]> - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } - - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - - callStatic: { - greet(overrides?: CallOverrides): Promise - - setGreeting(_greeting: string, overrides?: CallOverrides): Promise - } - - filters: {} - - estimateGas: { - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } - - populateTransaction: { - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } -} diff --git a/apps/playground/src/components/Ethereum/contracts/types/common.ts b/apps/playground/src/components/Ethereum/contracts/types/common.ts deleted file mode 100644 index 5e38dde419..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/common.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { Listener } from "@ethersproject/providers" -import type { Event, EventFilter } from "ethers" - -export interface TypedEvent = any, TArgsObject = any> extends Event { - args: TArgsArray & TArgsObject -} - -export interface TypedEventFilter<_TEvent extends TypedEvent> extends EventFilter {} - -export interface TypedListener { - (...listenerArg: [...__TypechainArgsArray, TEvent]): void -} - -type __TypechainArgsArray = T extends TypedEvent ? U : never - -export interface OnEvent { - ( - eventFilter: TypedEventFilter, - listener: TypedListener - ): TRes - (eventName: string, listener: Listener): TRes -} - -export type MinEthersFactory = { - deploy(...a: ARGS[]): Promise -} - -export type GetContractTypeFromFactory = F extends MinEthersFactory ? C : never - -export type GetARGsTypeFromFactory = F extends MinEthersFactory - ? Parameters - : never diff --git a/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts b/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts deleted file mode 100644 index 47727c4a66..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Provider, TransactionRequest } from "@ethersproject/providers" -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import { Contract, ContractFactory, Overrides, Signer, utils } from "ethers" - -import type { Greeter, GreeterInterface } from "../Greeter" - -const _abi = [ - { - inputs: [ - { - internalType: "string", - name: "_greeting", - type: "string", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [], - name: "greet", - outputs: [ - { - internalType: "string", - name: "", - type: "string", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "_greeting", - type: "string", - }, - ], - name: "setGreeting", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] - -const _bytecode = - "0x60806040523480156200001157600080fd5b5060405162000c3238038062000c32833981810160405281019062000037919062000278565b6200006760405180606001604052806022815260200162000c1060229139826200008760201b620001ce1760201c565b80600090805190602001906200007f92919062000156565b5050620004c5565b620001298282604051602401620000a0929190620002fe565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200012d60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b8280546200016490620003ea565b90600052602060002090601f016020900481019282620001885760008555620001d4565b82601f10620001a357805160ff1916838001178555620001d4565b82800160010185558215620001d4579182015b82811115620001d3578251825591602001919060010190620001b6565b5b509050620001e39190620001e7565b5090565b5b8082111562000202576000816000905550600101620001e8565b5090565b60006200021d620002178462000362565b62000339565b9050828152602081018484840111156200023657600080fd5b62000243848285620003b4565b509392505050565b600082601f8301126200025d57600080fd5b81516200026f84826020860162000206565b91505092915050565b6000602082840312156200028b57600080fd5b600082015167ffffffffffffffff811115620002a657600080fd5b620002b4848285016200024b565b91505092915050565b6000620002ca8262000398565b620002d68185620003a3565b9350620002e8818560208601620003b4565b620002f381620004b4565b840191505092915050565b600060408201905081810360008301526200031a8185620002bd565b90508181036020830152620003308184620002bd565b90509392505050565b60006200034562000358565b905062000353828262000420565b919050565b6000604051905090565b600067ffffffffffffffff82111562000380576200037f62000485565b5b6200038b82620004b4565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015620003d4578082015181840152602081019050620003b7565b83811115620003e4576000848401525b50505050565b600060028204905060018216806200040357607f821691505b602082108114156200041a576200041962000456565b5b50919050565b6200042b82620004b4565b810181811067ffffffffffffffff821117156200044d576200044c62000485565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61073b80620004d56000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a264697066735822122062b06e5bdee39e73f7ae7ef8606fe9f23da851629e4e297316ce7747f5074b1964736f6c634300080400334465706c6f79696e67206120477265657465722077697468206772656574696e673a" - -type GreeterConstructorParams = [signer?: Signer] | ConstructorParameters - -const isSuperArgs = ( - xs: GreeterConstructorParams -): xs is ConstructorParameters => xs.length > 1 - -export class Greeter__factory extends ContractFactory { - constructor(...args: GreeterConstructorParams) { - if (isSuperArgs(args)) { - super(...args) - } else { - super(_abi, _bytecode, args[0]) - } - } - - override deploy( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise { - return super.deploy(_greeting, overrides || {}) as Promise - } - override getDeployTransaction( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): TransactionRequest { - return super.getDeployTransaction(_greeting, overrides || {}) - } - override attach(address: string): Greeter { - return super.attach(address) as Greeter - } - override connect(signer: Signer): Greeter__factory { - return super.connect(signer) as Greeter__factory - } - - static readonly bytecode = _bytecode - static readonly abi = _abi - static createInterface(): GreeterInterface { - return new utils.Interface(_abi) as GreeterInterface - } - static connect(address: string, signerOrProvider: Signer | Provider): Greeter { - return new Contract(address, _abi, signerOrProvider) as Greeter - } -} diff --git a/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts b/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts deleted file mode 100644 index 0dc323c349..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -export { Greeter__factory } from "./Greeter__factory" diff --git a/apps/playground/src/components/Ethereum/contracts/types/index.ts b/apps/playground/src/components/Ethereum/contracts/types/index.ts deleted file mode 100644 index c171bb5fc8..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -export type { Greeter } from "./Greeter" -export * as factories from "./factories" -export { Greeter__factory } from "./factories/Greeter__factory" diff --git a/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx b/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx index 4532ce42b8..e6d970441a 100644 --- a/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx +++ b/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx @@ -1,17 +1,26 @@ -import { ethers } from "ethers" import { formatUnits } from "ethers/lib/utils.js" import { useCallback, useRef, useState } from "react" -import { erc20ABI, useAccount, useContractRead, useNetwork } from "wagmi" +import { + erc20ABI, + useAccount, + useContractRead, + useNetwork, + usePublicClient, + useWalletClient, +} from "wagmi" import { useDeployment } from "../../../contracts" import { useErc20Contract } from "./context" export const ERC20ContractSelect = () => { const { chain } = useNetwork() - const { isConnected, address: account, connector } = useAccount() + const { isConnected, address: account } = useAccount() const [address, setAddress] = useErc20Contract() + const { data: walletClient } = useWalletClient() + const publicClient = usePublicClient() + const { bytecode } = useDeployment("TestERC20", chain?.id ?? 0) const [isDeploying, setIsDeploying] = useState(false) const [deployError, setDeployError] = useState() @@ -21,18 +30,15 @@ export const ERC20ContractSelect = () => { setIsDeploying(true) setDeployError(undefined) try { - const provider = await connector?.getProvider() - if (!provider || !chain) return + if (!walletClient) throw new Error("No wallet client") - const web3Provider = new ethers.providers.Web3Provider(provider) - const transaction: ethers.providers.TransactionRequest = { - from: account, - data: bytecode, - chainId: chain.id, - } + const hash = await walletClient.sendTransaction({ + data: bytecode as `0x${string}`, + }) + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }) - const txHash = await web3Provider.send("eth_sendTransaction", [transaction]) - const receipt = await web3Provider.waitForTransaction(txHash) if (!receipt.contractAddress) throw new Error("No contract address in receipt") setAddress(receipt.contractAddress as `0x${string}`) @@ -43,7 +49,7 @@ export const ERC20ContractSelect = () => { setDeployError(err as Error) } setIsDeploying(false) - }, [account, bytecode, chain, connector, setAddress]) + }, [bytecode, publicClient, setAddress, walletClient]) const { data: symbol, error: errorSymbol } = useContractRead({ address: address as `0x${string}`, diff --git a/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx b/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx index 90a2538e27..0f50de98da 100644 --- a/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx +++ b/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx @@ -1,16 +1,15 @@ -import { ethers } from "ethers" -import { parseUnits } from "ethers/lib/utils" import { useCallback } from "react" import { useForm } from "react-hook-form" import { useLocalStorage } from "react-use" import { Button } from "talisman-ui" +import { parseUnits } from "viem" import { erc20ABI, useAccount, useContractRead, useContractWrite, usePrepareContractWrite, - useSendTransaction, + useWalletClient, } from "wagmi" import { TransactionReceipt } from "../shared/TransactionReceipt" @@ -24,8 +23,8 @@ const DEFAULT_VALUE: FormData = { } export const ERC20Send = () => { - const { isConnected, address, connector } = useAccount() - + const { isConnected, address } = useAccount() + const { data: walletClient } = useWalletClient() const [contractAddress] = useErc20Contract() const [defaultValues, setDefaultValues] = useLocalStorage("pg:send-erc20", DEFAULT_VALUE) @@ -40,7 +39,7 @@ export const ERC20Send = () => { const formData = watch() - const { data: decimals } = useContractRead({ + const { data: decimals = 18 } = useContractRead({ address: contractAddress as `0x${string}`, abi: erc20ABI, functionName: "decimals", @@ -64,55 +63,32 @@ export const ERC20Send = () => { args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], }) - const { isLoading: writeIsLoading } = useContractWrite({ - address: contractAddress as `0x${string}`, - abi: erc20ABI, - functionName: "transfer", - mode: "recklesslyUnprepared", - args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], - }) - const { - sendTransaction, + isLoading: writeIsLoading, + write: send, + error: sendError, isLoading: sendIsLoading, isSuccess: sendIsSuccess, isError: sendIsError, data: senddata, - error: sendError, - } = useSendTransaction(config) + } = useContractWrite(config) const onSubmit = (data: FormData) => { setDefaultValues(data) - sendTransaction?.() + send?.() } // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(erc20ABI) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "transfer" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [ - formData.recipient, - parseUnits(formData.amount, decimals), - ]) + if (!walletClient) return - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from: address, - to: contractAddress, - data, - }, - ], + walletClient.writeContract({ + address: contractAddress as `0x${string}`, + abi: erc20ABI, + functionName: "transfer", + args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], }) - }, [address, connector, contractAddress, decimals, formData.amount, formData.recipient]) + }, [contractAddress, decimals, formData.amount, formData.recipient, walletClient]) if (!isConnected) return null diff --git a/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx b/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx index b1bf5f702e..a2356d0893 100644 --- a/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx +++ b/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx @@ -1,16 +1,25 @@ -import { ethers } from "ethers" import { useCallback, useRef, useState } from "react" -import { erc721ABI, useAccount, useContractRead, useNetwork } from "wagmi" +import { + erc721ABI, + useAccount, + useContractRead, + useNetwork, + usePublicClient, + useWalletClient, +} from "wagmi" import { useDeployment } from "../../../contracts" import { useErc721Contract } from "./context" export const ERC721ContractSelect = () => { const { chain } = useNetwork() - const { isConnected, address: account, connector } = useAccount() + const { isConnected, address: account } = useAccount() const [address, setAddress] = useErc721Contract() + const { data: walletClient } = useWalletClient() + const publicClient = usePublicClient() + const { bytecode } = useDeployment("TestERC721", chain?.id ?? 0) const [isDeploying, setIsDeploying] = useState(false) const [deployError, setDeployError] = useState() @@ -20,18 +29,15 @@ export const ERC721ContractSelect = () => { setIsDeploying(true) setDeployError(undefined) try { - const provider = await connector?.getProvider() - if (!provider || !chain) return + if (!walletClient) throw new Error("No wallet client") - const web3Provider = new ethers.providers.Web3Provider(provider) - const transaction: ethers.providers.TransactionRequest = { - from: account, - data: bytecode, - chainId: chain.id, - } + const hash = await walletClient.sendTransaction({ + data: bytecode as `0x${string}`, + }) + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }) - const txHash = await web3Provider.send("eth_sendTransaction", [transaction]) - const receipt = await web3Provider.waitForTransaction(txHash) if (!receipt.contractAddress) throw new Error("No contract address in receipt") setAddress(receipt.contractAddress as `0x${string}`) @@ -42,7 +48,7 @@ export const ERC721ContractSelect = () => { setDeployError(err as Error) } setIsDeploying(false) - }, [account, bytecode, chain, connector, setAddress]) + }, [bytecode, publicClient, setAddress, walletClient]) const { data: symbol, error: errorSymbol } = useContractRead({ address: address as `0x${string}`, diff --git a/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx b/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx index 8eee47d072..2dae13a03e 100644 --- a/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx +++ b/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx @@ -1,16 +1,14 @@ -import { BigNumber, ethers } from "ethers" import { useCallback, useEffect, useState } from "react" import { useForm } from "react-hook-form" import { useLocalStorage } from "react-use" import { Button } from "talisman-ui" import { - erc20ABI, erc721ABI, useAccount, useContractRead, useContractWrite, usePrepareContractWrite, - useSendTransaction, + useWalletClient, } from "wagmi" import { TransactionReceipt } from "../shared/TransactionReceipt" @@ -65,8 +63,8 @@ const DEFAULT_VALUE: FormData = { } export const ERC721Send = () => { - const { isConnected, address, connector } = useAccount() - + const { isConnected, address } = useAccount() + const { data: walletClient } = useWalletClient() const [contractAddress] = useErc721Contract() const [defaultValues, setDefaultValues] = useLocalStorage("pg:send-erc721", DEFAULT_VALUE) @@ -85,7 +83,7 @@ export const ERC721Send = () => { address: contractAddress as `0x${string}`, abi: erc721ABI, functionName: "tokenURI", - args: [BigNumber.from(formData.tokenId)], + args: [BigInt(formData.tokenId)], enabled: !!contractAddress && !!formData.tokenId?.length, watch: true, }) @@ -104,33 +102,17 @@ export const ERC721Send = () => { abi: erc721ABI, functionName: "safeTransferFrom", enabled: !!contractAddress && !!balanceOfSelfData, - args: [ - address as `0x${string}`, - formData.recipient as `0x${string}`, - BigNumber.from(formData.tokenId), - ], - }) - - const { isLoading: writeIsLoading } = useContractWrite({ - address: contractAddress as `0x${string}`, - abi: erc721ABI, - functionName: "safeTransferFrom", - mode: "recklesslyUnprepared", - args: [ - address as `0x${string}`, - formData.recipient as `0x${string}`, - BigNumber.from(formData.tokenId), - ], + args: [address as `0x${string}`, formData.recipient as `0x${string}`, BigInt(formData.tokenId)], }) const { - sendTransaction, + write: sendTransaction, isLoading: sendIsLoading, isSuccess: sendIsSuccess, isError: sendIsError, data: senddata, error: sendError, - } = useSendTransaction(config) + } = useContractWrite(config) const onSubmit = (data: FormData) => { setDefaultValues(data) @@ -139,32 +121,19 @@ export const ERC721Send = () => { // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(erc20ABI) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "transfer" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [ - address, - formData.recipient, - formData.tokenId, - ]) - - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from: address, - to: contractAddress, - data, - }, + if (!walletClient) return + + walletClient.writeContract({ + address: contractAddress as `0x${string}`, + abi: erc721ABI, + functionName: "safeTransferFrom", + args: [ + address as `0x${string}`, + formData.recipient as `0x${string}`, + BigInt(formData.tokenId), ], }) - }, [address, connector, contractAddress, formData]) + }, [address, contractAddress, formData.recipient, formData.tokenId, walletClient]) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [metadata, setMetadata] = useState() @@ -235,7 +204,7 @@ export const ERC721Send = () => {