diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 7debdcfb2e..4293162cd4 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -1949,13 +1949,22 @@ export default class EthereumApi implements Api { const wallet = this.#wallet; const isKnownAccount = wallet.knownAccounts.has(fromString); - const privateKey = wallet.unlockedAccounts.get(fromString); + let privateKey = wallet.unlockedAccounts.get(fromString); if (privateKey === undefined) { - const msg = isKnownAccount - ? "authentication needed: passphrase or unlock" - : "sender account not recognized"; - throw new Error(msg); + // if unlock all option is set to true + // then create a fake private key for unknown account + if(this.#options.wallet.unlockAll) + { + privateKey = wallet.createFakePrivateKey(fromString) + } + else + { + const msg = isKnownAccount + ? "authentication needed: passphrase or unlock" + : "sender account not recognized"; + throw new Error(msg); + } } await autofillDefaultTransactionValues( diff --git a/src/chains/ethereum/ethereum/tests/api/eth/sendTransaction.test.ts b/src/chains/ethereum/ethereum/tests/api/eth/sendTransaction.test.ts index 80b8f172af..525141e971 100644 --- a/src/chains/ethereum/ethereum/tests/api/eth/sendTransaction.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/eth/sendTransaction.test.ts @@ -11,6 +11,8 @@ import Wallet from "../../../src/wallet"; import { SECP256K1_N } from "@ganache/secp256k1"; import { Data, Quantity } from "@ganache/utils"; +const ZERO_ADDRESS = "0x" + "0".repeat(40); + describe("api", () => { describe("eth", () => { describe("sendTransaction", () => { @@ -223,7 +225,6 @@ describe("api", () => { describe("unlocked accounts", () => { it("can send transactions from an unlocked 0x0 address", async () => { - const ZERO_ADDRESS = "0x" + "0".repeat(40); const provider = await getProvider({ miner: { defaultGasPrice: 0 @@ -357,6 +358,83 @@ describe("api", () => { assert(largeKey.pk < SECP256K1_N); }); }); + + describe("unlock all accounts", async () => { + const providerOptions : EthereumProviderOptions = { + miner: { + defaultGasPrice: 0 + }, + chain: { + // use berlin here because we just want to test if we can use the + // "zero" address, and we do this by transferring value while + // setting the gasPrice to `0`. This isn't possible after the + // `london` hardfork currently, as we don't provide an option to + // allow for a 0 `maxFeePerGas` value. + // TODO: remove once we have a configurable `maxFeePerGas` + hardfork: "berlin" + } + } + + it("can send transactions from an unlocked 0x0 address when unlock all is true", async () => { + providerOptions.wallet = { + unlockAll: true + } + const provider = await getProvider( + providerOptions + ); + const [from] = await provider.send("eth_accounts"); + + await provider.send("eth_subscribe", ["newHeads"]); + const initialZeroBalance = "0x1234"; + await provider.send("eth_sendTransaction", [ + { from: from, to: ZERO_ADDRESS, value: initialZeroBalance } + ]); + await provider.once("message"); + const initialBalance = await provider.send("eth_getBalance", [ + ZERO_ADDRESS + ]); + assert.strictEqual( + initialBalance, + initialZeroBalance, + "Zero address's balance isn't correct" + ); + const removeValueFromZeroAmount = "0x123"; + await provider.send("eth_sendTransaction", [ + { from: ZERO_ADDRESS, to: from, value: removeValueFromZeroAmount } + ]); + await provider.once("message"); + const afterSendBalance = BigInt( + await provider.send("eth_getBalance", [ZERO_ADDRESS]) + ); + assert.strictEqual( + BigInt(initialZeroBalance) - BigInt(removeValueFromZeroAmount), + afterSendBalance, + "Zero address's balance isn't correct" + ); + }); + + it("cannot send transactions from an unlocked 0x0 address when unlock all is false", async () => { + providerOptions.wallet = { + unlockAll: false + } + const provider = await getProvider( + providerOptions + ); + const [from] = await provider.send("eth_accounts"); + + await provider.send("eth_subscribe", ["newHeads"]); + const removeValueFromZeroAmount = "0x123"; + const badSend = async () => { + return provider.send("eth_sendTransaction", [ + { from: ZERO_ADDRESS, to: from, value: removeValueFromZeroAmount } + ]); + }; + await assert.rejects( + badSend, + "Error: sender account not recognized" + ); + }); + }); }); }); }); diff --git a/src/chains/ethereum/options/src/wallet-options.ts b/src/chains/ethereum/options/src/wallet-options.ts index f4eeeb46e3..82203c899f 100644 --- a/src/chains/ethereum/options/src/wallet-options.ts +++ b/src/chains/ethereum/options/src/wallet-options.ts @@ -202,6 +202,16 @@ export type WalletConfig = { hd_path: string; }; }; + + /** + * Unlock all accounts irrespective of known or unknown status. + * + * @defaultValue false + */ + unlockAll: { + type: boolean; + hasDefault: true; + }; }; exclusiveGroups: [ ["accounts", "totalAccounts"], @@ -326,5 +336,13 @@ export const WalletOptions: Definitions = { default: () => ["m", "44'", "60'", "0'", "0"], legacyName: "hd_path", cliType: "string" - } + }, + unlockAll: { + normalize, + cliDescription: + "Unlock all accounts irrespective of known or unknown status.", + default: () => false, + cliAliases: ["unlockAll"], + cliType: "boolean" + }, };