Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates

This commit is contained in:
jacque006
2023-03-29 12:02:14 -06:00
38 changed files with 2758 additions and 755 deletions

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-77f1638",
"bls-wallet-clients": "0.8.2-1452ef5",
"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-77f1638:
version "0.8.2-77f1638"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-77f1638.tgz#61212aac502aed821bc6036cb95a2cdaa9c0b67e"
integrity sha512-2gTVHUvs4+/fBwgtcZCO+DsKTnJAiebYrpCUdygIqZaFeHdEyp6xU2F8ZT+XOcNoMeN1B9TT5EupV1RTvISFkQ==
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==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"

View File

@@ -23,15 +23,15 @@ MAX_UNCONFIRMED_AGGREGATIONS=3
LOG_QUERIES=true
TEST_LOGGING=true
REQUIRE_FEES=false
BREAKEVEN_OPERATION_COUNT=4.5
REQUIRE_FEES=true
BREAKEVEN_OPERATION_COUNT=2.5
ALLOW_LOSSES=true
FEE_TYPE=ether
AUTO_CREATE_INTERNAL_BLS_WALLET=true
PRIORITY_FEE_PER_GAS=0
PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8

View File

@@ -6,6 +6,63 @@ Accepts transaction bundles (including bundles that contain a single
transaction) and submits aggregations of these bundles to the configured
Verification Gateway.
## Docker Usage
Docker images of the aggregator are
[available on DockerHub](https://hub.docker.com/r/blswallet/aggregator).
If you're targeting a network that
[already has a deployment of the BLSWallet contracts](../contracts/networks),
you can use these images standalone (without this repository) as follows:
```sh
mkdir aggregator
cd aggregator
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/.env.example >.env
# Replace CHOSEN_NETWORK below
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/contracts/networks/CHOSEN_NETWORK.json >networkConfig.json
```
In `.env`:
- Change `RPC_URL`
- (If using `localhost`, you probably want `host.docker.internal`)
- Change `PRIVATE_KEY_AGG`
- Ignore `NETWORK_CONFIG_PATH` (it's not used inside docker)
- See [Configuration](#configuration) for more detail and other options
If you're running in production, you might want to set
`AUTO_CREATE_INTERNAL_BLS_WALLET` to `false`. The internal BLS wallet is needed
for user fee estimation. Creating it is a one-time setup that will use
`PRIVATE_KEY_AGG` to pay for gas. You can create it explicitly like this:
```sh
docker run \
--rm \
-it \
--mount type=bind,source="$PWD/.env",target=/app/.env \
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
blswallet/aggregator \
./ts/programs/createInternalBlsWallet.ts
```
Finally, start the aggregator:
```sh
docker run \
--name choose-container-name \ # Optional
-d \ # Optional
-p3000:3000 \ # If you chose a different PORT in .env, change it here too
--restart=unless-stopped \ # Optional
--mount type=bind,source="$PWD/.env",target=/app/.env \
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
blswallet/aggregator # Tags of the form :git-$VERSION are also available
```
(You may need to remove the comments before pasting into your terminal.)
## Installation
Install [Deno](deno.land)
@@ -69,6 +126,20 @@ Can be run locally or hosted.
# ./programs/aggregator.ts --env <name>
```
**Note**: It's also possible to run the aggregator directly from github:
```sh
deno run \
--allow-net \
--allow-env \
--allow-read=. \
--allow-write=. \
https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/programs/aggregator.ts
```
(This can be done without a clone of the repository, but you'll still need to
set up `.env` and your network config.)
## Testing
- launch optimism
@@ -260,16 +331,9 @@ Make sure your Deno version is
## Hosting Guide
1. Configure your server to allow TCP on ports 80 and 443
2. Follow the [Installation](#Installation) instructions
3. Install docker and nginx:
2. Install docker and nginx:
`sudo apt update && sudo apt install docker.io nginx`
4. Run `./programs/build.ts`
- If you're using a named environment, add `--env <name>`
- If `docker` requires `sudo`, add `--sudo-docker`
5. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
3. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
```json
{
@@ -283,18 +347,9 @@ Make sure your Deno version is
and restart docker `sudo systemctl restart docker`
6. Load the docker image: `sudo docker load <docker-image.tar.gz`
7. Copy `./programs/start-docker.sh` onto the server
8. Run the aggregator:
```sh
VERSION=abc1234 ./start-docker.sh
# Replace abc1234 with the first 7 characters of the git sha used when building
# A .env file is also required. You can also specify an alternative path using
# ENV_PATH=/custom/path/to/.env
```
9. Create `/etc/nginx/sites-available/aggregator`
4. Follow the [Docker Usage](#docker-usage) instructions (just use port 3000,
external requests are handled by nginx)
5. Create `/etc/nginx/sites-available/aggregator`
```nginx
server {
@@ -317,7 +372,7 @@ This allows you to add some static content at `/home/aggregator/static-content`.
Adding static content is optional; requests that don't match static content will
be passed to the aggregator.
10. Create a symlink in sites-enabled
6. Create a symlink in sites-enabled
```sh
ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
@@ -325,5 +380,5 @@ ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
Reload nginx for config to take effect: `sudo nginx -s reload`
11. Set up https for your domain by following the instructions at
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.
7. Set up https for your domain by following the instructions at
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.

View File

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

View File

@@ -10,6 +10,6 @@ const wallet = await TestBlsWallet(
);
console.log({
privateKey: wallet.privateKey,
privateKey: wallet.blsWalletSigner.privateKey,
address: wallet.walletContract.address,
});

View File

@@ -133,7 +133,10 @@ export default class EthereumService {
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
const chainId = await wallet.getChainId();
const blsWalletSigner = await initBlsWalletSigner({ chainId });
const blsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey: aggPrivateKey,
});
return new EthereumService(
emit,

View File

@@ -146,6 +146,75 @@ Practically, this means you have to first estimate the fee using `aggregator.est
### Paying aggregator fees with native currency (ETH)
```ts
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
const wallet = await BlsWalletWrapper.connect(
privateKey,
verificationGatewayAddress,
provider,
);
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
// Create a fee estimate bundle
const estimateFeeBundle = wallet.sign({
nonce,
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 1,
// Provide 1 wei with this action so that the fee transfer to
// tx.origin can be included in the gas estimate.
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
],
});
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
// Add a safety premium to the fee to account for fluctuations in gas estimation
const safetyDivisor = 5;
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
const safetyPremium = feeRequired.div(safetyDivisor);
const safeFee = feeRequired.add(safetyPremium);
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: safeFee, // fee amount
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
],
});
```
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.
```ts
@@ -157,89 +226,84 @@ const wallet = await BlsWalletWrapper.connect(
provider,
);
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
const estimateFee = await aggregator.estimateFee(bundle); // Remember to include no payment with this bundle
// Create a fee estimate bundle
const estimateFeeBundle = wallet.sign({
nonce,
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
aggregatorUtilitiesContract.address,
1,
]),
},
{
ethValue: 0,
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendTokenToTxOrigin",
[tokenContract.address, 1],
),
},
],
});
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
// Add a safety premium to the fee to account for fluctuations in gas estimation
const safetyDivisor = 5;
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
const safetyPremium = feeRequired.div(safetyDivisor);
const safeFee = feeRequired.add(safetyPremium);
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... do your transaction/actions here (approve, transfer, etc.)
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
// then, if the aggregator is using native chain currency, such as ETH
{
ethValue: estimateFee.feeRequired, // fee amount
contractAddress: aggregatorAddress,
encodedFunction: "0x"
}
// or you can use the AggregatorUtilities.sol contract
// this makes the bundle submittable from any ETH paid aggregator
{
ethValue: estimateFee.feeRequired, // fee amount
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
'sendEthToTxOrigin', [],
),
}
],
// Note the additional approve action when transfering ERC20 tokens
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
aggregatorUtilitiesContract.address,
safeFee, // fee amount
]),
},
{
ethValue: 0,
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendTokenToTxOrigin",
[
tokenContract.address,
safeFee, // fee amount
],
),
},
],
});
```
### Paying aggregator fees with custom currency (ERC20)
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
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
const wallet = await BlsWalletWrapper.connect(
privateKey,
verificationGatewayAddress,
provider,
);
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
const estimateFee = await aggregator.estimateFee(bundle); // Remember to include no payment with this bundle
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... do your transaction/actions here (approve, transfer, etc.)
// then, if the aggregator is using an ERC20 token
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData(
'transfer',
[
aggregatorAddress,
estimateFee.feeRequired, // fee amount
],
),
}
// or you can use the AggregatorUtilities.sol contract
// this makes the bundle submittable from any paid aggregator with that token
// note the additional approve action
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData(
"approve",
[
aggregatorUtilitiesContract.address,
estimateFee.feeRequired, // fee amount
],
),
},
{
ethValue: 0,
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendTokenToTxOrigin",
[
tokenContract.address,
estimateFee.feeRequired, // fee amount
],
),
},
],
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
]),
},
],
});
```
@@ -301,10 +365,10 @@ import ethers from "ethers";
import { initBlsWalletSigner } from "bls-wallet-clients";
(async () => {
const signer = await initBlsWalletSigner({ chainId: 10 });
const privateKey = "0x...256 bits of private hex data here";
const signer = await initBlsWalletSigner({ chainId: 10, privateKey });
const someToken = new ethers.Contract(
...
// See https://docs.ethers.io/v5/getting-started/
@@ -323,7 +387,6 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
ethers.BigNumber.from(10).pow(18),
]),
},
privateKey,
);
// Send bundle to an aggregator or use it with VerificationGateway directly.

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.8.2-77f1638",
"version": "0.8.2-1452ef5",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",

View File

@@ -1,80 +1,151 @@
/* eslint-disable camelcase */
import { ethers, BigNumber } from "ethers";
import { parseEther, Deferrable } from "ethers/lib/utils";
import { Deferrable } from "ethers/lib/utils";
import { ActionDataDto, BundleDto } from "./signer/types";
import { ActionData, Bundle, PublicKey } from "./signer/types";
import Aggregator, { BundleReceipt } from "./Aggregator";
import BlsSigner, { UncheckedBlsSigner, _constructorGuard } from "./BlsSigner";
import BlsSigner, {
TransactionBatchResponse,
UncheckedBlsSigner,
_constructorGuard,
} from "./BlsSigner";
import poll from "./helpers/poll";
import BlsWalletWrapper from "./BlsWalletWrapper";
import {
AggregatorUtilities__factory,
BLSWallet__factory,
VerificationGateway__factory,
} from "../typechain-types";
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
export type PublicKeyLinkedToActions = {
publicKey: PublicKey;
actions: Array<ActionData>;
};
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
readonly aggregator: Aggregator;
readonly verificationGatewayAddress: string;
signer!: BlsSigner;
readonly aggregatorUtilitiesAddress: string;
constructor(
aggregatorUrl: string,
verificationGatewayAddress: string,
aggregatorUtilitiesAddress: string,
url?: string,
network?: ethers.providers.Networkish,
) {
super(url, network);
this.aggregator = new Aggregator(aggregatorUrl);
this.verificationGatewayAddress = verificationGatewayAddress;
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
}
// TODO: bls-wallet #410 estimate gas for a transaction
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 signedTransaction = await this.signer.signTransaction(transaction);
const bundleDto: BundleDto = JSON.parse(signedTransaction);
const action: ActionData = {
ethValue: resolvedTransaction.value?.toString() ?? "0",
contractAddress: resolvedTransaction.to.toString(),
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
};
const gasEstimate = await this.aggregator.estimateFee(bundleDto);
return parseEther(gasEstimate.feeRequired);
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(
throwawayBlsWalletWrapper.sign({
nonce,
actions: [...actionWithFeePaymentAction],
}),
);
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
return addSafetyPremiumToFee(feeRequired);
}
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);
if (bundle.operations.length > 1) {
throw new Error(
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
);
}
const resolvedTransaction = await signedTransaction;
const bundleDto: BundleDto = JSON.parse(resolvedTransaction);
const result = await this.aggregator.add(bundleDto);
const result = await this.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
// TODO: bls-wallet #375 Add multi-action transactions to BlsProvider & BlsSigner
// We're assuming the first operation and action constitute the correct values. We will need to refactor this when we add multi-action transactions
const actionData: ActionDataDto = {
ethValue: bundleDto.operations[0].actions[0].ethValue,
contractAddress: bundleDto.operations[0].actions[0].contractAddress,
encodedFunction: bundleDto.operations[0].actions[0].encodedFunction,
const actionData: ActionData = {
ethValue: bundle.operations[0].actions[0].ethValue,
contractAddress: bundle.operations[0].actions[0].contractAddress,
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
};
return this.signer.constructTransactionResponse(
return await this._constructTransactionResponse(
actionData,
bundle.senderPublicKeys[0],
result.hash,
);
}
async sendTransactionBatch(
signedTransactionBatch: string,
): Promise<TransactionBatchResponse> {
const bundle: Bundle = JSON.parse(signedTransactionBatch);
const result = await this.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
bundle.senderPublicKeys.map((publicKey, i) => {
const operation = bundle.operations[i];
const actions = operation.actions;
return {
publicKey,
actions,
};
});
return await this._constructTransactionBatchResponse(
publicKeysLinkedToActions,
result.hash,
this.signer.wallet.address,
);
}
@@ -82,18 +153,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
privateKey: string,
addressOrIndex?: string | number,
): BlsSigner {
if (this.signer) {
return this.signer;
}
const signer = new BlsSigner(
_constructorGuard,
this,
privateKey,
addressOrIndex,
);
this.signer = signer;
return signer;
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
}
override getUncheckedSigner(
@@ -122,6 +182,27 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
);
}
override async getTransactionCount(
address: string | Promise<string>,
blockTag?:
| ethers.providers.BlockTag
| Promise<ethers.providers.BlockTag>
| undefined,
): Promise<number> {
const walletContract = BLSWallet__factory.connect(await address, this);
const code = await walletContract.provider.getCode(address, blockTag);
if (code === "0x") {
// The wallet doesn't exist yet. Wallets are lazily created, so the nonce
// is effectively zero, since that will be accepted as valid for a first
// operation that also creates the wallet.
return 0;
}
return Number(await walletContract.nonce());
}
async _getTransactionReceipt(
transactionHash: string,
confirmations: number,
@@ -164,4 +245,149 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
status: bundleReceipt.status,
};
}
_addFeePaymentActionForFeeEstimation(
actions: Array<ActionData>,
): Array<ActionData> {
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
this.aggregatorUtilitiesAddress,
this,
);
return [
...actions,
{
// Provide 1 wei with this action so that the fee transfer to
// tx.origin can be included in the gas estimate.
ethValue: 1,
contractAddress: this.aggregatorUtilitiesAddress,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
];
}
_addFeePaymentActionWithSafeFee(
actions: Array<ActionData>,
fee: BigNumber,
): Array<ActionData> {
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
this.aggregatorUtilitiesAddress,
this,
);
return [
...actions,
{
ethValue: fee,
contractAddress: this.aggregatorUtilitiesAddress,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
];
}
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

@@ -1,15 +1,57 @@
import { ethers, BigNumber, Signer, Bytes } from "ethers";
import { Deferrable, hexlify, isBytes, RLP } from "ethers/lib/utils";
/* eslint-disable camelcase */
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
import {
AccessListish,
Deferrable,
hexlify,
isBytes,
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";
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
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
*/
export type BatchOptions = {
gas?: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
maxFeePerGas: BigNumberish;
nonce: BigNumberish;
chainId: number;
accessList?: AccessListish;
};
/**
* @property transactions - an array of transaction objects
* @property batchOptions - optional batch options taken into account by smart contract wallets
*/
export type TransactionBatch = {
transactions: Array<ethers.providers.TransactionRequest>;
batchOptions?: BatchOptions;
};
export interface TransactionBatchResponse {
transactions: Array<ethers.providers.TransactionResponse>;
awaitBatchReceipt: (
confirmations?: number,
) => Promise<ethers.providers.TransactionReceipt>;
}
export default class BlsSigner extends Signer {
override readonly provider: BlsProvider;
readonly verificationGatewayAddress!: string;
readonly aggregatorUtilitiesAddress!: string;
wallet!: BlsWalletWrapper;
_index: number;
_address: string;
@@ -25,6 +67,7 @@ export default class BlsSigner extends Signer {
super();
this.provider = provider;
this.verificationGatewayAddress = this.provider.verificationGatewayAddress;
this.aggregatorUtilitiesAddress = this.provider.aggregatorUtilitiesAddress;
this.initPromise = this.initializeWallet(privateKey);
if (constructorGuard !== _constructorGuard) {
@@ -67,12 +110,7 @@ export default class BlsSigner extends Signer {
throw new TypeError("Transaction.to should be defined");
}
// TODO: bls-wallet #375 Add multi-action transactions to BlsProvider & BlsSigner
const action: ActionData = {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
};
const validatedTransaction = await this._validateTransaction(transaction);
const nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
@@ -80,9 +118,21 @@ export default class BlsSigner extends Signer {
this.provider,
);
const bundle = await this.wallet.signWithGasEstimate({
const action: ActionData = {
ethValue: validatedTransaction.value?.toString() ?? "0",
contractAddress: validatedTransaction.to!.toString(),
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
};
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
nonce,
actions: [action],
actions: [...actionsWithSafeFee],
});
const result = await this.provider.aggregator.add(bundle);
@@ -90,10 +140,87 @@ 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,
nonce,
);
}
async sendTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatchResponse> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
};
},
);
const actionsWithFeePaymentAction =
this.provider._addFeePaymentActionForFeeEstimation(actions);
const feeEstimate = await this.provider.aggregator.estimateFee(
this.wallet.sign({
nonce,
actions: [...actionsWithFeePaymentAction],
}),
);
const safeFee = addSafetyPremiumToFee(
BigNumber.from(feeEstimate.feeRequired),
);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
actions,
safeFee,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
const result = await this.provider.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
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,
);
}
@@ -108,40 +235,6 @@ export default class BlsSigner extends Signer {
return this._address;
}
// Construct a response that follows the ethers TransactionResponse type
async constructTransactionResponse(
action: ActionData,
hash: string,
from: string,
nonce?: BigNumber,
): Promise<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: BigNumber.from(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);
},
};
}
/**
* 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.
@@ -163,14 +256,12 @@ export default class BlsSigner extends Signer {
): 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(
@@ -179,10 +270,75 @@ export default class BlsSigner extends Signer {
this.provider,
);
const bundle = await this.wallet.signWithGasEstimate({
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
nonce,
actions: [action],
actions: [...actionsWithSafeFee],
});
return JSON.stringify(bundleToDto(bundle));
}
async signTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<string> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
};
},
);
const actionsWithFeePaymentAction =
this.provider._addFeePaymentActionForFeeEstimation(actions);
const feeEstimate = await this.provider.aggregator.estimateFee(
this.wallet.sign({
nonce,
actions: [...actionsWithFeePaymentAction],
}),
);
const safeFee = addSafetyPremiumToFee(
BigNumber.from(feeEstimate.feeRequired),
);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
actions,
safeFee,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
return JSON.stringify(bundleToDto(bundle));
}
@@ -214,10 +370,10 @@ export default class BlsSigner extends Signer {
return new UncheckedBlsSigner(
_constructorGuard,
this.provider,
this.wallet?.privateKey ??
this.wallet?.blsWalletSigner.privateKey ??
(async (): Promise<string> => {
await this.initPromise;
return this.wallet.privateKey;
return this.wallet.blsWalletSigner.privateKey;
})(),
this._address || this._index,
);
@@ -233,6 +389,69 @@ export default class BlsSigner extends Signer {
async _legacySignMessage(message: Bytes | string): Promise<string> {
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> {
const expectedChainId = await this.getChainId();
if (batchOptions.chainId !== expectedChainId) {
throw new Error(
`Supplied chain ID ${batchOptions.chainId} does not match the expected chain ID ${expectedChainId}`,
);
}
batchOptions.nonce = BigNumber.from(batchOptions.nonce);
return batchOptions;
}
}
export class UncheckedBlsSigner extends BlsSigner {
@@ -244,7 +463,7 @@ export class UncheckedBlsSigner extends BlsSigner {
const transactionResponse = await super.sendTransaction(transaction);
return {
hash: transactionResponse.hash,
nonce: 1,
nonce: NaN,
gasLimit: BigNumber.from(0),
gasPrice: BigNumber.from(0),
data: "",

View File

@@ -29,7 +29,6 @@ export default class BlsWalletWrapper {
public blockGasLimit: BigNumber = BigNumber.from(0);
private constructor(
public blsWalletSigner: BlsWalletSigner,
public privateKey: string,
public walletContract: BLSWallet,
public defaultGatewayAddress: string,
) {
@@ -65,22 +64,23 @@ export default class BlsWalletWrapper {
* @param privateKey private key associated with the wallet
* @param verificationGatewayAddress address of the VerficationGateway contract
* @param signerOrProvider ethers.js Signer or Provider
* @param blsWalletSigner (optional) a BLS Wallet signer
* @returns The wallet's address
*/
static async Address(
privateKey: string,
verificationGatewayAddress: string,
signerOrProvider: SignerOrProvider,
blsWalletSigner?: BlsWalletSigner,
): Promise<string> {
blsWalletSigner ??= await this.#BlsWalletSigner(signerOrProvider);
const blsWalletSigner = await this.#BlsWalletSigner(
signerOrProvider,
privateKey,
);
const verificationGateway = VerificationGatewayFactory.connect(
verificationGatewayAddress,
signerOrProvider,
);
const pubKeyHash = blsWalletSigner.getPublicKeyHash(privateKey);
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
const existingAddress = await verificationGateway.walletFromHash(
pubKeyHash,
@@ -98,7 +98,6 @@ export default class BlsWalletWrapper {
blsWalletSigner,
verificationGateway,
expectedAddress,
privateKey,
);
return expectedAddress;
@@ -147,11 +146,11 @@ export default class BlsWalletWrapper {
);
const blsWalletSigner = await initBlsWalletSigner({
chainId: (await verificationGateway.provider.getNetwork()).chainId,
privateKey,
});
const blsWalletWrapper = new BlsWalletWrapper(
blsWalletSigner,
privateKey,
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
verificationGateway.address,
);
@@ -164,7 +163,7 @@ export default class BlsWalletWrapper {
async syncWallet(verificationGateway: VerificationGateway) {
this.address = await BlsWalletWrapper.Address(
this.privateKey,
this.blsWalletSigner.privateKey,
verificationGateway.address,
verificationGateway.provider,
);
@@ -272,16 +271,12 @@ export default class BlsWalletWrapper {
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
sign(operation: Operation): Bundle {
return this.blsWalletSigner.sign(
operation,
this.privateKey,
this.walletContract.address,
);
return this.blsWalletSigner.sign(operation, this.walletContract.address);
}
/** Sign a message */
signMessage(message: string): Signature {
return this.blsWalletSigner.signMessage(message, this.privateKey);
return this.blsWalletSigner.signMessage(message);
}
/**
@@ -290,7 +285,7 @@ export default class BlsWalletWrapper {
* @returns Wallet's BLS public key.
*/
PublicKey(): PublicKey {
return this.blsWalletSigner.getPublicKey(this.privateKey);
return this.blsWalletSigner.getPublicKey();
}
/**
@@ -299,7 +294,7 @@ export default class BlsWalletWrapper {
* @returns Wallet's BLS public key as a string.
*/
PublicKeyStr(): string {
return this.blsWalletSigner.getPublicKeyStr(this.privateKey);
return this.blsWalletSigner.getPublicKeyStr();
}
async getSetRecoveryHashBundle(
@@ -307,7 +302,7 @@ export default class BlsWalletWrapper {
recoverWalletAddress: string,
): Promise<Bundle> {
const saltHash = ethers.utils.formatBytes32String(salt);
const walletHash = this.blsWalletSigner.getPublicKeyHash(this.privateKey);
const walletHash = this.blsWalletSigner.getPublicKeyHash();
const recoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverWalletAddress, walletHash, saltHash],
@@ -369,13 +364,37 @@ export default class BlsWalletWrapper {
static async #BlsWalletSigner(
signerOrProvider: SignerOrProvider,
privateKey: string,
): Promise<BlsWalletSigner> {
const chainId =
"getChainId" in signerOrProvider
? await signerOrProvider.getChainId()
: (await signerOrProvider.getNetwork()).chainId;
return await initBlsWalletSigner({ chainId });
return await initBlsWalletSigner({ chainId, privateKey });
}
/**
* Binds the BlsWalletSigner instance to a new private key and chainId
*
* @returns The updated BlsWalletSigner object
*/
async setBlsWalletSigner(
signerOrProvider: SignerOrProvider,
privateKey: string,
): Promise<BlsWalletSigner> {
const chainId =
"getChainId" in signerOrProvider
? await signerOrProvider.getChainId()
: (await signerOrProvider.getNetwork()).chainId;
const newBlsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey,
});
this.blsWalletSigner = newBlsWalletSigner;
return newBlsWalletSigner;
}
// Calculates the expected address the wallet will be created at
@@ -413,9 +432,8 @@ export default class BlsWalletWrapper {
blsWalletSigner: BlsWalletSigner,
verificationGateway: VerificationGateway,
walletAddress: string,
privateKey: string,
): Promise<void> {
const pubKeyHash = blsWalletSigner.getPublicKeyHash(privateKey);
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
const existingPubKeyHash = await verificationGateway.hashFromWallet(
walletAddress,
);

View File

@@ -0,0 +1,18 @@
import { BigNumber } from "ethers";
/**
* Used to add a small safety premium to estimated fees. This protects
* against small fluctuations is gas estimation, and thus increases
* the chance that bundles get accepted during aggregation.
*
* @param feeEstimate fee required for bundle
* @param safetyDivisor optional safety divisor. Default is 5
* @returns fee estimate with added safety premium
*/
export default function addSafetyPremiumToFee(
feeEstimate: BigNumber,
safetyDivisor: number = 5,
): BigNumber {
const safetyPremium = feeEstimate.div(safetyDivisor);
return feeEstimate.add(safetyPremium);
}

View File

@@ -1,8 +1,12 @@
import { signer } from "@thehubbleproject/bls";
import { PublicKey } from "./types";
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
(privateKey: string): PublicKey => {
export default (
signerFactory: signer.BlsSignerFactory,
domain: Uint8Array,
privateKey: string,
) =>
(): PublicKey => {
const signer = signerFactory.getSigner(domain, privateKey);
return signer.pubkey;
};

View File

@@ -3,11 +3,15 @@ import { signer } from "@thehubbleproject/bls";
import getPublicKey from "./getPublicKey";
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
(privateKey: string): string => {
const publicKey = getPublicKey(signerFactory, domain)(privateKey);
export default (
signerFactory: signer.BlsSignerFactory,
domain: Uint8Array,
privateKey: string,
) =>
(): string => {
const publicKey = getPublicKey(signerFactory, domain, privateKey);
return solidityKeccak256(
["uint256", "uint256", "uint256", "uint256"],
publicKey,
publicKey(),
);
};

View File

@@ -1,7 +1,11 @@
import { signer, mcl } from "@thehubbleproject/bls";
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
(privateKey: string): string => {
export default (
signerFactory: signer.BlsSignerFactory,
domain: Uint8Array,
privateKey: string,
) =>
(): string => {
const signer = signerFactory.getSigner(domain, privateKey);
return mcl.dumpG2(signer.pubkey);
};

View File

@@ -18,9 +18,11 @@ export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
export async function initBlsWalletSigner({
domain = defaultDomain,
chainId,
privateKey,
}: {
domain?: Uint8Array;
chainId: number;
privateKey: string;
}) {
// Note: Getting signers via this factory ensures that mcl-wasm's underlying
// init() has been called when signing. However, other operations such as
@@ -32,11 +34,12 @@ export async function initBlsWalletSigner({
return {
aggregate,
getPublicKey: getPublicKey(signerFactory, domain),
getPublicKeyHash: getPublicKeyHash(signerFactory, domain),
getPublicKeyStr: getPublicKeyStr(signerFactory, domain),
sign: sign(signerFactory, domain, chainId),
signMessage: signMessage(signerFactory, domain),
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),
privateKey,
};
}

View File

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

View File

@@ -1,7 +1,11 @@
import { signer, mcl } from "@thehubbleproject/bls";
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
(message: string, privateKey: string): mcl.solG1 => {
export default (
signerFactory: signer.BlsSignerFactory,
domain: Uint8Array,
privateKey: string,
) =>
(message: string): mcl.solG1 => {
const signer = signerFactory.getSigner(domain, privateKey);
return signer.sign(message);
};

View File

@@ -2,11 +2,12 @@ import { expect } from "chai";
import { ethers } from "ethers";
import { parseEther } from "ethers/lib/utils";
import { Experimental } from "../src";
import { Experimental, BlsWalletWrapper } from "../src";
import BlsSigner, { UncheckedBlsSigner } from "../src/BlsSigner";
let aggregatorUrl: string;
let verificationGateway: string;
let aggregatorUtilities: string;
let rpcUrl: string;
let network: ethers.providers.Networkish;
@@ -20,17 +21,19 @@ describe("BlsProvider", () => {
beforeEach(async () => {
aggregatorUrl = "http://localhost:3000";
verificationGateway = "mockVerificationGatewayAddress";
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
rpcUrl = "http://localhost:8545";
network = {
name: "localhost",
chainId: 0x7a69,
chainId: 0x539, // 1337
};
privateKey = ethers.Wallet.createRandom().privateKey;
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
@@ -57,48 +60,29 @@ 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,
);
// Act
const newPrivateKey = ethers.Wallet.createRandom().privateKey;
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
// 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,
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,
);
});
@@ -106,6 +90,7 @@ describe("BlsProvider", () => {
// Arrange
const transaction = {
value: parseEther("1"),
// Explicitly omit 'to'
};
// Act
@@ -118,27 +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,
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",
);
});
@@ -152,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,11 +1,12 @@
import { expect } from "chai";
import { ethers, Wallet } from "ethers";
import { ethers } from "ethers";
import { Experimental } from "../src";
import { Experimental, BlsWalletWrapper } from "../src";
import { UncheckedBlsSigner } from "../src/BlsSigner";
let aggregatorUrl: string;
let verificationGateway: string;
let aggregatorUtilities: string;
let rpcUrl: string;
let network: ethers.providers.Networkish;
@@ -17,17 +18,19 @@ describe("BlsSigner", () => {
beforeEach(async () => {
aggregatorUrl = "http://localhost:3000";
verificationGateway = "mockVerificationGatewayAddress";
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
rpcUrl = "http://localhost:8545";
network = {
name: "localhost",
chainId: 0x7a69,
};
privateKey = Wallet.createRandom().privateKey;
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);

View File

@@ -48,15 +48,16 @@ const samples = (() => {
describe("index", () => {
it("signs and verifies transaction", async () => {
const { sign, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
});
const { bundleTemplate, privateKey, otherPrivateKey, walletAddress } =
samples;
const bundle = sign(bundleTemplate, privateKey, walletAddress);
const { sign, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
const bundle = sign(bundleTemplate, walletAddress);
expect(bundle.signature).to.deep.equal([
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
@@ -65,9 +66,16 @@ describe("index", () => {
expect(verify(bundle, walletAddress)).to.equal(true);
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey: otherPrivateKey,
});
const bundleBadSig = {
...bundle,
signature: sign(bundleTemplate, otherPrivateKey, walletAddress).signature,
signature: signWithOtherPrivateKey(bundleTemplate, walletAddress)
.signature,
};
expect(verify(bundleBadSig, walletAddress)).to.equal(false);
@@ -94,11 +102,6 @@ describe("index", () => {
});
it("aggregates transactions", async () => {
const { sign, aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
});
const {
bundleTemplate,
privateKey,
@@ -107,8 +110,19 @@ describe("index", () => {
otherWalletAddress,
} = samples;
const bundle1 = sign(bundleTemplate, privateKey, walletAddress);
const bundle2 = sign(bundleTemplate, otherPrivateKey, otherWalletAddress);
const { sign, aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey: otherPrivateKey,
});
const bundle1 = sign(bundleTemplate, walletAddress);
const bundle2 = signWithOtherPrivateKey(bundleTemplate, otherWalletAddress);
const aggBundle = aggregate([bundle1, bundle2]);
expect(aggBundle.signature).to.deep.equal([
@@ -146,13 +160,14 @@ describe("index", () => {
});
it("can aggregate transactions which already have multiple subTransactions", async () => {
const { bundleTemplate, privateKey, walletAddress } = samples;
const { sign, aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
const { bundleTemplate, privateKey, walletAddress } = samples;
const bundles = Range(4).map((i) =>
sign(
{
@@ -164,7 +179,6 @@ describe("index", () => {
},
],
},
privateKey,
walletAddress,
),
);
@@ -178,12 +192,15 @@ describe("index", () => {
});
it("generates expected publicKeyStr", async () => {
const { privateKey } = samples;
const { getPublicKeyStr } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
expect(getPublicKeyStr(samples.privateKey)).to.equal(
expect(getPublicKeyStr()).to.equal(
[
"0x",
"027c3c0483be2722a29a0229bef64b2d8c1f8d4e954b0203d01ce342608b6eb8",
@@ -195,9 +212,12 @@ describe("index", () => {
});
it("aggregates an empty bundle", async () => {
const { privateKey } = samples;
const { aggregate } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
const emptyBundle = aggregate([]);
@@ -207,9 +227,12 @@ describe("index", () => {
});
it("verifies an empty bundle", async () => {
const { privateKey } = samples;
const { aggregate, verify } = await initBlsWalletSigner({
chainId: 123,
domain,
privateKey,
});
const emptyBundle = aggregate([]);

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract MockTokenSpender {
function TransferERC20ToSelf(address _token, uint256 _amount) public {
IERC20(_token).transferFrom(msg.sender, address(this), _amount);
}
function TransferERC721ToSelf(address _token, uint256 _tokenId) public {
IERC721(_token).transferFrom(msg.sender, address(this), _tokenId);
}
}

View File

@@ -59,6 +59,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

@@ -101,6 +101,8 @@ export default class Fixture {
bundleCompressor.addCompressor(1, blsRegistrationCompressor);
bundleCompressor.addCompressor(0, fallbackCompressor);
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
return new Fixture(
chainId,
ethers.provider,
@@ -114,7 +116,7 @@ export default class Fixture {
fallbackCompressor,
utilities,
blsRegistration,
await initBlsWalletSigner({ chainId }),
await initBlsWalletSigner({ chainId, privateKey }),
);
}

View File

@@ -33,7 +33,7 @@ export async function proxyAdminBundle(
);
const encodedWalletAdminCall =
fx.verificationGateway.interface.encodeFunctionData("walletAdminCall", [
wallet.blsWalletSigner.getPublicKeyHash(wallet.privateKey),
wallet.blsWalletSigner.getPublicKeyHash(),
encodedGetProxyAdmin,
]);
const action: ActionData = {

View File

@@ -1,9 +1,10 @@
import chai, { expect } from "chai";
import { ethers } from "ethers";
import { BigNumber, ethers } from "ethers";
import { formatEther, parseEther } from "ethers/lib/utils";
import {
BlsWalletWrapper,
bundleToDto,
Experimental,
MockERC20Factory,
NetworkConfig,
@@ -14,6 +15,7 @@ let networkConfig: NetworkConfig;
let aggregatorUrl: string;
let verificationGateway: string;
let aggregatorUtilities: string;
let rpcUrl: string;
let network: ethers.providers.Networkish;
@@ -29,17 +31,19 @@ describe("BlsProvider", () => {
aggregatorUrl = "http://localhost:3000";
verificationGateway = networkConfig.addresses.verificationGateway;
aggregatorUtilities = networkConfig.addresses.utilities;
rpcUrl = "http://localhost:8545";
network = {
name: "localhost",
chainId: 0x539, // 1337
};
privateKey = ethers.Wallet.createRandom().privateKey;
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
@@ -48,7 +52,7 @@ describe("BlsProvider", () => {
regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
const fundedWallet = new ethers.Wallet(
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // HH Account #2 private key
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat Account #2 private key
regularProvider,
);
@@ -78,14 +82,13 @@ describe("BlsProvider", () => {
expect(formatEther(result)).to.equal(expectedSupply);
});
// TODO: bls-wallet #410 estimate gas for a transaction
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
@@ -96,21 +99,17 @@ describe("BlsProvider", () => {
await expect(gasEstimate()).to.not.be.rejected;
});
it("should send ETH (empty call) given a valid bundle successfully", async () => {
it("should send ETH (empty call) given a valid bundle", async () => {
// Arrange
const recipient = ethers.Wallet.createRandom().address;
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);
@@ -122,7 +121,244 @@ describe("BlsProvider", () => {
).to.equal(expectedBalance);
});
it("should throw an error when sending multiple signed operations to sendTransaction", async () => {
// Arrange
const expectedAmount = parseEther("1");
const verySafeFee = parseEther("0.1");
const firstRecipient = ethers.Wallet.createRandom().address;
const secondRecipient = ethers.Wallet.createRandom().address;
const firstActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
[
{
ethValue: expectedAmount,
contractAddress: firstRecipient,
encodedFunction: "0x",
},
],
verySafeFee,
);
const secondActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
[
{
ethValue: expectedAmount,
contractAddress: secondRecipient,
encodedFunction: "0x",
},
],
verySafeFee,
);
const firstOperation = {
nonce: await blsSigner.wallet.Nonce(),
actions: [...firstActionWithSafeFee],
};
const secondOperation = {
nonce: (await blsSigner.wallet.Nonce()).add(1),
actions: [...secondActionWithSafeFee],
};
const firstBundle = blsSigner.wallet.sign(firstOperation);
const secondBundle = blsSigner.wallet.sign(secondOperation);
const aggregatedBundle = blsSigner.wallet.blsWalletSigner.aggregate([
firstBundle,
secondBundle,
]);
// Act
const result = async () =>
await blsProvider.sendTransaction(
JSON.stringify(bundleToDto(aggregatedBundle)),
);
// Assert
await expect(result()).to.rejectedWith(
Error,
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
);
});
it("should get the account nonce when the signer constructs the transaction response", async () => {
// Arrange
const spy = chai.spy.on(BlsWalletWrapper, "Nonce");
const signedTransaction = await blsSigner.signTransaction({
value: parseEther("1"),
to: ethers.Wallet.createRandom().address,
data: "0x",
});
// Act
await blsProvider.sendTransaction(signedTransaction);
// Assert
// 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(2);
chai.spy.restore(spy);
});
it("should throw an error when sending a modified signed transaction", async () => {
// Arrange
const address = await blsSigner.getAddress();
const signedTransaction = await blsSigner.signTransaction({
value: parseEther("1"),
to: ethers.Wallet.createRandom().address,
data: "0x",
});
const userBundle = JSON.parse(signedTransaction);
userBundle.operations[0].actions[0].ethValue = parseEther("2");
const invalidBundle = JSON.stringify(userBundle);
// Act
const result = async () => await blsProvider.sendTransaction(invalidBundle);
// Assert
await expect(result()).to.be.rejectedWith(
Error,
`[{"type":"invalid-signature","description":"invalid signature for wallet address ${address}"}]`,
);
});
it("should send a batch of ETH transfers (empty calls) given a valid bundle", async () => {
// Arrange
const expectedAmount = parseEther("1");
const recipients = [];
const unsignedTransactionBatch = [];
for (let i = 0; i < 3; i++) {
recipients.push(ethers.Wallet.createRandom().address);
unsignedTransactionBatch.push({
to: recipients[i],
value: expectedAmount,
});
}
const signedTransactionBatch = await blsSigner.signTransactionBatch({
transactions: unsignedTransactionBatch,
});
// Act
const result = await blsProvider.sendTransactionBatch(
signedTransactionBatch,
);
await result.awaitBatchReceipt();
// Assert
expect(await blsProvider.getBalance(recipients[0])).to.equal(
expectedAmount,
);
expect(await blsProvider.getBalance(recipients[1])).to.equal(
expectedAmount,
);
expect(await blsProvider.getBalance(recipients[2])).to.equal(
expectedAmount,
);
});
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");
const firstRecipient = ethers.Wallet.createRandom().address;
const secondRecipient = ethers.Wallet.createRandom().address;
const firstActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
[
{
ethValue: expectedAmount,
contractAddress: firstRecipient,
encodedFunction: "0x",
},
],
verySafeFee,
);
const secondActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
[
{
ethValue: expectedAmount,
contractAddress: secondRecipient,
encodedFunction: "0x",
},
],
verySafeFee,
);
const firstOperation = {
nonce: await blsSigner.wallet.Nonce(),
actions: [...firstActionWithSafeFee],
};
const secondOperation = {
nonce: (await blsSigner.wallet.Nonce()).add(1),
actions: [...secondActionWithSafeFee],
};
const firstBundle = blsSigner.wallet.sign(firstOperation);
const secondBundle = blsSigner.wallet.sign(secondOperation);
const aggregatedBundle = blsSigner.wallet.blsWalletSigner.aggregate([
firstBundle,
secondBundle,
]);
// Act
const transactionBatchResponse = await blsProvider.sendTransactionBatch(
JSON.stringify(bundleToDto(aggregatedBundle)),
);
await transactionBatchResponse.awaitBatchReceipt();
// Assert
expect(await blsProvider.getBalance(firstRecipient)).to.equal(
expectedAmount,
);
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 () => {
// Arrange
const spy = chai.spy.on(BlsWalletWrapper, "Nonce");
const recipient = ethers.Wallet.createRandom().address;
@@ -133,40 +369,46 @@ describe("BlsProvider", () => {
to: recipient,
data: "0x",
};
const signedTransaction = await blsSigner.signTransaction(
unsignedTransaction,
);
const signedTransaction = await blsSigner.signTransactionBatch({
transactions: [unsignedTransaction],
});
// Act
await blsProvider.sendTransaction(signedTransaction);
await blsProvider.sendTransactionBatch(signedTransaction);
// Assert
// Once when calling "signer.signTransaction", and once when calling "signer.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.twice;
expect(spy).to.have.been.called.exactly(2);
chai.spy.restore(spy);
});
it("should return failures as a json string and throw an error when sending an invalid transaction", async () => {
it("should throw an error when sending a modified signed transaction", async () => {
// Arrange
const invalidEthValue = parseEther("-1");
const address = await blsSigner.getAddress();
const unsignedTransaction = {
value: invalidEthValue,
to: ethers.Wallet.createRandom().address,
data: "0x",
};
const signedTransaction = await blsSigner.signTransaction(
unsignedTransaction,
);
const signedTransaction = await blsSigner.signTransactionBatch({
transactions: [
{
value: parseEther("1"),
to: ethers.Wallet.createRandom().address,
data: "0x",
},
],
});
const userBundle = JSON.parse(signedTransaction);
userBundle.operations[0].actions[0].ethValue = parseEther("2");
const invalidBundle = JSON.stringify(userBundle);
// Act
const result = async () =>
await blsProvider.sendTransaction(signedTransaction);
await blsProvider.sendTransactionBatch(invalidBundle);
// Assert
await expect(result()).to.be.rejectedWith(
Error,
'[{"type":"invalid-format","description":"field operations: element 0: field actions: element 0: field ethValue: hex string: missing 0x prefix"},{"type":"invalid-format","description":"field operations: element 0: field actions: element 0: field ethValue: hex string: incorrect byte length: 8.5"}]',
`[{"type":"invalid-signature","description":"invalid signature for wallet address ${address}"}]`,
);
});
@@ -193,9 +435,8 @@ describe("BlsProvider", () => {
it("should wait for a transaction and resolve once transaction hash is included in the block", async () => {
// Arrange
const recipient = ethers.Wallet.createRandom().address;
const transactionResponse = await blsSigner.sendTransaction({
to: recipient,
to: ethers.Wallet.createRandom().address,
value: parseEther("1"),
});
@@ -255,9 +496,8 @@ describe("BlsProvider", () => {
it("should retrieve a transaction receipt given a valid hash", async () => {
// Arrange
const recipient = ethers.Wallet.createRandom().address;
const transactionResponse = await blsSigner.sendTransaction({
to: recipient,
to: ethers.Wallet.createRandom().address,
value: parseEther("1"),
});
@@ -313,13 +553,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(
@@ -355,6 +593,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,
@@ -410,86 +649,195 @@ describe("BlsProvider", () => {
expect(chainId).to.equal(expectedChainId);
expect(accounts).to.deep.equal(expectedAccounts);
});
});
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 HH account #2 private key here to avoid nonce too low errors.
wallet = new ethers.Wallet(
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // HH acount #2 private key
regularProvider,
);
});
it("calls a getter method on a contract", async () => {
it("should return the number of transactions an address has sent", async function () {
// Arrange
const expectedSupply = "1000000.0";
const testERC20 = MockERC20Factory.connect(
networkConfig.addresses.testToken,
regularProvider,
);
const transaction = {
to: testERC20.address,
data: testERC20.interface.encodeFunctionData("totalSupply"),
};
const address = await blsSigner.getAddress();
const expectedFirstTransactionCount = 0;
const expectedSecondTransactionCount = 1;
// 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,
const firstTransactionCount = await blsProvider.getTransactionCount(
address,
);
// Act
const transactionResponse = await regularProvider.getTransaction(
expectedTransactionResponse.hash,
const sendTransaction = await blsSigner.sendTransaction({
value: BigNumber.from(1),
to: ethers.Wallet.createRandom().address,
});
await sendTransaction.wait();
const secondTransactionCount = await blsProvider.getTransactionCount(
address,
);
// Assert
expect(transactionResponse).to.be.an("object").that.deep.includes({
hash: expectedTransactionResponse.hash,
type: expectedTransactionResponse.type,
accessList: expectedTransactionResponse.accessList,
expect(firstTransactionCount).to.equal(expectedFirstTransactionCount);
expect(secondTransactionCount).to.equal(expectedSecondTransactionCount);
});
it("should return the number of transactions an address has sent at a specified block tag", async function () {
// Arrange
const expectedTransactionCount = 0;
const sendTransaction = await blsSigner.sendTransaction({
value: BigNumber.from(1),
to: ethers.Wallet.createRandom().address,
});
await sendTransaction.wait();
// Act
const transactionCount = await blsProvider.getTransactionCount(
await blsSigner.getAddress(),
"earliest",
);
// Assert
expect(transactionCount).to.equal(expectedTransactionCount);
});
it("should return the block from the network", async function () {
// Arrange
const expectedBlock = await regularProvider.getBlock(1);
// Act
const block = await blsProvider.getBlock(1);
// Assert
expect(block).to.be.an("object").that.deep.includes({
hash: expectedBlock.hash,
parentHash: expectedBlock.parentHash,
number: expectedBlock.number,
timestamp: expectedBlock.timestamp,
difficulty: expectedBlock.difficulty,
miner: expectedBlock.miner,
extraData: expectedBlock.extraData,
transactions: expectedBlock.transactions,
});
expect(block.gasLimit).to.deep.equal(expectedBlock.gasLimit);
expect(block.gasUsed).to.deep.equal(expectedBlock.gasUsed);
expect(block.baseFeePerGas).to.deep.equal(expectedBlock.baseFeePerGas);
expect(block._difficulty).to.deep.equal(expectedBlock._difficulty);
});
it("should return the block from the network with an array of TransactionResponse objects", async function () {
// Arrange
const expectedBlock = await regularProvider.getBlockWithTransactions(1);
// Act
const block = await blsProvider.getBlockWithTransactions(1);
// Assert
// Assert block
expect(block).to.be.an("object").that.deep.includes({
hash: expectedBlock.hash,
parentHash: expectedBlock.parentHash,
number: expectedBlock.number,
timestamp: expectedBlock.timestamp,
difficulty: expectedBlock.difficulty,
miner: expectedBlock.miner,
extraData: expectedBlock.extraData,
});
expect(block.gasLimit).to.deep.equal(expectedBlock.gasLimit);
expect(block.gasUsed).to.deep.equal(expectedBlock.gasUsed);
expect(block.baseFeePerGas).to.deep.equal(expectedBlock.baseFeePerGas);
expect(block._difficulty).to.deep.equal(expectedBlock._difficulty);
// Assert transaction in block
expect(block.transactions[0]).to.be.an("object").that.deep.includes({
hash: expectedBlock.transactions[0].hash,
type: expectedBlock.transactions[0].type,
accessList: expectedBlock.transactions[0].accessList,
blockHash: expectedBlock.transactions[0].blockHash,
blockNumber: expectedBlock.transactions[0].blockNumber,
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,
from: expectedBlock.transactions[0].from,
to: expectedBlock.transactions[0].to,
nonce: expectedBlock.transactions[0].nonce,
data: expectedBlock.transactions[0].data,
r: expectedBlock.transactions[0].r,
s: expectedBlock.transactions[0].s,
v: expectedBlock.transactions[0].v,
creates: null,
chainId: expectedTransactionResponse.chainId,
chainId: expectedBlock.transactions[0].chainId,
});
expect(transactionResponse).to.include.keys(
"wait",
"blockHash",
"blockNumber",
"gasPrice",
expect(block.transactions[0].gasPrice).to.deep.equal(
expectedBlock.transactions[0].gasPrice,
);
expect(block.transactions[0].maxPriorityFeePerGas).to.deep.equal(
expectedBlock.transactions[0].maxPriorityFeePerGas,
);
expect(block.transactions[0].maxFeePerGas).to.deep.equal(
expectedBlock.transactions[0].maxFeePerGas,
);
expect(block.transactions[0].gasLimit).to.deep.equal(
expectedBlock.transactions[0].gasLimit,
);
expect(block.transactions[0].value).to.deep.equal(
expectedBlock.transactions[0].value,
);
// Not sure why confirmations from the expected block is 1 above confirmations from blsProvider result.
// Last assertion doube checks this against another method and the confirmation number is correct according to this.
expect(block.transactions[0].confirmations).to.deep.equal(
expectedBlock.transactions[0].confirmations + 1,
);
// confirm that confirmations match via provider.getTransaction()
expect(block.transactions[0].confirmations).to.deep.equal(
(await blsProvider.getTransaction(block.transactions[0].hash))
.confirmations,
);
});
it("should return the network the provider is connected to", async () => {
// Arrange
const expectedNetwork = { name: "localhost", chainId: 1337 };
// Act
const network = await blsProvider.getNetwork();
// Assert
expect(network).to.deep.equal(expectedNetwork);
});
it("should return the block number at the most recent block", async () => {
// Arrange
const expectedBlockNumber = await regularProvider.getBlockNumber();
// Act
const blockNumber = await blsProvider.getBlockNumber();
// Assert
expect(blockNumber).to.deep.equal(expectedBlockNumber);
});
it("should return an estimate of gas price to use in a transaction", async () => {
// Arrange
const expectedGasPrice = await regularProvider.getGasPrice();
// Act
const gasPrice = await blsProvider.getGasPrice();
// Assert
expect(gasPrice).to.deep.equal(expectedGasPrice);
});
it("should return the current recommended FeeData to use in a transaction", async () => {
// Arrange
const expectedFeeData = await regularProvider.getFeeData();
// Act
const feeData = await blsProvider.getFeeData();
// Assert
expect(feeData.lastBaseFeePerGas).to.deep.equal(
expectedFeeData.lastBaseFeePerGas,
);
expect(feeData.maxFeePerGas).to.deep.equal(expectedFeeData.maxFeePerGas);
expect(feeData.maxPriorityFeePerGas).to.deep.equal(
expectedFeeData.maxPriorityFeePerGas,
);
expect(feeData.gasPrice).to.deep.equal(expectedFeeData.gasPrice);
});
});

View File

@@ -2,18 +2,20 @@ import { expect } from "chai";
import { ethers } from "hardhat";
import { BigNumber, utils, Wallet } from "ethers";
import { Experimental } from "../clients/src";
import { Experimental, BlsWalletWrapper } from "../clients/src";
import getNetworkConfig from "../shared/helpers/getNetworkConfig";
describe("Provider tests", function () {
let blsProvider;
let blsSigner;
let fundedWallet: Wallet;
this.beforeAll(async function () {
this.beforeAll(async () => {
const networkConfig = await getNetworkConfig("local");
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const aggregatorUrl = "http://localhost:3000";
const verificationGateway = networkConfig.addresses.verificationGateway;
const aggregatorUtilities = networkConfig.addresses.utilities;
const rpcUrl = "http://localhost:8545";
const network = {
name: "localhost",
@@ -22,9 +24,11 @@ describe("Provider tests", function () {
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network,
);
blsSigner = blsProvider.getSigner(privateKey);
});
describe("ERC20", async function () {
@@ -32,11 +36,18 @@ describe("Provider tests", function () {
let tokenSupply: BigNumber;
let recipient;
this.beforeAll(async function () {
this.beforeAll(async () => {
fundedWallet = new ethers.Wallet(
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // HH Account #2 private key
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat Account #2 private key
new ethers.providers.JsonRpcProvider("http://localhost:8545"),
);
const tx = await fundedWallet.sendTransaction({
to: await blsSigner.getAddress(),
value: utils.parseEther("100"),
});
await tx.wait();
recipient = ethers.Wallet.createRandom().address;
tokenSupply = utils.parseUnits("1000000");
const MockERC20 = await ethers.getContractFactory("MockERC20");
@@ -47,24 +58,221 @@ describe("Provider tests", function () {
);
await mockERC20.deployed();
await mockERC20.transfer(recipient, tokenSupply);
await mockERC20.transfer(recipient, tokenSupply.div(2));
await mockERC20.transfer(
await blsSigner.getAddress(),
tokenSupply.div(2),
);
});
it("balanceOf() call", async function () {
it("balanceOf() call", async () => {
// Arrange & Act
const balance = await mockERC20.connect(blsProvider).balanceOf(recipient);
expect(balance).to.equal(tokenSupply);
// Assert
expect(balance).to.equal(tokenSupply.div(2));
});
it("calls balanceOf successfully after instantiating Contract class with BlsProvider", async function () {
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);
expect(balance).to.equal(tokenSupply.div(2));
});
it("should add event listener that is triggered by a custom filter", async () => {
// Arrange
blsProvider.removeAllListeners();
const erc20 = new ethers.Contract(
mockERC20.address,
mockERC20.interface,
blsProvider,
);
let value = "";
const setValue = async () => {
value = "Value set on event";
};
const amountToTransfer = ethers.utils.parseUnits("1");
const balanceBefore = await erc20.balanceOf(recipient);
const filter = {
address: mockERC20.address,
topics: [utils.id("Transfer(address,address,uint256)")],
};
// Act
const listenerCountBeforeEventListener =
blsProvider.listenerCount(filter);
blsProvider.on(filter, setValue); // set value when event occurs
const tx = await erc20
.connect(blsSigner)
.transfer(recipient, amountToTransfer);
await tx.wait();
const listenerCountDuringEventListener =
blsProvider.listenerCount(filter);
blsProvider.off(filter);
// Assert
expect((await erc20.balanceOf(recipient)).sub(balanceBefore)).to.equal(
amountToTransfer,
);
expect(listenerCountBeforeEventListener).to.equal(0);
expect(listenerCountDuringEventListener).to.equal(1);
expect(blsProvider.listenerCount(filter)).to.equal(0);
expect(value).to.equal("Value set on event");
});
it("should add event listener that is triggered by a custom filter and is removed automatically", async () => {
// Arrange
blsProvider.removeAllListeners();
const erc20 = new ethers.Contract(
mockERC20.address,
mockERC20.interface,
blsProvider,
);
let value = "";
const setValue = async () => {
value = "Value set on event";
};
const amountToTransfer = ethers.utils.parseUnits("1");
const balanceBefore = await erc20.balanceOf(recipient);
const filter = {
address: mockERC20.address,
topics: [utils.id("Transfer(address,address,uint256)")],
};
// Act
const listenerCountBeforeEventListener =
blsProvider.listenerCount(filter);
blsProvider.once(filter, setValue); // set value when event occurs
const listenerCountDuringEventListener =
blsProvider.listenerCount(filter);
const tx = await erc20
.connect(blsSigner)
.transfer(recipient, amountToTransfer);
await tx.wait();
// wait 1 second to ensure listener count updates
await new Promise((resolve) => setTimeout(resolve, 2000));
// Assert
expect((await erc20.balanceOf(recipient)).sub(balanceBefore)).to.equal(
amountToTransfer,
);
expect(listenerCountBeforeEventListener).to.equal(0);
expect(listenerCountDuringEventListener).to.equal(1);
expect(blsProvider.listenerCount(filter)).to.equal(0);
expect(value).to.equal("Value set on event");
});
it("should return the logs for matching filters", async () => {
// Arrange
const amountToTransfer = ethers.utils.parseUnits("1");
const balanceBefore = await mockERC20.balanceOf(recipient);
const tx = await mockERC20
.connect(blsSigner)
.transfer(recipient, amountToTransfer);
const receipt = await tx.wait();
const transferAbi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const erc20Interface = new ethers.utils.Interface(transferAbi);
const expectedLogs = await blsProvider.getLogs({
fromBlock: "earliest",
toBlock: "latest",
address: mockERC20.address,
topics: [ethers.utils.id("Transfer(address,address,uint256)")],
});
// Act
const logs = await blsProvider.getLogs({
fromBlock: "earliest",
toBlock: "latest",
address: mockERC20.address,
topics: [ethers.utils.id("Transfer(address,address,uint256)")],
});
// Assert
const transferLog = logs.find(
(log) => log.transactionHash === receipt.transactionHash,
);
const transferEvent = erc20Interface.parseLog(transferLog);
expect(
(await mockERC20.balanceOf(recipient)).sub(balanceBefore),
).to.equal(amountToTransfer);
expect(logs).to.deep.equal(expectedLogs);
expect(transferEvent.args.value).to.equal(amountToTransfer);
expect(transferEvent.args.from).to.equal(await blsSigner.getAddress());
expect(transferEvent.args.to).to.equal(recipient);
});
it("should get code located at address and block number", async function () {
// Arrange
const provider = new ethers.providers.JsonRpcProvider();
const expectedCode = await provider.getCode(mockERC20.address);
// Act
const code = await blsProvider.getCode(mockERC20.address);
// Assert
expect(code).to.equal(expectedCode);
});
it("should return '0x' if no code located at address", async function () {
// Arrange
const fakeAddress = ethers.Wallet.createRandom().address;
const expectedCode = "0x";
// Act
const invalidAddress = await blsProvider.getCode(fakeAddress);
const realAddressBeforeDeployment = await blsProvider.getCode(
mockERC20.address,
"earliest",
);
// Assert
expect(invalidAddress).to.equal(expectedCode);
expect(realAddressBeforeDeployment).to.equal(expectedCode);
});
it("should return the Bytes32 value of the storage slot position at erc20 address", async function () {
// Arrange
const provider = new ethers.providers.JsonRpcProvider();
const expectedStorage1 = await provider.getStorageAt(
mockERC20.address,
1,
);
const expectedStorage2 = await provider.getStorageAt(
mockERC20.address,
2,
);
// Act
const storage1 = await blsProvider.getStorageAt(mockERC20.address, 1);
const storage2 = await blsProvider.getStorageAt(mockERC20.address, 2);
// Assert
expect(storage1).to.equal(expectedStorage1); // 0x0000000000000000000000000000000000000000000000000000000000000000
expect(storage2).to.equal(expectedStorage2); // 0x00000000000000000000000000000000000000000000d3c21bcecceda1000000
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -33,7 +33,7 @@ describe("Recovery", async function () {
let blsWallet: BLSWallet;
let blsWallet3: BLSWallet;
let recoverySigner;
let hash1, hash2, hashAttacker;
let wallet1PublicKeyHash, wallet2PublicKeyHash, walletAttackerPublicKeyHash;
let salt;
let recoveryHash;
beforeEach(async function () {
@@ -48,15 +48,14 @@ describe("Recovery", async function () {
blsWallet3 = await ethers.getContractAt("BLSWallet", wallet3.address);
recoverySigner = (await ethers.getSigners())[1];
hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey);
hash2 = wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey);
hashAttacker = wallet2.blsWalletSigner.getPublicKeyHash(
walletAttacker.privateKey,
);
wallet1PublicKeyHash = wallet1.blsWalletSigner.getPublicKeyHash();
wallet2PublicKeyHash = wallet2.blsWalletSigner.getPublicKeyHash();
walletAttackerPublicKeyHash =
walletAttacker.blsWalletSigner.getPublicKeyHash();
salt = "0x1234567812345678123456781234567812345678123456781234567812345678";
recoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverySigner.address, hash1, salt],
[recoverySigner.address, wallet1PublicKeyHash, salt],
);
});
@@ -71,12 +70,12 @@ describe("Recovery", async function () {
)
).wait();
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(hash1);
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(wallet1PublicKeyHash);
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet2.privateKey,
wallet2.blsWalletSigner.privateKey,
);
await fx.call(
@@ -105,7 +104,7 @@ describe("Recovery", async function () {
}),
);
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(hash2);
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(wallet2PublicKeyHash);
});
it("should NOT override public key hash after creation", async function () {
@@ -119,15 +118,15 @@ describe("Recovery", async function () {
)
).wait();
let walletForHash = await vg.walletFromHash(hash1);
let walletForHash = await vg.walletFromHash(wallet1PublicKeyHash);
expect(BigNumber.from(walletForHash)).to.not.equal(BigNumber.from(0));
expect(walletForHash).to.equal(wallet1.address);
let hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(BigNumber.from(hashFromWallet)).to.not.equal(BigNumber.from(0));
expect(hashFromWallet).to.equal(hash1);
expect(hashFromWallet).to.equal(wallet1PublicKeyHash);
let publicKeyFromHash = await getPublicKeyFromHash(vg, hash1);
let publicKeyFromHash = await getPublicKeyFromHash(vg, wallet1PublicKeyHash);
expect(publicKeyFromHash).to.deep.equal(wallet1.PublicKey());
await fx.advanceTimeBy(safetyDelaySeconds + 1);
@@ -147,13 +146,13 @@ describe("Recovery", async function () {
}),
);
walletForHash = await vg.walletFromHash(hash1);
walletForHash = await vg.walletFromHash(wallet1PublicKeyHash);
expect(walletForHash).to.equal(wallet1.address);
hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(hashFromWallet).to.equal(hash1);
expect(hashFromWallet).to.equal(wallet1PublicKeyHash);
publicKeyFromHash = await getPublicKeyFromHash(vg, hash1);
publicKeyFromHash = await getPublicKeyFromHash(vg, wallet1PublicKeyHash);
expect(publicKeyFromHash).to.deep.equal(wallet1.PublicKey());
});
@@ -173,7 +172,7 @@ describe("Recovery", async function () {
salt = "0x" + "AB".repeat(32);
const newRecoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverySigner.address, hash1, salt],
[recoverySigner.address, wallet1PublicKeyHash, salt],
);
await fx.call(
wallet1,
@@ -263,7 +262,7 @@ describe("Recovery", async function () {
// wallet 2 address is recovery address of wallet 1
recoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[wallet2.address, hash1, salt],
[wallet2.address, wallet1PublicKeyHash, salt],
);
let w1Nonce = 0;
await fx.call(
@@ -280,7 +279,7 @@ describe("Recovery", async function () {
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet3.privateKey,
wallet3.blsWalletSigner.privateKey,
);
// wallet 2 recovers wallet 1 to key 3
@@ -326,7 +325,7 @@ describe("Recovery", async function () {
const attackSignature = await signWalletAddress(
fx,
wallet1.address,
walletAttacker.privateKey,
walletAttacker.blsWalletSigner.privateKey,
);
// Attacker assumed to have compromised wallet1 bls key, and wishes to reset
@@ -339,12 +338,20 @@ describe("Recovery", async function () {
recoveredWalletNonce++,
30_000_000,
);
const pendingKey = await Promise.all(
[0, 1, 2, 3].map(async (i) =>
(await vg.pendingBLSPublicKeyFromHash(hash1, i)).toHexString(),
(
await vg.pendingBLSPublicKeyFromHash(wallet1PublicKeyHash, i)
).toHexString(),
),
);
expect(pendingKey).to.deep.equal(walletAttacker.PublicKey());
const attackerPublicKeyHexStrings = walletAttacker
.PublicKey()
.map((keyElement) => BigNumber.from(keyElement).toHexString());
expect(pendingKey).to.deep.equal(attackerPublicKeyHexStrings);
await fx.advanceTimeBy(safetyDelaySeconds / 2); // wait half the time
// NB: advancing the time makes an empty tx with lazywallet[1]
@@ -370,20 +377,20 @@ describe("Recovery", async function () {
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet2.privateKey,
wallet2.blsWalletSigner.privateKey,
);
const safeKey = wallet2.PublicKey();
await (
await fx.verificationGateway
.connect(recoverySigner)
.recoverWallet(addressSignature, hash1, salt, safeKey)
.recoverWallet(addressSignature, wallet1PublicKeyHash, salt, safeKey)
).wait();
// key reset via recovery
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(hash2);
await expect(vg.walletFromHash(hash2)).to.eventually.eql(wallet1.address);
await expect(getPublicKeyFromHash(vg, hash2)).to.eventually.deep.equal(
await expect(vg.hashFromWallet(wallet1.address)).to.eventually.eql(wallet2PublicKeyHash);
await expect(vg.walletFromHash(wallet2PublicKeyHash)).to.eventually.eql(wallet1.address);
await expect(getPublicKeyFromHash(vg, wallet2PublicKeyHash)).to.eventually.deep.equal(
safeKey,
);
@@ -442,19 +449,22 @@ describe("Recovery", async function () {
wallet1.PublicKey(),
);
await expect(vg.walletFromHash(hashAttacker)).to.eventually.not.equal(
await expect(vg.walletFromHash(wallet1PublicKeyHash)).to.eventually.not.equal(
blsWallet.address,
);
await expect(vg.walletFromHash(hash2)).to.eventually.equal(
await expect(vg.walletFromHash(walletAttackerPublicKeyHash)).to.eventually.equal(
blsWallet.address,
);
await expect(vg.walletFromHash(wallet2PublicKeyHash)).to.eventually.equal(
blsWallet.address,
);
// // verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway)
// verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway)
const res = await fx.callStatic(
wallet2,
vg,
"setTrustedBLSGateway",
[hash2, vg.address],
[wallet2PublicKeyHash, vg.address],
await wallet2.Nonce(),
30_000_000,
);
@@ -471,7 +481,7 @@ describe("Recovery", async function () {
// Attacker users recovery signer to set their recovery hash
const attackerRecoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverySigner.address, hashAttacker, salt],
[recoverySigner.address, walletAttackerPublicKeyHash, salt],
);
await fx.call(
walletAttacker,
@@ -492,7 +502,7 @@ describe("Recovery", async function () {
const addressSignature = await signWalletAddress(
fx,
walletAttacker.address,
walletAttacker.privateKey,
walletAttacker.blsWalletSigner.privateKey,
);
const wallet1Key = await wallet1.PublicKey();
@@ -500,7 +510,7 @@ describe("Recovery", async function () {
await expect(
fx.verificationGateway
.connect(recoverySigner)
.recoverWallet(addressSignature, hashAttacker, salt, wallet1Key),
.recoverWallet(addressSignature, walletAttackerPublicKeyHash, salt, wallet1Key),
).to.be.rejectedWith("VG: Sig not verified");
});
@@ -530,12 +540,17 @@ describe("Recovery", async function () {
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet2.privateKey,
wallet2.blsWalletSigner.privateKey,
);
const recoveryTxn = await fx.verificationGateway
.connect(recoverySigner)
.recoverWallet(addressSignature, hash1, salt, wallet2.PublicKey());
.recoverWallet(
addressSignature,
wallet1PublicKeyHash,
salt,
wallet2.PublicKey(),
);
await recoveryTxn.wait();
const [
@@ -551,8 +566,8 @@ describe("Recovery", async function () {
wallet2.Nonce(),
getPublicKeyFromHash(vg, hash2),
]);
expect(wallet1PubkeyHash).to.eql(hash2);
expect(wallet2PubkeyHash).to.eql(hash2);
expect(wallet1PubkeyHash).to.eql(wallet2PublicKeyHash);
expect(wallet2PubkeyHash).to.eql(wallet2PublicKeyHash);
expect(wallet1Nonce.toNumber()).to.eql(wallet2Nonce.toNumber());
expect(publicKey).to.deep.equal(wallet2.PublicKey());

View File

@@ -93,7 +93,7 @@ describe("Upgrade", async function () {
// Recreate hubble bls signer
const walletOldVg = await fx.createBLSWallet();
const walletAddress = walletOldVg.address;
const blsSecret = walletOldVg.privateKey;
const blsSecret = walletOldVg.blsWalletSigner.privateKey;
const wallet = await BlsWalletWrapper.connect(
blsSecret,
@@ -124,9 +124,7 @@ describe("Upgrade", async function () {
// Advance time one week
await fx.advanceTimeBy(safetyDelaySeconds + 1);
const hash = walletOldVg.blsWalletSigner.getPublicKeyHash(
walletOldVg.privateKey,
);
const hash = walletOldVg.blsWalletSigner.getPublicKeyHash();
const setExternalWalletAction: ActionData = {
ethValue: BigNumber.from(0),
@@ -291,19 +289,19 @@ describe("Upgrade", async function () {
const wallet1 = await fx.createBLSWallet();
const wallet2 = await fx.createBLSWallet();
// Process an empty operation for wallet1 so that the gateway knows about
// its hash mapping
await (
await fx.verificationGateway.processBundle(
wallet1.sign({
nonce: 0,
gas: 30_000_000,
actions: [],
}),
)
).wait();
const wallet1 = await BlsWalletWrapper.connect(
lazyWallet1.privateKey,
vg1.address,
vg1.provider,
);
const hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey);
const wallet2 = await BlsWalletWrapper.connect(
lazyWallet2.privateKey,
vg1.address,
vg1.provider,
);
const hash1 = wallet1.blsWalletSigner.getPublicKeyHash();
await expect(vg1.walletFromHash(hash1)).to.eventually.equal(
wallet1.address,
@@ -353,7 +351,7 @@ describe("Upgrade", async function () {
// wallet 1's hash is pointed to null address
// wallet 2's hash is now pointed to wallet 1's address
const hash2 = wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey);
const hash2 = wallet2.blsWalletSigner.getPublicKeyHash();
await fx.advanceTimeBy(safetyDelaySeconds + 1);

View File

@@ -40,7 +40,7 @@ describe("WalletActions", async function () {
const calculatedAddress = ethers.utils.getCreate2Address(
fx.verificationGateway.address,
fx.blsWalletSigner.getPublicKeyHash(wallet.privateKey),
wallet.blsWalletSigner.getPublicKeyHash(),
ethers.utils.solidityKeccak256(
["bytes", "bytes"],
[
@@ -171,7 +171,7 @@ describe("WalletActions", async function () {
contractAddress: fx.verificationGateway.address,
encodedFunction: fx.verificationGateway.interface.encodeFunctionData(
"walletFromHash",
[fx.blsWalletSigner.getPublicKeyHash(wallet.privateKey)],
[fx.blsWalletSigner.getPublicKeyHash()],
),
},
],

View File

@@ -465,16 +465,6 @@
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
dependencies:
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/units@5.7.0":
version "5.7.0"
@@ -989,6 +979,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.5":
version "0.14.5"
resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804"
@@ -3573,6 +3598,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"
@@ -6524,6 +6554,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"
@@ -6821,6 +6856,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"
@@ -7452,6 +7492,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"
@@ -7950,6 +8001,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"
@@ -8954,6 +9012,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"
@@ -9438,7 +9508,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==
@@ -9803,7 +9873,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

@@ -106,7 +106,7 @@ yarn run dev:chrome # or dev:firefox, dev:opera
### Additional troubleshooting tips
- In general, the bundle or submission issues we've encountered have been us misconfiguring the data in the bundle or not configuring the aggregator properly.
- Be careful using HH accounts 0 and 1 in your code when running a local aggregator. This is because the local aggregator config uses the same key pairs as Hardhat accounts 0 and 1 by default. You can get around this by not using accounts 0 and 1 elsewhere, or changing the default accounts that the aggregator uses locally.
- Be careful using Hardhat accounts 0 and 1 in your code when running a local aggregator. This is because the local aggregator config uses the same key pairs as Hardhat accounts 0 and 1 by default. You can get around this by not using accounts 0 and 1 elsewhere, or changing the default accounts that the aggregator uses locally.
### Tests

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-77f1638",
"bls-wallet-clients": "0.8.2-1452ef5",
"browser-passworder": "^2.0.3",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",

View File

@@ -124,7 +124,10 @@ export default class KeyringController {
() => new Error('Wallet already exists'),
);
const { address, privateKey } = await this.BlsWalletWrapper(pKey);
const {
address,
blsWalletSigner: { privateKey },
} = await this.BlsWalletWrapper(pKey);
return { address, privateKey };
},

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-77f1638:
version "0.8.2-77f1638"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-77f1638.tgz#61212aac502aed821bc6036cb95a2cdaa9c0b67e"
integrity sha512-2gTVHUvs4+/fBwgtcZCO+DsKTnJAiebYrpCUdygIqZaFeHdEyp6xU2F8ZT+XOcNoMeN1B9TT5EupV1RTvISFkQ==
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==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"