diff --git a/bin/testTokenEndpoints.ts b/bin/testTokenEndpoints.ts new file mode 100644 index 00000000..1d8a3060 --- /dev/null +++ b/bin/testTokenEndpoints.ts @@ -0,0 +1,558 @@ +import fetch from 'node-fetch' + +const baseUrl = process.env.TEST_URL ?? 'http://127.0.0.1:8008' + +interface TestResult { + name: string + passed: boolean + error?: string + response?: { + status: number + body: unknown + } +} + +const testResults: TestResult[] = [] + +const runTest = async ( + name: string, + testFn: () => Promise<{ + passed: boolean + error?: string + response?: { status: number; body: unknown } + }> +): Promise => { + try { + const result = await testFn() + testResults.push({ + name, + passed: result.passed, + error: result.error, + response: result.response + }) + const status = result.passed ? '✓' : '✗' + console.log(`${status} ${name}`) + if (!result.passed && result.error != null && result.error !== '') { + console.log(` Error: ${result.error}`) + } + if (result.response != null) { + console.log(` Status: ${result.response.status}`) + if (result.response.status >= 400) { + console.log(` Body: ${JSON.stringify(result.response.body, null, 2)}`) + } else if (!result.passed) { + // Show response body for debugging failed tests + console.log( + ` Response: ${JSON.stringify(result.response.body, null, 2)}` + ) + } + } + } catch (error: unknown) { + testResults.push({ + name, + passed: false, + error: String(error) + }) + console.log(`✗ ${name}`) + console.log(` Error: ${String(error)}`) + } +} + +const makeRequest = async ( + endpoint: string, + queryParams?: Record +): Promise<{ status: number; body: unknown }> => { + const url = new URL(`${baseUrl}${endpoint}`) + if (queryParams != null) { + Object.entries(queryParams).forEach(([key, value]) => { + url.searchParams.append(key, value) + }) + } + + const response = await fetch(url.toString()) + const body = await response.json().catch(async () => await response.text()) + + return { + status: response.status, + body + } +} + +// --------------------------- +// Test: /v1/getToken +// --------------------------- + +const testGetToken = async (): Promise => { + console.log('\n=== Testing /v1/getToken ===\n') + + // Test case: tokenId="faketoken", pluginIds="ethereum", result=[] + await runTest( + 'getToken - faketoken with ethereum pluginId (expect empty array)', + async () => { + const response = await makeRequest('/v1/getToken', { + tokenId: 'faketoken', + pluginId: 'ethereum' + }) + const passed = + response.status === 200 && + Array.isArray(response.body) && + response.body.length === 0 + const bodyLength = Array.isArray(response.body) + ? response.body.length + : 'not an array' + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with empty array, got length ${String(bodyLength)}` + } + } + ) + + // Test case: tokenId="dac17f958d2ee523a2206206994597c13d831ec7", pluginIds="ethereum" + // result=[{rank: 3, contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", currencyCode: "USDT", displayName: "Tether", multiplier: 6, chainPluginId: "ethereum", tokenId: "dac17f958d2ee523a2206206994597c13d831ec7"}] + await runTest( + 'getToken - USDT tokenId with ethereum pluginId (expect USDT object)', + async () => { + const response = await makeRequest('/v1/getToken', { + tokenId: 'dac17f958d2ee523a2206206994597c13d831ec7', + pluginId: 'ethereum' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const hasLength = isArray && bodyArray.length === 1 + let hasCorrectFields = false + if (hasLength) { + const item = bodyArray[0] as { + rank?: number + contractAddress?: string + currencyCode?: string + displayName?: string + multiplier?: number + chainPluginId?: string + tokenId?: string + } + hasCorrectFields = + item.contractAddress === + '0xdac17f958d2ee523a2206206994597c13d831ec7' && + item.currencyCode === 'USDT' && + item.displayName === 'Tether' && + item.multiplier === 6 && + item.chainPluginId === 'ethereum' && + item.tokenId === 'dac17f958d2ee523a2206206994597c13d831ec7' + } + const passed = response.status === 200 && hasLength && hasCorrectFields + const bodyLength = isArray ? bodyArray.length : 'N/A' + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array containing USDT object. Array: ${String( + isArray + )}, Length: ${String(bodyLength)}, Fields match: ${String( + hasCorrectFields + )}` + } + } + ) +} + +// --------------------------- +// Test: /v1/findTokens +// --------------------------- + +const testFindTokens = async (): Promise => { + console.log('\n=== Testing /v1/findTokens ===\n') + + // Test case: searchTerm="", result=error + await runTest('findTokens - empty searchTerm (expect error)', async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: '' + }) + const passed = response.status === 400 + return { + passed, + response, + error: passed + ? undefined + : `Expected 400 error for empty searchTerm, got status ${response.status}` + } + }) + + // Test case: searchTerm="USDC", result=array length greater than 1 + await runTest( + 'findTokens - USDC search (expect array length > 1)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: 'USDC' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length > 1 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length > 1, got length ${length}` + } + } + ) + + // Test case: searchTerm="USDC", pluginIds="ethereum", result=array length greater than 1 + await runTest( + 'findTokens - USDC search with ethereum pluginId (expect array length > 1)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: 'USDC', + pluginIds: 'ethereum' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length > 1 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length > 1, got length ${length}` + } + } + ) + + // Test case: searchTerm="0xdac17f958d2ee523a2206206994597c13d831ec7", pluginIds="ethereum", result=array length equal 1 + await runTest( + 'findTokens - contract address search lowercase (expect array length = 1)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: '0xdac17f958d2ee523a2206206994597c13d831ec7', + pluginIds: 'ethereum' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 1 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length = 1, got length ${length}` + } + } + ) + + // Test case: searchTerm="0xdAC17F958D2ee523a2206206994597C13D831ec7", pluginIds="ethereum", result=array length equal 1 + await runTest( + 'findTokens - contract address search mixed case (expect array length = 1)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + pluginIds: 'ethereum' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 1 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length = 1, got length ${length}` + } + } + ) + + // Test case: searchTerm="0x5D02aB2ff137604eD236A06d52BFEac93133E689", pluginIds="polygon", result=array length equal 0 + await runTest( + 'findTokens - non-existent contract address search (expect array length = 0)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: '0x5D02aB2ff137604eD236A06d52BFEac93133E689', + pluginIds: 'polygon' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 0 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length = 0, got length ${length}` + } + } + ) + + // Test case: searchTerm="USDC", pluginIds="ethereum,polygon", result=array length greater than 2 + await runTest( + 'findTokens - USDC search with ethereum,polygon pluginIds (expect array length > 2)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: 'USDC', + pluginIds: 'ethereum,polygon' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length > 2 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length > 2, got length ${length}` + } + } + ) + + // Test case: searchTerm="USDC", pluginIds="ethereum,polygon,bitcoin", result=array length greater than 2 + await runTest( + 'findTokens - USDC search with ethereum,polygon,bitcoin pluginIds (expect array length > 2)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: 'USDC', + pluginIds: 'ethereum,polygon,bitcoin' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length > 2 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length > 2, got length ${length}` + } + } + ) + + // Test case: searchTerm="USDCUSDCUSDCUSDCUSDCUSDCUSDCUSDC", result=[] + await runTest( + 'findTokens - long fake search term (expect empty array)', + async () => { + const response = await makeRequest('/v1/findTokens', { + searchTerm: 'USDCUSDCUSDCUSDCUSDCUSDCUSDCUSDC' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 0 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with empty array, got length ${length}` + } + } + ) +} + +// --------------------------- +// Test: /v1/listTokens +// --------------------------- + +const testListTokens = async (): Promise => { + console.log('\n=== Testing /v1/listTokens ===\n') + + // Test case: page=1, pageSize=10, result=array length 10 + await runTest( + 'listTokens - page 1, pageSize 10 (expect array length = 10)', + async () => { + const response = await makeRequest('/v1/listTokens', { + page: '1', + pageSize: '10' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 10 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length = 10, got length ${length}` + } + } + ) + + // Test case: page=1, pageSize=5, pluginIds="ethereum", result=error + await runTest( + 'listTokens - page 1, pageSize 5, pluginIds ethereum (expect error)', + async () => { + const response = await makeRequest('/v1/listTokens', { + page: '1', + pageSize: '5', + pluginIds: 'ethereum' + }) + const passed = response.status === 400 + return { + passed, + response, + error: passed + ? undefined + : `Expected 400 error for pageSize < 10, got status ${response.status}` + } + } + ) + + // Test case: page=-1, pageSize=15, pluginIds="ethereum", result=error + await runTest( + 'listTokens - page -1, pageSize 15, pluginIds ethereum (expect error)', + async () => { + const response = await makeRequest('/v1/listTokens', { + page: '-1', + pageSize: '15', + pluginIds: 'ethereum' + }) + const passed = response.status === 400 + return { + passed, + response, + error: passed + ? undefined + : `Expected 400 error for page < 0, got status ${response.status}` + } + } + ) + + // Test case: page=1, pageSize=15, pluginIds="ethereum", result=array length 15 + await runTest( + 'listTokens - page 1, pageSize 15, pluginIds ethereum (expect array length = 15)', + async () => { + const response = await makeRequest('/v1/listTokens', { + page: '1', + pageSize: '15', + pluginIds: 'ethereum' + }) + const isArray = Array.isArray(response.body) + const bodyArray = isArray ? (response.body as unknown[]) : [] + const length = bodyArray.length + const passed = response.status === 200 && isArray && length === 15 + return { + passed, + response, + error: passed + ? undefined + : `Expected 200 with array length = 15, got length ${length}` + } + } + ) +} + +// --------------------------- +// Command-line argument parsing +// --------------------------- + +const parseArgs = (): string | null => { + const args = process.argv.slice(2) + + // Check for --test or -t flag + const testIndex = args.findIndex(arg => arg === '--test' || arg === '-t') + if (testIndex !== -1 && args[testIndex + 1] != null) { + return args[testIndex + 1] + } + + // Check for positional argument (first arg if it's a valid test name) + if (args.length > 0) { + const testName = args[0].toLowerCase() + if ( + testName === 'gettoken' || + testName === 'findtokens' || + testName === 'listtokens' + ) { + return testName + } + } + + return null +} + +const getTestName = (arg: string | null): string | null => { + if (arg == null) return null + + const normalized = arg.toLowerCase() + if (normalized === 'gettoken' || normalized === 'get') { + return 'getToken' + } + if (normalized === 'findtokens' || normalized === 'find') { + return 'findTokens' + } + if (normalized === 'listtokens' || normalized === 'list') { + return 'listTokens' + } + + return null +} + +// --------------------------- +// Main +// --------------------------- + +const main = async (): Promise => { + const testArg = parseArgs() + const testName = getTestName(testArg) + + console.log(`Testing endpoints at ${baseUrl}`) + if (testName != null) { + console.log(`Running only: ${testName}`) + } + console.log('='.repeat(60)) + + if (testName === null || testName === 'getToken') { + await testGetToken() + } + if (testName === null || testName === 'findTokens') { + await testFindTokens() + } + if (testName === null || testName === 'listTokens') { + await testListTokens() + } + + if (testName != null && testResults.length === 0) { + console.log(`\nNo tests found for: ${testArg}`) + console.log('Valid test names: getToken, findTokens, listTokens') + process.exit(1) + } + + // Summary + console.log('\n' + '='.repeat(60)) + console.log('SUMMARY') + console.log('='.repeat(60)) + const passed = testResults.filter(r => r.passed).length + const failed = testResults.filter(r => !r.passed).length + const total = testResults.length + + console.log(`Total tests: ${total}`) + console.log(`Passed: ${passed}`) + console.log(`Failed: ${failed}`) + + if (failed > 0) { + console.log('\nFailed tests:') + testResults + .filter(r => !r.passed) + .forEach(r => { + console.log(` - ${r.name}`) + if (r.error != null && r.error !== '') { + console.log(` ${r.error}`) + } + }) + process.exit(1) + } else { + console.log('\nAll tests passed!') + process.exit(0) + } +} + +main().catch((error: unknown) => { + console.error('Fatal error:', error) + process.exit(1) +}) diff --git a/package.json b/package.json index 72cb4fa8..f40a8f1c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "*.{js,jsx,ts,tsx}": "eslint" }, "devDependencies": { + "@solana/web3.js": "^1.98.4", "@types/chai": "^4.2.9", "@types/express": "^4.17.13", "@types/mocha": "^7.0.1", diff --git a/src/coinrankEngine.ts b/src/coinrankEngine.ts index 3ddfe91a..02b02ee9 100644 --- a/src/coinrankEngine.ts +++ b/src/coinrankEngine.ts @@ -13,7 +13,6 @@ import { getDelay, logger, snooze } from './utils/utils' const PAGE_SIZE = 250 const DEFAULT_WAIT_MS = 5 * 1000 const MAX_WAIT_MS = 5 * 60 * 1000 -const NUM_PAGES = 8 const { defaultFiatCode } = config @@ -63,8 +62,10 @@ export const coinrankEngine = async ( const reply = await response.json() const marketsPage = asCoingeckoMarkets(reply) markets = [...markets, ...marketsPage] + if (marketsPage.length < PAGE_SIZE) { + break + } page++ - if (page > NUM_PAGES) break } const data: CoinrankRedis = { lastUpdate, markets } await setAsync( diff --git a/src/types.ts b/src/types.ts index 2133cfe7..d764642e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,8 @@ import { asArray, asDate, + asEither, + asNull, asNumber, asObject, asOptional, @@ -35,7 +37,7 @@ const asCoingeckoAsset = (raw: any) => { image: asString, current_price: asOptional(asNumber), market_cap: asOptional(asNumber), - market_cap_rank: asNumber, + market_cap_rank: asEither(asNumber, asNull), high_24h: asOptional(asNumber), low_24h: asOptional(asNumber), diff --git a/src/v3/constants.ts b/src/v3/constants.ts index d0909a71..8ce851b2 100644 --- a/src/v3/constants.ts +++ b/src/v3/constants.ts @@ -5,6 +5,8 @@ export const TWENTY_FOUR_HOURS = 24 * ONE_HOUR export const LEADERBOARD_KEY = 'edgerates:topAssets' export const TOKEN_TYPES_KEY = 'tokenTypes' +export const NETWORK_LOCATION_TYPES_KEY = 'networkLocationTypes' +export const TOKEN_OVERRIDES_KEY = 'tokenOverrides' export const CRYPTO_LIMIT = 100 export const FIAT_LIMIT = 256 diff --git a/src/v3/getTokenInfo.ts b/src/v3/getTokenInfo.ts new file mode 100644 index 00000000..1b5a2f3a --- /dev/null +++ b/src/v3/getTokenInfo.ts @@ -0,0 +1,302 @@ +import { asObject, asOptional, asString } from 'cleaners' +import type { MangoSelector } from 'nano' +import type { HttpResponse } from 'serverlet' +import type { ExpressRequest } from 'serverlet/express' + +import { dbTokens } from './providers/couch' +import { asTokenInfoDoc, type EdgeTokenInfo } from './types' +import { toCryptoKey } from './utils' + +// --------------------------- +// Shared helpers +// --------------------------- + +const parsePluginIds = (pluginIds?: string): string[] | undefined => + pluginIds?.split(',').filter(Boolean) + +const chooseTokenIdIndex = (withPlugin: boolean): [string, string] => + withPlugin + ? ['idxTokenIdPlugin', 'idx_tokenId_plugin'] + : ['idxTokenId', 'idx_tokenId'] + +const chooseTextIndex = (withPlugin: boolean): [string, string] => + withPlugin + ? ['idxTokensTextPlugin', 'idx_tokens_text_plugin'] + : ['idxTokensText', 'idx_tokens_text'] + +const chooseRankIndex = (withPlugin: boolean): [string, string] => + withPlugin ? ['idxRankPlugin', 'idx_rank_plugin'] : ['idxRank', 'idx_rank'] + +const getTokenDocs = async (params: { + selector: MangoSelector + useIndex?: [string, string] + limit?: number + skip?: number +}): Promise => { + const { selector, useIndex, limit, skip } = params + const response = await dbTokens.find({ + selector, + use_index: useIndex, + limit, + skip, + sort: [{ rank: 'asc' }] + }) + return response.docs.map(doc => asTokenInfoDoc(doc).doc) +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +const buildMangoSelector = (options: { + searchTerm?: string + field?: 'currencyCode' | 'displayName' | 'tokenId' | 'contractAddress' + chainPluginIds?: string[] +}): MangoSelector => { + const { searchTerm, field, chainPluginIds } = options + + const selector: MangoSelector = {} + + if (chainPluginIds != null && chainPluginIds.length > 0) { + selector.chainPluginId = + chainPluginIds.length === 1 ? chainPluginIds[0] : { $in: chainPluginIds } + } + + if (searchTerm == null) return selector + + // Handle the field search + if (field === 'currencyCode') { + selector.currencyCode = { + $regex: `(?i)^${escapeRegex(searchTerm)}` + } + } else if (field === 'displayName') { + selector.displayName = { $regex: `(?i).*${escapeRegex(searchTerm)}.*` } + } else if (field === 'tokenId') { + selector.tokenId = searchTerm + } else if (field === 'contractAddress') { + selector.contractAddress = { + $regex: `(?i).*${escapeRegex(searchTerm)}.*` + } + } + + return selector +} + +// --------------------------- +// Endpoints +// --------------------------- + +const asGetTokenQuery = asObject({ + tokenId: asString, + pluginId: asString +}) + +export const getTokenV1 = async ( + request: ExpressRequest +): Promise => { + try { + const { tokenId, pluginId } = asGetTokenQuery(request.req.query) + + const mangoSelector = buildMangoSelector({ + searchTerm: tokenId, + field: 'tokenId', + chainPluginIds: [pluginId] + }) + + const result = await getTokenDocs({ + selector: mangoSelector, + useIndex: chooseTokenIdIndex(true), + limit: 1 + }) + + if (result.length === 0) { + return { + status: 404, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ error: 'Token not found' }) + } + } + + return { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(result[0]) + } + } catch (e) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ error: `Invalid request body ${String(e)}` }) + } + } +} + +const asFindTokenQuery = asObject({ + searchTerm: asString, + pluginIds: asOptional(asString) +}) + +export const findTokensV1 = async ( + request: ExpressRequest +): Promise => { + try { + const { searchTerm, pluginIds } = asFindTokenQuery(request.req.query) + if (searchTerm.length === 0) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + error: 'Search term must be at least 1 character' + }) + } + } + + const whitelistPluginIds = parsePluginIds(pluginIds) + const usingPluginFilter = + whitelistPluginIds != null && whitelistPluginIds.length > 0 + + const textIndex = chooseTextIndex(usingPluginFilter) + + // Currency code lookup + const currencyCodeSelector = buildMangoSelector({ + searchTerm, + field: 'currencyCode', + chainPluginIds: whitelistPluginIds + }) + const currencyCodeMatches = await getTokenDocs({ + selector: currencyCodeSelector, + useIndex: textIndex, + limit: 100 + }) + + // Display name lookup + const displayNameSelector = buildMangoSelector({ + searchTerm, + field: 'displayName', + chainPluginIds: whitelistPluginIds + }) + const displayNameMatches = await getTokenDocs({ + selector: displayNameSelector, + useIndex: textIndex, + limit: 100 + }) + + // Token ID lookup + const tokenIdIndex = chooseTokenIdIndex(usingPluginFilter) + const tokenIdSelector = buildMangoSelector({ + searchTerm, + field: 'tokenId', + chainPluginIds: whitelistPluginIds + }) + const tokenIdMatches = await getTokenDocs({ + selector: tokenIdSelector, + useIndex: tokenIdIndex, + limit: 100 + }) + + // Contract address lookup + const contractAddressSelector = buildMangoSelector({ + searchTerm, + field: 'contractAddress', + chainPluginIds: whitelistPluginIds + }) + const contractAddressMatches = await getTokenDocs({ + selector: contractAddressSelector, + useIndex: textIndex, + limit: 100 + }) + + // Filter out duplicates + const uniqueResults: Record = {} + for (const doc of [ + ...currencyCodeMatches, + ...displayNameMatches, + ...tokenIdMatches, + ...contractAddressMatches + ]) { + const key = toCryptoKey({ + pluginId: doc.chainPluginId, + tokenId: doc.tokenId + }) + uniqueResults[key] ??= doc + } + const sortedResults = Object.values(uniqueResults).sort( + (a, b) => a.rank - b.rank + ) + + return { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(sortedResults) + } + } catch (e) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ error: `Invalid request body ${String(e)}` }) + } + } +} + +const asListTokensQuery = asObject({ + page: asOptional(asString, '0'), + pageSize: asOptional(asString, '10'), + pluginIds: asOptional(asString) +}) + +export const listTokensV1 = async ( + request: ExpressRequest +): Promise => { + try { + const { + page: pageStr, + pageSize: pageSizeStr, + pluginIds + } = asListTokensQuery(request.req.query) + const page = Number(pageStr) + const pageSize = Number(pageSizeStr) + + if (!Number.isInteger(page) || page < 0) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + error: 'Page must be an integer greater than or equal to 0' + }) + } + } + if (!Number.isInteger(pageSize) || pageSize > 100 || pageSize < 10) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + error: 'Page size must be an integer between 10 and 100' + }) + } + } + + const whitelistPluginIds = parsePluginIds(pluginIds) + const usingPluginFilter = + whitelistPluginIds != null && whitelistPluginIds.length > 0 + + const selector = buildMangoSelector({ + chainPluginIds: whitelistPluginIds + }) + + const result = await getTokenDocs({ + selector, + useIndex: chooseRankIndex(usingPluginFilter), + limit: pageSize, + skip: page * pageSize + }) + + return { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(result) + } + } catch (e) { + return { + status: 400, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ error: `Invalid request body ${String(e)}` }) + } + } +} diff --git a/src/v3/index.ts b/src/v3/index.ts index 100b78fc..c3416142 100644 --- a/src/v3/index.ts +++ b/src/v3/index.ts @@ -4,6 +4,7 @@ import { pickMethod, pickPath, withCors } from 'serverlet' import { makeExpressRoute } from 'serverlet/express' import { config } from '../config' +import { findTokensV1, getTokenV1, listTokensV1 } from './getTokenInfo' import { ratesV2, rateV2, @@ -21,6 +22,9 @@ function server(): void { const server = withCors( pickPath({ '/': pickMethod({ GET: heartbeatV3 }), + '/v1/getToken': pickMethod({ GET: getTokenV1 }), + '/v1/findTokens': pickMethod({ GET: findTokensV1 }), + '/v1/listTokens': pickMethod({ GET: listTokensV1 }), '/v2/exchangeRate': pickMethod({ GET: rateV2 }), '/v2/exchangeRates': pickMethod({ POST: ratesV2 }), '/v2/coinrank': pickMethod({ GET: sendCoinranksV2 }), diff --git a/src/v3/providers/coingecko/coingecko.ts b/src/v3/providers/coingecko/coingecko.ts index 3e4a1cf1..cf046854 100644 --- a/src/v3/providers/coingecko/coingecko.ts +++ b/src/v3/providers/coingecko/coingecko.ts @@ -1,27 +1,40 @@ import { asArray, asMaybe, asNumber, asObject, asString } from 'cleaners' -import { asCouchDoc, syncedDocument } from 'edge-server-tools' +import { asCouchDoc, type CouchDoc, syncedDocument } from 'edge-server-tools' import { coinrankEngine } from '../../../coinrankEngine' import { config } from '../../../config' +import { REDIS_COINRANK_KEY_PREFIX } from '../../../constants' import { coingeckoAssetsInternal } from '../../../providers/coingecko' import type { AssetMap } from '../../../rates' +import type { CoinrankMarkets } from '../../../types' import { hsetAsync } from '../../../utils/dbUtils' -import { dateOnly, snooze } from '../../../utils/utils' -import { TOKEN_TYPES_KEY } from '../../constants' +import { dateOnly, logger, snooze } from '../../../utils/utils' +import { + NETWORK_LOCATION_TYPES_KEY, + TOKEN_OVERRIDES_KEY, + TOKEN_TYPES_KEY +} from '../../constants' import { asCrossChainDoc, + asNetworkLocationTypeMap, asNumberMap, asStringNullMap, + asTokenInfoDoc, asTokenMap, + asTokenMappingsDoc, + asTokenOverride, asTokenTypeMap, type CrossChainMapping, + type EdgeTokenInfo, type NumberMap, type RateBuckets, type RateEngine, type RateProvider, type TokenMap, + tokenOverrideToEdgeTokenInfo, wasCrossChainDoc, - wasExistingMappings + wasExistingMappings, + wasTokenInfoDoc } from '../../types' import { create30MinuteSyncInterval, @@ -31,7 +44,9 @@ import { reduceRequestedCryptoRates, toCryptoKey } from '../../utils' -import { dbSettings } from '../couch' +import { dbSettings, dbTokens } from '../couch' +import { getAsync } from '../redis' +import { getNetworkLocation } from './currencyUtils' import { coingeckoMainnetCurrencyMapping, coingeckoPlatformIdMapping @@ -238,6 +253,205 @@ const tokenMapping: RateEngine = async () => { ) } +const asTokenList = asObject({ + tokens: asArray( + asObject({ + // chainId: asEither(asNumber, asNull), + address: asString, + name: asString, + symbol: asString, + decimals: asNumber + // logoURI: asString + }) + ) +}) + +// Builds token info documents by merging CoinGecko token lists with manual +// overrides, then writes only new or changed documents to CouchDB. +// NetworkLocation is carried forward from existing docs to avoid redundant +// RPC calls (e.g. Solana getAccountInfo) on every run; it is only fetched +// for genuinely new tokens. +const updateTokenInfos = async (): Promise => { + const newTokenInfoDocs: Array> = [] + const mergedTokenInfos = new Map() + + const coingeckoIdsDocument = await dbSettings.get('coingecko:automated') + const coingeckoIds = asTokenMappingsDoc(coingeckoIdsDocument).doc + + const tokenTypesDoc = await dbSettings.get(TOKEN_TYPES_KEY) + const tokenTypes = asCouchDoc(asStringNullMap)(tokenTypesDoc).doc + + const crosschainDocument = await dbSettings.get('crosschain:automated') + const crosschain = asCrossChainDoc(crosschainDocument).doc + + const networkLocationTypesDoc = await dbSettings.get( + NETWORK_LOCATION_TYPES_KEY + ) + const networkLocationTypes = asCouchDoc(asNetworkLocationTypeMap)( + networkLocationTypesDoc + ).doc + + const coinranksStr = await getAsync(`${REDIS_COINRANK_KEY_PREFIX}_iso:USD`) + if (coinranksStr == null) return + const coinrankMarkets: CoinrankMarkets = JSON.parse(coinranksStr)?.markets + const idRankMap = new Map() + for (const market of coinrankMarkets) { + idRankMap.set(market.assetId, market.rank) + } + + for (const [edgePluginId, platform] of Object.entries( + platformIdMappingSyncDoc.doc + )) { + const tokenType = tokenTypes[edgePluginId] + if (platform == null || tokenType == null) continue + + try { + const response = await fetchCoingecko( + `${config.providers.coingeckopro.uri}/api/v3/token_lists/${platform}/all.json` + ) + const tokenList = asTokenList(response).tokens + + for (const token of tokenList) { + const tokenId = createTokenId(tokenType, token.symbol, token.address) + if (tokenId == null) continue + const cryptoKey = toCryptoKey({ pluginId: edgePluginId, tokenId }) + if (coingeckoIds[cryptoKey] == null) continue + + let id: string | undefined = coingeckoIds[cryptoKey]?.id + if (id == null) { + const crosschainAsset = crosschain[cryptoKey] + if (crosschainAsset != null) { + const crosschainKey = toCryptoKey({ + pluginId: crosschainAsset.destChain, + tokenId: crosschainAsset.tokenId + }) + id = coingeckoIds[crosschainKey]?.id + } + } + + const rank = idRankMap.get(id) ?? Number.MAX_SAFE_INTEGER + + const newInfo: EdgeTokenInfo = { + rank, + contractAddress: token.address, + currencyCode: token.symbol, + displayName: token.name, + decimals: token.decimals, + networkLocation: undefined, + chainPluginId: edgePluginId, + tokenId + } + mergedTokenInfos.set(cryptoKey, newInfo) + } + } catch (error) { + logger(`${edgePluginId} ${platform} tokenList failure`, error) + } + } + + const tokenOverridesDocument = await dbSettings.get(TOKEN_OVERRIDES_KEY) + const tokenOverrides = asCouchDoc(asObject(asArray(asTokenOverride)))( + tokenOverridesDocument + ).doc + + for (const [pluginId, tokens] of Object.entries(tokenOverrides)) { + const tokenType = tokenTypes[pluginId] + if (tokenType == null) continue + + for (const token of tokens) { + const overrideInfo = tokenOverrideToEdgeTokenInfo( + token, + pluginId, + tokenType + ) + if (overrideInfo == null) continue + + const cryptoKey = toCryptoKey({ + pluginId, + tokenId: overrideInfo.tokenId + }) + const coingeckoTokenInfo = mergedTokenInfos.get(cryptoKey) + + // Coingecko priority: rank/contractAddress + // Manual priority: currencyCode/decimals/displayName/networkLocation. + if (coingeckoTokenInfo == null) { + mergedTokenInfos.set(cryptoKey, overrideInfo) + } else { + mergedTokenInfos.set(cryptoKey, { + ...coingeckoTokenInfo, + currencyCode: overrideInfo.currencyCode, + decimals: overrideInfo.decimals, + displayName: overrideInfo.displayName, + networkLocation: overrideInfo.networkLocation + }) + } + } + } + + const hasTokenInfoChange = ( + existing: EdgeTokenInfo, + next: EdgeTokenInfo + ): boolean => { + return ( + existing.rank !== next.rank || + existing.contractAddress !== next.contractAddress || + existing.currencyCode !== next.currencyCode || + existing.displayName !== next.displayName || + existing.decimals !== next.decimals || + JSON.stringify(existing.networkLocation ?? null) !== + JSON.stringify(next.networkLocation ?? null) || + existing.chainPluginId !== next.chainPluginId || + existing.tokenId !== next.tokenId + ) + } + + const keys = Array.from(mergedTokenInfos.keys()) + const existingDocs = await dbTokens.fetch({ keys }) + + const existingMap = new Map() + for (const row of existingDocs.rows) { + if ('error' in row || row.doc == null) continue + const parsed = asMaybe(asTokenInfoDoc)(row.doc) + if (parsed == null) continue + existingMap.set(row.id, { doc: parsed.doc, rev: row.doc._rev }) + } + + for (const [cryptoKey, newInfo] of mergedTokenInfos) { + const existing = existingMap.get(cryptoKey) + if (existing != null) { + newInfo.networkLocation ??= existing.doc.networkLocation + if (hasTokenInfoChange(existing.doc, newInfo)) { + newTokenInfoDocs.push( + wasTokenInfoDoc({ + doc: newInfo, + id: cryptoKey, + rev: existing.rev + }) + ) + } + } else { + const networkLocation = await getNetworkLocation( + networkLocationTypes[newInfo.chainPluginId] ?? null, + newInfo.contractAddress + ) + if (networkLocation != null) { + newInfo.networkLocation = networkLocation + newTokenInfoDocs.push( + wasTokenInfoDoc({ + doc: newInfo, + id: cryptoKey + }) + ) + } + } + } + + if (newTokenInfoDocs.length > 0) { + await dbTokens.bulk({ + docs: newTokenInfoDocs + }) + } +} + const getCurrentRates = async (ids: Set): Promise => { const out: NumberMap = {} try { @@ -380,6 +594,10 @@ export const coingecko: RateProvider = { { frequency: 'day', engine: coingeckoAssetsEngine + }, + { + frequency: 'hour', + engine: updateTokenInfos } ] } diff --git a/src/v3/providers/coingecko/currencyUtils.ts b/src/v3/providers/coingecko/currencyUtils.ts new file mode 100644 index 00000000..e4d80bd0 --- /dev/null +++ b/src/v3/providers/coingecko/currencyUtils.ts @@ -0,0 +1,58 @@ +import { Connection, PublicKey } from '@solana/web3.js' +import { asString } from 'cleaners' + +import { logger } from '../../../utils/utils' +import type { JsonObject, NetworkLocationTypeMap } from '../../types' + +const solanaRpc = 'https://api.mainnet-beta.solana.com' +const solanaConnection = new Connection(solanaRpc) + +const createSolanaNetworkLocation = async ( + contractAddress: string +): Promise => { + try { + const accountInfo = await solanaConnection.getAccountInfo( + new PublicKey(contractAddress) + ) + if (accountInfo == null) return + + const owner = accountInfo.owner.toBase58() + logger(`createSolanaNetworkLocation success ${contractAddress} ${owner}`) + + return { + contractAddress, + tokenProgram: owner + } + } catch (e) { + logger(`createSolanaNetworkLocation error ${contractAddress} ${String(e)}`) + } +} + +const createRippleNetworkLocation = async ( + contractAddress: string +): Promise => { + try { + const [currency, issuer] = contractAddress.split('.') + + return { + currency: asString(currency), + issuer: asString(issuer) + } + } catch (e) { + logger(`createRippleNetworkLocation error ${contractAddress} ${String(e)}`) + } +} + +export const getNetworkLocation = async ( + networkLocationType: NetworkLocationTypeMap[string], + contractAddress: string +): Promise => { + switch (networkLocationType) { + case 'xrpl': + return await createRippleNetworkLocation(contractAddress) + case 'solana': + return await createSolanaNetworkLocation(contractAddress) + default: + return { contractAddress } + } +} diff --git a/src/v3/providers/couch.ts b/src/v3/providers/couch.ts index fab10e68..9b4f35c1 100644 --- a/src/v3/providers/couch.ts +++ b/src/v3/providers/couch.ts @@ -10,6 +10,7 @@ import type nano from 'nano' import { config } from '../../config' import { asRateDocument, + type EdgeTokenInfo, type RateBuckets, type RateDocument, type RateProvider, @@ -29,6 +30,8 @@ export const dbSettings: nano.DocumentScope = couchDB.default.db.use('rates_settings') export const dbData: nano.DocumentScope = couchDB.default.db.use('rates_data') +export const dbTokens: nano.DocumentScope = + couchDB.default.db.use('rates_tokens') const asRatesDoc = asCouchDoc(asRateDocument) const wasRatesDoc = uncleaner(asRatesDoc) diff --git a/src/v3/providers/edgerates/defaults.ts b/src/v3/providers/edgerates/defaults.ts index 22a28839..def0aa3d 100644 --- a/src/v3/providers/edgerates/defaults.ts +++ b/src/v3/providers/edgerates/defaults.ts @@ -1,4 +1,10 @@ -import type { CrossChainMapping, EdgeAsset, TokenTypeMap } from '../../types' +import type { + CrossChainMapping, + EdgeAsset, + NetworkLocationTypeMap, + TokenOverride, + TokenTypeMap +} from '../../types' export const defaultCrypto: EdgeAsset[] = [ { pluginId: 'bitcoin', tokenId: null }, @@ -497,6 +503,11 @@ export const defaultTokenTypes: TokenTypeMap = { zksync: 'evm' } +export const defaultNetworkLocationTypes: NetworkLocationTypeMap = { + ripple: 'xrpl', + solana: 'solana' +} + export const defaultPlatformPriority: Record = { bitcoin: 10, ethereum: 20, @@ -606,3 +617,676 @@ export const defaultCrossChainMapping: CrossChainMapping = { tokenId: 'tcy' } } + +export const defaultTokenOverrides: Record = { + ripple: [ + { + currencyCode: 'RLUSD', + displayName: 'Ripple USD', + decimals: 18, + networkLocation: { + currency: '524C555344000000000000000000000000000000', + issuer: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De' + } + }, + { + currencyCode: 'SOLO', + displayName: 'Sologenic', + decimals: 18, + networkLocation: { + currency: '534F4C4F00000000000000000000000000000000', + issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz' + } + }, + { + currencyCode: 'USD', + displayName: 'Gatehub USD', + decimals: 18, + networkLocation: { + currency: 'USD', + issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' + } + }, + { + currencyCode: 'EUR', + displayName: 'Gatehub EUR', + decimals: 18, + networkLocation: { + currency: 'EUR', + issuer: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq' + } + }, + { + currencyCode: 'USD', + displayName: 'Bitstamp USD', + decimals: 18, + networkLocation: { + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + }, + { + currencyCode: 'EUR', + displayName: 'Bitstamp EUR', + decimals: 18, + networkLocation: { + currency: 'EUR', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' + } + }, + { + currencyCode: 'USD', + displayName: 'Stably USD', + decimals: 18, + networkLocation: { + currency: 'USD', + issuer: 'rEn9eRkX25wfGPLysUMAvZ84jAzFNpT5fL' + } + }, + { + currencyCode: 'USDC', + displayName: 'USDC', + decimals: 6, + networkLocation: { + currency: '5553444300000000000000000000000000000000', + issuer: 'rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE' + } + }, + { + currencyCode: 'CORE', + displayName: 'Coreum', + decimals: 18, + networkLocation: { + currency: '434F524500000000000000000000000000000000', + issuer: 'rcoreNywaoz2ZCQ8Lg2EbSLnGuRBmun6D' + } + } + ], + solana: [ + { + currencyCode: '$CWIF', + displayName: 'catwifhat', + decimals: 2, + networkLocation: { + contractAddress: '7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'BIO', + displayName: 'BIO', + decimals: 9, + networkLocation: { + contractAddress: 'bioJ9JTqW62MLz7UKHU69gtKhPpGi1BQhccj2kmSvUJ', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'ABBVx', + displayName: 'AbbVie xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XswbinNKyPmzTa5CskMbCPvMW6G5CMnZXZEeQSSQoie', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'ABTx', + displayName: 'Abbott xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsHtf5RpxsQ7jeJ9ivNewouZKJHbPxhPoEy6yYvULr7', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'ACNx', + displayName: 'Accenture xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs5UJzmCRQ8DWZjskExdSQDnbE6iLkRu2jjrRAB1JSU', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'GOOGLx', + displayName: 'Alphabet xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsCPL9dNWBMvFtTmwcCA5v3xWPSMEBCszbQdiLLq6aN', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'AMZNx', + displayName: 'Amazon xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs3eBt7uRfJX8QUs4suhyU8p2M6DoUDrJyWBa8LLZsg', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'AMBRx', + displayName: 'Amber xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsaQTCgebC2KPbf27KUhdv5JFvHhQ4GDAPURwrEhAzb', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'AAPLx', + displayName: 'Apple xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsbEhLAtcf6HdfpFZ5xEMdqW8nfAvcsP5bdudRLJzJp', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'APPx', + displayName: 'AppLovin xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsPdAVBi8Zc1xvv53k4JcMrQaEDTgkGqKYeh7AYgPHV', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'AZNx', + displayName: 'AstraZeneca xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs3ZFkPYT2BN7qBMqf1j1bfTeTm1rFzEFSsQ1z3wAKU', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'BACx', + displayName: 'Bank of America xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XswsQk4duEQmCbGzfqUUWYmi7pV7xpJ9eEmLHXCaEQP', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'BRK.Bx', + displayName: 'Berkshire Hathaway xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs6B6zawENwAbWVi7w92rjazLuAr5Az59qgWKcNb45x', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'AVGOx', + displayName: 'Broadcom xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsgSaSvNSqLTtFuyWPBhK9196Xb9Bbdyjj4fH3cPJGo', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CVXx', + displayName: 'Chevron xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsNNMt7WTNA2sV3jrb1NNfNgapxRF5i4i6GcnTRRHts', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CRCLx', + displayName: 'Circle xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CSCOx', + displayName: 'Cisco xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsr3pdLQyXvDJBFgpR5nexCEZwXvigb8wbPYp4YoNFf', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'KOx', + displayName: 'Coca-Cola xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsaBXg8dU5cPM6ehmVctMkVqoiRG2ZjMo1cyBJ3AykQ', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'COINx', + displayName: 'Coinbase xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs7ZdzSHLU9ftNJsii5fCeJhoRWSC32SQGzGQtePxNu', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CMCSAx', + displayName: 'Comcast xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsvKCaNsxg2GN8jjUmq71qukMJr7Q1c5R2Mk9P8kcS8', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CRWDx', + displayName: 'CrowdStrike xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs7xXqkcK7K8urEqGg52SECi79dRp2cEKKuYjUePYDw', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'DHRx', + displayName: 'Danaher xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xseo8tgCZfkHxWS9xbFYeKFyMSbWEvZGFV1Gh53GtCV', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'DFDVx', + displayName: 'DFDV xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs2yquAgsHByNzx68WJC55WHjHBvG9JsMB7CWjTLyPy', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'LLYx', + displayName: 'Eli Lilly xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsnuv4omNoHozR6EEW5mXkw8Nrny5rB3jVfLqi6gKMH', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'XOMx', + displayName: 'Exxon Mobil xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsaHND8sHyfMfsWPj6kSdd5VwvCayZvjYgKmmcNL5qh', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'GMEx', + displayName: 'Gamestop xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsf9mBktVB9BSU5kf4nHxPq5hCBJ2j2ui3ecFGxPRGc', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'GLDx', + displayName: 'Gold xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'GSx', + displayName: 'Goldman Sachs xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsgaUyp4jd1fNBCxgtTKkW64xnnhQcvgaxzsbAq5ZD1', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'HDx', + displayName: 'Home Depot xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XszjVtyhowGjSC5odCqBpW1CtXXwXjYokymrk7fGKD3', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'HONx', + displayName: 'Honeywell xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsRbLZthfABAPAfumWNEJhPyiKDW6TvDVeAeW7oKqA2', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'INTCx', + displayName: 'Intel xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XshPgPdXFRWB8tP1j82rebb2Q9rPgGX37RuqzohmArM', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'IBMx', + displayName: 'International Business Machines xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XspwhyYPdWVM8XBHZnpS9hgyag9MKjLRyE3tVfmCbSr', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'JNJx', + displayName: 'Johnson & Johnson xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsGVi5eo1Dh2zUpic4qACcjuWGjNv8GCt3dm5XcX6Dn', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'JPMx', + displayName: 'JPMorgan Chase xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsMAqkcKsUewDrzVkait4e5u4y8REgtyS7jWgCpLV2C', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'LINx', + displayName: 'Linde xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsSr8anD1hkvNMu8XQiVcmiaTP7XGvYu7Q58LdmtE8Z', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MRVLx', + displayName: 'Marvell xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsuxRGDzbLjnJ72v74b7p9VY6N66uYgTCyfwwRjVCJA', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MAx', + displayName: 'Mastercard xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsApJFV9MAktqnAc6jqzsHVujxkGm9xcSUffaBoYLKC', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MCDx', + displayName: "McDonald's xStock", + decimals: 8, + networkLocation: { + contractAddress: 'XsqE9cRRpzxcGKDXj1BJ7Xmg4GRhZoyY1KpmGSxAWT2', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MDTx', + displayName: 'Medtronic xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsDgw22qRLTv5Uwuzn6T63cW69exG41T6gwQhEK22u2', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MRKx', + displayName: 'Merck xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsnQnU7AdbRZYe2akqqpibDdXjkieGFfSkbkjX1Sd1X', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'METAx', + displayName: 'Meta xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MSFTx', + displayName: 'Microsoft xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XspzcW1PRtgf6Wj92HCiZdjzKCyFekVD8P5Ueh3dRMX', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'MSTRx', + displayName: 'MicroStrategy xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsP7xzNPvEHS1m6qfanPUGjNmdnmsLKEoNAnHjdxxyZ', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'QQQx', + displayName: 'Nasdaq xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs8S1uUs1zvS2p7iwtsG3b6fkhpvmwz4GYU3gWAmWHZ', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'NFLXx', + displayName: 'Netflix xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsEH7wWfJJu2ZT3UCFeVfALnVA6CP5ur7Ee11KmzVpL', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'NVOx', + displayName: 'Novo Nordisk xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsfAzPzYrYjd4Dpa9BU3cusBsvWfVB9gBcyGC87S57n', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'NVDAx', + displayName: 'NVIDIA xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'ORCLx', + displayName: 'Oracle xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsjFwUPiLofddX5cWFHW35GCbXcSu1BCUGfxoQAQjeL', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PLTRx', + displayName: 'Palantir xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsoBhf2ufR8fTyNSjqfU71DYGaE6Z3SUGAidpzriAA4', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PEPx', + displayName: 'PepsiCo xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsv99frTRUeornyvCfvhnDesQDWuvns1M852Pez91vF', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PFEx', + displayName: 'Pfizer xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsAtbqkAP1HJxy7hFDeq7ok6yM43DQ9mQ1Rh861X8rw', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PMx', + displayName: 'Philip Morris xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xsba6tUnSjDae2VcopDB6FGGDaxRrewFCDa5hKn5vT3', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PGx', + displayName: 'Procter & Gamble xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsYdjDjNUygZ7yGKfQaB6TxLh2gC6RRjzLtLAGJrhzV', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'HOODx', + displayName: 'Robinhood xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsvNBAYkrDRNhA7wPHQfX3ZUXZyZLdnCQDfHZ56bzpg', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'CRMx', + displayName: 'Salesforce xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsczbcQ3zfcgAEt9qHQES8pxKAVG5rujPSHQEXi4kaN', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'SPYx', + displayName: 'SP500 xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'TSLAx', + displayName: 'Tesla xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'TMOx', + displayName: 'Thermo Fisher xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs8drBWy3Sd5QY3aifG9kt9KFs2K3PGZmx7jWrsrk57', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'TQQQx', + displayName: 'TQQQ xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsjQP3iMAaQ3kQScQKthQpx9ALRbjKAjQtHg6TFomoc', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'UNHx', + displayName: 'UnitedHealth xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XszvaiXGPwvk2nwb3o9C1CX4K6zH8sez11E6uyup6fe', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'VTIx', + displayName: 'Vanguard xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsssYEQjzxBCFgvYFFNuhJFBeHNdLWYeUSP8F45cDr9', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'Vx', + displayName: 'Visa xStock', + decimals: 8, + networkLocation: { + contractAddress: 'XsqgsbXwWogGJsNcVZ3TyVouy2MbTkfCFhCGGGcQZ2p', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'WMTx', + displayName: 'Walmart xStock', + decimals: 8, + networkLocation: { + contractAddress: 'Xs151QeqTCiuKtinzfRATnUESM2xTU6V9Wy8Vy538ci', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'SPINE', + displayName: 'SpineDAO', + decimals: 9, + networkLocation: { + contractAddress: 'spinezMPKxkBpf4Q9xET2587fehM3LuKe4xoAoXtSjR', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + }, + { + currencyCode: 'PYUSD', + displayName: 'PayPal USD', + decimals: 6, + networkLocation: { + contractAddress: '2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo', + tokenProgram: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + } + } + ] +} diff --git a/src/v3/providers/edgerates/edgerates.ts b/src/v3/providers/edgerates/edgerates.ts index 2ba4cfbb..76af1125 100644 --- a/src/v3/providers/edgerates/edgerates.ts +++ b/src/v3/providers/edgerates/edgerates.ts @@ -17,7 +17,9 @@ import { defaultCrossChainMapping, defaultCrypto, defaultFiat, + defaultNetworkLocationTypes, defaultPlatformPriority, + defaultTokenOverrides, defaultTokenTypes } from './defaults' @@ -122,6 +124,8 @@ export const edgerates: RateProvider = { fiat: defaultFiat }, tokenTypes: defaultTokenTypes, + networkLocationTypes: defaultNetworkLocationTypes, + tokenOverrides: defaultTokenOverrides, platformPriority: defaultPlatformPriority, 'crosschain:automated': {}, crosschain: defaultCrossChainMapping diff --git a/src/v3/setupDatabases.ts b/src/v3/setupDatabases.ts index fcfecb33..4914a186 100644 --- a/src/v3/setupDatabases.ts +++ b/src/v3/setupDatabases.ts @@ -1,4 +1,8 @@ -import { type DatabaseSetup, setupDatabase } from 'edge-server-tools' +import { + type DatabaseSetup, + makeMangoIndex, + setupDatabase +} from 'edge-server-tools' import { readFileSync } from 'fs' import { join } from 'path' @@ -44,6 +48,40 @@ const createDatabases = async (): Promise => { options: { partitioned: false }, templates: {}, syncedDocuments: [] + }, + { + name: 'rates_tokens', + options: { partitioned: false }, + documents: { + '_design/idxTokensText': makeMangoIndex('idx_tokens_text', [ + 'currencyCode', + 'displayName', + 'tokenId', + 'contractAddress' + ]), + '_design/idxTokensTextPlugin': makeMangoIndex( + 'idx_tokens_text_plugin', + [ + 'currencyCode', + 'displayName', + 'tokenId', + 'chainPluginId', + 'contractAddress' + ] + ), + '_design/idxTokenIdPlugin': makeMangoIndex('idx_tokenId_plugin', [ + 'chainPluginId', + 'tokenId' + ]), + '_design/idxTokenId': makeMangoIndex('idx_tokenId', ['tokenId']), + '_design/idxRank': makeMangoIndex('idx_rank', ['rank']), + '_design/idxRankPlugin': makeMangoIndex('idx_rank_plugin', [ + 'chainPluginId', + 'rank' + ]) + }, + templates: {}, + syncedDocuments: [] } ] diff --git a/src/v3/types.ts b/src/v3/types.ts index 3ddb7315..ceb415a0 100644 --- a/src/v3/types.ts +++ b/src/v3/types.ts @@ -13,6 +13,8 @@ import { } from 'cleaners' import { asCouchDoc, type DatabaseSetup } from 'edge-server-tools' +import { createTokenId } from './utils' + const asEdgeTokenId = asEither(asString, asNull) export type EdgeTokenId = ReturnType @@ -151,7 +153,7 @@ export const asRateDocument = asObject({ }) export type RateDocument = ReturnType -const asTokenMappingsDoc = asCouchDoc(asTokenMap) +export const asTokenMappingsDoc = asCouchDoc(asTokenMap) export const wasExistingMappings = uncleaner(asTokenMappingsDoc) type TokenType = @@ -164,6 +166,17 @@ type TokenType = | null export type TokenTypeMap = Record +/** A JSON object (as opposed to an array or primitive). */ +export type JsonObject = Record + +const asNetworkLocationType = asEither( + asValue('xrpl'), + asValue('solana'), + asNull +) +export const asNetworkLocationTypeMap = asObject(asNetworkLocationType) +export type NetworkLocationTypeMap = ReturnType + const asTokenType = asEither( asValue('simple'), asValue('evm'), @@ -201,3 +214,66 @@ export const asV2CurrencyCodeMapDoc = (raw: any) => { } export type V2CurrencyCodeMapDoc = ReturnType + +const asJsonObject = (raw: unknown): JsonObject => { + if (raw == null || typeof raw !== 'object') { + throw new TypeError('Expected a JSON object') + } + return raw as JsonObject +} + +// couch id [chainCode]:[tokenId] +const asEdgeTokenInfo = asObject({ + rank: asNumber, // v3/coins/markets + contractAddress: asString, // v3/coins/list, v3/coins/markets, v3/token_lists + currencyCode: asString, // v3/coins/list, v3/coins/markets, v3/token_lists + displayName: asString, // v3/coins/list, v3/coins/markets, v3/token_lists + decimals: asNumber, // v3/token_lists + networkLocation: asOptional(asJsonObject), + chainPluginId: asString, // v3/coins/list, v3/token_lists + tokenId: asString // v3/coins/list, v3/token_lists +}) +export type EdgeTokenInfo = ReturnType + +export const asTokenInfoDoc = asCouchDoc(asEdgeTokenInfo) +export const wasTokenInfoDoc = uncleaner(asTokenInfoDoc) + +export const asTokenOverride = asObject({ + currencyCode: asString, + displayName: asString, + decimals: asNumber, + networkLocation: asOptional(asJsonObject) +}) +export type TokenOverride = ReturnType + +const asContractAddressNetworkLocation = asObject({ + contractAddress: asString +}) + +const getContractAddress = (networkLocation: JsonObject): string | undefined => { + const contractAddressNetworkLocation = asMaybe( + asContractAddressNetworkLocation + )(networkLocation) + return contractAddressNetworkLocation?.contractAddress +} + +export const tokenOverrideToEdgeTokenInfo = ( + token: TokenOverride, + pluginId: string, + tokenType: string | null +): EdgeTokenInfo | undefined => { + const contractAddress = getContractAddress(token.networkLocation ?? {}) + const tokenId = createTokenId(tokenType, token.currencyCode, contractAddress) + if (tokenId == null) return + + return { + rank: Number.MAX_SAFE_INTEGER, + contractAddress: contractAddress ?? tokenId, + currencyCode: token.currencyCode, + displayName: token.displayName, + decimals: token.decimals, + networkLocation: token.networkLocation, + chainPluginId: pluginId, + tokenId + } +} diff --git a/yarn.lock b/yarn.lock index 217980d7..97e3b8c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,11 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/runtime@^7.25.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + "@babel/runtime@^7.6.3": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" @@ -169,6 +174,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/curves@^1.4.2": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/hashes@1.8.0", "@noble/hashes@^1.4.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@node-redis/bloom@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@node-redis/bloom/-/bloom-1.0.1.tgz#144474a0b7dc4a4b91badea2cfa9538ce0a1854e" @@ -247,6 +264,64 @@ dependencies: any-observable "^0.3.0" +"@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/codecs-core@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.3.0.tgz#6bf2bb565cb1ae880f8018635c92f751465d8695" + integrity sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw== + dependencies: + "@solana/errors" "2.3.0" + +"@solana/codecs-numbers@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz#ac7e7f38aaf7fcd22ce2061fbdcd625e73828dc6" + integrity sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg== + dependencies: + "@solana/codecs-core" "2.3.0" + "@solana/errors" "2.3.0" + +"@solana/errors@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.3.0.tgz#4ac9380343dbeffb9dffbcb77c28d0e457c5fa31" + integrity sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ== + dependencies: + chalk "^5.4.1" + commander "^14.0.0" + +"@solana/web3.js@^1.98.4": + version "1.98.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" + integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + +"@swc/helpers@^0.5.11": + version "0.5.18" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.18.tgz#feeeabea0d10106ee25aaf900165df911ab6d3b1" + integrity sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ== + dependencies: + tslib "^2.8.0" + "@types/body-parser@*": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" @@ -272,6 +347,13 @@ dependencies: "@types/node" "*" +"@types/connect@^3.4.33": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" @@ -329,6 +411,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== +"@types/node@^12.12.54": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -357,6 +444,25 @@ "@types/mime" "^1" "@types/node" "*" +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^5.36.2": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -565,6 +671,13 @@ acorn@^8.15.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +agentkeepalive@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -823,6 +936,18 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-x@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" + integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== + dependencies: + safe-buffer "^5.0.1" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + basic-auth@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" @@ -847,6 +972,11 @@ bn.js@^4.11.7: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== +bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" + integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== + body-parser@1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -865,6 +995,15 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -899,6 +1038,28 @@ browser-stdout@1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== +bs58@^4.0.0, bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.1.0.tgz#a4623541dd23867626bb08a051ec0d2ec0b70294" + integrity sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw== + dependencies: + node-gyp-build "^4.3.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -993,6 +1154,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.4.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -1092,6 +1258,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^14.0.0: + version "14.0.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2" + integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw== + +commander@^2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^4.0.0, commander@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -1311,6 +1487,11 @@ define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1577,6 +1758,18 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -1843,6 +2036,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eventemitter3@^5.0.1: + version "5.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb" + integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== + execa@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -1896,6 +2094,11 @@ express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" @@ -1932,6 +2135,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + fastq@^1.6.0: version "1.19.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" @@ -2391,6 +2599,13 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/husky/-/husky-5.2.0.tgz#fc5e1c2300d34855d47de4753607d00943fc0802" @@ -2403,6 +2618,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -2755,6 +2975,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + iterator.prototype@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" @@ -2783,6 +3008,24 @@ jackspeak@^4.1.1: dependencies: "@isaacs/cliui" "^8.0.2" +jayson@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.3.0.tgz#22eb8f3dcf37a5e893830e5451f32bde6d1bde4d" + integrity sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + stream-json "^1.9.1" + uuid "^8.3.2" + ws "^7.5.10" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2823,6 +3066,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -3189,7 +3437,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.3, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3269,6 +3517,18 @@ node-fetch@^2.6.0, node-fetch@^2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -3899,13 +4159,21 @@ rimraf@^6.0.1: glob "^11.0.0" package-json-from-dist "^1.0.0" -rimraf@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" - integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== - dependencies: - glob "^11.0.0" - package-json-from-dist "^1.0.0" +rpc-websockets@^9.0.2: + version "9.3.3" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.3.3.tgz#b6be8b217f2a19bbc5fc184ef492e992b524556b" + integrity sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" run-parallel@^1.1.9: version "1.2.0" @@ -3937,7 +4205,7 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4192,6 +4460,18 @@ stop-iteration-iterator@^1.1.0: es-errors "^1.3.0" internal-slot "^1.1.0" +stream-chain@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09" + integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA== + +stream-json@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.9.1.tgz#e3fec03e984a503718946c170db7d74556c2a187" + integrity sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw== + dependencies: + stream-chain "^2.2.5" + string-argv@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" @@ -4429,6 +4709,11 @@ sucrase@^3.35.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + supports-color@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" @@ -4477,6 +4762,11 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872" integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -4503,6 +4793,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -4535,6 +4830,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.0.tgz#f1f3528301621a53220d58373ae510ff747a66bc" integrity sha512-BmndXUtiTn/VDDrJzQE7Mm22Ix3PxgLltW9bSNLoeCY31gnG2OPx0QqJnuc9oMIKioYrz487i6K9o4Pdn0j+Kg== +tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -4634,11 +4934,23 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -4652,6 +4964,19 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" @@ -4776,6 +5101,16 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.5.0: + version "8.19.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.19.0.tgz#ddc2bdfa5b9ad860204f5a72a4863a8895fd8c8b" + integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg== + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"