diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 25e4271cf..9f1984697 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -193,6 +193,7 @@ import * as unipoolSameToken from './unipool-same-token'; import * as unipoolUniv2Lp from './unipool-univ2-lp'; import * as unipoolXSushi from './unipool-xsushi'; import * as poap from './poap'; +import * as poapUnlock from './poap-unlock'; import * as poapWithWeight from './poap-with-weight'; import * as poapWithWeightV2 from './poap-with-weight-v2'; import * as uniswapV3 from './uniswap-v3'; @@ -652,6 +653,7 @@ const strategies = { 'unipool-univ2-lp': unipoolUniv2Lp, 'unipool-xsushi': unipoolXSushi, poap: poap, + 'poap-unlock': poapUnlock, 'poap-with-weight': poapWithWeight, 'poap-with-weight-v2': poapWithWeightV2, 'uniswap-v3': uniswapV3, diff --git a/src/strategies/poap-unlock/README.md b/src/strategies/poap-unlock/README.md new file mode 100644 index 000000000..972afa56d --- /dev/null +++ b/src/strategies/poap-unlock/README.md @@ -0,0 +1,14 @@ +# POAP-unlock (erc721) + +Each POAP is implemented as an erc721 with a max supply tokens. + +Returns 1 if account owns any tokens. If `eventsIds` is passed, than it returns 1 if account owns tokens where the event id is included in `eventIds`. + +Here are some examples of parameters: + +```json +{ + "symbol": "POAP", + "eventIds": ["1213", "1293"] +} +``` diff --git a/src/strategies/poap-unlock/examples.json b/src/strategies/poap-unlock/examples.json new file mode 100644 index 000000000..b0e7f6787 --- /dev/null +++ b/src/strategies/poap-unlock/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "poap-unlock", + "params": { + "symbol": "POAP", + "eventIds": ["1213", "1293"] + } + }, + "network": "100", + "addresses": [ + "0x837d21cfda71e93e5257f95ce2c49751675ebcb1", + "0x00ac36c51500e900ab0f4e692fc1338cf70571b2", + "0xdd6f702c2907ce401888d993d7dc185e7a824466" + ], + "snapshot": 25283078 + } +] diff --git a/src/strategies/poap-unlock/index.ts b/src/strategies/poap-unlock/index.ts new file mode 100644 index 000000000..8bb3eb4f6 --- /dev/null +++ b/src/strategies/poap-unlock/index.ts @@ -0,0 +1,112 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import { strategy as erc721Strategy } from '../erc721'; + +export const author = 'paulperegud'; +export const version = '1.0.0'; + +// subgraph query in filter has max length of 500 +const EVENT_IDS_LIMIT = 500; +const POAP_API_ENDPOINT_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap', + '100': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai' +}; +// subgraph query in filter has max length of 500 +const MAX_ACCOUNTS_IN_QUERY = 500; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const shouldFilterForEvents = (options?.eventIds?.length ?? 0) > 0; + + if (!shouldFilterForEvents) { + const results = await erc721Strategy( + space, + network, + provider, + addresses, + { address: '0x22C1f6050E56d2876009903609a2cC3fEf83B415' }, + snapshot + ); + return addresses.reduce((map, address) => { + map[getAddress(address)] = results[address] ?? 0; + return map; + }, {}); + } + + if (shouldFilterForEvents && options.eventIds.length > EVENT_IDS_LIMIT) { + throw new Error(`Max number (${EVENT_IDS_LIMIT}) of event ids exceeded`); + } + + const addressesMap = addresses.reduce((map, address) => { + map[getAddress(address)] = 0; + return map; + }, {}); + const lowercaseAddresses = Object.keys(addressesMap).map((address) => + address.toLowerCase() + ); + + // batch addresses to query into slices of MAX_ACCOUNTS_IN_QUERY size + const lowercaseAddressBatches: string[][] = []; + for (let i = 0; i < lowercaseAddresses.length; i += MAX_ACCOUNTS_IN_QUERY) { + const slice = lowercaseAddresses.slice(i, i + MAX_ACCOUNTS_IN_QUERY); + lowercaseAddressBatches.push(slice); + } + + const query = { + accounts: { + __args: { + where: { + id_in: [] as string[] + } + }, + id: true, + tokens: { + __args: { + where: { + event_in: options.eventIds + } + }, + id: true + } + } + }; + if (snapshot !== 'latest') { + query.accounts.__args['block'] = { number: snapshot }; + } + + const results = await Promise.allSettled<{ + accounts: { id: string; tokens?: { id: string }[] }[]; + }>( + lowercaseAddressBatches.map((addresses) => { + query.accounts.__args.where.id_in = addresses; + return subgraphRequest(POAP_API_ENDPOINT_URL[network], query); + }) + ); + + for (const supplyResponse of results) { + if (supplyResponse.status === 'rejected') continue; + + for (const account of supplyResponse.value.accounts) { + const accountId = getAddress(account.id); + + if (addressesMap[accountId] === undefined) continue; + + addressesMap[accountId] = account.tokens?.length ?? 0; + } + + } + + for (let address in addressesMap) { + if (addressesMap[address] > 0) { + addressesMap[address] = 1; + } + } + + return addressesMap; +}