-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lens): update validation method #463
Changes from all commits
5043a5c
8b3a767
70f4a74
1c56e74
fba03a1
060ee11
7e602af
39944c4
876f262
5eb4648
945d659
10957c4
0becd9a
e19b9bc
4574fa4
107b59c
b7636c5
d20552b
645e14a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@rabbitholegg/questdk-plugin-utils": minor | ||
"@rabbitholegg/questdk-plugin-lens": minor | ||
--- | ||
|
||
update validation method for lens |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Alchemy, Network } from 'alchemy-sdk' | ||
|
||
const settings = { | ||
apiKey: process.env.ALCHEMY_API_KEY, | ||
network: Network.MATIC_MAINNET, | ||
requestTimeout: 2000, | ||
} | ||
|
||
export const alchemy = new Alchemy(settings) |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
import { alchemy } from './alchemy' | ||
import { | ||
LensClient, | ||
production, | ||
type SimpleCollectOpenActionSettingsFragment, | ||
type MultirecipientFeeCollectOpenActionSettingsFragment, | ||
type SimpleCollectOpenActionSettingsFragment, | ||
production, | ||
} from '@lens-protocol/client' | ||
import { getClient } from './client' | ||
import { Chains } from '@rabbitholegg/questdk-plugin-utils' | ||
import { Address } from 'viem' | ||
import { GetTransfersForOwnerTransferType } from 'alchemy-sdk' | ||
import axios from 'axios' | ||
import { Address, zeroAddress } from 'viem' | ||
|
||
const lensClient = new LensClient({ | ||
environment: production, | ||
|
@@ -20,53 +21,96 @@ export async function hasAddressCollectedPost( | |
if (collectAddress === null) { | ||
return false | ||
} | ||
const amountOwned = await checkAddressOwnsCollect(address, collectAddress) | ||
return amountOwned > 0n | ||
return await checkAddressMintedCollect(address, collectAddress) | ||
} | ||
|
||
export async function getCollectAddress(postId: string) { | ||
const result = await lensClient.publication.fetch({ | ||
forId: postId, | ||
}) | ||
if (!result || result?.__typename === 'Mirror') { | ||
try { | ||
const result = await lensClient.publication.fetch({ | ||
forId: postId, | ||
}) | ||
if (!result || result?.__typename === 'Mirror') { | ||
return null | ||
} | ||
const openActions = result.openActionModules | ||
if (!openActions || openActions.length === 0) { | ||
return null | ||
} | ||
const collectActions = openActions.filter( | ||
(action) => | ||
action.__typename === 'SimpleCollectOpenActionSettings' || | ||
action.__typename === 'MultirecipientFeeCollectOpenActionSettings', | ||
) as Array< | ||
| MultirecipientFeeCollectOpenActionSettingsFragment | ||
| SimpleCollectOpenActionSettingsFragment | ||
> | ||
|
||
const collectNft = collectActions.find((action) => action.collectNft) | ||
const collectAddress = collectNft?.collectNft?.toLowerCase() | ||
return (collectAddress as Address) ?? null | ||
} catch (error) { | ||
console.error('Error fetching collect contract address', error) | ||
return null | ||
} | ||
const openActions = result.openActionModules | ||
if (!openActions || openActions.length === 0) { | ||
return null | ||
} | ||
|
||
export async function checkAddressMintedCollect( | ||
actor: Address, | ||
collectAddress: Address, | ||
) { | ||
try { | ||
return await checkMintedUsingAlchemy(actor, collectAddress) | ||
} catch (err) { | ||
console.error('Error while using alchemy api', err) | ||
return await checkMintedUsingReservoir(actor, collectAddress) | ||
} | ||
const collectActions = openActions.filter( | ||
(action) => | ||
action.__typename === 'SimpleCollectOpenActionSettings' || | ||
action.__typename === 'MultirecipientFeeCollectOpenActionSettings', | ||
) as Array< | ||
| MultirecipientFeeCollectOpenActionSettingsFragment | ||
| SimpleCollectOpenActionSettingsFragment | ||
> | ||
} | ||
|
||
const collectNft = collectActions.find((action) => action.collectNft) | ||
const collectAddress = collectNft?.collectNft?.toLowerCase() | ||
return (collectAddress as Address) ?? null | ||
async function checkMintedUsingAlchemy( | ||
actor: Address, | ||
collectAddress: Address, | ||
) { | ||
// alchemy will work without an api key, but we risk being rate-limited | ||
if (!process.env.ALCHEMY_API_KEY) { | ||
console.error('Alchemy API key not found') | ||
} | ||
|
||
const options = { | ||
contractAddresses: [collectAddress], | ||
} | ||
|
||
const transfers = await alchemy.nft.getTransfersForOwner( | ||
actor, | ||
GetTransfersForOwnerTransferType.TO, | ||
options, | ||
) | ||
|
||
// only count nfts that originate from zeroAddress | ||
const mintedNfts = transfers.nfts.filter((nft) => nft.from === zeroAddress) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clever - you know we could actually handle this just with the contract |
||
|
||
return mintedNfts.length > 0 | ||
} | ||
|
||
export async function checkAddressOwnsCollect( | ||
async function checkMintedUsingReservoir( | ||
actor: Address, | ||
collectAddress: Address, | ||
) { | ||
const client = getClient(Chains.POLYGON_POS) | ||
const result = await client.readContract({ | ||
address: collectAddress as Address, | ||
abi: [ | ||
{ | ||
inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], | ||
name: 'balanceOf', | ||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], | ||
stateMutability: 'view', | ||
type: 'function', | ||
}, | ||
], | ||
functionName: 'balanceOf', | ||
args: [actor], | ||
const baseUrl = 'https://api-polygon.reservoir.tools/users/activity/v6' | ||
const params = new URLSearchParams({ | ||
users: actor, | ||
collection: collectAddress, | ||
limit: '1', | ||
types: 'mint', | ||
}) | ||
return result | ||
|
||
const options = { | ||
method: 'GET', | ||
url: `${baseUrl}?${params.toString()}`, | ||
headers: { | ||
accept: '*/*', | ||
}, | ||
} | ||
|
||
const response = await axios.request(options) | ||
return response.data?.activities?.length > 0 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
export { PluginActionNotImplementedError } from './plugin' | ||
export { | ||
PluginActionNotImplementedError, | ||
ValidationNotValid, | ||
} from './plugin' |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't required, but I put an arbitrary value as the requestTimeout. Thought was to let it timeout soonish and hit the reservoir backup. I couldn't find the default value and didn't want it to hang for too long.
Not sure if this is too much or too little.