├── 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 |