diff --git a/contracts/clients/src/BlsProvider.ts b/contracts/clients/src/BlsProvider.ts index ca1eb36b..926558e7 100644 --- a/contracts/clients/src/BlsProvider.ts +++ b/contracts/clients/src/BlsProvider.ts @@ -2,7 +2,7 @@ import { ethers, BigNumber } from "ethers"; import { Deferrable } from "ethers/lib/utils"; -import { ActionData, Bundle } from "./signer/types"; +import { ActionData, Bundle, PublicKey } from "./signer/types"; import Aggregator, { BundleReceipt } from "./Aggregator"; import BlsSigner, { TransactionBatchResponse, @@ -14,14 +14,19 @@ import BlsWalletWrapper from "./BlsWalletWrapper"; import { AggregatorUtilities__factory, BLSWallet__factory, + VerificationGateway__factory, } from "../typechain-types"; import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee"; +export type PublicKeyLinkedToActions = { + publicKey: PublicKey; + actions: Array; +}; + export default class BlsProvider extends ethers.providers.JsonRpcProvider { readonly aggregator: Aggregator; readonly verificationGatewayAddress: string; readonly aggregatorUtilitiesAddress: string; - signer!: BlsSigner; constructor( aggregatorUrl: string, @@ -39,34 +44,43 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { override async estimateGas( transaction: Deferrable, ): Promise { - if (!transaction.to) { + const resolvedTransaction = await ethers.utils.resolveProperties( + transaction, + ); + + if (!resolvedTransaction.to) { throw new TypeError("Transaction.to should be defined"); } - - // TODO: bls-wallet #413 Move references to private key outside of BlsSigner. - // Without doing this, we would have to call `const signer = this.getSigner(privateKey)`. - // We do not want to pass the private key to this method. - if (!this.signer) { - throw new Error("Call provider.getSigner first"); + if (!resolvedTransaction.from) { + throw new TypeError("Transaction.from should be defined"); } const action: ActionData = { - ethValue: transaction.value?.toString() ?? "0", - contractAddress: transaction.to.toString(), - encodedFunction: transaction.data?.toString() ?? "0x", + ethValue: resolvedTransaction.value?.toString() ?? "0", + contractAddress: resolvedTransaction.to.toString(), + encodedFunction: resolvedTransaction.data?.toString() ?? "0x", }; - const nonce = await BlsWalletWrapper.Nonce( - this.signer.wallet.PublicKey(), - this.verificationGatewayAddress, - this, + const nonce = await this.getTransactionCount( + resolvedTransaction.from.toString(), ); const actionWithFeePaymentAction = this._addFeePaymentActionForFeeEstimation([action]); + // TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle + // There is no way to estimate the cost of a bundle without signing a bundle. The + // alternative would be to use a signer instance in this method which is undesirable, + // as this would result in tight coupling between a provider and a signer. + const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey(); + const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect( + throwawayPrivateKey, + this.verificationGatewayAddress, + this, + ); + const feeEstimate = await this.aggregator.estimateFee( - this.signer.wallet.sign({ + throwawayBlsWalletWrapper.sign({ nonce, actions: [...actionWithFeePaymentAction], }), @@ -79,13 +93,6 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { override async sendTransaction( signedTransaction: string | Promise, ): Promise { - // TODO: bls-wallet #413 Move references to private key outside of BlsSigner. - // Without doing this, we would have to call `const signer = this.getSigner(privateKey)`. - // We do not want to pass the private key to this method. - if (!this.signer) { - throw new Error("Call provider.getSigner first"); - } - const resolvedTransaction = await signedTransaction; const bundle: Bundle = JSON.parse(resolvedTransaction); @@ -107,23 +114,16 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { encodedFunction: bundle.operations[0].actions[0].encodedFunction, }; - return this.signer.constructTransactionResponse( + return await this._constructTransactionResponse( actionData, + bundle.senderPublicKeys[0], result.hash, - this.signer.wallet.address, ); } async sendTransactionBatch( signedTransactionBatch: string, ): Promise { - // TODO: bls-wallet #413 Move references to private key outside of BlsSigner. - // Without doing this, we would have to call `const signer = this.getSigner(privateKey)`. - // We do not want to pass the private key to this method. - if (!this.signer) { - throw new Error("Call provider.getSigner first"); - } - const bundle: Bundle = JSON.parse(signedTransactionBatch); const result = await this.aggregator.add(bundle); @@ -132,14 +132,20 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { throw new Error(JSON.stringify(result.failures)); } - const actionData: Array = bundle.operations - .map((operation) => operation.actions) - .flat(); + const publicKeysLinkedToActions: Array = + bundle.senderPublicKeys.map((publicKey, i) => { + const operation = bundle.operations[i]; + const actions = operation.actions; - return this.signer.constructTransactionBatchResponse( - actionData, + return { + publicKey, + actions, + }; + }); + + return await this._constructTransactionBatchResponse( + publicKeysLinkedToActions, result.hash, - this.signer.wallet.address, ); } @@ -147,18 +153,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { privateKey: string, addressOrIndex?: string | number, ): BlsSigner { - if (this.signer) { - return this.signer; - } - - const signer = new BlsSigner( - _constructorGuard, - this, - privateKey, - addressOrIndex, - ); - this.signer = signer; - return signer; + return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex); } override getUncheckedSigner( @@ -295,4 +290,104 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider { }, ]; } + + async _constructTransactionResponse( + action: ActionData, + publicKey: PublicKey, + hash: string, + nonce?: BigNumber, + ): Promise { + const chainId = await this.send("eth_chainId", []); + + if (!nonce) { + nonce = await BlsWalletWrapper.Nonce( + publicKey, + this.verificationGatewayAddress, + this, + ); + } + + const verificationGateway = VerificationGateway__factory.connect( + this.verificationGatewayAddress, + this, + ); + const from = await BlsWalletWrapper.AddressFromPublicKey( + publicKey, + verificationGateway, + ); + + return { + hash, + to: action.contractAddress, + from, + nonce: nonce.toNumber(), + gasLimit: BigNumber.from("0x0"), + data: action.encodedFunction.toString(), + value: BigNumber.from(action.ethValue), + chainId: parseInt(chainId, 16), + type: 2, + confirmations: 1, + wait: (confirmations?: number) => { + return this.waitForTransaction(hash, confirmations); + }, + }; + } + + async _constructTransactionBatchResponse( + publicKeysLinkedToActions: Array, + hash: string, + nonce?: BigNumber, + ): Promise { + const chainId = await this.send("eth_chainId", []); + const verificationGateway = VerificationGateway__factory.connect( + this.verificationGatewayAddress, + this, + ); + + const transactions: Array = []; + + for (const publicKeyLinkedToActions of publicKeysLinkedToActions) { + const from = await BlsWalletWrapper.AddressFromPublicKey( + publicKeyLinkedToActions.publicKey, + verificationGateway, + ); + + if (!nonce) { + nonce = await BlsWalletWrapper.Nonce( + publicKeyLinkedToActions.publicKey, + this.verificationGatewayAddress, + this, + ); + } + + for (const action of publicKeyLinkedToActions.actions) { + if (action.contractAddress === this.aggregatorUtilitiesAddress) { + break; + } + + transactions.push({ + hash, + to: action.contractAddress, + from, + nonce: nonce!.toNumber(), + gasLimit: BigNumber.from("0x0"), + data: action.encodedFunction.toString(), + value: BigNumber.from(action.ethValue), + chainId: parseInt(chainId, 16), + type: 2, + confirmations: 1, + wait: (confirmations?: number) => { + return this.waitForTransaction(hash, confirmations); + }, + }); + } + } + + return { + transactions, + awaitBatchReceipt: (confirmations?: number) => { + return this.waitForTransaction(hash, confirmations); + }, + }; + } } diff --git a/contracts/clients/src/BlsSigner.ts b/contracts/clients/src/BlsSigner.ts index 7f39e4c7..fee359a2 100644 --- a/contracts/clients/src/BlsSigner.ts +++ b/contracts/clients/src/BlsSigner.ts @@ -8,7 +8,7 @@ import { RLP, } from "ethers/lib/utils"; -import BlsProvider from "./BlsProvider"; +import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider"; import BlsWalletWrapper from "./BlsWalletWrapper"; import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee"; import { ActionData, bundleToDto } from "./signer"; @@ -61,7 +61,7 @@ export default class BlsSigner extends Signer { constructor( constructorGuard: Record, provider: BlsProvider, - privateKey: string, + privateKey: string | Promise, readonly addressOrIndex?: string | number, ) { super(); @@ -92,9 +92,10 @@ export default class BlsSigner extends Signer { } } - private async initializeWallet(privateKey: string) { + private async initializeWallet(privateKey: string | Promise) { + const resolvedPrivateKey = await privateKey; this.wallet = await BlsWalletWrapper.connect( - privateKey, + resolvedPrivateKey, this.verificationGatewayAddress, this.provider, ); @@ -109,6 +110,8 @@ export default class BlsSigner extends Signer { throw new TypeError("Transaction.to should be defined"); } + const validatedTransaction = await this._validateTransaction(transaction); + const nonce = await BlsWalletWrapper.Nonce( this.wallet.PublicKey(), this.verificationGatewayAddress, @@ -116,12 +119,12 @@ export default class BlsSigner extends Signer { ); const action: ActionData = { - ethValue: transaction.value?.toString() ?? "0", - contractAddress: transaction.to.toString(), - encodedFunction: transaction.data?.toString() ?? "0x", + ethValue: validatedTransaction.value?.toString() ?? "0", + contractAddress: validatedTransaction.to!.toString(), + encodedFunction: validatedTransaction.data?.toString() ?? "0x", }; - const feeEstimate = await this.provider.estimateGas(transaction); + const feeEstimate = await this.provider.estimateGas(validatedTransaction); const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee( [action], feeEstimate, @@ -137,10 +140,10 @@ export default class BlsSigner extends Signer { throw new Error(JSON.stringify(result.failures)); } - return this.constructTransactionResponse( + return await this.provider._constructTransactionResponse( action, + bundle.senderPublicKeys[0], result.hash, - this.wallet.address, nonce, ); } @@ -150,13 +153,13 @@ export default class BlsSigner extends Signer { ): Promise { await this.initPromise; + const validatedTransactionBatch = await this._validateTransactionBatch( + transactionBatch, + ); + let nonce: BigNumber; if (transactionBatch.batchOptions) { - const validatedBatchOptions = await this._validateBatchOptions( - transactionBatch.batchOptions, - ); - - nonce = validatedBatchOptions.nonce as BigNumber; + nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber; } else { nonce = await BlsWalletWrapper.Nonce( this.wallet.PublicKey(), @@ -166,11 +169,7 @@ export default class BlsSigner extends Signer { } const actions: Array = transactionBatch.transactions.map( - (transaction, i) => { - if (!transaction.to) { - throw new TypeError(`Transaction.to is missing on transaction ${i}`); - } - + (transaction) => { return { ethValue: transaction.value?.toString() ?? "0", contractAddress: transaction.to!.toString(), @@ -208,10 +207,20 @@ export default class BlsSigner extends Signer { throw new Error(JSON.stringify(result.failures)); } - return this.constructTransactionBatchResponse( - actions, + const publicKeysLinkedToActions: Array = + bundle.senderPublicKeys.map((publicKey, i) => { + const operation = bundle.operations[i]; + const actions = operation.actions; + + return { + publicKey, + actions, + }; + }); + + return await this.provider._constructTransactionBatchResponse( + publicKeysLinkedToActions, result.hash, - this.wallet.address, nonce, ); } @@ -226,83 +235,6 @@ export default class BlsSigner extends Signer { return this._address; } - // Construct a response that follows the ethers TransactionResponse type - async constructTransactionResponse( - action: ActionData, - hash: string, - from: string, - nonce?: BigNumber, - ): Promise { - await this.initPromise; - const chainId = await this.getChainId(); - if (!nonce) { - nonce = await BlsWalletWrapper.Nonce( - this.wallet.PublicKey(), - this.verificationGatewayAddress, - this.provider, - ); - } - - return { - hash, - to: action.contractAddress, - from, - nonce: nonce.toNumber(), - gasLimit: BigNumber.from("0x0"), - data: action.encodedFunction.toString(), - value: BigNumber.from(action.ethValue), - chainId, - type: 2, - confirmations: 1, - wait: (confirmations?: number) => { - return this.provider.waitForTransaction(hash, confirmations); - }, - }; - } - - async constructTransactionBatchResponse( - actions: Array, - hash: string, - from: string, - nonce?: BigNumber, - ): Promise { - await this.initPromise; - const chainId = await this.getChainId(); - if (!nonce) { - nonce = await BlsWalletWrapper.Nonce( - this.wallet.PublicKey(), - this.verificationGatewayAddress, - this.provider, - ); - } - - const transactions: Array = - actions.map((action) => { - return { - hash, - to: action.contractAddress, - from, - nonce: nonce!.toNumber(), - gasLimit: BigNumber.from("0x0"), - data: action.encodedFunction.toString(), - value: BigNumber.from(action.ethValue), - chainId, - type: 2, - confirmations: 1, - wait: (confirmations?: number) => { - return this.provider.waitForTransaction(hash, confirmations); - }, - }; - }); - - return { - transactions, - awaitBatchReceipt: (confirmations?: number) => { - return this.provider.waitForTransaction(hash, confirmations); - }, - }; - } - /** * This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider. * The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients. @@ -324,14 +256,12 @@ export default class BlsSigner extends Signer { ): Promise { await this.initPromise; - if (!transaction.to) { - throw new TypeError("Transaction.to should be defined"); - } + const validatedTransaction = await this._validateTransaction(transaction); const action: ActionData = { - ethValue: transaction.value?.toString() ?? "0", - contractAddress: transaction.to.toString(), - encodedFunction: transaction.data?.toString() ?? "0x", + ethValue: validatedTransaction.value?.toString() ?? "0", + contractAddress: validatedTransaction.to!.toString(), + encodedFunction: validatedTransaction.data?.toString() ?? "0x", }; const nonce = await BlsWalletWrapper.Nonce( @@ -340,7 +270,7 @@ export default class BlsSigner extends Signer { this.provider, ); - const feeEstimate = await this.provider.estimateGas(transaction); + const feeEstimate = await this.provider.estimateGas(validatedTransaction); const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee( [action], @@ -360,13 +290,13 @@ export default class BlsSigner extends Signer { ): Promise { await this.initPromise; + const validatedTransactionBatch = await this._validateTransactionBatch( + transactionBatch, + ); + let nonce: BigNumber; if (transactionBatch.batchOptions) { - const validatedBatchOptions = await this._validateBatchOptions( - transactionBatch.batchOptions, - ); - - nonce = validatedBatchOptions.nonce as BigNumber; + nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber; } else { nonce = await BlsWalletWrapper.Nonce( this.wallet.PublicKey(), @@ -376,11 +306,7 @@ export default class BlsSigner extends Signer { } const actions: Array = transactionBatch.transactions.map( - (transaction, i) => { - if (!transaction.to) { - throw new TypeError(`Transaction.to is missing on transaction ${i}`); - } - + (transaction) => { return { ethValue: transaction.value?.toString() ?? "0", contractAddress: transaction.to!.toString(), @@ -464,6 +390,54 @@ export default class BlsSigner extends Signer { throw new Error("_legacySignMessage() is not implemented"); } + async _validateTransaction( + transaction: Deferrable, + ): Promise { + const resolvedTransaction = await ethers.utils.resolveProperties( + transaction, + ); + + if (!resolvedTransaction.to) { + throw new TypeError("Transaction.to should be defined"); + } + + if (!resolvedTransaction.from) { + resolvedTransaction.from = await this.getAddress(); + } + + return resolvedTransaction; + } + + async _validateTransactionBatch( + transactionBatch: TransactionBatch, + ): Promise { + const signerAddress = await this.getAddress(); + + const validatedTransactions: Array = + transactionBatch.transactions.map((transaction, i) => { + if (!transaction.to) { + throw new TypeError(`Transaction.to is missing on transaction ${i}`); + } + + if (!transaction.from) { + transaction.from = signerAddress; + } + + return { + ...transaction, + }; + }); + + const validatedBatchOptions = transactionBatch.batchOptions + ? await this._validateBatchOptions(transactionBatch.batchOptions) + : transactionBatch.batchOptions; + + return { + transactions: validatedTransactions, + batchOptions: validatedBatchOptions, + }; + } + async _validateBatchOptions( batchOptions: BatchOptions, ): Promise { @@ -489,7 +463,7 @@ export class UncheckedBlsSigner extends BlsSigner { const transactionResponse = await super.sendTransaction(transaction); return { hash: transactionResponse.hash, - nonce: 1, + nonce: NaN, gasLimit: BigNumber.from(0), gasPrice: BigNumber.from(0), data: "", diff --git a/contracts/clients/test/BlsProvider.test.ts b/contracts/clients/test/BlsProvider.test.ts index 5b1cea67..7cd24c58 100644 --- a/contracts/clients/test/BlsProvider.test.ts +++ b/contracts/clients/test/BlsProvider.test.ts @@ -25,7 +25,7 @@ describe("BlsProvider", () => { rpcUrl = "http://localhost:8545"; network = { name: "localhost", - chainId: 0x7a69, + chainId: 0x539, // 1337 }; privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey(); @@ -60,11 +60,12 @@ describe("BlsProvider", () => { expect(uncheckedBlsSigner).to.be.instanceOf(UncheckedBlsSigner); }); - it("should return a new signer if one has not been instantiated", async () => { + it("should return a new signer", async () => { // Arrange + const newVerificationGateway = "newMockVerificationGatewayAddress"; const newBlsProvider = new Experimental.BlsProvider( aggregatorUrl, - verificationGateway, + newVerificationGateway, aggregatorUtilities, rpcUrl, network, @@ -77,41 +78,37 @@ describe("BlsProvider", () => { // Assert expect(newBlsSigner).to.not.equal(blsSigner); - expect(newBlsSigner).to.equal(newBlsProvider.getSigner(newPrivateKey)); - }); - - it("should throw an error when this.signer has not been assigned", async () => { - // Arrange - const newBlsProvider = new Experimental.BlsProvider( - aggregatorUrl, + expect(newBlsSigner.provider.verificationGatewayAddress).to.not.equal( verificationGateway, - aggregatorUtilities, - rpcUrl, - network, ); + expect(newBlsSigner.provider.verificationGatewayAddress).to.equal( + newVerificationGateway, + ); + }); - const recipient = ethers.Wallet.createRandom().address; - const value = parseEther("1"); - const transactionRequest = { - to: recipient, - value, + it("should throw an error estimating gas when 'transaction.to' has not been defined", async () => { + // Arrange + const transaction = { + value: parseEther("1"), + // Explicitly omit 'to' }; // Act - const gasEstimate = async () => - await newBlsProvider.estimateGas(transactionRequest); + const result = async () => await blsProvider.estimateGas(transaction); // Assert - await expect(gasEstimate()).to.be.rejectedWith( - Error, - "Call provider.getSigner first", + await expect(result()).to.be.rejectedWith( + TypeError, + "Transaction.to should be defined", ); }); - it("should throw an error estimating gas when 'transaction.to' has not been defined", async () => { + it("should throw an error estimating gas when 'transaction.from' has not been defined", async () => { // Arrange const transaction = { value: parseEther("1"), + to: ethers.Wallet.createRandom().address, + // Explicitly omit 'from' }; // Act @@ -120,43 +117,104 @@ describe("BlsProvider", () => { // Assert await expect(result()).to.be.rejectedWith( TypeError, - "Transaction.to should be defined", + "Transaction.from should be defined", ); }); - it("should throw an error sending a transaction when this.signer is not defined", async () => { + it("should return the connection info for the provider", () => { // Arrange - const newBlsProvider = new Experimental.BlsProvider( - aggregatorUrl, - verificationGateway, - aggregatorUtilities, - rpcUrl, - network, - ); - const signedTransaction = blsSigner.signTransaction({ - to: ethers.Wallet.createRandom().address, - value: parseEther("1"), - }); + const expectedConnection = regularProvider.connection; + + // Act + const connection = blsProvider.connection; + + // Assert + expect(connection).to.deep.equal(expectedConnection); + }); + + it("should throw an error when sending invalid signed transactions", async () => { + // Arrange + const invalidTransaction = "Invalid signed transaction"; // Act const result = async () => - await newBlsProvider.sendTransaction(signedTransaction); + await blsProvider.sendTransaction(invalidTransaction); + const batchResult = async () => + await blsProvider.sendTransaction(invalidTransaction); // Assert await expect(result()).to.be.rejectedWith( Error, - "Call provider.getSigner first", + "Unexpected token I in JSON at position 0", + ); + await expect(batchResult()).to.be.rejectedWith( + Error, + "Unexpected token I in JSON at position 0", ); }); - it("should return the connection info for the provider", () => { + it("should get the polling interval", async () => { // Arrange - const expectedConnection = regularProvider.connection; + const expectedpollingInterval = 4000; // default + const updatedInterval = 1000; // Act - const connection = blsProvider.connection; + const pollingInterval = blsProvider.pollingInterval; + blsProvider.pollingInterval = updatedInterval; + const updatedPollingInterval = blsProvider.pollingInterval; // Assert - expect(connection).to.deep.equal(expectedConnection); + expect(pollingInterval).to.equal(expectedpollingInterval); + expect(updatedPollingInterval).to.equal(updatedInterval); + }); + + it("should get the event listener count and remove all listeners", async () => { + blsProvider.on("block", () => {}); + blsProvider.on("error", () => {}); + expect(blsProvider.listenerCount("block")).to.equal(1); + expect(blsProvider.listenerCount("error")).to.equal(1); + expect(blsProvider.listenerCount()).to.equal(2); + + blsProvider.removeAllListeners(); + expect(blsProvider.listenerCount("block")).to.equal(0); + expect(blsProvider.listenerCount("error")).to.equal(0); + expect(blsProvider.listenerCount()).to.equal(0); + }); + + it("should return true and an array of listeners if polling", async () => { + // Arrange + const expectedListener = () => {}; + + // Act + blsProvider.on("block", expectedListener); + const listeners = blsProvider.listeners("block"); + const isPolling = blsProvider.polling; + blsProvider.removeAllListeners(); + + // Assert + expect(listeners).to.deep.equal([expectedListener]); + expect(isPolling).to.be.true; + }); + + it("should be a provider", async () => { + // Arrange & Act + const isProvider = Experimental.BlsProvider.isProvider(blsProvider); + const isProviderWithInvalidProvider = + Experimental.BlsProvider.isProvider(blsSigner); + + // Assert + expect(isProvider).to.equal(true); + expect(isProviderWithInvalidProvider).to.equal(false); + }); + + it("should a return a promise which will stall until the network has heen established", async () => { + // Arrange + const expectedReady = { name: "localhost", chainId: 1337 }; + + // Act + const ready = await blsProvider.ready; + + // Assert + expect(ready).to.deep.equal(expectedReady); }); }); diff --git a/contracts/package.json b/contracts/package.json index 9f3c5f21..0d44a220 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -56,6 +56,7 @@ "mcl-wasm": "^1.0.3", "prettier": "^2.7.1", "prettier-plugin-solidity": "^1.0.0-beta.24", + "sinon": "^15.0.2", "solhint": "^3.3.7", "solidity-coverage": "^0.8.2", "ts-node": "^10.9.1", diff --git a/contracts/test-integration/BlsProvider.test.ts b/contracts/test-integration/BlsProvider.test.ts index fa61cb32..de9104dc 100644 --- a/contracts/test-integration/BlsProvider.test.ts +++ b/contracts/test-integration/BlsProvider.test.ts @@ -85,11 +85,11 @@ describe("BlsProvider", () => { it("should estimate gas without throwing an error", async () => { // Arrange - const recipient = ethers.Wallet.createRandom().address; - const transactionAmount = parseEther("1"); + const getAddressPromise = blsSigner.getAddress(); const transactionRequest = { - to: recipient, - value: transactionAmount, + to: ethers.Wallet.createRandom().address, + value: parseEther("1"), + from: getAddressPromise, }; // Act @@ -106,15 +106,11 @@ describe("BlsProvider", () => { const expectedBalance = parseEther("1"); const balanceBefore = await blsProvider.getBalance(recipient); - const unsignedTransaction = { + const signedTransaction = await blsSigner.signTransaction({ value: expectedBalance.toString(), to: recipient, data: "0x", - }; - - const signedTransaction = await blsSigner.signTransaction( - unsignedTransaction, - ); + }); // Act const transaction = await blsProvider.sendTransaction(signedTransaction); @@ -187,25 +183,19 @@ describe("BlsProvider", () => { it("should get the account nonce when the signer constructs the transaction response", async () => { // Arrange const spy = chai.spy.on(BlsWalletWrapper, "Nonce"); - const recipient = ethers.Wallet.createRandom().address; - const expectedBalance = parseEther("1"); - - const unsignedTransaction = { - value: expectedBalance.toString(), - to: recipient, + const signedTransaction = await blsSigner.signTransaction({ + value: parseEther("1"), + to: ethers.Wallet.createRandom().address, data: "0x", - }; - const signedTransaction = await blsSigner.signTransaction( - unsignedTransaction, - ); + }); // Act await blsProvider.sendTransaction(signedTransaction); // Assert - // Once when calling "signer.signTransaction", once when calling "blsProvider.estimateGas", and once when calling "blsSigner.constructTransactionResponse". + // Once when calling "signer.signTransaction", and once when calling "blsSigner.constructTransactionResponse". // This unit test is concerned with the latter being called. - expect(spy).to.have.been.called.exactly(3); + expect(spy).to.have.been.called.exactly(2); chai.spy.restore(spy); }); @@ -233,27 +223,6 @@ describe("BlsProvider", () => { ); }); - it("should throw an error when sending invalid signed transactions", async () => { - // Arrange - const invalidTransaction = "Invalid signed transaction"; - - // Act - const result = async () => - await blsProvider.sendTransaction(invalidTransaction); - const batchResult = async () => - await blsProvider.sendTransaction(invalidTransaction); - - // Assert - await expect(result()).to.be.rejectedWith( - Error, - "Unexpected token I in JSON at position 0", - ); - await expect(batchResult()).to.be.rejectedWith( - Error, - "Unexpected token I in JSON at position 0", - ); - }); - it("should send a batch of ETH transfers (empty calls) given a valid bundle", async () => { // Arrange const expectedAmount = parseEther("1"); @@ -290,7 +259,7 @@ describe("BlsProvider", () => { ); }); - it("should send a batch of ETH transfers (empty calls) given two aggregated bundles", async () => { + it("should send a batch of ETH transfers (empty calls) given two aggregated bundles and return a transaction batch response", async () => { // Arrange const expectedAmount = parseEther("1"); const verySafeFee = parseEther("0.1"); @@ -336,10 +305,10 @@ describe("BlsProvider", () => { ]); // Act - const result = await blsProvider.sendTransactionBatch( + const transactionBatchResponse = await blsProvider.sendTransactionBatch( JSON.stringify(bundleToDto(aggregatedBundle)), ); - await result.awaitBatchReceipt(); + await transactionBatchResponse.awaitBatchReceipt(); // Assert expect(await blsProvider.getBalance(firstRecipient)).to.equal( @@ -348,6 +317,46 @@ describe("BlsProvider", () => { expect(await blsProvider.getBalance(secondRecipient)).to.equal( expectedAmount, ); + + // tx 1 + expect(transactionBatchResponse.transactions[0]) + .to.be.an("object") + .that.includes({ + hash: transactionBatchResponse.transactions[0].hash, + to: firstRecipient, + from: blsSigner.wallet.address, + data: "0x", + chainId: 1337, + type: 2, + confirmations: 1, + }); + expect(transactionBatchResponse.transactions[0].nonce).to.equal(0); + expect(transactionBatchResponse.transactions[0].gasLimit).to.equal( + BigNumber.from("0x0"), + ); + expect(transactionBatchResponse.transactions[0].value).to.equal( + BigNumber.from(expectedAmount), + ); + + // tx 2 + expect(transactionBatchResponse.transactions[1]) + .to.be.an("object") + .that.includes({ + hash: transactionBatchResponse.transactions[1].hash, + to: secondRecipient, + from: blsSigner.wallet.address, + data: "0x", + chainId: 1337, + type: 2, + confirmations: 1, + }); + expect(transactionBatchResponse.transactions[1].nonce).to.equal(0); + expect(transactionBatchResponse.transactions[1].gasLimit).to.equal( + BigNumber.from("0x0"), + ); + expect(transactionBatchResponse.transactions[1].value).to.equal( + BigNumber.from(expectedAmount), + ); }); it("should get the account nonce when the signer constructs the transaction batch response", async () => { @@ -427,9 +436,8 @@ describe("BlsProvider", () => { it("should wait for a transaction and resolve once transaction hash is included in the block", async () => { // Arrange - const recipient = ethers.Wallet.createRandom().address; const transactionResponse = await blsSigner.sendTransaction({ - to: recipient, + to: ethers.Wallet.createRandom().address, value: parseEther("1"), }); @@ -489,9 +497,8 @@ describe("BlsProvider", () => { it("should retrieve a transaction receipt given a valid hash", async () => { // Arrange - const recipient = ethers.Wallet.createRandom().address; const transactionResponse = await blsSigner.sendTransaction({ - to: recipient, + to: ethers.Wallet.createRandom().address, value: parseEther("1"), }); @@ -547,13 +554,11 @@ describe("BlsProvider", () => { expect(transactionReceipt.effectiveGasPrice).to.be.an("object"); }); - it("gets a transaction given a valid transaction hash", async () => { + it("should get a transaction given a valid transaction hash", async () => { // Arrange - const recipient = ethers.Wallet.createRandom().address; - const transactionAmount = parseEther("1"); const transactionRequest = { - to: recipient, - value: transactionAmount, + to: ethers.Wallet.createRandom().address, + value: parseEther("1"), }; const expectedTransactionResponse = await blsSigner.sendTransaction( @@ -589,6 +594,7 @@ describe("BlsProvider", () => { // expects a different address when running as part of our github workflow. // from: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", chainId: expectedTransactionResponse.chainId, + // chainId: parseInt(expectedTransactionResponse.chainId, 16), type: 2, accessList: [], blockNumber: transactionReceipt.blockNumber, @@ -645,68 +651,9 @@ describe("BlsProvider", () => { expect(accounts).to.deep.equal(expectedAccounts); }); - it("should get the polling interval", async () => { - // Arrange - const expectedpollingInterval = 4000; // default - const updatedInterval = 1000; - - // Act - const pollingInterval = blsProvider.pollingInterval; - blsProvider.pollingInterval = updatedInterval; - const updatedPollingInterval = blsProvider.pollingInterval; - - // Assert - expect(pollingInterval).to.equal(expectedpollingInterval); - expect(updatedPollingInterval).to.equal(updatedInterval); - }); - - it("should get the event listener count and remove all listeners", async () => { - blsProvider.on("block", () => {}); - blsProvider.on("error", () => {}); - expect(blsProvider.listenerCount("block")).to.equal(1); - expect(blsProvider.listenerCount("error")).to.equal(1); - expect(blsProvider.listenerCount()).to.equal(2); - - blsProvider.removeAllListeners(); - expect(blsProvider.listenerCount("block")).to.equal(0); - expect(blsProvider.listenerCount("error")).to.equal(0); - expect(blsProvider.listenerCount()).to.equal(0); - }); - - it("should return true and an array of listeners if polling", async () => { - // Arrange - const expectedListener = () => {}; - - // Act - blsProvider.on("block", expectedListener); - const listeners = blsProvider.listeners("block"); - const isPolling = blsProvider.polling; - blsProvider.removeAllListeners(); - - // Assert - expect(listeners).to.deep.equal([expectedListener]); - expect(isPolling).to.be.true; - }); - - it("should be a provider", async () => { - // Arrange & Act - const isProvider = Experimental.BlsProvider.isProvider(blsProvider); - const isProviderWithInvalidProvider = - Experimental.BlsProvider.isProvider(blsSigner); - - // Assert - expect(isProvider).to.equal(true); - expect(isProviderWithInvalidProvider).to.equal(false); - }); - it("should return the number of transactions an address has sent", async function () { // Arrange - const transaction = { - value: BigNumber.from(1), - to: ethers.Wallet.createRandom().address, - }; const address = await blsSigner.getAddress(); - const expectedFirstTransactionCount = 0; const expectedSecondTransactionCount = 1; @@ -715,7 +662,10 @@ describe("BlsProvider", () => { address, ); - const sendTransaction = await blsSigner.sendTransaction(transaction); + const sendTransaction = await blsSigner.sendTransaction({ + value: BigNumber.from(1), + to: ethers.Wallet.createRandom().address, + }); await sendTransaction.wait(); const secondTransactionCount = await blsProvider.getTransactionCount( @@ -891,97 +841,4 @@ describe("BlsProvider", () => { ); expect(feeData.gasPrice).to.deep.equal(expectedFeeData.gasPrice); }); - - it("should a return a promise which will stall until the network has heen established", async () => { - // Arrange - const expectedReady = { name: "localhost", chainId: 1337 }; - - // Act - const ready = await blsProvider.ready; - - // Assert - expect(ready).to.deep.equal(expectedReady); - }); -}); - -describe("JsonRpcProvider", () => { - let wallet: ethers.Wallet; - - beforeEach(async () => { - rpcUrl = "http://localhost:8545"; - regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl); - // First two Hardhat account private keys are used in aggregator .env. We choose to use Hardhat account #2 private key here to avoid nonce too low errors. - wallet = new ethers.Wallet( - "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat acount #2 private key - regularProvider, - ); - }); - - it("calls a getter method on a contract", async () => { - // Arrange - const expectedSupply = "1000000.0"; - const testERC20 = MockERC20__factory.connect( - networkConfig.addresses.testToken, - regularProvider, - ); - - const transaction = { - to: testERC20.address, - data: testERC20.interface.encodeFunctionData("totalSupply"), - }; - - // Act - const result = await regularProvider.call(transaction); - - // Assert - expect(formatEther(result)).to.equal(expectedSupply); - }); - - it("gets a transaction given a valid transaction hash", async () => { - // Arrange - const recipient = ethers.Wallet.createRandom().address; - const transactionAmount = parseEther("1"); - const transactionRequest = { - to: recipient, - value: transactionAmount, - }; - - const expectedTransactionResponse = await wallet.sendTransaction( - transactionRequest, - ); - - // Act - const transactionResponse = await regularProvider.getTransaction( - expectedTransactionResponse.hash, - ); - - // Assert - expect(transactionResponse).to.be.an("object").that.deep.includes({ - hash: expectedTransactionResponse.hash, - type: expectedTransactionResponse.type, - accessList: expectedTransactionResponse.accessList, - transactionIndex: 0, - confirmations: 1, - from: expectedTransactionResponse.from, - maxPriorityFeePerGas: expectedTransactionResponse.maxPriorityFeePerGas, - maxFeePerGas: expectedTransactionResponse.maxFeePerGas, - gasLimit: expectedTransactionResponse.gasLimit, - to: expectedTransactionResponse.to, - value: expectedTransactionResponse.value, - nonce: expectedTransactionResponse.nonce, - data: expectedTransactionResponse.data, - r: expectedTransactionResponse.r, - s: expectedTransactionResponse.s, - v: expectedTransactionResponse.v, - creates: null, - chainId: expectedTransactionResponse.chainId, - }); - - expect(transactionResponse).to.include.keys( - "wait", - "blockHash", - "blockNumber", - "gasPrice", - ); - }); }); diff --git a/contracts/test-integration/BlsProviderContractInteraction.test.ts b/contracts/test-integration/BlsProviderContractInteraction.test.ts index 7ff62886..fafe8ef6 100644 --- a/contracts/test-integration/BlsProviderContractInteraction.test.ts +++ b/contracts/test-integration/BlsProviderContractInteraction.test.ts @@ -66,18 +66,25 @@ describe("Provider tests", function () { }); it("balanceOf() call", async () => { + // Arrange & Act const balance = await mockERC20.connect(blsProvider).balanceOf(recipient); + + // Assert expect(balance).to.equal(tokenSupply.div(2)); }); it("calls balanceOf successfully after instantiating Contract class with BlsProvider", async () => { + // Arrange const erc20 = new ethers.Contract( mockERC20.address, mockERC20.interface, blsProvider, ); + + // Act const balance = await erc20.balanceOf(recipient); + // Assert expect(erc20.provider).to.equal(blsProvider); expect(balance).to.equal(tokenSupply.div(2)); }); @@ -162,7 +169,7 @@ describe("Provider tests", function () { await tx.wait(); // wait 1 second to ensure listener count updates - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Assert expect((await erc20.balanceOf(recipient)).sub(balanceBefore)).to.equal( diff --git a/contracts/test-integration/BlsSigner.test.ts b/contracts/test-integration/BlsSigner.test.ts index d90f9a34..10e7274c 100644 --- a/contracts/test-integration/BlsSigner.test.ts +++ b/contracts/test-integration/BlsSigner.test.ts @@ -1,5 +1,4 @@ /* eslint-disable camelcase */ -import { ethers as hardhatEthers } from "hardhat"; import chai, { expect } from "chai"; import { ethers, BigNumber } from "ethers"; import { @@ -8,7 +7,7 @@ import { RLP, formatEther, } from "ethers/lib/utils"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import sinon from "sinon"; import { Experimental, @@ -34,6 +33,7 @@ let blsProvider: InstanceType; let blsSigner: InstanceType; let regularProvider: ethers.providers.JsonRpcProvider; +let fundedWallet: ethers.Wallet; describe("BlsSigner", () => { beforeEach(async () => { @@ -61,7 +61,7 @@ describe("BlsSigner", () => { regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl); - const fundedWallet = new ethers.Wallet( + fundedWallet = new ethers.Wallet( "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat Account #2 private key regularProvider, ); @@ -91,22 +91,6 @@ describe("BlsSigner", () => { ).to.equal(expectedBalance); }); - it("should throw an error sending a transaction when 'transaction.to' has not been defined", async () => { - // Arrange - const transaction = { - value: parseEther("1"), - }; - - // Act - const result = async () => await blsSigner.sendTransaction(transaction); - - // Assert - await expect(result()).to.be.rejectedWith( - TypeError, - "Transaction.to should be defined", - ); - }); - it("should throw an error when sending an invalid transaction", async () => { // Arrange const invalidValue = parseEther("-1"); @@ -394,35 +378,6 @@ describe("BlsSigner", () => { ); }); - it("should throw an error sending a transaction batch when this.signer is not defined", async () => { - // Arrange - const newBlsProvider = new Experimental.BlsProvider( - aggregatorUrl, - verificationGateway, - aggregatorUtilities, - rpcUrl, - network, - ); - const signedTransaction = await blsSigner.signTransactionBatch({ - transactions: [ - { - value: parseEther("1"), - to: ethers.Wallet.createRandom().address, - }, - ], - }); - - // Act - const result = async () => - await newBlsProvider.sendTransactionBatch(signedTransaction); - - // Assert - await expect(result()).to.be.rejectedWith( - Error, - "Call provider.getSigner first", - ); - }); - it("should validate batch options", async () => { // Arrange const batchOptions = { @@ -492,7 +447,7 @@ describe("BlsSigner", () => { ); }); - it("should not throw an error when invalid private key is supplied after a valid getSigner call", async () => { + it("should throw an error when invalid private key is supplied after a valid getSigner call", async () => { // Arrange const newBlsSigner = blsProvider.getSigner("invalidPrivateKey"); @@ -504,7 +459,10 @@ describe("BlsSigner", () => { }); // Assert - await expect(result()).to.not.be.rejectedWith(Error); + await expect(result()).to.be.rejectedWith( + Error, + "Expect hex but got invalidPrivateKey", + ); }); it("should retrieve the account address", async () => { @@ -522,22 +480,6 @@ describe("BlsSigner", () => { expect(address).to.equal(expectedAddress); }); - it("should throw an error signing a transaction when transaction.to has not been defined", async () => { - // Arrange - const transaction = { - value: parseEther("1"), - }; - - // Act - const result = async () => await blsSigner.sendTransaction(transaction); - - // Assert - await expect(result()).to.be.rejectedWith( - TypeError, - "Transaction.to should be defined", - ); - }); - it("should sign a transaction to create a bundleDto and serialize the result", async () => { // Arrange const recipient = ethers.Wallet.createRandom().address; @@ -545,6 +487,7 @@ describe("BlsSigner", () => { value: "1000000000000000000", to: recipient, data: "0x", + from: await blsSigner.getAddress(), }; // get expected signature @@ -567,6 +510,14 @@ describe("BlsSigner", () => { blsSigner, ); + // BlsWalletWrapper.getRandomBlsPrivateKey from "estimateGas" method results in slightly different + // fee estimates. Which leads to a different signature. This fake avoids signature mismatch by stubbing a constant value. + sinon.replace( + BlsWalletWrapper, + "getRandomBlsPrivateKey", + sinon.fake.resolves(privateKey), + ); + const expectedFeeEstimate = await blsProvider.estimateGas(transaction); const actionsWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee( @@ -596,20 +547,19 @@ describe("BlsSigner", () => { expect(bundleDto.signature).to.deep.equal( expectedBundleSignatureHexStrings, ); + sinon.restore(); }); it("should throw an error when signing an invalid transaction", async () => { // Arrange const invalidEthValue = parseEther("-1"); - const unsignedTransaction = { - value: invalidEthValue, - to: ethers.Wallet.createRandom().address, - }; - // Act const result = async () => - await blsSigner.signTransaction(unsignedTransaction); + await blsSigner.signTransaction({ + value: invalidEthValue, + to: ethers.Wallet.createRandom().address, + }); // Assert await expect(result()).to.be.rejectedWith( @@ -768,13 +718,12 @@ describe("BlsSigner", () => { // Arrange const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - to: recipient, - value: transactionAmount, - }; // Act - const result = blsSigner.checkTransaction(transaction); + const result = blsSigner.checkTransaction({ + to: recipient, + value: transactionAmount, + }); // Assert const resolvedResult = await resolveProperties(result); @@ -791,13 +740,12 @@ describe("BlsSigner", () => { // Arrange const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - to: recipient, - value: transactionAmount, - }; // Act - const result = await blsSigner.populateTransaction(transaction); + const result = await blsSigner.populateTransaction({ + to: recipient, + value: transactionAmount, + }); // Assert expect(result).to.be.an("object").that.includes({ @@ -850,23 +798,25 @@ describe("BlsSigner", () => { }); it("should await the init promise when connecting to an unchecked bls signer", async () => { - // Arrange & Act + // Arrange const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey(); const newBlsSigner = blsProvider.getSigner(newPrivateKey); const uncheckedBlsSigner = newBlsSigner.connectUnchecked(); + await fundedWallet.sendTransaction({ + to: await uncheckedBlsSigner.getAddress(), + value: parseEther("1.1"), + }); + const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - value: transactionAmount, - to: recipient, - }; const balanceBefore = await blsProvider.getBalance(recipient); // Act - const uncheckedResponse = await uncheckedBlsSigner.sendTransaction( - transaction, - ); + const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({ + value: transactionAmount, + to: recipient, + }); await uncheckedResponse.wait(); // Assert @@ -875,9 +825,7 @@ describe("BlsSigner", () => { ).to.equal(transactionAmount); }); - // TODO (merge-ok) https://github.com/web3well/bls-wallet/issues/427 - // This test is identical to the above test except this one uses a new instance of a provider, yet fails to find the tx receipt - it.skip("should get the transaction receipt when using a new provider and connecting to an unchecked bls signer", async () => { + it("should get the transaction receipt when using a new provider and connecting to an unchecked bls signer", async () => { // Arrange & Act const newBlsProvider = new Experimental.BlsProvider( aggregatorUrl, @@ -890,18 +838,20 @@ describe("BlsSigner", () => { const newBlsSigner = newBlsProvider.getSigner(newPrivateKey); const uncheckedBlsSigner = newBlsSigner.connectUnchecked(); + await fundedWallet.sendTransaction({ + to: await uncheckedBlsSigner.getAddress(), + value: parseEther("1.1"), + }); + const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - value: transactionAmount, - to: recipient, - }; const balanceBefore = await blsProvider.getBalance(recipient); // Act - const uncheckedResponse = await uncheckedBlsSigner.sendTransaction( - transaction, - ); + const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({ + value: transactionAmount, + to: recipient, + }); await uncheckedResponse.wait(); // Assert @@ -914,16 +864,13 @@ describe("BlsSigner", () => { // Arrange const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - value: transactionAmount, - to: recipient, - }; const balanceBefore = await blsProvider.getBalance(recipient); // Act - const uncheckedTransactionHash = await blsSigner.sendUncheckedTransaction( - transaction, - ); + const uncheckedTransactionHash = await blsSigner.sendUncheckedTransaction({ + value: transactionAmount, + to: recipient, + }); await blsProvider.getTransactionReceipt(uncheckedTransactionHash); // Assert @@ -938,22 +885,18 @@ describe("BlsSigner", () => { const recipient = ethers.Wallet.createRandom().address; const transactionAmount = parseEther("1"); - const transaction = { - value: transactionAmount, - to: recipient, - }; const balanceBefore = await blsProvider.getBalance(recipient); // Act - const uncheckedResponse = await uncheckedBlsSigner.sendTransaction( - transaction, - ); + const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({ + value: transactionAmount, + to: recipient, + }); await uncheckedResponse.wait(); // Assert expect(uncheckedResponse).to.be.an("object").that.includes({ hash: uncheckedResponse.hash, - nonce: 1, data: "", chainId: 0, confirmations: 0, @@ -963,6 +906,7 @@ describe("BlsSigner", () => { expect(uncheckedResponse.gasLimit).to.equal(BigNumber.from("0")); expect(uncheckedResponse.gasPrice).to.equal(BigNumber.from("0")); expect(uncheckedResponse.value).to.equal(BigNumber.from("0")); + expect(isNaN(uncheckedResponse.nonce)).to.be.true; expect( (await blsProvider.getBalance(recipient)).sub(balanceBefore), @@ -1031,7 +975,7 @@ describe("BlsSigner", () => { const expectedTransactionCount = 0; const sendTransaction = await blsSigner.sendTransaction({ - value: BigNumber.from(1), + value: parseEther("1"), to: ethers.Wallet.createRandom().address, }); await sendTransaction.wait(); @@ -1053,14 +997,12 @@ describe("BlsSigner", () => { blsProvider, ); - const transaction = { + // Act + const result = await blsSigner.call({ to: testERC20.address, data: testERC20.interface.encodeFunctionData("totalSupply"), // Explicitly omit 'from' - }; - - // Act - const result = await blsSigner.call(transaction); + }); // Assert expect(formatEther(result)).to.equal("1000000.0"); @@ -1077,14 +1019,14 @@ describe("BlsSigner", () => { // Arrange const spy = chai.spy.on(Experimental.BlsProvider.prototype, "estimateGas"); const recipient = ethers.Wallet.createRandom().address; - const transaction = { - to: recipient, - value: parseEther("1"), - // Explicitly omit 'from' - }; // Act - const gasEstimate = async () => await blsSigner.estimateGas(transaction); + const gasEstimate = async () => + await blsSigner.estimateGas({ + to: recipient, + value: parseEther("1"), + // Explicitly omit 'from' + }); // Assert await expect(gasEstimate()).to.not.be.rejected; @@ -1125,102 +1067,87 @@ describe("BlsSigner", () => { "account unlock with HTTP access is forbidden", ); }); -}); - -describe("JsonRpcSigner", () => { - let signers: SignerWithAddress[]; - let wallet: ethers.Wallet; - - beforeEach(async () => { - signers = await hardhatEthers.getSigners(); - rpcUrl = "http://localhost:8545"; - regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl); - // First two Hardhat account private keys are used in aggregator .env. We choose to use Hardhat account #2 private key here to avoid nonce too low errors. - wallet = new ethers.Wallet( - "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat account #2 private key - regularProvider, - ); - }); - - it("should retrieve the account address", async () => { - // Arrange - const expectedAddress = signers[2].address; - - // Act - const address = await wallet.getAddress(); - - // Assert - expect(address).to.equal(expectedAddress); - }); - it("should send ETH (empty call) successfully", async () => { + it("should validate a transaction request", async () => { // Arrange const recipient = ethers.Wallet.createRandom().address; - const expectedBalance = parseEther("1"); - const recipientBalanceBefore = await regularProvider.getBalance(recipient); + const getBalancePromise = blsSigner.getBalance(); + const expectedValidatedTransaction = { + to: recipient, + value: await blsSigner.getBalance(), + from: await blsSigner.getAddress(), + }; // Act - const transaction = await wallet.sendTransaction({ + const validatedTransaction = await blsSigner._validateTransaction({ to: recipient, - value: expectedBalance, + value: getBalancePromise, }); - await transaction.wait(); // Assert - expect( - (await regularProvider.getBalance(recipient)).sub(recipientBalanceBefore), - ).to.equal(expectedBalance); + expect(validatedTransaction).to.deep.equal(expectedValidatedTransaction); }); - it("should check transaction", async () => { - // Arrange - const recipient = ethers.Wallet.createRandom().address; - const transactionAmount = parseEther("1"); - const transaction = { - to: recipient, - value: transactionAmount, - }; - - // Act - const result = wallet.checkTransaction(transaction); + it("should throw an error validating a transaction request when transaction.to is not defined", async () => { + // Arrange & Act + const result = async () => + await blsSigner._validateTransaction({ + value: await blsSigner.getBalance(), + }); // Assert - const resolvedResult = await resolveProperties(result); - expect(resolvedResult) - .to.be.an("object") - .that.includes({ - to: recipient, - value: transactionAmount, - from: await wallet.getAddress(), - }); + await expect(result()).to.be.rejectedWith( + TypeError, + "Transaction.to should be defined", + ); }); - it("should populate transaction", async () => { + it("should validate a transaction batch", async () => { // Arrange const recipient = ethers.Wallet.createRandom().address; - const transactionAmount = parseEther("1"); - const transaction = { - to: recipient, - value: transactionAmount, + const amount = await blsSigner.getBalance(); + const expectedValidatedTransactionBatch = { + transactions: [ + { + to: recipient, + value: amount, + from: await blsSigner.getAddress(), + }, + ], + batchOptions: undefined, }; // Act - const result = await wallet.populateTransaction(transaction); + const validatedTransaction = await blsSigner._validateTransactionBatch({ + transactions: [ + { + to: recipient, + value: amount, + }, + ], + }); // Assert - expect(result).to.be.an("object").that.includes({ - to: recipient, - value: transactionAmount, - from: signers[2].address, - type: 2, - chainId: 1337, - }); + expect(validatedTransaction).to.deep.equal( + expectedValidatedTransactionBatch, + ); + }); - expect(result).to.include.keys( - "maxFeePerGas", - "maxPriorityFeePerGas", - "nonce", - "gasLimit", + it("should throw an error validating a transaction batch when transaction.to is not defined", async () => { + // Arrange & Act + const result = async () => + await blsSigner._validateTransactionBatch({ + transactions: [ + { + value: await blsSigner.getBalance(), + }, + ], + }); + + // Assert + await expect(result()).to.be.rejectedWith( + TypeError, + "Transaction.to is missing on transaction 0", ); }); }); diff --git a/contracts/test-integration/BlsSignerContractInteraction.test.ts b/contracts/test-integration/BlsSignerContractInteraction.test.ts index 3d163dcb..d56f696c 100644 --- a/contracts/test-integration/BlsSignerContractInteraction.test.ts +++ b/contracts/test-integration/BlsSignerContractInteraction.test.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber, utils, Wallet } from "ethers"; +import sinon from "sinon"; import { Experimental, BlsWalletWrapper } from "../clients/src"; import getNetworkConfig from "../shared/helpers/getNetworkConfig"; @@ -86,7 +87,10 @@ describe("Signer contract interaction tests", function () { // TODO: Add Contract deployment support #182 it("deploying contract using BlsSigner fails", async () => { + // Arrange const NewMockERC20 = await ethers.getContractFactory("MockERC20"); + + // Act const deployNewMockERC20 = async () => await NewMockERC20.connect(blsSigners[0]).deploy( "AnyToken", @@ -94,6 +98,7 @@ describe("Signer contract interaction tests", function () { tokenSupply, ); + // Assert await expect(deployNewMockERC20()).to.be.rejectedWith( TypeError, "Transaction.to should be defined", @@ -101,37 +106,42 @@ describe("Signer contract interaction tests", function () { }); it("calls balanceOf successfully after instantiating Contract class with BlsSigner", async () => { + // Arrange const blsSignerAddress = await blsSigners[0].getAddress(); const ERC20 = new ethers.Contract( mockERC20.address, mockERC20.interface, blsSigners[0], ); - expect(ERC20.signer).to.equal(blsSigners[0]); + // Act const initialBalance = await ERC20.balanceOf(blsSignerAddress); + + // Assert + expect(ERC20.signer).to.equal(blsSigners[0]); expect(initialBalance).to.equal(tokenSupply); }); it("transfer() call", async () => { + // Arrange const recipient = await blsSigners[1].getAddress(); + const initialBalance = await mockERC20.balanceOf(recipient); - const fee = mockERC20 - .connect(blsSigners[0]) - .estimateGas.transfer(recipient, tokenSupply.div(2)); - - await expect(fee).to.not.be.rejected; - + // Act const tx = await mockERC20 .connect(blsSigners[0]) .transfer(recipient, tokenSupply.div(2)); await tx.wait(); + // Assert const newReceipientBalance = await mockERC20.balanceOf(recipient); - expect(newReceipientBalance).to.equal(tokenSupply.div(2)); + expect(newReceipientBalance.sub(initialBalance)).to.equal( + tokenSupply.div(2), + ); }); it("calls transfer() successfully after instantiating Contract class with BlsSigner", async () => { + // Arrange const ERC20 = new ethers.Contract( mockERC20.address, mockERC20.interface, @@ -141,12 +151,11 @@ describe("Signer contract interaction tests", function () { const initialBalance = await mockERC20.balanceOf(recipient); const erc20ToTransfer = utils.parseEther("53.2134222"); - const fee = ERC20.estimateGas.transfer(recipient, erc20ToTransfer); - await expect(fee).to.not.be.rejected; - + // Act const tx = await ERC20.transfer(recipient, erc20ToTransfer); await tx.wait(); + // Assert const newReceipientBalance = await mockERC20.balanceOf(recipient); expect(newReceipientBalance.sub(initialBalance)).to.equal( erc20ToTransfer, @@ -154,32 +163,25 @@ describe("Signer contract interaction tests", function () { }); it("approve() and transferFrom() calls", async () => { + // Arrange const owner = await blsSigners[0].getAddress(); const spender = await blsSigners[1].getAddress(); const initialBalance = await mockERC20.balanceOf(spender); const erc20ToTransfer = utils.parseEther("11.0"); - const approveFee = mockERC20 - .connect(blsSigners[0]) - .estimateGas.approve(spender, erc20ToTransfer); - await expect(approveFee).to.not.be.rejected; - + // Act const txApprove = await mockERC20 .connect(blsSigners[0]) .approve(spender, erc20ToTransfer); await txApprove.wait(); - const transferFee = mockERC20 - .connect(blsSigners[1]) - .estimateGas.transferFrom(owner, spender, erc20ToTransfer); - await expect(transferFee).to.not.be.rejected; - const txTransferFrom = await mockERC20 .connect(blsSigners[1]) .transferFrom(owner, spender, erc20ToTransfer); await txTransferFrom.wait(); + // Assert const newBalance = await mockERC20.balanceOf(spender); expect(newBalance.sub(initialBalance)).to.equal(erc20ToTransfer); }); @@ -228,24 +230,86 @@ describe("Signer contract interaction tests", function () { expect(newBalance.sub(initialBalance)).to.equal(erc20ToTransfer); }); + it("should return the expected fee", async () => { + // Arrange + const recipient = await blsSigners[1].getAddress(); + const amount = ethers.utils.parseUnits("1"); + const expectedTransaction = await mockERC20 + .connect(blsSigners[0]) + .populateTransaction.transfer(recipient, amount); + + // BlsWalletWrapper.getRandomBlsPrivateKey from "estimateGas" method results in slightly different + // fee estimates. This fake avoids this mismatch by stubbing a constant value. + sinon.replace( + BlsWalletWrapper, + "getRandomBlsPrivateKey", + sinon.fake.resolves(blsSigners[0].wallet.blsWalletSigner.privateKey), + ); + const expectedFee = await blsSigners[0].provider.estimateGas( + expectedTransaction, + ); + + // Act + const fee = await mockERC20 + .connect(blsSigners[0]) + .estimateGas.transfer(recipient, amount); + + // Assert + expect(fee).to.equal(expectedFee); + sinon.restore(); + }); + + it("should not fail when estimating gas for different scenarios", async () => { + // Arrange + const ERC20 = new ethers.Contract( + mockERC20.address, + mockERC20.interface, + blsSigners[0], + ); + const recipient = await blsSigners[1].getAddress(); + const spender = await blsSigners[2].getAddress(); + const owner = await blsSigners[3].getAddress(); + const erc20ToTransfer = utils.parseEther("1"); + + // Act + const transferFeeWithNewContractClass = ERC20.estimateGas.transfer( + recipient, + erc20ToTransfer, + ); + const approveFee = mockERC20 + .connect(blsSigners[3]) + .estimateGas.approve(spender, erc20ToTransfer); + const transferFromFee = mockERC20 + .connect(blsSigners[2]) + .estimateGas.transferFrom(owner, spender, erc20ToTransfer); + + // Assert + await expect(transferFeeWithNewContractClass).to.not.be.rejected; + await expect(approveFee).to.not.be.rejected; + await expect(transferFromFee).to.not.be.rejected; + }); + it("contract factory connects and reconnects to new signer", async () => { + // Arrange // Truncated as not required for test const mockERC20Bytecode = "0x60806040523480"; + // Act const contractFactory = new ethers.ContractFactory( mockERC20.interface, mockERC20Bytecode, blsSigners[0], ); + const newContractFactory = contractFactory.connect(blsSigners[1]); + // Assert expect(contractFactory.signer).to.equal(blsSigners[0]); - - const newContractFactory = contractFactory.connect(blsSigners[1]); expect(newContractFactory.signer).to.equal(blsSigners[1]); }); // TODO: Add Contract deployment support #182 it("deploying via new contract factory fails", async () => { + // Arrange // Taken from artifacts directory const mockERC20Bytecode = "0x60806040523480156200001157600080fd5b5060405162000dae38038062000dae83398101604081905262000034916200022b565b828260036200004483826200032c565b5060046200005382826200032c565b5050506200006833826200007160201b60201c565b5050506200041f565b6001600160a01b038216620000cc5760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015260640160405180910390fd5b8060026000828254620000e09190620003f8565b90915550506001600160a01b038216600090815260208190526040812080548392906200010f908490620003f8565b90915550506040518181526001600160a01b038316906000907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9060200160405180910390a35050565b505050565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200018657600080fd5b81516001600160401b0380821115620001a357620001a36200015e565b604051601f8301601f19908116603f01168101908282118183101715620001ce57620001ce6200015e565b81604052838152602092508683858801011115620001eb57600080fd5b600091505b838210156200020f5785820183015181830184015290820190620001f0565b83821115620002215760008385830101525b9695505050505050565b6000806000606084860312156200024157600080fd5b83516001600160401b03808211156200025957600080fd5b620002678783880162000174565b945060208601519150808211156200027e57600080fd5b506200028d8682870162000174565b925050604084015190509250925092565b600181811c90821680620002b357607f821691505b602082108103620002d457634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200015957600081815260208120601f850160051c81016020861015620003035750805b601f850160051c820191505b8181101562000324578281556001016200030f565b505050505050565b81516001600160401b038111156200034857620003486200015e565b62000360816200035984546200029e565b84620002da565b602080601f8311600181146200039857600084156200037f5750858301515b600019600386901b1c1916600185901b17855562000324565b600085815260208120601f198616915b82811015620003c957888601518255948401946001909101908401620003a8565b5085821015620003e85787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b600082198211156200041a57634e487b7160e01b600052601160045260246000fd5b500190565b61097f806200042f6000396000f3fe608060405234801561001057600080fd5b50600436106100a45760003560e01c806306fdde03146100a9578063095ea7b3146100c757806318160ddd146100ea57806323b872dd146100fc578063313ce5671461010f578063395093511461011e57806340c10f191461013157806370a082311461014657806395d89b411461016f578063a457c2d714610177578063a9059cbb1461018a578063dd62ed3e1461019d575b600080fd5b6100b16101b0565b6040516100be919061079d565b60405180910390f35b6100da6100d536600461080e565b610242565b60405190151581526020016100be565b6002545b6040519081526020016100be565b6100da61010a366004610838565b61025a565b604051601281526020016100be565b6100da61012c36600461080e565b61027e565b61014461013f36600461080e565b6102a0565b005b6100ee610154366004610874565b6001600160a01b031660009081526020819052604090205490565b6100b16102ae565b6100da61018536600461080e565b6102bd565b6100da61019836600461080e565b61033d565b6100ee6101ab366004610896565b61034b565b6060600380546101bf906108c9565b80601f01602080910402602001604051908101604052809291908181526020018280546101eb906108c9565b80156102385780601f1061020d57610100808354040283529160200191610238565b820191906000526020600020905b81548152906001019060200180831161021b57829003601f168201915b5050505050905090565b600033610250818585610376565b5060019392505050565b60003361026885828561049a565b610273858585610514565b506001949350505050565b600033610250818585610291838361034b565b61029b9190610903565b610376565b6102aa82826106d0565b5050565b6060600480546101bf906108c9565b600033816102cb828661034b565b9050838110156103305760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084015b60405180910390fd5b6102738286868403610376565b600033610250818585610514565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103d85760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608401610327565b6001600160a01b0382166104395760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608401610327565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b60006104a6848461034b565b9050600019811461050e57818110156105015760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610327565b61050e8484848403610376565b50505050565b6001600160a01b0383166105785760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608401610327565b6001600160a01b0382166105da5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608401610327565b6001600160a01b038316600090815260208190526040902054818110156106525760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608401610327565b6001600160a01b03808516600090815260208190526040808220858503905591851681529081208054849290610689908490610903565b92505081905550826001600160a01b0316846001600160a01b031660008051602061092a833981519152846040516106c391815260200190565b60405180910390a361050e565b6001600160a01b0382166107265760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610327565b80600260008282546107389190610903565b90915550506001600160a01b03821660009081526020819052604081208054839290610765908490610903565b90915550506040518181526001600160a01b0383169060009060008051602061092a8339815191529060200160405180910390a35050565b600060208083528351808285015260005b818110156107ca578581018301518582016040015282016107ae565b818111156107dc576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461080957600080fd5b919050565b6000806040838503121561082157600080fd5b61082a836107f2565b946020939093013593505050565b60008060006060848603121561084d57600080fd5b610856846107f2565b9250610864602085016107f2565b9150604084013590509250925092565b60006020828403121561088657600080fd5b61088f826107f2565b9392505050565b600080604083850312156108a957600080fd5b6108b2836107f2565b91506108c0602084016107f2565b90509250929050565b600181811c908216806108dd57607f821691505b6020821081036108fd57634e487b7160e01b600052602260045260246000fd5b50919050565b6000821982111561092457634e487b7160e01b600052601160045260246000fd5b50019056feddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa2646970667358221220ab6a94396731948535204a8026eaf25f6b11f6c7e951d939bf58529e4046659f64736f6c634300080f0033"; @@ -256,11 +320,11 @@ describe("Signer contract interaction tests", function () { blsSigners[0], ); - expect(contractFactory.signer).to.equal(blsSigners[0]); - + // Act const deploy = async () => await contractFactory.deploy("AnyToken", "TOK", tokenSupply); + // Assert await expect(deploy()).to.be.rejectedWith( TypeError, "Transaction.to should be defined", @@ -300,108 +364,93 @@ describe("Signer contract interaction tests", function () { }); it("safeMint() call passes with EOA address", async () => { + // Arrange const recipient = ethers.Wallet.createRandom().address; const tokenId = 2; - const fee = mockERC721 - .connect(blsSigners[0]) - .estimateGas.safeMint(recipient, tokenId); - await expect(fee).to.not.be.rejected; - + // Act const mint = await mockERC721 .connect(blsSigners[0]) .safeMint(recipient, tokenId); await mint.wait(); + // Assert expect(await mockERC721.connect(blsSigners[1]).ownerOf(tokenId)).to.equal( recipient, ); }); it("mint() call", async () => { + // Arrange const recipient = await blsSigners[1].getAddress(); const tokenId = 3; - const fee = mockERC721 - .connect(blsSigners[0]) - .estimateGas.mint(recipient, tokenId); - await expect(fee).to.not.be.rejected; - + // Act const mint = await mockERC721 .connect(blsSigners[0]) .mint(recipient, tokenId); await mint.wait(); + // Assert expect(await mockERC721.connect(blsSigners[1]).ownerOf(tokenId)).to.equal( recipient, ); }); it("balanceOf() call", async () => { + // Arrange const recipient = await blsSigners[1].getAddress(); const initialBalance = await mockERC721.balanceOf(recipient); const tokenId = 4; + // Act const mint = await mockERC721 .connect(blsSigners[0]) .mint(recipient, tokenId); await mint.wait(); + // Assert expect( (await mockERC721.balanceOf(recipient)).sub(initialBalance), ).to.equal(1); }); it("transfer() call", async () => { + // Arrange const tokenId = 5; const owner = await blsSigners[3].getAddress(); const recipient = await blsSigners[2].getAddress(); - // Mint a token to signer[3] const mint = await mockERC721.connect(blsSigners[0]).mint(owner, tokenId); await mint.wait(); - // Check signer[3] owns the token - expect(await mockERC721.ownerOf(tokenId)).to.equal(owner); - - const fee = mockERC721 - .connect(blsSigners[3]) - .estimateGas.transferFrom(owner, recipient, tokenId); - await expect(fee).to.not.be.rejected; - - // Transfer the token from signer 3 to signer 2 + // Act const transfer = await mockERC721 .connect(blsSigners[3]) .transferFrom(owner, recipient, tokenId); await transfer.wait(); - // Check signer[2] now owns the token + // Assert expect(await mockERC721.ownerOf(tokenId)).to.equal(recipient); }); it("approve() call", async () => { + // Arrange const owner = await blsSigners[4].getAddress(); const spender = await blsSigners[1].getAddress(); - // Mint a token to signer[4] const tokenId = 6; const mint = await mockERC721 .connect(blsSigners[0]) .safeMint(owner, tokenId); await mint.wait(); - const fee = mockERC721 - .connect(blsSigners[4]) - .estimateGas.approve(spender, tokenId); - await expect(fee).to.not.be.rejected; - - // Approve the token for signer[1] address + // Act const approve = await mockERC721 .connect(blsSigners[4]) .approve(spender, tokenId); await approve.wait(); - // Check signer[1]'s address is now an approved address for the token expect(await mockERC721.getApproved(tokenId)).to.equal(spender); }); @@ -450,5 +499,33 @@ describe("Signer contract interaction tests", function () { // Assert expect(await mockERC721.ownerOf(tokenId)).to.equal(spender); }); + + it("should not fail when estimating gas for different scenarios", async () => { + // Arrange + const recipient = await blsSigners[1].getAddress(); + const spender = await blsSigners[2].getAddress(); + const owner = await blsSigners[3].getAddress(); + const tokenId = 7; + + // Act + const safeMintFee = mockERC721 + .connect(blsSigners[0]) + .estimateGas.safeMint(recipient, tokenId); + const mintFee = mockERC721 + .connect(blsSigners[0]) + .estimateGas.mint(recipient, tokenId); + const approveFee = mockERC721 + .connect(blsSigners[1]) + .estimateGas.approve(spender, tokenId); + const transferFromFee = mockERC721 + .connect(blsSigners[2]) + .estimateGas.transferFrom(owner, recipient, tokenId); + + // Assert + await expect(safeMintFee).to.not.be.rejected; + await expect(mintFee).to.not.be.rejected; + await expect(approveFee).to.not.be.rejected; + await expect(transferFromFee).to.not.be.rejected; + }); }); }); diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 38207127..876ec690 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -981,6 +981,41 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + +"@sinonjs/samsam@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f" + integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1", "@solidity-parser/parser@^0.14.3": version "0.14.5" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804" @@ -3556,6 +3591,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -6463,6 +6503,11 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + keccak@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff" @@ -6760,6 +6805,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -7386,6 +7436,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" + integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers" "^10.0.2" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -7877,6 +7938,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -8884,6 +8952,18 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +sinon@^15.0.2: + version "15.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.2.tgz#f3e3aacb990bbaa8a7bb976e86118c5dc0154e66" + integrity sha512-PCVP63XZkg0/LOqQH5rEU4LILuvTFMb5tNxTHfs6VUMNnZz2XrnGSTZbAGITjzwQWbl/Bl/8hi4G3zZWjyBwHg== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^10.0.2" + "@sinonjs/samsam" "^7.0.1" + diff "^5.1.0" + nise "^5.1.4" + supports-color "^7.2.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -9368,7 +9448,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -9732,7 +9812,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==