mirror of
https://github.com/getwax/eth-global-lisbon-hackathon.git
synced 2026-01-08 23:37:56 -05:00
Add ERC-4337 trampoline browser extension
This commit is contained in:
10
trampoline/.babelrc
Normal file
10
trampoline/.babelrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
// "react-app"
|
||||
],
|
||||
"plugins": [
|
||||
// "@babel/plugin-proposal-class-properties",
|
||||
]
|
||||
}
|
||||
6
trampoline/.eslintrc
Normal file
6
trampoline/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "react-app",
|
||||
"globals": {
|
||||
"chrome": "readonly"
|
||||
}
|
||||
}
|
||||
62
trampoline/.gitignore
vendored
Normal file
62
trampoline/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# zip
|
||||
/zip
|
||||
|
||||
# misc
|
||||
.vscode
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.history
|
||||
|
||||
# secrets
|
||||
secrets.*.js
|
||||
|
||||
yarn-error.log
|
||||
|
||||
node_modules
|
||||
.env
|
||||
coverage
|
||||
coverage.json
|
||||
typechain
|
||||
|
||||
# Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
|
||||
node_modules
|
||||
.env
|
||||
coverage
|
||||
coverage.json
|
||||
typechain
|
||||
|
||||
# Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
||||
|
||||
node_modules
|
||||
.env
|
||||
coverage
|
||||
coverage.json
|
||||
typechain
|
||||
|
||||
# Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
typechain-types
|
||||
deployments
|
||||
mnemonic.txt
|
||||
6
trampoline/.prettierrc
Normal file
6
trampoline/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"requirePragma": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
21
trampoline/LICENSE
Normal file
21
trampoline/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Michael Xieyang Liu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
281
trampoline/README.md
Executable file
281
trampoline/README.md
Executable file
@@ -0,0 +1,281 @@
|
||||
<img src="src/assets/img/logo.png" width="260"/>
|
||||
|
||||
Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets with React 18 and Webpack 5 support.
|
||||
|
||||
## Installation and Running
|
||||
|
||||
### Steps:
|
||||
|
||||
1. Verify that your [Node.js](https://nodejs.org/) version is >= **18.12.0**.
|
||||
2. Clone this repository.
|
||||
3. Make sure you configure the `provider` in `src/exconfig.ts` to the `Goerli` network.
|
||||
4. Edit the `bundler` URL pointing to `Goerli` network and accepting EntryPoint=`0x0576a174D229E3cFA37253523E645A78A0C91B57`
|
||||
5. Run `yarn install` to install the dependencies.
|
||||
6. Run `yarn start`
|
||||
7. Load your extension in Chrome by following these steps:
|
||||
1. Go to `chrome://extensions/`
|
||||
2. Enable `Developer mode`
|
||||
3. Click on `Load unpacked extension`
|
||||
4. Select the `build` folder.
|
||||
8. Happy hacking.
|
||||
|
||||
> **Warning**
|
||||
> Auto refresh is disabled by default, so you will have to manually refresh the page.
|
||||
> If you make changes in background script or account-api, you will also have to refresh the background page. Check instructions on how to do that below.
|
||||
|
||||
> **Warning**
|
||||
> Logs of all the blockchain interactions are shown in the background script. Do keep it open for faster debugging.
|
||||
|
||||
### How to see and refresh background page
|
||||
|
||||
1. Open extension's page: `chrome://extensions/`
|
||||
2. Find the Trampoline extension, and click Details.
|
||||
3. Check the `Inspect views` area and click on `background page` to inspect it's logs
|
||||
4. To refresh click `cmd + r` or `ctrl + r` in the background inspect page to refresh the background script.
|
||||
5. You can reload the extension completely too, the state is always kept in localstorage so nothing will be lost.
|
||||
|
||||
## Config
|
||||
|
||||
Config of the extension can be set in `excnfig.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
// Enable or disable password for the user.
|
||||
"enablePasswordEncryption": true,
|
||||
// Show default transaction screen
|
||||
"showTransactionConfirmationScreen": true,
|
||||
// stateVersion is the version of state stored in localstorage of your browser. If you want to reset your extension, change this number to a new version and that will invalidate the older state.
|
||||
stateVersion: '0.1',
|
||||
// Network that your SCW supports. Currently this app only supports a single network, we will soon have support for multiple networks in future
|
||||
"network": {
|
||||
"chainID": "5",
|
||||
"family": "EVM",
|
||||
"name": "Goerli",
|
||||
"provider": "https://goerli.infura.io/v3/bdabe9d2f9244005af0f566398e648da",
|
||||
"entryPointAddress": "0x0F46c65C17AA6b4102046935F33301f0510B163A",
|
||||
"bundler": "https://app.stackup.sh/api/v1/bundler/96771b1b09e802669c33a3fc50f517f0f514a40da6448e24640ecfd83263d336",
|
||||
"baseAsset": {
|
||||
"symbol": "ETH",
|
||||
"name": "ETH",
|
||||
"decimals": 18,
|
||||
"image": "https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Network
|
||||
|
||||
1. Make sure EntryPoint is deployed on the target network.
|
||||
2. Edit the `entryPointAddress` in `src/exconfig.ts`.
|
||||
3. Add your network details in `hardhat.condig.ts`.
|
||||
4. Deploy the factory using `INFURA_ID=<required> npx hardhat deploy --network <network>`.
|
||||
5. Edit the `factory_address` in `src/exconfig.ts`
|
||||
6. Edit the `bundler` url in `src/exconfig.ts` that points to your network and accepts requests for your EntryPoint.
|
||||
7. Run `yarn start`
|
||||
|
||||
### Local Network
|
||||
|
||||
1. Run a local hardhat node with `npx hardhat node` or use the node inside the bundler repo.
|
||||
2. Deploy EntryPoint from [the infinitism repo](https://github.com/eth-infinitism/account-abstraction), you can find the instructions [below](#how-to-deploy-entrypoint-locally).
|
||||
3. Edit the `entryPointAddress` in `src/exconfig.ts`.
|
||||
4. Deploy the factory using `npx hardhat deploy --network localhost`.
|
||||
5. Edit the `factory_address` in `src/exconfig.ts`
|
||||
6. Start a local bunder from [the infinitism repo](https://github.com/eth-infinitism/bundler), you can find the instructions [below](#how-to-run-bundler-locally).
|
||||
7. Edit the `bundler` to `http://localhost:9000/rpc` url in `src/exconfig.ts` that points to your network and accepts requests for your EntryPoint.
|
||||
8. Run `yarn start`
|
||||
|
||||
### How to deploy EntryPoint Locally
|
||||
|
||||
1. Clone the repo https://github.com/eth-infinitism/account-abstraction
|
||||
2. Run `yarn install` to install the dependencies.
|
||||
3. Deploy EntryPoint with `DEBUG=true MNEMONIC_FILE=<path-to-mnemonic-file> yarn deploy --network dev`
|
||||
|
||||
### How to run bundler Locally
|
||||
|
||||
1. Clone the repo https://github.com/eth-infinitism/bundler
|
||||
2. Run `yarn install` to install the dependencies.
|
||||
3. Run `yarn preprocess` to compile all the local dependencies.
|
||||
4. Edit `bundler.config.json` at `packages/bundler/localconfig`:
|
||||
a. Edit `network` to your local hardhat node
|
||||
b. Edit the `entryPoint` address that you got while deploying it using instructions above.
|
||||
c. Make sure your mnemonic & beneficiary are setup correctly.
|
||||
5. Run the bunder using `yarn bundler --unsafe --auto`
|
||||
|
||||
---
|
||||
|
||||
## Extension Structure
|
||||
|
||||
1. You can change the icons at `src/assets/img/icon-34.png` and `src/assets/img/icon-128.png` for the chrome extension.
|
||||
|
||||
## Wallet Structure
|
||||
|
||||
All your extension's account code must be placed in the `src/pages/Account` folder.
|
||||
|
||||
There are two subfolders in `src/pages/Account`:
|
||||
|
||||
- account-api
|
||||
- components
|
||||
|
||||
### account-api folder
|
||||
|
||||
This folder is used to define the `AccountAPI` of your specific account implementation. Every implementation must implement `AccountApiType`.
|
||||
|
||||
```typescript
|
||||
export abstract class AccountApiType extends BaseAccountAPI {
|
||||
abstract serialize: () => Promise<object>;
|
||||
|
||||
/** sign a message for the user */
|
||||
abstract signMessage: (
|
||||
request?: MessageSigningRequest,
|
||||
context?: any
|
||||
) => Promise<string>;
|
||||
|
||||
abstract signUserOpWithContext(
|
||||
userOp: UserOperationStruct,
|
||||
context?: any
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
export declare abstract class BaseAccountAPI {
|
||||
/**
|
||||
* return the value to put into the "initCode" field, if the contract is not yet deployed.
|
||||
* this value holds the "factory" address, followed by this account's information
|
||||
*/
|
||||
abstract getAccountInitCode(): Promise<string>;
|
||||
/**
|
||||
* return current account's nonce.
|
||||
*/
|
||||
abstract getNonce(): Promise<BigNumber>;
|
||||
/**
|
||||
* encode the call from entryPoint through our account to the target contract.
|
||||
* @param target
|
||||
* @param value
|
||||
* @param data
|
||||
*/
|
||||
abstract encodeExecute(
|
||||
target: string,
|
||||
value: BigNumberish,
|
||||
data: string
|
||||
): Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
The boilerplate includes a SimpleAccount Implementation by Eth-Infinitism, which you can find [here](https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/SimpleAccountAPI.ts).
|
||||
|
||||
### components folder
|
||||
|
||||
This folder is used to define the components that will be used in the Chrome extension. This folder should contain two subfolders.
|
||||
|
||||
- onboarding
|
||||
- sign-message
|
||||
- transaction
|
||||
|
||||
The `onboarding` folder defines the component that will be displayed to the user during the creation of a new wallet. You can display custom information or collect user inputs if needed.
|
||||
|
||||
The signature of the `OnboardingComponent` is defined as follows.
|
||||
|
||||
```typescript
|
||||
export interface OnboardingComponentProps {
|
||||
onOnboardingComplete: (context?: any) => void;
|
||||
}
|
||||
|
||||
export interface OnboardingComponent
|
||||
extends React.FC<OnboardingComponentProps> {}
|
||||
```
|
||||
|
||||
Once the component has collected enough information from the user, it should pass the collected information to `onOnboardingComplete` as the `context` parameter. This `context` will be passed on to your `account-api`
|
||||
|
||||
The signature of the `account-api` is as follows, which shows how the `context` will be passed:
|
||||
|
||||
```typescript
|
||||
export interface AccountApiParamsType extends BaseApiParams {
|
||||
context?: any;
|
||||
}
|
||||
|
||||
export type AccountImplementationType = new (
|
||||
params: AccountApiParamsType
|
||||
) => AccountApiType;
|
||||
```
|
||||
|
||||
The `sign-message` folder defines the component that will be displayed to the user whenever the dapp requests the user to sign any message, i.e. dapp calls `personal_sign` RPC method. You can display custom information or collect user inputs if needed.
|
||||
|
||||
The signature of the `SignMessageComponenet` is defined as follows.
|
||||
|
||||
```typescript
|
||||
export interface SignMessageComponenetProps {
|
||||
onComplete: (context?: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface SignMessageComponenet
|
||||
extends React.FC<SignMessageComponenetProps> {}
|
||||
```
|
||||
|
||||
Once the component has collected enough information from the user, it should pass the collected information to `onComplete` as the `context` parameter. This `context` will be passed on to your `signMessage` function of `account-api`
|
||||
|
||||
The signature of the `signMessage` is as follows, which shows how the `context` will be passed:
|
||||
|
||||
```typescript
|
||||
/** sign a message for the user */
|
||||
abstract signMessage: (
|
||||
request?: MessageSigningRequest,
|
||||
context?: any
|
||||
) => Promise<string>;
|
||||
```
|
||||
|
||||
The `transaction` folder defines the component that will be displayed to the user whenever the dapp requests to initiate a transaction, i.e. dapp calls `eth_sendTransaction` RPC method. You can display custom information or collect user inputs if needed.
|
||||
|
||||
The signature of the `TransactionComponent` is defined as follows.
|
||||
|
||||
```typescript
|
||||
export interface TransactionComponentProps {
|
||||
transaction: EthersTransactionRequest;
|
||||
onComplete: (
|
||||
modifiedTransaction: EthersTransactionRequest,
|
||||
context?: any
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TransactionComponent
|
||||
extends React.FC<TransactionComponentProps> {}
|
||||
```
|
||||
|
||||
Once the component has collected enough information from the user, it should pass the collected information to `onComplete` as the `context` parameter. You can also modify the transaction if you want and return it also as a parameter of `onComplete` function. This `context` and `modifiedTransaction` will be passed on to your `createUnsignedUserOp` function of `account-api`
|
||||
|
||||
The signature of the `createUnsignedUserOp` is as follows, which shows how the `context` will be passed:
|
||||
|
||||
```typescript
|
||||
/** sign a message for the user */
|
||||
abstract createUnsignedUserOp: (
|
||||
request?: MessageSigningRequest,
|
||||
context?: any
|
||||
) => Promise<string>;
|
||||
```
|
||||
|
||||
If you want you can also attach a paymaster here if your wallet wants to sponsor the transaction as well. The paymaster information will be displayed to the user.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Is the password screen mandatory?
|
||||
|
||||
No you can disable that by setting `enablePasswordEncryption` flag to `false` in `exconfig.ts`.
|
||||
|
||||
> **Warning:** the local storage will be unencrypted and your wallet must return an encrypted state when `serialize` function of `account-api` willo be called or else the user's fund will be at risk.
|
||||
|
||||
### Is the view transaction screen mandatory?
|
||||
|
||||
If you want to show a custom screen then you must present it to the user in `TransactionComponent` and set `showTransactionConfirmationScreen` to `false`.
|
||||
|
||||
### How do I, as a wallet provider attach a custom paymaster?
|
||||
|
||||
You must return the paymaster information in the `userOp` constructed by the function `createUnsignedUserOp`.
|
||||
|
||||
> **Warning:** If `showTransactionConfirmationScreen` has been disabled then the user will not be aware of paymaster and you must inform the user about paymaster in your custom transaction confirmation screen.
|
||||
|
||||
## Webpack auto-reload and HRM Errors
|
||||
|
||||
This repository is based on the boilerplate code found at [lxieyang/chrome-extension-boilerplate-react](https://github.com/lxieyang/chrome-extension-boilerplate-react). To understand how hot-reloading and content scripts work, refer to its README.
|
||||
|
||||
### LOGO Attributions
|
||||
|
||||
Designed by Tomo Saito, a designer and artist at the Ethereum Foundation. [@tomosaito](https://twitter.com/tomosaito)
|
||||
18
trampoline/contracts/Greeter.sol
Normal file
18
trampoline/contracts/Greeter.sol
Normal file
@@ -0,0 +1,18 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract Greeter {
|
||||
string greeting;
|
||||
|
||||
constructor(string memory _greeting) {
|
||||
greeting = _greeting;
|
||||
}
|
||||
|
||||
function greet() public view returns (string memory) {
|
||||
return greeting;
|
||||
}
|
||||
|
||||
function setGreeting(string memory _greeting) public {
|
||||
greeting = _greeting;
|
||||
}
|
||||
}
|
||||
13
trampoline/deploy/deploy.ts
Normal file
13
trampoline/deploy/deploy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { HardhatRuntimeEnvironment } from 'hardhat/types';
|
||||
import { DeployFunction } from 'hardhat-deploy/types';
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const accounts = await hre.getUnnamedAccounts();
|
||||
await hre.deployments.deploy('Greeter', {
|
||||
from: accounts[0],
|
||||
deterministicDeployment: true,
|
||||
args: ['Test'],
|
||||
log: true,
|
||||
});
|
||||
};
|
||||
export default func;
|
||||
51
trampoline/hardhat.config.ts
Normal file
51
trampoline/hardhat.config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// import '@nomiclabs/hardhat-waffle';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import 'hardhat-deploy';
|
||||
import '@typechain/hardhat';
|
||||
import { HardhatUserConfig } from 'hardhat/types';
|
||||
import * as fs from 'fs';
|
||||
import '@nomiclabs/hardhat-etherscan';
|
||||
|
||||
const mnemonicFileName =
|
||||
process.env.MNEMONIC_FILE ??
|
||||
`${process.env.HOME}/.secret/testnet-mnemonic.txt`;
|
||||
let mnemonic = 'test '.repeat(11) + 'junk';
|
||||
if (fs.existsSync(mnemonicFileName)) {
|
||||
mnemonic = fs.readFileSync(mnemonicFileName, 'ascii');
|
||||
}
|
||||
|
||||
function getNetwork1(url: string): {
|
||||
url: string;
|
||||
accounts: { mnemonic: string };
|
||||
} {
|
||||
return {
|
||||
url,
|
||||
accounts: { mnemonic },
|
||||
};
|
||||
}
|
||||
|
||||
function getNetwork(name: string): {
|
||||
url: string;
|
||||
accounts: { mnemonic: string };
|
||||
} {
|
||||
return getNetwork1(`https://${name}.infura.io/v3/${process.env.INFURA_ID}`);
|
||||
// return getNetwork1(`wss://${name}.infura.io/ws/v3/${process.env.INFURA_ID}`)
|
||||
}
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
defaultNetwork: 'hardhat',
|
||||
solidity: {
|
||||
compilers: [{ version: '0.8.12', settings: {} }, { version: '0.5.0' }],
|
||||
},
|
||||
typechain: {
|
||||
outDir: 'src/pages/Account/account-api/typechain-types',
|
||||
},
|
||||
networks: {
|
||||
goerli: getNetwork('goerli'),
|
||||
},
|
||||
etherscan: {
|
||||
apiKey: process.env.ETHERSCAN_API_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
116
trampoline/package.json
Executable file
116
trampoline/package.json
Executable file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"name": "trampoline",
|
||||
"version": "5.0.4",
|
||||
"description": "Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lxieyang/chrome-extension-boilerplate-react.git"
|
||||
},
|
||||
"scripts": {
|
||||
"server": "node utils/webserver.js",
|
||||
"start": "QUIET=false NODE_ENV=development run-p devtools \"dev {@}\" --",
|
||||
"dev": "npx hardhat clean && npx hardhat compile && webpack --config webpack.config.js --watch",
|
||||
"hardhat-clean-comile": "npx hardhat clean && npx hardhat compile",
|
||||
"devtools": "npx redux-devtools --hostname=localhost --port=8000 --open",
|
||||
"build": "npx hardhat clean && npx hardhat compile && node utils/build.js",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@account-abstraction/contracts": "^0.6.0",
|
||||
"@account-abstraction/sdk": "^0.6.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@metamask/browser-passworder": "^4.0.2",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/material": "^5.11.6",
|
||||
"@nomicfoundation/hardhat-toolbox": "^2.0.1",
|
||||
"@peculiar/asn1-ecc": "^2.3.4",
|
||||
"@peculiar/asn1-schema": "^2.3.3",
|
||||
"@redux-devtools/cli": "^2.0.0",
|
||||
"@redux-devtools/remote": "^0.8.0",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@simplewebauthn/typescript-types": "^7.0.0",
|
||||
"base64url": "^3.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"daisyui": "^2.49.0",
|
||||
"elliptic": "^6.5.4",
|
||||
"emittery": "^1.0.1",
|
||||
"ethers": "^5.7.2",
|
||||
"extensionizer": "^1.0.1",
|
||||
"magic-sdk": "^13.4.0",
|
||||
"node-polyfill-webpack-plugin": "^2.0.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"redux-devtools": "^3.7.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"siwe": "^1.1.6",
|
||||
"ssestream": "^1.1.0",
|
||||
"uuid": "^9.0.0",
|
||||
"wagmi": "^0.11.3",
|
||||
"webext-redux": "^2.1.9",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"webpack-livereload-plugin": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "^1.0.6",
|
||||
"@nomicfoundation/hardhat-network-helpers": "^1.0.8",
|
||||
"@nomiclabs/hardhat-ethers": "^2.2.2",
|
||||
"@nomiclabs/hardhat-etherscan": "^3.1.6",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@typechain/ethers-v5": "^10.2.0",
|
||||
"@typechain/hardhat": "^6.1.5",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/chrome": "^0.0.212",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"hardhat": "^2.12.7",
|
||||
"hardhat-deploy": "^0.11.24",
|
||||
"hardhat-gas-reporter": "^1.0.9",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.3",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.7",
|
||||
"sass": "^1.57.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"solidity-coverage": "^0.8.2",
|
||||
"source-map-loader": "^3.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"ts-loader": "^9.4.2",
|
||||
"type-fest": "^3.5.2",
|
||||
"typechain": "^8.1.1",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"zip-webpack-plugin": "^4.0.1"
|
||||
}
|
||||
}
|
||||
22
trampoline/scripts/localSendMoney.ts
Normal file
22
trampoline/scripts/localSendMoney.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// We require the Hardhat Runtime Environment explicitly here. This is optional
|
||||
// but useful for running the script in a standalone fashion through `node <script>`.
|
||||
//
|
||||
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
|
||||
// will compile your contracts, add the Hardhat Runtime Environment's members to the
|
||||
// global scope, and execute the script.
|
||||
import { ethers } from 'hardhat';
|
||||
|
||||
async function main() {
|
||||
const [signer] = await ethers.getSigners();
|
||||
signer.sendTransaction({
|
||||
to: '0x0A77cEdFB8459084aB81CF6786EadDaCf7974146',
|
||||
value: ethers.utils.parseEther('1'),
|
||||
});
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
BIN
trampoline/src/assets/img/dapp_favicon_default@2x.png
Normal file
BIN
trampoline/src/assets/img/dapp_favicon_default@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
trampoline/src/assets/img/icon-128.png
Normal file
BIN
trampoline/src/assets/img/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
trampoline/src/assets/img/icon-34.png
Normal file
BIN
trampoline/src/assets/img/icon-34.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
trampoline/src/assets/img/logo.png
Normal file
BIN
trampoline/src/assets/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
230
trampoline/src/assets/img/logo.svg
Normal file
230
trampoline/src/assets/img/logo.svg
Normal file
@@ -0,0 +1,230 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 599.124 142.705">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f89f72;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #f58b77;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fab06c;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #faad6c;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #f9a66f;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #677bbc;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #f69175;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: #f79874;
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: #fdd0b2;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: #fbb46b;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: #fcb868;
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: #fdbe68;
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: #fdc068;
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #fcf8c8;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #8dcb8e;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #8eacb1;
|
||||
}
|
||||
|
||||
.cls-17, .cls-18 {
|
||||
fill: #ceebed;
|
||||
}
|
||||
|
||||
.cls-19 {
|
||||
fill: #b082ba;
|
||||
}
|
||||
|
||||
.cls-20 {
|
||||
fill: #c3a1cb;
|
||||
}
|
||||
|
||||
.cls-21, .cls-22 {
|
||||
fill: #37374d;
|
||||
}
|
||||
|
||||
.cls-23 {
|
||||
fill: #54bc7b;
|
||||
}
|
||||
|
||||
.cls-22 {
|
||||
stroke: #37374d;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: .2px;
|
||||
}
|
||||
|
||||
.cls-18 {
|
||||
opacity: .75;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-15" d="m60.839,115.602l-2.2,21.02c-.17,1.63-1.12,2.92-2.33,3.34-.27.09-.55.14-.84.14-1.59,0-2.95-1.49-3.16-3.48l-1.86-17.74-.34-3.28c-.1-.89.06-1.79.42-2.55.16-.35.38-.68.63-.96.59-.65,1.34-1.01,2.11-1.01h4.4c.78,0,1.53.36,2.11,1.01.26.28.47.61.63.96.37.76.52,1.66.43,2.55Z"/>
|
||||
<path class="cls-15" d="m28.789,99.102l-4.82,6.42-16.08,21.39c-.55.73-1.43,1.17-2.34,1.17-1.06,0-2.01-.54-2.54-1.45-.53-.92-.54-2.01-.02-2.93l8.68-15.37,3.69-6.53,3.34-5.91c.52-.92,1.5-1.5,2.56-1.5h5.19c1.13,0,2.12.61,2.62,1.63.51,1.02.41,2.17-.28,3.08Z"/>
|
||||
<path class="cls-15" d="m108.059,126.632c-.06.11-.13.21-.22.31v.01c-.55.72-1.39,1.13-2.31,1.13s-1.8-.44-2.35-1.17l-10.89-14.5-5.48-7.29-4.53-6.02c-.68-.91-.79-2.06-.28-3.08.51-1.02,1.49-1.63,2.63-1.63h5.19c1.06,0,2.03.58,2.56,1.5l3.16,5.59,12.55,22.22c.52.92.51,2.01-.03,2.93Z"/>
|
||||
<g>
|
||||
<path class="cls-21" d="m57.674,110.386h-4.401c-.979,0-1.912.438-2.629,1.234-.927,1.029-1.387,2.546-1.228,4.058l2.2,21.018c.245,2.342,1.903,4.108,3.857,4.108s3.611-1.766,3.856-4.108l2.2-21.018c.159-1.512-.301-3.029-1.228-4.058-.717-.796-1.65-1.234-2.629-1.234Zm3.161,5.219l-2.2,21.018c-.209,1.985-1.567,3.481-3.161,3.481s-2.953-1.497-3.162-3.481l-2.2-21.018c-.138-1.317.256-2.632,1.053-3.517.582-.646,1.331-1.002,2.108-1.002h4.401c.777,0,1.526.356,2.108,1.002.797.885,1.19,2.199,1.053,3.517Z"/>
|
||||
<path class="cls-21" d="m26.445,93.697h-5.189c-1.31,0-2.523.708-3.167,1.848l-15.71,27.814c-.644,1.139-.634,2.493.025,3.622.659,1.13,1.834,1.805,3.142,1.805,1.138,0,2.225-.543,2.908-1.453l20.898-27.813c.834-1.109.967-2.569.348-3.811-.62-1.241-1.867-2.012-3.255-2.012Zm-20.899,34.389c-1.057,0-2.005-.544-2.537-1.457-.532-.912-.54-2.006-.021-2.925l15.71-27.814c.521-.921,1.5-1.493,2.558-1.493h5.189c1.138,0,2.12.607,2.628,1.625.509,1.018.403,2.167-.28,3.077l-20.899,27.813c-.552.735-1.43,1.173-2.348,1.173Z"/>
|
||||
<path class="cls-21" d="m108.695,123.359l-15.711-27.814c-.644-1.14-1.857-1.848-3.167-1.848h-5.189c-1.387,0-2.634.771-3.254,2.012-.62,1.241-.487,2.701.346,3.811l20.899,27.813c.684.91,1.771,1.453,2.909,1.453,1.308,0,2.481-.674,3.141-1.804.66-1.129.67-2.484.026-3.623Zm-3.167,4.727c-.919,0-1.797-.438-2.349-1.173l-20.899-27.813c-.684-.909-.788-2.06-.28-3.077.509-1.017,1.491-1.625,2.628-1.625h5.189c1.058,0,2.037.572,2.558,1.493l15.711,27.813c.52.92.512,2.014-.021,2.926-.532.912-1.48,1.457-2.536,1.457Z"/>
|
||||
</g>
|
||||
<path class="cls-23" d="m23.969,105.522l-16.08,21.39c-.55.73-1.43,1.17-2.34,1.17l10.93-16.84c-3.172-1.631-4.659-3.177-4.659-3.177l3.539-6.263,8.61,3.72Z"/>
|
||||
<path class="cls-23" d="m60.839,115.602l-2.2,21.02c-.17,1.63-1.12,2.92-2.33,3.34l1.079-20.913c-4.33.25-6.939-.167-6.939-.167l-.34-3.28c-.1-.89.06-1.79.42-2.55h9.88c.37.76.52,1.66.43,2.55Z"/>
|
||||
<path class="cls-23" d="m108.059,126.632c-.06.11-.13.21-.22.31l-10.77-17.16c-2.69,1.74-4.78,2.63-4.78,2.63l-5.48-7.29,8.73-3.64,12.55,22.22c.52.92.51,2.01-.03,2.93Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-20" d="m84.332,81.154s0,0,0,0c-.182-.064-.366-.127-.551-.189h.001c.184.062.368.125.549.189Z"/>
|
||||
<path class="cls-20" d="m82.923,80.686s0,0,0,0c-.186-.058-.375-.114-.564-.169,0,0,0,0,.001,0,.188.056.376.111.562.169Z"/>
|
||||
<path class="cls-20" d="m79.934,79.863h0c-.186-.045-.374-.087-.561-.131h.002c.186.044.374.086.558.131Z"/>
|
||||
<path class="cls-20" d="m81.453,80.256h0c-.187-.051-.377-.1-.566-.149,0,0,.001,0,.002,0,.188.049.377.098.563.149Z"/>
|
||||
<path class="cls-20" d="m86.931,82.215s0,0,0,0c-.169-.08-.338-.158-.512-.235,0,0,0,0,0,0,.173.077.342.155.51.235Z"/>
|
||||
<path class="cls-20" d="m89.159,83.453s0,0,.001,0c-.147-.097-.292-.195-.447-.289,0,0,0,0,0,0,.154.094.299.191.446.288Z"/>
|
||||
<path class="cls-20" d="m85.673,81.663s0,0,0,0c-.177-.072-.355-.142-.536-.212h0c.18.07.358.14.534.212Z"/>
|
||||
<path class="cls-20" d="m88.095,82.81s0,0,.001,0c-.159-.088-.317-.175-.482-.26h0c.164.085.322.172.48.26Z"/>
|
||||
<path class="cls-20" d="m78.377,79.505s0,0,0,0c-.177-.038-.357-.074-.536-.111.001,0,.002,0,.003,0,.177.037.356.072.532.11Z"/>
|
||||
<path class="cls-20" d="m76.79,79.18s0,0,0,0c-.152-.029-.305-.056-.457-.084.002,0,.004,0,.006.001.15.028.301.054.451.083Z"/>
|
||||
<path class="cls-20" d="m66.764,77.794s.001,0,.002,0c-.38-.036-.755-.07-1.125-.101,0,0,0,0,0,0,.369.032.743.066,1.122.101Z"/>
|
||||
<path class="cls-20" d="m65.327,77.667s0,0,.001,0c-.407-.034-.806-.066-1.197-.095,0,0,0,0,0,0,.391.029.789.061,1.195.095Z"/>
|
||||
<path class="cls-20" d="m63.945,77.558s0,0,.001,0c-.441-.032-.87-.062-1.288-.089,0,0,0,0,0,0,.417.027.846.057,1.286.089Z"/>
|
||||
<path class="cls-20" d="m62.644,77.468c-3.622-.234-6.333-.278-7.102-.278h0c.769,0,3.48.044,7.102.278,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-20" d="m72.653,78.489s.008.001.012.002c-.214-.031-.429-.064-.642-.094,0,0,0,0,0,0,.209.029.42.061.63.092Z"/>
|
||||
<path class="cls-20" d="m90.107,84.146s0,0,.001.001c-.134-.108-.264-.217-.407-.322,0,0,0,0,0,0,.142.104.272.213.405.321Z"/>
|
||||
<path class="cls-20" d="m68.25,77.942s.001,0,.002,0c-.355-.037-.708-.074-1.056-.108,0,0,0,0,0,0,.347.034.699.07,1.053.108Z"/>
|
||||
<path class="cls-20" d="m71.275,78.3s.003,0,.005,0c-.296-.039-.592-.079-.885-.115,0,0,0,0,0,0,.292.036.586.076.88.114Z"/>
|
||||
<path class="cls-20" d="m69.763,78.111s.002,0,.003,0c-.33-.039-.66-.078-.986-.114,0,0,0,0,0,0,.325.036.653.074.982.113Z"/>
|
||||
<path class="cls-20" d="m92.232,86.718s.003.006.005.009c-.099-.214-.2-.427-.328-.631,0,0,0,0,0,0,.126.201.225.411.323.622Z"/>
|
||||
<path class="cls-20" d="m75.168,78.884c.007.001.014.002.022.003-.007-.001-.014-.002-.022-.003Z"/>
|
||||
<path class="cls-20" d="m91.627,85.709s.001.002.002.003c-.104-.142-.2-.286-.318-.423,0,0,0,0,0,0,.116.136.212.279.315.42Z"/>
|
||||
<path class="cls-20" d="m19.872,101.704c10.47,4.84,26.74,6.156,35.67,6.156s20.538-.695,34.352-5.509c16.861-5.876,11.989-16.6,11.898-17.061h0v-.01c-9.82-13.47-43.66-13.47-46.25-13.47-3.03,0-48.92,0-48.92,21.62,0,3.9,1.59,7.1,4.22,9.72,11.84,11.9,44.7,11.9,44.7,11.9h0s-32.86,0-44.7-11.9c2.4-2.57,4.82-3.386,9.03-1.446Zm35.72-26.184s36.752,0,36.76,11.426c.251.612.385,1.262.385,1.953,0,1.632-1.138,2.997-3.003,4.141-8.374,5.147-31.615,5.799-34.192,5.799-1.18,0-6.689-.139-13.117-.779-.028-.003-.057-.006-.086-.009-.495-.05-.994-.102-1.498-.158-.047-.005-.093-.01-.14-.015-.526-.059-1.055-.12-1.588-.186-.004,0-.008,0-.013-.002-1.107-.137-2.223-.289-3.333-.459-.007-.001-.015-.002-.022-.003-.52-.08-1.037-.164-1.552-.251-.075-.013-.149-.026-.223-.039-.461-.079-.919-.162-1.374-.248-.079-.015-.158-.03-.237-.045-.487-.094-.969-.191-1.446-.292-.029-.006-.059-.012-.088-.018-1.049-.225-2.068-.47-3.043-.736-.009-.003-.018-.005-.027-.008-.456-.125-.899-.255-1.333-.389-.067-.021-.132-.042-.198-.063-.385-.121-.759-.246-1.124-.375-.068-.024-.136-.048-.203-.072-.384-.139-.756-.282-1.114-.43-.03-.012-.062-.024-.091-.037-.795-.333-1.522-.688-2.167-1.07-.023-.014-.044-.028-.066-.042-.205-.123-.41-.246-.597-.374,0,0,0-.01-.01-.01,0,0,0,0,0,0-.361-.15-4.37-2.61-4.37-4.71,0-2.27,3.85-12.5,39.11-12.5Z"/>
|
||||
<path class="cls-20" d="m90.931,84.892s.001,0,.002.002c-.119-.121-.232-.244-.361-.362,0,0,0,0,0,0,.129.117.241.24.359.36Z"/>
|
||||
<path class="cls-6" d="m92.031,88.9c0-.288-.028-.571-.081-.848-.029-.144-.08-.282-.123-.423-.034-.116-.06-.234-.104-.347-3.026-7.614-27.312-9.39-36.183-9.39-9.468,0-36.496,2.022-36.496,11.007,0,.207.027.409.072.607.008.035.019.07.029.105.044.165.103.327.177.486.017.037.033.074.052.111.137.268.326.525.542.777.135.157.282.314.453.47.059.054.123.107.185.16.172.147.36.292.562.437.123.087.244.175.376.26.065.042.133.083.201.125.225.137.458.273.707.405.016.009.033.018.049.026,2.645,1.385,6.584,2.446,10.908,3.242.169.031.337.062.507.092.094.017.19.032.284.049,8.009,1.379,16.922,1.828,20.646,1.883.255.003.509.005.745.005,7.003,0,22.791-1.131,31.119-4.384,3.257-1.273,5.372-2.871,5.372-4.854Z"/>
|
||||
<path class="cls-6" d="m91.724,87.282c.044.113.069.231.104.347-.035-.115-.059-.234-.104-.347Z"/>
|
||||
<path class="cls-6" d="m54.795,98.133c-3.724-.055-12.637-.504-20.646-1.883,7.4,1.294,15.743,1.833,20.646,1.883Z"/>
|
||||
<path class="cls-6" d="m19.146,89.612c-.009-.035-.021-.07-.029-.105.008.035.019.07.029.105Z"/>
|
||||
<path class="cls-6" d="m21.693,92.436c-.067-.041-.135-.083-.201-.125.066.042.133.083.201.125Z"/>
|
||||
<path class="cls-6" d="m22.449,92.867c-.017-.009-.033-.018-.049-.026.016.009.033.018.049.026Z"/>
|
||||
<path class="cls-6" d="m33.865,96.201c-.17-.03-.338-.062-.507-.092.169.031.337.062.507.092Z"/>
|
||||
<path class="cls-6" d="m92.036,88.899c0-.29-.031-.572-.085-.847.053.277.081.56.081.848,0,1.982-2.115,3.581-5.372,4.854,3.26-1.273,5.376-2.872,5.376-4.855Z"/>
|
||||
<path class="cls-6" d="m19.374,90.208c-.019-.037-.034-.074-.052-.111.017.037.033.074.052.111Z"/>
|
||||
<path class="cls-6" d="m20.369,91.454c-.17-.156-.318-.313-.453-.47.137.159.285.316.453.47Z"/>
|
||||
<path class="cls-6" d="m21.116,92.051c-.202-.144-.39-.29-.562-.437.174.148.361.294.562.437Z"/>
|
||||
<g>
|
||||
<path class="cls-21" d="m102.403,84.941c-.014-.025-.029-.05-.046-.073-9.546-13.094-40.673-13.758-46.816-13.758-3.243,0-14.684.196-25.752,2.72-15.615,3.561-23.868,10.338-23.868,19.6,0,3.853,1.489,7.29,4.426,10.216,5.016,5.041,14.161,8.611,27.178,10.608,9.636,1.479,17.933,1.496,18.016,1.496.123,0,12.445-.031,24.606-2.719,16.358-3.615,25.004-10.393,25.004-19.601,0-3.124-.925-5.98-2.749-8.489Zm-.612.339v.01c1.69,2.3,2.66,4.99,2.66,8.14,0,21.62-48.91,21.62-48.91,21.62,0,0-32.86,0-44.7-11.9-2.63-2.62-4.22-5.82-4.22-9.72,0-21.62,45.89-21.62,48.92-21.62,2.59,0,36.43,0,46.25,13.47Z"/>
|
||||
<path class="cls-21" d="m18.483,89.826c.054.195.125.386.212.573.025.053.05.106.077.159.134.257.288.51.48.752h.01c.073.091.172.174.253.262.122.133.24.267.38.394.12.11.257.215.389.322.144.115.285.231.442.343.045.032.081.067.127.099.01,0,.01.01.01.01.187.128.392.251.597.374.023.014.043.028.066.042.645.381,1.372.737,2.167,1.07.03.013.061.024.091.037.358.148.73.291,1.114.43.067.024.135.048.203.072.365.129.739.254,1.124.375.066.021.132.042.198.063.434.134.878.264,1.333.389.009.003.018.005.027.008.974.266,1.994.511,3.043.736.029.006.059.012.088.018.477.102.959.199,1.446.292.079.015.158.03.237.045.455.086.913.169,1.374.248.074.013.149.026.223.039.515.087,1.033.172,1.552.251.007.001.015.002.022.003,1.11.17,2.227.323,3.333.459.004,0,.008.001.013.002.532.066,1.062.127,1.588.186.047.005.093.01.14.015.503.056,1.003.108,1.498.158.028.003.057.006.086.009,6.429.64,11.937.779,13.117.779,2.577,0,25.818-.652,34.192-5.799,1.865-1.145,3.003-2.509,3.003-4.141,0-.691-.134-1.341-.385-1.953,0,.001,0,.002,0,.004-.031-.077-.079-.148-.115-.223-.001-.003-.003-.006-.005-.009-.098-.21-.197-.421-.323-.622,0,0,0,0,0,0-.055-.089-.123-.173-.188-.258-.032-.042-.062-.086-.094-.128-.103-.141-.199-.284-.315-.42,0,0,0,0,0,0-.116-.135-.25-.264-.378-.395,0,0,0-.001-.002-.002-.118-.121-.231-.243-.359-.36,0,0,0,0,0,0-.119-.108-.25-.211-.38-.316-.029-.023-.056-.047-.085-.071-.133-.108-.263-.217-.405-.321-.105-.077-.219-.151-.33-.225-.072-.048-.14-.097-.212-.145,0,0,0,0-.001,0-.147-.097-.292-.194-.446-.288,0,0,0,0,0,0-.198-.121-.406-.238-.616-.354,0,0,0,0-.001,0-.158-.087-.316-.175-.48-.26h0c-.222-.115-.451-.226-.683-.335,0,0,0,0,0,0-.168-.079-.337-.158-.51-.235-.025-.011-.05-.021-.075-.032-.22-.097-.443-.192-.671-.285,0,0,0,0,0,0-.176-.072-.354-.142-.534-.211h0c-.264-.102-.532-.201-.805-.298,0,0,0,0,0,0-.181-.064-.364-.126-.549-.188h-.001c-.17-.057-.343-.112-.515-.168-.115-.037-.228-.076-.344-.112-.185-.058-.373-.113-.562-.169,0,0,0,0-.001,0-.3-.089-.601-.177-.907-.261h0c-.186-.051-.375-.1-.563-.149-.288-.075-.577-.15-.869-.222-.029-.007-.057-.015-.086-.022h0c-.185-.045-.372-.087-.558-.131h-.002c-.331-.078-.661-.155-.997-.227,0,0,0,0,0,0-.176-.038-.354-.073-.532-.11,0,0-.002,0-.003,0-.349-.073-.698-.146-1.051-.214,0,0,0,0,0,0-.15-.029-.301-.055-.451-.083-.002,0-.004,0-.006-.001-.38-.071-.761-.142-1.143-.208-.007-.001-.014-.002-.022-.003-.834-.144-1.67-.272-2.503-.393-.004,0-.008-.001-.012-.002-.21-.03-.421-.063-.63-.092,0,0,0,0,0,0-.144-.02-.287-.038-.431-.057-.105-.014-.211-.028-.317-.042-.294-.039-.588-.078-.88-.114-.046-.006-.09-.011-.136-.016-.165-.02-.329-.039-.494-.058,0,0-.002,0-.003,0-.329-.039-.657-.078-.982-.113-.011-.001-.021-.002-.032-.003-.167-.018-.331-.035-.497-.052,0,0-.001,0-.002,0-.354-.037-.706-.074-1.053-.108,0,0,0,0,0,0-.145-.014-.287-.027-.431-.04,0,0-.001,0-.002,0-.379-.036-.754-.07-1.122-.101,0,0,0,0,0,0-.105-.009-.208-.017-.313-.026,0,0,0,0-.001,0-.406-.034-.804-.066-1.195-.095,0,0,0,0,0,0-.062-.005-.123-.009-.185-.014,0,0,0,0-.001,0-.44-.032-.869-.062-1.286-.089,0,0,0,0,0,0-.001,0-.002,0-.004,0-.004,0-.008,0-.011,0-3.621-.234-6.332-.278-7.102-.278-1.331,0-8.456.127-16.118,1.159-6.749.909-13.914,2.519-17.911,5.37-1.97,1.405-3.17,3.111-3.17,5.181,0,.262.038.515.094.763.013.055.032.109.047.164Zm68.177,3.928c-8.328,3.253-24.116,4.384-31.119,4.384-.236,0-.49-.002-.745-.005-4.903-.05-13.245-.589-20.646-1.883-.094-.017-.19-.032-.284-.049-.17-.03-.338-.061-.507-.092-4.324-.796-8.263-1.857-10.908-3.242-.017-.009-.033-.018-.049-.026-.249-.132-.482-.267-.707-.405-.068-.041-.135-.083-.201-.125-.133-.085-.253-.173-.376-.26-.201-.143-.388-.288-.562-.437-.062-.053-.127-.106-.185-.16-.167-.153-.315-.31-.453-.47-.102-.119-.196-.239-.284-.36-.092-.138-.187-.277-.258-.416-.019-.037-.034-.074-.052-.111-.074-.159-.133-.321-.177-.486-.009-.035-.021-.07-.029-.105-.045-.199-.072-.401-.072-.607,0-8.985,27.028-11.007,36.496-11.007,8.871,0,33.157,1.776,36.183,9.39.045.113.068.231.104.347.043.141.095.279.123.423.055.275.085.557.085.847,0,1.983-2.117,3.581-5.376,4.855Z"/>
|
||||
</g>
|
||||
<path class="cls-19" d="m104.452,93.43c0-3.15-.97-5.84-2.66-8.14.091.461,4.963,11.185-11.898,17.061-13.814,4.814-25.432,5.509-34.352,5.509s-25.2-1.316-35.67-6.156c-4.21-1.94-6.63-1.124-9.03,1.446,11.84,11.9,44.7,11.9,44.7,11.9.002,0,48.91,0,48.91-21.62Z"/>
|
||||
<path class="cls-19" d="m101.791,85.29h0c1.69,2.3,2.66,4.99,2.66,8.14,0,21.62-48.908,21.62-48.91,21.62h0s48.91,0,48.91-21.62c0-3.15-.97-5.84-2.66-8.14Z"/>
|
||||
<path class="cls-19" d="m20.851,92.73c-.046-.032-.082-.067-.127-.099-.157-.111-.298-.227-.44-.342-.133-.107-.27-.212-.39-.323-.139-.128-.257-.261-.378-.393-.082-.089-.18-.172-.254-.264h-.01c-.192-.242-.345-.494-.48-.752-.027-.052-.052-.105-.077-.158-.087-.187-.158-.379-.212-.574-.015-.054-.034-.108-.047-.162-.056-.248-.094-.501-.094-.763,0-2.071,1.2-3.776,3.171-5.181,3.987-2.845,11.13-4.454,17.865-5.364,7.68-1.038,14.831-1.165,16.164-1.165h0c.769,0,3.481.044,7.102.278.005,0,.009,0,.014,0,.418.027.847.057,1.288.089.062.005.123.009.185.014.391.029.79.061,1.197.095.104.009.207.017.313.026.37.032.745.066,1.125.101.144.013.286.026.431.04.348.034.701.07,1.056.108.177.018.35.036.528.055.326.036.656.075.986.114.21.025.418.048.629.074.293.036.589.076.885.115.248.032.494.063.743.097.214.03.428.063.642.094.833.12,1.669.249,2.503.393.007.001.014.002.022.003.382.066.763.137,1.143.208.152.028.305.055.457.084.353.068.701.141,1.051.214.179.037.358.073.536.111.335.073.666.15.997.227.187.044.375.086.561.131.321.078.637.161.952.243.189.05.379.098.566.15.307.084.607.172.907.261.189.056.378.112.564.169.291.09.575.184.858.279.185.062.369.125.551.189.273.097.541.196.805.298.181.07.359.14.536.212.253.103.502.208.745.316.174.077.344.156.512.235.233.11.462.221.683.335.165.085.323.173.482.26.21.116.418.233.616.354.155.094.3.192.447.289.185.122.37.244.542.37.143.105.273.214.407.322.158.127.318.253.463.385.129.117.242.241.361.362.128.131.262.259.378.395.117.137.214.281.318.423.094.128.198.252.28.384.128.204.229.418.328.631.034.074.083.144.114.22-.008-11.426-36.76-11.426-36.76-11.426-35.26,0-39.11,10.23-39.11,12.5,0,2.1,4.009,4.559,4.37,4.71Z"/>
|
||||
<path class="cls-19" d="m19.894,91.967s0,0,0,0c.121.11.257.216.39.323,0,0,0,0,0,0-.132-.106-.269-.212-.389-.322Z"/>
|
||||
<path class="cls-19" d="m19.262,91.31h0c.073.091.172.175.254.264,0,0,0,0-.001-.001-.081-.088-.179-.172-.253-.262Z"/>
|
||||
<path class="cls-19" d="m18.772,90.558c.134.258.288.51.48.752h0c-.192-.242-.345-.494-.48-.752,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-19" d="m55.541,77.19h0c-1.333,0-8.484.127-16.164,1.165.015-.002.03-.004.045-.006,7.663-1.032,14.788-1.159,16.118-1.159Z"/>
|
||||
<path class="cls-19" d="m18.342,88.9c0-2.07,1.2-3.776,3.17-5.181,0,0,0,0,0,0-1.97,1.405-3.171,3.111-3.171,5.181,0,.262.038.515.094.763,0,0,0,0,0,0-.056-.248-.094-.501-.094-.763Z"/>
|
||||
<path class="cls-19" d="m20.724,92.631s0,0,0,0c.045.032.081.067.127.099,0,0,0,0,0,0-.046-.032-.082-.067-.127-.099Z"/>
|
||||
<path class="cls-19" d="m18.483,89.826s0,0,0,0c.054.196.125.387.212.574,0,0,0,0,0,0-.087-.187-.157-.378-.212-.573Z"/>
|
||||
<path class="cls-19" d="m19.894,91.966c-.139-.127-.257-.261-.378-.393.122.132.239.265.378.393Z"/>
|
||||
<path class="cls-19" d="m18.772,90.558c-.027-.052-.052-.105-.077-.158.025.053.049.106.077.158Z"/>
|
||||
<path class="cls-19" d="m18.483,89.825c-.015-.054-.034-.108-.047-.162.012.055.032.108.047.162Z"/>
|
||||
<path class="cls-19" d="m20.724,92.631c-.156-.111-.297-.227-.44-.342.143.115.284.231.44.342Z"/>
|
||||
<path class="cls-19" d="m92.352,86.95s0-.002,0-.004c-.031-.076-.08-.145-.114-.22.035.076.083.147.115.223Z"/>
|
||||
<path class="cls-19" d="m90.933,84.894c.128.131.262.259.378.395-.116-.135-.25-.264-.378-.395Z"/>
|
||||
<path class="cls-19" d="m85.674,81.664c.253.103.502.208.745.316-.244-.108-.492-.213-.745-.316Z"/>
|
||||
<path class="cls-19" d="m82.924,80.686c.291.09.575.184.858.279-.283-.095-.567-.189-.858-.279Z"/>
|
||||
<path class="cls-19" d="m84.333,81.154c.273.097.541.196.805.298-.264-.102-.532-.201-.805-.298Z"/>
|
||||
<path class="cls-19" d="m79.935,79.863c.321.078.637.161.952.243-.316-.083-.631-.165-.952-.243Z"/>
|
||||
<path class="cls-19" d="m81.453,80.256c.306.084.607.172.907.261-.3-.089-.6-.177-.907-.261Z"/>
|
||||
<path class="cls-19" d="m86.932,82.215c.233.11.462.221.683.335-.222-.114-.451-.225-.683-.335Z"/>
|
||||
<path class="cls-19" d="m91.629,85.711c.094.128.198.252.28.384-.082-.132-.186-.256-.28-.384Z"/>
|
||||
<path class="cls-19" d="m90.108,84.147c.158.127.318.253.463.385-.145-.132-.306-.258-.463-.385Z"/>
|
||||
<path class="cls-19" d="m88.097,82.811c.21.116.418.233.616.354-.198-.121-.406-.238-.616-.354Z"/>
|
||||
<path class="cls-19" d="m89.16,83.454c.185.122.37.244.542.37-.172-.127-.357-.248-.542-.37Z"/>
|
||||
<path class="cls-19" d="m78.377,79.505c.335.073.666.15.997.227-.331-.077-.661-.154-.997-.227Z"/>
|
||||
<path class="cls-19" d="m63.946,77.558c.062.005.123.009.185.014-.062-.005-.123-.009-.185-.014Z"/>
|
||||
<path class="cls-19" d="m68.252,77.943c.176.018.351.036.528.055-.178-.019-.352-.037-.528-.055Z"/>
|
||||
<path class="cls-19" d="m69.766,78.112c.21.025.418.048.629.074-.211-.026-.419-.049-.629-.074Z"/>
|
||||
<path class="cls-19" d="m65.328,77.667c.104.009.207.017.313.026-.105-.009-.208-.017-.313-.026Z"/>
|
||||
<path class="cls-19" d="m21.512,83.719c3.988-2.844,11.13-4.454,17.865-5.364-6.736.909-13.878,2.519-17.865,5.364Z"/>
|
||||
<path class="cls-19" d="m62.644,77.468s.009,0,.014,0c-.005,0-.009,0-.014,0Z"/>
|
||||
<path class="cls-19" d="m66.766,77.794c.144.013.286.026.431.04-.145-.014-.287-.027-.431-.04Z"/>
|
||||
<path class="cls-19" d="m71.28,78.3c.248.032.494.063.743.097-.249-.034-.495-.065-.743-.097Z"/>
|
||||
<path class="cls-19" d="m76.791,79.18c.353.068.702.141,1.051.214-.349-.073-.698-.146-1.051-.214Z"/>
|
||||
<path class="cls-19" d="m75.19,78.887c.382.066.763.137,1.143.208-.381-.071-.761-.143-1.143-.208Z"/>
|
||||
<path class="cls-19" d="m72.665,78.491c.833.121,1.669.249,2.503.393-.834-.143-1.67-.272-2.503-.393Z"/>
|
||||
<path class="cls-19" d="m18.436,89.662s0,0,0,0c.013.055.032.108.047.162,0,0,0,0,0,0-.015-.055-.034-.108-.047-.164Z"/>
|
||||
<path class="cls-19" d="m39.423,78.349c-.015.002-.03.004-.045.006-6.735.91-13.877,2.52-17.865,5.364,0,0,0,0,0,0,3.997-2.85,11.162-4.461,17.911-5.37Z"/>
|
||||
<path class="cls-19" d="m20.283,92.288s0,0,0,0c.143.115.284.231.44.342,0,0,0,0,0,0-.157-.111-.298-.227-.442-.343Z"/>
|
||||
<path class="cls-19" d="m18.695,90.399s0,0,0,0c.025.053.05.106.077.158,0,0,0,0,0,0-.027-.053-.052-.106-.077-.159Z"/>
|
||||
<path class="cls-19" d="m19.514,91.572s0,0,.001.001c.122.132.24.266.378.393,0,0,0,0,0,0-.139-.128-.258-.262-.38-.394Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-18" d="m72.699,70.732c-8.132,1.567-16.792,1.585-17.156,1.585-.921,0-22.628-.075-29.753-6.622-.181-.165-.35-.334-.51-.506-5.43,2.311-9.404,5.755-9.404,10.911,0,2.532,1.271,4.612,3.362,6.325,8.899,7.27,32.664,7.764,36.299,7.764,4.486,0,39.661-.746,39.661-14.089,0-2.889-1.25-5.253-3.31-7.165v-.01c-1.605-1.499-3.709-2.727-6.088-3.74-2.267,2.442-6.637,4.303-13.101,5.548Z"/>
|
||||
<path class="cls-18" d="m55.537,59.798h.02c5.131,0,19.883.395,30.042,4.542.841-1.032,1.33-2.206,1.33-3.549,0-2.031-.833-3.724-2.232-5.127v-.011h-.012c-6.811-6.843-26.98-6.843-29.142-6.843-2.614,0-31.397,0-31.397,11.981,0,1.347.483,2.521,1.324,3.548,2.5-1.015,5.438-1.878,8.841-2.577,8.634-1.773,17.716-1.965,21.227-1.965Z"/>
|
||||
<path class="cls-17" d="m55.543,71.616c2.999,0,23.323-.417,29.565-6.719-10.988-4.389-27.312-4.4-29.55-4.4h-.02c-2.163,0-18.543-.004-29.564,4.395.095.095.187.192.289.284,6.568,6.035,26.332,6.439,29.281,6.439Z"/>
|
||||
<path class="cls-16" d="m55.557,59.798h-.02c-3.511,0-12.594.191-21.228,1.965-3.403.699-6.341,1.562-8.841,2.577.088.107.177.213.272.317.074.08.153.158.231.237,11.021-4.399,27.401-4.395,29.564-4.395h.02c2.239,0,18.562.011,29.55,4.4.09-.091.165-.189.249-.283.082-.091.166-.181.242-.274-10.158-4.148-24.91-4.542-30.042-4.542Z"/>
|
||||
<path class="cls-21" d="m92.434,68.486c-.021-.026-.043-.051-.068-.074-1.62-1.512-3.713-2.76-6.085-3.79.89-1.157,1.348-2.433,1.348-3.832,0-2.09-.804-3.965-2.39-5.574-.033-.041-.071-.079-.112-.111-6.555-6.49-24.292-6.995-29.584-6.995-2.774,0-9.965.147-16.815,1.511-10.14,2.019-15.282,5.777-15.282,11.17,0,1.391.454,2.674,1.35,3.831-6.381,2.754-9.619,6.592-9.619,11.478,0,2.59,1.217,4.9,3.618,6.866,9.587,7.832,35.638,7.923,36.743,7.923,1.648,0,40.361-.168,40.361-14.789,0-2.889-1.165-5.45-3.464-7.613Zm-36.897,21.702c-3.635,0-27.4-.494-36.299-7.764-2.091-1.712-3.362-3.793-3.362-6.325,0-5.155,3.974-8.599,9.404-10.911.161.171.329.34.51.506,7.125,6.547,28.832,6.622,29.753,6.622.365,0,9.025-.018,17.156-1.585,6.464-1.245,10.834-3.106,13.101-5.548,2.38,1.013,4.483,2.241,6.088,3.74v.01c2.059,1.912,3.31,4.276,3.31,7.165,0,13.343-35.175,14.089-39.661,14.089Zm.006-41.379c2.162,0,22.331,0,29.142,6.843h.012v.011c1.399,1.402,2.232,3.096,2.232,5.127,0,1.344-.489,2.518-1.33,3.549-.076.093-.16.183-.242.274-.084.094-.159.191-.249.283-6.241,6.302-26.566,6.719-29.565,6.719-2.949,0-22.713-.404-29.281-6.439-.102-.093-.194-.189-.289-.284-.078-.078-.158-.156-.231-.236-.095-.104-.185-.21-.272-.317-.841-1.027-1.324-2.201-1.324-3.548,0-11.981,28.784-11.981,31.397-11.981Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-14" points="36.855 36.028 55.187 44.002 55.187 3.573 36.855 36.028"/>
|
||||
<polygon class="cls-13" points="74.222 35.38 55.886 3.55 55.886 43.985 74.222 35.38"/>
|
||||
<polygon class="cls-9" points="37.344 37.004 55.187 62.832 55.187 44.766 52.018 43.387 37.344 37.004"/>
|
||||
<polygon class="cls-9" points="37.344 37.004 37.344 37.004 52.018 43.387 37.344 37.004"/>
|
||||
<polygon class="cls-2" points="55.886 62.81 73.715 36.392 69.264 38.48 55.886 44.759 55.886 62.81"/>
|
||||
<polygon class="cls-2" points="69.264 38.48 55.886 44.758 55.886 44.759 69.264 38.48"/>
|
||||
<path class="cls-21" d="m75.019,35.361L55.84,2.066c-.005-.008-.014-.009-.019-.017-.021-.03-.052-.048-.08-.07-.031-.024-.058-.051-.096-.064-.008-.003-.012-.011-.02-.013-.031-.009-.06.006-.091.006-.03,0-.058-.014-.088-.006-.008.002-.012.01-.019.013-.038.013-.066.04-.096.064-.028.022-.059.04-.079.071-.005.008-.015.009-.019.017l-19.178,33.953c-.026.047-.039.098-.042.151,0,.009.003.017.003.026,0,.036,0,.072.012.107.008.023.024.041.037.062.005.008.002.018.007.025l19.178,27.76s.004.003.006.005c.005.007.014.01.019.017.037.044.085.078.14.099.008.003.012.012.02.014.034.011.068.016.103.016.035,0,.069-.005.104-.016.009-.003.013-.012.021-.015.053-.021.098-.054.134-.096.007-.008.018-.013.024-.022.002-.002.005-.003.007-.006l19.179-28.419c.005-.007.002-.017.007-.025.013-.022.029-.042.037-.066.011-.035.011-.072.011-.108,0-.008.003-.015.002-.022-.004-.052-.017-.103-.043-.149Zm-1.304,1.031l-17.829,26.418v-18.051h0s13.378-6.278,13.378-6.278l4.451-2.089ZM55.187,3.573v40.43l-18.332-7.974L55.187,3.573Zm.699,40.413V3.55l18.336,31.831-18.336,8.605Zm-18.542-6.981l17.843,7.761v18.066l-17.843-25.828h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-22" d="m55.604,1.9c.007,0,.014,0,.021.003.008.002.012.011.02.013.038.013.066.04.096.064.028.022.059.039.08.07.005.007.014.009.019.017l19.179,33.294c.027.046.04.097.043.149,0,.007-.002.015-.002.022,0,.037,0,.073-.011.108-.008.025-.024.044-.037.066-.004.008-.002.017-.007.024l-8.748,12.962c6.903.763,14.918,2.499,18.87,6.412.041.033.078.07.112.111,1.586,1.61,2.39,3.485,2.39,5.574,0,1.398-.457,2.675-1.348,3.832,2.372,1.03,4.466,2.278,6.085,3.79.024.023.047.048.068.074,2.299,2.163,3.464,4.724,3.464,7.614,0,1.013-.203,1.951-.549,2.83,2.854,1.622,5.286,3.575,7.009,5.938.017.023.032.048.046.073,1.824,2.509,2.749,5.365,2.749,8.489,0,4.671-2.235,8.713-6.563,12.038l10.106,17.891c.644,1.139.634,2.494-.026,3.623-.659,1.129-1.833,1.804-3.141,1.804-1.138,0-2.226-.543-2.909-1.453l-12.873-17.131c-2.831,1.086-6.019,2.038-9.599,2.829-6.907,1.527-13.863,2.196-18.609,2.49-.004.053-.003.105-.009.158l-2.2,21.018c-.245,2.342-1.903,4.108-3.856,4.108s-3.612-1.766-3.857-4.108l-2.2-21.018c-.006-.055-.005-.11-.009-.166-3.212-.201-7.395-.57-11.881-1.258-6.337-.972-11.748-2.32-16.219-4.026l-12.852,17.104c-.684.91-1.771,1.453-2.908,1.453-1.308,0-2.482-.675-3.142-1.805-.659-1.129-.669-2.483-.025-3.622l10.087-17.859c-.772-.594-1.48-1.212-2.119-1.854-2.937-2.926-4.426-6.363-4.426-10.216,0-5.891,3.352-10.771,9.8-14.464-.36-.912-.546-1.868-.546-2.866,0-4.886,3.238-8.724,9.619-11.478-.896-1.157-1.35-2.44-1.35-3.831,0-5.393,5.142-9.151,15.282-11.17,1.965-.391,3.954-.68,5.863-.896l-8.52-12.333c-.005-.008-.003-.017-.007-.025-.012-.021-.029-.039-.037-.062-.011-.035-.012-.071-.012-.107,0-.009-.003-.017-.003-.026.003-.052.016-.104.042-.151L55.232,2.069c.004-.008.014-.01.019-.017.021-.031.051-.049.079-.071.031-.024.058-.051.096-.064.008-.003.011-.011.019-.013.007-.002.014-.002.02-.002.023,0,.045.008.068.008.024,0,.047-.009.07-.009m0-1.8c-.024,0-.05,0-.077.002-.021,0-.042-.001-.062-.001-.156,0-.331.023-.482.063-.122.032-.242.078-.355.136-.187.09-.329.198-.427.278-.061.044-.166.129-.28.257-.1.107-.188.225-.259.351l-19.176,33.95c-.159.283-.25.592-.271.919-.005.084-.005.173.002.261.006.136.027.333.1.554.03.09.073.189.123.285.046.098.1.192.162.279l6.832,9.89c-1.065.16-2.087.338-3.059.531-11.102,2.21-16.731,6.562-16.731,12.935,0,1.062.204,2.077.608,3.036-5.893,3.011-8.878,7.133-8.878,12.272,0,.692.074,1.377.22,2.046-6.205,3.94-9.474,9.2-9.474,15.284,0,4.349,1.667,8.215,4.955,11.491.337.339.698.677,1.078,1.01L.811,122.474c-.962,1.702-.948,3.726.038,5.415.985,1.689,2.741,2.697,4.696,2.697,1.7,0,3.325-.812,4.347-2.171l12.049-16.035c4.337,1.532,9.48,2.76,15.311,3.654,3.349.514,6.879.907,10.513,1.172l2.06,19.678c.341,3.261,2.769,5.721,5.648,5.721s5.305-2.459,5.647-5.721l2.059-19.669c4.65-.337,11.022-1.025,17.357-2.426,3.066-.678,5.944-1.494,8.577-2.433l12.067,16.059c1.022,1.36,2.647,2.171,4.348,2.171,1.955,0,3.71-1.008,4.695-2.696.986-1.688,1.001-3.713.039-5.416l-9.362-16.573c4.017-3.51,6.051-7.699,6.051-12.47,0-3.483-1.023-6.671-3.042-9.478-.031-.049-.063-.096-.096-.143-1.537-2.108-3.664-4.022-6.336-5.703.146-.663.22-1.333.22-2.006,0-3.375-1.333-6.355-3.961-8.859-.044-.048-.09-.095-.137-.14-1.286-1.2-2.888-2.295-4.781-3.269.406-.962.611-1.979.611-3.041,0-2.558-.963-4.843-2.862-6.791-.07-.079-.144-.154-.223-.223-3.198-3.131-8.895-5.312-16.961-6.498l7.114-10.542c.049-.072.092-.149.13-.228.045-.084.096-.194.137-.325.076-.245.092-.455.095-.581.004-.071.003-.143-.001-.211-.024-.339-.117-.648-.278-.928L57.4,1.168c-.08-.139-.182-.27-.298-.385-.097-.101-.186-.174-.259-.228-.088-.07-.206-.156-.36-.235-.123-.066-.253-.119-.385-.155-.154-.042-.334-.066-.494-.066h0Z"/>
|
||||
<g>
|
||||
<path class="cls-2" d="m178.64,48.449c3.272,0,4.795-.305,5.937-.533,1.065-.305,1.675-.533,3.12-.533,2.969,0,9.134,5.861,9.134,14.081,0,3.806-1.979,6.317-5.86,6.317-6.622,0-6.546-8.981-10.428-8.981-2.968,0-2.663,4.11-2.663,4.719v23.366c0,9.057,7.002,4.719,7.002,10.046,0,2.359-1.446,5.708-15.679,5.708-3.882,0-18.114.152-18.114-5.708,0-4.034,4.719-2.436,6.24-5.328.762-1.599,1.066-6.165,1.142-8.068l.609-16.82c.305-7.992-1.294-7.916-2.359-7.916-3.425,0-5.1,8.981-11.265,8.981-2.968,0-4.719-2.74-4.719-5.48,0-6.47,5.48-14.918,9.818-14.918.533,0,1.37.229,2.816.533,1.445.228,3.501.533,6.545.533h18.724Z"/>
|
||||
<path class="cls-7" d="m200.861,78.741c0-7.459-4.719-3.273-4.719-7.916,0-6.013,17.81-7.688,18.495-7.688,2.892,0,2.968.761,3.272,3.349.076.914,0,2.664,1.294,2.664,1.827,0,2.969-6.013,9.514-6.013,5.1,0,8.448,3.806,8.448,8.829,0,5.1-4.338,9.133-9.361,9.133-4.947,0-6.317-4.338-8.296-4.338-2.131,0-1.751,5.1-1.751,6.47,0,2.283.229,5.479.609,8.372.456,3.349,6.241,1.294,6.241,5.86,0,4.795-6.698,5.176-15.07,5.176-3.501,0-13.853.152-13.853-5.1,0-3.197,3.501-3.197,4.339-5.86.608-1.903.837-6.241.837-8.373v-4.566Z"/>
|
||||
<path class="cls-8" d="m275.523,83.612c0,13.396,4.567,6.089,4.567,10.199,0,4.338-5.938,9.057-11.493,9.057-6.013,0-6.165-3.881-8.067-3.881-.914,0-1.903.989-3.577,1.902-1.675.99-3.958,1.979-7.459,1.979-6.317,0-12.635-3.349-12.635-10.884,0-5.404,5.176-12.102,18.571-12.102,2.359,0,3.425.152,3.425-2.588,0-2.512-.076-7.763-3.577-7.763-4.566,0-5.023,8.22-11.797,8.22-2.436,0-4.263-1.75-4.263-4.186,0-6.469,12.863-10.655,20.094-10.655,4.338,0,16.211,1.674,16.211,13.091v7.611Zm-22.833,6.698c0,1.979,1.065,4.11,3.272,4.11,2.893,0,3.121-3.121,3.121-5.404,0-1.674.076-3.577-2.055-3.577-2.588,0-4.339,2.436-4.339,4.871Z"/>
|
||||
<path class="cls-1" d="m284.578,80.567c0-4.871-.99-6.241-1.751-6.926-.989-.837-2.055-.761-2.055-2.74,0-5.632,18.875-7.764,19.027-7.764,1.903,0,2.055,1.675,2.055,3.273,0,.914-.304,3.501,1.294,3.501.762,0,1.979-1.674,4.11-3.425,2.131-1.674,5.176-3.349,9.666-3.349,9.058,0,9.134,6.774,10.731,6.774.609,0,1.979-1.674,4.187-3.425,2.283-1.674,5.327-3.349,9.209-3.349,7.23,0,11.722,3.958,11.722,11.417v16.059c0,4.415,2.815,4.795,2.815,7.002,0,4.795-8.677,5.023-11.645,5.023-2.512,0-11.341-.533-11.341-4.491,0-2.816,2.664-2.74,2.664-7.154v-6.85c0-3.197.381-9.59-3.958-9.59-4.262,0-3.882,5.175-3.882,7.992v4.719c0,9.362,2.74,7.763,2.74,10.58,0,4.719-9.133,4.795-11.873,4.795-11.493,0-12.102-2.131-12.025-4.262.076-4.034,3.882-.837,3.653-9.895v-4.338c0-2.74.381-9.59-3.958-9.59-4.262,0-3.882,5.175-3.882,7.992v6.165c0,7.002,2.056,6.546,2.056,9.286,0,1.979-1.751,4.643-12.178,4.643-2.969,0-11.95.076-11.95-3.958,0-3.425,4.567-1.598,4.567-11.112v-7.002Z"/>
|
||||
<path class="cls-5" d="m361.216,86.732c0-7.992-.456-9.742-.837-10.123-.989-.989-5.1,0-5.1-4.338,0-5.176,15.679-9.134,21.312-9.134,1.826,0,2.131.609,2.131,2.284,0,.533.076,1.598.837,1.598,1.066,0,5.328-4.11,11.569-4.11,9.971,0,15.298,9.97,15.298,18.875,0,10.199-7.23,21.083-18.343,21.083-3.577,0-7.154-1.903-7.839-1.903-1.065,0-1.218,1.599-1.218,2.436,0,5.785,4.719,3.958,4.719,7.688,0,4.262-6.926,4.795-9.895,4.795-10.199,0-18.19-3.729-18.19-7.611,0-3.425,3.12-2.131,4.49-5.099.762-1.599,1.065-3.806,1.065-12.254v-4.186Zm20.854-12.559c-2.739,0-3.653,1.142-3.653,14.994,0,2.512,1.218,3.425,3.882,3.425,4.719,0,5.328-5.1,5.328-8.753s-.762-9.666-5.557-9.666Z"/>
|
||||
<path class="cls-4" d="m407.184,84.221c0-13.624,11.949-21.311,24.508-21.311,13.319,0,20.931,10.427,20.931,19.865,0,12.482-13.396,20.093-24.584,20.093-11.265,0-20.854-6.622-20.854-18.647Zm21.234-12.33c-2.512,0-3.881,2.359-3.729,4.567.304,4.795,1.674,16.516,6.926,16.82,2.892.152,3.501-2.283,3.501-4.719,0-3.425-1.522-16.668-6.698-16.668Z"/>
|
||||
<path class="cls-3" d="m475.909,88.178c0,1.37-.076,3.882.305,5.1.838,2.74,4.263,1.446,4.263,4.643,0,5.1-9.286,4.719-12.559,4.719-3.425,0-14.08.38-14.08-5.023,0-2.664,3.272-1.674,4.186-4.491.989-3.121.989-12.634.989-16.364,0-2.968.152-8.677-.532-12.102-1.142-5.632-5.633-3.653-5.633-7.992,0-6.622,19.789-8.22,20.854-8.22,2.588,0,3.197,1.674,3.197,3.958,0,1.75-.99,10.123-.99,21.007v14.765Z"/>
|
||||
<path class="cls-10" d="m502.622,87.722c0,8.981,4.49,5.708,4.49,9.742,0,5.176-9.666,5.176-13.091,5.176-13.091,0-13.624-2.969-13.624-4.795,0-2.74,3.121-2.893,4.034-5.252.989-2.664,1.142-13.7.305-16.516-.686-2.664-4.567-1.827-4.567-5.251,0-5.404,19.332-7.688,20.094-7.688,2.283,0,2.359,1.751,2.359,3.349v21.235Zm-15.07-27.02c-3.196,0-6.621-1.446-6.621-5.175,0-5.023,10.047-7.078,13.928-7.078,3.045,0,7.383.989,7.383,4.795,0,5.937-10.351,7.458-14.689,7.458Z"/>
|
||||
<path class="cls-11" d="m528.725,89.092c0,7.383,2.588,6.089,2.588,8.981,0,4.719-10.122,4.567-13.243,4.567-3.044,0-10.808,0-10.808-4.567,0-3.653,3.501-.38,3.729-7.839l.305-11.417c.152-5.632-4.491-3.806-4.491-7.839s17.506-7.84,19.028-7.84c1.674,0,2.739,1.446,2.739,2.969,0,.457-.075.913-.075,1.37,0,.914.38,1.979,1.521,1.979,1.218,0,1.903-1.598,3.425-3.197,1.522-1.522,3.806-3.121,8.145-3.121,15.755,0,13.091,15.451,13.852,27.02.381,6.013,3.502,4.643,3.502,7.916,0,1.37.304,4.567-14.005,4.567-2.74,0-10.047.152-10.047-4.11,0-2.74,2.512-1.446,2.893-6.393.381-5.404,2.512-17.582-4.643-17.582-4.871,0-4.415,5.251-4.415,8.677v5.86Z"/>
|
||||
<path class="cls-12" d="m577.889,84.297c-.838,0-2.056-.229-2.056,1.065,0,3.806,4.947,7.002,8.524,7.002,5.709,0,8.524-3.425,10.123-3.425s2.968,2.359,2.968,3.806c0,5.86-10.731,10.123-17.581,10.123-13.7,0-20.778-9.894-20.778-19.712,0-11.721,9.971-20.246,21.387-20.246,12.939,0,18.647,9.97,18.647,15.679,0,5.023-2.283,5.708-4.719,5.708h-16.516Zm6.621-9.59c0-2.74-1.675-4.871-4.49-4.871-2.512,0-4.566,2.587-4.566,5.023,0,2.74,2.587,2.131,4.566,2.131,2.055,0,4.49.457,4.49-2.283Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 34 KiB |
19
trampoline/src/containers/Greetings/Greetings.jsx
Normal file
19
trampoline/src/containers/Greetings/Greetings.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import icon from '../../assets/img/icon-128.png';
|
||||
|
||||
class GreetingComponent extends Component {
|
||||
state = {
|
||||
name: 'dev',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello, {this.state.name}!</p>
|
||||
<img src={icon} alt="extension icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GreetingComponent;
|
||||
22
trampoline/src/exconfig.ts
Normal file
22
trampoline/src/exconfig.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
enablePasswordEncryption: false,
|
||||
showTransactionConfirmationScreen: true,
|
||||
factory_address: '0x9406Cc6185a346906296840746125a0E44976454',
|
||||
stateVersion: '0.1',
|
||||
network: {
|
||||
chainID: '11155111',
|
||||
family: 'EVM',
|
||||
name: 'Sepolia',
|
||||
provider: 'https://sepolia.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
|
||||
entryPointAddress: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
|
||||
bundler: 'https://sepolia.voltaire.candidewallet.com/rpc',
|
||||
baseAsset: {
|
||||
symbol: 'ETH',
|
||||
name: 'ETH',
|
||||
decimals: 18,
|
||||
image:
|
||||
'https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp',
|
||||
},
|
||||
},
|
||||
};
|
||||
25
trampoline/src/manifest.json
Executable file
25
trampoline/src/manifest.json
Executable file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Trampoline Example",
|
||||
"description": "Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets",
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": ["ex_background.bundle.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_title": "Taho",
|
||||
"default_icon": "icon-34.png",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["ex_contentScript.bundle.js"]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": ["popup.html", "*.js", "*.json"]
|
||||
}
|
||||
140
trampoline/src/pages/Account/account-api/account-api.ts
Normal file
140
trampoline/src/pages/Account/account-api/account-api.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { BigNumber, BigNumberish, ethers, Wallet } from 'ethers';
|
||||
import {
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
SimpleAccountFactory,
|
||||
SimpleAccountFactory__factory,
|
||||
UserOperationStruct,
|
||||
} from '@account-abstraction/contracts';
|
||||
import { arrayify, hexConcat } from 'ethers/lib/utils';
|
||||
|
||||
import { AccountApiParamsType, AccountApiType } from './types';
|
||||
import { MessageSigningRequest } from '../../Background/redux-slices/signing';
|
||||
import { TransactionDetailsForUserOp } from '@account-abstraction/sdk/dist/src/TransactionDetailsForUserOp';
|
||||
import config from '../../../exconfig';
|
||||
|
||||
const FACTORY_ADDRESS =
|
||||
config.factory_address || '0x6C583EE7f3a80cB53dDc4789B0Af1aaFf90e55F3';
|
||||
|
||||
/**
|
||||
* An implementation of the BaseAccountAPI using the SimpleAccount contract.
|
||||
* - contract deployer gets "entrypoint", "owner" addresses and "index" nonce
|
||||
* - owner signs requests using normal "Ethereum Signed Message" (ether's signer.signMessage())
|
||||
* - nonce method is "nonce()"
|
||||
* - execute method is "execFromEntryPoint()"
|
||||
*/
|
||||
class SimpleAccountAPI extends AccountApiType {
|
||||
name: string;
|
||||
factoryAddress?: string;
|
||||
owner: Wallet;
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* our account contract.
|
||||
* should support the "execFromEntryPoint" and "nonce" methods
|
||||
*/
|
||||
accountContract?: SimpleAccount;
|
||||
|
||||
factory?: SimpleAccountFactory;
|
||||
|
||||
constructor(params: AccountApiParamsType<{}, { privateKey: string }>) {
|
||||
super(params);
|
||||
this.factoryAddress = FACTORY_ADDRESS;
|
||||
|
||||
this.owner = params.deserializeState?.privateKey
|
||||
? new ethers.Wallet(params.deserializeState?.privateKey)
|
||||
: ethers.Wallet.createRandom();
|
||||
this.index = 0;
|
||||
this.name = 'SimpleAccountAPI';
|
||||
}
|
||||
|
||||
serialize = async (): Promise<{ privateKey: string }> => {
|
||||
return {
|
||||
privateKey: this.owner.privateKey,
|
||||
};
|
||||
};
|
||||
|
||||
async _getAccountContract(): Promise<SimpleAccount> {
|
||||
if (this.accountContract == null) {
|
||||
this.accountContract = SimpleAccount__factory.connect(
|
||||
await this.getAccountAddress(),
|
||||
this.provider
|
||||
);
|
||||
}
|
||||
return this.accountContract;
|
||||
}
|
||||
|
||||
/**
|
||||
* return the value to put into the "initCode" field, if the account is not yet deployed.
|
||||
* this value holds the "factory" address, followed by this account's information
|
||||
*/
|
||||
async getAccountInitCode(): Promise<string> {
|
||||
if (this.factory == null) {
|
||||
if (this.factoryAddress != null && this.factoryAddress !== '') {
|
||||
this.factory = SimpleAccountFactory__factory.connect(
|
||||
this.factoryAddress,
|
||||
this.provider
|
||||
);
|
||||
} else {
|
||||
throw new Error('no factory to get initCode');
|
||||
}
|
||||
}
|
||||
return hexConcat([
|
||||
this.factory.address,
|
||||
this.factory.interface.encodeFunctionData('createAccount', [
|
||||
await this.owner.getAddress(),
|
||||
this.index,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
async getNonce(): Promise<BigNumber> {
|
||||
if (await this.checkAccountPhantom()) {
|
||||
return BigNumber.from(0);
|
||||
}
|
||||
const accountContract = await this._getAccountContract();
|
||||
return await accountContract.getNonce();
|
||||
}
|
||||
|
||||
/**
|
||||
* encode a method call from entryPoint to our contract
|
||||
* @param target
|
||||
* @param value
|
||||
* @param data
|
||||
*/
|
||||
async encodeExecute(
|
||||
target: string,
|
||||
value: BigNumberish,
|
||||
data: string
|
||||
): Promise<string> {
|
||||
const accountContract = await this._getAccountContract();
|
||||
return accountContract.interface.encodeFunctionData('execute', [
|
||||
target,
|
||||
value,
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
async signUserOpHash(userOpHash: string): Promise<string> {
|
||||
return await this.owner.signMessage(arrayify(userOpHash));
|
||||
}
|
||||
|
||||
signMessage = async (
|
||||
context: any,
|
||||
request?: MessageSigningRequest
|
||||
): Promise<string> => {
|
||||
return this.owner.signMessage(request?.rawSigningData || '');
|
||||
};
|
||||
|
||||
signUserOpWithContext = async (
|
||||
userOp: UserOperationStruct,
|
||||
context: any
|
||||
): Promise<UserOperationStruct> => {
|
||||
return {
|
||||
...userOp,
|
||||
signature: await this.signUserOpHash(await this.getUserOpHash(userOp)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default SimpleAccountAPI;
|
||||
3
trampoline/src/pages/Account/account-api/index.ts
Normal file
3
trampoline/src/pages/Account/account-api/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import AccountApi from './account-api';
|
||||
|
||||
export default AccountApi;
|
||||
31
trampoline/src/pages/Account/account-api/types.ts
Normal file
31
trampoline/src/pages/Account/account-api/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts';
|
||||
import {
|
||||
BaseAccountAPI,
|
||||
BaseApiParams,
|
||||
} from '@account-abstraction/sdk/dist/src/BaseAccountAPI';
|
||||
import { TransactionDetailsForUserOp } from '@account-abstraction/sdk/dist/src/TransactionDetailsForUserOp';
|
||||
import { MessageSigningRequest } from '../../Background/redux-slices/signing';
|
||||
|
||||
export abstract class AccountApiType extends BaseAccountAPI {
|
||||
abstract serialize: () => Promise<object>;
|
||||
|
||||
/** sign a message for the use */
|
||||
abstract signMessage: (
|
||||
request?: MessageSigningRequest,
|
||||
context?: any
|
||||
) => Promise<string>;
|
||||
|
||||
abstract signUserOpWithContext(
|
||||
userOp: UserOperationStruct,
|
||||
context?: any
|
||||
): Promise<UserOperationStruct>;
|
||||
}
|
||||
|
||||
export interface AccountApiParamsType<T, S> extends BaseApiParams {
|
||||
context?: T;
|
||||
deserializeState?: S;
|
||||
}
|
||||
|
||||
export type AccountImplementationType = new (
|
||||
params: AccountApiParamsType<any, any>
|
||||
) => AccountApiType;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Onboarding from './onboarding';
|
||||
|
||||
export default Onboarding;
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Stack } from '@mui/system';
|
||||
import React from 'react';
|
||||
import { OnboardingComponent, OnboardingComponentProps } from '../types';
|
||||
|
||||
const Onboarding: OnboardingComponent = ({
|
||||
onOnboardingComplete,
|
||||
}: OnboardingComponentProps) => {
|
||||
return (
|
||||
<Box sx={{ padding: 2 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h3" gutterBottom>
|
||||
Customisable Account Component
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
You can show as many steps as you want in this dummy component. You
|
||||
need to call the function <b>onOnboardingComplete</b> passed as a
|
||||
props to this component. <br />
|
||||
<br />
|
||||
The function takes a context as a parameter, this context will be
|
||||
passed to your AccountApi when creating a new account.
|
||||
<br />
|
||||
This Component is defined in exported in{' '}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
trampoline/src/pages/Account/components/onboarding/index.ts
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
|
||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={() => onOnboardingComplete()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
@@ -0,0 +1,3 @@
|
||||
import SignMessage from './sign-message';
|
||||
|
||||
export default SignMessage;
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const SignMessage = ({
|
||||
onComplete,
|
||||
}: {
|
||||
onComplete: (context: any) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography variant="h3" gutterBottom>
|
||||
Customaisable sign message Account Component
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
You can show as many steps as you want in this dummy component. You
|
||||
need to call the function <b>onComplete</b> passed as a props to this
|
||||
component. <br />
|
||||
<br />
|
||||
The function takes a context as a parameter, this context will be
|
||||
passed to your AccountApi when creating a new account.
|
||||
<br />
|
||||
This Component is defined in exported in
|
||||
trampoline/src/pages/Account/components/sign-message/index.ts
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
|
||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
||||
<Button size="large" variant="contained" onClick={() => onComplete()}>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignMessage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Transaction from './transaction';
|
||||
|
||||
export default Transaction;
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
import { EthersTransactionRequest } from '../../../Background/services/provider-bridge';
|
||||
import { TransactionComponentProps } from '../types';
|
||||
|
||||
const Transaction = ({
|
||||
transaction,
|
||||
onComplete,
|
||||
onReject,
|
||||
}: TransactionComponentProps) => {
|
||||
const [loader, setLoader] = React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography variant="h3" gutterBottom>
|
||||
Dummy Account Component
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
You can show as many steps as you want in this dummy component. You
|
||||
need to call the function <b>onComplete</b> passed as a props to this
|
||||
component. <br />
|
||||
<br />
|
||||
The function takes a modifiedTransactions & context as a parameter,
|
||||
the context will be passed to your AccountApi when creating a new
|
||||
account. While modifiedTransactions will be agreed upon by the user.
|
||||
<br />
|
||||
This Component is defined in exported in{' '}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
trampoline/src/pages/Account/components/transaction/index.ts
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
|
||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
||||
<Button
|
||||
disabled={loader}
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
onComplete(transaction, undefined);
|
||||
setLoader(true);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
{loader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
35
trampoline/src/pages/Account/components/types.ts
Normal file
35
trampoline/src/pages/Account/components/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts';
|
||||
import { EthersTransactionRequest } from '../../Background/services/types';
|
||||
|
||||
export interface OnboardingComponentProps {
|
||||
accountName: string;
|
||||
onOnboardingComplete: (context?: any) => void;
|
||||
}
|
||||
|
||||
export interface OnboardingComponent
|
||||
extends React.FC<OnboardingComponentProps> {}
|
||||
|
||||
export interface TransactionComponentProps {
|
||||
transaction: EthersTransactionRequest;
|
||||
onReject: () => Promise<void>;
|
||||
onComplete: (
|
||||
modifiedTransaction: EthersTransactionRequest,
|
||||
context?: any
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TransactionComponent
|
||||
extends React.FC<TransactionComponentProps> {}
|
||||
|
||||
export interface SignMessageComponenetProps {
|
||||
onComplete: (context?: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface SignMessageComponenet
|
||||
extends React.FC<SignMessageComponenetProps> {}
|
||||
|
||||
export interface AccountImplementationComponentsType {
|
||||
Onboarding?: OnboardingComponent;
|
||||
Transaction?: TransactionComponent;
|
||||
SignMessage?: SignMessageComponenet;
|
||||
}
|
||||
3
trampoline/src/pages/Account/index.ts
Normal file
3
trampoline/src/pages/Account/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const ActiveAccountImplementation: string = 'active';
|
||||
|
||||
export { ActiveAccountImplementation };
|
||||
45
trampoline/src/pages/Account/useAccountApi.ts
Normal file
45
trampoline/src/pages/Account/useAccountApi.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBackgroundDispatch, useBackgroundSelector } from '../App/hooks';
|
||||
import { callAccountApiThunk } from '../Background/redux-slices/account';
|
||||
import {
|
||||
getAccountApiCallResult,
|
||||
getActiveAccount,
|
||||
} from '../Background/redux-slices/selectors/accountSelectors';
|
||||
|
||||
const useAccountApi = () => {
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const activeAccount = useBackgroundSelector(getActiveAccount);
|
||||
|
||||
const backgroundDispatch = useBackgroundDispatch();
|
||||
|
||||
const { accountApiCallResult, accountApiCallResultState } =
|
||||
useBackgroundSelector(getAccountApiCallResult);
|
||||
|
||||
const callAccountApi = useCallback(
|
||||
async (functionName: string, args?: any[]) => {
|
||||
setLoading(true);
|
||||
if (activeAccount) {
|
||||
await backgroundDispatch(
|
||||
callAccountApiThunk({ address: activeAccount, functionName, args })
|
||||
);
|
||||
}
|
||||
},
|
||||
[backgroundDispatch, activeAccount]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountApiCallResultState === 'set' && loading) {
|
||||
setResult(accountApiCallResult);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [accountApiCallResult, accountApiCallResultState, loading]);
|
||||
|
||||
return {
|
||||
result,
|
||||
loading,
|
||||
callAccountApi,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAccountApi;
|
||||
100
trampoline/src/pages/App/app.tsx
Normal file
100
trampoline/src/pages/App/app.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import {
|
||||
ProtectedRouteHasAccounts,
|
||||
ProtectedRouteKeyringUnlocked,
|
||||
} from './protected-route';
|
||||
import Home from './pages/home';
|
||||
import Onboarding from './pages/onboarding';
|
||||
import NewAccounts from './pages/new-accounts';
|
||||
import { InitializeKeyring } from './pages/keyring';
|
||||
import { WagmiConfig, createClient, configureChains, goerli } from 'wagmi';
|
||||
import { useBackgroundSelector } from './hooks';
|
||||
import { getActiveNetwork } from '../Background/redux-slices/selectors/networkSelectors';
|
||||
import DeployAccount from './pages/deploy-account';
|
||||
import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc';
|
||||
import '../Content/index';
|
||||
|
||||
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
|
||||
import Config from '../../exconfig';
|
||||
import TransferAsset from './pages/transfer-asset';
|
||||
console.debug('---- LAUNCHING WITH CONFIG ----', Config);
|
||||
|
||||
const App = () => {
|
||||
const activeNetwork = useBackgroundSelector(getActiveNetwork);
|
||||
|
||||
const client = useMemo(() => {
|
||||
const { chains, provider, webSocketProvider } = configureChains(
|
||||
[goerli],
|
||||
[
|
||||
jsonRpcProvider({
|
||||
rpc: (chain) => ({
|
||||
http: activeNetwork.provider,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
return createClient({
|
||||
provider,
|
||||
webSocketProvider,
|
||||
connectors: [
|
||||
new WalletConnectConnector({
|
||||
chains,
|
||||
options: {
|
||||
qrcode: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}, [activeNetwork]);
|
||||
|
||||
return (
|
||||
<WagmiConfig client={client}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRouteHasAccounts>
|
||||
<ProtectedRouteKeyringUnlocked>
|
||||
<Home />
|
||||
</ProtectedRouteKeyringUnlocked>
|
||||
</ProtectedRouteHasAccounts>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/transfer-assets"
|
||||
element={
|
||||
<ProtectedRouteHasAccounts>
|
||||
<ProtectedRouteKeyringUnlocked>
|
||||
<TransferAsset />
|
||||
</ProtectedRouteKeyringUnlocked>
|
||||
</ProtectedRouteHasAccounts>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/deploy-account"
|
||||
element={
|
||||
<ProtectedRouteHasAccounts>
|
||||
<ProtectedRouteKeyringUnlocked>
|
||||
<DeployAccount />
|
||||
</ProtectedRouteKeyringUnlocked>
|
||||
</ProtectedRouteHasAccounts>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/accounts/new"
|
||||
element={
|
||||
<ProtectedRouteKeyringUnlocked>
|
||||
<NewAccounts />
|
||||
</ProtectedRouteKeyringUnlocked>
|
||||
}
|
||||
/>
|
||||
<Route path="/keyring/initialize" element={<InitializeKeyring />} />
|
||||
<Route path="/onboarding/intro" element={<Onboarding />} />
|
||||
</Routes>
|
||||
</WagmiConfig>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Box, Tab, Tabs } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const AccountActivity = () => {
|
||||
const [activeTab, setActiveTab] = useState<'assets' | 'activity'>('assets');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Tabs
|
||||
variant="fullWidth"
|
||||
value={activeTab}
|
||||
onChange={(e, newTab) => setActiveTab(newTab)}
|
||||
sx={{
|
||||
borderBottom: '1px solid rgb(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<Tab label="Assets" value="assets" />
|
||||
<Tab label="Activity" value="activity" />
|
||||
</Tabs>
|
||||
{/* <TabPanel value="assets">Assets List</TabPanel> */}
|
||||
{/* <TabPanel value="activity">Activity</TabPanel> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountActivity;
|
||||
@@ -0,0 +1,3 @@
|
||||
import AccountActivity from './account-activity';
|
||||
|
||||
export default AccountActivity;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Stack, Typography, Chip, Tooltip } from '@mui/material';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getActiveNetwork } from '../../../Background/redux-slices/selectors/networkSelectors';
|
||||
import { useBackgroundDispatch, useBackgroundSelector } from '../../hooks';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import {
|
||||
AccountData,
|
||||
getAccountData,
|
||||
} from '../../../Background/redux-slices/account';
|
||||
import { getAccountEVMData } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const AccountBalanceInfo = ({ address }: { address: string }) => {
|
||||
const navigate = useNavigate();
|
||||
const activeNetwork = useBackgroundSelector(getActiveNetwork);
|
||||
const accountData: AccountData | 'loading' = useBackgroundSelector((state) =>
|
||||
getAccountEVMData(state, { address, chainId: activeNetwork.chainID })
|
||||
);
|
||||
|
||||
const walletDeployed: boolean = useMemo(
|
||||
() => (accountData === 'loading' ? false : accountData.accountDeployed),
|
||||
[accountData]
|
||||
);
|
||||
|
||||
const backgroundDispatch = useBackgroundDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
backgroundDispatch(getAccountData(address));
|
||||
}, [backgroundDispatch, address]);
|
||||
|
||||
return (
|
||||
<Stack spacing={1} justifyContent="center" alignItems="center">
|
||||
{activeNetwork.baseAsset.image && (
|
||||
<img
|
||||
height={40}
|
||||
src={activeNetwork.baseAsset.image}
|
||||
alt={`${activeNetwork.baseAsset.name} asset logo`}
|
||||
/>
|
||||
)}
|
||||
{accountData !== 'loading' &&
|
||||
accountData.balances &&
|
||||
accountData.balances[activeNetwork.baseAsset.symbol] && (
|
||||
<Typography variant="h3">
|
||||
{parseFloat(
|
||||
accountData.balances[activeNetwork.baseAsset.symbol].assetAmount
|
||||
.amount
|
||||
).toFixed(4)}{' '}
|
||||
{activeNetwork.baseAsset.symbol}
|
||||
</Typography>
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
walletDeployed
|
||||
? `Wallet has been deployed on ${activeNetwork.name} chain`
|
||||
: `Wallet is not deployed on ${activeNetwork.name} chain, it will be deployed upon the first transaction`
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate('/deploy-account')}
|
||||
variant="outlined"
|
||||
color={walletDeployed ? 'success' : 'error'}
|
||||
size="small"
|
||||
icon={walletDeployed ? <CheckCircleIcon /> : <CancelIcon />}
|
||||
label={
|
||||
accountData === 'loading'
|
||||
? 'Loading deployment status...'
|
||||
: walletDeployed
|
||||
? 'Deployed'
|
||||
: 'Not deployed'
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountBalanceInfo;
|
||||
@@ -0,0 +1,3 @@
|
||||
import AccountBalanceInfo from './account-balance-info';
|
||||
|
||||
export default AccountBalanceInfo;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Box, Tooltip, Typography } from '@mui/material';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { getAccountInfo } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { useBackgroundSelector } from '../../hooks';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
const AccountInfo = ({
|
||||
address,
|
||||
showOptions = true,
|
||||
}: {
|
||||
address: string;
|
||||
showOptions: boolean;
|
||||
}) => {
|
||||
const [tooltipMessage, setTooltipMessage] = useState<string>('Copy address');
|
||||
|
||||
const accountInfo = useBackgroundSelector((state) =>
|
||||
getAccountInfo(state, address)
|
||||
);
|
||||
|
||||
const copyAddress = useCallback(async () => {
|
||||
await navigator.clipboard.writeText(address);
|
||||
setTooltipMessage('Address copied');
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.20)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
>
|
||||
<Tooltip title={tooltipMessage} enterDelay={0}>
|
||||
<Box
|
||||
onClick={copyAddress}
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
minWidth: 300,
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
background: '#f2f4f6',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{accountInfo.name}</Typography>
|
||||
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="overline">
|
||||
{address.substring(0, 5)}...
|
||||
{address.substring(address.length - 5)}
|
||||
</Typography>
|
||||
<ContentCopyIcon sx={{ height: 16, cursor: 'pointer' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
{showOptions && <MoreVertIcon sx={{ position: 'absolute', right: 0 }} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountInfo;
|
||||
@@ -0,0 +1,3 @@
|
||||
import AccountInfo from './account-info';
|
||||
|
||||
export default AccountInfo;
|
||||
79
trampoline/src/pages/App/components/header/header.tsx
Normal file
79
trampoline/src/pages/App/components/header/header.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
import logo from '../../../../assets/img/logo.svg';
|
||||
import {
|
||||
getActiveNetwork,
|
||||
getSupportedNetworks,
|
||||
} from '../../../Background/redux-slices/selectors/networkSelectors';
|
||||
import { useBackgroundSelector } from '../../hooks';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const activeNetwork = useBackgroundSelector(getActiveNetwork);
|
||||
const supportedNetworks = useBackgroundSelector(getSupportedNetworks);
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
mr: 4,
|
||||
ml: 4,
|
||||
mt: 2,
|
||||
mb: 2,
|
||||
height: 60,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<img height={30} src={logo} className="App-logo" alt="logo" />
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<FormControl sx={{ minWidth: 80 }}>
|
||||
<InputLabel id="chain-selector">Chain</InputLabel>
|
||||
<Select
|
||||
labelId="chain-selector"
|
||||
id="chain-selector"
|
||||
value={activeNetwork.chainID}
|
||||
label="Chain"
|
||||
// onChange={handleChange}
|
||||
>
|
||||
{supportedNetworks.map((network) => (
|
||||
<MenuItem key={network.chainID} value={network.chainID}>
|
||||
{network.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<SettingsIcon fontSize="large" />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
3
trampoline/src/pages/App/components/header/index.ts
Normal file
3
trampoline/src/pages/App/components/header/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Header from './header';
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,3 @@
|
||||
import TransferAssetButton from './transfer-asset-button';
|
||||
|
||||
export default TransferAssetButton;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import SendRoundedIcon from '@mui/icons-material/SendRounded';
|
||||
import StoreIcon from '@mui/icons-material/Store';
|
||||
import { Avatar, Stack, Tooltip, Typography, useTheme } from '@mui/material';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import { ethers } from 'ethers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const TransferAssetButton = ({ activeAccount }: { activeAccount: string }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sendMoney = useCallback(async () => {
|
||||
// if (window.ethereum) {
|
||||
// const accounts = await window.ethereum.request({
|
||||
// method: 'eth_requestAccounts',
|
||||
// });
|
||||
// const txHash = await window.ethereum.request({
|
||||
// method: 'eth_sendTransaction',
|
||||
// params: [
|
||||
// {
|
||||
// from: activeAccount,
|
||||
// to: ethers.constants.AddressZero,
|
||||
// data: '0x',
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// console.log(txHash);
|
||||
// }
|
||||
}, [activeAccount]);
|
||||
|
||||
return (
|
||||
<Stack direction={'row'} spacing={4}>
|
||||
<Tooltip title="Coming soon">
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={'4px'}
|
||||
sx={{ cursor: 'not-allowed', opacity: 0.5 }}
|
||||
>
|
||||
<Avatar sx={{ bgcolor: theme.palette.primary.main }}>
|
||||
<StoreIcon />
|
||||
</Avatar>
|
||||
<Typography variant="button">Buy</Typography>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={'4px'}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Avatar sx={{ bgcolor: theme.palette.primary.main }}>
|
||||
<SendRoundedIcon
|
||||
onClick={() => navigate('/transfer-assets')}
|
||||
sx={{ transform: 'rotate(-45deg)', ml: '4px', mb: '6px' }}
|
||||
/>
|
||||
</Avatar>
|
||||
<Typography variant="button">Send</Typography>
|
||||
</Stack>
|
||||
<Tooltip title="Coming soon">
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={'4px'}
|
||||
sx={{ cursor: 'not-allowed', opacity: 0.5 }}
|
||||
>
|
||||
<Avatar sx={{ bgcolor: theme.palette.primary.main }}>
|
||||
<SwapHorizIcon />
|
||||
</Avatar>
|
||||
<Typography variant="button">Swap</Typography>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransferAssetButton;
|
||||
19
trampoline/src/pages/App/constants/constants.ts
Normal file
19
trampoline/src/pages/App/constants/constants.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Onboarding from '../../Account/components/onboarding';
|
||||
import Transaction from '../../Account/components/transaction';
|
||||
import SignMessage from '../../Account/components/sign-message';
|
||||
import { AccountImplementationComponentsType } from '../../Account/components/types';
|
||||
import { ActiveAccountImplementation } from '../../Account';
|
||||
|
||||
const AccountImplementation: AccountImplementationComponentsType = {
|
||||
Onboarding,
|
||||
Transaction,
|
||||
SignMessage,
|
||||
};
|
||||
|
||||
const AccountImplementations: {
|
||||
[name: string]: AccountImplementationComponentsType;
|
||||
} = {
|
||||
active: AccountImplementation,
|
||||
};
|
||||
|
||||
export { ActiveAccountImplementation, AccountImplementations };
|
||||
1
trampoline/src/pages/App/constants/index.ts
Normal file
1
trampoline/src/pages/App/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
2
trampoline/src/pages/App/hooks/index.ts
Normal file
2
trampoline/src/pages/App/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './keyring-hooks';
|
||||
export * from './redux-hooks';
|
||||
49
trampoline/src/pages/App/hooks/keyring-hooks.ts
Normal file
49
trampoline/src/pages/App/hooks/keyring-hooks.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { selectKeyringStatus } from '../../Background/redux-slices/selectors/keyringsSelectors';
|
||||
|
||||
import { BackgroundDispatch } from '../../Background/redux-slices';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useBackgroundSelector } from './redux-hooks';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AsyncifyFn<K> = K extends (...args: any[]) => any
|
||||
? (...args: Parameters<K>) => Promise<ReturnType<K>>
|
||||
: never;
|
||||
|
||||
export const useBackgroundDispatch = (): AsyncifyFn<BackgroundDispatch> =>
|
||||
useDispatch<BackgroundDispatch>() as AsyncifyFn<BackgroundDispatch>;
|
||||
|
||||
export const useAreKeyringsUnlocked = (
|
||||
redirectIfNot: boolean,
|
||||
redirectTo: string
|
||||
): boolean => {
|
||||
const keyringStatus = useBackgroundSelector(selectKeyringStatus);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const currentUrl = useMemo(() => location.pathname, [location]);
|
||||
|
||||
let redirectTarget: string | undefined;
|
||||
if (keyringStatus === 'uninitialized') {
|
||||
redirectTarget = '/keyring/initialize';
|
||||
} else if (keyringStatus === 'locked') {
|
||||
redirectTarget = '/keyring/unlock';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
redirectIfNot &&
|
||||
typeof redirectTarget !== 'undefined' &&
|
||||
currentUrl !== redirectTarget
|
||||
) {
|
||||
navigate(redirectTarget, {
|
||||
state: {
|
||||
redirectTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return keyringStatus === 'unlocked';
|
||||
};
|
||||
5
trampoline/src/pages/App/hooks/redux-hooks.ts
Normal file
5
trampoline/src/pages/App/hooks/redux-hooks.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RootState } from '../../Background/redux-slices';
|
||||
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
|
||||
export const useBackgroundSelector: TypedUseSelectorHook<RootState> =
|
||||
useSelector;
|
||||
48
trampoline/src/pages/App/index.css
Normal file
48
trampoline/src/pages/App/index.css
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Josh's Custom CSS Reset
|
||||
https://www.joshwcomeau.com/css/custom-css-reset/
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background: #f2f4f6;
|
||||
}
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
15
trampoline/src/pages/App/index.html
Normal file
15
trampoline/src/pages/App/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>
|
||||
Trampoline is a chrome extension boilerplate code to showcase your own
|
||||
Smart Contract Wallets
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
37
trampoline/src/pages/App/index.jsx
Normal file
37
trampoline/src/pages/App/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import App from './app';
|
||||
import './index.css';
|
||||
import { Store } from 'webext-redux';
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
const store = new Store();
|
||||
|
||||
Object.assign(store, {
|
||||
dispatch: store.dispatch.bind(store),
|
||||
getState: store.getState.bind(store),
|
||||
subscribe: store.subscribe.bind(store),
|
||||
});
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
|
||||
store
|
||||
.ready()
|
||||
.then(() => {
|
||||
if (container) {
|
||||
const root = createRoot(container); // createRoot(container!) if you use TypeScript
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
220
trampoline/src/pages/App/pages/deploy-account/deploy-account.tsx
Normal file
220
trampoline/src/pages/App/pages/deploy-account/deploy-account.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Stack,
|
||||
Step,
|
||||
StepContent,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { getAccountData } from '../../../Background/redux-slices/account';
|
||||
import {
|
||||
getAccountEVMData,
|
||||
getActiveAccount,
|
||||
} from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { getActiveNetwork } from '../../../Background/redux-slices/selectors/networkSelectors';
|
||||
import { useBackgroundDispatch, useBackgroundSelector } from '../../hooks';
|
||||
import AccountBalanceInfo from '../../components/account-balance-info';
|
||||
import AccountInfo from '../../components/account-info';
|
||||
import Header from '../../components/header';
|
||||
import { useState } from 'react';
|
||||
import { BigNumber, ethers } from 'ethers';
|
||||
import { useProvider } from 'wagmi';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
sendTransaction,
|
||||
sendTransactionsRequest,
|
||||
} from '../../../Background/redux-slices/transactions';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const DeployAccount = () => {
|
||||
const navigate = useNavigate();
|
||||
const [deployLoader, setDeployLoader] = useState<boolean>(false);
|
||||
const [tooltipMessage, setTooltipMessage] = useState<string>('Copy address');
|
||||
const activeAccount = useBackgroundSelector(getActiveAccount);
|
||||
const activeNetwork = useBackgroundSelector(getActiveNetwork);
|
||||
const provider = useProvider();
|
||||
const accountData = useBackgroundSelector((state) =>
|
||||
getAccountEVMData(state, {
|
||||
chainId: activeNetwork.chainID,
|
||||
address: activeAccount || '',
|
||||
})
|
||||
);
|
||||
|
||||
const walletDeployed: boolean = useMemo(
|
||||
() => (accountData === 'loading' ? false : accountData.accountDeployed),
|
||||
[accountData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (walletDeployed) {
|
||||
alert('Account already deployed');
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate, walletDeployed]);
|
||||
|
||||
const backgroundDispatch = useBackgroundDispatch();
|
||||
|
||||
const [minimumRequiredFundsPrice, setMinimumRequiredFundsPrice] =
|
||||
useState<BigNumber>(BigNumber.from(0));
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMinimumRequiredFundsPrice = async () => {
|
||||
if (accountData !== 'loading') {
|
||||
const gasPrice = await provider.getGasPrice();
|
||||
|
||||
setMinimumRequiredFundsPrice(
|
||||
ethers.utils
|
||||
.parseEther(accountData.minimumRequiredFunds)
|
||||
.mul(gasPrice)
|
||||
.add(ethers.utils.parseEther('0.001')) // TODO: read from config
|
||||
);
|
||||
}
|
||||
};
|
||||
fetchMinimumRequiredFundsPrice();
|
||||
}, [accountData, provider]);
|
||||
|
||||
let isButtonDisabled = useMemo(() => {
|
||||
if (accountData === 'loading') return true;
|
||||
if (!accountData.balances) return true;
|
||||
if (
|
||||
ethers.utils
|
||||
.parseEther(
|
||||
accountData.balances[activeNetwork.baseAsset.symbol].assetAmount
|
||||
.amount
|
||||
)
|
||||
.lte(minimumRequiredFundsPrice)
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}, [accountData, activeNetwork, minimumRequiredFundsPrice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isButtonDisabled) return;
|
||||
const timer = setInterval(() => {
|
||||
if (activeAccount) backgroundDispatch(getAccountData(activeAccount));
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [activeAccount, backgroundDispatch, isButtonDisabled]);
|
||||
|
||||
const copyAddress = useCallback(async () => {
|
||||
await navigator.clipboard.writeText(activeAccount || '');
|
||||
setTooltipMessage('Address copied');
|
||||
setTimeout(() => {
|
||||
setTooltipMessage('Copy address');
|
||||
}, 6000);
|
||||
}, [activeAccount]);
|
||||
|
||||
const deployAcount = useCallback(async () => {
|
||||
if (!activeAccount) return;
|
||||
setDeployLoader(true);
|
||||
|
||||
if (window.ethereum) {
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [
|
||||
{
|
||||
from: activeAccount,
|
||||
to: ethers.constants.AddressZero,
|
||||
data: '0x',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(accounts, txHash);
|
||||
alert('success');
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
// await backgroundDispatch(sendTransaction(activeAccount));
|
||||
}, [activeAccount, navigate]);
|
||||
|
||||
return (
|
||||
<Container sx={{ width: '62vw', height: '100vh' }}>
|
||||
<Header />
|
||||
<Card sx={{ ml: 4, mr: 4, mt: 2, mb: 2 }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography textAlign="center" variant="h6">
|
||||
Account not deployed
|
||||
</Typography>
|
||||
</Box>
|
||||
{activeAccount && (
|
||||
<AccountInfo showOptions={false} address={activeAccount} />
|
||||
)}
|
||||
{activeAccount && <AccountBalanceInfo address={activeAccount} />}
|
||||
<Box sx={{ m: 4 }}>
|
||||
<Typography variant="h6">Perform the following steps:</Typography>
|
||||
<Stepper activeStep={isButtonDisabled ? 0 : 1} orientation="vertical">
|
||||
<Step key={0}>
|
||||
<StepLabel optional={null}>Transfer Funds</StepLabel>
|
||||
<StepContent>
|
||||
<Typography>
|
||||
Transfer more than{' '}
|
||||
<Typography component={'span'}>
|
||||
{ethers.utils.formatEther(minimumRequiredFundsPrice)}{' '}
|
||||
{activeNetwork.baseAsset.symbol}
|
||||
</Typography>{' '}
|
||||
to the account
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Tooltip title={tooltipMessage} enterDelay={0}>
|
||||
<Button
|
||||
onClick={copyAddress}
|
||||
variant="contained"
|
||||
sx={{ mt: 1, mr: 1 }}
|
||||
>
|
||||
Copy address
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step key={1}>
|
||||
<StepLabel optional={null}>Initiate Deploy Transaction</StepLabel>
|
||||
<StepContent>
|
||||
<Typography>
|
||||
Initiate the deployment transaction, it may take some time for
|
||||
the transaction to be added to the blockchain.
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Button
|
||||
disabled={deployLoader}
|
||||
onClick={deployAcount}
|
||||
variant="contained"
|
||||
sx={{ mt: 1, mr: 1, position: 'relative' }}
|
||||
>
|
||||
Deploy Account
|
||||
{deployLoader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Box>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployAccount;
|
||||
3
trampoline/src/pages/App/pages/deploy-account/index.ts
Normal file
3
trampoline/src/pages/App/pages/deploy-account/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DeployAccount from './deploy-account';
|
||||
|
||||
export default DeployAccount;
|
||||
53
trampoline/src/pages/App/pages/home/home.tsx
Normal file
53
trampoline/src/pages/App/pages/home/home.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Container,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getActiveAccount } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import AccountActivity from '../../components/account-activity';
|
||||
import AccountBalanceInfo from '../../components/account-balance-info';
|
||||
import AccountInfo from '../../components/account-info';
|
||||
import Header from '../../components/header';
|
||||
import TransferAssetButton from '../../components/transfer-asset-button';
|
||||
import { useBackgroundSelector } from '../../hooks';
|
||||
|
||||
const Home = () => {
|
||||
const activeAccount = useBackgroundSelector(getActiveAccount);
|
||||
|
||||
return (
|
||||
<Container sx={{ width: '62vw', height: '100vh' }}>
|
||||
<Header />
|
||||
<Card sx={{ ml: 4, mr: 4, mt: 2, mb: 2 }}>
|
||||
<CardContent>
|
||||
{activeAccount && <AccountInfo address={activeAccount}></AccountInfo>}
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ m: 2 }}
|
||||
>
|
||||
<AccountBalanceInfo address={activeAccount} />
|
||||
</Box>
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ m: 4 }}
|
||||
>
|
||||
<TransferAssetButton activeAccount={activeAccount || ''} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
3
trampoline/src/pages/App/pages/home/index.ts
Normal file
3
trampoline/src/pages/App/pages/home/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Home from './home';
|
||||
|
||||
export default Home;
|
||||
3
trampoline/src/pages/App/pages/keyring/index.ts
Normal file
3
trampoline/src/pages/App/pages/keyring/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import InitializeKeyring from './initialize-keyring';
|
||||
|
||||
export { InitializeKeyring };
|
||||
216
trampoline/src/pages/App/pages/keyring/initialize-keyring.tsx
Normal file
216
trampoline/src/pages/App/pages/keyring/initialize-keyring.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
Container,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Link,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { initializeKeyring } from '../../../Background/redux-slices/keyrings';
|
||||
import { selectKeyringStatus } from '../../../Background/redux-slices/selectors/keyringsSelectors';
|
||||
import { useBackgroundDispatch, useBackgroundSelector } from '../../hooks';
|
||||
import Config from '../../../../exconfig';
|
||||
|
||||
const InitializeKeyring = () => {
|
||||
const keyringState = useBackgroundSelector(selectKeyringStatus);
|
||||
const navigate = useNavigate();
|
||||
const { state } = useLocation();
|
||||
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [declaration, setDeclaration] = useState<boolean>(true);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [showLoader, setShowLoader] = useState<boolean>(false);
|
||||
|
||||
const backgroundDispatch = useBackgroundDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (Config.enablePasswordEncryption === false) {
|
||||
setShowLoader(true);
|
||||
backgroundDispatch(initializeKeyring('12345'));
|
||||
}
|
||||
}, [backgroundDispatch, setShowLoader]);
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
const handleMouseDownPassword = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const validatePassword = useCallback((): boolean => {
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords don't match with each other");
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
}, [password, confirmPassword]);
|
||||
|
||||
const onSetPasswordClick = useCallback(() => {
|
||||
if (!validatePassword()) return;
|
||||
setShowLoader(true);
|
||||
backgroundDispatch(initializeKeyring(password));
|
||||
// setShowLoader(false);
|
||||
}, [validatePassword, backgroundDispatch, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyringState === 'locked') {
|
||||
navigate('/keyring/unlock');
|
||||
}
|
||||
if (keyringState === 'unlocked') {
|
||||
navigate((state && state.redirectTo) || '/');
|
||||
}
|
||||
}, [keyringState, navigate, state]);
|
||||
|
||||
return Config.enablePasswordEncryption ? (
|
||||
<Container sx={{ height: '100vh' }}>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{ height: '100%' }}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: 600,
|
||||
p: 2,
|
||||
border: '1px solid #d6d9dc',
|
||||
borderRadius: 5,
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography textAlign="center" variant="h3" gutterBottom>
|
||||
Create password
|
||||
</Typography>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
>
|
||||
This password will unlock your account only on this device. We can
|
||||
not recover this password, <Link>learn more</Link>
|
||||
</Typography>
|
||||
<FormGroup sx={{ p: 2, pt: 4 }}>
|
||||
<FormControl sx={{ m: 1 }} variant="outlined">
|
||||
<InputLabel htmlFor="password">Password</InputLabel>
|
||||
<OutlinedInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoFocus
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
label="Password"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl sx={{ m: 1, mt: 2 }} variant="outlined">
|
||||
<InputLabel htmlFor="confirm-password">
|
||||
Confirm Password
|
||||
</InputLabel>
|
||||
<OutlinedInput
|
||||
onBlur={validatePassword}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
id="confirm-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
label="Confirm Password"
|
||||
/>
|
||||
{error ? <FormHelperText error>{error}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
<FormControl sx={{ m: 1 }} variant="outlined">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
checked={declaration}
|
||||
onChange={(e, checked) => setDeclaration(checked)}
|
||||
/>{' '}
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
I understand that TRAMPOLINE Account cannot recover this
|
||||
password for me
|
||||
</Typography>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</CardContent>
|
||||
<CardActions sx={{ width: '100%', pl: 2, pr: 2, pt: 0 }}>
|
||||
<Stack spacing={2} sx={{ width: '100%', pl: 2, pr: 2 }}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Button
|
||||
sx={{ width: '100%' }}
|
||||
disabled={
|
||||
password.length === 0 ||
|
||||
confirmPassword.length === 0 ||
|
||||
showLoader
|
||||
}
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={onSetPasswordClick}
|
||||
>
|
||||
Set password
|
||||
</Button>
|
||||
{showLoader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default InitializeKeyring;
|
||||
3
trampoline/src/pages/App/pages/new-accounts/index.ts
Normal file
3
trampoline/src/pages/App/pages/new-accounts/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import NewAccount from './new-account';
|
||||
|
||||
export default NewAccount;
|
||||
201
trampoline/src/pages/App/pages/new-accounts/new-account.tsx
Normal file
201
trampoline/src/pages/App/pages/new-accounts/new-account.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Container,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
InputLabel,
|
||||
Link,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ActiveAccountImplementation,
|
||||
AccountImplementations,
|
||||
} from '../../constants';
|
||||
import { useBackgroundDispatch, useBackgroundSelector } from '../../hooks';
|
||||
import { createNewAccount } from '../../../Background/redux-slices/keyrings';
|
||||
import { getSupportedNetworks } from '../../../Background/redux-slices/selectors/networkSelectors';
|
||||
import { EVMNetwork } from '../../../Background/types/network';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getAccountAdded } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { resetAccountAdded } from '../../../Background/redux-slices/account';
|
||||
import { FlashOffOutlined } from '@mui/icons-material';
|
||||
|
||||
const TakeNameComponent = ({
|
||||
name,
|
||||
setName,
|
||||
showLoader,
|
||||
nextStage,
|
||||
}: {
|
||||
name: string;
|
||||
setName: (name: string) => void;
|
||||
showLoader: boolean;
|
||||
nextStage: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography textAlign="center" variant="h3" gutterBottom>
|
||||
New account
|
||||
</Typography>
|
||||
<Typography textAlign="center" variant="body1" color="text.secondary">
|
||||
Give a name to your account so that you can recoganise it easily.
|
||||
</Typography>
|
||||
<FormGroup sx={{ p: 2, pt: 4 }}>
|
||||
<FormControl sx={{ m: 1 }} variant="outlined">
|
||||
<InputLabel htmlFor="name">Name</InputLabel>
|
||||
<OutlinedInput
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoFocus
|
||||
id="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</CardContent>
|
||||
<CardActions sx={{ width: '100%', pl: 2, pr: 2, pt: 0 }}>
|
||||
<Stack spacing={2} sx={{ width: '100%', pl: 2, pr: 2 }}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Button
|
||||
sx={{ width: '100%' }}
|
||||
disabled={name.length === 0 || showLoader}
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={nextStage}
|
||||
>
|
||||
Set name
|
||||
</Button>
|
||||
{showLoader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountOnboarding =
|
||||
AccountImplementations[ActiveAccountImplementation].Onboarding;
|
||||
|
||||
const NewAccount = () => {
|
||||
const [stage, setStage] = useState<'name' | 'account-onboarding'>('name');
|
||||
const [name, setName] = useState<string>('');
|
||||
const [showLoader, setShowLoader] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const backgroundDispatch = useBackgroundDispatch();
|
||||
|
||||
const supportedNetworks: Array<EVMNetwork> =
|
||||
useBackgroundSelector(getSupportedNetworks);
|
||||
|
||||
const addingAccount: string | null = useBackgroundSelector(getAccountAdded);
|
||||
|
||||
useEffect(() => {
|
||||
if (addingAccount) {
|
||||
backgroundDispatch(resetAccountAdded());
|
||||
navigate('/');
|
||||
}
|
||||
}, [addingAccount, backgroundDispatch, navigate]);
|
||||
|
||||
const onOnboardingComplete = useCallback(
|
||||
async (context?: any) => {
|
||||
setShowLoader(true);
|
||||
await backgroundDispatch(
|
||||
createNewAccount({
|
||||
name: name,
|
||||
chainIds: supportedNetworks.map((network) => network.chainID),
|
||||
implementation: ActiveAccountImplementation,
|
||||
context,
|
||||
})
|
||||
);
|
||||
setShowLoader(false);
|
||||
},
|
||||
[backgroundDispatch, supportedNetworks, name]
|
||||
);
|
||||
|
||||
const nextStage = useCallback(() => {
|
||||
setShowLoader(true);
|
||||
if (stage === 'name' && AccountOnboarding) {
|
||||
setStage('account-onboarding');
|
||||
}
|
||||
if (stage === 'name' && !AccountOnboarding) {
|
||||
onOnboardingComplete();
|
||||
}
|
||||
setShowLoader(false);
|
||||
}, [stage, setStage, onOnboardingComplete]);
|
||||
|
||||
return (
|
||||
<Container sx={{ height: '100vh' }}>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{ height: '100%' }}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: 600,
|
||||
minHeight: 300,
|
||||
p: 2,
|
||||
border: '1px solid #d6d9dc',
|
||||
background: 'white',
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
{showLoader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!showLoader && stage === 'name' && (
|
||||
<TakeNameComponent
|
||||
name={name}
|
||||
setName={setName}
|
||||
showLoader={showLoader}
|
||||
nextStage={nextStage}
|
||||
/>
|
||||
)}
|
||||
{!showLoader &&
|
||||
stage === 'account-onboarding' &&
|
||||
AccountOnboarding && (
|
||||
<AccountOnboarding
|
||||
accountName={name}
|
||||
onOnboardingComplete={onOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewAccount;
|
||||
3
trampoline/src/pages/App/pages/onboarding/index.ts
Normal file
3
trampoline/src/pages/App/pages/onboarding/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Onboarding from './onboarding';
|
||||
|
||||
export default Onboarding;
|
||||
81
trampoline/src/pages/App/pages/onboarding/intro.tsx
Normal file
81
trampoline/src/pages/App/pages/onboarding/intro.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Link,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import logo from '../../../../assets/img/logo.svg';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Intro = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{ height: '100%' }}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: 600,
|
||||
p: 2,
|
||||
border: '1px solid #d6d9dc',
|
||||
borderRadius: 5,
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography textAlign="center" variant="h3" gutterBottom>
|
||||
Start your eth journey
|
||||
</Typography>
|
||||
<Typography textAlign="center" variant="body1" color="text.secondary">
|
||||
Your smart contract account with unlimited possibilities,{' '}
|
||||
<Link href="https://github.com/eth-infinitism/trampoline">
|
||||
learn more
|
||||
</Link>
|
||||
</Typography>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ p: 5 }}
|
||||
>
|
||||
<img height={250} src={logo} className="App-logo" alt="logo" />
|
||||
</Box>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
sx={{ fontSize: 14 }}
|
||||
color="text.secondary"
|
||||
gutterBottom
|
||||
>
|
||||
Ethereum Foundation
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
|
||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={() => navigate('/accounts/new')}
|
||||
>
|
||||
Create/recover new account
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardActions>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Intro;
|
||||
26
trampoline/src/pages/App/pages/onboarding/onboarding.tsx
Normal file
26
trampoline/src/pages/App/pages/onboarding/onboarding.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Container } from '@mui/material';
|
||||
import React, { useEffect } from 'react';
|
||||
import { redirect } from 'react-router-dom';
|
||||
import { getAddressCount } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { useBackgroundSelector } from '../../hooks';
|
||||
import Intro from './intro';
|
||||
|
||||
const Onboarding = () => {
|
||||
const hasAccounts = useBackgroundSelector(
|
||||
(state) => getAddressCount(state) > 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAccounts) {
|
||||
redirect('/');
|
||||
}
|
||||
}, [hasAccounts]);
|
||||
|
||||
return (
|
||||
<Container sx={{ height: '100vh' }}>
|
||||
<Intro />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
3
trampoline/src/pages/App/pages/transfer-asset/index.ts
Normal file
3
trampoline/src/pages/App/pages/transfer-asset/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TransferAsset from './transfer-asset';
|
||||
|
||||
export default TransferAsset;
|
||||
139
trampoline/src/pages/App/pages/transfer-asset/transfer-asset.tsx
Normal file
139
trampoline/src/pages/App/pages/transfer-asset/transfer-asset.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Container,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React, { useCallback } from 'react';
|
||||
import Header from '../../components/header';
|
||||
import { ethers } from 'ethers';
|
||||
import { useBackgroundSelector } from '../../hooks';
|
||||
import { getActiveAccount } from '../../../Background/redux-slices/selectors/accountSelectors';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const TransferAsset = () => {
|
||||
const navigate = useNavigate();
|
||||
const [toAddress, setToAddress] = React.useState<string>('');
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
const [error, setError] = React.useState<string>('');
|
||||
const activeAccount = useBackgroundSelector(getActiveAccount);
|
||||
const [loader, setLoader] = React.useState<boolean>(false);
|
||||
|
||||
const sendEth = useCallback(async () => {
|
||||
if (!ethers.utils.isAddress(toAddress)) {
|
||||
setError('Invalid to address');
|
||||
return;
|
||||
}
|
||||
setLoader(true);
|
||||
setError('');
|
||||
|
||||
if (window.ethereum) {
|
||||
await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [
|
||||
{
|
||||
from: activeAccount,
|
||||
to: toAddress,
|
||||
data: '0x',
|
||||
value: ethers.utils.parseEther(value),
|
||||
},
|
||||
],
|
||||
});
|
||||
console.log(txHash);
|
||||
navigate('/');
|
||||
}
|
||||
setLoader(false);
|
||||
}, [activeAccount, navigate, toAddress, value]);
|
||||
|
||||
return (
|
||||
<Container sx={{ width: '62vw', height: '100vh' }}>
|
||||
<Header />
|
||||
<Card sx={{ ml: 4, mr: 4, mt: 2, mb: 2 }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.20)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Transfer Eth</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="div"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ mt: 4 }}
|
||||
>
|
||||
<FormGroup sx={{ p: 2, pt: 4 }}>
|
||||
<FormControl sx={{ m: 1, width: 300 }} variant="outlined">
|
||||
<InputLabel htmlFor="password">Send to</InputLabel>
|
||||
<OutlinedInput
|
||||
value={toAddress}
|
||||
onChange={(e) => setToAddress(e.target.value)}
|
||||
autoFocus
|
||||
label="Send to"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl sx={{ m: 1, width: 300 }} variant="outlined">
|
||||
<InputLabel htmlFor="password">Value</InputLabel>
|
||||
<OutlinedInput
|
||||
endAdornment={
|
||||
<InputAdornment position="end">ETH</InputAdornment>
|
||||
}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
label="Value"
|
||||
/>
|
||||
</FormControl>
|
||||
<Typography variant="body1" color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
<Button
|
||||
disabled={loader}
|
||||
onClick={sendEth}
|
||||
sx={{ mt: 4 }}
|
||||
size="large"
|
||||
variant="contained"
|
||||
>
|
||||
Send
|
||||
{loader && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransferAsset;
|
||||
35
trampoline/src/pages/App/protected-route.tsx
Normal file
35
trampoline/src/pages/App/protected-route.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { getAddressCount } from '../Background/redux-slices/selectors/accountSelectors';
|
||||
import { useAreKeyringsUnlocked, useBackgroundSelector } from './hooks';
|
||||
|
||||
export const ProtectedRouteHasAccounts = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactElement;
|
||||
}) => {
|
||||
const hasAccounts = useBackgroundSelector(
|
||||
(state) => getAddressCount(state) > 0
|
||||
);
|
||||
let location = useLocation();
|
||||
|
||||
if (!hasAccounts) {
|
||||
return (
|
||||
<Navigate to="/onboarding/intro" state={{ from: location }} replace />
|
||||
);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
export const ProtectedRouteKeyringUnlocked = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactElement;
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const areKeyringsUnlocked: boolean = useAreKeyringsUnlocked(true, pathname);
|
||||
|
||||
if (areKeyringsUnlocked) return children;
|
||||
return <></>;
|
||||
};
|
||||
19
trampoline/src/pages/Background/constants/constants.ts
Normal file
19
trampoline/src/pages/Background/constants/constants.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import AccountApi from '../../Account/account-api';
|
||||
import { AccountImplementationType } from '../../Account/account-api/types';
|
||||
import { ActiveAccountImplementation } from '../../Account/';
|
||||
|
||||
const AccountImplementation: AccountImplementationType = AccountApi;
|
||||
|
||||
const AccountImplementations: {
|
||||
[name: string]: AccountImplementationType;
|
||||
} = {
|
||||
[ActiveAccountImplementation]: AccountImplementation,
|
||||
};
|
||||
|
||||
export const PROVIDER_BRIDGE_TARGET = 'aa-extension-provider-bridge';
|
||||
export const WINDOW_PROVIDER_TARGET = 'aa-extension-window-provider';
|
||||
export const EXTERNAL_PORT_NAME = 'aa-extension-external';
|
||||
|
||||
export const AA_EXTENSION_CONFIG = 'aa-extension_getConfig';
|
||||
|
||||
export { ActiveAccountImplementation, AccountImplementations };
|
||||
1
trampoline/src/pages/Background/constants/index.ts
Normal file
1
trampoline/src/pages/Background/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
11
trampoline/src/pages/Background/index.ts
Normal file
11
trampoline/src/pages/Background/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import startMain from './main';
|
||||
/**
|
||||
* @metamask/browser-passworder uses window.crypto and since
|
||||
* background script is a service worker window is not available anymore.
|
||||
* Below is a quick but dirty fix for now.
|
||||
*/
|
||||
// global.window = {
|
||||
// crypto: crypto,
|
||||
// };
|
||||
|
||||
startMain();
|
||||
52
trampoline/src/pages/Background/main.ts
Normal file
52
trampoline/src/pages/Background/main.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { RootState } from './redux-slices';
|
||||
import KeyringService from './services/keyring';
|
||||
import MainServiceManager, {
|
||||
MainServiceManagerServicesMap,
|
||||
} from './services/main';
|
||||
import ProviderBridgeService from './services/provider-bridge';
|
||||
import Config from '../../exconfig';
|
||||
console.debug('---- LAUNCHING WITH CONFIG ----', Config);
|
||||
|
||||
chrome.runtime.onInstalled.addListener((e) => {
|
||||
if (e.reason === chrome.runtime.OnInstalledReason.INSTALL) {
|
||||
const url = chrome.runtime.getURL('app.html');
|
||||
chrome.tabs.create({
|
||||
url,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const serviceInitializer = async (
|
||||
mainServiceManager: MainServiceManager
|
||||
): Promise<MainServiceManagerServicesMap> => {
|
||||
const storeState: RootState = mainServiceManager.store.getState();
|
||||
|
||||
const keyringService = await KeyringService.create({
|
||||
mainServiceManager: mainServiceManager,
|
||||
initialState: storeState.keyrings.vault,
|
||||
provider: storeState.network.activeNetwork.provider || '',
|
||||
bundler: storeState.network.activeNetwork.bundler || '',
|
||||
entryPointAddress: storeState.network.activeNetwork.entryPointAddress,
|
||||
});
|
||||
|
||||
const providerBridgeService = await ProviderBridgeService.create({
|
||||
mainServiceManager,
|
||||
});
|
||||
|
||||
return {
|
||||
[KeyringService.name]: keyringService,
|
||||
[ProviderBridgeService.name]: providerBridgeService,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the API subsystems, including all services.
|
||||
*/
|
||||
export default async function startMain(): Promise<MainServiceManager> {
|
||||
const mainService = await MainServiceManager.create(
|
||||
'background',
|
||||
serviceInitializer
|
||||
);
|
||||
mainService.startService();
|
||||
return mainService.started();
|
||||
}
|
||||
189
trampoline/src/pages/Background/redux-slices/account.ts
Normal file
189
trampoline/src/pages/Background/redux-slices/account.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DomainName, HexString, URI } from '../types/common';
|
||||
import { EVMNetwork } from '../types/network';
|
||||
import { AccountBalance } from '../types/account';
|
||||
import { createBackgroundAsyncThunk } from './utils';
|
||||
import KeyringService from '../services/keyring';
|
||||
import { RootState } from '.';
|
||||
import { BigNumber } from 'ethers';
|
||||
|
||||
export type AccountData = {
|
||||
address: HexString;
|
||||
network: EVMNetwork;
|
||||
accountDeployed: boolean;
|
||||
minimumRequiredFunds: string;
|
||||
balances?: {
|
||||
[assetSymbol: string]: AccountBalance;
|
||||
};
|
||||
ens?: {
|
||||
name?: DomainName;
|
||||
avatarURL?: URI;
|
||||
};
|
||||
};
|
||||
|
||||
type AccountsByChainID = {
|
||||
[chainID: string]: {
|
||||
[address: string]: AccountData | 'loading';
|
||||
};
|
||||
};
|
||||
|
||||
export interface AccountState {
|
||||
account?: HexString;
|
||||
accountAdded: HexString | null;
|
||||
hasAccountError?: boolean;
|
||||
accountsData: {
|
||||
info: {
|
||||
[address: string]: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
evm: AccountsByChainID;
|
||||
};
|
||||
accountApiCallResult?: any;
|
||||
accountApiCallResultState?: 'awaiting' | 'set';
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
accountsData: { evm: {}, info: {} },
|
||||
accountAdded: null,
|
||||
combinedData: {
|
||||
totalMainCurrencyValue: '',
|
||||
assets: [],
|
||||
},
|
||||
} as AccountState;
|
||||
|
||||
const accountSlice = createSlice({
|
||||
name: 'account',
|
||||
initialState,
|
||||
reducers: {
|
||||
resetAccountAdded: (state): AccountState => ({
|
||||
...state,
|
||||
accountAdded: null,
|
||||
}),
|
||||
addNewAccount: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
name: string;
|
||||
makeActive: boolean;
|
||||
chainIds: Array<string>;
|
||||
address: string;
|
||||
};
|
||||
}
|
||||
): AccountState => ({
|
||||
...state,
|
||||
account: payload.makeActive ? payload.address : state.account,
|
||||
accountAdded: payload.address,
|
||||
accountsData: {
|
||||
info: {
|
||||
...state.accountsData.info,
|
||||
[payload.address]: {
|
||||
name: payload.name,
|
||||
},
|
||||
},
|
||||
evm: {
|
||||
...state.accountsData.evm,
|
||||
...payload.chainIds.reduce(
|
||||
(result: AccountsByChainID, chainId: string) => {
|
||||
result[chainId] = {
|
||||
[payload.address]: 'loading',
|
||||
};
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
setAccountApiCallResult: (
|
||||
state: AccountState,
|
||||
{ payload }: { payload: any }
|
||||
) => ({
|
||||
...state,
|
||||
accountApiCallResult: payload,
|
||||
}),
|
||||
setAccountApiCallResultState: (
|
||||
state: AccountState,
|
||||
{ payload }: { payload: 'awaiting' | 'set' }
|
||||
) => ({
|
||||
...state,
|
||||
accountApiCallResultState: payload,
|
||||
}),
|
||||
setAccountData: (
|
||||
state: AccountState,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: AccountData;
|
||||
}
|
||||
): AccountState => ({
|
||||
...state,
|
||||
accountsData: {
|
||||
...state.accountsData,
|
||||
evm: {
|
||||
...state.accountsData.evm,
|
||||
[payload.network.chainID]: {
|
||||
[payload.address]: payload,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const { addNewAccount, resetAccountAdded, setAccountData } =
|
||||
accountSlice.actions;
|
||||
export default accountSlice.reducer;
|
||||
|
||||
export const getAccountData = createBackgroundAsyncThunk(
|
||||
'account/getAccountData',
|
||||
async (address: string, { dispatch, extra: { mainServiceManager } }) => {
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
const activeNetwork = (mainServiceManager.store.getState() as RootState)
|
||||
.network.activeNetwork;
|
||||
keyringService.getAccountData(address, activeNetwork).then((accountData) =>
|
||||
dispatch(
|
||||
setAccountData({
|
||||
minimumRequiredFunds: accountData.minimumRequiredFunds,
|
||||
address: address,
|
||||
network: activeNetwork,
|
||||
accountDeployed: accountData.accountDeployed,
|
||||
balances: accountData.balances,
|
||||
ens: accountData.ens,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const callAccountApiThunk = createBackgroundAsyncThunk(
|
||||
'account/callAccountApiThunk',
|
||||
async (
|
||||
{
|
||||
address,
|
||||
functionName,
|
||||
args,
|
||||
}: { address: string; functionName: string; args?: any[] },
|
||||
{ dispatch, extra: { mainServiceManager } }
|
||||
) => {
|
||||
dispatch(accountSlice.actions.setAccountApiCallResultState('awaiting'));
|
||||
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
|
||||
const result = await keyringService.callAccountApi(
|
||||
address,
|
||||
functionName,
|
||||
args
|
||||
);
|
||||
|
||||
dispatch(accountSlice.actions.setAccountApiCallResult(result));
|
||||
dispatch(accountSlice.actions.setAccountApiCallResultState('set'));
|
||||
}
|
||||
);
|
||||
101
trampoline/src/pages/Background/redux-slices/index.ts
Normal file
101
trampoline/src/pages/Background/redux-slices/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import MainServiceManager from '../services/main';
|
||||
import { decodeJSON, encodeJSON } from '../utils';
|
||||
import { devToolsEnhancer } from '@redux-devtools/remote';
|
||||
import { combineReducers, configureStore, isPlain } from '@reduxjs/toolkit';
|
||||
import { alias } from 'webext-redux';
|
||||
import account from './account';
|
||||
import keyrings from './keyrings';
|
||||
import network from './network';
|
||||
import transactions from './transactions';
|
||||
import dappPermissions from './permissions';
|
||||
import signing from './signing';
|
||||
import { allAliases } from './utils';
|
||||
import Config from '../../../exconfig';
|
||||
import { debounce } from '@mui/material';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
account,
|
||||
keyrings,
|
||||
network,
|
||||
dappPermissions,
|
||||
signing,
|
||||
transactions,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
// This sanitizer runs on store and action data before serializing for remote
|
||||
// redux devtools. The goal is to end up with an object that is directly
|
||||
// JSON-serializable and deserializable; the remote end will display the
|
||||
// resulting objects without additional processing or decoding logic.
|
||||
const devToolsSanitizer = (input: unknown) => {
|
||||
switch (typeof input) {
|
||||
// We can make use of encodeJSON instead of recursively looping through
|
||||
// the input
|
||||
case 'bigint':
|
||||
case 'object':
|
||||
return JSON.parse(encodeJSON(input));
|
||||
// We only need to sanitize bigints and objects that may or may not contain
|
||||
// them.
|
||||
default:
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
const persistStoreFn = <T>(state: T) => {
|
||||
// Browser extension storage supports JSON natively, despite that we have
|
||||
// to stringify to preserve BigInts
|
||||
localStorage.setItem('state', encodeJSON(state));
|
||||
localStorage.setItem('version', Config.stateVersion);
|
||||
};
|
||||
|
||||
const persistStoreState = debounce(persistStoreFn, 50);
|
||||
|
||||
const reduxCache = (store) => (next) => (action) => {
|
||||
const result = next(action);
|
||||
const state = store.getState();
|
||||
|
||||
persistStoreState(state);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const initializeStore = (
|
||||
preloadedState,
|
||||
mainServiceManager: MainServiceManager
|
||||
) =>
|
||||
configureStore({
|
||||
preloadedState: preloadedState,
|
||||
reducer: rootReducer,
|
||||
devTools: false,
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
const middleware = getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
isSerializable: (value: unknown) =>
|
||||
isPlain(value) || typeof value === 'bigint',
|
||||
},
|
||||
thunk: { extraArgument: { mainServiceManager } },
|
||||
});
|
||||
|
||||
middleware.unshift(alias(allAliases));
|
||||
middleware.push(reduxCache);
|
||||
|
||||
return middleware;
|
||||
},
|
||||
enhancers:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? [
|
||||
devToolsEnhancer({
|
||||
hostname: 'localhost',
|
||||
port: 8000,
|
||||
realtime: true,
|
||||
actionSanitizer: devToolsSanitizer,
|
||||
stateSanitizer: devToolsSanitizer,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
});
|
||||
|
||||
export type ReduxStoreType = ReturnType<typeof initializeStore>;
|
||||
export type BackgroundDispatch = ReduxStoreType['dispatch'];
|
||||
124
trampoline/src/pages/Background/redux-slices/keyrings.ts
Normal file
124
trampoline/src/pages/Background/redux-slices/keyrings.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { Keyring, KeyringMetadata } from '../types/keyrings';
|
||||
import { createBackgroundAsyncThunk } from './utils';
|
||||
import { NewAccountView } from '../types/chrome-messages';
|
||||
import { RootState } from '.';
|
||||
import KeyringService from '../services/keyring';
|
||||
import { addNewAccount, getAccountData } from './account';
|
||||
import { EVMNetwork } from '../types/network';
|
||||
|
||||
export type Vault = {
|
||||
vault: string;
|
||||
encryptionKey?: string;
|
||||
encryptionSalt?: string;
|
||||
};
|
||||
|
||||
export type KeyringsState = {
|
||||
keyrings: Keyring[];
|
||||
keyringMetadata: {
|
||||
[keyringId: string]: KeyringMetadata;
|
||||
};
|
||||
importing: false | 'pending' | 'done';
|
||||
status: 'locked' | 'unlocked' | 'uninitialized';
|
||||
vault: Vault;
|
||||
keyringToVerify: {
|
||||
id: string;
|
||||
mnemonic: string[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
export const initialState: KeyringsState = {
|
||||
keyrings: [],
|
||||
keyringMetadata: {},
|
||||
vault: {
|
||||
vault:
|
||||
'{"data":"Ukexw7sD847Dj98jjvGP+USD","iv":"+X2ZjepqanEDFIJneBDHcw==","salt":"LWHFdiZSZwESRu0M5vBaeLIBwszt8zclfbUH4h8tWFU="}',
|
||||
encryptionKey:
|
||||
'{"alg":"A256GCM","ext":true,"k":"SnGTN4MUv2Ugv7wy_dGvb-Tmz-CKNnMYbyBHIfUbYJg","key_ops":["encrypt","decrypt"],"kty":"oct"}',
|
||||
encryptionSalt: 'LWHFdiZSZwESRu0M5vBaeLIBwszt8zclfbUH4h8tWFU=',
|
||||
},
|
||||
importing: false,
|
||||
status: 'uninitialized',
|
||||
keyringToVerify: null,
|
||||
};
|
||||
|
||||
const keyringsSlice = createSlice({
|
||||
name: 'account',
|
||||
initialState,
|
||||
reducers: {
|
||||
keyringLocked: (state) => ({
|
||||
...state,
|
||||
status: state.status !== 'uninitialized' ? 'locked' : 'uninitialized',
|
||||
vault: {
|
||||
vault: state.vault.vault,
|
||||
},
|
||||
}),
|
||||
keyringUnlocked: (state) => ({ ...state, status: 'unlocked' }),
|
||||
vaultUpdate: (
|
||||
state,
|
||||
{
|
||||
payload: vault,
|
||||
}: {
|
||||
payload: Vault;
|
||||
}
|
||||
) => ({
|
||||
...state,
|
||||
vault,
|
||||
status:
|
||||
!vault.encryptionKey && state.status !== 'uninitialized'
|
||||
? 'locked'
|
||||
: state.status,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const { keyringLocked, vaultUpdate, keyringUnlocked } =
|
||||
keyringsSlice.actions;
|
||||
export default keyringsSlice.reducer;
|
||||
|
||||
/**
|
||||
* -------------------------------
|
||||
* Background Actions
|
||||
* -------------------------------
|
||||
*/
|
||||
|
||||
export const initializeKeyring = createBackgroundAsyncThunk(
|
||||
'keyring/initialize',
|
||||
async (password: string, { dispatch, extra: { mainServiceManager } }) => {
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
await keyringService.createPassword(password);
|
||||
}
|
||||
);
|
||||
|
||||
export const createNewAccount = createBackgroundAsyncThunk(
|
||||
'keyring/createNewAccount',
|
||||
async (
|
||||
{
|
||||
name,
|
||||
implementation,
|
||||
context,
|
||||
chainIds,
|
||||
}: {
|
||||
name: string;
|
||||
chainIds: Array<string>;
|
||||
implementation: string;
|
||||
context?: any;
|
||||
},
|
||||
{ dispatch, extra: { mainServiceManager } }
|
||||
) => {
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
const address = await keyringService.addAccount(implementation, context);
|
||||
dispatch(
|
||||
addNewAccount({
|
||||
name,
|
||||
makeActive: true,
|
||||
chainIds: chainIds,
|
||||
address: address,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
46
trampoline/src/pages/Background/redux-slices/network.ts
Normal file
46
trampoline/src/pages/Background/redux-slices/network.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { EVMNetwork } from '../types/network';
|
||||
import Config from '../../../exconfig';
|
||||
|
||||
export type Vault = {
|
||||
vault: string;
|
||||
encryptionKey?: string;
|
||||
encryptionSalt?: string;
|
||||
};
|
||||
|
||||
export type NetworkState = {
|
||||
activeNetwork: EVMNetwork;
|
||||
supportedNetworks: Array<EVMNetwork>;
|
||||
};
|
||||
|
||||
const GoerliNetwork: EVMNetwork = Config.network || {
|
||||
chainID: '5',
|
||||
family: 'EVM',
|
||||
name: 'Goerli',
|
||||
provider: 'https://goerli.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
|
||||
entryPointAddress: '0x0F46c65C17AA6b4102046935F33301f0510B163A',
|
||||
bundler:
|
||||
'https://app.stackup.sh/api/v1/bundler/96771b1b09e802669c33a3fc50f517f0f514a40da6448e24640ecfd83263d336',
|
||||
baseAsset: {
|
||||
symbol: 'ETH',
|
||||
name: 'ETH',
|
||||
decimals: 18,
|
||||
image:
|
||||
'https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp',
|
||||
},
|
||||
};
|
||||
|
||||
export const initialState: NetworkState = {
|
||||
activeNetwork: GoerliNetwork,
|
||||
supportedNetworks: [GoerliNetwork],
|
||||
};
|
||||
|
||||
type NetworkReducers = {};
|
||||
|
||||
const networkSlice = createSlice<NetworkState, NetworkReducers, 'network'>({
|
||||
name: 'network',
|
||||
initialState,
|
||||
reducers: {},
|
||||
});
|
||||
|
||||
export default networkSlice.reducer;
|
||||
138
trampoline/src/pages/Background/redux-slices/permissions.ts
Normal file
138
trampoline/src/pages/Background/redux-slices/permissions.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import ProviderBridgeService, {
|
||||
PermissionRequest,
|
||||
} from '../services/provider-bridge';
|
||||
import { createBackgroundAsyncThunk } from './utils';
|
||||
|
||||
export type DappPermissionState = {
|
||||
permissionRequests: { [origin: string]: PermissionRequest };
|
||||
allowed: {
|
||||
evm: {
|
||||
[origin_address: string]: PermissionRequest | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const initialState: DappPermissionState = {
|
||||
permissionRequests: {},
|
||||
allowed: {
|
||||
evm: {},
|
||||
},
|
||||
};
|
||||
|
||||
type DappPermissionReducers = {
|
||||
requestPermission: (
|
||||
state: DappPermissionState,
|
||||
{ payload }: { payload: PermissionRequest }
|
||||
) => DappPermissionState;
|
||||
grantPermission: (
|
||||
state: DappPermissionState,
|
||||
{ payload }: { payload: PermissionRequest }
|
||||
) => DappPermissionState;
|
||||
permissionDenyOrRevoke: (
|
||||
state: DappPermissionState,
|
||||
{ payload }: { payload: PermissionRequest }
|
||||
) => DappPermissionState;
|
||||
};
|
||||
|
||||
const dappPermissionSlice = createSlice<
|
||||
DappPermissionState,
|
||||
DappPermissionReducers,
|
||||
'dapp-permission'
|
||||
>({
|
||||
name: 'dapp-permission',
|
||||
initialState,
|
||||
reducers: {
|
||||
requestPermission: (state, { payload: permissionRequest }) => {
|
||||
return {
|
||||
...state,
|
||||
permissionRequests: {
|
||||
...state.permissionRequests,
|
||||
[permissionRequest.origin]: { ...permissionRequest },
|
||||
},
|
||||
};
|
||||
},
|
||||
grantPermission: (state, { payload: permission }) => {
|
||||
return {
|
||||
...state,
|
||||
permissionRequests: {
|
||||
...state.permissionRequests,
|
||||
[permission.origin]: undefined,
|
||||
},
|
||||
allowed: {
|
||||
evm: {
|
||||
...state.allowed.evm,
|
||||
[`${permission.origin}_${permission.accountAddress}`]: permission,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
permissionDenyOrRevoke: (state, { payload: permission }) => {
|
||||
const permissionRequests = {
|
||||
...state.permissionRequests,
|
||||
};
|
||||
delete permissionRequests[permission.origin];
|
||||
|
||||
const allowed = {
|
||||
evm: {
|
||||
...state.allowed.evm,
|
||||
},
|
||||
};
|
||||
|
||||
delete allowed.evm[`${permission.origin}_${permission.accountAddress}`];
|
||||
|
||||
return {
|
||||
...state,
|
||||
permissionRequests: {
|
||||
...permissionRequests,
|
||||
},
|
||||
allowed: {
|
||||
...allowed,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { requestPermission } = dappPermissionSlice.actions;
|
||||
export default dappPermissionSlice.reducer;
|
||||
|
||||
export const denyOrRevokePermission = createBackgroundAsyncThunk(
|
||||
'dapp-permission/denyOrRevokePermission',
|
||||
async (
|
||||
permission: PermissionRequest,
|
||||
{ dispatch, extra: { mainServiceManager } }
|
||||
) => {
|
||||
const newPermission: PermissionRequest = {
|
||||
...permission,
|
||||
state: 'deny',
|
||||
};
|
||||
|
||||
dispatch(dappPermissionSlice.actions.permissionDenyOrRevoke(newPermission));
|
||||
const providerBridgeService = mainServiceManager.getService(
|
||||
ProviderBridgeService.name
|
||||
) as ProviderBridgeService;
|
||||
|
||||
providerBridgeService.denyOrRevokePermission(newPermission);
|
||||
}
|
||||
);
|
||||
|
||||
// Async thunk to bubble the permissionGrant action from store to emitter.
|
||||
export const grantPermission = createBackgroundAsyncThunk(
|
||||
'dapp-permission/permissionGrant',
|
||||
async (
|
||||
permission: PermissionRequest,
|
||||
{ dispatch, extra: { mainServiceManager } }
|
||||
) => {
|
||||
const newPermission: PermissionRequest = {
|
||||
...permission,
|
||||
state: 'allow',
|
||||
};
|
||||
dispatch(dappPermissionSlice.actions.grantPermission(newPermission));
|
||||
const providerBridgeService = mainServiceManager.getService(
|
||||
ProviderBridgeService.name
|
||||
) as ProviderBridgeService;
|
||||
|
||||
providerBridgeService.grantPermission(newPermission);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
|
||||
const getAccountState = (state: RootState) => state.account;
|
||||
|
||||
export const getAllAddresses = createSelector(getAccountState, (account) => {
|
||||
const ret = [
|
||||
...Array.from(
|
||||
new Set(
|
||||
Object.values(account.accountsData.evm).flatMap((chainAddresses) =>
|
||||
Object.keys(chainAddresses)
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
return ret;
|
||||
});
|
||||
|
||||
const getAccountsData = createSelector(
|
||||
getAccountState,
|
||||
(account) => account.accountsData
|
||||
);
|
||||
|
||||
export const getAccountsEVMData = createSelector(
|
||||
getAccountsData,
|
||||
(accountsData) => accountsData.evm
|
||||
);
|
||||
|
||||
export const getAddressCount = createSelector(
|
||||
getAllAddresses,
|
||||
(allAddresses) => allAddresses.length
|
||||
);
|
||||
|
||||
export const getAccountAdded = createSelector(
|
||||
getAccountState,
|
||||
(account) => account.accountAdded
|
||||
);
|
||||
|
||||
export const getActiveAccount = createSelector(
|
||||
getAccountState,
|
||||
(account) => account.account
|
||||
);
|
||||
|
||||
export const getAccountEVMData = createSelector(
|
||||
[
|
||||
getAccountsEVMData,
|
||||
(state, { chainId, address }: { chainId: string; address: string }) => ({
|
||||
chainId,
|
||||
address,
|
||||
}),
|
||||
],
|
||||
(evm, { chainId, address }) => evm[chainId][address]
|
||||
);
|
||||
|
||||
export const getAccountInfo = createSelector(
|
||||
[getAccountsData, (state, address) => address],
|
||||
(accountsData, address) => accountsData.info[address]
|
||||
);
|
||||
|
||||
export const getAccountApiCallResult = createSelector(
|
||||
getAccountState,
|
||||
(account) => ({
|
||||
accountApiCallResult: account.accountApiCallResult,
|
||||
accountApiCallResultState: account.accountApiCallResultState,
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
import { HexString } from '../../types/common';
|
||||
import { DappPermissionState } from '../permissions';
|
||||
|
||||
const getDappPermissionState = (state: RootState) => state.dappPermissions;
|
||||
|
||||
export const selectPermissionRequests = createSelector(
|
||||
getDappPermissionState,
|
||||
(slice: DappPermissionState) => Object.values(slice.permissionRequests)
|
||||
);
|
||||
|
||||
export const selectPendingPermissionRequests = createSelector(
|
||||
selectPermissionRequests,
|
||||
(permissionRequests) => {
|
||||
return permissionRequests.filter((p) => p.state === 'request');
|
||||
}
|
||||
);
|
||||
|
||||
export const selectCurrentPendingPermission = createSelector(
|
||||
selectPendingPermissionRequests,
|
||||
(permissionRequests) => {
|
||||
return permissionRequests.length > 0 ? permissionRequests[0] : undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectOriginPermissionState = createSelector(
|
||||
getDappPermissionState,
|
||||
(slice: DappPermissionState) => slice.allowed.evm
|
||||
);
|
||||
|
||||
export const selectCurrentOriginPermission = createSelector(
|
||||
[
|
||||
selectOriginPermissionState,
|
||||
(state, { origin, address }: { origin: string; address: HexString }) => ({
|
||||
origin,
|
||||
address,
|
||||
}),
|
||||
],
|
||||
(evmState: DappPermissionState['allowed']['evm'], { origin, address }) =>
|
||||
evmState[`${origin}_${address}`]
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
import { KeyringsState } from '../keyrings';
|
||||
|
||||
export const selectKeyrings = createSelector(
|
||||
(state: RootState) => state.keyrings,
|
||||
(keyrings) => keyrings
|
||||
);
|
||||
|
||||
export const selectLoadedKeyrings = createSelector(
|
||||
selectKeyrings,
|
||||
(keyrings) => keyrings.keyrings
|
||||
);
|
||||
|
||||
export const selectKeyringStatus = createSelector(
|
||||
selectKeyrings,
|
||||
(keyrings) => (keyrings as KeyringsState).status
|
||||
);
|
||||
|
||||
export const selectKeyringVault = createSelector(
|
||||
selectKeyrings,
|
||||
(keyrings) => (keyrings as KeyringsState).vault
|
||||
);
|
||||
|
||||
export const selectKeyringView = createSelector(
|
||||
[selectKeyrings, (state, implementation: string) => implementation],
|
||||
(keyrings, implementation: string) =>
|
||||
(keyrings as KeyringsState).keyringMetadata[implementation]
|
||||
? (keyrings as KeyringsState).keyringMetadata[implementation].view
|
||||
: undefined
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
|
||||
const getNetworkState = (state: RootState) => state.network;
|
||||
|
||||
export const getActiveNetwork = createSelector(
|
||||
getNetworkState,
|
||||
(network) => network.activeNetwork
|
||||
);
|
||||
|
||||
export const getSupportedNetworks = createSelector(
|
||||
getNetworkState,
|
||||
(network) => network.supportedNetworks
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
|
||||
const getSigningState = (state: RootState) => state.signing;
|
||||
|
||||
export const selectCurrentPendingSignDataRequest = createSelector(
|
||||
getSigningState,
|
||||
(signing) => {
|
||||
return signing.signDataRequest;
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '..';
|
||||
|
||||
const getTransactionsState = (state: RootState) => state.transactions;
|
||||
|
||||
export const selectCurrentPendingSendTransactionRequest = createSelector(
|
||||
getTransactionsState,
|
||||
(transactions) => ({
|
||||
transactionRequest: transactions.transactionRequest,
|
||||
origin: transactions.requestOrigin,
|
||||
})
|
||||
);
|
||||
|
||||
export const selectCurrentPendingSendTransactionUserOp = createSelector(
|
||||
getTransactionsState,
|
||||
(transactions) => transactions.unsignedUserOperation
|
||||
);
|
||||
231
trampoline/src/pages/Background/redux-slices/signing.ts
Normal file
231
trampoline/src/pages/Background/redux-slices/signing.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from '.';
|
||||
import KeyringService from '../services/keyring';
|
||||
import ProviderBridgeService from '../services/provider-bridge';
|
||||
import { HexString } from '../types/common';
|
||||
import { AddressOnNetwork, EVMNetwork } from '../types/network';
|
||||
import { createBackgroundAsyncThunk } from './utils';
|
||||
|
||||
export type Vault = {
|
||||
vault: string;
|
||||
encryptionKey?: string;
|
||||
encryptionSalt?: string;
|
||||
};
|
||||
|
||||
export type NetworkState = {
|
||||
activeNetwork: EVMNetwork;
|
||||
supportedNetworks: Array<EVMNetwork>;
|
||||
};
|
||||
|
||||
const GoerliNetwork: EVMNetwork = {
|
||||
chainID: '5',
|
||||
family: 'EVM',
|
||||
name: 'Goerli',
|
||||
provider: 'https://goerli.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
|
||||
entryPointAddress: '0x0F46c65C17AA6b4102046935F33301f0510B163A',
|
||||
baseAsset: {
|
||||
symbol: 'ETH',
|
||||
name: 'ETH',
|
||||
decimals: 18,
|
||||
image:
|
||||
'https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp',
|
||||
},
|
||||
};
|
||||
|
||||
export type EIP712DomainType = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
chainId?: number | string;
|
||||
verifyingContract?: HexString;
|
||||
};
|
||||
|
||||
export interface TypedDataField {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type EIP712TypedData<T = Record<string, unknown>> = {
|
||||
domain: EIP712DomainType;
|
||||
types: Record<string, TypedDataField[]>;
|
||||
message: T;
|
||||
primaryType: string;
|
||||
};
|
||||
|
||||
export type SignTypedDataRequest = {
|
||||
account: AddressOnNetwork;
|
||||
typedData: EIP712TypedData;
|
||||
};
|
||||
|
||||
export type EIP2612SignTypedDataAnnotation = {
|
||||
type: 'EIP-2612';
|
||||
source: string;
|
||||
displayFields: {
|
||||
owner: string;
|
||||
tokenContract: string;
|
||||
spender: string;
|
||||
value: string;
|
||||
nonce: number;
|
||||
expiry: string;
|
||||
token?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SignTypedDataAnnotation = EIP2612SignTypedDataAnnotation;
|
||||
|
||||
export type EnrichedSignTypedDataRequest = SignTypedDataRequest & {
|
||||
annotation?: SignTypedDataAnnotation;
|
||||
};
|
||||
|
||||
export type EIP191Data = string;
|
||||
|
||||
// spec found https://eips.ethereum.org/EIPS/eip-4361
|
||||
export interface EIP4361Data {
|
||||
/**
|
||||
* The message string that was parsed to produce this EIP-4361 data.
|
||||
*/
|
||||
unparsedMessageData: string;
|
||||
domain: string;
|
||||
address: string;
|
||||
version: string;
|
||||
chainId: number;
|
||||
nonce: string;
|
||||
expiration?: string;
|
||||
statement?: string;
|
||||
}
|
||||
|
||||
type EIP191SigningData = {
|
||||
messageType: 'eip191';
|
||||
signingData: EIP191Data;
|
||||
};
|
||||
|
||||
type EIP4361SigningData = {
|
||||
messageType: 'eip4361';
|
||||
signingData: EIP4361Data;
|
||||
};
|
||||
|
||||
export type MessageSigningData = EIP191SigningData | EIP4361SigningData;
|
||||
|
||||
export type MessageSigningRequest<
|
||||
T extends MessageSigningData = MessageSigningData
|
||||
> = T & {
|
||||
origin: string;
|
||||
account: AddressOnNetwork;
|
||||
rawSigningData: string;
|
||||
};
|
||||
|
||||
type SigningState = {
|
||||
signedTypedData: string | undefined;
|
||||
typedDataRequest: EnrichedSignTypedDataRequest | undefined;
|
||||
|
||||
signedData: string | undefined;
|
||||
signDataRequest: MessageSigningRequest | undefined;
|
||||
};
|
||||
|
||||
export const initialState: SigningState = {
|
||||
typedDataRequest: undefined,
|
||||
signedTypedData: undefined,
|
||||
|
||||
signedData: undefined,
|
||||
signDataRequest: undefined,
|
||||
};
|
||||
|
||||
type SigningReducers = {
|
||||
signedTypedData: (
|
||||
state: SigningState,
|
||||
{ payload }: { payload: string }
|
||||
) => SigningState;
|
||||
typedDataRequest: (
|
||||
state: SigningState,
|
||||
{ payload }: { payload: EnrichedSignTypedDataRequest }
|
||||
) => SigningState;
|
||||
signDataRequest: (
|
||||
state: SigningState,
|
||||
{ payload }: { payload: MessageSigningRequest }
|
||||
) => SigningState;
|
||||
signedData: (
|
||||
state: SigningState,
|
||||
{ payload }: { payload: string }
|
||||
) => SigningState;
|
||||
clearSigningState: (state: SigningState) => SigningState;
|
||||
};
|
||||
|
||||
const signingSlice = createSlice<SigningState, SigningReducers, 'signing'>({
|
||||
name: 'signing',
|
||||
initialState,
|
||||
reducers: {
|
||||
signedTypedData: (state, { payload }: { payload: string }) => ({
|
||||
...state,
|
||||
signedTypedData: payload,
|
||||
typedDataRequest: undefined,
|
||||
}),
|
||||
typedDataRequest: (
|
||||
state,
|
||||
{ payload }: { payload: EnrichedSignTypedDataRequest }
|
||||
) => ({
|
||||
...state,
|
||||
typedDataRequest: payload,
|
||||
}),
|
||||
signDataRequest: (
|
||||
state,
|
||||
{ payload }: { payload: MessageSigningRequest }
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
signDataRequest: payload,
|
||||
};
|
||||
},
|
||||
signedData: (state, { payload }: { payload: string }) => ({
|
||||
...state,
|
||||
signedData: payload,
|
||||
signDataRequest: undefined,
|
||||
}),
|
||||
clearSigningState: (state) => ({
|
||||
...state,
|
||||
typedDataRequest: undefined,
|
||||
signDataRequest: undefined,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
signedTypedData,
|
||||
typedDataRequest,
|
||||
signedData,
|
||||
signDataRequest,
|
||||
clearSigningState,
|
||||
} = signingSlice.actions;
|
||||
|
||||
export default signingSlice.reducer;
|
||||
|
||||
export const getSignedData = createBackgroundAsyncThunk(
|
||||
'signing/getSignedData',
|
||||
async (context: any, { dispatch, extra: { mainServiceManager } }) => {
|
||||
const pendingSigningDataRequest = (
|
||||
mainServiceManager.store.getState() as RootState
|
||||
).signing.signDataRequest;
|
||||
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
|
||||
const activeAccount = (mainServiceManager.store.getState() as RootState)
|
||||
.account.account;
|
||||
|
||||
const signedMessage = await keyringService.personalSign(
|
||||
activeAccount || '',
|
||||
context,
|
||||
pendingSigningDataRequest
|
||||
);
|
||||
|
||||
dispatch(signedData(signedMessage));
|
||||
|
||||
const providerBridgeService = mainServiceManager.getService(
|
||||
ProviderBridgeService.name
|
||||
) as ProviderBridgeService;
|
||||
|
||||
providerBridgeService.resolveRequest(
|
||||
pendingSigningDataRequest?.origin || '',
|
||||
signedMessage
|
||||
);
|
||||
}
|
||||
);
|
||||
228
trampoline/src/pages/Background/redux-slices/transactions.ts
Normal file
228
trampoline/src/pages/Background/redux-slices/transactions.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from '.';
|
||||
import KeyringService from '../services/keyring';
|
||||
import ProviderBridgeService, {
|
||||
EthersTransactionRequest,
|
||||
} from '../services/provider-bridge';
|
||||
import { createBackgroundAsyncThunk } from './utils';
|
||||
|
||||
export type TransactionState = {
|
||||
transactionRequest?: EthersTransactionRequest;
|
||||
transactionsRequest?: EthersTransactionRequest[];
|
||||
modifiedTransactionsRequest?: EthersTransactionRequest[];
|
||||
|
||||
requestOrigin?: string;
|
||||
userOperationRequest?: Partial<UserOperationStruct>;
|
||||
unsignedUserOperation?: UserOperationStruct;
|
||||
};
|
||||
|
||||
export const initialState: TransactionState = {
|
||||
transactionsRequest: undefined,
|
||||
transactionRequest: undefined,
|
||||
userOperationRequest: undefined,
|
||||
unsignedUserOperation: undefined,
|
||||
};
|
||||
|
||||
type SigningReducers = {
|
||||
sendTransactionRequest: (
|
||||
state: TransactionState,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
transactionRequest: EthersTransactionRequest;
|
||||
origin: string;
|
||||
};
|
||||
}
|
||||
) => TransactionState;
|
||||
sendTransactionsRequest: (
|
||||
state: TransactionState,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: {
|
||||
transactionsRequest: EthersTransactionRequest[];
|
||||
origin: string;
|
||||
};
|
||||
}
|
||||
) => TransactionState;
|
||||
setModifyTransactionsRequest: (
|
||||
state: TransactionState,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: EthersTransactionRequest[];
|
||||
}
|
||||
) => TransactionState;
|
||||
sendUserOperationRquest: (
|
||||
state: TransactionState,
|
||||
{ payload }: { payload: UserOperationStruct }
|
||||
) => TransactionState;
|
||||
setUnsignedUserOperation: (
|
||||
state: TransactionState,
|
||||
{ payload }: { payload: UserOperationStruct }
|
||||
) => TransactionState;
|
||||
clearTransactionState: (state: TransactionState) => TransactionState;
|
||||
};
|
||||
|
||||
const transactionsSlice = createSlice<
|
||||
TransactionState,
|
||||
SigningReducers,
|
||||
'signing'
|
||||
>({
|
||||
name: 'signing',
|
||||
initialState,
|
||||
reducers: {
|
||||
sendTransactionRequest: (
|
||||
state,
|
||||
{
|
||||
payload: { transactionRequest, origin },
|
||||
}: {
|
||||
payload: {
|
||||
transactionRequest: EthersTransactionRequest;
|
||||
origin: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
transactionRequest: transactionRequest,
|
||||
requestOrigin: origin,
|
||||
};
|
||||
},
|
||||
sendTransactionsRequest: (
|
||||
state,
|
||||
{
|
||||
payload: { transactionsRequest, origin },
|
||||
}: {
|
||||
payload: {
|
||||
transactionsRequest: EthersTransactionRequest[];
|
||||
origin: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
transactionsRequest: transactionsRequest,
|
||||
requestOrigin: origin,
|
||||
};
|
||||
},
|
||||
setModifyTransactionsRequest: (
|
||||
state,
|
||||
{
|
||||
payload,
|
||||
}: {
|
||||
payload: EthersTransactionRequest[];
|
||||
}
|
||||
) => ({
|
||||
...state,
|
||||
modifiedTransactionsRequest: payload,
|
||||
}),
|
||||
sendUserOperationRquest: (
|
||||
state,
|
||||
{ payload }: { payload: UserOperationStruct }
|
||||
) => ({
|
||||
...state,
|
||||
userOperationRequest: payload,
|
||||
}),
|
||||
setUnsignedUserOperation: (
|
||||
state,
|
||||
{ payload }: { payload: UserOperationStruct }
|
||||
) => ({
|
||||
...state,
|
||||
unsignedUserOperation: payload,
|
||||
}),
|
||||
clearTransactionState: (state) => ({
|
||||
...state,
|
||||
typedDataRequest: undefined,
|
||||
signDataRequest: undefined,
|
||||
transactionRequest: undefined,
|
||||
transactionsRequest: undefined,
|
||||
modifiedTransactionsRequest: undefined,
|
||||
requestOrigin: undefined,
|
||||
userOperationRequest: undefined,
|
||||
unsignedUserOperation: undefined,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
sendTransactionRequest,
|
||||
sendTransactionsRequest,
|
||||
setModifyTransactionsRequest,
|
||||
sendUserOperationRquest,
|
||||
setUnsignedUserOperation,
|
||||
clearTransactionState,
|
||||
} = transactionsSlice.actions;
|
||||
|
||||
export default transactionsSlice.reducer;
|
||||
|
||||
export const sendTransaction = createBackgroundAsyncThunk(
|
||||
'transactions/sendTransaction',
|
||||
async (
|
||||
{ address, context }: { address: string; context?: any },
|
||||
{ dispatch, extra: { mainServiceManager } }
|
||||
) => {
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
|
||||
const state = mainServiceManager.store.getState() as RootState;
|
||||
const unsignedUserOp = state.transactions.unsignedUserOperation;
|
||||
const origin = state.transactions.requestOrigin;
|
||||
|
||||
if (unsignedUserOp) {
|
||||
const signedUserOp = await keyringService.signUserOpWithContext(
|
||||
address,
|
||||
unsignedUserOp,
|
||||
context
|
||||
);
|
||||
const txnHash = keyringService.sendUserOp(address, signedUserOp);
|
||||
|
||||
dispatch(clearTransactionState());
|
||||
|
||||
const providerBridgeService = mainServiceManager.getService(
|
||||
ProviderBridgeService.name
|
||||
) as ProviderBridgeService;
|
||||
|
||||
providerBridgeService.resolveRequest(origin || '', txnHash);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const createUnsignedUserOp = createBackgroundAsyncThunk(
|
||||
'transactions/createUnsignedUserOp',
|
||||
async (address: string, { dispatch, extra: { mainServiceManager } }) => {
|
||||
const keyringService = mainServiceManager.getService(
|
||||
KeyringService.name
|
||||
) as KeyringService;
|
||||
|
||||
const state = mainServiceManager.store.getState() as RootState;
|
||||
const transactionRequest = state.transactions.transactionRequest;
|
||||
|
||||
if (transactionRequest) {
|
||||
const userOp = await keyringService.createUnsignedUserOp(
|
||||
address,
|
||||
transactionRequest
|
||||
);
|
||||
dispatch(setUnsignedUserOperation(userOp));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const rejectTransaction = createBackgroundAsyncThunk(
|
||||
'transactions/rejectTransaction',
|
||||
async (address: string, { dispatch, extra: { mainServiceManager } }) => {
|
||||
dispatch(clearTransactionState());
|
||||
|
||||
const requestOrigin = (mainServiceManager.store.getState() as RootState)
|
||||
.transactions.requestOrigin;
|
||||
|
||||
const providerBridgeService = mainServiceManager.getService(
|
||||
ProviderBridgeService.name
|
||||
) as ProviderBridgeService;
|
||||
|
||||
providerBridgeService.rejectRequest(requestOrigin || '', '');
|
||||
}
|
||||
);
|
||||
171
trampoline/src/pages/Background/redux-slices/utils.ts
Normal file
171
trampoline/src/pages/Background/redux-slices/utils.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import MainServiceManager from '../services/main';
|
||||
import {
|
||||
Action,
|
||||
AsyncThunk,
|
||||
AsyncThunkAction,
|
||||
AsyncThunkOptions,
|
||||
AsyncThunkPayloadCreator,
|
||||
createAsyncThunk,
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
// Below, we use `any` to deal with the fact that allAliases is a heterogeneous
|
||||
// collection of async thunk actions whose payload types have little in common
|
||||
// with each other.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* A list of all webext-redux aliases that have been registered globally. These
|
||||
* are generally updated automatically by helpers like
|
||||
* `createBackgroundAsyncThunk` and should rarely need to be touched directly.
|
||||
*
|
||||
* webext-redux aliases are actions that are only run in the background script,
|
||||
* but can be invoked with a payload in UI and other scripts. Their type and
|
||||
* payload is relayed to the background, and the background uses an enriched
|
||||
* action creator to turn them into the final intent. They are primarily used
|
||||
* for more complex actions that require middleware to process, such as thunks.
|
||||
*
|
||||
* @see {@link createBackgroundAsyncThunk} for an example use.
|
||||
*/
|
||||
export const allAliases: Record<
|
||||
string,
|
||||
(action: {
|
||||
type: string;
|
||||
payload: any;
|
||||
}) => AsyncThunkAction<unknown, unknown, any>
|
||||
> = {};
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// All props of an AsyncThunk.
|
||||
type AsyncThunkProps = keyof AsyncThunk<
|
||||
unknown,
|
||||
unknown,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
|
||||
// The type system will make sure we've listed all additional props that redux
|
||||
// toolkit adds to the AsyncThunk action creator below.
|
||||
//
|
||||
// The approach is a bit ugly, but the goal here is transparent usage wrt
|
||||
// createAsyncThunk, and this allows ensuring that any future upgrades don't
|
||||
// break expectations without breaking the compile.
|
||||
type ExhaustivePropList<PropListType, TargetType> =
|
||||
PropListType extends readonly (infer T)[]
|
||||
? keyof TargetType extends T
|
||||
? readonly T[]
|
||||
: never
|
||||
: never;
|
||||
const asyncThunkProperties = (() => {
|
||||
const temp = ['typePrefix', 'pending', 'rejected', 'fulfilled'] as const;
|
||||
|
||||
const exhaustiveList: ExhaustivePropList<
|
||||
typeof temp,
|
||||
AsyncThunk<unknown, unknown, Record<string, unknown>>
|
||||
> = temp;
|
||||
|
||||
return exhaustiveList;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Create an async thunk action that will always run in the background script,
|
||||
* and dispatches lifecycle actions (pending, fulfilled, rejected) on the
|
||||
* shared store. The lifecycle actions are observable on synced non-background
|
||||
* stores.
|
||||
*
|
||||
* NOTE: To ensure the action is handled correctly, a central location should
|
||||
* add the webext-redux `alias` middleware to the background store, referencing
|
||||
* the `allAliases` variable in this module. This variable exposes all async
|
||||
* thunks for use with the `alias` middleware.
|
||||
*
|
||||
* @see {@link createAsyncThunk} for more information on the `options` parameter
|
||||
* and the `pending`, `rejected`, and `fulfilled` properties on the
|
||||
* returned action creator. Also note that the async thunk action creator
|
||||
* returned directly by `createAsyncThunk` is not directly exposed.
|
||||
*
|
||||
* @param typePrefix This is both the name of the action that starts the thunk
|
||||
* process, and the prefix for the three generated actions that update the
|
||||
* thunk status---`pending`, `rejected`, and `fulfilled`---based on the
|
||||
* payload creator's promise status.
|
||||
* @param payloadCreator A function that will always run in the background
|
||||
* script; this takes the action payload and runs an async action whose
|
||||
* result is eventually dispatched normally into the redux store. When
|
||||
* the function is initially invoked, the `typePrefix`-pending action is
|
||||
* dispatched; if the function's returned promise resolves,
|
||||
* `typePrefix`-fulfilled or `typePrefix`-rejected is dispatched on the
|
||||
* store depending on the promise's settled status. When -fulfilled is
|
||||
* dispatched, the payload is the fulfilled value of the promise.
|
||||
* @param options Additional options specified by `createAsyncThunk`, including
|
||||
* conditions for executing the thunk vs not.
|
||||
*
|
||||
* @return A function that takes the payload and returns a plain action for
|
||||
* dispatching on the background store. This function has four
|
||||
* additional properties, which are the same as those returned by
|
||||
* `createAsyncThunk`.
|
||||
*/
|
||||
export function createBackgroundAsyncThunk<
|
||||
TypePrefix extends string,
|
||||
Returned,
|
||||
ThunkArg = void,
|
||||
ThunkApiConfig = {
|
||||
extra: { mainServiceManager: MainServiceManager };
|
||||
}
|
||||
>(
|
||||
typePrefix: TypePrefix,
|
||||
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
|
||||
options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
|
||||
): ((payload: ThunkArg) => Action<TypePrefix> & { payload: ThunkArg }) &
|
||||
Pick<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>, AsyncThunkProps> {
|
||||
// Exit early if this type prefix is already aliased for handling in the
|
||||
// background script.
|
||||
if (allAliases[typePrefix]) {
|
||||
throw new Error('Attempted to register an alias twice.');
|
||||
}
|
||||
|
||||
// Use reduxtools' createAsyncThunk to build the infrastructure.
|
||||
const baseThunkActionCreator = createAsyncThunk(
|
||||
typePrefix,
|
||||
async (...args: Parameters<typeof payloadCreator>) => {
|
||||
try {
|
||||
return await payloadCreator(...args);
|
||||
} catch (error) {
|
||||
console.error('Async thunk failed', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
// Wrap the top-level action creator to make it compatible with webext-redux.
|
||||
const webextActionCreator = Object.assign(
|
||||
(payload: ThunkArg) => ({
|
||||
type: typePrefix,
|
||||
payload,
|
||||
}),
|
||||
// Copy the utility props on the redux-tools version to our version.
|
||||
Object.fromEntries(
|
||||
asyncThunkProperties.map((prop) => [prop, baseThunkActionCreator[prop]])
|
||||
) as Pick<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>, AsyncThunkProps>
|
||||
);
|
||||
|
||||
// Register the alias to ensure it will always get proxied back to the
|
||||
// background script, where we will run our proxy action creator to fire off
|
||||
// the thunk correctly.
|
||||
allAliases[typePrefix] = (action: { type: string; payload: ThunkArg }) =>
|
||||
baseThunkActionCreator(action.payload);
|
||||
|
||||
return webextActionCreator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility type to extract the fulfillment type of an async thunk. Useful when
|
||||
* wanting to declare something as "the type that this thunk will return once
|
||||
* it completes".
|
||||
*/
|
||||
export type AsyncThunkFulfillmentType<T> = T extends Pick<
|
||||
// We don't really need the other two inferred values.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
AsyncThunk<infer Returned, infer _1, infer _2>,
|
||||
'fulfilled'
|
||||
>
|
||||
? Returned
|
||||
: never;
|
||||
|
||||
export const noopAction = createBackgroundAsyncThunk('noop', () => {});
|
||||
208
trampoline/src/pages/Background/services/base.ts
Normal file
208
trampoline/src/pages/Background/services/base.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import Emittery from 'emittery';
|
||||
import { Alarms } from 'webextension-polyfill';
|
||||
import MainServiceManager from './main';
|
||||
import { Service, ServiceLifecycleEvents } from './types';
|
||||
|
||||
/**
|
||||
* An alarm schedule for use in the `browser.alarms` API.
|
||||
*
|
||||
* Note that even if `periodInMinutes` is less than 1, the alarm will only fire
|
||||
* a maximum of once a minute in Chrome for a packaged extension. When an
|
||||
* extension is loaded unpacked (from a directory for development), periods
|
||||
* less than 1 minute are respected across browsers.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/alarms/create|
|
||||
* The MDN docs for `alarms.create`}.
|
||||
*/
|
||||
type AlarmSchedule =
|
||||
| {
|
||||
when: number;
|
||||
periodInMinutes?: number;
|
||||
}
|
||||
| {
|
||||
delayInMinutes: number;
|
||||
periodInMinutes?: number;
|
||||
}
|
||||
| { periodInMinutes: number };
|
||||
|
||||
/**
|
||||
* An object carrying the same information as {@link AlarmSchedule}, but that
|
||||
* also provides a handler to handle the specified alarm. Designed for use with
|
||||
* {@link AlarmHandlerScheduleMap}, which allows for disambiguating between
|
||||
* different alarms.
|
||||
*
|
||||
* Also provides an optional `runAtStart` property that will immediately fire
|
||||
* the handler at service start for the first time instead of waiting for the
|
||||
* first scheduled run to execute.
|
||||
*/
|
||||
export type AlarmHandlerSchedule = {
|
||||
schedule: AlarmSchedule;
|
||||
handler: (alarm?: Alarms.Alarm) => void;
|
||||
runAtStart?: boolean;
|
||||
};
|
||||
|
||||
/*
|
||||
* An object mapping alarm names to their designated schedules. Alarm names are
|
||||
* used to disambiguate between different alarms when they are fired, so as to
|
||||
* fire the handler associated with the appropriate alarm.
|
||||
*/
|
||||
export type AlarmHandlerScheduleMap = {
|
||||
[alarmName: string]: AlarmHandlerSchedule;
|
||||
};
|
||||
|
||||
export type BaseServiceCreateProps = {
|
||||
mainServiceManager?: MainServiceManager;
|
||||
};
|
||||
|
||||
export default abstract class BaseService<Events extends ServiceLifecycleEvents>
|
||||
implements Service<Events>
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc Service.emitter}
|
||||
*/
|
||||
readonly emitter = new Emittery<Events>();
|
||||
|
||||
/**
|
||||
* Takes the set of alarm schedules that this service wants to run. Schedules
|
||||
* are not added until `startService` is called.
|
||||
*/
|
||||
protected constructor(
|
||||
protected readonly alarmSchedules: AlarmHandlerScheduleMap = {}
|
||||
) {}
|
||||
|
||||
private serviceState: 'unstarted' | 'started' | 'stopped' = 'unstarted';
|
||||
|
||||
/**
|
||||
* {@inheritdoc Service.started}
|
||||
*
|
||||
* @throws {Error} If the service has already been stopped.
|
||||
*/
|
||||
readonly started = async (): Promise<this> => {
|
||||
switch (this.serviceState) {
|
||||
case 'started':
|
||||
return this;
|
||||
|
||||
case 'stopped':
|
||||
throw new Error('Service is already stopped and cannot be restarted.');
|
||||
|
||||
case 'unstarted':
|
||||
return this.emitter.once('serviceStarted').then(() => this);
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = this.serviceState;
|
||||
throw new Error(`Unreachable code: ${exhaustiveCheck}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abstract _startService(): Promise<void>;
|
||||
abstract _stopService(): Promise<void>;
|
||||
|
||||
/**
|
||||
* {@inheritdoc Service.startService}
|
||||
*
|
||||
* Subclasses should extend `internalStartService` to handle additional
|
||||
* starting tasks.
|
||||
*
|
||||
* @throws {Error} If the service has already been stopped.
|
||||
*
|
||||
* @sealed
|
||||
*/
|
||||
readonly startService = async (): Promise<void> => {
|
||||
switch (this.serviceState) {
|
||||
case 'started':
|
||||
return;
|
||||
|
||||
case 'stopped':
|
||||
throw new Error('Service is already stopped and cannot be restarted.');
|
||||
|
||||
case 'unstarted':
|
||||
this.serviceState = 'started';
|
||||
await this.internalStartService();
|
||||
await this._startService();
|
||||
this.emitter.emit('serviceStarted', undefined);
|
||||
break;
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = this.serviceState;
|
||||
throw new Error(`Unreachable code: ${exhaustiveCheck}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for subclass starting tasks. Subclasses should call
|
||||
* `await super.internalStartService()`, as the base implementation sets up
|
||||
* all alarms and their handling.
|
||||
*/
|
||||
protected async internalStartService(): Promise<void> {
|
||||
const scheduleEntries = Object.entries(this.alarmSchedules);
|
||||
|
||||
scheduleEntries.forEach(([name, { schedule, runAtStart, handler }]) => {
|
||||
chrome.alarms.create(name, schedule);
|
||||
|
||||
if (runAtStart) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
|
||||
if (scheduleEntries.length > 0) {
|
||||
chrome.alarms.onAlarm.addListener(this.handleAlarm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for subclass stopping tasks. Subclasses should call
|
||||
* `await super.internalStopService()`, as the base implementation cleans up
|
||||
* all alarms and their handling.
|
||||
*/
|
||||
protected async internalStopService(): Promise<void> {
|
||||
const scheduleNames = Object.keys(this.alarmSchedules);
|
||||
|
||||
scheduleNames.forEach((alarmName) => chrome.alarms.clear(alarmName));
|
||||
|
||||
if (scheduleNames.length > 0) {
|
||||
chrome.alarms.onAlarm.removeListener(this.handleAlarm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Service.stopService}
|
||||
*
|
||||
* Subclasses should extend `internalStopService` to handle additional
|
||||
* stopping tasks.
|
||||
*
|
||||
* @throws {Error} If the service has never been started.
|
||||
*
|
||||
* @sealed
|
||||
*/
|
||||
readonly stopService = async (): Promise<void> => {
|
||||
switch (this.serviceState) {
|
||||
case 'unstarted':
|
||||
throw new Error('Attempted to stop a service that was never started.');
|
||||
|
||||
case 'stopped':
|
||||
return;
|
||||
|
||||
case 'started':
|
||||
this.serviceState = 'stopped';
|
||||
await this.internalStopService();
|
||||
await this._stopService();
|
||||
this.emitter.emit('serviceStopped', undefined);
|
||||
break;
|
||||
|
||||
default: {
|
||||
const exhaustiveCheck: never = this.serviceState;
|
||||
throw new Error(`Unreachable code: ${exhaustiveCheck}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Default handler for alarms. By default, calls the defined handler for the
|
||||
* named alarm, if available. Override for custom behavior.
|
||||
*/
|
||||
protected handleAlarm = (alarm: Alarms.Alarm): void => {
|
||||
this.alarmSchedules[alarm.name]?.handler(alarm);
|
||||
};
|
||||
}
|
||||
465
trampoline/src/pages/Background/services/keyring.ts
Normal file
465
trampoline/src/pages/Background/services/keyring.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
import { keyringUnlocked, Vault, vaultUpdate } from '../redux-slices/keyrings';
|
||||
import BaseService, { BaseServiceCreateProps } from './base';
|
||||
import MainServiceManager from './main';
|
||||
import { ServiceLifecycleEvents } from './types';
|
||||
import * as encryptor from '@metamask/browser-passworder';
|
||||
import { Provider } from '@ethersproject/providers';
|
||||
import { BigNumber, ethers } from 'ethers';
|
||||
import { AccountApiType } from '../../Account/account-api/types';
|
||||
import {
|
||||
AccountImplementations,
|
||||
ActiveAccountImplementation,
|
||||
} from '../constants';
|
||||
import { HttpRpcClient, PaymasterAPI } from '@account-abstraction/sdk';
|
||||
import { MessageSigningRequest } from '../redux-slices/signing';
|
||||
import { AccountData } from '../redux-slices/account';
|
||||
import { AccountBalance } from '../types/account';
|
||||
import { DomainName, URI } from '../types/common';
|
||||
import { EVMNetwork } from '../types/network';
|
||||
import { EthersTransactionRequest } from './types';
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts';
|
||||
|
||||
interface Events extends ServiceLifecycleEvents {
|
||||
createPassword: string;
|
||||
}
|
||||
|
||||
type KeyringSerialisedState = {
|
||||
type: string;
|
||||
address: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
export type KeyringServiceCreateProps = {
|
||||
initialState?: Vault;
|
||||
provider: string;
|
||||
bundler: string;
|
||||
entryPointAddress: string;
|
||||
} & BaseServiceCreateProps;
|
||||
|
||||
export default class KeyringService extends BaseService<Events> {
|
||||
keyrings: {
|
||||
[address: string]: AccountApiType;
|
||||
};
|
||||
vault?: string;
|
||||
password?: string;
|
||||
encryptionKey?: string;
|
||||
encryptionSalt?: string;
|
||||
provider: Provider;
|
||||
bundler?: HttpRpcClient;
|
||||
paymasterAPI?: PaymasterAPI;
|
||||
|
||||
constructor(
|
||||
readonly mainServiceManager: MainServiceManager,
|
||||
provider: string,
|
||||
bundler: string,
|
||||
readonly entryPointAddress: string,
|
||||
vault?: string
|
||||
) {
|
||||
super();
|
||||
this.keyrings = {};
|
||||
this.provider = new ethers.providers.JsonRpcBatchProvider(provider);
|
||||
this.provider
|
||||
.getNetwork()
|
||||
.then((net) => net.chainId)
|
||||
.then(async (chainId) => {
|
||||
let bundlerRPC;
|
||||
try {
|
||||
bundlerRPC = new ethers.providers.JsonRpcProvider(bundler);
|
||||
} catch (e) {
|
||||
throw new Error(`Bundler network is not connected on url ${bundler}`);
|
||||
}
|
||||
|
||||
if (bundlerRPC) {
|
||||
const supportedEntryPoint = await bundlerRPC.send(
|
||||
'eth_supportedEntryPoints',
|
||||
[]
|
||||
);
|
||||
if (!supportedEntryPoint.includes(entryPointAddress)) {
|
||||
throw new Error(
|
||||
`Bundler network doesn't support entryPoint ${entryPointAddress}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const code = await this.provider.getCode(entryPointAddress);
|
||||
if (code === '0x')
|
||||
throw new Error(`Entrypoint not deployed at ${entryPointAddress}`);
|
||||
|
||||
this.bundler = new HttpRpcClient(bundler, entryPointAddress, chainId);
|
||||
});
|
||||
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
async unlockVault(
|
||||
password?: string,
|
||||
encryptionKey?: string,
|
||||
encryptionSalt?: string
|
||||
): Promise<{ [address: string]: AccountApiType }> {
|
||||
if (!this.vault) throw new Error('No vault to restore');
|
||||
|
||||
let vault: any;
|
||||
|
||||
if (password) {
|
||||
const result = await encryptor.decryptWithDetail(password, this.vault);
|
||||
vault = result.vault;
|
||||
this.password = password;
|
||||
this.encryptionKey = result.exportedKeyString;
|
||||
this.encryptionSalt = result.salt;
|
||||
} else {
|
||||
const parsedEncryptedVault = JSON.parse(this.vault);
|
||||
|
||||
if (encryptionSalt !== parsedEncryptedVault.salt) {
|
||||
throw new Error('Encryption key and salt provided are expired');
|
||||
}
|
||||
|
||||
const key = await encryptor.importKey(encryptionKey || '');
|
||||
vault = await encryptor.decryptWithKey(key, parsedEncryptedVault);
|
||||
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.encryptionSalt = encryptionSalt;
|
||||
}
|
||||
|
||||
await Promise.all(vault.map(this._restoreKeyring));
|
||||
return this.keyrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore Keyring Helper
|
||||
*
|
||||
* Attempts to initialize a new keyring from the provided serialized payload.
|
||||
* On success, returns the resulting keyring instance.
|
||||
*
|
||||
* @param {object} serialized - The serialized keyring.
|
||||
* @returns {Promise<Keyring|undefined>} The deserialized keyring or undefined if the keyring type is unsupported.
|
||||
*/
|
||||
_restoreKeyring = async (
|
||||
serialized: KeyringSerialisedState
|
||||
): Promise<AccountApiType | undefined> => {
|
||||
const { address, type, data } = serialized;
|
||||
|
||||
const keyring = await this._newKeyring(address, type, data);
|
||||
|
||||
this.keyrings[address] = keyring;
|
||||
|
||||
return keyring;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate, initialize and return a new keyring
|
||||
*
|
||||
* The keyring instantiated is of the given `type`.
|
||||
*
|
||||
* @param {string} type - The type of keyring to add.
|
||||
* @param {object} data - The data to restore a previously serialized keyring.
|
||||
* @returns {Promise<Keyring>} The new keyring.
|
||||
*/
|
||||
async _newKeyring(
|
||||
address: string,
|
||||
type: string,
|
||||
data: any
|
||||
): Promise<AccountApiType> {
|
||||
const account = new AccountImplementations[type]({
|
||||
provider: this.provider,
|
||||
entryPointAddress: this.entryPointAddress,
|
||||
paymasterAPI: this.paymasterAPI,
|
||||
deserializeState: data,
|
||||
});
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Keyrings
|
||||
*
|
||||
* Deallocates all currently managed keyrings and accounts.
|
||||
* Used before initializing a new vault.
|
||||
*/
|
||||
|
||||
/* eslint-disable require-await */
|
||||
clearKeyrings = async (): Promise<void> => {
|
||||
// clear keyrings from memory
|
||||
this.keyrings = {};
|
||||
};
|
||||
|
||||
registerEventListeners = () => {};
|
||||
|
||||
removeEventListeners = () => {};
|
||||
|
||||
updateStore = () => {};
|
||||
|
||||
createPassword = async (password: string) => {
|
||||
this.password = password;
|
||||
await this.persistAllKeyrings();
|
||||
this.keyringUnlocked();
|
||||
};
|
||||
|
||||
keyringUnlocked = () => {
|
||||
this.mainServiceManager.store.dispatch(keyringUnlocked());
|
||||
};
|
||||
|
||||
persistAllKeyrings = async () => {
|
||||
if (!this.password && !this.encryptionKey) {
|
||||
throw new Error(
|
||||
'Cannot persist vault without password and encryption key'
|
||||
);
|
||||
}
|
||||
|
||||
const serializedKeyrings: KeyringSerialisedState[] = await Promise.all(
|
||||
Object.values(this.keyrings).map(async (keyring) => {
|
||||
const [address, data] = await Promise.all([
|
||||
await keyring.getAccountAddress(),
|
||||
keyring.serialize(),
|
||||
]);
|
||||
return { type: ActiveAccountImplementation, address, data };
|
||||
})
|
||||
);
|
||||
|
||||
let vault: string;
|
||||
|
||||
if (this.password) {
|
||||
const { vault: newVault, exportedKeyString } =
|
||||
await encryptor.encryptWithDetail(this.password, serializedKeyrings);
|
||||
vault = newVault;
|
||||
this.encryptionKey = exportedKeyString;
|
||||
this.encryptionSalt = JSON.parse(newVault).salt;
|
||||
} else {
|
||||
const key = await encryptor.importKey(this.encryptionKey || '');
|
||||
const vaultJSON = await encryptor.encryptWithKey(key, serializedKeyrings);
|
||||
vaultJSON.salt = this.encryptionSalt;
|
||||
vault = JSON.stringify(vaultJSON);
|
||||
}
|
||||
|
||||
this.mainServiceManager.store.dispatch(
|
||||
vaultUpdate({
|
||||
vault,
|
||||
encryptionKey: this.encryptionKey,
|
||||
encryptionSalt: this.encryptionSalt,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
sendUnlockKeyringChromeMessage = () => {};
|
||||
|
||||
createKeyringForImplementation = async (implementation: string) => {};
|
||||
|
||||
addAccount = async (
|
||||
implementation: string,
|
||||
context?: any
|
||||
): Promise<string> => {
|
||||
const account = new AccountImplementations[implementation]({
|
||||
provider: this.provider,
|
||||
entryPointAddress: this.entryPointAddress,
|
||||
context,
|
||||
paymasterAPI: this.paymasterAPI,
|
||||
});
|
||||
const address = await account.getAccountAddress();
|
||||
if (address === ethers.constants.AddressZero)
|
||||
throw new Error(
|
||||
`EntryPoint getAccountAddress returned error and returned address ${ethers.constants.AddressZero}, check factory contract is properly deployed.`
|
||||
);
|
||||
this.keyrings[address] = account;
|
||||
await this.persistAllKeyrings();
|
||||
return account.getAccountAddress();
|
||||
};
|
||||
|
||||
getAccountData = async (
|
||||
address: string,
|
||||
activeNetwork: EVMNetwork
|
||||
): Promise<{
|
||||
accountDeployed: boolean;
|
||||
minimumRequiredFunds: string;
|
||||
balances?: {
|
||||
[assetSymbol: string]: AccountBalance;
|
||||
};
|
||||
ens?: {
|
||||
name?: DomainName;
|
||||
avatarURL?: URI;
|
||||
};
|
||||
}> => {
|
||||
const response: {
|
||||
accountDeployed: boolean;
|
||||
minimumRequiredFunds: string;
|
||||
balances?: {
|
||||
[assetSymbol: string]: AccountBalance;
|
||||
};
|
||||
ens?: {
|
||||
name?: DomainName;
|
||||
avatarURL?: URI;
|
||||
};
|
||||
} = {
|
||||
accountDeployed: false,
|
||||
minimumRequiredFunds: '0',
|
||||
balances: undefined,
|
||||
ens: undefined,
|
||||
};
|
||||
const code = await this.provider.getCode(address);
|
||||
if (code !== '0x') response.accountDeployed = true;
|
||||
|
||||
const keyring = this.keyrings[address];
|
||||
|
||||
response.minimumRequiredFunds = ethers.utils.formatEther(
|
||||
BigNumber.from(
|
||||
await keyring.estimateCreationGas(await keyring.getInitCode())
|
||||
)
|
||||
);
|
||||
|
||||
const balance = await this.provider.getBalance(address);
|
||||
|
||||
response.balances = {
|
||||
[activeNetwork.baseAsset.symbol]: {
|
||||
address: '0x',
|
||||
assetAmount: {
|
||||
asset: {
|
||||
symbol: activeNetwork.baseAsset.symbol,
|
||||
name: activeNetwork.baseAsset.name,
|
||||
},
|
||||
amount: ethers.utils.formatEther(balance),
|
||||
},
|
||||
network: activeNetwork,
|
||||
retrievedAt: Date.now(),
|
||||
dataSource: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
personalSign = async (
|
||||
address: string,
|
||||
context: any,
|
||||
request?: MessageSigningRequest
|
||||
): Promise<string> => {
|
||||
const keyring = this.keyrings[address];
|
||||
|
||||
if (!keyring) throw new Error('No keyring for the address found');
|
||||
|
||||
return keyring.signMessage(context, request);
|
||||
};
|
||||
|
||||
callAccountApi = async (
|
||||
address: string,
|
||||
functionName: string,
|
||||
args?: any[]
|
||||
) => {
|
||||
const keyring = this.keyrings[address];
|
||||
|
||||
return args ? keyring[functionName](...args) : keyring[functionName]();
|
||||
};
|
||||
|
||||
signUserOpWithContext = async (
|
||||
address: string,
|
||||
userOp: UserOperationStruct,
|
||||
context?: any
|
||||
): Promise<UserOperationStruct> => {
|
||||
const keyring = this.keyrings[address];
|
||||
|
||||
return keyring.signUserOpWithContext(userOp, context);
|
||||
};
|
||||
|
||||
sendUserOp = async (
|
||||
address: string,
|
||||
userOp: UserOperationStruct
|
||||
): Promise<string | null> => {
|
||||
if (this.bundler) {
|
||||
const userOpHash = await this.bundler.sendUserOpToBundler(userOp);
|
||||
const keyring = this.keyrings[address];
|
||||
return await keyring.getUserOpReceipt(userOpHash);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
createUnsignedUserOp = async (
|
||||
address: string,
|
||||
transaction: EthersTransactionRequest
|
||||
): Promise<UserOperationStruct> => {
|
||||
const keyring = this.keyrings[address];
|
||||
const userOp = await keyring.createUnsignedUserOp({
|
||||
target: transaction.to,
|
||||
data: transaction.data
|
||||
? ethers.utils.hexConcat([transaction.data])
|
||||
: '0x',
|
||||
value: transaction.value
|
||||
? ethers.BigNumber.from(transaction.value)
|
||||
: undefined,
|
||||
gasLimit: transaction.gasLimit,
|
||||
maxFeePerGas: transaction.maxFeePerGas,
|
||||
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
|
||||
});
|
||||
|
||||
userOp.sender = await userOp.sender;
|
||||
userOp.nonce = ethers.BigNumber.from(await userOp.nonce).toHexString();
|
||||
userOp.initCode = await userOp.initCode;
|
||||
userOp.callData = await userOp.callData;
|
||||
userOp.callGasLimit = ethers.BigNumber.from(
|
||||
await userOp.callGasLimit
|
||||
).toHexString();
|
||||
userOp.verificationGasLimit = ethers.BigNumber.from(
|
||||
await userOp.verificationGasLimit
|
||||
).toHexString();
|
||||
userOp.preVerificationGas = await userOp.preVerificationGas;
|
||||
userOp.maxFeePerGas = ethers.BigNumber.from(
|
||||
await userOp.maxFeePerGas
|
||||
).toHexString();
|
||||
userOp.maxPriorityFeePerGas = ethers.BigNumber.from(
|
||||
await userOp.maxPriorityFeePerGas
|
||||
).toHexString();
|
||||
userOp.paymasterAndData = await userOp.paymasterAndData;
|
||||
userOp.signature = await userOp.signature;
|
||||
|
||||
const gasParameters = await this.bundler?.estimateUserOpGas(
|
||||
await keyring.signUserOp(userOp)
|
||||
);
|
||||
|
||||
userOp.callGasLimit = ethers.BigNumber.from(
|
||||
gasParameters?.callGasLimit || userOp.callGasLimit
|
||||
).toHexString();
|
||||
userOp.verificationGasLimit = ethers.BigNumber.from(
|
||||
gasParameters?.verificationGas || userOp.verificationGasLimit
|
||||
).toHexString();
|
||||
userOp.preVerificationGas = ethers.BigNumber.from(
|
||||
gasParameters?.preVerificationGas || userOp.preVerificationGas
|
||||
).toHexString();
|
||||
|
||||
return userOp;
|
||||
};
|
||||
|
||||
validateKeyringViewInputValue = async () => {};
|
||||
|
||||
static async create({
|
||||
mainServiceManager,
|
||||
initialState,
|
||||
provider,
|
||||
bundler,
|
||||
entryPointAddress,
|
||||
}: KeyringServiceCreateProps): Promise<KeyringService> {
|
||||
if (!mainServiceManager)
|
||||
throw new Error('mainServiceManager is needed for Keyring Servie');
|
||||
|
||||
const keyringService = new KeyringService(
|
||||
mainServiceManager,
|
||||
provider,
|
||||
bundler,
|
||||
entryPointAddress,
|
||||
initialState?.vault
|
||||
);
|
||||
|
||||
if (initialState?.encryptionKey && initialState?.encryptionSalt) {
|
||||
await keyringService.unlockVault(
|
||||
undefined,
|
||||
initialState?.encryptionKey,
|
||||
initialState?.encryptionSalt
|
||||
);
|
||||
}
|
||||
|
||||
return keyringService;
|
||||
}
|
||||
|
||||
_startService = async (): Promise<void> => {
|
||||
this.registerEventListeners();
|
||||
};
|
||||
|
||||
_stopService = async (): Promise<void> => {
|
||||
this.removeEventListeners();
|
||||
};
|
||||
}
|
||||
76
trampoline/src/pages/Background/services/main.ts
Normal file
76
trampoline/src/pages/Background/services/main.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { wrapStore } from 'webext-redux';
|
||||
import { initializeStore, ReduxStoreType } from '../redux-slices';
|
||||
import BaseService from './base';
|
||||
import Config from '../../../exconfig';
|
||||
import { decodeJSON } from '../utils';
|
||||
import { initialState as initialNetworkState } from '../redux-slices/network';
|
||||
import { initialState as initialTransactionsState } from '../redux-slices/transactions';
|
||||
|
||||
export interface MainServiceManagerServicesMap {
|
||||
[key: string]: BaseService<any>;
|
||||
}
|
||||
|
||||
export interface MainServiceManagerProps {
|
||||
services: MainServiceManagerServicesMap;
|
||||
}
|
||||
|
||||
export default class MainServiceManager extends BaseService<never> {
|
||||
store: ReduxStoreType;
|
||||
services?: MainServiceManagerServicesMap;
|
||||
|
||||
constructor(readonly name: string) {
|
||||
super();
|
||||
let state = {};
|
||||
const version = localStorage.getItem('version');
|
||||
if (version === Config.stateVersion) {
|
||||
const stateFromStorage = decodeJSON(
|
||||
localStorage.getItem('state') || ''
|
||||
) as {};
|
||||
if (
|
||||
stateFromStorage &&
|
||||
stateFromStorage.network &&
|
||||
stateFromStorage.network.activeNetwork.chainID ===
|
||||
initialNetworkState.activeNetwork.chainID
|
||||
) {
|
||||
state = stateFromStorage;
|
||||
}
|
||||
}
|
||||
state.network = initialNetworkState;
|
||||
state.transactions = initialTransactionsState;
|
||||
this.store = initializeStore(state, this);
|
||||
wrapStore(this.store);
|
||||
}
|
||||
|
||||
init = async (props: MainServiceManagerProps) => {
|
||||
this.services = props.services;
|
||||
};
|
||||
|
||||
static async create(
|
||||
name: string,
|
||||
serviceInitializer: (
|
||||
mainServiceManager: MainServiceManager
|
||||
) => Promise<MainServiceManagerServicesMap>
|
||||
) {
|
||||
const mainServiceManager = new this(name);
|
||||
|
||||
await mainServiceManager.init({
|
||||
services: await serviceInitializer(mainServiceManager),
|
||||
});
|
||||
|
||||
return mainServiceManager;
|
||||
}
|
||||
|
||||
getService = (name: string): BaseService<any> => {
|
||||
if (!this.services) throw new Error('No services initialised');
|
||||
return this.services[name];
|
||||
};
|
||||
|
||||
_startService = async (): Promise<void> => {
|
||||
if (!this.services) throw new Error('No services initialised');
|
||||
Object.values(this.services).map((service) => service.startService());
|
||||
};
|
||||
_stopService = async (): Promise<void> => {
|
||||
if (!this.services) throw new Error('No services initialised');
|
||||
Object.values(this.services).map((service) => service.stopService());
|
||||
};
|
||||
}
|
||||
730
trampoline/src/pages/Background/services/provider-bridge.ts
Normal file
730
trampoline/src/pages/Background/services/provider-bridge.ts
Normal file
@@ -0,0 +1,730 @@
|
||||
import BaseService, { BaseServiceCreateProps } from './base';
|
||||
import MainServiceManager from './main';
|
||||
import { EthersTransactionRequest, ServiceLifecycleEvents } from './types';
|
||||
import browser from 'webextension-polyfill';
|
||||
import {
|
||||
EIP1193ErrorPayload,
|
||||
PortResponseEvent,
|
||||
RPCRequest,
|
||||
} from '../../Content/types';
|
||||
import { AA_EXTENSION_CONFIG, EXTERNAL_PORT_NAME } from '../constants';
|
||||
import { RootState } from '../redux-slices';
|
||||
import { isAAExtensionConfigPayload } from '../../Content/window-provider/runtime-type-checks';
|
||||
import showExtensionPopup, {
|
||||
checkPermissionSign,
|
||||
checkPermissionSignTransaction,
|
||||
parseSigningData,
|
||||
toHexChainID,
|
||||
} from '../utils';
|
||||
import {
|
||||
EIP1193Error,
|
||||
EIP1193_ERROR_CODES,
|
||||
isEIP1193Error,
|
||||
} from '../../Content/window-provider/eip-1193';
|
||||
import { AllowedQueryParamPage } from '../types/chrome-messages';
|
||||
import { requestPermission } from '../redux-slices/permissions';
|
||||
import { ethers } from 'ethers';
|
||||
import { hexlify, toUtf8Bytes } from 'ethers/lib/utils.js';
|
||||
import { signDataRequest } from '../redux-slices/signing';
|
||||
import { HexString } from '../types/common';
|
||||
import { sendTransactionRequest } from '../redux-slices/transactions';
|
||||
|
||||
type JsonRpcTransactionRequest = Omit<EthersTransactionRequest, 'gasLimit'> & {
|
||||
gas?: string;
|
||||
input?: string;
|
||||
annotation?: string;
|
||||
};
|
||||
|
||||
export type PermissionRequest = {
|
||||
key: string;
|
||||
origin: string;
|
||||
faviconUrl: string;
|
||||
chainID: string;
|
||||
title: string;
|
||||
state: 'request' | 'allow' | 'deny';
|
||||
accountAddress: string;
|
||||
};
|
||||
|
||||
// https://eips.ethereum.org/EIPS/eip-3326
|
||||
export type SwitchEthereumChainParameter = {
|
||||
chainId: string;
|
||||
};
|
||||
|
||||
// https://eips.ethereum.org/EIPS/eip-3085
|
||||
export type AddEthereumChainParameter = {
|
||||
chainId: string;
|
||||
blockExplorerUrls?: string[];
|
||||
chainName?: string;
|
||||
iconUrls?: string[];
|
||||
nativeCurrency?: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
rpcUrls?: string[];
|
||||
};
|
||||
|
||||
export type PortRequestEvent = {
|
||||
id: string;
|
||||
request: RPCRequest;
|
||||
};
|
||||
|
||||
export type PermissionMap = {
|
||||
evm: {
|
||||
[chainID: string]: {
|
||||
[address: string]: {
|
||||
[origin: string]: PermissionRequest;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type Events = ServiceLifecycleEvents & {
|
||||
requestPermission: PermissionRequest;
|
||||
initializeAllowedPages: PermissionMap;
|
||||
setClaimReferrer: string;
|
||||
walletConnectInit: string;
|
||||
};
|
||||
|
||||
type ProviderBridgeServiceProps = {} & BaseServiceCreateProps;
|
||||
|
||||
function parsedRPCErrorResponse(error: { body: string }):
|
||||
| {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
| undefined {
|
||||
try {
|
||||
const parsedError = JSON.parse(error.body).error;
|
||||
return {
|
||||
/**
|
||||
* The code should be the same as for user rejected requests because otherwise it will not be displayed.
|
||||
*/
|
||||
code: 4001,
|
||||
message:
|
||||
'message' in parsedError && parsedError.message
|
||||
? parsedError.message[0].toUpperCase() + parsedError.message.slice(1)
|
||||
: EIP1193_ERROR_CODES.rpcErrorNotParsed.message,
|
||||
};
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ProviderBridgeService extends BaseService<Events> {
|
||||
#pendingRequests: {
|
||||
[origin: string]: {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (value: unknown) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
openPorts: Array<browser.Runtime.Port> = [];
|
||||
|
||||
static create = async ({
|
||||
mainServiceManager,
|
||||
}: ProviderBridgeServiceProps): Promise<ProviderBridgeService> => {
|
||||
if (!mainServiceManager)
|
||||
throw new Error(
|
||||
'mainServiceManager is needed for Provider Bridge Servie'
|
||||
);
|
||||
return new this(mainServiceManager);
|
||||
};
|
||||
|
||||
_startService = async () => {};
|
||||
|
||||
_stopService = async () => {};
|
||||
|
||||
private constructor(readonly mainServiceManager: MainServiceManager) {
|
||||
super();
|
||||
|
||||
browser.runtime.onConnect.addListener(
|
||||
async (port: browser.Runtime.Port) => {
|
||||
if (port.name === EXTERNAL_PORT_NAME && port.sender?.url) {
|
||||
port.onMessage.addListener((event: any) => {
|
||||
this.onMessageListener(port, event);
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
this.openPorts = this.openPorts.filter(
|
||||
(openPort) => openPort !== port
|
||||
);
|
||||
});
|
||||
this.openPorts.push(port);
|
||||
|
||||
// we need to send this info ASAP so it arrives before the webpage is initializing
|
||||
// so we can set our provider into the correct state, BEFORE the page has a chance to
|
||||
// to cache it, store it etc.
|
||||
port.postMessage({
|
||||
id: 'aa-extension',
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
method: AA_EXTENSION_CONFIG,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async onMessageListener(
|
||||
port: browser.Runtime.Port,
|
||||
event: PortRequestEvent
|
||||
): Promise<void> {
|
||||
const { url, tab } = port.sender;
|
||||
if (typeof url === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const { origin } = new URL(url);
|
||||
const response: PortResponseEvent = {
|
||||
id: event.id,
|
||||
jsonrpc: '2.0',
|
||||
result: [],
|
||||
};
|
||||
const network = (this.mainServiceManager.store.getState() as RootState)
|
||||
.network.activeNetwork;
|
||||
const originPermission = await this.checkPermission(origin);
|
||||
if (isAAExtensionConfigPayload(event.request)) {
|
||||
// let's start with the internal communication
|
||||
response.id = 'aa-extension';
|
||||
response.result = {
|
||||
method: event.request.method,
|
||||
chainId: toHexChainID(network.chainID),
|
||||
};
|
||||
} else if (
|
||||
event.request.method === 'eth_chainId' ||
|
||||
event.request.method === 'net_version'
|
||||
) {
|
||||
response.result = await this.routeSafeRPCRequest(
|
||||
event.request.method,
|
||||
event.request.params,
|
||||
origin
|
||||
);
|
||||
} else if (typeof originPermission !== 'undefined') {
|
||||
// // if it's not internal but dapp has permission to communicate we proxy the request
|
||||
// TODO: here comes format validation
|
||||
response.result = await this.routeContentScriptRPCRequest(
|
||||
originPermission,
|
||||
event.request.method,
|
||||
event.request.params,
|
||||
origin
|
||||
);
|
||||
} else if (
|
||||
event.request.method === 'wallet_addEthereumChain' ||
|
||||
event.request.method === 'wallet_switchEthereumChain'
|
||||
) {
|
||||
response.result = await this.routeSafeRPCRequest(
|
||||
event.request.method,
|
||||
event.request.params,
|
||||
origin
|
||||
);
|
||||
} else if (
|
||||
event.request.method === 'eth_requestAccounts' ||
|
||||
event.request.method === 'eth_accounts'
|
||||
) {
|
||||
// if it's external communication AND the dApp does not have permission BUT asks for it
|
||||
// then let's ask the user what he/she thinks
|
||||
const state: RootState =
|
||||
this.mainServiceManager.store.getState() as RootState;
|
||||
const address = state.account.account;
|
||||
const network = state.network.activeNetwork;
|
||||
|
||||
if (!address) {
|
||||
response.result = new EIP1193Error(
|
||||
EIP1193_ERROR_CODES.userRejectedRequest
|
||||
).toJSON();
|
||||
} else {
|
||||
// Get last prefferec chainID for the DAPP
|
||||
// const dAppChainID = Number(
|
||||
// (await this.internalEthereumProviderService.routeSafeRPCRequest(
|
||||
// "eth_chainId",
|
||||
// [],
|
||||
// origin
|
||||
// )) as string
|
||||
// ).toString()
|
||||
|
||||
// these params are taken directly from the dapp website
|
||||
const [title, faviconUrl] = event.request.params as string[];
|
||||
const permissionRequest: PermissionRequest = {
|
||||
key: `${origin}_${address}_${network.chainID}`,
|
||||
origin,
|
||||
chainID: network.chainID,
|
||||
faviconUrl: faviconUrl || tab?.favIconUrl || '', // if favicon was not found on the website then try with browser's `tab`
|
||||
title,
|
||||
state: 'request',
|
||||
accountAddress: address,
|
||||
};
|
||||
// TODO:// add ask permision from a user in popup
|
||||
const blockUntilUserAction = await this.requestPermission(
|
||||
permissionRequest
|
||||
);
|
||||
await blockUntilUserAction;
|
||||
|
||||
// Fetch the latest permission
|
||||
const persistedPermission = await this.checkPermission(origin);
|
||||
if (typeof persistedPermission !== 'undefined') {
|
||||
// if agrees then let's return the account data
|
||||
|
||||
response.result = await this.routeContentScriptRPCRequest(
|
||||
persistedPermission,
|
||||
'eth_accounts',
|
||||
event.request.params,
|
||||
origin
|
||||
);
|
||||
} else {
|
||||
// if user does NOT agree, then reject
|
||||
response.result = new EIP1193Error(
|
||||
EIP1193_ERROR_CODES.userRejectedRequest
|
||||
).toJSON();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sorry dear dApp, there is no love for you here
|
||||
response.result = new EIP1193Error(
|
||||
EIP1193_ERROR_CODES.unauthorized
|
||||
).toJSON();
|
||||
}
|
||||
port.postMessage(response);
|
||||
}
|
||||
|
||||
async checkPermission(
|
||||
origin: string
|
||||
): Promise<PermissionRequest | undefined> {
|
||||
const state: RootState =
|
||||
this.mainServiceManager.store.getState() as RootState;
|
||||
|
||||
const account = state.account.account || '';
|
||||
|
||||
return state.dappPermissions.allowed.evm[`${origin}_${account}`];
|
||||
}
|
||||
|
||||
async grantPermission(permission: PermissionRequest): Promise<void> {
|
||||
// FIXME proper error handling if this happens - should not tho
|
||||
if (permission.state !== 'allow' || !permission.accountAddress) return;
|
||||
|
||||
if (this.#pendingRequests[permission.origin]) {
|
||||
this.#pendingRequests[permission.origin].resolve(permission);
|
||||
delete this.#pendingRequests[permission.origin];
|
||||
}
|
||||
}
|
||||
|
||||
async resolveRequest(origin: string, result: any): Promise<void> {
|
||||
if (this.#pendingRequests[origin]) {
|
||||
this.#pendingRequests[origin].resolve(result);
|
||||
delete this.#pendingRequests[origin];
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRequest(origin: string, result: any): Promise<void> {
|
||||
if (this.#pendingRequests[origin]) {
|
||||
this.#pendingRequests[origin].reject(result);
|
||||
delete this.#pendingRequests[origin];
|
||||
}
|
||||
}
|
||||
|
||||
async denyOrRevokePermission(permission: PermissionRequest) {
|
||||
if (permission.state !== 'deny' || !permission.accountAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#pendingRequests[permission.origin]) {
|
||||
this.#pendingRequests[permission.origin].reject('Time to move on');
|
||||
delete this.#pendingRequests[permission.origin];
|
||||
}
|
||||
|
||||
this.notifyContentScriptsAboutAddressChange();
|
||||
}
|
||||
|
||||
notifyContentScriptsAboutAddressChange(newAddress?: string): void {
|
||||
this.openPorts.forEach(async (port) => {
|
||||
// we know that url exists because it was required to store the port
|
||||
const { origin } = new URL(port.sender?.url as string);
|
||||
const { chainID } = (
|
||||
this.mainServiceManager.store.getState() as RootState
|
||||
).network.activeNetwork;
|
||||
|
||||
if (await this.checkPermission(origin, chainID)) {
|
||||
port.postMessage({
|
||||
id: 'aa-extension',
|
||||
result: {
|
||||
method: 'aa-extension_accountChanged',
|
||||
address: [newAddress],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
port.postMessage({
|
||||
id: 'aa-extension',
|
||||
result: {
|
||||
method: 'aa-extension_accountChanged',
|
||||
address: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async requestPermission(
|
||||
permissionRequest: PermissionRequest
|
||||
): Promise<unknown> {
|
||||
this.emitter.emit('requestPermission', permissionRequest);
|
||||
this.mainServiceManager.store.dispatch(
|
||||
requestPermission(permissionRequest)
|
||||
);
|
||||
await showExtensionPopup(AllowedQueryParamPage.dappPermission);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#pendingRequests[permissionRequest.origin] = {
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async signData(
|
||||
{
|
||||
input,
|
||||
account,
|
||||
}: {
|
||||
input: string;
|
||||
account: string;
|
||||
},
|
||||
origin: string
|
||||
) {
|
||||
const state: RootState =
|
||||
this.mainServiceManager.store.getState() as RootState;
|
||||
|
||||
const hexInput = input.match(/^0x[0-9A-Fa-f]*$/)
|
||||
? input
|
||||
: hexlify(toUtf8Bytes(input));
|
||||
const typeAndData = parseSigningData(input);
|
||||
const currentNetwork = state.network.activeNetwork;
|
||||
|
||||
this.mainServiceManager.store.dispatch(
|
||||
signDataRequest({
|
||||
origin: origin,
|
||||
account: {
|
||||
address: account,
|
||||
network: currentNetwork,
|
||||
},
|
||||
rawSigningData: hexInput,
|
||||
...typeAndData,
|
||||
})
|
||||
);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.#pendingRequests[origin] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
private async sendTransaction(
|
||||
transactionRequest: JsonRpcTransactionRequest,
|
||||
origin: string
|
||||
) {
|
||||
this.mainServiceManager.store.dispatch(
|
||||
sendTransactionRequest({
|
||||
transactionRequest: transactionRequest,
|
||||
origin: origin,
|
||||
})
|
||||
);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.#pendingRequests[origin] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
async routeSafeRPCRequest(
|
||||
method: string,
|
||||
params: RPCRequest['params'],
|
||||
origin: string
|
||||
): Promise<unknown> {
|
||||
const state: RootState =
|
||||
this.mainServiceManager.store.getState() as RootState;
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(
|
||||
state.network.activeNetwork.provider
|
||||
);
|
||||
|
||||
switch (method) {
|
||||
// supported alchemy methods: https://docs.alchemy.com/alchemy/apis/ethereum
|
||||
// case 'eth_signTypedData':
|
||||
// case 'eth_signTypedData_v1':
|
||||
// case 'eth_signTypedData_v3':
|
||||
// case 'eth_signTypedData_v4':
|
||||
// return this.signTypedData({
|
||||
// account: {
|
||||
// address: params[0] as string,
|
||||
// network: state.network.activeNetwork,
|
||||
// },
|
||||
// typedData: JSON.parse(params[1] as string),
|
||||
// });
|
||||
case 'eth_chainId':
|
||||
return toHexChainID(state.network.activeNetwork.chainID);
|
||||
case 'eth_blockNumber':
|
||||
case 'eth_call':
|
||||
case 'eth_estimateGas':
|
||||
case 'eth_feeHistory':
|
||||
case 'eth_gasPrice':
|
||||
case 'eth_getBalance':
|
||||
case 'eth_getBlockByHash':
|
||||
case 'eth_getBlockByNumber':
|
||||
case 'eth_getBlockTransactionCountByHash':
|
||||
case 'eth_getBlockTransactionCountByNumber':
|
||||
case 'eth_getCode':
|
||||
case 'eth_getFilterChanges':
|
||||
case 'eth_getFilterLogs':
|
||||
case 'eth_getLogs':
|
||||
case 'eth_getProof':
|
||||
case 'eth_getStorageAt':
|
||||
case 'eth_getTransactionByBlockHashAndIndex':
|
||||
case 'eth_getTransactionByBlockNumberAndIndex':
|
||||
case 'eth_getTransactionByHash':
|
||||
case 'eth_getTransactionCount':
|
||||
case 'eth_getTransactionReceipt':
|
||||
case 'eth_getUncleByBlockHashAndIndex':
|
||||
case 'eth_getUncleByBlockNumberAndIndex':
|
||||
case 'eth_getUncleCountByBlockHash':
|
||||
case 'eth_getUncleCountByBlockNumber':
|
||||
case 'eth_maxPriorityFeePerGas':
|
||||
case 'eth_newBlockFilter':
|
||||
case 'eth_newFilter':
|
||||
case 'eth_newPendingTransactionFilter':
|
||||
case 'eth_protocolVersion':
|
||||
case 'eth_sendRawTransaction':
|
||||
case 'eth_subscribe':
|
||||
case 'eth_syncing':
|
||||
case 'eth_uninstallFilter':
|
||||
case 'eth_unsubscribe':
|
||||
case 'net_listening':
|
||||
case 'net_version':
|
||||
case 'web3_clientVersion':
|
||||
case 'web3_sha3':
|
||||
return provider.send(method, params);
|
||||
case 'eth_accounts': {
|
||||
// This is a special method, because Alchemy provider DO support it, but always return null (because they do not store keys.)
|
||||
const address = state.account.account;
|
||||
return [address];
|
||||
}
|
||||
case 'eth_sendTransaction':
|
||||
return this.sendTransaction(
|
||||
{
|
||||
...(params[0] as JsonRpcTransactionRequest),
|
||||
},
|
||||
origin
|
||||
);
|
||||
// case 'eth_signTransaction':
|
||||
// return this.signTransaction(
|
||||
// params[0] as JsonRpcTransactionRequest,
|
||||
// origin
|
||||
// ).then((signedTransaction) =>
|
||||
// serializeEthersTransaction(
|
||||
// ethersTransactionFromSignedTransaction(signedTransaction),
|
||||
// {
|
||||
// r: signedTransaction.r,
|
||||
// s: signedTransaction.s,
|
||||
// v: signedTransaction.v,
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// case 'eth_sign': // --- important wallet methods ---
|
||||
// return this.signData(
|
||||
// {
|
||||
// input: params[1] as string,
|
||||
// account: params[0] as string,
|
||||
// },
|
||||
// origin
|
||||
// );
|
||||
case 'personal_sign':
|
||||
return this.signData(
|
||||
{
|
||||
input: params[0] as string,
|
||||
account: params[1] as string,
|
||||
},
|
||||
origin
|
||||
);
|
||||
case 'wallet_addEthereumChain': {
|
||||
// const chainInfo = params[0] as AddEthereumChainParameter;
|
||||
// const { chainId } = chainInfo;
|
||||
// const supportedNetwork = await this.getTrackedNetworkByChainId(chainId);
|
||||
// if (supportedNetwork) {
|
||||
// this.switchToSupportedNetwork(origin, supportedNetwork);
|
||||
// return null;
|
||||
// }
|
||||
// if (!FeatureFlags.SUPPORT_CUSTOM_NETWORKS) {
|
||||
// // Dissallow adding new chains until feature flag is turned on.
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.methodNotSupported);
|
||||
// }
|
||||
// try {
|
||||
// const validatedParam = validateAddEthereumChainParameter(chainInfo);
|
||||
// await this.chainService.addCustomChain(validatedParam);
|
||||
// return null;
|
||||
// } catch (e) {
|
||||
// logger.error(e);
|
||||
// throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest);
|
||||
// }
|
||||
}
|
||||
case 'wallet_switchEthereumChain': {
|
||||
// const newChainId = (params[0] as SwitchEthereumChainParameter).chainId;
|
||||
// const supportedNetwork = await this.getTrackedNetworkByChainId(
|
||||
// newChainId
|
||||
// );
|
||||
// if (supportedNetwork) {
|
||||
// this.switchToSupportedNetwork(origin, supportedNetwork);
|
||||
// return null;
|
||||
// }
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.chainDisconnected);
|
||||
}
|
||||
case 'metamask_getProviderState': // --- important MM only methods ---
|
||||
case 'metamask_sendDomainMetadata':
|
||||
case 'wallet_requestPermissions':
|
||||
case 'wallet_watchAsset':
|
||||
case 'estimateGas': // --- eip1193-bridge only method --
|
||||
case 'eth_coinbase': // --- MM only methods ---
|
||||
case 'eth_decrypt':
|
||||
case 'eth_getEncryptionPublicKey':
|
||||
case 'eth_getWork':
|
||||
case 'eth_hashrate':
|
||||
case 'eth_mining':
|
||||
case 'eth_submitHashrate':
|
||||
case 'eth_submitWork':
|
||||
case 'metamask_accountsChanged':
|
||||
case 'metamask_chainChanged':
|
||||
case 'metamask_logWeb3ShimUsage':
|
||||
case 'metamask_unlockStateChanged':
|
||||
case 'metamask_watchAsset':
|
||||
case 'net_peerCount':
|
||||
case 'wallet_accountsChanged':
|
||||
case 'wallet_registerOnboarding':
|
||||
default:
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.unsupportedMethod);
|
||||
}
|
||||
}
|
||||
|
||||
async routeSafeRequest(
|
||||
method: string,
|
||||
params: unknown[],
|
||||
origin: string,
|
||||
popupPromise: Promise<browser.Windows.Window>
|
||||
): Promise<unknown> {
|
||||
const response = await this.routeSafeRPCRequest(
|
||||
method,
|
||||
params,
|
||||
origin
|
||||
).finally(async () => {
|
||||
// Close the popup once we're done submitting.
|
||||
const popup = await popupPromise;
|
||||
if (typeof popup.id !== 'undefined') {
|
||||
browser.windows.remove(popup.id);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
async routeContentScriptRPCRequest(
|
||||
enablingPermission: PermissionRequest,
|
||||
method: string,
|
||||
params: RPCRequest['params'],
|
||||
origin: string
|
||||
): Promise<unknown> {
|
||||
try {
|
||||
switch (method) {
|
||||
case 'eth_requestAccounts':
|
||||
case 'eth_accounts':
|
||||
return [enablingPermission.accountAddress];
|
||||
// case 'eth_signTypedData':
|
||||
// case 'eth_signTypedData_v1':
|
||||
// case 'eth_signTypedData_v3':
|
||||
// case 'eth_signTypedData_v4':
|
||||
// checkPermissionSignTypedData(
|
||||
// params[0] as HexString,
|
||||
// enablingPermission
|
||||
// );
|
||||
|
||||
// return await this.routeSafeRequest(
|
||||
// method,
|
||||
// params,
|
||||
// origin,
|
||||
// showExtensionPopup(AllowedQueryParamPage.signData)
|
||||
// );
|
||||
// case 'eth_sign':
|
||||
// checkPermissionSign(params[0] as HexString, enablingPermission);
|
||||
|
||||
// return await this.routeSafeRequest(
|
||||
// method,
|
||||
// params,
|
||||
// origin,
|
||||
// showExtensionPopup(AllowedQueryParamPage.personalSignData)
|
||||
// );
|
||||
case 'personal_sign':
|
||||
checkPermissionSign(params[1] as HexString, enablingPermission);
|
||||
|
||||
return await this.routeSafeRequest(
|
||||
method,
|
||||
params,
|
||||
origin,
|
||||
showExtensionPopup(AllowedQueryParamPage.personalSignData)
|
||||
);
|
||||
// case 'eth_signTransaction':
|
||||
case 'eth_sendTransaction':
|
||||
checkPermissionSignTransaction(
|
||||
{
|
||||
// A dApp can't know what should be the next nonce because it can't access
|
||||
// the information about how many tx are in the signing process inside the
|
||||
// wallet. Nonce should be assigned only by the wallet.
|
||||
...(params[0] as EthersTransactionRequest),
|
||||
nonce: undefined,
|
||||
},
|
||||
enablingPermission
|
||||
);
|
||||
|
||||
return await this.routeSafeRequest(
|
||||
method,
|
||||
params,
|
||||
origin,
|
||||
showExtensionPopup(AllowedQueryParamPage.signTransaction)
|
||||
);
|
||||
|
||||
default: {
|
||||
return await this.routeSafeRPCRequest(method, params, origin);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return this.handleRPCErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
handleRPCErrorResponse(error: unknown) {
|
||||
let response;
|
||||
console.error('error processing request', error);
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
/**
|
||||
* Get error per the RPC method’s specification
|
||||
*/
|
||||
if ('eip1193Error' in error) {
|
||||
const { eip1193Error } = error as {
|
||||
eip1193Error: EIP1193ErrorPayload;
|
||||
};
|
||||
if (isEIP1193Error(eip1193Error)) {
|
||||
response = eip1193Error;
|
||||
}
|
||||
/**
|
||||
* In the case of a non-matching error message, the error is returned without being nested in an object.
|
||||
* This is due to the error handling implementation.
|
||||
* Check the code for more details https://github.com/ethers-io/ethers.js/blob/master/packages/providers/src.ts/json-rpc-provider.ts#L96:L130
|
||||
*/
|
||||
} else if ('body' in error) {
|
||||
response = parsedRPCErrorResponse(error as { body: string });
|
||||
} else if ('error' in error) {
|
||||
response = parsedRPCErrorResponse(
|
||||
(error as { error: { body: string } }).error
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If no specific error is obtained return a user rejected request error
|
||||
*/
|
||||
return (
|
||||
response ??
|
||||
new EIP1193Error(EIP1193_ERROR_CODES.rpcErrorNotParsed).toJSON()
|
||||
);
|
||||
}
|
||||
}
|
||||
105
trampoline/src/pages/Background/services/types.ts
Normal file
105
trampoline/src/pages/Background/services/types.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import Emittery from 'emittery';
|
||||
import { BigNumberish, BytesLike } from 'ethers';
|
||||
import { AccessListish } from 'ethers/lib/utils.js';
|
||||
|
||||
export interface ServiceLifecycleEvents {
|
||||
serviceStarted: void;
|
||||
serviceStopped: void;
|
||||
}
|
||||
|
||||
export type EthersTransactionRequest = {
|
||||
to: string;
|
||||
from?: string;
|
||||
nonce?: BigNumberish;
|
||||
|
||||
gasLimit?: BigNumberish;
|
||||
gasPrice?: BigNumberish;
|
||||
|
||||
data?: BytesLike;
|
||||
value?: BigNumberish;
|
||||
chainId?: number;
|
||||
|
||||
type?: number;
|
||||
accessList?: AccessListish;
|
||||
|
||||
maxPriorityFeePerGas?: BigNumberish;
|
||||
maxFeePerGas?: BigNumberish;
|
||||
|
||||
customData?: Record<string, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A simple interface for service lifecycles and event emission. Services
|
||||
* should emit a `serviceStarted` event when they are started, and a
|
||||
* `serviceStopped` event when they are stopped. It is strongly recommended
|
||||
* that all services have a single static `create` method to create but not
|
||||
* start the service. This method should adhere to `ServiceCreatorFunction` for
|
||||
* consistency.
|
||||
*
|
||||
* Services are generally considered to have three phases: created, started,
|
||||
* and stopped. Once services are created, they should have any internal and
|
||||
* local state initialized. Once started, they should have any external state,
|
||||
* such as subscriptions to external systems and polling set up. Events should
|
||||
* not be emitted before a service is started. This creates an ideal moment to
|
||||
* hook up event handlers _after_ the service has been created but _before_ it
|
||||
* has been started.
|
||||
*
|
||||
* Implementors, see {@link BaseService} for a good base class that handles
|
||||
* most of the lifecycle details.
|
||||
*/
|
||||
export interface Service<T extends ServiceLifecycleEvents> {
|
||||
/**
|
||||
* The emitter for service events. Services are generally expected to emit
|
||||
* events when any action of interest occurs.
|
||||
*
|
||||
* All services should emit a `serviceStarted` event when the service has
|
||||
* finished its start process, and a `serviceStopped` event when the service
|
||||
* has finished its stop process.
|
||||
*/
|
||||
readonly emitter: Emittery<T>;
|
||||
|
||||
/**
|
||||
* Waits for any internal initialization to occur before fulfilling the
|
||||
* returned promise. The promise returns the same instance, so that it can be
|
||||
* chained. Calling `started` does _not_ start the service! Instead, it acts
|
||||
* as a hook for calling things once a service has been started.
|
||||
*/
|
||||
started(): Promise<this>;
|
||||
|
||||
/**
|
||||
* Starts any internal monitoring, scheduling, etc and then resolves the
|
||||
* returned promise. May not wait for all data to be resolved before
|
||||
* resolving the returned promise, just for all relevant processes to be
|
||||
* kicked off. Events should only be emitted after a service is started.
|
||||
* This includes any initialization events that provide a starting view of
|
||||
* persisted data.
|
||||
*
|
||||
* Calling this method more than once should be a noop. Starting a service
|
||||
* that has already been started and stopped is not guaranteed to work.
|
||||
*
|
||||
* When the start process is complete, the `serviceStarted` event should be
|
||||
* emitted on the service's event emitter.
|
||||
*
|
||||
* @returns An immediately-resolved promise if the service is already
|
||||
* started, otherwise a promise that will resolve once the service
|
||||
* has finished starting.
|
||||
*/
|
||||
startService(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stops any internal monitoring, scheduling, etc and then resolves the
|
||||
* returned promise. May not wait for all in-flight requests to be completed
|
||||
* before resolving the returned promise, just for all relevant scheduling to
|
||||
* be halted.
|
||||
*
|
||||
* Calling this method more than once should be a noop.
|
||||
*
|
||||
* When the stop process is complete, the `serviceStopped` event should be
|
||||
* emitted on the service's event emitter.
|
||||
*
|
||||
* @returns An immediately-resolved promise if the service is already
|
||||
* stopped, otherwise a promise that will resolve once the service
|
||||
* has finished stopping.
|
||||
*/
|
||||
stopService(): Promise<void>;
|
||||
}
|
||||
31
trampoline/src/pages/Background/types/account.ts
Normal file
31
trampoline/src/pages/Background/types/account.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AnyAssetAmount } from './asset';
|
||||
import { HexString } from './common';
|
||||
import { EVMNetwork } from './network';
|
||||
|
||||
export type AccountBalance = {
|
||||
/**
|
||||
* The address whose balance was measured.
|
||||
*/
|
||||
address: HexString;
|
||||
/**
|
||||
* The measured balance and the asset in which it's denominated.
|
||||
*/
|
||||
assetAmount: AnyAssetAmount;
|
||||
/**
|
||||
* The network on which the account balance was measured.
|
||||
*/
|
||||
network: EVMNetwork;
|
||||
/**
|
||||
* The block height at while the balance measurement is valid.
|
||||
*/
|
||||
blockHeight?: string;
|
||||
/**
|
||||
* When the account balance was measured, using Unix epoch timestamps.
|
||||
*/
|
||||
retrievedAt: number;
|
||||
/**
|
||||
* A loose attempt at tracking balance data provenance, in case providers
|
||||
* disagree and need to be disambiguated.
|
||||
*/
|
||||
dataSource: 'alchemy' | 'local' | 'infura' | 'custom';
|
||||
};
|
||||
33
trampoline/src/pages/Background/types/asset.ts
Normal file
33
trampoline/src/pages/Background/types/asset.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Metadata for a given asset, as well as the one or more token lists that
|
||||
* provided that metadata.
|
||||
*
|
||||
* Note that the metadata is entirely optional.
|
||||
*/
|
||||
export type AssetMetadata = {
|
||||
logoURL?: string;
|
||||
websiteURL?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The name and symbol of an arbitrary asset, fungible or non-fungible,
|
||||
* alongside potential metadata about that asset.
|
||||
*/
|
||||
export type Asset = {
|
||||
symbol: string;
|
||||
name: string;
|
||||
metadata?: AssetMetadata;
|
||||
};
|
||||
|
||||
/*
|
||||
* A union of all assets we expect to price.
|
||||
*/
|
||||
export type AnyAsset = Asset;
|
||||
|
||||
/*
|
||||
* The primary type representing amounts in fungible asset transactions.
|
||||
*/
|
||||
export type AnyAssetAmount<T extends AnyAsset = AnyAsset> = {
|
||||
asset: T;
|
||||
amount: string;
|
||||
};
|
||||
71
trampoline/src/pages/Background/types/chrome-messages.ts
Normal file
71
trampoline/src/pages/Background/types/chrome-messages.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
KeyringInputError,
|
||||
KeyringView,
|
||||
KeyringViewInputFieldValue,
|
||||
KeyringViewUserInput,
|
||||
StoreState,
|
||||
VaultState,
|
||||
} from '@epf-wallet/keyring-controller';
|
||||
|
||||
export type ChromeMessages<T> = {
|
||||
type:
|
||||
| 'keyring/createPassword'
|
||||
| 'keyring/locked'
|
||||
| 'keyring/unlock'
|
||||
| 'keyring/unlocked'
|
||||
| 'keyring/vaultUpdate'
|
||||
| 'keyring/createKeyringForImplementation'
|
||||
| 'keyring/newAccountView'
|
||||
| 'keyring/validateKeyringViewInputValue'
|
||||
| 'keyring/addAcount';
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export type CreatePasswordChromeMessage = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UnlockedKeyringChromeMessage = {
|
||||
storeState: StoreState;
|
||||
};
|
||||
|
||||
export type UnlockKeyringChromeMessage = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type VaultUpdate = {
|
||||
vault: VaultState;
|
||||
};
|
||||
|
||||
export type CreateKeyringForImplementation = {
|
||||
implementation: string;
|
||||
};
|
||||
|
||||
export type NewAccountView = {
|
||||
implementation: string;
|
||||
view: KeyringView | undefined;
|
||||
};
|
||||
|
||||
export type ValidateKeyringViewInputValue = {
|
||||
implementation: string;
|
||||
inputs: Array<KeyringViewInputFieldValue>;
|
||||
};
|
||||
|
||||
export type KeyringInputErrorMessage = {
|
||||
errors: Array<KeyringInputError>;
|
||||
};
|
||||
|
||||
export type AddAcount = {
|
||||
implementation: string;
|
||||
userInputs: KeyringViewUserInput;
|
||||
};
|
||||
|
||||
export const AllowedQueryParamPage = {
|
||||
signTransaction: '/sign-transaction',
|
||||
dappPermission: '/dapp-permission',
|
||||
signData: '/sign-data',
|
||||
personalSignData: '/personal-sign',
|
||||
} as const;
|
||||
|
||||
export type AllowedQueryParamPageType =
|
||||
(typeof AllowedQueryParamPage)[keyof typeof AllowedQueryParamPage];
|
||||
32
trampoline/src/pages/Background/types/common.ts
Normal file
32
trampoline/src/pages/Background/types/common.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Named type for strings that should be hexadecimal numbers.
|
||||
*
|
||||
* Currently *does not offer type safety*, just documentation value; see
|
||||
* https://github.com/microsoft/TypeScript/issues/202 and
|
||||
* https://github.com/microsoft/TypeScript/issues/41160 for TS features that
|
||||
* would give this some more teeth. Right now, any `string` can be assigned
|
||||
* into a variable of type `HexString` and vice versa.
|
||||
*/
|
||||
export type HexString = string;
|
||||
|
||||
/**
|
||||
* Named type for strings that should be domain names.
|
||||
*
|
||||
* Currently *does not offer type safety*, just documentation value; see
|
||||
* https://github.com/microsoft/TypeScript/issues/202 and
|
||||
* https://github.com/microsoft/TypeScript/issues/41160 for TS features that
|
||||
* would give this some more teeth. Right now, any `string` can be assigned
|
||||
* into a variable of type `DomainName` and vice versa.
|
||||
*/
|
||||
export type DomainName = string;
|
||||
|
||||
/**
|
||||
* Named type for strings that should be URIs.
|
||||
*
|
||||
* Currently *does not offer type safety*, just documentation value; see
|
||||
* https://github.com/microsoft/TypeScript/issues/202 and
|
||||
* https://github.com/microsoft/TypeScript/issues/41160 for TS features that
|
||||
* would give this some more teeth. Right now, any `string` can be assigned
|
||||
* into a variable of type `URI` and vice versa.
|
||||
*/
|
||||
export type URI = string;
|
||||
11
trampoline/src/pages/Background/types/keyrings.ts
Normal file
11
trampoline/src/pages/Background/types/keyrings.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { KeyringView } from '@epf-wallet/keyring-controller';
|
||||
|
||||
export type Keyring = {
|
||||
id: string | null;
|
||||
addresses: string[];
|
||||
};
|
||||
|
||||
export interface KeyringMetadata {
|
||||
view: KeyringView | null;
|
||||
source: 'import' | 'internal';
|
||||
}
|
||||
42
trampoline/src/pages/Background/types/network.ts
Normal file
42
trampoline/src/pages/Background/types/network.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { HexString } from './common';
|
||||
|
||||
export type NetworkFamily = 'EVM';
|
||||
|
||||
/**
|
||||
* Base asset of the network
|
||||
* Should be structurally compatible with FungibleAsset
|
||||
*/
|
||||
export type NetworkBaseAsset = {
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
contractAddress?: string;
|
||||
image?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a cryptocurrency network; these can potentially be L1 or L2.
|
||||
*/
|
||||
export type Network = {
|
||||
// two Networks must never share a name.
|
||||
name: string;
|
||||
baseAsset: NetworkBaseAsset;
|
||||
family: NetworkFamily;
|
||||
chainID?: string;
|
||||
provider: string;
|
||||
bundler: string;
|
||||
entryPointAddress: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An EVM-style network which *must* include a chainID.
|
||||
*/
|
||||
export type EVMNetwork = Network & {
|
||||
chainID: string;
|
||||
family: 'EVM';
|
||||
};
|
||||
|
||||
export type AddressOnNetwork = {
|
||||
address: HexString;
|
||||
network: EVMNetwork;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type SCWImplementation = string;
|
||||
222
trampoline/src/pages/Background/utils/index.ts
Normal file
222
trampoline/src/pages/Background/utils/index.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { hexlify, toUtf8Bytes, toUtf8String } from 'ethers/lib/utils.js';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { AllowedQueryParamPageType } from '../types/chrome-messages';
|
||||
import { SiweMessage } from 'siwe';
|
||||
import { HexString } from '../types/common';
|
||||
import {
|
||||
EthersTransactionRequest,
|
||||
PermissionRequest,
|
||||
} from '../services/provider-bridge';
|
||||
import {
|
||||
EIP1193Error,
|
||||
EIP1193_ERROR_CODES,
|
||||
} from '../../Content/window-provider/eip-1193';
|
||||
/**
|
||||
* Encode an unknown input as JSON, special-casing bigints and undefined.
|
||||
*
|
||||
* @param input an object, array, or primitive to encode as JSON
|
||||
*/
|
||||
export function encodeJSON(input: unknown): string {
|
||||
return JSON.stringify(input, (_, value) => {
|
||||
if (typeof value === 'bigint') {
|
||||
return { B_I_G_I_N_T: value.toString() };
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a JSON string, as encoded by `encodeJSON`, including bigint support.
|
||||
* Note that the functions aren't invertible, as `encodeJSON` discards
|
||||
* `undefined`.
|
||||
*
|
||||
* @param input a string output from `encodeJSON`
|
||||
*/
|
||||
export function decodeJSON(input: string): unknown {
|
||||
return JSON.parse(input, (_, value) =>
|
||||
value !== null && typeof value === 'object' && 'B_I_G_I_N_T' in value
|
||||
? BigInt(value.B_I_G_I_N_T)
|
||||
: value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 0x-prefixed hexadecimal representation of a number or string chainID
|
||||
* while also handling cases where an already hexlified chainID is passed in.
|
||||
*/
|
||||
export function toHexChainID(chainID: string | number): string {
|
||||
if (typeof chainID === 'string' && chainID.startsWith('0x')) {
|
||||
return chainID.toLowerCase();
|
||||
}
|
||||
return `0x${BigInt(chainID).toString(16)}`;
|
||||
}
|
||||
|
||||
export default async function showExtensionPopup(
|
||||
url: AllowedQueryParamPageType
|
||||
): Promise<browser.Windows.Window> {
|
||||
const { left = 0, top, width = 1920 } = await browser.windows.getCurrent();
|
||||
const popupWidth = 384;
|
||||
const popupHeight = 628;
|
||||
return browser.windows.create({
|
||||
url: `${browser.runtime.getURL('popup.html')}#${url}`,
|
||||
type: 'popup',
|
||||
left: left + width - popupWidth,
|
||||
top,
|
||||
width: popupWidth,
|
||||
height: popupHeight,
|
||||
focused: true,
|
||||
});
|
||||
}
|
||||
|
||||
export type EIP191Data = string;
|
||||
|
||||
// spec found https://eips.ethereum.org/EIPS/eip-4361
|
||||
export interface EIP4361Data {
|
||||
/**
|
||||
* The message string that was parsed to produce this EIP-4361 data.
|
||||
*/
|
||||
unparsedMessageData: string;
|
||||
domain: string;
|
||||
address: string;
|
||||
version: string;
|
||||
chainId: number;
|
||||
nonce: string;
|
||||
expiration?: string;
|
||||
statement?: string;
|
||||
}
|
||||
|
||||
type EIP191SigningData = {
|
||||
messageType: 'eip191';
|
||||
signingData: EIP191Data;
|
||||
};
|
||||
|
||||
type EIP4361SigningData = {
|
||||
messageType: 'eip4361';
|
||||
signingData: EIP4361Data;
|
||||
};
|
||||
|
||||
export type MessageSigningData = EIP191SigningData | EIP4361SigningData;
|
||||
|
||||
const checkEIP4361: (message: string) => EIP4361Data | undefined = (
|
||||
message
|
||||
) => {
|
||||
try {
|
||||
const siweMessage = new SiweMessage(message);
|
||||
return {
|
||||
unparsedMessageData: message,
|
||||
domain: siweMessage.domain,
|
||||
address: siweMessage.address,
|
||||
statement: siweMessage.statement,
|
||||
version: siweMessage.version,
|
||||
chainId: siweMessage.chainId,
|
||||
expiration: siweMessage.expirationTime,
|
||||
nonce: siweMessage.nonce,
|
||||
};
|
||||
} catch (err) {
|
||||
// console.error(err)
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a string and parses the string into a ExpectedSigningData Type
|
||||
*
|
||||
* EIP4361 standard can be found https://eips.ethereum.org/EIPS/eip-4361
|
||||
*/
|
||||
export function parseSigningData(signingData: string): MessageSigningData {
|
||||
let normalizedData = signingData;
|
||||
|
||||
// Attempt to normalize hex signing data to a UTF-8 string message. If the
|
||||
// signing data is <= 32 bytes long, assume it's a hash or other short data
|
||||
// that need not be normalized to a regular UTF-8 string.
|
||||
if (signingData.startsWith('0x') && signingData.length > 66) {
|
||||
let possibleMessageString: string | undefined;
|
||||
try {
|
||||
possibleMessageString = toUtf8String(signingData);
|
||||
// Below, if the signing data is not a valid UTF-8 string, we move on
|
||||
// with an undefined possibleMessageString.
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (err) {}
|
||||
|
||||
// If the hex was parsable as UTF-8 and re-converting to bytes in a hex
|
||||
// string produces the identical output, accept it as a valid string and
|
||||
// set the interpreted data to the UTF-8 string.
|
||||
if (
|
||||
possibleMessageString !== undefined &&
|
||||
hexlify(toUtf8Bytes(possibleMessageString)) === signingData.toLowerCase()
|
||||
) {
|
||||
normalizedData = possibleMessageString;
|
||||
}
|
||||
}
|
||||
|
||||
const data = checkEIP4361(normalizedData);
|
||||
if (data) {
|
||||
return {
|
||||
messageType: 'eip4361',
|
||||
signingData: data,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
messageType: 'eip191',
|
||||
signingData: normalizedData,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHexAddress(address: any) {
|
||||
var addressString =
|
||||
typeof address === 'object' && !('toLowerCase' in address)
|
||||
? address.toString('hex')
|
||||
: address;
|
||||
var noPrefix = addressString.replace(/^0x/, '');
|
||||
var even = noPrefix.length % 2 === 0 ? noPrefix : '0' + noPrefix;
|
||||
return '0x' + Buffer.from(even, 'hex').toString('hex');
|
||||
}
|
||||
|
||||
export function sameEVMAddress(
|
||||
address1: string | Buffer | undefined | null,
|
||||
address2: string | Buffer | undefined | null
|
||||
): boolean {
|
||||
if (
|
||||
typeof address1 === 'undefined' ||
|
||||
typeof address2 === 'undefined' ||
|
||||
address1 === null ||
|
||||
address2 === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return normalizeHexAddress(address1) === normalizeHexAddress(address2);
|
||||
}
|
||||
|
||||
export function checkPermissionSign(
|
||||
walletAddress: HexString,
|
||||
enablingPermission: PermissionRequest
|
||||
): void {
|
||||
if (
|
||||
enablingPermission.state !== 'allow' ||
|
||||
!sameEVMAddress(walletAddress, enablingPermission.accountAddress)
|
||||
) {
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.unauthorized);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkPermissionSignTransaction(
|
||||
transactionRequest: EthersTransactionRequest,
|
||||
enablingPermission: PermissionRequest
|
||||
): void {
|
||||
if (typeof transactionRequest.chainId !== 'undefined') {
|
||||
if (
|
||||
toHexChainID(transactionRequest.chainId) !==
|
||||
toHexChainID(enablingPermission.chainID)
|
||||
) {
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.unauthorized);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enablingPermission.state !== 'allow' ||
|
||||
!sameEVMAddress(transactionRequest.from, enablingPermission.accountAddress)
|
||||
) {
|
||||
throw new EIP1193Error(EIP1193_ERROR_CODES.unauthorized);
|
||||
}
|
||||
}
|
||||
89
trampoline/src/pages/Content/index.ts
Normal file
89
trampoline/src/pages/Content/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/// <reference types="chrome"/>
|
||||
|
||||
import {
|
||||
AA_EXTENSION_CONFIG,
|
||||
EXTERNAL_PORT_NAME,
|
||||
PROVIDER_BRIDGE_TARGET,
|
||||
WINDOW_PROVIDER_TARGET,
|
||||
} from '../Background/constants';
|
||||
|
||||
const windowOriginAtLoadTime = window.location.origin;
|
||||
|
||||
export function connectProviderBridge(): void {
|
||||
const port = chrome.runtime.connect({ name: EXTERNAL_PORT_NAME });
|
||||
window.addEventListener('message', (event) => {
|
||||
if (
|
||||
event.origin === windowOriginAtLoadTime && // we want to recieve msgs only from the in-page script
|
||||
event.source === window && // we want to recieve msgs only from the in-page script
|
||||
event.data.target === PROVIDER_BRIDGE_TARGET
|
||||
) {
|
||||
// if dapp wants to connect let's grab its details
|
||||
if (
|
||||
event.data.request.method === 'eth_requestAccounts' ||
|
||||
event.data.request.method === 'eth_accounts'
|
||||
) {
|
||||
const faviconElements: NodeListOf<HTMLLinkElement> =
|
||||
window.document.querySelectorAll("link[rel*='icon']");
|
||||
const largestFavicon = [...faviconElements].sort((el) =>
|
||||
parseInt(el.sizes?.toString().split('x')[0], 10)
|
||||
)[0];
|
||||
const faviconUrl = largestFavicon?.href ?? '';
|
||||
const { title } = window.document ?? '';
|
||||
|
||||
event.data.request.params.push(title, faviconUrl);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`%c content: inpage >>> background: ${JSON.stringify(event.data)}`,
|
||||
'background: #bada55; color: #222'
|
||||
);
|
||||
|
||||
port.postMessage(event.data);
|
||||
}
|
||||
});
|
||||
|
||||
port.onMessage.addListener((data) => {
|
||||
console.log(
|
||||
`%c content: background >>> inpage: ${JSON.stringify(data)}`,
|
||||
'background: #222; color: #bada55'
|
||||
);
|
||||
window.postMessage(
|
||||
{
|
||||
...data,
|
||||
target: WINDOW_PROVIDER_TARGET,
|
||||
},
|
||||
windowOriginAtLoadTime
|
||||
);
|
||||
});
|
||||
|
||||
// let's grab the internal config that also has chainId info
|
||||
// we send the config on port initialization, but that needs to
|
||||
// be as fast as possible, so we omit the chainId information
|
||||
// from that payload to save the service call
|
||||
port.postMessage({
|
||||
request: { method: AA_EXTENSION_CONFIG, origin: windowOriginAtLoadTime },
|
||||
});
|
||||
}
|
||||
|
||||
function injectWindowProvider(): void {
|
||||
if (document.contentType !== 'text/html') return;
|
||||
|
||||
try {
|
||||
const container = document.head || document.documentElement;
|
||||
const scriptTag = document.createElement('script');
|
||||
// this makes the script loading blocking which is good for us
|
||||
// bc we want to load before anybody has a chance to temper w/ the window obj
|
||||
scriptTag.setAttribute('async', 'false');
|
||||
scriptTag.src = chrome.runtime.getURL('ex_injectScript.bundle.js');
|
||||
container.insertBefore(scriptTag, container.children[0]);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`aa-account: oh nos the content-script failed to initilaize the window provider.
|
||||
${e}
|
||||
It's time to be dead...🗡`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
injectWindowProvider();
|
||||
connectProviderBridge();
|
||||
114
trampoline/src/pages/Content/inject.ts
Normal file
114
trampoline/src/pages/Content/inject.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
WindowRequestEvent,
|
||||
WindowListener,
|
||||
Window,
|
||||
WindowEthereum,
|
||||
WalletProvider,
|
||||
} from './types';
|
||||
import AAWindowProvider from './window-provider';
|
||||
|
||||
Object.defineProperty(window, 'aa-provider', {
|
||||
value: new AAWindowProvider({
|
||||
postMessage: (data: WindowRequestEvent) =>
|
||||
window.postMessage(data, window.location.origin),
|
||||
addEventListener: (fn: WindowListener) =>
|
||||
window.addEventListener('message', fn, false),
|
||||
removeEventListener: (fn: WindowListener) =>
|
||||
window.removeEventListener('message', fn, false),
|
||||
origin: window.location.origin,
|
||||
}),
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
if (!(window as Window).walletRouter) {
|
||||
Object.defineProperty(window, 'walletRouter', {
|
||||
value: {
|
||||
currentProvider: (window as Window)['aa-provider'],
|
||||
lastInjectedProvider: window.ethereum,
|
||||
providers: [
|
||||
// deduplicate the providers array: https://medium.com/@jakubsynowiec/unique-array-values-in-javascript-7c932682766c
|
||||
...new Set([
|
||||
(window as Window)['aa-provider'],
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
...(window.ethereum
|
||||
? // let's use the providers that has already been registered
|
||||
// This format is used by coinbase wallet
|
||||
Array.isArray(window.ethereum.providers)
|
||||
? [...window.ethereum.providers, window.ethereum]
|
||||
: [window.ethereum]
|
||||
: []),
|
||||
(window as Window)['aa-provider'],
|
||||
]),
|
||||
],
|
||||
getProviderInfo(provider: WalletProvider) {
|
||||
return (
|
||||
provider.providerInfo || {
|
||||
label: 'Injected Provider',
|
||||
injectedNamespace: 'ethereum',
|
||||
}
|
||||
);
|
||||
},
|
||||
setSelectedProvider() {},
|
||||
addProvider(newProvider: WalletProvider) {
|
||||
if (!this.providers.includes(newProvider)) {
|
||||
this.providers.push(newProvider);
|
||||
}
|
||||
|
||||
this.lastInjectedProvider = newProvider;
|
||||
},
|
||||
},
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
|
||||
let cachedWindowEthereumProxy: WindowEthereum;
|
||||
let cachedCurrentProvider: WalletProvider;
|
||||
|
||||
Object.defineProperty(window, 'ethereum', {
|
||||
get() {
|
||||
const walletRouter = (window as Window).walletRouter;
|
||||
|
||||
if (!walletRouter) return undefined;
|
||||
|
||||
if (
|
||||
cachedWindowEthereumProxy &&
|
||||
cachedCurrentProvider === walletRouter.currentProvider
|
||||
) {
|
||||
return cachedWindowEthereumProxy;
|
||||
}
|
||||
cachedWindowEthereumProxy = new Proxy(walletRouter.currentProvider, {
|
||||
get(target, prop, receiver) {
|
||||
if (
|
||||
walletRouter &&
|
||||
!(prop in walletRouter.currentProvider) &&
|
||||
prop in walletRouter
|
||||
) {
|
||||
// Uniswap MM connector checks the providers array for the MM provider and forces to use that
|
||||
// https://github.com/Uniswap/web3-react/blob/main/packages/metamask/src/index.ts#L57
|
||||
// as a workaround we need to remove this list for uniswap so the actual provider change can work after reload.
|
||||
// The same is true for `galaxy.eco`
|
||||
if (
|
||||
(window.location.href.includes('app.uniswap.org') ||
|
||||
window.location.href.includes('kwenta.io') ||
|
||||
window.location.href.includes('galxe.com')) &&
|
||||
prop === 'providers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// let's publish the api of `window.walletRouter` also on `window.ethereum` for better discoverability
|
||||
|
||||
// @ts-expect-error ts accepts symbols as index only from 4.4
|
||||
// https://stackoverflow.com/questions/59118271/using-symbol-as-object-key-type-in-typescript
|
||||
return window.walletRouter[prop];
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
cachedCurrentProvider = walletRouter.currentProvider;
|
||||
|
||||
return cachedWindowEthereumProxy;
|
||||
},
|
||||
});
|
||||
101
trampoline/src/pages/Content/types.ts
Normal file
101
trampoline/src/pages/Content/types.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { EIP1193_ERROR_CODES } from './window-provider/eip-1193';
|
||||
|
||||
export type WalletProvider = {
|
||||
providerInfo?: {
|
||||
label: string;
|
||||
injectedNamespace: string;
|
||||
iconURL: string;
|
||||
identityFlag?: string;
|
||||
checkIdentity?: () => boolean;
|
||||
};
|
||||
on: (
|
||||
eventName: string | symbol,
|
||||
listener: (...args: unknown[]) => void
|
||||
) => unknown;
|
||||
removeListener: (
|
||||
eventName: string | symbol,
|
||||
listener: (...args: unknown[]) => void
|
||||
) => unknown;
|
||||
[optionalProps: string]: unknown;
|
||||
};
|
||||
|
||||
export type WindowEthereum = WalletProvider & {
|
||||
isMetaMask?: boolean;
|
||||
autoRefreshOnNetworkChange?: boolean;
|
||||
};
|
||||
|
||||
export interface Window {
|
||||
'aa-provider'?: {};
|
||||
walletRouter?: {
|
||||
currentProvider: WalletProvider;
|
||||
providers: WalletProvider[];
|
||||
shouldSetTallyForCurrentProvider: (
|
||||
shouldSetTally: boolean,
|
||||
shouldReload?: boolean
|
||||
) => void;
|
||||
getProviderInfo: (
|
||||
provider: WalletProvider
|
||||
) => WalletProvider['providerInfo'];
|
||||
addProvider: (newProvider: WalletProvider) => void;
|
||||
};
|
||||
ethereum?: WindowEthereum;
|
||||
oldEthereum?: WindowEthereum;
|
||||
}
|
||||
|
||||
export type RPCRequest = {
|
||||
method: string;
|
||||
params: Array<unknown>; // This typing is required by ethers.js but is not EIP-1193 compatible
|
||||
};
|
||||
|
||||
export type WindowRequestEvent = {
|
||||
id: string;
|
||||
target: unknown;
|
||||
request: RPCRequest;
|
||||
};
|
||||
|
||||
export type WindowResponseEvent = {
|
||||
origin: string;
|
||||
source: unknown;
|
||||
data: { id: string; target: string; result: unknown };
|
||||
};
|
||||
|
||||
export type WindowListener = (event: WindowResponseEvent) => void;
|
||||
|
||||
export type PortResponseEvent = {
|
||||
id: string;
|
||||
jsonrpc: '2.0';
|
||||
result: unknown;
|
||||
};
|
||||
|
||||
export type EIP1193ErrorPayload =
|
||||
| (typeof EIP1193_ERROR_CODES)[keyof typeof EIP1193_ERROR_CODES] & {
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
export type WindowTransport = {
|
||||
postMessage: (data: WindowRequestEvent) => void;
|
||||
addEventListener: (listener: WindowListener) => void;
|
||||
removeEventListener: (listener: WindowListener) => void;
|
||||
origin: string;
|
||||
};
|
||||
|
||||
export type PortListenerFn = (callback: unknown, ...params: unknown[]) => void;
|
||||
export type PortListener = (listener: PortListenerFn) => void;
|
||||
|
||||
export type PortTransport = {
|
||||
postMessage: (data: unknown) => void;
|
||||
addEventListener: PortListener;
|
||||
removeEventListener: PortListener;
|
||||
origin: string;
|
||||
};
|
||||
|
||||
export type ProviderTransport = WindowTransport | PortTransport;
|
||||
|
||||
export type EthersSendCallback = (error: unknown, response: unknown) => void;
|
||||
|
||||
export type AAExtensionConfigPayload = {
|
||||
method: 'aa-extension_getConfig';
|
||||
chainId?: string;
|
||||
shouldReload?: boolean;
|
||||
[prop: string]: unknown;
|
||||
};
|
||||
83
trampoline/src/pages/Content/window-provider/eip-1193.ts
Normal file
83
trampoline/src/pages/Content/window-provider/eip-1193.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// https://eips.ethereum.org/EIPS/eip-1193#request
|
||||
|
||||
import { isNumber, isObject, isString } from './runtime-type-checks';
|
||||
|
||||
export type RequestArgument = {
|
||||
readonly method: string;
|
||||
readonly params?: Array<unknown>;
|
||||
};
|
||||
|
||||
export const EIP1193_ERROR_CODES = {
|
||||
userRejectedRequest: {
|
||||
code: 4001,
|
||||
message: 'The user rejected the request.',
|
||||
},
|
||||
unauthorized: {
|
||||
code: 4100,
|
||||
message:
|
||||
'The requested method and/or account has not been authorized by the user.',
|
||||
},
|
||||
unsupportedMethod: {
|
||||
code: 4200,
|
||||
message: 'The Provider does not support the requested method.',
|
||||
},
|
||||
disconnected: {
|
||||
// 4900 is intended to indicate that the Provider is disconnected from all chains
|
||||
code: 4900,
|
||||
message: 'The Provider is disconnected from all chains.',
|
||||
},
|
||||
chainDisconnected: {
|
||||
// 4901 is intended to indicate that the Provider is disconnected from a specific chain only.
|
||||
// In other words, 4901 implies that the Provider is connected to other chains, just not the requested one.
|
||||
code: 4901,
|
||||
message: 'The Provider is not connected to the requested chain.',
|
||||
},
|
||||
rpcErrorNotParsed: {
|
||||
code: 5000,
|
||||
message:
|
||||
"The budler RPC response coudn't be parsed properly, check bundler logs",
|
||||
},
|
||||
methodNotSupported: {
|
||||
code: 4004,
|
||||
message: 'The method is not supported',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type EIP1193ErrorPayload =
|
||||
| (typeof EIP1193_ERROR_CODES)[keyof typeof EIP1193_ERROR_CODES] & {
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
export type EIP1193ErrorCodeNumbers = Pick<
|
||||
(typeof EIP1193_ERROR_CODES)[keyof typeof EIP1193_ERROR_CODES],
|
||||
'code'
|
||||
>;
|
||||
export class EIP1193Error extends Error {
|
||||
constructor(public eip1193Error: EIP1193ErrorPayload) {
|
||||
super(eip1193Error.message);
|
||||
}
|
||||
|
||||
toJSON(): unknown {
|
||||
return this.eip1193Error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isEIP1193ErrorCodeNumber(
|
||||
code: unknown
|
||||
): code is EIP1193ErrorCodeNumbers {
|
||||
return (
|
||||
isNumber(code) &&
|
||||
Object.values(EIP1193_ERROR_CODES)
|
||||
.map((e) => e.code as number)
|
||||
.includes(code)
|
||||
);
|
||||
}
|
||||
|
||||
export function isEIP1193Error(arg: unknown): arg is EIP1193ErrorPayload {
|
||||
return (
|
||||
isObject(arg) &&
|
||||
isNumber(arg.code) &&
|
||||
isEIP1193ErrorCodeNumber(arg.code) &&
|
||||
isString(arg.message)
|
||||
);
|
||||
}
|
||||
253
trampoline/src/pages/Content/window-provider/index.ts
Normal file
253
trampoline/src/pages/Content/window-provider/index.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
PROVIDER_BRIDGE_TARGET,
|
||||
WINDOW_PROVIDER_TARGET,
|
||||
} from '../../Background/constants';
|
||||
import {
|
||||
EthersSendCallback,
|
||||
ProviderTransport,
|
||||
WalletProvider,
|
||||
} from '../types';
|
||||
import { isEIP1193Error, RequestArgument } from './eip-1193';
|
||||
import {
|
||||
isObject,
|
||||
isPortResponseEvent,
|
||||
isWindowResponseEvent,
|
||||
} from './runtime-type-checks';
|
||||
|
||||
const METAMASK_STATE_MOCK = {
|
||||
accounts: null,
|
||||
isConnected: false,
|
||||
isUnlocked: false,
|
||||
initialized: false,
|
||||
isPermanentlyDisconnected: false,
|
||||
};
|
||||
|
||||
export default class AAWindowProvider extends EventEmitter {
|
||||
// TODO: This should come from the background with onConnect when any interaction is initiated by the dApp.
|
||||
// onboard.js relies on this, or uses a deprecated api. It seemed to be a reasonable workaround for now.
|
||||
chainId = '0x5';
|
||||
|
||||
selectedAddress: string | undefined;
|
||||
|
||||
connected = false;
|
||||
|
||||
isAAExtension = true;
|
||||
|
||||
isMetaMask = false;
|
||||
|
||||
isWeb3 = true;
|
||||
|
||||
requestResolvers = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (value: unknown) => void;
|
||||
sendData: {
|
||||
id: string;
|
||||
target: string;
|
||||
request: Required<RequestArgument>;
|
||||
};
|
||||
}
|
||||
>();
|
||||
|
||||
_state?: typeof METAMASK_STATE_MOCK;
|
||||
|
||||
providerInfo = {
|
||||
label: 'AA Chrome Extension!',
|
||||
injectedNamespace: 'AA-Chrome-Extension',
|
||||
identityFlag: 'isAAExtension',
|
||||
checkIdentity: (provider: WalletProvider) =>
|
||||
!!provider && !!provider.isAAExtension,
|
||||
} as const;
|
||||
|
||||
constructor(public transport: ProviderTransport) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Some dApps may have a problem with preserving a reference to a provider object.
|
||||
* This is the result of incorrect assignment.
|
||||
* In such a case, the object this is undefined
|
||||
* which results in an error in the execution of the request.
|
||||
* The request function should always have a provider object set.
|
||||
*/
|
||||
// this.transport.addEventListener(internalListener);
|
||||
this.transport.addEventListener(this.internalBridgeListener);
|
||||
}
|
||||
|
||||
private internalBridgeListener = (event: unknown): void => {
|
||||
let id: string;
|
||||
let result: unknown;
|
||||
|
||||
if (isWindowResponseEvent(event)) {
|
||||
if (
|
||||
event.origin !== this.transport.origin || // filter to messages claiming to be from the provider-bridge script
|
||||
event.source !== window || // we want to recieve messages only from the provider-bridge script
|
||||
event.data.target !== WINDOW_PROVIDER_TARGET
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
({ id, result } = event.data);
|
||||
} else if (isPortResponseEvent(event)) {
|
||||
({ id, result } = event);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestResolver = this.requestResolvers.get(id);
|
||||
|
||||
if (!requestResolver) return;
|
||||
|
||||
const { sendData, reject, resolve } = requestResolver;
|
||||
|
||||
this.requestResolvers.delete(sendData.id);
|
||||
|
||||
const { method: sentMethod } = sendData.request;
|
||||
|
||||
if (isEIP1193Error(result)) {
|
||||
reject(result);
|
||||
}
|
||||
|
||||
// let's emit connected on the first successful response from background
|
||||
if (!this.connected) {
|
||||
this.connected = true;
|
||||
this.emit('connect', { chainId: this.chainId });
|
||||
}
|
||||
|
||||
switch (sentMethod) {
|
||||
case 'wallet_switchEthereumChain':
|
||||
case 'wallet_addEthereumChain':
|
||||
// null result indicates successful chain change https://eips.ethereum.org/EIPS/eip-3326#specification
|
||||
if (result === null) {
|
||||
this.handleChainIdChange(
|
||||
(sendData.request.params[0] as { chainId: string }).chainId
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'eth_chainId':
|
||||
case 'net_version':
|
||||
if (
|
||||
typeof result === 'string' &&
|
||||
Number(this.chainId) !== Number(result)
|
||||
) {
|
||||
this.handleChainIdChange(result);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'eth_accounts':
|
||||
case 'eth_requestAccounts':
|
||||
if (Array.isArray(result) && result.length !== 0) {
|
||||
this.handleAddressChange(result);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
// deprecated EIP-1193 method
|
||||
async enable(): Promise<unknown> {
|
||||
return this.request({ method: 'eth_requestAccounts' });
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
// deprecated EIP1193 send for web3-react injected provider Send type:
|
||||
// https://github.com/NoahZinsmeister/web3-react/blob/d0b038c748a42ec85641a307e6c588546d86afc2/packages/injected-connector/src/types.ts#L4
|
||||
send(method: string, params: Array<unknown>): Promise<unknown>;
|
||||
// deprecated EIP1193 send for ethers.js Web3Provider > ExternalProvider:
|
||||
// https://github.com/ethers-io/ethers.js/blob/73a46efea32c3f9a4833ed77896a216e3d3752a0/packages/providers/src.ts/web3-provider.ts#L19
|
||||
send(
|
||||
request: RequestArgument,
|
||||
callback: (error: unknown, response: unknown) => void
|
||||
): void;
|
||||
send(
|
||||
methodOrRequest: string | RequestArgument,
|
||||
paramsOrCallback: Array<unknown> | EthersSendCallback
|
||||
): Promise<unknown> | void {
|
||||
if (
|
||||
typeof methodOrRequest === 'string' &&
|
||||
typeof paramsOrCallback !== 'function'
|
||||
) {
|
||||
return this.request({
|
||||
method: methodOrRequest,
|
||||
params: paramsOrCallback,
|
||||
});
|
||||
}
|
||||
|
||||
if (isObject(methodOrRequest) && typeof paramsOrCallback === 'function') {
|
||||
return this.sendAsync(methodOrRequest, paramsOrCallback);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Unsupported function parameters'));
|
||||
}
|
||||
|
||||
// deprecated EIP-1193 method
|
||||
// added as some dapps are still using it
|
||||
sendAsync(
|
||||
request: RequestArgument & { id?: number; jsonrpc?: string },
|
||||
callback: (error: unknown, response: unknown) => void
|
||||
): Promise<unknown> | void {
|
||||
return this.request(request).then(
|
||||
(response) =>
|
||||
callback(null, {
|
||||
result: response,
|
||||
id: request.id,
|
||||
jsonrpc: request.jsonrpc,
|
||||
}),
|
||||
(error) => callback(error, null)
|
||||
);
|
||||
}
|
||||
|
||||
// Provider-wide counter for requests.
|
||||
private requestID = 0n;
|
||||
|
||||
request = (arg: RequestArgument): Promise<unknown> => {
|
||||
const { method, params = [] } = arg;
|
||||
if (typeof method !== 'string') {
|
||||
return Promise.reject(new Error(`unsupported method type: ${method}`));
|
||||
}
|
||||
|
||||
const sendData = {
|
||||
id: this.requestID.toString(),
|
||||
target: PROVIDER_BRIDGE_TARGET,
|
||||
request: {
|
||||
method,
|
||||
params,
|
||||
},
|
||||
};
|
||||
|
||||
this.requestID += 1n;
|
||||
|
||||
this.transport.postMessage(sendData);
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
this.requestResolvers.set(sendData.id, {
|
||||
resolve,
|
||||
reject,
|
||||
sendData,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleChainIdChange(chainId: string): void {
|
||||
this.chainId = chainId;
|
||||
this.emit('chainChanged', chainId);
|
||||
this.emit('networkChanged', Number(chainId).toString());
|
||||
}
|
||||
|
||||
handleAddressChange(address: Array<string>): void {
|
||||
if (this.selectedAddress !== address[0]) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.selectedAddress = address[0];
|
||||
this.emit('accountsChanged', address);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user