UNPKG

@ar.io/sdk

Version:

[![codecov](https://codecov.io/gh/ar-io/ar-io-sdk/graph/badge.svg?token=7dXKcT7dJy)](https://codecov.io/gh/ar-io/ar-io-sdk)

258 lines (257 loc) 8.88 kB
import { ARIO_MAINNET_PROCESS_ID, ARWEAVE_TX_REGEX } from '../constants.js'; import { isDistributedEpoch, } from '../types/io.js'; import { parseAoEpochData } from './ao.js'; export const validateArweaveId = (id) => { return ARWEAVE_TX_REGEX.test(id); }; export function isBlockHeight(height) { return height !== undefined && !isNaN(parseInt(height.toString())); } /** * Prune tags that are undefined or empty. * @param tags - The tags to prune. * @returns The pruned tags. */ export const pruneTags = (tags) => { return tags.filter((tag) => tag.value !== undefined && tag.value !== ''); }; export const paginationParamsToTags = (params) => { const tags = [ { name: 'Cursor', value: params?.cursor?.toString() }, { name: 'Limit', value: params?.limit?.toString() }, { name: 'Sort-By', value: params?.sortBy?.toString() }, { name: 'Sort-Order', value: params?.sortOrder?.toString() }, ]; return pruneTags(tags); }; /** * Get the epoch with distribution data for the current epoch * @param arweave - The Arweave instance * @returns The epoch with distribution data */ export const getEpochDataFromGql = async ({ arweave, epochIndex, processId = ARIO_MAINNET_PROCESS_ID, retries = 3, gqlUrl = 'https://arweave-search.goldsky.com/graphql', }) => { // fetch from gql const query = epochDistributionNoticeGqlQuery({ epochIndex, processId }); // add three retries with exponential backoff for (let i = 0; i < retries; i++) { try { const response = (await fetch(gqlUrl, { method: 'POST', body: query, headers: { 'Content-Type': 'application/json', }, }).then((res) => res.json())); // parse the nodes to get the id if (response?.data?.transactions?.edges?.length === 0) { return undefined; } const id = response.data.transactions.edges[0].node.id; // fetch the transaction from arweave const transaction = await arweave.api.get(id); // assert it is the correct type return parseAoEpochData(transaction.data); } catch (error) { if (i === retries - 1) throw error; // Re-throw on final attempt // exponential backoff await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000)); } } return undefined; }; export const getEpochDataFromGqlWithCUFallback = async ({ arweave, ao, epochIndex, processId = ARIO_MAINNET_PROCESS_ID, }) => { const gqlResult = await getEpochDataFromGql({ arweave, epochIndex, processId, }); if (gqlResult) { return gqlResult; } const gqlFallbackResult = await getEpochDataFromGqlFallback({ ao, epochIndex, processId, }); if (gqlFallbackResult) { return gqlFallbackResult; } return undefined; }; export const getEpochDataFromGqlFallback = async ({ ao, epochIndex, processId = ARIO_MAINNET_PROCESS_ID, gqlUrl = 'https://arweave-search.goldsky.com/graphql', }) => { const query = epochDistributionNoticeGqlQueryFallback({ epochIndex, processId, }); const response = await fetch(gqlUrl, { method: 'POST', body: query, headers: { 'Content-Type': 'application/json', }, }); const responseJson = (await response.json()); if (responseJson.data.transactions.edges.length === 0) { return undefined; } for (const edge of responseJson.data.transactions.edges) { const id = edge.node.id; const messageResult = await ao .result({ message: id, process: processId, }) .catch(() => undefined); if (!messageResult) { continue; } for (const message of messageResult?.Messages ?? []) { const data = JSON.parse(message.Data); const tags = message.Tags; // check if the message results include epoch-distribution-notice for the requested epoch index if (tags.some((tag) => tag.name === 'Action' && tag.value === 'Epoch-Distribution-Notice') && tags.some((tag) => tag.name === 'Epoch-Index' && tag.value === epochIndex.toString())) { return parseAoEpochData(data); } } } return undefined; }; /** * Get the epoch with distribution data for the current epoch * @param arweave - The Arweave instance * @param epochIndex - The index of the epoch * @param processId - The process ID (optional, defaults to ARIO_MAINNET_PROCESS_ID) * @returns string - The stringified GQL query */ export const epochDistributionNoticeGqlQuery = ({ epochIndex, processId = ARIO_MAINNET_PROCESS_ID, authorities = ['fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY'], }) => { // write the query const gqlQuery = JSON.stringify({ query: ` query { transactions( tags: [ { name: "From-Process", values: ["${processId}"] } { name: "Action", values: ["Epoch-Distribution-Notice"] } { name: "Epoch-Index", values: ["${epochIndex}"] } { name: "Data-Protocol", values: ["ao"] } ], owners: [${authorities.map((a) => `"${a}"`).join(',')}], first: 1, sort: HEIGHT_DESC ) { edges { node { id } } } } `, }); return gqlQuery; }; // fallback query if the distribution notice does not get cranked export const epochDistributionNoticeGqlQueryFallback = ({ processId = ARIO_MAINNET_PROCESS_ID, owners = ['OAb-n-ZugyN598kZNpfOy0ACelGVmwCQ0kYbgNGDUK8'], // ar.io team wallet ticks once a day }) => { return JSON.stringify({ query: ` query { transactions( tags: [ { name: "Action", values: ["Tick"] } ], first: 100, owners: [${owners.map((a) => `"${a}"`).join(',')}], recipients: ["${processId}"], sort: HEIGHT_DESC ) { edges { node { id } } } } `, }); }; export function sortAndPaginateEpochDataIntoEligibleDistributions(epochData, params) { const rewards = []; const sortBy = params?.sortBy ?? 'eligibleReward'; const sortOrder = params?.sortOrder ?? 'desc'; const limit = params?.limit ?? 100; if (!isDistributedEpoch(epochData)) { return { hasMore: false, items: [], totalItems: 0, limit, sortOrder, sortBy, }; } const eligibleDistributions = epochData?.distributions.rewards.eligible; for (const [gatewayAddress, reward] of Object.entries(eligibleDistributions)) { rewards.push({ type: 'operatorReward', recipient: gatewayAddress, eligibleReward: reward.operatorReward, cursorId: gatewayAddress + '_' + gatewayAddress, gatewayAddress, }); for (const [delegateAddress, delegateRewardQty] of Object.entries(reward.delegateRewards)) { rewards.push({ type: 'delegateReward', recipient: delegateAddress, eligibleReward: delegateRewardQty, cursorId: gatewayAddress + '_' + delegateAddress, gatewayAddress, }); } } // sort the rewards by the sortBy rewards.sort((a, b) => { const aSort = a[sortBy]; const bSort = b[sortBy]; if (aSort === bSort || aSort === undefined || bSort === undefined) { return 0; } if (sortOrder === 'asc') { return aSort > bSort ? 1 : -1; } return aSort < bSort ? 1 : -1; }); // paginate the rewards const start = params?.cursor !== undefined ? rewards.findIndex((r) => r.cursorId === params.cursor) + 1 : 0; const end = limit ? start + limit : rewards.length; return { hasMore: end < rewards.length, items: rewards.slice(start, end), totalItems: rewards.length, limit, sortOrder, nextCursor: rewards[end]?.cursorId, sortBy, }; } export function removeEligibleRewardsFromEpochData(epochData) { if (!isDistributedEpoch(epochData)) { return epochData; } return { ...epochData, distributions: { ...epochData.distributions, rewards: { ...epochData.distributions.rewards, // @ts-expect-error -- remove eligible rewards eligible: undefined, }, }, }; }