mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-12 23:38:04 -05:00
Compare commits
31 Commits
v0.5.0
...
tx-data-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac01049d7 | ||
|
|
8dce0947b0 | ||
|
|
f87cc7e6cb | ||
|
|
77fb5f1263 | ||
|
|
7aa4ffe10b | ||
|
|
e409094050 | ||
|
|
5a32f14ff6 | ||
|
|
0f5a0656e2 | ||
|
|
04359f7518 | ||
|
|
9b4d1c5401 | ||
|
|
1ce28b9c2c | ||
|
|
54227a57b0 | ||
|
|
f9ce7be5b5 | ||
|
|
2a20bfeb8d | ||
|
|
275d593b5c | ||
|
|
00d948376a | ||
|
|
0e51ecb5fe | ||
|
|
576e778855 | ||
|
|
4d170f73dd | ||
|
|
cd010324a5 | ||
|
|
29542e4c98 | ||
|
|
1ff60d5dd1 | ||
|
|
ebf415c573 | ||
|
|
b296a01a80 | ||
|
|
0e7e42154b | ||
|
|
0940333e3c | ||
|
|
831632ce8a | ||
|
|
a58dcdaee7 | ||
|
|
162072155d | ||
|
|
e675062a53 | ||
|
|
95a107f6b8 |
@@ -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-2a20bfe",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -887,10 +887,10 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@0.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-2a20bfe:
|
||||
version "0.9.0-2a20bfe"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-2a20bfe.tgz#2e39757a18df3ba78d816ae15f6b88000443a2a6"
|
||||
integrity sha512-w4efcArPBEowrAkIdVYc2mOLlkN8E5O9eIqEhoo6IrRVrN21p/JVNdoot4N3o5MAKFbeaYfid/u9lL6p2DNdiw==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
64120
aggregator/data/blocksSample.json
Normal file
64120
aggregator/data/blocksSample.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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-2a20bfe";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
@@ -70,12 +70,13 @@ export {
|
||||
getConfig,
|
||||
MockERC20Factory,
|
||||
VerificationGatewayFactory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
|
||||
|
||||
// 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-2a20bfe";
|
||||
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 { mapValues, once } from "npm:@s-libs/micro-dash@15.2.0";
|
||||
|
||||
49
aggregator/manualTests/analysis/ByteStream.ts
Normal file
49
aggregator/manualTests/analysis/ByteStream.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
|
||||
export default class ByteStream {
|
||||
pos = 2;
|
||||
|
||||
constructor(public data: string) {
|
||||
assert(/^0x[0-9a-fA-F]*/.test(data));
|
||||
assert(data.length % 2 === 0);
|
||||
}
|
||||
|
||||
getN(len: number): string {
|
||||
const newPos = this.pos + 2 * len;
|
||||
assert(newPos <= this.data.length);
|
||||
|
||||
const res = `0x${this.data.slice(this.pos, newPos)}`;
|
||||
this.pos = newPos;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
peekN(len: number) {
|
||||
const res = this.getN(len);
|
||||
this.pos -= 2 * len;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
get(): number {
|
||||
return parseInt(this.getN(1).slice(2), 16);
|
||||
}
|
||||
|
||||
peek(): number {
|
||||
const res = this.get();
|
||||
this.pos -= 2;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getTail(): string {
|
||||
const res = `0x${this.data.slice(this.pos)}`;
|
||||
this.pos = this.data.length;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bytesRemaining() {
|
||||
return (this.data.length - this.pos) / 2;
|
||||
}
|
||||
}
|
||||
125
aggregator/manualTests/analysis/Calculator.ts
Normal file
125
aggregator/manualTests/analysis/Calculator.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { mapValues, once } from "../../deps.ts";
|
||||
|
||||
import blocks from "../../data/blocksSample.json" assert { type: "json" };
|
||||
import { sum } from "./util.ts";
|
||||
import MultiEncoder from "./MultiEncoder.ts";
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
|
||||
export default class Calculator {
|
||||
constructor(
|
||||
public multiEncoder: MultiEncoder,
|
||||
) {}
|
||||
|
||||
transactions = once(() => blocks.map((b) => b.transactions).flat());
|
||||
transactionData = once(() => this.transactions().map((tx) => tx.input));
|
||||
|
||||
encodedTransactionData = once(() =>
|
||||
this.transactionData().map((data) => this.multiEncoder.encode(data))
|
||||
);
|
||||
|
||||
decodedTransactionData = once(() =>
|
||||
this.encodedTransactionData().map(
|
||||
(input) => this.multiEncoder.decode(input),
|
||||
)
|
||||
);
|
||||
|
||||
checkDecodedTransactionData = once(() => {
|
||||
const transactionData = this.transactions().map((tx) => tx.input);
|
||||
const decodedTransactionData = this.decodedTransactionData();
|
||||
|
||||
const len = transactionData.length;
|
||||
assert(decodedTransactionData.length === len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
assert(
|
||||
transactionData[i] === decodedTransactionData[i],
|
||||
`tx ${i}: ${transactionData[i]} !== ${decodedTransactionData[i]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
txDataByMethodId = once(() => {
|
||||
const txDataByMethodId: Record<string, string[]> = {};
|
||||
|
||||
for (const data of this.transactionData()) {
|
||||
txDataByMethodId[data.slice(0, 10)] ??= [];
|
||||
txDataByMethodId[data.slice(0, 10)].push("0x" + data.slice(10));
|
||||
}
|
||||
|
||||
return txDataByMethodId;
|
||||
});
|
||||
|
||||
txDataStatsByMethodId = once(() =>
|
||||
mapValues(
|
||||
this.txDataByMethodId(),
|
||||
(dataArray, methodId) => {
|
||||
const count = dataArray.length;
|
||||
|
||||
const baselineLen = dataArray
|
||||
.map((data) => 1 + (methodId.length / 2 - 1) + (data.length / 2 - 1))
|
||||
.reduce(sum);
|
||||
|
||||
const avgBaselineLen = baselineLen / count;
|
||||
|
||||
const encodedLen = dataArray
|
||||
.map((data) =>
|
||||
this.multiEncoder.encode(methodId + data.slice(2)).length / 2 - 1
|
||||
)
|
||||
.reduce(sum);
|
||||
|
||||
const avgEncodedLen = encodedLen / count;
|
||||
|
||||
return {
|
||||
count,
|
||||
baselineLen,
|
||||
avgBaselineLen,
|
||||
encodedLen,
|
||||
avgEncodedLen,
|
||||
};
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
popularMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].count - a[1].count
|
||||
);
|
||||
});
|
||||
|
||||
biggestMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].baselineLen -
|
||||
a[1].baselineLen
|
||||
);
|
||||
});
|
||||
|
||||
biggestEncodedMethods = once(() => {
|
||||
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
|
||||
b[1].encodedLen -
|
||||
a[1].encodedLen
|
||||
);
|
||||
});
|
||||
|
||||
totalLength = once(() =>
|
||||
this.transactions().map((t) => t.input.length / 2 - 1).reduce(sum)
|
||||
);
|
||||
|
||||
baselineEncodedLength = once(() => {
|
||||
let len = this.totalLength();
|
||||
|
||||
for (const txData of this.transactionData()) {
|
||||
len += VLQ.encode(txData.length / 2 - 1).length / 2 - 1;
|
||||
}
|
||||
|
||||
return len;
|
||||
});
|
||||
|
||||
totalEncodedLength = once(() =>
|
||||
this.encodedTransactionData().map((data) => data.length / 2 - 1).reduce(sum)
|
||||
);
|
||||
|
||||
compressionRatio = once(() =>
|
||||
this.totalEncodedLength() / this.baselineEncodedLength()
|
||||
);
|
||||
}
|
||||
61
aggregator/manualTests/analysis/MultiEncoder.ts
Normal file
61
aggregator/manualTests/analysis/MultiEncoder.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import assert from "../../src/helpers/assert.ts";
|
||||
import nil from "../../src/helpers/nil.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
export type Encoder = {
|
||||
encode(data: string): string | nil;
|
||||
decode(encodedData: string): string;
|
||||
};
|
||||
|
||||
export default class MultiEncoder {
|
||||
encoders: {
|
||||
id: number;
|
||||
encoder: Encoder;
|
||||
}[] = [];
|
||||
|
||||
encodersById: Record<number, Encoder> = {};
|
||||
|
||||
register(id: number, encoder: Encoder) {
|
||||
assert(id !== 0);
|
||||
this.encoders.push({ id, encoder });
|
||||
this.encodersById[id] = encoder;
|
||||
}
|
||||
|
||||
encode(data: string): string {
|
||||
for (const { id, encoder } of this.encoders) {
|
||||
const encoded = encoder.encode(data);
|
||||
|
||||
if (encoded === nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(id),
|
||||
encoded,
|
||||
]);
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(0),
|
||||
VLQ.encode(data.length / 2 - 1),
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
decode(data: string): string {
|
||||
const stream = new ByteStream(data);
|
||||
const id = VLQ.decode(stream);
|
||||
|
||||
if (id.eq(0)) {
|
||||
const len = VLQ.decode(stream);
|
||||
return stream.getN(len.toNumber());
|
||||
}
|
||||
|
||||
const encoder = this.encodersById[id.toNumber()];
|
||||
assert(encoder !== nil);
|
||||
|
||||
return encoder.decode(stream.getTail());
|
||||
}
|
||||
}
|
||||
50
aggregator/manualTests/analysis/PseudoFloat.ts
Normal file
50
aggregator/manualTests/analysis/PseudoFloat.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace PseudoFloat {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
if (x.eq(0)) {
|
||||
return "0x00";
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
|
||||
while (x.mod(10).eq(0) && exponent < 30) {
|
||||
x = x.div(10);
|
||||
exponent++;
|
||||
}
|
||||
|
||||
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
|
||||
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
|
||||
|
||||
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return hexJoin([`0x${firstByte}`, VLQ.encode(x.div(8))]);
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
const firstByte = stream.get();
|
||||
|
||||
if (firstByte == 0) {
|
||||
return BigNumber.from(0);
|
||||
}
|
||||
|
||||
const exponent = ((firstByte & 0xf8) >> 3) - 1;
|
||||
|
||||
let mantissa = VLQ.decode(stream);
|
||||
|
||||
mantissa = mantissa.shl(3);
|
||||
mantissa = mantissa.add(firstByte & 0x07);
|
||||
|
||||
return mantissa.mul(BigNumber.from(10).pow(exponent));
|
||||
}
|
||||
}
|
||||
|
||||
export default PseudoFloat;
|
||||
28
aggregator/manualTests/analysis/RegIndex.ts
Normal file
28
aggregator/manualTests/analysis/RegIndex.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
import VLQ from "./VLQ.ts";
|
||||
import { hexJoin } from "./util.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace RegIndex {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const vlqValue = x.div(0x10000);
|
||||
const remainder = x.mod(0x10000);
|
||||
|
||||
return hexJoin([
|
||||
VLQ.encode(vlqValue),
|
||||
remainder.toNumber().toString(16).padStart(4, "0"),
|
||||
]);
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
const vlqValue = VLQ.decode(stream);
|
||||
const remainder = parseInt(stream.getTail().slice(2), 16);
|
||||
|
||||
return vlqValue.mul(0x10000).add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
export default RegIndex;
|
||||
57
aggregator/manualTests/analysis/VLQ.ts
Normal file
57
aggregator/manualTests/analysis/VLQ.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { BigNumber, BigNumberish } from "../../deps.ts";
|
||||
import ByteStream from "./ByteStream.ts";
|
||||
|
||||
// deno-lint-ignore no-namespace
|
||||
namespace VLQ {
|
||||
export function encode(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
while (true) {
|
||||
const segment = x.mod(128);
|
||||
segments.unshift(segment.toNumber());
|
||||
x = x.sub(segment);
|
||||
x = x.div(128);
|
||||
|
||||
if (x.eq(0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = "0x";
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const keepGoing = i !== segments.length - 1;
|
||||
|
||||
const byte = (keepGoing ? 128 : 0) + segments[i];
|
||||
result += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function decode(stream: ByteStream) {
|
||||
let value = BigNumber.from(0);
|
||||
|
||||
while (true) {
|
||||
const currentByte = stream.get();
|
||||
|
||||
// Add the lowest 7 bits to the value
|
||||
value = value.add(currentByte & 0x7f);
|
||||
|
||||
// If the highest bit is zero, stop
|
||||
if ((currentByte & 0x80) === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We're continuing. Shift the value 7 bits to the left (higher) to
|
||||
// make room.
|
||||
value = value.shl(7);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export default VLQ;
|
||||
@@ -0,0 +1,30 @@
|
||||
import nil from "../../../src/helpers/nil.ts";
|
||||
import ByteStream from "../ByteStream.ts";
|
||||
import { Encoder } from "../MultiEncoder.ts";
|
||||
import PseudoFloat from "../PseudoFloat.ts";
|
||||
import { bigNumberToWord, hexJoin } from "../util.ts";
|
||||
|
||||
export default class ERC20TransferEncoder implements Encoder {
|
||||
encode(data: string): string | nil {
|
||||
const stream = new ByteStream(data);
|
||||
|
||||
if (stream.bytesRemaining() !== 68 || stream.getN(4) !== "0xa9059cbb") {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
"0x" + stream.getN(32).slice(26),
|
||||
PseudoFloat.encode(stream.getN(32)),
|
||||
]);
|
||||
}
|
||||
|
||||
decode(encodedData: string): string {
|
||||
const stream = new ByteStream(encodedData);
|
||||
|
||||
return hexJoin([
|
||||
"0xa9059cbb",
|
||||
"0x000000000000000000000000" + stream.getN(20).slice(2),
|
||||
bigNumberToWord(PseudoFloat.decode(stream)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
104
aggregator/manualTests/analysis/encoders/FallbackEncoder.ts
Normal file
104
aggregator/manualTests/analysis/encoders/FallbackEncoder.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import assert from "../../../src/helpers/assert.ts";
|
||||
import nil from "../../../src/helpers/nil.ts";
|
||||
import ByteStream from "../ByteStream.ts";
|
||||
import { Encoder } from "../MultiEncoder.ts";
|
||||
import PseudoFloat from "../PseudoFloat.ts";
|
||||
import VLQ from "../VLQ.ts";
|
||||
import { bigNumberToWord, getDataWords, hexJoin } from "../util.ts";
|
||||
|
||||
export default class FallbackEncoder implements Encoder {
|
||||
encode(data: string): string {
|
||||
const len = data.length / 2 - 1;
|
||||
|
||||
if ((data.length / 2 - 1) % 32 !== 4) {
|
||||
return hexJoin([
|
||||
VLQ.encode(2 * len),
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
const res: string[] = [];
|
||||
|
||||
const words = getDataWords(`0x${data.slice(10)}`);
|
||||
res.push(VLQ.encode(2 * words.length + 1));
|
||||
|
||||
res.push(data.slice(0, 10));
|
||||
|
||||
for (const word of words) {
|
||||
let encoding = hexJoin(["0x00", word]);
|
||||
|
||||
const altEncodings = [
|
||||
hexJoin(["0x01", VLQ.encode(word)]),
|
||||
hexJoin(["0x02", PseudoFloat.encode(word)]),
|
||||
word.startsWith("0x000000000000000000000000")
|
||||
? hexJoin(["0x03", `0x${word.slice(26)}`])
|
||||
: nil,
|
||||
];
|
||||
|
||||
for (const altEncoding of altEncodings) {
|
||||
if (altEncoding === nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (altEncoding.length < encoding.length) {
|
||||
encoding = altEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
res.push(encoding);
|
||||
}
|
||||
|
||||
return hexJoin(res);
|
||||
}
|
||||
|
||||
decode(encodedData: string): string {
|
||||
const stream = new ByteStream(encodedData);
|
||||
|
||||
const leadingVlq = VLQ.decode(stream);
|
||||
|
||||
if (leadingVlq.mod(2).eq(0)) {
|
||||
const len = leadingVlq.div(2);
|
||||
return stream.getN(len.toNumber());
|
||||
}
|
||||
|
||||
const wordLen = leadingVlq.div(2).toNumber();
|
||||
|
||||
const methodId = stream.getN(4);
|
||||
|
||||
const words: string[] = [];
|
||||
|
||||
for (let i = 0; i < wordLen; i++) {
|
||||
const typeId = stream.get();
|
||||
|
||||
switch (typeId) {
|
||||
case 0: {
|
||||
words.push(stream.getN(32));
|
||||
break;
|
||||
}
|
||||
|
||||
case 1: {
|
||||
words.push(bigNumberToWord(VLQ.decode(stream)));
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
words.push(bigNumberToWord(PseudoFloat.decode(stream)));
|
||||
break;
|
||||
}
|
||||
|
||||
case 3: {
|
||||
words.push(`0x000000000000000000000000${stream.getN(20).slice(2)}`);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert(false, `Unrecognized typeId ${typeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
methodId,
|
||||
...words,
|
||||
]);
|
||||
}
|
||||
}
|
||||
18
aggregator/manualTests/analysis/run.ts
Normal file
18
aggregator/manualTests/analysis/run.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Calculator from "./Calculator.ts";
|
||||
import MultiEncoder from "./MultiEncoder.ts";
|
||||
import ERC20TransferEncoder from "./encoders/ERC20TransferEncoder.ts";
|
||||
import FallbackEncoder from "./encoders/FallbackEncoder.ts";
|
||||
|
||||
const multiEncoder = new MultiEncoder();
|
||||
|
||||
multiEncoder.register(2, new ERC20TransferEncoder());
|
||||
multiEncoder.register(1, new FallbackEncoder());
|
||||
|
||||
const calc = new Calculator(multiEncoder);
|
||||
|
||||
// calc.checkDecodedTransactionData();
|
||||
|
||||
console.log(
|
||||
"biggestEncodedMethods",
|
||||
calc.biggestEncodedMethods().slice(0, 10),
|
||||
);
|
||||
31
aggregator/manualTests/analysis/util.ts
Normal file
31
aggregator/manualTests/analysis/util.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { BigNumber } from "../../deps.ts";
|
||||
|
||||
export function sum(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function getDataWords(data: string) {
|
||||
const res = [];
|
||||
|
||||
for (let i = 2; i < data.length; i += 64) {
|
||||
res.push("0x" + data.slice(i, i + 64));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function hexJoin(hexStrings: string[]) {
|
||||
return "0x" + hexStrings.map(remove0x).join("");
|
||||
}
|
||||
|
||||
export function remove0x(hexString: string) {
|
||||
if (!hexString.startsWith("0x")) {
|
||||
throw new Error("Expected 0x prefix");
|
||||
}
|
||||
|
||||
return hexString.slice(2);
|
||||
}
|
||||
|
||||
export function bigNumberToWord(x: BigNumber) {
|
||||
return "0x" + x.toHexString().slice(2).padStart(64, "0");
|
||||
}
|
||||
@@ -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,11 @@ 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 ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
|
||||
export type AddBundleResponse = { hash: string } | {
|
||||
failures: TransactionFailure[];
|
||||
@@ -156,14 +158,15 @@ 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 +176,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 +204,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 +239,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 +322,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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.0-2a20bfe",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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-2a20bfe",
|
||||
"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
|
||||
|
||||
@@ -2898,10 +2898,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-2a20bfe:
|
||||
version "0.9.0-2a20bfe"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-2a20bfe.tgz#2e39757a18df3ba78d816ae15f6b88000443a2a6"
|
||||
integrity sha512-w4efcArPBEowrAkIdVYc2mOLlkN8E5O9eIqEhoo6IrRVrN21p/JVNdoot4N3o5MAKFbeaYfid/u9lL6p2DNdiw==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
Reference in New Issue
Block a user