Skip to content

Commit

Permalink
feat: add utils package
Browse files Browse the repository at this point in the history
  • Loading branch information
thelostone-mc committed Aug 13, 2021
1 parent 739effe commit f102a99
Show file tree
Hide file tree
Showing 12 changed files with 1,106 additions and 2 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"test": "lerna run test",
"app": "yarn workspace @dgrants/app",
"contracts": "yarn workspace @dgrants/contracts",
"types": "yarn workspace @dgrants/types"
"types": "yarn workspace @dgrants/types",
"utils": "yarn workspace @dgrants/utils"
},
"husky": {
"hooks": {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"references": [
{ "path": "app" },
{ "path": "contracts" },
{ "path": "types" }
{ "path": "types" },
{ "path": "utils" }
],
"exclude": ["node_modules"]
}
4 changes: 4 additions & 0 deletions utils/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*.js
/*.ts
/tests/**/*.js
/dist/*
15 changes: 15 additions & 0 deletions utils/.eslintrc.js
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,
},
},
],
};
17 changes: 17 additions & 0 deletions utils/README.md
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
```
24 changes: 24 additions & 0 deletions utils/package.json
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"
}
}
3 changes: 3 additions & 0 deletions utils/prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require('../.prettierrc.js'),
};
46 changes: 46 additions & 0 deletions utils/src/merkle-distributor/balance-tree.ts
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))
}
}
124 changes: 124 additions & 0 deletions utils/src/merkle-distributor/merkle-tree.ts
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))
}
}
99 changes: 99 additions & 0 deletions utils/src/merkle-distributor/parse-balance-map.ts
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,
}
}
27 changes: 27 additions & 0 deletions utils/tsconfig.json
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"]
}
Loading

0 comments on commit f102a99

Please sign in to comment.