diff --git a/apps/token-server/config.ts b/apps/token-server/config.ts --- a/apps/token-server/config.ts +++ b/apps/token-server/config.ts @@ -13,8 +13,18 @@ handler: RateLimitExceededEventHandler; } +interface DbCollections { + blacklist: { name: string }; +} + +interface DatabaseConfig { + name: string; + collections: DbCollections; +} + interface TokenServerConfig { port: Number; + db: DatabaseConfig; chronikUrls: string[]; eligibilityResetSeconds: number; rewardsTokenId: string; @@ -33,6 +43,10 @@ const config: TokenServerConfig = { port: 3333, + db: { + name: 'tokenServerDb', + collections: { blacklist: { name: 'blacklist' } }, + }, chronikUrls: [ 'https://chronik-native1.fabien.cash', 'https://chronik-native2.fabien.cash', diff --git a/apps/token-server/index.ts b/apps/token-server/index.ts --- a/apps/token-server/index.ts +++ b/apps/token-server/index.ts @@ -11,55 +11,93 @@ import fs from 'fs'; import { Ecc, initWasm } from 'ecash-lib'; import { rateLimit } from 'express-rate-limit'; +import { MongoClient } from 'mongodb'; +import { initializeDb } from './src/db'; // Connect to available in-node chronik servers const chronik = new ChronikClient(config.chronikUrls); +// Connect to database +// Connection URL (default) +const MONGODB_URL = `mongodb://${secrets.prod.db.username}:${secrets.prod.db.password}@${secrets.prod.db.containerName}:${secrets.prod.db.port}`; +const client = new MongoClient(MONGODB_URL); +// Check if database exists + // Initialize websocket connection and log incoming blocks initWasm().then( () => { - const ecc = new Ecc(); - // Initialize telegramBot - const telegramBot = initializeTelegramBot( - secrets.prod.botId, - secrets.prod.approvedMods, - fs, - ); + initializeDb(client).then( + db => { + const ecc = new Ecc(); + // Initialize telegramBot + const telegramBot = initializeTelegramBot( + secrets.prod.botId, + secrets.prod.approvedMods, + fs, + ); - // Start the express app to expose API endpoints - const server = startExpressServer( - config.port, - chronik, - telegramBot, - fs, - ecc, - rateLimit(config.limiter), - rateLimit(config.tokenLimiter), - ); - console.log(`Express server started on port ${config.port}`); + // Start the express app to expose API endpoints + const server = startExpressServer( + config.port, + db, + chronik, + telegramBot, + fs, + ecc, + rateLimit(config.limiter), + rateLimit(config.tokenLimiter), + ); + console.log(`Express server started on port ${config.port}`); - // Gracefully shut down on app termination - process.on('SIGTERM', () => { - // kill <pid> from terminal - server.close(); - console.log('token-server shut down by SIGTERM'); - // Shut down the telegram bot - telegramBot.stopPolling(); - process.exit(0); - }); + // Gracefully shut down on app termination + process.on('SIGTERM', () => { + // kill <pid> from terminal + server.close(); + console.log('token-server shut down by SIGTERM'); + // Shut down the telegram bot + telegramBot.stopPolling(); - process.on('SIGINT', () => { - // ctrl + c in nodejs - server.close(); - console.log('token-server shut down by ctrl+c'); - // Shut down the telegram bot - telegramBot.stopPolling(); - process.exit(0); - }); + // Shut down the database + client.close().then(() => { + console.log('MongoDB connection closed'); + // Shut down token-server in non-error condition + process.exit(0); + }); + }); + + process.on('SIGINT', () => { + // ctrl + c in nodejs + server.close(); + console.log('token-server shut down by ctrl+c'); + // Shut down the telegram bot + telegramBot.stopPolling(); + + // Shut down the database + client.close().then(() => { + console.log('MongoDB connection closed'); + // Shut down token-server in non-error condition + process.exit(0); + }); + }); + }, + err => { + console.log(`Error initializing database`, err); + // Shut down the database + client.close().then(() => { + console.log('MongoDB connection closed'); + // Shut down token-server in error condition + process.exit(1); + }); + }, + ); }, err => { - console.log(`Error initializing websocket in token-server`, err); - // Shut down in error condition - process.exit(1); + console.log(`Error initializing webassembly in token-server`, err); + // Shut down the database + client.close().then(() => { + console.log('MongoDB connection closed'); + // Shut down token-server in error condition + process.exit(1); + }); }, ); diff --git a/apps/token-server/package-lock.json b/apps/token-server/package-lock.json --- a/apps/token-server/package-lock.json +++ b/apps/token-server/package-lock.json @@ -22,6 +22,7 @@ "express": "^4.18.3", "express-rate-limit": "^7.4.0", "helmet": "^7.1.0", + "mongodb": "^6.10.0", "multer": "^1.4.5-lts.1", "node-telegram-bot-api": "^0.65.1", "sharp": "^0.33.2", @@ -31,6 +32,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/mocha": "^10.0.6", + "@types/mongodb": "^4.0.6", "@types/multer": "^1.4.11", "@types/node": "^20.11.29", "@types/node-telegram-bot-api": "^0.64.6", @@ -42,6 +44,7 @@ "mocha": "^10.3.0", "mocha-junit-reporter": "^2.2.1", "mocha-suppress-logs": "^0.5.1", + "mongodb-memory-server": "^10.1.2", "nyc": "^15.1.0", "supertest": "^6.3.4", "ts-node": "^10.9.2", @@ -1394,6 +1397,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", @@ -1560,6 +1572,16 @@ "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", "dev": true }, + "node_modules/@types/mongodb": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-4.0.6.tgz", + "integrity": "sha512-XTbn1Z1j7fHzC1Vkd9LYO48lO2C581r+oRCi/KNzcTHIri7hEaya8r9vxoHJiKr+oeUWVK69+9xr84Mp+aReaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mongodb": "*" + } + }, "node_modules/@types/multer": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", @@ -1674,6 +1696,21 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1722,6 +1759,19 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1906,6 +1956,16 @@ "node": ">=0.8" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1987,12 +2047,27 @@ "node": ">=4" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2180,6 +2255,25 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", + "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3315,6 +3409,13 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4013,6 +4114,20 @@ "node": ">=0.10" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4799,6 +4914,12 @@ "url": "https://github.com/sponsors/streamich" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -4979,6 +5100,152 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mongodb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", + "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongodb-memory-server": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.2.tgz", + "integrity": "sha512-aDGEWuUVHTiBvaaq03LbpvvSk8IVtepbvp314p1cq7f2xdSpl7igMnYpPfYY5nkks1I5I6OL2ypHjaJj4kBp+g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "10.1.2", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.2.tgz", + "integrity": "sha512-5Wpz712CuDCKTn/40UZ+kMZlav4Y2imbpWuJU5wjuZk6s3+Jg8akTIBW9jQiFS8wgymu6iTg99Iw0XcypsLyQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^6.9.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.7.0", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mongodb-memory-server-core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5026,6 +5293,19 @@ "node": ">= 0.6" } }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -5472,6 +5752,13 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -5677,6 +5964,13 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6313,6 +6607,15 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -6384,6 +6687,21 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6578,6 +6896,18 @@ "node": ">=8" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6612,6 +6942,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6672,6 +7009,18 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -6725,10 +7074,11 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "devOptional": true + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "devOptional": true, + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -7027,6 +7377,28 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7216,6 +7588,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -8012,6 +8398,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", @@ -8160,6 +8554,15 @@ "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", "dev": true }, + "@types/mongodb": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-4.0.6.tgz", + "integrity": "sha512-XTbn1Z1j7fHzC1Vkd9LYO48lO2C581r+oRCi/KNzcTHIri7hEaya8r9vxoHJiKr+oeUWVK69+9xr84Mp+aReaw==", + "dev": true, + "requires": { + "mongodb": "*" + } + }, "@types/multer": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", @@ -8273,6 +8676,19 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "requires": { + "@types/webidl-conversions": "*" + } + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -8307,6 +8723,15 @@ "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -8451,6 +8876,15 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, + "async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8502,12 +8936,25 @@ } } }, + "b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "optional": true + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -8662,6 +9109,17 @@ "update-browserslist-db": "^1.0.13" } }, + "bson": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", + "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9586,6 +10044,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -10081,6 +10545,16 @@ "sshpk": "^1.14.1" } }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10646,6 +11120,11 @@ "tslib": "^2.0.0" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10779,6 +11258,84 @@ "clone": "^2.1.2" } }, + "mongodb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", + "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", + "requires": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + } + }, + "mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "requires": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "mongodb-memory-server": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.2.tgz", + "integrity": "sha512-aDGEWuUVHTiBvaaq03LbpvvSk8IVtepbvp314p1cq7f2xdSpl7igMnYpPfYY5nkks1I5I6OL2ypHjaJj4kBp+g==", + "dev": true, + "requires": { + "mongodb-memory-server-core": "10.1.2", + "tslib": "^2.7.0" + } + }, + "mongodb-memory-server-core": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.2.tgz", + "integrity": "sha512-5Wpz712CuDCKTn/40UZ+kMZlav4Y2imbpWuJU5wjuZk6s3+Jg8akTIBW9jQiFS8wgymu6iTg99Iw0XcypsLyQA==", + "dev": true, + "requires": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^6.9.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.7.0", + "yauzl": "^3.1.3" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10819,6 +11376,15 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -11161,6 +11727,12 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11308,6 +11880,12 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11796,6 +12374,14 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "requires": { + "memory-pager": "^1.0.2" + } + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -11847,6 +12433,18 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, + "streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -11994,6 +12592,17 @@ "has-flag": "^4.0.0" } }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -12021,6 +12630,12 @@ } } }, + "text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -12066,6 +12681,14 @@ "url-parse": "^1.5.3" } }, + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "requires": { + "punycode": "^2.3.0" + } + }, "ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -12096,9 +12719,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "devOptional": true }, "tunnel-agent": { @@ -12313,6 +12936,20 @@ } } }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12456,6 +13093,16 @@ } } }, + "yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/apps/token-server/package.json b/apps/token-server/package.json --- a/apps/token-server/package.json +++ b/apps/token-server/package.json @@ -32,6 +32,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/mocha": "^10.0.6", + "@types/mongodb": "^4.0.6", "@types/multer": "^1.4.11", "@types/node": "^20.11.29", "@types/node-telegram-bot-api": "^0.64.6", @@ -43,6 +44,7 @@ "mocha": "^10.3.0", "mocha-junit-reporter": "^2.2.1", "mocha-suppress-logs": "^0.5.1", + "mongodb-memory-server": "^10.1.2", "nyc": "^15.1.0", "supertest": "^6.3.4", "ts-node": "^10.9.2", @@ -62,6 +64,7 @@ "express": "^4.18.3", "express-rate-limit": "^7.4.0", "helmet": "^7.1.0", + "mongodb": "^6.10.0", "multer": "^1.4.5-lts.1", "node-telegram-bot-api": "^0.65.1", "sharp": "^0.33.2", diff --git a/apps/token-server/secrets.sample.ts b/apps/token-server/secrets.sample.ts --- a/apps/token-server/secrets.sample.ts +++ b/apps/token-server/secrets.sample.ts @@ -10,6 +10,12 @@ approvedMods: number[]; wallet: ServerWallet; recaptchaSecret: string; + db: { + username: string; + password: string; + containerName: string; + port: number; + }; } interface TokenServerSecrets { @@ -32,6 +38,12 @@ ), }, recaptchaSecret: 'reCAPTCHA_COPY_SECRET_KEY', + db: { + username: 'username', + password: 'password', + containerName: 'localhost', + port: 27017, + }, }, prod: { botId: 'yourBotId', @@ -50,6 +62,12 @@ ), }, recaptchaSecret: 'reCAPTCHA_COPY_SECRET_KEY', + db: { + username: 'username', + password: 'password', + containerName: 'localhost', + port: 27017, + }, }, }; diff --git a/apps/token-server/src/db.ts b/apps/token-server/src/db.ts new file mode 100644 --- /dev/null +++ b/apps/token-server/src/db.ts @@ -0,0 +1,176 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { MongoClient, Db, Collection, CollectionInfo } from 'mongodb'; +import config from '../config'; + +interface BlacklistEntry { + /** tokenId of blacklisted token */ + tokenId: string; + /** A short explanation of why this token was blacklisted, e.g. "impersonating tether" */ + reason: string; + /** + * When this token was added to the blacklist + * We use number instead of Date as the API returns JSON + */ + timestamp: number; + /** string describing who added this token to the blacklist */ + addedBy: string; +} + +const initialBlacklistTokens = [ + { + tokenId: + '09c53c9a9fe0df2cb729dd6f99f2b836c59b842d6652becd85658e277caab611', + reason: 'Impersonates Blazer (site that runs poker tournaments)', + }, + { + tokenId: + '9c662233f8553e72ab3848a37d72fbc3f894611aae43033cde707213a537bba0', + reason: 'Impersonates BUX stablecoin', + }, + { + tokenId: + '6dcb149e77a8f86a85d2fb8505dadb194994a922102fcea6309f2818de9ee173', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '059308a0d6ef0443d8bd014ac85f830d98780b1ce53bc2326680ed27e99803f6', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '2a328dbe125bd0ef8d199b2b4f20ce84bb36a7c0d12246668163a6077d4f494b', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '3387978c85f382632ecb5cdc23c4912c4c22688790d9264f84c3c1351c049719', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '07da70e787181ac67a34f9292b4e13a93cd081e4ca540a8ddafe4cc86ee26e2d', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '4e56e9bedfb654560eb1917b2e2fa40473cf26a8a9a0f84e0b0e91a9cce1df65', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '2a33476bcd30bfbc5e57fb33da26f641020a53c925db7394e6d3b8eecf82e2ec', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + 'b69dcc90c72e852e1dc712704cb376e588cee6266a51e647c61a724c00625cc8', + reason: 'Impersonating a USD stablecoin', + }, + { + tokenId: + '7c14895521c158798478a64d146f67f22e1c8c5b962422ed47636fda71d82f1d', + reason: 'Impersonating Meta and attempting to use their logo', + }, + { + tokenId: + '6f231d49fefd938a9a6b4e6b93d14c7127e11bd5621056eb9c6528164b9d7ce0', + reason: 'Impersonating Meta and attempting to use their logo', + }, + { + tokenId: + 'a6a16ac38d37e35c9f9eb81e9014827cef9da105a94607ec16a2c6e76224d098', + reason: 'Impersonating corporate brand using their logo', + }, + { + tokenId: + 'db2e95abe66f6b1f21a860a177b7a73565182185a99b6043b5183f59df7ecfbf', + reason: 'Impersonating corporate brand using their logo', + }, + { + tokenId: + '4c008a1cd5002063d2942daed16ff0e118bc3e41c7c0a4155ac096ee5a389c21', + reason: 'Impersonating RAIPAY', + }, +]; +const initialBlacklist = initialBlacklistTokens.map(item => ({ + ...item, + // When added to this file + timestamp: Math.round(new Date(1730090292122).getTime() / 1000), + addedBy: 'Initial Setup', +})); +export { initialBlacklist }; + +export const initializeDb = async ( + client: MongoClient, + blacklist = initialBlacklist, +): Promise<Db> => { + await client.connect(); + console.log('Successfully connected to mongod database'); + const db: Db = client.db(config.db.name); + + const collections: CollectionInfo[] = await db.listCollections().toArray(); + + const blacklistCollectionName = config.db.collections.blacklist.name; + + // Check if the collection exists in the list + const blacklistExists: boolean = collections.some( + collection => collection.name === blacklistCollectionName, + ); + + if (!blacklistExists) { + // If the blacklist does not exist, initialize it + const blacklistedTokenIds: Collection = db.collection( + blacklistCollectionName, + ); + // Index by tokenId which is unique, ensuring we do not enter the same tokenId more than once + // This also improves query times + blacklistedTokenIds.createIndex({ tokenId: 1 }, { unique: true }); + + // Initialize blacklist + const result = await blacklistedTokenIds.insertMany(blacklist); + console.log( + `${result.insertedCount} tokens inserted into ${blacklistCollectionName}`, + ); + } else { + // If the blacklist exists, log how many entries we have + const blacklistedTokenCount = await db + .collection(blacklistCollectionName) + .countDocuments(); + console.log( + `Collection "${blacklistCollectionName}" exists and includes ${blacklistedTokenCount} tokens. Continuing token-server startup...`, + ); + } + + return db; +}; + +export const getBlacklistedTokenIds = async (db: Db): Promise<string[]> => { + const collection = db.collection(config.db.collections.blacklist.name); + + // Query only for tokenId fields + const projection = { _id: 0, tokenId: 1 }; + const tokenIds = await collection + .find({}, { projection }) + .map(doc => doc.tokenId) + .toArray(); + + return tokenIds; +}; + +export const getOneBlacklistEntry = async ( + db: Db, + tokenId: string, +): Promise<BlacklistEntry | null> => { + const collection = db.collection(config.db.collections.blacklist.name); + // Don't return _id + const projection = { _id: 0 }; + + // Query for a single document where tokenId matches + const result = await collection.findOne({ tokenId }, { projection }); + + return result as BlacklistEntry | null; +}; diff --git a/apps/token-server/src/routes.ts b/apps/token-server/src/routes.ts --- a/apps/token-server/src/routes.ts +++ b/apps/token-server/src/routes.ts @@ -14,14 +14,16 @@ import { isAddressEligibleForTokenReward } from './rewards'; import { sendReward, sendXecAirdrop } from './transactions'; import { ChronikClient } from 'chronik-client'; -import { isTokenImageRequest } from './validation'; +import { isTokenImageRequest, isValidTokenId } from './validation'; import makeBlockie from 'ethereum-blockies-base64'; import TelegramBot from 'node-telegram-bot-api'; -import { alertNewTokenIcon } from '../src/telegram'; +import { alertNewTokenIcon } from './telegram'; +import { getBlacklistedTokenIds, getOneBlacklistEntry } from './db'; import cashaddr from 'ecashaddrjs'; import { Ecc } from 'ecash-lib'; import { RateLimitRequestHandler } from 'express-rate-limit'; import axios from 'axios'; +import { Db } from 'mongodb'; /** * routes.ts @@ -62,6 +64,7 @@ export const startExpressServer = ( port: Number, + db: Db, chronik: ChronikClient, telegramBot: TelegramBot, fs: any, @@ -101,6 +104,58 @@ }); }); + app.get('/blacklist', async function (req: Request, res: Response) { + logIpInfo(req); + try { + const tokenIds = await getBlacklistedTokenIds(db); + return res.status(200).json({ status: 'success', tokenIds }); + } catch (err) { + console.error('Error retrieving tokenIds:', err); + return res.status(500).json({ + status: 'error', + message: 'Failed to retrieve tokenIds', + }); + } + }); + + app.get( + '/blacklist/:tokenId', + async function (req: Request, res: Response) { + logIpInfo(req); + const tokenId = req.params.tokenId; + + if (!isValidTokenId(tokenId)) { + return res.status(500).json({ + status: 'error', + message: `Invalid tokenId: ${tokenId}`, + }); + } + try { + // Check the blacklist + const entry = await getOneBlacklistEntry(db, tokenId); + console.log(`entry`, entry); + if (entry) { + return res.status(200).json({ + status: 'success', + isBlacklisted: true, + entry, + }); + } else { + return res.status(200).json({ + status: 'success', + isBlacklisted: false, + }); + } + } catch (err) { + console.error(`Error retrieving /blacklist/${tokenId}`, err); + return res.status(500).json({ + status: 'error', + message: `Failed to retrieve tokenId ${tokenId} from the database`, + }); + } + }, + ); + app.get( '/is-eligible/:address', async function (req: Request, res: Response) { diff --git a/apps/token-server/src/validation.ts b/apps/token-server/src/validation.ts --- a/apps/token-server/src/validation.ts +++ b/apps/token-server/src/validation.ts @@ -7,6 +7,9 @@ // Match if input is a string that ends with a 64-char lowercase hex string and .png extension const TOKEN_ICON_REQUEST_REGEX = new RegExp(/^\/([0-9]+)\/[a-f0-9]{64}.png$/); +// TokenId regex +const TOKEN_ID_REGEX = new RegExp(/[a-f0-9]{64}/); + /** * Determine if a request caught by 404 was for a token icon * @param req express request, e.g. /slpv1/512/3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109.png @@ -15,3 +18,7 @@ export const isTokenImageRequest = (req: Request): boolean => { return TOKEN_ICON_REQUEST_REGEX.test(req.url); }; + +export const isValidTokenId = (tokenId: string): boolean => { + return TOKEN_ID_REGEX.test(tokenId); +}; diff --git a/apps/token-server/test/db.test.ts b/apps/token-server/test/db.test.ts new file mode 100644 --- /dev/null +++ b/apps/token-server/test/db.test.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// Mock mongodb +import { MongoClient, Db } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + initializeDb, + getBlacklistedTokenIds, + initialBlacklist, + getOneBlacklistEntry, +} from '../src/db'; +import * as assert from 'assert'; +import config from '../config'; + +// Clone initialBlacklist before initializing the database +// initializeDb(initialBlacklist) will modify the entries by adding an "_id" key +const mockBlacklist = initialBlacklist.map(entry => ({ ...entry })); + +describe('db.ts, token-server database unit tests', async function () { + let mongoServer: MongoMemoryServer, testMongoClient: MongoClient; + before(async () => { + // Start mongo memory server before running this suite of unit tests + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); + + after(async () => { + // Shut down mongo memory server after running this suite of unit tests + await testMongoClient.close(); + await mongoServer.stop(); + }); + + let testDb: Db; + beforeEach(async () => { + testDb = await initializeDb(testMongoClient, initialBlacklist); + }); + + afterEach(async () => { + // Wipe the database after each unit test + await testDb.dropDatabase(); + }); + + it('initializeDb returns a mongo db instance of the expected schema', async function () { + const { namespace } = testDb; + assert.strictEqual(namespace, config.db.name); + }); + it('getBlacklistedTokenIds can fetch an array of all blacklisted token ids', async function () { + const tokenIds = await getBlacklistedTokenIds(testDb); + assert.deepEqual( + tokenIds, + mockBlacklist.map(entry => entry.tokenId), + ); + }); + it('getOneBlacklistEntry returns expected information for a blacklisted tokenId', async function () { + const blacklistedTokenId = mockBlacklist[0].tokenId; + const entry = await getOneBlacklistEntry(testDb, blacklistedTokenId); + assert.deepEqual(entry, mockBlacklist[0]); + }); + it('getOneBlacklistEntry returns null if tokenId cannot be found on the blacklist', async function () { + const blacklistedTokenId = + '0000000000000000000000000000000000000000000000000000000000000000'; + const entry = await getOneBlacklistEntry(testDb, blacklistedTokenId); + assert.equal(entry, null); + }); +}); diff --git a/apps/token-server/test/routes.test.ts b/apps/token-server/test/routes.test.ts --- a/apps/token-server/test/routes.test.ts +++ b/apps/token-server/test/routes.test.ts @@ -20,19 +20,42 @@ } from './vectors'; import { Ecc, initWasm } from 'ecash-lib'; import { rateLimit } from 'express-rate-limit'; +import { MongoClient, Db } from 'mongodb'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + initializeDb, + initialBlacklist, + getBlacklistedTokenIds, +} from '../src/db'; const axios = require('axios'); const MockAdapter = require('axios-mock-adapter'); +// Clone initialBlacklist before initializing the database +// initializeDb(initialBlacklist) will modify the entries by adding an "_id" key +const mockBlacklist = initialBlacklist.map(entry => ({ ...entry })); + describe('routes.js', async function () { let ecc: Ecc; + let mongoServer: MongoMemoryServer, testMongoClient: MongoClient; before(async () => { // Initialize web assembly await initWasm(); // Initialize Ecc ecc = new Ecc(); + // Start mongo memory server before running this suite of unit tests + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); + + after(async () => { + // Shut down mongo memory server after running this suite of unit tests + await testMongoClient.close(); + await mongoServer.stop(); }); let app: http.Server; + let badDbApp: http.Server; const SERVER_WALLET_ADDRESS = secrets.prod.wallet.address; const SERVER_WALLET_OUTPUTSCRIPT = cashaddr.getOutputScriptFromAddress( SERVER_WALLET_ADDRESS, @@ -146,7 +169,9 @@ // Initialize fs, to be memfs in these tests let fs: any; + let testDb: Db; beforeEach(async () => { + testDb = await initializeDb(testMongoClient, initialBlacklist); // Mock expected file structure for fs const fileStructureJson: any = {}; // Create mock empty directories for all supported sizes @@ -158,6 +183,32 @@ const TEST_PORT = 5000; app = startExpressServer( TEST_PORT, + testDb, + mockedChronikClient, + mockedTgBot as unknown as TelegramBot, + fs, + ecc, + // We need higher rate limits so we do not rate limit ourselves in the tests + rateLimit({ + windowMs: 60000, + limit: 100, // Limit each IP to 10 requests per `window` + standardHeaders: 'draft-7', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header + legacyHeaders: false, // Disable the `X-RateLimit-*` headers. + message: 'You have rate limited your own unit tests.', + }), + // In tests, keep the same rate limits for token rewards + rateLimit({ + windowMs: 60000, + limit: 100, // Limit each IP to 10 requests per `window` + standardHeaders: 'draft-7', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header + legacyHeaders: false, // Disable the `X-RateLimit-*` headers. + message: 'You have rate limited your own unit tests.', + }), + ); + const TEST_PORT_BAD_DB = 5001; + badDbApp = startExpressServer( + TEST_PORT_BAD_DB, + {} as unknown as Db, mockedChronikClient, mockedTgBot as unknown as TelegramBot, fs, @@ -185,6 +236,9 @@ vol.reset(); // Stop express server app.close(); + badDbApp.close(); + // Wipe the database after each unit test + await testDb.dropDatabase(); }); it('/status returns expected status', function () { return request(app) @@ -859,4 +913,70 @@ msg: 'Error resizing uploaded token icon', }); }); + it('/blacklist returns tokenIds of the blacklist', function () { + return request(app) + .get(`/blacklist`) + .expect(200) + .expect('Content-Type', /json/) + .expect({ + status: 'success', + tokenIds: mockBlacklist.map(entry => entry.tokenId), + }); + }); + it('/blacklist returns tokenIds of the blacklist', function () { + return request(badDbApp) + .get(`/blacklist`) + .expect(500) + .expect('Content-Type', /json/) + .expect({ + status: 'error', + message: 'Failed to retrieve tokenIds', + }); + }); + it('/blacklist/:tokenId returns expected entry for a valid tokenId in the blacklist', function () { + const tokenId = mockBlacklist[0].tokenId; + return request(app) + .get(`/blacklist/${tokenId}`) + .expect(200) + .expect('Content-Type', /json/) + .expect({ + status: 'success', + isBlacklisted: true, + entry: mockBlacklist[0], + }); + }); + it('/blacklist/:tokenId returns expected error for an invalid tokenId', function () { + const tokenId = 'not a token id'; + return request(app) + .get(`/blacklist/${tokenId}`) + .expect(500) + .expect('Content-Type', /json/) + .expect({ + status: 'error', + message: `Invalid tokenId: ${tokenId}`, + }); + }); + it('/blacklist/:tokenId returns expected entry for a valid tokenId NOT in the blacklist', function () { + const tokenId = + '0000000000000000000000000000000000000000000000000000000000000000'; + return request(app) + .get(`/blacklist/${tokenId}`) + .expect(200) + .expect('Content-Type', /json/) + .expect({ + status: 'success', + isBlacklisted: false, + }); + }); + it('/blacklist/:tokenId returns expected error if database fails to lookup a valid tokenId', function () { + const tokenId = mockBlacklist[0].tokenId; + return request(badDbApp) + .get(`/blacklist/${tokenId}`) + .expect(500) + .expect('Content-Type', /json/) + .expect({ + status: 'error', + message: `Failed to retrieve tokenId ${tokenId} from the database`, + }); + }); }); diff --git a/apps/token-server/test/validation.test.ts b/apps/token-server/test/validation.test.ts --- a/apps/token-server/test/validation.test.ts +++ b/apps/token-server/test/validation.test.ts @@ -3,7 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. import * as assert from 'assert'; -import { isTokenImageRequest } from '../src/validation'; +import { isTokenImageRequest, isValidTokenId } from '../src/validation'; import vectors from './vectors'; describe('validation.ts', function () { @@ -16,4 +16,13 @@ }); }); }); + describe('We can validate a tokenId', function () { + const { returns } = vectors.isValidTokenId; + returns.forEach(vector => { + const { description, string, returned } = vector; + it(description, function () { + assert.equal(isValidTokenId(string), returned); + }); + }); + }); }); diff --git a/apps/token-server/test/vectors.ts b/apps/token-server/test/vectors.ts --- a/apps/token-server/test/vectors.ts +++ b/apps/token-server/test/vectors.ts @@ -320,6 +320,16 @@ error: Error; } +interface IsValidTokenIdVector { + returns: IsValidTokenIdReturn[]; +} + +interface IsValidTokenIdReturn { + description: string; + string: string; + returned: boolean; +} + interface TestVectors { hasInputsFromOutputScript: HasInputsFromOutputScriptVector; addressReceivedToken: AddressReceivedTokenReturnVector; @@ -332,6 +342,7 @@ getSlpInputsAndOutputs: GetSlpInputsAndOutputsVector; sendReward: SendRewardVector; sendXecAirdrop: SendXecAirdropVector; + isValidTokenId: IsValidTokenIdVector; } const vectors: TestVectors = { @@ -1236,6 +1247,45 @@ }, ], }, + isValidTokenId: { + returns: [ + { + description: 'Valid tokenId', + string: '0000000000000000000000000000000000000000000000000000000000000000', + returned: true, + }, + { + description: 'Valid hex but 63 chars is invalid', + string: '000000000000000000000000000000000000000000000000000000000000000', + returned: false, + }, + { + description: 'Valid hex but 31 bytes (62 chars) is invalid', + string: '00000000000000000000000000000000000000000000000000000000000000', + returned: false, + }, + { + description: 'Valid hex but 65 chars is invalid', + string: '00000000000000000000000000000000000000000000000000000000000000000', + returned: true, + }, + { + description: 'Valid hex but 33 bytes (66 chars) is invalid', + string: '000000000000000000000000000000000000000000000000000000000000000000', + returned: true, + }, + { + description: 'Valid length but invalid hex is invalid', + string: 'g000000000000000000000000000000000000000000000000000000000000000', + returned: false, + }, + { + description: 'Empty string is invalid', + string: '', + returned: false, + }, + ], + }, }; export default vectors; diff --git a/cashtab/src/config/token.js b/cashtab/src/config/token.js --- a/cashtab/src/config/token.js +++ b/cashtab/src/config/token.js @@ -21,6 +21,8 @@ '2a328dbe125bd0ef8d199b2b4f20ce84bb36a7c0d12246668163a6077d4f494b', '3387978c85f382632ecb5cdc23c4912c4c22688790d9264f84c3c1351c049719', '07da70e787181ac67a34f9292b4e13a93cd081e4ca540a8ddafe4cc86ee26e2d', + '2a33476bcd30bfbc5e57fb33da26f641020a53c925db7394e6d3b8eecf82e2ec', + 'b69dcc90c72e852e1dc712704cb376e588cee6266a51e647c61a724c00625cc8', // fake facebook/meta '7c14895521c158798478a64d146f67f22e1c8c5b962422ed47636fda71d82f1d', '6f231d49fefd938a9a6b4e6b93d14c7127e11bd5621056eb9c6528164b9d7ce0',