diff --git a/dexs/pactswap/index.ts b/dexs/pactswap/index.ts new file mode 100644 index 0000000000..34abec09ad --- /dev/null +++ b/dexs/pactswap/index.ts @@ -0,0 +1,23 @@ +import { FetchOptions, FetchV2, SimpleAdapter } from "../../adapters/types"; +import { fetchVolumeFromPactswapAPI, PACTSWAP_SUPPORTED_CHAINS } from "./pactswap"; + +const fetch: FetchV2 = async (options: FetchOptions) => { + const timeframes = await fetchVolumeFromPactswapAPI(options.chain, options.startTimestamp, options.endTimestamp); + + const dailyVolume = options.createBalances(); + timeframes.forEach(({ volume }) => { + dailyVolume.addUSDValue(Number(volume)); + }); + + return { + dailyVolume, + }; +}; + +const adapter: SimpleAdapter = { + version: 2, + fetch, + chains: Array.from(PACTSWAP_SUPPORTED_CHAINS) +}; + +export default adapter; \ No newline at end of file diff --git a/dexs/pactswap/pactswap.ts b/dexs/pactswap/pactswap.ts new file mode 100644 index 0000000000..af1979fb7d --- /dev/null +++ b/dexs/pactswap/pactswap.ts @@ -0,0 +1,56 @@ +import fetchURL from "../../utils/fetchURL"; +import { CHAIN } from "../../helpers/chains"; + +export const PACTSWAP_SUPPORTED_CHAINS = [ + CHAIN.BSC, + CHAIN.BITCOIN, + CHAIN.DOGE, + CHAIN.ETHEREUM, + CHAIN.LITECOIN, + CHAIN.POLYGON, + CHAIN.TRON, +] as const; + +const INDEXER_URL = "https://pactswap-indexer.coinhq.store/api/v1/metrics/"; + +// CAIP-122 chain IDs to bigint for Bitcoin like chains +// # Bitcoin mainnet (see https://github.com/bitcoin/bips/blob/master/bip-0122.mediawiki#definition-of-chain-id) +// bip122:000000000019d6689c085ae165831e93 +// # Litecoin mainnet +// bip122:12a765e31ffd4059bada1e25190f6e98 +// # Dogecoin mainnet +const PACTSWAP_CHAIN_ID_MAP: Record = { + [CHAIN.BSC]: 56n, + [CHAIN.BITCOIN]: BigInt("0x".concat("000000000019d6689c085ae165831e93")), + [CHAIN.DOGE]: BigInt("0x".concat("1a91e3dace36e2be3bf030a65679fe82")), + [CHAIN.ETHEREUM]: 1n, + [CHAIN.LITECOIN]: BigInt("0x".concat("12a765e31ffd4059bada1e25190f6e98")), + [CHAIN.POLYGON]: 137n, + [CHAIN.TRON]: 128n, +}; + +export const fetchVolumeFromPactswapAPI = async ( + chain: string, + startTimestamp: number, + endTimestamp: number +): Promise> => { + const chainId = PACTSWAP_CHAIN_ID_MAP[chain]; + try { + const response = await fetchURL( + `${INDEXER_URL}swaps/timeseries/volume?chain_id=${chainId}×tamp_gt=${startTimestamp}×tamp_lt=${endTimestamp}&interval=day` + ) as Array<{ + timestamp: string; + volume: string; + }>; + return response + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 2) + .filter(Boolean); + } catch (error) { + console.error(error); + return []; + } +}; \ No newline at end of file diff --git a/fees/pactswap/helpers.ts b/fees/pactswap/helpers.ts new file mode 100644 index 0000000000..eb347be5e3 --- /dev/null +++ b/fees/pactswap/helpers.ts @@ -0,0 +1,144 @@ +import { gql, request } from "graphql-request" + +const COINWEB_URL = 'https://api-cloud.coinweb.io' +const queryCount = gql` + query( + $start: NaiveDateTime + $end: NaiveDateTime + $feeSmartContract: String + ) { + network(net: BNB) { + countClaims( + issuer: { + FromSmartContract: $feeSmartContract + } + firstPartOfKey: ["FUNDS"] + datetimeRange: { from: $start, to: $end } + ) + } +} +` + +type QueryFee = { + network: { + fetchClaims: { + claims: Array<{ + blockInfo: FeeClaimBlockInfo + claim: FeeClaimClaim + }> + hasNextPage: boolean + nextPageAnchor: { + hash: string + height: number + } + } + } +} + +type FeeClaimBlockInfo = { + height: number + time: string + hash: string +} +type FeeClaimClaim = { + content: { + body: string + feesStored: `0x${string}` + key: string + } +} +const queryFee = gql` +query ( + $feeSmartContract: String! + $start: NaiveDateTime + $end: NaiveDateTime + $countToReturn: Int! +) { + network(net: BNB) { + fetchClaims( + issuer: { FromSmartContract: $feeSmartContract } + firstPartOfKey: ["FUNDS"] + maxClaimsToReturn: $countToReturn + datetimeRange: { from: $start, to: $end } + ) { + claims { + blockInfo { + height + time + hash + } + claim { + content { + body + feesStored + key + } + issuer + } + } + hasNextPage + nextPageAnchor { + hash + height + } + } + } +}` + +export const getFeeSmartContract = async(): Promise => { + const data = await fetch('https://app.pactswap.io/build-info.json') + const json = await data.json() + try { + const feeSmartContract = json.BTC.L2_CONTRACT_ADDRESS_MAKER.module.instance.parameters.content.owner; + return feeSmartContract + } catch (error) { + throw new Error('Failed to get fee smart contract', { cause: error }) + } +} + +export const getFeeClaimsCount = async(feeSmartContract: string, startTime: string, endTime: string): Promise => { + const countResponse = await request(COINWEB_URL, queryCount, { + start: startTime, + end: endTime, + feeSmartContract: feeSmartContract, + }); + + const count = countResponse.network.countClaims; + return Number(count); +} + +const getFeeClaims = async(feeSmartContract: string, startTime: string, endTime: string, countToReturn: number) => { + const feeResponse = await request(COINWEB_URL, queryFee, { + start: startTime, + end: endTime, + feeSmartContract: feeSmartContract, + countToReturn: countToReturn, + }); + return feeResponse.network.fetchClaims; +} + +export const oneDayInSeconds = 86400; + +export const getFee = async(feeSmartContract: string, startTime: string, endTime: string, countToReturn: number): Promise => { + const feeClaims = await getFeeClaims(feeSmartContract, startTime, endTime, countToReturn); + if (feeClaims.claims.length === 1) { + return Number(feeClaims.claims[0].claim.content.feesStored); + } + + let nearesToEndTime: typeof feeClaims.claims[0] | undefined = undefined; + + for (const claim of feeClaims.claims) { + if (new Date(claim.blockInfo.time) <= new Date(startTime)) { + nearesToEndTime = claim; + } + } + + const bottomClaim = nearesToEndTime || feeClaims.claims[0]; + const bottomFee = Number(bottomClaim.claim.content.feesStored); + + const latesFeeClaim = feeClaims.claims.at(-1); + const latestFee = Number(latesFeeClaim?.claim.content.feesStored); + + const fee = latestFee - bottomFee; + return fee; +} \ No newline at end of file diff --git a/fees/pactswap/index.ts b/fees/pactswap/index.ts new file mode 100644 index 0000000000..512c6564be --- /dev/null +++ b/fees/pactswap/index.ts @@ -0,0 +1,49 @@ +import { FetchOptions, FetchV2, SimpleAdapter } from "../../adapters/types"; +import { CHAIN } from "../../helpers/chains"; +import { getFee, getFeeClaimsCount, getFeeSmartContract, oneDayInSeconds } from "./helpers"; + +const fetch: FetchV2 = async (options: FetchOptions) => { + const dailyFees = options.createBalances(); + const feeSmartContract = await getFeeSmartContract(); + const formattedFeeSmartContract = feeSmartContract.startsWith('0x') ? feeSmartContract : `0x${feeSmartContract}`; + + let startTime = new Date(options.fromTimestamp * 1000).toISOString().replace('Z', ''); + const endTime = new Date(options.toTimestamp * 1000).toISOString().replace('Z', ''); + + let count = await getFeeClaimsCount(formattedFeeSmartContract, startTime, endTime); + + if (!count) { + return { dailyFees } + } + + const maxDays = 30; + let currentDays = 0; + + while (count === 1 && currentDays < maxDays) { + startTime = new Date(new Date(startTime).getTime() - oneDayInSeconds * 1000 * (currentDays + 1)).toISOString().replace('Z', ''); + count = await getFeeClaimsCount(formattedFeeSmartContract, startTime, endTime); + currentDays++; + } + + const fee = await getFee(formattedFeeSmartContract, startTime, endTime, count); + + dailyFees.addGasToken(BigInt(fee).toString()); + + return { dailyFees } +}; + +const adapter: SimpleAdapter = { + version: 2, + adapter: { + [CHAIN.COINWEB]: { + fetch, + } + }, + methodology: { + Fees: 'All fees paid by users for finalising transactions.', + Revenue: 'All fees are distributed to PactSwap fee pool.', + ProtocolRevenue: 'All fees are distributed to PactSwap fee pool.', + } +}; + +export default adapter; diff --git a/helpers/chains.ts b/helpers/chains.ts index bcca12d3d7..cfadb11465 100644 --- a/helpers/chains.ts +++ b/helpers/chains.ts @@ -272,4 +272,5 @@ export enum CHAIN { HIBACHI = "hibachi", SATORI = "satori", SHIBARIUM = "shibarium", + COINWEB = "coinweb", }