mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-10 06:17:55 -05:00
Add hash bundle fn & ability to get aggregate bundle to client module
This commit is contained in:
@@ -30,8 +30,8 @@ export type AddBundleResponse = { hash: string } | {
|
||||
};
|
||||
|
||||
type BundleWithoutSignature = {
|
||||
publicKey: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
|
||||
operation: Omit<Operation, "gas">
|
||||
senderPublicKeys: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
|
||||
operations: Omit<Operation, "gas">;
|
||||
};
|
||||
|
||||
export default class BundleService {
|
||||
@@ -253,8 +253,8 @@ export default class BundleService {
|
||||
async #hashSubBundles(bundle: Bundle): Promise<Array<string>> {
|
||||
const bundlesWithoutSignature: Array<BundleWithoutSignature> =
|
||||
bundle.operations.map((operation, index) => ({
|
||||
publicKey: bundle.senderPublicKeys[index],
|
||||
operation: {
|
||||
senderPublicKeys: bundle.senderPublicKeys[index],
|
||||
operations: {
|
||||
nonce: operation.nonce,
|
||||
actions: operation.actions,
|
||||
},
|
||||
@@ -263,8 +263,8 @@ export default class BundleService {
|
||||
const serializedBundlesWithoutSignature = bundlesWithoutSignature.map(
|
||||
bundleWithoutSignature => {
|
||||
return JSON.stringify({
|
||||
publicKey: bundleWithoutSignature.publicKey,
|
||||
operation: bundleWithoutSignature.operation,
|
||||
senderPublicKeys: bundleWithoutSignature.senderPublicKeys,
|
||||
operations: bundleWithoutSignature.operations,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -274,16 +274,16 @@ Fixture.test("hashes bundle with single operation", async (fx) => {
|
||||
|
||||
const expectedSubBundleHashes = await Promise.all(bundle.operations.map(async (operation, index) => {
|
||||
const bundlesWithoutSignature = {
|
||||
publicKey: bundle.senderPublicKeys[index],
|
||||
operation: {
|
||||
senderPublicKeys: bundle.senderPublicKeys[index],
|
||||
operations: {
|
||||
nonce: operation.nonce,
|
||||
actions: operation.actions,
|
||||
},
|
||||
}
|
||||
|
||||
const serializedBundle = JSON.stringify({
|
||||
publicKey: bundlesWithoutSignature.publicKey,
|
||||
operation: bundlesWithoutSignature.operation,
|
||||
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
|
||||
operations: bundlesWithoutSignature.operations,
|
||||
});
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))
|
||||
@@ -341,16 +341,16 @@ Fixture.test("hashes bundle with multiple operations", async (fx) => {
|
||||
|
||||
const expectedSubBundleHashes = await Promise.all(bundle.operations.map(async (operation, index) => {
|
||||
const bundlesWithoutSignature = {
|
||||
publicKey: bundle.senderPublicKeys[index],
|
||||
operation: {
|
||||
senderPublicKeys: bundle.senderPublicKeys[index],
|
||||
operations: {
|
||||
nonce: operation.nonce,
|
||||
actions: operation.actions,
|
||||
},
|
||||
}
|
||||
|
||||
const serializedBundle = JSON.stringify({
|
||||
publicKey: bundlesWithoutSignature.publicKey,
|
||||
operation: bundlesWithoutSignature.operation,
|
||||
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
|
||||
operations: bundlesWithoutSignature.operations,
|
||||
});
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))
|
||||
@@ -377,16 +377,16 @@ Fixture.test("hashes empty bundle", async (fx) => {
|
||||
|
||||
const expectedSubBundleHashes = bundle.operations.map(async (operation, index) => {
|
||||
const bundlesWithoutSignature = {
|
||||
publicKey: bundle.senderPublicKeys[index],
|
||||
operation: {
|
||||
senderPublicKeys: bundle.senderPublicKeys[index],
|
||||
operations: {
|
||||
nonce: operation.nonce,
|
||||
actions: operation.actions,
|
||||
},
|
||||
}
|
||||
|
||||
const serializedBundle = JSON.stringify({
|
||||
publicKey: bundlesWithoutSignature.publicKey,
|
||||
operation: bundlesWithoutSignature.operation,
|
||||
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
|
||||
operations: bundlesWithoutSignature.operations,
|
||||
});
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))
|
||||
|
||||
@@ -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}/aggrgateBundle/${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}`, {
|
||||
|
||||
@@ -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(
|
||||
|
||||
50
contracts/clients/src/helpers/hashBundle.ts
Normal file
50
contracts/clients/src/helpers/hashBundle.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BigNumberish, ethers } from "ethers";
|
||||
import { Bundle, Operation } from "../signer";
|
||||
|
||||
type BundleWithoutSignature = {
|
||||
senderPublicKeys: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
|
||||
operations: Omit<Operation, "gas">;
|
||||
};
|
||||
|
||||
export 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 bundlesWithoutSignature: Array<BundleWithoutSignature> =
|
||||
bundle.operations.map((operation, index) => ({
|
||||
senderPublicKeys: bundle.senderPublicKeys[index],
|
||||
operations: {
|
||||
nonce: operation.nonce,
|
||||
actions: operation.actions,
|
||||
},
|
||||
}));
|
||||
|
||||
const serializedBundlesWithoutSignature = bundlesWithoutSignature.map(
|
||||
(bundleWithoutSignature) => {
|
||||
return JSON.stringify({
|
||||
senderPublicKeys: bundleWithoutSignature.senderPublicKeys,
|
||||
operations: bundleWithoutSignature.operations,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const bundleSubHashes = serializedBundlesWithoutSignature.map(
|
||||
async (serializedBundleWithoutSignature) => {
|
||||
const bundleHash = ethers.utils.keccak256(
|
||||
ethers.utils.toUtf8Bytes(serializedBundleWithoutSignature),
|
||||
);
|
||||
|
||||
const encoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(encoding);
|
||||
},
|
||||
);
|
||||
|
||||
const concatenatedHashes = bundleSubHashes.join("");
|
||||
return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(concatenatedHashes));
|
||||
}
|
||||
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); // A keccak256 hash is 32 bytes, or 64 characters, plus the "0x" prefix
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user