-
Notifications
You must be signed in to change notification settings - Fork 49
feat: finish implementing affiliate revenue endpoint #1229
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
base: develop
Are you sure you want to change the base?
Changes from all commits
579ccea
66f1b99
06252e5
0098b66
8bbbee4
91bad42
ca0fddc
31de586
3cfdcb5
e000a71
e93a7bc
26e88f7
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 |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ | |
| "@shapeshiftoss/common-api": "^10.0.0", | ||
| "@shapeshiftoss/prometheus": "^10.0.0", | ||
| "bottleneck": "^2.19.5", | ||
| "elliptic-sdk": "^0.7.2" | ||
| "elliptic-sdk": "^0.7.2", | ||
| "lru-cache": "^10.2.0", | ||
| "viem": "^2.33.2" | ||
|
Comment on lines
+18
to
+20
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check latest versions for the three dependencies
echo "=== Latest versions from npm registry ==="
for pkg in elliptic-sdk lru-cache viem; do
version=$(curl -s "https://registry.npmjs.org/$pkg" | jq -r '.["dist-tags"].latest // "not found"')
echo "$pkg: $version"
done
# Also get some metadata for context
echo -e "\n=== Package metadata ==="
curl -s "https://registry.npmjs.org/elliptic-sdk" | jq -r '{name: .name, latest: .["dist-tags"].latest, updated: .time.modified}' 2>/dev/null | head -5
curl -s "https://registry.npmjs.org/viem" | jq -r '{name: .name, latest: .["dist-tags"].latest, updated: .time.modified}' 2>/dev/null | head -5
curl -s "https://registry.npmjs.org/lru-cache" | jq -r '{name: .name, latest: .["dist-tags"].latest, updated: .time.modified}' 2>/dev/null | head -5
# Check npm audit data via npm registry (package advisories)
echo -e "\n=== Checking package vulnerability data ==="
curl -s "https://registry.npmjs.org/elliptic-sdk" | jq -r '.readme' 2>/dev/null | head -20Repository: shapeshift/unchained Length of output: 839 🌐 Web query:
💡 Result: Short answer: I couldn't find any public 2025 security advisories specifically for the npm package "elliptic‑sdk". The package appears on npm (latest published versions ~0.9.x) with no recorded advisories, but it depends on/uses crypto libraries — notably the widely used "elliptic" library — which had multiple security issues disclosed in 2024–2025 that can affect projects that depend on it. See sources below and check your dependency tree (npm ls / audit) to confirm indirect exposure. Key related findings (2024–2025)
References
If you want, I can:
Citations:
🌐 Web query:
💡 Result: As of Jan 5, 2026:
If you want, I can watch releases or check for any newly published advisories and notify you. Sources: 🌐 Web query:
💡 Result: I found no public security advisories for the npm package lru-cache as of January 5, 2026. Sources: Snyk (no known issues) [1], the package page on npm (recent releases, no advisories listed) [2], and ReversingLabs/Spectra scans (no vulnerabilities) [3]. If you want, I can run a dependency scan on a specific project or monitor advisories for future changes. Upgrade elliptic-sdk to 0.9.1 or later due to critical security vulnerabilities in the elliptic cryptography library. The version specified (0.7.2) is outdated. The underlying Additionally, viem is 10 patch versions behind (2.33.2 → 2.43.5); consider updating as well, though no security advisories were found for viem itself. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import axios from 'axios' | ||
| import { Fees } from '..' | ||
| import { | ||
| getCacheableThreshold, | ||
| getDateEndTimestamp, | ||
| getDateStartTimestamp, | ||
| groupFeesByDate, | ||
| saveCachedFees, | ||
| splitDateRange, | ||
| tryGetCachedFees, | ||
| } from '../cache' | ||
| import { SLIP44 } from '../constants' | ||
| import { BEBOP_API_KEY, BEBOP_API_URL, FEE_BPS_DENOMINATOR, NANOSECONDS_PER_SECOND, SHAPESHIFT_REFERRER } from './constants' | ||
| import type { TradesResponse } from './types' | ||
|
|
||
| const fetchFeesFromAPI = async (startTimestamp: number, endTimestamp: number): Promise<Fees[]> => { | ||
| const fees: Fees[] = [] | ||
|
|
||
| const start = startTimestamp * NANOSECONDS_PER_SECOND | ||
| const end = endTimestamp * NANOSECONDS_PER_SECOND | ||
|
|
||
| const { data } = await axios.get<TradesResponse>(BEBOP_API_URL, { | ||
| params: { source: SHAPESHIFT_REFERRER, start, end }, | ||
| headers: { 'source-auth': BEBOP_API_KEY }, | ||
| }) | ||
|
|
||
| for (const trade of data.results) { | ||
| if (!trade.partnerFeeBps || !trade.partnerFeeNative) continue | ||
|
|
||
| const chainId = `eip155:${trade.chain_id}` | ||
| const assetId = `${chainId}/slip44:${SLIP44.ETHEREUM}` | ||
|
|
||
| fees.push({ | ||
| chainId, | ||
| assetId, | ||
| service: 'bebop', | ||
| txHash: trade.txHash, | ||
| timestamp: Math.floor(new Date(trade.timestamp).getTime() / 1000), | ||
| amount: trade.partnerFeeNative, | ||
| amountUsd: | ||
| trade.volumeUsd !== undefined ? String(trade.volumeUsd * (Number(trade.partnerFeeBps) / FEE_BPS_DENOMINATOR)) : undefined, | ||
| }) | ||
| } | ||
|
|
||
| return fees | ||
| } | ||
|
|
||
| export const getFees = async (startTimestamp: number, endTimestamp: number): Promise<Fees[]> => { | ||
| const threshold = getCacheableThreshold() | ||
| const { cacheableDates, recentStart } = splitDateRange(startTimestamp, endTimestamp, threshold) | ||
|
|
||
| const cachedFees: Fees[] = [] | ||
| const datesToFetch: string[] = [] | ||
| let cacheHits = 0 | ||
| let cacheMisses = 0 | ||
|
|
||
| for (const date of cacheableDates) { | ||
| const cached = tryGetCachedFees('bebop', 'all', date) | ||
| if (cached) { | ||
| cachedFees.push(...cached) | ||
| cacheHits++ | ||
| } else { | ||
| datesToFetch.push(date) | ||
| cacheMisses++ | ||
| } | ||
| } | ||
|
|
||
| const newFees: Fees[] = [] | ||
| if (datesToFetch.length > 0) { | ||
| const fetchStart = getDateStartTimestamp(datesToFetch[0]) | ||
| const fetchEnd = getDateEndTimestamp(datesToFetch[datesToFetch.length - 1]) | ||
| const fetched = await fetchFeesFromAPI(fetchStart, fetchEnd) | ||
|
|
||
| const feesByDate = groupFeesByDate(fetched) | ||
| for (const date of datesToFetch) { | ||
| saveCachedFees('bebop', 'all', date, feesByDate[date] || []) | ||
| } | ||
| newFees.push(...fetched) | ||
| } | ||
|
|
||
| const recentFees: Fees[] = [] | ||
| if (recentStart !== null) { | ||
| recentFees.push(...(await fetchFeesFromAPI(recentStart, endTimestamp))) | ||
| } | ||
|
|
||
| console.log(`[bebop] Cache stats: ${cacheHits} hits, ${cacheMisses} misses`) | ||
|
|
||
| return [...cachedFees, ...newFees, ...recentFees] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export const BEBOP_API_KEY = process.env.BEBOP_API_KEY | ||
|
|
||
| if (!BEBOP_API_KEY) throw new Error('BEBOP_API_KEY env var not set') | ||
|
|
||
| export const BEBOP_API_URL = 'https://api.bebop.xyz/history/v2/trades' | ||
| export const SHAPESHIFT_REFERRER = 'shapeshift' | ||
| export const NANOSECONDS_PER_SECOND = 1_000_000_000 | ||
| export const FEE_BPS_DENOMINATOR = 10000 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { getFees } from './bebop' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| export type TradesResponse = { | ||
| results: Array<{ | ||
| chain_id: number | ||
| txHash: string | ||
| status: string | ||
| type: string | ||
| taker: string | ||
| receiver: string | ||
| sellTokens: Record<string, { amount?: string; amountUsd?: number }> | ||
| buyTokens: Record<string, { amount?: string; amountUsd?: number }> | ||
| volumeUsd?: number | ||
| gasFeeUsd?: number | ||
| timestamp: string | ||
| route: 'JAM' | 'PMM' | ||
| gasless: boolean | ||
| partnerFeeNative?: string | ||
| partnerFeeBps?: string | ||
| }> | ||
| nextAvailableTimestamp?: string | ||
| metadata: { | ||
| timestamp: string | ||
| results?: number | ||
| tokens: Record< | ||
| string, | ||
| Record< | ||
| string, | ||
| { | ||
| name: string | ||
| symbol: string | ||
| decimals: number | ||
| displayDecimals?: number | ||
| icon?: string | ||
| } | ||
| > | ||
| > | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { createHash } from 'crypto' | ||
| import { encodeAbiParameters, parseAbiParameters } from 'viem' | ||
| import { Fees } from '..' | ||
| import { getDateRange, getDateStartTimestamp } from '../cache' | ||
| import { | ||
| API_SUCCESS_CODE, | ||
| BUTTERSWAP_AFFILIATE_ID, | ||
| BUTTERSWAP_CONTRACT, | ||
| FALLBACK_TOKENS, | ||
| GET_TOTAL_BALANCE_SELECTOR, | ||
| HEX_PREFIX_LENGTH, | ||
| HEX_RADIX, | ||
| MAP_CHAIN_ID, | ||
| MAP_USDT_ADDRESS, | ||
| TOKEN_CACHE_TTL_MS, | ||
| TOKEN_LIST_API, | ||
| UINT256_HEX_LENGTH, | ||
| USDT_DECIMALS, | ||
| } from './constants' | ||
| import type { TokenListResponse } from './types' | ||
| import { estimateBlockFromTimestamp, rpcCall } from './utils' | ||
|
|
||
| let cachedTokens: string[] | null = null | ||
| let tokensCachedAt = 0 | ||
|
|
||
| const fetchTokenList = async (): Promise<string[]> => { | ||
| const now = Date.now() | ||
| if (cachedTokens && now - tokensCachedAt < TOKEN_CACHE_TTL_MS) { | ||
| return cachedTokens | ||
| } | ||
|
|
||
| try { | ||
| const response = await fetch(TOKEN_LIST_API) | ||
| const data: TokenListResponse = await response.json() | ||
|
|
||
| if (data.errno === API_SUCCESS_CODE && data.data?.items?.length > 0) { | ||
| cachedTokens = data.data.items.map((t) => t.address.toLowerCase()) | ||
| tokensCachedAt = now | ||
| return cachedTokens | ||
| } | ||
| } catch { | ||
| // Fall through to return fallback tokens | ||
| } | ||
|
|
||
| return FALLBACK_TOKENS | ||
| } | ||
|
|
||
| const getBlockNumber = async (): Promise<number> => { | ||
| const result = await rpcCall<string>('eth_blockNumber', []) | ||
| return parseInt(result, HEX_RADIX) | ||
| } | ||
|
|
||
| const getTotalBalance = async (blockNumber: number, tokens: string[]): Promise<bigint> => { | ||
| const params = encodeAbiParameters(parseAbiParameters('uint256, address[], address'), [ | ||
| BigInt(BUTTERSWAP_AFFILIATE_ID), | ||
| tokens as `0x${string}`[], | ||
| MAP_USDT_ADDRESS as `0x${string}`, | ||
| ]) | ||
|
|
||
| const data = GET_TOTAL_BALANCE_SELECTOR + params.slice(HEX_PREFIX_LENGTH) | ||
| const blockHex = `0x${blockNumber.toString(HEX_RADIX)}` | ||
|
|
||
| const result = await rpcCall<string>('eth_call', [{ to: BUTTERSWAP_CONTRACT, data }, blockHex]) | ||
|
|
||
| return BigInt(result.slice(0, UINT256_HEX_LENGTH)) | ||
| } | ||
|
|
||
| const generateSyntheticTxHash = (service: string, date: string): string => { | ||
| return '0x' + createHash('sha256').update(`${service}-${date}`).digest('hex') | ||
| } | ||
|
|
||
| export const getFees = async (startTimestamp: number, endTimestamp: number): Promise<Array<Fees>> => { | ||
| const tokens = await fetchTokenList() | ||
|
|
||
| const currentBlock = await getBlockNumber() | ||
| const now = Math.floor(Date.now() / 1000) | ||
|
|
||
| const startBlock = estimateBlockFromTimestamp(currentBlock, now, startTimestamp) | ||
| const endBlock = estimateBlockFromTimestamp(currentBlock, now, endTimestamp) | ||
|
|
||
| const [balanceAtStart, balanceAtEnd] = await Promise.all([ | ||
| getTotalBalance(startBlock, tokens), | ||
| getTotalBalance(endBlock, tokens), | ||
| ]).catch(error => { | ||
| const message = error instanceof Error ? error.message : String(error) | ||
| throw new Error(`Failed to query ButterSwap balance: ${message}`) | ||
| }) | ||
|
|
||
| const feesForPeriod = balanceAtEnd - balanceAtStart | ||
|
|
||
| if (feesForPeriod <= BigInt(0)) { | ||
| return [] | ||
| } | ||
|
|
||
| const dates = getDateRange(startTimestamp, endTimestamp) | ||
| const numDays = dates.length | ||
|
|
||
| const feesPerDay = feesForPeriod / BigInt(numDays) | ||
| const feesPerDayUsd = Number(feesPerDay) / 10 ** USDT_DECIMALS | ||
|
|
||
| return dates.map((date) => ({ | ||
| service: 'butterswap', | ||
| amount: feesPerDay.toString(), | ||
| amountUsd: feesPerDayUsd.toString(), | ||
| chainId: MAP_CHAIN_ID, | ||
| assetId: `${MAP_CHAIN_ID}/erc20:${MAP_USDT_ADDRESS}`, | ||
| timestamp: getDateStartTimestamp(date), | ||
| txHash: generateSyntheticTxHash('butterswap', date), | ||
| })) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { BUTTERSWAP_AFFILIATE_ID, BUTTERSWAP_CONTRACT, MAP_CHAIN_ID, MAP_RPC_URL, MAP_USDT_ADDRESS } from '../constants' | ||
|
|
||
| export { BUTTERSWAP_AFFILIATE_ID, BUTTERSWAP_CONTRACT, MAP_CHAIN_ID, MAP_RPC_URL, MAP_USDT_ADDRESS } | ||
|
|
||
| export const BLOCK_TIME_SECONDS = 5 | ||
| export const USDT_DECIMALS = 18 | ||
| export const TOKEN_LIST_API = 'https://butterapi.chainservice.io/api/token/bam/list' | ||
| export const TOKEN_CACHE_TTL_MS = 60 * 60 * 1000 | ||
|
|
||
| export const GET_TOTAL_BALANCE_SELECTOR = '0x47b2f8d9' | ||
| export const API_SUCCESS_CODE = 0 | ||
| export const HEX_RADIX = 16 | ||
| export const HEX_PREFIX_LENGTH = 2 | ||
| export const UINT256_HEX_LENGTH = 66 | ||
|
|
||
| export const FALLBACK_TOKENS = [ | ||
| '0x05ab928d446d8ce6761e368c8e7be03c3168a9ec', | ||
| '0x33daba9618a75a7aff103e53afe530fbacf4a3dd', | ||
| '0x9f722b2cb30093f766221fd0d37964949ed66918', | ||
| '0xb877e3562a660c7861117c2f1361a26abaf19beb', | ||
| '0x5de6606ae1250c64560a603b40078de268240fdd', | ||
| '0xc478a25240d9c072ebec5109b417e0a78a41667c', | ||
| '0x593a37fe0f6dfd0b6c5a051e9a44aa0f6922a1a2', | ||
| '0x0e9e7317c7132604c009c9860a259a3da33a3ed3', | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { getFees } from './butterswap' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export type RpcResponse<T> = { | ||
| jsonrpc: string | ||
| id: number | ||
| result: T | ||
| error?: { code: number; message: string } | ||
| } | ||
|
|
||
| export type TokenListResponse = { | ||
| errno: number | ||
| message: string | ||
| data: { | ||
| items: Array<{ address: string; symbol: string }> | ||
| total: number | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.