Skip to content
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ monitoring/my-kube-prometheus/manifests
monitoring/my-kube-prometheus/alertmanager-config.yaml

# misc
.DS_Store
.DS_Store
docker-compose.override.yml
4 changes: 3 additions & 1 deletion node/proxy/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: shapeshift/unchained

Length of output: 839


🌐 Web query:

elliptic-sdk npm security vulnerabilities 2025

💡 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)

  • Multiple CVEs and advisories affecting the "elliptic" crypto library (private‑key extraction, signature malleability, ECDSA truncation/_truncateToN issues, etc.) were disclosed or tracked in 2024–2025. Examples include advisory entries and vendor reports in early 2025. [1][2][3][4]
  • A Positive Technologies/OSV advisory (PT-2025-7253 / GHSA-vjh7-7g9h-fjfh) reported private‑key leakage via signing malformed input (published Feb 12, 2025). [2]
  • Aggregators and trackers (ResolvedSecurity, Vulert, ResolvedSecurity CVE listings) list CVE‑style issues in elliptic across 2024–2025 (signature malleability, ECDSA/_truncateToN, authentication/truncation anomalies). [3][4][5]

References

  1. elliptic‑sdk npm page (package info). [npm package page]. (npmjs.com)
  2. PT/OSV advisory for elliptic private‑key extraction (PT‑2025‑7253 / GHSA-vjh7‑7g9h‑fjfh). (vulners.com)
  3. ResolvedSecurity entry for CVE‑2024‑48948 (elliptic ECDSA/_truncateToN). (resolvedsecurity.com)
  4. ResolvedSecurity entries for signature malleability CVEs in elliptic (CVE‑2024‑42460 / CVE‑2024‑42461). (resolvedsecurity.com)
  5. Vulert / vulnerability database summary of critical private‑key extraction in elliptic (Dec 2025 entry summarizing disclosures). (vulert.com)

If you want, I can:

  • scan your project's dependencies for "elliptic" or other vulnerable packages (upload your package.json / run commands), or
  • fetch the latest npm audit / advisory feeds and produce an actionable remediation list.

Citations:


🌐 Web query:

viem ethereum library latest version January 2026 security

💡 Result:

As of Jan 5, 2026:

  • Latest published npm version: viem 2.37.6. [1]
  • Security status: the viem GitHub security page lists 2.x as supported and shows no published security advisories; Snyk reports no direct vulnerabilities in the package. [2][3]

If you want, I can watch releases or check for any newly published advisories and notify you.

Sources:
[1] npm package page for viem.
[2] wevm/viem Security page (GitHub).
[3] Snyk package page for viem.


🌐 Web query:

lru-cache npm package security advisories

💡 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 elliptic library has multiple critical vulnerabilities disclosed in late 2024 and early 2025, including private-key extraction (PT-2025-7253), ECDSA truncation attacks (CVE-2024-48948), and signature malleability issues (CVE-2024-42460/42461). Update to 0.9.1+ immediately to ensure these do not expose your cryptographic operations.

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
In @node/proxy/api/package.json around lines 18-20, The package.json dependency
"elliptic-sdk" is pinned to 0.7.2 which depends on vulnerable elliptic releases;
update "elliptic-sdk" to at least 0.9.1 (e.g., "elliptic-sdk": "^0.9.1") to pull
in the fixes, optionally bump "viem" to a recent patch (e.g., "viem": "^2.43.5")
while you're editing dependencies, then reinstall and regenerate the lockfile
(npm install / yarn install) and run the test suite and dependency-audit/SCA to
verify no regression; ensure the package.json entries for "elliptic-sdk" and
"viem" are updated and commit the updated lockfile.

}
}
77 changes: 0 additions & 77 deletions node/proxy/api/src/affiliateRevenue/bebop.ts

This file was deleted.

89 changes: 89 additions & 0 deletions node/proxy/api/src/affiliateRevenue/bebop/bebop.ts
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]
}
8 changes: 8 additions & 0 deletions node/proxy/api/src/affiliateRevenue/bebop/constants.ts
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
1 change: 1 addition & 0 deletions node/proxy/api/src/affiliateRevenue/bebop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getFees } from './bebop'
37 changes: 37 additions & 0 deletions node/proxy/api/src/affiliateRevenue/bebop/types.ts
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
}
>
>
}
}
110 changes: 110 additions & 0 deletions node/proxy/api/src/affiliateRevenue/butterswap/butterswap.ts
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),
}))
}
25 changes: 25 additions & 0 deletions node/proxy/api/src/affiliateRevenue/butterswap/constants.ts
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',
]
1 change: 1 addition & 0 deletions node/proxy/api/src/affiliateRevenue/butterswap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getFees } from './butterswap'
15 changes: 15 additions & 0 deletions node/proxy/api/src/affiliateRevenue/butterswap/types.ts
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
}
}
Loading