mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-11 00:27:55 -05:00
Compare commits
24 Commits
private-ke
...
auditIssue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdecab26ee | ||
|
|
e2b6e4f1ee | ||
|
|
7c2901f243 | ||
|
|
4a7331c4a7 | ||
|
|
1fb4a557ab | ||
|
|
7862b5278a | ||
|
|
e2ef1ed62e | ||
|
|
96bfb32e5b | ||
|
|
2f9a3442f8 | ||
|
|
8833c5f0af | ||
|
|
9505ed425b | ||
|
|
7435d9976e | ||
|
|
2dae355817 | ||
|
|
e734209df0 | ||
|
|
fedf05cc7e | ||
|
|
7dc7bb4a5e | ||
|
|
d2c6cff629 | ||
|
|
25469d50e4 | ||
|
|
548301d32d | ||
|
|
ac7cd956a8 | ||
|
|
5423f65503 | ||
|
|
3c0f36f444 | ||
|
|
32c6b13e7d | ||
|
|
e3bbd393d8 |
2
.github/workflows/aggregator.yml
vendored
2
.github/workflows/aggregator.yml
vendored
@@ -76,3 +76,5 @@ jobs:
|
||||
|
||||
- run: cp .env.local.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read
|
||||
- uses: mxschmitt/action-tmate@v3
|
||||
- run: sleep 3600
|
||||
|
||||
@@ -11,6 +11,7 @@ You can watch a full end-to-end demo of the project [here](https://www.youtube.c
|
||||
- See an [overview](./docs/system_overview.md) of BLS Wallet & how the components work together.
|
||||
- Use BLS Wallet in [a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md).
|
||||
- Use BLS Wallet in [your L2 dApp](./docs/use_bls_wallet_dapp.md) for cheaper, multi action transactions.
|
||||
- Use BLS Wallet components and features with an [ethers.js provider and signer](./use_bls_provider.md)
|
||||
|
||||
### Setup your development environment
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "0.8.2-1452ef5",
|
||||
"bls-wallet-clients": "0.8.2-1fb4a55",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -887,10 +887,10 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@0.8.2-1452ef5:
|
||||
version "0.8.2-1452ef5"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
|
||||
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
|
||||
bls-wallet-clients@0.8.2-1fb4a55:
|
||||
version "0.8.2-1fb4a55"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
|
||||
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
@@ -53,7 +53,7 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
@@ -64,10 +64,10 @@ export {
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
|
||||
@@ -60,14 +60,14 @@ export type BundleRow = Row;
|
||||
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
if (Array.isArray(rawRow)) {
|
||||
rawRow = {
|
||||
id: rawRow[0],
|
||||
status: rawRow[1],
|
||||
hash: rawRow[2],
|
||||
bundle: rawRow[3],
|
||||
eligibleAfter: rawRow[4],
|
||||
nextEligibilityDelay: rawRow[5],
|
||||
submitError: rawRow[6],
|
||||
receipt: rawRow[7],
|
||||
id: rawRow[0] as number,
|
||||
status: rawRow[1] as string,
|
||||
hash: rawRow[2] as string,
|
||||
bundle: rawRow[3] as string,
|
||||
eligibleAfter: rawRow[4] as string,
|
||||
nextEligibilityDelay: rawRow[5] as string,
|
||||
submitError: rawRow[6] as string | null,
|
||||
receipt: rawRow[7] as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ export default class EthereumService {
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey: aggPrivateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
return new EthereumService(
|
||||
|
||||
@@ -142,7 +142,7 @@ const bundle = wallet.sign({
|
||||
|
||||
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
|
||||
|
||||
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
|
||||
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
|
||||
|
||||
### Paying aggregator fees with native currency (ETH)
|
||||
|
||||
@@ -198,21 +198,6 @@ const bundle = wallet.sign({
|
||||
});
|
||||
```
|
||||
|
||||
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
|
||||
|
||||
```ts
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: safeFee, // fee amount
|
||||
contractAddress: aggregatorAddress,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Paying aggregator fees with custom currency (ERC20)
|
||||
|
||||
The aggregator must be set up to accept ERC20 tokens in order for this to work.
|
||||
@@ -288,25 +273,6 @@ const bundle = wallet.sign({
|
||||
});
|
||||
```
|
||||
|
||||
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
|
||||
|
||||
```ts
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: tokenContract.address,
|
||||
encodedFunction: tokenContract.interface.encodeFunctionData("transfer", [
|
||||
aggregatorAddress,
|
||||
safeFee, // fee amount
|
||||
]),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## VerificationGateway
|
||||
|
||||
Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
|
||||
@@ -366,8 +332,13 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
|
||||
|
||||
(async () => {
|
||||
const privateKey = "0x...256 bits of private hex data here";
|
||||
const verificationGatewayAddress = "0x123...456";
|
||||
|
||||
const signer = await initBlsWalletSigner({ chainId: 10, privateKey });
|
||||
const signer = await initBlsWalletSigner({
|
||||
chainId: 10,
|
||||
privateKey,
|
||||
verificationGatewayAddress
|
||||
});
|
||||
|
||||
const someToken = new ethers.Contract(
|
||||
...
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { mcl } from "@thehubbleproject/bls";
|
||||
import privateKeyStorage from "./privateKeyStorage";
|
||||
import SecretStorage, { SecretReference } from "./SecretStorage";
|
||||
|
||||
export default class PrivateKey {
|
||||
#secretReference: SecretReference;
|
||||
|
||||
private constructor(secretReference: SecretReference) {
|
||||
this.#secretReference = secretReference;
|
||||
}
|
||||
|
||||
// Freely create private keys and pass them around - this class doesn't
|
||||
// actually store the private key string.
|
||||
static async generateRandom(): Promise<PrivateKey> {
|
||||
await mcl.init();
|
||||
const privateKeyString = mcl.randFr().serializeToHexStr();
|
||||
const secretReference = privateKeyStorage.storeSecret(privateKeyString);
|
||||
|
||||
return new PrivateKey(secretReference);
|
||||
}
|
||||
|
||||
// Reading the private key requires access to privateKeyStorage, and you'll
|
||||
// only import that if you *really* need the private key.
|
||||
read(privateKeyStorage: SecretStorage<string>): string {
|
||||
return privateKeyStorage.readSecret(this.#secretReference);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export type SecretReference = {
|
||||
type: "secret-reference";
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export default class SecretStorage<T> {
|
||||
#secrets = new WeakMap<SecretReference, T>();
|
||||
|
||||
storeSecret(secret: T, name?: string): SecretReference {
|
||||
const secretReference: SecretReference = { type: "secret-reference", name };
|
||||
this.#secrets.set(secretReference, secret);
|
||||
|
||||
return secretReference;
|
||||
}
|
||||
|
||||
readSecret(secretReference: SecretReference): T {
|
||||
const secret = this.#secrets.get(secretReference);
|
||||
|
||||
if (secret === undefined) {
|
||||
throw new Error(`Secret ${secretReference.name} not found`);
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import SecretStorage from "./SecretStorage";
|
||||
|
||||
export default new SecretStorage<string>();
|
||||
@@ -2,10 +2,13 @@
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { Deferrable } from "ethers/lib/utils";
|
||||
|
||||
import { ActionData, Bundle } from "./signer/types";
|
||||
import { ActionData, Bundle, PublicKey } from "./signer/types";
|
||||
import Aggregator, { BundleReceipt } from "./Aggregator";
|
||||
import BlsSigner, {
|
||||
TransactionBatchResponse,
|
||||
// Used for sendTransactionBatch TSdoc comment
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
TransactionBatch,
|
||||
UncheckedBlsSigner,
|
||||
_constructorGuard,
|
||||
} from "./BlsSigner";
|
||||
@@ -14,15 +17,28 @@ import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import {
|
||||
AggregatorUtilities__factory,
|
||||
BLSWallet__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "../typechain-types";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
|
||||
/** Public key linked to actions parsed from a bundle */
|
||||
export type PublicKeyLinkedToActions = {
|
||||
publicKey: PublicKey;
|
||||
actions: Array<ActionData>;
|
||||
};
|
||||
|
||||
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
readonly aggregator: Aggregator;
|
||||
readonly verificationGatewayAddress: string;
|
||||
readonly aggregatorUtilitiesAddress: string;
|
||||
signer!: BlsSigner;
|
||||
|
||||
/**
|
||||
* @param aggregatorUrl The url for an aggregator instance
|
||||
* @param verificationGatewayAddress Verification gateway contract address
|
||||
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
|
||||
* @param url Rpc url
|
||||
* @param network The network the provider should connect to
|
||||
*/
|
||||
constructor(
|
||||
aggregatorUrl: string,
|
||||
verificationGatewayAddress: string,
|
||||
@@ -36,37 +52,50 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
|
||||
*/
|
||||
override async estimateGas(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<BigNumber> {
|
||||
if (!transaction.to) {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
if (!resolvedTransaction.from) {
|
||||
throw new TypeError("Transaction.from should be defined");
|
||||
}
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: resolvedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: resolvedTransaction.to.toString(),
|
||||
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.signer.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
const nonce = await this.getTransactionCount(
|
||||
resolvedTransaction.from.toString(),
|
||||
);
|
||||
|
||||
const actionWithFeePaymentAction =
|
||||
this._addFeePaymentActionForFeeEstimation([action]);
|
||||
|
||||
// TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle
|
||||
// There is no way to estimate the cost of a bundle without signing a bundle. The
|
||||
// alternative would be to use a signer instance in this method which is undesirable,
|
||||
// as this would result in tight coupling between a provider and a signer.
|
||||
const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
throwawayPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(
|
||||
this.signer.wallet.sign({
|
||||
throwawayBlsWalletWrapper.sign({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
}),
|
||||
@@ -76,16 +105,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return addSafetyPremiumToFee(feeRequired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transaction to be executed. Adds the signed bundle to the aggregator
|
||||
*
|
||||
* @param signedTransaction A signed bundle
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
signedTransaction: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
}
|
||||
|
||||
const resolvedTransaction = await signedTransaction;
|
||||
const bundle: Bundle = JSON.parse(resolvedTransaction);
|
||||
|
||||
@@ -107,23 +135,20 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
|
||||
};
|
||||
|
||||
return this.signer.constructTransactionResponse(
|
||||
return await this._constructTransactionResponse(
|
||||
actionData,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
this.signer.wallet.address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signedTransactionBatch A signed {@link TransactionBatch}
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
signedTransactionBatch: string,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
}
|
||||
|
||||
const bundle: Bundle = JSON.parse(signedTransactionBatch);
|
||||
|
||||
const result = await this.aggregator.add(bundle);
|
||||
@@ -132,35 +157,40 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
const actionData: Array<ActionData> = bundle.operations
|
||||
.map((operation) => operation.actions)
|
||||
.flat();
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return this.signer.constructTransactionBatchResponse(
|
||||
actionData,
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
this.signer.wallet.address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new BlsSigner instance
|
||||
*/
|
||||
override getSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string | number,
|
||||
): BlsSigner {
|
||||
if (this.signer) {
|
||||
return this.signer;
|
||||
}
|
||||
|
||||
const signer = new BlsSigner(
|
||||
_constructorGuard,
|
||||
this,
|
||||
privateKey,
|
||||
addressOrIndex,
|
||||
);
|
||||
this.signer = signer;
|
||||
return signer;
|
||||
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new UncheckedBlsSigner instance
|
||||
*/
|
||||
override getUncheckedSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string,
|
||||
@@ -168,6 +198,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
|
||||
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
|
||||
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
|
||||
*/
|
||||
override async getTransactionReceipt(
|
||||
transactionHash: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
@@ -175,6 +214,17 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
|
||||
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
|
||||
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
|
||||
* @param retries The number of retries to poll the receipt for
|
||||
* @returns
|
||||
*/
|
||||
override async waitForTransaction(
|
||||
transactionHash: string,
|
||||
confirmations?: number,
|
||||
@@ -187,6 +237,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The address that the method gets the transaction count from
|
||||
* @param blockTag The specific block tag to get the transaction count from
|
||||
* @returns The number of transactions an account has sent
|
||||
*/
|
||||
override async getTransactionCount(
|
||||
address: string | Promise<string>,
|
||||
blockTag?:
|
||||
@@ -295,4 +350,104 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async _constructTransactionResponse(
|
||||
action: ActionData,
|
||||
publicKey: PublicKey,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async _constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions: Array<PublicKeyLinkedToActions>,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const transactions: Array<ethers.providers.TransactionResponse> = [];
|
||||
|
||||
for (const publicKeyLinkedToActions of publicKeysLinkedToActions) {
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
for (const action of publicKeyLinkedToActions.actions) {
|
||||
if (action.contractAddress === this.aggregatorUtilitiesAddress) {
|
||||
break;
|
||||
}
|
||||
|
||||
transactions.push({
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce!.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transactions,
|
||||
awaitBatchReceipt: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RLP,
|
||||
} from "ethers/lib/utils";
|
||||
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
import { ActionData, bundleToDto } from "./signer";
|
||||
@@ -16,11 +16,13 @@ import { ActionData, bundleToDto } from "./signer";
|
||||
export const _constructorGuard = {};
|
||||
|
||||
/**
|
||||
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) transaction gas limit
|
||||
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) miner tip aka priority fee
|
||||
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) the maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
|
||||
* @property nonce - integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
|
||||
* @property chainId - chain ID that this transaction is valid on
|
||||
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
|
||||
*
|
||||
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
|
||||
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
|
||||
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
|
||||
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
|
||||
* @property chainId - Chain ID that this transaction is valid on
|
||||
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
|
||||
*/
|
||||
export type BatchOptions = {
|
||||
@@ -33,14 +35,18 @@ export type BatchOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - an array of transaction objects
|
||||
* @property batchOptions - optional batch options taken into account by smart contract wallets
|
||||
* @property transactions - An array of Ethers transaction objects
|
||||
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
|
||||
*/
|
||||
export type TransactionBatch = {
|
||||
transactions: Array<ethers.providers.TransactionRequest>;
|
||||
batchOptions?: BatchOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - An array of Ethers transaction response objects
|
||||
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
|
||||
*/
|
||||
export interface TransactionBatchResponse {
|
||||
transactions: Array<ethers.providers.TransactionResponse>;
|
||||
awaitBatchReceipt: (
|
||||
@@ -58,10 +64,16 @@ export default class BlsSigner extends Signer {
|
||||
|
||||
readonly initPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* @param constructorGuard Prevents BlsSigner constructor being called directly
|
||||
* @param provider BlsProvider accociated with this signer
|
||||
* @param privateKey Private key for the account this signer represents
|
||||
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
|
||||
*/
|
||||
constructor(
|
||||
constructorGuard: Record<string, unknown>,
|
||||
provider: BlsProvider,
|
||||
privateKey: string,
|
||||
privateKey: string | Promise<string>,
|
||||
readonly addressOrIndex?: string | number,
|
||||
) {
|
||||
super();
|
||||
@@ -92,14 +104,28 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeWallet(privateKey: string) {
|
||||
/** Instantiates a BLS Wallet and then connects the signer to it */
|
||||
private async initializeWallet(privateKey: string | Promise<string>) {
|
||||
const resolvedPrivateKey = await privateKey;
|
||||
this.wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
resolvedPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transactions to be executed. Converts the TransactionRequest
|
||||
* to a bundle and adds it to the aggregator
|
||||
*
|
||||
* @remarks The transaction hash returned in the transaction response does
|
||||
* NOT correspond to a transaction hash that can be viewed on a block
|
||||
* explorer. It instead represents the bundle hash, which can be used to
|
||||
* get a transaction receipt that has a hash that can be used on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
@@ -109,6 +135,8 @@ export default class BlsSigner extends Signer {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
@@ -116,12 +144,12 @@ export default class BlsSigner extends Signer {
|
||||
);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(transaction);
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
@@ -137,26 +165,30 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
return this.constructTransactionResponse(
|
||||
return await this.provider._constructTransactionResponse(
|
||||
action,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
this.wallet.address,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
const validatedBatchOptions = await this._validateBatchOptions(
|
||||
transactionBatch.batchOptions,
|
||||
);
|
||||
|
||||
nonce = validatedBatchOptions.nonce as BigNumber;
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -166,11 +198,7 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
@@ -208,14 +236,27 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
return this.constructTransactionBatchResponse(
|
||||
actions,
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this.provider._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
this.wallet.address,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The address associated with the BlsSigner
|
||||
*/
|
||||
async getAddress(): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (this._address) {
|
||||
@@ -226,83 +267,6 @@ export default class BlsSigner extends Signer {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
// Construct a response that follows the ethers TransactionResponse type
|
||||
async constructTransactionResponse(
|
||||
action: ActionData,
|
||||
hash: string,
|
||||
from: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
const chainId = await this.getChainId();
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async constructTransactionBatchResponse(
|
||||
actions: Array<ActionData>,
|
||||
hash: string,
|
||||
from: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
await this.initPromise;
|
||||
const chainId = await this.getChainId();
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
const transactions: Array<ethers.providers.TransactionResponse> =
|
||||
actions.map((action) => {
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce!.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
transactions,
|
||||
awaitBatchReceipt: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider.
|
||||
* The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients.
|
||||
@@ -319,19 +283,23 @@ export default class BlsSigner extends Signer {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks Signs a transaction that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A signed bundle as a string
|
||||
*/
|
||||
override async signTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
@@ -340,7 +308,7 @@ export default class BlsSigner extends Signer {
|
||||
this.provider,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(transaction);
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
@@ -355,18 +323,24 @@ export default class BlsSigner extends Signer {
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction batch that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A signed bundle containing all transactions from the transaction batch as a string
|
||||
*/
|
||||
async signTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
const validatedBatchOptions = await this._validateBatchOptions(
|
||||
transactionBatch.batchOptions,
|
||||
);
|
||||
|
||||
nonce = validatedBatchOptions.nonce as BigNumber;
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -376,11 +350,7 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
@@ -416,8 +386,8 @@ export default class BlsSigner extends Signer {
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/** Sign a message */
|
||||
// TODO: Come back to this once we support EIP-1271
|
||||
/** Signs a message */
|
||||
// TODO: bls-wallet #201 Come back to this once we support EIP-1271
|
||||
override async signMessage(message: Bytes | string): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (isBytes(message)) {
|
||||
@@ -440,6 +410,9 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error("_signTypedData() is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A new Signer object which does not perform additional checks when sending a transaction
|
||||
*/
|
||||
connectUnchecked(): BlsSigner {
|
||||
return new UncheckedBlsSigner(
|
||||
_constructorGuard,
|
||||
@@ -453,6 +426,10 @@ export default class BlsSigner extends Signer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns Transaction hash for the transaction, corresponds to a bundle hash
|
||||
*/
|
||||
async sendUncheckedTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
@@ -464,6 +441,54 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error("_legacySignMessage() is not implemented");
|
||||
}
|
||||
|
||||
async _validateTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionRequest> {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
if (!resolvedTransaction.from) {
|
||||
resolvedTransaction.from = await this.getAddress();
|
||||
}
|
||||
|
||||
return resolvedTransaction;
|
||||
}
|
||||
|
||||
async _validateTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatch> {
|
||||
const signerAddress = await this.getAddress();
|
||||
|
||||
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
|
||||
transactionBatch.transactions.map((transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
if (!transaction.from) {
|
||||
transaction.from = signerAddress;
|
||||
}
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
};
|
||||
});
|
||||
|
||||
const validatedBatchOptions = transactionBatch.batchOptions
|
||||
? await this._validateBatchOptions(transactionBatch.batchOptions)
|
||||
: transactionBatch.batchOptions;
|
||||
|
||||
return {
|
||||
transactions: validatedTransactions,
|
||||
batchOptions: validatedBatchOptions,
|
||||
};
|
||||
}
|
||||
|
||||
async _validateBatchOptions(
|
||||
batchOptions: BatchOptions,
|
||||
): Promise<BatchOptions> {
|
||||
@@ -481,6 +506,12 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
export class UncheckedBlsSigner extends BlsSigner {
|
||||
/**
|
||||
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns The transaction response object with only the transaction hash property populated with a valid value
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
@@ -489,7 +520,7 @@ export class UncheckedBlsSigner extends BlsSigner {
|
||||
const transactionResponse = await super.sendTransaction(transaction);
|
||||
return {
|
||||
hash: transactionResponse.hash,
|
||||
nonce: 1,
|
||||
nonce: NaN,
|
||||
gasLimit: BigNumber.from(0),
|
||||
gasPrice: BigNumber.from(0),
|
||||
data: "",
|
||||
|
||||
@@ -74,6 +74,7 @@ export default class BlsWalletWrapper {
|
||||
const blsWalletSigner = await this.#BlsWalletSigner(
|
||||
signerOrProvider,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
);
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
@@ -147,6 +148,7 @@ export default class BlsWalletWrapper {
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: (await verificationGateway.provider.getNetwork()).chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
@@ -321,13 +323,18 @@ export default class BlsWalletWrapper {
|
||||
static async #BlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
return await initBlsWalletSigner({ chainId, privateKey });
|
||||
return await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,6 +354,7 @@ export default class BlsWalletWrapper {
|
||||
const newBlsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress: this.walletContract.address,
|
||||
});
|
||||
|
||||
this.blsWalletSigner = newBlsWalletSigner;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { arrayify, keccak256 } from "ethers/lib/utils";
|
||||
|
||||
export default arrayify(keccak256("0xfeedbee5"));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { keccak256, solidityPack } from "ethers/lib/utils";
|
||||
import { Operation } from "./types";
|
||||
|
||||
export default (chainId: number) =>
|
||||
export default () =>
|
||||
(operation: Operation, walletAddress: string): string => {
|
||||
let encodedActionData = "0x";
|
||||
|
||||
@@ -18,7 +18,7 @@ export default (chainId: number) =>
|
||||
}
|
||||
|
||||
return solidityPack(
|
||||
["uint256", "address", "uint256", "bytes32"],
|
||||
[chainId, walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
["address", "uint256", "bytes32"],
|
||||
[walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
);
|
||||
};
|
||||
|
||||
15
contracts/clients/src/signer/getDomain.ts
Normal file
15
contracts/clients/src/signer/getDomain.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { arrayify, solidityPack } from "ethers/lib/utils";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export default (
|
||||
chainId: number,
|
||||
verificationGatewayAddress: string,
|
||||
type: string,
|
||||
): Uint8Array => {
|
||||
const encoded = solidityPack(
|
||||
["uint256", "address", "string"],
|
||||
[chainId, verificationGatewayAddress, type],
|
||||
);
|
||||
|
||||
return arrayify(utils.keccak256(encoded));
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { signer } from "@thehubbleproject/bls";
|
||||
|
||||
import aggregate from "./aggregate";
|
||||
import defaultDomain from "./defaultDomain";
|
||||
import getDomain from "./getDomain";
|
||||
import getPublicKey from "./getPublicKey";
|
||||
import getPublicKeyHash from "./getPublicKeyHash";
|
||||
import getPublicKeyStr from "./getPublicKeyStr";
|
||||
@@ -16,11 +16,11 @@ export * from "./conversions";
|
||||
export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
|
||||
|
||||
export async function initBlsWalletSigner({
|
||||
domain = defaultDomain,
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
}: {
|
||||
domain?: Uint8Array;
|
||||
verificationGatewayAddress: string;
|
||||
chainId: number;
|
||||
privateKey: string;
|
||||
}) {
|
||||
@@ -32,14 +32,17 @@ export async function initBlsWalletSigner({
|
||||
// properly initialized for all use cases, not just signing.
|
||||
const signerFactory = await signer.BlsSignerFactory.new();
|
||||
|
||||
const bundleDomain = getDomain(chainId, verificationGatewayAddress, "Bundle");
|
||||
const walletDomain = getDomain(chainId, verificationGatewayAddress, "Wallet");
|
||||
|
||||
return {
|
||||
aggregate,
|
||||
getPublicKey: getPublicKey(signerFactory, domain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, domain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, domain, privateKey),
|
||||
sign: sign(signerFactory, domain, chainId, privateKey),
|
||||
signMessage: signMessage(signerFactory, domain, privateKey),
|
||||
verify: verify(domain, chainId),
|
||||
getPublicKey: getPublicKey(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, bundleDomain, privateKey),
|
||||
sign: sign(signerFactory, bundleDomain, chainId, privateKey),
|
||||
signMessage: signMessage(signerFactory, walletDomain, privateKey),
|
||||
verify: verify(bundleDomain, chainId),
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (
|
||||
) =>
|
||||
(operation: Operation, walletAddress: string): Bundle => {
|
||||
const signer = signerFactory.getSigner(domain, privateKey);
|
||||
const message = encodeMessageForSigning(chainId)(operation, walletAddress);
|
||||
const message = encodeMessageForSigning()(operation, walletAddress);
|
||||
const signature = signer.sign(message);
|
||||
|
||||
return {
|
||||
|
||||
@@ -26,7 +26,7 @@ export default (domain: Uint8Array, chainId: number) =>
|
||||
BigNumber.from(n3).toHexString(),
|
||||
]),
|
||||
bundle.operations.map((op) =>
|
||||
encodeMessageForSigning(chainId)(op, walletAddress),
|
||||
encodeMessageForSigning()(op, walletAddress),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("BlsProvider", () => {
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539, // 1337
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
@@ -60,11 +60,12 @@ describe("BlsProvider", () => {
|
||||
expect(uncheckedBlsSigner).to.be.instanceOf(UncheckedBlsSigner);
|
||||
});
|
||||
|
||||
it("should return a new signer if one has not been instantiated", async () => {
|
||||
it("should return a new signer", async () => {
|
||||
// Arrange
|
||||
const newVerificationGateway = "newMockVerificationGatewayAddress";
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
newVerificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
@@ -77,34 +78,11 @@ describe("BlsProvider", () => {
|
||||
|
||||
// Assert
|
||||
expect(newBlsSigner).to.not.equal(blsSigner);
|
||||
expect(newBlsSigner).to.equal(newBlsProvider.getSigner(newPrivateKey));
|
||||
});
|
||||
|
||||
it("should throw an error when this.signer has not been assigned", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
expect(newBlsSigner.provider.verificationGatewayAddress).to.not.equal(
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const value = parseEther("1");
|
||||
const transactionRequest = {
|
||||
to: recipient,
|
||||
value,
|
||||
};
|
||||
|
||||
// Act
|
||||
const gasEstimate = async () =>
|
||||
await newBlsProvider.estimateGas(transactionRequest);
|
||||
|
||||
// Assert
|
||||
await expect(gasEstimate()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Call provider.getSigner first",
|
||||
expect(newBlsSigner.provider.verificationGatewayAddress).to.equal(
|
||||
newVerificationGateway,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,6 +90,7 @@ describe("BlsProvider", () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
// Explicitly omit 'to'
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -124,28 +103,21 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error sending a transaction when this.signer is not defined", async () => {
|
||||
it("should throw an error estimating gas when 'transaction.from' has not been defined", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
const signedTransaction = blsSigner.signTransaction({
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
});
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
// Explicitly omit 'from'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await newBlsProvider.sendTransaction(signedTransaction);
|
||||
const result = async () => await blsProvider.estimateGas(transaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Call provider.getSigner first",
|
||||
TypeError,
|
||||
"Transaction.from should be defined",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,4 +131,90 @@ describe("BlsProvider", () => {
|
||||
// Assert
|
||||
expect(connection).to.deep.equal(expectedConnection);
|
||||
});
|
||||
|
||||
it("should throw an error when sending invalid signed transactions", async () => {
|
||||
// Arrange
|
||||
const invalidTransaction = "Invalid signed transaction";
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
const batchResult = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
await expect(batchResult()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
});
|
||||
|
||||
it("should get the polling interval", async () => {
|
||||
// Arrange
|
||||
const expectedpollingInterval = 4000; // default
|
||||
const updatedInterval = 1000;
|
||||
|
||||
// Act
|
||||
const pollingInterval = blsProvider.pollingInterval;
|
||||
blsProvider.pollingInterval = updatedInterval;
|
||||
const updatedPollingInterval = blsProvider.pollingInterval;
|
||||
|
||||
// Assert
|
||||
expect(pollingInterval).to.equal(expectedpollingInterval);
|
||||
expect(updatedPollingInterval).to.equal(updatedInterval);
|
||||
});
|
||||
|
||||
it("should get the event listener count and remove all listeners", async () => {
|
||||
blsProvider.on("block", () => {});
|
||||
blsProvider.on("error", () => {});
|
||||
expect(blsProvider.listenerCount("block")).to.equal(1);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(1);
|
||||
expect(blsProvider.listenerCount()).to.equal(2);
|
||||
|
||||
blsProvider.removeAllListeners();
|
||||
expect(blsProvider.listenerCount("block")).to.equal(0);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(0);
|
||||
expect(blsProvider.listenerCount()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should return true and an array of listeners if polling", async () => {
|
||||
// Arrange
|
||||
const expectedListener = () => {};
|
||||
|
||||
// Act
|
||||
blsProvider.on("block", expectedListener);
|
||||
const listeners = blsProvider.listeners("block");
|
||||
const isPolling = blsProvider.polling;
|
||||
blsProvider.removeAllListeners();
|
||||
|
||||
// Assert
|
||||
expect(listeners).to.deep.equal([expectedListener]);
|
||||
expect(isPolling).to.be.true;
|
||||
});
|
||||
|
||||
it("should be a provider", async () => {
|
||||
// Arrange & Act
|
||||
const isProvider = Experimental.BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider =
|
||||
Experimental.BlsProvider.isProvider(blsSigner);
|
||||
|
||||
// Assert
|
||||
expect(isProvider).to.equal(true);
|
||||
expect(isProviderWithInvalidProvider).to.equal(false);
|
||||
});
|
||||
|
||||
it("should a return a promise which will stall until the network has heen established", async () => {
|
||||
// Arrange
|
||||
const expectedReady = { name: "localhost", chainId: 1337 };
|
||||
|
||||
// Act
|
||||
const ready = await blsProvider.ready;
|
||||
|
||||
// Assert
|
||||
expect(ready).to.deep.equal(expectedReady);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BigNumber } from "ethers";
|
||||
import { keccak256, arrayify } from "ethers/lib/utils";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { initBlsWalletSigner, Bundle, Operation } from "../src/signer";
|
||||
|
||||
import Range from "./helpers/Range";
|
||||
|
||||
const domain = arrayify(keccak256("0xfeedbee5"));
|
||||
const weiPerToken = BigNumber.from(10).pow(18);
|
||||
|
||||
const samples = (() => {
|
||||
@@ -52,22 +50,22 @@ describe("index", () => {
|
||||
|
||||
const { sign, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const bundle = sign(bundleTemplate, walletAddress);
|
||||
|
||||
expect(bundle.signature).to.deep.equal([
|
||||
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
|
||||
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
|
||||
"0x21135f40b38f55236ceb637ad8f2d6d4e8081bc1c37ea08273838f839008b9cd",
|
||||
"0x16515fb0821c039e127dd8e4a70c7004aec1baf698802fc16e7cf8d2ae0bb14a",
|
||||
]);
|
||||
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -111,12 +109,12 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -125,8 +123,8 @@ describe("index", () => {
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(aggBundle.signature).to.deep.equal([
|
||||
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
|
||||
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
|
||||
"0x20c3afd45d2c7cd72003752377cf6853569bccd23abf962967a9245091b69c3b",
|
||||
"0x1ff4a18f1e920206f849e50df41e7bab6377d3908a8198d9c9268ca01ae70552",
|
||||
]);
|
||||
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
@@ -163,7 +161,7 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -195,7 +193,7 @@ describe("index", () => {
|
||||
|
||||
const { getPublicKeyStr } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -215,7 +213,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -230,7 +228,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF",
|
||||
privateKey,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so
|
||||
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
import "hardhat/console.sol";
|
||||
|
||||
/**
|
||||
A non-upgradable gateway used to create BLSWallets and call them with
|
||||
verified Operations that have been respectively signed.
|
||||
@@ -18,8 +20,8 @@ is the calling wallet's address.
|
||||
*/
|
||||
contract VerificationGateway
|
||||
{
|
||||
/** Domain chosen arbitrarily */
|
||||
bytes32 BLS_DOMAIN = keccak256(abi.encodePacked(uint32(0xfeedbee5)));
|
||||
bytes32 WALLET_DOMAIN;
|
||||
bytes32 BUNDLE_DOMAIN;
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBLS public immutable blsLib;
|
||||
@@ -73,6 +75,16 @@ contract VerificationGateway
|
||||
blsLib = bls;
|
||||
blsWalletLogic = blsWalletImpl;
|
||||
walletProxyAdmin = ProxyAdmin(proxyAdmin);
|
||||
WALLET_DOMAIN = keccak256(abi.encodePacked(
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Wallet"
|
||||
));
|
||||
BUNDLE_DOMAIN = keccak256(abi.encodePacked(
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Bundle"
|
||||
));
|
||||
}
|
||||
|
||||
/** Throw if bundle not valid or signature verification fails */
|
||||
@@ -353,7 +365,7 @@ contract VerificationGateway
|
||||
) private {
|
||||
// verify the given wallet was signed for by the bls key
|
||||
uint256[2] memory addressMsg = blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
WALLET_DOMAIN,
|
||||
abi.encodePacked(wallet)
|
||||
);
|
||||
require(
|
||||
@@ -411,9 +423,8 @@ contract VerificationGateway
|
||||
);
|
||||
}
|
||||
return blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
BUNDLE_DOMAIN,
|
||||
abi.encodePacked(
|
||||
block.chainid,
|
||||
walletAddress,
|
||||
op.nonce,
|
||||
keccak256(encodedActionData)
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"mcl-wasm": "^1.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.24",
|
||||
"sinon": "^15.0.2",
|
||||
"solhint": "^3.3.7",
|
||||
"solidity-coverage": "^0.8.2",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -146,7 +146,11 @@ export default class Fixture {
|
||||
blsExpander,
|
||||
utilities,
|
||||
BLSWallet,
|
||||
await initBlsWalletSigner({ chainId, privateKey }),
|
||||
await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress: verificationGateway.address,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,11 +85,11 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should estimate gas without throwing an error", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const getAddressPromise = blsSigner.getAddress();
|
||||
const transactionRequest = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
value: parseEther("1"),
|
||||
from: getAddressPromise,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -106,15 +106,11 @@ describe("BlsProvider", () => {
|
||||
const expectedBalance = parseEther("1");
|
||||
const balanceBefore = await blsProvider.getBalance(recipient);
|
||||
|
||||
const unsignedTransaction = {
|
||||
const signedTransaction = await blsSigner.signTransaction({
|
||||
value: expectedBalance.toString(),
|
||||
to: recipient,
|
||||
data: "0x",
|
||||
};
|
||||
|
||||
const signedTransaction = await blsSigner.signTransaction(
|
||||
unsignedTransaction,
|
||||
);
|
||||
});
|
||||
|
||||
// Act
|
||||
const transaction = await blsProvider.sendTransaction(signedTransaction);
|
||||
@@ -187,25 +183,19 @@ describe("BlsProvider", () => {
|
||||
it("should get the account nonce when the signer constructs the transaction response", async () => {
|
||||
// Arrange
|
||||
const spy = chai.spy.on(BlsWalletWrapper, "Nonce");
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const expectedBalance = parseEther("1");
|
||||
|
||||
const unsignedTransaction = {
|
||||
value: expectedBalance.toString(),
|
||||
to: recipient,
|
||||
const signedTransaction = await blsSigner.signTransaction({
|
||||
value: parseEther("1"),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
data: "0x",
|
||||
};
|
||||
const signedTransaction = await blsSigner.signTransaction(
|
||||
unsignedTransaction,
|
||||
);
|
||||
});
|
||||
|
||||
// Act
|
||||
await blsProvider.sendTransaction(signedTransaction);
|
||||
|
||||
// Assert
|
||||
// Once when calling "signer.signTransaction", once when calling "blsProvider.estimateGas", and once when calling "blsSigner.constructTransactionResponse".
|
||||
// Once when calling "signer.signTransaction", and once when calling "blsSigner.constructTransactionResponse".
|
||||
// This unit test is concerned with the latter being called.
|
||||
expect(spy).to.have.been.called.exactly(3);
|
||||
expect(spy).to.have.been.called.exactly(2);
|
||||
chai.spy.restore(spy);
|
||||
});
|
||||
|
||||
@@ -233,27 +223,6 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error when sending invalid signed transactions", async () => {
|
||||
// Arrange
|
||||
const invalidTransaction = "Invalid signed transaction";
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
const batchResult = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
await expect(batchResult()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
});
|
||||
|
||||
it("should send a batch of ETH transfers (empty calls) given a valid bundle", async () => {
|
||||
// Arrange
|
||||
const expectedAmount = parseEther("1");
|
||||
@@ -290,7 +259,7 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should send a batch of ETH transfers (empty calls) given two aggregated bundles", async () => {
|
||||
it("should send a batch of ETH transfers (empty calls) given two aggregated bundles and return a transaction batch response", async () => {
|
||||
// Arrange
|
||||
const expectedAmount = parseEther("1");
|
||||
const verySafeFee = parseEther("0.1");
|
||||
@@ -336,10 +305,10 @@ describe("BlsProvider", () => {
|
||||
]);
|
||||
|
||||
// Act
|
||||
const result = await blsProvider.sendTransactionBatch(
|
||||
const transactionBatchResponse = await blsProvider.sendTransactionBatch(
|
||||
JSON.stringify(bundleToDto(aggregatedBundle)),
|
||||
);
|
||||
await result.awaitBatchReceipt();
|
||||
await transactionBatchResponse.awaitBatchReceipt();
|
||||
|
||||
// Assert
|
||||
expect(await blsProvider.getBalance(firstRecipient)).to.equal(
|
||||
@@ -348,6 +317,46 @@ describe("BlsProvider", () => {
|
||||
expect(await blsProvider.getBalance(secondRecipient)).to.equal(
|
||||
expectedAmount,
|
||||
);
|
||||
|
||||
// tx 1
|
||||
expect(transactionBatchResponse.transactions[0])
|
||||
.to.be.an("object")
|
||||
.that.includes({
|
||||
hash: transactionBatchResponse.transactions[0].hash,
|
||||
to: firstRecipient,
|
||||
from: blsSigner.wallet.address,
|
||||
data: "0x",
|
||||
chainId: 1337,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
});
|
||||
expect(transactionBatchResponse.transactions[0].nonce).to.equal(0);
|
||||
expect(transactionBatchResponse.transactions[0].gasLimit).to.equal(
|
||||
BigNumber.from("0x0"),
|
||||
);
|
||||
expect(transactionBatchResponse.transactions[0].value).to.equal(
|
||||
BigNumber.from(expectedAmount),
|
||||
);
|
||||
|
||||
// tx 2
|
||||
expect(transactionBatchResponse.transactions[1])
|
||||
.to.be.an("object")
|
||||
.that.includes({
|
||||
hash: transactionBatchResponse.transactions[1].hash,
|
||||
to: secondRecipient,
|
||||
from: blsSigner.wallet.address,
|
||||
data: "0x",
|
||||
chainId: 1337,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
});
|
||||
expect(transactionBatchResponse.transactions[1].nonce).to.equal(0);
|
||||
expect(transactionBatchResponse.transactions[1].gasLimit).to.equal(
|
||||
BigNumber.from("0x0"),
|
||||
);
|
||||
expect(transactionBatchResponse.transactions[1].value).to.equal(
|
||||
BigNumber.from(expectedAmount),
|
||||
);
|
||||
});
|
||||
|
||||
it("should get the account nonce when the signer constructs the transaction batch response", async () => {
|
||||
@@ -427,13 +436,12 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should wait for a transaction and resolve once transaction hash is included in the block", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionResponse = await blsSigner.sendTransaction({
|
||||
to: recipient,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
value: parseEther("1"),
|
||||
});
|
||||
|
||||
const expectedToAddress = "0x689A095B4507Bfa302eef8551F90fB322B3451c6"; // Verification Gateway address
|
||||
const expectedToAddress = "0x14EE47429DEf3462142AE5f8d1E263E0B137bA63"; // Verification Gateway address
|
||||
const expectedFromAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // Aggregator address (Hardhat account 0)
|
||||
|
||||
// Act
|
||||
@@ -489,13 +497,12 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should retrieve a transaction receipt given a valid hash", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionResponse = await blsSigner.sendTransaction({
|
||||
to: recipient,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
value: parseEther("1"),
|
||||
});
|
||||
|
||||
const expectedToAddress = "0x689A095B4507Bfa302eef8551F90fB322B3451c6"; // Verification Gateway address
|
||||
const expectedToAddress = "0x14EE47429DEf3462142AE5f8d1E263E0B137bA63"; // Verification Gateway address
|
||||
const expectedFromAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // Aggregator address (Hardhat account 0)
|
||||
|
||||
// Act
|
||||
@@ -547,13 +554,11 @@ describe("BlsProvider", () => {
|
||||
expect(transactionReceipt.effectiveGasPrice).to.be.an("object");
|
||||
});
|
||||
|
||||
it("gets a transaction given a valid transaction hash", async () => {
|
||||
it("should get a transaction given a valid transaction hash", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transactionRequest = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
value: parseEther("1"),
|
||||
};
|
||||
|
||||
const expectedTransactionResponse = await blsSigner.sendTransaction(
|
||||
@@ -589,6 +594,7 @@ describe("BlsProvider", () => {
|
||||
// expects a different address when running as part of our github workflow.
|
||||
// from: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
||||
chainId: expectedTransactionResponse.chainId,
|
||||
// chainId: parseInt(expectedTransactionResponse.chainId, 16),
|
||||
type: 2,
|
||||
accessList: [],
|
||||
blockNumber: transactionReceipt.blockNumber,
|
||||
@@ -645,68 +651,9 @@ describe("BlsProvider", () => {
|
||||
expect(accounts).to.deep.equal(expectedAccounts);
|
||||
});
|
||||
|
||||
it("should get the polling interval", async () => {
|
||||
// Arrange
|
||||
const expectedpollingInterval = 4000; // default
|
||||
const updatedInterval = 1000;
|
||||
|
||||
// Act
|
||||
const pollingInterval = blsProvider.pollingInterval;
|
||||
blsProvider.pollingInterval = updatedInterval;
|
||||
const updatedPollingInterval = blsProvider.pollingInterval;
|
||||
|
||||
// Assert
|
||||
expect(pollingInterval).to.equal(expectedpollingInterval);
|
||||
expect(updatedPollingInterval).to.equal(updatedInterval);
|
||||
});
|
||||
|
||||
it("should get the event listener count and remove all listeners", async () => {
|
||||
blsProvider.on("block", () => {});
|
||||
blsProvider.on("error", () => {});
|
||||
expect(blsProvider.listenerCount("block")).to.equal(1);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(1);
|
||||
expect(blsProvider.listenerCount()).to.equal(2);
|
||||
|
||||
blsProvider.removeAllListeners();
|
||||
expect(blsProvider.listenerCount("block")).to.equal(0);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(0);
|
||||
expect(blsProvider.listenerCount()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should return true and an array of listeners if polling", async () => {
|
||||
// Arrange
|
||||
const expectedListener = () => {};
|
||||
|
||||
// Act
|
||||
blsProvider.on("block", expectedListener);
|
||||
const listeners = blsProvider.listeners("block");
|
||||
const isPolling = blsProvider.polling;
|
||||
blsProvider.removeAllListeners();
|
||||
|
||||
// Assert
|
||||
expect(listeners).to.deep.equal([expectedListener]);
|
||||
expect(isPolling).to.be.true;
|
||||
});
|
||||
|
||||
it("should be a provider", async () => {
|
||||
// Arrange & Act
|
||||
const isProvider = Experimental.BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider =
|
||||
Experimental.BlsProvider.isProvider(blsSigner);
|
||||
|
||||
// Assert
|
||||
expect(isProvider).to.equal(true);
|
||||
expect(isProviderWithInvalidProvider).to.equal(false);
|
||||
});
|
||||
|
||||
it("should return the number of transactions an address has sent", async function () {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: BigNumber.from(1),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
};
|
||||
const address = await blsSigner.getAddress();
|
||||
|
||||
const expectedFirstTransactionCount = 0;
|
||||
const expectedSecondTransactionCount = 1;
|
||||
|
||||
@@ -715,7 +662,10 @@ describe("BlsProvider", () => {
|
||||
address,
|
||||
);
|
||||
|
||||
const sendTransaction = await blsSigner.sendTransaction(transaction);
|
||||
const sendTransaction = await blsSigner.sendTransaction({
|
||||
value: BigNumber.from(1),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
});
|
||||
await sendTransaction.wait();
|
||||
|
||||
const secondTransactionCount = await blsProvider.getTransactionCount(
|
||||
@@ -891,97 +841,4 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
expect(feeData.gasPrice).to.deep.equal(expectedFeeData.gasPrice);
|
||||
});
|
||||
|
||||
it("should a return a promise which will stall until the network has heen established", async () => {
|
||||
// Arrange
|
||||
const expectedReady = { name: "localhost", chainId: 1337 };
|
||||
|
||||
// Act
|
||||
const ready = await blsProvider.ready;
|
||||
|
||||
// Assert
|
||||
expect(ready).to.deep.equal(expectedReady);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JsonRpcProvider", () => {
|
||||
let wallet: ethers.Wallet;
|
||||
|
||||
beforeEach(async () => {
|
||||
rpcUrl = "http://localhost:8545";
|
||||
regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
||||
// First two Hardhat account private keys are used in aggregator .env. We choose to use Hardhat account #2 private key here to avoid nonce too low errors.
|
||||
wallet = new ethers.Wallet(
|
||||
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat acount #2 private key
|
||||
regularProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls a getter method on a contract", async () => {
|
||||
// Arrange
|
||||
const expectedSupply = "1000000.0";
|
||||
const testERC20 = MockERC20__factory.connect(
|
||||
networkConfig.addresses.testToken,
|
||||
regularProvider,
|
||||
);
|
||||
|
||||
const transaction = {
|
||||
to: testERC20.address,
|
||||
data: testERC20.interface.encodeFunctionData("totalSupply"),
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await regularProvider.call(transaction);
|
||||
|
||||
// Assert
|
||||
expect(formatEther(result)).to.equal(expectedSupply);
|
||||
});
|
||||
|
||||
it("gets a transaction given a valid transaction hash", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transactionRequest = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
};
|
||||
|
||||
const expectedTransactionResponse = await wallet.sendTransaction(
|
||||
transactionRequest,
|
||||
);
|
||||
|
||||
// Act
|
||||
const transactionResponse = await regularProvider.getTransaction(
|
||||
expectedTransactionResponse.hash,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(transactionResponse).to.be.an("object").that.deep.includes({
|
||||
hash: expectedTransactionResponse.hash,
|
||||
type: expectedTransactionResponse.type,
|
||||
accessList: expectedTransactionResponse.accessList,
|
||||
transactionIndex: 0,
|
||||
confirmations: 1,
|
||||
from: expectedTransactionResponse.from,
|
||||
maxPriorityFeePerGas: expectedTransactionResponse.maxPriorityFeePerGas,
|
||||
maxFeePerGas: expectedTransactionResponse.maxFeePerGas,
|
||||
gasLimit: expectedTransactionResponse.gasLimit,
|
||||
to: expectedTransactionResponse.to,
|
||||
value: expectedTransactionResponse.value,
|
||||
nonce: expectedTransactionResponse.nonce,
|
||||
data: expectedTransactionResponse.data,
|
||||
r: expectedTransactionResponse.r,
|
||||
s: expectedTransactionResponse.s,
|
||||
v: expectedTransactionResponse.v,
|
||||
creates: null,
|
||||
chainId: expectedTransactionResponse.chainId,
|
||||
});
|
||||
|
||||
expect(transactionResponse).to.include.keys(
|
||||
"wait",
|
||||
"blockHash",
|
||||
"blockNumber",
|
||||
"gasPrice",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,18 +66,25 @@ describe("Provider tests", function () {
|
||||
});
|
||||
|
||||
it("balanceOf() call", async () => {
|
||||
// Arrange & Act
|
||||
const balance = await mockERC20.connect(blsProvider).balanceOf(recipient);
|
||||
|
||||
// Assert
|
||||
expect(balance).to.equal(tokenSupply.div(2));
|
||||
});
|
||||
|
||||
it("calls balanceOf successfully after instantiating Contract class with BlsProvider", async () => {
|
||||
// Arrange
|
||||
const erc20 = new ethers.Contract(
|
||||
mockERC20.address,
|
||||
mockERC20.interface,
|
||||
blsProvider,
|
||||
);
|
||||
|
||||
// Act
|
||||
const balance = await erc20.balanceOf(recipient);
|
||||
|
||||
// Assert
|
||||
expect(erc20.provider).to.equal(blsProvider);
|
||||
expect(balance).to.equal(tokenSupply.div(2));
|
||||
});
|
||||
@@ -162,7 +169,7 @@ describe("Provider tests", function () {
|
||||
await tx.wait();
|
||||
|
||||
// wait 1 second to ensure listener count updates
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Assert
|
||||
expect((await erc20.balanceOf(recipient)).sub(balanceBefore)).to.equal(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers as hardhatEthers } from "hardhat";
|
||||
import chai, { expect } from "chai";
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import {
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
RLP,
|
||||
formatEther,
|
||||
} from "ethers/lib/utils";
|
||||
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
Experimental,
|
||||
@@ -34,6 +33,7 @@ let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
|
||||
let regularProvider: ethers.providers.JsonRpcProvider;
|
||||
let fundedWallet: ethers.Wallet;
|
||||
|
||||
describe("BlsSigner", () => {
|
||||
beforeEach(async () => {
|
||||
@@ -61,7 +61,7 @@ describe("BlsSigner", () => {
|
||||
|
||||
regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
const fundedWallet = new ethers.Wallet(
|
||||
fundedWallet = new ethers.Wallet(
|
||||
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat Account #2 private key
|
||||
regularProvider,
|
||||
);
|
||||
@@ -91,22 +91,6 @@ describe("BlsSigner", () => {
|
||||
).to.equal(expectedBalance);
|
||||
});
|
||||
|
||||
it("should throw an error sending a transaction when 'transaction.to' has not been defined", async () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () => await blsSigner.sendTransaction(transaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
TypeError,
|
||||
"Transaction.to should be defined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error when sending an invalid transaction", async () => {
|
||||
// Arrange
|
||||
const invalidValue = parseEther("-1");
|
||||
@@ -394,35 +378,6 @@ describe("BlsSigner", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error sending a transaction batch when this.signer is not defined", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
const signedTransaction = await blsSigner.signTransactionBatch({
|
||||
transactions: [
|
||||
{
|
||||
value: parseEther("1"),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await newBlsProvider.sendTransactionBatch(signedTransaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Call provider.getSigner first",
|
||||
);
|
||||
});
|
||||
|
||||
it("should validate batch options", async () => {
|
||||
// Arrange
|
||||
const batchOptions = {
|
||||
@@ -492,7 +447,7 @@ describe("BlsSigner", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw an error when invalid private key is supplied after a valid getSigner call", async () => {
|
||||
it("should throw an error when invalid private key is supplied after a valid getSigner call", async () => {
|
||||
// Arrange
|
||||
const newBlsSigner = blsProvider.getSigner("invalidPrivateKey");
|
||||
|
||||
@@ -504,7 +459,10 @@ describe("BlsSigner", () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.not.be.rejectedWith(Error);
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Expect hex but got invalidPrivateKey",
|
||||
);
|
||||
});
|
||||
|
||||
it("should retrieve the account address", async () => {
|
||||
@@ -522,22 +480,6 @@ describe("BlsSigner", () => {
|
||||
expect(address).to.equal(expectedAddress);
|
||||
});
|
||||
|
||||
it("should throw an error signing a transaction when transaction.to has not been defined", async () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () => await blsSigner.sendTransaction(transaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
TypeError,
|
||||
"Transaction.to should be defined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should sign a transaction to create a bundleDto and serialize the result", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
@@ -545,6 +487,7 @@ describe("BlsSigner", () => {
|
||||
value: "1000000000000000000",
|
||||
to: recipient,
|
||||
data: "0x",
|
||||
from: await blsSigner.getAddress(),
|
||||
};
|
||||
|
||||
// get expected signature
|
||||
@@ -567,6 +510,14 @@ describe("BlsSigner", () => {
|
||||
blsSigner,
|
||||
);
|
||||
|
||||
// BlsWalletWrapper.getRandomBlsPrivateKey from "estimateGas" method results in slightly different
|
||||
// fee estimates. Which leads to a different signature. This fake avoids signature mismatch by stubbing a constant value.
|
||||
sinon.replace(
|
||||
BlsWalletWrapper,
|
||||
"getRandomBlsPrivateKey",
|
||||
sinon.fake.resolves(privateKey),
|
||||
);
|
||||
|
||||
const expectedFeeEstimate = await blsProvider.estimateGas(transaction);
|
||||
|
||||
const actionsWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
@@ -596,20 +547,19 @@ describe("BlsSigner", () => {
|
||||
expect(bundleDto.signature).to.deep.equal(
|
||||
expectedBundleSignatureHexStrings,
|
||||
);
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should throw an error when signing an invalid transaction", async () => {
|
||||
// Arrange
|
||||
const invalidEthValue = parseEther("-1");
|
||||
|
||||
const unsignedTransaction = {
|
||||
value: invalidEthValue,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsSigner.signTransaction(unsignedTransaction);
|
||||
await blsSigner.signTransaction({
|
||||
value: invalidEthValue,
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
@@ -768,13 +718,12 @@ describe("BlsSigner", () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = blsSigner.checkTransaction(transaction);
|
||||
const result = blsSigner.checkTransaction({
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const resolvedResult = await resolveProperties(result);
|
||||
@@ -791,13 +740,12 @@ describe("BlsSigner", () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await blsSigner.populateTransaction(transaction);
|
||||
const result = await blsSigner.populateTransaction({
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).to.be.an("object").that.includes({
|
||||
@@ -850,23 +798,25 @@ describe("BlsSigner", () => {
|
||||
});
|
||||
|
||||
it("should await the init promise when connecting to an unchecked bls signer", async () => {
|
||||
// Arrange & Act
|
||||
// Arrange
|
||||
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const newBlsSigner = blsProvider.getSigner(newPrivateKey);
|
||||
const uncheckedBlsSigner = newBlsSigner.connectUnchecked();
|
||||
|
||||
await fundedWallet.sendTransaction({
|
||||
to: await uncheckedBlsSigner.getAddress(),
|
||||
value: parseEther("1.1"),
|
||||
});
|
||||
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
};
|
||||
const balanceBefore = await blsProvider.getBalance(recipient);
|
||||
|
||||
// Act
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction(
|
||||
transaction,
|
||||
);
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
});
|
||||
await uncheckedResponse.wait();
|
||||
|
||||
// Assert
|
||||
@@ -875,9 +825,7 @@ describe("BlsSigner", () => {
|
||||
).to.equal(transactionAmount);
|
||||
});
|
||||
|
||||
// TODO (merge-ok) https://github.com/web3well/bls-wallet/issues/427
|
||||
// This test is identical to the above test except this one uses a new instance of a provider, yet fails to find the tx receipt
|
||||
it.skip("should get the transaction receipt when using a new provider and connecting to an unchecked bls signer", async () => {
|
||||
it("should get the transaction receipt when using a new provider and connecting to an unchecked bls signer", async () => {
|
||||
// Arrange & Act
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
@@ -890,18 +838,20 @@ describe("BlsSigner", () => {
|
||||
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
|
||||
const uncheckedBlsSigner = newBlsSigner.connectUnchecked();
|
||||
|
||||
await fundedWallet.sendTransaction({
|
||||
to: await uncheckedBlsSigner.getAddress(),
|
||||
value: parseEther("1.1"),
|
||||
});
|
||||
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
};
|
||||
const balanceBefore = await blsProvider.getBalance(recipient);
|
||||
|
||||
// Act
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction(
|
||||
transaction,
|
||||
);
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
});
|
||||
await uncheckedResponse.wait();
|
||||
|
||||
// Assert
|
||||
@@ -914,16 +864,13 @@ describe("BlsSigner", () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
};
|
||||
const balanceBefore = await blsProvider.getBalance(recipient);
|
||||
|
||||
// Act
|
||||
const uncheckedTransactionHash = await blsSigner.sendUncheckedTransaction(
|
||||
transaction,
|
||||
);
|
||||
const uncheckedTransactionHash = await blsSigner.sendUncheckedTransaction({
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
});
|
||||
await blsProvider.getTransactionReceipt(uncheckedTransactionHash);
|
||||
|
||||
// Assert
|
||||
@@ -938,22 +885,18 @@ describe("BlsSigner", () => {
|
||||
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
};
|
||||
const balanceBefore = await blsProvider.getBalance(recipient);
|
||||
|
||||
// Act
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction(
|
||||
transaction,
|
||||
);
|
||||
const uncheckedResponse = await uncheckedBlsSigner.sendTransaction({
|
||||
value: transactionAmount,
|
||||
to: recipient,
|
||||
});
|
||||
await uncheckedResponse.wait();
|
||||
|
||||
// Assert
|
||||
expect(uncheckedResponse).to.be.an("object").that.includes({
|
||||
hash: uncheckedResponse.hash,
|
||||
nonce: 1,
|
||||
data: "",
|
||||
chainId: 0,
|
||||
confirmations: 0,
|
||||
@@ -963,6 +906,7 @@ describe("BlsSigner", () => {
|
||||
expect(uncheckedResponse.gasLimit).to.equal(BigNumber.from("0"));
|
||||
expect(uncheckedResponse.gasPrice).to.equal(BigNumber.from("0"));
|
||||
expect(uncheckedResponse.value).to.equal(BigNumber.from("0"));
|
||||
expect(isNaN(uncheckedResponse.nonce)).to.be.true;
|
||||
|
||||
expect(
|
||||
(await blsProvider.getBalance(recipient)).sub(balanceBefore),
|
||||
@@ -1031,7 +975,7 @@ describe("BlsSigner", () => {
|
||||
const expectedTransactionCount = 0;
|
||||
|
||||
const sendTransaction = await blsSigner.sendTransaction({
|
||||
value: BigNumber.from(1),
|
||||
value: parseEther("1"),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
});
|
||||
await sendTransaction.wait();
|
||||
@@ -1053,14 +997,12 @@ describe("BlsSigner", () => {
|
||||
blsProvider,
|
||||
);
|
||||
|
||||
const transaction = {
|
||||
// Act
|
||||
const result = await blsSigner.call({
|
||||
to: testERC20.address,
|
||||
data: testERC20.interface.encodeFunctionData("totalSupply"),
|
||||
// Explicitly omit 'from'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await blsSigner.call(transaction);
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(formatEther(result)).to.equal("1000000.0");
|
||||
@@ -1077,14 +1019,14 @@ describe("BlsSigner", () => {
|
||||
// Arrange
|
||||
const spy = chai.spy.on(Experimental.BlsProvider.prototype, "estimateGas");
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transaction = {
|
||||
to: recipient,
|
||||
value: parseEther("1"),
|
||||
// Explicitly omit 'from'
|
||||
};
|
||||
|
||||
// Act
|
||||
const gasEstimate = async () => await blsSigner.estimateGas(transaction);
|
||||
const gasEstimate = async () =>
|
||||
await blsSigner.estimateGas({
|
||||
to: recipient,
|
||||
value: parseEther("1"),
|
||||
// Explicitly omit 'from'
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(gasEstimate()).to.not.be.rejected;
|
||||
@@ -1125,102 +1067,87 @@ describe("BlsSigner", () => {
|
||||
"account unlock with HTTP access is forbidden",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JsonRpcSigner", () => {
|
||||
let signers: SignerWithAddress[];
|
||||
let wallet: ethers.Wallet;
|
||||
|
||||
beforeEach(async () => {
|
||||
signers = await hardhatEthers.getSigners();
|
||||
rpcUrl = "http://localhost:8545";
|
||||
regularProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
|
||||
// First two Hardhat account private keys are used in aggregator .env. We choose to use Hardhat account #2 private key here to avoid nonce too low errors.
|
||||
wallet = new ethers.Wallet(
|
||||
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // Hardhat account #2 private key
|
||||
regularProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it("should retrieve the account address", async () => {
|
||||
// Arrange
|
||||
const expectedAddress = signers[2].address;
|
||||
|
||||
// Act
|
||||
const address = await wallet.getAddress();
|
||||
|
||||
// Assert
|
||||
expect(address).to.equal(expectedAddress);
|
||||
});
|
||||
|
||||
it("should send ETH (empty call) successfully", async () => {
|
||||
it("should validate a transaction request", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const expectedBalance = parseEther("1");
|
||||
const recipientBalanceBefore = await regularProvider.getBalance(recipient);
|
||||
|
||||
// Act
|
||||
const transaction = await wallet.sendTransaction({
|
||||
const getBalancePromise = blsSigner.getBalance();
|
||||
const expectedValidatedTransaction = {
|
||||
to: recipient,
|
||||
value: expectedBalance,
|
||||
});
|
||||
await transaction.wait();
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
(await regularProvider.getBalance(recipient)).sub(recipientBalanceBefore),
|
||||
).to.equal(expectedBalance);
|
||||
});
|
||||
|
||||
it("should check transaction", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
value: await blsSigner.getBalance(),
|
||||
from: await blsSigner.getAddress(),
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = wallet.checkTransaction(transaction);
|
||||
const validatedTransaction = await blsSigner._validateTransaction({
|
||||
to: recipient,
|
||||
value: getBalancePromise,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const resolvedResult = await resolveProperties(result);
|
||||
expect(resolvedResult)
|
||||
.to.be.an("object")
|
||||
.that.includes({
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
from: await wallet.getAddress(),
|
||||
expect(validatedTransaction).to.deep.equal(expectedValidatedTransaction);
|
||||
});
|
||||
|
||||
it("should throw an error validating a transaction request when transaction.to is not defined", async () => {
|
||||
// Arrange & Act
|
||||
const result = async () =>
|
||||
await blsSigner._validateTransaction({
|
||||
value: await blsSigner.getBalance(),
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
TypeError,
|
||||
"Transaction.to should be defined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should populate transaction", async () => {
|
||||
it("should validate a transaction batch", async () => {
|
||||
// Arrange
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const transactionAmount = parseEther("1");
|
||||
const transaction = {
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
const amount = await blsSigner.getBalance();
|
||||
const expectedValidatedTransactionBatch = {
|
||||
transactions: [
|
||||
{
|
||||
to: recipient,
|
||||
value: amount,
|
||||
from: await blsSigner.getAddress(),
|
||||
},
|
||||
],
|
||||
batchOptions: undefined,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await wallet.populateTransaction(transaction);
|
||||
|
||||
// Assert
|
||||
expect(result).to.be.an("object").that.includes({
|
||||
to: recipient,
|
||||
value: transactionAmount,
|
||||
from: signers[2].address,
|
||||
type: 2,
|
||||
chainId: 1337,
|
||||
const validatedTransaction = await blsSigner._validateTransactionBatch({
|
||||
transactions: [
|
||||
{
|
||||
to: recipient,
|
||||
value: amount,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).to.include.keys(
|
||||
"maxFeePerGas",
|
||||
"maxPriorityFeePerGas",
|
||||
"nonce",
|
||||
"gasLimit",
|
||||
// Assert
|
||||
expect(validatedTransaction).to.deep.equal(
|
||||
expectedValidatedTransactionBatch,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error validating a transaction batch when transaction.to is not defined", async () => {
|
||||
// Arrange & Act
|
||||
const result = async () =>
|
||||
await blsSigner._validateTransactionBatch({
|
||||
transactions: [
|
||||
{
|
||||
value: await blsSigner.getBalance(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
TypeError,
|
||||
"Transaction.to is missing on transaction 0",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -121,14 +121,14 @@ describe("Upgrade", async function () {
|
||||
const walletAddress = walletOldVg.address;
|
||||
const blsSecret = walletOldVg.blsWalletSigner.privateKey;
|
||||
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
blsSecret,
|
||||
fx.verificationGateway.address,
|
||||
fx.verificationGateway.provider,
|
||||
);
|
||||
// Sign simple address message
|
||||
const addressMessage = solidityPack(["address"], [walletAddress]);
|
||||
const addressSignature = wallet.signMessage(addressMessage);
|
||||
const walletNewVg = await BlsWalletWrapper.connect(
|
||||
blsSecret,
|
||||
vg2.address,
|
||||
vg2.provider,
|
||||
);
|
||||
const addressSignature = walletNewVg.signMessage(addressMessage);
|
||||
|
||||
const proxyAdmin2Address = await vg2.walletProxyAdmin();
|
||||
// Get admin action to change proxy
|
||||
@@ -181,19 +181,18 @@ describe("Upgrade", async function () {
|
||||
{
|
||||
// Fail if setExternalWalletAction is skipped
|
||||
|
||||
const { successes } =
|
||||
await fx.verificationGateway.callStatic.processBundle(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(2),
|
||||
actions: [
|
||||
// skip: setExternalWalletAction,
|
||||
changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
const result = await fx.verificationGateway.callStatic.processBundle(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(2),
|
||||
actions: [
|
||||
// skip: setExternalWalletAction,
|
||||
changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(successes).to.deep.equal([false]);
|
||||
expect(result.successes).to.deep.equal([false]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -276,10 +275,11 @@ describe("Upgrade", async function () {
|
||||
// Check new verification gateway was set
|
||||
expect(await blsWallet.trustedBLSGateway()).to.equal(vg2.address);
|
||||
|
||||
await walletNewVg.syncWallet(vg2);
|
||||
// Check new gateway has wallet via static call through new gateway
|
||||
const bundleResult = await vg2.callStatic.processBundle(
|
||||
fx.blsWalletSigner.aggregate([
|
||||
walletOldVg.sign({
|
||||
walletNewVg.sign({
|
||||
nonce: BigNumber.from(3),
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -981,6 +981,41 @@
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
|
||||
integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
|
||||
|
||||
"@sinonjs/commons@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
|
||||
integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
|
||||
dependencies:
|
||||
type-detect "4.0.8"
|
||||
|
||||
"@sinonjs/commons@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"
|
||||
integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==
|
||||
dependencies:
|
||||
type-detect "4.0.8"
|
||||
|
||||
"@sinonjs/fake-timers@^10.0.2":
|
||||
version "10.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c"
|
||||
integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^2.0.0"
|
||||
|
||||
"@sinonjs/samsam@^7.0.1":
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f"
|
||||
integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^2.0.0"
|
||||
lodash.get "^4.4.2"
|
||||
type-detect "^4.0.8"
|
||||
|
||||
"@sinonjs/text-encoding@^0.7.1":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
|
||||
integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
|
||||
|
||||
"@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1", "@solidity-parser/parser@^0.14.3":
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804"
|
||||
@@ -3556,6 +3591,11 @@ diff@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
diff@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
|
||||
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||
@@ -6463,6 +6503,11 @@ jsprim@^1.2.2:
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
just-extend@^4.0.2:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
|
||||
integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
|
||||
|
||||
keccak@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff"
|
||||
@@ -6760,6 +6805,11 @@ lodash.camelcase@^4.3.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
@@ -7386,6 +7436,17 @@ nice-try@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
nise@^5.1.4:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
|
||||
integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^2.0.0"
|
||||
"@sinonjs/fake-timers" "^10.0.2"
|
||||
"@sinonjs/text-encoding" "^0.7.1"
|
||||
just-extend "^4.0.2"
|
||||
path-to-regexp "^1.7.0"
|
||||
|
||||
node-addon-api@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
|
||||
@@ -7877,6 +7938,13 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||
@@ -8884,6 +8952,18 @@ simple-get@^2.7.0:
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
sinon@^15.0.2:
|
||||
version "15.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.2.tgz#f3e3aacb990bbaa8a7bb976e86118c5dc0154e66"
|
||||
integrity sha512-PCVP63XZkg0/LOqQH5rEU4LILuvTFMb5tNxTHfs6VUMNnZz2XrnGSTZbAGITjzwQWbl/Bl/8hi4G3zZWjyBwHg==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.0"
|
||||
"@sinonjs/fake-timers" "^10.0.2"
|
||||
"@sinonjs/samsam" "^7.0.1"
|
||||
diff "^5.1.0"
|
||||
nise "^5.1.4"
|
||||
supports-color "^7.2.0"
|
||||
|
||||
slash@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
||||
@@ -9368,7 +9448,7 @@ supports-color@^5.3.0:
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
supports-color@^7.1.0, supports-color@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
@@ -9732,7 +9812,7 @@ type-check@~0.3.2:
|
||||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
|
||||
type-detect@^4.0.0, type-detect@^4.0.5:
|
||||
type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- [See an overview of BLS Wallet & how the components work together](./system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./use_bls_wallet_clients.md)
|
||||
- [Use BLS Wallet in your L2 dApp for cheaper, multi action transactions](./use_bls_wallet_dapp.md)
|
||||
- [Use BLS Wallet components and features with an ethers.js provider and signer](./use_bls_provider.md)
|
||||
- Setup the BLS Wallet components for:
|
||||
- [Local development](./local_development.md)
|
||||
- [Remote development](./remote_development.md)
|
||||
|
||||
224
docs/use_bls_provider.md
Normal file
224
docs/use_bls_provider.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# BLS Provider
|
||||
|
||||
This document will show you how to interact with the `BlsProvider` and `BlsSigner` classes.
|
||||
|
||||
The `BlsProvider` and `BlsSigner` are part of the `bls-wallet-clients` npm package, and help you interact with BLS Wallet components in a similar way you would use an Ethers provider and signer to interact with the Ethereum ecosystem. It offers developers a familiar develoment experience, while providing access to BLS Wallet components and features. Essentially it's a Ethers provider-shaped wrapper around `bls-wallet-clients`.
|
||||
|
||||
The `BlsProvider` and `BlsSigner` mimic the behaviour of an Ethers [JsonRpcProvider](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/) and [JsonRpcSigner](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/#JsonRpcSigner) respectively. In this implementation, note that the `BlsSigner` has knowledge of its own private key. For more information on Ethers providers and signers, visit the [Ethers v5 docs](https://docs.ethers.org/v5/).
|
||||
|
||||
The `BlsProvider` and `BlsSigner` are covered by over 100 test cases, including integration tests. If any functionality is not documented here, it will likely be documented by test cases.
|
||||
|
||||
# Creating instances
|
||||
|
||||
## BlsProvider
|
||||
|
||||
### Instantiating a BlsProvider
|
||||
|
||||
```ts
|
||||
import { Experimental } from "bls-wallet-clients";
|
||||
|
||||
const aggregatorUrl = "http://localhost:3000";
|
||||
const verificationGateway = "0x123";
|
||||
const aggregatorUtilities = "0x321";
|
||||
const rpcUrl = "http://localhost:8545";
|
||||
const network = {
|
||||
name: "localhost",
|
||||
chainId: 0x539, // 1337
|
||||
};
|
||||
|
||||
const provider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network
|
||||
);
|
||||
```
|
||||
|
||||
### BlsSigner
|
||||
|
||||
**Important:** Ensure that the BLS wallet you are linking the `BlsSigner` to via the private key is funded. Alternatively, if a wallet doesn't yet exist, it will be lazily created on the first transaction. In this scenario, you can create a random BLS private key with the following helper method and fund that account. It will need to be funded in order to send its first transaction.
|
||||
|
||||
```ts
|
||||
import { BlsWalletWrapper } from "bls-wallet-clients";
|
||||
|
||||
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
|
||||
const signer = provider.getSigner(privateKey);
|
||||
|
||||
// Send funds to this address if the wallet does not exist
|
||||
const address = await signer.getAddress();
|
||||
```
|
||||
|
||||
### Instantiating a BlsSigner
|
||||
|
||||
```ts
|
||||
// 32 random bytes
|
||||
const privateKey =
|
||||
"0x0001020304050607080910111213141516171819202122232425262728293031";
|
||||
|
||||
const signer = provider.getSigner(privateKey);
|
||||
```
|
||||
|
||||
# Send ETH
|
||||
|
||||
### Send ETH via BlsSigner
|
||||
|
||||
```ts
|
||||
const transactionResponse = await signer.sendTransaction({
|
||||
to: recipient,
|
||||
value: amountToTransfer,
|
||||
});
|
||||
const transactionReceipt = await transactionResponse.wait();
|
||||
```
|
||||
|
||||
### Send ETH via BlsProvider
|
||||
|
||||
```ts
|
||||
// Note the transaction must be signed via the BlsSigner first
|
||||
const signedTransaction = await signer.signTransaction({
|
||||
to: recipient,
|
||||
value: amountToTransfer,
|
||||
});
|
||||
|
||||
const transactionResponse = await provider.sendTransaction(signedTransaction);
|
||||
const transactionReceipt = await transactionResponse.wait();
|
||||
```
|
||||
|
||||
# Get a transaction receipt via a transaction hash
|
||||
|
||||
This will return a transaction receipt that corresponds to a transaction and transaction hash that can be quired on a block explorer.
|
||||
|
||||
```ts
|
||||
const transactionReceipt = await provider.getTransactionReceipt(
|
||||
transactionResponse.hash
|
||||
);
|
||||
```
|
||||
|
||||
# Multi-action transactions
|
||||
|
||||
### Send multi-action transactions with BlsSigner
|
||||
|
||||
```ts
|
||||
// using BlsSigner
|
||||
const transactionBatchResponse = await signer.sendTransactionBatch(
|
||||
{
|
||||
to: recipient1,
|
||||
value: amountToTransfer,
|
||||
},
|
||||
{
|
||||
to: recipient2,
|
||||
value: amountToTransfer,
|
||||
}
|
||||
);
|
||||
const transactionReceipt = await transactionBatchResponse.awaitBatchReceipt();
|
||||
```
|
||||
|
||||
### Send multi-action transactions with BlsProvider
|
||||
|
||||
```ts
|
||||
// Note the transaction must be signed via the BlsSigner first
|
||||
const signedTransactionBatch = await signer.signTransactionBatch(
|
||||
{
|
||||
to: recipient1,
|
||||
value: amountToTransfer,
|
||||
},
|
||||
{
|
||||
to: recipient2,
|
||||
value: amountToTransfer,
|
||||
}
|
||||
);
|
||||
|
||||
const transactionBatchResponse = await provider.sendTransactionBatch(
|
||||
signedTransactionBatch
|
||||
);
|
||||
const transactionReceipt = await transactionBatchResponse.awaitBatchReceipt();
|
||||
```
|
||||
|
||||
# Interacting with smart contracts
|
||||
|
||||
### Interacting with a deployed smart contract
|
||||
|
||||
```ts
|
||||
const ERC20 = new ethers.Contract(tokenAddress, tokenInterface, signer);
|
||||
|
||||
const transaction = await ERC20.transfer(recipient, amountToTransfer);
|
||||
await transaction.wait();
|
||||
```
|
||||
|
||||
### Interacting with a smart contract that hasn't been deployed
|
||||
|
||||
**Important:** You cannot deploy contracts with a BLS Wallet. Use a funded EOA for deploying
|
||||
contracts instead. Then make sure you connect to the contract instance with your `BlsSigner`.
|
||||
Contract deployments via a BLS Wallet is a feature tabled for our V2 contracts.
|
||||
|
||||
```ts
|
||||
// Deploying contracts must be done by a non-BLS Wallet account
|
||||
const nonBLSWalletAccount = new ethers.Wallet(fundedWalletPrivateKey, provider);
|
||||
|
||||
const ERC20 = await ethers.getContractFactory("ERC20");
|
||||
const erc20 = await ERC20.connect(nonBLSWalletAccount).deploy(
|
||||
tokenName,
|
||||
tokenSymbol,
|
||||
tokenSupply
|
||||
);
|
||||
await erc20.deployed();
|
||||
|
||||
const signer = provider.getSigner(privateKey);
|
||||
|
||||
const transaction = await erc20
|
||||
.connect(signer)
|
||||
.transfer(recipient, amountToTransfer);
|
||||
await transaction.wait();
|
||||
```
|
||||
|
||||
### Multi-action contract interactions
|
||||
|
||||
```ts
|
||||
// Working example of this setup can be found in BlsSignerContractInteraction.test.ts
|
||||
const transactionBatch = {
|
||||
transactions: [
|
||||
{
|
||||
to: ERC20.address,
|
||||
value: 0,
|
||||
data: ERC20.interface.encodeFunctionData("approve", [
|
||||
spender,
|
||||
amountToTransfer,
|
||||
]),
|
||||
},
|
||||
{
|
||||
to: spender,
|
||||
value: 0,
|
||||
data: mockTokenSpender.interface.encodeFunctionData(
|
||||
"TransferERC20ToSelf",
|
||||
[ERC20.address, amountToTransfer]
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await signer.sendTransactionBatch(transactionBatch);
|
||||
await result.awaitBatchReceipt();
|
||||
```
|
||||
|
||||
# Estimating gas for a transaction
|
||||
|
||||
The `BlsProvider` adds a small safety premium to the gas estimate to improve the likelyhood a bundle gets included during aggregation. If you'd like more fined grained control over the fee, you use the [helper method](../contracts//clients/README.md#estimating-and-paying-fees) from the `bls-wallet-clients` package directly instead.
|
||||
|
||||
### Estimating gas with BlsProvider
|
||||
|
||||
```ts
|
||||
const fee = await provider.estimateGas({
|
||||
to: recipient,
|
||||
value: amountToTransfer,
|
||||
});
|
||||
```
|
||||
|
||||
### Estimating gas when interacting with a contract
|
||||
|
||||
```ts
|
||||
const feeEstimate = await ERC20.estimateGas.transfer(
|
||||
recipient,
|
||||
amountToTransfer
|
||||
);
|
||||
```
|
||||
@@ -36,7 +36,7 @@
|
||||
"advanced-css-reset": "^1.2.2",
|
||||
"async-mutex": "^0.3.2",
|
||||
"axios": "^0.27.2",
|
||||
"bls-wallet-clients": "0.8.2-1452ef5",
|
||||
"bls-wallet-clients": "0.8.2-1fb4a55",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"bs58check": "^2.1.2",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
|
||||
@@ -2907,10 +2907,10 @@ blakejs@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
|
||||
integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
|
||||
|
||||
bls-wallet-clients@0.8.2-1452ef5:
|
||||
version "0.8.2-1452ef5"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
|
||||
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
|
||||
bls-wallet-clients@0.8.2-1fb4a55:
|
||||
version "0.8.2-1fb4a55"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
|
||||
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
Reference in New Issue
Block a user