├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .pnpm-debug.log ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── contracts ├── .env.SAMPLE ├── .gitignore ├── .gitkeep ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── README.md ├── contracts │ ├── CollabSplitter.sol │ ├── CollabSplitterFactory.sol │ ├── CollabSplitterFactory │ │ └── CollabSplitterFactoryStorage.sol │ └── mocks │ │ └── ERC20Mock.sol ├── deploy │ ├── 00_deploy_collab_splitter.js │ └── 01_deploy_collab_splitter_factory.js ├── deployments │ ├── mainnet │ │ ├── .chainId │ │ ├── CollabSplitter.json │ │ ├── CollabSplitterFactory.json │ │ ├── CollabSplitterFactory_Implementation.json │ │ ├── CollabSplitterFactory_Proxy.json │ │ ├── DefaultProxyAdmin.json │ │ └── solcInputs │ │ │ ├── 1635d55d57a0a2552952c0d22586ed23.json │ │ │ └── 60f67688df512955030de0b2c492b991.json │ └── rinkeby │ │ ├── .chainId │ │ ├── CollabSplitter.json │ │ ├── CollabSplitterFactory.json │ │ ├── CollabSplitterFactory_Implementation.json │ │ ├── CollabSplitterFactory_Proxy.json │ │ ├── DefaultProxyAdmin.json │ │ └── solcInputs │ │ ├── 1635d55d57a0a2552952c0d22586ed23.json │ │ └── 60f67688df512955030de0b2c492b991.json ├── hardhat.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── scripts │ └── local-graph.js └── test │ └── full-contract.js ├── doc └── collab-splitter-use-cases.png ├── graph ├── .gitignore ├── README.md ├── reset.sh ├── setup.sh ├── splitter │ ├── abis │ │ ├── CollabSplitter.json │ │ ├── CollabSplitterFactory.json │ │ └── ERC20.json │ ├── build │ │ ├── CollabSplitter │ │ │ └── abis │ │ │ │ └── CollabSplitter.json │ │ ├── CollabSplitterFactory │ │ │ ├── CollabSplitterFactory.wasm │ │ │ └── abis │ │ │ │ └── CollabSplitterFactory.json │ │ ├── CollabSplitterTokenPayment │ │ │ └── abis │ │ │ │ └── ERC20.json │ │ ├── schema.graphql │ │ └── subgraph.yaml │ ├── config │ │ ├── mainnet.json │ │ └── rinkeby.json │ ├── generated │ │ ├── CollabSplitterFactory │ │ │ └── CollabSplitterFactory.ts │ │ ├── CollabSplitterTokenPayment │ │ │ └── ERC20.ts │ │ ├── schema.ts │ │ ├── templates.ts │ │ └── templates │ │ │ └── CollabSplitter │ │ │ └── CollabSplitter.ts │ ├── package-lock.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── schema.graphql │ ├── src │ │ ├── constants.ts │ │ ├── mapping.ts │ │ └── providers │ │ │ ├── accounts.ts │ │ │ ├── erc20s.ts │ │ │ └── splitters.ts │ ├── subgraph.template.yaml │ ├── subgraph.yaml │ └── yarn.lock ├── start.sh └── tsconfig.json ├── package-lock.json ├── package.json ├── sdk ├── .gitignore ├── .gitkeep ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src │ ├── data │ │ └── abis │ │ │ ├── factory.js │ │ │ └── splitter.js │ ├── main.js │ ├── modules │ │ ├── collaboration.js │ │ ├── merkleproof.js │ │ └── thegraph.js │ └── utils │ │ └── numbers.js └── test │ ├── modules │ ├── collaboration.test.js │ ├── merkleproof.test.js │ └── thegraph.test.js │ └── utils │ └── numbers.test.js └── website ├── .env ├── .env.SAMPLE ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── app.html ├── app.postcss ├── css │ ├── base.css │ ├── common.css │ └── vars.css ├── global.d.ts ├── lib │ ├── components │ │ ├── Footer.svelte │ │ ├── Form.svelte │ │ ├── Header.svelte │ │ ├── Loading.svelte │ │ ├── Modals │ │ │ ├── AddRecipientModal.svelte │ │ │ ├── BaseModal.svelte │ │ │ └── CreationSuccessModal.svelte │ │ ├── Network │ │ │ └── OnlyKnownNetwork.svelte │ │ ├── OnlyConnected.svelte │ │ └── SVGs.svelte │ ├── data │ │ └── networks.ts │ ├── modules │ │ ├── network.ts │ │ ├── provider.js │ │ ├── variables.ts │ │ └── wallet.js │ └── utils │ │ ├── clipboard.ts │ │ └── utils.ts └── routes │ ├── __layout.svelte │ ├── collab │ ├── [id].svelte │ └── __layout.svelte │ ├── dashboard.svelte │ └── index.svelte ├── static ├── favicon.png └── images │ └── icons │ └── close-circle.svg ├── svelte.config.js ├── tailwind.config.cjs └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | codecov: codecov/codecov@3.1.1 5 | 6 | executors: 7 | contracts: 8 | docker: 9 | - image: cimg/node:lts 10 | 11 | jobs: 12 | build-and-test: 13 | environment: 14 | ENABLE_GAS_REPORT: "true" 15 | executor: contracts 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | keys: 20 | - v1-sdk-deps-{{ checksum "sdk/package-lock.json" }} 21 | - v1-sdk-deps- 22 | - restore_cache: 23 | keys: 24 | - v1-contracts-deps-{{ checksum "contracts/package-lock.json" }} 25 | - v1-contracts-deps- 26 | - run: 27 | name: Build & test SDK 28 | command: | 29 | cd sdk 30 | npm install 31 | npm run build 32 | npm run coverage 33 | - run: 34 | name: Test sol contracts 35 | command: | 36 | cd contracts 37 | npm install 38 | npm run coverage 39 | - save_cache: 40 | key: v1-sdk-deps-{{ checksum "sdk/package-lock.json" }} 41 | paths: 42 | - sdk/node_modules 43 | - save_cache: 44 | key: v1-contracts-deps-{{ checksum "contracts/package-lock.json" }} 45 | paths: 46 | - contracts/node_modules 47 | - store_test_results: 48 | path: ./contracts/coverage.json 49 | - store_test_results: 50 | path: ./sdk/coverage/coverage-final.json 51 | - codecov/upload: 52 | file: "./contracts/coverage.json" 53 | flags: "contracts" 54 | - codecov/upload: 55 | file: "./sdk/coverage/coverage-final.json" 56 | flags: "sdk" 57 | workflows: 58 | contracts: 59 | jobs: 60 | - build-and-test 61 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://gitcoin.co/grants/3631/stendhals-tools'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | { 2 | "0 debug pnpm:scope": { 3 | "selected": 1 4 | }, 5 | "1 error pnpm": { 6 | "code": "ERR_PNPM_NO_SCRIPT", 7 | "err": { 8 | "name": "pnpm", 9 | "message": "Missing script: dev", 10 | "code": "ERR_PNPM_NO_SCRIPT", 11 | "stack": "pnpm: Missing script: dev\n at Object.handler (/home/dievardump/.local/share/pnpm-global/5/node_modules/.pnpm/pnpm@6.22.2/node_modules/pnpm/dist/pnpm.cjs:129940:15)\n at async /home/dievardump/.local/share/pnpm-global/5/node_modules/.pnpm/pnpm@6.22.2/node_modules/pnpm/dist/pnpm.cjs:134121:20\n at async run (/home/dievardump/.local/share/pnpm-global/5/node_modules/.pnpm/pnpm@6.22.2/node_modules/pnpm/dist/pnpm.cjs:134095:34)\n at async runPnpm (/home/dievardump/.local/share/pnpm-global/5/node_modules/.pnpm/pnpm@6.22.2/node_modules/pnpm/dist/pnpm.cjs:134307:5)\n at async /home/dievardump/.local/share/pnpm-global/5/node_modules/.pnpm/pnpm@6.22.2/node_modules/pnpm/dist/pnpm.cjs:134299:7" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to collab-splitter 2 | 3 | This document contains some tips on how to collaborate in this project. 4 | 5 | ## Filing an issue 6 | 7 | If you find a bug or want to propose a new feature, please [open an issue](https://github.com/stendhal-labs/collab-splitter/issues/new). 8 | Pull requests are welcome, but we recommend you discuss it in an issue first, especially for big changes. This will increase the odds that we can accept your PR. 9 | 10 | ## Project structure 11 | 12 | This repository is a monorepo with the following subfolders: 13 | 14 | - **Solidity smart contracts** (in [`contracts/`](contracts/)) 15 | - **The Graph** (in [`graph/`](graph/)) for indexing blockchain events 16 | - **SDK** (in [`sdk/`](sdk/)) a small lib for using MerkleTree, Root and Proof for collaborations with `ethers.js` 17 | - **Website** (in [`website/`](website/)) the frond-end written in Svelte 18 | 19 | ## Installing 20 | 21 | To install the project's dependencies, run `npm install` in the root directory of the repository. 22 | 23 | ## Building & testing the projects 24 | 25 | Into each subfolders follow the `README.md` instructions to get everything runnning. 26 | 27 | ## Code formatting 28 | 29 | We use [Prettier](https://prettier.io/) to format all the code without any special configuration. Whatever Prettier does is considered The Right Thing. It's completely fine to commit non-prettied code and then reformat it in a later commit. 30 | 31 | We also have [eslint](https://eslint.org/) installed in all the projects. It checks that you have run Prettier and forbids some dangerous patterns. 32 | 33 | The linter is always run in the CI, so make sure it passes before pushing code. You can use `pnpm lint` and `pnpm lint:fix` inside the packages' folders. 34 | 35 | ## Branching 36 | 37 | We work on the branch [`main`](https://github.com/stendhal-labs/collab-splitter/tree/main). Versions of the different packages are always tagged and pushed to GitHub. So if you are looking for the latests released version of something, please refer to the tags. 38 | 39 | Please, branch from `main` when implementing a new feature or fixing a bug, and use it as the base branch in pull requests. 40 | 41 | ## Common errors 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Stendhal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Discord](https://img.shields.io/discord/778527994451853333) 2 | [![CircleCI](https://circleci.com/gh/stendhal-labs/collab-splitter/tree/main.svg?style=svg)](https://circleci.com/gh/stendhal-labs/collab-splitter/tree/main) 3 | [![codecov](https://codecov.io/gh/stendhal-labs/collab-splitter/branch/main/graph/badge.svg)](https://codecov.io/gh/stendhal-labs/collab-splitter) 4 | [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/stendhal-labs/collab-splitter/blob/main/LICENSE) 5 | 6 | # Collab splitter 7 | 8 | Collaboration Splitter allows to create a cheap contract in charge of 9 | receiving and splitting Ethereum and ERC20 payments. It can be used to 10 | split earnings from artworks sales if multiple artists were involved or as 11 | the recipient of royalties compatible with the new [EIP-2981: NFT Royalty Standard](https://eips.ethereum.org/EIPS/eip-2981) 12 | 13 | Available on https://www.collab-splitter.org/ 14 | 15 | The project has several components: 16 | 17 | - **Smartcontracts** (in [`contracts/`](contracts/)) 18 | - **The Graph** (in [`graph/`](graph/)) 19 | - **SDK** (in [`sdk/`](sdk/)) 20 | - **Website** (in [`website/`](website/)) 21 | 22 | ## Usage 23 | 24 | Use the SDK or create your contract directly [on the website](https://www.collab-splitter.org/) 25 | 26 | ![Use cases](doc/collab-splitter-use-cases.png) 27 | 28 | ### Create a collaboration splitter 29 | 30 | In order to create a splitter contract you will need: 31 | 32 | - An Ethereum wallet with some ethers (only needed by the splitter creator) 33 | - The list of recipients and their allocations e.g. 13.37% 34 | - A name for your collaboration 35 | 36 | Fill out the form, click on the 'Create' button and once your transaction is confirmed and the contract deployed, you will get the address to use for your NFT creations. 37 | 38 | ### See a collaboration / get info 39 | 40 | On this page you can see all the information about a deployed collab splitter. 41 | 42 | If you connect your wallet, you will be able to see if you can claim ETH or ERC20. 43 | 44 | https://purple-disk-1468.on.fleek.co/collab/0x 45 | 46 | ### Claim 47 | 48 | You can claim **ETH** or the listed **ERC20** from incoming transactions. 49 | 50 | You can also claim **individually** or **for all** the contract recipients. 51 | 52 | ## Gas costs 53 | 54 | A full gas report can be obtained in `./contracts` by running `npm run gas-report`. 55 | At the current time November 2021, and at 3682.73 eur/eth, all operations in average costs **around or less than 10 Euro**. 56 | But here are some useful info extracted from it: 57 | 58 | | Operation | Average gas cost @21gwei/gas | 59 | | -------------------------------------------- | ---------------------------- | 60 | | Collab Splitter Contract Creation (one time) | 146042 | 61 | | Claim all ETH and all ERC20 tokens | 130923 | 62 | | Claim ETH | 78647 | 63 | | Claim tokens of an ERC20 e.g. DAI | 137960 | 64 | 65 | Please note that gas costs for claiming ERC20 tokens can increase depending on their contract and implementation. 66 | 67 | If you want to help us reduce the gas costs, all suggestions/PR are more than welcomed ! 68 | 69 | ## FAQ 70 | 71 | > Is there a limit on the number of recipients ? 72 | 73 | Simple answer: **No** 74 | 75 | > How are gas fees handled? 76 | 77 | There are 2 potential sources of gas fees as a collaborator: 78 | 79 | - Gas fees related to the creation of the collab splitter contract that are paid only once by one of the artists (or even someone else) 80 | - Gas fees related to claims done by every collaborator when they want to transfert their funds 81 | 82 | Receiving ETH or ERC20 as sales revenues or royalties will always be paid by sender so 0 gas fees for a collaborator ! 83 | 84 | > How much decimals can I set for each split ? 85 | 86 | You can go down to 0.01%. We chose the .00 precision to make it easy to use as a percentage and various technical reasons. 87 | 88 | > Can I try it ? 89 | 90 | Yes of course, you can test all the features on the **Rinkeby** test network on https://rinkeby.collab-splitter.org/ 91 | 92 | Don't forget to get some Rinkeby ETH from the faucet https://faucet.rinkeby.io/ 93 | 94 | ## Installation 95 | 96 | Simply run `npm run install:all` or ` npm run install:all:pnpm` if you are using [pnpm](https://pnpm.io/). 97 | 98 | This will install all the necessary `node_modules`, to run each component individually please see their respective `README.md` files. 99 | 100 | ## Contributing 101 | 102 | Contributions are always welcome! Feel free to open any issue or send a pull request. 103 | Check out the [contribution guide](CONTRIBUTING.md/) to learn about how to set up ! 104 | 105 | ## License 106 | 107 | Collab splitter is released under the [MIT License](LICENSE). 108 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # Setting coverage targets per flag 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: 85% #overall project/ repo coverage 7 | contracts: 8 | target: 80% 9 | flags: 10 | - contracts 11 | sdk: 12 | target: 90% 13 | flags: 14 | - sdk 15 | 16 | # adding Flags to your `layout` configuration to show up in the PR comment 17 | comment: 18 | layout: "reach, diff, flags, files" 19 | behavior: default 20 | require_changes: false 21 | require_base: yes 22 | require_head: yes 23 | branches: null 24 | show_carryforward_flags: yes 25 | 26 | # New root YAML section = `flags:` 27 | # This is where you would define every flag from your 28 | # uploader, and update when new Flags added 29 | 30 | flags: 31 | contracts: 32 | paths: 33 | - contracts/src/ 34 | carryforward: false 35 | sdk: 36 | paths: 37 | - sdk/src/ 38 | carryforward: true -------------------------------------------------------------------------------- /contracts/.env.SAMPLE: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # this file is loaded after .env 3 | # and overrides the keys declared in it 4 | ####################################### 5 | 6 | ## etherscan api key 7 | ETHERSCAN_API_KEY= 8 | 9 | # PROVIDER 10 | PROVIDER= 11 | 12 | # RINKEBY DEPLOYER 13 | DEPLOYER_PKEY= 14 | 15 | ## Coinmarketcap api key for gas costs 16 | COINMARKETCAP_API_KEY= -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | #Hardhat files 4 | cache 5 | artifacts 6 | 7 | .env* 8 | !.env.SAMPLE 9 | 10 | deployments/localhost 11 | 12 | coverage 13 | coverage.json -------------------------------------------------------------------------------- /contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stendhal-labs/collab-splitter/27aaec728c5f1e5e9bd58126628d4be3477784d8/contracts/.gitkeep -------------------------------------------------------------------------------- /contracts/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /contracts/.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['mocks/ERC20Mock.sol'], 3 | }; 4 | -------------------------------------------------------------------------------- /contracts/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } -------------------------------------------------------------------------------- /contracts/README.md: -------------------------------------------------------------------------------- 1 | # Collaboration splitter smarcontracts 2 | 3 | Smartcontracts for collaboration splitters using Solidity `0.8.9` and [Hardhat](https://hardhat.org/) 4 | 5 | The most important files are : 6 | 7 | - `CollabSplitterFactory` which allow the creation and deployment of a CollabSplitter contract from user transaction. 8 | - `CollabSplitter` the minimal proxy contract receiving ETH or ERC20 to be split accross recipients. 9 | 10 | The allocations & associated recipients are described with a **Merkle Root** to greatly reduce gas comsumption. 11 | 12 | ## Installation 13 | 14 | ``` 15 | cd contracts 16 | npm install 17 | ``` 18 | 19 | Then copy the `env.SAMPLE` file to .env and replace your variables. 20 | 21 | ## Test 22 | 23 | `npm run test` 24 | 25 | ### Coverage 26 | 27 | See coverage using `npm run coverage` 28 | 29 | ### Gas reporter 30 | 31 | If you want to know more about gas usage, you can run `npm run gas-report` 32 | 33 | ## Deployments 34 | 35 | The `CollabSplitterFactory` has been deployed to the following network. 36 | 37 | | Network | Address | 38 | | ------- | ----------------------------------------------------------------------------------------------------------------------------- | 39 | | Rinbeky | [0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6](https://rinkeby.etherscan.io/address/0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6) | 40 | | Mainnet | [0x486E4CCd2970C1971f41AA16EEFf078f821F3E9a](https://etherscan.io/address/0x486e4ccd2970c1971f41aa16eeff078f821f3e9a) | 41 | -------------------------------------------------------------------------------- /contracts/contracts/CollabSplitterFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import '@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol'; 5 | import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; 6 | 7 | import './CollabSplitterFactory/CollabSplitterFactoryStorage.sol'; 8 | import './CollabSplitter.sol'; 9 | 10 | /// @title CollabSplitterFactory 11 | /// @author Simon Fremaux (@dievardump) 12 | /// @notice This contract allows people to create a "Splitter" -> a contract that will 13 | /// allow to split the ETH or ERC20 it received, between several addresses 14 | /// This contract is upgradeable, because we might have to add functionalities 15 | /// or versioning over time. 16 | /// However, the Factory has no authority over a Splitter after it's created 17 | /// which ensure that updates to the current contract 18 | /// won't create any problems / exploits on existing Splitter 19 | contract CollabSplitterFactory is 20 | OwnableUpgradeable, 21 | CollabSplitterFactoryStorage 22 | { 23 | using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; 24 | 25 | // emitted when a splitter contract is created 26 | event SplitterCreated( 27 | address indexed splitter, 28 | string name, 29 | address[] recipients, 30 | uint256[] amounts 31 | ); 32 | 33 | constructor() {} 34 | 35 | function initialize(address splitterImplementation, address owner_) 36 | external 37 | initializer 38 | { 39 | _setSplitterImplementation(splitterImplementation); 40 | 41 | if (owner_ != address(0)) { 42 | transferOwnership(owner_); 43 | } 44 | } 45 | 46 | /// @notice Getter for the Splitter Implementation 47 | function getSplitterImplementation() public view returns (address) { 48 | return _splitterImplementation; 49 | } 50 | 51 | /// @notice Creates a new CollabSplitter contract 52 | /// @dev the contract created is a minimal proxy to the _splitterImplementation 53 | /// the list of recipients (and the corresponding amounts) should then be used in the exact same order 54 | /// to create the merkleProof and merkleRoot 55 | /// @param name_ name of the Splitter (for convenience) 56 | /// @param merkleRoot merkle root of the tree of recipients 57 | /// @param recipients list of recipients 58 | /// @param amounts list of amounts 59 | /// @return newContract the address of the new contract 60 | function createSplitter( 61 | string memory name_, 62 | bytes32 merkleRoot, 63 | address[] memory recipients, 64 | uint256[] memory amounts 65 | ) external payable returns (address newContract) { 66 | require(_splitterImplementation != address(0), '!NO_IMPLEMENTATION!'); 67 | 68 | require(recipients.length == amounts.length, '!LENGTH_MISMATCH!'); 69 | 70 | uint256 total; 71 | for (uint256 i; i < amounts.length; i++) { 72 | require(amounts[i] != 0, '!NO_NULL_VALUE!'); 73 | total += amounts[i]; 74 | } 75 | 76 | require(total == 10000, '!VALUE_MUST_BE_100!'); 77 | 78 | // create minimal proxy to _splitterImplementation 79 | newContract = ClonesUpgradeable.clone(_splitterImplementation); 80 | 81 | // initialize the non upgradeable proxy 82 | CollabSplitter(payable(newContract)).initialize(merkleRoot); 83 | 84 | // emit an event with all the data needed to reconstruct later the merkle tree 85 | // and allow people to claim their eth / tokens 86 | // using events will allow to store everything in TheGraph (or similar) in a decentralized way 87 | // while still be less expensive than storing in the CollabSplitter storage 88 | emit SplitterCreated(newContract, name_, recipients, amounts); 89 | } 90 | 91 | /// @notice Setter for the Splitter Implementation 92 | /// @param implementation the address to proxy calls to 93 | function setSplitterImplementation(address implementation) 94 | public 95 | onlyOwner 96 | { 97 | _setSplitterImplementation(implementation); 98 | } 99 | 100 | /// @dev internal setter for the Splitter Implementation 101 | /// @param implementation the address to proxy calls to 102 | function _setSplitterImplementation(address implementation) internal { 103 | _splitterImplementation = implementation; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /contracts/contracts/CollabSplitterFactory/CollabSplitterFactoryStorage.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import '@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol'; 5 | 6 | /// @title CollabSplitterFactoryStorage 7 | /// @author Simon Fremaux (@dievardump) 8 | contract CollabSplitterFactoryStorage { 9 | // current Splitter implementation 10 | address internal _splitterImplementation; 11 | 12 | // gap 13 | uint256[50] private __gap; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/contracts/mocks/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 5 | 6 | // mock class using ERC20 7 | contract ERC20Mock is ERC20 { 8 | constructor(string memory name, string memory symbol) 9 | payable 10 | ERC20(name, symbol) 11 | {} 12 | 13 | function mint(address account, uint256 amount) public { 14 | _mint(account, amount); 15 | } 16 | 17 | function burn(address account, uint256 amount) public { 18 | _burn(account, amount); 19 | } 20 | 21 | function transferInternal( 22 | address from, 23 | address to, 24 | uint256 value 25 | ) public { 26 | _transfer(from, to, value); 27 | } 28 | 29 | function approveInternal( 30 | address owner, 31 | address spender, 32 | uint256 value 33 | ) public { 34 | _approve(owner, spender, value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/deploy/00_deploy_collab_splitter.js: -------------------------------------------------------------------------------- 1 | // deploy/00_deploy_my_contract.js 2 | module.exports = async ({ getNamedAccounts, deployments }) => { 3 | const { deploy } = deployments; 4 | const { deployer, signer } = await getNamedAccounts(); 5 | 6 | await deploy('CollabSplitter', { 7 | from: deployer, 8 | log: true, 9 | }); 10 | }; 11 | module.exports.tags = ['CollabSplitter']; 12 | -------------------------------------------------------------------------------- /contracts/deploy/01_deploy_collab_splitter_factory.js: -------------------------------------------------------------------------------- 1 | // deploy/00_deploy_my_contract.js 2 | module.exports = async ({ getNamedAccounts, deployments }) => { 3 | const { deploy } = deployments; 4 | const { deployer, signer } = await getNamedAccounts(); 5 | 6 | const CollabSplitter = await deployments.get('CollabSplitter'); 7 | 8 | await deploy('CollabSplitterFactory', { 9 | from: deployer, 10 | proxy: { 11 | proxyContract: 'OpenZeppelinTransparentProxy', 12 | execute: { 13 | init: { 14 | methodName: 'initialize', 15 | args: [ 16 | CollabSplitter.address, 17 | ethers.constants.AddressZero, 18 | ], 19 | }, 20 | }, 21 | }, 22 | log: true, 23 | }); 24 | }; 25 | module.exports.tags = ['CollabSplitterFactory']; 26 | -------------------------------------------------------------------------------- /contracts/deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /contracts/deployments/rinkeby/.chainId: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /contracts/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-ethers'); 2 | require('@nomiclabs/hardhat-waffle'); 3 | require('hardhat-deploy'); 4 | require('hardhat-deploy-ethers'); 5 | require('hardhat-tracer'); 6 | require('@nomiclabs/hardhat-etherscan'); 7 | require('hardhat-gas-reporter'); 8 | require('solidity-coverage'); 9 | require('@nomiclabs/hardhat-solhint'); 10 | const dotenv = require('dotenv'); 11 | const minimist = require('minimist'); 12 | 13 | function mergeConfigs(path) { 14 | const { parsed } = dotenv.config({ 15 | path, 16 | }); 17 | 18 | Object.keys(parsed).forEach((key) => { 19 | if ('' !== parsed[key]) { 20 | process.env[key] = parsed[key]; 21 | } 22 | }); 23 | } 24 | 25 | // load .env 26 | dotenv.config(); 27 | // override .env with specific .env.[network] 28 | var argv = minimist(process.argv.slice(2)); 29 | 30 | const networks = {}; 31 | if (argv.network && ['rinkeby', 'mainnet'].indexOf(argv.network) !== -1) { 32 | mergeConfigs(`.env.${argv.network}`); 33 | 34 | if (argv.network == 'rinkeby') { 35 | networks.rinkeby = { 36 | url: process.env.PROVIDER, 37 | accounts: [process.env.DEPLOYER_PKEY], 38 | }; 39 | } else if (argv.network == 'mainnet') { 40 | networks.mainnet = { 41 | url: process.env.PROVIDER, 42 | gasPrice: 58000000000, 43 | accounts: [process.env.DEPLOYER_PKEY], 44 | }; 45 | } 46 | } 47 | 48 | // You need to export an object to set up your config 49 | // Go to https://hardhat.org/config/ to learn more 50 | 51 | /** 52 | * @type import('hardhat/config').HardhatUserConfig 53 | */ 54 | module.exports = { 55 | solidity: { 56 | version: '0.8.9', 57 | settings: { 58 | optimizer: { 59 | enabled: true, 60 | runs: 200, 61 | }, 62 | }, 63 | }, 64 | networks, 65 | namedAccounts: { 66 | deployer: { 67 | default: 0, // here this will by default take the first account as deployer 68 | }, 69 | signer: { 70 | default: 1, // here this will by default take the second account as signer 71 | }, 72 | }, 73 | etherscan: { 74 | // Your API key for Etherscan 75 | // Obtain one at https://etherscan.io/ 76 | apiKey: process.env.ETHERSCAN_API_KEY, 77 | }, 78 | gasReporter: { 79 | enabled: process.env.ENABLE_GAS_REPORT ? true : false, 80 | gasPrice: 21, 81 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collabsplitter-contracts", 3 | "version": "0.0.1", 4 | "description": "Smart contract for the Collaboration Splitter Factory", 5 | "scripts": { 6 | "compile": "hardhat compile", 7 | "coverage": "env COVERAGE=true hardhat coverage", 8 | "deploy:local": "hardhat deploy --network localhost", 9 | "lint": "npm run lint:js && npm run lint:sol", 10 | "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", 11 | "lint:js": "eslint --ignore-path .gitignore .", 12 | "lint:js:fix": "eslint --ignore-path .gitignore . --fix", 13 | "lint:sol": "solhint 'contracts/**/*.sol' && prettier -c 'contracts/**/*.sol'", 14 | "lint:sol:fix": "prettier --write \"contracts/**/*.sol\"", 15 | "clean": "hardhat clean && rimraf build contracts/build", 16 | "prepare": "npm run clean && env COMPILE_MODE=production npm run compile", 17 | "test": "hardhat test", 18 | "gas-report": "env ENABLE_GAS_REPORT=true npm run test" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@nomiclabs/hardhat-ethers": "^2.0.2", 24 | "@nomiclabs/hardhat-etherscan": "^2.1.2", 25 | "@nomiclabs/hardhat-solhint": "^2.0.0", 26 | "@nomiclabs/hardhat-waffle": "^2.0.1", 27 | "@openzeppelin/contracts": "^4.2.0", 28 | "@openzeppelin/contracts-upgradeable": "^4.1.0", 29 | "chai": "^4.3.4", 30 | "dotenv": "^10.0.0", 31 | "eslint": "^6.5.1", 32 | "ethereum-waffle": "^3.3.0", 33 | "ethers": "^5.3.1", 34 | "hardhat": "^2.3.0", 35 | "hardhat-deploy": "^0.8.9", 36 | "hardhat-deploy-ethers": "*", 37 | "hardhat-gas-reporter": "^1.0.4", 38 | "hardhat-tracer": "*", 39 | "minimist": "^1.2.5", 40 | "solhint": "^3.3.6", 41 | "solidity-coverage": "^0.7.11" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/scripts/local-graph.js: -------------------------------------------------------------------------------- 1 | const { parseEther } = require('ethers/lib/utils'); 2 | const hre = require('hardhat'); 3 | // scripts/create-box.js 4 | const { deployments, ethers } = require('hardhat'); 5 | const sdk = require('../../sdk/dist/collab-splitter.umd.js'); 6 | 7 | async function main() { 8 | const { deployer } = await getNamedAccounts(); 9 | 10 | const recipients = [ 11 | '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', 12 | '0x28a8746e75304c0780E011BEd21C72cD78cd535E', 13 | '0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E', 14 | ]; 15 | 16 | const amounts = [5000, 4000, 1000]; 17 | 18 | const merkleRoot = sdk.getRoot( 19 | // creating the tree 20 | recipients.map((recipient, i) => ({ 21 | account: recipient, 22 | percent: amounts[i], 23 | })), 24 | ); 25 | 26 | const receipt = await deployments.execute( 27 | 'CollabSplitterFactory', 28 | { from: deployer, log: true }, 29 | 'createSplitter', 30 | 'Stendhal', 31 | merkleRoot, 32 | recipients, 33 | amounts, 34 | ); 35 | 36 | const splitterAddress = receipt.events[0].args[0]; 37 | 38 | // create an ERC20 Mock 39 | const ERC20Mock = await ethers.getContractFactory('ERC20Mock'); 40 | erc20Mock = await ERC20Mock.deploy('name', 'ticker'); 41 | await erc20Mock.deployed(); 42 | 43 | // mint some of this ERC20 to the CollabSplitter just created 44 | await erc20Mock.mint(splitterAddress, '10000000000000000000'); 45 | } 46 | 47 | main() 48 | .then(() => process.exit(0)) 49 | .catch((error) => { 50 | console.error(error); 51 | process.exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /doc/collab-splitter-use-cases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stendhal-labs/collab-splitter/27aaec728c5f1e5e9bd58126628d4be3477784d8/doc/collab-splitter-use-cases.png -------------------------------------------------------------------------------- /graph/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | graph-node -------------------------------------------------------------------------------- /graph/README.md: -------------------------------------------------------------------------------- 1 | This is the repository to create a Subgraph for **_Collab Splitter_** using [The Graph](https://thegraph.com/). 2 | 3 | # Installation 4 | 5 | After cloning this repository, install ganache-cli 6 | 7 | `npm install -g ganache-cli` 8 | 9 | and then you can either run `./setup.sh` or go to [The setup section](#setup) 10 | 11 | Then you will need to [start](#start) the the chain, [create and deploy](#create-and-deploy-the-graph) the graph 12 | 13 | The graph will now listen to the right contract events 14 | 15 | ## Start 16 | 17 | run `./start.sh` to run the ganache cli and the graph node 18 | 19 | The chain data will be stored in `./ganache-db` in order to be able to stop and restart at the same state 20 | 21 | ## Create and deploy the graph 22 | 23 | ``` 24 | cd splitter 25 | npm run create-local 26 | npm run deploy-local 27 | ``` 28 | 29 | # Manual setup (non-recommended) 30 | 31 | ## Graph-node 32 | 33 | `git clone https://github.com/graphprotocol/graph-node/` 34 | 35 | If you already have postgres or ipfs running you might want to stop them, TheGraph is not clever enough to launch them on unused ports... 36 | 37 | in a new terminal you will need to run these commands to setup TheGraph 38 | 39 | ``` 40 | cd graph-node/docker 41 | ./setup.sh 42 | ``` 43 | 44 | ## Running the app 45 | 46 | I do both next actions in separate terminals: 47 | 48 | First launch ganache chain 49 | 50 | `ganache-cli -h 0.0.0.0 --deterministic` 51 | 52 | Then launch TheGraph 53 | 54 | ``` 55 | cd graph-node/docker 56 | docker-compose up 57 | ``` 58 | 59 | You will also need to add data to the chain so the graph will be able to pick those up. 60 | In the `../contracts` repository, you can run: 61 | 62 | `npx hardhat run scripts/local-graph.js --network localhost` 63 | 64 | which will deploy a few contracts (SimpleSale, TransferProxy and 2 NFT contracts, add some NFTs to the contracts and do some sales, in order to populate this graph data) 65 | 66 | When the script is finished running, verify that the contracts address outputed are the same as the one in: `./splitter/subgraph.yaml` 67 | 68 | then you can: 69 | 70 | ``` 71 | cd splitter 72 | npm run codegen 73 | npm run build 74 | npm run create-local 75 | npm run deploy-local 76 | ``` 77 | 78 | ## Deploying the app 79 | 80 | In order to deploy the app, we will need: 81 | 82 | 1 - to create an app on TheGraph dashboard 83 | 84 | 2 - to set the same name in the deploying scripts in `package.json` 85 | 86 | 3 - to set the right contracts' `address`, `startBlock` and `network` in `./splitter/subgraph.yaml` 87 | 88 | We can then run: 89 | 90 | `npm run deploy` 91 | 92 | to deploy our graph. 93 | -------------------------------------------------------------------------------- /graph/reset.sh: -------------------------------------------------------------------------------- 1 | sudo rm -rf graph-node 2 | 3 | ./setup.sh 4 | 5 | ./start.sh 6 | -------------------------------------------------------------------------------- /graph/setup.sh: -------------------------------------------------------------------------------- 1 | npm install -g ganache-cli 2 | 3 | git clone https://github.com/graphprotocol/graph-node/ 4 | cd graph-node/docker 5 | ./setup.sh -------------------------------------------------------------------------------- /graph/splitter/abis/CollabSplitter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "operator", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "address", 14 | "name": "account", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "amount", 21 | "type": "uint256" 22 | }, 23 | { 24 | "indexed": false, 25 | "internalType": "address", 26 | "name": "token", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "ERC20Claimed", 31 | "type": "event" 32 | }, 33 | { 34 | "anonymous": false, 35 | "inputs": [ 36 | { 37 | "indexed": false, 38 | "internalType": "address", 39 | "name": "operator", 40 | "type": "address" 41 | }, 42 | { 43 | "indexed": false, 44 | "internalType": "address", 45 | "name": "account", 46 | "type": "address" 47 | }, 48 | { 49 | "indexed": false, 50 | "internalType": "uint256", 51 | "name": "amount", 52 | "type": "uint256" 53 | } 54 | ], 55 | "name": "ETHClaimed", 56 | "type": "event" 57 | }, 58 | { 59 | "inputs": [ 60 | { 61 | "internalType": "address", 62 | "name": "", 63 | "type": "address" 64 | } 65 | ], 66 | "name": "alreadyClaimed", 67 | "outputs": [ 68 | { 69 | "internalType": "uint256", 70 | "name": "", 71 | "type": "uint256" 72 | } 73 | ], 74 | "stateMutability": "view", 75 | "type": "function" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "account", 82 | "type": "address" 83 | }, 84 | { 85 | "internalType": "uint256", 86 | "name": "percent", 87 | "type": "uint256" 88 | }, 89 | { 90 | "internalType": "bytes32[]", 91 | "name": "merkleProof", 92 | "type": "bytes32[]" 93 | }, 94 | { 95 | "internalType": "address[]", 96 | "name": "erc20s", 97 | "type": "address[]" 98 | } 99 | ], 100 | "name": "claimBatch", 101 | "outputs": [], 102 | "stateMutability": "nonpayable", 103 | "type": "function" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "address", 109 | "name": "account", 110 | "type": "address" 111 | }, 112 | { 113 | "internalType": "uint256", 114 | "name": "percent", 115 | "type": "uint256" 116 | }, 117 | { 118 | "internalType": "bytes32[]", 119 | "name": "merkleProof", 120 | "type": "bytes32[]" 121 | }, 122 | { 123 | "internalType": "address[]", 124 | "name": "erc20s", 125 | "type": "address[]" 126 | } 127 | ], 128 | "name": "claimERC20", 129 | "outputs": [], 130 | "stateMutability": "nonpayable", 131 | "type": "function" 132 | }, 133 | { 134 | "inputs": [ 135 | { 136 | "internalType": "address", 137 | "name": "account", 138 | "type": "address" 139 | }, 140 | { 141 | "internalType": "uint256", 142 | "name": "percent", 143 | "type": "uint256" 144 | }, 145 | { 146 | "internalType": "bytes32[]", 147 | "name": "merkleProof", 148 | "type": "bytes32[]" 149 | } 150 | ], 151 | "name": "claimETH", 152 | "outputs": [], 153 | "stateMutability": "nonpayable", 154 | "type": "function" 155 | }, 156 | { 157 | "inputs": [ 158 | { 159 | "internalType": "address", 160 | "name": "", 161 | "type": "address" 162 | } 163 | ], 164 | "name": "erc20Data", 165 | "outputs": [ 166 | { 167 | "internalType": "uint256", 168 | "name": "totalReceived", 169 | "type": "uint256" 170 | }, 171 | { 172 | "internalType": "uint256", 173 | "name": "lastBalance", 174 | "type": "uint256" 175 | } 176 | ], 177 | "stateMutability": "view", 178 | "type": "function" 179 | }, 180 | { 181 | "inputs": [ 182 | { 183 | "internalType": "address[]", 184 | "name": "accounts", 185 | "type": "address[]" 186 | }, 187 | { 188 | "internalType": "uint256[]", 189 | "name": "percents", 190 | "type": "uint256[]" 191 | }, 192 | { 193 | "internalType": "address", 194 | "name": "token", 195 | "type": "address" 196 | } 197 | ], 198 | "name": "getBatchClaimableERC20", 199 | "outputs": [ 200 | { 201 | "internalType": "uint256[]", 202 | "name": "", 203 | "type": "uint256[]" 204 | } 205 | ], 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [ 211 | { 212 | "internalType": "address[]", 213 | "name": "accounts", 214 | "type": "address[]" 215 | }, 216 | { 217 | "internalType": "uint256[]", 218 | "name": "percents", 219 | "type": "uint256[]" 220 | } 221 | ], 222 | "name": "getBatchClaimableETH", 223 | "outputs": [ 224 | { 225 | "internalType": "uint256[]", 226 | "name": "", 227 | "type": "uint256[]" 228 | } 229 | ], 230 | "stateMutability": "view", 231 | "type": "function" 232 | }, 233 | { 234 | "inputs": [ 235 | { 236 | "internalType": "address", 237 | "name": "account", 238 | "type": "address" 239 | }, 240 | { 241 | "internalType": "address[]", 242 | "name": "tokens", 243 | "type": "address[]" 244 | } 245 | ], 246 | "name": "getBatchClaimed", 247 | "outputs": [ 248 | { 249 | "internalType": "uint256[]", 250 | "name": "", 251 | "type": "uint256[]" 252 | } 253 | ], 254 | "stateMutability": "view", 255 | "type": "function" 256 | }, 257 | { 258 | "inputs": [ 259 | { 260 | "internalType": "address", 261 | "name": "account", 262 | "type": "address" 263 | }, 264 | { 265 | "internalType": "uint256", 266 | "name": "percent", 267 | "type": "uint256" 268 | } 269 | ], 270 | "name": "getNode", 271 | "outputs": [ 272 | { 273 | "internalType": "bytes32", 274 | "name": "", 275 | "type": "bytes32" 276 | } 277 | ], 278 | "stateMutability": "pure", 279 | "type": "function" 280 | }, 281 | { 282 | "inputs": [ 283 | { 284 | "internalType": "bytes32", 285 | "name": "merkleRoot_", 286 | "type": "bytes32" 287 | } 288 | ], 289 | "name": "initialize", 290 | "outputs": [], 291 | "stateMutability": "nonpayable", 292 | "type": "function" 293 | }, 294 | { 295 | "inputs": [], 296 | "name": "merkleRoot", 297 | "outputs": [ 298 | { 299 | "internalType": "bytes32", 300 | "name": "", 301 | "type": "bytes32" 302 | } 303 | ], 304 | "stateMutability": "view", 305 | "type": "function" 306 | }, 307 | { 308 | "inputs": [], 309 | "name": "totalReceived", 310 | "outputs": [ 311 | { 312 | "internalType": "uint256", 313 | "name": "", 314 | "type": "uint256" 315 | } 316 | ], 317 | "stateMutability": "view", 318 | "type": "function" 319 | }, 320 | { 321 | "stateMutability": "payable", 322 | "type": "receive" 323 | } 324 | ] -------------------------------------------------------------------------------- /graph/splitter/abis/CollabSplitterFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "previousAdmin", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "address", 14 | "name": "newAdmin", 15 | "type": "address" 16 | } 17 | ], 18 | "name": "AdminChanged", 19 | "type": "event" 20 | }, 21 | { 22 | "anonymous": false, 23 | "inputs": [ 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "implementation", 28 | "type": "address" 29 | } 30 | ], 31 | "name": "Upgraded", 32 | "type": "event" 33 | }, 34 | { 35 | "stateMutability": "payable", 36 | "type": "fallback" 37 | }, 38 | { 39 | "inputs": [], 40 | "name": "admin", 41 | "outputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "", 45 | "type": "address" 46 | } 47 | ], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [ 53 | { 54 | "internalType": "address", 55 | "name": "newAdmin", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "changeAdmin", 60 | "outputs": [], 61 | "stateMutability": "nonpayable", 62 | "type": "function" 63 | }, 64 | { 65 | "inputs": [], 66 | "name": "implementation", 67 | "outputs": [ 68 | { 69 | "internalType": "address", 70 | "name": "", 71 | "type": "address" 72 | } 73 | ], 74 | "stateMutability": "nonpayable", 75 | "type": "function" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "newImplementation", 82 | "type": "address" 83 | } 84 | ], 85 | "name": "upgradeTo", 86 | "outputs": [], 87 | "stateMutability": "nonpayable", 88 | "type": "function" 89 | }, 90 | { 91 | "inputs": [ 92 | { 93 | "internalType": "address", 94 | "name": "newImplementation", 95 | "type": "address" 96 | }, 97 | { 98 | "internalType": "bytes", 99 | "name": "data", 100 | "type": "bytes" 101 | } 102 | ], 103 | "name": "upgradeToAndCall", 104 | "outputs": [], 105 | "stateMutability": "payable", 106 | "type": "function" 107 | }, 108 | { 109 | "stateMutability": "payable", 110 | "type": "receive" 111 | }, 112 | { 113 | "anonymous": false, 114 | "inputs": [ 115 | { 116 | "indexed": true, 117 | "internalType": "address", 118 | "name": "previousOwner", 119 | "type": "address" 120 | }, 121 | { 122 | "indexed": true, 123 | "internalType": "address", 124 | "name": "newOwner", 125 | "type": "address" 126 | } 127 | ], 128 | "name": "OwnershipTransferred", 129 | "type": "event" 130 | }, 131 | { 132 | "anonymous": false, 133 | "inputs": [ 134 | { 135 | "indexed": true, 136 | "internalType": "address", 137 | "name": "splitter", 138 | "type": "address" 139 | }, 140 | { 141 | "indexed": false, 142 | "internalType": "string", 143 | "name": "name", 144 | "type": "string" 145 | }, 146 | { 147 | "indexed": false, 148 | "internalType": "address[]", 149 | "name": "recipients", 150 | "type": "address[]" 151 | }, 152 | { 153 | "indexed": false, 154 | "internalType": "uint256[]", 155 | "name": "amounts", 156 | "type": "uint256[]" 157 | } 158 | ], 159 | "name": "SplitterCreated", 160 | "type": "event" 161 | }, 162 | { 163 | "inputs": [ 164 | { 165 | "internalType": "string", 166 | "name": "name_", 167 | "type": "string" 168 | }, 169 | { 170 | "internalType": "bytes32", 171 | "name": "merkleRoot", 172 | "type": "bytes32" 173 | }, 174 | { 175 | "internalType": "address[]", 176 | "name": "recipients", 177 | "type": "address[]" 178 | }, 179 | { 180 | "internalType": "uint256[]", 181 | "name": "amounts", 182 | "type": "uint256[]" 183 | } 184 | ], 185 | "name": "createSplitter", 186 | "outputs": [ 187 | { 188 | "internalType": "address", 189 | "name": "newContract", 190 | "type": "address" 191 | } 192 | ], 193 | "stateMutability": "payable", 194 | "type": "function" 195 | }, 196 | { 197 | "inputs": [], 198 | "name": "getSplitterImplementation", 199 | "outputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "", 203 | "type": "address" 204 | } 205 | ], 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [ 211 | { 212 | "internalType": "address", 213 | "name": "splitterImplementation", 214 | "type": "address" 215 | }, 216 | { 217 | "internalType": "address", 218 | "name": "owner_", 219 | "type": "address" 220 | } 221 | ], 222 | "name": "initialize", 223 | "outputs": [], 224 | "stateMutability": "nonpayable", 225 | "type": "function" 226 | }, 227 | { 228 | "inputs": [], 229 | "name": "owner", 230 | "outputs": [ 231 | { 232 | "internalType": "address", 233 | "name": "", 234 | "type": "address" 235 | } 236 | ], 237 | "stateMutability": "view", 238 | "type": "function" 239 | }, 240 | { 241 | "inputs": [], 242 | "name": "renounceOwnership", 243 | "outputs": [], 244 | "stateMutability": "nonpayable", 245 | "type": "function" 246 | }, 247 | { 248 | "inputs": [ 249 | { 250 | "internalType": "address", 251 | "name": "implementation", 252 | "type": "address" 253 | } 254 | ], 255 | "name": "setSplitterImplementation", 256 | "outputs": [], 257 | "stateMutability": "nonpayable", 258 | "type": "function" 259 | }, 260 | { 261 | "inputs": [ 262 | { 263 | "internalType": "address", 264 | "name": "newOwner", 265 | "type": "address" 266 | } 267 | ], 268 | "name": "transferOwnership", 269 | "outputs": [], 270 | "stateMutability": "nonpayable", 271 | "type": "function" 272 | }, 273 | { 274 | "inputs": [ 275 | { 276 | "internalType": "address", 277 | "name": "initialLogic", 278 | "type": "address" 279 | }, 280 | { 281 | "internalType": "address", 282 | "name": "initialAdmin", 283 | "type": "address" 284 | }, 285 | { 286 | "internalType": "bytes", 287 | "name": "_data", 288 | "type": "bytes" 289 | } 290 | ], 291 | "stateMutability": "payable", 292 | "type": "constructor" 293 | } 294 | ] -------------------------------------------------------------------------------- /graph/splitter/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "constant": true, 3 | "inputs": [], 4 | "name": "name", 5 | "outputs": [{ 6 | "name": "", 7 | "type": "string" 8 | }], 9 | "payable": false, 10 | "stateMutability": "view", 11 | "type": "function" 12 | }, 13 | { 14 | "constant": false, 15 | "inputs": [{ 16 | "name": "_spender", 17 | "type": "address" 18 | }, 19 | { 20 | "name": "_value", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "approve", 25 | "outputs": [{ 26 | "name": "", 27 | "type": "bool" 28 | }], 29 | "payable": false, 30 | "stateMutability": "nonpayable", 31 | "type": "function" 32 | }, 33 | { 34 | "constant": true, 35 | "inputs": [], 36 | "name": "totalSupply", 37 | "outputs": [{ 38 | "name": "", 39 | "type": "uint256" 40 | }], 41 | "payable": false, 42 | "stateMutability": "view", 43 | "type": "function" 44 | }, 45 | { 46 | "constant": false, 47 | "inputs": [{ 48 | "name": "_from", 49 | "type": "address" 50 | }, 51 | { 52 | "name": "_to", 53 | "type": "address" 54 | }, 55 | { 56 | "name": "_value", 57 | "type": "uint256" 58 | } 59 | ], 60 | "name": "transferFrom", 61 | "outputs": [{ 62 | "name": "", 63 | "type": "bool" 64 | }], 65 | "payable": false, 66 | "stateMutability": "nonpayable", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": true, 71 | "inputs": [], 72 | "name": "decimals", 73 | "outputs": [{ 74 | "name": "", 75 | "type": "uint8" 76 | }], 77 | "payable": false, 78 | "stateMutability": "view", 79 | "type": "function" 80 | }, 81 | { 82 | "constant": true, 83 | "inputs": [{ 84 | "name": "_owner", 85 | "type": "address" 86 | }], 87 | "name": "balanceOf", 88 | "outputs": [{ 89 | "name": "balance", 90 | "type": "uint256" 91 | }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": true, 98 | "inputs": [], 99 | "name": "symbol", 100 | "outputs": [{ 101 | "name": "", 102 | "type": "string" 103 | }], 104 | "payable": false, 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "constant": false, 110 | "inputs": [{ 111 | "name": "_to", 112 | "type": "address" 113 | }, 114 | { 115 | "name": "_value", 116 | "type": "uint256" 117 | } 118 | ], 119 | "name": "transfer", 120 | "outputs": [{ 121 | "name": "", 122 | "type": "bool" 123 | }], 124 | "payable": false, 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": true, 130 | "inputs": [{ 131 | "name": "_owner", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_spender", 136 | "type": "address" 137 | } 138 | ], 139 | "name": "allowance", 140 | "outputs": [{ 141 | "name": "", 142 | "type": "uint256" 143 | }], 144 | "payable": false, 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "payable": true, 150 | "stateMutability": "payable", 151 | "type": "fallback" 152 | }, 153 | { 154 | "anonymous": false, 155 | "inputs": [{ 156 | "indexed": true, 157 | "name": "owner", 158 | "type": "address" 159 | }, 160 | { 161 | "indexed": true, 162 | "name": "spender", 163 | "type": "address" 164 | }, 165 | { 166 | "indexed": false, 167 | "name": "value", 168 | "type": "uint256" 169 | } 170 | ], 171 | "name": "Approval", 172 | "type": "event" 173 | }, 174 | { 175 | "anonymous": false, 176 | "inputs": [{ 177 | "indexed": true, 178 | "name": "from", 179 | "type": "address" 180 | }, 181 | { 182 | "indexed": true, 183 | "name": "to", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": false, 188 | "name": "value", 189 | "type": "uint256" 190 | } 191 | ], 192 | "name": "Transfer", 193 | "type": "event" 194 | } 195 | ] -------------------------------------------------------------------------------- /graph/splitter/build/CollabSplitter/abis/CollabSplitter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "operator", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "address", 14 | "name": "account", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "amount", 21 | "type": "uint256" 22 | }, 23 | { 24 | "indexed": false, 25 | "internalType": "address", 26 | "name": "token", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "ERC20Claimed", 31 | "type": "event" 32 | }, 33 | { 34 | "anonymous": false, 35 | "inputs": [ 36 | { 37 | "indexed": false, 38 | "internalType": "address", 39 | "name": "operator", 40 | "type": "address" 41 | }, 42 | { 43 | "indexed": false, 44 | "internalType": "address", 45 | "name": "account", 46 | "type": "address" 47 | }, 48 | { 49 | "indexed": false, 50 | "internalType": "uint256", 51 | "name": "amount", 52 | "type": "uint256" 53 | } 54 | ], 55 | "name": "ETHClaimed", 56 | "type": "event" 57 | }, 58 | { 59 | "inputs": [ 60 | { 61 | "internalType": "address", 62 | "name": "", 63 | "type": "address" 64 | } 65 | ], 66 | "name": "alreadyClaimed", 67 | "outputs": [ 68 | { 69 | "internalType": "uint256", 70 | "name": "", 71 | "type": "uint256" 72 | } 73 | ], 74 | "stateMutability": "view", 75 | "type": "function" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "account", 82 | "type": "address" 83 | }, 84 | { 85 | "internalType": "uint256", 86 | "name": "percent", 87 | "type": "uint256" 88 | }, 89 | { 90 | "internalType": "bytes32[]", 91 | "name": "merkleProof", 92 | "type": "bytes32[]" 93 | }, 94 | { 95 | "internalType": "address[]", 96 | "name": "erc20s", 97 | "type": "address[]" 98 | } 99 | ], 100 | "name": "claimBatch", 101 | "outputs": [], 102 | "stateMutability": "nonpayable", 103 | "type": "function" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "address", 109 | "name": "account", 110 | "type": "address" 111 | }, 112 | { 113 | "internalType": "uint256", 114 | "name": "percent", 115 | "type": "uint256" 116 | }, 117 | { 118 | "internalType": "bytes32[]", 119 | "name": "merkleProof", 120 | "type": "bytes32[]" 121 | }, 122 | { 123 | "internalType": "address[]", 124 | "name": "erc20s", 125 | "type": "address[]" 126 | } 127 | ], 128 | "name": "claimERC20", 129 | "outputs": [], 130 | "stateMutability": "nonpayable", 131 | "type": "function" 132 | }, 133 | { 134 | "inputs": [ 135 | { 136 | "internalType": "address", 137 | "name": "account", 138 | "type": "address" 139 | }, 140 | { 141 | "internalType": "uint256", 142 | "name": "percent", 143 | "type": "uint256" 144 | }, 145 | { 146 | "internalType": "bytes32[]", 147 | "name": "merkleProof", 148 | "type": "bytes32[]" 149 | } 150 | ], 151 | "name": "claimETH", 152 | "outputs": [], 153 | "stateMutability": "nonpayable", 154 | "type": "function" 155 | }, 156 | { 157 | "inputs": [ 158 | { 159 | "internalType": "address", 160 | "name": "", 161 | "type": "address" 162 | } 163 | ], 164 | "name": "erc20Data", 165 | "outputs": [ 166 | { 167 | "internalType": "uint256", 168 | "name": "totalReceived", 169 | "type": "uint256" 170 | }, 171 | { 172 | "internalType": "uint256", 173 | "name": "lastBalance", 174 | "type": "uint256" 175 | } 176 | ], 177 | "stateMutability": "view", 178 | "type": "function" 179 | }, 180 | { 181 | "inputs": [ 182 | { 183 | "internalType": "address[]", 184 | "name": "accounts", 185 | "type": "address[]" 186 | }, 187 | { 188 | "internalType": "uint256[]", 189 | "name": "percents", 190 | "type": "uint256[]" 191 | }, 192 | { 193 | "internalType": "address", 194 | "name": "token", 195 | "type": "address" 196 | } 197 | ], 198 | "name": "getBatchClaimableERC20", 199 | "outputs": [ 200 | { 201 | "internalType": "uint256[]", 202 | "name": "", 203 | "type": "uint256[]" 204 | } 205 | ], 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [ 211 | { 212 | "internalType": "address[]", 213 | "name": "accounts", 214 | "type": "address[]" 215 | }, 216 | { 217 | "internalType": "uint256[]", 218 | "name": "percents", 219 | "type": "uint256[]" 220 | } 221 | ], 222 | "name": "getBatchClaimableETH", 223 | "outputs": [ 224 | { 225 | "internalType": "uint256[]", 226 | "name": "", 227 | "type": "uint256[]" 228 | } 229 | ], 230 | "stateMutability": "view", 231 | "type": "function" 232 | }, 233 | { 234 | "inputs": [ 235 | { 236 | "internalType": "address", 237 | "name": "account", 238 | "type": "address" 239 | }, 240 | { 241 | "internalType": "address[]", 242 | "name": "tokens", 243 | "type": "address[]" 244 | } 245 | ], 246 | "name": "getBatchClaimed", 247 | "outputs": [ 248 | { 249 | "internalType": "uint256[]", 250 | "name": "", 251 | "type": "uint256[]" 252 | } 253 | ], 254 | "stateMutability": "view", 255 | "type": "function" 256 | }, 257 | { 258 | "inputs": [ 259 | { 260 | "internalType": "address", 261 | "name": "account", 262 | "type": "address" 263 | }, 264 | { 265 | "internalType": "uint256", 266 | "name": "percent", 267 | "type": "uint256" 268 | } 269 | ], 270 | "name": "getNode", 271 | "outputs": [ 272 | { 273 | "internalType": "bytes32", 274 | "name": "", 275 | "type": "bytes32" 276 | } 277 | ], 278 | "stateMutability": "pure", 279 | "type": "function" 280 | }, 281 | { 282 | "inputs": [ 283 | { 284 | "internalType": "bytes32", 285 | "name": "merkleRoot_", 286 | "type": "bytes32" 287 | } 288 | ], 289 | "name": "initialize", 290 | "outputs": [], 291 | "stateMutability": "nonpayable", 292 | "type": "function" 293 | }, 294 | { 295 | "inputs": [], 296 | "name": "merkleRoot", 297 | "outputs": [ 298 | { 299 | "internalType": "bytes32", 300 | "name": "", 301 | "type": "bytes32" 302 | } 303 | ], 304 | "stateMutability": "view", 305 | "type": "function" 306 | }, 307 | { 308 | "inputs": [], 309 | "name": "totalReceived", 310 | "outputs": [ 311 | { 312 | "internalType": "uint256", 313 | "name": "", 314 | "type": "uint256" 315 | } 316 | ], 317 | "stateMutability": "view", 318 | "type": "function" 319 | }, 320 | { 321 | "stateMutability": "payable", 322 | "type": "receive" 323 | } 324 | ] -------------------------------------------------------------------------------- /graph/splitter/build/CollabSplitterFactory/CollabSplitterFactory.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stendhal-labs/collab-splitter/27aaec728c5f1e5e9bd58126628d4be3477784d8/graph/splitter/build/CollabSplitterFactory/CollabSplitterFactory.wasm -------------------------------------------------------------------------------- /graph/splitter/build/CollabSplitterFactory/abis/CollabSplitterFactory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "previousAdmin", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "address", 14 | "name": "newAdmin", 15 | "type": "address" 16 | } 17 | ], 18 | "name": "AdminChanged", 19 | "type": "event" 20 | }, 21 | { 22 | "anonymous": false, 23 | "inputs": [ 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "implementation", 28 | "type": "address" 29 | } 30 | ], 31 | "name": "Upgraded", 32 | "type": "event" 33 | }, 34 | { 35 | "stateMutability": "payable", 36 | "type": "fallback" 37 | }, 38 | { 39 | "inputs": [], 40 | "name": "admin", 41 | "outputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "", 45 | "type": "address" 46 | } 47 | ], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [ 53 | { 54 | "internalType": "address", 55 | "name": "newAdmin", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "changeAdmin", 60 | "outputs": [], 61 | "stateMutability": "nonpayable", 62 | "type": "function" 63 | }, 64 | { 65 | "inputs": [], 66 | "name": "implementation", 67 | "outputs": [ 68 | { 69 | "internalType": "address", 70 | "name": "", 71 | "type": "address" 72 | } 73 | ], 74 | "stateMutability": "nonpayable", 75 | "type": "function" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "newImplementation", 82 | "type": "address" 83 | } 84 | ], 85 | "name": "upgradeTo", 86 | "outputs": [], 87 | "stateMutability": "nonpayable", 88 | "type": "function" 89 | }, 90 | { 91 | "inputs": [ 92 | { 93 | "internalType": "address", 94 | "name": "newImplementation", 95 | "type": "address" 96 | }, 97 | { 98 | "internalType": "bytes", 99 | "name": "data", 100 | "type": "bytes" 101 | } 102 | ], 103 | "name": "upgradeToAndCall", 104 | "outputs": [], 105 | "stateMutability": "payable", 106 | "type": "function" 107 | }, 108 | { 109 | "stateMutability": "payable", 110 | "type": "receive" 111 | }, 112 | { 113 | "anonymous": false, 114 | "inputs": [ 115 | { 116 | "indexed": true, 117 | "internalType": "address", 118 | "name": "previousOwner", 119 | "type": "address" 120 | }, 121 | { 122 | "indexed": true, 123 | "internalType": "address", 124 | "name": "newOwner", 125 | "type": "address" 126 | } 127 | ], 128 | "name": "OwnershipTransferred", 129 | "type": "event" 130 | }, 131 | { 132 | "anonymous": false, 133 | "inputs": [ 134 | { 135 | "indexed": true, 136 | "internalType": "address", 137 | "name": "splitter", 138 | "type": "address" 139 | }, 140 | { 141 | "indexed": false, 142 | "internalType": "string", 143 | "name": "name", 144 | "type": "string" 145 | }, 146 | { 147 | "indexed": false, 148 | "internalType": "address[]", 149 | "name": "recipients", 150 | "type": "address[]" 151 | }, 152 | { 153 | "indexed": false, 154 | "internalType": "uint256[]", 155 | "name": "amounts", 156 | "type": "uint256[]" 157 | } 158 | ], 159 | "name": "SplitterCreated", 160 | "type": "event" 161 | }, 162 | { 163 | "inputs": [ 164 | { 165 | "internalType": "string", 166 | "name": "name_", 167 | "type": "string" 168 | }, 169 | { 170 | "internalType": "bytes32", 171 | "name": "merkleRoot", 172 | "type": "bytes32" 173 | }, 174 | { 175 | "internalType": "address[]", 176 | "name": "recipients", 177 | "type": "address[]" 178 | }, 179 | { 180 | "internalType": "uint256[]", 181 | "name": "amounts", 182 | "type": "uint256[]" 183 | } 184 | ], 185 | "name": "createSplitter", 186 | "outputs": [ 187 | { 188 | "internalType": "address", 189 | "name": "newContract", 190 | "type": "address" 191 | } 192 | ], 193 | "stateMutability": "payable", 194 | "type": "function" 195 | }, 196 | { 197 | "inputs": [], 198 | "name": "getSplitterImplementation", 199 | "outputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "", 203 | "type": "address" 204 | } 205 | ], 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [ 211 | { 212 | "internalType": "address", 213 | "name": "splitterImplementation", 214 | "type": "address" 215 | }, 216 | { 217 | "internalType": "address", 218 | "name": "owner_", 219 | "type": "address" 220 | } 221 | ], 222 | "name": "initialize", 223 | "outputs": [], 224 | "stateMutability": "nonpayable", 225 | "type": "function" 226 | }, 227 | { 228 | "inputs": [], 229 | "name": "owner", 230 | "outputs": [ 231 | { 232 | "internalType": "address", 233 | "name": "", 234 | "type": "address" 235 | } 236 | ], 237 | "stateMutability": "view", 238 | "type": "function" 239 | }, 240 | { 241 | "inputs": [], 242 | "name": "renounceOwnership", 243 | "outputs": [], 244 | "stateMutability": "nonpayable", 245 | "type": "function" 246 | }, 247 | { 248 | "inputs": [ 249 | { 250 | "internalType": "address", 251 | "name": "implementation", 252 | "type": "address" 253 | } 254 | ], 255 | "name": "setSplitterImplementation", 256 | "outputs": [], 257 | "stateMutability": "nonpayable", 258 | "type": "function" 259 | }, 260 | { 261 | "inputs": [ 262 | { 263 | "internalType": "address", 264 | "name": "newOwner", 265 | "type": "address" 266 | } 267 | ], 268 | "name": "transferOwnership", 269 | "outputs": [], 270 | "stateMutability": "nonpayable", 271 | "type": "function" 272 | }, 273 | { 274 | "inputs": [ 275 | { 276 | "internalType": "address", 277 | "name": "initialLogic", 278 | "type": "address" 279 | }, 280 | { 281 | "internalType": "address", 282 | "name": "initialAdmin", 283 | "type": "address" 284 | }, 285 | { 286 | "internalType": "bytes", 287 | "name": "_data", 288 | "type": "bytes" 289 | } 290 | ], 291 | "stateMutability": "payable", 292 | "type": "constructor" 293 | } 294 | ] -------------------------------------------------------------------------------- /graph/splitter/build/CollabSplitterTokenPayment/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] -------------------------------------------------------------------------------- /graph/splitter/build/schema.graphql: -------------------------------------------------------------------------------- 1 | type CollabSplitter @entity { 2 | id: ID! 3 | # contractURI: String! 4 | name: String! 5 | allocationsCount: BigInt! 6 | allocations: [Allocation!]! @derivedFrom(field: "splitter") 7 | tokens: [CollabSplitterToken!] @derivedFrom(field: "splitter") 8 | } 9 | 10 | type Allocation @entity { 11 | "-" 12 | id: ID! 13 | splitter: CollabSplitter! 14 | recipient: Account! 15 | allocation: BigInt! 16 | } 17 | 18 | type Account @entity { 19 | id: ID! 20 | allocations: [Allocation!]! @derivedFrom(field: "recipient") 21 | } 22 | 23 | type ERC20 @entity { 24 | id: ID! 25 | name: String! 26 | symbol: String! 27 | decimals: BigInt! 28 | splitters: [CollabSplitterToken!] @derivedFrom(field: "token") 29 | } 30 | 31 | type CollabSplitterToken @entity { 32 | id: ID! 33 | token: ERC20! 34 | splitter: CollabSplitter! 35 | } -------------------------------------------------------------------------------- /graph/splitter/build/subgraph.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.2 2 | schema: 3 | file: schema.graphql 4 | dataSources: 5 | - name: CollabSplitterFactory 6 | kind: ethereum/contract 7 | network: mainnet 8 | source: 9 | abi: CollabSplitterFactory 10 | startBlock: 0 11 | address: "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B" 12 | mapping: 13 | kind: ethereum/events 14 | apiVersion: 0.0.5 15 | language: wasm/assemblyscript 16 | file: CollabSplitterFactory/CollabSplitterFactory.wasm 17 | entities: 18 | - CollabSplitter 19 | - Account 20 | - Allocation 21 | abis: 22 | - name: CollabSplitterFactory 23 | file: CollabSplitterFactory/abis/CollabSplitterFactory.json 24 | eventHandlers: 25 | - event: SplitterCreated(indexed address,string,address[],uint256[]) 26 | handler: handleSplitterCreation 27 | - name: CollabSplitterTokenPayment 28 | kind: ethereum/contract 29 | network: mainnet 30 | source: 31 | abi: ERC20 32 | startBlock: 0 33 | mapping: 34 | kind: ethereum/events 35 | apiVersion: 0.0.5 36 | language: wasm/assemblyscript 37 | file: CollabSplitterFactory/CollabSplitterFactory.wasm 38 | entities: 39 | - CollabSplitter 40 | - ERC20 41 | - CollabSplitterToken 42 | abis: 43 | - name: ERC20 44 | file: CollabSplitterTokenPayment/abis/ERC20.json 45 | eventHandlers: 46 | - event: Transfer(indexed address,indexed address,uint256) 47 | handler: handleTokenPayment 48 | templates: 49 | - name: CollabSplitter 50 | kind: ethereum/contract 51 | network: mainnet 52 | source: 53 | abi: CollabSplitter 54 | mapping: 55 | kind: ethereum/events 56 | apiVersion: 0.0.5 57 | language: wasm/assemblyscript 58 | file: CollabSplitterFactory/CollabSplitterFactory.wasm 59 | entities: 60 | - CollabSplitter 61 | - Account 62 | - Allocation 63 | abis: 64 | - name: CollabSplitter 65 | file: CollabSplitter/abis/CollabSplitter.json 66 | eventHandlers: 67 | - event: ETHClaimed(address,address,uint256) 68 | handler: handleETHClaim 69 | - event: ERC20Claimed(address,address,uint256,address) 70 | handler: handleERC20Claim 71 | -------------------------------------------------------------------------------- /graph/splitter/config/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x486E4CCd2970C1971f41AA16EEFf078f821F3E9a", 3 | "network": "mainnet", 4 | "startBlock": 13745674 5 | } -------------------------------------------------------------------------------- /graph/splitter/config/rinkeby.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6", 3 | "network": "rinkeby", 4 | "startBlock":9509959 5 | } -------------------------------------------------------------------------------- /graph/splitter/generated/schema.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | 3 | import { 4 | TypedMap, 5 | Entity, 6 | Value, 7 | ValueKind, 8 | store, 9 | Address, 10 | Bytes, 11 | BigInt, 12 | BigDecimal 13 | } from "@graphprotocol/graph-ts"; 14 | 15 | export class CollabSplitter extends Entity { 16 | constructor(id: string) { 17 | super(); 18 | this.set("id", Value.fromString(id)); 19 | 20 | this.set("name", Value.fromString("")); 21 | this.set("allocationsCount", Value.fromBigInt(BigInt.zero())); 22 | } 23 | 24 | save(): void { 25 | let id = this.get("id"); 26 | assert(id != null, "Cannot save CollabSplitter entity without an ID"); 27 | if (id) { 28 | assert( 29 | id.kind == ValueKind.STRING, 30 | "Cannot save CollabSplitter entity with non-string ID. " + 31 | 'Considering using .toHex() to convert the "id" to a string.' 32 | ); 33 | store.set("CollabSplitter", id.toString(), this); 34 | } 35 | } 36 | 37 | static load(id: string): CollabSplitter | null { 38 | return changetype(store.get("CollabSplitter", id)); 39 | } 40 | 41 | get id(): string { 42 | let value = this.get("id"); 43 | return value!.toString(); 44 | } 45 | 46 | set id(value: string) { 47 | this.set("id", Value.fromString(value)); 48 | } 49 | 50 | get name(): string { 51 | let value = this.get("name"); 52 | return value!.toString(); 53 | } 54 | 55 | set name(value: string) { 56 | this.set("name", Value.fromString(value)); 57 | } 58 | 59 | get allocationsCount(): BigInt { 60 | let value = this.get("allocationsCount"); 61 | return value!.toBigInt(); 62 | } 63 | 64 | set allocationsCount(value: BigInt) { 65 | this.set("allocationsCount", Value.fromBigInt(value)); 66 | } 67 | 68 | get allocations(): Array { 69 | let value = this.get("allocations"); 70 | return value!.toStringArray(); 71 | } 72 | 73 | set allocations(value: Array) { 74 | this.set("allocations", Value.fromStringArray(value)); 75 | } 76 | 77 | get tokens(): Array | null { 78 | let value = this.get("tokens"); 79 | if (!value || value.kind == ValueKind.NULL) { 80 | return null; 81 | } else { 82 | return value.toStringArray(); 83 | } 84 | } 85 | 86 | set tokens(value: Array | null) { 87 | if (!value) { 88 | this.unset("tokens"); 89 | } else { 90 | this.set("tokens", Value.fromStringArray(>value)); 91 | } 92 | } 93 | } 94 | 95 | export class Allocation extends Entity { 96 | constructor(id: string) { 97 | super(); 98 | this.set("id", Value.fromString(id)); 99 | 100 | this.set("splitter", Value.fromString("")); 101 | this.set("recipient", Value.fromString("")); 102 | this.set("allocation", Value.fromBigInt(BigInt.zero())); 103 | } 104 | 105 | save(): void { 106 | let id = this.get("id"); 107 | assert(id != null, "Cannot save Allocation entity without an ID"); 108 | if (id) { 109 | assert( 110 | id.kind == ValueKind.STRING, 111 | "Cannot save Allocation entity with non-string ID. " + 112 | 'Considering using .toHex() to convert the "id" to a string.' 113 | ); 114 | store.set("Allocation", id.toString(), this); 115 | } 116 | } 117 | 118 | static load(id: string): Allocation | null { 119 | return changetype(store.get("Allocation", id)); 120 | } 121 | 122 | get id(): string { 123 | let value = this.get("id"); 124 | return value!.toString(); 125 | } 126 | 127 | set id(value: string) { 128 | this.set("id", Value.fromString(value)); 129 | } 130 | 131 | get splitter(): string { 132 | let value = this.get("splitter"); 133 | return value!.toString(); 134 | } 135 | 136 | set splitter(value: string) { 137 | this.set("splitter", Value.fromString(value)); 138 | } 139 | 140 | get recipient(): string { 141 | let value = this.get("recipient"); 142 | return value!.toString(); 143 | } 144 | 145 | set recipient(value: string) { 146 | this.set("recipient", Value.fromString(value)); 147 | } 148 | 149 | get allocation(): BigInt { 150 | let value = this.get("allocation"); 151 | return value!.toBigInt(); 152 | } 153 | 154 | set allocation(value: BigInt) { 155 | this.set("allocation", Value.fromBigInt(value)); 156 | } 157 | } 158 | 159 | export class Account extends Entity { 160 | constructor(id: string) { 161 | super(); 162 | this.set("id", Value.fromString(id)); 163 | } 164 | 165 | save(): void { 166 | let id = this.get("id"); 167 | assert(id != null, "Cannot save Account entity without an ID"); 168 | if (id) { 169 | assert( 170 | id.kind == ValueKind.STRING, 171 | "Cannot save Account entity with non-string ID. " + 172 | 'Considering using .toHex() to convert the "id" to a string.' 173 | ); 174 | store.set("Account", id.toString(), this); 175 | } 176 | } 177 | 178 | static load(id: string): Account | null { 179 | return changetype(store.get("Account", id)); 180 | } 181 | 182 | get id(): string { 183 | let value = this.get("id"); 184 | return value!.toString(); 185 | } 186 | 187 | set id(value: string) { 188 | this.set("id", Value.fromString(value)); 189 | } 190 | 191 | get allocations(): Array { 192 | let value = this.get("allocations"); 193 | return value!.toStringArray(); 194 | } 195 | 196 | set allocations(value: Array) { 197 | this.set("allocations", Value.fromStringArray(value)); 198 | } 199 | } 200 | 201 | export class ERC20 extends Entity { 202 | constructor(id: string) { 203 | super(); 204 | this.set("id", Value.fromString(id)); 205 | 206 | this.set("name", Value.fromString("")); 207 | this.set("symbol", Value.fromString("")); 208 | this.set("decimals", Value.fromBigInt(BigInt.zero())); 209 | } 210 | 211 | save(): void { 212 | let id = this.get("id"); 213 | assert(id != null, "Cannot save ERC20 entity without an ID"); 214 | if (id) { 215 | assert( 216 | id.kind == ValueKind.STRING, 217 | "Cannot save ERC20 entity with non-string ID. " + 218 | 'Considering using .toHex() to convert the "id" to a string.' 219 | ); 220 | store.set("ERC20", id.toString(), this); 221 | } 222 | } 223 | 224 | static load(id: string): ERC20 | null { 225 | return changetype(store.get("ERC20", id)); 226 | } 227 | 228 | get id(): string { 229 | let value = this.get("id"); 230 | return value!.toString(); 231 | } 232 | 233 | set id(value: string) { 234 | this.set("id", Value.fromString(value)); 235 | } 236 | 237 | get name(): string { 238 | let value = this.get("name"); 239 | return value!.toString(); 240 | } 241 | 242 | set name(value: string) { 243 | this.set("name", Value.fromString(value)); 244 | } 245 | 246 | get symbol(): string { 247 | let value = this.get("symbol"); 248 | return value!.toString(); 249 | } 250 | 251 | set symbol(value: string) { 252 | this.set("symbol", Value.fromString(value)); 253 | } 254 | 255 | get decimals(): BigInt { 256 | let value = this.get("decimals"); 257 | return value!.toBigInt(); 258 | } 259 | 260 | set decimals(value: BigInt) { 261 | this.set("decimals", Value.fromBigInt(value)); 262 | } 263 | 264 | get splitters(): Array | null { 265 | let value = this.get("splitters"); 266 | if (!value || value.kind == ValueKind.NULL) { 267 | return null; 268 | } else { 269 | return value.toStringArray(); 270 | } 271 | } 272 | 273 | set splitters(value: Array | null) { 274 | if (!value) { 275 | this.unset("splitters"); 276 | } else { 277 | this.set("splitters", Value.fromStringArray(>value)); 278 | } 279 | } 280 | } 281 | 282 | export class CollabSplitterToken extends Entity { 283 | constructor(id: string) { 284 | super(); 285 | this.set("id", Value.fromString(id)); 286 | 287 | this.set("token", Value.fromString("")); 288 | this.set("splitter", Value.fromString("")); 289 | } 290 | 291 | save(): void { 292 | let id = this.get("id"); 293 | assert(id != null, "Cannot save CollabSplitterToken entity without an ID"); 294 | if (id) { 295 | assert( 296 | id.kind == ValueKind.STRING, 297 | "Cannot save CollabSplitterToken entity with non-string ID. " + 298 | 'Considering using .toHex() to convert the "id" to a string.' 299 | ); 300 | store.set("CollabSplitterToken", id.toString(), this); 301 | } 302 | } 303 | 304 | static load(id: string): CollabSplitterToken | null { 305 | return changetype( 306 | store.get("CollabSplitterToken", id) 307 | ); 308 | } 309 | 310 | get id(): string { 311 | let value = this.get("id"); 312 | return value!.toString(); 313 | } 314 | 315 | set id(value: string) { 316 | this.set("id", Value.fromString(value)); 317 | } 318 | 319 | get token(): string { 320 | let value = this.get("token"); 321 | return value!.toString(); 322 | } 323 | 324 | set token(value: string) { 325 | this.set("token", Value.fromString(value)); 326 | } 327 | 328 | get splitter(): string { 329 | let value = this.get("splitter"); 330 | return value!.toString(); 331 | } 332 | 333 | set splitter(value: string) { 334 | this.set("splitter", Value.fromString(value)); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /graph/splitter/generated/templates.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | 3 | import { 4 | Address, 5 | DataSourceTemplate, 6 | DataSourceContext 7 | } from "@graphprotocol/graph-ts"; 8 | 9 | export class CollabSplitter extends DataSourceTemplate { 10 | static create(address: Address): void { 11 | DataSourceTemplate.create("CollabSplitter", [address.toHex()]); 12 | } 13 | 14 | static createWithContext(address: Address, context: DataSourceContext): void { 15 | DataSourceTemplate.createWithContext( 16 | "CollabSplitter", 17 | [address.toHex()], 18 | context 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graph/splitter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter-graph", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "collab-splitter-graph", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@graphprotocol/graph-ts": "^0.22.1", 11 | "@openzeppelin/contracts": "4.3.2", 12 | "@types/node": "^16.9.6", 13 | "mustache": "^3.1.0" 14 | } 15 | }, 16 | "node_modules/@graphprotocol/graph-ts": { 17 | "version": "0.22.1", 18 | "resolved": "https://registry.npmjs.org/@graphprotocol/graph-ts/-/graph-ts-0.22.1.tgz", 19 | "integrity": "sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==", 20 | "dev": true, 21 | "dependencies": { 22 | "assemblyscript": "0.19.10" 23 | } 24 | }, 25 | "node_modules/@openzeppelin/contracts": { 26 | "version": "4.3.2", 27 | "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.2.tgz", 28 | "integrity": "sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==", 29 | "dev": true 30 | }, 31 | "node_modules/@types/node": { 32 | "version": "16.10.1", 33 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz", 34 | "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==", 35 | "dev": true 36 | }, 37 | "node_modules/assemblyscript": { 38 | "version": "0.19.10", 39 | "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.19.10.tgz", 40 | "integrity": "sha512-HavcUBXB3mBTRGJcpvaQjmnmaqKHBGREjSPNsIvnAk2f9dj78y4BkMaSSdvBQYWcDDzsHQjyUC8stICFkD1Odg==", 41 | "dev": true, 42 | "dependencies": { 43 | "binaryen": "101.0.0-nightly.20210723", 44 | "long": "^4.0.0" 45 | }, 46 | "bin": { 47 | "asc": "bin/asc", 48 | "asinit": "bin/asinit" 49 | }, 50 | "funding": { 51 | "type": "opencollective", 52 | "url": "https://opencollective.com/assemblyscript" 53 | } 54 | }, 55 | "node_modules/binaryen": { 56 | "version": "101.0.0-nightly.20210723", 57 | "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-101.0.0-nightly.20210723.tgz", 58 | "integrity": "sha512-eioJNqhHlkguVSbblHOtLqlhtC882SOEPKmNFZaDuz1hzQjolxZ+eu3/kaS10n3sGPONsIZsO7R9fR00UyhEUA==", 59 | "dev": true, 60 | "bin": { 61 | "wasm-opt": "bin/wasm-opt" 62 | } 63 | }, 64 | "node_modules/long": { 65 | "version": "4.0.0", 66 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 67 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 68 | "dev": true 69 | }, 70 | "node_modules/mustache": { 71 | "version": "3.2.1", 72 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.2.1.tgz", 73 | "integrity": "sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA==", 74 | "dev": true, 75 | "bin": { 76 | "mustache": "bin/mustache" 77 | }, 78 | "engines": { 79 | "npm": ">=1.4.0" 80 | } 81 | } 82 | }, 83 | "dependencies": { 84 | "@graphprotocol/graph-ts": { 85 | "version": "0.22.1", 86 | "resolved": "https://registry.npmjs.org/@graphprotocol/graph-ts/-/graph-ts-0.22.1.tgz", 87 | "integrity": "sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==", 88 | "dev": true, 89 | "requires": { 90 | "assemblyscript": "0.19.10" 91 | } 92 | }, 93 | "@openzeppelin/contracts": { 94 | "version": "4.3.2", 95 | "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.2.tgz", 96 | "integrity": "sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==", 97 | "dev": true 98 | }, 99 | "@types/node": { 100 | "version": "16.10.1", 101 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz", 102 | "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==", 103 | "dev": true 104 | }, 105 | "assemblyscript": { 106 | "version": "0.19.10", 107 | "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.19.10.tgz", 108 | "integrity": "sha512-HavcUBXB3mBTRGJcpvaQjmnmaqKHBGREjSPNsIvnAk2f9dj78y4BkMaSSdvBQYWcDDzsHQjyUC8stICFkD1Odg==", 109 | "dev": true, 110 | "requires": { 111 | "binaryen": "101.0.0-nightly.20210723", 112 | "long": "^4.0.0" 113 | } 114 | }, 115 | "binaryen": { 116 | "version": "101.0.0-nightly.20210723", 117 | "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-101.0.0-nightly.20210723.tgz", 118 | "integrity": "sha512-eioJNqhHlkguVSbblHOtLqlhtC882SOEPKmNFZaDuz1hzQjolxZ+eu3/kaS10n3sGPONsIZsO7R9fR00UyhEUA==", 119 | "dev": true 120 | }, 121 | "long": { 122 | "version": "4.0.0", 123 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 124 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 125 | "dev": true 126 | }, 127 | "mustache": { 128 | "version": "3.2.1", 129 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.2.1.tgz", 130 | "integrity": "sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA==", 131 | "dev": true 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /graph/splitter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter-graph", 3 | "license": "MIT", 4 | "scripts": { 5 | "codegen": "graph codegen", 6 | "build": "graph build", 7 | "deploy:rinkeby": "graph deploy --node https://api.thegraph.com/deploy/ --ipfs https://api.thegraph.com/ipfs/ stendhal-labs/collab-splitter-rinkeby", 8 | "deploy:mainnet": "graph deploy --node https://api.thegraph.com/deploy/ --ipfs https://api.thegraph.com/ipfs/ stendhal-labs/collab-splitter-mainnet", 9 | "create-local": "graph create --node http://0.0.0.0:8020/ stendhal-labs/collab-splitter", 10 | "remove-local": "graph remove --node http://0.0.0.0:8020/ stendhal-labs/collab-splitter", 11 | "deploy-local": "graph deploy --node http://0.0.0.0:8020/ --ipfs http://0.0.0.0:5001 stendhal-labs/collab-splitter", 12 | "prepare:mainnet": "mustache config/mainnet.json subgraph.template.yaml > subgraph.yaml", 13 | "prepare:rinkeby": "mustache config/rinkeby.json subgraph.template.yaml > subgraph.yaml" 14 | }, 15 | "devDependencies": { 16 | "@graphprotocol/graph-ts": "^0.22.1", 17 | "@openzeppelin/contracts": "4.3.2", 18 | "@types/node": "^16.9.6", 19 | "mustache": "^3.1.0" 20 | } 21 | } -------------------------------------------------------------------------------- /graph/splitter/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | '@graphprotocol/graph-ts': ^0.22.1 5 | '@openzeppelin/contracts': 4.3.2 6 | '@types/node': ^16.9.6 7 | 8 | devDependencies: 9 | '@graphprotocol/graph-ts': 0.22.1 10 | '@openzeppelin/contracts': 4.3.2 11 | '@types/node': 16.9.6 12 | 13 | packages: 14 | 15 | /@graphprotocol/graph-ts/0.22.1: 16 | resolution: {integrity: sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==} 17 | dependencies: 18 | assemblyscript: 0.19.10 19 | dev: true 20 | 21 | /@openzeppelin/contracts/4.3.2: 22 | resolution: {integrity: sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==} 23 | dev: true 24 | 25 | /@types/node/16.9.6: 26 | resolution: {integrity: sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==} 27 | dev: true 28 | 29 | /assemblyscript/0.19.10: 30 | resolution: {integrity: sha512-HavcUBXB3mBTRGJcpvaQjmnmaqKHBGREjSPNsIvnAk2f9dj78y4BkMaSSdvBQYWcDDzsHQjyUC8stICFkD1Odg==} 31 | hasBin: true 32 | dependencies: 33 | binaryen: 101.0.0-nightly.20210723 34 | long: 4.0.0 35 | dev: true 36 | 37 | /binaryen/101.0.0-nightly.20210723: 38 | resolution: {integrity: sha512-eioJNqhHlkguVSbblHOtLqlhtC882SOEPKmNFZaDuz1hzQjolxZ+eu3/kaS10n3sGPONsIZsO7R9fR00UyhEUA==} 39 | hasBin: true 40 | dev: true 41 | 42 | /long/4.0.0: 43 | resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} 44 | dev: true 45 | -------------------------------------------------------------------------------- /graph/splitter/schema.graphql: -------------------------------------------------------------------------------- 1 | type CollabSplitter @entity { 2 | id: ID! 3 | # contractURI: String! 4 | name: String! 5 | allocationsCount: BigInt! 6 | allocations: [Allocation!]! @derivedFrom(field: "splitter") 7 | tokens: [CollabSplitterToken!] @derivedFrom(field: "splitter") 8 | } 9 | 10 | type Allocation @entity { 11 | "-" 12 | id: ID! 13 | splitter: CollabSplitter! 14 | recipient: Account! 15 | allocation: BigInt! 16 | } 17 | 18 | type Account @entity { 19 | id: ID! 20 | allocations: [Allocation!]! @derivedFrom(field: "recipient") 21 | } 22 | 23 | type ERC20 @entity { 24 | id: ID! 25 | name: String! 26 | symbol: String! 27 | decimals: BigInt! 28 | splitters: [CollabSplitterToken!] @derivedFrom(field: "token") 29 | } 30 | 31 | type CollabSplitterToken @entity { 32 | id: ID! 33 | token: ERC20! 34 | splitter: CollabSplitter! 35 | } -------------------------------------------------------------------------------- /graph/splitter/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigInt } from '@graphprotocol/graph-ts' 2 | 3 | let ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 4 | let ZERO = BigInt.fromI32(0); 5 | let ONE = BigInt.fromI32(1); 6 | 7 | export { 8 | ZERO, ZERO_ADDRESS, ONE 9 | }; -------------------------------------------------------------------------------- /graph/splitter/src/mapping.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, ethereum } from "@graphprotocol/graph-ts" 2 | 3 | import { log } from '@graphprotocol/graph-ts' 4 | import { SplitterCreated } from "../generated/CollabSplitterFactory/CollabSplitterFactory"; 5 | import { Transfer } from "../generated/CollabSplitterTokenPayment/ERC20"; 6 | import { CollabSplitterToken, ERC20 as ERC20Schema } from "../generated/schema"; 7 | import { ERC20 } from "../generated/CollabSplitterTokenPayment/ERC20"; 8 | 9 | import { ZERO_ADDRESS, ONE, ZERO } from './constants'; 10 | 11 | import * as erc20s from './providers/erc20s'; 12 | import * as splitters from './providers/splitters'; 13 | 14 | export function handleSplitterCreation(event: SplitterCreated) : void { 15 | splitters.create( 16 | event.params.splitter, 17 | event.params.name, 18 | event.params.recipients, 19 | event.params.amounts 20 | ); 21 | } 22 | 23 | export function handleTokenPayment(event: Transfer) : void { 24 | // first check if "event.params.to" is a CollabSplitter 25 | let splitter = splitters.get(event.params.to); 26 | if (splitter != null) { 27 | // create ids 28 | let erc20Id = event.address.toHex(); 29 | let collabSplitterTokenId = event.params.to.toHex().concat('-').concat(erc20Id); 30 | 31 | // check if there is already a CollabSplitterToken for this collab and this address 32 | let collabSplitterToken = CollabSplitterToken.load(collabSplitterTokenId); 33 | if (collabSplitterToken == null) { 34 | // get the ERC20 35 | let erc20 = erc20s.get(event.address); 36 | 37 | // create a collab splitter token 38 | collabSplitterToken = new CollabSplitterToken(collabSplitterTokenId); 39 | collabSplitterToken.token = erc20.id; 40 | collabSplitterToken.splitter = splitter.id; 41 | 42 | collabSplitterToken.save(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /graph/splitter/src/providers/accounts.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Address } from '@graphprotocol/graph-ts'; 3 | import { Account } from '../../generated/schema'; 4 | import { ONE, ZERO } from '../constants'; 5 | 6 | export function get(address: Address) : Account { 7 | let account = Account.load(address.toHex()); 8 | 9 | if (!account) { 10 | account = new Account(address.toHex()); 11 | account.save(); 12 | } 13 | 14 | return account as Account; 15 | } -------------------------------------------------------------------------------- /graph/splitter/src/providers/erc20s.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; 2 | 3 | import { ERC20 } from "../../generated/CollabSplitterTokenPayment/ERC20"; 4 | import { ERC20 as ERC20Schema } from "../../generated/schema"; 5 | 6 | export function get(address: Address): ERC20Schema { 7 | let erc20Id = address.toHex(); 8 | let erc20 = ERC20Schema.load(erc20Id); 9 | if (erc20 == null) { 10 | erc20 = new ERC20Schema(erc20Id); 11 | 12 | // connect to erc20 contract 13 | let contract = ERC20.bind(address); 14 | 15 | let tryName = contract.try_name(); 16 | erc20.name = tryName.reverted ? "" : tryName.value; 17 | 18 | let trySymbol = contract.try_symbol(); 19 | erc20.symbol = trySymbol.reverted ? "" : trySymbol.value; 20 | 21 | let tryDecimals = contract.try_decimals(); 22 | erc20.decimals = tryDecimals.reverted 23 | ? BigInt.fromI32(0) 24 | : BigInt.fromI32(tryDecimals.value); 25 | 26 | erc20.save(); 27 | } 28 | 29 | return erc20 as ERC20Schema; 30 | } 31 | -------------------------------------------------------------------------------- /graph/splitter/src/providers/splitters.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Address, BigInt } from '@graphprotocol/graph-ts'; 3 | import { Account, Allocation, CollabSplitter } from '../../generated/schema'; 4 | import { ONE, ZERO } from '../constants'; 5 | 6 | import * as accounts from './accounts'; 7 | 8 | export function get(address: Address) : CollabSplitter | null { 9 | let id = address.toHex(); 10 | return CollabSplitter.load(id); 11 | } 12 | 13 | export function create(address: Address, name: String, recipients: Address[], percents: BigInt[]) : CollabSplitter { 14 | let id = address.toHex(); 15 | let splitter = new CollabSplitter(id); 16 | 17 | splitter.name = name; 18 | splitter.allocationsCount = BigInt.fromI32(recipients.length); 19 | 20 | splitter.save(); 21 | 22 | for(let i = 0; i < recipients.length; i++) { 23 | let allocId = id.concat('-').concat((i+1).toString()); 24 | let allocation = new Allocation(allocId); 25 | allocation.splitter = id; 26 | allocation.recipient = accounts.get(recipients[i]).id; 27 | allocation.allocation = percents[i]; 28 | allocation.save(); 29 | } 30 | 31 | return splitter as CollabSplitter; 32 | } -------------------------------------------------------------------------------- /graph/splitter/subgraph.template.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.2 2 | schema: 3 | file: ./schema.graphql 4 | dataSources: 5 | - name: CollabSplitterFactory 6 | kind: ethereum/contract 7 | network: {{network}} 8 | source: 9 | abi: CollabSplitterFactory 10 | startBlock: {{startBlock}} 11 | address: "{{address}}" 12 | mapping: 13 | kind: ethereum/events 14 | apiVersion: 0.0.5 15 | language: wasm/assemblyscript 16 | file: ./src/mapping.ts 17 | entities: 18 | - CollabSplitter 19 | - Account 20 | - Allocation 21 | abis: 22 | - name: CollabSplitterFactory 23 | file: ./abis/CollabSplitterFactory.json 24 | eventHandlers: 25 | - event: SplitterCreated(indexed address,string,address[],uint256[]) 26 | handler: handleSplitterCreation 27 | - name: CollabSplitterTokenPayment 28 | kind: ethereum/contract 29 | network: {{network}} 30 | source: 31 | abi: ERC20 32 | startBlock: {{startBlock}} 33 | mapping: 34 | kind: ethereum/events 35 | apiVersion: 0.0.5 36 | language: wasm/assemblyscript 37 | file: ./src/mapping.ts 38 | entities: 39 | - CollabSplitter 40 | - ERC20 41 | - CollabSplitterToken 42 | abis: 43 | - name: ERC20 44 | file: ./abis/ERC20.json 45 | eventHandlers: 46 | - event: Transfer(indexed address,indexed address,uint256) 47 | handler: handleTokenPayment 48 | templates: 49 | - name: CollabSplitter 50 | kind: ethereum/contract 51 | network: {{network}} 52 | source: 53 | abi: CollabSplitter 54 | mapping: 55 | kind: ethereum/events 56 | apiVersion: 0.0.5 57 | language: wasm/assemblyscript 58 | file: ./src/mapping.ts 59 | entities: 60 | - CollabSplitter 61 | - Account 62 | - Allocation 63 | abis: 64 | - name: CollabSplitter 65 | file: ./abis/CollabSplitter.json 66 | eventHandlers: 67 | - event: ETHClaimed(address,address,uint256) 68 | handler: handleETHClaim 69 | - event: ERC20Claimed(address,address,uint256,address) 70 | handler: handleERC20Claim -------------------------------------------------------------------------------- /graph/splitter/subgraph.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.2 2 | schema: 3 | file: ./schema.graphql 4 | dataSources: 5 | - name: CollabSplitterFactory 6 | kind: ethereum/contract 7 | network: mainnet # thegraph localhost also names its network mainnet, convenient 8 | source: 9 | abi: CollabSplitterFactory 10 | startBlock: 0 11 | address: "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B" 12 | mapping: 13 | kind: ethereum/events 14 | apiVersion: 0.0.5 15 | language: wasm/assemblyscript 16 | file: ./src/mapping.ts 17 | entities: 18 | - CollabSplitter 19 | - Account 20 | - Allocation 21 | abis: 22 | - name: CollabSplitterFactory 23 | file: ./abis/CollabSplitterFactory.json 24 | eventHandlers: 25 | - event: SplitterCreated(indexed address,string,address[],uint256[]) 26 | handler: handleSplitterCreation 27 | - name: CollabSplitterTokenPayment 28 | kind: ethereum/contract 29 | network: mainnet # thegraph localhost also names its network mainnet, convenient 30 | source: 31 | abi: ERC20 32 | startBlock: 0 33 | mapping: 34 | kind: ethereum/events 35 | apiVersion: 0.0.5 36 | language: wasm/assemblyscript 37 | file: ./src/mapping.ts 38 | entities: 39 | - CollabSplitter 40 | - ERC20 41 | - CollabSplitterToken 42 | abis: 43 | - name: ERC20 44 | file: ./abis/ERC20.json 45 | eventHandlers: 46 | - event: Transfer(indexed address,indexed address,uint256) 47 | handler: handleTokenPayment 48 | templates: 49 | - name: CollabSplitter 50 | kind: ethereum/contract 51 | network: mainnet # thegraph localhost also names its network mainnet, convenient 52 | source: 53 | abi: CollabSplitter 54 | mapping: 55 | kind: ethereum/events 56 | apiVersion: 0.0.5 57 | language: wasm/assemblyscript 58 | file: ./src/mapping.ts 59 | entities: 60 | - CollabSplitter 61 | - Account 62 | - Allocation 63 | abis: 64 | - name: CollabSplitter 65 | file: ./abis/CollabSplitter.json 66 | eventHandlers: 67 | - event: ETHClaimed(address,address,uint256) 68 | handler: handleETHClaim 69 | - event: ERC20Claimed(address,address,uint256,address) 70 | handler: handleERC20Claim -------------------------------------------------------------------------------- /graph/splitter/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@graphprotocol/graph-ts@^0.22.1": 6 | "integrity" "sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==" 7 | "resolved" "https://registry.npmjs.org/@graphprotocol/graph-ts/-/graph-ts-0.22.1.tgz" 8 | "version" "0.22.1" 9 | dependencies: 10 | "assemblyscript" "0.19.10" 11 | 12 | "@openzeppelin/contracts@4.3.2": 13 | "integrity" "sha512-AybF1cesONZStg5kWf6ao9OlqTZuPqddvprc0ky7lrUVOjXeKpmQ2Y9FK+6ygxasb+4aic4O5pneFBfwVsRRRg==" 14 | "resolved" "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.3.2.tgz" 15 | "version" "4.3.2" 16 | 17 | "@types/node@^16.9.6": 18 | "integrity" "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==" 19 | "resolved" "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz" 20 | "version" "16.10.1" 21 | 22 | "assemblyscript@0.19.10": 23 | "integrity" "sha512-HavcUBXB3mBTRGJcpvaQjmnmaqKHBGREjSPNsIvnAk2f9dj78y4BkMaSSdvBQYWcDDzsHQjyUC8stICFkD1Odg==" 24 | "resolved" "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.19.10.tgz" 25 | "version" "0.19.10" 26 | dependencies: 27 | "binaryen" "101.0.0-nightly.20210723" 28 | "long" "^4.0.0" 29 | 30 | "binaryen@101.0.0-nightly.20210723": 31 | "integrity" "sha512-eioJNqhHlkguVSbblHOtLqlhtC882SOEPKmNFZaDuz1hzQjolxZ+eu3/kaS10n3sGPONsIZsO7R9fR00UyhEUA==" 32 | "resolved" "https://registry.npmjs.org/binaryen/-/binaryen-101.0.0-nightly.20210723.tgz" 33 | "version" "101.0.0-nightly.20210723" 34 | 35 | "long@^4.0.0": 36 | "integrity" "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 37 | "resolved" "https://registry.npmjs.org/long/-/long-4.0.0.tgz" 38 | "version" "4.0.0" 39 | 40 | "mustache@^3.1.0": 41 | "integrity" "sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA==" 42 | "resolved" "https://registry.npmjs.org/mustache/-/mustache-3.2.1.tgz" 43 | "version" "3.2.1" 44 | -------------------------------------------------------------------------------- /graph/start.sh: -------------------------------------------------------------------------------- 1 | cd graph-node/docker 2 | 3 | ganache-cli -h 0.0.0.0 --deterministic --db ./ganache-db & 4 | docker-compose up 5 | -------------------------------------------------------------------------------- /graph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extend": "./node_modules/@graphprotocol/graph-ts/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@graphprotocol/graph-ts"] 5 | } 6 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "hasInstallScript": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter", 3 | "version": "0.0.1", 4 | "description": "Collaboration Splitter allows to create a cheap contract in charge of receiving and splitting Ethereum and ERC20 payments e.g. a NFT made by several artists.", 5 | "homepage": "https://github.com/stendhal-labs/collab-splitter", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/stendhal-labs/collab-splitter.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/stendhal-labs/collab-splitter/issues" 12 | }, 13 | "keywords": [ 14 | "ethereum", 15 | "smartcontract", 16 | "collaboration", 17 | "splitter", 18 | "eip-2981" 19 | ], 20 | "license": "MIT", 21 | "scripts": { 22 | "install:all": "(cd sdk && npm install); (cd website && npm install); (cd contracts && npm install)", 23 | "install:all:pnpm": "(cd sdk && pnpm install); (cd website && pnpm install); (cd contracts && pnpm install)" 24 | } 25 | } -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | coverage 5 | .env -------------------------------------------------------------------------------- /sdk/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stendhal-labs/collab-splitter/27aaec728c5f1e5e9bd58126628d4be3477784d8/sdk/.gitkeep -------------------------------------------------------------------------------- /sdk/.npmrc: -------------------------------------------------------------------------------- 1 | registry = "https://registry.npmjs.com/" 2 | -------------------------------------------------------------------------------- /sdk/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/** 2 | node_modules/** 3 | -------------------------------------------------------------------------------- /sdk/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /sdk/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Stendhal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /sdk/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Collab Splitter SDK 3 | 4 | This SDK easily builds a small library (using ethers.js) to create a MerkleTree, Root and Proof for collaborations. 5 | 6 | ## Install 7 | 8 | `pnpm install` 9 | 10 | ## Dev 11 | 12 | `pnpm run dev` 13 | 14 | ## Build 15 | 16 | `pnpm run build` 17 | 18 | ## API 19 | 20 | `getNode(recipient, percentage)` 21 | 22 | Allows to calculate the hash of a node to use in the tree 23 | 24 | `getRoot` 25 | 26 | Allows to calculate the root of a tree given an array of [{ recipient, percentage }, ...] 27 | 28 | `getProof` 29 | 30 | Gets the proof (all needed nodes) to use when trying to claim. 31 | The result of the proof will be compared to the initial MerkleRoot to determine if a node is actually part of the tree. 32 | -------------------------------------------------------------------------------- /sdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | return { 3 | verbose: true, 4 | preset: 'rollup-jest', 5 | transform: { 6 | '\\.m?js$': ['rollup-jest', { output: { sourcemap: true } }] 7 | } 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter-sdk", 3 | "version": "1.0.0", 4 | "main": "dist/collab-splitter.cjs.js", 5 | "module": "dist/collab-splitter.esm.js", 6 | "browser": "dist/collab-splitter.umd.js", 7 | "engines": { 8 | "node": ">=14" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/stendhal-labs/collab-splitter.git", 13 | "directory": "sdk" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/stendhal-labs/collab-splitter/issues" 17 | }, 18 | "license": "MIT", 19 | "keywords": [ 20 | "ethereum", 21 | "smartcontract", 22 | "collaboration", 23 | "splitter", 24 | "eip-2981" 25 | ], 26 | "dependencies": { 27 | "ethers": "^5.4.7" 28 | }, 29 | "devDependencies": { 30 | "@rollup/plugin-commonjs": "^11.0.1", 31 | "@rollup/plugin-node-resolve": "^7.0.0", 32 | "@rollup/plugin-replace": "^3.0.0", 33 | "depay-web3-blockchains": "^2.6.0", 34 | "depay-web3-mock": "^8.2.7", 35 | "dotenv": "^10.0.0", 36 | "jest": "^27.3.1", 37 | "prettier": "2.4.1", 38 | "rollup": "^1.29.0", 39 | "rollup-jest": "^1.1.3", 40 | "rollup-plugin-sourcemaps": "^0.6.3" 41 | }, 42 | "scripts": { 43 | "build": "rollup -c", 44 | "dev": "rollup -c -w", 45 | "test": "jest", 46 | "test:watch": "jest --coverage --watch", 47 | "coverage": "jest --coverage", 48 | "pretest": "npm run build" 49 | }, 50 | "files": [ 51 | "dist" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import sourceMaps from 'rollup-plugin-sourcemaps'; 4 | import replace from '@rollup/plugin-replace'; 5 | import pkg from './package.json'; 6 | import { config } from 'dotenv'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | export default [ 11 | // browser-friendly UMD build 12 | { 13 | input: 'src/main.js', 14 | output: { 15 | name: 'collabSplitter', 16 | file: pkg.browser, 17 | format: 'umd', 18 | sourcemap: true 19 | }, 20 | plugins: [ 21 | replace({ 22 | 'process.env.FACTORY_ADDRESS': '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B', 23 | 'process.env.THEGRAPH_URL': JSON.stringify( 24 | 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby' 25 | ) 26 | }), 27 | resolve(), 28 | commonjs(), 29 | sourceMaps() 30 | ] 31 | }, 32 | 33 | // CommonJS (for Node) and ES module (for bundlers) build. 34 | // (We could have three entries in the configuration array 35 | // instead of two, but it's quicker to generate multiple 36 | // builds from a single configuration where possible, using 37 | // an array for the `output` option, where we can specify 38 | // `file` and `format` for each target) 39 | { 40 | input: 'src/main.js', 41 | external: ['ethers'], 42 | output: [ 43 | { 44 | file: pkg.main, 45 | format: 'cjs', 46 | sourcemap: true 47 | }, 48 | { 49 | file: pkg.module, 50 | format: 'es', 51 | sourcemap: true 52 | } 53 | ], 54 | plugins: [ 55 | replace({ 56 | preventAssignment: true, 57 | 'process.env.FACTORY_ADDRESS': '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B', 58 | 'process.env.THEGRAPH_URL': JSON.stringify( 59 | 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby' 60 | ) 61 | // process: JSON.stringify({ 62 | // env: { 63 | // isProd: production, 64 | // ...config().parsed 65 | // } 66 | // }) 67 | }), 68 | sourceMaps() 69 | ] 70 | } 71 | ]; 72 | -------------------------------------------------------------------------------- /sdk/src/data/abis/factory.js: -------------------------------------------------------------------------------- 1 | export const abi = [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: 'address', 8 | name: 'previousAdmin', 9 | type: 'address' 10 | }, 11 | { 12 | indexed: false, 13 | internalType: 'address', 14 | name: 'newAdmin', 15 | type: 'address' 16 | } 17 | ], 18 | name: 'AdminChanged', 19 | type: 'event' 20 | }, 21 | { 22 | anonymous: false, 23 | inputs: [ 24 | { 25 | indexed: true, 26 | internalType: 'address', 27 | name: 'implementation', 28 | type: 'address' 29 | } 30 | ], 31 | name: 'Upgraded', 32 | type: 'event' 33 | }, 34 | { 35 | stateMutability: 'payable', 36 | type: 'fallback' 37 | }, 38 | { 39 | inputs: [], 40 | name: 'admin', 41 | outputs: [ 42 | { 43 | internalType: 'address', 44 | name: '', 45 | type: 'address' 46 | } 47 | ], 48 | stateMutability: 'nonpayable', 49 | type: 'function' 50 | }, 51 | { 52 | inputs: [ 53 | { 54 | internalType: 'address', 55 | name: 'newAdmin', 56 | type: 'address' 57 | } 58 | ], 59 | name: 'changeAdmin', 60 | outputs: [], 61 | stateMutability: 'nonpayable', 62 | type: 'function' 63 | }, 64 | { 65 | inputs: [], 66 | name: 'implementation', 67 | outputs: [ 68 | { 69 | internalType: 'address', 70 | name: '', 71 | type: 'address' 72 | } 73 | ], 74 | stateMutability: 'nonpayable', 75 | type: 'function' 76 | }, 77 | { 78 | inputs: [ 79 | { 80 | internalType: 'address', 81 | name: 'newImplementation', 82 | type: 'address' 83 | } 84 | ], 85 | name: 'upgradeTo', 86 | outputs: [], 87 | stateMutability: 'nonpayable', 88 | type: 'function' 89 | }, 90 | { 91 | inputs: [ 92 | { 93 | internalType: 'address', 94 | name: 'newImplementation', 95 | type: 'address' 96 | }, 97 | { 98 | internalType: 'bytes', 99 | name: 'data', 100 | type: 'bytes' 101 | } 102 | ], 103 | name: 'upgradeToAndCall', 104 | outputs: [], 105 | stateMutability: 'payable', 106 | type: 'function' 107 | }, 108 | { 109 | stateMutability: 'payable', 110 | type: 'receive' 111 | }, 112 | { 113 | anonymous: false, 114 | inputs: [ 115 | { 116 | indexed: true, 117 | internalType: 'address', 118 | name: 'previousOwner', 119 | type: 'address' 120 | }, 121 | { 122 | indexed: true, 123 | internalType: 'address', 124 | name: 'newOwner', 125 | type: 'address' 126 | } 127 | ], 128 | name: 'OwnershipTransferred', 129 | type: 'event' 130 | }, 131 | { 132 | anonymous: false, 133 | inputs: [ 134 | { 135 | indexed: true, 136 | internalType: 'address', 137 | name: 'splitter', 138 | type: 'address' 139 | }, 140 | { 141 | indexed: false, 142 | internalType: 'string', 143 | name: 'name', 144 | type: 'string' 145 | }, 146 | { 147 | indexed: false, 148 | internalType: 'address[]', 149 | name: 'recipients', 150 | type: 'address[]' 151 | }, 152 | { 153 | indexed: false, 154 | internalType: 'uint256[]', 155 | name: 'amounts', 156 | type: 'uint256[]' 157 | } 158 | ], 159 | name: 'SplitterCreated', 160 | type: 'event' 161 | }, 162 | { 163 | inputs: [ 164 | { 165 | internalType: 'string', 166 | name: 'name_', 167 | type: 'string' 168 | }, 169 | { 170 | internalType: 'bytes32', 171 | name: 'merkleRoot', 172 | type: 'bytes32' 173 | }, 174 | { 175 | internalType: 'address[]', 176 | name: 'recipients', 177 | type: 'address[]' 178 | }, 179 | { 180 | internalType: 'uint256[]', 181 | name: 'amounts', 182 | type: 'uint256[]' 183 | } 184 | ], 185 | name: 'createSplitter', 186 | outputs: [ 187 | { 188 | internalType: 'address', 189 | name: 'newContract', 190 | type: 'address' 191 | } 192 | ], 193 | stateMutability: 'payable', 194 | type: 'function' 195 | }, 196 | { 197 | inputs: [], 198 | name: 'getSplitterImplementation', 199 | outputs: [ 200 | { 201 | internalType: 'address', 202 | name: '', 203 | type: 'address' 204 | } 205 | ], 206 | stateMutability: 'view', 207 | type: 'function' 208 | }, 209 | { 210 | inputs: [ 211 | { 212 | internalType: 'address', 213 | name: 'splitterImplementation', 214 | type: 'address' 215 | }, 216 | { 217 | internalType: 'address', 218 | name: 'owner_', 219 | type: 'address' 220 | } 221 | ], 222 | name: 'initialize', 223 | outputs: [], 224 | stateMutability: 'nonpayable', 225 | type: 'function' 226 | }, 227 | { 228 | inputs: [], 229 | name: 'owner', 230 | outputs: [ 231 | { 232 | internalType: 'address', 233 | name: '', 234 | type: 'address' 235 | } 236 | ], 237 | stateMutability: 'view', 238 | type: 'function' 239 | }, 240 | { 241 | inputs: [], 242 | name: 'renounceOwnership', 243 | outputs: [], 244 | stateMutability: 'nonpayable', 245 | type: 'function' 246 | }, 247 | { 248 | inputs: [ 249 | { 250 | internalType: 'address', 251 | name: 'implementation', 252 | type: 'address' 253 | } 254 | ], 255 | name: 'setSplitterImplementation', 256 | outputs: [], 257 | stateMutability: 'nonpayable', 258 | type: 'function' 259 | }, 260 | { 261 | inputs: [ 262 | { 263 | internalType: 'address', 264 | name: 'newOwner', 265 | type: 'address' 266 | } 267 | ], 268 | name: 'transferOwnership', 269 | outputs: [], 270 | stateMutability: 'nonpayable', 271 | type: 'function' 272 | }, 273 | { 274 | inputs: [ 275 | { 276 | internalType: 'address', 277 | name: 'initialLogic', 278 | type: 'address' 279 | }, 280 | { 281 | internalType: 'address', 282 | name: 'initialAdmin', 283 | type: 'address' 284 | }, 285 | { 286 | internalType: 'bytes', 287 | name: '_data', 288 | type: 'bytes' 289 | } 290 | ], 291 | stateMutability: 'payable', 292 | type: 'constructor' 293 | } 294 | ]; 295 | -------------------------------------------------------------------------------- /sdk/src/data/abis/splitter.js: -------------------------------------------------------------------------------- 1 | export const abi = [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: 'address', 8 | name: 'operator', 9 | type: 'address' 10 | }, 11 | { 12 | indexed: false, 13 | internalType: 'address', 14 | name: 'account', 15 | type: 'address' 16 | }, 17 | { 18 | indexed: false, 19 | internalType: 'uint256', 20 | name: 'amount', 21 | type: 'uint256' 22 | }, 23 | { 24 | indexed: false, 25 | internalType: 'address', 26 | name: 'token', 27 | type: 'address' 28 | } 29 | ], 30 | name: 'ERC20Claimed', 31 | type: 'event' 32 | }, 33 | { 34 | anonymous: false, 35 | inputs: [ 36 | { 37 | indexed: false, 38 | internalType: 'address', 39 | name: 'operator', 40 | type: 'address' 41 | }, 42 | { 43 | indexed: false, 44 | internalType: 'address', 45 | name: 'account', 46 | type: 'address' 47 | }, 48 | { 49 | indexed: false, 50 | internalType: 'uint256', 51 | name: 'amount', 52 | type: 'uint256' 53 | } 54 | ], 55 | name: 'ETHClaimed', 56 | type: 'event' 57 | }, 58 | { 59 | inputs: [ 60 | { 61 | internalType: 'address', 62 | name: '', 63 | type: 'address' 64 | } 65 | ], 66 | name: 'alreadyClaimed', 67 | outputs: [ 68 | { 69 | internalType: 'uint256', 70 | name: '', 71 | type: 'uint256' 72 | } 73 | ], 74 | stateMutability: 'view', 75 | type: 'function' 76 | }, 77 | { 78 | inputs: [ 79 | { 80 | internalType: 'address', 81 | name: 'account', 82 | type: 'address' 83 | }, 84 | { 85 | internalType: 'uint256', 86 | name: 'percent', 87 | type: 'uint256' 88 | }, 89 | { 90 | internalType: 'bytes32[]', 91 | name: 'merkleProof', 92 | type: 'bytes32[]' 93 | }, 94 | { 95 | internalType: 'address[]', 96 | name: 'erc20s', 97 | type: 'address[]' 98 | } 99 | ], 100 | name: 'claimBatch', 101 | outputs: [], 102 | stateMutability: 'nonpayable', 103 | type: 'function' 104 | }, 105 | { 106 | inputs: [ 107 | { 108 | internalType: 'address', 109 | name: 'account', 110 | type: 'address' 111 | }, 112 | { 113 | internalType: 'uint256', 114 | name: 'percent', 115 | type: 'uint256' 116 | }, 117 | { 118 | internalType: 'bytes32[]', 119 | name: 'merkleProof', 120 | type: 'bytes32[]' 121 | }, 122 | { 123 | internalType: 'address[]', 124 | name: 'erc20s', 125 | type: 'address[]' 126 | } 127 | ], 128 | name: 'claimERC20', 129 | outputs: [], 130 | stateMutability: 'nonpayable', 131 | type: 'function' 132 | }, 133 | { 134 | inputs: [ 135 | { 136 | internalType: 'address', 137 | name: 'account', 138 | type: 'address' 139 | }, 140 | { 141 | internalType: 'uint256', 142 | name: 'percent', 143 | type: 'uint256' 144 | }, 145 | { 146 | internalType: 'bytes32[]', 147 | name: 'merkleProof', 148 | type: 'bytes32[]' 149 | } 150 | ], 151 | name: 'claimETH', 152 | outputs: [], 153 | stateMutability: 'nonpayable', 154 | type: 'function' 155 | }, 156 | { 157 | inputs: [ 158 | { 159 | internalType: 'address', 160 | name: '', 161 | type: 'address' 162 | } 163 | ], 164 | name: 'erc20Data', 165 | outputs: [ 166 | { 167 | internalType: 'uint256', 168 | name: 'totalReceived', 169 | type: 'uint256' 170 | }, 171 | { 172 | internalType: 'uint256', 173 | name: 'lastBalance', 174 | type: 'uint256' 175 | } 176 | ], 177 | stateMutability: 'view', 178 | type: 'function' 179 | }, 180 | { 181 | inputs: [ 182 | { 183 | internalType: 'address[]', 184 | name: 'accounts', 185 | type: 'address[]' 186 | }, 187 | { 188 | internalType: 'uint256[]', 189 | name: 'percents', 190 | type: 'uint256[]' 191 | }, 192 | { 193 | internalType: 'address', 194 | name: 'token', 195 | type: 'address' 196 | } 197 | ], 198 | name: 'getBatchClaimableERC20', 199 | outputs: [ 200 | { 201 | internalType: 'uint256[]', 202 | name: '', 203 | type: 'uint256[]' 204 | } 205 | ], 206 | stateMutability: 'view', 207 | type: 'function' 208 | }, 209 | { 210 | inputs: [ 211 | { 212 | internalType: 'address[]', 213 | name: 'accounts', 214 | type: 'address[]' 215 | }, 216 | { 217 | internalType: 'uint256[]', 218 | name: 'percents', 219 | type: 'uint256[]' 220 | } 221 | ], 222 | name: 'getBatchClaimableETH', 223 | outputs: [ 224 | { 225 | internalType: 'uint256[]', 226 | name: '', 227 | type: 'uint256[]' 228 | } 229 | ], 230 | stateMutability: 'view', 231 | type: 'function' 232 | }, 233 | { 234 | inputs: [ 235 | { 236 | internalType: 'address', 237 | name: 'account', 238 | type: 'address' 239 | }, 240 | { 241 | internalType: 'address[]', 242 | name: 'tokens', 243 | type: 'address[]' 244 | } 245 | ], 246 | name: 'getBatchClaimed', 247 | outputs: [ 248 | { 249 | internalType: 'uint256[]', 250 | name: '', 251 | type: 'uint256[]' 252 | } 253 | ], 254 | stateMutability: 'view', 255 | type: 'function' 256 | }, 257 | { 258 | inputs: [ 259 | { 260 | internalType: 'address', 261 | name: 'account', 262 | type: 'address' 263 | }, 264 | { 265 | internalType: 'uint256', 266 | name: 'percent', 267 | type: 'uint256' 268 | } 269 | ], 270 | name: 'getNode', 271 | outputs: [ 272 | { 273 | internalType: 'bytes32', 274 | name: '', 275 | type: 'bytes32' 276 | } 277 | ], 278 | stateMutability: 'pure', 279 | type: 'function' 280 | }, 281 | { 282 | inputs: [ 283 | { 284 | internalType: 'bytes32', 285 | name: 'merkleRoot_', 286 | type: 'bytes32' 287 | } 288 | ], 289 | name: 'initialize', 290 | outputs: [], 291 | stateMutability: 'nonpayable', 292 | type: 'function' 293 | }, 294 | { 295 | inputs: [], 296 | name: 'merkleRoot', 297 | outputs: [ 298 | { 299 | internalType: 'bytes32', 300 | name: '', 301 | type: 'bytes32' 302 | } 303 | ], 304 | stateMutability: 'view', 305 | type: 'function' 306 | }, 307 | { 308 | inputs: [], 309 | name: 'totalReceived', 310 | outputs: [ 311 | { 312 | internalType: 'uint256', 313 | name: '', 314 | type: 'uint256' 315 | } 316 | ], 317 | stateMutability: 'view', 318 | type: 'function' 319 | }, 320 | { 321 | stateMutability: 'payable', 322 | type: 'receive' 323 | } 324 | ]; 325 | -------------------------------------------------------------------------------- /sdk/src/main.js: -------------------------------------------------------------------------------- 1 | export * from './modules/merkleproof'; 2 | export * from './modules/collaboration'; 3 | export * from './modules/thegraph'; 4 | export * from './utils/numbers'; 5 | 6 | export const FACTORY_ADDRESS_RINKEBY = '0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6'; 7 | export const THEGRAPH_URL_RINKEBY = 8 | 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby'; 9 | 10 | export const FACTORY_ADDRESS_MAINNET = '0x486E4CCd2970C1971f41AA16EEFf078f821F3E9a'; 11 | export const THEGRAPH_URL_MAINNET = 'https://thegraph.com/hosted-service/subgraph/stendhal-labs/collab-splitter-mainnet'; 12 | -------------------------------------------------------------------------------- /sdk/src/modules/collaboration.js: -------------------------------------------------------------------------------- 1 | import * as ethers from 'ethers'; 2 | 3 | import { abi as factoryABI } from '../data/abis/factory'; 4 | import { abi as splitterABI } from '../data/abis/splitter'; 5 | 6 | import { getProof, getRoot } from './merkleproof'; 7 | 8 | /** 9 | * Create a collab-splitter contract using the factory contract 10 | * (verification/validation of inputs are done by the factory contract in Solidity) 11 | * @param name string 12 | * @param recipients Recipient[] 13 | * @param signer Signer | Provider 14 | * @param factoryAddress contract address 15 | * @returns 16 | */ 17 | export async function create(name, recipients, signer, factoryAddress) { 18 | // calculate tree root 19 | const root = getRoot(recipients); 20 | 21 | // create contract 22 | const contract = new ethers.Contract(factoryAddress, factoryABI, signer); 23 | 24 | // create collab splitter 25 | return contract.createSplitter( 26 | name, 27 | root, 28 | recipients.map((a) => a.account), 29 | recipients.map((a) => a.percent) 30 | ); 31 | } 32 | 33 | export async function getTotalReceived(collabId, signer) { 34 | const contract = new ethers.Contract(collabId, splitterABI, signer); 35 | return contract.totalReceived(); 36 | } 37 | 38 | /** 39 | * Check if account can claim something (ETH or ERC20) by calling contract methods 40 | * @param collabId string 41 | * @param account string 42 | * @param percent string 43 | * @param tokenAddresses string[] 44 | * @param signer Signer | Provider 45 | * @returns 46 | */ 47 | export async function isThereSomethingToClaimForAccount( 48 | collabId, 49 | account, 50 | percent, 51 | tokenAddresses, 52 | signer 53 | ) { 54 | const claimable = await getClaimable(collabId, account, percent, tokenAddresses, signer); 55 | return isThereSomethingToClaim(claimable); 56 | } 57 | 58 | /** 59 | * Check if account can claim something (ETH or ERC20) from retrieved contract data 60 | * @param claimable 61 | * @returns 62 | */ 63 | export function isThereSomethingToClaim(claimable) { 64 | return ( 65 | claimable && 66 | ((claimable.eth && claimable.eth.gt(0)) || 67 | (claimable.erc20 && claimable.erc20.some((erc20) => erc20.gt(0)))) 68 | ); 69 | } 70 | 71 | /** 72 | * Check how much ETH & ERC20 can be claimed (for ONE account) 73 | * @param collabId string 74 | * @param account string 75 | * @param percent string 76 | * @param tokenAddresses string[] 77 | * @param signer Signer | Provider 78 | * @returns 79 | */ 80 | export async function getClaimable(collabId, account, percent, tokenAddresses, signer) { 81 | const batchClaimable = await getBatchClaimable( 82 | collabId, 83 | [account], 84 | [percent], 85 | tokenAddresses, 86 | signer 87 | ); 88 | 89 | return { 90 | eth: batchClaimable.eth[0], 91 | erc20: batchClaimable.erc20[0] 92 | }; 93 | } 94 | 95 | /** 96 | * Check how much ETH & ERC20 can be claimed (for multiple accounts) 97 | * @param collabId string 98 | * @param accounts string[] 99 | * @param percents string[] 100 | * @param tokenAddresses string[] 101 | * @param signer Signer | Provider 102 | * @returns 103 | */ 104 | export async function getBatchClaimable(collabId, accounts, percents, tokenAddresses, signer) { 105 | // create contract 106 | const contract = new ethers.Contract(collabId, splitterABI, signer); 107 | 108 | const claimableETH = await contract.getBatchClaimableETH(accounts, percents); 109 | let claimableERC20 = new Array(accounts.length); 110 | claimableERC20.fill([]); 111 | if (tokenAddresses.length > 0) { 112 | let claimableERC20ByToken = []; 113 | for (let token of tokenAddresses) { 114 | claimableERC20ByToken = await contract.getBatchClaimableERC20(accounts, percents, token); 115 | for (let j = 0; j < accounts.length; j++) { 116 | claimableERC20[j] = [...claimableERC20[j], claimableERC20ByToken[j]]; 117 | } 118 | } 119 | } 120 | 121 | return { 122 | eth: claimableETH, 123 | erc20: claimableERC20 124 | }; 125 | } 126 | 127 | /** 128 | * Get already claimed ETH & ERC20 tokens from contract 129 | * @param collabId string 130 | * @param account string 131 | * @param tokenAddresses string[] 132 | * @param signer Signer | Provider 133 | * @returns 134 | */ 135 | export async function getAlreadyClaimed(collabId, account, tokenAddresses, signer) { 136 | const contract = new ethers.Contract(collabId, splitterABI, signer); 137 | 138 | const alreadyClaimedETH = await contract.alreadyClaimed(account); 139 | let alreadyClaimedERC20 = []; 140 | if (tokenAddresses.length > 0) { 141 | alreadyClaimedERC20 = await contract.getBatchClaimed(account, tokenAddresses); 142 | } 143 | 144 | return { 145 | eth: alreadyClaimedETH, 146 | erc20: alreadyClaimedERC20 147 | }; 148 | } 149 | 150 | /** 151 | * Claim ETH & all available ERC20 tokens for a collab 152 | * @param account string 153 | * @param collab 154 | * @param signer Signer | Provider 155 | * @returns 156 | */ 157 | export async function claimBatch(account, collab, signer) { 158 | if (!(account && collab)) { 159 | throw new Error('Invalid parameter'); 160 | } 161 | // create contract 162 | const contract = new ethers.Contract(collab.id, splitterABI, signer); 163 | 164 | const recipients = collab.allocations.map((a) => ({ 165 | account: a.recipient.id, 166 | percent: a.allocation 167 | })); 168 | const accountIndex = recipients.findIndex((r) => account === r.account); 169 | 170 | const tokenAddresses = getTokenAddresses(collab); 171 | 172 | console.log(`Claiming ETH + ERC20s`); 173 | console.log( 174 | recipients[accountIndex].account, 175 | recipients[accountIndex].percent, 176 | getProof(recipients, accountIndex), 177 | tokenAddresses 178 | ); 179 | 180 | return contract.claimBatch( 181 | recipients[accountIndex].account, 182 | recipients[accountIndex].percent, 183 | getProof(recipients, accountIndex), 184 | tokenAddresses 185 | ); 186 | } 187 | 188 | /** 189 | * Claim ETH for a collab 190 | * @param account string 191 | * @param collab 192 | * @param signer Signer | Provider 193 | * @returns 194 | */ 195 | export async function claimETH(account, collab, signer) { 196 | if (!account || !collab) { 197 | throw new Error('Invalid parameter'); 198 | } 199 | // create contract 200 | const contract = new ethers.Contract(collab.id, splitterABI, signer); 201 | 202 | const recipients = collab.allocations.map((a) => ({ 203 | account: a.recipient.id, 204 | percent: a.allocation 205 | })); 206 | const accountIndex = recipients.findIndex((r) => account === r.account); 207 | 208 | console.log(`Claiming ETH`); 209 | console.log( 210 | recipients[accountIndex].account, 211 | recipients[accountIndex].percent, 212 | getProof(recipients, accountIndex) 213 | ); 214 | 215 | return contract.claimETH( 216 | recipients[accountIndex].account, 217 | recipients[accountIndex].percent, 218 | getProof(recipients, accountIndex) 219 | ); 220 | } 221 | /** 222 | * Claim tokens of a ERC20 contract for a collab 223 | * @param account string 224 | * @param collab 225 | * @param tokenAddress string 226 | * @param signer Signer | Provider 227 | * @returns 228 | */ 229 | export async function claimERC20(account, collab, tokenAddress, signer) { 230 | if (!account || !collab || !tokenAddress) { 231 | throw new Error('Invalid parameter'); 232 | } 233 | // create contract 234 | const contract = new ethers.Contract(collab.id, splitterABI, signer); 235 | 236 | const recipients = collab.allocations.map((a) => ({ 237 | account: a.recipient.id, 238 | percent: a.allocation 239 | })); 240 | const accountIndex = recipients.findIndex((r) => account === r.account); 241 | 242 | console.log(`Claiming ERC20`); 243 | console.log( 244 | recipients[accountIndex].account, 245 | recipients[accountIndex].percent, 246 | getProof(recipients, accountIndex), 247 | [tokenAddress] 248 | ); 249 | return contract.claimERC20( 250 | recipients[accountIndex].account, 251 | recipients[accountIndex].percent, 252 | getProof(recipients, accountIndex), 253 | [tokenAddress] 254 | ); 255 | } 256 | 257 | /** 258 | * Convert token contract addresses from TheGraph to string array 259 | * @param collab 260 | * @returns 261 | */ 262 | export function getTokenAddresses(collab) { 263 | return collab.tokens.map((token) => token.token.id); 264 | } 265 | -------------------------------------------------------------------------------- /sdk/src/modules/merkleproof.js: -------------------------------------------------------------------------------- 1 | import * as ethers from 'ethers'; 2 | 3 | /** 4 | * Get the Merkle node of an tuple account, percent 5 | * @param account 6 | * @param percent 7 | * @returns 8 | */ 9 | export function getNode(account, percent) { 10 | return ethers.utils.keccak256( 11 | ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [account, percent]) 12 | ); 13 | } 14 | 15 | /** 16 | * Get Merkle root of a collaboration (list of recipients with percent) 17 | * @param collab 18 | * @returns 19 | */ 20 | export function getRoot(collab) { 21 | return merkleRoot(collab.map((a) => getNode(a.account, a.percent))); 22 | } 23 | 24 | /** 25 | * Get the Merkle proof of a recipient (account, percent) 26 | * @param collab 27 | * @param index 28 | * @returns 29 | */ 30 | export function getProof(collab, index) { 31 | return merkleProof( 32 | collab.map((a) => getNode(a.account, a.percent)), 33 | getNode(collab[index].account, collab[index].percent) 34 | ); 35 | } 36 | 37 | function merkleRoot(txs) { 38 | if (txs.length === 1) { 39 | return txs[0]; 40 | } 41 | 42 | return merkleRoot(toPairs(txs).map((pair) => hashPair(pair[0], pair[1]))); 43 | } 44 | 45 | function merkleProof(txs, tx, proof = []) { 46 | if (txs.length === 1) { 47 | return proof; 48 | } 49 | 50 | const tree = []; 51 | 52 | toPairs(txs).forEach((pair) => { 53 | if (pair.length === 1) { 54 | pair[1] = pair[0]; 55 | } 56 | const hash = hashPair(pair[0], pair[1]); 57 | 58 | if (pair.includes(tx)) { 59 | const idx = (pair[0] === tx) | 0; 60 | // if (idx > pair.length - 1) { 61 | // console.error(`Array index out of bounds: index ${idx}, length ${pair.length}`); 62 | // } 63 | proof.push(pair[idx]); 64 | tx = hash; 65 | } 66 | 67 | tree.push(hash); 68 | }); 69 | 70 | return merkleProof(tree, tx, proof); 71 | } 72 | 73 | function merkleProofRoot(proof, tx) { 74 | return proof.reduce((root, [idx, tx]) => (idx ? hashPair(root, tx) : hashPair(tx, root)), tx); 75 | } 76 | 77 | function toPairs(arr) { 78 | return Array.from(Array(Math.ceil(arr.length / 2)), (_, i) => arr.slice(i * 2, i * 2 + 2)); 79 | } 80 | 81 | function hashPair(a, b = a) { 82 | let temp; 83 | // for some reason, MerkleRoot.verify() always put the lowest value left 84 | if (ethers.BigNumber.from(a).gt(ethers.BigNumber.from(b))) { 85 | temp = a; 86 | a = b; 87 | b = temp; 88 | } 89 | 90 | return ethers.utils.keccak256(ethers.utils.solidityPack(['bytes32', 'bytes32'], [a, b])); 91 | } 92 | -------------------------------------------------------------------------------- /sdk/src/modules/thegraph.js: -------------------------------------------------------------------------------- 1 | export async function getAllocationsByAccount(fetch, theGraphUrl, address) { 2 | const res = await req( 3 | fetch, 4 | theGraphUrl, 5 | `{ 6 | account(id:"${address}") { 7 | id 8 | allocations { 9 | id 10 | splitter{ 11 | id 12 | name 13 | allocationsCount 14 | allocations { 15 | recipient { 16 | id 17 | } 18 | allocation 19 | } 20 | tokens { 21 | token { 22 | id 23 | name 24 | symbol 25 | } 26 | } 27 | } 28 | allocation 29 | } 30 | } 31 | }` 32 | ); 33 | const data = await res.json(); 34 | 35 | return data.data.account ? data.data.account.allocations : []; 36 | } 37 | export async function getCollab(fetch, theGraphUrl, id) { 38 | const res = await req( 39 | fetch, 40 | theGraphUrl, 41 | `{ 42 | collabSplitter(id:"${id}"){ 43 | id 44 | name 45 | allocationsCount 46 | allocations{ 47 | id 48 | recipient { 49 | id 50 | } 51 | allocation 52 | } 53 | tokens { 54 | token { 55 | id 56 | name 57 | symbol 58 | } 59 | } 60 | } 61 | }` 62 | ); 63 | const data = await res.json(); 64 | 65 | return data.data.collabSplitter; 66 | } 67 | 68 | /** 69 | * Make the GraphQL query 70 | * @param {*} fetch 71 | * @param {*} theGraphUrl 72 | * @param {*} query 73 | * @returns 74 | */ 75 | async function req(fetch, theGraphUrl, query) { 76 | const res = await fetch(theGraphUrl, { 77 | method: 'POST', 78 | body: JSON.stringify({ query }) 79 | }); 80 | return res; 81 | } 82 | -------------------------------------------------------------------------------- /sdk/src/utils/numbers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Precision in Solidity contract: 10000 for 100% so multiplier is 1e2 3 | */ 4 | const DECIMAL_PRECISION_SCALE = 1e2; 5 | 6 | export function convertUIntToPercentage(bigint) { 7 | return bigint / DECIMAL_PRECISION_SCALE; 8 | } 9 | 10 | export function convertPercentageToUint(percentage) { 11 | return Math.floor(percentage * DECIMAL_PRECISION_SCALE); 12 | } 13 | -------------------------------------------------------------------------------- /sdk/test/modules/merkleproof.test.js: -------------------------------------------------------------------------------- 1 | import * as ethers from 'ethers'; 2 | import { getNode, getProof, getRoot } from '../../src/modules/merkleproof'; 3 | 4 | describe('getNode', () => { 5 | it('getNode OK', () => { 6 | // Arrange 7 | const account = '0xdc58f594e82f7592163687915f61b031d2102fdb'; 8 | const percent = 1000; 9 | // Act 10 | 11 | const node = getNode(account, percent); 12 | 13 | //Assert 14 | expect(node).toBe('0xfdcc495291813e45ff9f30856c3fcadfa9bb13c64b7c008de80c9789915261df'); 15 | }); 16 | }); 17 | 18 | describe('getRoot', () => { 19 | it('getRoot OK', () => { 20 | // Arrange 21 | const collab = [ 22 | { account: '0xD53ADeC981F32482cb8bbDb733791EA41DD64F74', percent: 1000 }, 23 | { account: '0x7642BE7ccd476c76a0b49c4c631bd9A403dF3E83', percent: 900 }, 24 | { account: '0x1ecc8e43660df8843B9172dF59367aAf18410E94', percent: 350 }, 25 | { account: '0xFED8706D9C6227E69e924526441043449559B249', percent: 250 }, 26 | { account: '0x18638D5563E763703B89c9A5171a060D420aF03E', percent: 100 }, 27 | { account: '0xD04634B1536C39d932FFB5Ef571f806C88a3A69C', percent: 1000 }, 28 | { account: '0xf33a205dCe3F05Bdd2a74875FB8E3b4A042Cd2cf', percent: 1050 }, 29 | { account: '0x5163C7ad7fe5637AC533280dF78cd801529c577d', percent: 4950 }, 30 | { account: '0xAA2f26eD45a3C55c9824F717C05460D198230943', percent: 200 }, 31 | { account: '0x178307eF9f8A88CEA9499078207e15F8d50428C6', percent: 100 }, 32 | { account: '0x45CC60482d650D58E54E7d0BE2D18D49586E8e6C', percent: 100 } 33 | ]; 34 | 35 | // Act 36 | const root = getRoot(collab); 37 | 38 | //Assert 39 | expect(root).toBe('0xe4c7fb3800eedd2bfae767dd66d53753043a9cc36cbb7ef3ab8b353e8e1f3bdc'); 40 | }); 41 | }); 42 | 43 | describe('getProof', () => { 44 | it('11 recipients', () => { 45 | // Arrange 46 | const collab = [ 47 | { account: '0xD53ADeC981F32482cb8bbDb733791EA41DD64F74', percent: 1000 }, 48 | { account: '0x7642BE7ccd476c76a0b49c4c631bd9A403dF3E83', percent: 900 }, 49 | { account: '0x1ecc8e43660df8843B9172dF59367aAf18410E94', percent: 350 }, 50 | { account: '0xFED8706D9C6227E69e924526441043449559B249', percent: 250 }, 51 | { account: '0x18638D5563E763703B89c9A5171a060D420aF03E', percent: 100 }, 52 | { account: '0xD04634B1536C39d932FFB5Ef571f806C88a3A69C', percent: 1000 }, 53 | { account: '0xf33a205dCe3F05Bdd2a74875FB8E3b4A042Cd2cf', percent: 1050 }, 54 | { account: '0x5163C7ad7fe5637AC533280dF78cd801529c577d', percent: 4950 }, 55 | { account: '0xAA2f26eD45a3C55c9824F717C05460D198230943', percent: 200 }, 56 | { account: '0x178307eF9f8A88CEA9499078207e15F8d50428C6', percent: 100 }, 57 | { account: '0x45CC60482d650D58E54E7d0BE2D18D49586E8e6C', percent: 100 } 58 | ]; 59 | const index = 1; 60 | 61 | // Act 62 | const proof = getProof(collab, 1); 63 | 64 | //Assert 65 | expect(proof).toEqual([ 66 | '0x4d4c8d9415b89b0b7ad6b1e9c1bd9042c4f477cf7e1c2e39d2a7c5ecfd430df9', 67 | '0xf358c032ddb09e4e9a99d1fc45f1d1ab2c00fecf476e1cb99cb4957939bd9db9', 68 | '0xf3b0c46cddf20b754dfaf0655cf0a6c4dea3de2cae66a8f30eebf87f7b3f50de', 69 | '0x45617ba582385cc439e4b0240a59c02936a72ddf449e1cd41e0f2eb62da200ae' 70 | ]); 71 | }); 72 | it('undefined merkleProof[0] error with 3 recipients', () => { 73 | // Arrange 74 | const collab = [ 75 | { 76 | account: ethers.utils.getAddress('0xf4274229bee63d4a6d1edde6919afa815f6e1a25'), 77 | percent: '1000' 78 | }, 79 | { 80 | account: ethers.utils.getAddress('0xf4274229bee63d4a6d1edde6919afa815f6e1a24'), 81 | percent: '1000' 82 | }, 83 | { 84 | account: ethers.utils.getAddress('0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1'), 85 | percent: '8000' 86 | } 87 | ]; 88 | const index = 2; 89 | 90 | // Act 91 | const proof = getProof(collab, index); 92 | 93 | //Assert 94 | expect(proof).toEqual([ 95 | '0x311a521d1f42e8a29951eb8eb01853a85f4ae5b125872d1d082002c74f55a99f', 96 | '0xd4e84c8d2a7b7ca89ca0004242e53b3968503f21f04098078384bf96e8087827' 97 | ]); 98 | }); 99 | it('4 recipients', () => { 100 | // Arrange 101 | const collab = [ 102 | { 103 | account: ethers.utils.getAddress('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'), 104 | percent: '2500' 105 | }, 106 | { 107 | account: ethers.utils.getAddress('0xd03ea8624C8C5987235048901fB614fDcA89b117'), 108 | percent: '2500' 109 | }, 110 | { 111 | account: ethers.utils.getAddress('0x178307eF9f8A88CEA9499078207e15F8d50428C6'), 112 | percent: '2500' 113 | }, 114 | { 115 | account: ethers.utils.getAddress('0x45CC60482d650D58E54E7d0BE2D18D49586E8e6C'), 116 | percent: '2500' 117 | } 118 | ]; 119 | const index = 0; 120 | 121 | // Act 122 | const proof = getProof(collab, index); 123 | 124 | //Assert 125 | expect(proof).toEqual([ 126 | '0x35c86ef1e398febacf078a86c631859479360a9662af5986002ce092a7735e14', 127 | '0x75e4a6020c81cf47cbf0ff7fc635359766ea52317134f8a73a8edd170f9a0dbb' 128 | ]); 129 | }); 130 | it('2 recipients', () => { 131 | // Arrange 132 | const collab = [ 133 | { 134 | account: ethers.utils.getAddress('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'), 135 | percent: '4900' 136 | }, 137 | { 138 | account: ethers.utils.getAddress('0xd03ea8624C8C5987235048901fB614fDcA89b117'), 139 | percent: '5100' 140 | } 141 | ]; 142 | const index = 0; 143 | 144 | // Act 145 | const proof = getProof(collab, index); 146 | 147 | //Assert 148 | expect(proof).toEqual(['0xf21c91086f8942ce5413b4b2d54aa7d2b9b9bc141081fa9293b6808ca758b4d8']); 149 | }); 150 | it('1 recipient', () => { 151 | // Arrange 152 | const collab = [ 153 | { 154 | account: ethers.utils.getAddress('0xf4274229bee63d4a6d1edde6919afa815f6e1a25'), 155 | percent: '10000' 156 | } 157 | ]; 158 | const index = 0; 159 | 160 | // Act 161 | const proof = getProof(collab, index); 162 | 163 | //Assert 164 | expect(proof).toEqual([]); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /sdk/test/modules/thegraph.test.js: -------------------------------------------------------------------------------- 1 | import { getCollab, getAllocationsByAccount } from '../../src/modules/thegraph'; 2 | 3 | beforeAll(() => { 4 | process.env = { 5 | THEGRAPH_URL: 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby' 6 | }; 7 | }); 8 | describe('getAllocationsByAccount()', () => { 9 | it('existing account with 1 collab', async () => { 10 | // Arrange 11 | 12 | // Act 13 | global.fetch = jest.fn(() => 14 | Promise.resolve({ 15 | json: () => 16 | Promise.resolve({ 17 | data: { 18 | account: { 19 | allocations: [ 20 | { 21 | allocation: '5000', 22 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-1', 23 | splitter: { 24 | allocations: [ 25 | { 26 | allocation: '5000', 27 | recipient: { 28 | id: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 29 | } 30 | }, 31 | { 32 | allocation: '5000', 33 | recipient: { 34 | id: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' 35 | } 36 | } 37 | ], 38 | allocationsCount: '2', 39 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34', 40 | name: 'Test Stendhal', 41 | tokens: [] 42 | } 43 | } 44 | ], 45 | id: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 46 | } 47 | } 48 | }) 49 | }) 50 | ); 51 | const result = await getAllocationsByAccount( 52 | fetch, 53 | process.env.THEGRAPH_URL, 54 | '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 55 | ); 56 | 57 | // Assert 58 | expect(result).toEqual([ 59 | { 60 | allocation: '5000', 61 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-1', 62 | splitter: { 63 | allocations: [ 64 | { 65 | allocation: '5000', 66 | recipient: { 67 | id: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 68 | } 69 | }, 70 | { 71 | allocation: '5000', 72 | recipient: { 73 | id: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' 74 | } 75 | } 76 | ], 77 | allocationsCount: '2', 78 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34', 79 | name: 'Test Stendhal', 80 | tokens: [] 81 | } 82 | } 83 | ]); 84 | }); 85 | 86 | it('non existing account', async () => { 87 | // Arrange 88 | 89 | // Act 90 | global.fetch = jest.fn(() => 91 | Promise.resolve({ 92 | json: () => 93 | Promise.resolve({ 94 | data: { account: null } 95 | }) 96 | }) 97 | ); 98 | const result = await getAllocationsByAccount(fetch, process.env.THEGRAPH_URL, '0x123'); 99 | 100 | // Assert 101 | expect(result).toEqual([]); 102 | }); 103 | }); 104 | 105 | describe('getCollab()', () => { 106 | it('existing collab', async () => { 107 | // Act 108 | global.fetch = jest.fn(() => 109 | Promise.resolve({ 110 | json: () => 111 | Promise.resolve({ 112 | data: { 113 | collabSplitter: { 114 | allocations: [ 115 | { 116 | allocation: '5000', 117 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-1', 118 | recipient: { 119 | id: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 120 | } 121 | }, 122 | { 123 | allocation: '5000', 124 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-2', 125 | recipient: { 126 | id: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' 127 | } 128 | } 129 | ], 130 | allocationsCount: '2', 131 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34', 132 | name: 'Test Stendhal', 133 | tokens: [] 134 | } 135 | } 136 | }) 137 | }) 138 | ); 139 | const result = await getCollab( 140 | fetch, 141 | process.env.THEGRAPH_URL, 142 | '0xc506cd1a02b4c1223b005298b54f16aca31bbf34' 143 | ); 144 | 145 | // Assert 146 | expect(result).toEqual({ 147 | allocations: [ 148 | { 149 | allocation: '5000', 150 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-1', 151 | recipient: { 152 | id: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' 153 | } 154 | }, 155 | { 156 | allocation: '5000', 157 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34-2', 158 | recipient: { 159 | id: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' 160 | } 161 | } 162 | ], 163 | allocationsCount: '2', 164 | id: '0xc506cd1a02b4c1223b005298b54f16aca31bbf34', 165 | name: 'Test Stendhal', 166 | tokens: [] 167 | }); 168 | }); 169 | it('non existing collab', async () => { 170 | // Arrange 171 | 172 | // Act 173 | global.fetch = jest.fn(() => 174 | Promise.resolve({ 175 | json: () => 176 | Promise.resolve({ 177 | data: { account: null } 178 | }) 179 | }) 180 | ); 181 | const result = await getCollab(fetch, process.env.THEGRAPH_URL, '0x123'); 182 | 183 | // Assert 184 | expect(result).toBeUndefined(); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /sdk/test/utils/numbers.test.js: -------------------------------------------------------------------------------- 1 | import { convertUIntToPercentage, convertPercentageToUint } from '../../src/utils/numbers'; 2 | 3 | describe('convertUIntToPercentage()', () => { 4 | it('4999', () => { 5 | // Arrange 6 | const bigint = 4999; 7 | // Act 8 | const result = convertUIntToPercentage(bigint); 9 | 10 | // Assert 11 | expect(result).toEqual(49.99); 12 | }); 13 | it('0', () => { 14 | // Arrange 15 | const bigint = 0; 16 | // Act 17 | const result = convertUIntToPercentage(bigint); 18 | 19 | // Assert 20 | expect(result).toEqual(0); 21 | }); 22 | it('20000', () => { 23 | // Arrange 24 | const bigint = 20000; 25 | // Act 26 | const result = convertUIntToPercentage(bigint); 27 | 28 | // Assert 29 | expect(result).toEqual(200.0); 30 | }); 31 | }); 32 | describe('convertPercentageToUint()', () => { 33 | it('49.99%', () => { 34 | // Arrange 35 | const percent = 49.99; 36 | // Act 37 | const result = convertPercentageToUint(percent); 38 | 39 | // Assert 40 | expect(result).toEqual(4999); 41 | }); 42 | it('0%', () => { 43 | // Arrange 44 | const percent = 0; 45 | // Act 46 | const result = convertPercentageToUint(percent); 47 | 48 | // Assert 49 | expect(result).toEqual(0); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /website/.env: -------------------------------------------------------------------------------- 1 | VITE_FACTORY_ADDRESS=0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B 2 | VITE_THEGRAPH_URL=http://192.168.1.44:8000/subgraphs/name/dievardump/collab-splitter 3 | VITE_EXPLORER_URL=https://rinkeby.etherscan.io -------------------------------------------------------------------------------- /website/.env.SAMPLE: -------------------------------------------------------------------------------- 1 | VITE_FACTORY_ADDRESS=0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6 2 | VITE_THEGRAPH_URL=https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby 3 | VITE_EXPLORER_URL=https://rinkeby.etherscan.io -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | functions 5 | .svelte-kit 6 | .idea 7 | .svelte 8 | *.log -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | .svelte-kit/** 2 | static/** 3 | build/** 4 | node_modules/** 5 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Collab-splitter website 2 | 3 | A convenient website to interact with the smart contract (Factory or created collab splitter) & see indexed info from TheGraph. 4 | 5 | Built with [SvelteKit](https://kit.svelte.dev/) using [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte) 6 | 7 | ## Developing 8 | 9 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 10 | 11 | ```bash 12 | npm run dev 13 | 14 | # or start the server and open the app in a new browser tab 15 | npm run dev -- --open 16 | ``` 17 | 18 | ## Building 19 | 20 | Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then: 21 | 22 | ```bash 23 | npm run build 24 | ``` 25 | 26 | > You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production. 27 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-splitter-website", 3 | "version": "0.0.1", 4 | "author": "Anthony Graignic", 5 | "license": "MIT", 6 | "scripts": { 7 | "preinstall": "cd ../sdk && npm install", 8 | "dev": "svelte-kit dev", 9 | "prebuild": "cd ../sdk && npm run build", 10 | "build": "svelte-kit build", 11 | "preview": "svelte-kit preview", 12 | "check": "svelte-check --tsconfig ./tsconfig.json", 13 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 14 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", 15 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." 16 | }, 17 | "devDependencies": { 18 | "@sveltejs/kit": "next", 19 | "@types/jest": "^27.0.2", 20 | "@typescript-eslint/eslint-plugin": "^4.33.0", 21 | "@typescript-eslint/parser": "^4.33.0", 22 | "autoprefixer": "^10.4.0", 23 | "cssnano": "^5.0.8", 24 | "eslint": "^7.32.0", 25 | "eslint-config-prettier": "^8.3.0", 26 | "eslint-plugin-svelte3": "^3.2.1", 27 | "postcss": "^8.3.11", 28 | "postcss-load-config": "^3.1.0", 29 | "prettier": "~2.2.1", 30 | "prettier-plugin-svelte": "^2.4.0", 31 | "svelte": "^3.44.0", 32 | "svelte-check": "^2.2.7", 33 | "svelte-preprocess": "^4.9.8", 34 | "sveltejs-adapter-ipfs": "^0.1.10", 35 | "tailwindcss": "^2.2.17", 36 | "tslib": "^2.3.1", 37 | "typescript": "^4.4.4" 38 | }, 39 | "type": "module", 40 | "dependencies": { 41 | "yasp-modals": "^1.0.7" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /website/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require("tailwindcss"); 2 | const autoprefixer = require("autoprefixer"); 3 | const cssnano = require("cssnano"); 4 | 5 | const mode = process.env.NODE_ENV; 6 | const dev = mode === "development"; 7 | 8 | const config = { 9 | plugins: [ 10 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 11 | tailwindcss(), 12 | //But others, like autoprefixer, need to run after, 13 | autoprefixer(), 14 | !dev && cssnano({ 15 | preset: "default", 16 | }) 17 | ], 18 | }; 19 | 20 | module.exports = config; -------------------------------------------------------------------------------- /website/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %svelte.head% 9 | 10 | 11 |
%svelte.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /website/src/app.postcss: -------------------------------------------------------------------------------- 1 | @import './css/base.css'; 2 | @import "./css/vars.css"; 3 | @import "./css/common.css"; -------------------------------------------------------------------------------- /website/src/css/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /website/src/css/common.css: -------------------------------------------------------------------------------- 1 | html { 2 | color: var(--primary); 3 | background-color: var(--secondary); 4 | 5 | --current-color: var(--primary); 6 | --current-bg: var(--secondary); 7 | } 8 | 9 | #svelte { 10 | @apply flex flex-col; 11 | min-height: 100vh; 12 | } 13 | 14 | main { 15 | flex: 1 1 0; 16 | } 17 | 18 | hr { 19 | border: 1px solid var(--secondary); 20 | } 21 | 22 | input { 23 | color: var(--secondary); 24 | background-color: var(--primary); 25 | } 26 | 27 | button { 28 | @apply bg-blue-600 rounded px-2 py-2 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-opacity-50 font-bold; 29 | } 30 | 31 | button.light { 32 | @apply bg-transparent border-solid border border-blue-600; 33 | @apply !text-blue-600; 34 | } 35 | 36 | a { 37 | text-decoration: underline; 38 | text-decoration-color: var(--current-color); 39 | } 40 | 41 | button:disabled { 42 | opacity: 0.5; 43 | pointer-events: none; 44 | } 45 | 46 | main { 47 | @apply container mx-auto px-2; 48 | } 49 | 50 | h1 { 51 | @apply text-center text-2xl my-6; 52 | @apply font-light uppercase; 53 | position: relative; 54 | } 55 | 56 | h1::after, 57 | h2::after, 58 | h3::after { 59 | content: ''; 60 | position: absolute; 61 | top: 100%; 62 | left: 50%; 63 | transform: translateX(-50%); 64 | height: 2px; 65 | width: 40px; 66 | background-color: var(--current-color); 67 | } 68 | 69 | h2 { 70 | @apply text-center text-xl my-4; 71 | @apply font-light uppercase; 72 | position: relative; 73 | } 74 | h3 { 75 | @apply text-center text-lg my-3; 76 | @apply font-light uppercase; 77 | position: relative; 78 | } 79 | 80 | .error { 81 | @apply text-center mt-8 text-red-500; 82 | } 83 | 84 | .modals__close { 85 | display: none; 86 | } 87 | -------------------------------------------------------------------------------- /website/src/css/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: rgba(20, 20, 20, 1); 3 | --black-25: rgba(20, 20, 20, 0.25); 4 | --black-10: rgba(20, 20, 20, 0.1); 5 | --whitey: rgba(245, 245, 245, 1); 6 | --whitey-25: rgba(245, 245, 245, 0.25); 7 | --whitey-10: rgba(245, 245, 245, 0.1); 8 | 9 | --primary: var(--whitey); 10 | --primary-25: var(--whitey-25); 11 | --primary-10: var(--whitey-10); 12 | --secondary: var(--black); 13 | --secondary-25: var(--black-25); 14 | --secondary-10: var(--black-10); 15 | } 16 | -------------------------------------------------------------------------------- /website/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | interface ImportMetaEnv { 6 | NODE_ENV: string; 7 | } 8 | -------------------------------------------------------------------------------- /website/src/lib/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 | 30 | 31 | 43 | -------------------------------------------------------------------------------- /website/src/lib/components/Form.svelte: -------------------------------------------------------------------------------- 1 | 107 | 108 |
109 |
110 | 111 | 117 |
118 |
119 | Recipients * 120 | {#if recipients.length} 121 | 122 | 123 | 124 | 126 | 127 | 129 | 130 | 131 | {#each recipients as recipient, index} 132 | 133 | 134 | 137 | 140 | 148 | 149 | {/each} 150 | 151 |
125 | AddressAllocation 128 |
135 | {shortenAddress(recipient.account)} 136 | 138 | {recipient.percent}% 139 | 141 | 144 | 147 |
152 | {:else} 153 |

No recipient yet!

154 | {/if} 155 |
156 |
157 | 158 |
159 |

160 | Total: {sum}% 161 | 162 | {#if sum > 100} 163 | Error: total is greater than 100%. 164 | {:else if sum < 100} 165 | 166 | ({100 - sum}% left to split) 167 | 168 | {/if} 169 | 170 |

171 |

172 | {recipients.length} recipients. 173 |

174 | 175 | 176 | 177 | {#if !mustReset} 178 | 187 | {:else} 188 | 189 | {/if} 190 | 191 | 192 |
193 | 194 | 259 | -------------------------------------------------------------------------------- /website/src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |
13 | Collab splitter 15 | 16 | 27 |
28 | {#if !$connected} 29 | 30 | {:else} 31 | {#await $signer.getAddress() then address} 32 | 46 | {/await} 47 | {/if} 48 |
49 |
50 | 51 | 149 | -------------------------------------------------------------------------------- /website/src/lib/components/Loading.svelte: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 | 14 | 15 | 16 |
17 | 18 | 23 | -------------------------------------------------------------------------------- /website/src/lib/components/Modals/AddRecipientModal.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 |

Add recipient

22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 42 | % 43 |
44 |
45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 87 | -------------------------------------------------------------------------------- /website/src/lib/components/Modals/BaseModal.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#if canClose} 24 | 27 | {/if} 28 | 29 | 30 | 31 |
32 | 33 | 69 | -------------------------------------------------------------------------------- /website/src/lib/components/Modals/CreationSuccessModal.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |

Success

21 |
22 | Congratulations! 23 |

You just created your new Collab Splitter contract

24 |
25 |
26 |
Name
27 |
{name}
28 |
Address
29 |
30 | {address} 31 | {#if !copied} 32 | 33 | {:else} 34 | ✓ Copied 35 | {/if} 36 |
37 |
Links
38 |
39 | 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 | 83 | -------------------------------------------------------------------------------- /website/src/lib/components/Network/OnlyKnownNetwork.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if !$currentNetwork} 8 | {#if showMessage} 9 |
10 |

Unknown network.

11 |

12 | This section needs access to the blockchain to work.
Please change network on your 13 | wallet for one of the following: 14 |

15 |

16 | {listNetworks() 17 | .map((network) => network.name) 18 | .join(', ')} 19 |

20 |
21 | {/if} 22 | {:else} 23 | 24 | {/if} 25 | 26 | 38 | -------------------------------------------------------------------------------- /website/src/lib/components/OnlyConnected.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if !$connected} 10 |
11 | 12 |
13 | {:else} 14 | 15 | {/if} 16 | 17 | 22 | -------------------------------------------------------------------------------- /website/src/lib/components/SVGs.svelte: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 33 | 34 | 35 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | GitHub 112 | 113 | 114 | -------------------------------------------------------------------------------- /website/src/lib/data/networks.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '1': { 3 | name: 'Ethereum', 4 | private: false, 5 | factory_address: '0x486E4CCd2970C1971f41AA16EEFf078f821F3E9a', 6 | graph_url: 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-mainnet', 7 | explorer_url: 'https://etherscan.io', 8 | }, 9 | '4': { 10 | name: 'Rinkeby', 11 | private: true, // this network only shows up if it's selected; because test network 12 | factory_address:'0x916Edd1cbf7A77924168409a24c343Aff22Ac7f6', 13 | graph_url: 'https://api.thegraph.com/subgraphs/name/stendhal-labs/collab-splitter-rinkeby', 14 | explorer_url: 'https://rinkeby.etherscan.io', 15 | } 16 | }; -------------------------------------------------------------------------------- /website/src/lib/modules/network.ts: -------------------------------------------------------------------------------- 1 | import { variables } from '$lib/modules/variables'; 2 | 3 | import networks from '$lib/data/networks'; 4 | import { writable } from 'svelte/store'; 5 | 6 | export const currentNetwork = writable(null); 7 | 8 | export function getNetwork(chainId) { 9 | chainId = parseInt(chainId); 10 | return networks[chainId]; 11 | } 12 | 13 | export function listNetworks(includePrivates = (variables.NODE_ENV == 'development')) { 14 | return ( 15 | Object.keys(networks) 16 | .map((networkId) => { 17 | const network = networks[networkId]; 18 | network.id = networkId; 19 | return network; 20 | }) 21 | // filter on vite env 22 | .filter((network) => includePrivates || !network.private) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /website/src/lib/modules/provider.js: -------------------------------------------------------------------------------- 1 | let ethereumProvider = null; 2 | let signer = null; 3 | let provider = null; 4 | 5 | export async function connectWallet() { 6 | ethereumProvider = window.ethereum; 7 | if (!ethereumProvider) { 8 | throw new Error('No ethereum provider.'); 9 | } 10 | 11 | await ethereumProvider 12 | .request({ method: 'eth_requestAccounts' }); 13 | 14 | // A Web3Provider wraps a standard Web3 provider, which is 15 | // what Metamask injects as window.ethereum into each page 16 | provider = new ethers.providers.Web3Provider(ethereumProvider); 17 | 18 | return { provider, ethereumProvider }; 19 | } 20 | -------------------------------------------------------------------------------- /website/src/lib/modules/variables.ts: -------------------------------------------------------------------------------- 1 | export const variables = { 2 | NODE_ENV: import.meta.env.DEV == true ? 'development' : 'production', 3 | }; 4 | -------------------------------------------------------------------------------- /website/src/lib/modules/wallet.js: -------------------------------------------------------------------------------- 1 | import { get, writable } from 'svelte/store'; 2 | import { connectWallet } from './provider'; 3 | 4 | export const ethereumProvider = writable(null); 5 | export const signer = writable(null); 6 | export const provider = writable(null); 7 | export const chainId = writable(null); 8 | export const connected = writable(false); 9 | export const account = writable(null); 10 | 11 | ethereumProvider.subscribe((value) => { 12 | if (value) { 13 | value.on('accountsChanged', handleAccountsChanged); 14 | value.on('chainChanged', handleChainChanged); 15 | } 16 | }); 17 | 18 | signer.subscribe((value) => { 19 | if (value) { 20 | connected.set(true); 21 | } else { 22 | connected.set(false); 23 | account.set(null); 24 | } 25 | }); 26 | 27 | export async function connect() { 28 | const instances = await connectWallet(); 29 | provider.set(instances.provider); 30 | ethereumProvider.set(instances.ethereumProvider); 31 | 32 | handleChainChanged(instances.ethereumProvider.chainId); 33 | handleAccountsChanged(); 34 | } 35 | 36 | // on account change 37 | async function handleAccountsChanged() { 38 | // set account before signer as OnlyConnected render based on signer 39 | const address = await getProvider().getSigner().getAddress(); 40 | account.set(address.toLowerCase()); 41 | signer.set(getProvider().getSigner()); 42 | } 43 | 44 | function handleChainChanged(_chainId) { 45 | chainId.set(_chainId); 46 | } 47 | 48 | export function getAccount() { 49 | return get(account); 50 | } 51 | 52 | export function getSigner() { 53 | return get(signer); 54 | } 55 | 56 | export function getEthereumProvider() { 57 | return get(ethereumProvider); 58 | } 59 | 60 | export function getProvider() { 61 | return get(provider); 62 | } 63 | -------------------------------------------------------------------------------- /website/src/lib/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | export async function copyToClipBoard(text: string) { 2 | if (navigator.clipboard) { 3 | await navigator.clipboard.writeText(text); 4 | } else { 5 | console.log('Write to clipBoard not supported'); 6 | } 7 | } -------------------------------------------------------------------------------- /website/src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function shortenAddress(addr: string): string { 2 | if (!addr) return ''; 3 | return (addr.substr(0, 6) + '...' + addr.substr(-3)).toLocaleLowerCase(); 4 | } 5 | 6 | 7 | export function isAddressValid(address = '') { 8 | try { 9 | return ethers.utils.getAddress(address); 10 | } catch (e) { 11 | return false; 12 | } 13 | } -------------------------------------------------------------------------------- /website/src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 |