Skip to content
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(tokens): add helpers for fetching metadata #77

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-knives-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rabbitholegg/questdk": minor
---

Adds a client and helper functions for fetching metadata given either contract or boost information
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@
"dependencies": {
"abitype": "^0.9.0",
"rimraf": "^5.0.1",
"viem": "^1.15.1"
"viem": "2.7.9",
"axios": "1.6.7"
},
"repository": "rabbitholegg/questdk",
"publishConfig": {
Expand Down
67 changes: 56 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions src/client/ethereumClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
fetchERC721Media,
fetchERC721Metadata,
fetchERC721MetadataByUUID,
} from '../tokens/erc721.js'
import {
fetchERC1155Media,
fetchERC1155Metadata,
fetchERC1155MetadataByUUID,
} from '../tokens/erc1155.js'
import { type PublicClient, createClient, http } from 'viem'
/**
* Initializes an Ethereum client with the specified RPC URL and provides methods for ERC721 and ERC1155 token interactions.
* @param {string} rpcUrl - The RPC URL for connecting to the Ethereum network.
* @returns An object with methods for fetching token metadata and media.
*/
function createEthereumClient(rpcUrl: string) {
const client = createClient({
transport: http(rpcUrl),
}) as PublicClient

return {
fetchERC721Metadata: (contractAddress: string, tokenId: number) =>
fetchERC721Metadata(client, contractAddress, tokenId),
fetchERC721Media: (contractAddress: string, tokenId: number) =>
fetchERC721Media(client, contractAddress, tokenId),
fetchERC1155Metadata: (contractAddress: string, tokenId: number) =>
fetchERC1155Metadata(client, contractAddress, tokenId),
fetchERC1155Media: (contractAddress: string, tokenId: number) =>
fetchERC1155Media(client, contractAddress, tokenId),
fetchERC721MetadataByUUID: (uuid: string) =>
fetchERC721MetadataByUUID(client, uuid),
fetchERC1155MetadataByUUID: (uuid: string) =>
fetchERC1155MetadataByUUID(client, uuid),
}
}

export default createEthereumClient
7 changes: 4 additions & 3 deletions src/filter/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import type {
TransactionFilter,
} from './types.js'
import {
type AbiFunction,
type Address,
type TransactionEIP1559,
decodeAbiParameters,
decodeFunctionData,
getAbiItem,
getFunctionSelector,
isAddress,
parseAbiParameters,
slice,
toFunctionSelector,
} from 'viem'
type OperatorKey = keyof typeof operators

Expand Down Expand Up @@ -217,7 +218,7 @@ export const handleAbiDecode = (context: any, filter: AbiFilter) => {
abi: filter.$abi,
name: functionName,
args,
})
}) as AbiFunction

const namedArgs = [...abiItem.inputs].reduce(
(acc: Record<string, any>, input, index) => {
Expand Down Expand Up @@ -253,7 +254,7 @@ export const handleAbstractAbiDecode = (
for (let i = 0; i < elementCount; i++) {
const abiItem = $abiAbstract![i]
if (abiItem.type === 'function') {
const functionSelector = getFunctionSelector(abiItem)
const functionSelector = toFunctionSelector(abiItem)
// We want to omit the leading 0x from the function selector
const functionSelectorSubstring = functionSelector.substring(2)
const index = contextMap.get(functionSelectorSubstring)
Expand Down
21 changes: 21 additions & 0 deletions src/quests/fetchQuestData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import axios from 'axios'

/**
* Fetches quest data from RabbitHole API and extracts action parameters.
* @param {string} uuid The UUID of the quest.
* @returns {Promise<any>} The action parameters extracted from the quest data.
*/
export async function fetchQuestActionParams(uuid: string): Promise<any> {
const endpoint = `https://api.rabbithole.gg/v1.2/quest/public/${uuid}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would add a TODO or open up an issue to update this endpoint to a Boost API in the near future


try {
const response = await axios.get(endpoint)
const actionParams = response.data.actionParams
return actionParams
} catch (error) {
console.error('Error fetching quest data:', error)
throw new Error('Failed to fetch quest data')
}
}

export default fetchQuestActionParams
65 changes: 65 additions & 0 deletions src/tokens/erc1155.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import fetchQuestActionParams from '../quests/fetchQuestData.js'
import axios from 'axios'
import { type Address, type PublicClient } from 'viem'

/**
* Fetches ERC1155 token metadata from a given contract.
* @async
* @param {PublicClient} client The Viem client instance.
* @param {string} contractAddress The ERC1155 contract address.
* @param {number} tokenId The token ID.
* @returns {Promise<any>} The metadata of the ERC1155 token.
*/
export async function fetchERC1155Metadata(
client: PublicClient,
contractAddress: string,
tokenId: number,
): Promise<any> {
const tokenURI: string = await (client.readContract({
address: contractAddress as Address,
abi: [
'function uri(uint256 tokenId) external view returns (string memory)',
],
functionName: 'uri',
args: [tokenId],
}) as Promise<string>)

const response = await axios.get(tokenURI)
return response.data
}

/**
* Fetches the media URL from ERC1155 token metadata and performs basic validation or sanitization.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think there's "basic validation or sanitization"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not - I'll change this comment we discussed using a service for this but I think we're just going to handle it with the dom purify in the front-end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth purifying on the BE wherever we serve the info to prevent passing dangerous data to third party clients or w/e

* @async
* @param {PublicClient} client - The Viem client instance.
* @param {string} contractAddress - The ERC1155 contract address.
* @param {number} tokenId - The token ID.
* @returns {Promise<string | undefined>} - The media URL if available and valid.
*/
export async function fetchERC1155Media(
client: PublicClient,
contractAddress: string,
tokenId: number,
): Promise<string | undefined> {
const metadata = await fetchERC1155Metadata(client, contractAddress, tokenId)
return metadata.image || metadata.animation_url
}

/**
* Fetches ERC1155 token metadata using quest UUID from RabbitHole.
* @param {PublicClient} client The Viem client instance.
* @param {string} uuid The UUID of the quest.
* @returns {Promise<any>} The metadata of the ERC1155 token associated with the quest.
*/
export async function fetchERC1155MetadataByUUID(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Wonder if it makes sense to rename function to fetchERC1155MetadataByQuestUUID? I don't like the long variable name I suggested though lol

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, rather a more specific long name

client: PublicClient,
uuid: string,
): Promise<any> {
const actionParams = await fetchQuestActionParams(uuid)
if (actionParams.type !== 'mint') {
throw new Error('Quest action is not of type mint')
}
const contractAddress = actionParams.data.contractAddress
const tokenId = actionParams.data.tokenId
return fetchERC1155Metadata(client, contractAddress, tokenId)
}
Loading
Loading