From 047ddaeac41c07fedb446a35e5c00b6f90cce79e Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Fri, 19 Aug 2022 12:22:30 +0200 Subject: [PATCH 1/2] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fe390b2..5054444 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ src/types/* !src/types/factories src/types/factories/* !src/types/factories/Greeter__factory.ts +**/.DS_Store From 7dc6064c4a7dd61f9c218e6e06d39ea50643c117 Mon Sep 17 00:00:00 2001 From: Mick de Graaf Date: Wed, 24 Aug 2022 15:19:22 +0200 Subject: [PATCH 2/2] Distribution scripts --- README.md | 27 +++++++ hardhat.config.ts | 1 + package.json | 3 +- tasks/merkle/index.ts | 151 +++++++++++++++++++++++++++++++++++++ utils/ChannelMerkleTree.ts | 19 ++++- yarn.lock | 147 ++++++++++++++++++++++++++++++++++-- 6 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 tasks/merkle/index.ts diff --git a/README.md b/README.md index 1e1b448..44bb4ce 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,33 @@ TBD ## Usage +### Generating random drops for testing + +```sh +npx hardhat generate-dummy-amounts --output output2.json --tokens 0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002 +``` + +### Generate cumalative amounts from previous drops + +```sh +npx hardhat generate-cummalitive-amounts --prev prev.json --new new.json --output cummalative.json +``` + +### Generating merle tree files + +```sh +# Seperate files for each address +npx hardhat generate-merkle-tree --input output.json --output tree --seperate +# One file with all the proofs +npx hardhat generate-merkle-tree --input output.json --output tree +``` + +### Uploading to ipfs through pinata +```sh +npx hardhat pinata-pin --input ./tree --pinata-key PINATA_KEY --pinata-secret PINATA_SECRET +``` + + ### Pre Requisites Before running any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an environment diff --git a/hardhat.config.ts b/hardhat.config.ts index 7d30683..6b106db 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,6 +6,7 @@ import "solidity-coverage"; import "./tasks/accounts"; import "./tasks/deploy"; +import "./tasks/merkle"; import { resolve } from "path"; diff --git a/package.json b/package.json index 7002ee9..49ae288 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain" }, "dependencies": { - "@openzeppelin/contracts": "^4.5.0" + "@openzeppelin/contracts": "^4.5.0", + "@pinata/sdk": "^1.1.26" } } diff --git a/tasks/merkle/index.ts b/tasks/merkle/index.ts new file mode 100644 index 0000000..6765553 --- /dev/null +++ b/tasks/merkle/index.ts @@ -0,0 +1,151 @@ +import { BigNumber } from "ethers"; +import { parseEther } from "ethers/lib/utils"; +import { task } from "hardhat/config"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { readdir, writeFile } from "node:fs/promises"; +import pinataSDK from "@pinata/sdk"; +import ChannelMerkleTree, { ChannelMerkleTreeEntry } from "../../utils/ChannelMerkleTree"; + +interface Amounts { + [address: string]: { + [token: string]: string; + }; +} + +interface Entries { + [address: string]: { + [token: string]: { + cumulativeAmount: string; + proof: string[]; + }; + } +} + +task("generate-dummy-amounts", "example: npx hardhat generate-dummy-amounts --output output.json --tokens 0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002") + .addParam("output") + .addParam("tokens", "tokens to generate amounts for comma separated ie: 0x00000000.....11111,0x0x0000000....2222") + .setAction(async(taskArgs, { ethers }) => { + const signers = await ethers.getSigners(); + + const amounts: Amounts = {}; + const tokens = taskArgs.tokens.split(",").map((token: string) => token.trim()); + + for(let signer of signers) { + const tokensShuffled = tokens.sort(() => 0.5 - Math.random()); + const tokensRandomSubset = tokensShuffled.slice(0, Math.floor(Math.random() * tokensShuffled.length)); + // Do nothing if there are no entries for this signer + if(tokensRandomSubset.length === 0) { + continue; + } + + amounts[signer.address] = {}; + + for(let token of tokensRandomSubset) { + amounts[signer.address][token] = parseEther( + (Math.random() * 1000000).toFixed(18).toString() + ).toString(); + } + } + + const output = JSON.stringify(amounts, null, 2); + writeFileSync(taskArgs.output, output); +}); + +task("generate-cumalitive-amounts") + .addParam("prev") + .addParam("new") + .addParam("output") + .setAction(async(taskArgs, { ethers }) => { + const prevAmounts: Amounts = JSON.parse(readFileSync(taskArgs.prev).toString()); + const newAmounts: Amounts = JSON.parse(readFileSync(taskArgs.new).toString()); + + const cumalitiveAmounts: Amounts = {...prevAmounts}; + + for(let address in newAmounts) { + // If not present yet create new entry + if(!cumalitiveAmounts[address]) { + cumalitiveAmounts[address] = {...newAmounts[address]}; + continue; + } + // Otherwise add new amounts to existing entry + for(let token in newAmounts[address]) { + if(!cumalitiveAmounts[address][token]) { + cumalitiveAmounts[address][token] = newAmounts[address][token]; + continue; + } + cumalitiveAmounts[address][token] = BigNumber.from(prevAmounts[address][token]).add(BigNumber.from(newAmounts[address][token])).toString(); + } + } + + const output = JSON.stringify(cumalitiveAmounts, null, 2); + writeFileSync(taskArgs.output, output); +}); + +task("generate-merkle-tree") + .addParam("input") + .addParam("output") + .addFlag("seperate") + .setAction(async(taskArgs, { ethers }) => { + const input: Amounts = JSON.parse(readFileSync(taskArgs.input).toString()); + const merkleEntries: ChannelMerkleTreeEntry[] = []; + + for(let address in input) { + for(let token in input[address]) { + merkleEntries.push({ + token, + address, + cumulativeAmount: input[address][token], + }); + } + } + + const merkleTree = new ChannelMerkleTree(merkleEntries); + const result: Entries = {}; + + for(let address in input) { + result[address] = {}; + for(let token in input[address]) { + result[address][token] = { + cumulativeAmount: input[address][token], + proof: merkleTree.getProof(address, token, input[address][token]), + }; + } + } + + if(existsSync(taskArgs.output)) { + rmSync(taskArgs.output, { recursive: true }); + } + mkdirSync(taskArgs.output, { recursive: true }); + + await writeFile(`${taskArgs.output}/root.json`, JSON.stringify(merkleTree.merkleTree.getRoot(), null, 2)); + + if(!taskArgs.seperate) { + const output = JSON.stringify(result, null, 2); + writeFileSync(`${taskArgs.output}/tree.json`, output); + return; + } + + const writePromises: Promise[] = []; + for(let address in result) { + const output = JSON.stringify(result[address], null, 2); + // console.log(output); + writePromises.push(writeFile(`${taskArgs.output}/${address}.json`, output)); + } + + + + await Promise.all(writePromises); +}); + +task("pinata-pin") + .addParam("input") + // .addParam("ipfsNode") + .addParam("pinataKey") + .addParam("pinataSecret") + .setAction(async(taskArgs, { ethers }) => { + const files = await readdir(taskArgs.input); + const pinata = pinataSDK(taskArgs.pinataKey, taskArgs.pinataSecret); + const response = await pinata.pinFromFS(taskArgs.input); + + console.log(response); +}); diff --git a/utils/ChannelMerkleTree.ts b/utils/ChannelMerkleTree.ts index 6e91d78..f743639 100644 --- a/utils/ChannelMerkleTree.ts +++ b/utils/ChannelMerkleTree.ts @@ -1,10 +1,16 @@ -import { ethers } from "ethers"; +import { BigNumberish, ethers } from "ethers"; import { MerkleTree } from "./MerkleTree"; +export interface ChannelMerkleTreeEntry { + token: string; + address: string; + cumulativeAmount: BigNumberish; +} + export default class ChannelMerkleTree { merkleTree: MerkleTree; - constructor(entries: { token: string; address: string; cumulativeAmount: number }[]) { + constructor(entries: ChannelMerkleTreeEntry[]) { const hashes = entries.map(({ token, address, cumulativeAmount }) => this.hashEntry(address, token, cumulativeAmount), ); @@ -12,13 +18,18 @@ export default class ChannelMerkleTree { this.merkleTree = new MerkleTree(hashes); } - hashEntry(address: string, token: string, cumulativeAmount: number) { + hashEntry(address: string, token: string, cumulativeAmount: BigNumberish) { return ethers.utils.solidityKeccak256(["address", "address", "uint256"], [address, token, cumulativeAmount]); } - getProof = (address: string, token: string, cumulativeAmount: number) => { + getProof = (address: string, token: string, cumulativeAmount: BigNumberish) => { const hash = this.hashEntry(address, token, cumulativeAmount); return this.merkleTree.getProof(hash); }; + + getProofFromEntry = (entry: ChannelMerkleTreeEntry) => { + return this.getProof(entry.address, entry.token, entry.cumulativeAmount); + } + } diff --git a/yarn.lock b/yarn.lock index 42894a5..9c109a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,6 +1566,7 @@ __metadata: "@nomiclabs/hardhat-etherscan": ^3.0.0 "@nomiclabs/hardhat-waffle": ^2.0.2 "@openzeppelin/contracts": ^4.5.0 + "@pinata/sdk": ^1.1.26 "@typechain/ethers-v5": ^9.0.0 "@typechain/hardhat": ^4.0.0 "@types/chai": ^4.3.0 @@ -1704,6 +1705,19 @@ __metadata: languageName: node linkType: hard +"@pinata/sdk@npm:^1.1.26": + version: 1.1.26 + resolution: "@pinata/sdk@npm:1.1.26" + dependencies: + axios: ^0.21.1 + base-path-converter: ^1.0.2 + form-data: ^2.3.3 + is-ipfs: ^0.6.0 + recursive-fs: ^1.1.2 + checksum: 8b67fa2b634360db6555c8acbc40cf002056fc01475265aa6815b0c7d58e869aa138fc7cb1b279aedf3301cf66b1a19547c3f23f1e4333625287e8f11719b5d4 + languageName: node + linkType: hard + "@resolver-engine/core@npm:^0.3.3": version: 0.3.3 resolution: "@resolver-engine/core@npm:0.3.3" @@ -3058,6 +3072,15 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.21.1": + version: 0.21.4 + resolution: "axios@npm:0.21.4" + dependencies: + follow-redirects: ^1.14.0 + checksum: 44245f24ac971e7458f3120c92f9d66d1fc695e8b97019139de5b0cc65d9b8104647db01e5f46917728edfc0cfd88eb30fc4c55e6053eef4ace76768ce95ff3c + languageName: node + linkType: hard + "babel-code-frame@npm:^6.26.0": version: 6.26.0 resolution: "babel-code-frame@npm:6.26.0" @@ -3703,6 +3726,13 @@ __metadata: languageName: node linkType: hard +"base-path-converter@npm:^1.0.2": + version: 1.0.2 + resolution: "base-path-converter@npm:1.0.2" + checksum: 9fca25198e4588ab46389426ee51b81d296000dbb132fe9228b90f93d0f89dead66468f63da37519e972c634c1ccfe136442c300b9762fb83265532cf2ac32e0 + languageName: node + linkType: hard + "base-x@npm:^3.0.2, base-x@npm:^3.0.8": version: 3.0.8 resolution: "base-x@npm:3.0.8" @@ -3982,7 +4012,7 @@ __metadata: languageName: node linkType: hard -"bs58@npm:^4.0.0": +"bs58@npm:^4.0.0, bs58@npm:^4.0.1": version: 4.0.1 resolution: "bs58@npm:4.0.1" dependencies: @@ -4419,7 +4449,7 @@ __metadata: languageName: node linkType: hard -"cids@npm:^0.7.1": +"cids@npm:^0.7.1, cids@npm:~0.7.0": version: 0.7.5 resolution: "cids@npm:0.7.5" dependencies: @@ -4432,6 +4462,19 @@ __metadata: languageName: node linkType: hard +"cids@npm:~0.8.0": + version: 0.8.3 + resolution: "cids@npm:0.8.3" + dependencies: + buffer: ^5.6.0 + class-is: ^1.1.0 + multibase: ^1.0.0 + multicodec: ^1.0.1 + multihashes: ^1.0.1 + checksum: ca4b18e421a6f5e446e63f296ad5c91b55bd4dd4880a78777857b2279460259946691d383928503c4381f0e05f998c7bfab5b6e623acc2d4d237571d99c53d9d + languageName: node + linkType: hard + "cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": version: 1.0.4 resolution: "cipher-base@npm:1.0.4" @@ -7386,6 +7429,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.14.0": + version: 1.15.1 + resolution: "follow-redirects@npm:1.15.1" + peerDependenciesMeta: + debug: + optional: true + checksum: 6aa4e3e3cdfa3b9314801a1cd192ba756a53479d9d8cca65bf4db3a3e8834e62139245cd2f9566147c8dfe2efff1700d3e6aefd103de4004a7b99985e71dd533 + languageName: node + linkType: hard + "for-each@npm:^0.3.3, for-each@npm:~0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -7416,7 +7469,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^2.2.0": +"form-data@npm:^2.2.0, form-data@npm:^2.3.3": version: 2.5.1 resolution: "form-data@npm:2.5.1" dependencies: @@ -8753,6 +8806,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"ip-regex@npm:^4.0.0": + version: 4.3.0 + resolution: "ip-regex@npm:4.3.0" + checksum: 7ff904b891221b1847f3fdf3dbb3e6a8660dc39bc283f79eb7ed88f5338e1a3d1104b779bc83759159be266249c59c2160e779ee39446d79d4ed0890dfd06f08 + languageName: node + linkType: hard + "ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -9038,6 +9098,29 @@ fsevents@~2.1.1: languageName: node linkType: hard +"is-ip@npm:^3.1.0": + version: 3.1.0 + resolution: "is-ip@npm:3.1.0" + dependencies: + ip-regex: ^4.0.0 + checksum: da2c2b282407194adf2320bade0bad94be9c9d0bdab85ff45b1b62d8185f31c65dff3884519d57bf270277e5ea2046c7916a6e5a6db22fe4b7ddcdd3760f23eb + languageName: node + linkType: hard + +"is-ipfs@npm:^0.6.0": + version: 0.6.3 + resolution: "is-ipfs@npm:0.6.3" + dependencies: + bs58: ^4.0.1 + cids: ~0.7.0 + mafmt: ^7.0.0 + multiaddr: ^7.2.1 + multibase: ~0.6.0 + multihashes: ~0.4.13 + checksum: 10670511dc954e56512449e38faae43b6b36f29dd0132911d951db6e988d6af9daa1f8fb54f16867a17540f0338050addb2a0c1ceba6482a059913031e441ee4 + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -10223,6 +10306,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"mafmt@npm:^7.0.0": + version: 7.1.0 + resolution: "mafmt@npm:7.1.0" + dependencies: + multiaddr: ^7.3.0 + checksum: 5d891f2007e99e6bee0b741b07f65ab81c4bce4a0baab08d97f2b34a72a0b7647e3b6a36a3377162adf56faed18be9a62bc772ef64a4f15e65ea4d034be705f0 + languageName: node + linkType: hard + "make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -10877,6 +10969,20 @@ fsevents@~2.1.1: languageName: node linkType: hard +"multiaddr@npm:^7.2.1, multiaddr@npm:^7.3.0": + version: 7.5.0 + resolution: "multiaddr@npm:7.5.0" + dependencies: + buffer: ^5.5.0 + cids: ~0.8.0 + class-is: ^1.1.0 + is-ip: ^3.1.0 + multibase: ^0.7.0 + varint: ^5.0.0 + checksum: b1228f75af074f7797c37e5701c32732ccbb8828543d24f1a4b39a164c9407d8ae3a6783860fffd5956939d34acf21f76dae426c2dd6f5f598482c70eeae31cc + languageName: node + linkType: hard + "multibase@npm:^0.7.0": version: 0.7.0 resolution: "multibase@npm:0.7.0" @@ -10887,6 +10993,16 @@ fsevents@~2.1.1: languageName: node linkType: hard +"multibase@npm:^1.0.0, multibase@npm:^1.0.1": + version: 1.0.1 + resolution: "multibase@npm:1.0.1" + dependencies: + base-x: ^3.0.8 + buffer: ^5.5.0 + checksum: 5d34398f81dca137aafe65a171ed5d637cf789bebb4fd33e11c186bfecbe6435a3d4f5c0cf15282607215ccc3a55ff4150a42067e7bc7756a42554e5fbc6d0d5 + languageName: node + linkType: hard + "multibase@npm:~0.6.0": version: 0.6.1 resolution: "multibase@npm:0.6.1" @@ -10906,7 +11022,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"multicodec@npm:^1.0.0": +"multicodec@npm:^1.0.0, multicodec@npm:^1.0.1": version: 1.0.4 resolution: "multicodec@npm:1.0.4" dependencies: @@ -10916,7 +11032,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"multihashes@npm:^0.4.15, multihashes@npm:~0.4.15": +"multihashes@npm:^0.4.15, multihashes@npm:~0.4.13, multihashes@npm:~0.4.15": version: 0.4.21 resolution: "multihashes@npm:0.4.21" dependencies: @@ -10927,6 +11043,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"multihashes@npm:^1.0.1": + version: 1.0.1 + resolution: "multihashes@npm:1.0.1" + dependencies: + buffer: ^5.6.0 + multibase: ^1.0.1 + varint: ^5.0.0 + checksum: 21e338dfb23900f7c038ac708fab598b33bc3d8d02f636ff753969c575b934f979dec76936ca142c6fd126a8bd030f7f391a44a3681c92cab28311c8b0b70589 + languageName: node + linkType: hard + "mute-stream@npm:0.0.7": version: 0.0.7 resolution: "mute-stream@npm:0.0.7" @@ -12439,6 +12566,16 @@ fsevents@~2.1.1: languageName: node linkType: hard +"recursive-fs@npm:^1.1.2": + version: 1.1.2 + resolution: "recursive-fs@npm:1.1.2" + bin: + recursive-copy: ./bin/recursive-copy + recursive-delete: ./bin/recursive-delete + checksum: 6b330d2e6b1359b179736c77ecdf64cc98e778fde676ca7b39b3ea6045dbab4d659d625ffb45e1c31871e8ee2c9bc73f9cf72f661baecf1053677614ceef0989 + languageName: node + linkType: hard + "recursive-readdir@npm:^2.2.2": version: 2.2.2 resolution: "recursive-readdir@npm:2.2.2"