From 4fb365fc3d7a5be31969feff46147f9bd4c18261 Mon Sep 17 00:00:00 2001 From: Jacob Homanics <32080359+Hotmanics@users.noreply.github.com> Date: Fri, 10 Nov 2023 05:22:51 -0600 Subject: [PATCH] [hats-protocol-hat-ids] added hat ids stategy (#1339) * added hat ids stategy * Update src/strategies/hats-protocol-hat-ids/README.md Co-authored-by: Chaitanya * updated read me --------- Co-authored-by: Chaitanya --- .../hats-protocol-hat-ids/README.md | 30 ++ .../hats-protocol-hat-ids/examples.json | 26 ++ src/strategies/hats-protocol-hat-ids/index.ts | 259 ++++++++++++++++++ .../hats-protocol-hat-ids/schema.json | 34 +++ src/strategies/index.ts | 4 +- 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 src/strategies/hats-protocol-hat-ids/README.md create mode 100644 src/strategies/hats-protocol-hat-ids/examples.json create mode 100644 src/strategies/hats-protocol-hat-ids/index.ts create mode 100644 src/strategies/hats-protocol-hat-ids/schema.json diff --git a/src/strategies/hats-protocol-hat-ids/README.md b/src/strategies/hats-protocol-hat-ids/README.md new file mode 100644 index 000000000..5b6a324fc --- /dev/null +++ b/src/strategies/hats-protocol-hat-ids/README.md @@ -0,0 +1,30 @@ +# hats-protocol-hat-ids + +Grants voting power based on if voter has a set of specified hat (IP based). + +Here is an example of parameters: + +```json +{ + "address": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137", + "hatIds": "[\"68\"]" +} +``` + +or + +```json +{ + "address": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137", + "hatIds": "[\"68\", \"68.1\"]" +} +``` + +or + +```json +{ + "address": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137", + "hatIds": "[\"68\", \"68.1\", \"68.1.1\"]" +} +``` diff --git a/src/strategies/hats-protocol-hat-ids/examples.json b/src/strategies/hats-protocol-hat-ids/examples.json new file mode 100644 index 000000000..23215aa25 --- /dev/null +++ b/src/strategies/hats-protocol-hat-ids/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "hats-protocol-hat-ids", + "params": { + "address": "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137", + "hatIds": [ + "68", + "68.1", + "68.1.1", + "68.1.1.1" + ] + } + }, + "network": "5", + "addresses": [ + "0xc4f6578c24c599F195c0758aD3D4861758d703A3", + "0xa6aF0566EF4eF7E8f38913f69d4e55c06F00A5aC", + "0x00e7332F9Cd4C05a0645AC959Fb1Be60ec24F94f", + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x4D6Ed22Ed0850384622EF129932aE29D27a89eD3" + ], + "snapshot": 10015682 + } +] \ No newline at end of file diff --git a/src/strategies/hats-protocol-hat-ids/index.ts b/src/strategies/hats-protocol-hat-ids/index.ts new file mode 100644 index 000000000..ca9ec1321 --- /dev/null +++ b/src/strategies/hats-protocol-hat-ids/index.ts @@ -0,0 +1,259 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { multicall } from '../../utils'; + +export const author = 'hotmanics'; +export const version = '1.0.0'; + +const abi = [ + 'function isWearerOfHat(address _user, uint256 _hatId) external view returns (bool isWearer)' +]; + +async function subgraphRequestHats({ url, snapshot, treeIp }) { + + let treeHex = treeIdDecimalToHex(treeIp); + + const params = { + tree: { + __args: { + id: treeHex + }, + id: true, + hats: { + id: true, + wearers: { + id: true + } + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.tree.__args.block = { number: snapshot }; + } + const result = await subgraphRequest(url, params); + return result; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + //This strategy currently enforces that all hatIds passed in are from the same tree. + for (let i = 0; i < options.hatIds.length; i++) { + let lhs = treeIpFromIp(options.hatIds[i]); + + for (let j = 0; j < options.hatIds.length; j++) { + let rhs = treeIpFromIp(options.hatIds[j]); + if (lhs !== rhs) { + throw Error("You can only use hats from the same tree!"); + } + } + } + + //all hatIds are assumed to be from the same tree, set selectedTree to any, and continue. + let selectedTree = treeIpFromIp(options.hatIds[0]); + + const request = { + url: getActiveNetworkSubgraphURL(network), + snapshot, + treeIp: selectedTree + } + + const result = await subgraphRequestHats(request); + + let validHats: any[] = []; + + for (let j = 0; j < options.hatIds.length; j++) { + for (let i = 0; i < result.tree.hats.length; i++) { + + let hatIpHex = HatIpToHex(options.hatIds[j]); + + if (hatIpHex === result.tree.hats[i].id) { + validHats.push(result.tree.hats[i]); + break; + } + } + } + + let wearersInAddresses = []; + + addresses.forEach((address) => { + const wearer = checkIfExists(address, validHats); + wearersInAddresses = wearersInAddresses.concat(wearer); + }); + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + wearersInAddresses.forEach((wearer) => { + multi.call(wearer.address, options.address, 'isWearerOfHat', [ + wearer.address, + wearer.hat + ]); + }); + + const multiResult = await multi.execute(); + + const myObj = {}; + + wearersInAddresses.forEach((wearer) => { + myObj[wearer.address] = 0; + for (const result of multiResult) { + if (wearer.address === result.address) { + myObj[wearer.address] = 1; + break; + } + } + }); + + return myObj; +} + +function getActiveNetworkSubgraphURL(network) { + + let url; + + switch (network) { + case '1': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-ethereum'; + break; + case '10': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-optimism'; + break; + case '5': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-goerli'; + break; + case '137': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-polygon'; + break; + case '100': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-gnosis-chain'; + break; + case '42161': + url = 'https://api.thegraph.com/subgraphs/name/hats-protocol/hats-v1-arbitrum'; + break; + } + + return url; +} + +function checkIfExists(address, hats) { + const addressWithHats = []; + hats.forEach((hat) => { + hat.wearers.forEach((wearer) => { + if (getAddress(wearer.id) === address) { + const addressWithHat = { + address: getAddress(wearer.id), + hat: BigInt(hat.id) + }; + addressWithHats.push(addressWithHat); + } + }); + }); + return addressWithHats; +} + +function treeIdDecimalToHex(treeId: number): string { + return "0x" + treeId.toString(16).padStart(8, "0"); +} + +function HatIpToHex(hatIp) { + let observedChunk = hatIp; + + const sections: Number[] = []; + + while (true) { + if (observedChunk.indexOf(".") === -1) { + let section = observedChunk.substring(0, observedChunk.length); + sections.push(Number(section)); + break; + } + + let section = observedChunk.substring(0, observedChunk.indexOf(".")); + observedChunk = observedChunk.substring(observedChunk.indexOf(".") + 1, observedChunk.length); + + sections.push(Number(section)); + } + + let constructedResult = "0x"; + + for (let i = 0; i < sections.length; i++) { + let hex = sections[i].toString(16); + + if (i === 0) { + constructedResult += hex.padStart(10 - hex.length, "0"); + } else { + constructedResult += hex.padStart(5 - hex.length, "0"); + } + + } + + constructedResult = constructedResult.padEnd(66, "0"); + return constructedResult; +} + +function treeIpFromIp(hatIp) { + let treeIp; + + if (hatIp.indexOf(".") === -1) + treeIp = hatIp; + else + treeIp = hatIp.substring(0, hatIp.indexOf(".")); + + return Number(treeIp); +} + +class Multicaller { + public network: string; + public provider: StaticJsonRpcProvider; + public abi: any[]; + public options: any = {}; + public calls: any[] = []; + public paths: any[] = []; + + constructor( + network: string, + provider: StaticJsonRpcProvider, + abi: any[], + options? + ) { + this.network = network; + this.provider = provider; + this.abi = abi; + this.options = options || {}; + } + + call(path, address, fn, params?): Multicaller { + this.calls.push([address, fn, params]); + this.paths.push(path); + return this; + } + + async execute(): Promise { + const obj = []; + const result = await multicall( + this.network, + this.provider, + this.abi, + this.calls, + this.options + ); + result.forEach((r, i) => { + obj.push({ + address: this.paths[i], + value: r + }); + }); + this.calls = []; + this.paths = []; + return obj; + } +} \ No newline at end of file diff --git a/src/strategies/hats-protocol-hat-ids/schema.json b/src/strategies/hats-protocol-hat-ids/schema.json new file mode 100644 index 000000000..6c64583cb --- /dev/null +++ b/src/strategies/hats-protocol-hat-ids/schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "Contract address", + "examples": [ + "e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" + ], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "hatIds": { + "type": "array", + "title": "Hat Ids", + "examples": [ + "e.g. [\"68\", \"68.1\", \"68.1.1\", \"68.1.1.1\"" + ] + } + }, + "required": [ + "address", + "hatIds" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 00b037886..af349d5f2 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -472,6 +472,7 @@ import * as stationScoreIfBadge from './station-score-if-badge'; import * as stationConstantIfBadge from './station-constant-if-badge'; import * as mangroveStationQVScaledToMGV from './mangrove-station-qv-scaled-to-mgv'; import * as floki from './floki'; +import * as hatsProtocolHatIds from "./hats-protocol-hat-ids"; const strategies = { 'cap-voting-power': capVotingPower, @@ -952,7 +953,8 @@ const strategies = { 'station-score-if-badge': stationScoreIfBadge, 'station-constant-if-badge': stationConstantIfBadge, 'mangrove-station-qv-scaled-to-mgv': mangroveStationQVScaledToMGV, - floki + floki, + 'hats-protocol-hat-ids': hatsProtocolHatIds }; Object.keys(strategies).forEach(function (strategyName) {