├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .gitbook.yaml ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .huskyrc.js ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── docs ├── README.md ├── SUMMARY.md ├── comparison_to_others │ └── README.md ├── getting_started │ └── README.md └── using_aztec │ └── README.md ├── lerna.json ├── package.json ├── packages ├── contract-artifacts │ ├── .eslintrc.js │ ├── .gitignore │ ├── .lintstagedrc │ ├── .prettierrc.js │ ├── addresses │ │ └── rinkeby.json │ ├── contracts │ │ ├── NoteStream.ts │ │ ├── StreamUtilities.ts │ │ ├── Types.ts │ │ └── contracts.ts │ ├── package.json │ ├── src │ │ ├── addresses.ts │ │ ├── artifacts.ts │ │ └── index.ts │ └── tsconfig.json ├── contracts │ ├── .env.development │ ├── .eslintrc.js │ ├── .gitattributes │ ├── .gitignore │ ├── .lintstagedrc │ ├── README.md │ ├── buidler.config.js │ ├── contracts │ │ ├── AZTEC │ │ │ └── Imports.sol │ │ ├── NoteStream.sol │ │ ├── StreamUtilities.sol │ │ ├── Types.sol │ │ ├── mocks │ │ │ └── StreamUtilitiesMock.sol │ │ └── test │ │ │ ├── ERC20Mintable.sol │ │ │ └── Imports.sol │ ├── env.js │ ├── package.json │ ├── scripts │ │ ├── deploy.js │ │ └── deployZkAsset.js │ ├── tasks │ │ └── export.js │ └── test │ │ ├── NoteStream │ │ ├── cancelStream.js │ │ ├── constructor.js │ │ ├── createStream.js │ │ ├── getStream.js │ │ ├── index.js │ │ └── withdrawFromStream.js │ │ ├── StreamUtilities │ │ ├── getRatio.js │ │ ├── index.js │ │ ├── processCancellation.js │ │ ├── processWithdrawal.js │ │ ├── validateJoinSplitProof.js │ │ └── validateRatioProof.js │ │ ├── fixtures.js │ │ └── runTests.js ├── dev-utils │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── .lintstagedrc │ ├── .prettierrc.js │ ├── README.md │ ├── babel.config.js │ ├── package.json │ └── src │ │ ├── chaiPlugin.js │ │ ├── constants.js │ │ ├── errors.js │ │ ├── index.js │ │ └── mochaContexts.js ├── monorepo-scripts │ ├── .lintstagedrc │ ├── ci │ │ └── hasChanged.sh │ └── package.json ├── react-app │ ├── .env │ ├── .eslintrc.js │ ├── .lintstagedrc │ ├── .prettierrc.js │ ├── README.md │ ├── now.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── abis │ │ │ └── ERC20Detailed.ts │ │ ├── components │ │ │ ├── Link.tsx │ │ │ ├── Sidebar │ │ │ │ ├── SideBarLinks.tsx │ │ │ │ ├── WalletButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── StreamTable │ │ │ │ ├── StreamRow.tsx │ │ │ │ └── index.tsx │ │ │ ├── Table │ │ │ │ ├── TableHead.tsx │ │ │ │ ├── columns.ts │ │ │ │ ├── index.tsx │ │ │ │ └── sorting.ts │ │ │ ├── display │ │ │ │ └── DoubleProgressBar.tsx │ │ │ ├── form │ │ │ │ ├── AddressInput.tsx │ │ │ │ ├── AmountInput.tsx │ │ │ │ └── ZkAssetSelect.tsx │ │ │ └── modals │ │ │ │ └── CreateStreamModal.tsx │ │ ├── contexts │ │ │ ├── AztecContext.tsx │ │ │ └── OnboardContext.tsx │ │ ├── graphql │ │ │ └── streams.ts │ │ ├── hooks │ │ │ ├── useDecodedNote.ts │ │ │ └── useENSName.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── ExchangePage.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── ReceivePage.tsx │ │ │ └── SendPage.tsx │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ ├── setupTests.ts │ │ ├── theme.tsx │ │ ├── types │ │ │ ├── declarations.d.ts │ │ │ └── types.ts │ │ └── utils │ │ │ ├── ens │ │ │ └── lookupAddress.ts │ │ │ ├── links.ts │ │ │ ├── note.ts │ │ │ ├── proofs │ │ │ ├── dividend.ts │ │ │ ├── index.ts │ │ │ ├── joinsplit.ts │ │ │ └── proofs.ts │ │ │ ├── stream │ │ │ ├── cancellation.ts │ │ │ ├── creation.ts │ │ │ ├── index.ts │ │ │ └── withdrawal.ts │ │ │ ├── time.ts │ │ │ ├── transak.ts │ │ │ └── units │ │ │ └── convertToTokenValue.ts │ └── tsconfig.json └── subgraph │ ├── .eslintrc.js │ ├── .gitignore │ ├── .lintstagedrc │ ├── .prettierrc.js │ ├── README.md │ ├── abis │ └── NoteStream.json │ ├── networks.yaml │ ├── package.json │ ├── schema.graphql │ ├── src │ └── mappings │ │ ├── noteStream.ts │ │ ├── tokens.ts │ │ ├── transactions.ts │ │ └── zkAssets.ts │ ├── subgraph.template.yaml │ ├── templatify.js │ ├── tsconfig.json │ └── updateAddresses.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | welcome: circleci/welcome-orb@0.4.1 5 | 6 | jobs: 7 | build: 8 | working_directory: ~/repo 9 | docker: 10 | - image: circleci/node:10.18.1 11 | steps: 12 | - checkout 13 | - run: 14 | name: 'Update NPM' 15 | command: sudo npm install -g npm@6.13.4 16 | - run: 17 | name: 'Update Yarn' 18 | command: yarn policies set-version 1.22.4 19 | - run: 20 | name: 'Set caching variables' 21 | command: | 22 | LAST_SUCCESSFUL_BUILD_URL="https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/tree/dev?filter=successful&limit=1" 23 | LAST_SUCCESSFUL_COMMIT=`curl -Ss -u "$CIRCLE_TOKEN:" $LAST_SUCCESSFUL_BUILD_URL | jq -r '.[0]["vcs_revision"]'` 24 | echo $LAST_SUCCESSFUL_COMMIT > /tmp/last-successful-commit 25 | echo $CIRCLE_SHA1 > /tmp/current-commit 26 | - restore_cache: 27 | keys: 28 | - build-cache-{{ .Branch }}-{{ checksum "/tmp/last-successful-commit" }} 29 | - build-cache-dev-{{ checksum "/tmp/last-successful-commit" }} 30 | - run: 31 | name: 'Install Dependencies' 32 | command: yarn install 33 | - run: 34 | name: 'Build Packages' 35 | command: yarn build 36 | - save_cache: 37 | key: repo-{{ .Environment.CIRCLE_SHA1 }} 38 | paths: 39 | - ~/repo 40 | - save_cache: 41 | key: build-cache-{{ .Branch }}-{{ checksum "/tmp/current-commit" }} 42 | paths: 43 | - ~/repo/packages/contract-artifacts/node_modules 44 | - ~/repo/packages/contract-artifacts/lib 45 | - ~/repo/packages/contracts/node_modules 46 | - ~/repo/packages/dev-utils/node_modules 47 | - ~/repo/packages/react-app/node_modules 48 | - ~/repo/packages/subgraph/node_modules 49 | test: 50 | working_directory: ~/repo 51 | docker: 52 | - image: circleci/node:10.18.1 53 | steps: 54 | - restore_cache: 55 | keys: 56 | - repo-{{ .Environment.CIRCLE_SHA1 }} 57 | - run: 58 | name: 'Test Packages' 59 | command: yarn test 60 | 61 | workflows: 62 | version: 2 63 | main: 64 | jobs: 65 | - build: 66 | filters: 67 | branches: 68 | ignore: gh-pages 69 | - test: 70 | requires: 71 | - build 72 | filters: 73 | branches: 74 | ignore: gh-pages 75 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # ignored directories 2 | node_modules/* 3 | /build/* 4 | /demo/* 5 | /lib/* 6 | 7 | # ignored file types 8 | **/*.json 9 | **/*.md 10 | **/*.lock 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base', 4 | ], 5 | parser: 'babel-eslint', 6 | env: { 7 | es6: true, 8 | }, 9 | rules: { 10 | 'max-len': ['error', { 11 | code: 120, 12 | ignoreComments: true, 13 | ignoreTrailingComments: true, 14 | ignoreUrls: true, 15 | ignoreStrings: true, 16 | ignoreTemplateLiterals: true, 17 | ignoreRegExpLiterals: true, 18 | }], 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | 3 | ​structure: 4 | readme: README.md 5 | summary: SUMMARY.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ["https://gitcoin.co/grants/733/notestream"] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.*.local 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # subgraph 25 | /packages/subgraph/build/ 26 | /packages/subgraph/src/types/ 27 | 28 | # now 29 | .now 30 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lerna run --concurrency 1 --stream precommit --since HEAD" 5 | } 6 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | ['@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | module.exports = { 3 | extends: [ 4 | '@commitlint/config-conventional', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the NoteStream Docs 2 | 3 | If you are interested in how to use NoteStream to privately stream Ethereum tokens, then please look at our [Getting Started](./getting_started/README.md) page. 4 | 5 | # Github 6 | 7 | The codebase for the NoteStream contracts and frontend are hosted publicly on [Github](https://github.com/TomAFrench/NoteStream). 8 | 9 | # Networks 10 | 11 | As NoteStream is in rapid development, it is currently only deployed on the Rinkeby test network. We hope to deploy to other testnets and subsequently the Ethereum Mainnet in due time. 12 | 13 | # Gas Costs 14 | 15 | We have not measured the gas costs associated with using NoteStream in detail, however we expect the majority of the cost to be from validating the ZK proof required for all transactions using AZTEC Protocol. Since EIP 1108, this takes in the range of 200,000-300,000 gas which corresponds to roughly 5-6x that of a standard ERC20 token transfer. -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary​ 2 | 3 | 4 | * [Getting Started](getting_started/README.md) 5 | * [Using Aztec notes](using_aztec/README.md) 6 | * [Comparison to other money streaming options](comparison_to_others/README.md) 7 | 8 | -------------------------------------------------------------------------------- /docs/comparison_to_others/README.md: -------------------------------------------------------------------------------- 1 | # Sablier -------------------------------------------------------------------------------- /docs/getting_started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | ## Trade ERC20 tokens for ZkAssets 3 | 4 | Many of the tokens which you will want to stream are going to be ERC20 tokens such as DAI. These don't have the privacy features needed for NoteStream to work. Luckily we can wrap these tokens into an ZkAsset before we start streaming them. 5 | To do this click the "Deposit ERC20 tokens for private assets" and enter the number of tokens you want to convert into ZkAssets. 6 | 7 | ## Deposit a note into a stream 8 | 9 | You can create a stream by clicking the "Create a new stream" button and then following the shown instructions. 10 | It is important to note that the stream recipient must have registered for an AZTEC address in order for then to be able to receive a stream. This is done automatically upon first visiting the NoteStream website. 11 | 12 | ## Withdrawing from a stream 13 | 14 | The NoteStream app continually calculates the maximum amount which you can withdraw from the streams you are receiving. If you click the "withdraw" button next to the stream you want to withdraw from then a ZK proof will be generated to withdraw this amount from the stream note to your wallet. 15 | 16 | ## Converting back into ERC20 tokens 17 | 18 | Once you have withdrawn your ZkAssets, you can convert them back into ERC20 tokens in much the same way as converting ERC20s into ZkAssets. 19 | Of course, when you convert ZkAssets back into ERC20 tokens you lose the privacy properties given by AZTEC notes. There is also the possibility to leak information on the value of a stream if you immediately convert a withdrawal back into ERC20 tokens. 20 | 21 | # Terminology 22 | ## AZTEC Note 23 | 24 | AZTEC notes can be thought of as tokens on Ethereum for which all balances are encrypted so only their owners can view them. 25 | This is a gross oversimplification as these notes instead work on UTXO model (similar to Bitcoin) rather than then fungible balance model of ERC20 tokens which are all familiar with. 26 | 27 | ## ZkAsset 28 | 29 | A zero knowledge representation of an ERC20 token. For example: DAI is represented by the zkDAI ZkAsset. 30 | 31 | ## Stream Note 32 | The AZTEC note held by the NoteStream contract for a given stream. This note represents the total value of the stream at any time. 33 | 34 | # How it works 35 | ## Where is my money held? 36 | 37 | Each stream is made up of an AZTEC note locked on the NoteStream smart contract. The logic of this smart contract is such that only the stream's sender or receiver may interact with this note. 38 | 39 | ## How can my streams be private if everything on Ethereum is public? 40 | 41 | You're right that everything that happens on the Ethereum network is available for anyone to inspect, however NoteStream uses AZTEC Protocol which allows funds to be transferred as "notes" for which the value is encrypted. Everyone can see that a stream exists but nobody but you will know how much value it contains. 42 | 43 | # Interacting with NoteStream 44 | ## Can I cancel streams? 45 | 46 | Yes. Both the stream sender and recipient can cancel the stream at any time. This will send the appropriate fraction of the stream note's value to each party and then delete the stream. 47 | Can I modify a stream in progress? 48 | No. We're looking at the possibility to allow a stream's sender to modify a stream in progress in certain ways, e.g. extending the stream by topping up the stream note's value. 49 | 50 | # Privacy 51 | 52 | There are a number of privacy enhancing measures you can take using ZkAssets which are general rather than NoteStream-specific. Please see ["Using Aztec notes"](./using_aztec/README.md) for more information. 53 | 54 | ## NoteStream-specific public information 55 | 56 | There are two times at which information about the stream is made visible 57 | 58 | ### Creating a stream 59 | 60 | A stream is made up of the following information: 61 | - Sender address 62 | - Recipent address 63 | - Stream note hash 64 | - ZkAsset address 65 | - Start Time 66 | - Stop Time 67 | 68 | An observer will then be able to see who is streaming to whom, what kind of token they are streaming and when this stream occurred. e.g. 69 | > Alice streamed Bob an unknown amount of zkDAI represented by the AZTEC note with hash 0x1a3...cE1 from 9:00am until 5:00pm on 12/4/20 70 | 71 | ### Withdrawing/cancelling a stream 72 | 73 | Withdrawal and cancellation transactions leak the same information. Here we discuss a withdrawal transaction as an example. 74 | 75 | Each withdrawal transaction includes information on the fraction of the stream's duration which is being withdrawn. This is required such that the NoteStream contract can ensure that the withdrawal is valid. 76 | An observer may then for example see that the recipient is withdrawing a value corresponding to a certain fraction of the remaining value on the stream note. 77 | 78 | However it is important to note that without knowledge of the initial value of the stream note then it is impossible to determine the absolute value being withdrawn. e.g. 79 | 80 | > Bob withdrew 50% of the value of the stream at 1:00pm on 12/4/20 81 | 82 | 83 | # Security 84 | ## Is NoteStream safe? 85 | Currently there are a number of known security flaws which mean that NoteStream should not be used for any Mainnet funds (as such, there is no current Mainnet deployment.). I'm currently speaking with AZTEC about updates to their SDK in order to fix these. 86 | 87 | ## How do I know you can't steal my funds? 88 | 89 | All Notestream contracts are open source and verified on Etherscan. -------------------------------------------------------------------------------- /docs/using_aztec/README.md: -------------------------------------------------------------------------------- 1 | # Issues associated with depositing/withdrawing ERC20 tokens 2 | 3 | An important factor to keep in mind is that the process of depositing ERC20s into a ZkAsset doesn't immediately make them private, i.e. there will be a transaction visible on the blockchain in which a certain amount of DAI is converted into a number of zkDAI notes. 4 | It's impossible to tell what each individual note is worth but the sum of them must equal the number of ERC20s deposited. In the worst case scenario, if all of those notes are then used in a single transaction (such as creation of a NoteStream stream) then it's obvious that the transaction value is equal to that of the ERC20 deposit. 5 | 6 | This might sound like it means that it's impossible to have privacy using ZkAssets as anyone can trace your notes back to when they were deposited. However as people send ZkAssets to each other and notes are split and joined, a given deposit may be linked to a huge amount of notes spread over a vast number of people. We're very quickly at a point where we can see that 1000 people all together own the value from a given deposit but it's impossible to work out exactly who owns what fraction. 7 | 8 | ## Improving privacy of deposits 9 | 10 | Even before this mixing behaviour there are steps you can take to improve your privacy. When depositing ERC20s into a ZkAsset its possible to create a number of notes which have zero value attached. This might sound pointless but it allows you then spend your entire deposit without letting anyone know how much you've spent. 11 | 12 | An observer will only be able to tell that your stream is worth at most equal to your deposit but it could be anything less than that. 13 | This behaviour is implemented automatically by the AZTEC sdk so you don't need to worry about it. 14 | 15 | ## Take aways 16 | 17 | In order to improve the privacy of your transactions using ZkAssets it is best to 18 | 19 | - Have a long history of transactions using this ZkAsset since your last deposit (idealling receiving ZkAsset funds from other people as well) 20 | - Deposit an amount of ERC20 tokens in excess of what you are planning on immediately streaming. -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.13.1", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "independent", 7 | "command": { 8 | "publish": { 9 | "ignoreChanges": [ 10 | "*.md", 11 | "lib", 12 | "scripts", 13 | "test/**/*" 14 | ] 15 | } 16 | }, 17 | "npmClient": "yarn", 18 | "useWorkspaces": true 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/monorepo", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "ethereum", 6 | "react", 7 | "workspaces", 8 | "yarn" 9 | ], 10 | "private": true, 11 | "scripts": { 12 | "pre-commit": "lint-staged", 13 | "build": "lerna link && yarn wsrun --package $PKG --recursive --stages -c build", 14 | "clean": "yarn wsrun --package $PKG --parallel -c clean", 15 | "clean:node_modules": "lerna clean --yes; shx rm -rf node_modules", 16 | "commit": "git-cz", 17 | "contracts:compile": "yarn workspace @notestream/contracts compile", 18 | "contracts:deploy": "yarn workspace @notestream/contracts deploy", 19 | "contracts:test": "yarn workspace @notestream/contracts test", 20 | "react-app:build": "yarn workspace @notestream/react-app build", 21 | "react-app:eject": "yarn workspace @notestream/react-app eject", 22 | "react-app:start": "yarn workspace @notestream/react-app start", 23 | "react-app:test": "yarn workspace @notestream/react-app test", 24 | "subgraph:auth": "yarn workspace @notestream/subgraph auth", 25 | "subgraph:codegen": "yarn workspace @notestream/subgraph codegen", 26 | "subgraph:build": "yarn workspace @notestream/subgraph build", 27 | "subgraph:deploy": "yarn workspace @notestream/subgraph deploy", 28 | "test": "yarn wsrun --package $PKG --serial -c --if has:changed --ifDependency test", 29 | "wsrun": "wsrun --exclude-missing --fast-exit" 30 | }, 31 | "workspaces": { 32 | "packages": [ 33 | "packages/*" 34 | ], 35 | "nohoist": [ 36 | "**/@graphprotocol/graph-ts", 37 | "**/@graphprotocol/graph-ts/**" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "^8.3.5", 42 | "@commitlint/config-conventional": "^8.3.4", 43 | "commitizen": "^4.0.4", 44 | "husky": "^4.2.5", 45 | "lerna": "^3.20.2", 46 | "now": "^18.0.0", 47 | "wsrun": "^5.2.0" 48 | }, 49 | "config": { 50 | "commitizen": { 51 | "path": "./node_modules/cz-conventional-changelog" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/contract-artifacts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | '../../.eslintrc.js', 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 6 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | ], 9 | "env": { 10 | "es6": true, 11 | "browser": true 12 | }, 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "project": "./tsconfig.json", 16 | "tsconfigRootDir": __dirname, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint", 21 | ], 22 | "rules": { 23 | "import/extensions": [ 24 | "error", 25 | "ignorePackages", 26 | { 27 | "js": "never", 28 | "ts": "never" 29 | } 30 | ] 31 | }, 32 | "settings": { 33 | 'import/resolver': { 34 | node: { 35 | extensions: ['.js', '.ts'] 36 | }, 37 | }, 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/contract-artifacts/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /packages/contract-artifacts/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/contract-artifacts/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | tabWidth: 2 6 | }; -------------------------------------------------------------------------------- /packages/contract-artifacts/addresses/rinkeby.json: -------------------------------------------------------------------------------- 1 | { 2 | "ACE": "0x065178E11D516D115eA9437336f8d1bF4178f48c", 3 | "NoteStream": "0xCcb98Efa4eA6a3814ece095f73264c43D7D50071" 4 | } 5 | -------------------------------------------------------------------------------- /packages/contract-artifacts/contracts/StreamUtilities.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | contractName: 'StreamUtilities', 3 | abi: [], 4 | bytecode: 5 | '0x60556023600b82828239805160001a607314601657fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a72315820b0c8047152ae625605cc7c7239b8e424dfb7a37caeb5490776c10883d23299b664736f6c634300050f0032', 6 | deployedBytecode: 7 | '0x73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a72315820b0c8047152ae625605cc7c7239b8e424dfb7a37caeb5490776c10883d23299b664736f6c634300050f0032', 8 | linkReferences: {}, 9 | deployedLinkReferences: {}, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/contract-artifacts/contracts/Types.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | contractName: 'Types', 3 | abi: [], 4 | bytecode: 5 | '0x60556023600b82828239805160001a607314601657fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a72315820c6de8fa01af98cbb93ea48dee56a2beaa2f6e7cace3b23b6e79c686855a1cb6064736f6c634300050f0032', 6 | deployedBytecode: 7 | '0x73000000000000000000000000000000000000000030146080604052600080fdfea265627a7a72315820c6de8fa01af98cbb93ea48dee56a2beaa2f6e7cace3b23b6e79c686855a1cb6064736f6c634300050f0032', 8 | linkReferences: {}, 9 | deployedLinkReferences: {}, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/contract-artifacts/contracts/contracts.ts: -------------------------------------------------------------------------------- 1 | export default ['NoteStream', 'StreamUtilities', 'Types']; 2 | -------------------------------------------------------------------------------- /packages/contract-artifacts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/contract-artifacts", 3 | "description": "Addresses of deployed AZTEC contracts on Ethereum mainnet and testnets", 4 | "version": "0.2.0", 5 | "author": "Tom French", 6 | "bugs": { 7 | "url": "https://github.com/TomAFrench/NoteStream/issues" 8 | }, 9 | "devDependencies": { 10 | "@babel/cli": "^7.4.3", 11 | "@babel/core": "^7.4.0", 12 | "@babel/plugin-proposal-object-rest-spread": "^7.4.0", 13 | "@babel/preset-env": "^7.9.0", 14 | "@babel/preset-typescript": "^7.9.0", 15 | "@typescript-eslint/eslint-plugin-tslint": "^2.27.0", 16 | "eslint": "^5.15.3", 17 | "eslint-config-airbnb-base": "^13.1.0", 18 | "eslint-config-prettier": "^6.0.0", 19 | "eslint-plugin-import": "^2.20.2", 20 | "lint-staged": "^10.2.2", 21 | "shx": "^0.3.2", 22 | "typescript": "^3.8.3" 23 | }, 24 | "engines": { 25 | "node": ">=8.3" 26 | }, 27 | "files": [ 28 | "/abis", 29 | "/addresses", 30 | "/lib" 31 | ], 32 | "homepage": "https://github.com/TomAFrench/NoteStream#readme", 33 | "keywords": [ 34 | "aztec", 35 | "blockchain", 36 | "confidentiality", 37 | "cryptography", 38 | "ethereum", 39 | "privacy" 40 | ], 41 | "license": "LGPL-3.0", 42 | "main": "./lib", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/TomAFrench/NoteStream.git" 46 | }, 47 | "scripts": { 48 | "precommit": "lint-staged", 49 | "commit": "git-cz", 50 | "prepare": "yarn clean && yarn lint --fix", 51 | "build": "yarn prepare && yarn build:types && yarn build:js", 52 | "build:js": " babel src --out-dir lib --extensions '.ts,.tsx' --root-mode upward ./src", 53 | "build:types": "tsc --emitDeclarationOnly", 54 | "clean": "shx rm -rf ./lib", 55 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh contract-artifacts", 56 | "lint": "eslint --config .eslintrc.js --ext .js,.ts . ", 57 | "watch": "yarn build --watch" 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-conventional-changelog" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/contract-artifacts/src/addresses.ts: -------------------------------------------------------------------------------- 1 | // import mainnetAddresses from "../addresses/mainnet.json"; 2 | import rinkebyAddresses from '../addresses/rinkeby.json'; 3 | 4 | export type Address = string; 5 | 6 | export interface Deployment { 7 | ACE: Address; 8 | NoteStream: Address; 9 | } 10 | 11 | /** 12 | * Used to get addresses of contracts that have been deployed to either the 13 | * Ethereum mainnet or a supported testnet. Throws if there are no known 14 | * contracts deployed on the corresponding network. 15 | * @param networkId The desired networkId. 16 | * @returns The set of addresses for contracts which have been deployed on the 17 | * given networkId. 18 | */ 19 | const getContractAddressesForNetwork = (networkId: number): Deployment => { 20 | switch (networkId) { 21 | // case 1: 22 | // return mainnetAddresses; 23 | case 4: 24 | return rinkebyAddresses; 25 | default: 26 | throw new Error( 27 | `Unknown network id (${networkId}). No known NoteStream contracts have been deployed on this network.`, 28 | ); 29 | } 30 | }; 31 | 32 | export default getContractAddressesForNetwork; 33 | -------------------------------------------------------------------------------- /packages/contract-artifacts/src/artifacts.ts: -------------------------------------------------------------------------------- 1 | import NoteStream from '../contracts/NoteStream'; 2 | import StreamUtilities from '../contracts/StreamUtilities'; 3 | import Types from '../contracts/Types'; 4 | 5 | export default { 6 | NoteStream, 7 | StreamUtilities, 8 | Types, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/contract-artifacts/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as artifacts } from './artifacts'; 2 | export { default as getContractAddressesForNetwork } from './addresses'; 3 | -------------------------------------------------------------------------------- /packages/contract-artifacts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "lib": ["ES2015", "DOM"], 5 | "module": "ES2015", 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "ES2015", 9 | "declaration": true, 10 | /* Strict Type-Checking Options */ 11 | "noImplicitAny": true, 12 | "strict": true, 13 | /* Module Resolution Options */ 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "skipLibCheck": false, 18 | /* Advanced Options */ 19 | "forceConsistentCasingInFileNames": true 20 | }, 21 | "include": ["./src/**/*", "./contracts/**/*", "./lib/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/contracts/.env.development: -------------------------------------------------------------------------------- 1 | TESTING_ACCOUNT=0x4EA13A06F40EBAE073F8BCE58A504C6FD95DE00F958BC1D7FCD8D97E33AE9215 2 | TESTING_ACCOUNT_ADDRESS=0xcF217475D84997E9c0EbA3052E1F818916fE3eEC 3 | TESTING_ACCOUNT_MNEMONIC=system box custom picture wonder across logic love program pyramid position plunge 4 | INFURA_API_KEY=eca546af737a406487d1e844a99d7955 5 | -------------------------------------------------------------------------------- /packages/contracts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '../../.eslintrc.js', 4 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 5 | ], 6 | env: { 7 | mocha: true, 8 | }, 9 | rules: { 10 | 'import/extensions': [ 11 | 'error', 12 | 'ignorePackages', 13 | { 14 | js: 'never', 15 | ts: 'never', 16 | }, 17 | ], 18 | 'import/no-extraneous-dependencies': [ 19 | 'error', 20 | { devDependencies: ['**/*.js'] }, 21 | ], 22 | }, 23 | overrides: [ 24 | { 25 | files: ['test/**/*.js'], 26 | rules: { 27 | 'func-names': 'off', 28 | }, 29 | }, 30 | { 31 | files: ['scripts/**/*.js'], 32 | rules: { 33 | 'no-console': 'off', 34 | }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /packages/contracts/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | #Buidler files 2 | cache 3 | artifacts 4 | -------------------------------------------------------------------------------- /packages/contracts/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/contracts/README.md: -------------------------------------------------------------------------------- 1 | ## @notestream/contracts 2 | 3 | A minimalist, opinionated structure for managing smart contract ABIs and addresses.
4 | 5 | [Read more about Application Binary Interfaces (ABIs) here](https://ethereum.stackexchange.com/questions/234/what-is-an-abi-and-why-is-it-needed-to-interact-with-contracts). 6 | -------------------------------------------------------------------------------- /packages/contracts/buidler.config.js: -------------------------------------------------------------------------------- 1 | const { usePlugin, task } = require('@nomiclabs/buidler/config'); 2 | const { exportContracts } = require('./tasks/export.js'); 3 | 4 | usePlugin('@nomiclabs/buidler-waffle'); 5 | usePlugin('@nomiclabs/buidler-etherscan'); 6 | require('dotenv').config({ path: '.env.development' }); 7 | 8 | task('export', 'Exports the contract ABIs').setAction(async (args, bre) => { 9 | exportContracts(bre); 10 | }); 11 | 12 | module.exports = { 13 | solc: { 14 | version: '0.5.15', 15 | optimizer: { 16 | enabled: true, 17 | runs: 200, 18 | }, 19 | evmVersion: 'istanbul', 20 | }, 21 | paths: { 22 | artifacts: './build', 23 | }, 24 | // contracts_build_directory: "./build", 25 | mocha: { 26 | bail: true, 27 | enableTimeouts: false, 28 | reporter: 'spec', 29 | }, 30 | networks: { 31 | rinkeby: { 32 | url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, 33 | chainId: 4, 34 | from: process.env.TESTING_ACCOUNT_ADDRESS, 35 | accounts: [process.env.TESTING_ACCOUNT], 36 | // gas: 5500000, 37 | gasPrice: 10000000000, 38 | }, 39 | }, 40 | etherscan: { 41 | url: 'https://api-rinkeby.etherscan.io/api', 42 | apiKey: process.env.ETHERSCAN_API_KEY, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/contracts/contracts/AZTEC/Imports.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "@aztec/protocol/contracts/ACE/ACE.sol"; 4 | import "@aztec/protocol/contracts/ACE/noteRegistry/epochs/201912/base/FactoryBase201912.sol"; 5 | import "@aztec/protocol/contracts/ACE/validators/joinSplit/JoinSplit.sol"; 6 | import "@aztec/protocol/contracts/ACE/validators/dividend/Dividend.sol"; 7 | import "@aztec/protocol/contracts/ERC1724/ZkAsset.sol"; 8 | 9 | 10 | // You might think this file is a bit odd, but let me explain. 11 | // We only use some contracts in our tests, which means Truffle 12 | // will not compile it for us, because it is from an external 13 | // dependency. 14 | // 15 | // We are now left with three options: 16 | // - Copy/paste these contracts 17 | // - Run the tests with `truffle compile --all` on 18 | // - Or trick Truffle by claiming we use it in a Solidity test 19 | // 20 | // You know which one I went for. 21 | 22 | contract Imports { 23 | constructor() public {} 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/contracts/Types.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | 4 | /** 5 | * @title NoteStream Types 6 | * @author NoteStream 7 | */ 8 | library Types { 9 | struct AztecStream { 10 | bytes32 noteHash; 11 | uint256 startTime; 12 | uint256 lastWithdrawTime; 13 | uint256 stopTime; 14 | address recipient; 15 | address sender; 16 | address tokenAddress; 17 | bool isEntity; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/contracts/mocks/StreamUtilitiesMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../StreamUtilities.sol"; 5 | 6 | 7 | contract StreamUtilitiesMock { 8 | 9 | // The provided struct object is stored here as StreamUtilities expects a storage variable. 10 | Types.AztecStream public stream; 11 | 12 | function getRatio(bytes memory _proofData) 13 | public 14 | pure 15 | returns (uint256 ratio) 16 | { 17 | return StreamUtilities.getRatio(_proofData); 18 | } 19 | 20 | function validateRatioProof( 21 | address _aceContractAddress, 22 | bytes memory _proof1, 23 | uint256 _withdrawDuration, 24 | Types.AztecStream memory _stream 25 | ) 26 | public 27 | returns (bytes memory, bytes memory) 28 | { 29 | stream = _stream; 30 | return StreamUtilities._validateRatioProof(_aceContractAddress, _proof1, _withdrawDuration, stream); 31 | } 32 | 33 | function validateJoinSplitProof( 34 | address _aceContractAddress, 35 | bytes memory _proof2, 36 | bytes32 _withdrawalNoteHash, 37 | Types.AztecStream memory _stream 38 | ) public returns (bytes memory proof2Outputs) { 39 | stream = _stream; 40 | return StreamUtilities._validateJoinSplitProof(_aceContractAddress, _proof2, _withdrawalNoteHash, stream); 41 | } 42 | 43 | function processWithdrawal( 44 | address _aceContractAddress, 45 | bytes memory _proof2, 46 | bytes memory _proof1OutputNotes, 47 | Types.AztecStream memory _stream 48 | ) public returns (bytes32) { 49 | stream = _stream; 50 | return StreamUtilities._processWithdrawal(_aceContractAddress, _proof2, _proof1OutputNotes, stream); 51 | } 52 | 53 | function processCancelation( 54 | address _aceContractAddress, 55 | bytes memory _proof2, 56 | bytes memory _proof1OutputNotes, 57 | Types.AztecStream memory _stream 58 | ) public returns (bool) { 59 | stream = _stream; 60 | return StreamUtilities._processCancelation(_aceContractAddress, _proof2, _proof1OutputNotes, stream); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/contracts/contracts/test/ERC20Mintable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.15; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol"; 5 | 6 | /** 7 | * @title ERC20Mintable 8 | * @dev ERC20 minting logic 9 | * Sourced from OpenZeppelin and thoroughly butchered to remove security guards. 10 | * Anybody can mint - STRICTLY FOR TEST PURPOSES 11 | */ 12 | contract ERC20Mintable is ERC20, ERC20Detailed { 13 | 14 | /** 15 | * @dev Sets the values for {name} and {symbol} and {decimals} 16 | * 17 | * All three of these values are immutable: they can only be set once during 18 | * construction. 19 | */ 20 | constructor (string memory name, string memory symbol, uint8 decimals) public ERC20Detailed(name,symbol, decimals){} 21 | 22 | /** 23 | * @dev Function to mint tokens 24 | * @param _to The address that will receive the minted tokens. 25 | * @param _value The amount of tokens to mint. 26 | * @return A boolean that indicates if the operation was successful. 27 | */ 28 | function mint(address _to, uint256 _value) public returns (bool) { 29 | _mint(_to, _value); 30 | return true; 31 | } 32 | } -------------------------------------------------------------------------------- /packages/contracts/contracts/test/Imports.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "@aztec/protocol/contracts/ACE/noteRegistry/epochs/201912/base/FactoryBase201912.sol"; 4 | import "@aztec/protocol/contracts/ACE/ACE.sol"; 5 | import "@aztec/protocol/contracts/ACE/validators/joinSplit/JoinSplit.sol"; 6 | import "@aztec/protocol/contracts/ACE/validators/dividend/Dividend.sol"; 7 | 8 | // You might think this file is a bit odd, but let me explain. 9 | // We only use some contracts in our tests, which means Truffle 10 | // will not compile it for us, because it is from an external 11 | // dependency. 12 | // 13 | // We are now left with three options: 14 | // - Copy/paste these contracts 15 | // - Run the tests with `truffle compile --all` on 16 | // - Or trick Truffle by claiming we use it in a Solidity test 17 | // 18 | // You know which one I went for. 19 | 20 | contract Imports { 21 | constructor() public {} 22 | } 23 | -------------------------------------------------------------------------------- /packages/contracts/env.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import dotenv from 'dotenv'; 4 | 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = 'development'; 7 | } 8 | 9 | const { NODE_ENV } = process.env; 10 | const prefix = path.resolve(__dirname, './.env'); 11 | const dotenvFiles = [ 12 | `${prefix}.${NODE_ENV}.local`, 13 | `${prefix}.${NODE_ENV}`, 14 | prefix, 15 | ].filter(Boolean); 16 | 17 | dotenvFiles.forEach((dotenvFile) => { 18 | if (fs.existsSync(dotenvFile)) { 19 | dotenv.config({ path: dotenvFile }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/contracts", 3 | "version": "0.1.0", 4 | "description": "NoteStream contracts", 5 | "main": "./src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/TomAFrench/NoteStream.git" 9 | }, 10 | "author": "Tom French", 11 | "license": "ISC", 12 | "bugs": { 13 | "url": "https://github.com/TomAFrench/NoteStream/issues" 14 | }, 15 | "homepage": "https://github.com/TomAFrench/NoteStream#readme", 16 | "scripts": { 17 | "precommit": "lint-staged", 18 | "commit": "git-cz", 19 | "compile": "buidler compile", 20 | "clean": "buidler clean", 21 | "deploy": "buidler run scripts/deploy.js", 22 | "export": "buidler export", 23 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh contracts", 24 | "test": "buidler test test/runTests.js", 25 | "lint:js": "eslint --config .eslintrc.js ./scripts ./test" 26 | }, 27 | "files": [ 28 | "/contracts" 29 | ], 30 | "devDependencies": { 31 | "@aztec/bn128": "^1.3.2", 32 | "@aztec/contract-addresses": "1.16.1", 33 | "@aztec/contract-artifacts": "^1.22.0", 34 | "@aztec/dev-utils": "^2.3.1", 35 | "@aztec/protocol": "^1.7.0", 36 | "@nomiclabs/buidler": "^1.3.3", 37 | "@nomiclabs/buidler-ethers": "^1.3.3", 38 | "@nomiclabs/buidler-etherscan": "^1.3.3", 39 | "@nomiclabs/buidler-waffle": "^1.3.3", 40 | "@notestream/dev-utils": "^0.1.0", 41 | "@openzeppelin/contracts": "^2.5.0", 42 | "@openzeppelin/upgrades": "^2.8.0", 43 | "chai": "^4.2.0", 44 | "chalk": "^4.0.0", 45 | "crypto": "^1.0.1", 46 | "dotenv": "^8.2.0", 47 | "eslint": "^7.0.0", 48 | "eslint-config-airbnb": "^18.1.0", 49 | "eslint-config-prettier": "^6.11.0", 50 | "eslint-plugin-prettier": "^3.1.3", 51 | "ethereum-waffle": "^2.5.0", 52 | "ethers": "^4.0.47", 53 | "lint-staged": "^10.2.2", 54 | "moment": "^2.24.0" 55 | }, 56 | "config": { 57 | "commitizen": { 58 | "path": "./node_modules/cz-conventional-changelog" 59 | } 60 | }, 61 | "dependencies": { 62 | "@aztec/protocol": "^1.7.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/contracts/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const env = require('@nomiclabs/buidler'); 3 | const { getContractAddressesForNetwork } = require('@aztec/contract-addresses'); 4 | const bn128 = require('@aztec/bn128'); 5 | const { proofs } = require('@aztec/dev-utils'); 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const chalk = require('chalk'); 10 | 11 | const addressDirectory = path.resolve( 12 | __dirname, 13 | '../../contract-artifacts/addresses/' 14 | ); 15 | 16 | const TESTING_ADDRESS = '0xC6E67ee008a7720722e42F34f30a16d806A45c3F'; 17 | 18 | async function deployAZTEC() { 19 | process.stdout.write(`Deploying ${chalk.cyan('ACE')}...\r`); 20 | const ACE = env.artifacts.require('ACE'); 21 | const ace = await ACE.new(); 22 | console.log( 23 | `Deployed ${chalk.cyan('ACE')} to ${chalk.yellow(ace.address)}` 24 | ); 25 | 26 | console.log('Setting CRS'); 27 | await ace.setCommonReferenceString(bn128.CRS); 28 | 29 | const { JOIN_SPLIT_PROOF, DIVIDEND_PROOF } = proofs; 30 | 31 | process.stdout.write( 32 | `Deploying ${chalk.cyan('JoinSplit validator contract')}...\r` 33 | ); 34 | const JoinSplitValidator = env.artifacts.require('./JoinSplit'); 35 | const joinSplitValidator = await JoinSplitValidator.new(); 36 | await ace.setProof(JOIN_SPLIT_PROOF, joinSplitValidator.address); 37 | console.log( 38 | `Deployed ${chalk.cyan( 39 | 'JoinSplit validator contract' 40 | )} to ${chalk.yellow(joinSplitValidator.address)}` 41 | ); 42 | process.stdout.write( 43 | `Deploying ${chalk.cyan('Dividend validator contract')}...\r` 44 | ); 45 | const DividendValidator = env.artifacts.require('./Dividend'); 46 | const dividendValidator = await DividendValidator.new(); 47 | console.log( 48 | `Deployed ${chalk.cyan( 49 | 'Dividend validator contract' 50 | )} to ${chalk.yellow(dividendValidator.address)}` 51 | ); 52 | await ace.setProof(DIVIDEND_PROOF, dividendValidator.address); 53 | 54 | const generateFactoryId = (epoch, cryptoSystem, assetType) => 55 | epoch * 256 ** 2 + cryptoSystem * 256 ** 1 + assetType * 256 ** 0; 56 | 57 | process.stdout.write(`Deploying ${chalk.cyan('Note Registry')}...\r`); 58 | const BaseFactory = env.artifacts.require( 59 | './noteRegistry/epochs/201912/base/FactoryBase201912' 60 | ); 61 | const baseFactory = await BaseFactory.new(ace.address); 62 | console.log( 63 | `Deployed ${chalk.cyan('Note Registry')} to ${chalk.yellow( 64 | baseFactory.address 65 | )}` 66 | ); 67 | await ace.setFactory(generateFactoryId(1, 1, 1), baseFactory.address); 68 | 69 | return ace; 70 | } 71 | 72 | async function deployZkAsset(aceAddress) { 73 | process.stdout.write(`Deploying ${chalk.cyan('ERC20')}...\r`); 74 | const ERC20Mintable = env.artifacts.require('ERC20Mintable'); 75 | const erc20Mintable = await ERC20Mintable.new('TESTCOIN', 'TEST', 18); 76 | await erc20Mintable.mint(TESTING_ADDRESS, '100000'); 77 | console.log( 78 | `Deployed ${chalk.cyan('Note Registry')} to ${chalk.yellow( 79 | erc20Mintable.address 80 | )}` 81 | ); 82 | 83 | // Deploy a ZkAsset linked to this ERC20 84 | process.stdout.write(`Deploying ${chalk.cyan('ZkAsset')}...\r`); 85 | const ZkAsset = env.artifacts.require('ZkAsset'); 86 | const zkAsset = await ZkAsset.new(aceAddress, erc20Mintable.address, 1); 87 | console.log( 88 | `Deployed ${chalk.cyan('ZkAsset')} to ${chalk.yellow(zkAsset.address)}` 89 | ); 90 | 91 | return zkAsset; 92 | } 93 | 94 | function saveDeployedAddresses(addresses) { 95 | fs.writeFileSync( 96 | path.resolve(addressDirectory, `${env.network.name}.json`), 97 | JSON.stringify(addresses, null, 2) 98 | ); 99 | } 100 | 101 | async function main() { 102 | console.log(); 103 | // Read the address of the ACE contract on chosen network 104 | const networkId = env.network.config.chainId; 105 | const addresses = {}; 106 | 107 | try { 108 | addresses.ACE = getContractAddressesForNetwork(networkId).ACE; 109 | console.log( 110 | `Using existing ${chalk.cyan('ACE')} at ${chalk.yellow( 111 | addresses.ACE 112 | )}` 113 | ); 114 | } catch (e) { 115 | // throw new Error("Unsupported Network") 116 | console.log('This network is unsupported by AZTEC'); 117 | 118 | // Assume we're in BuidlerEVM/Ganache 119 | // We need to deploy ACE and a ZkAsset 120 | const ace = await deployAZTEC(); 121 | addresses.ACE = ace.address; 122 | 123 | const zkAsset = await deployZkAsset(addresses.ACE); 124 | addresses.ZkAsset = zkAsset.address; 125 | } 126 | 127 | process.stdout.write(`Deploying ${chalk.cyan('NoteStream')}...\r`); 128 | const NoteStream = env.artifacts.require('NoteStream'); 129 | const noteStream = await NoteStream.new(addresses.ACE); 130 | addresses.NoteStream = noteStream.address; 131 | 132 | console.log( 133 | `Deployed ${chalk.cyan('NoteStream')} to ${chalk.yellow( 134 | noteStream.address 135 | )}` 136 | ); 137 | 138 | // Write deployed addresses to file 139 | saveDeployedAddresses(addresses); 140 | } 141 | 142 | // We recommend this pattern to be able to use async/await everywhere 143 | // and properly handle errors. 144 | main() 145 | .then(() => process.exit(0)) 146 | .catch((error) => { 147 | console.error(error); 148 | process.exit(1); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/contracts/scripts/deployZkAsset.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const env = require('@nomiclabs/buidler'); 3 | const { getContractAddressesForNetwork } = require('@aztec/contract-addresses'); 4 | 5 | const chalk = require('chalk'); 6 | 7 | const erc20Address = '0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c'; 8 | 9 | async function main() { 10 | console.log(); 11 | const ethers = await env.ethers; 12 | // Read the address of the ACE contract on chosen network 13 | const networkId = env.network.config.chainId; 14 | 15 | const aceAddress = getContractAddressesForNetwork(networkId).ACE; 16 | console.log( 17 | `Using existing ${chalk.cyan('ACE')} at ${chalk.yellow(aceAddress)}` 18 | ); 19 | 20 | process.stdout.write(`Deploying ${chalk.cyan('ZkAsset')}...\r`); 21 | const ZkAsset = await ethers.getContractFactory('ZkAsset'); 22 | const zkAsset = await ZkAsset.deploy( 23 | aceAddress, 24 | erc20Address, 25 | '10000000000000000' 26 | ); 27 | await zkAsset.deployed(); 28 | 29 | console.log( 30 | `Deployed ${chalk.cyan('ZkAsset')} to ${chalk.yellow(zkAsset.address)}` 31 | ); 32 | } 33 | 34 | // We recommend this pattern to be able to use async/await everywhere 35 | // and properly handle errors. 36 | main() 37 | .then(() => process.exit(0)) 38 | .catch((error) => { 39 | console.error(error); 40 | process.exit(1); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/contracts/tasks/export.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | // const bre = require('@nomiclabs/buidler'); 5 | 6 | const publishDir = path.resolve( 7 | __dirname, 8 | '../../contract-artifacts/contracts/' 9 | ); 10 | 11 | async function exportContracts(bre) { 12 | const contractDir = bre.config.paths.sources; 13 | const artifactsDir = bre.config.paths.artifacts; 14 | if (!fs.existsSync(publishDir)) { 15 | fs.mkdirSync(publishDir); 16 | } 17 | const finalContractList = []; 18 | fs.readdirSync(contractDir).forEach((file) => { 19 | if (file.indexOf('.sol') >= 0) { 20 | const contractName = file.replace('.sol', ''); 21 | console.log( 22 | 'Publishing', 23 | chalk.cyan(contractName), 24 | 'to', 25 | chalk.yellow(publishDir) 26 | ); 27 | try { 28 | const contract = fs 29 | .readFileSync( 30 | path.resolve(artifactsDir, `${contractName}.json`) 31 | ) 32 | .toString(); 33 | fs.writeFileSync( 34 | `${publishDir}/${contractName}.ts`, 35 | `export default ${contract}` 36 | ); 37 | finalContractList.push(contractName); 38 | } catch (e) { 39 | console.log(e); 40 | } 41 | } 42 | }); 43 | fs.writeFileSync( 44 | `${publishDir}/contracts.ts`, 45 | `export default ${JSON.stringify(finalContractList)}` 46 | ); 47 | } 48 | 49 | module.exports = { exportContracts }; 50 | -------------------------------------------------------------------------------- /packages/contracts/test/NoteStream/constructor.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use, expect } = require('chai'); 3 | const { solidity, deployContract } = require('ethereum-waffle'); 4 | 5 | const { devConstants } = require('@notestream/dev-utils'); 6 | const NoteStream = require('../../build/NoteStream.json'); 7 | 8 | const { ZERO_ADDRESS } = devConstants; 9 | 10 | use(solidity); 11 | 12 | describe('NoteStream - constructor', function () { 13 | const { provider } = waffle; 14 | const [deployer] = provider.getWallets(); 15 | 16 | it('reverts when the ACE contract is the zero address', async function () { 17 | await expect( 18 | deployContract(deployer, NoteStream, [ZERO_ADDRESS]) 19 | ).to.be.revertedWith('ACE contract is the zero address'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/contracts/test/NoteStream/createStream.js: -------------------------------------------------------------------------------- 1 | // const { devConstants } = require("@notestream/dev-utils"); 2 | const { waffle } = require('@nomiclabs/buidler'); 3 | const { use, expect } = require('chai'); 4 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 5 | const { bigNumberify, Interface } = require('ethers/utils'); 6 | 7 | const { devConstants } = require('@notestream/dev-utils'); 8 | const moment = require('moment'); 9 | const crypto = require('crypto'); 10 | 11 | const { 12 | // STANDARD_SALARY, 13 | STANDARD_TIME_OFFSET, 14 | STANDARD_TIME_DELTA, 15 | ZERO_ADDRESS, 16 | } = devConstants; 17 | 18 | const NoteStream = require('../../build/NoteStream.json'); 19 | const { noteStreamFixture } = require('../fixtures'); 20 | 21 | use(solidity); 22 | 23 | describe('NoteStream - createStream', function () { 24 | const { provider } = waffle; 25 | const [sender, recipient] = provider.getWallets(); 26 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 27 | 28 | const NoteStreamInterface = new Interface(NoteStream.abi); 29 | 30 | let noteStream; 31 | let zkAsset; 32 | beforeEach(async function () { 33 | ({ noteStream, zkAsset } = await loadFixture(noteStreamFixture)); 34 | }); 35 | 36 | const now = bigNumberify(moment().format('X')); 37 | const startTime = now.add(STANDARD_TIME_OFFSET.toString()); 38 | const stopTime = startTime.add(STANDARD_TIME_DELTA.toString()); 39 | const notehash = crypto.randomBytes(32); 40 | 41 | describe('when not paused', function () { 42 | describe('when the recipient is valid', function () { 43 | it('creates the stream', async function () { 44 | const tx = await noteStream.createStream( 45 | recipient.address, 46 | notehash, 47 | zkAsset.address, 48 | startTime, 49 | stopTime 50 | ); 51 | const receipt = await tx.wait(); 52 | const { streamId } = NoteStreamInterface.parseLog( 53 | receipt.logs[receipt.logs.length - 1] 54 | ).values; 55 | 56 | const streamObject = await noteStream.getStream(streamId); 57 | expect(streamObject.sender).to.equal(sender.address); 58 | expect(streamObject.recipient).to.equal(recipient.address); 59 | expect(streamObject.noteHash).to.equal( 60 | `0x${notehash.toString('hex')}` 61 | ); 62 | 63 | expect(streamObject.tokenAddress).to.equal(zkAsset.address); 64 | expect(streamObject.startTime).to.equal(startTime); 65 | expect(streamObject.lastWithdrawTime).to.equal(startTime); 66 | expect(streamObject.stopTime).to.equal(stopTime); 67 | }); 68 | 69 | it('increases the next stream id', async function () { 70 | const currentStreamId = await noteStream.nextStreamId(); 71 | await noteStream.createStream( 72 | recipient.address, 73 | notehash, 74 | zkAsset.address, 75 | startTime, 76 | stopTime 77 | ); 78 | 79 | const nextStreamId = await noteStream.nextStreamId(); 80 | expect(nextStreamId).to.equal(currentStreamId.add(1)); 81 | }); 82 | 83 | it('emits a createStream event', async function () { 84 | await expect( 85 | noteStream.createStream( 86 | recipient.address, 87 | notehash, 88 | zkAsset.address, 89 | startTime, 90 | stopTime 91 | ) 92 | ).to.emit(noteStream, 'CreateStream'); 93 | }); 94 | 95 | it('reverts when the stream starts in the past', async function () { 96 | const invalidStartTime = now.sub( 97 | STANDARD_TIME_OFFSET.toString() 98 | ); 99 | await expect( 100 | noteStream.createStream( 101 | recipient.address, 102 | notehash, 103 | zkAsset.address, 104 | invalidStartTime, 105 | stopTime 106 | ) 107 | ).to.be.revertedWith('start time before block.timestamp'); 108 | }); 109 | 110 | it('reverts when the stream duration is zero', async function () { 111 | await expect( 112 | noteStream.createStream( 113 | recipient.address, 114 | notehash, 115 | zkAsset.address, 116 | startTime, 117 | startTime 118 | ) 119 | ).to.be.revertedWith('Stream duration not greater than zero'); 120 | }); 121 | 122 | it('reverts when the stream duration is zero', async function () { 123 | const invalidStopTime = startTime.sub( 124 | STANDARD_TIME_DELTA.toString() 125 | ); 126 | await expect( 127 | noteStream.createStream( 128 | recipient.address, 129 | notehash, 130 | zkAsset.address, 131 | startTime, 132 | invalidStopTime 133 | ) 134 | ).to.be.revertedWith('Stream duration not greater than zero'); 135 | }); 136 | }); 137 | 138 | it('reverts when the recipient is the caller itself', async function () { 139 | await expect( 140 | noteStream.createStream( 141 | sender.address, 142 | notehash, 143 | zkAsset.address, 144 | startTime, 145 | stopTime 146 | ) 147 | ).to.be.revertedWith('stream to the caller'); 148 | }); 149 | 150 | it('reverts when the recipient is the NoteStream contract itself', async function () { 151 | await expect( 152 | noteStream.createStream( 153 | noteStream.address, 154 | notehash, 155 | zkAsset.address, 156 | startTime, 157 | stopTime 158 | ) 159 | ).to.be.revertedWith('stream to the contract itself'); 160 | }); 161 | 162 | it('reverts when the recipient is the zero address', async function () { 163 | await expect( 164 | noteStream.createStream( 165 | ZERO_ADDRESS, 166 | notehash, 167 | zkAsset.address, 168 | startTime, 169 | stopTime 170 | ) 171 | ).to.be.revertedWith('stream to the zero address'); 172 | }); 173 | }); 174 | 175 | it('reverts when paused', async function () { 176 | // Note that `sender` coincides with the owner of the contract 177 | 178 | await noteStream.pause(); 179 | await expect( 180 | noteStream.createStream( 181 | recipient.address, 182 | notehash, 183 | zkAsset.address, 184 | startTime, 185 | stopTime 186 | ) 187 | ).to.be.revertedWith('Pausable: paused'); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /packages/contracts/test/NoteStream/getStream.js: -------------------------------------------------------------------------------- 1 | // const { devConstants } = require("@notestream/dev-utils"); 2 | const { waffle } = require('@nomiclabs/buidler'); 3 | const { use, expect } = require('chai'); 4 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 5 | const { bigNumberify } = require('ethers/utils'); 6 | 7 | const { noteStreamFixture } = require('../fixtures'); 8 | 9 | use(solidity); 10 | 11 | describe('NoteStream - getStream', function () { 12 | const { provider } = waffle; 13 | const [sender, recipient] = provider.getWallets(); 14 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 15 | 16 | let noteStream; 17 | beforeEach(async function () { 18 | ({ noteStream } = await loadFixture(noteStreamFixture)); 19 | }); 20 | 21 | it('reverts when the stream does not exist', async function () { 22 | const streamId = bigNumberify(419863); 23 | await expect(noteStream.getStream(streamId)).to.be.revertedWith( 24 | 'stream does not exist' 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/contracts/test/NoteStream/index.js: -------------------------------------------------------------------------------- 1 | require('./constructor.js'); 2 | require('./getStream.js'); 3 | require('./createStream.js'); 4 | require('./withdrawFromStream.js'); 5 | require('./cancelStream.js'); 6 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/getRatio.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use } = require('chai'); 3 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 4 | const { StreamUtilitiesFixture } = require('../fixtures'); 5 | 6 | use(solidity); 7 | 8 | describe('StreamUtilities - getRatio', function () { 9 | const { provider } = waffle; 10 | const [sender, recipient] = provider.getWallets(); 11 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | let streamUtilitiesMock; 15 | beforeEach(async function () { 16 | ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); 17 | }); 18 | 19 | it('returns the correct ratio'); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/index.js: -------------------------------------------------------------------------------- 1 | require('./getRatio.js'); 2 | require('./validateRatioProof.js'); 3 | require('./validateJoinSplitProof.js'); 4 | require('./processWithdrawal.js'); 5 | require('./processCancellation.js'); 6 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/processCancellation.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use } = require('chai'); 3 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 4 | const { StreamUtilitiesFixture } = require('../fixtures'); 5 | 6 | use(solidity); 7 | 8 | describe('StreamUtilities - processCancellation', function () { 9 | const { provider } = waffle; 10 | const [sender, recipient] = provider.getWallets(); 11 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | let streamUtilitiesMock; 15 | beforeEach(async function () { 16 | ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); 17 | }); 18 | 19 | it('reverts if withdraw note is not owned by recipient'); 20 | it('reverts if refund note is not owned by sender'); 21 | it('reverts if recipient does not have view access to the withdraw note'); 22 | it('reverts if sender does not have view access to the refund note'); 23 | it('emits a confidentialTransfer event'); 24 | it('returns true'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/processWithdrawal.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use } = require('chai'); 3 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 4 | const { StreamUtilitiesFixture } = require('../fixtures'); 5 | 6 | use(solidity); 7 | 8 | describe('StreamUtilities - processWithdrawal', function () { 9 | const { provider } = waffle; 10 | const [sender, recipient] = provider.getWallets(); 11 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | let streamUtilitiesMock; 15 | beforeEach(async function () { 16 | ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); 17 | }); 18 | 19 | it('reverts if new streamNote is not owned by NoteStream contract'); 20 | it('reverts if withdraw note is not owned by recipient'); 21 | it('reverts if sender does not have view access to the new streamNote'); 22 | it('reverts if recipient does not have view access to the new streamNote'); 23 | it('emits a confidentialTransfer event'); 24 | it('returns the hash of the first output note'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/validateJoinSplitProof.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use } = require('chai'); 3 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 4 | const { StreamUtilitiesFixture } = require('../fixtures'); 5 | 6 | use(solidity); 7 | 8 | describe('StreamUtilities - validateJoinSplitProof', function () { 9 | const { provider } = waffle; 10 | const [sender, recipient] = provider.getWallets(); 11 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | let streamUtilitiesMock; 15 | beforeEach(async function () { 16 | ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); 17 | }); 18 | 19 | it('reverts if proof has a non-zero public value transfer'); 20 | it('reverts if proof does not have one input note only'); 21 | it('reverts if proof does not have two output notes only'); 22 | it('reverts if proof does not use same withdraw note as dividend proof'); 23 | it('reverts if proof does not use stream note as input'); 24 | it('returns output notes of proof'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/contracts/test/StreamUtilities/validateRatioProof.js: -------------------------------------------------------------------------------- 1 | const { waffle } = require('@nomiclabs/buidler'); 2 | const { use } = require('chai'); 3 | const { solidity, createFixtureLoader } = require('ethereum-waffle'); 4 | const { StreamUtilitiesFixture } = require('../fixtures'); 5 | 6 | use(solidity); 7 | 8 | describe('StreamUtilities - validateRatioProof', function () { 9 | const { provider } = waffle; 10 | const [sender, recipient] = provider.getWallets(); 11 | const loadFixture = createFixtureLoader(provider, [sender, recipient]); 12 | 13 | // eslint-disable-next-line no-unused-vars 14 | let streamUtilitiesMock; 15 | beforeEach(async function () { 16 | ({ streamUtilitiesMock } = await loadFixture(StreamUtilitiesFixture)); 17 | }); 18 | 19 | it('reverts if proof ratio does not match withdrawal duration'); 20 | it('reverts if proof does not use stream note as source'); 21 | it('returns input and output notes of proof'); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/contracts/test/fixtures.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('ethers'); 2 | 3 | const { deployContract } = require('ethereum-waffle'); 4 | 5 | const bn128 = require('@aztec/bn128'); 6 | const { proofs } = require('@aztec/dev-utils'); 7 | 8 | const { JOIN_SPLIT_PROOF, DIVIDEND_PROOF } = proofs; 9 | 10 | const { 11 | ACE, 12 | FactoryBase201907, 13 | JoinSplit, 14 | Dividend, 15 | ZkAsset, 16 | } = require('@aztec/contract-artifacts'); 17 | const ERC20Mintable = require('../build/ERC20Mintable.json'); 18 | const NoteStream = require('../build/NoteStream.json'); 19 | const StreamUtilitiesMock = require('../build/StreamUtilitiesMock.json'); 20 | 21 | const generateFactoryId = (epoch, cryptoSystem, assetType) => { 22 | return epoch * 256 ** 2 + cryptoSystem * 256 ** 1 + assetType * 256 ** 0; 23 | }; 24 | 25 | async function aztecFixture(provider, [wallet]) { 26 | ethers.errors.setLogLevel('error'); 27 | 28 | // console.log('Starting deployment'); 29 | const ace = await deployContract(wallet, ACE, [], { gasLimit: 5000000 }); 30 | await ace.setCommonReferenceString(bn128.CRS); 31 | // console.log('Deployed ACE'); 32 | 33 | const joinSplitValidator = await deployContract(wallet, JoinSplit, []); 34 | await ace.setProof(JOIN_SPLIT_PROOF, joinSplitValidator.address); 35 | // console.log('Deployed JoinSplit'); 36 | 37 | const dividendValidator = await deployContract(wallet, Dividend, []); 38 | await ace.setProof(DIVIDEND_PROOF, dividendValidator.address); 39 | // console.log('Deployed Dividend'); 40 | 41 | const baseFactory = await deployContract(wallet, FactoryBase201907, [ 42 | ace.address, 43 | ]); 44 | await ace.setFactory(generateFactoryId(1, 1, 1), baseFactory.address); 45 | // console.log('Deployed Factory'); 46 | 47 | // ethers.errors.setLogLevel('warn'); 48 | return { ace, joinSplitValidator, dividendValidator, baseFactory }; 49 | } 50 | 51 | async function zkAssetFixture(provider, [wallet]) { 52 | ethers.errors.setLogLevel('error'); 53 | 54 | // deploy ace and initialise 55 | const { 56 | ace, 57 | joinSplitValidator, 58 | dividendValidator, 59 | baseFactory, 60 | } = await aztecFixture(provider, [wallet]); 61 | 62 | // Deploy ERC20 token and linked ZkAsset 63 | const token = await deployContract(wallet, ERC20Mintable, [ 64 | 'TESTCOIN', 65 | 'TEST', 66 | 18, 67 | ]); 68 | const zkAsset = await deployContract(wallet, ZkAsset, [ 69 | ace.address, 70 | token.address, 71 | 1, 72 | ]); 73 | 74 | // ethers.errors.setLogLevel('warn'); 75 | 76 | return { 77 | ace, 78 | joinSplitValidator, 79 | dividendValidator, 80 | baseFactory, 81 | token, 82 | zkAsset, 83 | }; 84 | } 85 | 86 | async function noteStreamFixture(provider, [wallet]) { 87 | ethers.errors.setLogLevel('error'); 88 | 89 | // deploy ace and initialise 90 | const { 91 | ace, 92 | joinSplitValidator, 93 | dividendValidator, 94 | baseFactory, 95 | token, 96 | zkAsset, 97 | } = await zkAssetFixture(provider, [wallet]); 98 | 99 | const noteStream = await deployContract(wallet, NoteStream, [ace.address]); 100 | 101 | return { 102 | ace, 103 | joinSplitValidator, 104 | dividendValidator, 105 | baseFactory, 106 | token, 107 | zkAsset, 108 | noteStream, 109 | }; 110 | } 111 | 112 | async function StreamUtilitiesFixture(provider, [wallet]) { 113 | ethers.errors.setLogLevel('error'); 114 | 115 | // deploy ace and initialise 116 | const { 117 | ace, 118 | joinSplitValidator, 119 | dividendValidator, 120 | baseFactory, 121 | token, 122 | zkAsset, 123 | } = await zkAssetFixture(provider, [wallet]); 124 | 125 | const streamUtilitiesMock = await deployContract( 126 | wallet, 127 | StreamUtilitiesMock 128 | ); 129 | 130 | return { 131 | ace, 132 | joinSplitValidator, 133 | dividendValidator, 134 | baseFactory, 135 | token, 136 | zkAsset, 137 | streamUtilitiesMock, 138 | }; 139 | } 140 | 141 | module.exports = { 142 | aztecFixture, 143 | zkAssetFixture, 144 | noteStreamFixture, 145 | StreamUtilitiesFixture, 146 | }; 147 | -------------------------------------------------------------------------------- /packages/contracts/test/runTests.js: -------------------------------------------------------------------------------- 1 | require('./StreamUtilities/index.js'); 2 | require('./NoteStream/index.js'); 3 | -------------------------------------------------------------------------------- /packages/dev-utils/.eslintignore: -------------------------------------------------------------------------------- 1 | lib/*.js 2 | -------------------------------------------------------------------------------- /packages/dev-utils/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "extends": ["airbnb-base", "plugin:prettier/recommended"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/dev-utils/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /packages/dev-utils/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/dev-utils/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | tabWidth: 2 6 | }; -------------------------------------------------------------------------------- /packages/dev-utils/README.md: -------------------------------------------------------------------------------- 1 | ## Dev Utils 2 | 3 | Dev utils to be shared across NoteStream projects and packages. 4 | 5 | ## Usage 6 | 7 | Install the module: 8 | 9 | ```bash 10 | $ yarn add @notestream/dev-utils 11 | ``` 12 | 13 | And import it in your project: 14 | 15 | ```js 16 | const devUtils = require("@notestream/dev-utils"); 17 | ``` 18 | 19 | ### Install Modules 20 | 21 | ```bash 22 | $ yarn install 23 | ``` 24 | 25 | ### Build 26 | 27 | ```bash 28 | $ yarn build 29 | ``` 30 | 31 | ### Lint 32 | 33 | ```bash 34 | $ yarn lint 35 | ``` 36 | 37 | ### Clean 38 | 39 | ```bash 40 | $ yarn clean 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/dev-utils/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/env'], 3 | plugins: [ 4 | '@babel/plugin-transform-runtime', 5 | '@babel/plugin-proposal-object-rest-spread', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/dev-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/dev-utils", 3 | "description": "Dev utils to be shared across NoteStream packages", 4 | "version": "0.1.0", 5 | "author": { 6 | "name": "Tom French" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/TomAFrench/NoteStream/issues" 10 | }, 11 | "dependencies": { 12 | "bignumber.js": "^9.0.0", 13 | "ether-time-traveler": "^1.0.0", 14 | "ethers": "^4.0.47", 15 | "ganache-time-traveler": "^1.0.14", 16 | "moment": "^2.25.3" 17 | }, 18 | "devDependencies": { 19 | "@babel/cli": "^7.5.5", 20 | "@babel/core": "^7.5.5", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.4.0", 22 | "@babel/plugin-transform-runtime": "^7.6.0", 23 | "@babel/preset-env": "^7.5.5", 24 | "@babel/runtime": "^7.7.6", 25 | "eslint": "^6.1.0", 26 | "eslint-config-airbnb-base": "^14.0.0", 27 | "eslint-config-prettier": "^6.7.0", 28 | "eslint-plugin-import": "^2.18.2", 29 | "lint-staged": "^10.2.2", 30 | "mocha": "^6.2.0", 31 | "shx": "^0.3.2" 32 | }, 33 | "files": [ 34 | "/lib" 35 | ], 36 | "homepage": "https://github.com/TomAFrench/NoteStream/tree/develop/packages/dev-utils#readme", 37 | "license": "LGPL-3.0", 38 | "main": "./lib", 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/TomAFrench/NoteStream.git", 45 | "directory": "packages/dev-utils" 46 | }, 47 | "scripts": { 48 | "precommit": "lint-staged", 49 | "commit": "git-cz", 50 | "build": "yarn clean && babel --copy-files --out-dir ./lib --root-mode upward ./src", 51 | "clean": "shx rm -rf ./lib", 52 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh dev-utils", 53 | "lint": "eslint --ignore-path ../../.eslintignore .", 54 | "watch": "yarn build --watch" 55 | }, 56 | "config": { 57 | "commitizen": { 58 | "path": "./node_modules/cz-conventional-changelog" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/dev-utils/src/chaiPlugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, no-else-return, no-param-reassign */ 2 | const BigNumber = require('bignumber.js'); 3 | 4 | const devConstants = require('./constants'); 5 | 6 | module.exports = (chai) => { 7 | // See https://twitter.com/nicksdjohnson/status/1132394932361023488 8 | const convert = (value) => { 9 | let number; 10 | 11 | if (typeof value === 'string' || typeof value === 'number') { 12 | number = new BigNumber(value); 13 | } else if (BigNumber.isBigNumber(value)) { 14 | number = value; 15 | } else { 16 | new chai.Assertion(value).assert( 17 | false, 18 | `expected ${value} to be an instance of string, number or BigNumber`, 19 | ); 20 | } 21 | 22 | return number; 23 | }; 24 | 25 | /** 26 | * Performs a boundary check instead of an equality check. 27 | * In real life circumstances,it can take up to 14 seconds for a block 28 | * to be broadcast on the Ethereum network, so we have to account for this. 29 | * 30 | * Note that we make two assumptions: 31 | * 32 | * 1. The payment rate is 1 token/ second, which is true for all tests in this repo. 33 | * 2. By default, the token has 18 decimals 34 | */ 35 | chai.Assertion.addMethod('tolerateTheBlockTimeVariation', function ( 36 | expected, 37 | scale = devConstants.STANDARD_SCALE, 38 | tolerateByAddition = true, 39 | ) { 40 | // eslint-disable-next-line no-underscore-dangle 41 | const actual = convert(this._obj); 42 | expected = convert(expected); 43 | scale = convert(scale); 44 | 45 | const blockTimeAverage = new BigNumber(14).multipliedBy(scale); 46 | if (tolerateByAddition) { 47 | const expectedCeiling = expected.plus(blockTimeAverage); 48 | 49 | return this.assert( 50 | actual.isGreaterThanOrEqualTo(expected) && 51 | actual.isLessThanOrEqualTo(expectedCeiling), 52 | `expected ${actual.toString()} to be >= than ${expected.toString()} and <= ${expectedCeiling.toString()}`, 53 | ); 54 | } else { 55 | const expectedFloor = expected.minus(blockTimeAverage); 56 | 57 | return this.assert( 58 | actual.isLessThanOrEqualTo(expected) && 59 | actual.isGreaterThanOrEqualTo(expectedFloor), 60 | `expected ${actual.toString()} to be <= than ${expected.toString()} and >= ${expectedFloor.toString()}`, 61 | ); 62 | } 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/dev-utils/src/constants.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | const STANDARD_SALARY = new BigNumber(3600).multipliedBy(1e18); 4 | 5 | module.exports = { 6 | FIVE_UNITS: new BigNumber(5).multipliedBy(1e18), 7 | GAS_LIMIT: 6721975, 8 | INITIAL_SUPPLY: STANDARD_SALARY.multipliedBy(1000), 9 | ONE_UNIT: new BigNumber(1).multipliedBy(1e18), 10 | RPC_URL: 'http://127.0.0.1:8545', 11 | RPC_PORT: 8545, 12 | STANDARD_SALARY, 13 | STANDARD_SCALE: new BigNumber(1e18), 14 | STANDARD_TIME_DELTA: new BigNumber(3600), 15 | STANDARD_TIME_OFFSET: new BigNumber(300), 16 | ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/dev-utils/src/errors.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AUTH_BOTH: 3 | 'only the sender or the recipient of the stream can perform this action', 4 | AUTH_RECIPIENT: 'only the stream recipient is allowed to perform this action', 5 | AUTH_SENDER: 'only the stream sender is allowed to perform this action', 6 | BLOCK_DELTA: 7 | 'the block difference needs to be higher than the payment interval', 8 | BLOCK_DELTA_MULTIPLICITY: 9 | 'the block difference needs to be a multiple of the payment interval', 10 | BLOCK_START: 11 | 'the start block needs to be higher than the current block number', 12 | BLOCK_STOP: 'the stop block needs to be higher than the start block', 13 | CONTRACT_ALLOWANCE: 'contract not allowed to transfer enough tokens', 14 | CONTRACT_EXISTENCE: 'token contract address needs to be provided', 15 | INSOLVENCY: 'not enough funds', 16 | STREAM_EXISTENCE: "stream doesn't exist", 17 | TERMS_NOT_CHANGED: 'stream has these terms already', 18 | }; 19 | -------------------------------------------------------------------------------- /packages/dev-utils/src/index.js: -------------------------------------------------------------------------------- 1 | const chaiPlugin = require('./chaiPlugin'); 2 | const devConstants = require('./constants'); 3 | const errors = require('./errors'); 4 | const mochaContexts = require('./mochaContexts'); 5 | 6 | module.exports = { 7 | chaiPlugin, 8 | devConstants, 9 | errors, 10 | mochaContexts, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/dev-utils/src/mochaContexts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | const { bigNumberify } = require('ethers/utils'); 3 | const moment = require('moment'); 4 | const traveler = require('ether-time-traveler'); 5 | 6 | const devConstants = require('./constants'); 7 | 8 | const { STANDARD_TIME_OFFSET, STANDARD_TIME_DELTA } = devConstants; 9 | 10 | function contextForSpecificTime( 11 | contextText, 12 | timeDuration, 13 | provider, 14 | functions, 15 | ) { 16 | const now = bigNumberify(moment().format('X')); 17 | let snapshot; 18 | 19 | describe(contextText, function () { 20 | beforeEach(async function () { 21 | snapshot = await traveler.takeSnapshot(provider); 22 | await traveler.advanceBlockAndSetTime( 23 | provider, 24 | now.add(timeDuration.toString()).toNumber(), 25 | ); 26 | }); 27 | 28 | functions(); 29 | 30 | afterEach(async function () { 31 | await traveler.revertToSnapshot(provider, snapshot); 32 | }); 33 | }); 34 | } 35 | 36 | function contextForStreamDidStartButNotEnd(provider, functions) { 37 | const timeDuration = STANDARD_TIME_OFFSET.plus(5); 38 | contextForSpecificTime( 39 | 'when the stream did start but not end', 40 | timeDuration, 41 | provider, 42 | functions, 43 | ); 44 | } 45 | 46 | function contextForStreamDidEnd(provider, functions) { 47 | const timeDuration = STANDARD_TIME_OFFSET.plus(STANDARD_TIME_DELTA).plus(5); 48 | contextForSpecificTime( 49 | 'when the stream did end', 50 | timeDuration, 51 | provider, 52 | functions, 53 | ); 54 | } 55 | 56 | module.exports = { 57 | contextForStreamDidStartButNotEnd, 58 | contextForStreamDidEnd, 59 | }; 60 | -------------------------------------------------------------------------------- /packages/monorepo-scripts/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/monorepo-scripts/ci/hasChanged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "$CIRCLE_TOKEN" ]]; then 4 | exit 0 5 | fi 6 | 7 | LAST_SUCCESSFUL_BUILD_URL="https://circleci.com/api/v1.1/project/github/TomAFrench/NoteStream/tree/dev?filter=successful&limit=1" 8 | LAST_SUCCESSFUL_COMMIT=`curl -Ss -u "$CIRCLE_TOKEN:" $LAST_SUCCESSFUL_BUILD_URL | jq -r '.[0]["vcs_revision"]'` 9 | CHANGED_MODULES=$(echo $(git diff --name-only $LAST_SUCCESSFUL_COMMIT | grep ^packages\/.*\/ || true) | tr " " "\n" | cut -d\/ -f2 | uniq | paste -sd\| -) 10 | 11 | if [[ $1 =~ $CHANGED_MODULES ]]; then 12 | exit 0 13 | else 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /packages/monorepo-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/monorepo-scripts", 3 | "version": "0.1.0", 4 | "description": "NoteStream monorepo scripts", 5 | "private": true, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/TomAFrench/NoteStream.git" 9 | }, 10 | "author": "Tom French", 11 | "license": "ISC", 12 | "bugs": { 13 | "url": "https://github.com/TomAFrench/NoteStream/issues" 14 | }, 15 | "homepage": "https://github.com/TomAFrench/NoteStream#readme", 16 | "scripts": { 17 | "precommit": "lint-staged", 18 | "commit": "git-cz", 19 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh monorepo-scripts", 20 | "lint": "eslint --ignore-path ../../.eslintignore ./" 21 | }, 22 | "config": { 23 | "commitizen": { 24 | "path": "./node_modules/cz-conventional-changelog" 25 | } 26 | }, 27 | "devDependencies": { 28 | "lint-staged": "^10.2.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-app/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_SUBGRAPH_URL='https://api.thegraph.com/subgraphs/name/tomafrench/notestream-rinkeby' 2 | REACT_APP_AZTEC_API_KEY = '9HRKN7S-JSZMRJM-KWSDWSY-B2VSRD9' 3 | REACT_APP_TRANSAK_API_KEY = '7acc4227-1611-4787-8349-8b32194b6dc1' 4 | REACT_APP_NETWORK_ID=4 -------------------------------------------------------------------------------- /packages/react-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | browser: true 5 | }, 6 | extends: [ 7 | '../../.eslintrc.js', 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 12 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 13 | ], 14 | ignorePatterns: [], 15 | parser: "@typescript-eslint/parser", 16 | parserOptions: { 17 | project: "./tsconfig.json", 18 | tsconfigRootDir: __dirname, 19 | sourceType: "module", 20 | ecmaFeatures: { 21 | jsx: true 22 | } 23 | }, 24 | plugins: [ 25 | "@typescript-eslint", 26 | // "@typescript-eslint/tslint" 27 | ], 28 | rules: { 29 | "spaced-comment": ["error", "always", { "markers": ["/"] }], 30 | "import/extensions": [ 31 | "error", 32 | "ignorePackages", 33 | { 34 | "js": "never", 35 | "jsx": "never", 36 | "ts": "never", 37 | "tsx": "never" 38 | } 39 | ], 40 | "react/jsx-uses-react": "error", 41 | "react/jsx-uses-vars": "error", 42 | "no-console": "off" 43 | }, 44 | settings: { 45 | react: { 46 | version: "detect" 47 | }, 48 | 'import/resolver': { 49 | node: { 50 | paths: ["src", "node_modules"], 51 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 52 | }, 53 | }, 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /packages/react-app/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint --fix" 4 | ], 5 | "*.tsx": [ 6 | "eslint --fix" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/react-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | tabWidth: 2 6 | }; -------------------------------------------------------------------------------- /packages/react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. -------------------------------------------------------------------------------- /packages/react-app/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "build": { 4 | "env": { 5 | "REACT_APP_SUBGRAPH_URL": "https://api.thegraph.com/subgraphs/name/tomafrench/notestream-rinkeby", 6 | "REACT_APP_NETWORK_ID": "4" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /packages/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/react-app", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/TomAFrench/NoteStream.git" 7 | }, 8 | "author": "Tom French", 9 | "license": "ISC", 10 | "bugs": { 11 | "url": "https://github.com/TomAFrench/NoteStream/issues" 12 | }, 13 | "dependencies": { 14 | "@apollo/client": "^3.0.0-beta.41", 15 | "@aztec/secp256k1": "^1.2.0", 16 | "@material-ui/core": "^4.9.5", 17 | "@material-ui/icons": "^4.9.1", 18 | "@notestream/contract-artifacts": "^1.0.1", 19 | "@transak/transak-sdk": "^1.0.17", 20 | "@types/testing-library__dom": "^7.5.0", 21 | "aztec.js": "^0.17.0", 22 | "bn.js": "^5.1.1", 23 | "bnc-onboard": "^1.9.0", 24 | "ethers": "^4.0.46", 25 | "graphql": "^14.6.0", 26 | "moment": "^2.24.0", 27 | "prop-types": "^15.7.2", 28 | "react": "^16.13.0", 29 | "react-blockies": "^1.4.1", 30 | "react-copy-to-clipboard": "^5.0.2", 31 | "react-dom": "^16.13.0", 32 | "react-router": "^5.1.2", 33 | "react-router-dom": "^5.2.0", 34 | "react-scripts": "^3.4.1", 35 | "zkasset-metadata": "^0.2.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.8.4", 39 | "@babel/core": "^7.8.4", 40 | "@babel/node": "^7.8.4", 41 | "@babel/preset-env": "^7.8.4", 42 | "@babel/preset-typescript": "^7.9.0", 43 | "@testing-library/jest-dom": "^4.2.4", 44 | "@testing-library/react": "^9.3.2", 45 | "@testing-library/user-event": "^7.1.2", 46 | "@types/bn.js": "^4.11.6", 47 | "@types/react-copy-to-clipboard": "^4.3.0", 48 | "@types/react-router-dom": "^5.1.5", 49 | "@typescript-eslint/eslint-plugin-tslint": "^2.27.0", 50 | "eslint": "^6.8.0", 51 | "eslint-config-airbnb-base": "^14.1.0", 52 | "eslint-config-prettier": "^6.10.1", 53 | "eslint-plugin-import": "^2.20.2", 54 | "eslint-plugin-prettier": "^3.1.2", 55 | "eslint-plugin-react": "^7.19.0", 56 | "lint-staged": "^10.2.2", 57 | "prettier": "^2.0.4", 58 | "shx": "^0.3.2", 59 | "typescript": "^3.8.3" 60 | }, 61 | "scripts": { 62 | "precommit": "lint-staged", 63 | "commit": "git-cz", 64 | "prestart": "yarn clean && yarn lint --fix", 65 | "start": "react-scripts start", 66 | "build": "react-scripts build", 67 | "eject": "react-scripts eject", 68 | "clean": "shx rm -rf ./build", 69 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh react-app", 70 | "lint": "eslint --config .eslintrc.js ./src --ext .js,.jsx,.ts,.tsx" 71 | }, 72 | "eslintConfig": { 73 | "extends": "react-app" 74 | }, 75 | "browserslist": { 76 | "production": [ 77 | ">0.2%", 78 | "not dead", 79 | "not op_mini all" 80 | ], 81 | "development": [ 82 | "last 1 chrome version", 83 | "last 1 firefox version", 84 | "last 1 safari version" 85 | ] 86 | }, 87 | "config": { 88 | "commitizen": { 89 | "path": "./node_modules/cz-conventional-changelog" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomAFrench/NoteStream/ab1e24dad291a19283938d4e9d560e95c9173303/packages/react-app/public/favicon.ico -------------------------------------------------------------------------------- /packages/react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 28 | NoteStream 29 | 30 | 31 | 32 | 33 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /packages/react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomAFrench/NoteStream/ab1e24dad291a19283938d4e9d560e95c9173303/packages/react-app/public/logo192.png -------------------------------------------------------------------------------- /packages/react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomAFrench/NoteStream/ab1e24dad291a19283938d4e9d560e95c9173303/packages/react-app/public/logo512.png -------------------------------------------------------------------------------- /packages/react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | // Redirect, 5 | Route, 6 | Switch, 7 | } from 'react-router-dom'; 8 | import CssBaseline from '@material-ui/core/CssBaseline'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | import { 12 | getContractAddressesForNetwork, 13 | abis, 14 | } from '@notestream/contract-artifacts'; 15 | import { Contract } from 'ethers'; 16 | import { Web3Provider } from 'ethers/providers'; 17 | 18 | import { useWalletProvider, useNetwork } from './contexts/OnboardContext'; 19 | 20 | import HomePage from './pages/HomePage'; 21 | import ExchangePage from './pages/ExchangePage'; 22 | import SendPage from './pages/SendPage'; 23 | import ReceivePage from './pages/ReceivePage'; 24 | 25 | import SideBar from './components/Sidebar'; 26 | 27 | const useStyles = makeStyles((theme) => ({ 28 | root: { 29 | display: 'flex', 30 | }, 31 | content: { 32 | flexGrow: 1, 33 | padding: theme.spacing(3), 34 | transition: theme.transitions.create('margin', { 35 | easing: theme.transitions.easing.sharp, 36 | duration: theme.transitions.duration.leavingScreen, 37 | }), 38 | }, 39 | contentShift: { 40 | transition: theme.transitions.create('margin', { 41 | easing: theme.transitions.easing.easeOut, 42 | duration: theme.transitions.duration.enteringScreen, 43 | }), 44 | }, 45 | paper: { 46 | padding: theme.spacing(2), 47 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 48 | padding: theme.spacing(3), 49 | }, 50 | }, 51 | pageElement: { 52 | marginTop: theme.spacing(1.5), 53 | marginBottom: theme.spacing(1.5), 54 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 55 | marginTop: theme.spacing(3), 56 | marginBottom: theme.spacing(3), 57 | }, 58 | }, 59 | icon: { 60 | marginRight: theme.spacing(2), 61 | }, 62 | title: { 63 | flexGrow: 1, 64 | }, 65 | })); 66 | 67 | const App = (): ReactElement => { 68 | const classes = useStyles(); 69 | const provider = useWalletProvider(); 70 | const { appNetworkId } = useNetwork(); 71 | const [open, setOpen] = useState(true); 72 | const [streamContract, setStreamContract] = useState(); 73 | 74 | useEffect(() => { 75 | if (appNetworkId && provider) { 76 | const { NoteStream } = getContractAddressesForNetwork(appNetworkId); 77 | const signer = new Web3Provider(provider).getSigner(); 78 | const noteStreamContract = new Contract( 79 | NoteStream, 80 | abis.NoteStream, 81 | signer, 82 | ); 83 | setStreamContract(noteStreamContract); 84 | } 85 | }, [provider, appNetworkId]); 86 | 87 | return ( 88 |
89 | 90 | 91 | 92 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |
111 |
112 | ); 113 | }; 114 | 115 | export default App; 116 | -------------------------------------------------------------------------------- /packages/react-app/src/abis/ERC20Detailed.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | contractName: 'ERC20Detailed', 3 | abi: [ 4 | { 5 | inputs: [ 6 | { 7 | internalType: 'string', 8 | name: 'name', 9 | type: 'string', 10 | }, 11 | { 12 | internalType: 'string', 13 | name: 'symbol', 14 | type: 'string', 15 | }, 16 | { 17 | internalType: 'uint8', 18 | name: 'decimals', 19 | type: 'uint8', 20 | }, 21 | ], 22 | payable: false, 23 | stateMutability: 'nonpayable', 24 | type: 'constructor', 25 | }, 26 | { 27 | anonymous: false, 28 | inputs: [ 29 | { 30 | indexed: true, 31 | internalType: 'address', 32 | name: 'owner', 33 | type: 'address', 34 | }, 35 | { 36 | indexed: true, 37 | internalType: 'address', 38 | name: 'spender', 39 | type: 'address', 40 | }, 41 | { 42 | indexed: false, 43 | internalType: 'uint256', 44 | name: 'value', 45 | type: 'uint256', 46 | }, 47 | ], 48 | name: 'Approval', 49 | type: 'event', 50 | }, 51 | { 52 | anonymous: false, 53 | inputs: [ 54 | { 55 | indexed: true, 56 | internalType: 'address', 57 | name: 'from', 58 | type: 'address', 59 | }, 60 | { 61 | indexed: true, 62 | internalType: 'address', 63 | name: 'to', 64 | type: 'address', 65 | }, 66 | { 67 | indexed: false, 68 | internalType: 'uint256', 69 | name: 'value', 70 | type: 'uint256', 71 | }, 72 | ], 73 | name: 'Transfer', 74 | type: 'event', 75 | }, 76 | { 77 | constant: true, 78 | inputs: [ 79 | { 80 | internalType: 'address', 81 | name: 'owner', 82 | type: 'address', 83 | }, 84 | { 85 | internalType: 'address', 86 | name: 'spender', 87 | type: 'address', 88 | }, 89 | ], 90 | name: 'allowance', 91 | outputs: [ 92 | { 93 | internalType: 'uint256', 94 | name: '', 95 | type: 'uint256', 96 | }, 97 | ], 98 | payable: false, 99 | stateMutability: 'view', 100 | type: 'function', 101 | }, 102 | { 103 | constant: false, 104 | inputs: [ 105 | { 106 | internalType: 'address', 107 | name: 'spender', 108 | type: 'address', 109 | }, 110 | { 111 | internalType: 'uint256', 112 | name: 'amount', 113 | type: 'uint256', 114 | }, 115 | ], 116 | name: 'approve', 117 | outputs: [ 118 | { 119 | internalType: 'bool', 120 | name: '', 121 | type: 'bool', 122 | }, 123 | ], 124 | payable: false, 125 | stateMutability: 'nonpayable', 126 | type: 'function', 127 | }, 128 | { 129 | constant: true, 130 | inputs: [ 131 | { 132 | internalType: 'address', 133 | name: 'account', 134 | type: 'address', 135 | }, 136 | ], 137 | name: 'balanceOf', 138 | outputs: [ 139 | { 140 | internalType: 'uint256', 141 | name: '', 142 | type: 'uint256', 143 | }, 144 | ], 145 | payable: false, 146 | stateMutability: 'view', 147 | type: 'function', 148 | }, 149 | { 150 | constant: true, 151 | inputs: [], 152 | name: 'decimals', 153 | outputs: [ 154 | { 155 | internalType: 'uint8', 156 | name: '', 157 | type: 'uint8', 158 | }, 159 | ], 160 | payable: false, 161 | stateMutability: 'view', 162 | type: 'function', 163 | }, 164 | { 165 | constant: true, 166 | inputs: [], 167 | name: 'name', 168 | outputs: [ 169 | { 170 | internalType: 'string', 171 | name: '', 172 | type: 'string', 173 | }, 174 | ], 175 | payable: false, 176 | stateMutability: 'view', 177 | type: 'function', 178 | }, 179 | { 180 | constant: true, 181 | inputs: [], 182 | name: 'symbol', 183 | outputs: [ 184 | { 185 | internalType: 'string', 186 | name: '', 187 | type: 'string', 188 | }, 189 | ], 190 | payable: false, 191 | stateMutability: 'view', 192 | type: 'function', 193 | }, 194 | { 195 | constant: true, 196 | inputs: [], 197 | name: 'totalSupply', 198 | outputs: [ 199 | { 200 | internalType: 'uint256', 201 | name: '', 202 | type: 'uint256', 203 | }, 204 | ], 205 | payable: false, 206 | stateMutability: 'view', 207 | type: 'function', 208 | }, 209 | { 210 | constant: false, 211 | inputs: [ 212 | { 213 | internalType: 'address', 214 | name: 'recipient', 215 | type: 'address', 216 | }, 217 | { 218 | internalType: 'uint256', 219 | name: 'amount', 220 | type: 'uint256', 221 | }, 222 | ], 223 | name: 'transfer', 224 | outputs: [ 225 | { 226 | internalType: 'bool', 227 | name: '', 228 | type: 'bool', 229 | }, 230 | ], 231 | payable: false, 232 | stateMutability: 'nonpayable', 233 | type: 'function', 234 | }, 235 | { 236 | constant: false, 237 | inputs: [ 238 | { 239 | internalType: 'address', 240 | name: 'sender', 241 | type: 'address', 242 | }, 243 | { 244 | internalType: 'address', 245 | name: 'recipient', 246 | type: 'address', 247 | }, 248 | { 249 | internalType: 'uint256', 250 | name: 'amount', 251 | type: 'uint256', 252 | }, 253 | ], 254 | name: 'transferFrom', 255 | outputs: [ 256 | { 257 | internalType: 'bool', 258 | name: '', 259 | type: 'bool', 260 | }, 261 | ], 262 | payable: false, 263 | stateMutability: 'nonpayable', 264 | type: 'function', 265 | }, 266 | ], 267 | bytecode: '0x', 268 | deployedBytecode: '0x', 269 | linkReferences: {}, 270 | deployedLinkReferences: {}, 271 | }; 272 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import isExternal from '../utils/links'; 5 | 6 | const flexLink = (props: any): ReactElement => { 7 | return isExternal(props.to) ? ( 8 | 9 | {props.children} 10 | 11 | ) : ( 12 | {props.children} 13 | ); 14 | }; 15 | 16 | export default flexLink; 17 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Sidebar/SideBarLinks.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect, useState } from 'react'; 2 | import { useTheme } from '@material-ui/core/styles'; 3 | import List from '@material-ui/core/List'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import Divider from '@material-ui/core/Divider'; 8 | 9 | import HomeIcon from '@material-ui/icons/Home'; 10 | import LocalAtmIcon from '@material-ui/icons/LocalAtm'; 11 | import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet'; 12 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; 13 | import SwapHorizIcon from '@material-ui/icons/SwapHoriz'; 14 | import HelpIcon from '@material-ui/icons/Help'; 15 | import GitHubIcon from '@material-ui/icons/GitHub'; 16 | 17 | import TransakSDK from '@transak/transak-sdk'; 18 | import Link from '../Link'; 19 | 20 | import setupTransak from '../../utils/transak'; 21 | import { useAddress } from '../../contexts/OnboardContext'; 22 | 23 | const mainLinks = [ 24 | { text: 'Home', icon: , url: '/' }, 25 | { text: 'Convert assets', icon: , url: '/exchange' }, 26 | { text: 'Create new stream', icon: , url: '/send' }, 27 | { 28 | text: 'Collect earnings', 29 | icon: , 30 | url: '/receive', 31 | }, 32 | ]; 33 | 34 | const secondaryLinks = [ 35 | { text: 'FAQ', icon: , url: 'https://docs.note.stream' }, 36 | { 37 | text: 'Github', 38 | icon: , 39 | url: 'https://github.com/TomAFrench/NoteStream', 40 | }, 41 | ]; 42 | 43 | const NavLinkItem = ({ 44 | text, 45 | url, 46 | icon, 47 | }: { 48 | text: string; 49 | url: string; 50 | icon: ReactElement; 51 | }): ReactElement => ( 52 | 53 | {icon} 54 | 55 | 56 | ); 57 | 58 | const SideBarLinks = (): ReactElement => { 59 | const theme = useTheme(); 60 | const userAddress = useAddress(); 61 | const [transak, setTransak] = useState(); 62 | 63 | useEffect(() => { 64 | setTransak(setupTransak(userAddress, theme.palette.primary.main)); 65 | }, [userAddress, theme.palette.primary.main]); 66 | 67 | return ( 68 | 69 | {mainLinks.map((link, index) => ( 70 | 76 | ))} 77 | 78 | => transak.init()}> 79 | 80 | 81 | 82 | 83 | 84 | {secondaryLinks.map((link, index) => ( 85 | 91 | ))} 92 | 93 | ); 94 | }; 95 | 96 | export default SideBarLinks; 97 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Sidebar/WalletButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import ListItem from '@material-ui/core/ListItem'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import ListItemText from '@material-ui/core/ListItemText'; 6 | import Avatar from '@material-ui/core/Avatar'; 7 | import Blockies from 'react-blockies'; 8 | 9 | import { useAddress, useSetup } from '../../contexts/OnboardContext'; 10 | import useENSName from '../../hooks/useENSName'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | avatar: { 14 | marginLeft: theme.spacing(-1), 15 | }, 16 | })); 17 | 18 | const trimAddress = (address: string): string => 19 | `${address.slice(0, 6)}...${address.slice(-5, -1)}`; 20 | 21 | const WalletButton = (): ReactElement => { 22 | const classes = useStyles(); 23 | const setup = useSetup(); 24 | const userAddress = useAddress(); 25 | const ensName = useENSName(userAddress); 26 | 27 | if (!userAddress) { 28 | return ( 29 | setup()}> 30 | {/* */} 31 | 32 | 33 | ); 34 | } 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default WalletButton; 48 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Drawer from '@material-ui/core/Drawer'; 4 | import Divider from '@material-ui/core/Divider'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 8 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 9 | 10 | import { Typography } from '@material-ui/core'; 11 | import SideBarLinks from './SideBarLinks'; 12 | import WalletButton from './WalletButton'; 13 | 14 | const drawerWidth = 240; 15 | 16 | const useStyles = makeStyles((theme) => ({ 17 | drawer: { 18 | width: drawerWidth, 19 | flexShrink: 0, 20 | whiteSpace: 'nowrap', 21 | }, 22 | drawerOpen: { 23 | width: drawerWidth, 24 | transition: theme.transitions.create('width', { 25 | easing: theme.transitions.easing.sharp, 26 | duration: theme.transitions.duration.enteringScreen, 27 | }), 28 | }, 29 | drawerClose: { 30 | transition: theme.transitions.create('width', { 31 | easing: theme.transitions.easing.sharp, 32 | duration: theme.transitions.duration.leavingScreen, 33 | }), 34 | width: theme.spacing(7), 35 | [theme.breakpoints.up('sm')]: { 36 | width: theme.spacing(8), 37 | }, 38 | }, 39 | paper: { overflowX: 'hidden' }, 40 | toolbar: { 41 | padding: theme.spacing(0, 1), 42 | // necessary for content to be below app bar 43 | ...theme.mixins.toolbar, 44 | }, 45 | })); 46 | 47 | const SideBar = ({ 48 | open, 49 | setOpen, 50 | }: { 51 | open: boolean; 52 | setOpen: Function; 53 | }): ReactElement => { 54 | const classes = useStyles(); 55 | 56 | const toggleDraw = (): void => { 57 | setOpen(!open); 58 | }; 59 | 60 | return ( 61 | 72 | 80 | 81 | NoteStream 82 | 83 | 84 | 85 | {open ? : } 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default SideBar; 98 | -------------------------------------------------------------------------------- /packages/react-app/src/components/StreamTable/StreamRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Button from '@material-ui/core/Button'; 5 | import TableCell from '@material-ui/core/TableCell'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | 8 | import moment from 'moment'; 9 | 10 | import { Contract } from 'ethers'; 11 | 12 | import { withdrawFunds, cancelStream } from '../../utils/stream'; 13 | import { Stream, ZkNote } from '../../types/types'; 14 | 15 | import { useAztec } from '../../contexts/AztecContext'; 16 | import { useAddress } from '../../contexts/OnboardContext'; 17 | import { convertToTokenValueDisplay } from '../../utils/units/convertToTokenValue'; 18 | import DoubleProgressBar from '../display/DoubleProgressBar'; 19 | import useENSName from '../../hooks/useENSName'; 20 | import useDecodedNote from '../../hooks/useDecodedNote'; 21 | 22 | const StreamRow = ({ 23 | stream, 24 | streamContract, 25 | role, 26 | }: { 27 | stream: Stream; 28 | streamContract: Contract; 29 | role: string; 30 | }): ReactElement | null => { 31 | const userAddress = useAddress(); 32 | const aztec = useAztec(); 33 | const { 34 | sender, 35 | recipient, 36 | id, 37 | startTime, 38 | lastWithdrawTime, 39 | stopTime, 40 | noteHash, 41 | zkAsset, 42 | } = stream; 43 | const note: ZkNote | undefined = useDecodedNote(noteHash); 44 | const displayName = useENSName(role === 'recipient' ? sender : recipient); 45 | 46 | const displayValue = 47 | note?.value && 48 | convertToTokenValueDisplay( 49 | note.value, 50 | zkAsset.scalingFactor, 51 | zkAsset.linkedToken.decimals, 52 | ); 53 | 54 | const button = 55 | role === 'recipient' ? ( 56 | 63 | ) : ( 64 | 73 | ); 74 | 75 | if (!displayName || note?.value === undefined || !zkAsset.symbol) { 76 | return null; 77 | } 78 | return ( 79 | 80 | 81 | {displayName} 82 | 83 | {`${displayValue} ${zkAsset.symbol}`} 84 | 85 | 90 | 91 | 92 | {moment.unix(startTime).format('MMM D, YYYY - HH:mm')} 93 | 94 | 95 | {moment.unix(stopTime).format('MMM D, YYYY - HH:mm')} 96 | 97 | {button} 98 | 99 | ); 100 | }; 101 | 102 | StreamRow.propTypes = { 103 | streamContract: PropTypes.instanceOf(Contract), 104 | stream: PropTypes.object.isRequired, 105 | role: PropTypes.string.isRequired, 106 | }; 107 | 108 | export default StreamRow; 109 | -------------------------------------------------------------------------------- /packages/react-app/src/components/StreamTable/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useCallback } from 'react'; 2 | import moment from 'moment'; 3 | 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableRow from '@material-ui/core/TableRow'; 6 | 7 | import { useQuery } from '@apollo/client'; 8 | import { Contract } from 'ethers'; 9 | import { Button, Grid, CircularProgress } from '@material-ui/core'; 10 | import { generateColumns, STREAM_TABLE_ID, Column } from '../Table/columns'; 11 | import { cellWidth } from '../Table/TableHead'; 12 | import Table from '../Table'; 13 | 14 | import { 15 | GET_SENDER_STREAMS, 16 | GET_RECIPIENT_STREAMS, 17 | } from '../../graphql/streams'; 18 | import { useAddress } from '../../contexts/OnboardContext'; 19 | import { Stream, Hash, ZkNote } from '../../types/types'; 20 | import { cancelStream } from '../../utils/stream'; 21 | import { convertToTokenValueDisplay } from '../../utils/units/convertToTokenValue'; 22 | 23 | import { useAztec } from '../../contexts/AztecContext'; 24 | import useDecodedNote from '../../hooks/useDecodedNote'; 25 | import useENSName from '../../hooks/useENSName'; 26 | import DoubleProgressBar from '../display/DoubleProgressBar'; 27 | 28 | type HumanReadableStream = { 29 | id: number; 30 | recipient: string; 31 | sender: string; 32 | noteHash: Hash; 33 | humanDeposit: string; 34 | humanStartTime: string; 35 | humanStopTime: string; 36 | 37 | zkAsset: any; 38 | }; 39 | type TableRowData = HumanReadableStream & { 40 | humanStartTimeOrder: number; 41 | humanStopTimeOrder: number; 42 | humanLastWithdrawTimeOrder: number; 43 | cancelStream: Function; 44 | }; 45 | 46 | const humanReadableStream = (stream: Stream): HumanReadableStream => { 47 | const { 48 | id, 49 | recipient, 50 | noteHash, 51 | sender, 52 | startTime, 53 | stopTime, 54 | zkAsset, 55 | } = stream; 56 | const humanStartTime: string = moment 57 | .unix(startTime) 58 | .format('MMM D, YYYY - HH:mm'); 59 | const humanStopTime: string = moment 60 | .unix(stopTime) 61 | .format('MMM D, YYYY - HH:mm'); 62 | const humanDeposit = ''; 63 | 64 | return { 65 | id, 66 | recipient, 67 | sender, 68 | noteHash, 69 | humanDeposit, 70 | humanStartTime, 71 | humanStopTime, 72 | zkAsset, 73 | }; 74 | }; 75 | 76 | function NewStreamTable({ 77 | role, 78 | streamContract, 79 | }: { 80 | role: 'sender' | 'recipient'; 81 | streamContract: Contract; 82 | }): ReactElement { 83 | const userAddress = useAddress(); 84 | const aztec = useAztec(); 85 | const { loading, error, data } = useQuery( 86 | role === 'sender' ? GET_SENDER_STREAMS : GET_RECIPIENT_STREAMS, 87 | { 88 | variables: { address: userAddress || '' }, 89 | fetchPolicy: 'network-only', 90 | }, 91 | ); 92 | 93 | const cancelSelectedStream = useCallback( 94 | (id) => cancelStream(aztec, streamContract, id, userAddress), 95 | [aztec, streamContract, userAddress], 96 | ); 97 | 98 | if (loading || error) { 99 | return ( 100 | 107 | 108 | 109 | ); 110 | } 111 | 112 | const columns = generateColumns(); 113 | 114 | const streamInProgress = data.streams.filter( 115 | (stream: Stream) => stream.cancellation == null, 116 | ); 117 | console.log(streamInProgress); 118 | 119 | const tableContents: TableRowData[] = streamInProgress.map( 120 | (stream: Stream) => ({ 121 | ...humanReadableStream(stream), 122 | humanStartTimeOrder: stream.startTime, 123 | humanStopTimeOrder: stream.stopTime, 124 | humanLastWithdrawTimeOrder: stream.lastWithdrawTime, 125 | }), 126 | ); 127 | return ( 128 | 139 | {(sortedData: TableRowData[]): any => 140 | sortedData.map((row: TableRowData) => ( 141 | 148 | )) 149 | } 150 |
151 | ); 152 | } 153 | 154 | const NewStreamRow = ({ 155 | role, 156 | row, 157 | columns, 158 | cancelSelectedStream, 159 | }: { 160 | role: 'sender' | 'recipient'; 161 | row: TableRowData; 162 | columns: Column[]; 163 | cancelSelectedStream: Function; 164 | }): ReactElement => { 165 | const { 166 | id, 167 | humanStartTime, 168 | humanStopTime, 169 | noteHash, 170 | recipient, 171 | sender, 172 | humanStartTimeOrder, 173 | humanStopTimeOrder, 174 | humanLastWithdrawTimeOrder, 175 | zkAsset, 176 | }: TableRowData = row; 177 | const note: ZkNote | undefined = useDecodedNote(noteHash); 178 | const displayName = useENSName(role === 'sender' ? recipient : sender); 179 | const displayValue = 180 | note?.value && 181 | convertToTokenValueDisplay( 182 | note.value, 183 | zkAsset.scalingFactor, 184 | zkAsset.linkedToken.decimals, 185 | ); 186 | 187 | return ( 188 | 189 | 195 | {id} 196 | 197 | 203 | {displayName} 204 | 205 | 211 | {`${displayValue} ${zkAsset.symbol}`} 212 | 213 | 219 | 224 | 225 | 231 | {humanStartTime} 232 | 233 | 239 | {humanStopTime} 240 | 241 | 242 | 249 | 250 | 251 | ); 252 | }; 253 | 254 | export default NewStreamTable; 255 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Table/TableHead.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import TableCell from '@material-ui/core/TableCell'; 4 | import TableHead from '@material-ui/core/TableHead'; 5 | import TableRow from '@material-ui/core/TableRow'; 6 | import TableSortLabel from '@material-ui/core/TableSortLabel'; 7 | import { Column } from './columns'; 8 | 9 | export const cellWidth = (width?: number): object | undefined => { 10 | if (!width) { 11 | return undefined; 12 | } 13 | 14 | return { 15 | maxWidth: `${width}px`, 16 | }; 17 | }; 18 | 19 | function TableHeader(props: any): ReactElement { 20 | const { columns, order, orderBy, onSort } = props; 21 | 22 | const changeSort = (property: string, orderAttr: boolean) => () => { 23 | onSort(property, orderAttr); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | {columns.map((column: Column) => ( 30 | 36 | {column.static ? ( 37 |
{column.label}
38 | ) : ( 39 | 45 | {column.label} 46 | 47 | )} 48 |
49 | ))} 50 |
51 |
52 | ); 53 | } 54 | 55 | export default TableHeader; 56 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Table/columns.ts: -------------------------------------------------------------------------------- 1 | export const STREAM_TABLE_ID = 'id'; 2 | export const STREAM_TABLE_RECIPIENT_ID = 'recipient'; 3 | export const STREAM_TABLE_DEPOSIT_ID = 'humanDeposit'; 4 | export const STREAM_TABLE_PROGRESS_ID = 'progress'; 5 | export const STREAM_TABLE_START_TIME_ID = 'humanStartTime'; 6 | export const STREAM_TABLE_END_TIME_ID = 'humanStopTime'; 7 | export const STREAM_TABLE_STATUS_ID = 'status'; 8 | export const STREAM_TABLE_ACTION_ID = 'action'; 9 | 10 | export type Column = { 11 | id: string; 12 | order: boolean; 13 | disablePadding: boolean; 14 | label: string; 15 | custom: boolean; 16 | align?: 'right' | 'inherit' | 'left' | 'center' | 'justify' | undefined; 17 | width?: number; 18 | style?: any; 19 | static?: boolean; 20 | }; 21 | 22 | export const generateColumns = (): Column[] => { 23 | const nonceColumn: Column = { 24 | id: STREAM_TABLE_ID, 25 | disablePadding: false, 26 | label: 'Stream ID', 27 | custom: false, 28 | order: false, 29 | width: 10, 30 | }; 31 | 32 | const recipientColumn: Column = { 33 | id: STREAM_TABLE_RECIPIENT_ID, 34 | order: false, 35 | disablePadding: false, 36 | label: 'To', 37 | custom: false, 38 | width: 200, 39 | }; 40 | 41 | const depositColumn: Column = { 42 | id: STREAM_TABLE_DEPOSIT_ID, 43 | order: false, 44 | disablePadding: false, 45 | label: 'Value', 46 | custom: false, 47 | width: 120, 48 | static: true, 49 | }; 50 | 51 | const progressColumn: Column = { 52 | id: STREAM_TABLE_PROGRESS_ID, 53 | disablePadding: false, 54 | order: true, 55 | label: 'Progress', 56 | custom: false, 57 | static: true, 58 | }; 59 | 60 | const startColumn: Column = { 61 | id: STREAM_TABLE_START_TIME_ID, 62 | disablePadding: false, 63 | order: true, 64 | label: 'Start Time', 65 | custom: false, 66 | }; 67 | 68 | const endColumn: Column = { 69 | id: STREAM_TABLE_END_TIME_ID, 70 | disablePadding: false, 71 | order: true, 72 | label: 'End Time', 73 | custom: false, 74 | }; 75 | 76 | const actionColumn: Column = { 77 | id: STREAM_TABLE_ACTION_ID, 78 | order: false, 79 | disablePadding: false, 80 | label: '', 81 | custom: true, 82 | align: 'right', 83 | static: true, 84 | }; 85 | 86 | return [ 87 | nonceColumn, 88 | recipientColumn, 89 | depositColumn, 90 | progressColumn, 91 | startColumn, 92 | endColumn, 93 | actionColumn, 94 | ]; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react'; 2 | 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import Table from '@material-ui/core/Table'; 5 | import TableBody from '@material-ui/core/TableBody'; 6 | import TablePagination from '@material-ui/core/TablePagination'; 7 | 8 | import { makeStyles } from '@material-ui/core'; 9 | import TableHead from './TableHead'; 10 | import { getSorting, stableSort, Order } from './sorting'; 11 | import { Column } from './columns'; 12 | 13 | const sm = '8px'; 14 | const xl = '32px'; 15 | const xxl = '40px'; 16 | 17 | const useStyles = makeStyles(() => ({ 18 | root: { 19 | backgroundColor: 'white', 20 | borderTopRightRadius: sm, 21 | borderTopLeftRadius: sm, 22 | // boxShadow: "1px 2px 10px 0 rgba(212, 212, 211, 0.59)", 23 | }, 24 | selectRoot: { 25 | lineHeight: xxl, 26 | backgroundColor: 'white', 27 | }, 28 | white: { 29 | backgroundColor: 'white', 30 | }, 31 | paginationRoot: { 32 | backgroundColor: 'white', 33 | // boxShadow: "1px 2px 10px 0 rgba(212, 212, 211, 0.59)", 34 | marginBottom: xl, 35 | borderBottomRightRadius: sm, 36 | borderBottomLeftRadius: sm, 37 | }, 38 | loader: { 39 | // boxShadow: "1px 2px 10px 0 rgba(212, 212, 211, 0.59)", 40 | }, 41 | })); 42 | 43 | const FIXED_HEIGHT = 49; 44 | 45 | const backProps = { 46 | 'aria-label': 'Previous Page', 47 | }; 48 | 49 | const nextProps = { 50 | 'aria-label': 'Next Page', 51 | }; 52 | 53 | const getEmptyStyle = (emptyRows: number): object => ({ 54 | height: FIXED_HEIGHT * emptyRows, 55 | borderTopRightRadius: sm, 56 | borderTopLeftRadius: sm, 57 | backgroundColor: 'white', 58 | width: '100%', 59 | display: 'flex', 60 | justifyContent: 'center', 61 | alignItems: 'center', 62 | }); 63 | 64 | type Props = { 65 | children: Function; 66 | columns: Column[]; 67 | data: any[]; 68 | defaultFixed: boolean; 69 | defaultOrder: Order; 70 | defaultOrderBy: string; 71 | defaultRowsPerPage: number; 72 | disableLoadingOnEmptyTable?: boolean; 73 | disablePagination?: boolean; 74 | label: string; 75 | noBorder?: boolean; 76 | size: number; 77 | }; 78 | 79 | type State = { 80 | page: number; 81 | order: Order | undefined; 82 | orderBy: string | undefined; 83 | fixed: boolean | undefined; 84 | orderProp: boolean; 85 | rowsPerPage: number | undefined; 86 | }; 87 | 88 | const GnoTable = (props: Props): ReactElement => { 89 | const classes = useStyles(); 90 | 91 | const { 92 | children, 93 | columns, 94 | data, 95 | defaultFixed, 96 | defaultOrder = 'asc', 97 | defaultOrderBy, 98 | defaultRowsPerPage = 5, 99 | disableLoadingOnEmptyTable = false, 100 | disablePagination = false, 101 | label, 102 | noBorder, 103 | size, 104 | }: Props = props; 105 | 106 | const [page, setPage] = useState(0); 107 | const [rowsPerPage, setRowsPerPage] = useState(); 108 | const [order, setOrder] = useState(); 109 | const [orderBy, setOrderBy] = useState(); 110 | const [fixed, setFixed] = useState(); 111 | const [orderProp, setOrderProp] = useState(); 112 | 113 | useEffect(() => { 114 | if (defaultOrderBy && columns) { 115 | const defaultOrderCol: Column | undefined = columns.find( 116 | ({ id }: { id: string }) => id === defaultOrderBy, 117 | ); 118 | 119 | if (defaultOrderCol?.order) { 120 | setOrderProp(true); 121 | } 122 | } 123 | }, [defaultOrderBy, columns]); 124 | 125 | const onSort = (newOrderBy: string, newOrderProp: boolean): void => { 126 | let newOrder: Order = 'desc'; 127 | 128 | // if table was previously sorted by the user 129 | if (order && orderBy === newOrderBy && order === 'desc') { 130 | newOrder = 'asc'; 131 | } else if (!order && defaultOrder === 'desc') { 132 | // if it was not sorted and defaultOrder is used 133 | newOrder = 'asc'; 134 | } 135 | setOrder(newOrder); 136 | setOrderBy(newOrderBy); 137 | setOrderProp(newOrderProp); 138 | setFixed(false); 139 | }; 140 | 141 | const handleChangePage = (e: any, newPage: number): void => { 142 | setPage(newPage); 143 | }; 144 | 145 | const handleChangeRowsPerPage = (e: any): void => { 146 | const newRowsPerPage = Number(e.target.value); 147 | setRowsPerPage(newRowsPerPage); 148 | }; 149 | 150 | const orderByParam: string = orderBy || defaultOrderBy; 151 | const orderParam: Order = order || defaultOrder; 152 | const displayRows: number = rowsPerPage || defaultRowsPerPage; 153 | const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed; 154 | 155 | // const paginationClasses = { 156 | // selectRoot: classes.selectRoot, 157 | // root: !noBorder && classes.paginationRoot, 158 | // input: classes.white, 159 | // }; 160 | 161 | let sortedData = stableSort( 162 | data, 163 | getSorting(orderParam, orderByParam, orderProp as boolean), 164 | fixedParam, 165 | ); 166 | 167 | if (!disablePagination) { 168 | sortedData = sortedData.slice( 169 | page * displayRows, 170 | page * displayRows + displayRows, 171 | ); 172 | } 173 | 174 | const emptyRows = 175 | displayRows - Math.min(displayRows, data.length - page * displayRows); 176 | const isEmpty = size === 0 && !disableLoadingOnEmptyTable; 177 | 178 | return ( 179 | <> 180 | {!isEmpty && ( 181 | 186 | 192 | {children(sortedData)} 193 |
194 | )} 195 | {isEmpty && ( 196 |
197 | 198 |
199 | )} 200 | {!disablePagination && ( 201 | 213 | )} 214 | 215 | ); 216 | }; 217 | 218 | export default GnoTable; 219 | -------------------------------------------------------------------------------- /packages/react-app/src/components/Table/sorting.ts: -------------------------------------------------------------------------------- 1 | export const FIXED = 'fixed'; 2 | 3 | export const buildOrderFieldFrom = (attr: string): string => `${attr}Order`; 4 | 5 | export type Order = 'asc' | 'desc'; 6 | 7 | const desc = (a: any, b: any, orderBy: string, orderProp: boolean): number => { 8 | const order = orderProp ? buildOrderFieldFrom(orderBy) : orderBy; 9 | 10 | if (b[order] < a[order]) { 11 | return -1; 12 | } 13 | if (b[order] > a[order]) { 14 | return 1; 15 | } 16 | 17 | return 0; 18 | }; 19 | 20 | // eslint-disable-next-line 21 | export const stableSort = (dataArray:any, cmp: Function, fixed:any) => { 22 | const fixedElems = fixed ? dataArray.filter((elem: any) => elem.fixed) : []; 23 | const data = fixed 24 | ? dataArray.filter((elem: any) => !elem[FIXED]) 25 | : dataArray; 26 | let stabilizedThis = data.map((el: any, index: number) => [el, index]); 27 | 28 | stabilizedThis = stabilizedThis.sort((a: any, b: any) => { 29 | const order = cmp(a[0], b[0]); 30 | 31 | if (order !== 0) { 32 | return order; 33 | } 34 | 35 | return a[1] - b[1]; 36 | }); 37 | 38 | const sortedElems = stabilizedThis.map((el: any) => el[0]); 39 | 40 | return fixedElems.concat(sortedElems); 41 | }; 42 | 43 | export const getSorting = ( 44 | order: Order, 45 | orderBy: string, 46 | orderProp: boolean, 47 | ): Function => 48 | order === 'desc' 49 | ? (a: any, b: any) => desc(a, b, orderBy, orderProp) 50 | : (a: any, b: any) => -desc(a, b, orderBy, orderProp); 51 | -------------------------------------------------------------------------------- /packages/react-app/src/components/display/DoubleProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, ReactElement, useMemo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import LinearProgress from '@material-ui/core/LinearProgress'; 5 | 6 | import moment from 'moment'; 7 | 8 | import calculateTime from '../../utils/time'; 9 | 10 | const DoubleProgressBar = ({ 11 | startTime, 12 | stopTime, 13 | lastWithdrawTime, 14 | }: { 15 | startTime: number; 16 | stopTime: number; 17 | lastWithdrawTime: number; 18 | }): ReactElement | null => { 19 | const [timePercentage, setTimePercentage] = useState(0); 20 | 21 | useEffect(() => { 22 | const intervalId = setInterval(() => { 23 | const newTimePercentage = calculateTime( 24 | moment.unix(startTime), 25 | moment.unix(stopTime), 26 | ); 27 | setTimePercentage(parseFloat(newTimePercentage.toFixed(2))); 28 | }, 1000); 29 | return (): void => { 30 | clearInterval(intervalId); 31 | }; 32 | }, [startTime, stopTime]); 33 | 34 | const withdrawPercentage = useMemo( 35 | () => 36 | calculateTime( 37 | moment.unix(startTime), 38 | moment.unix(stopTime), 39 | moment.unix(lastWithdrawTime), 40 | ), 41 | [startTime, stopTime, lastWithdrawTime], 42 | ); 43 | 44 | return ( 45 | <> 46 | 47 | 52 | 53 | ); 54 | }; 55 | 56 | DoubleProgressBar.propTypes = { 57 | startTime: PropTypes.number.isRequired, 58 | stopTime: PropTypes.number.isRequired, 59 | lastWithdrawTime: PropTypes.number.isRequired, 60 | }; 61 | 62 | export default DoubleProgressBar; 63 | -------------------------------------------------------------------------------- /packages/react-app/src/components/form/AddressInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useEffect } from 'react'; 2 | import { utils } from 'ethers'; 3 | 4 | import TextField from '@material-ui/core/TextField'; 5 | import { Address, AztecSDK } from '../../types/types'; 6 | 7 | const AddressInput = (props: any): ReactElement => { 8 | const { 9 | address, 10 | setAddress, 11 | aztec, 12 | }: { 13 | address: Address; 14 | setAddress: Function; 15 | aztec: AztecSDK; 16 | } = props; 17 | 18 | const [invalidAddress, setInvalidAddress] = useState(false); 19 | 20 | useEffect(() => { 21 | const checkAddressIsValid = async (testAddress: Address): Promise => { 22 | try { 23 | if (utils.getAddress(testAddress)) { 24 | const user = await aztec.user(testAddress); 25 | setInvalidAddress(!user.registered); 26 | } 27 | } catch (e) { 28 | // Don't show error state if user is still typing 29 | setInvalidAddress(false); 30 | } 31 | }; 32 | 33 | checkAddressIsValid(address); 34 | }, [aztec, address]); 35 | 36 | return ( 37 | { 40 | setAddress(val.target.value); 41 | }} 42 | {...props} 43 | error={invalidAddress} 44 | helperText={invalidAddress && 'Recipient does not have an Aztec account'} 45 | /> 46 | ); 47 | }; 48 | 49 | export default AddressInput; 50 | -------------------------------------------------------------------------------- /packages/react-app/src/components/form/AmountInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import TextField from '@material-ui/core/TextField'; 4 | import InputAdornment from '@material-ui/core/InputAdornment'; 5 | 6 | const AmountInput = (props: any): ReactElement => { 7 | const { 8 | amount, 9 | setAmount, 10 | balance, 11 | symbol, 12 | }: { 13 | amount: string; 14 | setAmount: Function; 15 | balance: string; 16 | symbol: string; 17 | } = props; 18 | 19 | return ( 20 | { 24 | setAmount(val.target.value); 25 | }} 26 | InputProps={{ 27 | endAdornment: ( 28 | {`Balance: ${balance} ${symbol}`} 29 | ), 30 | }} 31 | /> 32 | ); 33 | }; 34 | 35 | export default AmountInput; 36 | -------------------------------------------------------------------------------- /packages/react-app/src/components/form/ZkAssetSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import TextField from '@material-ui/core/TextField'; 4 | import { 5 | Address, 6 | zkAssetMetadata, 7 | ZkAsset, 8 | zkAssetMap, 9 | } from '../../types/types'; 10 | 11 | const ZkAssetSelect = ({ 12 | currentAsset, 13 | updateAsset, 14 | assetList, 15 | }: { 16 | currentAsset?: ZkAsset; 17 | updateAsset: Function; 18 | assetList: zkAssetMap; 19 | }): ReactElement => { 20 | return ( 21 | => updateAsset(val.target.value)} 26 | SelectProps={{ 27 | native: true, 28 | }} 29 | variant="filled" 30 | fullWidth 31 | // className={classes.formControl} 32 | > 33 | {Object.entries(assetList).map( 34 | ([address, metadata]: [Address, zkAssetMetadata]) => ( 35 | 38 | ), 39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default ZkAssetSelect; 45 | -------------------------------------------------------------------------------- /packages/react-app/src/contexts/AztecContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | createContext, 4 | ReactElement, 5 | useContext, 6 | } from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import getZkAssetsForNetwork from 'zkasset-metadata'; 9 | 10 | import { getContractAddressesForNetwork } from '@notestream/contract-artifacts'; 11 | import { zkAssetMap, AztecSDK } from '../types/types'; 12 | 13 | interface Props { 14 | children: ReactElement | Array; 15 | } 16 | 17 | interface State { 18 | aztec: AztecSDK; 19 | zkAssets: zkAssetMap; 20 | } 21 | 22 | export const AztecContext = createContext({} as State); 23 | 24 | export function useAztecContext(): State { 25 | return useContext(AztecContext); 26 | } 27 | 28 | const NETWORK_ID: number = parseInt( 29 | process.env.REACT_APP_NETWORK_ID as string, 30 | 10, 31 | ); 32 | 33 | async function setup(networkId: number): Promise { 34 | const addresses = getContractAddressesForNetwork(networkId); 35 | await window.aztec.enable({ 36 | contractAddresses: { 37 | ACE: addresses.ACE, 38 | }, 39 | apiKey: process.env.REACT_APP_AZTEC_API_KEY, // API key for use with GSN for free txs. 40 | }); 41 | } 42 | 43 | class AztecProvider extends Component { 44 | state: Readonly = { 45 | aztec: {}, 46 | zkAssets: {}, 47 | }; 48 | 49 | static propTypes = { 50 | children: PropTypes.any.isRequired, 51 | }; 52 | 53 | constructor(props: Props) { 54 | super(props); 55 | 56 | this.state = { 57 | ...this.state, 58 | zkAssets: getZkAssetsForNetwork(NETWORK_ID), 59 | }; 60 | } 61 | 62 | componentDidMount(): void { 63 | window.addEventListener('load', () => { 64 | setup(NETWORK_ID).then(() => { 65 | this.setState({ aztec: window.aztec }); 66 | }); 67 | }); 68 | } 69 | 70 | render(): ReactElement { 71 | return ( 72 | 73 | {this.props.children} 74 | 75 | ); 76 | } 77 | } 78 | 79 | export const useAztec = (): AztecSDK => { 80 | const { aztec } = useAztecContext(); 81 | return aztec; 82 | }; 83 | 84 | export const useZkAssets = (): zkAssetMap => { 85 | const { zkAssets } = useAztecContext(); 86 | return zkAssets; 87 | }; 88 | 89 | export default AztecProvider; 90 | -------------------------------------------------------------------------------- /packages/react-app/src/contexts/OnboardContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | createContext, 4 | ReactElement, 5 | useContext, 6 | } from 'react'; 7 | import PropTypes from 'prop-types'; 8 | 9 | import Onboard from 'bnc-onboard'; 10 | import { 11 | API, 12 | ConfigOptions, 13 | Initialization, 14 | UserState, 15 | WalletCheckInit, 16 | Wallet, 17 | WalletInitOptions, 18 | // eslint-disable-next-line import/no-unresolved 19 | } from 'bnc-onboard/dist/src/interfaces'; 20 | import { Address } from '../types/types'; 21 | 22 | interface Props { 23 | children: ReactElement | Array; 24 | } 25 | 26 | interface State extends UserState { 27 | onboard: API; 28 | setup: Function; 29 | } 30 | 31 | export const OnboardContext = createContext({} as State); 32 | 33 | export function useOnboardContext(): State { 34 | return useContext(OnboardContext); 35 | } 36 | 37 | const walletChecks: Array = [ 38 | { checkName: 'connect' }, 39 | { checkName: 'network' }, 40 | ]; 41 | 42 | const wallets: Array = [ 43 | { walletName: 'metamask', preferred: true }, 44 | ]; 45 | 46 | // dappid is mandatory so will have throw away id for local usage. 47 | const testid = 'c212885d-e81d-416f-ac37-06d9ad2cf5af'; 48 | 49 | class OnboardProvider extends Component { 50 | state: Readonly = { 51 | onboard: {} as API, 52 | address: '', 53 | balance: '', 54 | network: 0, 55 | wallet: {} as Wallet, 56 | mobileDevice: false, 57 | appNetworkId: 0, 58 | setup: () => null, 59 | }; 60 | 61 | static propTypes = { 62 | children: PropTypes.any.isRequired, 63 | }; 64 | 65 | constructor(props: Props) { 66 | super(props); 67 | 68 | const initialisation: Initialization = { 69 | dappId: testid, 70 | networkId: parseInt(process.env.REACT_APP_NETWORK_ID as string, 10), 71 | walletCheck: walletChecks, 72 | walletSelect: { 73 | heading: 'Select a wallet to connect to NoteStream', 74 | description: 75 | 'To use NoteStream you need an Ethereum wallet. Please select one from below:', 76 | wallets, 77 | }, 78 | subscriptions: { 79 | address: (address: Address): void => { 80 | this.setState({ address }); 81 | }, 82 | balance: (balance: string): void => { 83 | this.setState({ balance }); 84 | }, 85 | network: (network: number): void => { 86 | this.setState({ network }); 87 | }, 88 | wallet: (wallet: Wallet): void => { 89 | this.setState({ wallet }); 90 | }, 91 | }, 92 | }; 93 | 94 | const onboard = Onboard(initialisation); 95 | 96 | this.state = { 97 | ...this.state, 98 | onboard, 99 | }; 100 | } 101 | 102 | componentDidMount(): void { 103 | this.setup('MetaMask'); 104 | } 105 | 106 | setup = async (defaultWallet: string): Promise => { 107 | const { onboard } = this.state; 108 | try { 109 | const selected = await onboard.walletSelect(defaultWallet); 110 | if (selected) { 111 | const ready = await onboard.walletCheck(); 112 | if (ready) { 113 | const walletState = onboard.getState(); 114 | this.setState({ ...walletState }); 115 | console.log(walletState); 116 | } else { 117 | // Connection to wallet failed 118 | } 119 | } else { 120 | // User aborted set up 121 | } 122 | } catch (error) { 123 | console.log('error onboarding', error); 124 | } 125 | }; 126 | 127 | setConfig = (config: ConfigOptions): void => 128 | this.state.onboard.config(config); 129 | 130 | render(): ReactElement { 131 | return ( 132 | 133 | {this.props.children} 134 | 135 | ); 136 | } 137 | } 138 | 139 | export const useOnboard = (): API => { 140 | const { onboard } = useOnboardContext(); 141 | return onboard; 142 | }; 143 | 144 | export const useGetState = (): UserState => { 145 | const { onboard } = useOnboardContext(); 146 | return onboard.getState(); 147 | }; 148 | 149 | export const useAddress = (): Address => { 150 | const { address } = useOnboardContext(); 151 | return address; 152 | }; 153 | 154 | export const useWallet = (): Wallet => { 155 | const { wallet } = useOnboardContext(); 156 | return wallet; 157 | }; 158 | 159 | export const useNetwork = (): { network: number; appNetworkId: number } => { 160 | const { network, appNetworkId } = useOnboardContext(); 161 | return { network, appNetworkId }; 162 | }; 163 | 164 | export const useSetup = (): Function => { 165 | const { setup } = useOnboardContext(); 166 | return setup; 167 | }; 168 | 169 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 170 | export const useWalletProvider = (): any | null => { 171 | const { provider } = useWallet() || {}; 172 | return provider; 173 | }; 174 | 175 | export default OnboardProvider; 176 | -------------------------------------------------------------------------------- /packages/react-app/src/graphql/streams.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_RECIPIENT_STREAMS = gql` 4 | query getStreams($address: String!) { 5 | streams(where: { recipient: $address }) { 6 | id 7 | cancellation { 8 | id 9 | } 10 | lastWithdrawTime 11 | noteHash 12 | recipient 13 | sender 14 | startTime 15 | stopTime 16 | zkAsset { 17 | id 18 | symbol 19 | scalingFactor 20 | linkedToken { 21 | id 22 | decimals 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | export const GET_SENDER_STREAMS = gql` 30 | query getStreams($address: String!) { 31 | streams(where: { sender: $address }) { 32 | id 33 | cancellation { 34 | id 35 | } 36 | lastWithdrawTime 37 | noteHash 38 | recipient 39 | sender 40 | startTime 41 | stopTime 42 | zkAsset { 43 | id 44 | symbol 45 | scalingFactor 46 | linkedToken { 47 | id 48 | decimals 49 | } 50 | } 51 | } 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /packages/react-app/src/hooks/useDecodedNote.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import { Hash, ZkNote } from '../types/types'; 4 | import { useAztec } from '../contexts/AztecContext'; 5 | 6 | const useDecodedNote = (noteHash: Hash): ZkNote | undefined => { 7 | const { zkNote } = useAztec(); 8 | const [note, setNote] = useState(); 9 | 10 | useEffect(() => { 11 | const decodeNote = async (hash: Hash, i = 1): Promise => { 12 | const decodedNote = await zkNote(hash); 13 | if (decodedNote.valid) { 14 | setNote(decodedNote); 15 | } else if (i < 10) { 16 | // There seems to be an issue where on initial loadup where zkNote always returns an invalid note. 17 | // On subsequent renders after the aztec sdk has loaded fully then this works fine. 18 | // We then add a simple limited retry logic to fix the initial render. 19 | setTimeout(() => { 20 | decodeNote(hash, i + 1); 21 | }, 400); 22 | } 23 | }; 24 | 25 | if (zkNote) { 26 | decodeNote(noteHash); 27 | } 28 | }, [zkNote, noteHash]); 29 | 30 | return note; 31 | }; 32 | 33 | export default useDecodedNote; 34 | -------------------------------------------------------------------------------- /packages/react-app/src/hooks/useENSName.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Web3Provider } from 'ethers/providers'; 3 | import { Address } from '../types/types'; 4 | import { useWalletProvider } from '../contexts/OnboardContext'; 5 | import lookupAddress from '../utils/ens/lookupAddress'; 6 | 7 | const useENSName = (address: Address): string => { 8 | const provider = useWalletProvider(); 9 | const [ensName, setEnsName] = useState(address); 10 | 11 | useEffect(() => { 12 | if (provider) { 13 | lookupAddress(new Web3Provider(provider), address).then((name: string) => 14 | setEnsName(name), 15 | ); 16 | } 17 | }, [provider, address]); 18 | 19 | return ensName; 20 | }; 21 | 22 | export default useENSName; 23 | -------------------------------------------------------------------------------- /packages/react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .bn-onboard-custom.bn-onboard-modal { 16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 17 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 18 | sans-serif; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { ThemeProvider } from '@material-ui/core/styles'; 4 | 5 | import './index.css'; 6 | import { 7 | ApolloClient, 8 | ApolloProvider, 9 | HttpLink, 10 | InMemoryCache, 11 | } from '@apollo/client'; 12 | import App from './App'; 13 | import * as serviceWorker from './serviceWorker'; 14 | import OnboardProvider from './contexts/OnboardContext'; 15 | import AztecProvider from './contexts/AztecContext'; 16 | import theme from './theme'; 17 | 18 | const client = new ApolloClient({ 19 | cache: new InMemoryCache(), 20 | link: new HttpLink({ 21 | uri: process.env.REACT_APP_SUBGRAPH_URL, 22 | }), 23 | }); 24 | 25 | ReactDOM.render( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById('root'), 36 | ); 37 | 38 | // If you want your app to work offline and load faster, you can change 39 | // unregister() to register() below. Note this comes with some pitfalls. 40 | // Learn more about service workers: https://bit.ly/CRA-PWA 41 | serviceWorker.unregister(); 42 | -------------------------------------------------------------------------------- /packages/react-app/src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import { Paper, Grid, Typography, CircularProgress } from '@material-ui/core'; 5 | 6 | import { Contract } from 'ethers'; 7 | import StreamTable from '../components/StreamTable'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | paper: { 11 | padding: theme.spacing(2), 12 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 13 | padding: theme.spacing(3), 14 | }, 15 | }, 16 | pageElement: { 17 | marginTop: theme.spacing(1.5), 18 | marginBottom: theme.spacing(1.5), 19 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 20 | marginTop: theme.spacing(3), 21 | marginBottom: theme.spacing(3), 22 | }, 23 | }, 24 | icon: { 25 | marginRight: theme.spacing(2), 26 | }, 27 | })); 28 | 29 | const HomePage = ({ 30 | streamContract, 31 | }: { 32 | streamContract?: Contract; 33 | }): ReactElement => { 34 | const classes = useStyles(); 35 | 36 | if (!streamContract) { 37 | return ( 38 | 39 | 46 | 47 | 48 | 49 | ); 50 | } 51 | return ( 52 | 53 | 54 | 55 | Incoming streams 56 | 57 | 58 | 59 | 60 | 61 | Outgoing streams 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default HomePage; 70 | -------------------------------------------------------------------------------- /packages/react-app/src/pages/ReceivePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Paper from '@material-ui/core/Paper'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | 8 | import { Contract } from 'ethers'; 9 | import StreamTable from '../components/StreamTable'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | paper: { 13 | padding: theme.spacing(2), 14 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 15 | padding: theme.spacing(3), 16 | }, 17 | }, 18 | pageElement: { 19 | marginTop: theme.spacing(1.5), 20 | marginBottom: theme.spacing(1.5), 21 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 22 | marginTop: theme.spacing(3), 23 | marginBottom: theme.spacing(3), 24 | }, 25 | }, 26 | icon: { 27 | marginRight: theme.spacing(2), 28 | }, 29 | })); 30 | 31 | const ReceivePage = ({ 32 | streamContract, 33 | }: { 34 | streamContract?: Contract; 35 | }): ReactElement => { 36 | const classes = useStyles(); 37 | 38 | if (!streamContract) { 39 | return ( 40 | 41 | 48 | 49 | 50 | 51 | ); 52 | } 53 | return ( 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default ReceivePage; 61 | -------------------------------------------------------------------------------- /packages/react-app/src/pages/SendPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Paper from '@material-ui/core/Paper'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | 8 | import { Contract } from 'ethers'; 9 | import CreateStreamDialog from '../components/modals/CreateStreamModal'; 10 | 11 | import StreamTable from '../components/StreamTable'; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | paper: { 15 | marginTop: theme.spacing(1.5), 16 | marginBottom: theme.spacing(1.5), 17 | padding: theme.spacing(2), 18 | [theme.breakpoints.up(800 + theme.spacing(3) * 2)]: { 19 | padding: theme.spacing(3), 20 | marginTop: theme.spacing(3), 21 | marginBottom: theme.spacing(3), 22 | }, 23 | }, 24 | })); 25 | 26 | const SendPage = ({ 27 | streamContract, 28 | }: { 29 | streamContract?: Contract; 30 | }): ReactElement => { 31 | const classes = useStyles(); 32 | 33 | if (!streamContract) { 34 | return ( 35 | 36 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default SendPage; 66 | -------------------------------------------------------------------------------- /packages/react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-app/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | type Config = { 14 | onSuccess: Function; 15 | onUpdate: Function; 16 | }; 17 | 18 | const isLocalhost = Boolean( 19 | window.location.hostname === 'localhost' || 20 | // [::1] is the IPv6 localhost address. 21 | window.location.hostname === '[::1]' || 22 | // 127.0.0.0/8 are considered localhost for IPv4. 23 | // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec 24 | window.location.hostname.match( 25 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 26 | ), 27 | ); 28 | 29 | function registerValidSW(swUrl: string, config: Config): void { 30 | navigator.serviceWorker 31 | .register(swUrl) 32 | .then((registration) => { 33 | // eslint-disable-next-line no-param-reassign 34 | registration.onupdatefound = (): void => { 35 | const installingWorker = registration.installing; 36 | if (installingWorker == null) { 37 | return; 38 | } 39 | installingWorker.onstatechange = (): void => { 40 | if (installingWorker.state === 'installed') { 41 | if (navigator.serviceWorker.controller) { 42 | // At this point, the updated precached content has been fetched, 43 | // but the previous service worker will still serve the older 44 | // content until all client tabs are closed. 45 | console.log( 46 | 'New content is available and will be used when all ' + 47 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 48 | ); 49 | 50 | // Execute callback 51 | if (config && config.onUpdate) { 52 | config.onUpdate(registration); 53 | } 54 | } else { 55 | // At this point, everything has been precached. 56 | // It's the perfect time to display a 57 | // "Content is cached for offline use." message. 58 | console.log('Content is cached for offline use.'); 59 | 60 | // Execute callback 61 | if (config && config.onSuccess) { 62 | config.onSuccess(registration); 63 | } 64 | } 65 | } 66 | }; 67 | }; 68 | }) 69 | .catch((error) => { 70 | console.error('Error during service worker registration:', error); 71 | }); 72 | } 73 | 74 | function checkValidServiceWorker(swUrl: string, config: Config): void { 75 | // Check if the service worker can be found. If it can't reload the page. 76 | fetch(swUrl, { 77 | headers: { 'Service-Worker': 'script' }, 78 | }) 79 | .then((response) => { 80 | // Ensure service worker exists, and that we really are getting a JS file. 81 | const contentType = response.headers.get('content-type'); 82 | if ( 83 | response.status === 404 || 84 | (contentType != null && !contentType.includes('javascript')) 85 | ) { 86 | // No service worker found. Probably a different app. Reload the page. 87 | navigator.serviceWorker.ready.then((registration) => { 88 | registration.unregister().then(() => { 89 | window.location.reload(); 90 | }); 91 | }); 92 | } else { 93 | // Service worker found. Proceed as normal. 94 | registerValidSW(swUrl, config); 95 | } 96 | }) 97 | .catch(() => { 98 | console.log( 99 | 'No internet connection found. App is running in offline mode.', 100 | ); 101 | }); 102 | } 103 | 104 | export function register(config: Config): void { 105 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 106 | // The URL constructor is available in all browsers that support SW. 107 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 108 | if (publicUrl.origin !== window.location.origin) { 109 | // Our service worker won't work if PUBLIC_URL is on a different origin 110 | // from what our page is served on. This might happen if a CDN is used to 111 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 112 | return; 113 | } 114 | 115 | window.addEventListener('load', () => { 116 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 117 | 118 | if (isLocalhost) { 119 | // This is running on localhost. Let's check if a service worker still exists or not. 120 | checkValidServiceWorker(swUrl, config); 121 | 122 | // Add some additional logging to localhost, pointing developers to the 123 | // service worker/PWA documentation. 124 | navigator.serviceWorker.ready.then(() => { 125 | console.log( 126 | 'This web app is being served cache-first by a service ' + 127 | 'worker. To learn more, visit https://bit.ly/CRA-PWA', 128 | ); 129 | }); 130 | } else { 131 | // Is not localhost. Just register service worker 132 | registerValidSW(swUrl, config); 133 | } 134 | }); 135 | } 136 | } 137 | 138 | export function unregister(): void { 139 | if ('serviceWorker' in navigator) { 140 | navigator.serviceWorker.ready 141 | .then((registration) => { 142 | registration.unregister(); 143 | }) 144 | .catch((error) => { 145 | console.error(error.message); 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /packages/react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import '@testing-library/jest-dom/extend-expect'; 7 | -------------------------------------------------------------------------------- /packages/react-app/src/theme.tsx: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import { cyan, orange, grey } from '@material-ui/core/colors'; 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: cyan, 7 | secondary: orange, 8 | background: { 9 | default: grey[200], 10 | }, 11 | }, 12 | }); 13 | 14 | export default theme; 15 | -------------------------------------------------------------------------------- /packages/react-app/src/types/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@aztec/secp256k1'; 2 | 3 | declare module 'zkasset-metadata'; 4 | 5 | declare module '@notestream/contract-artifacts'; 6 | 7 | declare module 'react-blockies'; 8 | 9 | declare module '@transak/transak-sdk'; 10 | -------------------------------------------------------------------------------- /packages/react-app/src/types/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import BN from 'bn.js'; 3 | 4 | export type Address = string; 5 | 6 | export type Hash = string; 7 | 8 | export type AztecSDK = any; 9 | 10 | declare global { 11 | interface Window { 12 | aztec: AztecSDK; 13 | ethereum: any; 14 | web3: any; 15 | } 16 | } 17 | 18 | export interface Stream { 19 | id: number; 20 | cancellation: any; 21 | sender: Address; 22 | recipient: Address; 23 | startTime: number; 24 | lastWithdrawTime: number; 25 | stopTime: number; 26 | tokenAddress: Address; 27 | noteHash: Hash; 28 | zkAsset: any; 29 | } 30 | 31 | export type ZkNote = { 32 | asset: { 33 | address: Address; 34 | linkedTokenAddress: Address; 35 | }; 36 | id: Hash; 37 | noteHash: Hash; 38 | owner: { 39 | address: Address; 40 | }; 41 | status: 'CREATED' | 'DESTROYED'; 42 | value: number; 43 | viewingKey: string; 44 | valid: boolean; 45 | visible: boolean; 46 | destroyed: boolean; 47 | }; 48 | 49 | export type Note = { 50 | a: BN; 51 | k: BN; 52 | gamma: BN; 53 | sigma: BN; 54 | noteHash: Hash; 55 | metadata: string; 56 | owner: Address; 57 | }; 58 | 59 | export type Fraction = { 60 | numerator: number; 61 | denominator: number; 62 | err: number; 63 | }; 64 | 65 | export type Dividend = { 66 | source: number; 67 | target: number; 68 | residual: number; 69 | }; 70 | 71 | export type ZkAsset = { 72 | address: Address; 73 | scalingFactor: number; 74 | balance: Function; 75 | balanceOfLinkedToken: Function; 76 | deposit: Function; 77 | send: Function; 78 | withdraw: Function; 79 | linkedTokenAddress: Address; 80 | token: Token; 81 | toTokenValue: Function; 82 | toNoteValue: Function; 83 | }; 84 | 85 | export type Token = { 86 | name: string; 87 | symbol: string; 88 | decimals: number; 89 | }; 90 | 91 | export type zkAssetMetadata = { 92 | name: string; 93 | symbol: string; 94 | scalingFactor: number; 95 | linkedToken: Address; 96 | }; 97 | 98 | export type zkAssetMap = { 99 | [name: string]: zkAssetMetadata; 100 | }; 101 | 102 | export type Proof = { 103 | blindingFactors: any; 104 | blindingScalars: any; 105 | challenge: any; 106 | challengeHash: { data: string[] }; 107 | data: string[][]; 108 | hash: string; 109 | inputNotes: Note[]; 110 | m: number; 111 | notes: Note[]; 112 | output: string; 113 | outputNotes: Note[]; 114 | outputs: string; 115 | publicOwner: Address; 116 | publicValue: BN; 117 | rollingHash: { data: string[] }; 118 | sender: Address; 119 | type: string; 120 | validatedProofHash: string; 121 | encodeABI: Function; 122 | }; 123 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/ens/lookupAddress.ts: -------------------------------------------------------------------------------- 1 | import { Web3Provider } from 'ethers/providers'; 2 | import { getAddress } from 'ethers/utils'; 3 | 4 | const lookupAddress = async ( 5 | provider: Web3Provider, 6 | address: string, 7 | ): Promise => { 8 | try { 9 | const reportedName = await provider.lookupAddress(address); 10 | const resolvedAddress = await provider.resolveName(reportedName); 11 | if (getAddress(address) === getAddress(resolvedAddress)) { 12 | return reportedName; 13 | } 14 | } catch (e) { 15 | // Do nothing 16 | } 17 | return ''; 18 | }; 19 | 20 | export default lookupAddress; 21 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/links.ts: -------------------------------------------------------------------------------- 1 | const isExternal = (url: string): boolean => { 2 | return url.startsWith('https'); 3 | }; 4 | 5 | export default isExternal; 6 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/note.ts: -------------------------------------------------------------------------------- 1 | import { Fraction, Dividend } from '../types/types'; 2 | /* 3 | * Calculate the optimal fraction for the Dividend proof to minimise the remainder 4 | */ 5 | export function getFraction(value: number, maxdenom = 10000): Fraction { 6 | const best = { numerator: 1, denominator: 1, err: Math.abs(value - 1) }; 7 | for ( 8 | let denominator = 1; 9 | best.err > 0 && denominator <= maxdenom; 10 | denominator += 1 11 | ) { 12 | const numerator = Math.round(value * denominator); 13 | const err = Math.abs(value - numerator / denominator); 14 | if (err < best.err) { 15 | best.numerator = numerator; 16 | best.denominator = denominator; 17 | best.err = err; 18 | } 19 | } 20 | return best; 21 | } 22 | 23 | /* 24 | * Calculates the values of the target and remainder notes for a given source note and ratio za:zb 25 | */ 26 | export const computeRemainderNoteValue = ( 27 | source: number, 28 | za: number, 29 | zb: number, 30 | ): Dividend => { 31 | const target = Math.floor(source * (za / zb)); 32 | const residual = source * za - target * zb; 33 | 34 | return { 35 | source, 36 | target, 37 | residual, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/proofs/dividend.ts: -------------------------------------------------------------------------------- 1 | import { getFraction, computeRemainderNoteValue } from '../note'; 2 | import { 3 | Stream, 4 | Address, 5 | Fraction, 6 | Dividend, 7 | AztecSDK, 8 | Proof, 9 | } from '../../types/types'; 10 | 11 | const buildDividendProof = async ( 12 | stream: Stream, 13 | streamContractAddress: Address, 14 | withdrawalValue: number, 15 | aztec: AztecSDK, 16 | ): Promise => { 17 | const { recipient, noteHash } = stream; 18 | 19 | const payee = await aztec.user(recipient); 20 | 21 | const streamZkNote = await aztec.zkNote(noteHash); 22 | const streamNote = await streamZkNote.export(); 23 | 24 | const ratio: Fraction = getFraction(withdrawalValue / streamZkNote.value); 25 | 26 | console.table(ratio); 27 | 28 | const withdrawPayment: Dividend = computeRemainderNoteValue( 29 | streamZkNote.value, 30 | ratio.numerator, 31 | ratio.denominator, 32 | ); 33 | 34 | const withdrawPaymentNote = await payee.createNote(withdrawPayment.target, [ 35 | payee.address, 36 | ]); 37 | const remainderNote = await payee.createNote(withdrawPayment.residual); 38 | 39 | // Note: The current dividend proof implementation takes the residual note as an argument before the target note 40 | // It also has swapped the meanings of z_a and z_b relative to the documentation. 41 | // This results in the slightly unintuitive argument ordering below 42 | // see https://github.com/AztecProtocol/AZTEC/blob/develop/packages/aztec.js/src/proof/proofs/UTILITY/epoch0/dividend/index.js 43 | return new aztec.DividendProof( 44 | streamNote, 45 | remainderNote, 46 | withdrawPaymentNote, 47 | streamContractAddress, 48 | ratio.denominator, 49 | ratio.numerator, 50 | ); 51 | }; 52 | 53 | export default buildDividendProof; 54 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/proofs/index.ts: -------------------------------------------------------------------------------- 1 | import buildProofs from './proofs'; 2 | import { Stream, Address, AztecSDK, Proof } from '../../types/types'; 3 | 4 | // On a withdrawal the change note remains on the contract 5 | const buildWithdrawalProofs = ( 6 | aztec: AztecSDK, 7 | streamContractAddress: Address, 8 | streamObj: Stream, 9 | withdrawalValue: number, 10 | ): Promise<{ dividendProof: Proof; joinSplitProof: Proof }> => 11 | buildProofs( 12 | aztec, 13 | streamContractAddress, 14 | streamObj, 15 | withdrawalValue, 16 | streamContractAddress, 17 | ); 18 | 19 | // On a cancellation the change note is returned to the stream sender 20 | const buildCancellationProofs = ( 21 | aztec: AztecSDK, 22 | streamContractAddress: Address, 23 | streamObj: Stream, 24 | withdrawalValue: number, 25 | ): Promise<{ dividendProof: Proof; joinSplitProof: Proof }> => 26 | buildProofs( 27 | aztec, 28 | streamContractAddress, 29 | streamObj, 30 | withdrawalValue, 31 | streamObj.sender, 32 | ); 33 | 34 | export { buildWithdrawalProofs, buildCancellationProofs }; 35 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/proofs/joinsplit.ts: -------------------------------------------------------------------------------- 1 | import secp256k1 from '@aztec/secp256k1'; 2 | import { Stream, Address, Note, AztecSDK, Proof } from '../../types/types'; 3 | 4 | const buildJoinSplitProof = async ( 5 | stream: Stream, 6 | streamContractAddress: Address, 7 | streamNote: Note, 8 | withdrawPaymentNote: Note, 9 | changeNoteOwner: Address, 10 | aztec: AztecSDK, 11 | ): Promise => { 12 | const { sender, recipient } = stream; 13 | 14 | const payer = await aztec.user(sender); 15 | const payee = await aztec.user(recipient); 16 | const changeValue = Math.max( 17 | streamNote.k.toNumber() - withdrawPaymentNote.k.toNumber(), 18 | 0, 19 | ); 20 | 21 | const changeNote = await aztec.note.create( 22 | secp256k1.generateAccount().publicKey, 23 | changeValue, 24 | [ 25 | { address: payer.address, linkedPublicKey: payer.linkedPublicKey }, 26 | { address: payee.address, linkedPublicKey: payee.linkedPublicKey }, 27 | ], 28 | changeNoteOwner, 29 | ); 30 | 31 | return new aztec.JoinSplitProof( 32 | [streamNote], 33 | [withdrawPaymentNote, changeNote], 34 | streamContractAddress, 35 | 0, // No transfer from private to public assets or vice versa 36 | recipient, 37 | ); 38 | }; 39 | 40 | export default buildJoinSplitProof; 41 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/proofs/proofs.ts: -------------------------------------------------------------------------------- 1 | import buildDividendProof from './dividend'; 2 | import buildJoinSplitProof from './joinsplit'; 3 | import { Stream, Address, AztecSDK, Proof } from '../../types/types'; 4 | 5 | export default async function buildProofs( 6 | aztec: AztecSDK, 7 | streamContractAddress: Address, 8 | streamObj: Stream, 9 | withdrawalValue: number, 10 | changeNoteOwner: Address, 11 | ): Promise<{ dividendProof: Proof; joinSplitProof: Proof }> { 12 | const dividendProof = await buildDividendProof( 13 | streamObj, 14 | streamContractAddress, 15 | withdrawalValue, 16 | aztec, 17 | ); 18 | 19 | const [streamNote] = dividendProof.inputNotes; 20 | const [withdrawalNote] = dividendProof.outputNotes; 21 | 22 | const joinSplitProof = await buildJoinSplitProof( 23 | streamObj, 24 | streamContractAddress, 25 | streamNote, 26 | withdrawalNote, 27 | changeNoteOwner, 28 | aztec, 29 | ); 30 | 31 | return { 32 | dividendProof, 33 | joinSplitProof, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/stream/cancellation.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers'; 2 | import { calculateWithdrawal } from './withdrawal'; 3 | import { buildCancellationProofs } from '../proofs'; 4 | import { Address, AztecSDK, Stream, Proof } from '../../types/types'; 5 | 6 | const BUFFER_SECONDS = 120; 7 | 8 | export default async function cancelStream( 9 | aztec: AztecSDK, 10 | streamContract: Contract, 11 | streamId: number, 12 | userAddress: Address, 13 | ): Promise { 14 | const streamObj: Stream = await streamContract.getStream(streamId); 15 | 16 | const note = await aztec.zkNote(streamObj.noteHash); 17 | 18 | // If stream sender is cancelling the stream then they need to cancel 19 | // at a timestamp AFTER which the transaction is included in a block. 20 | // We then add a buffer of 2 minutes to the time which they try to cancel at. 21 | const bufferSeconds = userAddress === streamObj.sender ? BUFFER_SECONDS : 0; 22 | 23 | // Calculate a valid timestamp to cancel stream at 24 | const { withdrawalValue, withdrawalDuration } = calculateWithdrawal( 25 | note.value, 26 | streamObj.lastWithdrawTime, 27 | streamObj.stopTime, 28 | bufferSeconds, 29 | ); 30 | 31 | const { 32 | dividendProof, 33 | joinSplitProof, 34 | }: { 35 | dividendProof: Proof; 36 | joinSplitProof: Proof; 37 | } = await buildCancellationProofs( 38 | aztec, 39 | streamContract.address, 40 | streamObj, 41 | withdrawalValue, 42 | ); 43 | 44 | console.log('Cancelling stream:', streamId); 45 | console.log('Proofs:', dividendProof, joinSplitProof); 46 | return streamContract.cancelStream( 47 | streamId, 48 | dividendProof.encodeABI(), 49 | joinSplitProof.encodeABI(streamObj.tokenAddress), 50 | withdrawalDuration, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/stream/creation.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers'; 2 | import { Address, Hash, Note } from '../../types/types'; 3 | 4 | function initialiseStream( 5 | streamContract: Contract, 6 | payeeAddress: Address, 7 | noteForStreamContract: Note, 8 | zkAssetAddress: Address, 9 | startTime: number, 10 | endTime: number, 11 | ): number { 12 | return streamContract 13 | .createStream( 14 | payeeAddress, 15 | noteForStreamContract.noteHash, 16 | zkAssetAddress, 17 | startTime, 18 | endTime, 19 | ) 20 | .then((err: any, txHash: Hash) => { 21 | if (err) { 22 | console.log(err); 23 | return null; 24 | } 25 | console.log('transaction hash', txHash); 26 | return txHash; 27 | }); 28 | } 29 | 30 | async function fundStream( 31 | streamContractAddress: Address, 32 | payerAddress: Address, 33 | payeeAddress: Address, 34 | sendAmount: number, 35 | asset: any, 36 | ): Promise { 37 | const { outputNotes } = await asset.send( 38 | [ 39 | { 40 | to: streamContractAddress, 41 | amount: sendAmount, 42 | aztecAccountNotRequired: true, 43 | numberOfOutputNotes: 1, // contract has one 44 | }, 45 | ], 46 | { userAccess: [payerAddress, payeeAddress] }, // Give view access to sender and recipient 47 | ); 48 | const noteForStreamContract = outputNotes[0]; 49 | console.log('noteForStreamContract', noteForStreamContract); 50 | return noteForStreamContract; 51 | } 52 | 53 | async function createStream( 54 | sendAmount: number, 55 | streamContract: Contract, 56 | payerAddress: Address, 57 | payeeAddress: Address, 58 | zkAsset: any, 59 | startTime: number, 60 | endTime: number, 61 | ): Promise { 62 | const streamNote = await fundStream( 63 | streamContract.address, 64 | payerAddress, 65 | payeeAddress, 66 | sendAmount, 67 | zkAsset, 68 | ); 69 | return initialiseStream( 70 | streamContract, 71 | payeeAddress, 72 | streamNote, 73 | zkAsset.address, 74 | startTime, 75 | endTime, 76 | ); 77 | } 78 | 79 | export default createStream; 80 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/stream/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createStream } from './creation'; 2 | export { default as withdrawFunds } from './withdrawal'; 3 | export { default as cancelStream } from './cancellation'; 4 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/stream/withdrawal.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { Contract } from 'ethers'; 4 | import { buildWithdrawalProofs } from '../proofs'; 5 | import { getFraction } from '../note'; 6 | import { AztecSDK, Proof } from '../../types/types'; 7 | 8 | function calculateSafeWithdrawal( 9 | currentBalance: number, 10 | remainingStreamLength: number, 11 | targettedWithdrawDuration: number, 12 | ): { withdrawalValue: number; withdrawalDuration: number } { 13 | // Find time period for single note to be unlocked then multiply by withdrawal 14 | const timeBetweenNotes = remainingStreamLength / currentBalance; 15 | 16 | // This is often a decimal so we want to get the smallest number of periods to generate a duration of an integer number of seconds 17 | const safeMultipleOfPeriods = getFraction(timeBetweenNotes).denominator; 18 | const safeTimeBetweenNotes = timeBetweenNotes * safeMultipleOfPeriods; 19 | 20 | // Calculate the number of complete safe periods which has passed to give number of withdrawable notes 21 | const withdrawalValue = 22 | Math.floor(targettedWithdrawDuration / safeTimeBetweenNotes) * 23 | safeMultipleOfPeriods; 24 | const withdrawalDuration = withdrawalValue * timeBetweenNotes; 25 | 26 | return { 27 | withdrawalValue, 28 | withdrawalDuration, 29 | }; 30 | } 31 | 32 | export function calculateWithdrawal( 33 | noteValue: number, 34 | lastWithdrawTime: number, 35 | stopTime: number, 36 | bufferTime = 0, 37 | ): { withdrawalValue: number; withdrawalDuration: number } { 38 | if (noteValue === 0) { 39 | return { 40 | withdrawalValue: 0, 41 | withdrawalDuration: 0, 42 | }; 43 | } 44 | 45 | const remainingStreamLength = moment 46 | .duration(moment.unix(stopTime).diff(moment.unix(lastWithdrawTime))) 47 | .asSeconds(); 48 | 49 | // withdraw up to now or to end of stream 50 | if (moment().isAfter(moment.unix(stopTime))) { 51 | return { 52 | withdrawalValue: noteValue, 53 | withdrawalDuration: remainingStreamLength, 54 | }; 55 | } 56 | 57 | const targettedWithdrawDuration = moment 58 | .duration(moment().startOf('second').diff(moment.unix(lastWithdrawTime))) 59 | .add(bufferTime, 'seconds') 60 | .asSeconds(); 61 | 62 | const { withdrawalValue, withdrawalDuration } = calculateSafeWithdrawal( 63 | noteValue, 64 | remainingStreamLength, 65 | targettedWithdrawDuration, 66 | ); 67 | 68 | return { 69 | withdrawalValue, 70 | withdrawalDuration, 71 | }; 72 | } 73 | 74 | async function withdrawFunds( 75 | aztec: AztecSDK, 76 | streamContract: Contract, 77 | streamId: number, 78 | ): Promise { 79 | const streamObj = await streamContract.getStream(streamId); 80 | 81 | const note = await aztec.zkNote(streamObj.noteHash); 82 | 83 | // Calculate what value of the stream is redeemable 84 | const { withdrawalValue, withdrawalDuration } = calculateWithdrawal( 85 | note.value, 86 | streamObj.lastWithdrawTime, 87 | streamObj.stopTime, 88 | ); 89 | 90 | const { 91 | dividendProof, 92 | joinSplitProof, 93 | }: { 94 | dividendProof: Proof; 95 | joinSplitProof: Proof; 96 | } = await buildWithdrawalProofs( 97 | aztec, 98 | streamContract.address, 99 | streamObj, 100 | withdrawalValue, 101 | ); 102 | 103 | console.log('Withdrawing from stream:', streamId); 104 | console.log('Proofs:', dividendProof, joinSplitProof); 105 | return streamContract.withdrawFromStream( 106 | streamId, 107 | dividendProof.encodeABI(), 108 | joinSplitProof.encodeABI(streamObj.tokenAddress), 109 | withdrawalDuration, 110 | ); 111 | } 112 | 113 | export default withdrawFunds; 114 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from 'moment'; 2 | 3 | /* 4 | * Calculates the fraction of time through an interval the current time is. 5 | */ 6 | const calculateTime = ( 7 | startTime: Moment, 8 | endTime: Moment, 9 | testTime: Moment = moment(), 10 | ): number => { 11 | if (startTime.isAfter(testTime)) { 12 | return 0; 13 | } 14 | if (endTime.isBefore(testTime)) { 15 | return 100; 16 | } 17 | 18 | const elapsedTime = moment 19 | .duration(testTime.diff(startTime)) 20 | .asMilliseconds(); 21 | const streamDuration = moment 22 | .duration(endTime.diff(startTime)) 23 | .asMilliseconds(); 24 | const percentageElapsed = 100 * (elapsedTime / streamDuration); 25 | return percentageElapsed; 26 | }; 27 | 28 | export default calculateTime; 29 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/transak.ts: -------------------------------------------------------------------------------- 1 | import TransakSDK from '@transak/transak-sdk'; 2 | 3 | import { Address } from '../types/types'; 4 | 5 | const setupTransak = (userAddress: Address, themeColor: string): any => { 6 | return new TransakSDK({ 7 | apiKey: process.env.REACT_APP_TRANSAK_API_KEY, 8 | environment: 'STAGING', 9 | exchangeScreenTitle: 'Purchase crypto instantly', 10 | defaultCryptoCurrency: 'ETH', 11 | cryptoCurrencyList: 'ETH,DAI', 12 | walletAddress: userAddress, 13 | themeColor, 14 | // fiatCurrency: 'GBP', 15 | // fiatAmount: 100, 16 | email: '', 17 | redirectURL: '', 18 | hostURL: window.location.origin, 19 | widgetHeight: '600px', 20 | widgetWidth: '450px', 21 | }); 22 | }; 23 | 24 | export default setupTransak; 25 | -------------------------------------------------------------------------------- /packages/react-app/src/utils/units/convertToTokenValue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigNumberish, 3 | formatUnits, 4 | bigNumberify, 5 | BigNumber, 6 | } from 'ethers/utils'; 7 | 8 | export const convertToTokenValue = ( 9 | value: BigNumberish, 10 | scalingFactor: BigNumberish, 11 | ): BigNumber => { 12 | return bigNumberify(value).mul(scalingFactor); 13 | }; 14 | 15 | export const convertToTokenValueDisplay = ( 16 | value: BigNumberish, 17 | scalingFactor: BigNumberish, 18 | decimals: number, 19 | ): string => { 20 | return formatUnits(convertToTokenValue(value, scalingFactor), decimals); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "DOM" 5 | ], 6 | "module": "esnext", 7 | "outDir": "build", 8 | "sourceMap": true, 9 | "target": "ES2015", 10 | "allowJs": true, 11 | "noImplicitAny": true, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "skipLibCheck": false, 17 | "forceConsistentCasingInFileNames": true, 18 | "jsx": "react", 19 | "allowSyntheticDefaultImports": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | }, 23 | "include": [ 24 | "./src/**/*" 25 | ], 26 | } -------------------------------------------------------------------------------- /packages/subgraph/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | }, 5 | "extends": [ 6 | 'eslint:recommended', 7 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 8 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 9 | ], 10 | "rules": { 11 | "no-console": "off", 12 | "import/prefer-default-export": "off", 13 | "import/extensions": [ 14 | "error", 15 | "ignorePackages", 16 | { 17 | "ts": "never", 18 | } 19 | ], 20 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.js"]}] 21 | }, 22 | "settings": { 23 | 'import/resolver': { 24 | node: { 25 | paths: ["src"], 26 | extensions: ['.ts'] 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/subgraph/.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .vscode/ 3 | build/ 4 | lib/ 5 | node_modules/ 6 | src/types/ 7 | 8 | # files 9 | .DS_Store 10 | .env 11 | subgraph.yaml 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | *.log 16 | -------------------------------------------------------------------------------- /packages/subgraph/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint --fix" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/subgraph/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | tabWidth: 2 6 | }; -------------------------------------------------------------------------------- /packages/subgraph/README.md: -------------------------------------------------------------------------------- 1 | ## NoteStream Subgraph 2 | 3 | The Graph is a tool that helps dapps index their blockchain data so that users don't have to wait hours or more for the website to load. 4 | 5 | This is our subgraph, a collection of schemas and event handlers that parse the data broadcast on the Ethereum blockchain to GraphQL form. 6 | 7 | Our smart contracts can be found in the [NoteStream monorepo](https://github.com/TomAFrench/NoteStream). 8 | 9 | ## Contributing 10 | 11 | Before you can build, create and deploy this subgraph, you have to execute the following commands in the terminal: 12 | 13 | ```bash 14 | $ yarn 15 | $ yarn prepare:mainnet 16 | ``` 17 | 18 | The first command installs all external dependencies, while the latter generates the `subgraph.yaml` file, which is 19 | required by The Graph. 20 | 21 | We use [Handlebars](https://github.com/wycats/handlebars.js/) to compile a [template subgraph](./subgraph.template.yaml) and add the parameters specific to each 22 | network (Mainnet, Goerli, Kovan, Rinkeby, Ropsten). The network can be changed via the `NETWORK_NAME` environment 23 | variable or directly by choosing a different "prepare" script. See [package.json](./package.json) for all options. 24 | 25 | ## Queries 26 | 27 | ### Querying All Streams 28 | 29 | ```graphql 30 | { 31 | streams { 32 | id 33 | cancellation { 34 | recipientBalance 35 | timestamp 36 | txhash 37 | } 38 | deposit 39 | lastWithdrawTime 40 | recipient 41 | sender 42 | startTime 43 | stopTime 44 | timestamp 45 | txs { 46 | id 47 | block 48 | event 49 | from 50 | timestamp 51 | to 52 | } 53 | withdrawals { 54 | id 55 | duration 56 | } 57 | zkAsset { 58 | id 59 | name 60 | scalingFactor 61 | symbol 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ### Querying All Transactions 68 | 69 | ```graphql 70 | { 71 | transactions { 72 | id 73 | block 74 | event 75 | from 76 | stream { 77 | id 78 | } 79 | timestamp 80 | to 81 | } 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /packages/subgraph/abis/NoteStream.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_aceContractAddress", 7 | "type": "address" 8 | } 9 | ], 10 | "payable": false, 11 | "stateMutability": "nonpayable", 12 | "type": "constructor" 13 | }, 14 | { 15 | "anonymous": false, 16 | "inputs": [ 17 | { 18 | "indexed": true, 19 | "internalType": "uint256", 20 | "name": "streamId", 21 | "type": "uint256" 22 | }, 23 | { 24 | "indexed": true, 25 | "internalType": "address", 26 | "name": "sender", 27 | "type": "address" 28 | }, 29 | { 30 | "indexed": true, 31 | "internalType": "address", 32 | "name": "recipient", 33 | "type": "address" 34 | }, 35 | { 36 | "indexed": false, 37 | "internalType": "uint256", 38 | "name": "cancelDuration", 39 | "type": "uint256" 40 | } 41 | ], 42 | "name": "CancelStream", 43 | "type": "event" 44 | }, 45 | { 46 | "anonymous": false, 47 | "inputs": [ 48 | { 49 | "indexed": true, 50 | "internalType": "uint256", 51 | "name": "streamId", 52 | "type": "uint256" 53 | }, 54 | { 55 | "indexed": true, 56 | "internalType": "address", 57 | "name": "sender", 58 | "type": "address" 59 | }, 60 | { 61 | "indexed": true, 62 | "internalType": "address", 63 | "name": "recipient", 64 | "type": "address" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "address", 69 | "name": "zkAsset", 70 | "type": "address" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "bytes32", 75 | "name": "noteHash", 76 | "type": "bytes32" 77 | }, 78 | { 79 | "indexed": false, 80 | "internalType": "uint256", 81 | "name": "startTime", 82 | "type": "uint256" 83 | }, 84 | { 85 | "indexed": false, 86 | "internalType": "uint256", 87 | "name": "stopTime", 88 | "type": "uint256" 89 | } 90 | ], 91 | "name": "CreateStream", 92 | "type": "event" 93 | }, 94 | { 95 | "anonymous": false, 96 | "inputs": [ 97 | { 98 | "indexed": true, 99 | "internalType": "uint256", 100 | "name": "streamId", 101 | "type": "uint256" 102 | }, 103 | { 104 | "indexed": true, 105 | "internalType": "address", 106 | "name": "sender", 107 | "type": "address" 108 | }, 109 | { 110 | "indexed": true, 111 | "internalType": "address", 112 | "name": "recipient", 113 | "type": "address" 114 | }, 115 | { 116 | "indexed": false, 117 | "internalType": "bytes32", 118 | "name": "noteHash", 119 | "type": "bytes32" 120 | }, 121 | { 122 | "indexed": false, 123 | "internalType": "uint256", 124 | "name": "withdrawDuration", 125 | "type": "uint256" 126 | } 127 | ], 128 | "name": "WithdrawFromStream", 129 | "type": "event" 130 | }, 131 | { 132 | "constant": false, 133 | "inputs": [ 134 | { 135 | "internalType": "uint256", 136 | "name": "streamId", 137 | "type": "uint256" 138 | }, 139 | { 140 | "internalType": "bytes", 141 | "name": "_proof1", 142 | "type": "bytes" 143 | }, 144 | { 145 | "internalType": "bytes", 146 | "name": "_proof2", 147 | "type": "bytes" 148 | }, 149 | { 150 | "internalType": "uint256", 151 | "name": "_unclaimedTime", 152 | "type": "uint256" 153 | } 154 | ], 155 | "name": "cancelStream", 156 | "outputs": [ 157 | { 158 | "internalType": "bool", 159 | "name": "", 160 | "type": "bool" 161 | } 162 | ], 163 | "payable": false, 164 | "stateMutability": "nonpayable", 165 | "type": "function" 166 | }, 167 | { 168 | "constant": false, 169 | "inputs": [ 170 | { 171 | "internalType": "address", 172 | "name": "recipient", 173 | "type": "address" 174 | }, 175 | { 176 | "internalType": "bytes32", 177 | "name": "noteHash", 178 | "type": "bytes32" 179 | }, 180 | { 181 | "internalType": "address", 182 | "name": "tokenAddress", 183 | "type": "address" 184 | }, 185 | { 186 | "internalType": "uint256", 187 | "name": "startTime", 188 | "type": "uint256" 189 | }, 190 | { 191 | "internalType": "uint256", 192 | "name": "stopTime", 193 | "type": "uint256" 194 | } 195 | ], 196 | "name": "createStream", 197 | "outputs": [ 198 | { 199 | "internalType": "uint256", 200 | "name": "", 201 | "type": "uint256" 202 | } 203 | ], 204 | "payable": false, 205 | "stateMutability": "nonpayable", 206 | "type": "function" 207 | }, 208 | { 209 | "constant": true, 210 | "inputs": [ 211 | { 212 | "internalType": "uint256", 213 | "name": "streamId", 214 | "type": "uint256" 215 | } 216 | ], 217 | "name": "getStream", 218 | "outputs": [ 219 | { 220 | "internalType": "address", 221 | "name": "sender", 222 | "type": "address" 223 | }, 224 | { 225 | "internalType": "address", 226 | "name": "recipient", 227 | "type": "address" 228 | }, 229 | { 230 | "internalType": "bytes32", 231 | "name": "noteHash", 232 | "type": "bytes32" 233 | }, 234 | { 235 | "internalType": "address", 236 | "name": "tokenAddress", 237 | "type": "address" 238 | }, 239 | { 240 | "internalType": "uint256", 241 | "name": "startTime", 242 | "type": "uint256" 243 | }, 244 | { 245 | "internalType": "uint256", 246 | "name": "lastWithdrawTime", 247 | "type": "uint256" 248 | }, 249 | { 250 | "internalType": "uint256", 251 | "name": "stopTime", 252 | "type": "uint256" 253 | } 254 | ], 255 | "payable": false, 256 | "stateMutability": "view", 257 | "type": "function" 258 | }, 259 | { 260 | "constant": true, 261 | "inputs": [], 262 | "name": "nextStreamId", 263 | "outputs": [ 264 | { 265 | "internalType": "uint256", 266 | "name": "", 267 | "type": "uint256" 268 | } 269 | ], 270 | "payable": false, 271 | "stateMutability": "view", 272 | "type": "function" 273 | }, 274 | { 275 | "constant": false, 276 | "inputs": [ 277 | { 278 | "internalType": "uint256", 279 | "name": "streamId", 280 | "type": "uint256" 281 | }, 282 | { 283 | "internalType": "bytes", 284 | "name": "_proof1", 285 | "type": "bytes" 286 | }, 287 | { 288 | "internalType": "bytes", 289 | "name": "_proof2", 290 | "type": "bytes" 291 | }, 292 | { 293 | "internalType": "uint256", 294 | "name": "_streamDurationToWithdraw", 295 | "type": "uint256" 296 | } 297 | ], 298 | "name": "withdrawFromStream", 299 | "outputs": [], 300 | "payable": false, 301 | "stateMutability": "nonpayable", 302 | "type": "function" 303 | } 304 | ] 305 | -------------------------------------------------------------------------------- /packages/subgraph/networks.yaml: -------------------------------------------------------------------------------- 1 | rinkeby: 2 | contracts: 3 | NoteStream: 4 | address: '0xCcb98Efa4eA6a3814ece095f73264c43D7D50071' 5 | startBlock: 6625807 6 | networkName: rinkeby 7 | -------------------------------------------------------------------------------- /packages/subgraph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notestream/subgraph", 3 | "description": "GraphQL server built with The Graph", 4 | "version": "2.1.0", 5 | "author": { 6 | "name": "Tom French", 7 | "email": "tom@tomfren.ch", 8 | "url": "https://github.com/TomAFrench/NoteStream" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/TomAFrench/NoteStream/issues" 12 | }, 13 | "devDependencies": { 14 | "@graphprotocol/graph-cli": "^0.18.0", 15 | "@graphprotocol/graph-ts": "^0.18.1", 16 | "axios": "^0.19.0", 17 | "fs-extra": "^8.1.0", 18 | "handlebars": "^4.5.2", 19 | "js-yaml": "^3.13.1", 20 | "lint-staged": "^10.2.2", 21 | "shx": "^0.3.2", 22 | "typy": "^3.3.0" 23 | }, 24 | "license": "LGPL-3.0", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/TomAFrench/NoteStream.git" 28 | }, 29 | "scripts": { 30 | "precommit": "lint-staged", 31 | "commit": "git-cz", 32 | "clean": "shx rm -rf ./build ./src/types ./subgraph.yaml", 33 | "codegen": "graph codegen --debug --output-dir src/types/", 34 | "create:local": "graph create tomafrench/notestream --node http://127.0.0.1:8020", 35 | "deploy": "graph deploy $SUBGRAPH --debug --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", 36 | "deploy:goerli": "yarn prepare:goerli && SUBGRAPH=tomafrench/notestream-goerli yarn deploy", 37 | "deploy:kovan": "yarn prepare:kovan && SUBGRAPH=tomafrench/notestream-kovan yarn deploy", 38 | "deploy:local": "graph deploy tomafrench/notestream --debug --ipfs http://127.0.0.1:5001 --node http://127.0.0.1:8020", 39 | "deploy:mainnet": "yarn prepare:mainnet && SUBGRAPH=tomafrench/notestream yarn deploy", 40 | "deploy:rinkeby": "yarn prepare:rinkeby && SUBGRAPH=tomafrench/notestream-rinkeby yarn deploy", 41 | "deploy:ropsten": "yarn prepare:ropsten && SUBGRAPH=tomafrench/notestream-ropsten yarn deploy", 42 | "prepare:goerli": "NETWORK_NAME=goerli node ./updateAddresses.js && NETWORK_NAME=goerli node ./templatify.js", 43 | "prepare:kovan": "NETWORK_NAME=kovan node ./updateAddresses.js && NETWORK_NAME=kovan node ./templatify.js", 44 | "prepare:mainnet": "NETWORK_NAME=mainnet node ./updateAddresses.js && NETWORK_NAME=mainnet node ./templatify.js", 45 | "prepare:rinkeby": "NETWORK_NAME=rinkeby node ./updateAddresses.js && NETWORK_NAME=rinkeby node ./templatify.js", 46 | "prepare:ropsten": "NETWORK_NAME=ropsten node ./updateAddresses.js && NETWORK_NAME=ropsten node ./templatify.js", 47 | "prepare:local": "NETWORK_NAME=local node ./updateAddresses.js && NETWORK_NAME=local node ./templatify.js", 48 | "remove-local": "graph remove tomafrench/notestream --node http://127.0.0.1:8020/", 49 | "reset-local": "yarn codegen && yarn remove-local && yarn create-local && yarn deploy-local", 50 | "has:changed": "bash ../monorepo-scripts/ci/hasChanged.sh subgraph" 51 | }, 52 | "config": { 53 | "commitizen": { 54 | "path": "./node_modules/cz-conventional-changelog" 55 | } 56 | }, 57 | "dependencies": { 58 | "@graphprotocol/graph-ts": "^0.18.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/subgraph/schema.graphql: -------------------------------------------------------------------------------- 1 | ### Generic ### 2 | 3 | type Token @entity { 4 | id: ID! 5 | name: String 6 | decimals: Int! 7 | symbol: String 8 | # Only interested in tokens with a linked zkAsset 9 | zkAsset: ZkAsset! 10 | } 11 | 12 | type ZkAsset @entity { 13 | id: ID! 14 | name: String 15 | scalingFactor: BigInt! 16 | symbol: String 17 | # ZkAsset may be purely private 18 | linkedToken: Token 19 | } 20 | 21 | interface TransactionEvent { 22 | id: ID! 23 | block: Int! 24 | event: String! 25 | from: Bytes! 26 | stream: Stream! 27 | timestamp: BigInt! 28 | to: Bytes 29 | } 30 | 31 | ### NoteStream ### 32 | 33 | # The id is the stream's id 34 | type Cancellation implements TransactionEvent @entity { 35 | id: ID! 36 | block: Int! 37 | event: String! 38 | from: Bytes! 39 | stream: Stream! 40 | timestamp: BigInt! 41 | to: Bytes 42 | duration: BigInt! 43 | } 44 | 45 | type Withdrawal implements TransactionEvent @entity { 46 | id: ID! 47 | block: Int! 48 | event: String! 49 | from: Bytes! 50 | stream: Stream! 51 | timestamp: BigInt! 52 | to: Bytes 53 | duration: BigInt! 54 | } 55 | 56 | type Stream @entity { 57 | id: ID! 58 | cancellation: Cancellation 59 | lastWithdrawTime: BigInt! 60 | # Optional as cancelled streams don't have a note 61 | noteHash: Bytes 62 | recipient: Bytes! 63 | sender: Bytes! 64 | startTime: BigInt! 65 | stopTime: BigInt! 66 | timestamp: BigInt! 67 | # @derivedFrom makes it possible to map large sets of data to the same stream 68 | txs: [TransactionEvent!] @derivedFrom(field: "stream") 69 | withdrawals: [Withdrawal!] @derivedFrom(field: "stream") 70 | zkAsset: ZkAsset 71 | } 72 | -------------------------------------------------------------------------------- /packages/subgraph/src/mappings/noteStream.ts: -------------------------------------------------------------------------------- 1 | import { Cancellation, Stream, Withdrawal } from '../types/schema'; 2 | import { 3 | CreateStream as CreateStreamEvent, 4 | WithdrawFromStream as WithdrawFromStreamEvent, 5 | CancelStream as CancelStreamEvent, 6 | } from '../types/NoteStream/NoteStream'; 7 | import { addZkAsset } from './zkAssets'; 8 | 9 | export function handleCreateStream(event: CreateStreamEvent): void { 10 | /* Create adjacent but important object */ 11 | addZkAsset(event.params.zkAsset.toHex()); 12 | /* Create the stream object */ 13 | const streamId = event.params.streamId.toString(); 14 | const stream = new Stream(streamId); 15 | stream.lastWithdrawTime = event.params.startTime; 16 | stream.noteHash = event.params.noteHash; 17 | stream.recipient = event.params.recipient; 18 | stream.sender = event.params.sender; 19 | stream.startTime = event.params.startTime; 20 | stream.stopTime = event.params.stopTime; 21 | stream.timestamp = event.block.timestamp; 22 | stream.zkAsset = event.params.zkAsset.toHex(); 23 | stream.save(); 24 | } 25 | 26 | export function handleWithdrawFromStream(event: WithdrawFromStreamEvent): void { 27 | const streamId = event.params.streamId.toString(); 28 | const stream = Stream.load(streamId); 29 | if (stream == null) { 30 | return; 31 | } 32 | stream.noteHash = event.params.noteHash; 33 | stream.lastWithdrawTime = stream.lastWithdrawTime.plus( 34 | event.params.withdrawDuration, 35 | ); 36 | stream.save(); 37 | 38 | const withdrawal = new Withdrawal(event.transaction.hash.toHex()); 39 | withdrawal.block = event.block.number.toI32(); 40 | withdrawal.event = 'WithdrawFromStream'; 41 | withdrawal.from = event.transaction.from; 42 | withdrawal.stream = streamId; 43 | withdrawal.timestamp = event.block.timestamp; 44 | withdrawal.to = event.transaction.to; 45 | withdrawal.duration = event.params.withdrawDuration; 46 | withdrawal.save(); 47 | } 48 | 49 | export function handleCancelStream(event: CancelStreamEvent): void { 50 | const streamId = event.params.streamId.toString(); 51 | const stream = Stream.load(streamId); 52 | if (stream == null) { 53 | return; 54 | } 55 | 56 | const cancellation = new Cancellation(event.transaction.hash.toHex()); 57 | 58 | cancellation.block = event.block.number.toI32(); 59 | cancellation.event = 'CancelStream'; 60 | cancellation.from = event.transaction.from; 61 | cancellation.stream = streamId; 62 | cancellation.timestamp = event.block.timestamp; 63 | cancellation.to = event.transaction.to; 64 | cancellation.duration = event.params.cancelDuration; 65 | cancellation.save(); 66 | 67 | stream.cancellation = event.transaction.hash.toHex(); 68 | stream.save(); 69 | } 70 | -------------------------------------------------------------------------------- /packages/subgraph/src/mappings/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../types/schema'; 2 | 3 | export function addToken(address: string, zkAsset: string): void { 4 | let token = Token.load(address); 5 | if (token != null) { 6 | return; 7 | } 8 | 9 | /* Mainnet */ 10 | token = new Token(address); 11 | token.zkAsset = zkAsset; 12 | /* Testnets */ 13 | if (address == '0xc3dbf84abb494ce5199d5d4d815b10ec29529ff8') { 14 | token.name = 'TestnetDAI'; 15 | token.decimals = 18; 16 | token.symbol = 'DAI'; 17 | } else if (address == '0x1f9061b953bba0e36bf50f21876132dcf276fc6e') { 18 | token.name = 'ZEENUS'; 19 | token.decimals = 0; 20 | token.symbol = 'ZEENUS'; 21 | } else if (address == '0x022e292b44b5a146f2e8ee36ff44d3dd863c915c') { 22 | token.name = 'XEENUS'; 23 | token.decimals = 18; 24 | token.symbol = 'XEENUS'; 25 | } else { 26 | token.name = 'unknown'; 27 | token.decimals = 0; 28 | token.symbol = 'unknown'; 29 | } 30 | 31 | token.save(); 32 | } 33 | -------------------------------------------------------------------------------- /packages/subgraph/src/mappings/transactions.ts: -------------------------------------------------------------------------------- 1 | // import { ethereum } from '@graphprotocol/graph-ts'; 2 | 3 | // import { Transaction } from '../types/schema'; 4 | 5 | // export function addTransaction( 6 | // name: string, 7 | // event: ethereum.Event, 8 | // streamId: string, 9 | // ): void { 10 | // const transaction = new Transaction(event.transaction.hash.toHex()); 11 | // transaction.event = name; 12 | // transaction.block = event.block.number.toI32(); 13 | // transaction.from = event.transaction.from; 14 | // transaction.stream = streamId; 15 | // transaction.timestamp = event.block.timestamp; 16 | // transaction.to = event.transaction.to; 17 | // transaction.save(); 18 | // } 19 | -------------------------------------------------------------------------------- /packages/subgraph/src/mappings/zkAssets.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@graphprotocol/graph-ts'; 2 | import { ZkAsset } from '../types/schema'; 3 | import { addToken } from './tokens'; 4 | 5 | export function addZkAsset(address: string): void { 6 | let zkAsset = ZkAsset.load(address); 7 | if (zkAsset != null) { 8 | return; 9 | } 10 | 11 | /* Mainnet */ 12 | zkAsset = new ZkAsset(address); 13 | 14 | /* Testnets */ 15 | if (address == '0x9a9c00d7015f2708f30875e2d81f206bb5052e31') { 16 | zkAsset.name = 'zkTestnetDAI'; 17 | zkAsset.scalingFactor = json.toBigInt('10000000000000000'); 18 | zkAsset.symbol = 'zkDAI'; 19 | zkAsset.linkedToken = '0xc3dbf84abb494ce5199d5d4d815b10ec29529ff8'; 20 | addToken(zkAsset.linkedToken, '0x9a9c00d7015f2708f30875e2d81f206bb5052e31'); 21 | } else if (address == '0xfd3cebb289b26ad63a389365187689fe21f204cd') { 22 | zkAsset.name = 'zkZEENUS'; 23 | zkAsset.scalingFactor = json.toBigInt('1'); 24 | zkAsset.symbol = 'zkZEENUS'; 25 | zkAsset.linkedToken = '0x1f9061b953bba0e36bf50f21876132dcf276fc6e'; 26 | addToken(zkAsset.linkedToken, '0xfd3cebb289b26ad63a389365187689fe21f204cd'); 27 | } else if (address == '0x232d758910c5249f1ccc3cb774001da1a685de3f') { 28 | zkAsset.name = 'zkXEENUS'; 29 | zkAsset.scalingFactor = json.toBigInt('50000000000000000'); 30 | zkAsset.symbol = 'zkXEENUS'; 31 | zkAsset.linkedToken = '0x022e292b44b5a146f2e8ee36ff44d3dd863c915c'; 32 | addToken(zkAsset.linkedToken, '0x232d758910c5249f1ccc3cb774001da1a685de3f'); 33 | } else { 34 | zkAsset.name = 'unknown'; 35 | zkAsset.scalingFactor = json.toBigInt('10000000000000000'); 36 | zkAsset.symbol = 'unknown'; 37 | } 38 | 39 | zkAsset.save(); 40 | } 41 | -------------------------------------------------------------------------------- /packages/subgraph/subgraph.template.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.2 2 | description: NoteStream is the Ethereum protocol for private real-time finance 3 | repository: https://github.com/TomAFrech/NoteStream/packages/subgraph 4 | schema: 5 | file: ./schema.graphql 6 | dataSources: 7 | - kind: ethereum/contract 8 | name: NoteStream 9 | network: {{networkName}} 10 | source: 11 | abi: NoteStream 12 | address: '{{contracts.NoteStream.address}}' 13 | startBlock: {{contracts.NoteStream.startBlock}} 14 | mapping: 15 | kind: ethereum/events 16 | apiVersion: 0.0.3 17 | abis: 18 | - name: NoteStream 19 | file: ./abis/NoteStream.json 20 | entities: 21 | - Cancellation 22 | - Stream 23 | - Transaction 24 | - Withdrawal 25 | - ZkAsset 26 | eventHandlers: 27 | - event: CreateStream(indexed uint256,indexed address,indexed address,address,bytes32,uint256,uint256) 28 | handler: handleCreateStream 29 | - event: WithdrawFromStream(indexed uint256,indexed address,indexed address,bytes32,uint256) 30 | handler: handleWithdrawFromStream 31 | - event: CancelStream(indexed uint256,indexed address,indexed address,uint256) 32 | handler: handleCancelStream 33 | file: ./src/mappings/noteStream.ts 34 | language: wasm/assemblyscript -------------------------------------------------------------------------------- /packages/subgraph/templatify.js: -------------------------------------------------------------------------------- 1 | const Handlebars = require('handlebars'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const yaml = require('js-yaml'); 5 | 6 | const { t } = require('typy'); 7 | 8 | function getNetworkNameForSubgraph() { 9 | switch (process.env.SUBGRAPH) { 10 | case undefined: 11 | case 'tomafrench/notestream': 12 | return 'mainnet'; 13 | case 'tomafrench/notestream-goerli': 14 | return 'goerli'; 15 | case 'tomafrench/notestream-kovan': 16 | return 'kovan'; 17 | case 'tomafrench/notestream-rinkeby': 18 | return 'rinkeby'; 19 | case 'tomafrench/notestream-ropsten': 20 | return 'ropsten'; 21 | case 'tomafrench/notestream-local': 22 | return 'local'; 23 | default: 24 | return null; 25 | } 26 | } 27 | 28 | (async () => { 29 | const networksFilePath = path.join(__dirname, 'networks.yaml'); 30 | const networks = yaml.load( 31 | await fs.readFile(networksFilePath, { encoding: 'utf-8' }), 32 | ); 33 | 34 | const networkName = process.env.NETWORK_NAME || getNetworkNameForSubgraph(); 35 | const network = t(networks, networkName).safeObject; 36 | if (t(network).isFalsy) { 37 | throw new Error( 38 | 'Please set either a "NETWORK_NAME" or a "SUBGRAPH" environment variable', 39 | ); 40 | } 41 | 42 | const subgraphTemplateFilePath = path.join( 43 | __dirname, 44 | 'subgraph.template.yaml', 45 | ); 46 | const source = await fs.readFile(subgraphTemplateFilePath, 'utf-8'); 47 | const template = Handlebars.compile(source); 48 | const result = template(network); 49 | await fs.writeFile(path.join(__dirname, 'subgraph.yaml'), result); 50 | 51 | console.log('🎉 subgraph.yaml successfully generated'); 52 | })(); 53 | -------------------------------------------------------------------------------- /packages/subgraph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "noImplicitAny": true, 5 | "noImplicitReturns": true, 6 | "noImplicitThis": true, 7 | "noEmitOnError": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "Node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/subgraph/updateAddresses.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const yaml = require('js-yaml'); 4 | 5 | const { t } = require('typy'); 6 | 7 | function getNetworkNameForSubgraph() { 8 | switch (process.env.SUBGRAPH) { 9 | case undefined: 10 | case 'tomafrench/notestream': 11 | return 'mainnet'; 12 | case 'tomafrench/notestream-goerli': 13 | return 'goerli'; 14 | case 'tomafrench/notestream-kovan': 15 | return 'kovan'; 16 | case 'tomafrench/notestream-rinkeby': 17 | return 'rinkeby'; 18 | case 'tomafrench/notestream-ropsten': 19 | return 'ropsten'; 20 | case 'tomafrench/notestream-local': 21 | return 'local'; 22 | default: 23 | return null; 24 | } 25 | } 26 | 27 | (async () => { 28 | const networkName = process.env.NETWORK_NAME || getNetworkNameForSubgraph(); 29 | const addressesDirectory = path.join( 30 | __dirname, 31 | '../contract-artifacts/addresses/', 32 | ); 33 | const addressesFilePath = path.join( 34 | addressesDirectory, 35 | `${networkName}.json`, 36 | ); 37 | const addresses = JSON.parse( 38 | await fs.readFile(addressesFilePath, { encoding: 'utf-8' }), 39 | ); 40 | 41 | const networksFilePath = path.join(__dirname, 'networks.yaml'); 42 | const networks = yaml.load( 43 | await fs.readFile(networksFilePath, { encoding: 'utf-8' }), 44 | ); 45 | 46 | const network = t(networks, networkName).safeObject; 47 | if (t(network).isFalsy) { 48 | throw new Error( 49 | 'Please set either a "NETWORK_NAME" or a "SUBGRAPH" environment variable', 50 | ); 51 | } 52 | 53 | network.contracts.NoteStream.address = addresses.NoteStream; 54 | await fs.writeFile(networksFilePath, yaml.safeDump(networks)); 55 | 56 | console.log('🎉 networks.yaml successfully updated'); 57 | })(); 58 | --------------------------------------------------------------------------------