mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-10 08:07:54 -05:00
Compare commits
253 Commits
private-ke
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6f844b822 | ||
|
|
d7f1917ed7 | ||
|
|
8edaab3a71 | ||
|
|
c5d83f4650 | ||
|
|
ddb8f72864 | ||
|
|
32c9c6c7ed | ||
|
|
48e472389d | ||
|
|
72c7b7c3c5 | ||
|
|
9c5c3ba950 | ||
|
|
d7369af0cf | ||
|
|
fc5a668ed6 | ||
|
|
40a3335a38 | ||
|
|
9b2ab4bb48 | ||
|
|
a609cb1f8e | ||
|
|
553d9ba81d | ||
|
|
ddbe420f53 | ||
|
|
fbab084b52 | ||
|
|
fff8dd397f | ||
|
|
926637130a | ||
|
|
0047f14a0f | ||
|
|
0d52ddb20f | ||
|
|
ffd7037573 | ||
|
|
daa51bce52 | ||
|
|
69fa24daff | ||
|
|
01a908e26f | ||
|
|
8c91d2faa3 | ||
|
|
b2ab3b766d | ||
|
|
c2afe790cf | ||
|
|
c736179e75 | ||
|
|
dc488ecc00 | ||
|
|
3032cf0917 | ||
|
|
e29c18c63f | ||
|
|
e42ccf2f1f | ||
|
|
ef8f2c7a13 | ||
|
|
0da9350fd6 | ||
|
|
5cacd017d1 | ||
|
|
02c8565e98 | ||
|
|
52d49e16d8 | ||
|
|
b84aebfaa9 | ||
|
|
bdca7833d3 | ||
|
|
56fa5b6e82 | ||
|
|
2cf961ff13 | ||
|
|
9e8269b841 | ||
|
|
1139b4e4f1 | ||
|
|
f5026347e1 | ||
|
|
638e1a1fb2 | ||
|
|
fb5537a118 | ||
|
|
4eeb9afb25 | ||
|
|
1e86c15892 | ||
|
|
f8a8c490e8 | ||
|
|
28f3983fb2 | ||
|
|
b653eaf5ab | ||
|
|
84a89cb5b6 | ||
|
|
9922c3a79c | ||
|
|
f4fdf7148d | ||
|
|
51f4a9588a | ||
|
|
0b1f037ba7 | ||
|
|
000126d226 | ||
|
|
d401e745cf | ||
|
|
21e0532fc0 | ||
|
|
6d1f96ab2e | ||
|
|
008dcf0b09 | ||
|
|
1eed20fa6e | ||
|
|
126d82266b | ||
|
|
ff1f253012 | ||
|
|
c34db600cd | ||
|
|
a37ae25990 | ||
|
|
b48dc20db4 | ||
|
|
4d20166d6f | ||
|
|
7a63b3aa4d | ||
|
|
bc0d57007f | ||
|
|
28226f71e5 | ||
|
|
5ad1314efc | ||
|
|
10c7d54d12 | ||
|
|
0cdc6430e4 | ||
|
|
e1906cdbb7 | ||
|
|
a3b4877c11 | ||
|
|
b7ac4fd77c | ||
|
|
c88b05d0ec | ||
|
|
f6ab5d93ed | ||
|
|
c07dc63896 | ||
|
|
70ca00f089 | ||
|
|
6156b86b22 | ||
|
|
639f6133bf | ||
|
|
1ec0330adb | ||
|
|
cb5932776c | ||
|
|
115907c74f | ||
|
|
5529e078d9 | ||
|
|
4fd593ac1d | ||
|
|
ff27bd0469 | ||
|
|
cac4669cb9 | ||
|
|
5fe9170c3e | ||
|
|
ad9350eb68 | ||
|
|
513df2229e | ||
|
|
f08fa1e9ff | ||
|
|
aa8cd1d681 | ||
|
|
e6326835bb | ||
|
|
cb216fa7d7 | ||
|
|
7978ed0690 | ||
|
|
d86cf09716 | ||
|
|
dc6ebc24d6 | ||
|
|
7b07df3aba | ||
|
|
2aa7e352f5 | ||
|
|
de12b3c62f | ||
|
|
e1248c6b63 | ||
|
|
b988fbc92c | ||
|
|
96bfb32e5b | ||
|
|
7435d9976e | ||
|
|
2dae355817 | ||
|
|
e734209df0 | ||
|
|
4c9c0ed898 | ||
|
|
fcbfd7cc62 | ||
|
|
13f34dd02d | ||
|
|
b565c33193 | ||
|
|
c1404502f3 | ||
|
|
74f9d9020c | ||
|
|
d2c6cff629 | ||
|
|
25469d50e4 | ||
|
|
548301d32d | ||
|
|
4da348d9e2 | ||
|
|
51d7681626 | ||
|
|
7c3fbd4d40 | ||
|
|
ac7cd956a8 | ||
|
|
5423f65503 | ||
|
|
fdf80fc5fb | ||
|
|
3c0f36f444 | ||
|
|
32c6b13e7d | ||
|
|
e3bbd393d8 | ||
|
|
b0ba263eb4 | ||
|
|
b04281cdac | ||
|
|
e196a101ff | ||
|
|
995ec24d1f | ||
|
|
96c61e9932 | ||
|
|
8b76734316 | ||
|
|
519e6f88c4 | ||
|
|
7f803bd10a | ||
|
|
1e71ef3c78 | ||
|
|
708a6d0c2d | ||
|
|
42ea205f79 | ||
|
|
52974e01f6 | ||
|
|
13837e6729 | ||
|
|
7a911a3634 | ||
|
|
4ac580d1df | ||
|
|
30b16185fb | ||
|
|
a9e511c3f0 | ||
|
|
1468c91dc0 | ||
|
|
5ce0943f00 | ||
|
|
8ccad86fdf | ||
|
|
bc0c272d65 | ||
|
|
3a0a1e5f41 | ||
|
|
6de210b81b | ||
|
|
7239d51a57 | ||
|
|
5ce1723ed5 | ||
|
|
547bb7f34d | ||
|
|
059b1d4b1a | ||
|
|
a01d7426ea | ||
|
|
80d551d14e | ||
|
|
e2d8399dbf | ||
|
|
7522fb655f | ||
|
|
fa489ab75f | ||
|
|
41ac18fb86 | ||
|
|
c390ac0321 | ||
|
|
2adb7ace4c | ||
|
|
f90e79d306 | ||
|
|
f3b5552fb2 | ||
|
|
4130335f5a | ||
|
|
43cf586131 | ||
|
|
f4c2e21c9b | ||
|
|
f8223693fa | ||
|
|
e35b160e87 | ||
|
|
cd570155da | ||
|
|
7a9f26d218 | ||
|
|
13779ffe3b | ||
|
|
699f84cede | ||
|
|
e858c3c826 | ||
|
|
4052997b15 | ||
|
|
c260e60443 | ||
|
|
480ee1d3d0 | ||
|
|
25c73d6697 | ||
|
|
1052cc6c28 | ||
|
|
af420d9a7b | ||
|
|
fde1c61e93 | ||
|
|
8bc62681f6 | ||
|
|
bac557a113 | ||
|
|
1464476cec | ||
|
|
306b90e83d | ||
|
|
d7348e581d | ||
|
|
5b529facd1 | ||
|
|
4d7a83705b | ||
|
|
8e9cd18934 | ||
|
|
87eb27053a | ||
|
|
dd1a19b8d8 | ||
|
|
a9bc94c641 | ||
|
|
e416c13635 | ||
|
|
2b57673de4 | ||
|
|
edae8c17f0 | ||
|
|
e1e654eac5 | ||
|
|
9202cee106 | ||
|
|
1d887e696e | ||
|
|
51e6666776 | ||
|
|
74d31d561f | ||
|
|
a666a2b9c9 | ||
|
|
d2da8d8e7e | ||
|
|
1018131c27 | ||
|
|
e1a306f9f2 | ||
|
|
847bbddd25 | ||
|
|
a4aaa42964 | ||
|
|
07da3694f2 | ||
|
|
a968e86a20 | ||
|
|
c5e3461d60 | ||
|
|
81c3d1c965 | ||
|
|
30ddc6f8ab | ||
|
|
0ca6f4f8fa | ||
|
|
365084ed86 | ||
|
|
2daff8b542 | ||
|
|
027f899fe3 | ||
|
|
9f76ceb9f8 | ||
|
|
a348962e42 | ||
|
|
3894fe68a2 | ||
|
|
8972ccbc5c | ||
|
|
4b42d2d960 | ||
|
|
16a043279b | ||
|
|
59473044a0 | ||
|
|
a800935f51 | ||
|
|
a54b677720 | ||
|
|
2d6ee184a6 | ||
|
|
0b2cca4816 | ||
|
|
e4cde3496d | ||
|
|
29d8884754 | ||
|
|
28ebbe62f4 | ||
|
|
b4b757ce8b | ||
|
|
470ac78044 | ||
|
|
cf16d8f833 | ||
|
|
1e666325ba | ||
|
|
78646e8d11 | ||
|
|
ae7886aba3 | ||
|
|
86fc7c5895 | ||
|
|
0afefe9c1e | ||
|
|
71a3520a0b | ||
|
|
b97c8b5e6d | ||
|
|
73967cef4e | ||
|
|
3506fb6117 | ||
|
|
5918b12a75 | ||
|
|
fef73de225 | ||
|
|
321a2556b1 | ||
|
|
03d61443e8 | ||
|
|
77d9ff8823 | ||
|
|
f0cfe0d941 | ||
|
|
711f898d96 | ||
|
|
7def3c9b2e | ||
|
|
139d8e04e4 | ||
|
|
59388de662 | ||
|
|
1928d59c58 |
@@ -8,10 +8,6 @@ runs:
|
||||
shell: bash
|
||||
run: yarn hardhat node &
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat fundDeployer --network gethDev
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
|
||||
2
.github/workflows/aggregator.yml
vendored
2
.github/workflows/aggregator.yml
vendored
@@ -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
|
||||
|
||||
@@ -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.9.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -9,6 +9,7 @@ const BundleDto = io.type({
|
||||
),
|
||||
operations: io.array(io.type({
|
||||
nonce: io.string,
|
||||
gas: io.string,
|
||||
actions: io.array(io.type({
|
||||
ethValue: io.string,
|
||||
contractAddress: io.string,
|
||||
|
||||
@@ -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.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0.tgz#edfbdb24011856b52d9b438af174b6acbeda27ec"
|
||||
integrity sha512-ebEifAPkGfTft6xdVVgQfC6HEXzgw+wX2d76w2K1OUsB4FeKiAYRLMXtnKtl7tdQoMknHElD6xrLChKaCACYLQ==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
@@ -41,6 +41,7 @@ export const keccak256 = ethers.utils.keccak256;
|
||||
export const shuffled: <T>(array: T[]) => T[] = ethers.utils.shuffled;
|
||||
|
||||
export type {
|
||||
ActionData,
|
||||
AggregatorUtilities,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
@@ -53,21 +54,26 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletWrapper,
|
||||
BundleCompressor,
|
||||
ContractsConnector,
|
||||
decodeError,
|
||||
ERC20__factory,
|
||||
Erc20Compressor,
|
||||
ERC20Factory,
|
||||
FallbackCompressor,
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
MockERC20Factory,
|
||||
VerificationGatewayFactory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
|
||||
// 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.9.0";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const client = new AggregatorClient(env.ORIGIN);
|
||||
const fx = await Fixture.create(import.meta.url);
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -17,6 +17,7 @@ const bundle: Bundle = {
|
||||
senderPublicKeys: [[dummyHex(32), dummyHex(32), dummyHex(32), dummyHex(32)]],
|
||||
operations: [{
|
||||
nonce: BigNumber.from(0),
|
||||
gas: BigNumber.from(0),
|
||||
actions: [{
|
||||
ethValue: BigNumber.from(0),
|
||||
contractAddress: dummyHex(20),
|
||||
|
||||
@@ -19,7 +19,7 @@ await (await adminWallet.sendTransaction({
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 1,
|
||||
|
||||
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ethers } from "../../deps.ts";
|
||||
|
||||
export default async function receiptOf(
|
||||
responsePromise: Promise<ethers.providers.TransactionResponse>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
const response = await responsePromise;
|
||||
const receipt = await response.wait();
|
||||
|
||||
return receipt;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -22,7 +22,7 @@ const walletIndex = Number(walletIndexStr);
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const wallet = await TestBlsWallet(provider, walletIndex);
|
||||
@@ -48,21 +48,23 @@ const mintAction: ActionData = {
|
||||
),
|
||||
};
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
console.log({ feeEstimation });
|
||||
|
||||
@@ -91,7 +93,7 @@ const feeAction: ActionData = {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [mintAction, feeAction],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { delay, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { delay, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -14,16 +14,14 @@ const ethereumService = await EthereumService.create(
|
||||
(evt) => {
|
||||
console.log(evt);
|
||||
},
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
const startBalance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -29,10 +29,10 @@ if (!Number.isFinite(walletN)) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
@@ -67,17 +67,19 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
assert(feeEstimation.feeType === "ether");
|
||||
|
||||
@@ -104,10 +106,12 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
bundles.push(wallet.sign({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}));
|
||||
bundles.push(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Sending mint bundles to aggregator");
|
||||
|
||||
34
aggregator/manualTests/registerWallet.ts
Executable file
34
aggregator/manualTests/registerWallet.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ContractsConnector, ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import receiptOf from "./helpers/receiptOf.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const connector = await ContractsConnector.create(adminWallet);
|
||||
|
||||
const addressRegistry = await connector.AddressRegistry();
|
||||
const blsPublicKeyRegistry = await connector.BLSPublicKeyRegistry();
|
||||
|
||||
await receiptOf(
|
||||
addressRegistry.register("0xCB1ca1e8DF1055636d7D07c3099c9de3c65CAAB4"),
|
||||
);
|
||||
|
||||
await receiptOf(
|
||||
blsPublicKeyRegistry.register(
|
||||
// You can get this in Quill by running this in the console of the wallet
|
||||
// page (the page you get by clicking on the extension icon)
|
||||
// JSON.stringify(debug.wallets[0].blsWalletSigner.getPublicKey())
|
||||
|
||||
[
|
||||
"0x0ad7e63a4bbfdad440beda1fe7fdfb77a59f2a6d991700c6cf4c3654a52389a9",
|
||||
"0x0adaa93bdfda0f6b259a80c1af7ccf3451c35c1e175483927a8052bdbf59f801",
|
||||
"0x1f56aa1bb1419c741f0a474e51f33da0ffc81ea870e2e2c440db72539a9efb9e",
|
||||
"0x2f1f7e5d586d6ca5de3c8c198c3be3b998a2b6df7ee8a367a1e58f8b36fd524d",
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -43,7 +43,7 @@ const { addresses } = await getNetworkConfig();
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
@@ -56,7 +56,7 @@ const [recvWallet, ...sendWallets] = await Promise.all(
|
||||
log("Checking/minting test tokens...");
|
||||
|
||||
for (const wallet of sendWallets) {
|
||||
const testErc20 = MockERC20__factory.connect(
|
||||
const testErc20 = MockERC20Factory.connect(
|
||||
addresses.testToken,
|
||||
adminWallet,
|
||||
);
|
||||
@@ -90,7 +90,7 @@ let txsAdded = 0;
|
||||
let txsCompleted = 0;
|
||||
let sendWalletIndex = 0;
|
||||
|
||||
pollingLoop(() => {
|
||||
pollingLoop(async () => {
|
||||
// Send transactions
|
||||
|
||||
const lead = txsSent - txsCompleted;
|
||||
@@ -102,7 +102,7 @@ pollingLoop(() => {
|
||||
const nonce = nextNonceMap.get(sendWallet)!;
|
||||
nextNonceMap.set(sendWallet, nonce.add(1));
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { AggregatorClient, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
// import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -15,15 +15,13 @@ const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
// (evt) => {
|
||||
// console.log(evt);
|
||||
// },
|
||||
// addresses.verificationGateway,
|
||||
// addresses.utilities,
|
||||
// env.PRIVATE_KEY_AGG,
|
||||
// );
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
VerificationGateway__factory,
|
||||
VerificationGatewayFactory,
|
||||
Wallet,
|
||||
} from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
@@ -14,7 +14,7 @@ const wallet = new Wallet(env.PRIVATE_KEY_AGG, provider);
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const vg = VerificationGateway__factory.connect(
|
||||
const vg = VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
wallet,
|
||||
);
|
||||
@@ -32,10 +32,12 @@ const nonce = await internalBlsWallet.Nonce();
|
||||
if (!nonce.eq(0)) {
|
||||
console.log("Already exists with nonce", nonce.toNumber());
|
||||
} else {
|
||||
await (await vg.processBundle(internalBlsWallet.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await vg.processBundle(
|
||||
await internalBlsWallet.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
|
||||
console.log("Created successfully");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export async function run(...cmd: string[]): Promise<void> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "inherit", stderr: "inherit" });
|
||||
|
||||
const unloadListener = () => {
|
||||
@@ -20,6 +22,8 @@ export async function run(...cmd: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
export async function String(...cmd: string[]): Promise<string> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "piped" });
|
||||
|
||||
if (process.stdout === null) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Bundle,
|
||||
decodeError,
|
||||
ERC20,
|
||||
ERC20__factory,
|
||||
ERC20Factory,
|
||||
ethers,
|
||||
OperationResultError,
|
||||
Semaphore,
|
||||
@@ -70,6 +70,7 @@ export type AggregationStrategyResult = {
|
||||
aggregateBundle: Bundle | nil;
|
||||
includedRows: BundleRow[];
|
||||
bundleOverheadCost: BigNumber;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: BigNumber;
|
||||
expectedMaxCost: BigNumber;
|
||||
failedRows: BundleRow[];
|
||||
@@ -105,7 +106,8 @@ export default class AggregationStrategy {
|
||||
async run(eligibleRows: BundleRow[]): Promise<AggregationStrategyResult> {
|
||||
eligibleRows = await this.#filterRows(eligibleRows);
|
||||
|
||||
const bundleOverheadGas = await this.measureBundleOverheadGas();
|
||||
const { bundleOverheadGas, bundleOverheadLen } = await this
|
||||
.measureBundleOverhead();
|
||||
|
||||
let aggregateBundle = this.blsWalletSigner.aggregate([]);
|
||||
let aggregateGas = bundleOverheadGas;
|
||||
@@ -147,6 +149,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -164,6 +167,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -217,6 +221,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -256,6 +261,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -410,13 +416,21 @@ export default class AggregationStrategy {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAggregateGas = (aggregateGas
|
||||
.add(gasEstimate)
|
||||
.sub(bundleOverheadGas));
|
||||
const bundleEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
const newAggregateGas = aggregateGas.add(bundleEstimate);
|
||||
|
||||
if (newAggregateGas.gt(this.config.maxGasPerBundle)) {
|
||||
// Bundle would cause us to exceed maxGasPerBundle, so don't include it,
|
||||
// but also don't mark it as failed.
|
||||
this.emit({
|
||||
type: "aggregate-bundle-exceeds-max-gas",
|
||||
data: {
|
||||
hash: row.hash,
|
||||
gasEstimate: bundleEstimate.toNumber(),
|
||||
aggregateGasEstimate: newAggregateGas.toNumber(),
|
||||
maxGasPerBundle: this.config.maxGasPerBundle,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -451,14 +465,16 @@ export default class AggregationStrategy {
|
||||
.callStaticSequenceWithMeasure(
|
||||
feeToken
|
||||
? es.Call(feeToken, "balanceOf", [es.wallet.address])
|
||||
: es.Call(es.utilities, "ethBalanceOf", [es.wallet.address]),
|
||||
bundles.map((bundle) =>
|
||||
: es.Call(es.aggregatorUtilities, "ethBalanceOf", [
|
||||
es.wallet.address,
|
||||
]),
|
||||
await Promise.all(bundles.map(async (bundle) =>
|
||||
es.Call(
|
||||
es.verificationGateway,
|
||||
"processBundle",
|
||||
[bundle],
|
||||
es.bundleCompressor.blsExpanderDelegator,
|
||||
"run",
|
||||
[await es.bundleCompressor.compress(bundle)],
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
return Range(bundles.length).map((i) => {
|
||||
@@ -514,7 +530,7 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ERC20__factory.connect(
|
||||
return ERC20Factory.connect(
|
||||
this.config.fees.address,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -525,10 +541,12 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bundleOverheadGas ??= await this.measureBundleOverheadGas();
|
||||
bundleOverheadGas ??=
|
||||
(await this.measureBundleOverhead()).bundleOverheadGas;
|
||||
|
||||
const gasEstimate = await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
const gasEstimate = await this.ethereumService.estimateCompressedGas(
|
||||
bundle,
|
||||
);
|
||||
|
||||
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
|
||||
@@ -617,8 +635,7 @@ export default class AggregationStrategy {
|
||||
}
|
||||
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
await this.ethereumService.estimateCompressedGas(bundle);
|
||||
|
||||
return {
|
||||
success,
|
||||
@@ -631,7 +648,7 @@ export default class AggregationStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
async measureBundleOverheadGas() {
|
||||
async measureBundleOverhead() {
|
||||
// The simple way to do this would be to estimate the gas of an empty
|
||||
// bundle. However, an empty bundle is a bit of a special case, in
|
||||
// particular the on-chain BLS library outright refuses to validate it. So
|
||||
@@ -647,19 +664,50 @@ export default class AggregationStrategy {
|
||||
// wallet creation would be included in the bundle overhead.
|
||||
assert(nonce.gt(0));
|
||||
|
||||
const bundle1 = wallet.sign({ nonce, actions: [] });
|
||||
const bundle2 = wallet.sign({ nonce: nonce.add(1), actions: [] });
|
||||
const bundle1 = wallet.sign({ nonce, gas: 1_000_000, actions: [] });
|
||||
|
||||
const bundle2 = wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
|
||||
es.verificationGateway.estimateGas.processBundle(bundle1),
|
||||
es.verificationGateway.estimateGas.processBundle(
|
||||
es.estimateCompressedGas(bundle1),
|
||||
es.estimateCompressedGas(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
|
||||
|
||||
return oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
const bundleOverheadGas = oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
|
||||
const [compressedBundle1, compressedBundle12] = await Promise.all([
|
||||
es.bundleCompressor.compress(bundle1),
|
||||
es.bundleCompressor.compress(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const [oneOpLen, twoOpLen] = await Promise.all([
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle1,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle12,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
]);
|
||||
|
||||
const opMarginalLen = twoOpLen - oneOpLen;
|
||||
const bundleOverheadLen = oneOpLen - opMarginalLen;
|
||||
|
||||
return {
|
||||
bundleOverheadGas,
|
||||
bundleOverheadLen,
|
||||
};
|
||||
}
|
||||
|
||||
async #TokenDecimals(): Promise<number> {
|
||||
|
||||
@@ -15,6 +15,7 @@ type AppEvent =
|
||||
data: {
|
||||
includedRows: number;
|
||||
bundleOverheadCost: string;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: string;
|
||||
expectedMaxCost: string;
|
||||
};
|
||||
@@ -32,10 +33,24 @@ type AppEvent =
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "aggregate-bundle-exceeds-max-gas";
|
||||
data: {
|
||||
hash: string;
|
||||
gasEstimate: number;
|
||||
aggregateGasEstimate: number;
|
||||
maxGasPerBundle: number;
|
||||
};
|
||||
}
|
||||
| { type: "unprofitable-despite-breakeven-operations" }
|
||||
| {
|
||||
type: "submission-attempt";
|
||||
data: { publicKeyShorts: string[]; attemptNumber: number };
|
||||
data: {
|
||||
publicKeyShorts: string[];
|
||||
attemptNumber: number;
|
||||
txLen: number;
|
||||
compressedTxLen: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "submission-attempt-failed";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -257,6 +257,7 @@ export default class BundleService {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost,
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -268,6 +269,7 @@ export default class BundleService {
|
||||
data: {
|
||||
includedRows: includedRows.length,
|
||||
bundleOverheadCost: ethers.utils.formatEther(bundleOverheadCost),
|
||||
bundleOverheadLen,
|
||||
expectedFee: ethers.utils.formatEther(expectedFee),
|
||||
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import {
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory,
|
||||
BaseContract,
|
||||
BigNumber,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
BundleCompressor,
|
||||
BytesLike,
|
||||
ContractsConnector,
|
||||
delay,
|
||||
Erc20Compressor,
|
||||
ethers,
|
||||
FallbackCompressor,
|
||||
initBlsWalletSigner,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
Wallet,
|
||||
} from "../../deps.ts";
|
||||
|
||||
@@ -64,30 +67,23 @@ type DecodeReturnType<
|
||||
Method extends keyof Contract["callStatic"],
|
||||
> = EnforceArray<AsyncReturnType<Contract["callStatic"][Method]>>;
|
||||
|
||||
export default class EthereumService {
|
||||
verificationGateway: VerificationGateway;
|
||||
utilities: AggregatorUtilities;
|
||||
type ExpanderEntryPoint = AsyncReturnType<
|
||||
ContractsConnector["ExpanderEntryPoint"]
|
||||
>;
|
||||
|
||||
export default class EthereumService {
|
||||
constructor(
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public wallet: Wallet,
|
||||
public provider: ethers.providers.Provider,
|
||||
public blsWalletWrapper: BlsWalletWrapper,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
public verificationGateway: VerificationGateway,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
public expanderEntryPoint: ExpanderEntryPoint,
|
||||
public bundleCompressor: BundleCompressor,
|
||||
public nextNonce: BigNumber,
|
||||
) {
|
||||
this.verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
this.wallet,
|
||||
);
|
||||
|
||||
this.utilities = AggregatorUtilities__factory.connect(
|
||||
utilitiesAddress,
|
||||
this.wallet,
|
||||
);
|
||||
}
|
||||
) {}
|
||||
|
||||
NextNonce() {
|
||||
const result = this.nextNonce;
|
||||
@@ -97,17 +93,35 @@ export default class EthereumService {
|
||||
|
||||
static async create(
|
||||
emit: (evt: AppEvent) => void,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
aggPrivateKey: string,
|
||||
): Promise<EthereumService> {
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
provider.pollingInterval = env.RPC_POLLING_INTERVAL;
|
||||
const wallet = EthereumService.Wallet(provider, aggPrivateKey);
|
||||
|
||||
const contractsConnector = await ContractsConnector.create(wallet);
|
||||
|
||||
const [
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
blsExpanderDelegator,
|
||||
erc20Expander,
|
||||
blsRegistration,
|
||||
fallbackExpander,
|
||||
expanderEntryPoint,
|
||||
] = await Promise.all([
|
||||
contractsConnector.VerificationGateway(),
|
||||
contractsConnector.AggregatorUtilities(),
|
||||
contractsConnector.BLSExpanderDelegator(),
|
||||
contractsConnector.ERC20Expander(),
|
||||
contractsConnector.BLSRegistration(),
|
||||
contractsConnector.FallbackExpander(),
|
||||
contractsConnector.ExpanderEntryPoint(),
|
||||
]);
|
||||
|
||||
const blsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
aggPrivateKey,
|
||||
verificationGatewayAddress,
|
||||
verificationGateway.address,
|
||||
provider,
|
||||
);
|
||||
|
||||
@@ -122,13 +136,12 @@ export default class EthereumService {
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
await (await VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
wallet,
|
||||
).processBundle(blsWalletWrapper.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await verificationGateway.processBundle(
|
||||
await blsWalletWrapper.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
}
|
||||
|
||||
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
|
||||
@@ -136,16 +149,32 @@ export default class EthereumService {
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey: aggPrivateKey,
|
||||
verificationGatewayAddress: verificationGateway.address,
|
||||
});
|
||||
|
||||
const bundleCompressor = new BundleCompressor(blsExpanderDelegator);
|
||||
|
||||
const [erc20Compressor, blsRegistrationCompressor, fallbackCompressor] =
|
||||
await Promise.all([
|
||||
Erc20Compressor.wrap(erc20Expander),
|
||||
BlsRegistrationCompressor.wrap(blsRegistration),
|
||||
FallbackCompressor.wrap(fallbackExpander),
|
||||
]);
|
||||
|
||||
await bundleCompressor.addCompressor(erc20Compressor);
|
||||
await bundleCompressor.addCompressor(blsRegistrationCompressor);
|
||||
await bundleCompressor.addCompressor(fallbackCompressor);
|
||||
|
||||
return new EthereumService(
|
||||
emit,
|
||||
wallet,
|
||||
provider,
|
||||
blsWalletWrapper,
|
||||
blsWalletSigner,
|
||||
verificationGatewayAddress,
|
||||
utilitiesAddress,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
expanderEntryPoint,
|
||||
bundleCompressor,
|
||||
nextNonce,
|
||||
);
|
||||
}
|
||||
@@ -218,9 +247,10 @@ export default class EthereumService {
|
||||
async callStaticSequence<Calls extends CallHelper<unknown>[]>(
|
||||
...calls: Calls
|
||||
): Promise<MapCallHelperReturns<Calls>> {
|
||||
const rawResults = await this.utilities.callStatic.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
const rawResults = await this.aggregatorUtilities.callStatic
|
||||
.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
|
||||
const results: CallResult<unknown>[] = rawResults.map(
|
||||
([success, result], i) => {
|
||||
@@ -288,22 +318,33 @@ export default class EthereumService {
|
||||
assert(bundle.operations.length > 0, "Cannot process empty bundle");
|
||||
assert(maxAttempts > 0, "Must have at least one attempt");
|
||||
|
||||
const processBundleArgs: Parameters<VerificationGateway["processBundle"]> =
|
||||
[
|
||||
bundle,
|
||||
{
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
},
|
||||
];
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
const [rawTx, rawCompressedTx] = await Promise.all([
|
||||
this.verificationGateway.populateTransaction.processBundle(bundle).then(
|
||||
(tx) => this.wallet.signTransaction(tx),
|
||||
),
|
||||
this.wallet.signTransaction({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
}),
|
||||
]);
|
||||
|
||||
const txLen = ethers.utils.hexDataLength(rawTx);
|
||||
const compressedTxLen = ethers.utils.hexDataLength(rawCompressedTx);
|
||||
|
||||
const txRequest: ethers.providers.TransactionRequest = {
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
};
|
||||
|
||||
const attempt = async () => {
|
||||
let txResponse: ethers.providers.TransactionResponse;
|
||||
|
||||
try {
|
||||
txResponse = await this.verificationGateway.processBundle(
|
||||
...processBundleArgs,
|
||||
);
|
||||
txResponse = await this.wallet.sendTransaction(txRequest);
|
||||
} catch (error) {
|
||||
if (/\binvalid transaction nonce\b/.test(error.message)) {
|
||||
// This can occur when the nonce is in the future, which can
|
||||
@@ -330,7 +371,7 @@ export default class EthereumService {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
this.emit({
|
||||
type: "submission-attempt",
|
||||
data: { attemptNumber: i + 1, publicKeyShorts },
|
||||
data: { attemptNumber: i + 1, publicKeyShorts, txLen, compressedTxLen },
|
||||
});
|
||||
|
||||
const attemptResult = await attempt();
|
||||
@@ -364,6 +405,15 @@ export default class EthereumService {
|
||||
throw new Error("Expected return or throw from attempt loop");
|
||||
}
|
||||
|
||||
async estimateCompressedGas(bundle: Bundle): Promise<BigNumber> {
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
return await this.wallet.estimateGas({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
});
|
||||
}
|
||||
|
||||
async GasConfig() {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
const previousBaseFee = block.baseFeePerGas;
|
||||
|
||||
16
aggregator/src/app/HealthRouter.ts
Normal file
16
aggregator/src/app/HealthRouter.ts
Normal 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;
|
||||
}
|
||||
11
aggregator/src/app/HealthService.ts
Normal file
11
aggregator/src/app/HealthService.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,14 @@ import errorHandler from "./errorHandler.ts";
|
||||
import notFoundHandler from "./notFoundHandler.ts";
|
||||
import Mutex from "../helpers/Mutex.ts";
|
||||
import Clock from "../helpers/Clock.ts";
|
||||
import getNetworkConfig from "../helpers/getNetworkConfig.ts";
|
||||
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();
|
||||
|
||||
const clock = Clock.create();
|
||||
|
||||
const bundleTableMutex = new Mutex();
|
||||
@@ -37,8 +36,6 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
emit,
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -64,10 +61,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();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BundleDto } from "../../deps.ts";
|
||||
|
||||
type ParseResult<T> = (
|
||||
type ParseResult<T> =
|
||||
| { success: T }
|
||||
| { failures: string[] }
|
||||
);
|
||||
| { failures: string[] };
|
||||
|
||||
type Parser<T> = (value: unknown) => ParseResult<T>;
|
||||
|
||||
@@ -96,14 +95,12 @@ export function parseArray<T>(
|
||||
};
|
||||
}
|
||||
|
||||
type DataTuple<ParserTuple> = (
|
||||
ParserTuple extends Parser<unknown>[] ? (
|
||||
type DataTuple<ParserTuple> = ParserTuple extends Parser<unknown>[] ? (
|
||||
ParserTuple extends [Parser<infer T>, ...infer Tail]
|
||||
? [T, ...DataTuple<Tail>]
|
||||
: []
|
||||
)
|
||||
: never
|
||||
);
|
||||
: never;
|
||||
|
||||
export function parseTuple<ParserTuple extends Parser<unknown>[]>(
|
||||
...parserTuple: ParserTuple
|
||||
@@ -188,6 +185,7 @@ const parseActionDataDto: Parser<ActionDataDto> = parseObject({
|
||||
|
||||
const parseOperationDto: Parser<OperationDto> = parseObject({
|
||||
nonce: parseHex(),
|
||||
gas: parseHex(),
|
||||
actions: parseArray(parseActionDataDto),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import Fixture from "./helpers/Fixture.ts";
|
||||
Fixture.test("nonzero fee estimate from default test config", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -51,7 +51,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
let bundle = wallet.sign({
|
||||
let bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -84,7 +84,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
assertEquals(feeEstimation.feeDetected, BigNumber.from(1));
|
||||
|
||||
// Redefine bundle using the estimated fee
|
||||
bundle = wallet.sign({
|
||||
bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -135,7 +135,7 @@ Fixture.test("includes submitError on failed row when bundle callStaticSequence
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ Fixture.test("adds valid bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
const tx = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -20,7 +20,7 @@ Fixture.test("adds valid bundle", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
@@ -33,6 +33,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -71,6 +72,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -103,6 +105,7 @@ Fixture.test(
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -148,6 +151,7 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -37,7 +37,7 @@ function approveAndSendTokensToOrigin(
|
||||
fx: Fixture,
|
||||
nonce: BigNumber,
|
||||
amount: BigNumber,
|
||||
): Operation {
|
||||
): Omit<Operation, "gas"> {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
return {
|
||||
@@ -48,13 +48,13 @@ function approveAndSendTokensToOrigin(
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"approve",
|
||||
[es.utilities.address, amount],
|
||||
[es.aggregatorUtilities.address, amount],
|
||||
),
|
||||
},
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendTokenToTxOrigin",
|
||||
[fx.testErc20.address, amount],
|
||||
),
|
||||
@@ -68,7 +68,7 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -108,8 +108,9 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
tokenBalance: oneToken,
|
||||
});
|
||||
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, await wallet.Nonce(), oneToken),
|
||||
0.1,
|
||||
);
|
||||
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
@@ -120,7 +121,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
oneToken,
|
||||
);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
@@ -129,7 +130,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
const bundleRow = bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
|
||||
@@ -158,13 +159,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
})).wait();
|
||||
|
||||
const estimation = await bundleService.aggregationStrategy.estimateFee(
|
||||
wallet.sign({
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -183,13 +184,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
.sub(1), // Already sent 1 wei before
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
ethValue: fee,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -245,8 +246,9 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
wallet: BlsWalletWrapper,
|
||||
fee: BigNumber,
|
||||
) {
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, nonce, fee),
|
||||
0.1,
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
@@ -275,7 +277,7 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
// Restore this value now that all the bundles are added together
|
||||
bundleService.config.breakevenOperationCount = breakevenOperationCount;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10);
|
||||
assertEquals(bundleService.bundleTable.count(), 10);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
|
||||
@@ -10,7 +10,7 @@ const bundleServiceConfig = {
|
||||
};
|
||||
|
||||
const aggregationStrategyConfig: AggregationStrategyConfig = {
|
||||
maxGasPerBundle: 900000,
|
||||
maxGasPerBundle: 1_000_000,
|
||||
fees: nil,
|
||||
bundleCheckingConcurrency: 8,
|
||||
};
|
||||
@@ -23,7 +23,7 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -78,20 +78,22 @@ Fixture.test("submits a full submission without delay", async (fx) => {
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
for (const b of bundles) {
|
||||
@@ -121,20 +123,22 @@ Fixture.test(
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// Prevent submission from triggering on max aggregation size.
|
||||
@@ -185,6 +189,7 @@ Fixture.test(
|
||||
const bundles = Range(3).reverse().map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -273,6 +278,7 @@ Fixture.test("retains failing bundle when its eligibility delay is smaller than
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -313,6 +319,7 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -13,6 +13,7 @@ const sampleRows: BundleRow[] = [
|
||||
operations: [
|
||||
{
|
||||
nonce: "0x01",
|
||||
gas: "0x01",
|
||||
actions: [
|
||||
{
|
||||
ethValue: "0x00",
|
||||
|
||||
@@ -8,7 +8,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const startBalance = await fx.testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -32,7 +32,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
Fixture.test("EthereumService submits transfer action", async (fx) => {
|
||||
const wallets = await fx.setupWallets(2);
|
||||
|
||||
const bundle = wallets[0].sign({
|
||||
const bundle = await wallets[0].signWithGasEstimate({
|
||||
nonce: await wallets[0].Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -62,6 +62,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce: walletNonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -75,6 +76,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -102,21 +104,24 @@ Fixture.test("EthereumService submits large aggregate mint bundle", async (fx) =
|
||||
const size = 11;
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
await Promise.all(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -137,6 +142,7 @@ Fixture.test("EthereumService sends large aggregate transfer bundle", async (fx)
|
||||
Range(size).map((i) =>
|
||||
sendWallet.sign({
|
||||
nonce: sendWalletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -170,6 +176,7 @@ Fixture.test(
|
||||
Range(5).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -238,7 +245,7 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
value: transferAmount,
|
||||
})).wait();
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce: await sendWallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -253,9 +260,9 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
const results = await es.callStaticSequence(
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.verificationGateway, "processBundle", [bundle]),
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
);
|
||||
|
||||
const [balanceResultBefore, , balanceResultAfter] = results;
|
||||
|
||||
10
aggregator/test/HealthService.test.ts
Normal file
10
aggregator/test/HealthService.test.ts
Normal 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));
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
MockERC20,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
NetworkConfig,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
@@ -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;
|
||||
@@ -74,11 +75,10 @@ export default class Fixture {
|
||||
static async create(testName: string): Promise<Fixture> {
|
||||
const netCfg = await getNetworkConfig();
|
||||
const rng = testRng.seed(testName);
|
||||
const emit = (evt: AppEvent) => fx.emit(evt);
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
(evt) => fx.emit(evt),
|
||||
netCfg.addresses.verificationGateway,
|
||||
netCfg.addresses.utilities,
|
||||
emit,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ export default class Fixture {
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
aggregationStrategyDefaultTestConfig,
|
||||
emit,
|
||||
),
|
||||
netCfg,
|
||||
);
|
||||
@@ -131,7 +132,7 @@ export default class Fixture {
|
||||
public aggregationStrategy: AggregationStrategy,
|
||||
public networkConfig: NetworkConfig,
|
||||
) {
|
||||
this.testErc20 = MockERC20__factory.connect(
|
||||
this.testErc20 = MockERC20Factory.connect(
|
||||
this.networkConfig.addresses.testToken,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -171,6 +172,7 @@ export default class Fixture {
|
||||
this.blsWalletSigner,
|
||||
this.ethereumService,
|
||||
aggregationStrategyConfig,
|
||||
this.emit,
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
@@ -245,7 +247,7 @@ export default class Fixture {
|
||||
const topUp = BigNumber.from(tokenBalance).sub(balance);
|
||||
|
||||
if (topUp.gt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -261,7 +263,7 @@ export default class Fixture {
|
||||
}
|
||||
|
||||
if (topUp.lt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -293,6 +295,12 @@ export default class Fixture {
|
||||
return wallets;
|
||||
}
|
||||
|
||||
createHealthCheckService() {
|
||||
const healthCheckService = new HealthService();
|
||||
|
||||
return healthCheckService;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (const job of this.cleanupJobs) {
|
||||
await job();
|
||||
|
||||
@@ -29,6 +29,7 @@ Deno.test("parseBundleDto accepts dummy values", () => {
|
||||
"operations": [
|
||||
{
|
||||
"nonce": "0x01",
|
||||
"gas": "0x01",
|
||||
"actions": [
|
||||
{
|
||||
"ethValue": "0x00",
|
||||
|
||||
@@ -8,12 +8,6 @@ OPTIMISM_TESETNET_URL=https://kovan.optimism.io
|
||||
OPTIMISM_URL=https://mainnet.optimism.io
|
||||
OPTIMISM_GOERLI_URL=https://goerli.optimism.io
|
||||
|
||||
# Only used for deploying the deployer contract at the same address on each evm network
|
||||
DEPLOYER_MNEMONIC="sock poet alone around radar forum quiz session observe rebel another choice"
|
||||
DEPLOYER_SET_INDEX=1
|
||||
DEPLOYER_ACCOUNT=0x6435e511f8908D5C733898C81831a4A3aFE31D07
|
||||
DEPLOYER_CONTRACT_ADDRESS=0x036d996D6855B83cd80142f2933d8C2617dA5617
|
||||
|
||||
# Used for deploying contracts via the deployment contract, and testing
|
||||
MAIN_MNEMONIC="test test test test test test test test test test test junk"
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ For each network, the deployer contract can be deployed with the following scrip
|
||||
|
||||
To run integration tests:
|
||||
|
||||
1. cd into `./contracts` and run `yarn start-hardhat`
|
||||
1. cd into `./contracts` and run `yarn start`
|
||||
2. cd into `./aggregator` and run `./programs/aggregator.ts`
|
||||
3. from `./contracts`, run `yarn test-integration`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.8.2-1452ef5",
|
||||
"version": "0.9.0",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
||||
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { AddressRegistry } from "../typechain-types/contracts/AddressRegistry";
|
||||
import { AddressRegistry__factory as AddressRegistryFactory } from "../typechain-types/factories/contracts/AddressRegistry__factory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
/**
|
||||
* A wrapper around the `AddressRegistry` contract to provide a more ergonomic
|
||||
* interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class AddressRegistryWrapper {
|
||||
constructor(public registry: AddressRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `AddressRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<AddressRegistryWrapper> {
|
||||
const factory = new AddressRegistryFactory(signer);
|
||||
|
||||
return new AddressRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new AddressRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new AddressRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup an address, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<string | undefined> {
|
||||
const address = await this.registry.addresses(id);
|
||||
|
||||
return address === ethers.constants.AddressZero ? undefined : address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an address to lookup an id - the reverse of what happens on chain, by
|
||||
* making use of the indexed `AddressRegistered` event.
|
||||
*/
|
||||
async reverseLookup(address: string): Promise<BigNumber | undefined> {
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.AddressRegistered(null, address),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address and returns the id that was assigned to it.
|
||||
*/
|
||||
async register(address: string): Promise<BigNumber> {
|
||||
await (await this.registry.register(address)).wait();
|
||||
|
||||
const id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address if it hasn't already been registered, and returns the
|
||||
* id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(address: string): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(address);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -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,56 +52,70 @@ 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,
|
||||
);
|
||||
// set to zero at all times as an error will be thrown. If the
|
||||
// nonce of the actual wallet is more than 0, there will be a
|
||||
// nonce mistmatch as signWithGasEstimate with check the operation
|
||||
// nonce against the throwawayBlsWalletWrapper nonce, which is always zero
|
||||
const nonce = 0;
|
||||
|
||||
const actionWithFeePaymentAction =
|
||||
this._addFeePaymentActionForFeeEstimation([action]);
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(
|
||||
this.signer.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
}),
|
||||
// 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 bundle = await throwawayBlsWalletWrapper.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(bundle);
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
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 +137,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 +159,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 +200,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 +216,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 +239,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 +352,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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils";
|
||||
import {
|
||||
BLSPublicKeyRegistry,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
} from "../typechain-types";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { PublicKey } from "./signer";
|
||||
|
||||
/**
|
||||
* A wrapper around the `BLSPublicKeyRegistry` contract to provide a more
|
||||
* ergonomic interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class BlsPublicKeyRegistryWrapper {
|
||||
constructor(public registry: BLSPublicKeyRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `BLSPublicKeyRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new BlsPublicKeyRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup a public key, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<PublicKey | undefined> {
|
||||
const blsPublicKey = await Promise.all([
|
||||
this.registry.blsPublicKeys(id, 0),
|
||||
this.registry.blsPublicKeys(id, 1),
|
||||
this.registry.blsPublicKeys(id, 2),
|
||||
this.registry.blsPublicKeys(id, 3),
|
||||
]);
|
||||
|
||||
if (blsPublicKey.every((x) => x.eq(0))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a public key to lookup an id - the reverse of what happens on chain,
|
||||
* by making use of the indexed `BLSPublicKeyRegistered` event.
|
||||
*/
|
||||
async reverseLookup(blsPublicKey: PublicKey): Promise<BigNumber | undefined> {
|
||||
const blsPublicKeyHash = solidityKeccak256(["uint256[4]"], [blsPublicKey]);
|
||||
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.BLSPublicKeyRegistered(null, blsPublicKeyHash),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key and returns the id.
|
||||
*/
|
||||
async register(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
await (await this.registry.register(blsPublicKey)).wait();
|
||||
|
||||
const id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key if it hasn't already been registered, and returns
|
||||
* the id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(blsPublicKey);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import { encodePseudoFloat, encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class BlsRegistrationCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public blsRegistration: BLSRegistration,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
blsRegistration: BLSRegistration,
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
blsRegistration.blsPublicKeyRegistry(),
|
||||
blsRegistration.addressRegistry(),
|
||||
blsRegistration.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistration,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<BlsRegistrationCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const blsRegistrationFactory = new BLSRegistrationFactory(signer);
|
||||
|
||||
const blsRegistrationContract = await blsRegistrationFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const blsRegistrationContract = await factory.connectOrDeploy(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const blsPublicKeyRegistryAddress = factoryViewer.calculateAddress(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const addressRegistryAddress = factoryViewer.calculateAddress(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const aggregatorUtilitiesAddress = factoryViewer.calculateAddress(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const blsRegistration = await factoryViewer.connectIfDeployed(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!blsRegistration) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await BlsRegistrationCompressor.wrap(blsRegistration);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.blsRegistration.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
if (operation.actions.length > 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be a non-paying call to blsRegistration.register with the user's
|
||||
// blsPublicKey
|
||||
const firstAction = operation.actions.at(0);
|
||||
|
||||
if (
|
||||
firstAction === undefined ||
|
||||
!BigNumber.from(firstAction.ethValue).isZero() ||
|
||||
firstAction.contractAddress !== this.blsRegistration.address ||
|
||||
ethers.utils.hexlify(firstAction.encodedFunction) !==
|
||||
this.blsRegistration.interface.encodeFunctionData("register", [
|
||||
blsPublicKey,
|
||||
])
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be absent or a non-zero payment to tx.origin
|
||||
const secondAction = operation.actions.at(1);
|
||||
|
||||
if (secondAction !== undefined) {
|
||||
if (
|
||||
BigNumber.from(secondAction.ethValue).isZero() ||
|
||||
secondAction.contractAddress !== this.aggregatorUtilities.address ||
|
||||
ethers.utils.hexlify(secondAction.encodedFunction) !==
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
encodeVLQ(operation.nonce),
|
||||
encodePseudoFloat(operation.gas),
|
||||
encodePseudoFloat(secondAction?.ethValue ?? 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,22 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
|
||||
import {
|
||||
AccessListish,
|
||||
Deferrable,
|
||||
hexlify,
|
||||
isBytes,
|
||||
RLP,
|
||||
} from "ethers/lib/utils";
|
||||
import { AccessListish, Deferrable, hexlify, isBytes } 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";
|
||||
import { ActionData, Signature, 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 +29,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 +58,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,22 +98,41 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -116,18 +141,18 @@ 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,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -137,26 +162,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 = BigNumber.from(validatedTransactionBatch.batchOptions!.nonce);
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -166,11 +195,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(),
|
||||
@@ -182,11 +207,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -198,7 +225,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -208,14 +235,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 +266,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 +282,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,14 +307,14 @@ 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],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -355,18 +322,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 +349,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(),
|
||||
@@ -392,11 +361,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -408,7 +379,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -416,8 +387,15 @@ 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. Because of the function signature enforced by ethers, we cannot return the signature
|
||||
* in it's default type. Instead, we return a concatenated string of the signature.
|
||||
*
|
||||
* Use BlsSigner.signedMessageToSignature to convert the concatenated signature string into a BLS Signature type.
|
||||
*
|
||||
* @param message the message to be signed
|
||||
* @returns a concatenated string of the signature
|
||||
*/
|
||||
override async signMessage(message: Bytes | string): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (isBytes(message)) {
|
||||
@@ -425,7 +403,18 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const signedMessage = this.wallet.signMessage(message);
|
||||
return RLP.encode(signedMessage);
|
||||
return (
|
||||
ethers.utils.hexlify(signedMessage[0]) +
|
||||
ethers.utils.hexlify(signedMessage[1]).substring(2)
|
||||
);
|
||||
}
|
||||
|
||||
/** helper method to convert blsSigner.signMessage concatenated signature string into BLS Signature type */
|
||||
static signedMessageToSignature(signedMessage: string): Signature {
|
||||
return [
|
||||
ethers.utils.hexlify(signedMessage.substring(0, 66)),
|
||||
ethers.utils.hexlify("0x" + signedMessage.substring(66, 130)),
|
||||
];
|
||||
}
|
||||
|
||||
override connect(provider: ethers.providers.Provider): BlsSigner {
|
||||
@@ -440,6 +429,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 +445,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 +460,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 +525,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 +539,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: "",
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { providers } from "ethers";
|
||||
import {
|
||||
BNPairingPrecompileCostEstimator,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
Create2Deployer,
|
||||
Create2Deployer__factory as Create2DeployerFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
BLSOpen,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSExpander,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
@@ -21,10 +15,7 @@ import { NetworkConfig } from "./NetworkConfig";
|
||||
* BLS Wallet Contracts
|
||||
*/
|
||||
export type BlsWalletContracts = Readonly<{
|
||||
create2Deployer: Create2Deployer;
|
||||
precompileCostEstimator: BNPairingPrecompileCostEstimator;
|
||||
verificationGateway: VerificationGateway;
|
||||
blsLibrary: BLSOpen;
|
||||
blsExpander: BLSExpander;
|
||||
aggregatorUtilities: AggregatorUtilities;
|
||||
testToken: MockERC20;
|
||||
@@ -41,32 +32,19 @@ export const connectToContracts = async (
|
||||
provider: providers.Provider,
|
||||
{ addresses }: NetworkConfig,
|
||||
): Promise<BlsWalletContracts> => {
|
||||
const [
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
] = await Promise.all([
|
||||
Create2DeployerFactory.connect(addresses.create2Deployer, provider),
|
||||
BNPairingPrecompileCostEstimatorFactory.connect(
|
||||
addresses.create2Deployer,
|
||||
provider,
|
||||
),
|
||||
VerificationGatewayFactory.connect(addresses.verificationGateway, provider),
|
||||
BLSOpenFactory.connect(addresses.blsLibrary, provider),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
const [verificationGateway, blsExpander, aggregatorUtilities, testToken] =
|
||||
await Promise.all([
|
||||
VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
provider,
|
||||
),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
|
||||
return {
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
|
||||
import {
|
||||
@@ -13,10 +11,10 @@ import {
|
||||
|
||||
import {
|
||||
BLSWallet,
|
||||
BLSWallet__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
BLSWallet__factory as BLSWalletFactory,
|
||||
TransparentUpgradeableProxy__factory as TransparentUpgradeableProxyFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
import getRandomBlsPrivateKey from "./signer/getRandomBlsPrivateKey";
|
||||
@@ -28,9 +26,11 @@ type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
*/
|
||||
export default class BlsWalletWrapper {
|
||||
public address: string;
|
||||
public blockGasLimit: BigNumber = BigNumber.from(0);
|
||||
private constructor(
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public walletContract: BLSWallet,
|
||||
public defaultGatewayAddress: string,
|
||||
) {
|
||||
this.address = walletContract.address;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
return BLSWallet__factory.connect(
|
||||
return BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -74,9 +74,10 @@ export default class BlsWalletWrapper {
|
||||
const blsWalletSigner = await this.#BlsWalletSigner(
|
||||
signerOrProvider,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
);
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -140,19 +141,24 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
provider: ethers.providers.Provider,
|
||||
): Promise<BlsWalletWrapper> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: (await verificationGateway.provider.getNetwork()).chainId,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
|
||||
verificationGateway.address,
|
||||
);
|
||||
blsWalletWrapper.blockGasLimit = (
|
||||
await provider.getBlock("latest")
|
||||
).gasLimit;
|
||||
|
||||
return blsWalletWrapper;
|
||||
}
|
||||
@@ -164,7 +170,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
this.walletContract = BLSWallet__factory.connect(
|
||||
this.walletContract = BLSWalletFactory.connect(
|
||||
this.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -194,7 +200,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
signerOrProvider: SignerOrProvider,
|
||||
): Promise<BigNumber> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -208,7 +214,7 @@ export default class BlsWalletWrapper {
|
||||
publicKeyHash,
|
||||
);
|
||||
|
||||
const walletContract = BLSWallet__factory.connect(
|
||||
const walletContract = BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -225,6 +231,46 @@ export default class BlsWalletWrapper {
|
||||
return await walletContract.nonce();
|
||||
}
|
||||
|
||||
/** Sign an operation with an estimate of the gas required. */
|
||||
async signWithGasEstimate(
|
||||
operation: Omit<Operation, "gas">,
|
||||
|
||||
/**
|
||||
* Optional: Multiply estimate by `(1+overhead)` to account for uncertainty.
|
||||
* Reduces the chance of running out of gas.
|
||||
*/
|
||||
overhead = 0,
|
||||
): Promise<Bundle> {
|
||||
let gas = await this.estimateGas(operation);
|
||||
gas = gas.add(gas.div(10000).mul(Math.ceil(overhead * 10000)));
|
||||
|
||||
return this.sign({ ...operation, gas });
|
||||
}
|
||||
|
||||
/** Estimate the gas needed for an operation. */
|
||||
async estimateGas(operation: Omit<Operation, "gas">): Promise<BigNumber> {
|
||||
const exists =
|
||||
(await this.walletContract.provider.getCode(this.address)) !== "0x";
|
||||
|
||||
const gatewayAddress = exists
|
||||
? await this.walletContract.trustedBLSGateway()
|
||||
: this.defaultGatewayAddress;
|
||||
|
||||
const gateway = VerificationGatewayFactory.connect(
|
||||
gatewayAddress,
|
||||
this.walletContract.provider,
|
||||
);
|
||||
|
||||
const gas = await gateway
|
||||
.connect(ethers.constants.AddressZero)
|
||||
.callStatic.measureOperationGas(this.PublicKey(), {
|
||||
...operation,
|
||||
gas: this.blockGasLimit,
|
||||
});
|
||||
|
||||
return gas;
|
||||
}
|
||||
|
||||
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
|
||||
sign(operation: Operation): Bundle {
|
||||
return this.blsWalletSigner.sign(operation, this.walletContract.address);
|
||||
@@ -264,7 +310,7 @@ export default class BlsWalletWrapper {
|
||||
[recoverWalletAddress, walletHash, saltHash],
|
||||
);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -284,13 +330,17 @@ export default class BlsWalletWrapper {
|
||||
newPrivateKey: string,
|
||||
recoverySalt: string,
|
||||
verificationGateway: VerificationGateway,
|
||||
signatureExpiryTimestamp: number,
|
||||
): Promise<Bundle> {
|
||||
const updatedWallet = await BlsWalletWrapper.connect(
|
||||
newPrivateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
const addressMessage = solidityPack(["address"], [recoveryAddress]);
|
||||
const addressMessage = solidityPack(
|
||||
["address", "uint256"],
|
||||
[recoveryAddress, signatureExpiryTimestamp],
|
||||
);
|
||||
const addressSignature = updatedWallet.signMessage(addressMessage);
|
||||
|
||||
const recoveryWalletHash = await verificationGateway.hashFromWallet(
|
||||
@@ -298,7 +348,7 @@ export default class BlsWalletWrapper {
|
||||
);
|
||||
const saltHash = ethers.utils.formatBytes32String(recoverySalt);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -311,6 +361,7 @@ export default class BlsWalletWrapper {
|
||||
recoveryWalletHash,
|
||||
saltHash,
|
||||
updatedWallet.PublicKey(),
|
||||
signatureExpiryTimestamp,
|
||||
],
|
||||
),
|
||||
},
|
||||
@@ -321,36 +372,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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the BlsWalletSigner instance to a new private key and chainId
|
||||
*
|
||||
* @returns The updated BlsWalletSigner object
|
||||
*/
|
||||
async setBlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
const newBlsWalletSigner = await initBlsWalletSigner({
|
||||
return await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
this.blsWalletSigner = newBlsWalletSigner;
|
||||
return newBlsWalletSigner;
|
||||
}
|
||||
|
||||
// Calculates the expected address the wallet will be created at
|
||||
@@ -364,7 +397,7 @@ export default class BlsWalletWrapper {
|
||||
]);
|
||||
|
||||
const initFunctionParams =
|
||||
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
|
||||
BLSWalletFactory.createInterface().encodeFunctionData("initialize", [
|
||||
verificationGateway.address,
|
||||
]);
|
||||
|
||||
@@ -374,7 +407,7 @@ export default class BlsWalletWrapper {
|
||||
ethers.utils.solidityKeccak256(
|
||||
["bytes", "bytes"],
|
||||
[
|
||||
TransparentUpgradeableProxy__factory.bytecode,
|
||||
TransparentUpgradeableProxyFactory.bytecode,
|
||||
ethers.utils.defaultAbiCoder.encode(
|
||||
["address", "address", "bytes"],
|
||||
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
|
||||
|
||||
98
contracts/clients/src/BundleCompressor.ts
Normal file
98
contracts/clients/src/BundleCompressor.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ethers } from "ethers";
|
||||
import { encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import { Bundle, Operation, PublicKey } from "./signer";
|
||||
import Range from "./helpers/Range";
|
||||
import { BLSExpanderDelegator } from "../typechain-types";
|
||||
|
||||
/**
|
||||
* Produces compressed bundles that can be passed to `BLSExpanderDelegator.run`
|
||||
* instead of `VerificationGateway.processBundle`.
|
||||
*
|
||||
* The compression of operations is delegated to other compressors that you
|
||||
* inject using `.addCompressor`. For each operation of the bundle, these
|
||||
* compressors are tried in the order they were added, and the first one that
|
||||
* succeeds is used. Note that `expanderIndex` is unrelated to this order - it
|
||||
* just needs to match the index that the corresponding expander contract is
|
||||
* registered at in BLSExpanderDelegator.
|
||||
*/
|
||||
export default class BundleCompressor {
|
||||
compressors: [number, IOperationCompressor][] = [];
|
||||
|
||||
constructor(public blsExpanderDelegator: BLSExpanderDelegator) {}
|
||||
|
||||
/** Add an operation compressor. */
|
||||
async addCompressor(compressor: IOperationCompressor) {
|
||||
const registrations = await this.blsExpanderDelegator.queryFilter(
|
||||
this.blsExpanderDelegator.filters.ExpanderRegistered(
|
||||
null,
|
||||
compressor.getExpanderAddress(),
|
||||
),
|
||||
);
|
||||
|
||||
const id = registrations.at(0)?.args?.id;
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Expander not registered");
|
||||
}
|
||||
|
||||
this.compressors.push([id.toNumber(), compressor]);
|
||||
}
|
||||
|
||||
/** Compresses a single operation. */
|
||||
async compressOperation(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string> {
|
||||
let expanderIndexAndData: [number, string] | undefined;
|
||||
|
||||
for (const [expanderIndex, compressor] of this.compressors) {
|
||||
let data: string | undefined;
|
||||
|
||||
try {
|
||||
data = await compressor.compress(blsPublicKey, operation);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expanderIndexAndData = [expanderIndex, data];
|
||||
break;
|
||||
}
|
||||
|
||||
if (expanderIndexAndData === undefined) {
|
||||
throw new Error("Failed to compress operation");
|
||||
}
|
||||
|
||||
const [expanderIndex, data] = expanderIndexAndData;
|
||||
|
||||
return hexJoin([encodeVLQ(expanderIndex), data]);
|
||||
}
|
||||
|
||||
/** Compresses a bundle. */
|
||||
async compress(bundle: Bundle): Promise<string> {
|
||||
const len = bundle.operations.length;
|
||||
|
||||
if (bundle.senderPublicKeys.length !== len) {
|
||||
throw new Error("ops vs keys length mismatch");
|
||||
}
|
||||
|
||||
const compressedOperations = await Promise.all(
|
||||
Range(len).map((i) =>
|
||||
this.compressOperation(
|
||||
bundle.senderPublicKeys[i],
|
||||
bundle.operations[i],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(bundle.operations.length),
|
||||
...compressedOperations,
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[2]"], [bundle.signature]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
165
contracts/clients/src/ContractsConnector.ts
Normal file
165
contracts/clients/src/ContractsConnector.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { ethers } from "ethers";
|
||||
import { SafeSingletonFactoryViewer } from "./SafeSingletonFactory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import assert from "./helpers/assert";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSExpanderDelegator__factory as BLSExpanderDelegatorFactory,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
ExpanderEntryPoint__factory as ExpanderEntryPointFactory,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
export default class ContractsConnector {
|
||||
constructor(
|
||||
public factoryViewer: SafeSingletonFactoryViewer,
|
||||
public salt: ethers.utils.BytesLike = ethers.utils.solidityPack(
|
||||
["uint256"],
|
||||
[0],
|
||||
),
|
||||
) {}
|
||||
|
||||
static async create(signerOrProvider: SignerOrProvider) {
|
||||
let provider: ethers.providers.Provider;
|
||||
|
||||
if ("getNetwork" in signerOrProvider) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
assert(
|
||||
signerOrProvider.provider !== undefined,
|
||||
"When using a signer, it's required to have a provider",
|
||||
);
|
||||
|
||||
provider = signerOrProvider.provider;
|
||||
}
|
||||
|
||||
const chainId = (await provider.getNetwork()).chainId;
|
||||
|
||||
const factoryViewer = new SafeSingletonFactoryViewer(
|
||||
signerOrProvider,
|
||||
chainId,
|
||||
);
|
||||
|
||||
return new ContractsConnector(factoryViewer);
|
||||
}
|
||||
|
||||
BNPairingPrecompileCostEstimator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BNPairingPrecompileCostEstimatorFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSOpen = once(() =>
|
||||
this.factoryViewer.connectOrThrow(BLSOpenFactory, [], this.salt),
|
||||
);
|
||||
|
||||
VerificationGateway = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
VerificationGatewayFactory,
|
||||
[(await this.BLSOpen()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AggregatorUtilities = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpanderDelegator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderDelegatorFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSPublicKeyRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AddressRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(AddressRegistryFactory, [], this.salt),
|
||||
);
|
||||
|
||||
FallbackExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
FallbackExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSRegistration = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSRegistrationFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ERC20Expander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ERC20ExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ExpanderEntryPoint = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ExpanderEntryPointFactory,
|
||||
[(await this.BLSExpanderDelegator()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function once<T extends {}>(fn: () => T): () => T {
|
||||
let result: T | undefined;
|
||||
|
||||
return () => {
|
||||
if (result === undefined) {
|
||||
result = fn();
|
||||
(fn as unknown as undefined) = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
394
contracts/clients/src/Erc20Compressor.ts
Normal file
394
contracts/clients/src/Erc20Compressor.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
ERC20Expander,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class Erc20Compressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public erc20Expander: ERC20Expander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(erc20Expander: ERC20Expander): Promise<Erc20Compressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
erc20Expander.blsPublicKeyRegistry(),
|
||||
erc20Expander.addressRegistry(),
|
||||
erc20Expander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20Expander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<Erc20Compressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const erc20ExpanderFactory = new ERC20ExpanderFactory(signer);
|
||||
|
||||
const erc20ExpanderContract = await erc20ExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20ExpanderContract = await factory.connectOrDeploy(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20Expander = await factoryViewer.connectIfDeployed(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!erc20Expander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await Erc20Compressor.wrap(erc20Expander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.erc20Expander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const success = await this.compressFunctionCall(
|
||||
action.encodedFunction,
|
||||
result,
|
||||
bitStream,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
|
||||
private async compressFunctionCall(
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
const encodedFunctionHex = ethers.utils.hexlify(encodedFunction);
|
||||
const parametersHex = ethers.utils.hexDataSlice(encodedFunctionHex, 4);
|
||||
|
||||
if (isMethod("transfer(address,uint256)", encodedFunction)) {
|
||||
return this.compressTransfer(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("transferFrom(address,address,uint256)", encodedFunction)) {
|
||||
return this.compressTransferFrom(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("approve(address,uint256)", encodedFunction)) {
|
||||
return this.compressApprove(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("mint(address,uint256)", encodedFunction)) {
|
||||
return this.compressMint(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async compressTransfer(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(0));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressTransferFrom(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 3 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(1));
|
||||
|
||||
const [from, to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, string, BigNumber];
|
||||
|
||||
await this.compressAddress(from, result, bitStream);
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressApprove(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [spender, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
if (value.eq(ethers.constants.MaxUint256)) {
|
||||
result.push(encodeVLQ(3));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
} else {
|
||||
result.push(encodeVLQ(2));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressMint(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(4));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressAddress(
|
||||
address: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(address);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(address);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMethod(
|
||||
signature: string,
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
): boolean {
|
||||
const methodId = ethers.utils
|
||||
.keccak256(ethers.utils.toUtf8Bytes(signature))
|
||||
.slice(0, 10);
|
||||
|
||||
return ethers.utils.hexlify(encodedFunction).startsWith(methodId);
|
||||
}
|
||||
7
contracts/clients/src/Experimental.ts
Normal file
7
contracts/clients/src/Experimental.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
247
contracts/clients/src/FallbackCompressor.ts
Normal file
247
contracts/clients/src/FallbackCompressor.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
FallbackExpander,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class FallbackCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public fallbackExpander: FallbackExpander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
fallbackExpander: FallbackExpander,
|
||||
): Promise<FallbackCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
fallbackExpander.blsPublicKeyRegistry(),
|
||||
fallbackExpander.addressRegistry(),
|
||||
fallbackExpander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<FallbackCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const fallbackExpanderFactory = new FallbackExpanderFactory(signer);
|
||||
|
||||
const fallbackExpanderContract = await fallbackExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpanderContract = await factory.connectOrDeploy(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpander = await factoryViewer.connectIfDeployed(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!fallbackExpander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await FallbackCompressor.wrap(fallbackExpander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.fallbackExpander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
result.push(encodePseudoFloat(action.ethValue));
|
||||
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const fnHex = ethers.utils.hexlify(action.encodedFunction);
|
||||
const fnLen = (fnHex.length - 2) / 2;
|
||||
|
||||
result.push(encodeVLQ(fnLen));
|
||||
result.push(fnHex);
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
}
|
||||
12
contracts/clients/src/IOperationCompressor.ts
Normal file
12
contracts/clients/src/IOperationCompressor.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Operation, PublicKey } from "./signer";
|
||||
|
||||
type IOperationCompressor = {
|
||||
getExpanderAddress(): string;
|
||||
|
||||
compress(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export default IOperationCompressor;
|
||||
@@ -10,10 +10,9 @@ export type NetworkConfig = {
|
||||
* Contract addresses
|
||||
*/
|
||||
addresses: {
|
||||
create2Deployer: string;
|
||||
safeSingletonFactory: string;
|
||||
precompileCostEstimator: string;
|
||||
verificationGateway: string;
|
||||
blsLibrary: string;
|
||||
blsExpander: string;
|
||||
utilities: string;
|
||||
testToken: string;
|
||||
@@ -24,11 +23,15 @@ export type NetworkConfig = {
|
||||
auxiliary: {
|
||||
chainid: number;
|
||||
/**
|
||||
* Domain used for BLS signing
|
||||
* Domain used for signing BLS Proof of Possession messages
|
||||
*/
|
||||
domain: string;
|
||||
walletDomain: string;
|
||||
/**
|
||||
* Starting block contracts began dpeloyment at
|
||||
* Domain used for signing BLS Bundle messages
|
||||
*/
|
||||
bundleDomain: string;
|
||||
/**
|
||||
* Starting block contracts began deployment at
|
||||
*/
|
||||
genesisBlock: number;
|
||||
/**
|
||||
@@ -54,19 +57,19 @@ export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
|
||||
return {
|
||||
parameters: assertUnknownRecord(cfg.parameters),
|
||||
addresses: {
|
||||
create2Deployer: assertString(cfg.addresses.create2Deployer),
|
||||
safeSingletonFactory: assertString(cfg.addresses.safeSingletonFactory),
|
||||
precompileCostEstimator: assertString(
|
||||
cfg.addresses.precompileCostEstimator,
|
||||
),
|
||||
verificationGateway: assertString(cfg.addresses.verificationGateway),
|
||||
blsLibrary: assertString(cfg.addresses.blsLibrary),
|
||||
blsExpander: assertString(cfg.addresses.blsExpander),
|
||||
utilities: assertString(cfg.addresses.utilities),
|
||||
testToken: assertString(cfg.addresses.testToken),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: assertNumber(cfg.auxiliary.chainid),
|
||||
domain: assertString(cfg.auxiliary.domain),
|
||||
walletDomain: assertString(cfg.auxiliary.walletDomain),
|
||||
bundleDomain: assertString(cfg.auxiliary.bundleDomain),
|
||||
genesisBlock: assertNumber(cfg.auxiliary.genesisBlock),
|
||||
deployedBy: assertString(cfg.auxiliary.deployedBy),
|
||||
version: assertString(cfg.auxiliary.version),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BigNumber, ContractReceipt, utils } from "ethers";
|
||||
import assert from "./helpers/assert";
|
||||
import { ActionData } from "./signer";
|
||||
import { Result } from "ethers/lib/utils";
|
||||
import { VerificationGateway__factory as VerificationGatewayFactory } from "../typechain-types";
|
||||
|
||||
export const errorSelectors = {
|
||||
Error: calculateAndCheckSelector("Error(string)", "0x08c379a0"),
|
||||
@@ -117,22 +119,45 @@ const getError = (
|
||||
export const getOperationResults = (
|
||||
txnReceipt: ContractReceipt,
|
||||
): OperationResult[] => {
|
||||
const walletOpProcessedEvents = txnReceipt.events?.filter(
|
||||
(e) => e.event === "WalletOperationProcessed",
|
||||
);
|
||||
if (!walletOpProcessedEvents?.length) {
|
||||
let walletOpProcessedEvents: Result[] = (txnReceipt.events ?? [])
|
||||
.filter((e) => e.event === "WalletOperationProcessed")
|
||||
.map(({ args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
|
||||
return args;
|
||||
});
|
||||
|
||||
if (walletOpProcessedEvents.length === 0 && txnReceipt.logs !== undefined) {
|
||||
const vgInterface = VerificationGatewayFactory.createInterface();
|
||||
|
||||
const WalletOperationProcessed = vgInterface.getEvent(
|
||||
"WalletOperationProcessed",
|
||||
);
|
||||
|
||||
walletOpProcessedEvents = txnReceipt.logs
|
||||
.filter(
|
||||
(log) =>
|
||||
log.topics[0] === vgInterface.getEventTopic(WalletOperationProcessed),
|
||||
)
|
||||
.map((log) =>
|
||||
vgInterface.decodeEventLog(
|
||||
WalletOperationProcessed,
|
||||
log.data,
|
||||
log.topics,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (walletOpProcessedEvents.length === 0) {
|
||||
throw new Error(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return walletOpProcessedEvents.reduce<OperationResult[]>(
|
||||
(opResults, { args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
const { wallet, nonce, actions: rawActions, success, results } = args;
|
||||
|
||||
(opResults, { wallet, nonce, actions: rawActions, success, results }) => {
|
||||
const actions = rawActions.map(
|
||||
({
|
||||
ethValue,
|
||||
@@ -150,7 +175,7 @@ export const getOperationResults = (
|
||||
);
|
||||
const error = getError(success, results);
|
||||
|
||||
return [
|
||||
const ret = [
|
||||
...opResults,
|
||||
{
|
||||
walletAddress: wallet,
|
||||
@@ -161,6 +186,8 @@ export const getOperationResults = (
|
||||
error,
|
||||
},
|
||||
];
|
||||
|
||||
return ret;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
import assert from "assert";
|
||||
import { ethers, Signer } from "ethers";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
|
||||
/**
|
||||
* Filters out the optional elements of an array type because an optional
|
||||
* element isn't considered to match First in [infer First, ...].
|
||||
*/
|
||||
type NonOptionalElementsOf<A extends unknown[]> = A extends [
|
||||
infer First,
|
||||
...infer Tail,
|
||||
]
|
||||
? [First, ...NonOptionalElementsOf<Tail>]
|
||||
: A extends [opt?: unknown]
|
||||
? []
|
||||
: never;
|
||||
|
||||
export type ContractFactoryConstructor = {
|
||||
new (): ethers.ContractFactory;
|
||||
};
|
||||
|
||||
export type DeployParams<CFC extends ContractFactoryConstructor> =
|
||||
NonOptionalElementsOf<Parameters<InstanceType<CFC>["deploy"]>>;
|
||||
|
||||
type Deployment = {
|
||||
gasPrice: number;
|
||||
gasLimit: number;
|
||||
signerAddress: string;
|
||||
transaction: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export default class SafeSingletonFactory {
|
||||
static sharedAddress = "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7";
|
||||
|
||||
static deployments: Record<number, Deployment | undefined> = {
|
||||
1337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf3820a96a",
|
||||
"0460c6ea9b8f791e5d9e67fbf2c70aba92bf88591c39ac3747ea1bedc2ef1750ca04b",
|
||||
"08a4b5cea15a56276513da7a0c0b34f16e89811d5dd911efba5f8625a921cc",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
31337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf382f4f5a",
|
||||
"00dc4d1d21b308094a30f5f93da35e4d72e99115378f135f2295bea47301a3165a063",
|
||||
"6b822daad40aa8c52dd5132f378c0c0e6d83b4898228c7e21c84e631a0b891",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
};
|
||||
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
viewer: SafeSingletonFactoryViewer;
|
||||
|
||||
private constructor(
|
||||
public signer: ethers.Signer,
|
||||
public chainId: number,
|
||||
public address: string,
|
||||
) {
|
||||
if (signer.provider === undefined) {
|
||||
throw new Error("Expected signer with provider");
|
||||
}
|
||||
|
||||
this.provider = signer.provider;
|
||||
|
||||
this.viewer = new SafeSingletonFactoryViewer(signer, chainId);
|
||||
}
|
||||
|
||||
static async init(signer: ethers.Signer): Promise<SafeSingletonFactory> {
|
||||
assert(signer.provider !== undefined, "Expected signer with provider");
|
||||
|
||||
const chainId = await signer.getChainId();
|
||||
|
||||
const address =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
const existingCode = await signer.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return new SafeSingletonFactory(signer, chainId, address);
|
||||
}
|
||||
|
||||
const deployment = SafeSingletonFactory.deployments[chainId];
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(
|
||||
[
|
||||
"Cannot get deployment for SafeSingletonFactory (check",
|
||||
"https://github.com/safe-global/safe-singleton-factory/tree/main/artifacts",
|
||||
`for chain id ${chainId})`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
// Fund the eoa account for the presigned transaction
|
||||
await (
|
||||
await signer.sendTransaction({
|
||||
to: deployment.signerAddress,
|
||||
value: ethers.BigNumber.from(deployment.gasPrice).mul(
|
||||
deployment.gasLimit,
|
||||
),
|
||||
})
|
||||
).wait();
|
||||
|
||||
await (
|
||||
await signer.provider.sendTransaction(deployment.transaction)
|
||||
).wait();
|
||||
|
||||
const deployedCode = await signer.provider.getCode(deployment.address);
|
||||
assert(deployedCode !== "0x", "Failed to deploy safe singleton factory");
|
||||
|
||||
return new SafeSingletonFactory(signer, chainId, deployment.address);
|
||||
}
|
||||
|
||||
static async from(signerOrFactory: ethers.Signer | SafeSingletonFactory) {
|
||||
if (signerOrFactory instanceof SafeSingletonFactory) {
|
||||
return signerOrFactory;
|
||||
}
|
||||
|
||||
return await SafeSingletonFactory.init(signerOrFactory);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
return this.viewer.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<boolean> {
|
||||
return this.viewer.isDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
let contract = await this.viewer.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (contract !== undefined) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrDeploy<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
|
||||
const deployTx = {
|
||||
to: this.address,
|
||||
data: ethers.utils.solidityPack(["uint256", "bytes"], [salt, initCode]),
|
||||
};
|
||||
|
||||
try {
|
||||
await (await this.signer.sendTransaction(deployTx)).wait();
|
||||
} catch (error) {
|
||||
if ((error as { code: string }).code !== "INSUFFICIENT_FUNDS") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const gasEstimate = await this.provider.estimateGas(deployTx);
|
||||
const gasPrice = await this.provider.getGasPrice();
|
||||
|
||||
const balance = await this.provider.getBalance(this.signer.getAddress());
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
"Account",
|
||||
await this.signer.getAddress(),
|
||||
"has insufficient funds:",
|
||||
ethers.utils.formatEther(balance),
|
||||
"ETH, need (approx):",
|
||||
ethers.utils.formatEther(gasEstimate.mul(gasPrice)),
|
||||
"ETH",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
const deployedCode = await this.provider.getCode(address);
|
||||
|
||||
assert(deployedCode !== "0x", "Failed to deploy to expected address");
|
||||
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeSingletonFactoryViewer {
|
||||
safeSingletonFactoryAddress: string;
|
||||
signer?: Signer;
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
constructor(
|
||||
public signerOrProvider: SignerOrProvider,
|
||||
public chainId: number,
|
||||
) {
|
||||
this.safeSingletonFactoryAddress =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
let provider: ethers.providers.Provider | undefined;
|
||||
|
||||
if (ethers.providers.Provider.isProvider(signerOrProvider)) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
provider = signerOrProvider.provider;
|
||||
this.signer = signerOrProvider;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
static async from(signerOrProvider: SignerOrProvider) {
|
||||
const provider = ethers.providers.Provider.isProvider(signerOrProvider)
|
||||
? signerOrProvider
|
||||
: signerOrProvider.provider;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
return new SafeSingletonFactoryViewer(signerOrProvider, network.chainId);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
return ethers.utils.getCreate2Address(
|
||||
this.safeSingletonFactoryAddress,
|
||||
salt,
|
||||
ethers.utils.keccak256(initCode),
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
return existingCode !== "0x";
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode === "0x") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
let contract = contractFactory.attach(address) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
|
||||
if (this.signer) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrThrow<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contract = await this.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(
|
||||
`Contract ${
|
||||
ContractFactoryConstructor.name
|
||||
} not deployed at ${this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
119
contracts/clients/src/encodeUtils.ts
Normal file
119
contracts/clients/src/encodeUtils.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BigNumber, BigNumberish, ethers } from "ethers";
|
||||
|
||||
export function hexJoin(hexStrings: string[]) {
|
||||
return "0x" + hexStrings.map(remove0x).join("");
|
||||
}
|
||||
|
||||
export function remove0x(hexString: string) {
|
||||
if (!hexString.startsWith("0x")) {
|
||||
throw new Error("Expected 0x prefix");
|
||||
}
|
||||
|
||||
return hexString.slice(2);
|
||||
}
|
||||
|
||||
export function encodeVLQ(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
while (true) {
|
||||
const segment = x.mod(128);
|
||||
segments.unshift(segment.toNumber());
|
||||
x = x.sub(segment);
|
||||
x = x.div(128);
|
||||
|
||||
if (x.eq(0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = "0x";
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const keepGoing = i !== segments.length - 1;
|
||||
|
||||
const byte = (keepGoing ? 128 : 0) + segments[i];
|
||||
result += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodePseudoFloat(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
if (x.eq(0)) {
|
||||
return "0x00";
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
|
||||
while (x.mod(10).eq(0) && exponent < 30) {
|
||||
x = x.div(10);
|
||||
exponent++;
|
||||
}
|
||||
|
||||
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
|
||||
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
|
||||
|
||||
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return hexJoin([`0x${firstByte}`, encodeVLQ(x.div(8))]);
|
||||
}
|
||||
|
||||
export function encodeRegIndex(regIndex: BigNumberish) {
|
||||
regIndex = BigNumber.from(regIndex);
|
||||
|
||||
const vlqValue = regIndex.div(0x010000);
|
||||
const fixedValue = regIndex.mod(0x010000).toNumber();
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(vlqValue),
|
||||
`0x${fixedValue.toString(16).padStart(4, "0")}`,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit streams are just the bits of a uint256 encoded as a VLQ.
|
||||
* (Technically the encoding is unbounded, but 256 booleans is a lot and it's
|
||||
* much easier to just decode the VLQ into a uint256 in the EVM.)
|
||||
*
|
||||
* Notably, the bits are little endian - the first bit is the *lowest* bit. This
|
||||
* is because the lowest bit is clearly the 1-valued bit, but the highest valued
|
||||
* bit could be anywhere - there's infinitely many zero-bits to choose from.
|
||||
*
|
||||
* If it wasn't for this need to be little endian, we'd definitely use big
|
||||
* endian (like our other encodings generally do), since that's preferred by the
|
||||
* EVM and the ecosystem:
|
||||
*
|
||||
* ```ts
|
||||
* const abi = new ethers.utils.AbiCoder():
|
||||
* console.log(abi.encode(["uint"], [0xff]));
|
||||
* // 0x00000000000000000000000000000000000000000000000000000000000000ff
|
||||
*
|
||||
* // If Ethereum used little endian (like x86), it would instead be:
|
||||
* // 0xff00000000000000000000000000000000000000000000000000000000000000
|
||||
* ```
|
||||
*/
|
||||
export function encodeBitStream(bitStream: boolean[]) {
|
||||
let stream = 0;
|
||||
let bitValue = 1;
|
||||
|
||||
const abi = new ethers.utils.AbiCoder();
|
||||
abi.encode(["uint"], [0xff]);
|
||||
|
||||
for (const bit of bitStream) {
|
||||
if (bit) {
|
||||
stream += bitValue;
|
||||
}
|
||||
|
||||
bitValue *= 2;
|
||||
}
|
||||
|
||||
const streamVLQ = encodeVLQ(stream);
|
||||
|
||||
return streamVLQ;
|
||||
}
|
||||
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
|
||||
export default SignerOrProvider;
|
||||
@@ -1,81 +1,46 @@
|
||||
import Aggregator from "./Aggregator";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsSigner from "./BlsSigner";
|
||||
export { default as Aggregator } from "./Aggregator";
|
||||
export { default as BlsWalletWrapper } from "./BlsWalletWrapper";
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { VerificationGateway__factory } from "../typechain-types/factories/contracts/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain-types/contracts/VerificationGateway";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AggregatorUtilities__factory } from "../typechain-types/factories/contracts/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain-types/contracts/AggregatorUtilities";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { ERC20__factory } from "../typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { MockERC20__factory } from "../typechain-types/factories/contracts/mock/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain-types/contracts/mock/MockERC20";
|
||||
|
||||
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
import {
|
||||
export { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
export {
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
} from "./MultiNetworkConfig";
|
||||
export { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
import {
|
||||
export {
|
||||
OperationResult,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
OperationResultError,
|
||||
} from "./OperationResults";
|
||||
import { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
export {
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
ERC20__factory as ERC20Factory,
|
||||
MockERC20__factory as MockERC20Factory,
|
||||
type VerificationGateway,
|
||||
type AggregatorUtilities,
|
||||
type ERC20,
|
||||
type MockERC20,
|
||||
} from "../typechain-types";
|
||||
|
||||
export * from "./signer";
|
||||
|
||||
const Experimental_ = {
|
||||
BlsProvider,
|
||||
BlsSigner,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
namespace Experimental {
|
||||
export const BlsProvider = Experimental_.BlsProvider;
|
||||
export const BlsSigner = Experimental_.BlsSigner;
|
||||
}
|
||||
|
||||
export {
|
||||
Aggregator,
|
||||
BlsWalletWrapper,
|
||||
NetworkConfig,
|
||||
getConfig,
|
||||
validateConfig,
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
OperationResult,
|
||||
OperationResultError,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway,
|
||||
// eslint-disable-next-line camelcase
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilities,
|
||||
// eslint-disable-next-line camelcase
|
||||
ERC20__factory,
|
||||
ERC20,
|
||||
// eslint-disable-next-line camelcase
|
||||
MockERC20__factory,
|
||||
MockERC20,
|
||||
BlsWalletContracts,
|
||||
connectToContracts,
|
||||
Experimental,
|
||||
};
|
||||
default as SafeSingletonFactory,
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
export { default as AddressRegistryWrapper } from "./AddressRegistryWrapper";
|
||||
export { default as BlsPublicKeyRegistryWrapper } from "./BlsPublicKeyRegistryWrapper";
|
||||
export { default as FallbackCompressor } from "./FallbackCompressor";
|
||||
export { default as Erc20Compressor } from "./Erc20Compressor";
|
||||
export { default as BlsRegistrationCompressor } from "./BlsRegistrationCompressor";
|
||||
export { default as BundleCompressor } from "./BundleCompressor";
|
||||
export { default as ContractsConnector } from "./ContractsConnector";
|
||||
export * from "./encodeUtils";
|
||||
|
||||
@@ -12,6 +12,7 @@ export function bundleToDto(bundle: Bundle): BundleDto {
|
||||
]),
|
||||
operations: bundle.operations.map((op) => ({
|
||||
nonce: BigNumber.from(op.nonce).toHexString(),
|
||||
gas: BigNumber.from(op.gas).toHexString(),
|
||||
actions: op.actions.map((a) => ({
|
||||
ethValue: BigNumber.from(a.ethValue).toHexString(),
|
||||
contractAddress: a.contractAddress,
|
||||
|
||||
@@ -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)],
|
||||
);
|
||||
};
|
||||
|
||||
17
contracts/clients/src/signer/getDomain.ts
Normal file
17
contracts/clients/src/signer/getDomain.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { arrayify, solidityPack } from "ethers/lib/utils";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export default (
|
||||
name: string,
|
||||
version: string,
|
||||
chainId: number,
|
||||
verificationGatewayAddress: string,
|
||||
type: string,
|
||||
): Uint8Array => {
|
||||
const encoded = solidityPack(
|
||||
["string", "string", "uint256", "address", "string"],
|
||||
[name, version, 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,13 +16,13 @@ export * from "./conversions";
|
||||
export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
|
||||
|
||||
export async function initBlsWalletSigner({
|
||||
domain = defaultDomain,
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
}: {
|
||||
domain?: Uint8Array;
|
||||
chainId: number;
|
||||
privateKey: string;
|
||||
verificationGatewayAddress: string;
|
||||
}) {
|
||||
// Note: Getting signers via this factory ensures that mcl-wasm's underlying
|
||||
// init() has been called when signing. However, other operations such as
|
||||
@@ -32,14 +32,32 @@ export async function initBlsWalletSigner({
|
||||
// properly initialized for all use cases, not just signing.
|
||||
const signerFactory = await signer.BlsSignerFactory.new();
|
||||
|
||||
const domainName = "BLS_WALLET";
|
||||
const domainVersion = "1";
|
||||
|
||||
const bundleDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainId,
|
||||
verificationGatewayAddress,
|
||||
"Bundle",
|
||||
);
|
||||
const walletDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
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, privateKey),
|
||||
signMessage: signMessage(signerFactory, walletDomain, privateKey),
|
||||
verify: verify(bundleDomain),
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ import { Bundle, Operation } from "./types";
|
||||
export default (
|
||||
signerFactory: signer.BlsSignerFactory,
|
||||
domain: Uint8Array,
|
||||
chainId: number,
|
||||
privateKey: string,
|
||||
) =>
|
||||
(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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ActionData = {
|
||||
|
||||
export type Operation = {
|
||||
nonce: BigNumberish;
|
||||
gas: BigNumberish;
|
||||
actions: ActionData[];
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export type ActionDataDto = {
|
||||
|
||||
export type OperationDto = {
|
||||
nonce: string;
|
||||
gas: string;
|
||||
actions: ActionDataDto[];
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import encodeMessageForSigning from "./encodeMessageForSigning";
|
||||
import type { Bundle } from "./types";
|
||||
import isValidEmptyBundle from "./isValidEmptyBundle";
|
||||
|
||||
export default (domain: Uint8Array, chainId: number) =>
|
||||
export default (domain: Uint8Array) =>
|
||||
(bundle: Bundle, walletAddress: string): boolean => {
|
||||
// hubbleBls verifier incorrectly rejects empty bundles
|
||||
if (isValidEmptyBundle(bundle)) {
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { parseEther } from "ethers/lib/utils";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import BlsSigner, { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { BlsProvider, BlsSigner, bundleToDto } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { initBlsWalletSigner } from "../src/signer";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
let verificationGateway: string;
|
||||
@@ -12,25 +13,25 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
let regularProvider: ethers.providers.JsonRpcProvider;
|
||||
|
||||
describe("BlsProvider", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539, // 1337
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -60,51 +61,29 @@ describe("BlsProvider", () => {
|
||||
expect(uncheckedBlsSigner).to.be.instanceOf(UncheckedBlsSigner);
|
||||
});
|
||||
|
||||
it("should return a new signer if one has not been instantiated", async () => {
|
||||
it("should return a new signer", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
const newVerificationGateway = "newMockVerificationGatewayAddress";
|
||||
const newBlsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
newVerificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
|
||||
// Act
|
||||
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const newPrivateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
|
||||
|
||||
// Assert
|
||||
expect(newBlsSigner).to.not.equal(blsSigner);
|
||||
expect(newBlsSigner).to.equal(newBlsProvider.getSigner(newPrivateKey));
|
||||
});
|
||||
|
||||
it("should throw an error when this.signer has not been assigned", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
expect(newBlsSigner.provider.verificationGatewayAddress).to.not.equal(
|
||||
verificationGateway,
|
||||
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 +91,7 @@ describe("BlsProvider", () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
// Explicitly omit 'to'
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -124,28 +104,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 +132,155 @@ 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 = BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider = 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);
|
||||
});
|
||||
|
||||
it("should throw an error when sending multiple signed operations to sendTransaction", async () => {
|
||||
// Arrange
|
||||
const mockWalletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const { sign, aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress: verificationGateway,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const expectedAmount = parseEther("1");
|
||||
const verySafeFee = parseEther("0.1");
|
||||
const firstRecipient = ethers.Wallet.createRandom().address;
|
||||
const secondRecipient = ethers.Wallet.createRandom().address;
|
||||
|
||||
const firstActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: firstRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
const secondActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: secondRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
|
||||
const nonce = BigNumber.from(0);
|
||||
|
||||
const firstOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...firstActionWithSafeFee],
|
||||
};
|
||||
const secondOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...secondActionWithSafeFee],
|
||||
};
|
||||
|
||||
const firstBundle = sign(firstOperation, mockWalletAddress);
|
||||
const secondBundle = sign(secondOperation, mockWalletAddress);
|
||||
|
||||
const aggregatedBundle = aggregate([firstBundle, secondBundle]);
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(
|
||||
JSON.stringify(bundleToDto(aggregatedBundle)),
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.rejectedWith(
|
||||
Error,
|
||||
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import { BlsProvider, BlsSigner } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
@@ -11,23 +11,23 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
describe("BlsSigner", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539,
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -74,13 +74,13 @@ describe("BlsSigner", () => {
|
||||
|
||||
// Assert
|
||||
expect(provider._isProvider).to.be.true;
|
||||
expect(provider).to.be.instanceOf(Experimental.BlsProvider);
|
||||
expect(provider).to.be.instanceOf(BlsProvider);
|
||||
});
|
||||
|
||||
it("should detect whether an object is a valid signer", async () => {
|
||||
// Arrange & Act
|
||||
const validSigner = Experimental.BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = Experimental.BlsSigner.isSigner({});
|
||||
const validSigner = BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = BlsSigner.isSigner({});
|
||||
|
||||
// Assert
|
||||
expect(validSigner).to.be.true;
|
||||
@@ -94,4 +94,27 @@ describe("BlsSigner", () => {
|
||||
// Assert
|
||||
expect(connect).to.throw(Error, "cannot alter JSON-RPC Signer connection");
|
||||
});
|
||||
|
||||
it("should throw error for wrong chain id when validating batch options", async () => {
|
||||
// Arrange
|
||||
const invalidChainId = 123;
|
||||
const batchOptions = {
|
||||
gas: BigNumber.from("40000"),
|
||||
maxPriorityFeePerGas: ethers.utils.parseUnits("0.5", "gwei"),
|
||||
maxFeePerGas: ethers.utils.parseUnits("23", "gwei"),
|
||||
nonce: 1,
|
||||
chainId: invalidChainId,
|
||||
accessList: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsSigner._validateBatchOptions(batchOptions);
|
||||
|
||||
// Assert
|
||||
expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
`Supplied chain ID ${invalidChainId} does not match the expected chain ID 1337`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,16 +112,6 @@ describe("OperationResults", () => {
|
||||
});
|
||||
|
||||
describe("getOperationResults", () => {
|
||||
it("fails if no events are in transaction", () => {
|
||||
const txnReceipt = {
|
||||
transactionHash: "0x111111",
|
||||
} as ContractReceipt;
|
||||
|
||||
expect(() => getOperationResults(txnReceipt)).to.throw(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails if no WalletOperationProcessed events are in transaction", () => {
|
||||
const event = { event: "Other" };
|
||||
const txnReceipt = {
|
||||
@@ -222,5 +212,159 @@ describe("OperationResults", () => {
|
||||
expect(r2.results).to.deep.equal(successfulEvent.args.results);
|
||||
expect(r2.error).to.eql(undefined);
|
||||
});
|
||||
|
||||
it("decodes WalletOperationProcessed logs", () => {
|
||||
const receipt = {
|
||||
// Sometimes you get .events but WalletOperationProcessed are not
|
||||
// decoded, this helps cover that case.
|
||||
// Eg: calling blsExpanderDelegator.run
|
||||
events: [] as unknown[],
|
||||
|
||||
logs: [
|
||||
// Unrelated log
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
topics: [
|
||||
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
|
||||
"0x000000000000000000000000e619cf09e1f0eb1f9172431ec49dbef4747f8fe7",
|
||||
],
|
||||
data: "0x",
|
||||
logIndex: 0,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x00000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a977",
|
||||
],
|
||||
data: "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed4200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 4,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for failing transfer (insufficient funds)
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed42",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000645c66760100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 9,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x0000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000429d069189e000000000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a97700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 14,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
],
|
||||
} as ContractReceipt;
|
||||
|
||||
const results = getOperationResults(receipt);
|
||||
|
||||
// Helps to smooth out oddities like `error: undefined` which we don't
|
||||
// care about
|
||||
const normalizedResults = JSON.parse(JSON.stringify(results));
|
||||
|
||||
expect(normalizedResults).to.deep.eq([
|
||||
{
|
||||
walletAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x016345785d8a0000",
|
||||
},
|
||||
contractAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
{
|
||||
walletAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x1bc16d674ec80000",
|
||||
},
|
||||
contractAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
results: [
|
||||
"0x5c667601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
|
||||
],
|
||||
error: {
|
||||
actionIndex: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
message: "Unexpected action error data: 0x",
|
||||
},
|
||||
},
|
||||
{
|
||||
walletAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x0429d069189e0000",
|
||||
},
|
||||
contractAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "chai";
|
||||
import { NetworkConfig } from "../src";
|
||||
import {
|
||||
UnvalidatedMultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
@@ -7,20 +8,20 @@ import {
|
||||
const getValue = (networkKey: string, propName: string) =>
|
||||
`${networkKey}-${propName}`;
|
||||
|
||||
const getSingleConfig = (networkKey: string) => ({
|
||||
const getSingleConfig = (networkKey: string): NetworkConfig => ({
|
||||
parameters: {},
|
||||
addresses: {
|
||||
create2Deployer: getValue(networkKey, "create2Deployer"),
|
||||
safeSingletonFactory: getValue(networkKey, "safeSingletonFactory"),
|
||||
precompileCostEstimator: getValue(networkKey, "precompileCostEstimator"),
|
||||
verificationGateway: getValue(networkKey, "verificationGateway"),
|
||||
blsLibrary: getValue(networkKey, "blsLibrary"),
|
||||
blsExpander: getValue(networkKey, "blsExpander"),
|
||||
utilities: getValue(networkKey, "utilities"),
|
||||
testToken: getValue(networkKey, "testToken"),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: 123,
|
||||
domain: getValue(networkKey, "domain"),
|
||||
walletDomain: getValue(networkKey, "walletDomain"),
|
||||
bundleDomain: getValue(networkKey, "bundleDomain"),
|
||||
genesisBlock: 456,
|
||||
deployedBy: getValue(networkKey, "deployedBy"),
|
||||
version: getValue(networkKey, "version"),
|
||||
@@ -71,13 +72,6 @@ describe("MultiNetworkConfig", () => {
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network1}.addresses.blsLibrary is set to a number`, async () => {
|
||||
validConfig[network1].addresses.blsLibrary = 1337;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network2}.auxiliary.chainid is set to a string`, async () => {
|
||||
validConfig[network1].auxiliary.chainid = "off-the-chain";
|
||||
|
||||
|
||||
@@ -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 = (() => {
|
||||
@@ -15,9 +13,12 @@ const samples = (() => {
|
||||
// Random addresses
|
||||
const walletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const otherWalletAddress = "0x42C8157D539825daFD6586B119db53761a2a91CD";
|
||||
const verificationGatewayAddress =
|
||||
"0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
|
||||
const bundleTemplate: Operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: BigNumber.from(0),
|
||||
@@ -42,6 +43,7 @@ const samples = (() => {
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -52,22 +54,22 @@ describe("index", () => {
|
||||
|
||||
const { sign, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const bundle = sign(bundleTemplate, walletAddress);
|
||||
|
||||
expect(bundle.signature).to.deep.equal([
|
||||
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
|
||||
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
|
||||
"0x117171c1a4af03133390b454989658d9c6ae7a7fe1c3958ad545e584e63ab5e3",
|
||||
"0x2f90b24bbc03de665816b3a632e0c7b5fb837c87541d9337480671613cf1359c",
|
||||
]);
|
||||
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -111,12 +113,12 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -125,8 +127,8 @@ describe("index", () => {
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(aggBundle.signature).to.deep.equal([
|
||||
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
|
||||
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
|
||||
"0x18b917c1f52155d9748025bd94aa07c0017af31dd2ef2a00289931f660e88ec9",
|
||||
"0x0235a99bcd1f0793efb7f3307cd349f211a433f60cfab795f5f976298f17a768",
|
||||
]);
|
||||
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
@@ -163,7 +165,7 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -195,7 +197,7 @@ describe("index", () => {
|
||||
|
||||
const { getPublicKeyStr } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -215,7 +217,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -230,7 +232,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
|
||||
25
contracts/contracts/AddressRegistry.sol
Normal file
25
contracts/contracts/AddressRegistry.sol
Normal file
@@ -0,0 +1,25 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract AddressRegistry {
|
||||
mapping(uint256 => address) public addresses;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event AddressRegistered(uint256 id, address indexed addr);
|
||||
|
||||
function register(address addr) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
addresses[id] = addr;
|
||||
|
||||
emit AddressRegistered(id, addr);
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (address) {
|
||||
address addr = addresses[id];
|
||||
require(addr != address(0), "AddressRegistry: Address not found");
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ contract BLSExpander {
|
||||
function blsCallMultiCheckRewardIncrease(
|
||||
IERC20 tokenRewardAddress,
|
||||
uint256 tokenRewardAmount,
|
||||
VerificationGateway.Bundle calldata bundle
|
||||
IWallet.Bundle calldata bundle
|
||||
// uint256[4][] calldata publicKeys,
|
||||
// uint256[2] memory signature,
|
||||
// VerificationGateway.TxSet[] calldata txs
|
||||
@@ -102,6 +102,7 @@ contract BLSExpander {
|
||||
function blsCallMultiSameCallerContractFunction(
|
||||
uint256[4] calldata publicKey,
|
||||
uint256 nonce,
|
||||
uint256 gas,
|
||||
uint256[2] calldata signature,
|
||||
address contractAddress,
|
||||
bytes4 methodId,
|
||||
@@ -109,7 +110,7 @@ contract BLSExpander {
|
||||
) external {
|
||||
uint256 length = encodedParamSets.length;
|
||||
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = signature;
|
||||
|
||||
bundle.senderPublicKeys = new uint256[4][](1);
|
||||
@@ -117,6 +118,7 @@ contract BLSExpander {
|
||||
|
||||
bundle.operations = new IWallet.Operation[](1);
|
||||
bundle.operations[0].nonce = nonce;
|
||||
bundle.operations[0].gas = gas;
|
||||
bundle.operations[0].actions = new IWallet.ActionData[](length);
|
||||
for (uint256 i=0; i<length; i++) {
|
||||
bundle.operations[0].actions[i].ethValue = 0;
|
||||
@@ -185,7 +187,7 @@ contract BLSExpander {
|
||||
}
|
||||
|
||||
// Use them to re-create bundle
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = addressBundle.signature;
|
||||
bundle.senderPublicKeys = senderPublicKeys;
|
||||
bundle.operations = addressBundle.operations;
|
||||
|
||||
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
@@ -0,0 +1,94 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./lib/VLQ.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
|
||||
// Aka gateway, but we only care about processBundle here
|
||||
interface IBundleProcessor {
|
||||
function processBundle(
|
||||
IWallet.Bundle memory bundle
|
||||
) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
);
|
||||
}
|
||||
|
||||
contract BLSExpanderDelegator {
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBundleProcessor gateway;
|
||||
|
||||
uint256 public nextExpanderId = 0;
|
||||
mapping(uint256 => IExpander) public expanders;
|
||||
event ExpanderRegistered(uint256 id, IExpander indexed expanderAddress);
|
||||
|
||||
constructor(IBundleProcessor gatewayParam) {
|
||||
gateway = gatewayParam;
|
||||
}
|
||||
|
||||
function run(bytes calldata stream) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
) {
|
||||
IWallet.Bundle memory bundle;
|
||||
|
||||
// The signature is just the last 64 bytes.
|
||||
uint256 opsByteLen = stream.length - 64;
|
||||
bundle.signature = abi.decode(stream[opsByteLen:], (uint256[2]));
|
||||
stream = stream[:opsByteLen]; // Only keep what we still need
|
||||
|
||||
uint256 vlqValue;
|
||||
|
||||
// Get the number of operations upfront so that we can allocate the
|
||||
// memory. This information is technically redundant but extracting it
|
||||
// by decoding everything before we have a place to put it would add a
|
||||
// lot of complexity. For <=127 operations it's only one extra byte.
|
||||
// Otherwise 2 bytes.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
|
||||
bundle.senderPublicKeys = new uint256[BLS_KEY_LEN][](vlqValue);
|
||||
bundle.operations = new IWallet.Operation[](vlqValue);
|
||||
|
||||
uint256 opsDecoded = 0;
|
||||
|
||||
while (stream.length > 0) {
|
||||
// First figure out which expander to use.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
IExpander expander = expanders[vlqValue];
|
||||
require(expander != IExpander(address(0)), "expander not found");
|
||||
|
||||
// Then use it to expand the operation.
|
||||
(
|
||||
uint256[BLS_KEY_LEN] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 expanderBytesRead
|
||||
) = expander.expand(stream);
|
||||
|
||||
// It would be more consistent to have .expand above return the new
|
||||
// stream, but there appears to be a bug in solidity that prevents
|
||||
// this.
|
||||
stream = stream[expanderBytesRead:];
|
||||
|
||||
bundle.senderPublicKeys[opsDecoded] = senderPublicKey;
|
||||
bundle.operations[opsDecoded] = operation;
|
||||
|
||||
opsDecoded++;
|
||||
}
|
||||
|
||||
// Finished expanding. Now just return the call.
|
||||
return gateway.processBundle(bundle);
|
||||
}
|
||||
|
||||
function registerExpander(
|
||||
IExpander expander
|
||||
) external {
|
||||
uint256 expanderId = nextExpanderId;
|
||||
nextExpanderId += 1;
|
||||
expanders[expanderId] = expander;
|
||||
|
||||
emit ExpanderRegistered(expanderId, expander);
|
||||
}
|
||||
}
|
||||
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
@@ -0,0 +1,35 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract BLSPublicKeyRegistry {
|
||||
mapping(uint256 => uint256[4]) public blsPublicKeys;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event BLSPublicKeyRegistered(uint256 id, bytes32 indexed blsPublicKeyHash);
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
blsPublicKeys[id] = blsPublicKey;
|
||||
|
||||
emit BLSPublicKeyRegistered(id, keccak256(abi.encode(blsPublicKey)));
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (uint256[4] memory) {
|
||||
uint256[4] memory blsPublicKey = blsPublicKeys[id];
|
||||
require(!isZeroBLSPublicKey(blsPublicKey), "BLSPublicKeyRegistry: BLS public key not found");
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
function isZeroBLSPublicKey(uint256[4] memory blsPublicKey) internal pure returns (bool) {
|
||||
for (uint i = 0; i < 4; i++) {
|
||||
if (blsPublicKey[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
81
contracts/contracts/BLSRegistration.sol
Normal file
81
contracts/contracts/BLSRegistration.sol
Normal file
@@ -0,0 +1,81 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AddressRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
|
||||
contract BLSRegistration is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
|
||||
uint256 nonce;
|
||||
(nonce, stream) = VLQ.decode(stream);
|
||||
|
||||
uint256 gas;
|
||||
(gas, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 txOriginPayment;
|
||||
(txOriginPayment, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 actionLen = 1;
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
actionLen += 1;
|
||||
}
|
||||
|
||||
operation = IWallet.Operation({
|
||||
nonce: nonce,
|
||||
gas: gas,
|
||||
actions: new IWallet.ActionData[](actionLen)
|
||||
});
|
||||
|
||||
operation.actions[0] = IWallet.ActionData({
|
||||
ethValue: 0,
|
||||
contractAddress: address(this),
|
||||
encodedFunction: abi.encodeWithSignature(
|
||||
"register(uint256[4])",
|
||||
senderPublicKey
|
||||
)
|
||||
});
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
operation.actions[1] = IWallet.ActionData({
|
||||
ethValue: txOriginPayment,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
blsPublicKeyRegistry.register(blsPublicKey);
|
||||
addressRegistry.register(msg.sender);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ pragma abicoder v2;
|
||||
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
interface IVerificationGateway {
|
||||
function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bool);
|
||||
}
|
||||
|
||||
/** Minimal upgradable smart contract wallet.
|
||||
Generic calls can only be requested by its trusted gateway.
|
||||
@@ -194,6 +197,22 @@ contract BLSWallet is Initializable, IWallet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev ERC-1271 signature validation
|
||||
*/
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) public view returns (bytes4 magicValue) {
|
||||
bool verified = IVerificationGateway(trustedBLSGateway).isValidSignature(hash, signature);
|
||||
|
||||
if (verified) {
|
||||
magicValue = 0x1626ba7e;
|
||||
} else {
|
||||
magicValue = 0xffffffff;
|
||||
}
|
||||
}
|
||||
|
||||
function stripMethodId(bytes memory encodedFunction) pure private returns(bytes memory) {
|
||||
bytes memory params = new bytes(encodedFunction.length - 4);
|
||||
for (uint256 i=0; i<params.length; i++) {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
|
||||
contract Create2Deployer {
|
||||
event Deployed(address sender, uint256 salt, address addr);
|
||||
|
||||
function addressFrom(
|
||||
address sender,
|
||||
uint256 salt,
|
||||
bytes calldata code
|
||||
) public pure returns (
|
||||
address predictedAddress
|
||||
) {
|
||||
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
sender,
|
||||
salt,
|
||||
keccak256(code)
|
||||
)))));
|
||||
}
|
||||
|
||||
function deploy(
|
||||
uint256 salt,
|
||||
bytes memory code
|
||||
) public {
|
||||
address addr;
|
||||
assembly {
|
||||
addr := create2(0, add(code, 0x20), mload(code), salt)
|
||||
if iszero(extcodesize(addr)) {
|
||||
revert(0, 0)
|
||||
}
|
||||
}
|
||||
emit Deployed(msg.sender, salt, addr);
|
||||
}
|
||||
|
||||
}
|
||||
316
contracts/contracts/ERC20Expander.sol
Normal file
316
contracts/contracts/ERC20Expander.sol
Normal file
@@ -0,0 +1,316 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./AddressRegistry.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/RegIndex.sol";
|
||||
import "./lib/VLQ.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
/**
|
||||
* An expander that supports ERC20 operations.
|
||||
*
|
||||
* This is a bit of a first pass, it could be more compact:
|
||||
* - Optimize specifically for transfer
|
||||
* - Require registries, removing the need for a bit stream
|
||||
* - Use a fixed gas value
|
||||
* - Use a dedicated ERC20 registry, allowing a single byte for popular
|
||||
* currencies (and 2 bytes for all but the most obscure currencies)
|
||||
* - Use a fixed tx.origin reward (or call contract that computes an appropriate
|
||||
* reward)
|
||||
*
|
||||
* This would get us down to about 11 bytes. (Current example is 20 bytes.)
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 0f - 0x0f = 0b1111 bit stream:
|
||||
* - 1: Use registry for BLS key
|
||||
* - 1: Include a tx.origin payment
|
||||
* - 1: Use registry for ERC20 address
|
||||
* - 1: Use registry for recipient address
|
||||
* 000000 - Registry index for sendWallet's public key
|
||||
* 00 - nonce: 0
|
||||
* 0bda28 - gas: 92,483
|
||||
* 02 - two actions
|
||||
* 000000 - Registry index for ERC20 address
|
||||
* 00 - transfer
|
||||
* 000002 - Registry index for recipient address
|
||||
* 9100 - 0.1 MCK (amount/value)
|
||||
* 6d00 - Pay 0.000005 ETH to tx.origin
|
||||
*/
|
||||
contract ERC20Expander is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
uint256 bitStream;
|
||||
|
||||
(bitStream, stream) = VLQ.decode(stream);
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(
|
||||
bitStream
|
||||
);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
senderPublicKey = blsPublicKeyRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
operation.nonce = decodedValue;
|
||||
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
operation.gas = decodedValue;
|
||||
|
||||
uint256 actionLen;
|
||||
(actionLen, stream) = VLQ.decode(stream);
|
||||
operation.actions = new IWallet.ActionData[](actionLen);
|
||||
|
||||
// hasTxOriginPayment
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
// We would use a separate variable for this, but the solidity
|
||||
// compiler makes it important to minimize local variables.
|
||||
actionLen -= 1;
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < actionLen; i++) {
|
||||
(
|
||||
operation.actions[i].contractAddress,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeAddress(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
|
||||
(
|
||||
operation.actions[i].encodedFunction,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeFunctionCall(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
if (actionLen < operation.actions.length) {
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
operation.actions[actionLen] = IWallet.ActionData({
|
||||
ethValue: decodedValue,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function decodeBit(uint256 bitStream) internal pure returns (bool, uint256) {
|
||||
return ((bitStream & 1) == 1, bitStream >> 1);
|
||||
}
|
||||
|
||||
// Following the naming convention this would be called
|
||||
// decodeEncodedFunction, but that's pretty confusing.
|
||||
function decodeFunctionCall(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 methodIndex;
|
||||
(methodIndex, stream) = VLQ.decode(stream);
|
||||
|
||||
if (methodIndex == 0) {
|
||||
return decodeTransfer(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 1) {
|
||||
return decodeTransferFrom(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 2) {
|
||||
return decodeApprove(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 3) {
|
||||
// Not a real method, but uint256Max is common for approve, and is
|
||||
// not represented efficiently by PseudoFloat.
|
||||
return decodeApproveMax(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 4) {
|
||||
return decodeMint(stream, bitStream);
|
||||
}
|
||||
|
||||
revert("Unrecognized ERC20 method index");
|
||||
}
|
||||
|
||||
function decodeTransfer(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("transfer(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeTransferFrom(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address from;
|
||||
(from, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"transferFrom(address,address,uint256)",
|
||||
from,
|
||||
to,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApprove(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApproveMax(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
type(uint256).max
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeMint(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("mint(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeAddress(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
address,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
return (addressRegistry.lookup(decodedValue), stream, bitStream);
|
||||
}
|
||||
|
||||
return (address(bytes20(stream[:20])), stream[20:], bitStream);
|
||||
}
|
||||
}
|
||||
24
contracts/contracts/ExpanderEntryPoint.sol
Normal file
24
contracts/contracts/ExpanderEntryPoint.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
interface IExpanderDelegator {
|
||||
function run(
|
||||
bytes calldata stream
|
||||
) external returns (bool[] memory successes, bytes[][] memory results);
|
||||
}
|
||||
|
||||
contract ExpanderEntryPoint {
|
||||
IExpanderDelegator expanderDelegator;
|
||||
|
||||
constructor(IExpanderDelegator expanderDelegatorParam) {
|
||||
expanderDelegator = expanderDelegatorParam;
|
||||
}
|
||||
|
||||
fallback(bytes calldata stream) external returns (bytes memory) {
|
||||
(bool[] memory successes, bytes[][] memory results) = expanderDelegator
|
||||
.run{ gas: type(uint256).max }(stream);
|
||||
|
||||
return abi.encode(successes, results);
|
||||
}
|
||||
}
|
||||
151
contracts/contracts/FallbackExpander.sol
Normal file
151
contracts/contracts/FallbackExpander.sol
Normal file
@@ -0,0 +1,151 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./AddressRegistry.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/RegIndex.sol";
|
||||
import "./lib/VLQ.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
/**
|
||||
* An expander that supports any operation.
|
||||
*
|
||||
* This is still a more compact encoding due to the use of VLQ and general byte
|
||||
* packing in several places where the solidity abi would just use a 32-byte
|
||||
* word.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 0x
|
||||
* 2409925687d52a67b435a011cf9ec82d390300cd12e5842d2a0c5e1c27898551 // BLS key
|
||||
* 0c4a8cbcc96cada40301e1d2a2d68425b5cf0e18f5cb12fa272f841017c36776 // BLS key
|
||||
* 27b9f42b237d75bcb0473e2eada290e62ec77048187484f8952fffe0239f7ba9 // BLS key
|
||||
* 24f1fc8a1f7256dc2914e524966309df2226fd329373aaaae1881bf5cd0c62f4 // BLS key
|
||||
*
|
||||
* 00 // nonce: 0
|
||||
* 3100 // gas: 100,000
|
||||
* 02 // two actions
|
||||
*
|
||||
* // Action 1
|
||||
* 7b0f // ethValue: 12300000000000000 (0.0123 ETH)
|
||||
* 70997970c51812dc3a010c7d01b50e0d17dc79c8 // contractAddress
|
||||
* 00 // encodedFunction: (empty)
|
||||
*
|
||||
* // Action 2
|
||||
* 6c01 // ethValue: 12000000000000 (0.000012 ETH)
|
||||
* 4bd2e4e99b50a2a9e6b9dabfa3c8dcd1f885f008 // contractAddress (AggUtils)
|
||||
* 04 // 4 bytes for encodedFunction
|
||||
* 1dfea6a0 // sendEthToTxOrigin
|
||||
*
|
||||
* The proposal doc for the new expander lists the same example ("Example of an
|
||||
* Expanded User Operation" https://hackmd.io/0q7H3Ad0Su-I4RWWK8wQPA) using the
|
||||
* solidity ABI, which uses 608 bytes. Here we've encoded the same thing (plus
|
||||
* gas) in 182 bytes, which is 70% smaller. (If you account for the zero-byte
|
||||
* discount, the saving is still over 30%.)
|
||||
*/
|
||||
contract FallbackExpander is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
uint256 bitStream;
|
||||
|
||||
(bitStream, stream) = VLQ.decode(stream);
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(
|
||||
bitStream
|
||||
);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
senderPublicKey = blsPublicKeyRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
operation.nonce = decodedValue;
|
||||
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
operation.gas = decodedValue;
|
||||
|
||||
uint256 actionLen;
|
||||
(actionLen, stream) = VLQ.decode(stream);
|
||||
operation.actions = new IWallet.ActionData[](actionLen);
|
||||
|
||||
// hasTxOriginPayment
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
// We would use a separate variable for this, but the solidity
|
||||
// compiler makes it important to minimize local variables.
|
||||
actionLen -= 1;
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < actionLen; i++) {
|
||||
uint256 ethValue;
|
||||
(ethValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
address contractAddress;
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
contractAddress = addressRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
contractAddress = address(bytes20(stream[:20]));
|
||||
stream = stream[20:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
bytes memory encodedFunction = stream[:decodedValue];
|
||||
stream = stream[decodedValue:];
|
||||
|
||||
operation.actions[i] = IWallet.ActionData({
|
||||
ethValue: ethValue,
|
||||
contractAddress: contractAddress,
|
||||
encodedFunction: encodedFunction
|
||||
});
|
||||
}
|
||||
|
||||
if (actionLen < operation.actions.length) {
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
operation.actions[actionLen] = IWallet.ActionData({
|
||||
ethValue: decodedValue,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function decodeBit(uint256 bitStream) internal pure returns (bool, uint256) {
|
||||
return ((bitStream & 1) == 1, bitStream >> 1);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
|
||||
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
||||
|
||||
import "./interfaces/IWallet.sol";
|
||||
import "./BLSWallet.sol";
|
||||
|
||||
/**
|
||||
A non-upgradable gateway used to create BLSWallets and call them with
|
||||
@@ -18,28 +19,24 @@ 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;
|
||||
string constant BLS_DOMAIN_NAME = "BLS_WALLET";
|
||||
string constant BLS_DOMAIN_VERSION = "1";
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBLS public immutable blsLib;
|
||||
ProxyAdmin public immutable walletProxyAdmin;
|
||||
address public immutable blsWalletLogic;
|
||||
ProxyAdmin public immutable walletProxyAdmin = new ProxyAdmin();
|
||||
BLSWallet public immutable blsWalletLogic = new BLSWallet();
|
||||
mapping(bytes32 => IWallet) public walletFromHash;
|
||||
mapping(IWallet => bytes32) public hashFromWallet;
|
||||
mapping(bytes32 => uint256[BLS_KEY_LEN]) public BLSPublicKeyFromHash;
|
||||
|
||||
//mapping from an existing wallet's bls key hash to pending variables when setting a new BLS key
|
||||
// mapping from an existing wallet's bls key hash to pending variables when setting a new BLS key
|
||||
mapping(bytes32 => uint256[BLS_KEY_LEN]) public pendingBLSPublicKeyFromHash;
|
||||
mapping(bytes32 => uint256[2]) public pendingMessageSenderSignatureFromHash;
|
||||
mapping(bytes32 => uint256) public pendingBLSPublicKeyTimeFromHash;
|
||||
|
||||
/** Aggregated signature with corresponding senders + operations */
|
||||
struct Bundle {
|
||||
uint256[2] signature;
|
||||
uint256[BLS_KEY_LEN][] senderPublicKeys;
|
||||
IWallet.Operation[] operations;
|
||||
}
|
||||
|
||||
event WalletCreated(
|
||||
address indexed wallet,
|
||||
uint256[BLS_KEY_LEN] publicKey
|
||||
@@ -65,24 +62,33 @@ contract VerificationGateway
|
||||
/**
|
||||
@param bls verified bls library contract address
|
||||
*/
|
||||
constructor(
|
||||
IBLS bls,
|
||||
address blsWalletImpl,
|
||||
address proxyAdmin
|
||||
) {
|
||||
constructor(IBLS bls) {
|
||||
blsLib = bls;
|
||||
blsWalletLogic = blsWalletImpl;
|
||||
walletProxyAdmin = ProxyAdmin(proxyAdmin);
|
||||
blsWalletLogic.initialize(address(0));
|
||||
WALLET_DOMAIN = keccak256(abi.encodePacked(
|
||||
BLS_DOMAIN_NAME,
|
||||
BLS_DOMAIN_VERSION,
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Wallet"
|
||||
));
|
||||
BUNDLE_DOMAIN = keccak256(abi.encodePacked(
|
||||
BLS_DOMAIN_NAME,
|
||||
BLS_DOMAIN_VERSION,
|
||||
block.chainid,
|
||||
address(this),
|
||||
"Bundle"
|
||||
));
|
||||
}
|
||||
|
||||
/** Throw if bundle not valid or signature verification fails */
|
||||
function verify(
|
||||
Bundle memory bundle
|
||||
IWallet.Bundle memory bundle
|
||||
) public view {
|
||||
uint256 opLength = bundle.operations.length;
|
||||
require(
|
||||
opLength == bundle.senderPublicKeys.length,
|
||||
"VG: Sender/op length mismatch"
|
||||
"VG: length mismatch"
|
||||
);
|
||||
uint256[2][] memory messages = new uint256[2][](opLength);
|
||||
|
||||
@@ -91,7 +97,7 @@ contract VerificationGateway
|
||||
bytes32 keyHash = keccak256(abi.encodePacked(bundle.senderPublicKeys[i]));
|
||||
address walletAddress = address(walletFromHash[keyHash]);
|
||||
if (walletAddress == address(0)) {
|
||||
walletAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
walletAddress = address(uint160(uint256(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
address(this),
|
||||
keyHash,
|
||||
@@ -120,6 +126,29 @@ contract VerificationGateway
|
||||
require(verified, "VG: Sig not verified");
|
||||
}
|
||||
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) public view returns (bool verified) {
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
require(existingHash != 0, "VG: not called from wallet");
|
||||
|
||||
uint256[BLS_KEY_LEN] memory publicKey = BLSPublicKeyFromHash[existingHash];
|
||||
|
||||
bytes memory hashBytes = abi.encode(hash);
|
||||
|
||||
uint256[2] memory message = blsLib.hashToPoint(
|
||||
WALLET_DOMAIN,
|
||||
hashBytes
|
||||
);
|
||||
|
||||
require(signature.length == 64, "VG: Sig bytes length must be 64");
|
||||
uint256[2] memory decodedSignature = abi.decode(signature, (uint256[2]));
|
||||
|
||||
verified = blsLib.verifySingle(decodedSignature, publicKey, message);
|
||||
}
|
||||
|
||||
/**
|
||||
If an existing wallet contract wishes to be called by this verification
|
||||
gateway, it can directly register itself with a simple signed msg.
|
||||
@@ -128,17 +157,19 @@ contract VerificationGateway
|
||||
@dev overrides previous wallet address registered with the given public key
|
||||
@param messageSenderSignature signature of message containing only the calling address
|
||||
@param publicKey that signed the caller's address
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function setBLSKeyForWallet(
|
||||
uint256[2] memory messageSenderSignature,
|
||||
uint256[BLS_KEY_LEN] memory publicKey
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) public {
|
||||
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: publicKey must be non-zero");
|
||||
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: key is zero");
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
if (existingHash == bytes32(0)) { // wallet does not yet have a bls key registered with this gateway
|
||||
// set it instantly
|
||||
safeSetWallet(messageSenderSignature, publicKey, wallet);
|
||||
safeSetWallet(messageSenderSignature, publicKey, wallet, signatureExpiryTimestamp);
|
||||
}
|
||||
else { // wallet already has a key registered, set after delay
|
||||
pendingMessageSenderSignatureFromHash[existingHash] = messageSenderSignature;
|
||||
@@ -147,11 +178,13 @@ contract VerificationGateway
|
||||
emit PendingBLSKeySet(existingHash, publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
function setPendingBLSKeyForWallet() public {
|
||||
/**
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function setPendingBLSKeyForWallet(uint256 signatureExpiryTimestamp) public {
|
||||
IWallet wallet = IWallet(msg.sender);
|
||||
bytes32 existingHash = hashFromWallet[wallet];
|
||||
require(existingHash != bytes32(0), "VG: hash does not exist for caller");
|
||||
require(existingHash != bytes32(0), "VG: hash not found");
|
||||
if (
|
||||
(pendingBLSPublicKeyTimeFromHash[existingHash] != 0) &&
|
||||
(block.timestamp > pendingBLSPublicKeyTimeFromHash[existingHash])
|
||||
@@ -159,7 +192,8 @@ contract VerificationGateway
|
||||
safeSetWallet(
|
||||
pendingMessageSenderSignatureFromHash[existingHash],
|
||||
pendingBLSPublicKeyFromHash[existingHash],
|
||||
wallet
|
||||
wallet,
|
||||
signatureExpiryTimestamp
|
||||
);
|
||||
pendingMessageSenderSignatureFromHash[existingHash] = [0,0];
|
||||
pendingBLSPublicKeyTimeFromHash[existingHash] = 0;
|
||||
@@ -196,7 +230,7 @@ contract VerificationGateway
|
||||
for (uint256 i=0; i<32; i++) {
|
||||
require(
|
||||
(encodedFunction[selectorOffset+i] == encodedAddress[i]),
|
||||
"VG: first param to proxy admin is not calling wallet"
|
||||
"VG: first param is not wallet"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -226,19 +260,21 @@ contract VerificationGateway
|
||||
@param blsKeyHash calling wallet's bls public key hash
|
||||
@param salt used in the recovery hash
|
||||
@param newBLSKey to set as the wallet's bls public key
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function recoverWallet(
|
||||
uint256[2] memory walletAddressSignature,
|
||||
bytes32 blsKeyHash,
|
||||
bytes32 salt,
|
||||
uint256[BLS_KEY_LEN] memory newBLSKey
|
||||
uint256[BLS_KEY_LEN] memory newBLSKey,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) public {
|
||||
IWallet wallet = walletFromHash[blsKeyHash];
|
||||
bytes32 recoveryHash = keccak256(
|
||||
abi.encodePacked(msg.sender, blsKeyHash, salt)
|
||||
);
|
||||
if (recoveryHash == wallet.recoveryHash()) {
|
||||
safeSetWallet(walletAddressSignature, newBLSKey, wallet);
|
||||
safeSetWallet(walletAddressSignature, newBLSKey, wallet, signatureExpiryTimestamp);
|
||||
wallet.recover();
|
||||
}
|
||||
}
|
||||
@@ -255,7 +291,7 @@ contract VerificationGateway
|
||||
assembly { size := extcodesize(blsGateway) }
|
||||
require(
|
||||
(blsGateway != address(0)) && (size > 0),
|
||||
"BLSWallet: gateway address param not valid"
|
||||
"VG: invalid gateway"
|
||||
);
|
||||
|
||||
IWallet wallet = walletFromHash[hash];
|
||||
@@ -281,7 +317,7 @@ contract VerificationGateway
|
||||
Can be called with a single operation with no actions.
|
||||
*/
|
||||
function processBundle(
|
||||
Bundle memory bundle
|
||||
IWallet.Bundle memory bundle
|
||||
) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
@@ -296,12 +332,12 @@ contract VerificationGateway
|
||||
IWallet wallet = getOrCreateWallet(bundle.senderPublicKeys[i]);
|
||||
|
||||
// check nonce then perform action
|
||||
if (bundle.operations[i].nonce == wallet.nonce()) {
|
||||
if (bundle.operations[i].nonce == wallet.nonce{gas:20000}()) {
|
||||
// request wallet perform operation
|
||||
(
|
||||
bool success,
|
||||
bytes[] memory resultSet
|
||||
) = wallet.performOperation(bundle.operations[i]);
|
||||
) = wallet.performOperation{gas:bundle.operations[i].gas}(bundle.operations[i]);
|
||||
successes[i] = success;
|
||||
results[i] = resultSet;
|
||||
emit WalletOperationProcessed(
|
||||
@@ -331,7 +367,7 @@ contract VerificationGateway
|
||||
address(walletProxyAdmin),
|
||||
getInitializeData()
|
||||
)));
|
||||
updateWalletHashMappings(publicKeyHash, blsWallet);
|
||||
updateWalletHashMappings(publicKeyHash, blsWallet, publicKey);
|
||||
emit WalletCreated(
|
||||
address(blsWallet),
|
||||
publicKey
|
||||
@@ -340,45 +376,78 @@ contract VerificationGateway
|
||||
return IWallet(blsWallet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for measuring the gas used by an operation, suitable for use in
|
||||
* the operation's required gas parameter.
|
||||
*
|
||||
* This has two important differences over standard estimateGas methods:
|
||||
* 1. It does not include gas for calldata, which has already been paid
|
||||
* 2. It works for wallets which don't yet exist
|
||||
*/
|
||||
function measureOperationGas(
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
IWallet.Operation calldata op
|
||||
) external returns (uint256) {
|
||||
// Don't allow this to actually be executed on chain. Static calls only.
|
||||
require(msg.sender == address(0), "VG: read only");
|
||||
|
||||
IWallet wallet = getOrCreateWallet(publicKey);
|
||||
|
||||
uint256 gasBefore = gasleft();
|
||||
wallet.performOperation(op);
|
||||
uint256 gasUsed = gasBefore - gasleft();
|
||||
|
||||
return gasUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
@dev safely sets/overwrites the wallet for the given public key, ensuring it is properly signed
|
||||
@param wallletAddressSignature signature of message containing only the wallet address
|
||||
@param walletAddressSignature signature of message containing only the wallet address
|
||||
@param publicKey that signed the wallet address
|
||||
@param wallet address to set
|
||||
@param signatureExpiryTimestamp that the signature is valid until
|
||||
*/
|
||||
function safeSetWallet(
|
||||
uint256[2] memory wallletAddressSignature,
|
||||
uint256[2] memory walletAddressSignature,
|
||||
uint256[BLS_KEY_LEN] memory publicKey,
|
||||
IWallet wallet
|
||||
IWallet wallet,
|
||||
uint256 signatureExpiryTimestamp
|
||||
) private {
|
||||
require(
|
||||
block.timestamp < signatureExpiryTimestamp,
|
||||
"VG: message expired"
|
||||
);
|
||||
// verify the given wallet was signed for by the bls key
|
||||
uint256[2] memory addressMsg = blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
abi.encodePacked(wallet)
|
||||
WALLET_DOMAIN,
|
||||
abi.encodePacked(wallet, signatureExpiryTimestamp)
|
||||
);
|
||||
require(
|
||||
blsLib.verifySingle(wallletAddressSignature, publicKey, addressMsg),
|
||||
"VG: Signature not verified for wallet address."
|
||||
blsLib.verifySingle(walletAddressSignature, publicKey, addressMsg),
|
||||
"VG: Sig not verified"
|
||||
);
|
||||
bytes32 publicKeyHash = keccak256(abi.encodePacked(
|
||||
publicKey
|
||||
));
|
||||
emit BLSKeySetForWallet(publicKey, wallet);
|
||||
updateWalletHashMappings(publicKeyHash, wallet);
|
||||
updateWalletHashMappings(publicKeyHash, wallet, publicKey);
|
||||
}
|
||||
|
||||
/** @dev Only to be called on wallet creation, and in `safeSetWallet` */
|
||||
function updateWalletHashMappings(
|
||||
bytes32 publicKeyHash,
|
||||
IWallet wallet
|
||||
IWallet wallet,
|
||||
uint256[BLS_KEY_LEN] memory publicKey
|
||||
) private {
|
||||
// remove reference from old hash
|
||||
bytes32 oldHash = hashFromWallet[wallet];
|
||||
walletFromHash[oldHash] = IWallet(address(0));
|
||||
BLSPublicKeyFromHash[oldHash] = [0,0,0,0];
|
||||
|
||||
// update new hash / wallet mappings
|
||||
walletFromHash[publicKeyHash] = wallet;
|
||||
hashFromWallet[wallet] = publicKeyHash;
|
||||
BLSPublicKeyFromHash[publicKeyHash] = publicKey;
|
||||
}
|
||||
|
||||
function getInitializeData() private view returns (bytes memory) {
|
||||
@@ -411,9 +480,8 @@ contract VerificationGateway
|
||||
);
|
||||
}
|
||||
return blsLib.hashToPoint(
|
||||
BLS_DOMAIN,
|
||||
BUNDLE_DOMAIN,
|
||||
abi.encodePacked(
|
||||
block.chainid,
|
||||
walletAddress,
|
||||
op.nonce,
|
||||
keccak256(encodedActionData)
|
||||
|
||||
13
contracts/contracts/interfaces/IExpander.sol
Normal file
13
contracts/contracts/interfaces/IExpander.sol
Normal file
@@ -0,0 +1,13 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./IWallet.sol";
|
||||
|
||||
interface IExpander {
|
||||
function expand(bytes calldata stream) external returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,16 @@ pragma abicoder v2;
|
||||
/** Interface for a contract wallet that can perform Operations
|
||||
*/
|
||||
interface IWallet {
|
||||
/** Aggregated signature with corresponding senders + operations */
|
||||
struct Bundle {
|
||||
uint256[2] signature;
|
||||
uint256[4][] senderPublicKeys;
|
||||
IWallet.Operation[] operations;
|
||||
}
|
||||
|
||||
struct Operation {
|
||||
uint256 nonce;
|
||||
uint256 gas;
|
||||
IWallet.ActionData[] actions;
|
||||
}
|
||||
|
||||
|
||||
117
contracts/contracts/lib/PseudoFloat.sol
Normal file
117
contracts/contracts/lib/PseudoFloat.sol
Normal file
@@ -0,0 +1,117 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./VLQ.sol";
|
||||
|
||||
/**
|
||||
* Like a float, but technically for integers. Also base 10.
|
||||
*
|
||||
* The pseudo-float is an encoding that can represent any uint256 value but
|
||||
* efficiently represents values with a small number of significant figures
|
||||
* (just 2 bytes for 3 significant figures).
|
||||
*
|
||||
* Zero is a special case, it's just 0x00.
|
||||
*
|
||||
* Otherwise, start with the value in scientific notation:
|
||||
*
|
||||
* 1.23 * 10^16 (e.g. 0.0123 ETH)
|
||||
*
|
||||
* Make the mantissa (1.23) a whole number by adjusting the exponent:
|
||||
*
|
||||
* 123 * 10^14
|
||||
*
|
||||
* We add 1 to the exponent and encode it in 5 bits:
|
||||
*
|
||||
* 01111 (=15)
|
||||
*
|
||||
* Note: The maximum value we can encode here is 31 (11111). This means the
|
||||
* maximum exponent is 30. Adjust the left side of the previous equation if
|
||||
* needed.
|
||||
*
|
||||
* Encode the left side in binary:
|
||||
*
|
||||
* 1111011 (=123)
|
||||
*
|
||||
* Our first byte is the 5-bit exponent followed by the three lowest bits of the
|
||||
* mantissa:
|
||||
*
|
||||
* 01111011
|
||||
* ^^^^^-------- 15 => exponent is 14
|
||||
* ^^^----- lowest 3 bits of the mantissa
|
||||
*
|
||||
* Encode the remaining bits of the mantissa as a VLQ:
|
||||
*
|
||||
* 00001111
|
||||
* ^------------ special VLQ bit, zero indicates this is the last byte
|
||||
* ^^^^^^^----- bits to use, put them together with 011 above to get
|
||||
* 0001111011, which is 123.
|
||||
*
|
||||
* Putting it together is two bytes:
|
||||
*
|
||||
* 0x7b0f
|
||||
*
|
||||
* Example 2:
|
||||
*
|
||||
* 0.883887085 ETH uses 5 bytes: 0x55b4d7c27d
|
||||
* 883887085 * 10^9
|
||||
* For exponent 9 we encode 10 as 5 bits: 01010
|
||||
* 883887085 is 110100101011110000101111101(101)
|
||||
*
|
||||
* 01010101 10110100 11010111 11000010 01111101
|
||||
* ^^^^^------------------------------------------- 10 => exponent is 9
|
||||
* ^^^---------------------------------------- lowest 3 bits
|
||||
* ^^^^^^^--^^^^^^^--^^^^^^^--^^^^^^^ higher bits
|
||||
* ^--------^--------^-------------------- 1 => not the last byte
|
||||
* ^----------- 0 => the last byte
|
||||
*
|
||||
* Note that the *encode* process is described above for explanatory purposes.
|
||||
* On-chain we need to *decode* to recover the value from the encoded binary
|
||||
* instead.
|
||||
*/
|
||||
library PseudoFloat {
|
||||
function decode(
|
||||
bytes calldata stream
|
||||
) internal pure returns (uint256, bytes calldata) {
|
||||
uint8 firstByte = uint8(stream[0]);
|
||||
|
||||
if (firstByte == 0) {
|
||||
return (0, stream[1:]);
|
||||
}
|
||||
|
||||
uint8 exponent = ((firstByte & 0xf8) >> 3) - 1;
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = VLQ.decode(stream[1:]);
|
||||
|
||||
value <<= 3;
|
||||
value += firstByte & 0x07;
|
||||
|
||||
// TODO (merge-ok): Exponentiation by squaring might be better here.
|
||||
// Counterpoints:
|
||||
// - The gas used is pretty low anyway
|
||||
// - For these low exponents (typically ~15), the benefit is unclear
|
||||
for (uint256 i = 0; i < exponent; i++) {
|
||||
value *= 10;
|
||||
}
|
||||
|
||||
return (value, stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as decode, but public.
|
||||
*
|
||||
* This is here because when a library function that is not internal
|
||||
* requires linking when used in other contracts. This avoids including a
|
||||
* copy of that function in the contract but it's complexity that we don't
|
||||
* want right now.
|
||||
*
|
||||
* What we do want though, is a public version so that we can call it
|
||||
* statically for testing.
|
||||
*/
|
||||
function decodePublic(
|
||||
bytes calldata stream
|
||||
) public pure returns (uint256, bytes calldata) {
|
||||
return decode(stream);
|
||||
}
|
||||
}
|
||||
56
contracts/contracts/lib/RegIndex.sol
Normal file
56
contracts/contracts/lib/RegIndex.sol
Normal file
@@ -0,0 +1,56 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.8.15;
|
||||
|
||||
import "./VLQ.sol";
|
||||
|
||||
/**
|
||||
* Registry Index
|
||||
*
|
||||
* This is just a VLQ followed by 2 fixed bytes.
|
||||
*
|
||||
* This format has a 3-byte minimum and allows for >8m indexes at 3 bytes. Exact
|
||||
* values are:
|
||||
* - 3 bytes: 2^23 = 8,388,608 indexes
|
||||
* - 4 bytes: 2^30 - 2^23 = 1,065,353,216 indexes
|
||||
* - 5 bytes: 2^37 - 2^30 = 136,365,211,648 indexes
|
||||
* (In theory, this goes all the way to uint256max, which uses 37 bytes.)
|
||||
*
|
||||
* This format has following advantages over VLQ:
|
||||
* - Provides 4x the number of indexes at each width.
|
||||
* - Avoids negative perception caused by the exclusivity of 1 and 2 byte
|
||||
* indexes.
|
||||
* - Allows us to say 'we use 3 bytes' as a reasonable approximation, since this
|
||||
* will be true for a long time. If asked, we can explain how this gracefully
|
||||
* expands to additional bytes as they become needed.
|
||||
*/
|
||||
library RegIndex {
|
||||
function decode(
|
||||
bytes calldata stream
|
||||
) internal pure returns (uint256, bytes calldata) {
|
||||
uint256 value;
|
||||
(value, stream) = VLQ.decode(stream);
|
||||
value <<= 16;
|
||||
|
||||
value += (uint256(uint8(stream[0])) << 8);
|
||||
value += uint256(uint8(stream[1]));
|
||||
|
||||
return (value, stream[2:]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as decode, but public.
|
||||
*
|
||||
* This is here because when a library function that is not internal
|
||||
* requires linking when used in other contracts. This avoids including a
|
||||
* copy of that function in the contract but it's complexity that we don't
|
||||
* want right now.
|
||||
*
|
||||
* What we do want though, is a public version so that we can call it
|
||||
* statically for testing.
|
||||
*/
|
||||
function decodePublic(
|
||||
bytes calldata stream
|
||||
) public pure returns (uint256, bytes calldata) {
|
||||
return decode(stream);
|
||||
}
|
||||
}
|
||||
68
contracts/contracts/lib/VLQ.sol
Normal file
68
contracts/contracts/lib/VLQ.sol
Normal file
@@ -0,0 +1,68 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
/**
|
||||
* Variable Length Quantity
|
||||
*
|
||||
* An unsigned integer format that provides an efficient representation of small
|
||||
* values while allowing for unlimited size (except for the 256bit limit in this
|
||||
* context).
|
||||
*
|
||||
* The first (highest) bit of each byte indicates whether to continue reading.
|
||||
* The other 7 bits are used as a big-endian representation of the number.
|
||||
*
|
||||
* Examples
|
||||
*
|
||||
* 0x00: 0
|
||||
* 0x01: 1
|
||||
* 0x7f: 127
|
||||
* 0x8100: 128
|
||||
* 0x8105: 133 = 1 * 128 + 5
|
||||
* 0x838005: 49,157 = 3 * 128^2 + 0 * 128^1 + 5 * 128^0
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Variable-length_quantity
|
||||
*/
|
||||
library VLQ {
|
||||
function decode(
|
||||
bytes calldata stream
|
||||
) internal pure returns (uint256, bytes calldata) {
|
||||
uint256 value = 0;
|
||||
uint256 bytesRead = 0;
|
||||
|
||||
while (true) {
|
||||
uint8 currentByte = uint8(stream[bytesRead++]);
|
||||
|
||||
// Add the lowest 7 bits to the value
|
||||
value += currentByte & 0x7f;
|
||||
|
||||
// If the highest bit is zero, stop
|
||||
if (currentByte & 0x80 == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We're continuing. Shift the value 7 bits to the left (higher) to
|
||||
// make room.
|
||||
value <<= 7;
|
||||
}
|
||||
|
||||
return (value, stream[bytesRead:]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as decode, but public.
|
||||
*
|
||||
* This is here because when a library function that is not internal
|
||||
* requires linking when used in other contracts. This avoids including a
|
||||
* copy of that function in the contract but it's complexity that we don't
|
||||
* want right now.
|
||||
*
|
||||
* What we do want though, is a public version so that we can call it
|
||||
* statically for testing.
|
||||
*/
|
||||
function decodePublic(
|
||||
bytes calldata stream
|
||||
) public pure returns (uint256, bytes calldata) {
|
||||
return decode(stream);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ library BLS {
|
||||
uint256 private constant MASK24 = 0xffffffffffffffffffffffffffffffffffffffffffffffff;
|
||||
|
||||
// estimator address
|
||||
address private constant COST_ESTIMATOR_ADDRESS = 0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4;
|
||||
address private constant COST_ESTIMATOR_ADDRESS = 0x6eb8F8d661eFe36daB11147830A1e690249bB830;
|
||||
|
||||
function verifySingle(
|
||||
uint256[2] memory signature,
|
||||
|
||||
@@ -9,7 +9,6 @@ import chaiAsPromised from "chai-as-promised";
|
||||
import spies from "chai-spies";
|
||||
import "hardhat-gas-reporter";
|
||||
import "solidity-coverage";
|
||||
import defaultDeployerWallets from "./shared/helpers/defaultDeployerWallet";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -52,21 +51,6 @@ task("privateKeys", "Prints the private keys for accounts")
|
||||
}
|
||||
});
|
||||
|
||||
task("fundDeployer", "Sends ETH to create2Deployer contract from first signer")
|
||||
.addOptionalParam("amount", "Amount of ETH to send", "1.0")
|
||||
.setAction(async ({ amount }: { amount: string }, hre) => {
|
||||
const [account0] = await hre.ethers.getSigners();
|
||||
const deployerAddress = defaultDeployerWallets(hre.ethers).address;
|
||||
|
||||
console.log(`${account0.address} -> ${deployerAddress} ${amount} ETH`);
|
||||
|
||||
const txnRes = await account0.sendTransaction({
|
||||
to: deployerAddress,
|
||||
value: hre.ethers.utils.parseEther(amount),
|
||||
});
|
||||
await txnRes.wait();
|
||||
});
|
||||
|
||||
task("sendEth", "Sends ETH to an address")
|
||||
.addParam("address", "Address to send ETH to", undefined, types.string)
|
||||
.addOptionalParam("amount", "Amount of ETH to send", "1.0")
|
||||
@@ -131,6 +115,7 @@ const config: HardhatUserConfig = {
|
||||
},
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1337,
|
||||
initialBaseFeePerGas: 0, // workaround from https://github.com/sc-forks/solidity-coverage/issues/652#issuecomment-896330136 . Remove when that issue is closed.
|
||||
accounts,
|
||||
blockGasLimit: 30_000_000,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
|
||||
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
|
||||
"blsLibrary": "0xF8a11BA6eceC43e23c9896b857128a4269290e39",
|
||||
"verificationGateway": "0xae7DF242c589D479A5cF8fEA681736e0E0Bb1FB9",
|
||||
"blsExpander": "0x4473e39a5F33A83B81387bb5F816354F04E724a3",
|
||||
"utilities": "0x76cE3c1F2E6d87c355560fCbd28ccAcAe03f95F6",
|
||||
"testToken": "0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f"
|
||||
"safeSingletonFactory": "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7",
|
||||
"precompileCostEstimator": "0x6eb8F8d661eFe36daB11147830A1e690249bB830",
|
||||
"verificationGateway": "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
"blsExpander": "0x30Ae0d96Ba812F53F69deDefd8b472ce43af23A5",
|
||||
"utilities": "0x4bD2E4e99B50A2a9e6b9dABfA3C8dCD1f885F008",
|
||||
"testToken": "0x30e69AfbE8c2E8ECe8d6920512452C8d13F8962A"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 421613,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 1206441,
|
||||
"deployedBy": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"version": "bc3d1463f163b742026f951a2574016966b5c857"
|
||||
"walletDomain": "0xb8b27430df81daac9be5d58dcca2e4dd4aae72a95c3fb3f0ee9b77e5357f6bec",
|
||||
"bundleDomain": "0xde7ef148d5c129757b1740441010559c8a17199c68ae144446131e62c0b953ff",
|
||||
"genesisBlock": 18628869,
|
||||
"deployedBy": "0x9D5e5038c47da189f8C67A587c66a952a8b45bAb",
|
||||
"version": "0d52ddb20f2a395f3fff13130459a8561533024b"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
|
||||
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
|
||||
"blsLibrary": "0xF8a11BA6eceC43e23c9896b857128a4269290e39",
|
||||
"verificationGateway": "0x643468269B044bA84D3F2190F601E3579d3236BB",
|
||||
"blsExpander": "0x7E24FBC8A777418f519fdaB565168f19b1C8e2Fa",
|
||||
"utilities": "0x76cE3c1F2E6d87c355560fCbd28ccAcAe03f95F6",
|
||||
"testToken": "0xD2a233b4F7b49641eef0Aa0E4927A299737b39CB"
|
||||
"safeSingletonFactory": "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7",
|
||||
"precompileCostEstimator": "0x6eb8F8d661eFe36daB11147830A1e690249bB830",
|
||||
"verificationGateway": "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
"blsExpander": "0x30Ae0d96Ba812F53F69deDefd8b472ce43af23A5",
|
||||
"utilities": "0x4bD2E4e99B50A2a9e6b9dABfA3C8dCD1f885F008",
|
||||
"testToken": "0xf2149397e902D6D4E958d027F323Fd37F7Bd11eF"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 420,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 4101595,
|
||||
"deployedBy": "0xe5deB006644a6FF28AC361Fe896D351a40AeA198",
|
||||
"version": "1d35f4e80a5f5316158e7f01e61a721fe62f0103"
|
||||
"walletDomain": "0x802fb686c57e5392e94fbeb158cf09881e634e133d41525be792fa87c47cfeaa",
|
||||
"bundleDomain": "0xa5112f769feb56d1fa9bed9680d850f1362a970daff8bdffc7a231ed0fc02a84",
|
||||
"genesisBlock": 8830153,
|
||||
"deployedBy": "0x9D5e5038c47da189f8C67A587c66a952a8b45bAb",
|
||||
"version": "090405ee0a630f4fb9f0c20ae995240f7474507d"
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,13 @@
|
||||
"author": "James Zaki",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.7.3"
|
||||
"@openzeppelin/contracts": "^4.7.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"ethers": "^5.7.2",
|
||||
"hardhat": "^2.12.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ethers": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ethereumjs/tx": "^4.0.1",
|
||||
@@ -40,7 +46,6 @@
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-spies": "^1.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
@@ -50,12 +55,11 @@
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"ethereum-waffle": "^3.4.4",
|
||||
"ethers": "^5.7.2",
|
||||
"hardhat": "^2.12.1",
|
||||
"hardhat-gas-reporter": "^1.0.9",
|
||||
"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",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable no-process-exit */
|
||||
|
||||
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
|
||||
// const hre = require("hardhat");
|
||||
|
||||
async function main() {
|
||||
// await hre.run('compile');
|
||||
|
||||
console.log(
|
||||
"PrecompileCostEstimator:",
|
||||
await deployAndRunPrecompileCostEstimator(),
|
||||
);
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-disable no-process-exit */
|
||||
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
async function main() {
|
||||
const fx = await Fixture.create();
|
||||
console.log(`Deployer account address: ${fx.addresses[0]}`);
|
||||
|
||||
console.log(`verificationGateway: ${fx.verificationGateway.address}`);
|
||||
console.log(`blsExpander: ${fx.blsExpander.address}`);
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import "dotenv";
|
||||
|
||||
// We require the Hardhat Runtime Environment explicitly here. This is optional
|
||||
// but useful for running the script in a standalone fashion through `node <script>`.
|
||||
//
|
||||
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
|
||||
// Runtime Environment's members available in the global scope.
|
||||
import { ethers, network } from "hardhat";
|
||||
import deployDeployer, {
|
||||
defaultDeployerWallet,
|
||||
} from "../shared/helpers/deployDeployer";
|
||||
|
||||
async function main() {
|
||||
// Hardhat always runs the compile task when running scripts with its command
|
||||
// line interface.
|
||||
//
|
||||
// If this script is run directly using `node` you may want to call compile
|
||||
// manually to make sure everything is compiled
|
||||
// await hre.run('compile');
|
||||
const deployer = defaultDeployerWallet();
|
||||
const eoaAddress = deployer.address;
|
||||
console.log(`
|
||||
Network: ${network.name},
|
||||
Account Index: ${process.env.DEPLOYER_SET_INDEX},
|
||||
eoaAddress: ${eoaAddress},
|
||||
nonce: ${await deployer.getTransactionCount()},
|
||||
balance: ${await ethers.provider.getBalance(eoaAddress)}
|
||||
`);
|
||||
|
||||
const create2Deployer = await deployDeployer();
|
||||
console.log("create2Deployer deployed to:", create2Deployer.address);
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -12,9 +12,8 @@ import { exec as execCb } from "child_process";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { ethers } from "hardhat";
|
||||
import { NetworkConfig } from "../clients/src";
|
||||
import deployDeployer from "../shared/helpers/deployDeployer";
|
||||
import precompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import deploy from "../shared/deploy";
|
||||
import getDomain from "../clients/src/signer/getDomain";
|
||||
|
||||
dotenv.config();
|
||||
const exec = util.promisify(execCb);
|
||||
@@ -39,38 +38,51 @@ async function main() {
|
||||
console.log("starting bls-wallet contracts deployment");
|
||||
const genesisBlock = await ethers.provider.getBlockNumber();
|
||||
|
||||
console.log("deploying create2Deployer...");
|
||||
const create2Deployer = await deployDeployer();
|
||||
const chainid = (await ethers.provider.getNetwork()).chainId;
|
||||
|
||||
console.log("deploying precompile cost estimator...");
|
||||
const precompileCostEstimatorAddress = await precompileCostEstimator();
|
||||
const [signer] = await ethers.getSigners();
|
||||
|
||||
console.log("deploying bls-wallet contracts...");
|
||||
const fx = await Fixture.create();
|
||||
const [deployedBy] = fx.addresses;
|
||||
console.log("deploying contracts...");
|
||||
const deployment = await deploy(signer);
|
||||
|
||||
console.log("deploying test token...");
|
||||
// These can be run in parallel
|
||||
const [testToken, version] = await Promise.all([deployToken(), getVersion()]);
|
||||
|
||||
const domainName = "BLS_WALLET";
|
||||
const domainVersion = "1";
|
||||
const walletDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainid,
|
||||
deployment.verificationGateway.address,
|
||||
"Wallet",
|
||||
);
|
||||
|
||||
const bundleDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainid,
|
||||
deployment.verificationGateway.address,
|
||||
"Bundle",
|
||||
);
|
||||
|
||||
const netCfg: NetworkConfig = {
|
||||
parameters: {},
|
||||
addresses: {
|
||||
create2Deployer: create2Deployer.address,
|
||||
precompileCostEstimator: precompileCostEstimatorAddress,
|
||||
blsLibrary: fx.blsLibrary.address,
|
||||
verificationGateway: fx.verificationGateway.address,
|
||||
blsExpander: fx.blsExpander.address,
|
||||
utilities: fx.utilities.address,
|
||||
safeSingletonFactory: deployment.singletonFactory.address,
|
||||
precompileCostEstimator: deployment.precompileCostEstimator.address,
|
||||
verificationGateway: deployment.verificationGateway.address,
|
||||
blsExpander: deployment.blsExpander.address,
|
||||
utilities: deployment.aggregatorUtilities.address,
|
||||
testToken,
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: fx.chainId,
|
||||
// From VerificationGateway.sol:BLS_DOMAIN
|
||||
domain:
|
||||
"0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
chainid,
|
||||
walletDomain: ethers.utils.hexlify(walletDomain),
|
||||
bundleDomain: ethers.utils.hexlify(bundleDomain),
|
||||
genesisBlock,
|
||||
deployedBy,
|
||||
deployedBy: signer.address,
|
||||
version,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ const createBlsExpanderAddressTransfers = async (
|
||||
const toAddress = ctx.rng.item(ctx.blsWallets, [blsWallet]).address;
|
||||
|
||||
const nonce = walletNonces[walletIdx]++;
|
||||
const bundle = blsWallet.sign({
|
||||
const bundle = await blsWallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [createTransferAction(ctx, toAddress)],
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ const createBlsExpanderAirdrop = async (
|
||||
nonce: await sendingWallet.Nonce(),
|
||||
actions,
|
||||
};
|
||||
const bundle = sendingWallet.sign(operation);
|
||||
const bundle = await sendingWallet.signWithGasEstimate(operation);
|
||||
|
||||
const encodedFunction = solidityPack(
|
||||
["bytes"],
|
||||
@@ -38,6 +38,7 @@ const createBlsExpanderAirdrop = async (
|
||||
.blsCallMultiSameCallerContractFunction(
|
||||
sendingWallet.PublicKey(),
|
||||
operation.nonce,
|
||||
bundle.operations[0].gas,
|
||||
bundle.signature,
|
||||
ctx.contracts.testToken.address,
|
||||
methodId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user