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

This commit is contained in:
jacque006
2023-04-03 10:41:42 -06:00
21 changed files with 432 additions and 65 deletions

View File

@@ -73,6 +73,8 @@ jobs:
run: yarn start &
- working-directory: ./contracts
run: ./scripts/wait-for-rpc.sh
- working-directory: ./contracts
run: ./scripts/wait-for-contract-deploy.sh
- run: cp .env.local.example .env
- run: deno test --allow-net --allow-env --allow-read

View File

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

View File

@@ -88,4 +88,5 @@ type AppEvent =
};
};
export default AppEvent;

View File

@@ -2,7 +2,6 @@ import { Router } from "../../deps.ts";
import failRequest from "./helpers/failRequest.ts";
import BundleHandler from "./helpers/BundleHandler.ts";
import nil from "../helpers/nil.ts";
import BundleService from "./BundleService.ts";
export default function BundleRouter(bundleService: BundleService) {

View File

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

View File

@@ -0,0 +1,16 @@
import { Router } from "../../deps.ts";
import HealthService from "./HealthService.ts";
export default function HealthRouter(healthService: HealthService) {
const router = new Router({ prefix: "/" });
router.get(
"health",
async (ctx) => {
const healthResults = await healthService.getHealth();
console.log(`Status: ${healthResults.status}\n`);
ctx.response.status = healthResults.status == 'healthy' ? 200 : 503;
ctx.response.body = { status: healthResults.status };
});
return router;
}

View File

@@ -0,0 +1,11 @@
export type ResourceHealth = 'healthy' | 'unhealthy';
type HealthCheckResult = {
status: ResourceHealth,
};
export default class HealthService {
getHealth(): Promise<HealthCheckResult> {
return Promise.resolve({ status: 'healthy' });
}
}

View File

@@ -15,6 +15,8 @@ import AppEvent from "./AppEvent.ts";
import BundleTable from "./BundleTable.ts";
import AggregationStrategy from "./AggregationStrategy.ts";
import AggregationStrategyRouter from "./AggregationStrategyRouter.ts";
import HealthService from "./HealthService.ts";
import HealthRouter from "./HealthRouter.ts";
export default async function app(emit: (evt: AppEvent) => void) {
const { addresses } = await getNetworkConfig();
@@ -64,10 +66,13 @@ export default async function app(emit: (evt: AppEvent) => void) {
bundleTable,
);
const healthService = new HealthService();
const routers = [
BundleRouter(bundleService),
AdminRouter(adminService),
AggregationStrategyRouter(aggregationStrategy),
HealthRouter(healthService),
];
const app = new Application();

View File

@@ -0,0 +1,10 @@
import { assertEquals } from "./deps.ts";
import Fixture from "./helpers/Fixture.ts";
Fixture.test("HealthService returns healthy", async (fx) => {
const healthCheckService = fx.createHealthCheckService()
const healthStatus = await healthCheckService.getHealth();
const expected = {"status":"healthy"};
assertEquals(JSON.stringify(healthStatus), JSON.stringify(expected));
});

View File

@@ -25,6 +25,7 @@ import BundleTable, { BundleRow } from "../../src/app/BundleTable.ts";
import AggregationStrategy, {
AggregationStrategyConfig,
} from "../../src/app/AggregationStrategy.ts";
import HealthService from "../../src/app/HealthService.ts";
// deno-lint-ignore no-explicit-any
type ExplicitAny = any;
@@ -292,6 +293,12 @@ export default class Fixture {
return wallets;
}
createHealthCheckService() {
const healthCheckService = new HealthService();
return healthCheckService;
}
async cleanup() {
for (const job of this.cleanupJobs) {

View File

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

View File

@@ -6,6 +6,9 @@ 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";
@@ -18,6 +21,7 @@ import {
} 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>;
@@ -28,6 +32,13 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
readonly verificationGatewayAddress: string;
readonly aggregatorUtilitiesAddress: string;
/**
* @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,
@@ -41,6 +52,10 @@ 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> {
@@ -90,6 +105,12 @@ 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> {
@@ -121,6 +142,10 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
);
}
/**
* @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> {
@@ -149,6 +174,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
);
}
/**
* @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,
@@ -156,6 +186,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
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,
@@ -163,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> {
@@ -170,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,
@@ -182,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?:

View File

@@ -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,6 +64,12 @@ 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,
@@ -92,6 +104,7 @@ export default class BlsSigner extends Signer {
}
}
/** 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(
@@ -101,6 +114,25 @@ export default class BlsSigner extends Signer {
);
}
/**
* @returns a random BLS private key
*/
static async getRandomBlsPrivateKey(): Promise<string> {
return await BlsWalletWrapper.getRandomBlsPrivateKey();
}
/**
* 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> {
@@ -148,6 +180,10 @@ export default class BlsSigner extends Signer {
);
}
/**
* @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> {
@@ -225,6 +261,9 @@ export default class BlsSigner extends Signer {
);
}
/**
* @returns The address associated with the BlsSigner
*/
async getAddress(): Promise<string> {
await this.initPromise;
if (this._address) {
@@ -251,6 +290,12 @@ 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> {
@@ -285,6 +330,12 @@ 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> {
@@ -342,8 +393,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)) {
@@ -366,6 +417,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,
@@ -379,6 +433,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> {
@@ -455,6 +513,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> {

View File

@@ -2,7 +2,7 @@ import { expect } from "chai";
import { ethers } from "ethers";
import { parseEther } from "ethers/lib/utils";
import { Experimental, BlsWalletWrapper } from "../src";
import { Experimental } from "../src";
import BlsSigner, { UncheckedBlsSigner } from "../src/BlsSigner";
let aggregatorUrl: string;
@@ -28,7 +28,7 @@ describe("BlsProvider", () => {
chainId: 0x539, // 1337
};
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
@@ -72,7 +72,7 @@ describe("BlsProvider", () => {
);
// Act
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const newPrivateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);

View File

@@ -1,7 +1,7 @@
import { expect } from "chai";
import { ethers } from "ethers";
import { Experimental, BlsWalletWrapper } from "../src";
import { Experimental } from "../src";
import { UncheckedBlsSigner } from "../src/BlsSigner";
let aggregatorUrl: string;
@@ -25,7 +25,7 @@ describe("BlsSigner", () => {
chainId: 0x7a69,
};
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,

View File

@@ -38,7 +38,7 @@ describe("BlsProvider", () => {
chainId: 0x539, // 1337
};
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,

View File

@@ -2,7 +2,7 @@ import { expect } from "chai";
import { ethers } from "hardhat";
import { BigNumber, utils, Wallet } from "ethers";
import { Experimental, BlsWalletWrapper } from "../clients/src";
import { Experimental } from "../clients/src";
import getNetworkConfig from "../shared/helpers/getNetworkConfig";
describe("Provider tests", function () {
@@ -12,7 +12,7 @@ describe("Provider tests", function () {
this.beforeAll(async () => {
const networkConfig = await getNetworkConfig("local");
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
const aggregatorUrl = "http://localhost:3000";
const verificationGateway = networkConfig.addresses.verificationGateway;
const aggregatorUtilities = networkConfig.addresses.utilities;

View File

@@ -48,7 +48,7 @@ describe("BlsSigner", () => {
chainId: 0x539, // 1337
};
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
@@ -802,7 +802,7 @@ describe("BlsSigner", () => {
it("should await the init promise when connecting to an unchecked bls signer", async () => {
// Arrange
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const newPrivateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
const newBlsSigner = blsProvider.getSigner(newPrivateKey);
const uncheckedBlsSigner = newBlsSigner.connectUnchecked();
@@ -837,7 +837,7 @@ describe("BlsSigner", () => {
rpcUrl,
network,
);
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const newPrivateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
const uncheckedBlsSigner = newBlsSigner.connectUnchecked();

View File

@@ -22,7 +22,7 @@ async function getRandomSigners(
const signers = [];
for (let i = 0; i < numSigners; i++) {
const privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const privateKey = await Experimental.BlsSigner.getRandomBlsPrivateKey();
const blsProvider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,

View File

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

224
docs/use_bls_provider.md Normal file
View File

@@ -0,0 +1,224 @@
# BLS Provider
This document will show you how to interact with the `BlsProvider` and `BlsSigner` classes.
The `BlsProvider` and `BlsSigner` are part of the `bls-wallet-clients` npm package, and help you interact with BLS Wallet components in a similar way you would use an Ethers provider and signer to interact with the Ethereum ecosystem. It offers developers a familiar develoment experience, while providing access to BLS Wallet components and features. Essentially it's a Ethers provider-shaped wrapper around `bls-wallet-clients`.
The `BlsProvider` and `BlsSigner` mimic the behaviour of an Ethers [JsonRpcProvider](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/) and [JsonRpcSigner](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/#JsonRpcSigner) respectively. In this implementation, note that the `BlsSigner` has knowledge of its own private key. For more information on Ethers providers and signers, visit the [Ethers v5 docs](https://docs.ethers.org/v5/).
The `BlsProvider` and `BlsSigner` are covered by over 100 test cases, including integration tests. If any functionality is not documented here, it will likely be documented by test cases.
# Creating instances
## BlsProvider
### Instantiating a BlsProvider
```ts
import { Experimental } from "bls-wallet-clients";
const aggregatorUrl = "http://localhost:3000";
const verificationGateway = "0x123";
const aggregatorUtilities = "0x321";
const rpcUrl = "http://localhost:8545";
const network = {
name: "localhost",
chainId: 0x539, // 1337
};
const provider = new Experimental.BlsProvider(
aggregatorUrl,
verificationGateway,
aggregatorUtilities,
rpcUrl,
network
);
```
### BlsSigner
**Important:** Ensure that the BLS wallet you are linking the `BlsSigner` to via the private key is funded. Alternatively, if a wallet doesn't yet exist, it will be lazily created on the first transaction. In this scenario, you can create a random BLS private key with the following helper method and fund that account. It will need to be funded in order to send its first transaction.
```ts
import { Experimental } from "bls-wallet-clients";
const privateKey = await Experimental.BlsSigner.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
);
```