mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-15 00:38:01 -05:00
Compare commits
116 Commits
v0.2.0
...
cells-walk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b8fec818c | ||
|
|
4fe1241ad2 | ||
|
|
23c037b97f | ||
|
|
bfbf059a13 | ||
|
|
d7cfb87e02 | ||
|
|
1e1820cc58 | ||
|
|
d52c4884ae | ||
|
|
553a007804 | ||
|
|
e4d166e8a2 | ||
|
|
4718851812 | ||
|
|
39b7d1ebc4 | ||
|
|
bb167a419f | ||
|
|
7baed92053 | ||
|
|
6d2c98aaf4 | ||
|
|
8c9604d8e4 | ||
|
|
a63b0c9d68 | ||
|
|
67bccb45a4 | ||
|
|
69ac847bac | ||
|
|
7d70686094 | ||
|
|
82c0d3c58f | ||
|
|
8281724ae2 | ||
|
|
ed6b7b77bd | ||
|
|
26fe1bfd6c | ||
|
|
a76b5164f3 | ||
|
|
f59860fbea | ||
|
|
dcc45cc5fc | ||
|
|
0ab259f350 | ||
|
|
2719637f75 | ||
|
|
a6e9a46822 | ||
|
|
34b52e7fb9 | ||
|
|
9cb51a0237 | ||
|
|
365157bed8 | ||
|
|
0458aee26c | ||
|
|
38d4051005 | ||
|
|
2a758abe46 | ||
|
|
9cdce8b30e | ||
|
|
6512fd7fa6 | ||
|
|
674ef797fb | ||
|
|
d756102404 | ||
|
|
7c5f95258e | ||
|
|
5a78d77fcf | ||
|
|
9e929d1b30 | ||
|
|
cae4e8ea5d | ||
|
|
94b3d966b7 | ||
|
|
dc9e7b068c | ||
|
|
048edbb9cc | ||
|
|
a2bd81f033 | ||
|
|
fa2f5fd968 | ||
|
|
3c69d7438f | ||
|
|
aa09b43a2a | ||
|
|
8713cc1c7d | ||
|
|
dba032f87b | ||
|
|
61a63dec41 | ||
|
|
f1bf808638 | ||
|
|
8f5f4ebbc8 | ||
|
|
1c84872df8 | ||
|
|
54e644b24c | ||
|
|
375e0927d5 | ||
|
|
aee5e56b76 | ||
|
|
a894a5b1be | ||
|
|
ecd664fc0c | ||
|
|
02129fc5cf | ||
|
|
59f77ea95b | ||
|
|
8d8d347fbe | ||
|
|
d25f144247 | ||
|
|
fb077141a0 | ||
|
|
f6d1e313fd | ||
|
|
016103a17e | ||
|
|
f54c1f6bda | ||
|
|
0c6396d36d | ||
|
|
7dbc0ba2c3 | ||
|
|
50448d753a | ||
|
|
6508139ce1 | ||
|
|
02d64c113f | ||
|
|
ddf743f48d | ||
|
|
6510604943 | ||
|
|
f4823e6e18 | ||
|
|
af44e9da34 | ||
|
|
40b207d278 | ||
|
|
796eb8327b | ||
|
|
7806342ddf | ||
|
|
43c2c9c33b | ||
|
|
151aba1516 | ||
|
|
e2ab533e9c | ||
|
|
f5d7b6060d | ||
|
|
769679c800 | ||
|
|
a68faa69c6 | ||
|
|
09f684172b | ||
|
|
2cc3720e4e | ||
|
|
994cf3596b | ||
|
|
ca32615f25 | ||
|
|
2d93ba0fc8 | ||
|
|
43d39efbe7 | ||
|
|
c8ed6765db | ||
|
|
91d04a691e | ||
|
|
5f905c333f | ||
|
|
f9945e7e67 | ||
|
|
4c2fe175c1 | ||
|
|
59e00ccfdd | ||
|
|
c38bf579b3 | ||
|
|
7f5fc5826b | ||
|
|
24912a885b | ||
|
|
b50546c3b6 | ||
|
|
c0145fc8a6 | ||
|
|
311c28ad8d | ||
|
|
a7ef6a1856 | ||
|
|
5a899337ee | ||
|
|
46f9d2d081 | ||
|
|
57cf1e5bd4 | ||
|
|
0d46468cda | ||
|
|
382e0e3c38 | ||
|
|
123e467f3e | ||
|
|
77f8e4cd0a | ||
|
|
bea30d9171 | ||
|
|
d34c7a5c76 | ||
|
|
dc0c11f666 |
@@ -2,10 +2,14 @@
|
||||
|
||||
An Ethereum Layer 2 smart contract wallet that uses [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) and aggregated transactions to reduce gas costs.
|
||||
|
||||
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g)
|
||||
|
||||
## Components
|
||||
|
||||
See each component's directory `README` for more details.
|
||||
|
||||

|
||||
|
||||
### Aggregator
|
||||
|
||||
Service which aggregates BLS wallet transactions.
|
||||
|
||||
2
aggregator-proxy/.gitignore
vendored
Normal file
2
aggregator-proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
5
aggregator-proxy/.npmignore
Normal file
5
aggregator-proxy/.npmignore
Normal file
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!/dist/src/**/*
|
||||
!/src/**/*
|
||||
!/package.json
|
||||
!/README.md
|
||||
3
aggregator-proxy/.vscode/settings.json
vendored
Normal file
3
aggregator-proxy/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.rulers": [80]
|
||||
}
|
||||
28
aggregator-proxy/README.md
Normal file
28
aggregator-proxy/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Aggregator Proxy
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
runAggregatorProxy,
|
||||
|
||||
// AggregatorProxyCallback,
|
||||
// ^ Alternatively, for manual control, import AggregatorProxyCallback to
|
||||
// just generate the req,res callback for use with http.createServer
|
||||
} from 'aggregator-proxy';
|
||||
|
||||
runAggregatorProxy(
|
||||
'https://arbitrum-testnet.blswallet.org',
|
||||
async bundle => {
|
||||
console.log('proxying bundle', JSON.stringify(bundle, null, 2));
|
||||
|
||||
// Return a different/augmented bundle to send to the upstream aggregator
|
||||
return bundle;
|
||||
},
|
||||
8080,
|
||||
'0.0.0.0',
|
||||
() => {
|
||||
console.log('Proxying aggregator on port 8080');
|
||||
},
|
||||
);
|
||||
```
|
||||
14
aggregator-proxy/manualTests/echoServer.ts
Normal file
14
aggregator-proxy/manualTests/echoServer.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { runAggregatorProxy } from "../src";
|
||||
|
||||
runAggregatorProxy(
|
||||
'https://arbitrum-testnet.blswallet.org',
|
||||
async b => {
|
||||
console.log('proxying bundle', JSON.stringify(b, null, 2));
|
||||
return b;
|
||||
},
|
||||
8080,
|
||||
'0.0.0.0',
|
||||
() => {
|
||||
console.log('Proxying aggregator on port 8080');
|
||||
},
|
||||
);
|
||||
29
aggregator-proxy/package.json
Normal file
29
aggregator-proxy/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "bls-wallet-aggregator-proxy",
|
||||
"version": "0.1.1",
|
||||
"main": "dist/src/index.js",
|
||||
"repository": "https://github.com/web3well/bls-wallet",
|
||||
"author": "Andrew Morris",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^3.3.0",
|
||||
"@koa/router": "^10.1.1",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-bodyparser": "^4.3.7",
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "^0.6.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"node-fetch": "2",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
||||
73
aggregator-proxy/src/AggregatorProxyCallback.ts
Normal file
73
aggregator-proxy/src/AggregatorProxyCallback.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import fetch from 'node-fetch';
|
||||
import Koa from 'koa';
|
||||
import cors from '@koa/cors';
|
||||
import Router from '@koa/router';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Bundle, bundleFromDto, Aggregator } from 'bls-wallet-clients';
|
||||
import reporter from 'io-ts-reporters';
|
||||
|
||||
import BundleDto from './BundleDto';
|
||||
|
||||
(globalThis as any).fetch ??= fetch;
|
||||
|
||||
export default function AggregatorProxyCallback(
|
||||
upstreamAggregatorUrl: string,
|
||||
bundleTransformer: (clientBundle: Bundle) => Bundle | Promise<Bundle>,
|
||||
) {
|
||||
const app = new Koa();
|
||||
app.use(cors());
|
||||
const upstreamAggregator = new Aggregator(upstreamAggregatorUrl);
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('/bundle', bodyParser(), async (ctx) => {
|
||||
const decodeResult = BundleDto.decode(ctx.request.body);
|
||||
|
||||
if ('left' in decodeResult) {
|
||||
ctx.status = 400;
|
||||
ctx.body = reporter.report(decodeResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientBundle = bundleFromDto(decodeResult.right);
|
||||
const transformedBundle = await bundleTransformer(clientBundle);
|
||||
|
||||
const addResult = await upstreamAggregator.add(transformedBundle);
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = addResult;
|
||||
});
|
||||
|
||||
router.post('/estimateFee', bodyParser(), async (ctx) => {
|
||||
const decodeResult = BundleDto.decode(ctx.request.body);
|
||||
|
||||
if ('left' in decodeResult) {
|
||||
ctx.status = 400;
|
||||
ctx.body = reporter.report(decodeResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientBundle = bundleFromDto(decodeResult.right);
|
||||
const transformedBundle = await bundleTransformer(clientBundle);
|
||||
|
||||
const estimateFeeResult = await upstreamAggregator.estimateFee(transformedBundle);
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = estimateFeeResult;
|
||||
});
|
||||
|
||||
router.get('/bundleReceipt/:hash', bodyParser(), async (ctx) => {
|
||||
const lookupResult = await upstreamAggregator.lookupReceipt(ctx.params.hash);
|
||||
|
||||
if (lookupResult === undefined) {
|
||||
ctx.status = 404;
|
||||
} else {
|
||||
ctx.status = 200;
|
||||
ctx.body = lookupResult;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(router.routes());
|
||||
|
||||
return app.callback();
|
||||
}
|
||||
20
aggregator-proxy/src/BundleDto.ts
Normal file
20
aggregator-proxy/src/BundleDto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import { BundleDto } from 'bls-wallet-clients';
|
||||
|
||||
const BundleDto = io.type({
|
||||
signature: io.tuple([io.string, io.string]),
|
||||
senderPublicKeys: io.array(
|
||||
io.tuple([io.string, io.string, io.string, io.string])
|
||||
),
|
||||
operations: io.array(io.type({
|
||||
nonce: io.string,
|
||||
actions: io.array(io.type({
|
||||
ethValue: io.string,
|
||||
contractAddress: io.string,
|
||||
encodedFunction: io.string,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
|
||||
export default BundleDto;
|
||||
2
aggregator-proxy/src/index.ts
Normal file
2
aggregator-proxy/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AggregatorProxyCallback } from './AggregatorProxyCallback';
|
||||
export { default as runAggregatorProxy } from './runAggregatorProxy';
|
||||
20
aggregator-proxy/src/runAggregatorProxy.ts
Normal file
20
aggregator-proxy/src/runAggregatorProxy.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import http from 'http';
|
||||
|
||||
import { Bundle } from 'bls-wallet-clients';
|
||||
import AggregatorProxyCallback from './AggregatorProxyCallback';
|
||||
|
||||
export default function runAggregatorProxy(
|
||||
upstreamAggregatorUrl: string,
|
||||
bundleTransformer: (clientBundle: Bundle) => Bundle | Promise<Bundle>,
|
||||
port?: number,
|
||||
hostname?: string,
|
||||
listeningListener?: () => void,
|
||||
) {
|
||||
const server = http.createServer(
|
||||
AggregatorProxyCallback(upstreamAggregatorUrl, bundleTransformer),
|
||||
);
|
||||
|
||||
server.listen(port, hostname, listeningListener);
|
||||
|
||||
return server;
|
||||
}
|
||||
101
aggregator-proxy/tsconfig.json
Normal file
101
aggregator-proxy/tsconfig.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
1504
aggregator-proxy/yarn.lock
Normal file
1504
aggregator-proxy/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
FROM denoland/deno:1.16.3
|
||||
FROM denoland/deno:1.20.6
|
||||
|
||||
ADD build /app
|
||||
WORKDIR /app
|
||||
|
||||
@@ -32,10 +32,10 @@ const bundle = wallet.sign({
|
||||
}],
|
||||
});
|
||||
|
||||
console.log("Calling estimateFee");
|
||||
// console.log("Calling estimateFee");
|
||||
|
||||
const feeEstimation = await client.estimateFee(bundle);
|
||||
console.log({ feeEstimation });
|
||||
// const feeEstimation = await client.estimateFee(bundle);
|
||||
// console.log({ feeEstimation });
|
||||
|
||||
console.log("Sending mint bundle to aggregator");
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"repository": "https://github.com/jzaki/bls-wallet/tree/main/client",
|
||||
"repository": "https://github.com/web3well/bls-wallet/tree/main/contracts/clients",
|
||||
"author": "Andrew Morris",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
|
||||
@@ -65,6 +65,8 @@ contract BLSWallet is Initializable, IBLSWallet
|
||||
trustedBLSGateway = blsGateway;
|
||||
pendingGatewayTime = type(uint256).max;
|
||||
pendingPAFunctionTime = type(uint256).max;
|
||||
pendingRecoveryHashTime = type(uint256).max;
|
||||
pendingBLSPublicKeyTime = type(uint256).max;
|
||||
}
|
||||
|
||||
/** */
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
|
||||
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
|
||||
"blsLibrary": "0x52ED3BAF9F4b60c67D2796e8ED5f35AfA3c4938a",
|
||||
"verificationGateway": "0x115A6d451f50D9E683be5DFaee9a1A22d96BD7a4",
|
||||
"blsExpander": "0xa89ac804aCF9D38B6b8827ba596678FAC23616FC",
|
||||
"testToken": "0x8b20135a1C7ad0B069ac0Be5640b84E1481B8854"
|
||||
"create2Deployer": "0xc1326d37b446bC7df7b36348C963BFcc8eF98Ce3",
|
||||
"precompileCostEstimator": "0x7a89f10F307Bd51b81eF8D1B2c5fa74c7E2d006D",
|
||||
"blsLibrary": "0x6F6a92362EA4299B5668dC4A75282bBFd42D4804",
|
||||
"verificationGateway": "0x697B3E6258B08201d316b31D69805B5F666b62C8",
|
||||
"blsExpander": "0xaf6E02eAf7855D587ffDE5c424a0991570b56944",
|
||||
"utilities": "0x5C176B9F019Bfe90cEc3b2492cC5e20f11c97855",
|
||||
"testToken": "0x09f2C81263B8C079CcE299B4B5b4C32cba0aA0F9"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 421611,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 8006116,
|
||||
"deployedBy": "0xf5F21252cEaEB40cAfa64D6367Ab902e3E371f3b",
|
||||
"version": "87e115b174fd75350fadf12096e9e1e0ee047d52"
|
||||
"genesisBlock": 11355502,
|
||||
"deployedBy": "0xcc12Dd5DefC8BCccAbfBA4bFBFECe09B4EDBF263",
|
||||
"version": "bea30d9171772faf855cdab909f321ce0834a495"
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,19 @@ export default class Create2Fixture {
|
||||
|
||||
// If contract doesn't exist at expected address, deploy it there
|
||||
if ((await ethers.provider.getCode(contractAddress)) === "0x") {
|
||||
await (await create2Deployer.deploy(salt, initCode)).wait();
|
||||
const receipt = await (
|
||||
await create2Deployer.deploy(salt, initCode)
|
||||
).wait();
|
||||
|
||||
console.log(
|
||||
[
|
||||
`Deployed ${contractName}`,
|
||||
`using ${receipt.gasUsed.toString()} gas:`,
|
||||
contractAddress,
|
||||
].join(" "),
|
||||
);
|
||||
} else {
|
||||
console.log(`${contractName} already deployed: ${contractAddress}`);
|
||||
}
|
||||
|
||||
return factory.attach(contractAddress);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PublicKey, BlsWalletWrapper, Signature } from "../clients/src";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
import { defaultDeployerAddress } from "../shared/helpers/deployDeployer";
|
||||
import { BLSWallet } from "../typechain";
|
||||
|
||||
const signWalletAddress = async (
|
||||
fx: Fixture,
|
||||
@@ -43,7 +44,7 @@ describe("Recovery", async function () {
|
||||
const safetyDelaySeconds = 7 * 24 * 60 * 60;
|
||||
let fx: Fixture;
|
||||
let wallet1, wallet2, walletAttacker;
|
||||
let blsWallet;
|
||||
let blsWallet: BLSWallet;
|
||||
let recoverySigner;
|
||||
let hash1, hash2;
|
||||
let salt;
|
||||
@@ -85,6 +86,18 @@ describe("Recovery", async function () {
|
||||
expect(await blsWallet.getBLSPublicKey()).to.eql(newKey);
|
||||
});
|
||||
|
||||
it("should NOT override public key after creation", async function () {
|
||||
const initialKey = await blsWallet.getBLSPublicKey();
|
||||
|
||||
const ZERO = ethers.BigNumber.from(0);
|
||||
expect(initialKey).to.not.eql([ZERO, ZERO, ZERO, ZERO]);
|
||||
|
||||
await blsWallet.setAnyPending();
|
||||
|
||||
const finalKey = await blsWallet.getBLSPublicKey();
|
||||
expect(finalKey).to.eql(initialKey);
|
||||
});
|
||||
|
||||
it("should set recovery hash", async function () {
|
||||
// set instantly from 0 value
|
||||
await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"url": "https://blswallet.org"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"node": ">=16.0.0",
|
||||
"yarn": ">= 1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -42,6 +42,8 @@
|
||||
"eth-rpc-errors": "^4.0.3",
|
||||
"ethers": "5.5.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"is-stream": "^3.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"phosphor-react": "^1.4.0",
|
||||
@@ -79,8 +81,8 @@
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-blockies": "^1.4.1",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/readable-stream": "^2.3.12",
|
||||
"@types/webextension-polyfill": "^0.8.2",
|
||||
"@types/webpack": "^4.41.29",
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { BlsWalletWrapper } from 'bls-wallet-clients';
|
||||
import type { Aggregator } from 'bls-wallet-clients';
|
||||
import type { ethers } from 'ethers';
|
||||
import type TypedEventEmitter from 'typed-emitter';
|
||||
import { storage } from 'webextension-polyfill';
|
||||
|
||||
import { PRIVATE_KEY_STORAGE_KEY } from './env';
|
||||
import generateRandomHex from './helpers/generateRandomHex';
|
||||
import TaskQueue from './common/TaskQueue';
|
||||
import { PageEvents } from './components/Page';
|
||||
import * as env from './env';
|
||||
|
||||
export type AppState = {
|
||||
privateKey?: string;
|
||||
walletAddress: {
|
||||
value?: string;
|
||||
loadCounter: number;
|
||||
};
|
||||
walletState: {
|
||||
nonce?: string;
|
||||
balance?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Events = {
|
||||
state(state: AppState): void;
|
||||
};
|
||||
|
||||
export default class App {
|
||||
events = new EventEmitter() as TypedEventEmitter<Events>;
|
||||
pageEvents = new EventEmitter() as PageEvents;
|
||||
cleanupTasks = new TaskQueue();
|
||||
wallet?: BlsWalletWrapper;
|
||||
|
||||
state: AppState;
|
||||
|
||||
constructor(
|
||||
public aggregator: Aggregator,
|
||||
public provider: ethers.providers.Provider,
|
||||
public _storage: typeof storage.local,
|
||||
) {
|
||||
this.state = {
|
||||
walletAddress: {
|
||||
loadCounter: 0,
|
||||
},
|
||||
walletState: {},
|
||||
};
|
||||
|
||||
this.setupStorage();
|
||||
this.setupBlockListener();
|
||||
}
|
||||
|
||||
setupStorage(): void {
|
||||
type Listener = Parameters<typeof storage.onChanged.addListener>[0];
|
||||
|
||||
const listener: Listener = (changes, areaName) => {
|
||||
if (areaName !== 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pkChange = changes[PRIVATE_KEY_STORAGE_KEY];
|
||||
|
||||
if (pkChange === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ privateKey: pkChange.newValue });
|
||||
};
|
||||
|
||||
storage.onChanged.addListener(listener);
|
||||
|
||||
this.cleanupTasks.push(() => {
|
||||
storage.onChanged.removeListener(listener);
|
||||
});
|
||||
|
||||
storage.local.get(PRIVATE_KEY_STORAGE_KEY).then((results) => {
|
||||
if (PRIVATE_KEY_STORAGE_KEY in results) {
|
||||
this.setState({ privateKey: results[PRIVATE_KEY_STORAGE_KEY] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupBlockListener(): void {
|
||||
const listener = () => this.checkWalletState();
|
||||
|
||||
this.provider.on('block', listener);
|
||||
|
||||
this.cleanupTasks.push(() => {
|
||||
this.provider.off('block', listener);
|
||||
});
|
||||
}
|
||||
|
||||
async checkWalletState(): Promise<void> {
|
||||
if (
|
||||
this.wallet &&
|
||||
this.state.privateKey &&
|
||||
this.wallet.privateKey === this.state.privateKey
|
||||
) {
|
||||
const newWalletState = {
|
||||
nonce: (await this.wallet.Nonce()).toString(),
|
||||
balance: (
|
||||
await this.provider.getBalance(this.wallet.address)
|
||||
).toString(),
|
||||
};
|
||||
|
||||
if (
|
||||
JSON.stringify(newWalletState) !==
|
||||
JSON.stringify(this.state.walletState)
|
||||
) {
|
||||
this.setState({
|
||||
walletState: newWalletState,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.cleanupTasks.run();
|
||||
}
|
||||
|
||||
setState(updates: Partial<AppState>): void {
|
||||
const oldPrivateKey = this.state.privateKey;
|
||||
const oldWalletAddress = this.state.walletAddress.value;
|
||||
|
||||
this.state = { ...this.state, ...updates };
|
||||
|
||||
// When the private key changes, update storage and check for an associated
|
||||
// wallet
|
||||
if (this.state.privateKey !== oldPrivateKey) {
|
||||
if (this.state.privateKey === undefined) {
|
||||
storage.local.remove(PRIVATE_KEY_STORAGE_KEY);
|
||||
} else {
|
||||
storage.local.set({
|
||||
[PRIVATE_KEY_STORAGE_KEY]: this.state.privateKey,
|
||||
});
|
||||
}
|
||||
|
||||
this.checkWalletAddress();
|
||||
}
|
||||
|
||||
// When the wallet address changes, clear the wallet nonce
|
||||
if (this.state.walletAddress.value !== oldWalletAddress) {
|
||||
this.state = { ...this.state, walletState: {} };
|
||||
}
|
||||
|
||||
this.events.emit('state', this.state);
|
||||
}
|
||||
|
||||
createPrivateKey(): void {
|
||||
this.setState({ privateKey: generateRandomHex(256) });
|
||||
}
|
||||
|
||||
loadPrivateKey(privateKey: string): void {
|
||||
const expectedBits = 256;
|
||||
const expectedBytes = expectedBits / 8;
|
||||
|
||||
const expectedLength =
|
||||
2 + // 0x
|
||||
2 * expectedBytes; // 2 hex characters per byte
|
||||
|
||||
if (privateKey.length !== expectedLength) {
|
||||
this.pageEvents.emit(
|
||||
'notification',
|
||||
'error',
|
||||
'Failed to restore private key: incorrect length',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/0x([0-9a-f])*$/i.test(privateKey)) {
|
||||
this.pageEvents.emit(
|
||||
'notification',
|
||||
'error',
|
||||
'Failed to restore private key: incorrect format',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ privateKey });
|
||||
}
|
||||
|
||||
deletePrivateKey(): void {
|
||||
this.setState({ privateKey: undefined });
|
||||
}
|
||||
|
||||
PublicKey(): string | undefined {
|
||||
if (this.state.privateKey === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.wallet?.PublicKeyStr();
|
||||
}
|
||||
|
||||
async checkWalletAddress(): Promise<void> {
|
||||
if (this.state.privateKey === undefined) {
|
||||
this.setState({
|
||||
walletAddress: {
|
||||
...this.state.walletAddress,
|
||||
value: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.incrementWalletAddressLoading();
|
||||
const lookupPrivateKey = this.state.privateKey;
|
||||
|
||||
try {
|
||||
this.wallet = await BlsWalletWrapper.connect(
|
||||
this.state.privateKey,
|
||||
env.NETWORK_CONFIG.addresses.verificationGateway,
|
||||
this.provider,
|
||||
);
|
||||
|
||||
if (this.state.privateKey !== lookupPrivateKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
walletAddress: {
|
||||
...this.state.walletAddress,
|
||||
value: this.wallet?.address,
|
||||
},
|
||||
});
|
||||
|
||||
this.checkWalletState();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.decrementWalletAddressLoading();
|
||||
}
|
||||
}
|
||||
|
||||
incrementWalletAddressLoading(): void {
|
||||
this.setState({
|
||||
walletAddress: {
|
||||
...this.state.walletAddress,
|
||||
loadCounter: this.state.walletAddress.loadCounter + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
decrementWalletAddressLoading(): void {
|
||||
this.setState({
|
||||
walletAddress: {
|
||||
...this.state.walletAddress,
|
||||
loadCounter: this.state.walletAddress.loadCounter - 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import BaseController from '../BaseController';
|
||||
import CellCollection from '../../cells/CellCollection';
|
||||
import ICell from '../../cells/ICell';
|
||||
import {
|
||||
CurrencyControllerConfig,
|
||||
CurrencyControllerState,
|
||||
@@ -7,97 +8,48 @@ import {
|
||||
// every ten minutes
|
||||
const POLLING_INTERVAL = 600_000;
|
||||
|
||||
export default class CurrencyController extends BaseController<
|
||||
CurrencyControllerConfig,
|
||||
CurrencyControllerState
|
||||
> {
|
||||
private conversionInterval: number;
|
||||
const defaultConfig: CurrencyControllerConfig = {
|
||||
pollInterval: POLLING_INTERVAL,
|
||||
};
|
||||
|
||||
constructor({
|
||||
config = {},
|
||||
state,
|
||||
}: {
|
||||
config: Partial<CurrencyControllerConfig>;
|
||||
state?: Partial<CurrencyControllerState>;
|
||||
}) {
|
||||
super({ config, state });
|
||||
this.defaultState = {
|
||||
export default class CurrencyController {
|
||||
private conversionInterval: number;
|
||||
public config: CurrencyControllerConfig;
|
||||
public state: ICell<CurrencyControllerState>;
|
||||
|
||||
constructor(
|
||||
config: CurrencyControllerConfig | undefined,
|
||||
storage: CellCollection,
|
||||
) {
|
||||
this.config = config ?? defaultConfig;
|
||||
|
||||
this.state = storage.Cell('CurrencyController', CurrencyControllerState, {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
nativeCurrency: 'ETH',
|
||||
} as CurrencyControllerState;
|
||||
|
||||
this.defaultConfig = {
|
||||
pollInterval: POLLING_INTERVAL,
|
||||
} as CurrencyControllerConfig;
|
||||
this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// PUBLIC METHODS
|
||||
//
|
||||
|
||||
public getNativeCurrency(): string {
|
||||
return this.state.nativeCurrency;
|
||||
}
|
||||
|
||||
public setNativeCurrency(nativeCurrency: string): void {
|
||||
this.update({
|
||||
nativeCurrency,
|
||||
ticker: nativeCurrency,
|
||||
} as CurrencyControllerState);
|
||||
}
|
||||
|
||||
public getCurrentCurrency(): string {
|
||||
return this.state.currentCurrency;
|
||||
}
|
||||
|
||||
public setCurrentCurrency(currentCurrency: string): void {
|
||||
this.update({ currentCurrency } as CurrencyControllerState);
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the conversionRate property
|
||||
*
|
||||
* @returns The conversion rate from ETH to the selected currency.
|
||||
*
|
||||
*/
|
||||
public getConversionRate(): number {
|
||||
return this.state.conversionRate;
|
||||
}
|
||||
|
||||
public setConversionRate(conversionRate: number): void {
|
||||
this.update({ conversionRate } as CurrencyControllerState);
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the conversionDate property
|
||||
*
|
||||
* @returns The date at which the conversion rate was set. Expressed in milliseconds since midnight of
|
||||
* January 1, 1970
|
||||
*
|
||||
*/
|
||||
public getConversionDate(): string {
|
||||
return this.state.conversionDate;
|
||||
}
|
||||
|
||||
public setConversionDate(conversionDate: string): void {
|
||||
this.update({ conversionDate } as CurrencyControllerState);
|
||||
public async update(stateUpdates: Partial<CurrencyControllerState>) {
|
||||
await this.state.write({
|
||||
...(await this.state.read()),
|
||||
...stateUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
async updateConversionRate(): Promise<void> {
|
||||
let currentCurrency = '';
|
||||
let nativeCurrency = '';
|
||||
try {
|
||||
// fiat
|
||||
currentCurrency = this.getCurrentCurrency();
|
||||
let state: CurrencyControllerState | undefined;
|
||||
|
||||
// crypto
|
||||
nativeCurrency = this.getNativeCurrency();
|
||||
try {
|
||||
state = await this.state.read();
|
||||
const apiUrl = `${
|
||||
this.config.api
|
||||
}?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}&api_key=${
|
||||
}?fsym=${state.nativeCurrency.toUpperCase()}&tsyms=${state.currentCurrency.toUpperCase()}&api_key=${
|
||||
process.env.CRYPTO_COMPARE_API_KEY
|
||||
}`;
|
||||
let response: Response;
|
||||
@@ -128,30 +80,38 @@ export default class CurrencyController extends BaseController<
|
||||
// this.setConversionRate(Number(parsedResponse.bid))
|
||||
// this.setConversionDate(Number(parsedResponse.timestamp))
|
||||
// } else
|
||||
if (parsedResponse[currentCurrency.toUpperCase()]) {
|
||||
if (parsedResponse[state.currentCurrency.toUpperCase()]) {
|
||||
// ETC
|
||||
this.setConversionRate(
|
||||
Number(parsedResponse[currentCurrency.toUpperCase()]),
|
||||
);
|
||||
this.setConversionDate((Date.now() / 1000).toString());
|
||||
this.update({
|
||||
conversionRate: Number(
|
||||
parsedResponse[state.currentCurrency.toUpperCase()],
|
||||
),
|
||||
conversionDate: (Date.now() / 1000).toString(),
|
||||
});
|
||||
} else {
|
||||
this.setConversionRate(0);
|
||||
this.setConversionDate('N/A');
|
||||
this.update({
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// reset current conversion rate
|
||||
console.warn(
|
||||
'Quill - Failed to query currency conversion:',
|
||||
nativeCurrency,
|
||||
currentCurrency,
|
||||
state?.nativeCurrency,
|
||||
state?.currentCurrency,
|
||||
error,
|
||||
);
|
||||
this.setConversionRate(0);
|
||||
this.setConversionDate('N/A');
|
||||
|
||||
this.update({
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
});
|
||||
|
||||
// throw error
|
||||
console.error(
|
||||
error,
|
||||
`CurrencyController - Failed to query rate for currency "${currentCurrency}"`,
|
||||
`CurrencyController - Failed to query rate for currency "${state?.currentCurrency}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { BaseConfig, BaseState } from '../interfaces';
|
||||
import * as io from 'io-ts';
|
||||
|
||||
export interface CurrencyControllerState extends BaseState {
|
||||
currentCurrency: string;
|
||||
conversionRate: number;
|
||||
conversionDate: string;
|
||||
nativeCurrency: string;
|
||||
ticker: string;
|
||||
}
|
||||
import { BaseConfig } from '../interfaces';
|
||||
|
||||
export const CurrencyControllerState = io.type({
|
||||
currentCurrency: io.string,
|
||||
conversionRate: io.number,
|
||||
conversionDate: io.string,
|
||||
nativeCurrency: io.string,
|
||||
});
|
||||
|
||||
export type CurrencyControllerState = io.TypeOf<typeof CurrencyControllerState>;
|
||||
|
||||
export interface CurrencyControllerConfig extends BaseConfig {
|
||||
pollInterval: number;
|
||||
api: string;
|
||||
api?: string;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,21 @@ import {
|
||||
JRPCResponse,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import { BigNumberish, BytesLike } from 'ethers';
|
||||
import { PROVIDER_JRPC_METHODS } from '../../common/constants';
|
||||
import web3_clientVersion from './web3_clientVersion';
|
||||
|
||||
type ProviderHandler<Params, Result> = (
|
||||
req: JRPCRequest<Params>,
|
||||
) => Promise<Result>;
|
||||
|
||||
function toAsyncMiddleware<Params, Result>(
|
||||
method: ProviderHandler<Params, Result>,
|
||||
) {
|
||||
return createAsyncMiddleware(
|
||||
async (req: JRPCRequest<Params>, res: JRPCResponse<Result>) => {
|
||||
res.result = await method(req);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type SendTransactionParams = {
|
||||
from: string;
|
||||
@@ -17,80 +31,20 @@ export type SendTransactionParams = {
|
||||
data: BytesLike;
|
||||
};
|
||||
|
||||
export interface IProviderHandlers {
|
||||
version: string;
|
||||
getAccounts: (req: JRPCRequest<unknown>) => Promise<string[]>;
|
||||
requestAccounts: (req: JRPCRequest<unknown>) => Promise<string[]>;
|
||||
getProviderState: (
|
||||
req: JRPCRequest<unknown>,
|
||||
) => Promise<{ accounts: string[]; chainId: string; isUnlocked: boolean }>;
|
||||
submitBatch: (req: JRPCRequest<SendTransactionParams>) => Promise<string>;
|
||||
}
|
||||
|
||||
export function createWalletMiddleware({
|
||||
version,
|
||||
getAccounts,
|
||||
requestAccounts,
|
||||
getProviderState,
|
||||
submitBatch,
|
||||
}: IProviderHandlers): JRPCMiddleware<string, unknown> {
|
||||
if (!getAccounts) {
|
||||
throw new Error('opts.getAccounts is required');
|
||||
}
|
||||
|
||||
if (!requestAccounts) {
|
||||
throw new Error('opts.requestAccounts is required');
|
||||
}
|
||||
|
||||
if (!getProviderStateFromController) {
|
||||
throw new Error('opts.getProviderState is required');
|
||||
}
|
||||
|
||||
async function lookupAccounts(
|
||||
req: JRPCRequest<unknown>,
|
||||
res: JRPCResponse<unknown>,
|
||||
): Promise<void> {
|
||||
res.result = await getAccounts(req);
|
||||
}
|
||||
|
||||
async function lookupDefaultAccount(
|
||||
req: JRPCRequest<unknown>,
|
||||
res: JRPCResponse<unknown>,
|
||||
): Promise<void> {
|
||||
const accounts = await getAccounts(req);
|
||||
res.result = accounts[0] || null;
|
||||
}
|
||||
|
||||
async function requestAccountsFromProvider(
|
||||
req: JRPCRequest<unknown>,
|
||||
res: JRPCResponse<unknown>,
|
||||
): Promise<void> {
|
||||
res.result = await requestAccounts(req);
|
||||
}
|
||||
|
||||
async function getProviderStateFromController(
|
||||
req: JRPCRequest<unknown>,
|
||||
res: JRPCResponse<unknown>,
|
||||
): Promise<void> {
|
||||
res.result = await getProviderState(req);
|
||||
}
|
||||
|
||||
async function submitTransaction(
|
||||
req: JRPCRequest<SendTransactionParams>,
|
||||
res: JRPCResponse<unknown>,
|
||||
): Promise<void> {
|
||||
res.result = await submitBatch(req);
|
||||
}
|
||||
|
||||
return createScaffoldMiddleware({
|
||||
web3_clientVersion: `Quill/v${version}`,
|
||||
// account lookups
|
||||
eth_accounts: createAsyncMiddleware(lookupAccounts),
|
||||
eth_coinbase: createAsyncMiddleware(lookupDefaultAccount),
|
||||
eth_requestAccounts: createAsyncMiddleware(requestAccountsFromProvider),
|
||||
[PROVIDER_JRPC_METHODS.GET_PROVIDER_STATE]: createAsyncMiddleware(
|
||||
getProviderStateFromController,
|
||||
),
|
||||
eth_sendTransaction: createAsyncMiddleware<any, any>(submitTransaction),
|
||||
});
|
||||
export type IProviderHandlers = Record<
|
||||
string,
|
||||
ProviderHandler<unknown, unknown>
|
||||
>;
|
||||
|
||||
export function createWalletMiddleware(
|
||||
handlers: IProviderHandlers,
|
||||
): JRPCMiddleware<string, unknown> {
|
||||
const asyncMiddlewares = Object.fromEntries(
|
||||
Object.keys(handlers).map((method) => [
|
||||
method,
|
||||
toAsyncMiddleware(handlers[method]),
|
||||
]),
|
||||
);
|
||||
|
||||
return createScaffoldMiddleware({ web3_clientVersion, ...asyncMiddlewares });
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
mergeMiddleware,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import { Aggregator } from 'bls-wallet-clients';
|
||||
import { AGGREGATOR_URL } from '../../env';
|
||||
|
||||
import PollingBlockTracker from '../Block/PollingBlockTracker';
|
||||
import { ProviderConfig } from '../constants';
|
||||
@@ -98,7 +97,7 @@ function createAggregatorMiddleware(): JRPCMiddleware<unknown, unknown> {
|
||||
if (hash in knownTransactions) {
|
||||
const knownTx = knownTransactions[hash];
|
||||
|
||||
const aggregator = new Aggregator(AGGREGATOR_URL);
|
||||
const aggregator = new Aggregator(knownTx.aggregatorUrl);
|
||||
const bundleReceipt = await aggregator.lookupReceipt(hash);
|
||||
|
||||
if (bundleReceipt === undefined) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const web3_clientVersion = `Quill/v${version}`;
|
||||
|
||||
export default web3_clientVersion;
|
||||
@@ -14,8 +14,8 @@ import { BigNumber } from 'ethers';
|
||||
import { Aggregator } from 'bls-wallet-clients';
|
||||
import {
|
||||
createRandomId,
|
||||
getAllReqParam,
|
||||
getDefaultProviderConfig,
|
||||
getFirstReqParam,
|
||||
getUserLanguage,
|
||||
} from './utils';
|
||||
import { ProviderConfig } from './constants';
|
||||
@@ -31,10 +31,7 @@ import {
|
||||
providerAsMiddleware,
|
||||
SafeEventEmitterProvider,
|
||||
} from './Network/INetworkController';
|
||||
import {
|
||||
IProviderHandlers,
|
||||
SendTransactionParams,
|
||||
} from './Network/createEthMiddleware';
|
||||
import { SendTransactionParams } from './Network/createEthMiddleware';
|
||||
import {
|
||||
AddressPreferences,
|
||||
PreferencesConfig,
|
||||
@@ -60,6 +57,11 @@ import createMetaRPCHandler from './streamHelpers/MetaRPCHandler';
|
||||
import { PROVIDER_NOTIFICATIONS } from '../common/constants';
|
||||
import { AGGREGATOR_URL } from '../env';
|
||||
import knownTransactions from './knownTransactions';
|
||||
import CellCollection from '../cells/CellCollection';
|
||||
import assert from '../helpers/assert';
|
||||
import mapValues from '../helpers/mapValues';
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
import Rpc, { rpcMap } from '../types/Rpc';
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
CurrencyControllerConfig: {
|
||||
@@ -112,18 +114,25 @@ export default class QuillController extends BaseController<
|
||||
|
||||
private preferencesController!: PreferencesController;
|
||||
|
||||
// This is just kept in memory because it supports setting the preferred
|
||||
// aggregator for the particular tab only.
|
||||
private tabPreferredAggregators: Record<number, string> = {};
|
||||
|
||||
getRequestAccountTabIds: () => Record<string, number>;
|
||||
getOpenQuillTabsIds: () => Record<number, boolean>;
|
||||
|
||||
// private txController!: TransactionController;
|
||||
|
||||
constructor({
|
||||
config,
|
||||
state,
|
||||
}: {
|
||||
config: Partial<QuillControllerConfig>;
|
||||
state: Partial<QuillControllerState>;
|
||||
}) {
|
||||
constructor(
|
||||
{
|
||||
config,
|
||||
state,
|
||||
}: {
|
||||
config: Partial<QuillControllerConfig>;
|
||||
state: Partial<QuillControllerState>;
|
||||
},
|
||||
public storage: CellCollection,
|
||||
) {
|
||||
super({ config, state });
|
||||
}
|
||||
|
||||
@@ -202,10 +211,10 @@ export default class QuillController extends BaseController<
|
||||
state: this.state.NetworkControllerState,
|
||||
});
|
||||
this.initializeProvider();
|
||||
this.currencyController = new CurrencyController({
|
||||
config: this.config.CurrencyControllerConfig,
|
||||
state: this.state.CurrencyControllerState,
|
||||
});
|
||||
this.currencyController = new CurrencyController(
|
||||
this.config.CurrencyControllerConfig,
|
||||
this.storage,
|
||||
);
|
||||
this.currencyController.updateConversionRate();
|
||||
this.currencyController.scheduleConversionInterval();
|
||||
|
||||
@@ -287,14 +296,14 @@ export default class QuillController extends BaseController<
|
||||
return {
|
||||
// etc
|
||||
getState: () => this.state,
|
||||
setCurrentCurrency:
|
||||
currencyController.setCurrentCurrency.bind(currencyController),
|
||||
setCurrentCurrency: (currentCurrency) =>
|
||||
currencyController.update({ currentCurrency }),
|
||||
setCurrentLocale: preferencesController.setUserLocale.bind(
|
||||
preferencesController,
|
||||
),
|
||||
|
||||
getRequestAccountTabIds: this.getRequestAccountTabIds,
|
||||
getOpenMetamaskTabsIds: this.getOpenQuillTabsIds,
|
||||
getOpenQuillTabsIds: this.getOpenQuillTabsIds,
|
||||
|
||||
// network management
|
||||
setProviderConfig:
|
||||
@@ -350,9 +359,9 @@ export default class QuillController extends BaseController<
|
||||
async setDefaultCurrency(currency: string): Promise<void> {
|
||||
const { ticker } = this.networkController.getProviderConfig();
|
||||
// This is ETH
|
||||
this.currencyController.setNativeCurrency(ticker);
|
||||
this.currencyController.update({ nativeCurrency: ticker });
|
||||
// This is USD
|
||||
this.currencyController.setCurrentCurrency(currency);
|
||||
this.currencyController.update({ currentCurrency: currency });
|
||||
await this.currencyController.updateConversionRate();
|
||||
this.preferencesController.setSelectedCurrency(currency);
|
||||
}
|
||||
@@ -450,7 +459,7 @@ export default class QuillController extends BaseController<
|
||||
// logging
|
||||
engine.push(createLoggerMiddleware(console));
|
||||
|
||||
// forward to metamask primary provider
|
||||
// forward to Quill primary provider
|
||||
engine.push(providerAsMiddleware(provider));
|
||||
return engine;
|
||||
}
|
||||
@@ -461,10 +470,6 @@ export default class QuillController extends BaseController<
|
||||
this.update({ PreferencesControllerState: state });
|
||||
});
|
||||
|
||||
this.currencyController.on('store', (state) => {
|
||||
this.update({ CurrencyControllerState: state });
|
||||
});
|
||||
|
||||
this.networkController.on('store', (state) => {
|
||||
this.update({ NetworkControllerState: state });
|
||||
});
|
||||
@@ -492,11 +497,10 @@ export default class QuillController extends BaseController<
|
||||
// });
|
||||
}
|
||||
|
||||
private initializeProvider(): SafeEventEmitterProvider {
|
||||
const providerHandlers: IProviderHandlers = {
|
||||
version: '1.0.0',
|
||||
private initializeProvider() {
|
||||
this.networkController.initializeProvider({
|
||||
// account management
|
||||
requestAccounts: async (req) => {
|
||||
eth_requestAccounts: async (req) => {
|
||||
const accounts = await this.requestAccounts();
|
||||
this.notifyConnections((req as any).origin, {
|
||||
method: PROVIDER_NOTIFICATIONS.UNLOCK_STATE_CHANGED,
|
||||
@@ -508,13 +512,9 @@ export default class QuillController extends BaseController<
|
||||
return accounts;
|
||||
},
|
||||
|
||||
getAccounts: async () =>
|
||||
// Expose no accounts if this origin has not been approved, preventing
|
||||
// account-requiring RPC methods from completing successfully
|
||||
// only show address if account is unlocked
|
||||
this.selectedAddress ? [this.selectedAddress] : [],
|
||||
eth_coinbase: async () => this.selectedAddress || null,
|
||||
|
||||
getProviderState: async () => {
|
||||
wallet_get_provider_state: async () => {
|
||||
return {
|
||||
accounts: this.selectedAddress ? [this.selectedAddress] : [],
|
||||
chainId: this.networkController.state.chainId,
|
||||
@@ -522,27 +522,35 @@ export default class QuillController extends BaseController<
|
||||
};
|
||||
},
|
||||
|
||||
submitBatch: async (req: any) => {
|
||||
const txParams = getFirstReqParam<SendTransactionParams>(req);
|
||||
eth_setPreferredAggregator: async (req: any) => {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.tabPreferredAggregators[req.tabId] = req.params[0];
|
||||
|
||||
const nonce = await this.keyringController.getNonce(txParams.from);
|
||||
const ethValue = txParams.value?.toString() ?? 0;
|
||||
return 'ok';
|
||||
},
|
||||
|
||||
eth_sendTransaction: async (req: any) => {
|
||||
const txParams = getAllReqParam<SendTransactionParams[]>(req);
|
||||
const { from } = txParams[0];
|
||||
|
||||
const actions = txParams.map((tx) => {
|
||||
return {
|
||||
ethValue: tx.value || '0',
|
||||
contractAddress: tx.to,
|
||||
encodedFunction: tx.data,
|
||||
};
|
||||
});
|
||||
|
||||
const nonce = await this.keyringController.getNonce(from);
|
||||
const tx = {
|
||||
nonce: nonce.toString(),
|
||||
actions: [
|
||||
{
|
||||
ethValue,
|
||||
contractAddress: txParams.to,
|
||||
encodedFunction: txParams.data,
|
||||
},
|
||||
],
|
||||
actions,
|
||||
};
|
||||
|
||||
const bundle = await this.keyringController.signTransactions(
|
||||
txParams.from,
|
||||
tx,
|
||||
);
|
||||
const agg = new Aggregator(AGGREGATOR_URL);
|
||||
const bundle = await this.keyringController.signTransactions(from, tx);
|
||||
const aggregatorUrl =
|
||||
this.tabPreferredAggregators[req.tabId] ?? AGGREGATOR_URL;
|
||||
const agg = new Aggregator(aggregatorUrl);
|
||||
const result = await agg.add(bundle);
|
||||
|
||||
if ('failures' in result) {
|
||||
@@ -550,17 +558,81 @@ export default class QuillController extends BaseController<
|
||||
}
|
||||
|
||||
knownTransactions[result.hash] = {
|
||||
...txParams,
|
||||
...txParams[0],
|
||||
nonce: nonce.toString(),
|
||||
value: ethValue,
|
||||
value: txParams[0].value || '0',
|
||||
aggregatorUrl,
|
||||
};
|
||||
|
||||
return result.hash;
|
||||
},
|
||||
|
||||
...this.makePublicRpc(),
|
||||
...this.makePrivateRpc(),
|
||||
});
|
||||
}
|
||||
|
||||
private makePublicRpc(): Record<string, unknown> {
|
||||
type MethodsWithOrigin = {
|
||||
[M in keyof Rpc['public']]: (
|
||||
origin: string,
|
||||
params: Parameters<Rpc['public'][M]>,
|
||||
) => ReturnType<Rpc['public'][M]>;
|
||||
};
|
||||
const providerProxy =
|
||||
this.networkController.initializeProvider(providerHandlers);
|
||||
return providerProxy;
|
||||
|
||||
const methods: MethodsWithOrigin = {
|
||||
eth_accounts: async (origin) => {
|
||||
if (origin === window.location.origin) {
|
||||
return this.keyringController.state.wallets.map(
|
||||
({ address }) => address,
|
||||
);
|
||||
}
|
||||
|
||||
// Expose no accounts if this origin has not been approved, preventing
|
||||
// account-requiring RPC methods from completing successfully
|
||||
// only show address if account is unlocked
|
||||
// FIXME: The comment above is not yet implemented.
|
||||
return this.selectedAddress ? [this.selectedAddress] : [];
|
||||
},
|
||||
};
|
||||
|
||||
return mapValues(methods, (method, methodName) => (req: any) => {
|
||||
const params = req.params ?? [];
|
||||
assert(rpcMap.public[methodName].params.is(params));
|
||||
return (method as ExplicitAny)(req.origin, params);
|
||||
});
|
||||
}
|
||||
|
||||
private makePrivateRpc(): Record<string, unknown> {
|
||||
const methods: Rpc['private'] = {
|
||||
quill_setSelectedAddress: async (newSelectedAddress) => {
|
||||
this.preferencesController.setSelectedAddress(newSelectedAddress);
|
||||
return 'ok';
|
||||
},
|
||||
|
||||
quill_createHDAccount: async () => {
|
||||
return this.keyringController.createHDAccount();
|
||||
},
|
||||
|
||||
quill_isOnboardingComplete: async () => {
|
||||
return this.keyringController.isOnboardingComplete();
|
||||
},
|
||||
|
||||
quill_setHDPhrase: async (phrase) => {
|
||||
this.keyringController.setHDPhrase(phrase);
|
||||
return 'ok';
|
||||
},
|
||||
};
|
||||
|
||||
return mapValues(methods, (method, methodName) => (req: any) => {
|
||||
if (req.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = req.params ?? [];
|
||||
assert(rpcMap.private[methodName].params.is(params));
|
||||
return (method as ExplicitAny)(...params);
|
||||
});
|
||||
}
|
||||
|
||||
private async requestAccounts(): Promise<string[]> {
|
||||
@@ -618,7 +690,7 @@ export default class QuillController extends BaseController<
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reference to a connection by origin. Ignores the 'metamask' origin.
|
||||
* Adds a reference to a connection by origin. Ignores the 'quill' origin.
|
||||
* Caller must ensure that the returned id is stored such that the reference
|
||||
* can be deleted later.
|
||||
*/
|
||||
|
||||
@@ -20,13 +20,7 @@ import { getDefaultProviderConfig } from './utils';
|
||||
import ControllerStoreStream from './streamHelpers/ControllerStoreStream';
|
||||
import ControllerStreamSink from './streamHelpers/ControllerStreamSink';
|
||||
import PortDuplexStream from '../common/PortStream';
|
||||
|
||||
window.QuillController = () => {
|
||||
throw new Error('window.QuillController not initialized');
|
||||
};
|
||||
window.KeyringController = () => {
|
||||
throw new Error('window.KeyringController not initialized');
|
||||
};
|
||||
import extensionLocalCellCollection from '../cells/extensionLocalCellCollection';
|
||||
|
||||
// const notificationManager = new NotificationManager();
|
||||
|
||||
@@ -90,22 +84,25 @@ async function loadStateFromPersistence(): Promise<QuillControllerState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the MetaMask Controller with any initial state and default language.
|
||||
* Initializes the Quill Controller with any initial state and default language.
|
||||
* Configures platform-specific error reporting strategy.
|
||||
* Streams emitted state updates to platform-specific storage strategy.
|
||||
* Creates platform listeners for new Dapps/Contexts, and sets up their data connections to the controller.
|
||||
*/
|
||||
function setupController(initState: unknown): void {
|
||||
//
|
||||
// MetaMask Controller
|
||||
// Quill Controller
|
||||
//
|
||||
console.log(initState, 'initstate');
|
||||
|
||||
const controller = new QuillController({
|
||||
// initial state
|
||||
state: initState as QuillControllerState,
|
||||
config: DEFAULT_CONFIG,
|
||||
});
|
||||
const controller = new QuillController(
|
||||
{
|
||||
// initial state
|
||||
state: initState as QuillControllerState,
|
||||
config: DEFAULT_CONFIG,
|
||||
},
|
||||
extensionLocalCellCollection,
|
||||
);
|
||||
|
||||
controller.init({
|
||||
state: initState as QuillControllerState,
|
||||
@@ -116,16 +113,13 @@ function setupController(initState: unknown): void {
|
||||
},
|
||||
});
|
||||
|
||||
window.QuillController = () => controller;
|
||||
window.KeyringController = () => controller.keyringController;
|
||||
|
||||
// setup state persistence
|
||||
pump(
|
||||
new ControllerStoreStream(controller),
|
||||
// debounce(1000),
|
||||
new ControllerStreamSink(persistData),
|
||||
(error) => {
|
||||
console.error('MetaMask - Persistence pipeline failed', error);
|
||||
console.error('Quill - Persistence pipeline failed', error);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -196,16 +190,16 @@ function setupController(initState: unknown): void {
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects a Port to the MetaMask controller via a multiplexed duplex stream.
|
||||
* This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages).
|
||||
* Connects a Port to the Quill controller via a multiplexed duplex stream.
|
||||
* This method identifies trusted (Quill) interfaces, and connects them differently from untrusted (web pages).
|
||||
*
|
||||
* @param {Port} remotePort - The port provided by a new context.
|
||||
*/
|
||||
function connectRemote(remotePort: Runtime.Port) {
|
||||
const processName = remotePort.name;
|
||||
const isMetaMaskInternalProcess = quillInternalProcessHash[processName];
|
||||
const isQuillInternalProcess = quillInternalProcessHash[processName];
|
||||
|
||||
if (isMetaMaskInternalProcess) {
|
||||
if (isQuillInternalProcess) {
|
||||
const portStream = new PortDuplexStream(remotePort);
|
||||
// communication with popup
|
||||
controller.isClientOpen = true;
|
||||
@@ -400,18 +394,18 @@ async function openPopup() {
|
||||
// TODO: remove this later
|
||||
if (process.env.NODE_ENV === 'random') openPopup();
|
||||
|
||||
// On first install, open a new tab with MetaMask
|
||||
// On first install, open a new tab with Quill
|
||||
runtime.onInstalled.addListener(({ reason }) => {
|
||||
if (
|
||||
reason === 'install' &&
|
||||
!(process.env.METAMASK_DEBUG || process.env.IN_TEST)
|
||||
!(process.env.QUILL_DEBUG || process.env.IN_TEST)
|
||||
) {
|
||||
openExtensionInBrowser();
|
||||
}
|
||||
});
|
||||
|
||||
function openExtensionInBrowser(route = null, queryString = null) {
|
||||
let extensionURL = runtime.getURL('home.html');
|
||||
let extensionURL = runtime.getURL('quillPage.html#/wallet');
|
||||
|
||||
if (route) {
|
||||
extensionURL += `#${route}`;
|
||||
|
||||
@@ -6,6 +6,7 @@ const knownTransactions: Record<
|
||||
SendTransactionParams & {
|
||||
nonce: string;
|
||||
value: BigNumberish;
|
||||
aggregatorUrl: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
|
||||
@@ -56,12 +56,20 @@ export const getRPCURL = (chainId: string): string => {
|
||||
// But this is ok for now.
|
||||
export const getFirstReqParam = <T>(req: any): T => {
|
||||
if (!Array.isArray(req.params)) {
|
||||
throw new Error(
|
||||
'req.params not array',
|
||||
);
|
||||
throw new Error('req.params not array');
|
||||
}
|
||||
if (!req.params.length) {
|
||||
throw new Error('req.params empty');
|
||||
}
|
||||
return req.params[0];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllReqParam = <T>(req: any): T => {
|
||||
if (!Array.isArray(req.params)) {
|
||||
throw new Error('req.params not array');
|
||||
}
|
||||
if (!req.params.length) {
|
||||
throw new Error('req.params empty');
|
||||
}
|
||||
return req.params;
|
||||
};
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { Component, ReactElement } from 'react';
|
||||
import Button from '../components/Button';
|
||||
import { PageEvents } from '../components/Page';
|
||||
import getPropOrUndefined from '../helpers/getPropOrUndefined';
|
||||
import type QuillEthereumProvider from '../PageContentScript/InPageProvider';
|
||||
|
||||
type Props = { events: PageEvents };
|
||||
|
||||
export default class CreateTransaction extends Component<Props> {
|
||||
amountRef?: HTMLInputElement;
|
||||
recipientRef?: HTMLInputElement;
|
||||
dataRef?: HTMLInputElement;
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<div className="create-transaction">
|
||||
<div className="section">
|
||||
<h1>Create Transaction</h1>
|
||||
</div>
|
||||
<div className="section body">
|
||||
<div className="form">
|
||||
<div>
|
||||
Amount:{' '}
|
||||
<input
|
||||
ref={(r) => (this.amountRef = r ?? undefined)}
|
||||
type="text"
|
||||
style={{ textAlign: 'end' }}
|
||||
/>{' '}
|
||||
ETH
|
||||
</div>
|
||||
<div>
|
||||
Contract/Recipient:{' '}
|
||||
<input
|
||||
ref={(r) => (this.recipientRef = r ?? undefined)}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Encoded Function Data:{' '}
|
||||
<input
|
||||
ref={(r) => (this.dataRef = r ?? undefined)}
|
||||
type="text"
|
||||
defaultValue="0x"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={async () => {
|
||||
const provider = getPropOrUndefined(window, 'ethereum') as
|
||||
| QuillEthereumProvider
|
||||
| undefined;
|
||||
|
||||
if (provider === undefined) {
|
||||
this.props.events.emit(
|
||||
'notification',
|
||||
'error',
|
||||
'Ethereum provider not found',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const amount = this.amountRef?.value || undefined;
|
||||
const to = this.recipientRef?.value || undefined;
|
||||
const data = this.dataRef?.value || undefined;
|
||||
|
||||
if (amount === undefined || to === undefined) {
|
||||
this.props.events.emit(
|
||||
'notification',
|
||||
'error',
|
||||
'Field(s) missing',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await provider.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [
|
||||
{
|
||||
value: ethers.utils
|
||||
.parseEther(amount)
|
||||
.toHexString(),
|
||||
to,
|
||||
data,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.props.events.emit(
|
||||
'notification',
|
||||
'info',
|
||||
`Result: ${result}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.props.events.emit(
|
||||
'notification',
|
||||
'error',
|
||||
(error as Error).message,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import CreateTransaction from './CreateTransaction';
|
||||
import Page, { PageEvents } from '../components/Page';
|
||||
|
||||
import '../ContentScript/index';
|
||||
import './styles.scss';
|
||||
|
||||
const pageEvents = new EventEmitter() as PageEvents;
|
||||
|
||||
ReactDOM.render(
|
||||
<Page events={pageEvents}>
|
||||
<CreateTransaction events={pageEvents} />
|
||||
</Page>,
|
||||
document.getElementById('create-transaction-root'),
|
||||
);
|
||||
@@ -1,13 +0,0 @@
|
||||
@import "../styles/quill";
|
||||
|
||||
#create-transaction-root {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.create-transaction {
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,7 @@ import type { JRPCRequest, JRPCSuccess } from '@toruslabs/openlogin-jrpc';
|
||||
import { EthereumRpcError } from 'eth-rpc-errors';
|
||||
import dequal from 'fast-deep-equal';
|
||||
import type { Duplex } from 'readable-stream';
|
||||
import {
|
||||
PROVIDER,
|
||||
PROVIDER_JRPC_METHODS,
|
||||
PROVIDER_NOTIFICATIONS,
|
||||
} from '../common/constants';
|
||||
import { PROVIDER, PROVIDER_NOTIFICATIONS } from '../common/constants';
|
||||
|
||||
import BaseProvider from './BaseProvider';
|
||||
import {
|
||||
@@ -18,7 +14,7 @@ import {
|
||||
} from './interfaces';
|
||||
import messages from './messages';
|
||||
|
||||
class QuillInPageProvider extends BaseProvider<InPageProviderState> {
|
||||
export class QuillInPageProvider extends BaseProvider<InPageProviderState> {
|
||||
/**
|
||||
* The chain ID of the currently connected Ethereum chain.
|
||||
* See [chainId.network]{@link https://chainid.network} for more information.
|
||||
@@ -99,7 +95,7 @@ class QuillInPageProvider extends BaseProvider<InPageProviderState> {
|
||||
async _initializeState(): Promise<void> {
|
||||
try {
|
||||
const { accounts, chainId, isUnlocked } = (await this.request({
|
||||
method: PROVIDER_JRPC_METHODS.GET_PROVIDER_STATE,
|
||||
method: 'wallet_get_provider_state',
|
||||
params: [],
|
||||
})) as InPageWalletProviderState;
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ export function initializeProvider({
|
||||
}
|
||||
|
||||
// setup background connection
|
||||
const metamaskStream = new BasePostMessageStream({
|
||||
const quillStream = new BasePostMessageStream({
|
||||
name: INPAGE,
|
||||
target: CONTENT_SCRIPT,
|
||||
});
|
||||
|
||||
initializeProvider({
|
||||
connectionStream: metamaskStream,
|
||||
connectionStream: quillStream,
|
||||
// setting true will set window.ethereum to the provider
|
||||
shouldSetOnWindow: true,
|
||||
});
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
import TaskQueue from '../common/TaskQueue';
|
||||
|
||||
import type App from '../App';
|
||||
import { AppState } from '../App';
|
||||
import LoadingScreen from './components/LoadingScreen';
|
||||
import Page from '../components/Page';
|
||||
import WalletHomeScreen from './components/WalletHomeScreen';
|
||||
import WelcomeScreen from './components/WelcomeScreen';
|
||||
import KeyEntryScreen from './components/KeyEntryScreen';
|
||||
|
||||
import '../styles/index.scss';
|
||||
|
||||
type Props = {
|
||||
appPromise: Promise<App>;
|
||||
};
|
||||
|
||||
type State = {
|
||||
app?: App;
|
||||
appState?: AppState;
|
||||
};
|
||||
|
||||
const useNewUI = true;
|
||||
|
||||
export default class Popup extends Component<Props, State> {
|
||||
cleanupTasks = new TaskQueue();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.props.appPromise.then((app) => {
|
||||
this.setState({ app });
|
||||
|
||||
app.events.on('state', appStateListener);
|
||||
this.cleanupTasks.push(() => app.events.off('state', appStateListener));
|
||||
});
|
||||
|
||||
const appStateListener = (appState: AppState) => {
|
||||
this.setState({ appState });
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.cleanupTasks.run();
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (!this.state.app) {
|
||||
return (
|
||||
<div className="popup">
|
||||
<LoadingScreen />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page classes={['popup']} events={this.state.app.pageEvents}>
|
||||
{(() => {
|
||||
if (this.state.app.state.privateKey === undefined) {
|
||||
return useNewUI ? (
|
||||
<WelcomeScreen />
|
||||
) : (
|
||||
<KeyEntryScreen app={this.state.app} />
|
||||
);
|
||||
}
|
||||
|
||||
return <WalletHomeScreen app={this.state.app} />;
|
||||
})()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
import App from '../../App';
|
||||
import Button from '../../components/Button';
|
||||
|
||||
import LargeQuillHeading from './LargeQuillHeading';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
};
|
||||
|
||||
type State = {
|
||||
pasteText: string;
|
||||
};
|
||||
|
||||
export default class KeyEntryScreen extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pasteText: '',
|
||||
};
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<div className="key-entry-screen">
|
||||
<LargeQuillHeading />
|
||||
<div className="body">
|
||||
<Button
|
||||
className={this.state.pasteText === '' ? 'btn-primary' : 'btn'}
|
||||
onPress={() => this.props.app.createPrivateKey()}
|
||||
>
|
||||
Create BLS Key
|
||||
</Button>
|
||||
<div style={{ color: '#ccc' }}>OR</div>
|
||||
<div style={{ flexGrow: 1, position: 'relative' }}>
|
||||
<textarea
|
||||
placeholder="Paste BLS Private Key..."
|
||||
value={this.state.pasteText}
|
||||
onInput={(evt) => {
|
||||
this.setState({
|
||||
pasteText: (evt.target as HTMLTextAreaElement).value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="restore-button"
|
||||
style={this.state.pasteText === '' ? { display: 'none' } : {}}
|
||||
>
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() =>
|
||||
this.props.app.loadPrivateKey(this.state.pasteText)
|
||||
}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="notice">
|
||||
Quill's development is in Alpha, please do not to use large
|
||||
sums of money
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import LargeQuillHeading from './LargeQuillHeading';
|
||||
|
||||
const LoadingScreen = (): ReactElement => (
|
||||
<div>
|
||||
<LargeQuillHeading />
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
@@ -1,14 +0,0 @@
|
||||
import App from '../../App';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
|
||||
export default function NotImplemented(app: App): void {
|
||||
app.pageEvents.emit('overlay', (close) => (
|
||||
<>
|
||||
<div style={{ marginBottom: '12px' }}>Not implemented</div>
|
||||
<Button className="btn-primary" onPress={close}>
|
||||
Ok
|
||||
</Button>
|
||||
</>
|
||||
));
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export const overrideScreenEnabled = false;
|
||||
|
||||
const OverrideScreen = (): ReactElement => <>Override screen</>;
|
||||
|
||||
export default OverrideScreen;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Component, ReactElement, ReactNode } from 'react';
|
||||
import defineAction from '../../helpers/defineAction';
|
||||
|
||||
type Props = {
|
||||
content: [string, ReactElement][];
|
||||
defaultTab?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedTab: string;
|
||||
};
|
||||
|
||||
export default class Tabs extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { selectedTab: props.defaultTab ?? props.content[0][0] };
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const selectedContent = this.props.content.find(
|
||||
([tabName]) => tabName === this.state.selectedTab,
|
||||
)?.[1];
|
||||
|
||||
return (
|
||||
<div className="tabs-container">
|
||||
<div className="tab-chooser">
|
||||
{this.props.content.map(([tabName], i) => {
|
||||
const selected = tabName === this.state.selectedTab;
|
||||
const notLast = i < this.props.content.length - 1;
|
||||
const nextSelected =
|
||||
notLast &&
|
||||
this.props.content[i + 1][0] === this.state.selectedTab;
|
||||
|
||||
const needsRightBorder = notLast && !selected && !nextSelected;
|
||||
|
||||
const classes = [
|
||||
...(selected ? ['selected'] : []),
|
||||
...(needsRightBorder ? ['needs-right-border'] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tabName}
|
||||
className={classes.join(' ')}
|
||||
{...defineAction(() =>
|
||||
this.setState({
|
||||
selectedTab: tabName,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{tabName}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="selected-content">{selectedContent}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Component, ReactElement } from 'react';
|
||||
import App from '../../App';
|
||||
import formatBalance from '../helpers/formatBalance';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
};
|
||||
|
||||
type State = {
|
||||
contractAddressText: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
contractAddressText: '',
|
||||
};
|
||||
|
||||
export default class TransactionTab extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = initialState;
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<div className="transaction-tab">
|
||||
<div className="balance">
|
||||
<div className="label">Balance:</div>
|
||||
<div className="value">
|
||||
{formatBalance(this.props.app.state.walletState.balance, 'ETH')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import App from '../../App';
|
||||
import CompactQuillHeading from '../../components/CompactQuillHeading';
|
||||
import Tabs from './Tabs';
|
||||
import TransactionTab from './TransactionTab';
|
||||
|
||||
const TransactionsScreen = (props: { app: App }): ReactElement => (
|
||||
<div className="transactions-screen">
|
||||
<CompactQuillHeading />
|
||||
<Tabs
|
||||
content={[
|
||||
['Transaction', <TransactionTab app={props.app} key={1} />],
|
||||
['Outbox', <>Not implemented</>],
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TransactionsScreen;
|
||||
@@ -1,283 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { runtime, tabs } from 'webextension-polyfill';
|
||||
|
||||
import { CREATE_TX_URL } from '../../env';
|
||||
import assertExists from '../../helpers/assertExists';
|
||||
import defineAction from '../../helpers/defineAction';
|
||||
import App from '../../App';
|
||||
import formatBalance from '../helpers/formatBalance';
|
||||
import formatCompactAddress from '../helpers/formatCompactAddress';
|
||||
import Button from '../../components/Button';
|
||||
import CompactQuillHeading from '../../components/CompactQuillHeading';
|
||||
import CopyIcon from './CopyIcon';
|
||||
import Grow from './Grow';
|
||||
|
||||
export type BlsKey = {
|
||||
public: string;
|
||||
private: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
};
|
||||
|
||||
const WalletHomeScreen = ({ app }: Props): ReactElement => (
|
||||
<div className="wallet-home-screen">
|
||||
<div className="section">
|
||||
<CompactQuillHeading />
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="field-list">
|
||||
<BLSKeyField app={app} />
|
||||
<NetworkField />
|
||||
{(() => {
|
||||
const { walletAddress, walletState } = app.state;
|
||||
if (walletAddress.loadCounter > 0 || !walletAddress.value) {
|
||||
return (
|
||||
<>
|
||||
<div />
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() => {
|
||||
/* no-op */
|
||||
}}
|
||||
loading={true}
|
||||
>
|
||||
Loading...
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddressField
|
||||
app={app}
|
||||
address={walletAddress.value}
|
||||
nonce={walletState.nonce}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<WalletContent app={app} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WalletHomeScreen;
|
||||
|
||||
const BLSKeyField = ({ app }: Props): ReactElement | null => {
|
||||
const publicKey = app.PublicKey();
|
||||
if (!publicKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ width: '17px' }}>
|
||||
<img
|
||||
src={runtime.getURL('assets/key.svg')}
|
||||
alt="key"
|
||||
width="14"
|
||||
height="15"
|
||||
/>
|
||||
</div>
|
||||
<div className="field-label">BLS Key:</div>
|
||||
<div
|
||||
className="field-value grow"
|
||||
{...defineAction(() => {
|
||||
navigator.clipboard.writeText(publicKey);
|
||||
app.pageEvents.emit(
|
||||
'notification',
|
||||
'info',
|
||||
'BLS public key copied to clipboard',
|
||||
);
|
||||
})}
|
||||
>
|
||||
<div className="grow">{formatCompactAddress(publicKey)}</div>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
<div className="field-trailer">
|
||||
<KeyIcon
|
||||
src={runtime.getURL('assets/download.svg')}
|
||||
text="Backup private key"
|
||||
onAction={() =>
|
||||
app.pageEvents.emit('overlay', (close) => (
|
||||
<CopyPrivateKeyPrompt app={app} close={close} />
|
||||
))
|
||||
}
|
||||
/>
|
||||
<KeyIcon
|
||||
src={runtime.getURL('assets/trashcan.svg')}
|
||||
text="Delete BLS key"
|
||||
onAction={() =>
|
||||
app.pageEvents.emit('overlay', (close) => (
|
||||
<DeleteKeyPrompt app={app} close={close} />
|
||||
))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkField = (): ReactElement => (
|
||||
<div>
|
||||
<div style={{ width: '17px' }}>
|
||||
<img
|
||||
src={runtime.getURL('assets/network.svg')}
|
||||
alt="network"
|
||||
width="14"
|
||||
height="15"
|
||||
/>
|
||||
</div>
|
||||
<div className="field-label">Network:</div>
|
||||
<select
|
||||
className="field-value grow"
|
||||
style={{
|
||||
backgroundImage: `url("${runtime.getURL(
|
||||
'assets/selector-down-arrow.svg',
|
||||
)}")`,
|
||||
}}
|
||||
>
|
||||
<option>Arbitrum</option>
|
||||
<option disabled>Optimism</option>
|
||||
</select>
|
||||
<div className="field-trailer" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const AddressField = (props: {
|
||||
app: App;
|
||||
address: string;
|
||||
nonce?: string;
|
||||
}): ReactElement => (
|
||||
<div>
|
||||
<div style={{ width: '17px' }}>
|
||||
<img
|
||||
src={runtime.getURL('assets/address.svg')}
|
||||
alt="address"
|
||||
width="14"
|
||||
height="15"
|
||||
/>
|
||||
</div>
|
||||
<div className="field-label">Address:</div>
|
||||
<div
|
||||
className="field-value grow"
|
||||
{...defineAction(() => {
|
||||
navigator.clipboard.writeText(props.address);
|
||||
props.app.pageEvents.emit(
|
||||
'notification',
|
||||
'info',
|
||||
'Address copied to clipboard',
|
||||
);
|
||||
})}
|
||||
>
|
||||
<div className="grow">{formatCompactAddress(props.address)}</div>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
<div className="field-trailer">#{props.nonce}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const WalletContent = ({ app }: Props): ReactElement => {
|
||||
if (!app.state.walletAddress.value) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="section wallet-content">
|
||||
<div className="balance">
|
||||
<div className="label">Balance:</div>
|
||||
<div className="value">
|
||||
{formatBalance(app.state.walletState.balance, 'ETH')}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() => {
|
||||
tabs.create({
|
||||
url: CREATE_TX_URL || 'createTransaction.html',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create Transaction
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyIcon = (props: {
|
||||
src: string;
|
||||
text: string;
|
||||
onAction: () => void;
|
||||
}): ReactElement => (
|
||||
<div className="key-icon" style={{ width: '22px', height: '22px' }}>
|
||||
<img
|
||||
src={props.src}
|
||||
alt={props.text}
|
||||
width="22"
|
||||
height="22"
|
||||
{...defineAction(props.onAction)}
|
||||
/>
|
||||
<div className="info-box">
|
||||
<Grow />
|
||||
<div className="content">{props.text}</div>
|
||||
</div>
|
||||
<div className="info-arrow" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const DeleteKeyPrompt = (props: {
|
||||
app: App;
|
||||
close: () => void;
|
||||
}): ReactElement => (
|
||||
<div className="delete-key-prompt">
|
||||
<div>
|
||||
Are you sure that you want to delete this key? You can only restore your
|
||||
wallet if you have backed up your private key.
|
||||
</div>
|
||||
<div />
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() => {
|
||||
props.app.deletePrivateKey();
|
||||
props.close();
|
||||
}}
|
||||
>
|
||||
Delete BLS key
|
||||
</Button>
|
||||
<Button className="btn-secondary" onPress={props.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CopyPrivateKeyPrompt = (props: {
|
||||
app: App;
|
||||
close: () => void;
|
||||
}): ReactElement => (
|
||||
<div className="delete-key-prompt">
|
||||
<div>
|
||||
You should make sure you store your private key somewhere safe. If you
|
||||
lose it, you won’t be able to restore your wallet.
|
||||
</div>
|
||||
<div />
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() => {
|
||||
navigator.clipboard.writeText(assertExists(props.app.state.privateKey));
|
||||
props.close();
|
||||
props.app.pageEvents.emit(
|
||||
'notification',
|
||||
'info',
|
||||
'BLS private key copied to clipboard',
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy private key
|
||||
</Button>
|
||||
<Button className="btn-secondary" onPress={props.close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -7,7 +7,7 @@ import Carousel from './Carousel';
|
||||
import LargeQuillHeading from './LargeQuillHeading';
|
||||
|
||||
const WelcomeScreen: FunctionComponent = () => (
|
||||
<div>
|
||||
<div className="welcome-screen">
|
||||
<LargeQuillHeading />
|
||||
<Carousel
|
||||
images={[
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
import { Aggregator } from 'bls-wallet-clients';
|
||||
import { ethers } from 'ethers';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { storage } from 'webextension-polyfill';
|
||||
|
||||
import App from '../App';
|
||||
import { getDefaultProviderConfig } from '../Controllers/utils';
|
||||
import { AGGREGATOR_URL } from '../env';
|
||||
import Popup from './Popup';
|
||||
|
||||
import './styles.scss';
|
||||
import '../styles/index.scss';
|
||||
import WelcomeScreen from './components/WelcomeScreen';
|
||||
|
||||
const appPromise = (async () => {
|
||||
const providerConfig = getDefaultProviderConfig();
|
||||
return new App(
|
||||
new Aggregator(AGGREGATOR_URL),
|
||||
new ethers.providers.JsonRpcProvider(providerConfig.rpcTarget),
|
||||
storage.local,
|
||||
);
|
||||
})();
|
||||
|
||||
ReactDOM.render(
|
||||
<Popup appPromise={appPromise} />,
|
||||
document.getElementById('popup-root'),
|
||||
);
|
||||
ReactDOM.render(<WelcomeScreen />, document.getElementById('popup-root'));
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.quill {
|
||||
.popup {
|
||||
.welcome-screen {
|
||||
.key-entry-screen {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
69
extension/source/QuillPage/CellsDemo/BalanceWidget.tsx
Normal file
69
extension/source/QuillPage/CellsDemo/BalanceWidget.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { FunctionComponent, useMemo } from 'react';
|
||||
import { FormulaCell } from '../../cells/FormulaCell';
|
||||
import MemoryCell from '../../cells/MemoryCell';
|
||||
import QuillContext from '../QuillContext';
|
||||
import { Display } from './Display';
|
||||
import TextBox from './TextBox';
|
||||
|
||||
const BalanceWidget: FunctionComponent = () => {
|
||||
const quillCtx = QuillContext.use();
|
||||
|
||||
const cells = useMemo(() => {
|
||||
const address = new MemoryCell('');
|
||||
|
||||
const balanceDisplay = new FormulaCell(
|
||||
{ address, _: quillCtx.blockNumber },
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
async ({ address }) => {
|
||||
const addressError = (() => {
|
||||
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
||||
return new Error('Invalid address');
|
||||
}
|
||||
|
||||
try {
|
||||
// Handles mixed case checksums
|
||||
ethers.utils.getAddress(address);
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
if (error.message.includes('bad address checksum')) {
|
||||
return new Error('bad address checksum');
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
})();
|
||||
|
||||
if (addressError) {
|
||||
return `(${addressError.message})`;
|
||||
}
|
||||
|
||||
const balance = await quillCtx.ethersProvider.getBalance(address);
|
||||
return `ETH: ${(+ethers.utils.formatEther(balance)).toFixed(3)}`;
|
||||
},
|
||||
);
|
||||
|
||||
return { address, balanceDisplay };
|
||||
}, [quillCtx]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>Address:</td>
|
||||
<td>
|
||||
<TextBox value={cells.address} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Balance:</td>
|
||||
<td>
|
||||
<Display cell={cells.balanceDisplay} />
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BalanceWidget;
|
||||
123
extension/source/QuillPage/CellsDemo/CellsDemoPage.tsx
Normal file
123
extension/source/QuillPage/CellsDemo/CellsDemoPage.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import { FunctionComponent, useMemo } from 'react';
|
||||
import { FormulaCell } from '../../cells/FormulaCell';
|
||||
import MemoryCell from '../../cells/MemoryCell';
|
||||
import useCell from '../../cells/useCell';
|
||||
import delay from '../../helpers/delay';
|
||||
import Range from '../../helpers/Range';
|
||||
import QuillContext from '../QuillContext';
|
||||
import BalanceWidget from './BalanceWidget';
|
||||
import CheckBox from './CheckBox';
|
||||
import { Counter } from './Counter';
|
||||
import DemoTable from './DemoTable';
|
||||
import { DisplayJson } from './DisplayJson';
|
||||
import Selector from './Selector';
|
||||
|
||||
export const CellsDemoPage: FunctionComponent = () => {
|
||||
const quill = QuillContext.use();
|
||||
|
||||
const cells = useMemo(() => {
|
||||
const page = new MemoryCell('math');
|
||||
|
||||
const a = quill.Cell('a', io.number, 3);
|
||||
const b = new MemoryCell(5);
|
||||
const c = new MemoryCell(0);
|
||||
const includeSlow = new MemoryCell(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const ab = new FormulaCell({ a, b }, ({ a, b }) => a * b);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const abSlow = new FormulaCell({ a, b }, async ({ a, b }) => {
|
||||
console.log(`calculating ${a}*${b}...`);
|
||||
await delay(500);
|
||||
const res = a * b;
|
||||
console.log(`...${res}`);
|
||||
return res;
|
||||
});
|
||||
|
||||
return { page, a, b, c, includeSlow, ab, abSlow };
|
||||
}, [quill]);
|
||||
|
||||
(window as any).cells = cells;
|
||||
|
||||
const includeSlowValue = useCell(cells.includeSlow);
|
||||
const cValue = useCell(cells.c);
|
||||
const pageValue = useCell(cells.page);
|
||||
|
||||
if (pageValue === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DemoTable>
|
||||
<tr>
|
||||
<td style={{ height: '3em' }}>page</td>
|
||||
<td>
|
||||
<Selector
|
||||
options={['math', 'blockNumber', 'balance', 'settings']}
|
||||
selection={cells.page}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{pageValue === 'math' && (
|
||||
<>
|
||||
<tr>
|
||||
<td>a: </td>
|
||||
<td>
|
||||
<Counter cell={cells.a} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>b: </td>
|
||||
<td>
|
||||
<Counter cell={cells.b} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ab: </td>
|
||||
<td>
|
||||
<DisplayJson cell={cells.ab} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
abSlow: <CheckBox cell={cells.includeSlow} />
|
||||
</td>
|
||||
<td>{includeSlowValue && <DisplayJson cell={cells.abSlow} />}</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
{pageValue === 'blockNumber' && (
|
||||
<>
|
||||
<tr>
|
||||
<td>components: </td>
|
||||
<td>
|
||||
<Counter cell={cells.c} />
|
||||
</td>
|
||||
</tr>
|
||||
{Range(cValue ?? 0).map((i) => (
|
||||
<tr key={i}>
|
||||
<td>blockNumber: </td>
|
||||
<td>
|
||||
<DisplayJson cell={quill.blockNumber} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{pageValue === 'balance' && <BalanceWidget />}
|
||||
{pageValue === 'settings' && (
|
||||
<>
|
||||
<tr>
|
||||
<td>Theme</td>
|
||||
<td>
|
||||
<Selector options={['light', 'dark']} selection={quill.theme} />
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</DemoTable>
|
||||
);
|
||||
};
|
||||
13
extension/source/QuillPage/CellsDemo/CellsDemoPage2.tsx
Normal file
13
extension/source/QuillPage/CellsDemo/CellsDemoPage2.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import DemoTable from './DemoTable';
|
||||
|
||||
export const CellsDemoPage2: FunctionComponent = () => {
|
||||
return (
|
||||
<DemoTable>
|
||||
<tr>
|
||||
<td>Hello:</td>
|
||||
<td>World</td>
|
||||
</tr>
|
||||
</DemoTable>
|
||||
);
|
||||
};
|
||||
22
extension/source/QuillPage/CellsDemo/CheckBox.tsx
Normal file
22
extension/source/QuillPage/CellsDemo/CheckBox.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import ICell from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
|
||||
const CheckBox: FunctionComponent<{ cell: ICell<boolean> }> = ({ cell }) => {
|
||||
const value = useCell(cell);
|
||||
|
||||
if (value === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={() => cell.write(!value)}
|
||||
style={{ width: 'initial' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBox;
|
||||
33
extension/source/QuillPage/CellsDemo/Counter.tsx
Normal file
33
extension/source/QuillPage/CellsDemo/Counter.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
import ICell from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
import Button from '../../components/Button';
|
||||
|
||||
export const Counter: FunctionComponent<{
|
||||
cell: ICell<number>;
|
||||
}> = ({ cell }) => {
|
||||
const value = useCell(cell);
|
||||
|
||||
if (value === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Button
|
||||
className="btn-secondary"
|
||||
onPress={() => value !== undefined && cell.write(value - 1)}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<div>{value}</div>
|
||||
<Button
|
||||
className="btn-secondary"
|
||||
onPress={() => value !== undefined && cell.write(value + 1)}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
extension/source/QuillPage/CellsDemo/DemoTable.tsx
Normal file
25
extension/source/QuillPage/CellsDemo/DemoTable.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
const DemoTable: FunctionComponent = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '2em',
|
||||
fontSize: '3em',
|
||||
lineHeight: '1.5em',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
<table className="demo-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: '380px', height: '0' }} />
|
||||
</tr>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoTable;
|
||||
11
extension/source/QuillPage/CellsDemo/Display.tsx
Normal file
11
extension/source/QuillPage/CellsDemo/Display.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import { IReadableCell } from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
|
||||
export const Display: FunctionComponent<{
|
||||
cell: IReadableCell<unknown>;
|
||||
}> = ({ cell }) => {
|
||||
const value = useCell(cell);
|
||||
return <>{value}</>;
|
||||
};
|
||||
16
extension/source/QuillPage/CellsDemo/DisplayJson.tsx
Normal file
16
extension/source/QuillPage/CellsDemo/DisplayJson.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
import { IReadableCell } from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
|
||||
export const DisplayJson: FunctionComponent<{
|
||||
cell: IReadableCell<unknown>;
|
||||
}> = ({ cell }) => {
|
||||
const value = useCell(cell);
|
||||
|
||||
return (
|
||||
<pre style={{ display: 'inline-block' }}>
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
30
extension/source/QuillPage/CellsDemo/Selector.tsx
Normal file
30
extension/source/QuillPage/CellsDemo/Selector.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import ICell from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
|
||||
const Selector: FunctionComponent<{
|
||||
options: string[];
|
||||
selection: ICell<string>;
|
||||
}> = ({ options, selection }) => {
|
||||
const selectionValue = useCell(selection);
|
||||
|
||||
if (selectionValue === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectionValue}
|
||||
onChange={(evt) => {
|
||||
selection.write(options[evt.target.selectedIndex]);
|
||||
}}
|
||||
style={{ border: '1px solid black' }}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default Selector;
|
||||
17
extension/source/QuillPage/CellsDemo/TextBox.tsx
Normal file
17
extension/source/QuillPage/CellsDemo/TextBox.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import ICell from '../../cells/ICell';
|
||||
import useCell from '../../cells/useCell';
|
||||
|
||||
const TextBox: FunctionComponent<{ value: ICell<string> }> = ({ value }) => {
|
||||
const valueValue = useCell(value) ?? '';
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={valueValue}
|
||||
onInput={(evt) => value.write((evt.target as HTMLInputElement).value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextBox;
|
||||
@@ -1,21 +1,24 @@
|
||||
import { FunctionComponent, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import QuillContext from '../QuillContext';
|
||||
|
||||
import OnboardingActionPanel from './OnboardingActionPanel';
|
||||
import OnboardingInfoPanel from './OnboardingInfoPanel';
|
||||
|
||||
const OnboardingPage: FunctionComponent = () => {
|
||||
const quillCtx = QuillContext.use();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const onboardingComplete = window
|
||||
.KeyringController()
|
||||
.isOnboardingComplete();
|
||||
(async () => {
|
||||
const onboardingComplete =
|
||||
await quillCtx.rpc.private.quill_isOnboardingComplete();
|
||||
|
||||
if (onboardingComplete) {
|
||||
navigate('/wallet/');
|
||||
}
|
||||
}, [navigate]);
|
||||
if (onboardingComplete) {
|
||||
navigate('/wallet/');
|
||||
}
|
||||
})();
|
||||
}, [navigate, quillCtx]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ArrowRight } from 'phosphor-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Button from '../../components/Button';
|
||||
import Range from '../../helpers/Range';
|
||||
import QuillContext from '../QuillContext';
|
||||
|
||||
const WordInReview: FunctionComponent<{
|
||||
index: number;
|
||||
@@ -49,6 +50,8 @@ const ReviewSecretPhrasePanel: FunctionComponent<{
|
||||
sampleIndexes?: number[];
|
||||
onBack: () => void;
|
||||
}> = ({ secretPhrase, sampleIndexes = [0, 3, 9, 11], onBack }) => {
|
||||
const quillCtx = QuillContext.use();
|
||||
|
||||
const len = sampleIndexes.length;
|
||||
|
||||
const [reviewWordStates, setReviewWordStates] = useState<boolean[]>(
|
||||
@@ -67,11 +70,9 @@ const ReviewSecretPhrasePanel: FunctionComponent<{
|
||||
const navigate = useNavigate();
|
||||
|
||||
const setHDWalletPhrase = async () => {
|
||||
window.KeyringController().setHDPhrase(secretPhrase.join(' '));
|
||||
window.KeyringController().createHDAccount();
|
||||
|
||||
const accounts = window.KeyringController().getAccounts();
|
||||
window.QuillController().getApi().setSelectedAddress(accounts[0]);
|
||||
await quillCtx.rpc.private.quill_setHDPhrase(secretPhrase.join(' '));
|
||||
const address = await quillCtx.rpc.private.quill_createHDAccount();
|
||||
await quillCtx.rpc.private.quill_setSelectedAddress(address);
|
||||
|
||||
navigate('/wallet');
|
||||
};
|
||||
|
||||
68
extension/source/QuillPage/QuillContext.ts
Normal file
68
extension/source/QuillPage/QuillContext.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as io from 'io-ts';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import mapValues from '../helpers/mapValues';
|
||||
import assert from '../helpers/assert';
|
||||
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
import Rpc, { rpcMap } from '../types/Rpc';
|
||||
import TimeCell from './TimeCell';
|
||||
import approximate from './approximate';
|
||||
import { FormulaCell } from '../cells/FormulaCell';
|
||||
import elcc from '../cells/extensionLocalCellCollection';
|
||||
|
||||
export default class QuillContext {
|
||||
ethersProvider: ethers.providers.Provider;
|
||||
rpc: Rpc;
|
||||
|
||||
Cell = elcc.Cell.bind(elcc);
|
||||
time = TimeCell(100);
|
||||
|
||||
blockNumber = new FormulaCell(
|
||||
{ _: approximate(this.time, 5000) },
|
||||
async () => {
|
||||
console.log('getting block number');
|
||||
return Number(await this.ethereum.request({ method: 'eth_blockNumber' }));
|
||||
},
|
||||
);
|
||||
|
||||
theme = this.Cell('theme', io.string, 'light');
|
||||
|
||||
constructor(public ethereum: QuillInPageProvider) {
|
||||
this.ethersProvider = new ethers.providers.Web3Provider(ethereum);
|
||||
|
||||
this.rpc = {
|
||||
public: mapValues(
|
||||
rpcMap.public,
|
||||
({ params: paramsType, output }, method) => {
|
||||
return async (...params: unknown[]) => {
|
||||
assert(paramsType.is(params));
|
||||
const response = await this.ethereum.request({ method, params });
|
||||
assert(output.is(response));
|
||||
return response as ExplicitAny;
|
||||
};
|
||||
},
|
||||
),
|
||||
private: mapValues(
|
||||
rpcMap.private,
|
||||
({ params: paramsType, output }, method) => {
|
||||
return async (...params: unknown[]) => {
|
||||
assert(paramsType.is(params));
|
||||
const response = await this.ethereum.request({ method, params });
|
||||
assert(output.is(response));
|
||||
return response as ExplicitAny;
|
||||
};
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private static context = createContext<QuillContext>({} as QuillContext);
|
||||
static Provider = QuillContext.context.Provider;
|
||||
|
||||
static use() {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useContext(QuillContext.context);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,28 @@ import { HashRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import OnboardingPage from './Onboarding/OnboardingPage';
|
||||
import { WalletPage } from './Wallet/WalletPage';
|
||||
import { CellsDemoPage } from './CellsDemo/CellsDemoPage';
|
||||
import { CellsDemoPage2 } from './CellsDemo/CellsDemoPage2';
|
||||
import QuillContext from './QuillContext';
|
||||
import useCell from '../cells/useCell';
|
||||
|
||||
const QuillPage: FunctionComponent = () => {
|
||||
const quillCtx = QuillContext.use();
|
||||
const theme = useCell(quillCtx.theme);
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
<Route path="/wallet/*" element={<WalletPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
<div className={`themable1 ${theme === 'dark' && 'dark-theme'}`}>
|
||||
<div className={`themable2 ${theme === 'dark' && 'dark-theme'}`}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
<Route path="/wallet/*" element={<WalletPage />} />
|
||||
<Route path="/cells-demo" element={<CellsDemoPage />} />
|
||||
<Route path="/cells-demo2" element={<CellsDemoPage2 />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
17
extension/source/QuillPage/TimeCell.ts
Normal file
17
extension/source/QuillPage/TimeCell.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IReadableCell } from '../cells/ICell';
|
||||
import MemoryCell from '../cells/MemoryCell';
|
||||
import delay from '../helpers/delay';
|
||||
|
||||
export default function TimeCell(accuracy: number): IReadableCell<number> {
|
||||
const cell = new MemoryCell(Math.floor(Date.now() / accuracy));
|
||||
|
||||
(async () => {
|
||||
while (true) {
|
||||
const now = Date.now();
|
||||
await delay(accuracy * Math.ceil(now / accuracy) - now);
|
||||
await cell.write(accuracy * Math.round(Date.now() / accuracy));
|
||||
}
|
||||
})();
|
||||
|
||||
return cell;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import QuillContext from '../QuillContext';
|
||||
import { ConnectionsWrapper } from './Connections/ConnectionWrapper';
|
||||
import { ContactsWrapper } from './Contacts/ContactsWrapper';
|
||||
import { Navigation } from './Navigation';
|
||||
@@ -42,18 +43,20 @@ const routes: IRoutes[] = [
|
||||
];
|
||||
|
||||
export const WalletPage: React.FunctionComponent = () => {
|
||||
const quillCtx = QuillContext.use();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
const onboardingComplete = window
|
||||
.KeyringController()
|
||||
.isOnboardingComplete();
|
||||
console.debug('onboardingComplete', onboardingComplete);
|
||||
(async () => {
|
||||
const onboardingComplete =
|
||||
await quillCtx.rpc.private.quill_isOnboardingComplete();
|
||||
console.debug('onboardingComplete', onboardingComplete);
|
||||
|
||||
if (!onboardingComplete) {
|
||||
navigate('/onboarding?p=1');
|
||||
}
|
||||
}, [navigate]);
|
||||
if (!onboardingComplete) {
|
||||
navigate('/onboarding?p=1');
|
||||
}
|
||||
})();
|
||||
}, [navigate, quillCtx]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FunctionComponent, useEffect, useState } from 'react';
|
||||
import Button from '../../../components/Button';
|
||||
import QuillContext from '../../QuillContext';
|
||||
import { WalletSummary } from './WalletSummary';
|
||||
|
||||
export interface IWallet {
|
||||
@@ -11,34 +12,36 @@ export interface IWallet {
|
||||
}
|
||||
|
||||
export const WalletsWrapper: FunctionComponent = () => {
|
||||
const quillCtx = QuillContext.use();
|
||||
|
||||
const [selected, setSelected] = useState<number>(0);
|
||||
const [wallets, setWallets] = useState<IWallet[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const setSelectedAddress = (address: string) => {
|
||||
window.QuillController().getApi().setSelectedAddress(address);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
|
||||
const accounts = window.KeyringController().getAccounts();
|
||||
const accounts = await quillCtx.rpc.public.eth_accounts();
|
||||
|
||||
setWallets(
|
||||
accounts.map((address: string, index: number) => {
|
||||
return {
|
||||
address,
|
||||
name: `wallet ${index}`,
|
||||
ether: 0,
|
||||
networks: 1,
|
||||
tokens: 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setLoading(false);
|
||||
setWallets(
|
||||
accounts.map((address: string, index: number) => {
|
||||
return {
|
||||
address,
|
||||
name: `wallet ${index}`,
|
||||
ether: 0,
|
||||
networks: 1,
|
||||
tokens: 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setLoading(false);
|
||||
|
||||
setSelectedAddress(accounts[0]);
|
||||
}, []);
|
||||
if (accounts[0]) {
|
||||
quillCtx.rpc.private.quill_setSelectedAddress(accounts[0]);
|
||||
}
|
||||
})();
|
||||
}, [quillCtx]);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
@@ -46,7 +49,7 @@ export const WalletsWrapper: FunctionComponent = () => {
|
||||
<div className="text-body">Wallets</div>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
await window.KeyringController().createHDAccount();
|
||||
await quillCtx.rpc.private.quill_createHDAccount();
|
||||
window.location.reload();
|
||||
}}
|
||||
children={'Add Wallet'}
|
||||
@@ -62,7 +65,7 @@ export const WalletsWrapper: FunctionComponent = () => {
|
||||
<WalletSummary
|
||||
onClick={() => {
|
||||
setSelected(index);
|
||||
setSelectedAddress(wallet.address);
|
||||
quillCtx.rpc.private.quill_setSelectedAddress(wallet.address);
|
||||
}}
|
||||
key={wallet.name}
|
||||
wallet={wallet}
|
||||
|
||||
20
extension/source/QuillPage/approximate.ts
Normal file
20
extension/source/QuillPage/approximate.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FormulaCell } from '../cells/FormulaCell';
|
||||
import { IReadableCell } from '../cells/ICell';
|
||||
|
||||
export default function approximate(
|
||||
value: IReadableCell<number>,
|
||||
accuracy: number,
|
||||
): IReadableCell<number> {
|
||||
return new FormulaCell<{ value: IReadableCell<number> }, number>(
|
||||
{ value },
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
({ value }) => value,
|
||||
(previous, latest) => {
|
||||
if (previous === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Math.abs(latest - previous) >= accuracy;
|
||||
},
|
||||
);
|
||||
}
|
||||
33
extension/source/QuillPage/getWindowEthereum.ts
Normal file
33
extension/source/QuillPage/getWindowEthereum.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import assert from '../helpers/assert';
|
||||
|
||||
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
|
||||
|
||||
export default function getWindowEthereum() {
|
||||
const windowAny = window as any;
|
||||
|
||||
if (windowAny.ethereum?.isQuill) {
|
||||
return Promise.resolve(windowAny.ethereum);
|
||||
}
|
||||
|
||||
return new Promise<QuillInPageProvider>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timed out waiting for window.ethereum'));
|
||||
}, 1000);
|
||||
|
||||
function handleEthereumInitialized() {
|
||||
clearTimeout(timeout);
|
||||
|
||||
window.removeEventListener(
|
||||
'ethereum#initialized',
|
||||
handleEthereumInitialized,
|
||||
);
|
||||
|
||||
const { ethereum } = windowAny;
|
||||
assert(ethereum?.isQuill === true);
|
||||
|
||||
resolve(ethereum);
|
||||
}
|
||||
|
||||
window.addEventListener('ethereum#initialized', handleEthereumInitialized);
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,23 @@
|
||||
import '../ContentScript';
|
||||
import '../styles/index.scss';
|
||||
import './styles.scss';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import QuillPage from './QuillPage';
|
||||
import QuillContext from './QuillContext';
|
||||
import getWindowEthereum from './getWindowEthereum';
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
import '../styles/index.scss';
|
||||
import './styles.scss';
|
||||
import '../Controllers/background';
|
||||
(window as any).Browser ??= Browser;
|
||||
|
||||
ReactDOM.render(<QuillPage />, document.getElementById('quill-page-root'));
|
||||
getWindowEthereum().then((ethereum) => {
|
||||
const quillContext = new QuillContext(ethereum);
|
||||
|
||||
ReactDOM.render(
|
||||
<QuillContext.Provider value={quillContext}>
|
||||
<QuillPage />
|
||||
</QuillContext.Provider>,
|
||||
document.getElementById('quill-page-root'),
|
||||
);
|
||||
});
|
||||
|
||||
7
extension/source/cells/AsyncIteratee.ts
Normal file
7
extension/source/cells/AsyncIteratee.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
type AsyncIteratee<I extends AsyncIterable<unknown>> = I extends AsyncIterable<
|
||||
infer T
|
||||
>
|
||||
? T
|
||||
: never;
|
||||
|
||||
export default AsyncIteratee;
|
||||
206
extension/source/cells/CellCollection.ts
Normal file
206
extension/source/cells/CellCollection.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
import ICell, { CellEmitter } from './ICell';
|
||||
import CellIterator from './CellIterator';
|
||||
import jsonHasChanged from './jsonHasChanged';
|
||||
import assert from '../helpers/assert';
|
||||
import IAsyncStorage from './IAsyncStorage';
|
||||
|
||||
export default class CellCollection {
|
||||
cells: Record<string, CollectionCell<ExplicitAny> | undefined> = {};
|
||||
|
||||
#asyncStorageChangeHandler = (keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const cell = this.cells[key];
|
||||
|
||||
if (cell) {
|
||||
cell.versionedRead();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constructor(public asyncStorage: IAsyncStorage) {
|
||||
asyncStorage.events.on('change', this.#asyncStorageChangeHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A `CellCollection` is usually intended to live for the life of your
|
||||
* application. In that case, calling .end is not required. However, if you're
|
||||
* doing something different and you'd like to clean up the change event
|
||||
* handler, this will take care of that.
|
||||
*/
|
||||
end() {
|
||||
this.asyncStorage.events.removeListener(
|
||||
'change',
|
||||
this.#asyncStorageChangeHandler,
|
||||
);
|
||||
}
|
||||
|
||||
Cell<T>(
|
||||
key: string,
|
||||
type: io.Type<T>,
|
||||
defaultValue: T,
|
||||
hasChanged = jsonHasChanged,
|
||||
): CollectionCell<T> {
|
||||
let cell = this.cells[key];
|
||||
|
||||
if (cell) {
|
||||
cell.applyType(type);
|
||||
return cell;
|
||||
}
|
||||
|
||||
cell = new CollectionCell(
|
||||
this.asyncStorage,
|
||||
key,
|
||||
type,
|
||||
defaultValue,
|
||||
hasChanged,
|
||||
);
|
||||
|
||||
this.cells[key] = cell;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
const cell = this.cells[key];
|
||||
cell?.end();
|
||||
delete this.cells[key];
|
||||
|
||||
await this.asyncStorage.write(key, io.undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export type Versioned<T> = { version: number; value: T };
|
||||
|
||||
export class CollectionCell<T> implements ICell<T> {
|
||||
events = new EventEmitter() as CellEmitter<T>;
|
||||
ended = false;
|
||||
|
||||
versionedType: io.Type<Versioned<T>>;
|
||||
lastSeen?: Versioned<T>;
|
||||
initialRead: Promise<void>;
|
||||
|
||||
constructor(
|
||||
public asyncStorage: IAsyncStorage,
|
||||
public key: string,
|
||||
public type: io.Type<T>,
|
||||
public defaultValue: T,
|
||||
public hasChanged: ICell<T>['hasChanged'] = jsonHasChanged,
|
||||
) {
|
||||
this.versionedType = io.type({ version: io.number, value: type });
|
||||
|
||||
this.initialRead = this.versionedRead().then((versionedReadResult) => {
|
||||
this.lastSeen = versionedReadResult;
|
||||
});
|
||||
}
|
||||
|
||||
applyType<X>(type: io.Type<X>) {
|
||||
if (type === io.unknown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type !== io.unknown && this.type.name !== type.name) {
|
||||
throw new Error(
|
||||
[
|
||||
'Tried to get existing storage cell with a different type',
|
||||
`(type: ${type.name}, existing: ${this.type.name})`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.lastSeen && !type.is(this.lastSeen.value)) {
|
||||
throw new Error(
|
||||
[
|
||||
`Type mismatch at storage key ${this.key}`,
|
||||
`contents: ${JSON.stringify(this.lastSeen.value)}`,
|
||||
`expected: ${type.name}`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.type === io.unknown) {
|
||||
this.type = type as unknown as io.Type<T>;
|
||||
this.versionedType = io.type({ version: io.number, value: this.type });
|
||||
}
|
||||
}
|
||||
|
||||
async read(): Promise<T> {
|
||||
const latest = await this.versionedRead();
|
||||
return latest.value;
|
||||
}
|
||||
|
||||
async write(newValue: T): Promise<void> {
|
||||
assert(!this.ended);
|
||||
assert(this.type.is(newValue));
|
||||
|
||||
await this.initialRead;
|
||||
|
||||
const newVersionedValue = {
|
||||
version: (this.lastSeen?.version ?? 0) + 1,
|
||||
value: newValue,
|
||||
};
|
||||
|
||||
const latest = await this.versionedRead();
|
||||
|
||||
if (!(newVersionedValue.version > latest.version)) {
|
||||
throw new Error('Rejecting write which is not newer than remote');
|
||||
}
|
||||
|
||||
await this.asyncStorage.write(
|
||||
this.key,
|
||||
this.versionedType,
|
||||
newVersionedValue,
|
||||
);
|
||||
|
||||
const { lastSeen: previous } = this;
|
||||
this.lastSeen = newVersionedValue;
|
||||
|
||||
if (this.hasChanged(previous?.value, newValue)) {
|
||||
this.events.emit('change', {
|
||||
previous: previous?.value,
|
||||
latest: newVersionedValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
this.events.emit('end');
|
||||
this.ended = true;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
return new CellIterator(this);
|
||||
}
|
||||
|
||||
async versionedRead(): Promise<Versioned<T>> {
|
||||
const readResult = await this.asyncStorage.read(
|
||||
this.key,
|
||||
this.versionedType,
|
||||
) ?? { version: 0, value: this.defaultValue };
|
||||
|
||||
if (!this.versionedType.is(readResult)) {
|
||||
throw new Error(
|
||||
[
|
||||
`Type mismatch at storage key ${this.key}`,
|
||||
`contents: ${JSON.stringify(readResult)}`,
|
||||
`expected: ${this.versionedType.name}`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.hasChanged(this.lastSeen?.value, readResult.value)) {
|
||||
this.events.emit('change', {
|
||||
previous: this.lastSeen?.value,
|
||||
latest: readResult.value,
|
||||
});
|
||||
}
|
||||
|
||||
this.lastSeen = readResult;
|
||||
|
||||
return readResult;
|
||||
}
|
||||
}
|
||||
69
extension/source/cells/CellIterator.ts
Normal file
69
extension/source/cells/CellIterator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
|
||||
import { ChangeEvent, IReadableCell } from './ICell';
|
||||
|
||||
export default class CellIterator<T> implements AsyncIterator<T> {
|
||||
lastProvided?: { value: T };
|
||||
cleanup = () => {};
|
||||
endHandler = () => {};
|
||||
|
||||
endListener = () => {
|
||||
this.endHandler();
|
||||
};
|
||||
|
||||
events = new EventEmitter() as TypedEmitter<{
|
||||
finished(): void;
|
||||
}>;
|
||||
|
||||
constructor(public cell: IReadableCell<T>) {
|
||||
this.cell.events.once('end', this.endListener);
|
||||
|
||||
this.cleanup = () => {
|
||||
this.cell.events.off('end', this.endListener);
|
||||
};
|
||||
}
|
||||
|
||||
async next() {
|
||||
const latestRead = await this.cell.read();
|
||||
|
||||
if (
|
||||
this.lastProvided === undefined ||
|
||||
this.cell.hasChanged(this.lastProvided.value, latestRead)
|
||||
) {
|
||||
this.lastProvided = { value: latestRead };
|
||||
return { value: latestRead, done: false };
|
||||
}
|
||||
|
||||
if (this.cell.ended) {
|
||||
return { value: undefined, done: true as const };
|
||||
}
|
||||
|
||||
return new Promise<IteratorResult<T>>((resolve) => {
|
||||
const changeHandler = ({ latest }: ChangeEvent<T>) => {
|
||||
this.cleanup();
|
||||
this.lastProvided = { value: latest };
|
||||
resolve({ value: latest });
|
||||
};
|
||||
|
||||
this.cell.events.once('change', changeHandler);
|
||||
|
||||
this.endHandler = () => {
|
||||
this.cleanup();
|
||||
resolve({ value: undefined, done: true });
|
||||
};
|
||||
|
||||
this.cleanup = () => {
|
||||
this.cell.events.off('change', changeHandler);
|
||||
this.cell.events.off('end', this.endListener);
|
||||
this.cleanup = () => {};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async return() {
|
||||
this.cleanup();
|
||||
this.events.emit('finished');
|
||||
return { value: undefined, done: true as const };
|
||||
}
|
||||
}
|
||||
238
extension/source/cells/FormulaCell.ts
Normal file
238
extension/source/cells/FormulaCell.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
|
||||
import AsyncReturnType from '../types/AsyncReturnType';
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
import CellIterator from './CellIterator';
|
||||
import { IReadableCell, ChangeEvent } from './ICell';
|
||||
import jsonHasChanged from './jsonHasChanged';
|
||||
import recordKeys from '../helpers/recordKeys';
|
||||
import MemoryCell from './MemoryCell';
|
||||
import AsyncIteratee from './AsyncIteratee';
|
||||
import Stoppable from './Stoppable';
|
||||
import nextEvent from './nextEvent';
|
||||
|
||||
type InputValues<InputCells extends Record<string, IReadableCell<unknown>>> = {
|
||||
[K in keyof InputCells]: AsyncReturnType<InputCells[K]['read']>;
|
||||
};
|
||||
|
||||
export class FormulaCell<
|
||||
InputCells extends Record<string, IReadableCell<unknown>>,
|
||||
T,
|
||||
> implements IReadableCell<Awaited<T>>
|
||||
{
|
||||
events = new EventEmitter() as TypedEmitter<{
|
||||
change(changeEvent: ChangeEvent<T>): void;
|
||||
end(): void;
|
||||
'zero-iterators'(): void;
|
||||
}>;
|
||||
|
||||
lastProvidedValue?: Awaited<T>;
|
||||
iterationCell?: MemoryCell<'pending' | { value: Awaited<T> }>;
|
||||
ended = false;
|
||||
iteratorCount = 0;
|
||||
|
||||
constructor(
|
||||
public inputCells: InputCells,
|
||||
public formula: (inputValues: InputValues<InputCells>) => T,
|
||||
public hasChanged: (
|
||||
previous: Awaited<T> | undefined,
|
||||
latest: Awaited<T>,
|
||||
) => boolean = jsonHasChanged,
|
||||
) {}
|
||||
|
||||
end() {
|
||||
this.events.emit('end');
|
||||
this.ended = true;
|
||||
}
|
||||
|
||||
async read(): Promise<Awaited<T>> {
|
||||
const iterationCell = this.iterateInputs();
|
||||
|
||||
try {
|
||||
for await (const iterationState of iterationCell) {
|
||||
if (iterationState === 'pending') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return iterationState.value;
|
||||
}
|
||||
} finally {
|
||||
this.decrementIterators();
|
||||
}
|
||||
|
||||
throw new Error('Inputs ended');
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<Awaited<T>> {
|
||||
this.iterateInputs();
|
||||
|
||||
const iterator = new CellIterator<Awaited<T>>(this);
|
||||
|
||||
iterator.events.on('finished', () => {
|
||||
this.decrementIterators();
|
||||
});
|
||||
|
||||
return iterator;
|
||||
}
|
||||
|
||||
decrementIterators() {
|
||||
this.iteratorCount -= 1;
|
||||
|
||||
if (this.iteratorCount === 0) {
|
||||
this.events.emit('zero-iterators');
|
||||
}
|
||||
}
|
||||
|
||||
iterateInputs() {
|
||||
this.iteratorCount += 1;
|
||||
|
||||
if (this.iterationCell) {
|
||||
return this.iterationCell;
|
||||
}
|
||||
|
||||
this.iterationCell = new MemoryCell<'pending' | { value: Awaited<T> }>(
|
||||
'pending',
|
||||
(previous, latest) => {
|
||||
if (previous === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previous === 'pending' || latest === 'pending') {
|
||||
return previous !== latest;
|
||||
}
|
||||
|
||||
return this.hasChanged(previous.value, latest.value);
|
||||
},
|
||||
);
|
||||
|
||||
const { iterationCell } = this;
|
||||
|
||||
(async () => {
|
||||
const stoppableSequence = new Stoppable(
|
||||
toIterableOfRecords(this.inputCells),
|
||||
);
|
||||
|
||||
{
|
||||
const handler = () => {
|
||||
stoppableSequence.stop();
|
||||
this.events.off('zero-iterators', handler);
|
||||
this.events.off('end', handler);
|
||||
};
|
||||
|
||||
this.events.once('zero-iterators', handler);
|
||||
this.events.once('end', handler);
|
||||
}
|
||||
|
||||
for await (const inputValues of stoppableSequence) {
|
||||
const latest = await this.formula(
|
||||
inputValues as InputValues<InputCells>,
|
||||
);
|
||||
|
||||
iterationCell.write({ value: latest });
|
||||
|
||||
if (this.hasChanged(this.lastProvidedValue, latest)) {
|
||||
this.events.emit('change', {
|
||||
previous: this.lastProvidedValue,
|
||||
latest,
|
||||
});
|
||||
|
||||
this.lastProvidedValue = latest;
|
||||
}
|
||||
}
|
||||
|
||||
iterationCell.end();
|
||||
this.iterationCell = undefined;
|
||||
|
||||
if (this.iteratorCount > 0) {
|
||||
this.end();
|
||||
}
|
||||
})();
|
||||
|
||||
return iterationCell;
|
||||
}
|
||||
}
|
||||
|
||||
function toIterableOfRecords<R extends Record<string, AsyncIterable<unknown>>>(
|
||||
recordOfIterables: R,
|
||||
): AsyncIterable<{ [K in keyof R]: AsyncIteratee<R[K]> }> {
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
const latest = {} as { [K in keyof R]: AsyncIteratee<R[K]> };
|
||||
let latestVersion = 0;
|
||||
let providedVersion = 0;
|
||||
let keysFilled = 0;
|
||||
const keysNeeded = recordKeys(recordOfIterables).length;
|
||||
let cleanup = () => {};
|
||||
|
||||
const events = new EventEmitter() as TypedEmitter<{
|
||||
updated(): void;
|
||||
end(): void;
|
||||
}>;
|
||||
|
||||
const endedPromise = nextEvent(events, 'end');
|
||||
|
||||
function end() {
|
||||
events.emit('end');
|
||||
cleanup();
|
||||
}
|
||||
|
||||
for (const key of recordKeys(recordOfIterables)) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
(async () => {
|
||||
const stoppableSequence = new Stoppable(recordOfIterables[key]);
|
||||
endedPromise.then(() => stoppableSequence.stop());
|
||||
|
||||
for await (const value of stoppableSequence) {
|
||||
if (!(key in latest)) {
|
||||
keysFilled += 1;
|
||||
}
|
||||
|
||||
latest[key] = value as ExplicitAny;
|
||||
|
||||
if (keysFilled === keysNeeded) {
|
||||
latestVersion += 1;
|
||||
events.emit('updated');
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return {
|
||||
async next() {
|
||||
if (latestVersion > providedVersion) {
|
||||
providedVersion = latestVersion;
|
||||
return { value: latest };
|
||||
}
|
||||
|
||||
return new Promise<IteratorResult<typeof latest>>((resolve) => {
|
||||
const updatedHandler = () => {
|
||||
cleanup();
|
||||
providedVersion = latestVersion;
|
||||
resolve({ value: latest });
|
||||
};
|
||||
|
||||
events.on('updated', updatedHandler);
|
||||
|
||||
const endHandler = () => {
|
||||
cleanup();
|
||||
resolve({ value: undefined, done: true });
|
||||
};
|
||||
|
||||
events.on('end', endHandler);
|
||||
|
||||
cleanup = () => {
|
||||
events.off('updated', updatedHandler);
|
||||
events.off('end', endHandler);
|
||||
};
|
||||
});
|
||||
},
|
||||
async return(): Promise<IteratorResult<typeof latest>> {
|
||||
end();
|
||||
return { value: undefined, done: true };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
12
extension/source/cells/IAsyncStorage.ts
Normal file
12
extension/source/cells/IAsyncStorage.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as io from 'io-ts';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
|
||||
type IAsyncStorage = {
|
||||
read<T>(key: string, type: io.Type<T>): Promise<T | undefined>;
|
||||
write<T>(key: string, type: io.Type<T>, value: T | undefined): Promise<void>;
|
||||
events: TypedEmitter<{
|
||||
change(keys: string[]): void,
|
||||
}>;
|
||||
};
|
||||
|
||||
export default IAsyncStorage;
|
||||
24
extension/source/cells/ICell.ts
Normal file
24
extension/source/cells/ICell.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
|
||||
type ICell<T> = {
|
||||
events: CellEmitter<T>;
|
||||
ended: boolean;
|
||||
read(): Promise<T>;
|
||||
write(newValue: T): Promise<void>;
|
||||
hasChanged(previous: T | undefined, latest: T): boolean;
|
||||
[Symbol.asyncIterator](): AsyncIterator<T>;
|
||||
};
|
||||
|
||||
export type IReadableCell<T> = Omit<ICell<T>, 'write'>;
|
||||
|
||||
export type ChangeEvent<T> = {
|
||||
previous?: T;
|
||||
latest: T;
|
||||
};
|
||||
|
||||
export type CellEmitter<T> = TypedEmitter<{
|
||||
change(changeEvent: ChangeEvent<T>): void;
|
||||
end(): void;
|
||||
}>;
|
||||
|
||||
export default ICell;
|
||||
43
extension/source/cells/MemoryCell.ts
Normal file
43
extension/source/cells/MemoryCell.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import assert from '../helpers/assert';
|
||||
import CellIterator from './CellIterator';
|
||||
|
||||
import ICell, { CellEmitter } from './ICell';
|
||||
import jsonHasChanged from './jsonHasChanged';
|
||||
|
||||
export default class MemoryCell<T> implements ICell<T> {
|
||||
events = new EventEmitter() as CellEmitter<T>;
|
||||
ended = false;
|
||||
|
||||
constructor(
|
||||
public value: T,
|
||||
public hasChanged: ICell<T>['hasChanged'] = jsonHasChanged,
|
||||
) {}
|
||||
|
||||
async read(): Promise<T> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
async write(newValue: T): Promise<void> {
|
||||
assert(!this.ended);
|
||||
|
||||
const { value: previous } = this;
|
||||
this.value = newValue;
|
||||
|
||||
if (this.hasChanged(previous, this.value)) {
|
||||
this.events.emit('change', {
|
||||
previous,
|
||||
latest: this.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
this.events.emit('end');
|
||||
this.ended = true;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
return new CellIterator(this);
|
||||
}
|
||||
}
|
||||
41
extension/source/cells/MemoryCellCollection.ts
Normal file
41
extension/source/cells/MemoryCellCollection.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as io from 'io-ts';
|
||||
import assert from '../helpers/assert';
|
||||
|
||||
import CellCollection from './CellCollection';
|
||||
import IAsyncStorage from './IAsyncStorage';
|
||||
|
||||
export default function MemoryCellCollection(
|
||||
memory: Record<string, unknown> = {},
|
||||
) {
|
||||
const events = new EventEmitter() as IAsyncStorage['events'];
|
||||
|
||||
return new CellCollection({
|
||||
async read<T>(key: string, type: io.Type<T>): Promise<T | undefined> {
|
||||
const readResult = memory[key];
|
||||
|
||||
if (readResult !== undefined) {
|
||||
assert(type.is(readResult));
|
||||
}
|
||||
|
||||
return readResult;
|
||||
},
|
||||
|
||||
async write<T>(
|
||||
key: string,
|
||||
type: io.Type<T>,
|
||||
value: T | undefined,
|
||||
): Promise<void> {
|
||||
if (value !== undefined) {
|
||||
assert(type.is(value));
|
||||
}
|
||||
|
||||
memory[key] = value;
|
||||
|
||||
events.emit('change', [key]);
|
||||
},
|
||||
|
||||
events,
|
||||
});
|
||||
}
|
||||
10
extension/source/cells/README.md
Normal file
10
extension/source/cells/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# cells
|
||||
|
||||
Cells is an internal library designed to enable:
|
||||
- Persistence
|
||||
- Synchronization
|
||||
- Modelling interior mutability
|
||||
|
||||
## [Video walkthrough](https://www.youtube.com/watch?v=0BwxE_EUcug)
|
||||
|
||||
[](https://www.youtube.com/watch?v=0BwxE_EUcug)
|
||||
44
extension/source/cells/Stoppable.ts
Normal file
44
extension/source/cells/Stoppable.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { once } from 'lodash-es';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import raceWithEvent from './raceWithEvent';
|
||||
|
||||
export default class Stoppable<T> {
|
||||
stopped = false;
|
||||
|
||||
events = new EventEmitter() as TypedEmitter<{
|
||||
stopped(): void;
|
||||
}>;
|
||||
|
||||
constructor(public asyncIterable: AsyncIterable<T>) {}
|
||||
|
||||
stop() {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopped = true;
|
||||
this.events.emit('stopped');
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
const aiIterator = this.asyncIterable[Symbol.asyncIterator]();
|
||||
const aiIteratorReturn = once(() => aiIterator.return?.());
|
||||
|
||||
return {
|
||||
next: async () => {
|
||||
if (this.stopped) {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
|
||||
return raceWithEvent(aiIterator.next(), this.events, 'stopped', () => {
|
||||
aiIteratorReturn();
|
||||
return { value: undefined, done: true };
|
||||
});
|
||||
},
|
||||
return() {
|
||||
return aiIteratorReturn();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
45
extension/source/cells/extensionLocalCellCollection.ts
Normal file
45
extension/source/cells/extensionLocalCellCollection.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as io from 'io-ts';
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
import assert from '../helpers/assert';
|
||||
import CellCollection from './CellCollection';
|
||||
import IAsyncStorage from './IAsyncStorage';
|
||||
|
||||
const events = new EventEmitter() as IAsyncStorage['events'];
|
||||
|
||||
Browser.storage.onChanged.addListener((changes) => {
|
||||
events.emit('change', Object.keys(changes));
|
||||
});
|
||||
|
||||
export default new CellCollection({
|
||||
async read<T>(key: string, type: io.Type<T>): Promise<T | undefined> {
|
||||
const readResult = (await Browser.storage.local.get(key))[key];
|
||||
|
||||
if (readResult !== undefined) {
|
||||
assert(type.is(readResult));
|
||||
}
|
||||
|
||||
return readResult;
|
||||
},
|
||||
|
||||
async write<T>(
|
||||
key: string,
|
||||
type: io.Type<T>,
|
||||
value: T | undefined,
|
||||
): Promise<void> {
|
||||
if (value === undefined) {
|
||||
Browser.storage.local.remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(type.is(value));
|
||||
|
||||
Browser.storage.local.set({
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
|
||||
events,
|
||||
});
|
||||
3
extension/source/cells/jsonHasChanged.ts
Normal file
3
extension/source/cells/jsonHasChanged.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function jsonHasChanged<T>(previous: T | undefined, latest: T) {
|
||||
return JSON.stringify(previous) !== JSON.stringify(latest);
|
||||
}
|
||||
45
extension/source/cells/localCellCollection.ts
Normal file
45
extension/source/cells/localCellCollection.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import assert from '../helpers/assert';
|
||||
import CellCollection from './CellCollection';
|
||||
import IAsyncStorage from './IAsyncStorage';
|
||||
|
||||
const events = new EventEmitter() as IAsyncStorage['events'];
|
||||
|
||||
window.addEventListener('storage', evt => {
|
||||
if (evt.key !== null) {
|
||||
events.emit('change', [evt.key]);
|
||||
}
|
||||
});
|
||||
|
||||
export default new CellCollection({
|
||||
async read<T>(key: string, type: io.Type<T>): Promise<T | undefined> {
|
||||
const readResultStr = localStorage.getItem(key) ?? undefined;
|
||||
const readResult = readResultStr && JSON.parse(readResultStr);
|
||||
|
||||
if (readResult !== undefined) {
|
||||
assert(type.is(readResult));
|
||||
}
|
||||
|
||||
return readResult;
|
||||
},
|
||||
|
||||
async write<T>(
|
||||
key: string,
|
||||
type: io.Type<T>,
|
||||
value: T | undefined,
|
||||
): Promise<void> {
|
||||
if (value === undefined) {
|
||||
localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(type.is(value));
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
|
||||
events,
|
||||
});
|
||||
14
extension/source/cells/nextEvent.ts
Normal file
14
extension/source/cells/nextEvent.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
|
||||
export default function nextEvent<
|
||||
Events extends Record<string, ExplicitAny>,
|
||||
E extends keyof Events,
|
||||
>(
|
||||
emitter: TypedEmitter<Events>,
|
||||
eventName: E,
|
||||
): Promise<Parameters<Events[E]>[0]> {
|
||||
return new Promise<Parameters<Events[E]>[0]>((resolve) => {
|
||||
emitter.once(eventName, resolve as ExplicitAny);
|
||||
});
|
||||
}
|
||||
31
extension/source/cells/raceWithEvent.ts
Normal file
31
extension/source/cells/raceWithEvent.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import ExplicitAny from '../types/ExplicitAny';
|
||||
|
||||
export default async function raceWithEvent<
|
||||
PromiseResult,
|
||||
EventResult,
|
||||
Events extends Record<string, ExplicitAny>,
|
||||
E extends keyof Events,
|
||||
>(
|
||||
promise: Promise<PromiseResult>,
|
||||
emitter: TypedEmitter<Events>,
|
||||
eventName: E,
|
||||
listener: (...params: Parameters<Events[E]>) => EventResult,
|
||||
): Promise<PromiseResult | EventResult> {
|
||||
const result = Promise.race([
|
||||
promise,
|
||||
new Promise<EventResult>((resolve) => {
|
||||
const wrappedListener = (...params: Parameters<Events[E]>) => {
|
||||
resolve(listener(...params));
|
||||
};
|
||||
|
||||
emitter.once(eventName, wrappedListener as ExplicitAny);
|
||||
|
||||
promise.finally(() =>
|
||||
emitter.off(eventName, wrappedListener as ExplicitAny),
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return result;
|
||||
}
|
||||
30
extension/source/cells/useCell.ts
Normal file
30
extension/source/cells/useCell.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import AsyncReturnType from '../types/AsyncReturnType';
|
||||
|
||||
import { IReadableCell } from './ICell';
|
||||
import Stoppable from './Stoppable';
|
||||
|
||||
export default function useCell<C extends IReadableCell<unknown>>(
|
||||
cellParam: C,
|
||||
) {
|
||||
type T = AsyncReturnType<C['read']>;
|
||||
const cell = cellParam as IReadableCell<T>;
|
||||
|
||||
const [value, setValue] = useState<T>();
|
||||
|
||||
useEffect(() => {
|
||||
const stoppableSequence = new Stoppable(cell);
|
||||
|
||||
(async () => {
|
||||
for await (const v of stoppableSequence) {
|
||||
setValue(v);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
stoppableSequence.stop();
|
||||
};
|
||||
}, [cell]);
|
||||
|
||||
return value;
|
||||
}
|
||||
23
extension/source/cells/useCellState.ts
Normal file
23
extension/source/cells/useCellState.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMemo } from 'react';
|
||||
import ICell from './ICell';
|
||||
import MemoryCell from './MemoryCell';
|
||||
import useCell from './useCell';
|
||||
|
||||
declare function useCellStateOverload<T>(
|
||||
initialValue: T,
|
||||
): [T, (newValue: T) => void, ICell<T>];
|
||||
|
||||
declare function useCellStateOverload<T>(): /* no argument given */
|
||||
[T | undefined, (newValue: T | undefined) => void, ICell<T | undefined>];
|
||||
|
||||
function useCellState<T>(
|
||||
initialValue: T,
|
||||
): [T, (newValue: T) => void, ICell<T>] {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const cell = useMemo(() => new MemoryCell(initialValue), []);
|
||||
const cellValue = useCell(cell);
|
||||
|
||||
return [cellValue ?? initialValue, (newValue) => cell.write(newValue), cell];
|
||||
}
|
||||
|
||||
export default useCellState as typeof useCellStateOverload;
|
||||
@@ -1,23 +0,0 @@
|
||||
type RpcMap = {
|
||||
eth_sendTransaction: {
|
||||
params: [
|
||||
{
|
||||
nonce?: string;
|
||||
gasPrice?: string;
|
||||
gas?: string;
|
||||
to?: string;
|
||||
from?: string;
|
||||
value?: string;
|
||||
data?: string;
|
||||
chainId?: string;
|
||||
},
|
||||
];
|
||||
result: string;
|
||||
};
|
||||
add: {
|
||||
params: [number, number];
|
||||
result: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default RpcMap;
|
||||
@@ -1,3 +0,0 @@
|
||||
type TransportClient = (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
export default TransportClient;
|
||||
@@ -2,10 +2,6 @@ export const CONTENT_SCRIPT = 'quill-contentscript';
|
||||
export const INPAGE = 'quill-inpage';
|
||||
export const PROVIDER = 'quill-provider';
|
||||
|
||||
export const PROVIDER_JRPC_METHODS = {
|
||||
GET_PROVIDER_STATE: 'wallet_get_provider_state',
|
||||
};
|
||||
|
||||
export const PROVIDER_NOTIFICATIONS = {
|
||||
ACCOUNTS_CHANGED: 'wallet_accounts_changed',
|
||||
CHAIN_CHANGED: 'wallet_chain_changed',
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import generateRandomHex from '../helpers/generateRandomHex';
|
||||
import TransportClient from './TransportClient';
|
||||
|
||||
const ackDelayMax = 100; // milliseconds
|
||||
|
||||
export function PostMessageTransportClient(target: string): TransportClient {
|
||||
return (...args: unknown[]) =>
|
||||
new Promise<unknown>((resolve, reject) => {
|
||||
const messageId = generateRandomHex(256);
|
||||
|
||||
const ackTimer = setTimeout(() => {
|
||||
reject(new Error('Message not acknowledged'));
|
||||
}, ackDelayMax);
|
||||
|
||||
function messageListener(evt: MessageEvent) {
|
||||
if (
|
||||
// Ignore messages without our id
|
||||
evt.data.messageId !== messageId ||
|
||||
// The message we send out has our id, also ignore that
|
||||
evt.data.target === target
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(ackTimer);
|
||||
|
||||
if (evt.data.type === 'ack') {
|
||||
// Do nothing (timer cleared on any messageId match above)
|
||||
} else if (evt.data.type === 'response') {
|
||||
window.removeEventListener('message', messageListener);
|
||||
resolve(evt.data.response);
|
||||
} else if (evt.data.type === 'error') {
|
||||
window.removeEventListener('message', messageListener);
|
||||
reject(new Error(evt.data.errorMessage));
|
||||
} else {
|
||||
console.warn('Unexpected message', evt);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
target,
|
||||
messageId,
|
||||
args,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function PostMessageTransportServer(
|
||||
target: string,
|
||||
handler: (...args: unknown[]) => Promise<unknown>,
|
||||
): { close(): void } {
|
||||
async function messageListener(evt: MessageEvent) {
|
||||
if (evt.data.target !== target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { messageId } = evt.data;
|
||||
|
||||
let replied = false;
|
||||
|
||||
// Send an ack if we don't send a reply asap
|
||||
setTimeout(() => {
|
||||
if (replied === false) {
|
||||
window.postMessage({ messageId, type: 'ack' }, '*');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await handler(...evt.data.args);
|
||||
window.postMessage({ messageId, type: 'response', response }, '*');
|
||||
} catch (error) {
|
||||
window.postMessage(
|
||||
{ messageId, type: 'error', errorMessage: (error as Error).message },
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
replied = true;
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
|
||||
return {
|
||||
close() {
|
||||
window.removeEventListener('message', messageListener);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import addErrorContext from './addErrorContext';
|
||||
|
||||
export default function validateOptionalStringRecord<
|
||||
Keys extends readonly string[],
|
||||
>(keys: Keys): (value: unknown) => { [K in Keys[number]]?: string } {
|
||||
return addErrorContext('validateOptionalStringRecord', (value) => {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error('Expected object');
|
||||
}
|
||||
|
||||
const valueRecord = value as Record<string, unknown>;
|
||||
|
||||
const result: { [K in Keys[number]]?: string } = {};
|
||||
|
||||
for (const k of keys) {
|
||||
const fieldValue = valueRecord[k];
|
||||
|
||||
if (fieldValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof fieldValue !== 'string') {
|
||||
throw new Error(
|
||||
`Expected field "${k}" to be a string (but was: ${typeof fieldValue})`,
|
||||
);
|
||||
}
|
||||
|
||||
result[k as Keys[number]] = fieldValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,7 @@ const Button = (props: {
|
||||
className={
|
||||
props.loading
|
||||
? 'btn-loading'
|
||||
: `flex gap-2 items-center ${props.className}`
|
||||
: `flex gap-2 items-center select-none ${props.className}`
|
||||
}
|
||||
onClick={props.onPress}
|
||||
onKeyDown={(evt) => {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
import delay from '../helpers/delay';
|
||||
import type { PageEvents } from './Page';
|
||||
|
||||
type Props = {
|
||||
events: PageEvents;
|
||||
};
|
||||
|
||||
type Level = 'info' | 'error';
|
||||
|
||||
type State = {
|
||||
activeCount: number;
|
||||
presentCount: number;
|
||||
level: Level;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
activeCount: 0,
|
||||
presentCount: 0,
|
||||
level: 'info',
|
||||
text: '',
|
||||
};
|
||||
|
||||
export default class NotificationContainer extends Component<Props, State> {
|
||||
targetState = initialState;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = initialState;
|
||||
this.props.events.on('notification', this.onNotify);
|
||||
}
|
||||
|
||||
onNotify = async (level: 'info' | 'error', text: string): Promise<void> => {
|
||||
this.setTarget({
|
||||
presentCount: this.targetState.presentCount + 1,
|
||||
level,
|
||||
text,
|
||||
});
|
||||
|
||||
await delay(0);
|
||||
this.setTarget({ activeCount: this.targetState.activeCount + 1 });
|
||||
|
||||
await delay(3000);
|
||||
this.setTarget({ activeCount: this.targetState.activeCount - 1 });
|
||||
|
||||
await delay(500);
|
||||
this.setTarget({ presentCount: this.targetState.presentCount - 1 });
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.events.off('notification', this.onNotify);
|
||||
}
|
||||
|
||||
setTarget(updates: Partial<State>): void {
|
||||
this.targetState = { ...this.targetState, ...updates };
|
||||
|
||||
super.setState(this.targetState);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const classes = ['notification', this.state.level];
|
||||
|
||||
if (this.state.activeCount > 0) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
if (this.state.presentCount > 0) {
|
||||
classes.push('present');
|
||||
}
|
||||
|
||||
return <div className={classes.join(' ')}>{this.state.text}</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Component, ReactElement, ReactNode } from 'react';
|
||||
import delay from '../helpers/delay';
|
||||
import type { PageEvents, PageOverlay } from './Page';
|
||||
|
||||
type Props = {
|
||||
events: PageEvents;
|
||||
};
|
||||
|
||||
type State = {
|
||||
activeCount: number;
|
||||
presentCount: number;
|
||||
overlayRenders: ReactElement[];
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
activeCount: 0,
|
||||
presentCount: 0,
|
||||
overlayRenders: [],
|
||||
};
|
||||
|
||||
export default class OverlayContainer extends Component<Props, State> {
|
||||
targetState = initialState;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = initialState;
|
||||
this.props.events.on('overlay', this.onOverlay);
|
||||
}
|
||||
|
||||
onOverlay = async (overlay: PageOverlay): Promise<void> => {
|
||||
const overlayRender = overlay(close);
|
||||
let isClosed = false;
|
||||
|
||||
this.setTarget({
|
||||
overlayRenders: [...this.targetState.overlayRenders, overlayRender],
|
||||
presentCount: this.targetState.presentCount + 1,
|
||||
});
|
||||
|
||||
await delay(0);
|
||||
this.setTarget({ activeCount: this.targetState.activeCount + 1 });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
|
||||
async function close() {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosed = true;
|
||||
|
||||
self.setTarget({ activeCount: self.targetState.activeCount - 1 });
|
||||
|
||||
await delay(500);
|
||||
|
||||
self.setTarget({
|
||||
overlayRenders: self.targetState.overlayRenders.filter(
|
||||
(oi) => oi !== overlayRender,
|
||||
),
|
||||
presentCount: self.targetState.presentCount - 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.events.off('overlay', this.onOverlay);
|
||||
}
|
||||
|
||||
setTarget(updates: Partial<State>): void {
|
||||
this.targetState = { ...this.targetState, ...updates };
|
||||
|
||||
super.setState(this.targetState);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const currentRender = this.state.overlayRenders.slice(-1)[0];
|
||||
|
||||
if (currentRender === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const classes = ['overlay'];
|
||||
|
||||
if (this.state.activeCount > 0) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
if (this.state.presentCount > 0) {
|
||||
classes.push('present');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.join(' ')}>
|
||||
<div className="content">{currentRender}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Component, ReactElement, ReactNode } from 'react';
|
||||
import TypedEventEmitter from 'typed-emitter';
|
||||
|
||||
import NotificationContainer from './NotificationContainer';
|
||||
import OverlayContainer from './OverlayContainer';
|
||||
import ScreenContainer from './ScreenContainer';
|
||||
|
||||
export type PageOverlay = (close: () => void) => ReactElement;
|
||||
|
||||
export type PageEventMap = {
|
||||
notification(level: 'info' | 'error', text: string): void;
|
||||
overlay(overlay: PageOverlay): void;
|
||||
screen(screen: ReactElement): void;
|
||||
};
|
||||
|
||||
export type PageEvents = TypedEventEmitter<PageEventMap>;
|
||||
|
||||
type Props = {
|
||||
classes?: string[];
|
||||
events: PageEvents;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type State = {};
|
||||
|
||||
export default class Page extends Component<Props, State> {
|
||||
render(): ReactNode {
|
||||
const classes = ['page', ...(this.props.classes ?? [])];
|
||||
|
||||
return (
|
||||
<div className={classes.join(' ')}>
|
||||
<ScreenContainer events={this.props.events}>
|
||||
{this.props.children}
|
||||
</ScreenContainer>
|
||||
<NotificationContainer events={this.props.events} />
|
||||
<OverlayContainer events={this.props.events} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Component, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import Button from './Button';
|
||||
// import DefaultScreen from '../Popup/components/DefaultScreen';
|
||||
import OverrideScreen, {
|
||||
overrideScreenEnabled,
|
||||
} from '../Popup/components/OverrideScreen';
|
||||
import type { PageEvents } from './Page';
|
||||
|
||||
type Props = {
|
||||
events: PageEvents;
|
||||
};
|
||||
|
||||
type State = {
|
||||
screens: ReactElement[];
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
screens: [],
|
||||
};
|
||||
|
||||
export default class ScreenContainer extends Component<Props, State> {
|
||||
targetState = initialState;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = initialState;
|
||||
|
||||
if (overrideScreenEnabled) {
|
||||
this.state.screens.push(<OverrideScreen key={1} />);
|
||||
}
|
||||
|
||||
this.props.events.on('screen', this.onScreen);
|
||||
}
|
||||
|
||||
onScreen = async (screen: ReactElement): Promise<void> => {
|
||||
this.setTarget({
|
||||
screens: [...this.targetState.screens, screen],
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.events.off('screen', this.onScreen);
|
||||
}
|
||||
|
||||
setTarget(updates: Partial<State>): void {
|
||||
this.targetState = { ...this.targetState, ...updates };
|
||||
|
||||
super.setState(this.targetState);
|
||||
}
|
||||
|
||||
back(): void {
|
||||
const newScreens = this.targetState.screens.slice();
|
||||
newScreens.pop();
|
||||
|
||||
this.setTarget({
|
||||
screens: newScreens,
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const currentScreen =
|
||||
this.state.screens.slice(-1)[0] ?? this.props.children;
|
||||
|
||||
return (
|
||||
<div className="screen">
|
||||
{currentScreen}
|
||||
{(() => {
|
||||
if (this.state.screens.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="back-container">
|
||||
<Button onPress={() => this.back()}>Back</Button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
20
extension/source/helpers/mapValues.ts
Normal file
20
extension/source/helpers/mapValues.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import recordKeys from './recordKeys';
|
||||
|
||||
export default function mapValues<
|
||||
InputRecord extends Record<string, unknown>,
|
||||
Output,
|
||||
>(
|
||||
inputs: InputRecord,
|
||||
mapper: (
|
||||
input: InputRecord[keyof InputRecord],
|
||||
key: keyof InputRecord,
|
||||
) => Output,
|
||||
): Record<keyof InputRecord, Output> {
|
||||
const res = {} as Record<keyof InputRecord, Output>;
|
||||
|
||||
for (const key of recordKeys(inputs)) {
|
||||
res[key] = mapper(inputs[key], key);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
10
extension/source/helpers/recordKeys.ts
Normal file
10
extension/source/helpers/recordKeys.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Wrapper around Object.keys which uses keyof to get the accurate key type.
|
||||
* (The builtin typing for Object.keys unnecessarily widens the type to
|
||||
* string[].)
|
||||
*/
|
||||
export default function recordKeys<R extends Record<string, unknown>>(
|
||||
record: R,
|
||||
): (keyof R)[] {
|
||||
return Object.keys(record) as (keyof R)[];
|
||||
}
|
||||
@@ -269,4 +269,25 @@
|
||||
.transaction-tab {
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.themable1.dark-theme {
|
||||
filter: unquote("invert()");
|
||||
background-color: white;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.themable2.dark-theme {
|
||||
filter: unquote("hue-rotate(180deg)");
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
td:nth-child(2) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
extension/source/types/AsyncReturnType.ts
Normal file
9
extension/source/types/AsyncReturnType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import ExplicitAny from './ExplicitAny';
|
||||
|
||||
type AsyncReturnType<T> = T extends (
|
||||
...args: ExplicitAny[]
|
||||
) => Promise<infer Ret>
|
||||
? Ret
|
||||
: never;
|
||||
|
||||
export default AsyncReturnType;
|
||||
11
extension/source/types/ExplicitAny.ts
Normal file
11
extension/source/types/ExplicitAny.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Use this when you need to use `any` to do something meta that is beyond
|
||||
* TypeScript's ability to keep track of it. This often happens with generics.
|
||||
* Make sure you only use this type in a limited scope that exposes a well-typed
|
||||
* boundary. If you need to resort to `any` for some other reason, please just
|
||||
* use it normally.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExplicitAny = any;
|
||||
|
||||
export default ExplicitAny;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user