mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-13 15:58:00 -05:00
Compare commits
54 Commits
v0.5.0
...
contract-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9476d34132 | ||
|
|
f8dba7891b | ||
|
|
405e23a7a0 | ||
|
|
48637336b3 | ||
|
|
d0be22aa49 | ||
|
|
5d98448c66 | ||
|
|
c72b0ea971 | ||
|
|
3251deca54 | ||
|
|
906538a295 | ||
|
|
ada85008ad | ||
|
|
d75868c182 | ||
|
|
abde5526c2 | ||
|
|
823ec47c42 | ||
|
|
2317b38e28 | ||
|
|
f31a35bd0e | ||
|
|
9ccd2545e8 | ||
|
|
25ec77c45c | ||
|
|
8bbb6d3a74 | ||
|
|
797ec3528f | ||
|
|
4af97846bb | ||
|
|
9a9b0284b3 | ||
|
|
60effa15a1 | ||
|
|
e4c7dfb01c | ||
|
|
50b957ad4f | ||
|
|
54227a57b0 | ||
|
|
f9ce7be5b5 | ||
|
|
2a20bfeb8d | ||
|
|
275d593b5c | ||
|
|
00d948376a | ||
|
|
0e51ecb5fe | ||
|
|
576e778855 | ||
|
|
4d170f73dd | ||
|
|
cd010324a5 | ||
|
|
29542e4c98 | ||
|
|
1ff60d5dd1 | ||
|
|
ebf415c573 | ||
|
|
b296a01a80 | ||
|
|
0e7e42154b | ||
|
|
0940333e3c | ||
|
|
831632ce8a | ||
|
|
534c5aa6ae | ||
|
|
1701a1b7bd | ||
|
|
dae68465ba | ||
|
|
f3c0f57b0b | ||
|
|
24b011af9b | ||
|
|
4fd11c0356 | ||
|
|
a58dcdaee7 | ||
|
|
162072155d | ||
|
|
e675062a53 | ||
|
|
95a107f6b8 | ||
|
|
5bd09ec4e5 | ||
|
|
b995c6d6a0 | ||
|
|
7c3aee1f70 | ||
|
|
1f52f159e4 |
@@ -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.9.0",
|
||||
"bls-wallet-clients": "0.9.0-405e23a",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -569,16 +569,6 @@
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
|
||||
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
|
||||
"@ethersproject/units@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -887,10 +877,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.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==
|
||||
bls-wallet-clients@0.9.0-405e23a:
|
||||
version "0.9.0-405e23a"
|
||||
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
|
||||
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
@@ -40,3 +40,7 @@ PRIORITY_FEE_PER_GAS=0
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
IS_OPTIMISM=false
|
||||
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS=0x420000000000000000000000000000000000000F
|
||||
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE=2
|
||||
|
||||
@@ -38,3 +38,5 @@ PRIORITY_FEE_PER_GAS=500000000
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
IS_OPTIMISM=false
|
||||
|
||||
@@ -35,3 +35,5 @@ PRIORITY_FEE_PER_GAS=500000000
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
IS_OPTIMISM=false
|
||||
|
||||
@@ -89,32 +89,35 @@ commands.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
| Name | Example Value | Description |
|
||||
| ---------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
|
||||
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
|
||||
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
|
||||
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
|
||||
| PORT | 3000 | The port to bind the aggregator to |
|
||||
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
|
||||
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregator’s onchain address |
|
||||
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
|
||||
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
|
||||
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
|
||||
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
|
||||
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
|
||||
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
|
||||
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
|
||||
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
|
||||
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
|
||||
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
|
||||
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
|
||||
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
|
||||
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
|
||||
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
|
||||
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
|
||||
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
|
||||
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
|
||||
| Name | Example Value | Description |
|
||||
| ------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
|
||||
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
|
||||
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
|
||||
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
|
||||
| PORT | 3000 | The port to bind the aggregator to |
|
||||
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
|
||||
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregator’s onchain address |
|
||||
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
|
||||
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
|
||||
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
|
||||
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
|
||||
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
|
||||
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
|
||||
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
|
||||
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
|
||||
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
|
||||
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
|
||||
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
|
||||
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
|
||||
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
|
||||
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
|
||||
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
|
||||
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
|
||||
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
|
||||
| IS_OPTIMISM | false | Optimism's strategy for charging for L1 fees requires special logic in the aggregator. In addition to gasEstimate * gasPrice, we need to replicate Optimism's calculation and pass it on to the user |
|
||||
| OPTIMISM_GAS_PRICE_ORACLE_ADDRESS | 0x420000000000000000000000000000000000000F | Address for the Optimism gas price oracle contract. Required when IS_OPTIMISM is true |
|
||||
| OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE | 2 | Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for the optimism-specific calculation. This gets passed on to users. Required when IS_OPTIMISM is true |
|
||||
|
||||
## Running
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
@@ -70,12 +70,14 @@ export {
|
||||
getConfig,
|
||||
MockERC20Factory,
|
||||
VerificationGatewayFactory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
export * as sqlite from "https://deno.land/x/sqlite@v3.7.0/mod.ts";
|
||||
export { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
|
||||
|
||||
export { pick } from "npm:@s-libs/micro-dash@15.2.0";
|
||||
|
||||
17
aggregator/manualTests/callOptimismGasPriceOracle.ts
Executable file
17
aggregator/manualTests/callOptimismGasPriceOracle.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import * as env from "../src/env.ts";
|
||||
import { ethers } from "../deps.ts";
|
||||
import OptimismGasPriceOracle from "../src/app/OptimismGasPriceOracle.ts";
|
||||
|
||||
const oracle = new OptimismGasPriceOracle(
|
||||
new ethers.providers.JsonRpcProvider(env.RPC_URL),
|
||||
);
|
||||
|
||||
const { l1BaseFee, overhead, scalar, decimals } = await oracle.getAllParams();
|
||||
|
||||
console.log({
|
||||
l1BaseFee: `${(l1BaseFee.toNumber() / 1e9).toFixed(3)} gwei`,
|
||||
overhead: `${overhead.toNumber()} L1 gas`,
|
||||
scalar: scalar.toNumber() / (10 ** decimals.toNumber()),
|
||||
});
|
||||
17
aggregator/manualTests/getOptimismL1Fee.ts
Executable file
17
aggregator/manualTests/getOptimismL1Fee.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import getOptimismL1Fee from "../src/helpers/getOptimismL1Fee.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const txHash = Deno.args[0];
|
||||
|
||||
if (!txHash.startsWith("0x")) {
|
||||
throw new Error("First arg should be tx hash");
|
||||
}
|
||||
|
||||
const l1Fee = await getOptimismL1Fee(provider, txHash);
|
||||
|
||||
console.log(`${ethers.utils.formatEther(l1Fee)} ETH`);
|
||||
15
aggregator/manualTests/getRawTransaction.ts
Executable file
15
aggregator/manualTests/getRawTransaction.ts
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import getRawTransaction from "../src/helpers/getRawTransaction.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const txHash = Deno.args[0];
|
||||
|
||||
if (!txHash.startsWith("0x")) {
|
||||
throw new Error("First arg should be tx hash");
|
||||
}
|
||||
|
||||
console.log(await getRawTransaction(provider, txHash));
|
||||
@@ -60,7 +60,7 @@ const bundles: Bundle[] = [];
|
||||
for (const [i, wallet] of wallets.entries()) {
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
console.log("Funding wallet", i);
|
||||
console.log("Funding wallet", i, "(1 wei to make estimateFee work)");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
@@ -92,7 +92,7 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
|
||||
// Ensure wallet can pay the fee
|
||||
if (balance.lt(fee)) {
|
||||
console.log("Funding wallet");
|
||||
console.log("Funding wallet", i, "(based on estimateFee)");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
|
||||
@@ -544,9 +544,10 @@ export default class AggregationStrategy {
|
||||
bundleOverheadGas ??=
|
||||
(await this.measureBundleOverhead()).bundleOverheadGas;
|
||||
|
||||
const gasEstimate = await this.ethereumService.estimateCompressedGas(
|
||||
bundle,
|
||||
);
|
||||
const gasEstimate = await this.ethereumService
|
||||
.estimateEffectiveCompressedGas(
|
||||
bundle,
|
||||
);
|
||||
|
||||
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
|
||||
@@ -630,12 +631,18 @@ export default class AggregationStrategy {
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo.requiredFee,
|
||||
expectedMaxCost: feeInfo.expectedMaxCost,
|
||||
errorReason: { message: "Insufficient fee" },
|
||||
errorReason: {
|
||||
message: [
|
||||
"Insufficient fee",
|
||||
`(provided: ${ethers.utils.formatEther(fee)},`,
|
||||
`required: ${ethers.utils.formatEther(feeInfo.requiredFee)})`,
|
||||
].join(" "),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.estimateCompressedGas(bundle);
|
||||
await this.ethereumService.estimateEffectiveCompressedGas(bundle);
|
||||
|
||||
return {
|
||||
success,
|
||||
@@ -673,8 +680,8 @@ export default class AggregationStrategy {
|
||||
});
|
||||
|
||||
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
|
||||
es.estimateCompressedGas(bundle1),
|
||||
es.estimateCompressedGas(
|
||||
es.estimateEffectiveCompressedGas(bundle1),
|
||||
es.estimateEffectiveCompressedGas(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HTTPMethods } from "../../deps.ts";
|
||||
|
||||
type AppEvent =
|
||||
| { type: "starting" }
|
||||
| { type: "listening"; data: { port: number } }
|
||||
| { type: "db-query"; data: { sql: string; params: unknown } }
|
||||
| { type: "waiting-unconfirmed-space" }
|
||||
|
||||
@@ -39,5 +39,19 @@ export default function BundleRouter(bundleService: BundleService) {
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"aggregateBundle/:subBundleHash",
|
||||
(ctx) => {
|
||||
const bundleRows = bundleService.lookupAggregateBundle(ctx.params.subBundleHash!);
|
||||
|
||||
if (bundleRows === nil || !bundleRows?.length) {
|
||||
ctx.response.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = bundleRows;
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
delay,
|
||||
ethers,
|
||||
Semaphore,
|
||||
VerificationGatewayFactory,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import { IClock } from "../helpers/Clock.ts";
|
||||
@@ -18,10 +19,12 @@ import * as env from "../env.ts";
|
||||
import runQueryGroup from "./runQueryGroup.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
|
||||
import BundleTable, { BundleRow } from "./BundleTable.ts";
|
||||
import plus from "./helpers/plus.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import getOptimismL1Fee from "../helpers/getOptimismL1Fee.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
|
||||
export type AddBundleResponse = { hash: string } | {
|
||||
failures: TransactionFailure[];
|
||||
@@ -34,6 +37,7 @@ export default class BundleService {
|
||||
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
|
||||
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
|
||||
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
|
||||
isOptimism: env.IS_OPTIMISM,
|
||||
};
|
||||
|
||||
unconfirmedBundles = new Set<Bundle>();
|
||||
@@ -156,14 +160,16 @@ export default class BundleService {
|
||||
|
||||
const failures: TransactionFailure[] = [];
|
||||
|
||||
for (const walletAddr of walletAddresses) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: `invalid signature for wallet address ${walletAddr}`,
|
||||
});
|
||||
}
|
||||
const signedCorrectly = this.blsWalletSigner.verify(
|
||||
bundle,
|
||||
walletAddresses,
|
||||
);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description:
|
||||
`invalid bundle signature for signature ${bundle.signature}`,
|
||||
});
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
@@ -173,7 +179,7 @@ export default class BundleService {
|
||||
}
|
||||
|
||||
return await this.runQueryGroup(async () => {
|
||||
const hash = makeHash();
|
||||
const hash = await this.hashBundle(bundle);
|
||||
|
||||
this.bundleTable.add({
|
||||
status: "pending",
|
||||
@@ -201,15 +207,21 @@ export default class BundleService {
|
||||
return this.bundleTable.findBundle(hash);
|
||||
}
|
||||
|
||||
lookupAggregateBundle(subBundleHash: string) {
|
||||
const subBundle = this.bundleTable.findBundle(subBundleHash);
|
||||
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!);
|
||||
}
|
||||
|
||||
receiptFromBundle(bundle: BundleRow) {
|
||||
if (!bundle.receipt) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const { receipt, hash } = bundle;
|
||||
const { receipt, hash, aggregateHash } = bundle;
|
||||
|
||||
return {
|
||||
bundleHash: hash,
|
||||
aggregateBundleHash: aggregateHash,
|
||||
to: receipt.to,
|
||||
from: receipt.from,
|
||||
contractAddress: receipt.contractAddress,
|
||||
@@ -230,6 +242,44 @@ export default class BundleService {
|
||||
};
|
||||
}
|
||||
|
||||
async hashBundle(bundle: Bundle): Promise<string> {
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const verifyMethodName = "verify";
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === verifyMethodName,
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await this.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
}
|
||||
|
||||
async runSubmission() {
|
||||
this.submissionsInProgress++;
|
||||
|
||||
@@ -275,6 +325,13 @@ export default class BundleService {
|
||||
},
|
||||
});
|
||||
|
||||
if (aggregateBundle) {
|
||||
const aggregateBundleHash = await this.hashBundle(aggregateBundle);
|
||||
for (const row of includedRows) {
|
||||
row.aggregateHash = aggregateBundleHash;
|
||||
}
|
||||
}
|
||||
|
||||
for (const failedRow of failedRows) {
|
||||
this.emit({
|
||||
type: "failed-row",
|
||||
@@ -363,7 +420,16 @@ export default class BundleService {
|
||||
const profit = balanceAfter.sub(balanceBefore);
|
||||
|
||||
/** What we paid to process the bundle */
|
||||
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
|
||||
let cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
|
||||
|
||||
if (this.config.isOptimism) {
|
||||
cost = cost.add(
|
||||
await getOptimismL1Fee(
|
||||
this.ethereumService.provider,
|
||||
receipt.transactionHash,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Fees collected from users */
|
||||
const actualFee = profit.add(cost);
|
||||
|
||||
@@ -30,6 +30,7 @@ type RawRow = {
|
||||
nextEligibilityDelay: string;
|
||||
submitError: string | null;
|
||||
receipt: string | null;
|
||||
aggregateHash: string | null;
|
||||
};
|
||||
|
||||
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
|
||||
@@ -44,17 +45,12 @@ type Row = {
|
||||
nextEligibilityDelay: BigNumber;
|
||||
submitError?: string;
|
||||
receipt?: ethers.ContractReceipt;
|
||||
aggregateHash?: string;
|
||||
};
|
||||
|
||||
type InsertRow = Omit<Row, "id">;
|
||||
type InsertRawRow = Omit<RawRow, "id">;
|
||||
|
||||
export function makeHash() {
|
||||
const buf = new Uint8Array(32);
|
||||
crypto.getRandomValues(buf);
|
||||
return ethers.utils.hexlify(buf);
|
||||
}
|
||||
|
||||
export type BundleRow = Row;
|
||||
|
||||
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
@@ -68,6 +64,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
nextEligibilityDelay: rawRow[5] as string,
|
||||
submitError: rawRow[6] as string | null,
|
||||
receipt: rawRow[7] as string | null,
|
||||
aggregateHash: rawRow[8] as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +96,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
|
||||
submitError: rawRow.submitError ?? nil,
|
||||
receipt,
|
||||
aggregateHash: rawRow.aggregateHash ?? nil,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,6 +107,7 @@ function toInsertRawRow(row: InsertRow): InsertRawRow {
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
eligibleAfter: toUint256Hex(row.eligibleAfter),
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
@@ -123,6 +122,7 @@ function toRawRow(row: Row): RawRow {
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
submitError: row.submitError ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,10 +140,11 @@ export default class BundleTable {
|
||||
eligibleAfter TEXT NOT NULL,
|
||||
nextEligibilityDelay TEXT NOT NULL,
|
||||
submitError TEXT,
|
||||
receipt TEXT
|
||||
receipt TEXT,
|
||||
aggregateHash TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
|
||||
this.onQuery(sql, params);
|
||||
@@ -164,7 +165,8 @@ export default class BundleTable {
|
||||
eligibleAfter,
|
||||
nextEligibilityDelay,
|
||||
submitError,
|
||||
receipt
|
||||
receipt,
|
||||
aggregateHash
|
||||
) VALUES (
|
||||
:id,
|
||||
:status,
|
||||
@@ -173,7 +175,8 @@ export default class BundleTable {
|
||||
:eligibleAfter,
|
||||
:nextEligibilityDelay,
|
||||
:submitError,
|
||||
:receipt
|
||||
:receipt,
|
||||
:aggregateHash
|
||||
)
|
||||
`,
|
||||
{
|
||||
@@ -184,6 +187,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,7 +206,8 @@ export default class BundleTable {
|
||||
eligibleAfter = :eligibleAfter,
|
||||
nextEligibilityDelay = :nextEligibilityDelay,
|
||||
submitError = :submitError,
|
||||
receipt = :receipt
|
||||
receipt = :receipt,
|
||||
aggregateHash = :aggregateHash
|
||||
WHERE
|
||||
id = :id
|
||||
`,
|
||||
@@ -215,6 +220,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -255,6 +261,21 @@ export default class BundleTable {
|
||||
return rows.map(fromRawRow)[0];
|
||||
}
|
||||
|
||||
findAggregateBundle(aggregateHash: string): Row[] | nil {
|
||||
const rows = this.dbQuery(
|
||||
`
|
||||
SELECT * from bundles
|
||||
WHERE
|
||||
aggregateHash = :aggregateHash AND
|
||||
status = 'confirmed'
|
||||
ORDER BY id ASC
|
||||
`,
|
||||
{ ":aggregateHash": aggregateHash },
|
||||
);
|
||||
|
||||
return rows.map(fromRawRow);
|
||||
}
|
||||
|
||||
count(): number {
|
||||
const result = this.dbQuery("SELECT COUNT(*) FROM bundles")[0][0];
|
||||
assert(typeof result === "number");
|
||||
|
||||
@@ -26,6 +26,8 @@ import toPublicKeyShort from "./helpers/toPublicKeyShort.ts";
|
||||
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import hexToUint8Array from "../helpers/hexToUint8Array.ts";
|
||||
import OptimismGasPriceOracle from "./OptimismGasPriceOracle.ts";
|
||||
|
||||
export type TxCheckResult = {
|
||||
failures: TransactionFailure[];
|
||||
@@ -76,6 +78,7 @@ export default class EthereumService {
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public wallet: Wallet,
|
||||
public provider: ethers.providers.Provider,
|
||||
public chainId: number,
|
||||
public blsWalletWrapper: BlsWalletWrapper,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public verificationGateway: VerificationGateway,
|
||||
@@ -169,6 +172,7 @@ export default class EthereumService {
|
||||
emit,
|
||||
wallet,
|
||||
provider,
|
||||
chainId,
|
||||
blsWalletWrapper,
|
||||
blsWalletSigner,
|
||||
verificationGateway,
|
||||
@@ -341,10 +345,10 @@ export default class EthereumService {
|
||||
};
|
||||
|
||||
const attempt = async () => {
|
||||
let txResponse: ethers.providers.TransactionResponse;
|
||||
let response: ethers.providers.TransactionResponse;
|
||||
|
||||
try {
|
||||
txResponse = await this.wallet.sendTransaction(txRequest);
|
||||
response = 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
|
||||
@@ -360,7 +364,10 @@ export default class EthereumService {
|
||||
}
|
||||
|
||||
try {
|
||||
return { type: "receipt" as const, value: await txResponse.wait() };
|
||||
return {
|
||||
type: "complete" as const,
|
||||
value: await response.wait(),
|
||||
};
|
||||
} catch (error) {
|
||||
return { type: "waitError" as const, value: error };
|
||||
}
|
||||
@@ -376,7 +383,7 @@ export default class EthereumService {
|
||||
|
||||
const attemptResult = await attempt();
|
||||
|
||||
if (attemptResult.type === "receipt") {
|
||||
if (attemptResult.type === "complete") {
|
||||
return attemptResult.value;
|
||||
}
|
||||
|
||||
@@ -405,17 +412,44 @@ export default class EthereumService {
|
||||
throw new Error("Expected return or throw from attempt loop");
|
||||
}
|
||||
|
||||
async estimateCompressedGas(bundle: Bundle): Promise<BigNumber> {
|
||||
/**
|
||||
* Estimates the amount of effective gas needed to process the bundle using
|
||||
* compression.
|
||||
*
|
||||
* Here 'effective' gas means the number you need to multiply by gasPrice in
|
||||
* order to get the right fee. There are a few cases here:
|
||||
*
|
||||
* 1. L1 chains (used in testing, eg gethDev)
|
||||
* - Effective gas is equal to regular gas
|
||||
* 2. Arbitrum
|
||||
* - The Arbitrum node already responds with effective gas when calling
|
||||
* estimateGas
|
||||
* 3. Optimism
|
||||
* - We estimate Optimism's calculation for the amount of L1 gas it will
|
||||
* charge for, and then convert that into an equivalend amount of L2 gas.
|
||||
*/
|
||||
async estimateEffectiveCompressedGas(bundle: Bundle): Promise<BigNumber> {
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
return await this.wallet.estimateGas({
|
||||
let gasEstimate = await this.wallet.estimateGas({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
});
|
||||
|
||||
if (env.IS_OPTIMISM) {
|
||||
const extraGasEstimate = await this.estimateOptimismL2GasNeededForL1Gas(
|
||||
compressedBundle,
|
||||
gasEstimate,
|
||||
);
|
||||
|
||||
gasEstimate = gasEstimate.add(extraGasEstimate);
|
||||
}
|
||||
|
||||
return gasEstimate;
|
||||
}
|
||||
|
||||
async GasConfig() {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
async GasConfig(block?: ethers.providers.Block) {
|
||||
block ??= await this.provider.getBlock("latest");
|
||||
const previousBaseFee = block.baseFeePerGas;
|
||||
assert(previousBaseFee !== null && previousBaseFee !== nil);
|
||||
|
||||
@@ -442,6 +476,66 @@ export default class EthereumService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the L1 gas that Optimism will charge us for and expresses it as
|
||||
* an amount of equivalent L2 gas.
|
||||
*
|
||||
* This is very similar to what Arbitrum does, but in Arbitrum it's built-in,
|
||||
* and you actually sign for that additional L2 gas. On Optimism, you only
|
||||
* sign for the actual L2 gas, and optimism just adds the L1 fee.
|
||||
*
|
||||
* For our purposes, this works as a way to normalize the behavior between
|
||||
* the different chains.
|
||||
*/
|
||||
async estimateOptimismL2GasNeededForL1Gas(
|
||||
compressedBundle: string,
|
||||
gasLimit: BigNumber,
|
||||
): Promise<BigNumber> {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
const gasConfig = await this.GasConfig(block);
|
||||
|
||||
const txBytes = await this.wallet.signTransaction({
|
||||
type: 2,
|
||||
chainId: this.chainId,
|
||||
nonce: this.nextNonce,
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
...gasConfig,
|
||||
gasLimit,
|
||||
});
|
||||
|
||||
let l1Gas = 0;
|
||||
|
||||
for (const byte of hexToUint8Array(txBytes)) {
|
||||
if (byte === 0) {
|
||||
l1Gas += 4;
|
||||
} else {
|
||||
l1Gas += 16;
|
||||
}
|
||||
}
|
||||
|
||||
const gasOracle = new OptimismGasPriceOracle(this.provider);
|
||||
|
||||
const { l1BaseFee, overhead, scalar, decimals } = await gasOracle
|
||||
.getAllParams();
|
||||
|
||||
const scalarNum = scalar.toNumber() / (10 ** decimals.toNumber());
|
||||
|
||||
l1Gas += overhead.toNumber();
|
||||
|
||||
assert(block.baseFeePerGas !== null && block.baseFeePerGas !== nil);
|
||||
assert(env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil);
|
||||
|
||||
const adjustedL1BaseFee = l1BaseFee.toNumber() * scalarNum *
|
||||
(1 + env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE / 100);
|
||||
|
||||
const feeRatio = adjustedL1BaseFee / block.baseFeePerGas.toNumber();
|
||||
|
||||
return BigNumber.from(
|
||||
Math.ceil(feeRatio * l1Gas),
|
||||
);
|
||||
}
|
||||
|
||||
private static Wallet(
|
||||
provider: ethers.providers.Provider,
|
||||
privateKey: string,
|
||||
|
||||
52
aggregator/src/app/OptimismGasPriceOracle.ts
Normal file
52
aggregator/src/app/OptimismGasPriceOracle.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BigNumber, ethers } from "../../deps.ts";
|
||||
import assert from "../helpers/assert.ts";
|
||||
import { OPTIMISM_GAS_PRICE_ORACLE_ADDRESS } from "../env.ts";
|
||||
|
||||
export default class OptimismGasPriceOracle {
|
||||
constructor(
|
||||
public provider: ethers.providers.Provider,
|
||||
) {}
|
||||
|
||||
private async callFn(method: string, blockTag?: ethers.providers.BlockTag) {
|
||||
const outputBytes = await this.provider.call({
|
||||
to: OPTIMISM_GAS_PRICE_ORACLE_ADDRESS,
|
||||
data: ethers.utils.id(method),
|
||||
}, blockTag);
|
||||
|
||||
const result = ethers.utils.defaultAbiCoder.decode(
|
||||
["uint256"],
|
||||
outputBytes,
|
||||
)[0];
|
||||
|
||||
assert(result instanceof BigNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async l1BaseFee(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("l1BaseFee()", blockTag);
|
||||
}
|
||||
|
||||
async overhead(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("overhead()", blockTag);
|
||||
}
|
||||
|
||||
async scalar(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("scalar()", blockTag);
|
||||
}
|
||||
|
||||
async decimals(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("decimals()", blockTag);
|
||||
}
|
||||
|
||||
async getAllParams(blockTag?: ethers.providers.BlockTag) {
|
||||
const [l1BaseFee, overhead, scalar, decimals] = await Promise.all([
|
||||
this.l1BaseFee(blockTag),
|
||||
this.overhead(blockTag),
|
||||
this.scalar(blockTag),
|
||||
this.decimals(blockTag),
|
||||
]);
|
||||
|
||||
return { l1BaseFee, overhead, scalar, decimals };
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import HealthService from "./HealthService.ts";
|
||||
import HealthRouter from "./HealthRouter.ts";
|
||||
|
||||
export default async function app(emit: (evt: AppEvent) => void) {
|
||||
emit({ type: "starting" });
|
||||
|
||||
const clock = Clock.create();
|
||||
|
||||
const bundleTableMutex = new Mutex();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import assert from "./helpers/assert.ts";
|
||||
import {
|
||||
optionalEnv,
|
||||
optionalNumberEnv,
|
||||
requireBigNumberEnv,
|
||||
requireBoolEnv,
|
||||
@@ -95,3 +97,38 @@ export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
|
||||
export const BUNDLE_CHECKING_CONCURRENCY = requireIntEnv(
|
||||
"BUNDLE_CHECKING_CONCURRENCY",
|
||||
);
|
||||
|
||||
/**
|
||||
* Optimism's strategy for charging for L1 fees requires special logic in the
|
||||
* aggregator. In addition to gasEstimate * gasPrice, we need to replicate
|
||||
* Optimism's calculation and pass it on to the user.
|
||||
*/
|
||||
export const IS_OPTIMISM = requireBoolEnv("IS_OPTIMISM");
|
||||
|
||||
/**
|
||||
* Address for the Optimism gas price oracle contract. Required when
|
||||
* IS_OPTIMISM is true.
|
||||
*/
|
||||
export const OPTIMISM_GAS_PRICE_ORACLE_ADDRESS = optionalEnv(
|
||||
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS",
|
||||
);
|
||||
|
||||
/**
|
||||
* Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for
|
||||
* the optimism-specific calculation. This gets passed on to users.
|
||||
* Required when IS_OPTIMISM is true.
|
||||
*/
|
||||
export const OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE = optionalNumberEnv(
|
||||
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE",
|
||||
);
|
||||
|
||||
if (IS_OPTIMISM) {
|
||||
assert(
|
||||
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil,
|
||||
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE is required when IS_OPTIMISM is true",
|
||||
);
|
||||
assert(
|
||||
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS !== nil,
|
||||
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS is required when IS_OPTIMISM is true",
|
||||
);
|
||||
}
|
||||
|
||||
50
aggregator/src/helpers/getOptimismL1Fee.ts
Normal file
50
aggregator/src/helpers/getOptimismL1Fee.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BigNumber, ethers } from "../../deps.ts";
|
||||
import OptimismGasPriceOracle from "../app/OptimismGasPriceOracle.ts";
|
||||
import assert from "./assert.ts";
|
||||
import getRawTransaction from "./getRawTransaction.ts";
|
||||
import hexToUint8Array from "./hexToUint8Array.ts";
|
||||
import nil from "./nil.ts";
|
||||
|
||||
export default async function getOptimismL1Fee(
|
||||
provider: ethers.providers.Provider,
|
||||
txResponseOrHash: string | ethers.providers.TransactionResponse,
|
||||
) {
|
||||
const tx = typeof txResponseOrHash === "string"
|
||||
? await provider.getTransaction(txResponseOrHash)
|
||||
: txResponseOrHash;
|
||||
|
||||
const rawTx = await getRawTransaction(provider, tx);
|
||||
|
||||
let l1Gas = 0;
|
||||
|
||||
for (const byte of hexToUint8Array(rawTx)) {
|
||||
if (byte === 0) {
|
||||
l1Gas += 4;
|
||||
} else {
|
||||
l1Gas += 16;
|
||||
}
|
||||
}
|
||||
|
||||
const gasOracle = new OptimismGasPriceOracle(provider);
|
||||
|
||||
assert(tx.blockNumber !== nil);
|
||||
|
||||
const {
|
||||
l1BaseFee,
|
||||
overhead,
|
||||
scalar,
|
||||
decimals,
|
||||
} = await gasOracle.getAllParams(tx.blockNumber);
|
||||
|
||||
l1Gas = l1Gas += overhead.toNumber();
|
||||
|
||||
const l1Fee = BigNumber
|
||||
.from(l1Gas)
|
||||
.mul(l1BaseFee)
|
||||
.mul(scalar)
|
||||
.div(
|
||||
BigNumber.from(10).pow(decimals),
|
||||
);
|
||||
|
||||
return l1Fee;
|
||||
}
|
||||
49
aggregator/src/helpers/getRawTransaction.ts
Normal file
49
aggregator/src/helpers/getRawTransaction.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ethers, pick } from "../../deps.ts";
|
||||
import assert from "./assert.ts";
|
||||
import nil from "./nil.ts";
|
||||
|
||||
export default async function getRawTransaction(
|
||||
provider: ethers.providers.Provider,
|
||||
txResponseOrHash: string | ethers.providers.TransactionResponse,
|
||||
) {
|
||||
const tx = typeof txResponseOrHash === "string"
|
||||
? await provider.getTransaction(txResponseOrHash)
|
||||
: txResponseOrHash;
|
||||
|
||||
const txHash = typeof txResponseOrHash === "string"
|
||||
? txResponseOrHash
|
||||
: tx.hash;
|
||||
|
||||
assert(typeof txHash === "string");
|
||||
|
||||
const { v, r, s } = tx;
|
||||
assert(r !== nil);
|
||||
|
||||
const txBytes = ethers.utils.serializeTransaction(
|
||||
pick(
|
||||
tx,
|
||||
"to",
|
||||
"nonce",
|
||||
"gasLimit",
|
||||
...(tx.type === 2 ? [] : ["gasPrice"] as const),
|
||||
"data",
|
||||
"value",
|
||||
"chainId",
|
||||
"type",
|
||||
...(tx.type !== 2 ? [] : [
|
||||
"accessList",
|
||||
"maxPriorityFeePerGas",
|
||||
"maxFeePerGas",
|
||||
] as const),
|
||||
),
|
||||
{ v, r, s },
|
||||
);
|
||||
|
||||
const reconstructedHash = ethers.utils.keccak256(txBytes);
|
||||
|
||||
if (reconstructedHash !== txHash) {
|
||||
throw new Error("Reconstructed hash did not match original hash");
|
||||
}
|
||||
|
||||
return txBytes;
|
||||
}
|
||||
16
aggregator/src/helpers/hexToUint8Array.ts
Normal file
16
aggregator/src/helpers/hexToUint8Array.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import assert from "./assert.ts";
|
||||
|
||||
export default function hexToUint8Array(hex: string) {
|
||||
assert(hex.startsWith("0x"));
|
||||
assert(hex.length % 2 === 0);
|
||||
|
||||
const len = (hex.length - 2) / 2;
|
||||
const result = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const hexPos = 2 * i + 2;
|
||||
result[i] = parseInt(hex.slice(hexPos, hexPos + 2), 16);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
|
||||
|
||||
import { BigNumber, Operation, VerificationGatewayFactory, assertBundleSucceeds, assertEquals, ethers } from "./deps.ts";
|
||||
import ExplicitAny from "../src/helpers/ExplicitAny.ts";
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("adds valid bundle", async (fx) => {
|
||||
@@ -54,7 +54,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
// sig test)
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -63,7 +63,47 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with valid signature but invalid public key", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet, otherWallet] = await fx.setupWallets(2);
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tx = wallet.sign(operation);
|
||||
const otherTx = otherWallet.sign(operation);
|
||||
|
||||
// Make the signature invalid
|
||||
// Note: Bug in bls prevents just corrupting the signature (see other invalid
|
||||
// sig test)
|
||||
tx.senderPublicKeys[0] = otherTx.senderPublicKeys[0];
|
||||
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
throw new Error("expected bundle to fail");
|
||||
}
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
assertEquals(res.failures.map((f) => f.description), [`invalid bundle signature for signature ${tx.signature}`]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
@@ -85,7 +125,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -94,7 +134,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["duplicate-nonce"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test(
|
||||
@@ -128,7 +168,7 @@ Fixture.test(
|
||||
// https://github.com/thehubbleproject/hubble-bls/pull/20
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -141,7 +181,7 @@ Fixture.test(
|
||||
);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -164,11 +204,232 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("Same bundle produces same hash", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const firstBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const secondBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 999999,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const firstBundleHash = await bundleService.hashBundle(firstBundle);
|
||||
const secondBundleHash = await bundleService.hashBundle(secondBundle);
|
||||
|
||||
assertEquals(firstBundleHash, secondBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with single operation", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with multiple operations", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 3],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 5],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes empty bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const bundle = fx.blsWalletSigner.aggregate([]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
// TODO (merge-ok): Add a mechanism for limiting the number of stored
|
||||
|
||||
@@ -358,3 +358,57 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
|
||||
assertEquals(failedBundleRow?.status, "failed");
|
||||
});
|
||||
|
||||
Fixture.test("Retrieves all sub bundles included in a submitted bundle from single a sub bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
|
||||
const wallets = await fx.setupWallets(3);
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
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],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
const subBundleHashes = await Promise.all(bundles.map(async (bundle) => {
|
||||
const res = await bundleService.add(bundle);
|
||||
|
||||
if ("failures" in res) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
|
||||
return res.hash;
|
||||
}));
|
||||
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
const firstSubBundle = bundleService.lookupBundle(subBundleHashes[0]);
|
||||
const secondSubBundle = bundleService.lookupBundle(subBundleHashes[1]);
|
||||
const thirdSubBundle = bundleService.lookupBundle(subBundleHashes[2]);
|
||||
|
||||
const orderedSubBundles = [firstSubBundle, secondSubBundle, thirdSubBundle].sort((a, b) => a!.id - b!.id);
|
||||
|
||||
for (const subBundleHash of subBundleHashes) {
|
||||
const aggregateBundle = bundleService.lookupAggregateBundle(subBundleHash);
|
||||
assertEquals(aggregateBundle?.[0], orderedSubBundles[0]);
|
||||
assertEquals(aggregateBundle?.[1], orderedSubBundles[1]);
|
||||
assertEquals(aggregateBundle?.[2], orderedSubBundles[2]);
|
||||
}
|
||||
});
|
||||
@@ -29,6 +29,7 @@ const sampleRows: BundleRow[] = [
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
submitError: nil,
|
||||
receipt: nil,
|
||||
aggregateHash: nil,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export const bundleServiceDefaultTestConfig:
|
||||
maxAggregationDelayMillis: 5000,
|
||||
maxUnconfirmedAggregations: 3,
|
||||
maxEligibilityDelay: 300,
|
||||
isOptimism: false,
|
||||
};
|
||||
|
||||
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {
|
||||
|
||||
@@ -44,6 +44,7 @@ export type BundleReceiptError = {
|
||||
*/
|
||||
export type BlsBundleReceipt = {
|
||||
bundleHash: string;
|
||||
aggregateBundleHash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,7 @@ export default class Aggregator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the fee required for a bundle by the aggreagtor to submit it.
|
||||
* Estimates the fee required for a bundle by the aggregator to submit it.
|
||||
*
|
||||
* @param bundle Bundle to estimates the fee for
|
||||
* @returns Estimate of the fee needed to submit the bundle
|
||||
@@ -125,6 +126,21 @@ export default class Aggregator {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aggregate bundle that a sub bundle was a part of.
|
||||
* This will return undefined if the bundle does not exist or does not have an aggregate bundle.
|
||||
*
|
||||
* @param hash Hash of the bundle to find the aggregate bundle for.
|
||||
* @returns The aggregate bundle, or undefined if either the sub bundle or aggregate bundle were not found.
|
||||
*/
|
||||
async getAggregateBundleFromSubBundle(
|
||||
subBundleHash: string,
|
||||
): Promise<Bundle | undefined> {
|
||||
return this.jsonGet<Bundle>(
|
||||
`${this.origin}/aggregateBundle/${subBundleHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: This should be private instead of exposed. Leaving as is for compatibility.
|
||||
async jsonPost(path: string, body: unknown): Promise<unknown> {
|
||||
const resp = await this.fetchImpl(`${this.origin}${path}`, {
|
||||
|
||||
@@ -343,7 +343,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return [
|
||||
...actions,
|
||||
{
|
||||
ethValue: fee,
|
||||
ethValue: fee.toHexString(),
|
||||
contractAddress: this.aggregatorUtilitiesAddress,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
|
||||
@@ -6,7 +6,7 @@ import { BigNumber } from "ethers";
|
||||
* the chance that bundles get accepted during aggregation.
|
||||
*
|
||||
* @param feeEstimate fee required for bundle
|
||||
* @param safetyDivisor optional safety divisor. Default is 5
|
||||
* @param safetyDivisor optional safety divisor. Default is 5 (adds a 20% safety margin)
|
||||
* @returns fee estimate with added safety premium
|
||||
*/
|
||||
export default function addSafetyPremiumToFee(
|
||||
|
||||
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { Bundle } from "../signer";
|
||||
import { VerificationGatewayFactory } from "../index";
|
||||
|
||||
/**
|
||||
* Generates a deterministic hash of a bundle. Because the signature of the bundle could change, along with the gas property on operations,
|
||||
* those values are set to 0 before hashing. This leads to a more consistent hash for variations of the same bundle.
|
||||
*
|
||||
* @remarks the hash output is senstive to the internal types of the bundle. For example, an identical bundle with a
|
||||
* BigNumber value for one of the properties, vs the same bundle with a hex string value for one of the properties, will
|
||||
* generate different hashes, even though the underlying value may be the same.
|
||||
*
|
||||
* @param bundle the signed bundle to generate the hash for
|
||||
* @param chainId the chain id of the network the bundle is being submitted to
|
||||
* @returns a deterministic hash of the bundle
|
||||
*/
|
||||
export default function hashBundle(bundle: Bundle, chainId: number): string {
|
||||
if (bundle.operations.length !== bundle.senderPublicKeys.length) {
|
||||
throw new Error(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
}
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const verifyMethodName = "verify";
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === verifyMethodName,
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as any],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
|
||||
const encoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(encoding);
|
||||
}
|
||||
@@ -18,6 +18,8 @@ export {
|
||||
OperationResultError,
|
||||
} from "./OperationResults";
|
||||
|
||||
export { default as hashBundle } from "./helpers/hashBundle";
|
||||
|
||||
export {
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Bundle } from "./types";
|
||||
import isValidEmptyBundle from "./isValidEmptyBundle";
|
||||
|
||||
export default (domain: Uint8Array) =>
|
||||
(bundle: Bundle, walletAddress: string): boolean => {
|
||||
(bundle: Bundle, walletAddresses: Array<string>): boolean => {
|
||||
// hubbleBls verifier incorrectly rejects empty bundles
|
||||
if (isValidEmptyBundle(bundle)) {
|
||||
return true;
|
||||
@@ -25,8 +25,8 @@ export default (domain: Uint8Array) =>
|
||||
BigNumber.from(n2).toHexString(),
|
||||
BigNumber.from(n3).toHexString(),
|
||||
]),
|
||||
bundle.operations.map((op) =>
|
||||
encodeMessageForSigning()(op, walletAddress),
|
||||
bundle.operations.map((op, i) =>
|
||||
encodeMessageForSigning()(op, walletAddresses[i]),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
65
contracts/clients/test/hashBundle.test.ts
Normal file
65
contracts/clients/test/hashBundle.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expect } from "chai";
|
||||
import hashBundle from "../src/helpers/hashBundle";
|
||||
import { Bundle } from "../src";
|
||||
import { BigNumber } from "ethers";
|
||||
|
||||
describe("hashBundle", () => {
|
||||
it("should return a valid hash when provided with a valid bundle and chainId", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act
|
||||
const result = hashBundle(bundle, chainId);
|
||||
|
||||
// Assert
|
||||
expect(result).to.be.a("string");
|
||||
expect(result.length).to.equal(66);
|
||||
});
|
||||
|
||||
it("should throw an error when the number of operations does not match the number of public keys", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle1: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [["0x4321", "0x4321", "0x4321", "0x4321"]],
|
||||
};
|
||||
const bundle2: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act & Assert
|
||||
expect(() => hashBundle(bundle1, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
|
||||
expect(() => hashBundle(bundle2, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe("index", () => {
|
||||
"0x2f90b24bbc03de665816b3a632e0c7b5fb837c87541d9337480671613cf1359c",
|
||||
]);
|
||||
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle, [walletAddress])).to.equal(true);
|
||||
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
@@ -79,7 +79,7 @@ describe("index", () => {
|
||||
.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadSig, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadSig, [walletAddress])).to.equal(false);
|
||||
|
||||
const bundleBadMessage: Bundle = {
|
||||
senderPublicKeys: bundle.senderPublicKeys,
|
||||
@@ -99,7 +99,7 @@ describe("index", () => {
|
||||
signature: bundle.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadMessage, [walletAddress])).to.equal(false);
|
||||
});
|
||||
|
||||
it("aggregates transactions", async () => {
|
||||
@@ -131,11 +131,15 @@ describe("index", () => {
|
||||
"0x0235a99bcd1f0793efb7f3307cd349f211a433f60cfab795f5f976298f17a768",
|
||||
]);
|
||||
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle2, otherWalletAddress)).to.equal(true);
|
||||
expect(verify(bundle1, [walletAddress])).to.equal(true);
|
||||
expect(verify(bundle2, [otherWalletAddress])).to.equal(true);
|
||||
|
||||
expect(verify(bundle1, otherWalletAddress)).to.equal(false);
|
||||
expect(verify(bundle2, walletAddress)).to.equal(false);
|
||||
expect(verify(bundle1, [otherWalletAddress])).to.equal(false);
|
||||
expect(verify(bundle2, [walletAddress])).to.equal(false);
|
||||
|
||||
expect(verify(aggBundle, [walletAddress, otherWalletAddress])).to.equal(
|
||||
true,
|
||||
);
|
||||
|
||||
const aggBundleBadMessage: Bundle = {
|
||||
...aggBundle,
|
||||
@@ -156,8 +160,12 @@ describe("index", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(verify(aggBundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(aggBundleBadMessage, otherWalletAddress)).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [walletAddress, otherWalletAddress]),
|
||||
).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [otherWalletAddress, walletAddress]),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it("can aggregate transactions which already have multiple subTransactions", async () => {
|
||||
@@ -188,8 +196,39 @@ describe("index", () => {
|
||||
const aggBundle2 = aggregate(bundles.slice(2, 4));
|
||||
|
||||
const aggAggBundle = aggregate([aggBundle1, aggBundle2]);
|
||||
const walletAddresses = new Array(4).fill(walletAddress);
|
||||
|
||||
expect(verify(aggAggBundle, walletAddress)).to.equal(true);
|
||||
expect(verify(aggAggBundle, walletAddresses)).to.equal(true);
|
||||
});
|
||||
|
||||
it("should fail to verify bundle with wallet address mismatches", async () => {
|
||||
const {
|
||||
bundleTemplate,
|
||||
privateKey,
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
} = samples;
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
const bundle1 = sign(bundleTemplate, walletAddress);
|
||||
const bundle2 = signWithOtherPrivateKey(bundleTemplate, otherWalletAddress);
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(verify(aggBundle, [otherWalletAddress, walletAddress])).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("generates expected publicKeyStr", async () => {
|
||||
@@ -238,6 +277,6 @@ describe("index", () => {
|
||||
|
||||
const emptyBundle = aggregate([]);
|
||||
|
||||
expect(verify(emptyBundle, samples.walletAddress)).to.equal(true);
|
||||
expect(verify(emptyBundle, [samples.walletAddress])).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
@@ -157,21 +157,17 @@ 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 signatureExpiryTimestamp
|
||||
uint256[BLS_KEY_LEN] memory publicKey
|
||||
) public {
|
||||
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, signatureExpiryTimestamp);
|
||||
}
|
||||
else { // wallet already has a key registered, set after delay
|
||||
|
||||
// Can't register new wallet contracts, only what this gateway deployed.
|
||||
if (existingHash != bytes32(0)) { // wallet already has a key registered, set after delay
|
||||
pendingMessageSenderSignatureFromHash[existingHash] = messageSenderSignature;
|
||||
pendingBLSPublicKeyFromHash[existingHash] = publicKey;
|
||||
pendingBLSPublicKeyTimeFromHash[existingHash] = block.timestamp + 604800; // 1 week from now
|
||||
@@ -235,6 +231,11 @@ contract VerificationGateway
|
||||
}
|
||||
}
|
||||
|
||||
require((selectorId != ProxyAdmin.upgrade.selector)
|
||||
&& (selectorId != ProxyAdmin.upgradeAndCall.selector),
|
||||
"VG: wallet not upgradable"
|
||||
);
|
||||
|
||||
wallet.setAnyPending();
|
||||
|
||||
// ensure wallet has pre-approved encodedFunction
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.15;
|
||||
|
||||
import "./VLQ.sol";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chai, { expect } from "chai";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { formatEther, parseEther } from "ethers/lib/utils";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
BlsSigner,
|
||||
MockERC20Factory,
|
||||
NetworkConfig,
|
||||
hashBundle,
|
||||
} from "../clients/src";
|
||||
import getNetworkConfig from "../shared/helpers/getNetworkConfig";
|
||||
|
||||
@@ -65,6 +67,7 @@ describe("BlsProvider", () => {
|
||||
|
||||
afterEach(() => {
|
||||
chai.spy.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("calls a getter method on a contract using call()", async () => {
|
||||
@@ -116,8 +119,6 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should throw an error when sending a modified signed transaction", async () => {
|
||||
// Arrange
|
||||
const address = await blsSigner.getAddress();
|
||||
|
||||
const signedTransaction = await blsSigner.signTransaction({
|
||||
value: parseEther("1"),
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
@@ -134,7 +135,7 @@ describe("BlsProvider", () => {
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
`[{"type":"invalid-signature","description":"invalid signature for wallet address ${address}"}]`,
|
||||
`[{"type":"invalid-signature","description":"invalid bundle signature for signature ${userBundle.signature}"}]`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -286,8 +287,6 @@ describe("BlsProvider", () => {
|
||||
|
||||
it("should throw an error when sending a modified signed transaction", async () => {
|
||||
// Arrange
|
||||
const address = await blsSigner.getAddress();
|
||||
|
||||
const signedTransaction = await blsSigner.signTransactionBatch({
|
||||
transactions: [
|
||||
{
|
||||
@@ -309,7 +308,7 @@ describe("BlsProvider", () => {
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
`[{"type":"invalid-signature","description":"invalid signature for wallet address ${address}"}]`,
|
||||
`[{"type":"invalid-signature","description":"invalid bundle signature for signature ${userBundle.signature}"}]`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -708,4 +707,49 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
expect(feeData.gasPrice).to.deep.equal(expectedFeeData.gasPrice);
|
||||
});
|
||||
|
||||
it("should return a deterministic hash generated by the aggregator that can be replicated by the client module", async () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
value: parseEther("1"),
|
||||
from: await blsSigner.getAddress(),
|
||||
};
|
||||
|
||||
const action = {
|
||||
ethValue: transaction.value?.toHexString(),
|
||||
contractAddress: transaction.to!.toString(),
|
||||
encodedFunction: "0x",
|
||||
};
|
||||
|
||||
// BlsWalletWrapper.getRandomBlsPrivateKey from "estimateGas" method results in slightly different
|
||||
// fee estimates. This fake avoids this mismatch by stubbing a constant value.
|
||||
sinon.replace(
|
||||
BlsWalletWrapper,
|
||||
"getRandomBlsPrivateKey",
|
||||
sinon.fake.resolves(blsSigner.wallet.blsWalletSigner.privateKey),
|
||||
);
|
||||
|
||||
const feeEstimate = await blsProvider.estimateGas(transaction);
|
||||
const actionsWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = blsSigner.wallet.sign({
|
||||
nonce: await blsSigner.wallet.Nonce(),
|
||||
gas: 100000,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
|
||||
// Act
|
||||
const transactionResponse = await blsSigner.sendTransaction(transaction);
|
||||
|
||||
// Assert
|
||||
const expectedTransactionHash = hashBundle(
|
||||
bundle,
|
||||
blsProvider.network.chainId,
|
||||
);
|
||||
expect(transactionResponse.hash).to.deep.equal(expectedTransactionHash);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("Recovery", async function () {
|
||||
wallet1,
|
||||
vg,
|
||||
"setBLSKeyForWallet",
|
||||
[addressSignature, wallet2.PublicKey(), signatureExpiryTimestamp],
|
||||
[addressSignature, wallet2.PublicKey()],
|
||||
1,
|
||||
30_000_000,
|
||||
);
|
||||
@@ -372,7 +372,7 @@ describe("Recovery", async function () {
|
||||
wallet1,
|
||||
vg,
|
||||
"setBLSKeyForWallet",
|
||||
[attackSignature, walletAttacker.PublicKey(), signatureExpiryTimestamp],
|
||||
[attackSignature, walletAttacker.PublicKey()],
|
||||
recoveredWalletNonce++,
|
||||
30_000_000,
|
||||
);
|
||||
@@ -660,7 +660,7 @@ describe("Recovery", async function () {
|
||||
wallet1,
|
||||
vg,
|
||||
"setBLSKeyForWallet",
|
||||
[addressSignature, wallet2.PublicKey(), invalidSignatureExpiryTimestamp],
|
||||
[addressSignature, wallet2.PublicKey()],
|
||||
1,
|
||||
30_000_000,
|
||||
);
|
||||
@@ -724,7 +724,7 @@ describe("Recovery", async function () {
|
||||
wallet1,
|
||||
vg,
|
||||
"setBLSKeyForWallet",
|
||||
[addressSignature, wallet2.PublicKey(), signatureExpiryTimestamp],
|
||||
[addressSignature, wallet2.PublicKey()],
|
||||
1,
|
||||
30_000_000,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { expect } from "chai";
|
||||
import { BigNumber, ContractReceipt } from "ethers";
|
||||
import { solidityPack } from "ethers/lib/utils";
|
||||
import { ethers, network } from "hardhat";
|
||||
import { ethers } from "hardhat";
|
||||
|
||||
import { expectPubkeysEql } from "./expect";
|
||||
import {
|
||||
ActionData,
|
||||
BlsWalletWrapper,
|
||||
getOperationResults,
|
||||
} from "../clients/src";
|
||||
import { ActionData, getOperationResults } from "../clients/src";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import {
|
||||
proxyAdminBundle,
|
||||
proxyAdminCall,
|
||||
} from "../shared/helpers/callProxyAdmin";
|
||||
import { proxyAdminCall } from "../shared/helpers/callProxyAdmin";
|
||||
import getPublicKeyFromHash from "../shared/helpers/getPublicKeyFromHash";
|
||||
import deploy from "../shared/deploy";
|
||||
|
||||
const expectOperationsToSucceed = (txnReceipt: ContractReceipt) => {
|
||||
const opResults = getOperationResults(txnReceipt);
|
||||
@@ -44,7 +36,7 @@ describe("Upgrade", async function () {
|
||||
fx = await Fixture.getSingleton();
|
||||
});
|
||||
|
||||
it("should upgrade wallet contract", async () => {
|
||||
it("should NOT upgrade wallet contract", async () => {
|
||||
const MockWalletUpgraded = await ethers.getContractFactory(
|
||||
"MockWalletUpgraded",
|
||||
);
|
||||
@@ -57,243 +49,19 @@ describe("Upgrade", async function () {
|
||||
wallet.address,
|
||||
mockWalletUpgraded.address,
|
||||
]);
|
||||
expectOperationsToSucceed(txnReceipt1);
|
||||
|
||||
// Advance time one week
|
||||
const latestTimestamp = (await ethers.provider.getBlock("latest"))
|
||||
.timestamp;
|
||||
await network.provider.send("evm_setNextBlockTimestamp", [
|
||||
BigNumber.from(latestTimestamp)
|
||||
.add(safetyDelaySeconds + 1)
|
||||
.toHexString(),
|
||||
]);
|
||||
expectOperationFailure(txnReceipt1, "VG: wallet not upgradable");
|
||||
|
||||
// make call
|
||||
const txnReceipt2 = await proxyAdminCall(fx, wallet, "upgrade", [
|
||||
const txnReceipt2 = await proxyAdminCall(fx, wallet, "upgradeAndCall", [
|
||||
wallet.address,
|
||||
mockWalletUpgraded.address,
|
||||
[],
|
||||
]);
|
||||
expectOperationsToSucceed(txnReceipt2);
|
||||
|
||||
const newBLSWallet = MockWalletUpgraded.attach(wallet.address);
|
||||
await (await newBLSWallet.setNewData(wallet.address)).wait();
|
||||
await expect(newBLSWallet.newData()).to.eventually.equal(wallet.address);
|
||||
expectOperationFailure(txnReceipt2, "VG: wallet not upgradable");
|
||||
});
|
||||
|
||||
it("should register with new verification gateway", async () => {
|
||||
// Deploy new verification gateway
|
||||
|
||||
const [signer] = await ethers.getSigners();
|
||||
|
||||
const deployment2 = await deploy(
|
||||
signer,
|
||||
ethers.utils.solidityPack(["uint256"], [2]),
|
||||
);
|
||||
|
||||
const vg2 = deployment2.verificationGateway;
|
||||
|
||||
// Recreate hubble bls signer
|
||||
const walletOldVg = await fx.createBLSWallet();
|
||||
const walletAddress = walletOldVg.address;
|
||||
const blsSecret = walletOldVg.blsWalletSigner.privateKey;
|
||||
|
||||
// Sign simple address message
|
||||
const walletNewVg = await BlsWalletWrapper.connect(
|
||||
blsSecret,
|
||||
vg2.address,
|
||||
vg2.provider,
|
||||
);
|
||||
|
||||
const signatureExpiryTimestamp =
|
||||
(await fx.provider.getBlock("latest")).timestamp +
|
||||
safetyDelaySeconds +
|
||||
signatureExpiryOffsetSeconds;
|
||||
const addressMessage = solidityPack(
|
||||
["address", "uint256"],
|
||||
[walletAddress, signatureExpiryTimestamp],
|
||||
);
|
||||
const addressSignature = walletNewVg.signMessage(addressMessage);
|
||||
|
||||
const proxyAdmin2Address = await vg2.walletProxyAdmin();
|
||||
// Get admin action to change proxy
|
||||
const bundle = await proxyAdminBundle(fx, walletOldVg, "changeProxyAdmin", [
|
||||
walletAddress,
|
||||
proxyAdmin2Address,
|
||||
]);
|
||||
const changeProxyAction = bundle.operations[0].actions[0];
|
||||
|
||||
// prepare call
|
||||
const txnReceipt = await proxyAdminCall(
|
||||
fx,
|
||||
walletOldVg,
|
||||
"changeProxyAdmin",
|
||||
[walletAddress, proxyAdmin2Address],
|
||||
);
|
||||
expectOperationsToSucceed(txnReceipt);
|
||||
|
||||
// Advance time one week
|
||||
await fx.advanceTimeBy(safetyDelaySeconds + 1);
|
||||
|
||||
const hash = walletOldVg.blsWalletSigner.getPublicKeyHash();
|
||||
|
||||
const setExternalWalletAction: ActionData = {
|
||||
ethValue: BigNumber.from(0),
|
||||
contractAddress: vg2.address,
|
||||
encodedFunction: vg2.interface.encodeFunctionData("setBLSKeyForWallet", [
|
||||
addressSignature,
|
||||
walletOldVg.PublicKey(),
|
||||
signatureExpiryTimestamp,
|
||||
]),
|
||||
};
|
||||
|
||||
const setTrustedBLSGatewayAction: ActionData = {
|
||||
ethValue: BigNumber.from(0),
|
||||
contractAddress: fx.verificationGateway.address,
|
||||
encodedFunction: fx.verificationGateway.interface.encodeFunctionData(
|
||||
"setTrustedBLSGateway",
|
||||
[hash, vg2.address],
|
||||
),
|
||||
};
|
||||
|
||||
// Upgrading the gateway requires these three steps:
|
||||
// 1. register external wallet in vg2
|
||||
// 2. change proxy admin to that in vg2
|
||||
// 3. lastly, set wallet's new trusted gateway
|
||||
//
|
||||
// If (1) or (2) are skipped, then (3) should fail, and therefore the whole
|
||||
// operation should fail.
|
||||
|
||||
{
|
||||
// Fail if setExternalWalletAction is skipped
|
||||
|
||||
const { successes } =
|
||||
await fx.verificationGateway.callStatic.processBundle(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(1),
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [
|
||||
// skip: setExternalWalletAction,
|
||||
changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(successes).to.deep.equal([false]);
|
||||
}
|
||||
|
||||
{
|
||||
// Fail if changeProxyAction is skipped
|
||||
|
||||
const { successes } =
|
||||
await fx.verificationGateway.callStatic.processBundle(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(1),
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [
|
||||
setExternalWalletAction,
|
||||
// skip: changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(successes).to.deep.equal([false]);
|
||||
}
|
||||
|
||||
{
|
||||
// Succeed if nothing is skipped
|
||||
|
||||
const { successes } =
|
||||
await fx.verificationGateway.callStatic.processBundle(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(1),
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [
|
||||
setExternalWalletAction,
|
||||
changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(successes).to.deep.equal([true]);
|
||||
}
|
||||
|
||||
await expect(vg2.walletFromHash(hash)).to.eventually.not.equal(
|
||||
walletAddress,
|
||||
);
|
||||
|
||||
// Now actually perform the upgrade so we can perform some more detailed
|
||||
// checks.
|
||||
await fx.processBundleWithExtraGas(
|
||||
walletOldVg.sign({
|
||||
nonce: BigNumber.from(1),
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [
|
||||
setExternalWalletAction,
|
||||
changeProxyAction,
|
||||
setTrustedBLSGatewayAction,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Create required objects for data/contracts for checks
|
||||
const proxyAdmin = await ethers.getContractAt(
|
||||
"ProxyAdmin",
|
||||
await vg2.walletProxyAdmin(),
|
||||
);
|
||||
|
||||
// Direct checks corresponding to each action
|
||||
await expect(vg2.walletFromHash(hash)).to.eventually.equal(walletAddress);
|
||||
await expect(vg2.hashFromWallet(walletAddress)).to.eventually.equal(hash);
|
||||
await expect(proxyAdmin.getProxyAdmin(walletAddress)).to.eventually.equal(
|
||||
proxyAdmin.address,
|
||||
);
|
||||
expectPubkeysEql(
|
||||
await getPublicKeyFromHash(vg2, hash),
|
||||
walletOldVg.PublicKey(),
|
||||
);
|
||||
|
||||
const blsWallet = await ethers.getContractAt("BLSWallet", walletAddress);
|
||||
// New verification gateway pending
|
||||
await expect(blsWallet.trustedBLSGateway()).to.eventually.equal(
|
||||
fx.verificationGateway.address,
|
||||
);
|
||||
// Advance time one week
|
||||
await fx.advanceTimeBy(safetyDelaySeconds + 1);
|
||||
// set pending
|
||||
await (await blsWallet.setAnyPending()).wait();
|
||||
// Check new verification gateway was set
|
||||
await expect(blsWallet.trustedBLSGateway()).to.eventually.equal(
|
||||
vg2.address,
|
||||
);
|
||||
|
||||
await walletNewVg.syncWallet(vg2);
|
||||
// Check new gateway has wallet via static call through new gateway
|
||||
const bundleResult = await vg2.callStatic.processBundle(
|
||||
fx.blsWalletSigner.aggregate([
|
||||
walletNewVg.sign({
|
||||
nonce: BigNumber.from(2),
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: vg2.address,
|
||||
encodedFunction: vg2.interface.encodeFunctionData(
|
||||
"walletFromHash",
|
||||
[hash],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const walletFromHashAddress = ethers.utils.defaultAbiCoder.decode(
|
||||
["address"],
|
||||
bundleResult.results[0][0], // first and only operation/action result
|
||||
)[0];
|
||||
expect(walletFromHashAddress).to.equal(walletAddress);
|
||||
// Still possible to point wallets to a new gateway if desired, just not with v1 deployment
|
||||
});
|
||||
|
||||
it("should change mapping of an address to hash", async () => {
|
||||
@@ -347,7 +115,6 @@ describe("Upgrade", async function () {
|
||||
encodedFunction: vg1.interface.encodeFunctionData("setBLSKeyForWallet", [
|
||||
addressSignature,
|
||||
wallet2.PublicKey(),
|
||||
signatureExpiryTimestamp,
|
||||
]),
|
||||
};
|
||||
|
||||
|
||||
@@ -112,9 +112,8 @@ yarn run dev:chrome # or dev:firefox, dev:opera
|
||||
|
||||
- In general, the bundle or submission issues we've encountered have been us misconfiguring the data in the bundle or not configuring the aggregator properly.
|
||||
- Be careful using Hardhat accounts 0 and 1 in your code when running a local aggregator. This is because the local aggregator config uses the same key pairs as Hardhat accounts 0 and 1 by default. You can get around this by not using accounts 0 and 1 elsewhere, or changing the default accounts that the aggregator uses locally.
|
||||
- When packages are updated in the aggregator, you'll need to reload the deno cache as the setup script won't do this for you. You can do this with `deno cache -r deps.ts` in the `./aggregator` directory.
|
||||
- If running Quill against a local node, and if you're using MetaMask to fund Quill, make sure the MetaMask
|
||||
localhost network uses chainId `1337`.
|
||||
- Sometimes there are issues related to the deno cache. You can clear it with `deno cache -r deps.ts test/deps.ts` in the `./aggregator` directory.
|
||||
- If running Quill against a local node, and if you're using MetaMask to fund Quill, make sure the MetaMask localhost network uses chainId `1337`.
|
||||
|
||||
### Tests
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"assert-browserify": "^2.0.0",
|
||||
"async-mutex": "^0.3.2",
|
||||
"axios": "^0.27.2",
|
||||
"bls-wallet-clients": "0.9.0",
|
||||
"bls-wallet-clients": "0.9.0-405e23a",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"bs58check": "^2.1.2",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
|
||||
@@ -24,7 +24,9 @@ const OnboardingActionPanel: FunctionComponent = () => {
|
||||
<div
|
||||
className={[
|
||||
'h-screen',
|
||||
'p-32',
|
||||
'p-4',
|
||||
'md:p-8',
|
||||
'xl:p-28',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'flex-grow',
|
||||
@@ -33,7 +35,7 @@ const OnboardingActionPanel: FunctionComponent = () => {
|
||||
].join(' ')}
|
||||
>
|
||||
<WorkflowNumbers max={3} />
|
||||
<div className="w-96">
|
||||
<div className="w-[24rem] lg:w-[28rem]">
|
||||
{
|
||||
[
|
||||
<PasswordCreationPanel
|
||||
|
||||
@@ -56,7 +56,7 @@ const OnboardingInfoPanel: FunctionComponent = () => {
|
||||
return (
|
||||
<div className="bg-blue-500 flex flex-col w-2/5">
|
||||
<div
|
||||
className="h-screen p-32 flex flex-col justify-between"
|
||||
className="h-screen p-4 md:p-8 xl:p-28 flex flex-col justify-between"
|
||||
style={{
|
||||
background: `center no-repeat url(${runtime.getURL(
|
||||
'assets/info-panel-pretty-curve.svg',
|
||||
@@ -64,11 +64,11 @@ const OnboardingInfoPanel: FunctionComponent = () => {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-64 w-full rounded-md"
|
||||
className="h-24 md:h-40 lg:h-64 w-full rounded-md"
|
||||
style={{
|
||||
background: `url(${runtime.getURL(
|
||||
`assets/onboarding-art-${pageIndex + 1}.svg`,
|
||||
)}) no-repeat center`,
|
||||
)}) no-repeat center contain`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-grow text-white py-8">
|
||||
|
||||
@@ -103,7 +103,7 @@ const ReviewSecretPhrasePanel: FunctionComponent<{
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[28rem]">
|
||||
<div className="w-[24rem] lg:w-[28rem]">
|
||||
<div className="mb-10">
|
||||
<div className="font-bold">
|
||||
Ok, last step before you get started with Quill!
|
||||
|
||||
@@ -17,7 +17,7 @@ const ViewSecretPhrasePanel: FunctionComponent<{
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-[28rem]">
|
||||
<div className="w-[24rem] lg:w-[28rem]">
|
||||
<div className="mb-10">
|
||||
<div className="font-bold">
|
||||
Congratulations!
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
List,
|
||||
CaretDoubleLeft,
|
||||
Wallet,
|
||||
Link as LinkIcon,
|
||||
AddressBook,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
Lock,
|
||||
} from 'phosphor-react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import QuillHeading from '../../components/QuillHeading';
|
||||
|
||||
const navigationTargets = [
|
||||
@@ -35,6 +38,7 @@ const navigationTargets = [
|
||||
|
||||
export const Navigation: React.FunctionComponent = () => {
|
||||
const { pathname } = useLocation();
|
||||
const [sidebarVisible, setSidebarVisible] = useState(false);
|
||||
|
||||
const isCurrentRoute = (target: string) => {
|
||||
if (pathname === target) {
|
||||
@@ -49,30 +53,79 @@ export const Navigation: React.FunctionComponent = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-52 px-4 py-12">
|
||||
<QuillHeading />
|
||||
<div className="mt-8 flex flex-col gap-4 justify-items-center">
|
||||
{navigationTargets.map((item) => (
|
||||
<Link to={item.link ?? item.target} key={item.name}>
|
||||
<div
|
||||
className={`flex gap-4 p-3 rounded-lg ${
|
||||
isCurrentRoute(item.target) && 'bg-grey-200'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`${isCurrentRoute(item.target) && 'text-blue-500'}`}
|
||||
<nav className="flex">
|
||||
<div
|
||||
className={`${
|
||||
sidebarVisible ? 'absolute' : 'flex-1'
|
||||
} items-center justify-center`}
|
||||
>
|
||||
<button
|
||||
className="p-4 lg:hidden"
|
||||
type="button"
|
||||
onClick={() => setSidebarVisible(!sidebarVisible)}
|
||||
>
|
||||
{sidebarVisible ? (
|
||||
<CaretDoubleLeft className="icon-md" />
|
||||
) : (
|
||||
<List className="icon-md" />
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`${
|
||||
sidebarVisible ? 'hidden' : 'flex-1'
|
||||
} p-2 lg:hidden mt-4 flex flex-col gap-4 justify-items-center`}
|
||||
>
|
||||
{navigationTargets.map((item) => (
|
||||
<Link to={item.link ?? item.target} key={item.name}>
|
||||
<div
|
||||
className={`flex gap-4 p-2 rounded-lg ${
|
||||
isCurrentRoute(item.target) && 'bg-grey-200'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
{item.name}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<span
|
||||
className={`${
|
||||
isCurrentRoute(item.target) && 'text-blue-500'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 p-3 rounded-lg mt-20">
|
||||
<Lock className="icon-md" />
|
||||
Lock
|
||||
|
||||
<div
|
||||
className={`w-52 px-4 py-12 ${
|
||||
sidebarVisible ? 'block' : 'hidden'
|
||||
} lg:block lg:flex-shrink-0`}
|
||||
>
|
||||
<QuillHeading />
|
||||
<div className="mt-8 flex flex-col gap-4 justify-items-center">
|
||||
{navigationTargets.map((item) => (
|
||||
<Link to={item.link ?? item.target} key={item.name}>
|
||||
<div
|
||||
className={`flex gap-4 p-3 rounded-lg ${
|
||||
isCurrentRoute(item.target) && 'bg-grey-200'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
isCurrentRoute(item.target) && 'text-blue-500'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
{item.name}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2 p-3 rounded-lg mt-20">
|
||||
<Lock className="icon-md" />
|
||||
Lock
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ const Balance: FunctionComponent<{ address: string }> = ({ address }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <>{ethers.utils.formatEther($balanceWeiHex)} ETH</>;
|
||||
return <>{ethers.utils.formatEther($balanceWeiHex).slice(0, 12)} ETH</>;
|
||||
};
|
||||
|
||||
export default Balance;
|
||||
|
||||
@@ -31,7 +31,7 @@ const WorkflowNumbers: FunctionComponent<{
|
||||
);
|
||||
};
|
||||
|
||||
const RecoverWalletModal = () => {
|
||||
const RecoverWalletModal = (props: { className?: string }) => {
|
||||
const [modalIsOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
||||
const [walletPrivateKey, setWalletPrivateKey] = useState<string>('');
|
||||
@@ -44,7 +44,7 @@ const RecoverWalletModal = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${props.className}`}>
|
||||
<Button onPress={() => setIsOpen(true)} className="btn-secondary">
|
||||
Import
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ const AmountSelector: FunctionComponent<{
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-body">Select Amount</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex justify-start gap-2">
|
||||
<TextBox
|
||||
value={amountValid}
|
||||
className="text-right"
|
||||
@@ -58,12 +58,12 @@ const AmountSelector: FunctionComponent<{
|
||||
<Display cell={selectedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex justify-start gap-2">
|
||||
<div>
|
||||
<CurrencyDisplay chainValue={amountValidNumber} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-start">
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={async () =>
|
||||
|
||||
@@ -17,11 +17,12 @@ const AssetSelector: FunctionComponent<{
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-body">Select Asset</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
className={[
|
||||
'flex',
|
||||
'flex-row',
|
||||
'flex-col',
|
||||
'sm:flex-row',
|
||||
'p-4',
|
||||
'gap-4',
|
||||
'rounded-lg',
|
||||
|
||||
@@ -49,7 +49,7 @@ const RecipientSelector: FunctionComponent<{
|
||||
<TextBox value={searchText} placeholder="Search" />
|
||||
</div>
|
||||
{recipients.length === 0 && 'No recipients found'}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||
{recipients.map((r) => {
|
||||
if (r === undefined) {
|
||||
return <div />;
|
||||
@@ -60,7 +60,9 @@ const RecipientSelector: FunctionComponent<{
|
||||
key={`${r.name}:${r.address}`}
|
||||
className={[
|
||||
'flex',
|
||||
'flex-row',
|
||||
'flex-col',
|
||||
'lg:flex-row',
|
||||
'flex-wrap',
|
||||
'p-4',
|
||||
'gap-4',
|
||||
'rounded-lg',
|
||||
|
||||
@@ -34,8 +34,8 @@ export const WalletSummary: React.FunctionComponent<IWalletSummary> = ({
|
||||
${expanded && 'bg-white border-2 border-blue-500 shadow-xl'}
|
||||
`}
|
||||
>
|
||||
<div className="flex place-items-center gap-4 ">
|
||||
<div className="w-5 h-5">
|
||||
<div className="flex place-items-center gap-4 flex-wrap">
|
||||
<div className="flex flex-row gap-2 items-center justify-center">
|
||||
<input
|
||||
type="radio"
|
||||
checked={expanded}
|
||||
@@ -43,15 +43,15 @@ export const WalletSummary: React.FunctionComponent<IWalletSummary> = ({
|
||||
className="h-5 w-5 cursor-pointer"
|
||||
{...onAction(onActionParam)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex place-items-center gap-2">
|
||||
<Blockies
|
||||
seed={wallet.address}
|
||||
className="rounded-md"
|
||||
size={5}
|
||||
scale={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex place-items-center gap-2">
|
||||
<div>
|
||||
{wallet.name}
|
||||
<div
|
||||
@@ -66,7 +66,7 @@ export const WalletSummary: React.FunctionComponent<IWalletSummary> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-body">
|
||||
<div className="text-sm md:text-body">
|
||||
<Balance address={wallet.address} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export const WalletSummary: React.FunctionComponent<IWalletSummary> = ({
|
||||
{/* Details */}
|
||||
{expanded && (
|
||||
<div className="mt-6">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<Button
|
||||
onPress={() => navigate('/wallets/send')}
|
||||
className="btn-primary"
|
||||
|
||||
@@ -24,11 +24,32 @@ export const WalletsWrapper: FunctionComponent = () => {
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex justify-between place-items-center">
|
||||
<div
|
||||
className={[
|
||||
'flex',
|
||||
'flex-col',
|
||||
'lg:flex-row',
|
||||
'justify-between',
|
||||
'place-items-center',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="text-body">Wallets</div>
|
||||
<div className="flex gap-2">
|
||||
<RecoverWalletModal />
|
||||
<Button onPress={rpc.addHDAccount} className="btn-primary">
|
||||
<div
|
||||
className={[
|
||||
'flex',
|
||||
'gap-2',
|
||||
'mt-4',
|
||||
'lg:mt-0',
|
||||
'lg:ml-2',
|
||||
'w-full',
|
||||
'lg:w-auto',
|
||||
].join(' ')}
|
||||
>
|
||||
<RecoverWalletModal className="w-1/2" />
|
||||
<Button
|
||||
onPress={rpc.addHDAccount}
|
||||
className="btn-primary w-1/2 justify-center"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,8 @@ export const WalletsPage: React.FunctionComponent = () => (
|
||||
'bg-grey-100',
|
||||
'border-x',
|
||||
'border-grey-300',
|
||||
'p-8',
|
||||
'p-4',
|
||||
'lg:p-8',
|
||||
'overflow-y-scroll',
|
||||
].join(' ')}
|
||||
>
|
||||
@@ -99,7 +100,7 @@ export const WalletsPage: React.FunctionComponent = () => (
|
||||
</div>
|
||||
|
||||
{/* details pane */}
|
||||
<div className="w-2/3 p-8 overflow-y-scroll">
|
||||
<div className="w-2/3 p-4 lg:p-8 overflow-y-scroll">
|
||||
<Routes>
|
||||
{routes.map((item) => (
|
||||
<Route
|
||||
|
||||
@@ -1791,16 +1791,6 @@
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
|
||||
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
|
||||
"@ethersproject/transactions@^5.5.0", "@ethersproject/transactions@^5.6.2":
|
||||
version "5.6.2"
|
||||
@@ -2898,10 +2888,10 @@ blakejs@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
|
||||
integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
|
||||
|
||||
bls-wallet-clients@0.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==
|
||||
bls-wallet-clients@0.9.0-405e23a:
|
||||
version "0.9.0-405e23a"
|
||||
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
|
||||
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
Reference in New Issue
Block a user