24 Commits

Author SHA1 Message Date
Blake Duncan
cdecab26ee Reorder workflow steps 2023-03-29 14:12:22 +01:00
Blake Duncan
e2b6e4f1ee Debug session 2023-03-29 14:05:40 +01:00
Blake Duncan
7c2901f243 update test verification gateway address 2023-03-29 13:26:26 +01:00
Blake Duncan
4a7331c4a7 Publish experimental bls-wallet-clients 2023-03-28 15:32:06 +01:00
Blake Duncan
1fb4a557ab Update param naming 2023-03-28 15:24:22 +01:00
Blake Duncan
7862b5278a Merge branch 'main' into auditIssue7 2023-03-28 15:16:40 +01:00
Blake Duncan
e2ef1ed62e Fix TS issue in aggregator 2023-03-28 14:42:46 +01:00
John Guilding
96bfb32e5b Merge pull request #562 from web3well/549-add-documentation-for-bls-provider-and-bls-signer
549 add documentation for bls provider and bls signer
2023-03-28 14:36:57 +01:00
Blake Duncan
2f9a3442f8 Fix linting issues 2023-03-28 14:27:40 +01:00
Blake Duncan
8833c5f0af Rename variables and code cleanup 2023-03-28 14:11:47 +01:00
Blake Duncan
9505ed425b Using seperate domains for proof of possession messages and for bundles 2023-03-28 13:11:08 +01:00
JohnGuilding
7435d9976e Fix typescript errors in BundleTable 2023-03-28 13:04:58 +01:00
JohnGuilding
2dae355817 Update root readme to include provider link 2023-03-28 11:07:20 +01:00
JohnGuilding
e734209df0 Remove BlsWalletWrapper in provider docs where possible 2023-03-28 11:06:50 +01:00
Blake Duncan
fedf05cc7e Fix test by signing message with correct verification gateway 2023-03-27 17:28:43 +01:00
Blake Duncan
7dc7bb4a5e use chainId and verification gateway in bls domain 2023-03-27 13:35:59 +01:00
JohnGuilding
d2c6cff629 Add missing documentation following rebase 2023-03-27 12:48:35 +01:00
JohnGuilding
25469d50e4 Remove directly paying aggregator fees docs example 2023-03-27 11:09:26 +01:00
JohnGuilding
548301d32d Add bls provider guide and ts doc comments 2023-03-27 11:09:00 +01:00
John Guilding
ac7cd956a8 Merge pull request #548 from web3well/413-improve-private-key-management-in-bls-provider-and-signer
413 improve private key management in bls provider and signer
2023-03-22 19:20:06 +00:00
JohnGuilding
5423f65503 Add comment explaining throwaway BlsWalletWrapper 2023-03-22 12:03:28 +00:00
JohnGuilding
3c0f36f444 re-add accidentally deleted tests 2023-03-16 17:19:36 +00:00
JohnGuilding
32c6b13e7d Cleanup test code & add contract interaction gas estimate tests 2023-03-16 16:59:28 +00:00
JohnGuilding
e3bbd393d8 Remove reference to signer from provider 2023-03-16 16:12:19 +00:00
35 changed files with 1237 additions and 863 deletions

View File

@@ -76,3 +76,5 @@ jobs:
- run: cp .env.local.example .env
- run: deno test --allow-net --allow-env --allow-read
- uses: mxschmitt/action-tmate@v3
- run: sleep 3600

View File

@@ -11,6 +11,7 @@ You can watch a full end-to-end demo of the project [here](https://www.youtube.c
- See an [overview](./docs/system_overview.md) of BLS Wallet & how the components work together.
- Use BLS Wallet in [a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md).
- Use BLS Wallet in [your L2 dApp](./docs/use_bls_wallet_dapp.md) for cheaper, multi action transactions.
- Use BLS Wallet components and features with an [ethers.js provider and signer](./use_bls_provider.md)
### Setup your development environment

View File

@@ -21,7 +21,7 @@
"@types/koa__cors": "^3.3.0",
"@types/koa__router": "^8.0.11",
"@types/node-fetch": "^2.6.1",
"bls-wallet-clients": "0.8.2-1452ef5",
"bls-wallet-clients": "0.8.2-1fb4a55",
"fp-ts": "^2.12.1",
"io-ts": "^2.2.16",
"io-ts-reporters": "^2.0.1",

View File

@@ -887,10 +887,10 @@ bech32@1.1.4:
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
bls-wallet-clients@0.8.2-1452ef5:
version "0.8.2-1452ef5"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
bls-wallet-clients@0.8.2-1fb4a55:
version "0.8.2-1fb4a55"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"

View File

@@ -53,7 +53,7 @@ export type {
PublicKey,
Signature,
VerificationGateway,
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
export {
Aggregator as AggregatorClient,
@@ -64,10 +64,10 @@ export {
getConfig,
MockERC20__factory,
VerificationGateway__factory,
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
// Workaround for esbuild's export-star bug
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
export { bundleFromDto, bundleToDto, initBlsWalletSigner };

View File

@@ -60,14 +60,14 @@ export type BundleRow = Row;
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
if (Array.isArray(rawRow)) {
rawRow = {
id: rawRow[0],
status: rawRow[1],
hash: rawRow[2],
bundle: rawRow[3],
eligibleAfter: rawRow[4],
nextEligibilityDelay: rawRow[5],
submitError: rawRow[6],
receipt: rawRow[7],
id: rawRow[0] as number,
status: rawRow[1] as string,
hash: rawRow[2] as string,
bundle: rawRow[3] as string,
eligibleAfter: rawRow[4] as string,
nextEligibilityDelay: rawRow[5] as string,
submitError: rawRow[6] as string | null,
receipt: rawRow[7] as string | null,
};
}

View File

@@ -136,6 +136,7 @@ export default class EthereumService {
const blsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey: aggPrivateKey,
verificationGatewayAddress,
});
return new EthereumService(

View File

@@ -142,7 +142,7 @@ const bundle = wallet.sign({
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
### Paying aggregator fees with native currency (ETH)
@@ -198,21 +198,6 @@ const bundle = wallet.sign({
});
```
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
```ts
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: safeFee, // fee amount
contractAddress: aggregatorAddress,
},
],
});
```
### Paying aggregator fees with custom currency (ERC20)
The aggregator must be set up to accept ERC20 tokens in order for this to work.
@@ -288,25 +273,6 @@ const bundle = wallet.sign({
});
```
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
```ts
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("transfer", [
aggregatorAddress,
safeFee, // fee amount
]),
},
],
});
```
## VerificationGateway
Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
@@ -366,8 +332,13 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
(async () => {
const privateKey = "0x...256 bits of private hex data here";
const verificationGatewayAddress = "0x123...456";
const signer = await initBlsWalletSigner({ chainId: 10, privateKey });
const signer = await initBlsWalletSigner({
chainId: 10,
privateKey,
verificationGatewayAddress
});
const someToken = new ethers.Contract(
...

View File

@@ -1,27 +0,0 @@
import { mcl } from "@thehubbleproject/bls";
import privateKeyStorage from "./privateKeyStorage";
import SecretStorage, { SecretReference } from "./SecretStorage";
export default class PrivateKey {
#secretReference: SecretReference;
private constructor(secretReference: SecretReference) {
this.#secretReference = secretReference;
}
// Freely create private keys and pass them around - this class doesn't
// actually store the private key string.
static async generateRandom(): Promise<PrivateKey> {
await mcl.init();
const privateKeyString = mcl.randFr().serializeToHexStr();
const secretReference = privateKeyStorage.storeSecret(privateKeyString);
return new PrivateKey(secretReference);
}
// Reading the private key requires access to privateKeyStorage, and you'll
// only import that if you *really* need the private key.
read(privateKeyStorage: SecretStorage<string>): string {
return privateKeyStorage.readSecret(this.#secretReference);
}
}

View File

@@ -1,25 +0,0 @@
export type SecretReference = {
type: "secret-reference";
name?: string;
};
export default class SecretStorage<T> {
#secrets = new WeakMap<SecretReference, T>();
storeSecret(secret: T, name?: string): SecretReference {
const secretReference: SecretReference = { type: "secret-reference", name };
this.#secrets.set(secretReference, secret);
return secretReference;
}
readSecret(secretReference: SecretReference): T {
const secret = this.#secrets.get(secretReference);
if (secret === undefined) {
throw new Error(`Secret ${secretReference.name} not found`);
}
return secret;
}
}

View File

@@ -1,3 +0,0 @@
import SecretStorage from "./SecretStorage";
export default new SecretStorage<string>();

View File

@@ -2,10 +2,13 @@
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,
// Used for sendTransactionBatch TSdoc comment
// eslint-disable-next-line no-unused-vars
TransactionBatch,
UncheckedBlsSigner,
_constructorGuard,
} from "./BlsSigner";
@@ -14,15 +17,28 @@ import BlsWalletWrapper from "./BlsWalletWrapper";
import {
AggregatorUtilities__factory,
BLSWallet__factory,
VerificationGateway__factory,
} from "../typechain-types";
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
/** Public key linked to actions parsed from a bundle */
export type PublicKeyLinkedToActions = {
publicKey: PublicKey;
actions: Array<ActionData>;
};
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
readonly aggregator: Aggregator;
readonly verificationGatewayAddress: string;
readonly aggregatorUtilitiesAddress: string;
signer!: BlsSigner;
/**
* @param aggregatorUrl The url for an aggregator instance
* @param verificationGatewayAddress Verification gateway contract address
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
* @param url Rpc url
* @param network The network the provider should connect to
*/
constructor(
aggregatorUrl: string,
verificationGatewayAddress: string,
@@ -36,37 +52,50 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
}
/**
* @param transaction Transaction request object
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
*/
override async estimateGas(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<BigNumber> {
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],
}),
@@ -76,16 +105,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return addSafetyPremiumToFee(feeRequired);
}
/**
* Sends transaction to be executed. Adds the signed bundle to the aggregator
*
* @param signedTransaction A signed bundle
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
signedTransaction: string | Promise<string>,
): Promise<ethers.providers.TransactionResponse> {
// 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 +135,20 @@ 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,
);
}
/**
* @param signedTransactionBatch A signed {@link TransactionBatch}
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
signedTransactionBatch: string,
): Promise<TransactionBatchResponse> {
// 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,35 +157,40 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
throw new Error(JSON.stringify(result.failures));
}
const actionData: Array<ActionData> = bundle.operations
.map((operation) => operation.actions)
.flat();
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
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,
);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new BlsSigner instance
*/
override getSigner(
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);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new UncheckedBlsSigner instance
*/
override getUncheckedSigner(
privateKey: string,
addressOrIndex?: string,
@@ -168,6 +198,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
*/
override async getTransactionReceipt(
transactionHash: string | Promise<string>,
): Promise<ethers.providers.TransactionReceipt> {
@@ -175,6 +214,17 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
* @param retries The number of retries to poll the receipt for
* @returns
*/
override async waitForTransaction(
transactionHash: string,
confirmations?: number,
@@ -187,6 +237,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
);
}
/**
* @param address The address that the method gets the transaction count from
* @param blockTag The specific block tag to get the transaction count from
* @returns The number of transactions an account has sent
*/
override async getTransactionCount(
address: string | Promise<string>,
blockTag?:
@@ -295,4 +350,104 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
},
];
}
async _constructTransactionResponse(
action: ActionData,
publicKey: PublicKey,
hash: string,
nonce?: BigNumber,
): Promise<ethers.providers.TransactionResponse> {
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<PublicKeyLinkedToActions>,
hash: string,
nonce?: BigNumber,
): Promise<TransactionBatchResponse> {
const chainId = await this.send("eth_chainId", []);
const verificationGateway = VerificationGateway__factory.connect(
this.verificationGatewayAddress,
this,
);
const transactions: Array<ethers.providers.TransactionResponse> = [];
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);
},
};
}
}

View File

@@ -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";
@@ -16,11 +16,13 @@ import { ActionData, bundleToDto } from "./signer";
export const _constructorGuard = {};
/**
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) transaction gas limit
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) miner tip aka priority fee
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) the maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
* @property nonce - integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
* @property chainId - chain ID that this transaction is valid on
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
*
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
* @property chainId - Chain ID that this transaction is valid on
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
*/
export type BatchOptions = {
@@ -33,14 +35,18 @@ export type BatchOptions = {
};
/**
* @property transactions - an array of transaction objects
* @property batchOptions - optional batch options taken into account by smart contract wallets
* @property transactions - An array of Ethers transaction objects
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
*/
export type TransactionBatch = {
transactions: Array<ethers.providers.TransactionRequest>;
batchOptions?: BatchOptions;
};
/**
* @property transactions - An array of Ethers transaction response objects
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
*/
export interface TransactionBatchResponse {
transactions: Array<ethers.providers.TransactionResponse>;
awaitBatchReceipt: (
@@ -58,10 +64,16 @@ export default class BlsSigner extends Signer {
readonly initPromise: Promise<void>;
/**
* @param constructorGuard Prevents BlsSigner constructor being called directly
* @param provider BlsProvider accociated with this signer
* @param privateKey Private key for the account this signer represents
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
*/
constructor(
constructorGuard: Record<string, unknown>,
provider: BlsProvider,
privateKey: string,
privateKey: string | Promise<string>,
readonly addressOrIndex?: string | number,
) {
super();
@@ -92,14 +104,28 @@ export default class BlsSigner extends Signer {
}
}
private async initializeWallet(privateKey: string) {
/** Instantiates a BLS Wallet and then connects the signer to it */
private async initializeWallet(privateKey: string | Promise<string>) {
const resolvedPrivateKey = await privateKey;
this.wallet = await BlsWalletWrapper.connect(
privateKey,
resolvedPrivateKey,
this.verificationGatewayAddress,
this.provider,
);
}
/**
* Sends transactions to be executed. Converts the TransactionRequest
* to a bundle and adds it to the aggregator
*
* @remarks The transaction hash returned in the transaction response does
* NOT correspond to a transaction hash that can be viewed on a block
* explorer. It instead represents the bundle hash, which can be used to
* get a transaction receipt that has a hash that can be used on a block explorer
*
* @param transaction Transaction request object
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionResponse> {
@@ -109,6 +135,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 +144,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,26 +165,30 @@ 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,
);
}
/**
* @param transactionBatch A transaction batch object
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatchResponse> {
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 +198,7 @@ export default class BlsSigner extends Signer {
}
const actions: Array<ActionData> = 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,14 +236,27 @@ export default class BlsSigner extends Signer {
throw new Error(JSON.stringify(result.failures));
}
return this.constructTransactionBatchResponse(
actions,
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
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,
);
}
/**
* @returns The address associated with the BlsSigner
*/
async getAddress(): Promise<string> {
await this.initPromise;
if (this._address) {
@@ -226,83 +267,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<ethers.providers.TransactionResponse> {
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<ActionData>,
hash: string,
from: string,
nonce?: BigNumber,
): Promise<TransactionBatchResponse> {
await this.initPromise;
const chainId = await this.getChainId();
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const transactions: Array<ethers.providers.TransactionResponse> =
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.
@@ -319,19 +283,23 @@ export default class BlsSigner extends Signer {
]);
}
/**
* @remarks Signs a transaction that can be executed by the BlsProvider
*
* @param transaction Transaction request object
* @returns A signed bundle as a string
*/
override async signTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
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 +308,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],
@@ -355,18 +323,24 @@ export default class BlsSigner extends Signer {
return JSON.stringify(bundleToDto(bundle));
}
/**
* Signs a transaction batch that can be executed by the BlsProvider
*
* @param transactionBatch A transaction batch object
* @returns A signed bundle containing all transactions from the transaction batch as a string
*/
async signTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<string> {
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 +350,7 @@ export default class BlsSigner extends Signer {
}
const actions: Array<ActionData> = 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(),
@@ -416,8 +386,8 @@ export default class BlsSigner extends Signer {
return JSON.stringify(bundleToDto(bundle));
}
/** Sign a message */
// TODO: Come back to this once we support EIP-1271
/** Signs a message */
// TODO: bls-wallet #201 Come back to this once we support EIP-1271
override async signMessage(message: Bytes | string): Promise<string> {
await this.initPromise;
if (isBytes(message)) {
@@ -440,6 +410,9 @@ export default class BlsSigner extends Signer {
throw new Error("_signTypedData() is not implemented");
}
/**
* @returns A new Signer object which does not perform additional checks when sending a transaction
*/
connectUnchecked(): BlsSigner {
return new UncheckedBlsSigner(
_constructorGuard,
@@ -453,6 +426,10 @@ export default class BlsSigner extends Signer {
);
}
/**
* @param transaction Transaction request object
* @returns Transaction hash for the transaction, corresponds to a bundle hash
*/
async sendUncheckedTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
@@ -464,6 +441,54 @@ export default class BlsSigner extends Signer {
throw new Error("_legacySignMessage() is not implemented");
}
async _validateTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionRequest> {
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<TransactionBatch> {
const signerAddress = await this.getAddress();
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
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<BatchOptions> {
@@ -481,6 +506,12 @@ export default class BlsSigner extends Signer {
}
export class UncheckedBlsSigner extends BlsSigner {
/**
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
*
* @param transaction Transaction request object
* @returns The transaction response object with only the transaction hash property populated with a valid value
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionResponse> {
@@ -489,7 +520,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: "",

View File

@@ -74,6 +74,7 @@ export default class BlsWalletWrapper {
const blsWalletSigner = await this.#BlsWalletSigner(
signerOrProvider,
privateKey,
verificationGatewayAddress,
);
const verificationGateway = VerificationGateway__factory.connect(
@@ -147,6 +148,7 @@ export default class BlsWalletWrapper {
const blsWalletSigner = await initBlsWalletSigner({
chainId: (await verificationGateway.provider.getNetwork()).chainId,
privateKey,
verificationGatewayAddress,
});
const blsWalletWrapper = new BlsWalletWrapper(
@@ -321,13 +323,18 @@ export default class BlsWalletWrapper {
static async #BlsWalletSigner(
signerOrProvider: SignerOrProvider,
privateKey: string,
verificationGatewayAddress: string,
): Promise<BlsWalletSigner> {
const chainId =
"getChainId" in signerOrProvider
? await signerOrProvider.getChainId()
: (await signerOrProvider.getNetwork()).chainId;
return await initBlsWalletSigner({ chainId, privateKey });
return await initBlsWalletSigner({
chainId,
privateKey,
verificationGatewayAddress,
});
}
/**
@@ -347,6 +354,7 @@ export default class BlsWalletWrapper {
const newBlsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey,
verificationGatewayAddress: this.walletContract.address,
});
this.blsWalletSigner = newBlsWalletSigner;

View File

@@ -1,3 +0,0 @@
import { arrayify, keccak256 } from "ethers/lib/utils";
export default arrayify(keccak256("0xfeedbee5"));

View File

@@ -1,7 +1,7 @@
import { keccak256, solidityPack } from "ethers/lib/utils";
import { Operation } from "./types";
export default (chainId: number) =>
export default () =>
(operation: Operation, walletAddress: string): string => {
let encodedActionData = "0x";
@@ -18,7 +18,7 @@ export default (chainId: number) =>
}
return solidityPack(
["uint256", "address", "uint256", "bytes32"],
[chainId, walletAddress, operation.nonce, keccak256(encodedActionData)],
["address", "uint256", "bytes32"],
[walletAddress, operation.nonce, keccak256(encodedActionData)],
);
};

View File

@@ -0,0 +1,15 @@
import { arrayify, solidityPack } from "ethers/lib/utils";
import { utils } from "ethers";
export default (
chainId: number,
verificationGatewayAddress: string,
type: string,
): Uint8Array => {
const encoded = solidityPack(
["uint256", "address", "string"],
[chainId, verificationGatewayAddress, type],
);
return arrayify(utils.keccak256(encoded));
};

View File

@@ -1,7 +1,7 @@
import { signer } from "@thehubbleproject/bls";
import aggregate from "./aggregate";
import defaultDomain from "./defaultDomain";
import getDomain from "./getDomain";
import getPublicKey from "./getPublicKey";
import getPublicKeyHash from "./getPublicKeyHash";
import getPublicKeyStr from "./getPublicKeyStr";
@@ -16,11 +16,11 @@ export * from "./conversions";
export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
export async function initBlsWalletSigner({
domain = defaultDomain,
chainId,
privateKey,
verificationGatewayAddress,
}: {
domain?: Uint8Array;
verificationGatewayAddress: string;
chainId: number;
privateKey: string;
}) {
@@ -32,14 +32,17 @@ export async function initBlsWalletSigner({
// properly initialized for all use cases, not just signing.
const signerFactory = await signer.BlsSignerFactory.new();
const bundleDomain = getDomain(chainId, verificationGatewayAddress, "Bundle");
const walletDomain = getDomain(chainId, verificationGatewayAddress, "Wallet");
return {
aggregate,
getPublicKey: getPublicKey(signerFactory, domain, privateKey),
getPublicKeyHash: getPublicKeyHash(signerFactory, domain, privateKey),
getPublicKeyStr: getPublicKeyStr(signerFactory, domain, privateKey),
sign: sign(signerFactory, domain, chainId, privateKey),
signMessage: signMessage(signerFactory, domain, privateKey),
verify: verify(domain, chainId),
getPublicKey: getPublicKey(signerFactory, bundleDomain, privateKey),
getPublicKeyHash: getPublicKeyHash(signerFactory, bundleDomain, privateKey),
getPublicKeyStr: getPublicKeyStr(signerFactory, bundleDomain, privateKey),
sign: sign(signerFactory, bundleDomain, chainId, privateKey),
signMessage: signMessage(signerFactory, walletDomain, privateKey),
verify: verify(bundleDomain, chainId),
privateKey,
};
}

View File

@@ -11,7 +11,7 @@ export default (
) =>
(operation: Operation, walletAddress: string): Bundle => {
const signer = signerFactory.getSigner(domain, privateKey);
const message = encodeMessageForSigning(chainId)(operation, walletAddress);
const message = encodeMessageForSigning()(operation, walletAddress);
const signature = signer.sign(message);
return {

View File

@@ -26,7 +26,7 @@ export default (domain: Uint8Array, chainId: number) =>
BigNumber.from(n3).toHexString(),
]),
bundle.operations.map((op) =>
encodeMessageForSigning(chainId)(op, walletAddress),
encodeMessageForSigning()(op, walletAddress),
),
);
};

View File

@@ -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,34 +78,11 @@ 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,
);
const recipient = ethers.Wallet.createRandom().address;
const value = parseEther("1");
const transactionRequest = {
to: recipient,
value,
};
// Act
const gasEstimate = async () =>
await newBlsProvider.estimateGas(transactionRequest);
// Assert
await expect(gasEstimate()).to.be.rejectedWith(
Error,
"Call provider.getSigner first",
expect(newBlsSigner.provider.verificationGatewayAddress).to.equal(
newVerificationGateway,
);
});
@@ -112,6 +90,7 @@ describe("BlsProvider", () => {
// Arrange
const transaction = {
value: parseEther("1"),
// Explicitly omit 'to'
};
// Act
@@ -124,28 +103,21 @@ describe("BlsProvider", () => {
);
});
it("should throw an error sending a transaction when this.signer is not defined", async () => {
it("should throw an error estimating gas when 'transaction.from' has not been defined", async () => {
// Arrange
const newBlsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
const signedTransaction = blsSigner.signTransaction({
to: ethers.Wallet.createRandom().address,
const transaction = {
value: parseEther("1"),
});
to: ethers.Wallet.createRandom().address,
// Explicitly omit 'from'
};
// Act
const result = async () =>
await newBlsProvider.sendTransaction(signedTransaction);
const result = async () => await blsProvider.estimateGas(transaction);
// Assert
await expect(result()).to.be.rejectedWith(
Error,
"Call provider.getSigner first",
TypeError,
"Transaction.from should be defined",
);
});
@@ -159,4 +131,90 @@ describe("BlsProvider", () => {
// 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 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 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 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);
});
});

View File

@@ -1,12 +1,10 @@
import { BigNumber } from "ethers";
import { keccak256, arrayify } from "ethers/lib/utils";
import { expect } from "chai";
import { initBlsWalletSigner, Bundle, Operation } from "../src/signer";
import Range from "./helpers/Range";
const domain = arrayify(keccak256("0xfeedbee5"));
const weiPerToken = BigNumber.from(10).pow(18);
const samples = (() => {
@@ -52,22 +50,22 @@ describe("index", () => {
const { sign, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});
const bundle = sign(bundleTemplate, walletAddress);
expect(bundle.signature).to.deep.equal([
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
"0x21135f40b38f55236ceb637ad8f2d6d4e8081bc1c37ea08273838f839008b9cd",
"0x16515fb0821c039e127dd8e4a70c7004aec1baf698802fc16e7cf8d2ae0bb14a",
]);
expect(verify(bundle, walletAddress)).to.equal(true);
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey: otherPrivateKey,
});
@@ -111,12 +109,12 @@ describe("index", () => {
const { sign, aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey: otherPrivateKey,
});
@@ -125,8 +123,8 @@ describe("index", () => {
const aggBundle = aggregate([bundle1, bundle2]);
expect(aggBundle.signature).to.deep.equal([
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
"0x20c3afd45d2c7cd72003752377cf6853569bccd23abf962967a9245091b69c3b",
"0x1ff4a18f1e920206f849e50df41e7bab6377d3908a8198d9c9268ca01ae70552",
]);
expect(verify(bundle1, walletAddress)).to.equal(true);
@@ -163,7 +161,7 @@ describe("index", () => {
const { sign, aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});
@@ -195,7 +193,7 @@ describe("index", () => {
const { getPublicKeyStr } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});
@@ -215,7 +213,7 @@ describe("index", () => {
const { aggregate } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});
@@ -230,7 +228,7 @@ describe("index", () => {
const { aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
privateKey,
});

View File

@@ -9,6 +9,8 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so
import "./interfaces/IWallet.sol";
import "hardhat/console.sol";
/**
A non-upgradable gateway used to create BLSWallets and call them with
verified Operations that have been respectively signed.
@@ -18,8 +20,8 @@ is the calling wallet's address.
*/
contract VerificationGateway
{
/** Domain chosen arbitrarily */
bytes32 BLS_DOMAIN = keccak256(abi.encodePacked(uint32(0xfeedbee5)));
bytes32 WALLET_DOMAIN;
bytes32 BUNDLE_DOMAIN;
uint8 constant BLS_KEY_LEN = 4;
IBLS public immutable blsLib;
@@ -73,6 +75,16 @@ contract VerificationGateway
blsLib = bls;
blsWalletLogic = blsWalletImpl;
walletProxyAdmin = ProxyAdmin(proxyAdmin);
WALLET_DOMAIN = keccak256(abi.encodePacked(
block.chainid,
address(this),
"Wallet"
));
BUNDLE_DOMAIN = keccak256(abi.encodePacked(
block.chainid,
address(this),
"Bundle"
));
}
/** Throw if bundle not valid or signature verification fails */
@@ -353,7 +365,7 @@ contract VerificationGateway
) private {
// verify the given wallet was signed for by the bls key
uint256[2] memory addressMsg = blsLib.hashToPoint(
BLS_DOMAIN,
WALLET_DOMAIN,
abi.encodePacked(wallet)
);
require(
@@ -411,9 +423,8 @@ contract VerificationGateway
);
}
return blsLib.hashToPoint(
BLS_DOMAIN,
BUNDLE_DOMAIN,
abi.encodePacked(
block.chainid,
walletAddress,
op.nonce,
keccak256(encodedActionData)

View File

@@ -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",

View File

@@ -146,7 +146,11 @@ export default class Fixture {
blsExpander,
utilities,
BLSWallet,
await initBlsWalletSigner({ chainId, privateKey }),
await initBlsWalletSigner({
chainId,
privateKey,
verificationGatewayAddress: verificationGateway.address,
}),
);
}

View File

@@ -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,13 +436,12 @@ 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"),
});
const expectedToAddress = "0x689A095B4507Bfa302eef8551F90fB322B3451c6"; // Verification Gateway address
const expectedToAddress = "0x14EE47429DEf3462142AE5f8d1E263E0B137bA63"; // Verification Gateway address
const expectedFromAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // Aggregator address (Hardhat account 0)
// Act
@@ -489,13 +497,12 @@ 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"),
});
const expectedToAddress = "0x689A095B4507Bfa302eef8551F90fB322B3451c6"; // Verification Gateway address
const expectedToAddress = "0x14EE47429DEf3462142AE5f8d1E263E0B137bA63"; // Verification Gateway address
const expectedFromAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // Aggregator address (Hardhat account 0)
// Act
@@ -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",
);
});
});

View File

@@ -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(

View File

@@ -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<typeof Experimental.BlsProvider>;
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
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);
// Act
const transaction = await wallet.sendTransaction({
const getBalancePromise = blsSigner.getBalance();
const expectedValidatedTransaction = {
to: recipient,
value: expectedBalance,
});
await transaction.wait();
// Assert
expect(
(await regularProvider.getBalance(recipient)).sub(recipientBalanceBefore),
).to.equal(expectedBalance);
});
it("should check transaction", async () => {
// Arrange
const recipient = ethers.Wallet.createRandom().address;
const transactionAmount = parseEther("1");
const transaction = {
to: recipient,
value: transactionAmount,
value: await blsSigner.getBalance(),
from: await blsSigner.getAddress(),
};
// Act
const result = wallet.checkTransaction(transaction);
const validatedTransaction = await blsSigner._validateTransaction({
to: recipient,
value: getBalancePromise,
});
// Assert
const resolvedResult = await resolveProperties(result);
expect(resolvedResult)
.to.be.an("object")
.that.includes({
to: recipient,
value: transactionAmount,
from: await wallet.getAddress(),
expect(validatedTransaction).to.deep.equal(expectedValidatedTransaction);
});
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
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);
// Assert
expect(result).to.be.an("object").that.includes({
to: recipient,
value: transactionAmount,
from: signers[2].address,
type: 2,
chainId: 1337,
const validatedTransaction = await blsSigner._validateTransactionBatch({
transactions: [
{
to: recipient,
value: amount,
},
],
});
expect(result).to.include.keys(
"maxFeePerGas",
"maxPriorityFeePerGas",
"nonce",
"gasLimit",
// Assert
expect(validatedTransaction).to.deep.equal(
expectedValidatedTransactionBatch,
);
});
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",
);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -121,14 +121,14 @@ describe("Upgrade", async function () {
const walletAddress = walletOldVg.address;
const blsSecret = walletOldVg.blsWalletSigner.privateKey;
const wallet = await BlsWalletWrapper.connect(
blsSecret,
fx.verificationGateway.address,
fx.verificationGateway.provider,
);
// Sign simple address message
const addressMessage = solidityPack(["address"], [walletAddress]);
const addressSignature = wallet.signMessage(addressMessage);
const walletNewVg = await BlsWalletWrapper.connect(
blsSecret,
vg2.address,
vg2.provider,
);
const addressSignature = walletNewVg.signMessage(addressMessage);
const proxyAdmin2Address = await vg2.walletProxyAdmin();
// Get admin action to change proxy
@@ -181,19 +181,18 @@ describe("Upgrade", async function () {
{
// Fail if setExternalWalletAction is skipped
const { successes } =
await fx.verificationGateway.callStatic.processBundle(
walletOldVg.sign({
nonce: BigNumber.from(2),
actions: [
// skip: setExternalWalletAction,
changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
const result = await fx.verificationGateway.callStatic.processBundle(
walletOldVg.sign({
nonce: BigNumber.from(2),
actions: [
// skip: setExternalWalletAction,
changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
expect(successes).to.deep.equal([false]);
expect(result.successes).to.deep.equal([false]);
}
{
@@ -276,10 +275,11 @@ describe("Upgrade", async function () {
// Check new verification gateway was set
expect(await blsWallet.trustedBLSGateway()).to.equal(vg2.address);
await walletNewVg.syncWallet(vg2);
// Check new gateway has wallet via static call through new gateway
const bundleResult = await vg2.callStatic.processBundle(
fx.blsWalletSigner.aggregate([
walletOldVg.sign({
walletNewVg.sign({
nonce: BigNumber.from(3),
actions: [
{

View File

@@ -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==

View File

@@ -3,6 +3,7 @@
- [See an overview of BLS Wallet & how the components work together](./system_overview.md)
- [Use BLS Wallet in a browser/NodeJS/Deno app](./use_bls_wallet_clients.md)
- [Use BLS Wallet in your L2 dApp for cheaper, multi action transactions](./use_bls_wallet_dapp.md)
- [Use BLS Wallet components and features with an ethers.js provider and signer](./use_bls_provider.md)
- Setup the BLS Wallet components for:
- [Local development](./local_development.md)
- [Remote development](./remote_development.md)

224
docs/use_bls_provider.md Normal file
View File

@@ -0,0 +1,224 @@
# BLS Provider
This document will show you how to interact with the `BlsProvider` and `BlsSigner` classes.
The `BlsProvider` and `BlsSigner` are part of the `bls-wallet-clients` npm package, and help you interact with BLS Wallet components in a similar way you would use an Ethers provider and signer to interact with the Ethereum ecosystem. It offers developers a familiar develoment experience, while providing access to BLS Wallet components and features. Essentially it's a Ethers provider-shaped wrapper around `bls-wallet-clients`.
The `BlsProvider` and `BlsSigner` mimic the behaviour of an Ethers [JsonRpcProvider](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/) and [JsonRpcSigner](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/#JsonRpcSigner) respectively. In this implementation, note that the `BlsSigner` has knowledge of its own private key. For more information on Ethers providers and signers, visit the [Ethers v5 docs](https://docs.ethers.org/v5/).
The `BlsProvider` and `BlsSigner` are covered by over 100 test cases, including integration tests. If any functionality is not documented here, it will likely be documented by test cases.
# Creating instances
## BlsProvider
### Instantiating a BlsProvider
```ts
import { Experimental } from "bls-wallet-clients";
const aggregatorUrl = "http://localhost:3000";
const verificationGateway = "0x123";
const aggregatorUtilities = "0x321";
const rpcUrl = "http://localhost:8545";
const network = {
name: "localhost",
chainId: 0x539, // 1337
};
const provider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network
);
```
### BlsSigner
**Important:** Ensure that the BLS wallet you are linking the `BlsSigner` to via the private key is funded. Alternatively, if a wallet doesn't yet exist, it will be lazily created on the first transaction. In this scenario, you can create a random BLS private key with the following helper method and fund that account. It will need to be funded in order to send its first transaction.
```ts
import { BlsWalletWrapper } from "bls-wallet-clients";
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const signer = provider.getSigner(privateKey);
// Send funds to this address if the wallet does not exist
const address = await signer.getAddress();
```
### Instantiating a BlsSigner
```ts
// 32 random bytes
const privateKey =
"0x0001020304050607080910111213141516171819202122232425262728293031";
const signer = provider.getSigner(privateKey);
```
# Send ETH
### Send ETH via BlsSigner
```ts
const transactionResponse = await signer.sendTransaction({
to: recipient,
value: amountToTransfer,
});
const transactionReceipt = await transactionResponse.wait();
```
### Send ETH via BlsProvider
```ts
// Note the transaction must be signed via the BlsSigner first
const signedTransaction = await signer.signTransaction({
to: recipient,
value: amountToTransfer,
});
const transactionResponse = await provider.sendTransaction(signedTransaction);
const transactionReceipt = await transactionResponse.wait();
```
# Get a transaction receipt via a transaction hash
This will return a transaction receipt that corresponds to a transaction and transaction hash that can be quired on a block explorer.
```ts
const transactionReceipt = await provider.getTransactionReceipt(
transactionResponse.hash
);
```
# Multi-action transactions
### Send multi-action transactions with BlsSigner
```ts
// using BlsSigner
const transactionBatchResponse = await signer.sendTransactionBatch(
{
to: recipient1,
value: amountToTransfer,
},
{
to: recipient2,
value: amountToTransfer,
}
);
const transactionReceipt = await transactionBatchResponse.awaitBatchReceipt();
```
### Send multi-action transactions with BlsProvider
```ts
// Note the transaction must be signed via the BlsSigner first
const signedTransactionBatch = await signer.signTransactionBatch(
{
to: recipient1,
value: amountToTransfer,
},
{
to: recipient2,
value: amountToTransfer,
}
);
const transactionBatchResponse = await provider.sendTransactionBatch(
signedTransactionBatch
);
const transactionReceipt = await transactionBatchResponse.awaitBatchReceipt();
```
# Interacting with smart contracts
### Interacting with a deployed smart contract
```ts
const ERC20 = new ethers.Contract(tokenAddress, tokenInterface, signer);
const transaction = await ERC20.transfer(recipient, amountToTransfer);
await transaction.wait();
```
### Interacting with a smart contract that hasn't been deployed
**Important:** You cannot deploy contracts with a BLS Wallet. Use a funded EOA for deploying
contracts instead. Then make sure you connect to the contract instance with your `BlsSigner`.
Contract deployments via a BLS Wallet is a feature tabled for our V2 contracts.
```ts
// Deploying contracts must be done by a non-BLS Wallet account
const nonBLSWalletAccount = new ethers.Wallet(fundedWalletPrivateKey, provider);
const ERC20 = await ethers.getContractFactory("ERC20");
const erc20 = await ERC20.connect(nonBLSWalletAccount).deploy(
tokenName,
tokenSymbol,
tokenSupply
);
await erc20.deployed();
const signer = provider.getSigner(privateKey);
const transaction = await erc20
.connect(signer)
.transfer(recipient, amountToTransfer);
await transaction.wait();
```
### Multi-action contract interactions
```ts
// Working example of this setup can be found in BlsSignerContractInteraction.test.ts
const transactionBatch = {
transactions: [
{
to: ERC20.address,
value: 0,
data: ERC20.interface.encodeFunctionData("approve", [
spender,
amountToTransfer,
]),
},
{
to: spender,
value: 0,
data: mockTokenSpender.interface.encodeFunctionData(
"TransferERC20ToSelf",
[ERC20.address, amountToTransfer]
),
},
],
};
const result = await signer.sendTransactionBatch(transactionBatch);
await result.awaitBatchReceipt();
```
# Estimating gas for a transaction
The `BlsProvider` adds a small safety premium to the gas estimate to improve the likelyhood a bundle gets included during aggregation. If you'd like more fined grained control over the fee, you use the [helper method](../contracts//clients/README.md#estimating-and-paying-fees) from the `bls-wallet-clients` package directly instead.
### Estimating gas with BlsProvider
```ts
const fee = await provider.estimateGas({
to: recipient,
value: amountToTransfer,
});
```
### Estimating gas when interacting with a contract
```ts
const feeEstimate = await ERC20.estimateGas.transfer(
recipient,
amountToTransfer
);
```

View File

@@ -36,7 +36,7 @@
"advanced-css-reset": "^1.2.2",
"async-mutex": "^0.3.2",
"axios": "^0.27.2",
"bls-wallet-clients": "0.8.2-1452ef5",
"bls-wallet-clients": "0.8.2-1fb4a55",
"browser-passworder": "^2.0.3",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",

View File

@@ -2907,10 +2907,10 @@ blakejs@^1.1.0:
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
bls-wallet-clients@0.8.2-1452ef5:
version "0.8.2-1452ef5"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
bls-wallet-clients@0.8.2-1fb4a55:
version "0.8.2-1fb4a55"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"