├── README.md
├── StealthSafe.png
├── Umbra.png
├── contracts
├── .gitignore
├── .idea
│ ├── .gitignore
│ ├── contracts.iml
│ ├── modules.xml
│ └── vcs.xml
├── README.md
└── src
│ ├── StealthKeyRegistry.sol
│ └── UmbraSafe.sol
├── erc5564SmartAccountScheme.md
└── frontend
├── .env
├── .gitignore
├── GlobalStyles.ts
├── README.md
├── components
├── Const.ts
├── eth-crypto
│ ├── decryptPrivateViewKey.tsx
│ ├── encryptPrivateViewKey.tsx
│ └── test.tsx
├── safe
│ ├── safeApiKit.tsx
│ ├── safeDeploy.tsx
│ └── safeEthersAdapter.tsx
├── safeKeyRegistry
│ ├── addSafe.tsx
│ └── getSafe.tsx
├── umbra
│ ├── generateAddressFromKey.tsx
│ ├── getStealthKeys.tsx
│ ├── keyPairExtended.tsx
│ └── umbraExtended.tsx
└── utils
│ ├── clientToSigner.tsx
│ └── getEvents.tsx
├── context
├── ReceiveContext.tsx
└── SendContext.tsx
├── hooks
└── ui
│ └── mediaQueryHooks.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── api
│ └── hello.ts
├── index.tsx
├── index_test.tsx
├── receive.tsx
├── receiveFunctions.tsx
├── send.tsx
└── sendFunctions.tsx
├── public
├── favicon.ico
├── next.svg
├── safe_logo.png
├── vercel.svg
└── xdai_logo.webp
├── styles
├── Home.module.css
└── globals.css
├── tsconfig.json
├── ui
└── organisms
│ ├── Common.Header
│ └── Common.Header.tsx
│ ├── Receive.ListOfWithdrawals
│ ├── Receive.ListOfWithdrawals.tsx
│ └── WithdrawButton.tsx
│ ├── Receive.RegisterSafe
│ ├── Receive.RegisterSafe.tsx
│ └── SuccessInitialized.tsx
│ ├── Receive.SelectSafe
│ └── Receive.SelectSafe.tsx
│ └── Send.ReceiverAndAmount
│ └── Send.ReceiverAndAmount.tsx
├── utils
└── web3
│ └── address.ts
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # Stealth Safes
2 |
3 | Stealth Safes enables stealth addresses for Safe multisigs, facilitating privacy-preserving transactions.
4 |
5 | ## Introduction
6 |
7 | A stealth address is a one-time use address for a specific transaction, generated by the payment sender and exclusively controlled by the payment recipient. It allows for transactional privacy: an external observer is unable to link the recipient to the stealth address. [This post](https://vitalik.ca/general/2023/01/20/stealth.html) by Vitalik Buterin is a great primer on the topic.
8 |
9 |
10 |
11 | There is currently an [EIP](https://eips.ethereum.org/EIPS/eip-5564) proposing a standardized approach to stealth addresses. In the meantime, a production-ready implementation of stealth addresses is the [Umbra protocol](https://app.umbra.cash/faq#how-does-it-work-technical).
12 |
13 |
14 |
15 | With Stealth Safes, a sender deploys a Safe mirroring the parameters of its parent Safe, except the owners are stealth addresses of the parent Safe owners. The Safe owners can then redeem the transfer from the stealth Safe using their personal stealth addresses and a relayer to fund gas from the funds in the stealth Safe itself. This ensures there is no apparent link between the stealth Safe and its parent Safe to an external observer.
16 |
17 | ## Links
18 |
19 | * [Initial implementation proposal](https://statuesque-shirt-254.notion.site/Stealth-Addresses-for-Safe-ad86245b95864289836fb360ed2427e1)
20 | * [POC for Stealth Safes developed at EthGlobal Paris 2023](https://ethglobal.com/showcase/stealth-safes-n6aj5):
21 | * 🏆 Gnosis Chain — Best Safe Project
22 | * 🏆 Safe — 🥇 Safe{Core} AA
23 |
24 | ## Contribute
25 |
26 | Contributions are welcome! Check issues in this repo and feel free to reach out.
--------------------------------------------------------------------------------
/StealthSafe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/StealthSafe.png
--------------------------------------------------------------------------------
/Umbra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/Umbra.png
--------------------------------------------------------------------------------
/contracts/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
--------------------------------------------------------------------------------
/contracts/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/contracts/.idea/contracts.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/contracts/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/contracts/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/contracts/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/contracts/README.md
--------------------------------------------------------------------------------
/contracts/src/StealthKeyRegistry.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.7;
4 |
5 | contract StealthKeyRegistry {
6 |
7 | // =========================================== Structs ============================================
8 |
9 | struct EncryptedSafeViewPrivateKey {
10 | bytes encKey;
11 | address owner;
12 | }
13 |
14 | // =========================================== Events ============================================
15 |
16 | /// @dev Event emitted when a multisig updates their registered stealth keys
17 | event StealthSafeKeyChanged(
18 | address indexed registrant, // safe address
19 | uint256 viewingPubKeyPrefix,
20 | uint256 viewingPubKey,
21 | address[] owners
22 | );
23 |
24 | // ======================================= State variables =======================================
25 |
26 | /**
27 | * @dev Mapping used to store one secp256k1 curve private key used for
28 | * decoding available stealth payments. The mapping records the private viewing
29 | * key, encrypted for the different safe users, so anyone can see the availability of new incoming payments.
30 | * Private view key can be set and read via the `setStealthKeys`
31 | * and `stealthKey` methods respectively.
32 | *
33 | * The mapping associates the safe's address to an array of EncryptedSafeViewPrivateKey.
34 | * Array contains the owner addresses, with the encrypted view keys of the Safe.
35 | */
36 | mapping(address => EncryptedSafeViewPrivateKey[]) safePrivateKeys;
37 |
38 | /**
39 | * @dev Mapping used to store one secp256k1 curve public key used for
40 | * receiving stealth payments. The mapping records the viewing public
41 | * key of the safe.
42 | * The spending key is not needed, as the spending address is the
43 | * Safe account. View key can be set and read via the `setStealthKeys`
44 | * and `stealthKey` methods respectively.
45 | *
46 | * The mapping associates the safe's address to another mapping, which itself maps
47 | * the public key prefix to the actual key. This scheme is used to avoid using an
48 | * extra storage slot for the public key prefix. For a given address, the mapping
49 | * contains a viewing key at position 2 or 3. See the setter/getter methods for
50 | * details of how these map to prefixes.
51 | */
52 | mapping(address => mapping(uint256 => uint256)) keys;
53 |
54 | // ======================================= Set Keys ===============================================
55 |
56 | /**
57 | * @notice Sets stealth view public key for the caller, and the encrypted private keys for all the safe owners
58 | * @param _viewingPubKeyPrefix Prefix of the viewing public key (2 or 3)
59 | * @param _viewingPubKey The public key to use for encryption
60 | * @param _safeViewPrivateKeyList Private view key of Safe stored encrypted for each safe viewer
61 | */
62 | function setStealthKeys(
63 | uint256 _viewingPubKeyPrefix,
64 | uint256 _viewingPubKey,
65 | EncryptedSafeViewPrivateKey[] calldata _safeViewPrivateKeyList
66 | ) external {
67 | _setStealthKeys(msg.sender, _viewingPubKeyPrefix, _viewingPubKey, _safeViewPrivateKeyList);
68 | }
69 |
70 | /**
71 | * @dev Internal method for setting stealth key that must be called after safety
72 | * check on registrant; see calling method for parameter details
73 | */
74 | function _setStealthKeys(
75 | address _registrant,
76 | uint256 _viewingPubKeyPrefix,
77 | uint256 _viewingPubKey,
78 | EncryptedSafeViewPrivateKey[] calldata _safeViewPrivateKeyList
79 | ) internal {
80 | // TODO check the msg.sender is actually a valid gnosis safe
81 | require(
82 | _safeViewPrivateKeyList.length > 0,
83 | "StealthSafeKeyRegistry: Invalid Keys lenght"
84 | );
85 | require(
86 | (_viewingPubKeyPrefix == 2 || _viewingPubKeyPrefix == 3),
87 | "StealthKeyRegistry: Invalid ViewingPubKey Prefix"
88 | );
89 |
90 | // Store viewPubKey
91 |
92 | // Delete any existing values from safePrivateKeys[_registrant]
93 | delete safePrivateKeys[_registrant];
94 |
95 | // Ensure the opposite prefix indices are empty
96 | delete keys[_registrant][5 - _viewingPubKeyPrefix];
97 |
98 | // Set the appropriate indices to the new key values
99 | keys[_registrant][_viewingPubKeyPrefix] = _viewingPubKey;
100 |
101 | // store viewPrivateKey
102 | address[] memory _owners = new address[](_safeViewPrivateKeyList.length);
103 | EncryptedSafeViewPrivateKey[] storage pKey = safePrivateKeys[_registrant];
104 |
105 | for (uint i=0; i<_safeViewPrivateKeyList.length; ++i) {
106 | _owners[i] = _safeViewPrivateKeyList[i].owner;
107 | pKey.push(
108 | EncryptedSafeViewPrivateKey( _safeViewPrivateKeyList[i].encKey, _safeViewPrivateKeyList[i].owner)
109 | );
110 | }
111 |
112 | // emit event that keys were registered for a stealth
113 | emit StealthSafeKeyChanged(_registrant, _viewingPubKeyPrefix, _viewingPubKey, _owners);
114 | }
115 |
116 | // ======================================= Get Keys ===============================================
117 |
118 | /**
119 | * @notice Returns the stealth key associated with an address.
120 | * @param _registrant The address whose keys to lookup.
121 | * @return viewingPubKeyPrefix Prefix of the viewing public key (2 or 3)
122 | * @return viewingPubKey The public key to use for encryption
123 | * @return safeViewPrivateKeyList Array of view private keys
124 | */
125 | function stealthKeys(address _registrant)
126 | external
127 | view
128 | returns (
129 | uint256 viewingPubKeyPrefix,
130 | uint256 viewingPubKey,
131 | EncryptedSafeViewPrivateKey[] memory safeViewPrivateKeyList
132 | )
133 | {
134 | // read view keys
135 | if (keys[_registrant][2] != 0) {
136 | viewingPubKeyPrefix = 2;
137 | viewingPubKey = keys[_registrant][2];
138 | } else {
139 | viewingPubKeyPrefix = 3;
140 | viewingPubKey = keys[_registrant][3];
141 | }
142 |
143 | // read private keys
144 | EncryptedSafeViewPrivateKey[] storage _safePrivateKeysStorageRef = safePrivateKeys[_registrant];
145 | safeViewPrivateKeyList = new EncryptedSafeViewPrivateKey[](_safePrivateKeysStorageRef.length);
146 | for (uint i = 0; i < _safePrivateKeysStorageRef.length; ++i) {
147 | safeViewPrivateKeyList[i] = _safePrivateKeysStorageRef[i];
148 | }
149 |
150 | return (viewingPubKeyPrefix, viewingPubKey, safeViewPrivateKeyList);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/contracts/src/UmbraSafe.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.0;
4 |
5 | contract UmbraSafe {
6 | // =========================================== Events ============================================
7 |
8 | /// @notice Emitted when a payment is sent
9 | event Announcement(
10 | address indexed receiver, // stealth address
11 | uint256 amount, // funds
12 | address indexed token, // token address or ETH placeholder
13 | bytes32 pkx, // ephemeral public key x coordinate
14 | bytes32 ciphertext // encrypted entropy and payload extension
15 | );
16 |
17 | // ======================================= State variables =======================================
18 |
19 | /// @dev Placeholder address used to identify transfer of native ETH
20 | address internal constant ETH_TOKEN_PLACHOLDER = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
21 |
22 | // ======================================= Send =================================================
23 |
24 | /**
25 | * @notice Send and announce ETH payment to a stealth address
26 | * @param _receiver Stealth address receiving the payment
27 | * @param _tollCommitment Exact toll the sender is paying; should equal contract toll;
28 | * the committment is used to prevent frontrunning attacks by the owner;
29 | * see https://github.com/ScopeLift/umbra-protocol/issues/54 for more information
30 | * @param _pkx X-coordinate of the ephemeral public key used to encrypt the payload
31 | * @param _ciphertext Encrypted entropy (used to generated the stealth address) and payload extension
32 | */
33 | function sendEth(
34 | address payable _receiver,
35 | uint256 _tollCommitment, // set to 0 for demo simplicity
36 | bytes32 _pkx, // ephemeral public key x coordinate
37 | bytes32 _ciphertext
38 | ) external payable {
39 | require(msg.value > 0, "msg.value cannot be 0");
40 | uint amount = msg.value;
41 | emit Announcement(_receiver, amount, ETH_TOKEN_PLACHOLDER, _pkx, _ciphertext);
42 | _receiver.call{value: amount}("");
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/erc5564SmartAccountScheme.md:
--------------------------------------------------------------------------------
1 | # Smart Account Implementation of SECP256k1 with View Tags
2 |
3 | This implementation is derived from EIP-5564 Scheme 0, [SECP256k1 with View Tags](https://eips.ethereum.org/EIPS/eip-5564), and extends it to smart accounts that are controlled by multiple EOAs (e.g. [Safe](https://github.com/safe-global/safe-contracts)).
4 |
5 | The following reference is divided into four sections:
6 |
7 | 1. Stealth address generation
8 | 2. Stealth smart account deployment
9 | 3. Parsing announcements
10 | 4. Stealth private key derivation
11 |
12 | Definitions:
13 |
14 | - *G* represents the generator point of the curve.
15 | - *Recipient* represents the owner(s) of all $n$ EOAs controlling the smart account
16 |
17 | ### Generation - Generate stealth address from stealth meta-address:
18 |
19 | - Recipient has access to the smart account viewing private key $p_{viewSa}$ from which public key $P_{viewSa}$ is derived.
20 | - Recipient has access to the $n$ private keys $p_{spend}$ , $p_{view}$ from which public keys $P_{spend}$ , $P_{view}$ are derived.
21 | - Recipient has published a smart account stealth meta-address that consists of the smart account address $a_{publicSa}$ and of the public key $P_{viewSa}$.
22 | - The smart account at $a_{publicSa}$ consists of $n$ controllers and parameters $t$.
23 | - A smart contract at $a_{deployer}$ allows anyone to deploy a smart account of the same type as the one found at $a_{publicSa}$.
24 | - Recipient has published $n$ stealth meta-addresses that consist of the public keys $P_{spend}$ and $P_{view}$.
25 | - Sender passes the smart account stealth meta-address to the `generateStealthAddress` function.
26 | - The `generateStealthAddress` function performs the following computations:
27 | - Generate a random 32-byte entropy ephemeral private key $p_{epheremal}$.
28 | - Derive the ephemeral public key $P_{ephemeral}$ from $p_{epheremal}$.
29 | - Parse the smart account address and viewing public key, $a_{publicSa}$ and $P_{viewSa}$, from the smart account stealth meta-address.
30 | - Parse the $n$ spending keys $P_{spend}$ from the stealth meta-addresses of the $N$ EOAs controlling $a_{publicSa}$.
31 | - A shared secret $s$ is computed as $s = p_{ephemeral} \cdot P_{viewSa}$ .
32 | - The secret is hashed $s_{h} = \mathrm{h}(s)$.
33 | - The view tag $v$ is extracted by taking the most significant byte $s_{h}[0]$.
34 | - Multiply the hashed shared secret with the generator point $S_{h} = s_{h} \cdot G$.
35 | - For each of the $n$ $P_{spend}$ , a stealth public key is computed as $P_{stealth} = P_{spend} + S_{h}$.
36 | - For each of the $n$ $P_{stealth}$, a stealth address $a_{stealth}$ is computed as $\mathrm{pubkeyToAddress(}P_{stealth}\mathrm{)}$.
37 | - The smart account stealth address is computed using [CREATE2](https://eips.ethereum.org/EIPS/eip-1014) as $a_{stealthSa} = \mathrm{predictAddress(}a_{deploy},a_{stealth}{\scriptstyle[1 \ldots n]}, t\mathrm{)}$.
38 | - The function returns the smart account stealth address $a_{stealthSa}$, the ephemeral public key $P_{ephemeral}$ and the view tag $v$.
39 |
40 | ### Stealth smart account deployment:
41 |
42 | - Sender has access to all owner stealth addresses $a_{stealth}$ and to smart account parameters $t$ found at $a_{publicSa}$.
43 | - The `deployStealthSmartAccount` function triggers the deployment of a stealth smart account:
44 | - Using CREATE2, the deployer contract at $a_{deploy}$ is called with $\mathrm{deploySmartAccount(}a_{stealth}{\scriptstyle[1 \ldots n]}, t\mathrm{)}$ via a relayer to preserve privacy
45 | - This will deploy the stealth smart account contract at the address predicted by the sender, $a_{stealthSa}$
46 | - The deployed stealth smart account can be controlled with the $n$ stealth private keys $p_{stealth}$
47 |
48 | ### Parsing - Locate one’s own stealth smart accounts:
49 |
50 | - User has access to the smart account viewing private key $p_{viewSa}$ and one of the $n$ EOA spending public keys $P_{spend}$.
51 | - User has access to a set of `Announcement` events and applies the `checkStealthAddress` function to each of them.
52 | - The `checkStealthAddress` function performs the following computations:
53 | - Shared secret $s$ is computed by multiplying the viewing private key with the ephemeral public key of the announcement $s = p_{viewSa} * P_{ephemeral}$.
54 | - The secret is hashed $s_{h} = \mathrm{h}(s)$.
55 | - The view tag $v$ is extracted by taking the most significant byte $s_{h}[0]$ and can be compared to the given view tag. If the view tags do not match, this `Announcement` is not for the user and the remaining steps can be skipped. If the view tags match, continue on.
56 | - Multiply the hashed shared secret with the generator point $S_{h} = s_{h} \cdot G$.
57 | - The stealth public key is computed as $P_{stealth} = P_{spend} + S_{h}$.
58 | - The derived stealth address $a_{stealth}$ is computed as $\mathrm{pubkeyToAddress(}P_{stealth}\mathrm{)}$.
59 | - Return `true` if an owner of the smart account stealth address of the announcement matches the derived stealth address, else return `false`.
60 |
61 | ### Stealth private key derivation:
62 |
63 | - Recipient has access to the smart account viewing private key $p_{viewSa}$ and the $n$ EOA spending private keys $p_{spend}$.
64 | - Recipient has access to a set of `Announcement` events for which the `checkStealthAddress` function returns `true`.
65 | - The `computeStealthKey` function performs the following computations:
66 | - Shared secret $s$ is computed by multiplying the viewing private key with the ephemeral public key of the announcement $s = p_{viewSa} * P_{ephemeral}$.
67 | - The secret is hashed $s_{h} = \mathrm{h}(s)$.
68 | - The $n$ stealth private keys are computed as $p_{stealth} = p_{spend} + s_{h}$.
69 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_WALLETCONNECT_ID="912700a50171dd26f221cab915984a73"
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | # idea IDE
38 | .idea
39 |
--------------------------------------------------------------------------------
/frontend/GlobalStyles.ts:
--------------------------------------------------------------------------------
1 | import {createTheme} from '@mui/material/styles';
2 |
3 | export const theme = createTheme({
4 | typography: {
5 | fontFamily: [
6 | 'Roboto',
7 | 'Outfit',
8 | '"Helvetica Neue"',
9 | 'Arial',
10 | 'sans-serif',
11 | '"Apple Color Emoji"',
12 | '"Segoe UI Emoji"',
13 | '"Segoe UI Symbol"',
14 | ].join(','),
15 | h1: {
16 | fontSize: 65,
17 | fontWeight: 500,
18 | fontFamily: "Outfit"
19 | },
20 | h2: {
21 | fontSize: 30,
22 | fontFamily: "Outfit",
23 | fontWeight: 600
24 | },
25 | h3: {
26 | fontSize: 24,
27 | fontFamily: "Outfit",
28 | },
29 | h4: {
30 | fontSize: 20,
31 | fontFamily: "Outfit",
32 | },
33 | h5: {
34 | fontSize: 16,
35 | fontFamily: "Outfit"
36 | },
37 | h6: {
38 | fontFamily: "Outfit"
39 | },
40 | body1: {
41 | fontSize: 18,
42 | fontFamily: "Roboto"
43 | },
44 | body2: {
45 | fontSize: 16,
46 | fontFamily: "Roboto"
47 | }
48 | },
49 | palette: {
50 | primary: {
51 | main: "#3A73F8",
52 | dark: "#759DFA",
53 | light: "#BFD3FD"
54 | },
55 | secondary: {
56 | main: "#F8B249",
57 | dark: "#FBC575",
58 | light: "#FDE4BF"
59 | },
60 | error: { main: '#F44336' },
61 | success: {
62 | main: '#447B3B',
63 | light: "#4CAF50",
64 | dark: "#1B5E20"
65 | },
66 | text: {
67 | primary: "#151515",
68 | secondary: "#565656",
69 | disabled: "#777777"
70 | }
71 | },
72 | components: {
73 | MuiCssBaseline: {
74 | styleOverrides: {
75 |
76 | body: {
77 | overflowX: "hidden"
78 | },
79 |
80 | }
81 | }
82 | }
83 | });
84 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
18 |
19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
20 |
21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
22 |
23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24 |
25 | ## Learn More
26 |
27 | To learn more about Next.js, take a look at the following resources:
28 |
29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31 |
32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33 |
34 | ## Deploy on Vercel
35 |
36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37 |
38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
39 |
--------------------------------------------------------------------------------
/frontend/components/Const.ts:
--------------------------------------------------------------------------------
1 | export const SAFE_VIEW_KEY_REGISTRY_ABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"registrant","type":"address"},{"indexed":false,"internalType":"uint256","name":"viewingPubKeyPrefix","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"viewingPubKey","type":"uint256"},{"indexed":false,"internalType":"address[]","name":"owners","type":"address[]"}],"name":"StealthSafeKeyChanged","type":"event"},{"inputs":[{"internalType":"uint256","name":"_viewingPubKeyPrefix","type":"uint256"},{"internalType":"uint256","name":"_viewingPubKey","type":"uint256"},{"components":[{"internalType":"bytes","name":"encKey","type":"bytes"},{"internalType":"address","name":"owner","type":"address"}],"internalType":"struct StealthKeyRegistry.EncryptedSafeViewPrivateKey[]","name":"_safeViewPrivateKeyList","type":"tuple[]"}],"name":"setStealthKeys","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_registrant","type":"address"}],"name":"stealthKeys","outputs":[{"internalType":"uint256","name":"viewingPubKeyPrefix","type":"uint256"},{"internalType":"uint256","name":"viewingPubKey","type":"uint256"},{"components":[{"internalType":"bytes","name":"encKey","type":"bytes"},{"internalType":"address","name":"owner","type":"address"}],"internalType":"struct StealthKeyRegistry.EncryptedSafeViewPrivateKey[]","name":"safeViewPrivateKeyList","type":"tuple[]"}],"stateMutability":"view","type":"function"}];
2 | export const SAFE_VIEW_KEY_REGISTRY_ADDRESS = "0xB83e67627F5710446D3D88D2387c483400312670";
3 | export const UMBRA_SAFE_ABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"bytes32","name":"pkx","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"ciphertext","type":"bytes32"}],"name":"Announcement","type":"event"},{"inputs":[{"internalType":"address payable","name":"_receiver","type":"address"},{"internalType":"uint256","name":"_tollCommitment","type":"uint256"},{"internalType":"bytes32","name":"_pkx","type":"bytes32"},{"internalType":"bytes32","name":"_ciphertext","type":"bytes32"}],"name":"sendEth","outputs":[],"stateMutability":"payable","type":"function"}];
4 | export const UMBRA_SAFE_ADDRESS = "0x95361e14DF30064FF39aE7b19E7aA938D2b1a5d0";
5 |
--------------------------------------------------------------------------------
/frontend/components/eth-crypto/decryptPrivateViewKey.tsx:
--------------------------------------------------------------------------------
1 | import EthCrypto from 'eth-crypto';
2 |
3 | export async function decryptPrivateViewKey(ownerPrivateViewingKey: string, safeEncryptedPrivateViewingKey: string) {
4 | const formatted = safeEncryptedPrivateViewingKey.slice(2)
5 | const decompressedEncryptedPrivateViewKey = EthCrypto.cipher.parse(formatted)
6 |
7 | const decryptedPrivateViewKey = await EthCrypto.decryptWithPrivateKey(
8 | ownerPrivateViewingKey,
9 | decompressedEncryptedPrivateViewKey
10 | )
11 | return decryptedPrivateViewKey
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/components/eth-crypto/encryptPrivateViewKey.tsx:
--------------------------------------------------------------------------------
1 | import EthCrypto from 'eth-crypto';
2 |
3 | export async function encryptPrivateViewKey(ownerPublicViewingKey: string, safePrivateViewingKey: string) {
4 | console.log(ownerPublicViewingKey)
5 | const encryptedPrivateViewKey = await EthCrypto.encryptWithPublicKey(
6 | ownerPublicViewingKey,
7 | safePrivateViewingKey
8 | )
9 | const compressedEncryptedPrivateViewKey = EthCrypto.cipher.stringify(encryptedPrivateViewKey)
10 | return compressedEncryptedPrivateViewKey
11 | }
--------------------------------------------------------------------------------
/frontend/components/eth-crypto/test.tsx:
--------------------------------------------------------------------------------
1 | import EthCrypto from 'eth-crypto';
2 | import { getPrivateKeys } from '@/components/umbra/umbraExtended'
3 | import { Signer } from 'ethers'
4 | import { getStealthKeys } from '@/components/umbra/getStealthKeys'
5 |
6 | export async function encryptPrivateViewKey(ownerPublicViewingKey: string, safePrivateViewingKey: string) {
7 | console.log(ownerPublicViewingKey)
8 | const encryptedPrivateViewKey = await EthCrypto.encryptWithPublicKey(
9 | ownerPublicViewingKey,
10 | safePrivateViewingKey
11 | )
12 | const compressedEncryptedPrivateViewKey = EthCrypto.cipher.stringify(encryptedPrivateViewKey)
13 | return compressedEncryptedPrivateViewKey
14 | }
15 |
16 | export async function decryptPrivateViewKey(ownerPrivateViewingKey: string, safeEncryptedPrivateViewingKey: string) {
17 | const formatted = safeEncryptedPrivateViewingKey.slice(2)
18 | const decompressedEncryptedPrivateViewKey = EthCrypto.cipher.parse(formatted)
19 |
20 | const decryptedPrivateViewKey = await EthCrypto.decryptWithPrivateKey(
21 | ownerPrivateViewingKey,
22 | decompressedEncryptedPrivateViewKey
23 | )
24 | console.log(decryptedPrivateViewKey)
25 | return decryptPrivateViewKey
26 | }
27 |
28 |
29 |
30 | export async function encryptDecrypt(ownerPublicViewingKey: string, safePrivateViewingKey: string, signer: Signer) {
31 | const stealthKeys = await getStealthKeys("0xD2661728b35916D0A15834c558D4e6E3b7567f76")
32 | console.log("stealthKeys", stealthKeys)
33 | console.log("ownerPublicViewingKey_useToEncrypt", "0447f7acd0960740f142217321448318d319102b3fcc17956a554bb3855487823405868bdc7f1b04f4c50400edb3afeafac13a48517e7713eea4a015fef17d4ec5")
34 | console.log("safePrivateViewingKey", safePrivateViewingKey)
35 | const safePrivateViewingKey_Encrypted = await encryptPrivateViewKey("0447f7acd0960740f142217321448318d319102b3fcc17956a554bb3855487823405868bdc7f1b04f4c50400edb3afeafac13a48517e7713eea4a015fef17d4ec5", safePrivateViewingKey)
36 | console.log("safePrivateViewingKey_Encrypted", safePrivateViewingKey_Encrypted)
37 | const privateKeys = await getPrivateKeys(signer as Signer)
38 | // console.log("privateKeys", privateKeys)
39 | // console.log("JSON.stringify(privateKeys)", JSON.stringify(privateKeys));
40 | console.log("privateKeys[\"viewingKeyPair\"]", privateKeys["viewingKeyPair"]);
41 | const safeEncryptedPrivateViewingKey = "0x" + safePrivateViewingKey_Encrypted
42 | const decrypt = await decryptPrivateViewKey(privateKeys.viewingKeyPair.privateKeyHex as string, safeEncryptedPrivateViewingKey)
43 | console.log(decrypt)
44 | }
45 |
46 | export async function encryptDecryptSample() {
47 | let ownerPublicViewingKey = "0450e5953846ce708e5487f99dd01703ba7225e76564859ca5809d6cfa4caa3aade038001e9e04043ef796f3fc8a8d87794c37230e869357ea051a5aaafbe635ae";
48 | let safePrivateViewingKey = "0x9519a854ef285c7ac24c61ea58ccfb83409c65d838a9269304dc988da4c734bc";
49 | console.log(ownerPublicViewingKey, safePrivateViewingKey)
50 | const encrypt = await encryptPrivateViewKey(ownerPublicViewingKey, safePrivateViewingKey)
51 | console.log("encrypt", encrypt);
52 | const privateKeys = JSON.parse("{\"spendingKeyPair\":{\"privateKeyHex\":\"0xdaaa378ad71c0dc756f2e61a9a0527bb545deecbc2329841738dee4dab48384d\",\"publicKeyHex\":\"0x04e334a0aa05155452c5e0f16ee620aba727665ceb55cf9e6d89e47a441c06d5b99f8ebca913cbe5755fd35a1b312af2511e81d1a4ad55312e5e1f239d57795021\"},\"viewingKeyPair\":{\"privateKeyHex\":\"0x9519a854ef285c7ac24c61ea58ccfb83409c65d838a9269304dc988da4c734bc\",\"publicKeyHex\":\"0x04f0fa2ac75951d1952c67eb775821dabc508632fe177133469bef00fd5fb247ecfe5277cab36b173d712033ffb4d4b33d221e8279448e8c4c3f0651c05cc5675e\"}}");
53 | console.log("privateKeys", privateKeys)
54 | const safeEncryptedPrivateViewingKey = "0x" + encrypt
55 | const decrypt = await decryptPrivateViewKey(privateKeys.viewingKeyPair.privateKeyHex as string, safeEncryptedPrivateViewingKey)
56 | console.log(decrypt)
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/components/safe/safeApiKit.tsx:
--------------------------------------------------------------------------------
1 | import safeService from './safeEthersAdapter'
2 |
3 | export async function getSafesForOwner(userAddress: string) {
4 | const safes = await safeService.getSafesByOwner(userAddress)
5 | return safes
6 | }
7 |
8 | export async function getSafeInfo(safeAddress: string) {
9 | const safeInfo = await safeService.getSafeInfo(safeAddress)
10 | return safeInfo
11 | }
12 |
13 | export async function estimateGas(safeAddress: string, safeTransaction: any) {
14 | const gasEstimate = await safeService.estimateSafeTransaction(safeAddress, safeTransaction)
15 | return gasEstimate
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/frontend/components/safe/safeDeploy.tsx:
--------------------------------------------------------------------------------
1 | import { SafeFactory, EthersAdapter } from "@safe-global/protocol-kit";
2 | import { Signer, ethers } from "ethers";
3 | import { EthAdapter } from "@safe-global/safe-core-sdk-types";
4 |
5 | export async function createSafe(owners: string[], threshold: number, signer: Signer) {
6 |
7 | const ethAdapter = new EthersAdapter({
8 | ethers,
9 | signerOrProvider: signer
10 | }) as unknown as EthAdapter
11 |
12 | const safeFactory = await SafeFactory.create({ ethAdapter })
13 | const safeAccountConfig = {
14 | owners: owners,
15 | threshold: threshold
16 | }
17 | const safeSdk = await safeFactory.deploySafe({safeAccountConfig})
18 | const safeAddress = await safeSdk.getAddress()
19 |
20 | return safeAddress
21 | }
--------------------------------------------------------------------------------
/frontend/components/safe/safeEthersAdapter.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { EthersAdapter } from '@safe-global/protocol-kit'
3 | import { EthAdapter } from '@safe-global/safe-core-sdk-types'
4 | import SafeApiKit from '@safe-global/api-kit'
5 |
6 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm")
7 | const safeOwner = provider.getSigner(0)
8 |
9 | export const ethAdapter = new EthersAdapter({
10 | ethers,
11 | signerOrProvider: safeOwner
12 | }) as unknown as EthAdapter
13 |
14 | const safeService = new SafeApiKit({
15 | txServiceUrl: "https://safe-transaction-gnosis-chain.safe.global/",
16 | ethAdapter
17 | })
18 |
19 | export default safeService
--------------------------------------------------------------------------------
/frontend/components/safeKeyRegistry/addSafe.tsx:
--------------------------------------------------------------------------------
1 | import { ethers, Signer } from "ethers";
2 | import safeService from "../safe/safeEthersAdapter";
3 | import Safe from "@safe-global/protocol-kit";
4 | import { ProposeTransactionProps } from "@safe-global/api-kit"
5 | import { EthersAdapter } from '@safe-global/protocol-kit'
6 | import { EthAdapter } from '@safe-global/safe-core-sdk-types'
7 | import {SAFE_VIEW_KEY_REGISTRY_ABI, SAFE_VIEW_KEY_REGISTRY_ADDRESS} from "@/components/Const";
8 |
9 | export async function addSafe(safeAddress: string, senderAddress: string, viewingPubKeyPrefix: number, viewingPubKey: string, safeViewPrivateKeyList: string[][], signer: Signer) {
10 |
11 | const ethAdapter = new EthersAdapter({
12 | ethers,
13 | signerOrProvider: signer
14 | }) as unknown as EthAdapter
15 |
16 | const contractAddress = SAFE_VIEW_KEY_REGISTRY_ADDRESS;
17 | const abi = SAFE_VIEW_KEY_REGISTRY_ABI;
18 | const iface = new ethers.utils.Interface(abi)
19 | const calldata = iface.encodeFunctionData("setStealthKeys", [viewingPubKeyPrefix, viewingPubKey, safeViewPrivateKeyList]);
20 |
21 | //needs to now generate a safe tx and let the initiating owner sign
22 | const safeSdk = await Safe.create({ ethAdapter: ethAdapter, safeAddress: safeAddress })
23 |
24 | const txData = {
25 | to: contractAddress,
26 | value: "0",
27 | data: calldata,
28 | } as Safe.TransactionData
29 |
30 | const safeTx = await safeSdk.createTransaction({ safeTransactionData: txData })
31 | const safeTxHash = await safeSdk.getTransactionHash(safeTx)
32 | const signature = await safeSdk.signTypedData(safeTx)
33 |
34 | const transactionConfig = {
35 | safeAddress: safeAddress,
36 | safeTransactionData: safeTx.data,
37 | safeTxHash: safeTxHash,
38 | senderAddress: senderAddress,
39 | senderSignature: signature.data,
40 | origin: "Stealth Safe",
41 | } as unknown as ProposeTransactionProps
42 |
43 | const propose = await safeService.proposeTransaction(transactionConfig)
44 |
45 | return propose
46 |
47 | }
48 |
49 | export async function executeTx(safeTransaction: any, signer: Signer, safeAddress: string) {
50 |
51 | const ethAdapter = new EthersAdapter({
52 | ethers,
53 | signerOrProvider: signer
54 | }) as unknown as EthAdapter
55 | console.log(safeTransaction)
56 | const safeSdk = await Safe.create({ ethAdapter: ethAdapter, safeAddress: safeAddress })
57 | const execute = await safeSdk.executeTransaction(safeTransaction)
58 | const receipt = await execute.transactionResponse?.wait()
59 | return receipt
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/frontend/components/safeKeyRegistry/getSafe.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { EthersAdapter } from '@safe-global/protocol-kit'
3 | import { EthAdapter } from '@safe-global/safe-core-sdk-types'
4 | import {SAFE_VIEW_KEY_REGISTRY_ABI, SAFE_VIEW_KEY_REGISTRY_ADDRESS} from "@/components/Const";
5 |
6 | export async function getSafe(safeAddress: string) {
7 |
8 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm")
9 | const safeOwner = provider.getSigner(0)
10 |
11 | const ethAdapter = new EthersAdapter({
12 | ethers,
13 | signerOrProvider: safeOwner
14 | }) as unknown as EthAdapter
15 | const contractAddress = SAFE_VIEW_KEY_REGISTRY_ADDRESS
16 | const abi = SAFE_VIEW_KEY_REGISTRY_ABI
17 |
18 | const contract = new ethers.Contract(contractAddress, abi, provider)
19 | const safeInfo = await contract.stealthKeys(safeAddress)
20 | console.log(safeInfo)
21 | return safeInfo
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/components/umbra/generateAddressFromKey.tsx:
--------------------------------------------------------------------------------
1 | import { KeyPair } from 'umbra/umbra-js/src/';
2 |
3 | export async function generateAddress(publicKey: string) {
4 | const keyPair = new KeyPair(publicKey);
5 | return keyPair.address;
6 | }
--------------------------------------------------------------------------------
/frontend/components/umbra/getStealthKeys.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers"
2 | import {StealthKeyRegistry} from "umbra/umbra-js/src";
3 |
4 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm")
5 | const registry = new StealthKeyRegistry(provider)
6 |
7 | export async function getStealthKeys(address: string) {
8 | let ownerKeys
9 | try {
10 | ownerKeys = await registry.getStealthKeys(address)
11 | } catch (error) {
12 | return { error: true }
13 | }
14 | return ownerKeys
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/components/umbra/keyPairExtended.tsx:
--------------------------------------------------------------------------------
1 | /*import { KeyPair } from 'umbra/umbra-js/src/';
2 | import {
3 | getSharedSecret as nobleGetSharedSecret,
4 | utils as nobleUtils,
5 | ProjectivePoint,
6 | } from '@noble/secp256k1';
7 |
8 |
9 | class KeyPairSafe extends KeyPair {
10 |
11 | encryptPrivKey(privKey: string) {
12 | // Get shared secret to use as encryption key
13 | const ephemeralPrivateKey = nobleUtils.randomPrivateKey();
14 | const ephemeralPublicKey = ProjectivePoint.fromPrivateKey(ephemeralPrivateKey);
15 | const ephemeralPrivateKeyHex = `0x${nobleUtils.bytesToHex(ephemeralPrivateKey)}`;
16 | const ephemeralPublicKeyHex = `0x${ephemeralPublicKey.toHex()}`;
17 | const sharedSecret = getSharedSecret(ephemeralPrivateKeyHex, this.publicKeyHex);
18 |
19 | // XOR random number with shared secret to get encrypted value
20 | const ciphertextBN = number.value.xor(sharedSecret);
21 | const ciphertext = hexZeroPad(ciphertextBN.toHexString(), 32); // 32 byte hex string with 0x prefix
22 | return { ephemeralPublicKey: ephemeralPublicKeyHex, ciphertext };
23 | }
24 |
25 | } */
--------------------------------------------------------------------------------
/frontend/components/umbra/umbraExtended.tsx:
--------------------------------------------------------------------------------
1 | import { Umbra, KeyPair, RandomNumber } from 'umbra/umbra-js/src/';
2 | import { lookupRecipient } from 'umbra/umbra-js/src/utils/utils';
3 | import {BigNumberish, Signer, ethers, ContractTransaction, BigNumber} from "ethers"
4 | import { hexlify, toUtf8Bytes, isHexString, sha256, accessListify } from 'ethers/lib/utils';
5 | import {UMBRA_SAFE_ABI, UMBRA_SAFE_ADDRESS} from "@/components/Const";
6 | import { getSafeInfo } from '../safe/safeApiKit';
7 | import { getAddress } from '@ethersproject/address';
8 | import { getEvents } from '@/components/utils/getEvents';
9 |
10 | export class UmbraSafe extends Umbra {
11 | // modification of Umbra's generatePrivateKeys function
12 | async generateSafePrivateKeys(signer: Signer){
13 | // Base message that will be signed
14 | const baseMessage = 'Sign this message to generate a key for your Safe on Umbra.\n\nOnly sign this message for a trusted client!'; // prettier-ignore
15 |
16 | // Append chain ID if not mainnet to mitigate replay attacks
17 | const { chainId } = await this.provider.getNetwork();
18 | const message = chainId === 1 ? baseMessage : `${baseMessage}\n\nChain ID: ${chainId}`;
19 |
20 | // Get 65 byte signature from user using personal_sign
21 | const userAddress = await signer.getAddress();
22 | const formattedMessage = hexlify(toUtf8Bytes(message));
23 | const signature = String(await this.provider.send('personal_sign', [formattedMessage, userAddress.toLowerCase()]));
24 |
25 | // If a user can no longer access funds because their wallet was using eth_sign before this update, stand up a
26 | // special "fund recovery login page" which uses the commented out code below to sign with eth_sign
27 | // const signature = await signer.signMessage(message);
28 |
29 | // Verify signature
30 | const isValidSignature = (sig: string) => isHexString(sig) && sig.length === 132;
31 | if (!isValidSignature(signature)) {
32 | throw new Error(`Invalid signature: ${signature}`);
33 | }
34 |
35 | // Split hex string signature into two 32 byte chunks
36 | const startIndex = 2; // first two characters are 0x, so skip these
37 | const length = 64; // each 32 byte chunk is in hex, so 64 characters
38 | const portion1 = signature.slice(startIndex, startIndex + length);
39 | const portion2 = signature.slice(startIndex + length, startIndex + length + length);
40 | const lastByte = signature.slice(signature.length - 2);
41 |
42 | if (`0x${portion1}${portion2}${lastByte}` !== signature) {
43 | throw new Error('Signature incorrectly generated or parsed');
44 | }
45 |
46 | // Hash the signature pieces to get the two private keys
47 | const spendingPrivateKey = sha256(`0x${portion1}`);
48 | const viewingPrivateKey = sha256(`0x${portion2}`);
49 |
50 | // Create KeyPair instances from the private keys and return them
51 | const spendingKeyPair = new KeyPair(spendingPrivateKey);
52 | const viewingKeyPair = new KeyPair(viewingPrivateKey);
53 | return { spendingKeyPair, viewingKeyPair };
54 | }
55 |
56 | async prepareSendSafe(recipientIds: string[], viewingPubKey: string, viewingPubKeyPrefix: string, lookupOverrides: any) {
57 | console.log(recipientIds)
58 | let recipients: {recipientId: string, stealthKeyPair: any, pubKeyXCoordinate: any, encryptedRandomNumber: any, stealthAddress: string}[] = []
59 | const viewingPubKeyUncompressed = KeyPair.getUncompressedFromX(viewingPubKey, Number(viewingPubKeyPrefix))
60 |
61 | const randomNumber = new RandomNumber();
62 |
63 | console.log("randomNumber", randomNumber);
64 | console.log("randomNumber.asHex", randomNumber.asHex);
65 |
66 | const viewingKeyPair = new KeyPair(viewingPubKeyUncompressed);
67 |
68 | const encrypted = viewingKeyPair.encrypt(randomNumber);
69 |
70 | const { pubKeyXCoordinate } = KeyPair.compressPublicKey(encrypted.ephemeralPublicKey);
71 |
72 | // Lookup recipient's public key
73 | for (let i = 0; i < recipientIds.length; i++) {
74 | console.log(recipientIds[i])
75 | const { spendingPublicKey } = await lookupRecipient(recipientIds[i], this.provider, lookupOverrides);
76 | if (!spendingPublicKey) {
77 | throw new Error(`Could not retrieve public keys for recipient ID ${recipientIds[i]}`);
78 | }
79 | const spendingKeyPair = new KeyPair(spendingPublicKey);
80 |
81 | const stealthKeyPair = spendingKeyPair.mulPublicKey(randomNumber);
82 |
83 | const stealthAddress = stealthKeyPair.address;
84 |
85 | recipients.push({recipientId: recipientIds[i], stealthKeyPair, pubKeyXCoordinate, encryptedRandomNumber: encrypted, stealthAddress})
86 | }
87 | return recipients
88 | }
89 |
90 | async scanSafe(spendingPublicKey: string, viewingPrivateKey: string, overrides: ScanOverrides = {}){
91 | console.log("ping")
92 | const announcements = await getEvents("Announcement");
93 | console.log(announcements)
94 | const userAnnouncements = announcements.reduce((userAnns, ann) => {
95 | const { amount, from, receiver, timestamp, token: tokenAddr, txHash } = ann.args;
96 | const { isForUser, randomNumber } = this.isAnnouncementForSafeUser(spendingPublicKey, viewingPrivateKey, ann);
97 | const token = getAddress(tokenAddr); // ensure checksummed address
98 | const isWithdrawn = false; // we always assume not withdrawn and leave it to the caller to check
99 | if (isForUser) userAnns.push({ randomNumber, receiver, amount, token, from, txHash, timestamp, isWithdrawn });
100 | return userAnns;
101 | }, [] as UserAnnouncement[]);
102 |
103 | return { userAnnouncements };
104 | }
105 |
106 | async isAnnouncementForSafeUser(spendingPublicKey: string, viewingPrivateKey: string, announcement: Announcement) {
107 |
108 | console.log("isAnnouncementForSafeUser", spendingPublicKey, viewingPrivateKey, announcement)
109 | try {
110 | // Get y-coordinate of public key from the x-coordinate by solving secp256k1 equation
111 | const { receiver, pkx, ciphertext } = announcement;
112 | const pkxBigNumber = BigNumber.from(pkx);
113 | console.log("pkxBigNumber", pkxBigNumber)
114 | const uncompressedPubKey = KeyPair.getUncompressedFromX(pkxBigNumber);
115 |
116 | // Decrypt to get random number
117 | const payload = { ephemeralPublicKey: uncompressedPubKey, ciphertext };
118 | const viewingKeyPair = new KeyPair(viewingPrivateKey);
119 | const randomNumber = viewingKeyPair.decrypt(payload);
120 |
121 | // Get what our receiving address would be with this random number
122 | const spendingKeyPair = new KeyPair(spendingPublicKey);
123 | const computedReceivingAddress = spendingKeyPair.mulPublicKey(randomNumber).address;
124 |
125 | // Get Safe owners
126 | console.log(receiver)
127 | const info = await getSafeInfo(receiver)
128 | const owners = info.owners
129 | console.log(owners)
130 |
131 | for (let i = 0; i < owners.length; i++) {
132 | if (computedReceivingAddress === owners[i]) {
133 | return { isForUser: true, randomNumber };
134 | }
135 | }
136 |
137 | } catch (err) {
138 | console.error(err);
139 | // We may reach here if people use the sendToken method improperly, e.g. by passing an invalid pkx, so we'd
140 | // fail when uncompressing. For now we just silently ignore these and return false
141 | return { isForUser: false, randomNumber: '' };
142 | }
143 | }
144 |
145 | }
146 |
147 | export async function generateKeys(signer: Signer) {
148 | const provider = signer.provider as ethers.providers.JsonRpcProvider;
149 | const umbraSafe = new UmbraSafe(provider, 100);
150 | const { viewingKeyPair } = await umbraSafe.generateSafePrivateKeys(signer);
151 | console.log("generateKeys_viewingKeyPair", viewingKeyPair);
152 | const { prefix: viewingPrefix, pubKeyXCoordinate: viewingPubKeyX } = KeyPair.compressPublicKey(viewingKeyPair.publicKeyHex)
153 | return { viewingKeyPair: viewingKeyPair, prefix: viewingPrefix, pubKeyXCoordinate: viewingPubKeyX };
154 | }
155 |
156 | export async function prepareSendToSafe(recipientIds: string[], viewingPubKey: string, viewingPubKeyPrefix: string) {
157 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm")
158 | const umbraSafe = new UmbraSafe(provider, 100)
159 | const response = await umbraSafe.prepareSendSafe(recipientIds, viewingPubKey, viewingPubKeyPrefix, {})
160 | return response
161 | }
162 |
163 | export async function sendPayment(stealthSafe: string, signer: Signer, pubKeyXCoordinate: string, encryptedCiphertext: string, amount: BigNumber) {
164 |
165 | const abi = UMBRA_SAFE_ABI;
166 | const contractAddress = UMBRA_SAFE_ADDRESS;
167 | const contract = new ethers.Contract(contractAddress, abi, signer)
168 | const call = await contract.sendEth(stealthSafe, "0", pubKeyXCoordinate, encryptedCiphertext, {value: amount.toString()})
169 | const receipt = await call.wait()
170 | return receipt
171 | }
172 |
173 |
174 | export async function getPrivateKeys(signer: Signer) {
175 | const provider = signer.provider as ethers.providers.JsonRpcProvider;
176 | const umbraSafe = new UmbraSafe(provider, 100);
177 | const { spendingKeyPair, viewingKeyPair } = await umbraSafe.generateSafePrivateKeys(signer);
178 | return { spendingKeyPair: spendingKeyPair, viewingKeyPair: viewingKeyPair};
179 | }
180 |
181 | export async function genPersonalPrivateKeys(signer: Signer) {
182 | const provider = signer.provider as ethers.providers.JsonRpcProvider;
183 | const umbraSafe = new UmbraSafe(provider, 100);
184 | const { spendingKeyPair, viewingKeyPair } = await umbraSafe.generatePrivateKeys(signer);
185 | return { spendingKeyPair: spendingKeyPair, viewingKeyPair: viewingKeyPair};
186 | }
187 |
188 | export async function scanPayments(spendingPublicKey: string, viewingSafePrivateKey: string) {
189 | console.log(spendingPublicKey, viewingSafePrivateKey)
190 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm")
191 | const umbraSafe = new UmbraSafe(provider, 100)
192 | const response = await umbraSafe.scanSafe(spendingPublicKey, viewingSafePrivateKey)
193 | return response
194 | }
195 |
196 |
--------------------------------------------------------------------------------
/frontend/components/utils/clientToSigner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { type WalletClient, useWalletClient } from 'wagmi'
3 | import { providers } from 'ethers'
4 |
5 | export function walletClientToSigner(walletClient: WalletClient) {
6 | const { account, chain, transport } = walletClient
7 | const network = {
8 | chainId: chain.id,
9 | name: chain.name,
10 | ensAddress: chain.contracts?.ensRegistry?.address,
11 | }
12 | const provider = new providers.Web3Provider(transport, network)
13 | const signer = provider.getSigner(account.address)
14 | return signer
15 | }
16 |
17 | /** Hook to convert a viem Wallet Client to an ethers.js Signer. */
18 | export function useEthersSigner({ chainId }: { chainId?: number } = {}) {
19 | const { data: walletClient } = useWalletClient({ chainId })
20 | return React.useMemo(
21 | () => (walletClient ? walletClientToSigner(walletClient) : undefined),
22 | [walletClient],
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/components/utils/getEvents.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { UMBRA_SAFE_ABI, UMBRA_SAFE_ADDRESS } from "../Const";
3 |
4 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm");
5 | const contractAddress = UMBRA_SAFE_ADDRESS;
6 | const contract = new ethers.Contract(contractAddress, UMBRA_SAFE_ABI, provider);
7 |
8 | export async function getEvents(eventName: string) {
9 | const filter = contract.filters[eventName]();
10 | const blockNumber = await provider.getBlockNumber();
11 | const startBlock = blockNumber - 5000;
12 | const logs = await provider.getLogs({
13 | fromBlock: startBlock,
14 | toBlock: 'latest',
15 | address: contract.address,
16 | topics: filter.topics,
17 | });
18 |
19 | const parsedLogs = [];
20 |
21 | for (let log of logs) {
22 | const parsedLog = contract.interface.parseLog(log);
23 | const tx = await provider.getTransaction(log.transactionHash);
24 | parsedLog.sender = tx.from;
25 | const block = await provider.getBlock(log.blockNumber);
26 | parsedLog.timestamp = new Date(block.timestamp * 1000);
27 | parsedLogs.push(parsedLog);
28 | }
29 |
30 | return parsedLogs;
31 | }
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/context/ReceiveContext.tsx:
--------------------------------------------------------------------------------
1 | // ReceiveContext.tsx
2 | import React, {createContext, useCallback, useContext, useState} from 'react';
3 | import {KeyPair} from "umbra/umbra-js/src/";
4 | import {getSafeInfo} from "@/components/safe/safeApiKit";
5 | import {getStealthKeys} from "@/components/umbra/getStealthKeys";
6 | import {generateAddress} from "@/components/umbra/generateAddressFromKey";
7 | import {BigNumber} from "ethers";
8 |
9 | export interface UserStealthAddress {
10 | owner: string;
11 | address: string;
12 | spendingPublicKey: string;
13 | viewingPublicKey: string;
14 | safeStealthViewPrivateEncKey?: string
15 | }
16 |
17 | export interface SafeViewKey {
18 | viewingKeyPair: KeyPair,
19 | prefix: number,
20 | pubKeyXCoordinate: string
21 | }
22 |
23 | export interface WithdrawSafe {
24 | date: number,
25 | amount: BigNumber,
26 | sender: string,
27 | randomNumber: string,
28 | stealthSafeReceiver: string,
29 | hasBeenInitiated: boolean,
30 | hasBeenExecuted: boolean,
31 | hasBeenWithdrawn: boolean
32 | }
33 |
34 | type ReceiveContextType = {
35 | safes: string[];
36 | selectedSafe: string;
37 | selectedSafeOwners: string[];
38 | ownersStealthKeys: UserStealthAddress[];
39 | safeViewKey: SafeViewKey | undefined;
40 | areAllSafeOwnersInitialized: boolean | undefined;
41 | isSelectedSafeInitialized: boolean | undefined;
42 | withdrawSafeList: WithdrawSafe[];
43 |
44 | setSafes: React.Dispatch>;
45 | setSelectedSafe: React.Dispatch>;
46 | setSelectedSafeOwners: React.Dispatch>;
47 | setOwnersStealthKeys: React.Dispatch>;
48 | setSafeViewKey: React.Dispatch>;
49 | setAreAllSafeOwnersInitialized: React.Dispatch>;
50 | setIsSelectedSafeInitialized: React.Dispatch>;
51 |
52 | fetchSafeInfo: () => Promise;
53 | overwriteWithdrawSafeList: (list: WithdrawSafe[]) => void;
54 | changeWithdrawSafe: (pos: number, ws: WithdrawSafe) => void;
55 | };
56 |
57 | // Initial state
58 | const initialReceiveState: ReceiveContextType = {
59 | safes: [],
60 | selectedSafe: '',
61 | selectedSafeOwners: [],
62 | ownersStealthKeys: [],
63 | safeViewKey: undefined,
64 | areAllSafeOwnersInitialized: undefined,
65 | isSelectedSafeInitialized: undefined,
66 | withdrawSafeList: [],
67 | setSafes: () => {},
68 | setSelectedSafe: () => {},
69 | setSelectedSafeOwners: () => {},
70 | setOwnersStealthKeys: () => {},
71 | setSafeViewKey: () => {},
72 | setAreAllSafeOwnersInitialized: () => {},
73 | setIsSelectedSafeInitialized: () => {},
74 | fetchSafeInfo: async () => {},
75 | overwriteWithdrawSafeList: () => {},
76 | changeWithdrawSafe: () => {},
77 | };
78 |
79 | // Create context
80 | export const ReceiveContext = createContext(initialReceiveState);
81 |
82 | // Custom hook for accessing the context
83 | export function useReceiveData() {
84 | return useContext(ReceiveContext);
85 | }
86 |
87 | // Provider component
88 | export const ReceiveProvider: React.FC> = ({ children }) => {
89 | const [safes, setSafes] = useState([]);
90 | const [selectedSafe, setSelectedSafe] = useState('');
91 | const [selectedSafeOwners, setSelectedSafeOwners] = useState([]);
92 | const [ownersStealthKeys, setOwnersStealthKeys] = useState([]);
93 | const [safeViewKey, setSafeViewKey] = useState(undefined);
94 | const [withdrawSafeList, setWithdrawSafeList] = useState([]);
95 | const [areAllSafeOwnersInitialized, setAreAllSafeOwnersInitialized] = useState(undefined);
96 | const [isSelectedSafeInitialized, setIsSelectedSafeInitialized] = useState(undefined);
97 |
98 | // retrieve the info of the safe and of the owners of it
99 | const fetchSafeInfo = useCallback(async () => {
100 | setSelectedSafeOwners([]);
101 | setAreAllSafeOwnersInitialized(undefined);
102 | setOwnersStealthKeys([]);
103 | const safeInfo = await getSafeInfo(selectedSafe)
104 | const owners = safeInfo.owners;
105 | setSelectedSafeOwners(owners);
106 | let safeStealthKeysArray: any = []
107 | for (let i = 0; i < owners.length; i++) {
108 | const safeStealthKeys = await getStealthKeys(owners[i]) as any
109 | if (safeStealthKeys.error) {
110 | setAreAllSafeOwnersInitialized(false);
111 | console.log("Make sure all owners have registered their stealth keys.");
112 | return;
113 | } else {
114 | setAreAllSafeOwnersInitialized(true);
115 | safeStealthKeys["owner"] = owners[i]
116 | safeStealthKeys["address"] = await generateAddress(safeStealthKeys.viewingPublicKey)
117 | safeStealthKeysArray.push(safeStealthKeys)
118 | }
119 | }
120 | setOwnersStealthKeys(safeStealthKeysArray);
121 | }, [selectedSafe, getSafeInfo, getStealthKeys, generateAddress]);
122 |
123 | // necessary to change the withdraw status
124 | const changeWithdrawSafe = useCallback((pos: number, ws: WithdrawSafe) => {
125 | if (pos<0 || pos >= withdrawSafeList.length) return;
126 | const wsList = JSON.parse(JSON.stringify(withdrawSafeList));
127 | wsList[pos] = ws;
128 | setWithdrawSafeList(wsList);
129 | }, []);
130 |
131 | // overrides the whole list with the given one
132 | const overwriteWithdrawSafeList = useCallback((_newWithdrawList: WithdrawSafe[]) => {
133 | setWithdrawSafeList(_newWithdrawList);
134 | }, [setWithdrawSafeList]);
135 |
136 | return (
137 |
157 | {children}
158 |
159 | );
160 | };
161 |
--------------------------------------------------------------------------------
/frontend/context/SendContext.tsx:
--------------------------------------------------------------------------------
1 | // SendContext.tsx
2 | import React, {createContext, useCallback, useContext, useState} from 'react';
3 | import {KeyPair} from "umbra/umbra-js/src/";
4 | import {getSafeInfo} from "@/components/safe/safeApiKit";
5 | import {getStealthKeys} from "@/components/umbra/getStealthKeys";
6 | import {generateAddress} from "@/components/umbra/generateAddressFromKey";
7 | import {getSafe} from "@/components/safeKeyRegistry/getSafe";
8 | import {prepareSendToSafe} from "@/components/umbra/umbraExtended";
9 | import {SafeInfoResponse} from "@safe-global/api-kit";
10 | import {BigNumber} from "ethers";
11 |
12 | export interface SafeStealthData {
13 | recipientId: string,
14 | stealthKeyPair: any,
15 | pubKeyXCoordinate: any,
16 | encryptedRandomNumber: any,
17 | stealthAddress: string
18 | }
19 |
20 | export enum SendTransactionStep {
21 | None,
22 | StealthSafeGenerated,
23 | FundsSent
24 | }
25 |
26 | type SendContextType = {
27 | sendTo: string;
28 | sendAmount: number;
29 | safeInfo: SafeInfoResponse | undefined;
30 | safeStealthDataList: SafeStealthData[];
31 | generatedSafeStealthAddress: string;
32 | sendTransactionCurrentStep: SendTransactionStep
33 | isReceiverValidAddress: boolean | undefined;
34 | isReceiverValidInitializedSafe: boolean | undefined;
35 | isStealthSafeGenerationInProgress: boolean | undefined;
36 | isSendFundInProgress: boolean;
37 |
38 | setSendTo: React.Dispatch>;
39 | setSendAmount: React.Dispatch>;
40 | setGeneratedSafeStealthAddress: React.Dispatch>;
41 | setSendTransactionCurrentStep: React.Dispatch>;
42 | setIsReceiverValidAddress: React.Dispatch>;
43 | setIsReceiverValidInitializedSafe: React.Dispatch>;
44 | setIsStealthSafeGenerationInProgress: React.Dispatch>;
45 | setIsSendFundInProgress: React.Dispatch>;
46 |
47 | fetchSafeInfo: () => Promise<{ safeInfo: SafeInfoResponse | undefined, safeStealthDataList: SafeStealthData[] } | undefined>;
48 | };
49 |
50 | // Initial state
51 | const initialReceiveState: SendContextType = {
52 | sendTo: '',
53 | sendAmount: 0,
54 | safeInfo: undefined,
55 | safeStealthDataList: [],
56 | generatedSafeStealthAddress: '',
57 | sendTransactionCurrentStep: SendTransactionStep.None,
58 | isReceiverValidAddress: undefined,
59 | isReceiverValidInitializedSafe: undefined,
60 | isStealthSafeGenerationInProgress: undefined,
61 | isSendFundInProgress: false,
62 | setSendTo: () => {},
63 | setSendAmount: () => {},
64 | setGeneratedSafeStealthAddress: () => {},
65 | setSendTransactionCurrentStep: () => {},
66 | setIsReceiverValidAddress: () => {},
67 | setIsReceiverValidInitializedSafe: () => {},
68 | setIsStealthSafeGenerationInProgress: () => {},
69 | setIsSendFundInProgress: () => {},
70 | fetchSafeInfo: async () => undefined
71 | };
72 |
73 | // Create context
74 | export const SendContext = createContext(initialReceiveState);
75 |
76 | // Custom hook for accessing the context
77 | export function useSendData() {
78 | return useContext(SendContext);
79 | }
80 |
81 | // Provider component
82 | export const SendProvider: React.FC> = ({ children }) => {
83 | const [sendTo, setSendTo] = useState('');
84 | const [sendAmount, setSendAmount] = useState(0);
85 | const [safeInfo, setSafeInfo] = useState(undefined);
86 | const [safeStealthDataList, setSafeStealthDataList] = useState([]);
87 | const [generatedSafeStealthAddress, setGeneratedSafeStealthAddress] = useState('');
88 | const [sendTransactionCurrentStep, setSendTransactionCurrentStep] = useState(SendTransactionStep.None);
89 | const [isReceiverValidAddress, setIsReceiverValidAddress] = useState(undefined);
90 | const [isReceiverValidInitializedSafe, setIsReceiverValidInitializedSafe] = useState(undefined);
91 | const [isStealthSafeGenerationInProgress, setIsStealthSafeGenerationInProgress] = useState(undefined);
92 | const [isSendFundInProgress, setIsSendFundInProgress] = useState(false);
93 |
94 | // get the basic information from a safe to understand if it's a stealth safe
95 | const fetchSafeInfo = useCallback(async (): Promise<{ safeInfo: SafeInfoResponse | undefined, safeStealthDataList: SafeStealthData[] } | undefined> => {
96 | if (!isReceiverValidAddress) return;
97 | setIsStealthSafeGenerationInProgress(true);
98 | let safeInfo;
99 | try {
100 | safeInfo = await getSafeInfo(sendTo);
101 | } catch (error) {
102 | setIsStealthSafeGenerationInProgress(false);
103 | setIsReceiverValidInitializedSafe(false);
104 | return;
105 | }
106 | setSafeInfo(safeInfo);
107 | const { viewingPubKey, viewingPubKeyPrefix} = await getSafe(safeInfo.address);
108 | // it's a safe but not initialized
109 | if (BigNumber.from(0).eq(viewingPubKey)) {
110 | setIsReceiverValidInitializedSafe(false);
111 | setIsStealthSafeGenerationInProgress(false);
112 | return;
113 | }
114 | const getStealthData = await prepareSendToSafe(safeInfo.owners, viewingPubKey, viewingPubKeyPrefix);
115 | setIsReceiverValidInitializedSafe(true);
116 | setSafeStealthDataList(getStealthData);
117 | setSendTransactionCurrentStep(SendTransactionStep.StealthSafeGenerated);
118 | return {
119 | safeInfo,
120 | safeStealthDataList: getStealthData
121 | }
122 | }, [sendTo, isReceiverValidAddress, getSafe, prepareSendToSafe]);
123 |
124 | return (
125 |
148 | {children}
149 |
150 | );
151 | };
152 |
--------------------------------------------------------------------------------
/frontend/hooks/ui/mediaQueryHooks.ts:
--------------------------------------------------------------------------------
1 | import {useMediaQuery} from "@mui/material";
2 | import {theme} from "../../GlobalStyles";
3 |
4 | export const useIsMobile = () => useMediaQuery(theme.breakpoints.down('sm'));
5 | export const useIsTablet = () => useMediaQuery(theme.breakpoints.down('md'));
6 |
--------------------------------------------------------------------------------
/frontend/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | typescript: {
5 | ignoreBuildErrors: true
6 | },
7 | webpack: (config, options) => {
8 | const { dir, defaultLoaders } = options;
9 |
10 | config.module.rules.push({
11 | test: /\.tsx?$/,
12 | include: [dir, /umbra\/umbra-js\/src/],
13 | use: [defaultLoaders.babel],
14 | });
15 |
16 | return config;
17 | },
18 | }
19 |
20 | module.exports = nextConfig
21 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@emotion/react": "^11.11.1",
13 | "@emotion/styled": "^11.11.0",
14 | "@ethersproject/bignumber": "^5.7.0",
15 | "@ethersproject/bytes": "^5.7.0",
16 | "@mui/icons-material": "^5.14.1",
17 | "@mui/lab": "^5.0.0-alpha.137",
18 | "@mui/material": "^5.14.1",
19 | "@noble/secp256k1": "^1.7.1",
20 | "@safe-global/api-kit": "^1.3.0",
21 | "@safe-global/protocol-kit": "^1.2.0",
22 | "@safe-global/relay-kit": "1.0.0",
23 | "@safe-global/safe-core-sdk-types": "^2.2.0",
24 | "@types/node": "20.4.2",
25 | "@types/react": "18.2.15",
26 | "@types/react-dom": "18.2.7",
27 | "@umbracash/umbra-js": "^0.1.2",
28 | "@web3modal/ethereum": "^2.7.0",
29 | "@web3modal/react": "^2.7.0",
30 | "date-fns": "^2.30.0",
31 | "eth-crypto": "^2.6.0",
32 | "ethers": "^5.7.2",
33 | "next": "13.4.11",
34 | "react": "18.2.0",
35 | "react-dom": "18.2.0",
36 | "typescript": "^5.1.6",
37 | "umbra": "ScopeLift/umbra-protocol",
38 | "viem": "^1.3.1",
39 | "wagmi": "^1.3.9"
40 | },
41 | "devDependencies": {
42 | "ts-loader": "^9.4.4"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import type { AppProps } from 'next/app'
3 | import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'
4 | import { Web3Modal } from '@web3modal/react'
5 | import { configureChains, createConfig, WagmiConfig } from 'wagmi'
6 | import { gnosis } from 'wagmi/chains'
7 | import process from 'process'
8 | import React from "react";
9 | import {ThemeProvider} from "@mui/system";
10 | import {theme} from "@/GlobalStyles";
11 | import {Container, CssBaseline} from "@mui/material";
12 | import {ReceiveProvider} from "@/context/ReceiveContext";
13 | import {SendProvider} from "@/context/SendContext";
14 | import Head from 'next/head'
15 |
16 | const chains = [gnosis]
17 | const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_ID as string
18 |
19 | const { publicClient } = configureChains(chains, [w3mProvider({ projectId })])
20 | const wagmiConfig = createConfig({
21 | autoConnect: true,
22 | connectors: w3mConnectors({ projectId, chains }),
23 | publicClient
24 | })
25 | const ethereumClient = new EthereumClient(wagmiConfig, chains)
26 |
27 |
28 | export default function App({ Component, pageProps }: AppProps) {
29 | return (
30 | <>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Stealth Safe - A POC by Sefu project
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | >
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 | import {Box, Button, Typography} from "@mui/material";
3 | import {Web3Button} from "@web3modal/react";
4 | import {useAccount} from "wagmi";
5 | import {useRouter} from "next/router";
6 | import {theme} from "@/GlobalStyles";
7 | import {useIsMobile} from "@/hooks/ui/mediaQueryHooks";
8 |
9 | /**
10 | *
11 | * @param {React.PropsWithChildren} props
12 | * @return {JSX.Element}
13 | * @constructor
14 | */
15 | const Index: React.FC = (props) => {
16 |
17 | const account = useAccount();
18 | const router = useRouter();
19 | const isMobile = useIsMobile();
20 |
21 | const [isAccountConnected, setIsAccountConnected] = useState(false);
22 |
23 | useEffect(() => {
24 | if (account.isConnected)
25 | setIsAccountConnected(true);
26 | }, [account])
27 |
28 |
29 | return (
30 |
37 |
42 |
43 | Stealth
44 |
45 |
47 |
48 | Safe
49 |
50 |
51 |
52 |
53 | Receive and send, blending Safe advantages and stealth privacy
54 |
55 |
56 |
57 | {
58 | isAccountConnected ?
59 |
62 | :
63 |
64 | }
65 |
66 |
67 | 🎥 See demo
68 |
69 |
74 |
75 |
76 | );
77 | };
78 |
79 | export interface IHome {
80 |
81 | }
82 |
83 | export default Index;
84 |
--------------------------------------------------------------------------------
/frontend/pages/index_test.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { Inter } from 'next/font/google'
3 | import styles from '@/styles/Home.module.css'
4 | import { Web3Button } from '@web3modal/react'
5 | import { useAccount } from 'wagmi'
6 | import { getSafesForOwner, getSafeInfo } from '@/components/safe/safeApiKit'
7 | import { useState, ChangeEvent, useEffect } from 'react'
8 | import { getStealthKeys } from '@/components/umbra/getStealthKeys'
9 | import { generateKeys } from '@/components/umbra/umbraExtended'
10 | import { useEthersSigner } from '@/components/utils/clientToSigner'
11 | import { Signer } from 'ethers'
12 | import { encryptPrivateViewKey } from '@/components/eth-crypto/encryptPrivateViewKey'
13 | import { generateAddress } from '@/components/umbra/generateAddressFromKey'
14 | import { addSafe, executeTx } from '@/components/safeKeyRegistry/addSafe'
15 | import safeService from '@/components/safe/safeEthersAdapter'
16 | import {encryptDecrypt, encryptDecryptSample} from '@/components/eth-crypto/test'
17 |
18 | const inter = Inter({ subsets: ['latin'] })
19 |
20 | export default function Home() {
21 |
22 | const { address } = useAccount()
23 | const signer = useEthersSigner()
24 | const [safes, setSafes] = useState([])
25 | const [selectedSafe, setSelectedSafe] = useState("")
26 | const [stealthKeyError, setStealthKeyError] = useState("")
27 | const [stealthKeys, setStealthKeys] = useState([[]])
28 | const [sharedSafeViewKey, setSharedSafeViewKey] = useState({})
29 | const [safeTransactions, setSafeTransactions] = useState()
30 |
31 | async function getSafes () {
32 | if (!address) return
33 | const safes = await getSafesForOwner(address as string)
34 | setSafes(safes.safes)
35 | setSelectedSafe(safes.safes[0])
36 | }
37 |
38 | async function fetchSafeInfo (safeAddress: string) {
39 | const safeInfo = await getSafeInfo(safeAddress)
40 | const owners = safeInfo.owners
41 | let safeStealthKeysArray: any = []
42 | for (let i = 0; i < owners.length; i++) {
43 | const safeStealthKeys = await getStealthKeys(owners[i]) as any
44 | if (safeStealthKeys.error) {
45 | setStealthKeyError("Make sure all owners have registered their stealth keys.")
46 | return
47 | } else {
48 | safeStealthKeys["owner"] = owners[i]
49 | safeStealthKeys["address"] = await generateAddress(safeStealthKeys.viewingPublicKey)
50 | safeStealthKeysArray.push(safeStealthKeys)
51 | }
52 | }
53 | setStealthKeys(safeStealthKeysArray)
54 | console.log("safeStealthKeysArray", safeStealthKeysArray);
55 | }
56 |
57 | const handleSafeChange = (e: ChangeEvent) => {
58 | setSelectedSafe(e.target.value)
59 | }
60 |
61 | useEffect(() => {
62 | if (safes.length > 0) {
63 | console.log(selectedSafe)
64 | fetchSafeInfo(selectedSafe)
65 | }
66 | }, [selectedSafe])
67 |
68 | async function generateSafeKeys () {
69 | const keys = await generateKeys(signer as Signer)
70 | for (let i = 0; i < stealthKeys.length; i++) {
71 | const pubKeySliced = stealthKeys[i].viewingPublicKey.slice(2)
72 | const encryptedKey = await encryptPrivateViewKey(pubKeySliced as string, keys.viewingKeyPair.privateKeyHex as string)
73 | stealthKeys[i]["encryptedKey"] = "0x"+encryptedKey
74 | }
75 | setSharedSafeViewKey(keys)
76 | setStealthKeys(stealthKeys)
77 | console.log(stealthKeys)
78 | }
79 |
80 | async function submitKeys () {
81 | const addToContract = await addSafe(selectedSafe, address as string, sharedSafeViewKey.prefix, sharedSafeViewKey.pubKeyXCoordinate, stealthKeys.map((key) => [key.encryptedKey, key.owner]), signer)
82 | }
83 |
84 | async function getTransactions () {
85 | const transactions = await safeService.getPendingTransactions(selectedSafe)
86 | console.log(transactions)
87 | setSafeTransactions(transactions)
88 | }
89 |
90 | async function executeTransaction () {
91 | console.log(safeTransactions.results[0])
92 | const execute = await executeTx(safeTransactions.results[0], signer as Signer, selectedSafe)
93 | console.log(execute)
94 | }
95 |
96 | async function test() {
97 | const keys = await generateKeys(signer as Signer)
98 | for (let i = 0; i < stealthKeys.length; i++) {
99 | if (stealthKeys[i].owner === address) {
100 | console.log(stealthKeys)
101 | const pubKeySliced = stealthKeys[i].viewingPublicKey.slice(2)
102 | console.log(stealthKeys[i])
103 | console.log("pubKeySliced, keys.viewingKeyPair.privateKeyHex", pubKeySliced, keys.viewingKeyPair.privateKeyHex, )
104 | const testData = await encryptDecrypt(pubKeySliced as string, keys.viewingKeyPair.privateKeyHex as string, signer as Signer)
105 | console.log(testData)
106 | }
107 | }
108 | }
109 |
110 | return (
111 | <>
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | {safes.length > 0 &&
121 |
126 | }
127 | {stealthKeyError != "" && {stealthKeyError}
}
128 |
129 |
130 |
131 | {safeTransactions && }
132 |
133 |
134 |
135 | >
136 | )
137 | }
138 |
--------------------------------------------------------------------------------
/frontend/pages/receive.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from 'react';
2 | import CommonHeader from "@/ui/organisms/Common.Header/Common.Header";
3 | import {Alert, Box, CircularProgress, Paper, Typography} from "@mui/material";
4 | import {Web3Button} from "@web3modal/react";
5 | import {getSafesForOwner} from "@/components/safe/safeApiKit";
6 | import {useAccount} from "wagmi";
7 | import {ReceiveProvider, useReceiveData} from "@/context/ReceiveContext";
8 | import ReceiveSelectSafe from "@/ui/organisms/Receive.SelectSafe/Receive.SelectSafe";
9 | import Link from "next/link";
10 | import ReceiveRegisterSafe from "@/ui/organisms/Receive.RegisterSafe/Receive.RegisterSafe";
11 | import ReceiveListOfWithdrawals from "@/ui/organisms/Receive.ListOfWithdrawals/Receive.ListOfWithdrawals";
12 |
13 | /**
14 | *
15 | * @param {React.PropsWithChildren} props
16 | * @return {JSX.Element}
17 | * @constructor
18 | */
19 | const Receive: React.FC = (props) => {
20 |
21 |
22 | const { address, isConnected } = useAccount();
23 | const receiveData = useReceiveData();
24 | const [isLoadingSafes, setIsLoadingSafes] = useState(false);
25 | const [isLoadingCheckSafeIsRegistered, setIsLoadingCheckSafeIsRegistered] = useState(false);
26 |
27 | // launch load of safes
28 | useEffect(() => {
29 | if (isConnected && address && receiveData.safes.length === 0) {
30 | setIsLoadingSafes(true);
31 | getSafes().then();
32 | }
33 | }, [isConnected, address]);
34 |
35 | // check if safe is registered
36 | useEffect(() => {
37 | if (receiveData.selectedSafe) {
38 | // TODO check if safe is registered or not
39 | setIsLoadingCheckSafeIsRegistered(true);
40 |
41 | }
42 | }, [receiveData.selectedSafe])
43 |
44 | // get the safes
45 | const getSafes = useCallback(async () => {
46 | if (!address) return;
47 | const safes = await getSafesForOwner(address as string);
48 | receiveData.setSafes(safes.safes);
49 | receiveData.setSelectedSafe(safes.safes[0]);
50 | setIsLoadingSafes(false);
51 | }, [address, receiveData]);
52 |
53 | return (
54 | <>
55 |
56 |
61 |
62 | {/* Into of the page */}
63 |
68 | Receive
69 |
70 |
75 | Receive money on a Safe you manage using a Stealth Safe
76 |
77 |
78 |
79 | {
80 | isLoadingSafes ?
81 |
82 | :
83 |
84 | }
85 |
86 |
87 | {/* Show progress if data of the safe is being loaded */}
88 | {
89 | receiveData.areAllSafeOwnersInitialized === undefined && !isLoadingSafes ?
90 |
91 | :
92 | ""
93 | }
94 |
95 | {/* Check if the users are initialized */}
96 | {
97 | receiveData.areAllSafeOwnersInitialized === false ?
98 | All the owners of the safe must be initialized in Umbra - Proceed to Umbra Setup Page
99 | :
100 | ""
101 | }
102 |
103 | {/* If all owners are initialized, proceed to check if the safe is registered, and later showing the possible withdrawals pending*/}
104 | {
105 | receiveData.areAllSafeOwnersInitialized ?
106 | <>
107 |
108 | >
109 | :
110 | ""
111 | }
112 |
113 | {
114 | receiveData.isSelectedSafeInitialized ?
115 |
116 |
117 |
118 | :
119 | ""
120 | }
121 |
122 |
123 | >
124 | );
125 | };
126 |
127 | export interface IReceive {
128 |
129 | }
130 |
131 | export default Receive;
132 |
--------------------------------------------------------------------------------
/frontend/pages/receiveFunctions.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { Inter } from 'next/font/google'
3 | import styles from '@/styles/Home.module.css'
4 | import { Web3Button } from '@web3modal/react'
5 | import { useAccount } from 'wagmi'
6 | import { useEthersSigner } from '@/components/utils/clientToSigner'
7 | import { genPersonalPrivateKeys, scanPayments } from '@/components/umbra/umbraExtended'
8 | import { Signer } from 'ethers'
9 | import { getSafe } from '@/components/safeKeyRegistry/getSafe'
10 | import { getSafesForOwner } from '@/components/safe/safeApiKit'
11 | import { useState, ChangeEvent } from 'react'
12 | import { decryptPrivateViewKey } from '@/components/eth-crypto/decryptPrivateViewKey'
13 | import { getEvents } from '@/components/utils/getEvents'
14 | import { KeyPair } from 'umbra/umbra-js/src/'
15 | import { getSafeInfo } from '@/components/safe/safeApiKit'
16 |
17 | const inter = Inter({ subsets: ['latin'] })
18 |
19 | export default function sendFunctions() {
20 |
21 | const { address } = useAccount()
22 | const signer = useEthersSigner()
23 | const [safes, setSafes] = useState([])
24 | const [selectedSafe, setSelectedSafe] = useState("")
25 | const [personalPrivateKeys, setPersonalPrivateViewKey] = useState()
26 | const [safeViewKeys, setSafeViewKeys] = useState()
27 | const [safePrivateViewKey, setSafePrivateViewKey] = useState()
28 | const [data, setData] = useState()
29 |
30 | const handleSafeChange = (e: ChangeEvent) => {
31 | setSelectedSafe(e.target.value)
32 | }
33 |
34 | async function getSafes () {
35 | if (!address) return
36 | const safes = await getSafesForOwner(address as string)
37 | setSafes(safes.safes)
38 | setSelectedSafe(safes.safes[0])
39 | }
40 |
41 | async function getPersonalPrivateKeys() {
42 | const privateKeys = await genPersonalPrivateKeys(signer as Signer)
43 | console.log(privateKeys)
44 | setPersonalPrivateViewKey(privateKeys)
45 | }
46 |
47 | async function getSafeViewKeys() {
48 | const { safeViewPrivateKeyList } = await getSafe(selectedSafe)
49 | console.log(safeViewPrivateKeyList)
50 | const ownerData = safeViewPrivateKeyList.filter((owner: any) => owner.owner === address)
51 | console.log(ownerData)
52 | setSafeViewKeys(ownerData)
53 | }
54 |
55 | async function decryptViewKey() {
56 | console.log(safeViewKeys[0][0])
57 | const decryptedViewKey = await decryptPrivateViewKey(personalPrivateKeys.viewingKeyPair.privateKeyHex, safeViewKeys[0][0])
58 | console.log(decryptedViewKey)
59 | setSafePrivateViewKey(decryptedViewKey)
60 | }
61 |
62 | async function scan() {
63 | const results = await getEvents("Announcement")//await scanPayments(personalPrivateKeys.spendingKeyPair.privateKeyHex, safePrivateViewKey)
64 | let dataArray = []
65 | for (let i = 0; i < results.length; i++) {
66 | const result = results[i]
67 | console.log(result.args)
68 | const uncompressedPubKey = KeyPair.getUncompressedFromX(result.args.pkx)
69 | console.log(uncompressedPubKey)
70 | const payload = { ephemeralPublicKey: uncompressedPubKey, ciphertext: result.args.ciphertext }
71 | console.log(safePrivateViewKey)
72 | const viewingKeyPair = new KeyPair(safePrivateViewKey)
73 | const randomNumber = viewingKeyPair.decrypt(payload)
74 | console.log(randomNumber)
75 | const spendingKeyPair = new KeyPair(personalPrivateKeys.spendingKeyPair.privateKeyHex)
76 | console.log(spendingKeyPair)
77 | const computedReceivingAddress = spendingKeyPair.mulPrivateKey(randomNumber)
78 | console.log(computedReceivingAddress)
79 | const safeInfo = await getSafeInfo(result.args.receiver)
80 | console.log(safeInfo)
81 | if (safeInfo.owners.includes(computedReceivingAddress.address)) {
82 | dataArray.push({ result, computedReceivingAddress })
83 | }
84 | }
85 | setData[dataArray]
86 | console.log(dataArray)
87 | }
88 |
89 | return (
90 | <>
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | {safes.length > 0 &&
100 |
105 | }
106 |
107 |
108 |
109 |
110 |
111 | >
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/frontend/pages/send.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from 'react';
2 | import CommonHeader from "@/ui/organisms/Common.Header/Common.Header";
3 | import {Alert, Box, Button, CircularProgress, IconButton, Typography} from "@mui/material";
4 | import SendReceiverAndAmount from "@/ui/organisms/Send.ReceiverAndAmount/Send.ReceiverAndAmount";
5 | import {SendTransactionStep, useSendData} from "@/context/SendContext";
6 | import {createSafe} from "@/components/safe/safeDeploy";
7 | import {ethers, Signer} from "ethers";
8 | import {useEthersSigner} from "@/components/utils/clientToSigner";
9 | import {sendPayment} from "@/components/umbra/umbraExtended";
10 | import {Close} from "@mui/icons-material";
11 | import {useRouter} from "next/router";
12 |
13 | /**
14 | *
15 | * @param {React.PropsWithChildren} props
16 | * @return {JSX.Element}
17 | * @constructor
18 | */
19 | const Send: React.FC = (props) => {
20 |
21 | const sendData = useSendData();
22 | const signer = useEthersSigner();
23 | const router = useRouter();
24 |
25 | const [fundsTransaction, setFundsTransaction] = useState("");
26 |
27 | useEffect(() => {
28 | if (sendData.isReceiverValidInitializedSafe) {
29 | // TODO - send the transaction to deploy a new safe
30 | sendData.setIsStealthSafeGenerationInProgress(true);
31 | }
32 | }, [sendData.isReceiverValidInitializedSafe]);
33 |
34 | // generates the stealth safe
35 | const generateStealthSafe = useCallback(async () => {
36 | // get the info from Safe and from the chain
37 | const fetchInitialData = await sendData.fetchSafeInfo();
38 | if (!fetchInitialData?.safeInfo || !fetchInitialData.safeStealthDataList) return;
39 | const stealthOwners = fetchInitialData.safeStealthDataList.map((owner: any) => owner.stealthAddress);
40 | const safeAddress = await createSafe(stealthOwners, fetchInitialData?.safeInfo?.threshold, signer as Signer);
41 | sendData.setGeneratedSafeStealthAddress(safeAddress);
42 | sendData.setIsStealthSafeGenerationInProgress(false);
43 | }, [sendData, signer]);
44 |
45 | // send the funds to the generated stealth address
46 | const sendFunds = useCallback(async () => {
47 | sendData.setIsSendFundInProgress(true);
48 | const tx = await sendPayment(
49 | sendData.generatedSafeStealthAddress,
50 | signer as Signer,
51 | sendData.safeStealthDataList[0].pubKeyXCoordinate,
52 | sendData.safeStealthDataList[0].encryptedRandomNumber.ciphertext,
53 | ethers.utils.parseEther(sendData.sendAmount.toString())
54 | );
55 | setFundsTransaction(tx.transactionHash);
56 | sendData.setSendTransactionCurrentStep(SendTransactionStep.FundsSent);
57 | sendData.setIsSendFundInProgress(false);
58 |
59 | // TODO - useEffect that listen for data and, once completed, resets everything
60 | }, [sendData]);
61 |
62 | return (
63 | <>
64 |
65 |
70 | {/* TODO - place a cool image above send */}
71 |
76 | Send
77 |
78 |
83 | Send money to a Safe address that has been activated within the StealthSafe Registry
84 |
85 |
86 |
87 |
88 |
89 | {
90 | sendData.isStealthSafeGenerationInProgress ? (
91 |
92 |
93 |
94 | Generating Stealth Safe
95 |
96 |
97 | )
98 | :
99 | sendData.isReceiverValidInitializedSafe === false ? (
100 | Address is not a valid Safe or it has not been initialized by receiver
101 | )
102 | :
103 | sendData.isSendFundInProgress ? (
104 |
105 |
106 |
107 | Sending Funds
108 |
109 |
110 | )
111 | :
112 | sendData.sendTransactionCurrentStep === SendTransactionStep.StealthSafeGenerated ? (
113 |
120 | )
121 | :
122 | sendData.sendTransactionCurrentStep === SendTransactionStep.FundsSent ? (
123 | {
129 | window.open(`https://gnosisscan.io/tx/${fundsTransaction}`);
130 | }}
131 | >
132 | See Transaction
133 |
134 | }
135 | >
136 | Funds sent!
137 |
138 | ) : (
139 |
146 | )
147 | }
148 |
149 |
150 | >
151 | );
152 | };
153 |
154 | export interface ISend {
155 |
156 | }
157 |
158 | export default Send;
159 |
--------------------------------------------------------------------------------
/frontend/pages/sendFunctions.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { Inter } from 'next/font/google'
3 | import styles from '@/styles/Home.module.css'
4 | import { Web3Button } from '@web3modal/react'
5 | import { useAccount } from 'wagmi'
6 | import { useEthersSigner } from '@/components/utils/clientToSigner'
7 | import { getSafeInfo } from '@/components/safe/safeApiKit'
8 | import { useState } from 'react'
9 | import { prepareSendToSafe } from '@/components/umbra/umbraExtended'
10 | import { createSafe } from '@/components/safe/safeDeploy'
11 | import { Signer } from 'ethers'
12 | import { getSafe } from '@/components/safeKeyRegistry/getSafe'
13 | import { sendPayment } from '@/components/umbra/umbraExtended'
14 |
15 | const inter = Inter({ subsets: ['latin'] })
16 |
17 | export default function sendFunctions() {
18 |
19 | const { address } = useAccount()
20 | const signer = useEthersSigner()
21 | const [selectedSafe, setSelectedSafe] = useState("")
22 | const [safeInfo, setSafeInfo] = useState()
23 | const [stealthData, setStealthData] = useState()
24 | const [sharedSafeViewKey, setSharedSafeViewKey] = useState({})
25 |
26 | async function fetchSafeInfo () {
27 | const safeInfo = await getSafeInfo(selectedSafe)
28 | setSafeInfo(safeInfo)
29 | const { viewingPubKey, viewingPubKeyPrefix} = await getSafe(selectedSafe)
30 | const getStealthData = await prepareSendToSafe(safeInfo.owners, viewingPubKey, viewingPubKeyPrefix)
31 | setStealthData(getStealthData)
32 | console.log(getStealthData)
33 | }
34 |
35 | const handleInputChange = (e: React.ChangeEvent) => {
36 | setSelectedSafe(e.target.value)
37 | }
38 |
39 | async function createStealthSafe () {
40 | const stealthOwners = stealthData.map((owner: any) => owner.stealthAddress)
41 | console.log(stealthOwners)
42 | const safeAddress = await createSafe(stealthOwners, safeInfo.threshold, signer as Signer)
43 | console.log(safeAddress)
44 | const sendToStealth = await send(safeAddress)
45 | }
46 |
47 | async function send(stealthSafe: string) {
48 | console.log(stealthSafe, signer, stealthData[0].pubKeyXCoordinate, stealthData[0].encryptedRandomNumber.ciphertext)
49 | const tx = await sendPayment(stealthSafe, signer as Signer, stealthData[0].pubKeyXCoordinate, stealthData[0].encryptedRandomNumber.ciphertext, 100000) //amount hardcoded, should be in ui
50 | console.log(tx)
51 | }
52 |
53 |
54 | return (
55 | <>
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | >
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/safe_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/frontend/public/safe_logo.png
--------------------------------------------------------------------------------
/frontend/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/xdai_logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/frontend/public/xdai_logo.webp
--------------------------------------------------------------------------------
/frontend/styles/Home.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/frontend/styles/Home.module.css
--------------------------------------------------------------------------------
/frontend/styles/globals.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluidkey/stealth-safe/ee6550337e284a85dfdfc3ba253bc90f8e09e5ce/frontend/styles/globals.css
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Common.Header/Common.Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Box, Typography} from "@mui/material";
3 | import Link from "next/link";
4 | import {theme} from "@/GlobalStyles";
5 | import {Web3Button} from "@web3modal/react";
6 | import {useIsMobile} from "@/hooks/ui/mediaQueryHooks";
7 | import {useRouter} from "next/router";
8 |
9 | /**
10 | *
11 | * @param {React.PropsWithChildren} props
12 | * @return {JSX.Element}
13 | * @constructor
14 | */
15 | const CommonHeader: React.FC = (props) => {
16 |
17 | const isMobile = useIsMobile();
18 | const router = useRouter();
19 |
20 |
21 | return (
22 |
26 |
33 |
34 |
41 |
42 | Stealth
43 |
44 |
46 |
47 | Safe
48 |
49 |
50 |
51 |
52 | {
53 | !isMobile ?
54 |
55 |
56 |
57 | :
58 | ""
59 | }
60 |
61 |
62 | router.push("/send")}
66 | sx={{
67 | cursor: "pointer",
68 | textDecoration: router.pathname === '/send' ? 'underline' : 'none',
69 | fontWeight: router.pathname === '/send' ? 500 : undefined,
70 | }}>
71 | Send
72 |
73 | router.push("/receive")}
77 | sx={{
78 | cursor: "pointer",
79 | textDecoration: router.pathname === '/receive' ? 'underline' : 'none',
80 | fontWeight: router.pathname === '/receive' ? 500 : undefined,
81 | }}>
82 | Receive
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export interface ICommonHeader {
90 |
91 | }
92 |
93 | export default CommonHeader;
94 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Receive.ListOfWithdrawals/Receive.ListOfWithdrawals.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect} from 'react';
2 | import {
3 | Box,
4 | Button,
5 | Paper,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableContainer,
10 | TableHead,
11 | TableRow,
12 | Typography
13 | } from "@mui/material";
14 | import {getKeyShortAddress} from "@/utils/web3/address";
15 | import Link from "next/link";
16 | import { format } from 'date-fns';
17 | import {theme} from "@/GlobalStyles";
18 | import WithdrawButton from "@/ui/organisms/Receive.ListOfWithdrawals/WithdrawButton";
19 | import {useReceiveData, WithdrawSafe} from "@/context/ReceiveContext";
20 | import {BigNumber, ethers, Signer} from "ethers";
21 | import {genPersonalPrivateKeys} from "@/components/umbra/umbraExtended";
22 | import {decryptPrivateViewKey} from "@/components/eth-crypto/decryptPrivateViewKey";
23 | import {getEvents} from "@/components/utils/getEvents";
24 | import {KeyPair} from "umbra/umbra-js/src";
25 | import {getSafeInfo} from "@/components/safe/safeApiKit";
26 | import {getSafe} from "@/components/safeKeyRegistry/getSafe";
27 | import {useAccount} from "wagmi";
28 | import {useEthersSigner} from "@/components/utils/clientToSigner";
29 |
30 |
31 | /**
32 | *
33 | * @param {React.PropsWithChildren} props
34 | * @return {JSX.Element}
35 | * @constructor
36 | */
37 | const ReceiveListOfWithdrawals: React.FC = (props) => {
38 |
39 |
40 | const receiveData = useReceiveData();
41 | const account = useAccount();
42 | const signer = useEthersSigner();
43 |
44 | useEffect(() => {
45 | if (signer)
46 | orchestrateRetrieveOfData().then();
47 | }, [signer]);
48 |
49 | const orchestrateRetrieveOfData = useCallback(async () => {
50 | const safeInfo = await getSafe(receiveData.selectedSafe);
51 | const encSafeViewPrivateKeysList = safeInfo['safeViewPrivateKeyList'];
52 | const myEncSafeViewPrivateKey = encSafeViewPrivateKeysList.find(e => e['owner'] === account.address);
53 | console.log("myEncSafeViewPrivateKey", myEncSafeViewPrivateKey);
54 | const personalPrivateKey = await genPersonalPrivateKeys(signer as Signer);
55 | const safeViewKeyPrivate = await decryptPrivateViewKey(personalPrivateKey.viewingKeyPair.privateKeyHex as string, myEncSafeViewPrivateKey['encKey']);
56 |
57 | console.log("safeViewKeyPrivate", safeViewKeyPrivate);
58 | const data = await scan(safeViewKeyPrivate, personalPrivateKey.spendingKeyPair.privateKeyHex as string);
59 | receiveData.overwriteWithdrawSafeList(data);
60 | }, [receiveData, getSafe, account, signer]);
61 |
62 | async function scan(safePrivateViewKey: string, personalSpendingPrivateKeyHex: string): Promise {
63 | const results = await getEvents("Announcement")//await scanPayments(personalPrivateKeys.spendingKeyPair.privateKeyHex, safePrivateViewKey)
64 | let dataArray = []
65 | for (let i = 0; i < results.length; i++) {
66 | const result = results[i]
67 | console.log("result.args", result.args)
68 | const uncompressedPubKey = KeyPair.getUncompressedFromX(result.args.pkx)
69 | console.log(uncompressedPubKey)
70 | const payload = { ephemeralPublicKey: uncompressedPubKey, ciphertext: result.args.ciphertext }
71 | console.log(safePrivateViewKey)
72 | const viewingKeyPair = new KeyPair(safePrivateViewKey)
73 | const randomNumber = viewingKeyPair.decrypt(payload)
74 | console.log(randomNumber)
75 | const spendingKeyPair = new KeyPair(personalSpendingPrivateKeyHex)
76 | console.log(spendingKeyPair)
77 | const computedReceivingAddress = spendingKeyPair.mulPrivateKey(randomNumber)
78 | console.log(computedReceivingAddress)
79 | const safeInfo = await getSafeInfo(result.args.receiver)
80 | console.log(safeInfo)
81 | if (safeInfo.owners.includes(computedReceivingAddress.address)) {
82 | dataArray.push({ result, computedReceivingAddress, randomNumber })
83 | }
84 | }
85 | console.log(dataArray)
86 | return dataArray.map(d => ({
87 | // @ts-ignore
88 | date: d.result.timestamp,
89 | amount: d.result.args[1],
90 | // @ts-ignore
91 | sender: d.result.sender,
92 | randomNumber: d.randomNumber,
93 | stealthSafeReceiver: d.result.args[0],
94 | hasBeenWithdrawn: false,
95 | hasBeenExecuted: false,
96 | hasBeenInitiated: false
97 | }))
98 | }
99 |
100 | return (
101 | <>
102 |
103 |
104 |
105 |
106 | Date Received
107 | Amount
108 | Sender
109 | Stealth Receiver
110 |
111 |
112 |
113 |
114 | {receiveData.withdrawSafeList.map((row) => (
115 |
119 |
120 |
121 | {format(new Date(row.date), 'yyyy MMM dd')}
122 |
123 |
124 | {format(new Date(row.date), 'hh:mm a')}
125 |
126 |
127 |
128 |
129 | {ethers.utils.formatEther(row.amount)}
130 | {/*
*/}
131 | xDAI
132 |
133 |
134 |
135 |
136 | {getKeyShortAddress(row.sender)}
137 |
138 |
139 |
140 |
141 | {getKeyShortAddress(row.stealthSafeReceiver)}
142 |
143 |
144 |
145 |
146 |
147 |
148 | ))}
149 |
150 |
151 |
152 | >
153 | );
154 | };
155 |
156 | export interface IReceiveListOfWithdrawals {
157 |
158 | }
159 |
160 | export default ReceiveListOfWithdrawals;
161 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Receive.ListOfWithdrawals/WithdrawButton.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from 'react';
2 | import {
3 | accordionClasses,
4 | Alert,
5 | Box,
6 | Button,
7 | CircularProgress,
8 | Dialog, DialogActions,
9 | DialogContent,
10 | DialogContentText,
11 | DialogTitle,
12 | Typography
13 | } from "@mui/material";
14 | import {useReceiveData, WithdrawSafe} from "@/context/ReceiveContext";
15 | import {DoneAllRounded} from "@mui/icons-material";
16 | import {theme} from "@/GlobalStyles";
17 | import {BigNumber, ethers, Signer} from "ethers";
18 | import {
19 | OperationType,
20 | SafeMultisigTransactionResponse,
21 | SafeTransactionDataPartial
22 | } from "@safe-global/safe-core-sdk-types";
23 | import {useEthersSigner} from "@/components/utils/clientToSigner";
24 | import {GelatoRelayPack} from "@safe-global/relay-kit";
25 | import Safe, {EthersAdapter, getSafeContract} from "@safe-global/protocol-kit";
26 | import {genPersonalPrivateKeys, UmbraSafe} from "@/components/umbra/umbraExtended";
27 | import safeService from "@/components/safe/safeEthersAdapter";
28 | import {KeyPair} from "umbra/umbra-js/src/";
29 | import {ProposeTransactionProps} from "@safe-global/api-kit";
30 | import {useAccount} from "wagmi";
31 | import { estimateGas } from '@/components/safe/safeApiKit';
32 |
33 | /**
34 | *
35 | * @param {React.PropsWithChildren} props
36 | * @return {JSX.Element}
37 | * @constructor
38 | */
39 | const WithdrawButton: React.FC = (props) => {
40 |
41 | const signer = useEthersSigner();
42 | const account = useAccount();
43 |
44 | const [pendingSafeTxs, setPendingSafeTxs] = useState([]);
45 | const [hasCheckPendingTxsRun, setHasCheckPendingTxsRun] = useState(false);
46 | const [showDialogWithMissingSigns, setShowDialogWithMissingSigns] = useState(false);
47 |
48 | // checks if there are txs pending for this safe once mounted the row
49 | useEffect(() => {
50 | if (props.withdrawSafeData.stealthSafeReceiver && !props.withdrawSafeData.hasBeenExecuted)
51 | hasSafePendingTx().then((txsPending) => {
52 | setPendingSafeTxs(txsPending);
53 | setHasCheckPendingTxsRun(true);
54 | });
55 | }, [props.withdrawSafeData.stealthSafeReceiver]);
56 |
57 | // function to check if there are pending txs
58 | const hasSafePendingTx = useCallback(async (): Promise => {
59 | return (await safeService.getPendingTransactions(props.withdrawSafeData.stealthSafeReceiver)).results;
60 | }, [props.withdrawSafeData.stealthSafeReceiver]);
61 |
62 | // returns the ethAdapter for the gnosis functions
63 | const getEthAdapter = useCallback(async (): Promise => {
64 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm");
65 | const privateKey = await genPersonalPrivateKeys(signer as Signer);
66 | const userStealthPrivateKey = UmbraSafe.computeStealthPrivateKey(privateKey.spendingKeyPair.privateKeyHex as string, props.withdrawSafeData.randomNumber);
67 | const wallet = new ethers.Wallet(userStealthPrivateKey, provider);
68 | return new EthersAdapter({
69 | ethers,
70 | signerOrProvider: wallet
71 | })
72 | }, []);
73 |
74 | // sign the pending tx or show the execute button
75 | const signAndExecute = useCallback(async () => {
76 | const hasAllSignsMade = pendingSafeTxs[0].confirmations && pendingSafeTxs[0].confirmations.length >= pendingSafeTxs[0].confirmationsRequired;
77 | if (hasAllSignsMade) {
78 | const ethAdapter = await getEthAdapter();
79 | const safeSDK = await Safe.create({
80 | ethAdapter,
81 | safeAddress: props.withdrawSafeData.stealthSafeReceiver
82 | })
83 | const safeSingletonContract = await getSafeContract({ ethAdapter, safeVersion: await safeSDK.getContractVersion() })
84 | const pendingTx = pendingSafeTxs[0];
85 | console.log("pendingTx", pendingTx);
86 | console.log("full arrat", [
87 | pendingTx.to,
88 | pendingTx.value,
89 | "0x",
90 | pendingTx.operation,
91 | pendingTx.safeTxGas,
92 | pendingTx.baseGas,
93 | pendingTx.gasPrice,
94 | pendingTx.gasToken,
95 | pendingTx.refundReceiver,
96 | // @ts-ignore
97 | pendingTx.confirmations[0].signature
98 | ]);
99 | const encodedTx = safeSingletonContract.encode('execTransaction', [
100 | pendingTx.to,
101 | pendingTx.value,
102 | "0x",
103 | pendingTx.operation,
104 | pendingTx.safeTxGas,
105 | pendingTx.baseGas,
106 | pendingTx.gasPrice,
107 | pendingTx.gasToken,
108 | pendingTx.refundReceiver,
109 | // @ts-ignore
110 | pendingTx.confirmations[0].signature
111 | ])
112 | const relayKit = new GelatoRelayPack()
113 | const options = {
114 | gasLimit: '500000'
115 | }
116 | console.log("encodedTx", encodedTx);
117 | const response = await relayKit.relayTransaction({
118 | target: props.withdrawSafeData.stealthSafeReceiver,
119 | encodedTransaction: encodedTx,
120 | chainId: 100,
121 | options: options
122 | })
123 | console.log(`Relay Transaction Task ID: https://relay.gelato.digital/tasks/status/${response.taskId}`)
124 | } else {
125 | // still sign are missing
126 | setShowDialogWithMissingSigns(true);
127 | }
128 | }, [pendingSafeTxs, getEthAdapter, props, getSafeContract]);
129 |
130 | // launch the correct logic based on the fact that there are already txs or not
131 | const startWithdraw = useCallback(async () => {
132 | // if (pendingSafeTxs.length > 0) {
133 | // await signAndExecute();
134 | // return;
135 | // }
136 |
137 | // if we're here, means we've not yet a tx to sign
138 | // Any address can be used for destination. In this example, we use vitalik.eth
139 | const destinationAddress = '0xb250c202310da0b15b82E985a30179e74f414457'
140 | const amount = props.withdrawSafeData.amount.toString();
141 | const gasLimit = '500000'
142 | const safeTransactionData = {
143 | to: destinationAddress,
144 | data: '0x',// leave blank for native token transfers
145 | value: amount,
146 | operation: OperationType.Call
147 | }
148 | const options = {
149 | gasLimit
150 | }
151 | // const ethAdapter = await getEthAdapter();
152 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm");
153 | const privateKey = await genPersonalPrivateKeys(signer as Signer);
154 | const userStealthPrivateKey = UmbraSafe.computeStealthPrivateKey(privateKey.spendingKeyPair.privateKeyHex as string, props.withdrawSafeData.randomNumber);
155 | const wallet = new ethers.Wallet(userStealthPrivateKey, provider);
156 | const ethAdapter = new EthersAdapter({
157 | ethers,
158 | signerOrProvider: wallet
159 | });
160 | const safeSDK = await Safe.create({
161 | ethAdapter,
162 | safeAddress: props.withdrawSafeData.stealthSafeReceiver
163 | })
164 | const relayKit = new GelatoRelayPack()
165 | const safeTransaction = await relayKit.relayTransaction(
166 | safeSDK,
167 | [safeTransactionData],
168 | options
169 | )
170 |
171 | console.log("safeTransaction", safeTransaction);
172 | const signedSafeTx = await safeSDK.signTransaction(safeTransaction)
173 |
174 | console.log("wallet.getAddress()", await wallet.getAddress(), "safe", props.withdrawSafeData.stealthSafeReceiver);
175 |
176 | const transactionConfig = {
177 | safeAddress: props.withdrawSafeData.stealthSafeReceiver,
178 | safeTransactionData: safeTransaction.data,
179 | safeTxHash: await safeSDK.getTransactionHash(safeTransaction),
180 | senderAddress: await wallet.getAddress(),
181 | senderSignature: signedSafeTx.encodedSignatures(),
182 | origin: "withdraw"
183 | } as unknown as ProposeTransactionProps
184 |
185 | const propose = await safeService.proposeTransaction(transactionConfig)
186 | console.log(propose)
187 |
188 | }, [props, pendingSafeTxs, signer, account]);
189 |
190 | // launch the correct logic based on the fact that there are already txs or not
191 | const startWithdrawDirect = useCallback(async () => {
192 | // if (pendingSafeTxs.length > 0) {
193 | // await signAndExecute();
194 | // return;
195 | // }
196 |
197 | // if we're here, means we've not yet a tx to sign
198 | // Any address can be used for destination. In this example, we use vitalik.eth
199 | const destinationAddress = '0xb250c202310da0b15b82E985a30179e74f414457'
200 | console.log("props.withdrawSafeData.amount", props.withdrawSafeData.amount.toString())
201 |
202 | const relayKit = new GelatoRelayPack()
203 | const gasLimit = '210000'
204 | const fee = await relayKit.getEstimateFee(100, gasLimit)
205 | console.log("fee", fee)
206 |
207 | const finalFee = ((BigNumber.from(fee)).mul(BigNumber.from("102"))).div(BigNumber.from("100"))
208 |
209 | console.log("finalFee", finalFee)
210 |
211 | const amount = ((props.withdrawSafeData.amount).sub(finalFee)).toString();
212 | console.log(amount)
213 |
214 | const safeTransactionData = {
215 | to: destinationAddress,
216 | data: '0x',// leave blank for native token transfers
217 | value: amount,
218 | operation: OperationType.Call
219 | }
220 | const options = {
221 | gasLimit
222 | }
223 | // const ethAdapter = await getEthAdapter();
224 | const provider = new ethers.providers.JsonRpcProvider("https://rpc.gnosis.gateway.fm");
225 | const privateKey = await genPersonalPrivateKeys(signer as Signer);
226 | const userStealthPrivateKey = UmbraSafe.computeStealthPrivateKey(privateKey.spendingKeyPair.privateKeyHex as string, props.withdrawSafeData.randomNumber);
227 | const wallet = new ethers.Wallet(userStealthPrivateKey, provider);
228 | const ethAdapter = new EthersAdapter({
229 | ethers,
230 | signerOrProvider: wallet
231 | });
232 | const safeSDK = await Safe.create({
233 | ethAdapter,
234 | safeAddress: props.withdrawSafeData.stealthSafeReceiver
235 | })
236 |
237 | const safeTransaction = await relayKit.createRelayedTransaction(
238 | safeSDK,
239 | [safeTransactionData],
240 | options
241 | )
242 |
243 | // const estimatedGas = await estimateGas(props.withdrawSafeData.stealthSafeReceiver, safeTransaction.data)
244 | // console.log("estimatedGas", estimatedGas)
245 | // not accurate, find another way to estimate gas so we can estimate the Gelato fee correctly, hardcoded for now
246 |
247 | console.log("safe", props.withdrawSafeData.stealthSafeReceiver)
248 | console.log("safeTransaction", safeTransaction)
249 | const signedSafeTx = await safeSDK.signTransaction(safeTransaction)
250 | const safeSingletonContract = await getSafeContract({ ethAdapter, safeVersion: await safeSDK.getContractVersion() })
251 |
252 | const encodedTx = safeSingletonContract.encode('execTransaction', [
253 | signedSafeTx.data.to,
254 | signedSafeTx.data.value,
255 | signedSafeTx.data.data,
256 | signedSafeTx.data.operation,
257 | signedSafeTx.data.safeTxGas,
258 | signedSafeTx.data.baseGas,
259 | signedSafeTx.data.gasPrice,
260 | signedSafeTx.data.gasToken,
261 | signedSafeTx.data.refundReceiver,
262 | signedSafeTx.encodedSignatures()
263 | ])
264 |
265 | console.log(encodedTx)
266 |
267 | const response = await relayKit.relayTransaction({
268 | target: props.withdrawSafeData.stealthSafeReceiver,
269 | encodedTransaction: encodedTx,
270 | chainId: 100,
271 | options: options
272 | })
273 |
274 | console.log(response)
275 |
276 |
277 |
278 |
279 |
280 |
281 | // const signedSafeTx = await safeSDK.signTransaction(safeTransaction)
282 | //
283 | // console.log("wallet.getAddress()", await wallet.getAddress());
284 | //
285 | // const transactionConfig = {
286 | // safeAddress: props.withdrawSafeData.stealthSafeReceiver,
287 | // safeTransactionData: safeTransaction.data,
288 | // safeTxHash: await safeSDK.getTransactionHash(safeTransaction),
289 | // senderAddress: await wallet.getAddress(),
290 | // senderSignature: signedSafeTx.encodedSignatures(),
291 | // origin: "withdraw"
292 | // } as unknown as ProposeTransactionProps
293 | //
294 | // const propose = await safeService.proposeTransaction(transactionConfig)
295 |
296 | }, [props, pendingSafeTxs, signer, account]);
297 |
298 | return (
299 | <>
300 | {
301 | props.withdrawSafeData.hasBeenWithdrawn ?
302 |
303 |
304 |
305 | Completed
306 |
307 |
308 | :
309 | ""
310 | }
311 | {
312 | props.withdrawSafeData.hasBeenExecuted ?
313 |
314 |
315 | : ""
316 | }
317 | {
318 | !props.withdrawSafeData.hasBeenWithdrawn && !props.withdrawSafeData.hasBeenExecuted ?
319 | <>
320 |
323 |
344 | >
345 | :
346 | ""
347 | }
348 | >
349 | );
350 | };
351 |
352 | export interface IWithdrawButton {
353 | withdrawSafeData: WithdrawSafe
354 | }
355 |
356 | export default WithdrawButton;
357 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Receive.RegisterSafe/Receive.RegisterSafe.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from 'react';
2 | import {SafeViewKey, useReceiveData, UserStealthAddress} from "@/context/ReceiveContext";
3 | import {Signer} from "ethers";
4 | import {encryptPrivateViewKey} from "@/components/eth-crypto/encryptPrivateViewKey";
5 | import {useEthersSigner} from "@/components/utils/clientToSigner";
6 | import {useAccount, useContractRead} from "wagmi";
7 | import {SAFE_VIEW_KEY_REGISTRY_ABI, SAFE_VIEW_KEY_REGISTRY_ADDRESS} from "@/components/Const";
8 | import {
9 | Alert,
10 | Box,
11 | Button,
12 | CircularProgress,
13 | Dialog,
14 | DialogActions,
15 | DialogContent,
16 | DialogContentText,
17 | DialogTitle, IconButton, Typography
18 | } from "@mui/material";
19 | import Link from "next/link";
20 | import {addSafe} from "@/components/safeKeyRegistry/addSafe";
21 | import {useRouter} from "next/router";
22 | import SuccessInitialized from "@/ui/organisms/Receive.RegisterSafe/SuccessInitialized";
23 | import {RefreshRounded} from "@mui/icons-material";
24 | import {generateKeys} from "@/components/umbra/umbraExtended";
25 |
26 | /**
27 | *
28 | * @param {React.PropsWithChildren} props
29 | * @return {JSX.Element}
30 | * @constructor
31 | */
32 | const ReceiveRegisterSafe: React.FC = (props) => {
33 |
34 | const receiveData = useReceiveData();
35 | const signer = useEthersSigner();
36 | const { address } = useAccount();
37 | const router = useRouter();
38 |
39 |
40 | const [showDialogToInitialize, setShowDialogToInitialize] = useState(false);
41 | const [internalInitializationState, setInternalInitializationState] = useState<"none" | "sending" | "sent">("none");
42 |
43 | const readStealthSafeKeys = useContractRead({
44 | address: SAFE_VIEW_KEY_REGISTRY_ADDRESS,
45 | abi: SAFE_VIEW_KEY_REGISTRY_ABI,
46 | functionName: 'stealthKeys',
47 | args: [receiveData.selectedSafe],
48 | watch: true
49 | });
50 |
51 | // check if safe is initialized or not once the read call to VIEW_KEY_SAFE_REGISTRY_ADDRESS is over
52 | useEffect(() => {
53 | if (readStealthSafeKeys.isSuccess) {
54 | const viewingSafePubKey = (readStealthSafeKeys.data as any)[1] as BigInt;
55 | if (viewingSafePubKey === BigInt(0)) {
56 | setShowDialogToInitialize(true);
57 | receiveData.setIsSelectedSafeInitialized(false);
58 | } else {
59 | setShowDialogToInitialize(false);
60 | receiveData.setIsSelectedSafeInitialized(true);
61 | }
62 | }
63 | }, [readStealthSafeKeys.isSuccess]);
64 |
65 | // generate the view keys for the Stealth registry, encrypting for the owners of the safe
66 | const generateSafeKeys = useCallback( async (): Promise<{ safeKeys: SafeViewKey, ownersKeys: UserStealthAddress[] }> => {
67 | const keys = await generateKeys(signer as Signer)
68 | const _tmpOwnersStealthKeys = JSON.parse(JSON.stringify(receiveData.ownersStealthKeys));
69 | for (let i = 0; i < _tmpOwnersStealthKeys.length; i++) {
70 | const pubKeySliced = _tmpOwnersStealthKeys[i].viewingPublicKey.slice(2)
71 | const encryptedKey = await encryptPrivateViewKey(pubKeySliced as string, keys.viewingKeyPair.privateKeyHex as string)
72 | _tmpOwnersStealthKeys[i]["safeStealthViewPrivateEncKey"] = "0x"+encryptedKey
73 | }
74 | receiveData.setOwnersStealthKeys(_tmpOwnersStealthKeys);
75 | receiveData.setSafeViewKey(keys);
76 | return {safeKeys: keys, ownersKeys: _tmpOwnersStealthKeys};
77 | }, [signer, receiveData]);
78 |
79 | // initialize the Safe by registering in the Stealth Key Vew Registry
80 | const initializeSafe = useCallback(async () => {
81 | setInternalInitializationState("sending");
82 | const generateSafeKeysResp = await generateSafeKeys();
83 | await addSafe(
84 | receiveData.selectedSafe,
85 | address as string,
86 | generateSafeKeysResp.safeKeys.prefix,
87 | generateSafeKeysResp.safeKeys.pubKeyXCoordinate,
88 | generateSafeKeysResp.ownersKeys.map((key) => [key.safeStealthViewPrivateEncKey as string, key.owner]),
89 | signer as Signer)
90 | setInternalInitializationState("sent");
91 | }, [generateSafeKeys, receiveData.selectedSafe]);
92 |
93 |
94 | return (
95 | <>
96 | {
97 | receiveData.isSelectedSafeInitialized ?
98 |
99 | :
100 | <>
101 |
102 | setShowDialogToInitialize(true)}>
105 | Initialize
106 |
107 | }
108 | >
109 | You need to initialize the Safe
110 |
111 | receiveData.fetchSafeInfo().then()}>
112 |
113 |
114 |
115 |
165 | >
166 | }
167 |
168 | >
169 | );
170 | };
171 |
172 | export interface IReceiveRegisterSafe {
173 |
174 | }
175 |
176 | export default ReceiveRegisterSafe;
177 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Receive.RegisterSafe/SuccessInitialized.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Alert, Box, Button, Collapse, IconButton} from "@mui/material";
3 | import {CloseIcon} from "@coinbase/wallet-sdk/dist/components/icons/CloseIcon";
4 | import {Close} from "@mui/icons-material";
5 |
6 | /**
7 | *
8 | * @param {React.PropsWithChildren} props
9 | * @return {JSX.Element}
10 | * @constructor
11 | */
12 | const SuccessInitialized: React.FC = (props) => {
13 |
14 | const [open, setOpen] = React.useState(true);
15 |
16 | return (
17 |
18 |
19 | {
25 | setOpen(false);
26 | }}
27 | >
28 |
29 |
30 | }
31 | sx={{ mb: 2 }}
32 | >
33 | Safe correctly initialized
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export interface ISuccessInitialized {
41 |
42 | }
43 |
44 | export default SuccessInitialized;
45 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Receive.SelectSafe/Receive.SelectSafe.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect} from 'react';
2 | import {FormControl, InputLabel, MenuItem, Select, SelectChangeEvent} from "@mui/material";
3 | import {useReceiveData} from "@/context/ReceiveContext";
4 | import {getSafeInfo} from "@/components/safe/safeApiKit";
5 | import {getStealthKeys} from "@/components/umbra/getStealthKeys";
6 | import {generateAddress} from "@/components/umbra/generateAddressFromKey";
7 |
8 | /**
9 | *
10 | * @param {React.PropsWithChildren} props
11 | * @return {JSX.Element}
12 | * @constructor
13 | */
14 | const ReceiveSelectSafe: React.FC = (props) => {
15 |
16 | const receiveData = useReceiveData();
17 |
18 | // every time a new safe is selected, fetch the infos
19 | useEffect(() => {
20 | if (receiveData.selectedSafe)
21 | receiveData.fetchSafeInfo().then();
22 | }, [receiveData.selectedSafe])
23 |
24 | // manages the new select of a safe
25 | const handleChange = useCallback((e: SelectChangeEvent) => {
26 | receiveData.setSafeViewKey(undefined);
27 | receiveData.setIsSelectedSafeInitialized(undefined);
28 | receiveData.setOwnersStealthKeys([]);
29 | receiveData.setSelectedSafeOwners([]);
30 | receiveData.setAreAllSafeOwnersInitialized(undefined);
31 | receiveData.setSelectedSafe(e.target.value as string); // need to cast value as string
32 | }, [receiveData.safes]);
33 |
34 | return (
35 |
36 | Select your Safe
37 |
51 |
52 | );
53 | };
54 |
55 | export interface IReceiveSelectSafe {
56 |
57 | }
58 |
59 | export default ReceiveSelectSafe;
60 |
--------------------------------------------------------------------------------
/frontend/ui/organisms/Send.ReceiverAndAmount/Send.ReceiverAndAmount.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState} from 'react';
2 | import {Box, InputAdornment, TextField, Typography} from "@mui/material";
3 | import {useSendData} from "@/context/SendContext";
4 | import {useAccount, useBalance} from "wagmi";
5 |
6 | /**
7 | *
8 | * @param {React.PropsWithChildren} props
9 | * @return {JSX.Element}
10 | * @constructor
11 | */
12 | const SendReceiverAndAmount: React.FC = (props) => {
13 | const sendData = useSendData();
14 | const account = useAccount();
15 |
16 | const inputxDaiRef = useRef(null);
17 |
18 | const [inputType, setInputType] = useState<"address" | "ens" | "invalid">("invalid");
19 | const [error, setError] = useState("");
20 | const [balanceData, setBalanceData] = useState(undefined);
21 |
22 | const { data, isError, isLoading, isSuccess } = useBalance({
23 | address: account.address
24 | })
25 |
26 | // set the balance once loaded
27 | useEffect(() => {
28 | if (!!data?.formatted) {
29 | setBalanceData(parseFloat(data?.formatted).toFixed(2));
30 | }
31 | }, [data?.formatted]);
32 |
33 | // the function to handle focus event
34 | const handleInputxDaiFocus = () => {
35 | if (inputxDaiRef.current && sendData.sendAmount === 0) {
36 | inputxDaiRef.current.select();
37 | }
38 | };
39 |
40 | const handleInputChange = (e: React.ChangeEvent) => {
41 | sendData.setIsReceiverValidInitializedSafe(undefined);
42 | const value = e.target.value;
43 | sendData.setSendTo(value);
44 |
45 | const isEthAddress = /^0x[a-fA-F0-9]{40}$/.test(value);
46 | const isEnsDomain = /^[a-z0-9]+\.eth$/.test(value) || /^[a-z0-9]+\.[a-z0-9]+\.eth$/.test(value);
47 |
48 | // TODO allow to add an ENS domain
49 |
50 | // TODO when the tx starts, place a button to cancel the freeze (at the moment user has to refresh the page, or complete the flow)
51 |
52 | if (isEthAddress) {
53 | setInputType("address");
54 | setError("");
55 | sendData.setIsReceiverValidAddress(true);
56 | } else {
57 | setInputType("invalid");
58 | sendData.setIsReceiverValidAddress(false);
59 | }
60 | }
61 |
62 | const handleBlur = () => {
63 | if (inputType === "invalid" && sendData.sendTo !== "") {
64 | setError("Invalid Ethereum Address");
65 | }
66 | if (sendData.sendTo == "") setError("");
67 | }
68 |
69 | const handleAmountChange = (e: React.ChangeEvent) => {
70 | const value = e.target.value;
71 | const amount = Number(value);
72 | if (!isNaN(amount) && amount >= 0.01) {
73 | sendData.setSendAmount(amount);
74 | } else sendData.setSendAmount(0)
75 | }
76 |
77 | return (
78 |
79 |
80 |
81 |
94 |
95 | xDAI,
101 | }}
102 | disabled={sendData.isStealthSafeGenerationInProgress || sendData.generatedSafeStealthAddress !== ""}
103 | inputProps={{ min: "0.01", step: "0.01", ref: inputxDaiRef }}
104 | onFocus={handleInputxDaiFocus} // Add this line
105 | sx={{
106 | width: 90,
107 | '& input::-webkit-inner-spin-button': {
108 | '-webkit-appearance': 'none',
109 | },
110 | '& input::-webkit-outer-spin-button': {
111 | '-webkit-appearance': 'none',
112 | },
113 | '& input': {
114 | '-moz-appearance': 'textfield',
115 | 'text-align': 'right'
116 | },
117 | }}
118 | />
119 |
120 |
121 | {
122 | balanceData !== null ?
123 |
124 |
125 | Balance: {balanceData} xDAI
126 |
127 |
128 | :
129 | ""
130 | }
131 |
132 | );
133 | };
134 |
135 | export interface ISendReceiverAndAmount {
136 |
137 | }
138 |
139 | export default SendReceiverAndAmount;
140 |
--------------------------------------------------------------------------------
/frontend/utils/web3/address.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Ths file contains functions user to manipulate addresses and their format, expecially
3 | * when not directly available on ethers
4 | */
5 |
6 | /**
7 | * Returns the first {numLetters} characters of a wallet address. The result exclude the starting 0x
8 | * @param address
9 | * @param numLetters
10 | * @param with0x
11 | */
12 | export const getFirstLetters = (address: string | undefined, numLetters: number) => {
13 | if (!address) return "";
14 | return address.replace("0x", "").substring(0, numLetters).toUpperCase()
15 | }
16 |
17 |
18 | /**
19 | * Given an address, returns the 0xAAAA...AAAA version
20 | * @param address
21 | * @param numLetters
22 | * @param with0x
23 | */
24 | export const getKeyShortAddress = (address: string | undefined) => {
25 | if (!address) return "";
26 | return address.substring(0, 6).toUpperCase() + "..." + address.substring(address.length-4, address.length).toUpperCase()
27 | }
28 |
29 |
--------------------------------------------------------------------------------