-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
739effe
commit f102a99
Showing
12 changed files
with
1,106 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/*.js | ||
/*.ts | ||
/tests/**/*.js | ||
/dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module.exports = { | ||
extends: '../.eslintrc', | ||
parserOptions: { | ||
project: './tsconfig.json', | ||
parser: '@typescript-eslint/parser', | ||
}, | ||
overrides: [ | ||
{ | ||
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], | ||
env: { | ||
mocha: true, | ||
}, | ||
}, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# `@dgrants/utils` | ||
|
||
This package exposes util functions which would be shared between the dgrants package | ||
|
||
- `contract` | ||
- `app` | ||
- `dcurve` | ||
|
||
|
||
## Structure | ||
|
||
``` | ||
. | ||
├── README.md # Getting started guide | ||
├── src | ||
| ├── merkle-distributor # uniswap's merkle-distributor utils | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "@dgrants/utils", | ||
"version": "0.0.1", | ||
"description": "utils package used between dgrants package", | ||
"keywords": [ | ||
"dgrants", | ||
"utils" | ||
], | ||
"scripts": { | ||
"clean": "rimraf dist", | ||
"build": "echo 'TODO'", | ||
"test": "echo 'TODO'", | ||
"lint": "eslint --ext .ts,.js,.vue .", | ||
"precommit": "lint-staged", | ||
"prettier": "prettier --write ." | ||
}, | ||
"volta": { | ||
"extends": "../package.json" | ||
}, | ||
"dependencies": { | ||
"ethereumjs-util": "^7.1.0", | ||
"ethers": "^5.4.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module.exports = { | ||
...require('../.prettierrc.js'), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// code sourced from: https://github.com/Uniswap/merkle-distributor/blob/master/src/balance-tree.ts | ||
import MerkleTree from './merkle-tree' | ||
import { BigNumber, utils } from 'ethers' | ||
|
||
export default class BalanceTree { | ||
private readonly tree: MerkleTree | ||
constructor(balances: { account: string; amount: BigNumber }[]) { | ||
this.tree = new MerkleTree( | ||
balances.map(({ account, amount }, index) => { | ||
return BalanceTree.toNode(index, account, amount) | ||
}) | ||
) | ||
} | ||
|
||
public static verifyProof( | ||
index: number | BigNumber, | ||
account: string, | ||
amount: BigNumber, | ||
proof: Buffer[], | ||
root: Buffer | ||
): boolean { | ||
let pair = BalanceTree.toNode(index, account, amount) | ||
for (const item of proof) { | ||
pair = MerkleTree.combinedHash(pair, item) | ||
} | ||
|
||
return pair.equals(root) | ||
} | ||
|
||
// keccak256(abi.encode(index, account, amount)) | ||
public static toNode(index: number | BigNumber, account: string, amount: BigNumber): Buffer { | ||
return Buffer.from( | ||
utils.solidityKeccak256(['uint256', 'address', 'uint256'], [index, account, amount]).substr(2), | ||
'hex' | ||
) | ||
} | ||
|
||
public getHexRoot(): string { | ||
return this.tree.getHexRoot() | ||
} | ||
|
||
// returns the hex bytes32 values of the proof | ||
public getProof(index: number | BigNumber, account: string, amount: BigNumber): string[] { | ||
return this.tree.getHexProof(BalanceTree.toNode(index, account, amount)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// code sourced from: https://github.com/Uniswap/merkle-distributor/blob/master/src/merkle-tree.ts | ||
import { bufferToHex, keccak256 } from 'ethereumjs-util' | ||
|
||
export default class MerkleTree { | ||
private readonly elements: Buffer[] | ||
private readonly bufferElementPositionIndex: { [hexElement: string]: number } | ||
private readonly layers: Buffer[][] | ||
|
||
constructor(elements: Buffer[]) { | ||
this.elements = [...elements] | ||
// Sort elements | ||
this.elements.sort(Buffer.compare) | ||
// Deduplicate elements | ||
this.elements = MerkleTree.bufDedup(this.elements) | ||
|
||
this.bufferElementPositionIndex = this.elements.reduce<{ [hexElement: string]: number }>((memo, el, index) => { | ||
memo[bufferToHex(el)] = index | ||
return memo | ||
}, {}) | ||
|
||
// Create layers | ||
this.layers = this.getLayers(this.elements) | ||
} | ||
|
||
getLayers(elements: Buffer[]): Buffer[][] { | ||
if (elements.length === 0) { | ||
throw new Error('empty tree') | ||
} | ||
|
||
const layers = [] | ||
layers.push(elements) | ||
|
||
// Get next layer until we reach the root | ||
while (layers[layers.length - 1].length > 1) { | ||
layers.push(this.getNextLayer(layers[layers.length - 1])) | ||
} | ||
|
||
return layers | ||
} | ||
|
||
getNextLayer(elements: Buffer[]): Buffer[] { | ||
return elements.reduce<Buffer[]>((layer, el, idx, arr) => { | ||
if (idx % 2 === 0) { | ||
// Hash the current element with its pair element | ||
layer.push(MerkleTree.combinedHash(el, arr[idx + 1])) | ||
} | ||
|
||
return layer | ||
}, []) | ||
} | ||
|
||
static combinedHash(first: Buffer, second: Buffer): Buffer { | ||
if (!first) { | ||
return second | ||
} | ||
if (!second) { | ||
return first | ||
} | ||
|
||
return keccak256(MerkleTree.sortAndConcat(first, second)) | ||
} | ||
|
||
getRoot(): Buffer { | ||
return this.layers[this.layers.length - 1][0] | ||
} | ||
|
||
getHexRoot(): string { | ||
return bufferToHex(this.getRoot()) | ||
} | ||
|
||
getProof(el: Buffer) { | ||
let idx = this.bufferElementPositionIndex[bufferToHex(el)] | ||
|
||
if (typeof idx !== 'number') { | ||
throw new Error('Element does not exist in Merkle tree') | ||
} | ||
|
||
return this.layers.reduce((proof, layer) => { | ||
const pairElement = MerkleTree.getPairElement(idx, layer) | ||
|
||
if (pairElement) { | ||
proof.push(pairElement) | ||
} | ||
|
||
idx = Math.floor(idx / 2) | ||
|
||
return proof | ||
}, []) | ||
} | ||
|
||
getHexProof(el: Buffer): string[] { | ||
const proof = this.getProof(el) | ||
|
||
return MerkleTree.bufArrToHexArr(proof) | ||
} | ||
|
||
private static getPairElement(idx: number, layer: Buffer[]): Buffer | null { | ||
const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1 | ||
|
||
if (pairIdx < layer.length) { | ||
return layer[pairIdx] | ||
} else { | ||
return null | ||
} | ||
} | ||
|
||
private static bufDedup(elements: Buffer[]): Buffer[] { | ||
return elements.filter((el, idx) => { | ||
return idx === 0 || !elements[idx - 1].equals(el) | ||
}) | ||
} | ||
|
||
private static bufArrToHexArr(arr: Buffer[]): string[] { | ||
if (arr.some((el) => !Buffer.isBuffer(el))) { | ||
throw new Error('Array is not an array of buffers') | ||
} | ||
|
||
return arr.map((el) => '0x' + el.toString('hex')) | ||
} | ||
|
||
private static sortAndConcat(...args: Buffer[]): Buffer { | ||
return Buffer.concat([...args].sort(Buffer.compare)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/** | ||
* code sourced from: https://github.com/Uniswap/merkle-distributor/blob/master/src/parse-balance-map.ts | ||
* | ||
* changes made: | ||
* - type OldFormat is exported | ||
* - type NewFormat is exported | ||
*/ | ||
import { BigNumber, utils } from 'ethers' | ||
import BalanceTree from './balance-tree' | ||
|
||
const { isAddress, getAddress } = utils | ||
|
||
// This is the blob that gets distributed and pinned to IPFS. | ||
// It is completely sufficient for recreating the entire merkle tree. | ||
// Anyone can verify that all air drops are included in the tree, | ||
// and the tree has no additional distributions. | ||
export interface MerkleDistributorInfo { | ||
merkleRoot: string | ||
tokenTotal: string | ||
claims: { | ||
[account: string]: { | ||
index: number | ||
amount: string | ||
proof: string[] | ||
flags?: { | ||
[flag: string]: boolean | ||
} | ||
} | ||
} | ||
} | ||
|
||
export type OldFormat = { [account: string]: number | string } | ||
export type NewFormat = { address: string; earnings: string; reasons: string } | ||
|
||
export function parseBalanceMap(balances: OldFormat | NewFormat[]): MerkleDistributorInfo { | ||
// if balances are in an old format, process them | ||
const balancesInNewFormat: NewFormat[] = Array.isArray(balances) | ||
? balances | ||
: Object.keys(balances).map( | ||
(account): NewFormat => ({ | ||
address: account, | ||
earnings: `0x${balances[account].toString(16)}`, | ||
reasons: '', | ||
}) | ||
) | ||
|
||
const dataByAddress = balancesInNewFormat.reduce<{ | ||
[address: string]: { amount: BigNumber; flags?: { [flag: string]: boolean } } | ||
}>((memo, { address: account, earnings, reasons }) => { | ||
if (!isAddress(account)) { | ||
throw new Error(`Found invalid address: ${account}`) | ||
} | ||
const parsed = getAddress(account) | ||
if (memo[parsed]) throw new Error(`Duplicate address: ${parsed}`) | ||
const parsedNum = BigNumber.from(earnings) | ||
if (parsedNum.lte(0)) throw new Error(`Invalid amount for account: ${account}`) | ||
|
||
const flags = { | ||
isSOCKS: reasons.includes('socks'), | ||
isLP: reasons.includes('lp'), | ||
isUser: reasons.includes('user'), | ||
} | ||
|
||
memo[parsed] = { amount: parsedNum, ...(reasons === '' ? {} : { flags }) } | ||
return memo | ||
}, {}) | ||
|
||
const sortedAddresses = Object.keys(dataByAddress).sort() | ||
|
||
// construct a tree | ||
const tree = new BalanceTree( | ||
sortedAddresses.map((address) => ({ account: address, amount: dataByAddress[address].amount })) | ||
) | ||
|
||
// generate claims | ||
const claims = sortedAddresses.reduce<{ | ||
[address: string]: { amount: string; index: number; proof: string[]; flags?: { [flag: string]: boolean } } | ||
}>((memo, address, index) => { | ||
const { amount, flags } = dataByAddress[address] | ||
memo[address] = { | ||
index, | ||
amount: amount.toHexString(), | ||
proof: tree.getProof(index, address, amount), | ||
...(flags ? { flags } : {}), | ||
} | ||
return memo | ||
}, {}) | ||
|
||
const tokenTotal: BigNumber = sortedAddresses.reduce<BigNumber>( | ||
(memo, key) => memo.add(dataByAddress[key].amount), | ||
BigNumber.from(0) | ||
) | ||
|
||
return { | ||
merkleRoot: tree.getHexRoot(), | ||
tokenTotal: tokenTotal.toHexString(), | ||
claims, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"extends": "../tsconfig.settings.json", | ||
"compilerOptions": { | ||
"baseUrl": ".", | ||
"allowSyntheticDefaultImports": true, | ||
"esModuleInterop": true, | ||
"importHelpers": true, | ||
"jsx": "preserve", | ||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"], | ||
"module": "esnext", | ||
"moduleResolution": "node", | ||
"paths": { "src/*": ["src/*"] }, | ||
"resolveJsonModule": true, | ||
"skipLibCheck": true, | ||
"sourceMap": true, | ||
"strict": true, | ||
"target": "esnext", | ||
"types": ["webpack-env", "mocha", "chai"] | ||
}, | ||
"include": [ | ||
"src/**/*.ts", | ||
"src/**/*.tsx", | ||
"tests/**/*.ts", | ||
"tests/**/*.tsx" | ||
], | ||
"exclude": ["node_modules"] | ||
} |
Oops, something went wrong.