Merge branch 'master' into natsoni/block-first-seen-audit
This commit is contained in:
		
						commit
						6884830da6
					
				| @ -27,6 +27,7 @@ | ||||
|     "AUTOMATIC_POOLS_UPDATE": false, | ||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", | ||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||
|     "POOLS_UPDATE_DELAY": 604800, | ||||
|     "AUDIT": false, | ||||
|     "RUST_GBT": true, | ||||
|     "LIMIT_GBT": false, | ||||
|  | ||||
| @ -28,6 +28,7 @@ | ||||
|     "INDEXING_BLOCKS_AMOUNT": 14, | ||||
|     "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", | ||||
|     "POOLS_UPDATE_DELAY": 604800, | ||||
|     "AUDIT": true, | ||||
|     "RUST_GBT": false, | ||||
|     "LIMIT_GBT": false, | ||||
|  | ||||
| @ -41,6 +41,7 @@ describe('Mempool Backend Config', () => { | ||||
|         STDOUT_LOG_MIN_PRIORITY: 'debug', | ||||
|         POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||
|         POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', | ||||
|         POOLS_UPDATE_DELAY: 604800, | ||||
|         AUDIT: false, | ||||
|         RUST_GBT: true, | ||||
|         LIMIT_GBT: false, | ||||
|  | ||||
| @ -32,6 +32,7 @@ interface IConfig { | ||||
|     AUTOMATIC_POOLS_UPDATE: boolean; | ||||
|     POOLS_JSON_URL: string, | ||||
|     POOLS_JSON_TREE_URL: string, | ||||
|     POOLS_UPDATE_DELAY: number, | ||||
|     AUDIT: boolean; | ||||
|     RUST_GBT: boolean; | ||||
|     LIMIT_GBT: boolean; | ||||
| @ -193,6 +194,7 @@ const defaults: IConfig = { | ||||
|     'AUTOMATIC_POOLS_UPDATE': false, | ||||
|     'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', | ||||
|     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||
|     'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
 | ||||
|     'AUDIT': false, | ||||
|     'RUST_GBT': true, | ||||
|     'LIMIT_GBT': false, | ||||
|  | ||||
| @ -211,6 +211,8 @@ class Server { | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     poolsUpdater.$startService(); | ||||
|   } | ||||
| 
 | ||||
|   async runMainUpdateLoop(): Promise<void> { | ||||
|  | ||||
| @ -6,16 +6,30 @@ import backendInfo from '../api/backend-info'; | ||||
| import logger from '../logger'; | ||||
| import { SocksProxyAgent } from 'socks-proxy-agent'; | ||||
| import * as https from 'https'; | ||||
| import { Common } from '../api/common'; | ||||
| 
 | ||||
| /** | ||||
|  * Maintain the most recent version of pools-v2.json | ||||
|  */ | ||||
| class PoolsUpdater { | ||||
|   tag = 'PoolsUpdater'; | ||||
| 
 | ||||
|   lastRun: number = 0; | ||||
|   currentSha: string | null = null; | ||||
|   poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; | ||||
|   treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; | ||||
| 
 | ||||
|   public async $startService(): Promise<void> { | ||||
|     while ('Bitcoin is still alive') { | ||||
|       try { | ||||
|         await this.updatePoolsJson(); | ||||
|       } catch (e: any) { | ||||
|         logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag); | ||||
|       } | ||||
|       await Common.sleep$(10000); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async updatePoolsJson(): Promise<void> { | ||||
|     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || | ||||
|       config.MEMPOOL.ENABLED === false | ||||
| @ -23,11 +37,8 @@ class PoolsUpdater { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const oneWeek = 604800; | ||||
|     const oneDay = 86400; | ||||
| 
 | ||||
|     const now = new Date().getTime() / 1000; | ||||
|     if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
 | ||||
|     if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
 | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
| @ -43,7 +54,7 @@ class PoolsUpdater { | ||||
|         this.currentSha = await this.getShaFromDb(); | ||||
|       } | ||||
| 
 | ||||
|       logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); | ||||
|       logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag); | ||||
|       if (this.currentSha !== null && this.currentSha === githubSha) { | ||||
|         return; | ||||
|       } | ||||
| @ -53,16 +64,16 @@ class PoolsUpdater { | ||||
|         config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
 | ||||
|         !process.env.npm_config_update_pools // We're not manually updating mining pool
 | ||||
|       ) { | ||||
|         logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`); | ||||
|         logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); | ||||
|         logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag); | ||||
|         logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; | ||||
|       if (this.currentSha === null) { | ||||
|         logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); | ||||
|         logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag); | ||||
|       } else { | ||||
|         logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); | ||||
|         logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); | ||||
|       } | ||||
|       const poolsJson = await this.query(this.poolsUrl); | ||||
|       if (poolsJson === undefined) { | ||||
| @ -71,7 +82,7 @@ class PoolsUpdater { | ||||
|       poolsParser.setMiningPools(poolsJson); | ||||
| 
 | ||||
|       if (config.DATABASE.ENABLED === false) { // Don't run db operations
 | ||||
|         logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`); | ||||
|         logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
| @ -81,14 +92,14 @@ class PoolsUpdater { | ||||
|         await this.updateDBSha(githubSha); | ||||
|         await DB.query('COMMIT;'); | ||||
|       } catch (e) { | ||||
|         logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); | ||||
|         logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); | ||||
|         await DB.query('ROLLBACK;'); | ||||
|       } | ||||
|       logger.info(`Mining pools-v2.json (${githubSha}) import completed`); | ||||
|       logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); | ||||
| 
 | ||||
|     } catch (e) { | ||||
|       this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
 | ||||
|       logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); | ||||
|       this.lastRun = now - 600; // Try again in 10 minutes
 | ||||
|       logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -102,7 +113,7 @@ class PoolsUpdater { | ||||
|         await DB.query('DELETE FROM state where name="pools_json_sha"'); | ||||
|         await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); | ||||
|       } catch (e) { | ||||
|         logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); | ||||
|         logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -115,7 +126,7 @@ class PoolsUpdater { | ||||
|       const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); | ||||
|       return (rows.length > 0 ? rows[0].string : null); | ||||
|     } catch (e) { | ||||
|       logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); | ||||
|       logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| @ -134,7 +145,7 @@ class PoolsUpdater { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); | ||||
|     logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag); | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
| @ -186,7 +197,7 @@ class PoolsUpdater { | ||||
|         } | ||||
|         return data.data; | ||||
|       } catch (e) { | ||||
|         logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|         logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag); | ||||
|         retry++; | ||||
|       } | ||||
|       await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); | ||||
|  | ||||
| @ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over | ||||
|     "AUTOMATIC_POOLS_UPDATE": false, | ||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", | ||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||
|     "POOLS_UPDATE_DELAY": 604800, | ||||
|     "CPFP_INDEXING": false, | ||||
|     "MAX_BLOCKS_BULK_QUERY": 0, | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 6, | ||||
| @ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides: | ||||
|       MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" | ||||
|       MEMPOOL_POOLS_JSON_URL: "" | ||||
|       MEMPOOL_POOLS_JSON_TREE_URL: "" | ||||
|       MEMPOOL_POOLS_UPDATE_DELAY: "" | ||||
|       MEMPOOL_CPFP_INDEXING: "" | ||||
|       MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" | ||||
|       MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" | ||||
|  | ||||
| @ -36,6 +36,7 @@ | ||||
|     "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, | ||||
|     "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", | ||||
|     "POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__, | ||||
|     "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, | ||||
|     "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ | ||||
|   }, | ||||
|  | ||||
| @ -29,6 +29,7 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} | ||||
| __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} | ||||
| __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} | ||||
| __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} | ||||
| __MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800} | ||||
| __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} | ||||
| __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true} | ||||
| __MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false} | ||||
| @ -188,6 +189,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT | ||||
| sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json | ||||
|  | ||||
							
								
								
									
										495
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										495
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -62,7 +62,7 @@ | ||||
|       "optionalDependencies": { | ||||
|         "@cypress/schematic": "^2.5.0", | ||||
|         "@types/cypress": "^1.1.3", | ||||
|         "cypress": "^13.14.0", | ||||
|         "cypress": "^13.15.0", | ||||
|         "cypress-fail-on-console-error": "~5.1.0", | ||||
|         "cypress-wait-until": "^2.0.1", | ||||
|         "mock-socket": "~9.3.1", | ||||
| @ -3113,9 +3113,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@cypress/request": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", | ||||
|       "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", | ||||
|       "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "aws-sign2": "~0.7.0", | ||||
| @ -3124,14 +3124,14 @@ | ||||
|         "combined-stream": "~1.0.6", | ||||
|         "extend": "~3.0.2", | ||||
|         "forever-agent": "~0.6.1", | ||||
|         "form-data": "~2.3.2", | ||||
|         "http-signature": "~1.3.6", | ||||
|         "form-data": "~4.0.0", | ||||
|         "http-signature": "~1.4.0", | ||||
|         "is-typedarray": "~1.0.0", | ||||
|         "isstream": "~0.1.2", | ||||
|         "json-stringify-safe": "~5.0.1", | ||||
|         "mime-types": "~2.1.19", | ||||
|         "performance-now": "^2.1.0", | ||||
|         "qs": "6.10.4", | ||||
|         "qs": "6.13.0", | ||||
|         "safe-buffer": "^5.1.2", | ||||
|         "tough-cookie": "^4.1.3", | ||||
|         "tunnel-agent": "^0.6.0", | ||||
| @ -4313,9 +4313,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-android-arm-eabi": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", | ||||
|       "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", | ||||
|       "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", | ||||
|       "cpu": [ | ||||
|         "arm" | ||||
|       ], | ||||
| @ -4325,9 +4325,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-android-arm64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", | ||||
|       "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", | ||||
|       "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -4337,9 +4337,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-darwin-arm64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", | ||||
|       "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", | ||||
|       "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -4349,9 +4349,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-darwin-x64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", | ||||
|       "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", | ||||
|       "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -4361,9 +4361,21 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-arm-gnueabihf": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", | ||||
|       "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", | ||||
|       "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", | ||||
|       "cpu": [ | ||||
|         "arm" | ||||
|       ], | ||||
|       "optional": true, | ||||
|       "os": [ | ||||
|         "linux" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-arm-musleabihf": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", | ||||
|       "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", | ||||
|       "cpu": [ | ||||
|         "arm" | ||||
|       ], | ||||
| @ -4373,9 +4385,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-arm64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -4385,9 +4397,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-arm64-musl": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", | ||||
|       "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", | ||||
|       "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -4396,10 +4408,22 @@ | ||||
|         "linux" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", | ||||
|       "cpu": [ | ||||
|         "ppc64" | ||||
|       ], | ||||
|       "optional": true, | ||||
|       "os": [ | ||||
|         "linux" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-riscv64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", | ||||
|       "cpu": [ | ||||
|         "riscv64" | ||||
|       ], | ||||
| @ -4408,10 +4432,22 @@ | ||||
|         "linux" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-s390x-gnu": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", | ||||
|       "cpu": [ | ||||
|         "s390x" | ||||
|       ], | ||||
|       "optional": true, | ||||
|       "os": [ | ||||
|         "linux" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-x64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -4421,9 +4457,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-linux-x64-musl": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", | ||||
|       "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", | ||||
|       "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -4433,9 +4469,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-win32-arm64-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", | ||||
|       "cpu": [ | ||||
|         "arm64" | ||||
|       ], | ||||
| @ -4445,9 +4481,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-win32-ia32-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", | ||||
|       "cpu": [ | ||||
|         "ia32" | ||||
|       ], | ||||
| @ -4457,9 +4493,9 @@ | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/@rollup/rollup-win32-x64-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", | ||||
|       "cpu": [ | ||||
|         "x64" | ||||
|       ], | ||||
| @ -4801,9 +4837,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/estree": { | ||||
|       "version": "1.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", | ||||
|       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" | ||||
|       "version": "1.0.6", | ||||
|       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", | ||||
|       "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" | ||||
|     }, | ||||
|     "node_modules/@types/express": { | ||||
|       "version": "4.17.13", | ||||
| @ -5797,9 +5833,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/aws4": { | ||||
|       "version": "1.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", | ||||
|       "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", | ||||
|       "version": "1.13.2", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", | ||||
|       "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "node_modules/axios": { | ||||
| @ -6065,20 +6101,6 @@ | ||||
|         "node": ">= 0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/body-parser/node_modules/qs": { | ||||
|       "version": "6.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|       "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|       "dependencies": { | ||||
|         "side-channel": "^1.0.6" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.6" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/ljharb" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/bonjour-service": { | ||||
|       "version": "1.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", | ||||
| @ -8045,13 +8067,13 @@ | ||||
|       "peer": true | ||||
|     }, | ||||
|     "node_modules/cypress": { | ||||
|       "version": "13.14.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", | ||||
|       "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", | ||||
|       "version": "13.15.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", | ||||
|       "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", | ||||
|       "hasInstallScript": true, | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "@cypress/request": "^3.0.1", | ||||
|         "@cypress/request": "^3.0.4", | ||||
|         "@cypress/xvfb": "^1.2.4", | ||||
|         "@types/sinonjs__fake-timers": "8.1.1", | ||||
|         "@types/sizzle": "^2.3.2", | ||||
| @ -9896,20 +9918,6 @@ | ||||
|         "node": ">= 0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/express/node_modules/qs": { | ||||
|       "version": "6.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|       "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|       "dependencies": { | ||||
|         "side-channel": "^1.0.6" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.6" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/ljharb" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/express/node_modules/safe-buffer": { | ||||
|       "version": "5.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | ||||
| @ -10305,17 +10313,17 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/form-data": { | ||||
|       "version": "2.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", | ||||
|       "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", | ||||
|       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.6", | ||||
|         "combined-stream": "^1.0.8", | ||||
|         "mime-types": "^2.1.12" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.12" | ||||
|         "node": ">= 6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/forwarded": { | ||||
| @ -10957,14 +10965,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/http-signature": { | ||||
|       "version": "1.3.6", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", | ||||
|       "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", | ||||
|       "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "jsprim": "^2.0.2", | ||||
|         "sshpk": "^1.14.1" | ||||
|         "sshpk": "^1.18.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.10" | ||||
| @ -14737,12 +14745,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/qs": { | ||||
|       "version": "6.10.4", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", | ||||
|       "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", | ||||
|       "optional": true, | ||||
|       "version": "6.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|       "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|       "dependencies": { | ||||
|         "side-channel": "^1.0.4" | ||||
|         "side-channel": "^1.0.6" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.6" | ||||
| @ -15198,11 +15205,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/rollup": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", | ||||
|       "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", | ||||
|       "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", | ||||
|       "dependencies": { | ||||
|         "@types/estree": "1.0.5" | ||||
|         "@types/estree": "1.0.6" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "rollup": "dist/bin/rollup" | ||||
| @ -15212,19 +15219,22 @@ | ||||
|         "npm": ">=8.0.0" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
|         "@rollup/rollup-android-arm-eabi": "4.13.0", | ||||
|         "@rollup/rollup-android-arm64": "4.13.0", | ||||
|         "@rollup/rollup-darwin-arm64": "4.13.0", | ||||
|         "@rollup/rollup-darwin-x64": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm64-musl": "4.13.0", | ||||
|         "@rollup/rollup-linux-riscv64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-x64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-x64-musl": "4.13.0", | ||||
|         "@rollup/rollup-win32-arm64-msvc": "4.13.0", | ||||
|         "@rollup/rollup-win32-ia32-msvc": "4.13.0", | ||||
|         "@rollup/rollup-win32-x64-msvc": "4.13.0", | ||||
|         "@rollup/rollup-android-arm-eabi": "4.24.0", | ||||
|         "@rollup/rollup-android-arm64": "4.24.0", | ||||
|         "@rollup/rollup-darwin-arm64": "4.24.0", | ||||
|         "@rollup/rollup-darwin-x64": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm-musleabihf": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm64-musl": "4.24.0", | ||||
|         "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-riscv64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-s390x-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-x64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-x64-musl": "4.24.0", | ||||
|         "@rollup/rollup-win32-arm64-msvc": "4.24.0", | ||||
|         "@rollup/rollup-win32-ia32-msvc": "4.24.0", | ||||
|         "@rollup/rollup-win32-x64-msvc": "4.24.0", | ||||
|         "fsevents": "~2.3.2" | ||||
|       } | ||||
|     }, | ||||
| @ -16129,9 +16139,9 @@ | ||||
|       "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" | ||||
|     }, | ||||
|     "node_modules/sshpk": { | ||||
|       "version": "1.17.0", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", | ||||
|       "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", | ||||
|       "version": "1.18.0", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", | ||||
|       "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "asn1": "~0.2.3", | ||||
| @ -16725,9 +16735,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tough-cookie": { | ||||
|       "version": "4.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", | ||||
|       "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", | ||||
|       "version": "4.1.4", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", | ||||
|       "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "psl": "^1.1.33", | ||||
| @ -17799,20 +17809,6 @@ | ||||
|         "proxy-from-env": "^1.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/wait-on/node_modules/form-data": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", | ||||
|       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.8", | ||||
|         "mime-types": "^2.1.12" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/wait-on/node_modules/proxy-from-env": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", | ||||
| @ -20466,9 +20462,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@cypress/request": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", | ||||
|       "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", | ||||
|       "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "aws-sign2": "~0.7.0", | ||||
| @ -20477,14 +20473,14 @@ | ||||
|         "combined-stream": "~1.0.6", | ||||
|         "extend": "~3.0.2", | ||||
|         "forever-agent": "~0.6.1", | ||||
|         "form-data": "~2.3.2", | ||||
|         "http-signature": "~1.3.6", | ||||
|         "form-data": "~4.0.0", | ||||
|         "http-signature": "~1.4.0", | ||||
|         "is-typedarray": "~1.0.0", | ||||
|         "isstream": "~0.1.2", | ||||
|         "json-stringify-safe": "~5.0.1", | ||||
|         "mime-types": "~2.1.19", | ||||
|         "performance-now": "^2.1.0", | ||||
|         "qs": "6.10.4", | ||||
|         "qs": "6.13.0", | ||||
|         "safe-buffer": "^5.1.2", | ||||
|         "tough-cookie": "^4.1.3", | ||||
|         "tunnel-agent": "^0.6.0", | ||||
| @ -21229,81 +21225,99 @@ | ||||
|       "peer": true | ||||
|     }, | ||||
|     "@rollup/rollup-android-arm-eabi": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", | ||||
|       "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", | ||||
|       "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-android-arm64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", | ||||
|       "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", | ||||
|       "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-darwin-arm64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", | ||||
|       "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", | ||||
|       "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-darwin-x64": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", | ||||
|       "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", | ||||
|       "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-arm-gnueabihf": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", | ||||
|       "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", | ||||
|       "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-arm-musleabihf": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", | ||||
|       "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-arm64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-arm64-musl": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", | ||||
|       "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", | ||||
|       "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-powerpc64le-gnu": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-riscv64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-s390x-gnu": { | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-x64-gnu": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", | ||||
|       "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", | ||||
|       "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-linux-x64-musl": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", | ||||
|       "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", | ||||
|       "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-win32-arm64-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-win32-ia32-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@rollup/rollup-win32-x64-msvc": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", | ||||
|       "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", | ||||
|       "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "@schematics/angular": { | ||||
| @ -21607,9 +21621,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@types/estree": { | ||||
|       "version": "1.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", | ||||
|       "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" | ||||
|       "version": "1.0.6", | ||||
|       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", | ||||
|       "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" | ||||
|     }, | ||||
|     "@types/express": { | ||||
|       "version": "4.17.13", | ||||
| @ -22369,9 +22383,9 @@ | ||||
|       "optional": true | ||||
|     }, | ||||
|     "aws4": { | ||||
|       "version": "1.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", | ||||
|       "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", | ||||
|       "version": "1.13.2", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", | ||||
|       "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "axios": { | ||||
| @ -22583,14 +22597,6 @@ | ||||
|           "requires": { | ||||
|             "ee-first": "1.1.1" | ||||
|           } | ||||
|         }, | ||||
|         "qs": { | ||||
|           "version": "6.13.0", | ||||
|           "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|           "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|           "requires": { | ||||
|             "side-channel": "^1.0.6" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @ -24100,12 +24106,12 @@ | ||||
|       "peer": true | ||||
|     }, | ||||
|     "cypress": { | ||||
|       "version": "13.14.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", | ||||
|       "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", | ||||
|       "version": "13.15.0", | ||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", | ||||
|       "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "@cypress/request": "^3.0.1", | ||||
|         "@cypress/request": "^3.0.4", | ||||
|         "@cypress/xvfb": "^1.2.4", | ||||
|         "@types/sinonjs__fake-timers": "8.1.1", | ||||
|         "@types/sizzle": "^2.3.2", | ||||
| @ -25554,14 +25560,6 @@ | ||||
|             "ee-first": "1.1.1" | ||||
|           } | ||||
|         }, | ||||
|         "qs": { | ||||
|           "version": "6.13.0", | ||||
|           "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|           "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|           "requires": { | ||||
|             "side-channel": "^1.0.6" | ||||
|           } | ||||
|         }, | ||||
|         "safe-buffer": { | ||||
|           "version": "5.2.1", | ||||
|           "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | ||||
| @ -25853,13 +25851,13 @@ | ||||
|       "optional": true | ||||
|     }, | ||||
|     "form-data": { | ||||
|       "version": "2.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", | ||||
|       "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", | ||||
|       "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.6", | ||||
|         "combined-stream": "^1.0.8", | ||||
|         "mime-types": "^2.1.12" | ||||
|       } | ||||
|     }, | ||||
| @ -26321,14 +26319,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "http-signature": { | ||||
|       "version": "1.3.6", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", | ||||
|       "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", | ||||
|       "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "jsprim": "^2.0.2", | ||||
|         "sshpk": "^1.14.1" | ||||
|         "sshpk": "^1.18.0" | ||||
|       } | ||||
|     }, | ||||
|     "https-browserify": { | ||||
| @ -29098,12 +29096,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "qs": { | ||||
|       "version": "6.10.4", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", | ||||
|       "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", | ||||
|       "optional": true, | ||||
|       "version": "6.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", | ||||
|       "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", | ||||
|       "requires": { | ||||
|         "side-channel": "^1.0.4" | ||||
|         "side-channel": "^1.0.6" | ||||
|       } | ||||
|     }, | ||||
|     "querystring": { | ||||
| @ -29456,24 +29453,27 @@ | ||||
|       } | ||||
|     }, | ||||
|     "rollup": { | ||||
|       "version": "4.13.0", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", | ||||
|       "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", | ||||
|       "version": "4.24.0", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", | ||||
|       "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", | ||||
|       "requires": { | ||||
|         "@rollup/rollup-android-arm-eabi": "4.13.0", | ||||
|         "@rollup/rollup-android-arm64": "4.13.0", | ||||
|         "@rollup/rollup-darwin-arm64": "4.13.0", | ||||
|         "@rollup/rollup-darwin-x64": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-arm64-musl": "4.13.0", | ||||
|         "@rollup/rollup-linux-riscv64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-x64-gnu": "4.13.0", | ||||
|         "@rollup/rollup-linux-x64-musl": "4.13.0", | ||||
|         "@rollup/rollup-win32-arm64-msvc": "4.13.0", | ||||
|         "@rollup/rollup-win32-ia32-msvc": "4.13.0", | ||||
|         "@rollup/rollup-win32-x64-msvc": "4.13.0", | ||||
|         "@types/estree": "1.0.5", | ||||
|         "@rollup/rollup-android-arm-eabi": "4.24.0", | ||||
|         "@rollup/rollup-android-arm64": "4.24.0", | ||||
|         "@rollup/rollup-darwin-arm64": "4.24.0", | ||||
|         "@rollup/rollup-darwin-x64": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm-musleabihf": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-arm64-musl": "4.24.0", | ||||
|         "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-riscv64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-s390x-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-x64-gnu": "4.24.0", | ||||
|         "@rollup/rollup-linux-x64-musl": "4.24.0", | ||||
|         "@rollup/rollup-win32-arm64-msvc": "4.24.0", | ||||
|         "@rollup/rollup-win32-ia32-msvc": "4.24.0", | ||||
|         "@rollup/rollup-win32-x64-msvc": "4.24.0", | ||||
|         "@types/estree": "1.0.6", | ||||
|         "fsevents": "~2.3.2" | ||||
|       } | ||||
|     }, | ||||
| @ -30167,9 +30167,9 @@ | ||||
|       "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" | ||||
|     }, | ||||
|     "sshpk": { | ||||
|       "version": "1.17.0", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", | ||||
|       "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", | ||||
|       "version": "1.18.0", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", | ||||
|       "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "asn1": "~0.2.3", | ||||
| @ -30615,9 +30615,9 @@ | ||||
|       "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" | ||||
|     }, | ||||
|     "tough-cookie": { | ||||
|       "version": "4.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", | ||||
|       "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", | ||||
|       "version": "4.1.4", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", | ||||
|       "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", | ||||
|       "optional": true, | ||||
|       "requires": { | ||||
|         "psl": "^1.1.33", | ||||
| @ -31248,17 +31248,6 @@ | ||||
|             "proxy-from-env": "^1.1.0" | ||||
|           } | ||||
|         }, | ||||
|         "form-data": { | ||||
|           "version": "4.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", | ||||
|           "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "asynckit": "^0.4.0", | ||||
|             "combined-stream": "^1.0.8", | ||||
|             "mime-types": "^2.1.12" | ||||
|           } | ||||
|         }, | ||||
|         "proxy-from-env": { | ||||
|           "version": "1.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", | ||||
|  | ||||
| @ -115,7 +115,7 @@ | ||||
|   "optionalDependencies": { | ||||
|     "@cypress/schematic": "^2.5.0", | ||||
|     "@types/cypress": "^1.1.3", | ||||
|     "cypress": "^13.14.0", | ||||
|     "cypress": "^13.15.0", | ||||
|     "cypress-fail-on-console-error": "~5.1.0", | ||||
|     "cypress-wait-until": "^2.0.1", | ||||
|     "mock-socket": "~9.3.1", | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { ZONE_SERVICE } from './injection-tokens'; | ||||
| import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './components/app/app.component'; | ||||
| import { ElectrsApiService } from './services/electrs-api.service'; | ||||
| import { OrdApiService } from './services/ord-api.service'; | ||||
| import { StateService } from './services/state.service'; | ||||
| import { CacheService } from './services/cache.service'; | ||||
| import { PriceService } from './services/price.service'; | ||||
| @ -32,6 +33,7 @@ import { DatePipe } from '@angular/common'; | ||||
| 
 | ||||
| const providers = [ | ||||
|   ElectrsApiService, | ||||
|   OrdApiService, | ||||
|   StateService, | ||||
|   CacheService, | ||||
|   PriceService, | ||||
|  | ||||
| @ -64,7 +64,7 @@ | ||||
|               <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> | ||||
|               <span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span> | ||||
|               <span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span> | ||||
|               <span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span> | ||||
|               <span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span> | ||||
|             </td> | ||||
|             <td class="date text-right" *ngIf="!this.widget"> | ||||
|               <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> | ||||
|  | ||||
| @ -219,11 +219,11 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|             address.is_pubkey | ||||
|               ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') | ||||
|               : this.electrsApiService.getAddressTransactions$(address.address), | ||||
|             (utxoCount >= 2 && utxoCount <= 500 ? (address.is_pubkey | ||||
|             (utxoCount > 2 && utxoCount <= 500 ? (address.is_pubkey | ||||
|               ? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') | ||||
|               : this.electrsApiService.getAddressUtxos$(address.address)) : of([])).pipe( | ||||
|               : this.electrsApiService.getAddressUtxos$(address.address)) : of(null)).pipe( | ||||
|                 catchError(() => { | ||||
|                   return of([]); | ||||
|                   return of(null); | ||||
|                 }) | ||||
|               ) | ||||
|           ]); | ||||
| @ -350,27 +350,29 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     // update utxos in-place
 | ||||
|     let utxosChanged = false; | ||||
|     for (const vin of transaction.vin) { | ||||
|       const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); | ||||
|       if (utxoIndex !== -1) { | ||||
|         this.utxos.splice(utxoIndex, 1); | ||||
|         utxosChanged = true; | ||||
|     if (this.utxos != null) { | ||||
|       let utxosChanged = false; | ||||
|       for (const vin of transaction.vin) { | ||||
|         const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); | ||||
|         if (utxoIndex !== -1) { | ||||
|           this.utxos.splice(utxoIndex, 1); | ||||
|           utxosChanged = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     for (const [index, vout] of transaction.vout.entries()) { | ||||
|       if (vout.scriptpubkey_address === this.address.address) { | ||||
|         this.utxos.push({ | ||||
|           txid: transaction.txid, | ||||
|           vout: index, | ||||
|           value: vout.value, | ||||
|           status: JSON.parse(JSON.stringify(transaction.status)), | ||||
|         }); | ||||
|         utxosChanged = true; | ||||
|       for (const [index, vout] of transaction.vout.entries()) { | ||||
|         if (vout.scriptpubkey_address === this.address.address) { | ||||
|           this.utxos.push({ | ||||
|             txid: transaction.txid, | ||||
|             vout: index, | ||||
|             value: vout.value, | ||||
|             status: JSON.parse(JSON.stringify(transaction.status)), | ||||
|           }); | ||||
|           utxosChanged = true; | ||||
|         } | ||||
|       } | ||||
|       if (utxosChanged) { | ||||
|         this.utxos = this.utxos.slice(); | ||||
|       } | ||||
|     } | ||||
|     if (utxosChanged) { | ||||
|       this.utxos = this.utxos.slice(); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
| @ -385,29 +387,31 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|     this.transactions = this.transactions.slice(); | ||||
| 
 | ||||
|     // update utxos in-place
 | ||||
|     let utxosChanged = false; | ||||
|     for (const vin of transaction.vin) { | ||||
|       if (vin.prevout?.scriptpubkey_address === this.address.address) { | ||||
|         this.utxos.push({ | ||||
|           txid: vin.txid, | ||||
|           vout: vin.vout, | ||||
|           value: vin.prevout.value, | ||||
|           status: { confirmed: true }, // Assuming the input was confirmed
 | ||||
|         }); | ||||
|         utxosChanged = true; | ||||
|       } | ||||
|     } | ||||
|     for (const [index, vout] of transaction.vout.entries()) { | ||||
|       if (vout.scriptpubkey_address === this.address.address) { | ||||
|         const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); | ||||
|         if (utxoIndex !== -1) { | ||||
|           this.utxos.splice(utxoIndex, 1); | ||||
|     if (this.utxos != null) { | ||||
|       let utxosChanged = false; | ||||
|       for (const vin of transaction.vin) { | ||||
|         if (vin.prevout?.scriptpubkey_address === this.address.address) { | ||||
|           this.utxos.push({ | ||||
|             txid: vin.txid, | ||||
|             vout: vin.vout, | ||||
|             value: vin.prevout.value, | ||||
|             status: { confirmed: true }, // Assuming the input was confirmed
 | ||||
|           }); | ||||
|           utxosChanged = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (utxosChanged) { | ||||
|       this.utxos = this.utxos.slice(); | ||||
|       for (const [index, vout] of transaction.vout.entries()) { | ||||
|         if (vout.scriptpubkey_address === this.address.address) { | ||||
|           const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); | ||||
|           if (utxoIndex !== -1) { | ||||
|             this.utxos.splice(utxoIndex, 1); | ||||
|             utxosChanged = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (utxosChanged) { | ||||
|         this.utxos = this.utxos.slice(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| @ -415,27 +419,29 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   confirmTransaction(transaction: Transaction): void { | ||||
|     // update utxos in-place
 | ||||
|     let utxosChanged = false; | ||||
|     for (const vin of transaction.vin) { | ||||
|       if (vin.prevout?.scriptpubkey_address === this.address.address) { | ||||
|         const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); | ||||
|         if (utxoIndex !== -1) { | ||||
|           this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); | ||||
|           utxosChanged = true; | ||||
|     if (this.utxos != null) { | ||||
|       let utxosChanged = false; | ||||
|       for (const vin of transaction.vin) { | ||||
|         if (vin.prevout?.scriptpubkey_address === this.address.address) { | ||||
|           const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); | ||||
|           if (utxoIndex !== -1) { | ||||
|             this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); | ||||
|             utxosChanged = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     for (const [index, vout] of transaction.vout.entries()) { | ||||
|       if (vout.scriptpubkey_address === this.address.address) { | ||||
|         const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); | ||||
|         if (utxoIndex !== -1) { | ||||
|           this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); | ||||
|           utxosChanged = true; | ||||
|       for (const [index, vout] of transaction.vout.entries()) { | ||||
|         if (vout.scriptpubkey_address === this.address.address) { | ||||
|           const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); | ||||
|           if (utxoIndex !== -1) { | ||||
|             this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); | ||||
|             utxosChanged = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (utxosChanged) { | ||||
|       this.utxos = this.utxos.slice(); | ||||
|       if (utxosChanged) { | ||||
|         this.utxos = this.utxos.slice(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -30,7 +30,7 @@ | ||||
|       @if (digitsInfo === '1.8-8') { | ||||
|         ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} | ||||
|       } @else {  | ||||
|         ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }} | ||||
|         ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : (satoshis < 1000 && satoshis > -1000 ? 0 : 1) : undefined : true }} | ||||
|       } | ||||
|       <span class="symbol"> | ||||
|         <ng-container *ngTemplateOutlet="prefix"></ng-container>sats | ||||
|  | ||||
| @ -66,10 +66,10 @@ | ||||
|                     [class.badge-success]="blockAudit?.matchRate >= 99" | ||||
|                     [class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99" | ||||
|                     [class.badge-danger]="blockAudit?.matchRate < 75" | ||||
|                     *ngIf="blockAudit?.matchRate != null; else nullHealth" | ||||
|                     *ngIf="blockAudit?.matchRate != null && blockAudit?.id === block.id; else nullHealth" | ||||
|                   >{{ blockAudit?.matchRate }}%</span> | ||||
|                   <ng-template #nullHealth> | ||||
|                     <ng-container *ngIf="!isLoadingOverview; else loadingHealth"> | ||||
|                     <ng-container *ngIf="!isLoadingOverview && blockAudit?.id === block.id; else loadingHealth"> | ||||
|                       <span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span> | ||||
|                     </ng-container> | ||||
|                   </ng-template> | ||||
|  | ||||
| @ -4,8 +4,8 @@ | ||||
|   <div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;"> | ||||
|     <h1 i18n="master-page.blocks">Blocks</h1> | ||||
|     <app-svg-images name="blocks-2-3" style="width: 275px; max-width: 90%; margin-top: -10px"></app-svg-images> | ||||
|     <div *ngIf="!widget && isLoading" class="spinner-border" role="status"></div> | ||||
|   </div> | ||||
|   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| .spinner-border { | ||||
|   height: 25px; | ||||
|   width: 25px; | ||||
|   margin-top: 13px; | ||||
|   margin-top: -10px; | ||||
|   margin-left: -13px; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
| 
 | ||||
| .container-xl { | ||||
|  | ||||
| @ -12,9 +12,15 @@ | ||||
|         <span class="badge mr-1 badge-og" *ngIf="user.ogRank"> | ||||
|           OG #{{ user.ogRank }} | ||||
|         </span> | ||||
|         <span class="badge mr-1 badge-default" [class]="'badge-' + user.subscription_tag" *ngIf="user.subscription_tag !== 'free'"> | ||||
|           {{ user.subscription_tag.toUpperCase() }} | ||||
|         </span> | ||||
|         @if (user.subscription_tag !== 'free') { | ||||
|           <span class="badge mr-1 badge-default" [class]="'badge-' + user.subscription_tag"> | ||||
|             {{ user.subscription_tag.toUpperCase() }} | ||||
|           </span> | ||||
|         } @else if (user.type === 'mining_pool') { | ||||
|           <span class="badge mr-1 badge-default" [class]="'badge-mining-pool'"> | ||||
|             MINING POOL | ||||
|           </span> | ||||
|         } | ||||
|       </span> | ||||
|       <a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0  menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')"> | ||||
|         <fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon> | ||||
|  | ||||
							
								
								
									
										65
									
								
								frontend/src/app/components/ord-data/ord-data.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								frontend/src/app/components/ord-data/ord-data.component.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| @if (minted) { | ||||
|   <ng-container i18n="ord.mint-n-runes"> | ||||
|     <span>Mint</span> | ||||
|     <span class="amount"> {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} </span> | ||||
|     <ng-container *ngTemplateOutlet="runeName; context: { $implicit: runestone.mint.toString() }"></ng-container> | ||||
|   </ng-container> | ||||
| } | ||||
| @if (runestone?.etching?.supply) { | ||||
|   @if (runestone?.etching.premine > 0) { | ||||
|     <ng-container i18n="ord.premine-n-runes"> | ||||
|       <span>Premine</span> | ||||
|       <span class="amount"> {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} </span> | ||||
|       {{ runestone.etching.symbol }} | ||||
|       <span class="name">{{ runestone.etching.spacedName }}</span> | ||||
|       <span> ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply)</span> | ||||
|     </ng-container> | ||||
|   } @else { | ||||
|     <ng-container i18n="ord.etch-rune"> | ||||
|       <span>Etching of</span> | ||||
|       {{ runestone.etching.symbol }} | ||||
|       <span class="name">{{ runestone.etching.spacedName }}</span> | ||||
|     </ng-container> | ||||
|   } | ||||
| } | ||||
| @if (transferredRunes?.length && type === 'vout') { | ||||
|   <div *ngFor="let rune of transferredRunes"> | ||||
|     <ng-container i18n="ord.transfer-rune"> | ||||
|       <span>Transfer</span> | ||||
|       <ng-container *ngTemplateOutlet="runeName; context: { $implicit: rune.key }"></ng-container> | ||||
|     </ng-container> | ||||
|   </div> | ||||
| } | ||||
| 
 | ||||
| @if (inscriptions?.length && type === 'vin') { | ||||
|   <div *ngFor="let contentType of inscriptionsData | keyvalue"> | ||||
|     <div> | ||||
|       @if (contentType.key !== 'undefined') { | ||||
|         <span class="badge badge-ord mr-1">{{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }}</span> | ||||
|       } @else { | ||||
|         <span class="badge badge-ord mr-1" i18n="unknown">Unknown</span> | ||||
|       } | ||||
|       <span class="badge badge-ord" *ngIf="contentType.value.totalSize > 0">{{ contentType.value.totalSize | bytes:2:'B':undefined:true }}</span> | ||||
|       <a *ngIf="contentType.value.delegate" [routerLink]="['/tx' | relativeUrl, contentType.value.delegate]"> | ||||
|         <span i18n="ord.source-inscription">Source inscription</span> | ||||
|       </a> | ||||
|     </div> | ||||
|     <pre *ngIf="contentType.value.json" class="name" style="white-space: pre-wrap; word-break: break-word;">{{ contentType.value.json | json }}</pre> | ||||
|     <pre *ngIf="contentType.value.text" class="name" style="white-space: pre-wrap; word-break: break-word;">{{ contentType.value.text }}</pre> | ||||
|     </div> | ||||
| } | ||||
| 
 | ||||
| @if (!runestone && type === 'vout') { | ||||
|   <div class="skeleton-loader" style="width: 50%;"></div> | ||||
| } | ||||
| 
 | ||||
| @if ((runestone && !minted && !runestone.etching?.supply && !transferredRunes?.length && type === 'vout') || (!inscriptions?.length && type === 'vin')) { | ||||
|   <i i18n="error.decoding-data">Error decoding data</i> | ||||
| } | ||||
| 
 | ||||
| <ng-template #runeName let-id> | ||||
|   {{ runeInfo[id]?.etching.symbol || '' }} | ||||
|   <a [routerLink]="id !== '1:0' ? ['/tx' | relativeUrl, runeInfo[id]?.txid] : null" [class.rune-link]="id !== '1:0'" [class.disabled]="id === '1:0'"> | ||||
|     <span class="name">{{ runeInfo[id]?.etching.spacedName }}</span> | ||||
|   </a> | ||||
| </ng-template> | ||||
							
								
								
									
										35
									
								
								frontend/src/app/components/ord-data/ord-data.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/src/app/components/ord-data/ord-data.component.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| .amount { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| a.rune-link { | ||||
| 	color: inherit; | ||||
| 	&:hover { | ||||
| 		text-decoration: underline; | ||||
| 		text-decoration-color: var(--transparent-fg);	 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| a.disabled { | ||||
| 	text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .name { | ||||
| 	color: var(--transparent-fg); | ||||
| 	font-weight: 700; | ||||
| } | ||||
| 
 | ||||
| .badge-ord { | ||||
| 	background-color: var(--grey); | ||||
| 	position: relative; | ||||
| 	top: -2px; | ||||
| 	font-size: 81%; | ||||
| 	&.primary { | ||||
| 		background-color: var(--primary); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pre { | ||||
| 	margin-top: 5px; | ||||
| 	max-height: 200px; | ||||
| } | ||||
							
								
								
									
										87
									
								
								frontend/src/app/components/ord-data/ord-data.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								frontend/src/app/components/ord-data/ord-data.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Runestone, Etching } from '../../shared/ord/rune.utils'; | ||||
| import { Inscription } from '../../shared/ord/inscription.utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-ord-data', | ||||
|   templateUrl: './ord-data.component.html', | ||||
|   styleUrls: ['./ord-data.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class OrdDataComponent implements OnChanges { | ||||
|   @Input() inscriptions: Inscription[]; | ||||
|   @Input() runestone: Runestone; | ||||
|   @Input() runeInfo: { [id: string]: { etching: Etching; txid: string } }; | ||||
|   @Input() type: 'vin' | 'vout'; | ||||
| 
 | ||||
|   toNumber = (value: bigint): number => Number(value); | ||||
| 
 | ||||
|   // Inscriptions
 | ||||
|   inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } }; | ||||
|   // Rune mints
 | ||||
|   minted: number; | ||||
|   // Rune transfers
 | ||||
|   transferredRunes: { key: string; etching: Etching; txid: string }[] = []; | ||||
| 
 | ||||
|   constructor() { } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.runestone && this.runestone) { | ||||
|       if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) { | ||||
|         const mint = this.runestone.mint.toString(); | ||||
|         const terms = this.runeInfo[mint].etching.terms; | ||||
|         const amount = terms?.amount; | ||||
|         const divisibility = this.runeInfo[mint].etching.divisibility; | ||||
|         if (amount) { | ||||
|           this.minted = this.getAmount(amount, divisibility); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.runestone.edicts.forEach(edict => { | ||||
|         if (this.runeInfo[edict.id.toString()]) { | ||||
|           this.transferredRunes.push({ key: edict.id.toString(), ...this.runeInfo[edict.id.toString()] }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (changes.inscriptions && this.inscriptions) { | ||||
| 
 | ||||
|       if (this.inscriptions?.length) { | ||||
|         this.inscriptionsData = {}; | ||||
|         this.inscriptions.forEach((inscription) => { | ||||
|           // General: count, total size, delegate
 | ||||
|           const key = inscription.content_type_str || 'undefined'; | ||||
|           if (!this.inscriptionsData[key]) { | ||||
|             this.inscriptionsData[key] = { count: 0, totalSize: 0 }; | ||||
|           } | ||||
|           this.inscriptionsData[key].count++; | ||||
|           this.inscriptionsData[key].totalSize += inscription.body_length; | ||||
|           if (inscription.delegate_txid && !this.inscriptionsData[key].delegate) { | ||||
|             this.inscriptionsData[key].delegate = inscription.delegate_txid; | ||||
|           } | ||||
| 
 | ||||
|           // Text / JSON data
 | ||||
|           if ((key.includes('text') || key.includes('json')) && !inscription.is_cropped && !this.inscriptionsData[key].text && !this.inscriptionsData[key].json) { | ||||
|             const decoder = new TextDecoder('utf-8'); | ||||
|             const text = decoder.decode(inscription.body); | ||||
|             try { | ||||
|               this.inscriptionsData[key].json = JSON.parse(text); | ||||
|               if (this.inscriptionsData[key].json['p']) { | ||||
|                 this.inscriptionsData[key].tag = this.inscriptionsData[key].json['p'].toUpperCase(); | ||||
|               } | ||||
|             } catch (e) { | ||||
|               this.inscriptionsData[key].text = text; | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getAmount(amount: bigint, divisibility: number): number { | ||||
|     const divisor = BigInt(10) ** BigInt(divisibility); | ||||
|     const result = amount / divisor; | ||||
| 
 | ||||
|     return result <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(result) : Number.MAX_SAFE_INTEGER; | ||||
|   } | ||||
| } | ||||
| @ -81,7 +81,8 @@ | ||||
|                     </ng-container> | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 <td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000}"> | ||||
|                 <td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000 || vin.isInscription}"> | ||||
|                   <button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button> | ||||
|                   <ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> | ||||
|                     <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound"> | ||||
|                       <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vin.prevout }"></ng-container> | ||||
| @ -96,6 +97,15 @@ | ||||
|                   </ng-template> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr *ngIf="showOrdData[tx.txid + '-vin-' + vindex]?.show" [ngClass]="{ | ||||
|                 'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex, | ||||
|                 'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address)) | ||||
|               }"> | ||||
|                 <td></td> | ||||
|                 <td colspan="2"> | ||||
|                   <app-ord-data [inscriptions]="showOrdData[tx.txid + '-vin-' + vindex]['inscriptions']" [type]="'vin'"></app-ord-data> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr *ngIf="(showDetails$ | async) === true"> | ||||
|                 <td colspan="3" class="details-container" > | ||||
|                   <table class="table table-striped table-fixed table-borderless details-table mb-3"> | ||||
| @ -236,7 +246,12 @@ | ||||
|                     </ng-template> | ||||
|                     <ng-template #defaultscriptpubkey_type> | ||||
|                       <ng-template [ngIf]="vout.scriptpubkey_type === 'op_return'" [ngIfElse]="otherPubkeyType"> | ||||
|                         OP_RETURN <a placement="bottom" [ngbTooltip]="vout.scriptpubkey_asm | hex2ascii"><span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="badge badge-secondary scriptmessage">{{ vout.scriptpubkey_asm | hex2ascii }}</span></a> | ||||
|                         OP_RETURN  | ||||
|                         @if (vout.isRunestone) { | ||||
|                           <button (click)="toggleOrdData(tx.txid, 'vout', vindex)" type="button" class="btn btn-sm badge badge-ord">Runestone</button> | ||||
|                         } @else { | ||||
|                           <a placement="bottom" [ngbTooltip]="vout.scriptpubkey_asm | hex2ascii"><span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="badge badge-secondary scriptmessage">{{ vout.scriptpubkey_asm | hex2ascii }}</span></a> | ||||
|                         } | ||||
|                       </ng-template> | ||||
|                       <ng-template #otherPubkeyType>{{ vout.scriptpubkey_type | scriptpubkeyType }}</ng-template> | ||||
|                     </ng-template> | ||||
| @ -276,6 +291,15 @@ | ||||
|                   </ng-template> | ||||
|                 </td> | ||||
|               </tr> | ||||
| 
 | ||||
|               <tr *ngIf="showOrdData[tx.txid + '-vout-' + vindex]?.show" [ngClass]="{ | ||||
|                 'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex, | ||||
|                 'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address)) | ||||
|               }"> | ||||
|                 <td colspan="3"> | ||||
|                     <app-ord-data [runestone]="showOrdData[tx.txid + '-vout-' + vindex]['runestone']" [runeInfo]="showOrdData[tx.txid + '-vout-' + vindex]['runeInfo']" [type]="'vout'"></app-ord-data> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr *ngIf="(showDetails$ | async) === true"> | ||||
|                 <td colspan="3" class=" details-container" > | ||||
|                   <table class="table table-striped table-borderless details-table mb-3"> | ||||
|  | ||||
| @ -175,4 +175,15 @@ h2 { | ||||
| 	.witness-item { | ||||
| 		overflow: hidden;	 | ||||
| 	} | ||||
| } | ||||
| } | ||||
| 
 | ||||
| .badge-ord { | ||||
| 	background-color: var(--grey); | ||||
| 	position: relative; | ||||
| 	top: -2px; | ||||
| 	font-size: 81%; | ||||
| 	border: 0; | ||||
| 	&.primary { | ||||
| 		background-color: var(--primary); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -6,11 +6,14 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| import { environment } from '../../../environments/environment'; | ||||
| import { AssetsService } from '../../services/assets.service'; | ||||
| import { filter, map, tap, switchMap, shareReplay, catchError } from 'rxjs/operators'; | ||||
| import { filter, map, tap, switchMap, catchError } from 'rxjs/operators'; | ||||
| import { BlockExtended } from '../../interfaces/node-api.interface'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { PriceService } from '../../services/price.service'; | ||||
| import { StorageService } from '../../services/storage.service'; | ||||
| import { OrdApiService } from '../../services/ord-api.service'; | ||||
| import { Inscription } from '../../shared/ord/inscription.utils'; | ||||
| import { Etching, Runestone } from '../../shared/ord/rune.utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-transactions-list', | ||||
| @ -50,12 +53,14 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|   outputRowLimit: number = 12; | ||||
|   showFullScript: { [vinIndex: number]: boolean } = {}; | ||||
|   showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {}; | ||||
|   showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {}; | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private apiService: ApiService, | ||||
|     private ordApiService: OrdApiService, | ||||
|     private assetsService: AssetsService, | ||||
|     private ref: ChangeDetectorRef, | ||||
|     private priceService: PriceService, | ||||
| @ -239,6 +244,24 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|             tap((price) => tx['price'] = price), | ||||
|           ).subscribe(); | ||||
|         } | ||||
| 
 | ||||
|         // Check for ord data fingerprints in inputs and outputs
 | ||||
|         if (this.stateService.network !== 'liquid' && this.stateService.network !== 'liquidtestnet') { | ||||
|           for (let i = 0; i < tx.vin.length; i++) { | ||||
|             if (tx.vin[i].prevout?.scriptpubkey_type === 'v1_p2tr' && tx.vin[i].witness?.length) { | ||||
|               const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50'); | ||||
|               if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) { | ||||
|                 tx.vin[i].isInscription = true; | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           for (let i = 0; i < tx.vout.length; i++) { | ||||
|             if (tx.vout[i]?.scriptpubkey?.startsWith('6a5d')) { | ||||
|               tx.vout[i].isRunestone = true; | ||||
|               break; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if (this.blockTime && this.transactions?.length && this.currency) { | ||||
| @ -372,6 +395,40 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|     this.showFullWitness[vinIndex][witnessIndex] = !this.showFullWitness[vinIndex][witnessIndex]; | ||||
|   } | ||||
| 
 | ||||
|   toggleOrdData(txid: string, type: 'vin' | 'vout', index: number) { | ||||
|     const tx = this.transactions.find((tx) => tx.txid === txid); | ||||
|     if (!tx) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const key = tx.txid + '-' + type + '-' + index; | ||||
|     this.showOrdData[key] = this.showOrdData[key] || { show: false }; | ||||
| 
 | ||||
|     if (type === 'vin') { | ||||
| 
 | ||||
|       if (!this.showOrdData[key].inscriptions) { | ||||
|         const hasAnnex = tx.vin[index].witness?.[tx.vin[index].witness.length - 1].startsWith('50'); | ||||
|         this.showOrdData[key].inscriptions = this.ordApiService.decodeInscriptions(tx.vin[index].witness[tx.vin[index].witness.length - (hasAnnex ? 3 : 2)]); | ||||
|       } | ||||
|       this.showOrdData[key].show = !this.showOrdData[key].show; | ||||
| 
 | ||||
|     } else if (type === 'vout') { | ||||
| 
 | ||||
|       if (!this.showOrdData[key].runestone) { | ||||
|         this.ordApiService.decodeRunestone$(tx).pipe( | ||||
|           tap((runestone) => { | ||||
|             if (runestone) { | ||||
|               Object.assign(this.showOrdData[key], runestone); | ||||
|               this.ref.markForCheck(); | ||||
|             } | ||||
|           }), | ||||
|         ).subscribe(); | ||||
|       } | ||||
|       this.showOrdData[key].show = !this.showOrdData[key].show; | ||||
| 
 | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.outspendsSubscription.unsubscribe(); | ||||
|     this.currencyChangeSubscription?.unsubscribe(); | ||||
|  | ||||
| @ -9163,11 +9163,13 @@ export const restApiDocsData = [ | ||||
|       Filters can be applied:<ul> | ||||
|       <li><code>status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code></li> | ||||
|       <li><code>timeframe</code>: <code>24h</code>, <code>3d</code>, <code>1w</code>, <code>1m</code>, <code>3m</code>, <code>6m</code>, <code>1y</code>, <code>2y</code>, <code>3y</code>, <code>4y</code>, <code>all</code></li> | ||||
|       <li><code>poolUniqueId</code>: any id from <a target="_blank" href="https://github.com/mempool/mining-pools/blob/master/pools-v2.json">https://github.com/mempool/mining-pools/blob/master/pools-v2.json</a>. <i>Note: This will return all acceleration requests accepted by the pool but the the listed transactions may have been mined by another pool.</i>
 | ||||
|       <li><code>minedByPoolUniqueId</code>: any id from <a target="_blank" href="https://github.com/mempool/mining-pools/blob/master/pools-v2.json">pools-v2.json</a> | ||||
|       <li><code>blockHash</code>: a block hash</a> | ||||
|       <li><code>blockHeight</code>: a block height</a> | ||||
|       <li><code>page</code>: the requested page number if using pagination <i>(min: 1)</i></a> | ||||
|       <li><code>pageLength</code>: the page lenght if using pagination <i>(min: 1, max: 50)</i></a> | ||||
|       <li><code>from</code>: unix timestamp (<i>overrides <code>timeframe</code></i>)</a> | ||||
|       <li><code>to</code>: unix timestamp (<i>overrides <code>timeframe</code></i>)</a> | ||||
|       </ul></p>` | ||||
|     }, | ||||
|     urlString: "/v1/services/accelerator/accelerations/history", | ||||
| @ -9187,21 +9189,22 @@ export const restApiDocsData = [ | ||||
|           headers: '', | ||||
|           response: `[
 | ||||
|   { | ||||
|     "txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d", | ||||
|     "status": "completed", | ||||
|     "added": 1707421053, | ||||
|     "lastUpdated": 1719134667, | ||||
|     "effectiveFee": 146, | ||||
|     "effectiveVsize": 141, | ||||
|     "feeDelta": 14000, | ||||
|     "blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003", | ||||
|     "blockHeight": 829559, | ||||
|     "bidBoost": 3239, | ||||
|     "boostVersion": "v1", | ||||
|     "txid": "f829900985aad885c13fb90555d27514b05a338202c7ef5d694f4813ad474487", | ||||
|     "status": "completed_provisional", | ||||
|     "added": 1728111527, | ||||
|     "lastUpdated": 1728112113, | ||||
|     "effectiveFee": 1385, | ||||
|     "effectiveVsize": 276, | ||||
|     "feeDelta": 3000, | ||||
|     "blockHash": "00000000000000000000cde89e34036ece454ca2d07ddd7f71ab46307ca87423", | ||||
|     "blockHeight": 864248, | ||||
|     "bidBoost": 65, | ||||
|     "boostVersion": "v2", | ||||
|     "pools": [ | ||||
|       111 | ||||
|       111, | ||||
|       115, | ||||
|     ], | ||||
|     "minedByPoolUniqueId": 111 | ||||
|     "minedByPoolUniqueId": 115 | ||||
|   } | ||||
| ]`,
 | ||||
|         }, | ||||
|  | ||||
| @ -74,6 +74,8 @@ export interface Vin { | ||||
|   issuance?: Issuance; | ||||
|   // Custom
 | ||||
|   lazy?: boolean; | ||||
|   // Ord
 | ||||
|   isInscription?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface Issuance { | ||||
| @ -98,6 +100,8 @@ export interface Vout { | ||||
|   valuecommitment?: number; | ||||
|   asset?: string; | ||||
|   pegout?: Pegout; | ||||
|   // Ord
 | ||||
|   isRunestone?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface Pegout { | ||||
|  | ||||
| @ -107,6 +107,10 @@ export class ElectrsApiService { | ||||
|     return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'}); | ||||
|   } | ||||
| 
 | ||||
|   getBlockTxId$(hash: string, index: number): Observable<string> { | ||||
|     return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txid/' + index, { responseType: 'text' }); | ||||
|   } | ||||
| 
 | ||||
|   getAddress$(address: string): Observable<Address> { | ||||
|     return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); | ||||
|   } | ||||
|  | ||||
							
								
								
									
										100
									
								
								frontend/src/app/services/ord-api.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								frontend/src/app/services/ord-api.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; | ||||
| import { Inscription } from '../shared/ord/inscription.utils'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils'; | ||||
| import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '../shared/ord/rune.utils'; | ||||
| import { ElectrsApiService } from './electrs-api.service'; | ||||
| 
 | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class OrdApiService { | ||||
| 
 | ||||
|   constructor( | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|   ) { } | ||||
| 
 | ||||
|   decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { | ||||
|     const runestone = decipherRunestone(tx); | ||||
|     const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; | ||||
| 
 | ||||
|     if (runestone) { | ||||
|       const runesToFetch: Set<string> = new Set(); | ||||
| 
 | ||||
|       if (runestone.mint) { | ||||
|         runesToFetch.add(runestone.mint.toString()); | ||||
|       } | ||||
| 
 | ||||
|       if (runestone.edicts.length) { | ||||
|         runestone.edicts.forEach(edict => { | ||||
|           runesToFetch.add(edict.id.toString()); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (runesToFetch.size) { | ||||
|         const runeEtchingObservables = Array.from(runesToFetch).map(runeId => this.getEtchingFromRuneId$(runeId)); | ||||
| 
 | ||||
|         return forkJoin(runeEtchingObservables).pipe( | ||||
|           map((etchings) => { | ||||
|             etchings.forEach((el) => { | ||||
|               if (el) { | ||||
|                 runeInfo[el.runeId] = { etching: el.etching, txid: el.txid }; | ||||
|               } | ||||
|             }); | ||||
|             return { runestone: runestone, runeInfo }; | ||||
|           }) | ||||
|         ); | ||||
|       } | ||||
|       return of({ runestone: runestone, runeInfo }); | ||||
|     } else { | ||||
|       return of({ runestone: null, runeInfo: {} }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Get etching from runeId by looking up the transaction that etched the rune
 | ||||
|   getEtchingFromRuneId$(runeId: string): Observable<{ runeId: string; etching: Etching; txid: string; }> { | ||||
|     if (runeId === '1:0') { | ||||
|       return of({ runeId, etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }); | ||||
|     } else { | ||||
|       const [blockNumber, txIndex] = runeId.split(':'); | ||||
|       return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockNumber)).pipe( | ||||
|         switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), | ||||
|         switchMap(txId => this.electrsApiService.getTransaction$(txId)), | ||||
|         switchMap(tx => { | ||||
|           const runestone = decipherRunestone(tx); | ||||
|           if (runestone) { | ||||
|             const etching = runestone.etching; | ||||
|             if (etching) { | ||||
|               return of({ runeId, etching, txid: tx.txid }); | ||||
|             } | ||||
|           } | ||||
|           return of(null); | ||||
|         }), | ||||
|         catchError(() => of(null)) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   decodeInscriptions(witness: string): Inscription[] | null { | ||||
| 
 | ||||
|     const inscriptions: Inscription[] = []; | ||||
|     const raw = hexToBytes(witness); | ||||
|     let startPosition = 0; | ||||
| 
 | ||||
|     while (true) { | ||||
|       const pointer = getNextInscriptionMark(raw, startPosition); | ||||
|       if (pointer === -1) break; | ||||
| 
 | ||||
|       const inscription = extractInscriptionData(raw, pointer); | ||||
|       if (inscription) { | ||||
|         inscriptions.push(inscription); | ||||
|       } | ||||
| 
 | ||||
|       startPosition = pointer; | ||||
|     } | ||||
| 
 | ||||
|     return inscriptions; | ||||
|   } | ||||
| } | ||||
| @ -9,13 +9,12 @@ import { IBackendInfo } from '../interfaces/websocket.interface'; | ||||
| import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; | ||||
| import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; | ||||
| 
 | ||||
| export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; | ||||
| export interface IUser { | ||||
|   username: string; | ||||
|   email: string | null; | ||||
|   passwordIsSet: boolean; | ||||
|   snsId: string; | ||||
|   type: ProductType; | ||||
|   type: 'enterprise' | 'community' | 'mining_pool'; | ||||
|   subscription_tag: string; | ||||
|   status: 'pending' | 'verified' | 'disabled'; | ||||
|   features: string | null; | ||||
|  | ||||
							
								
								
									
										409
									
								
								frontend/src/app/shared/ord/inscription.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										409
									
								
								frontend/src/app/shared/ord/inscription.utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,409 @@ | ||||
| // Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src
 | ||||
| // Utils functions to decode ord inscriptions
 | ||||
| 
 | ||||
| export const OP_FALSE = 0x00; | ||||
| export const OP_IF = 0x63; | ||||
| export const OP_0 = 0x00; | ||||
| 
 | ||||
| export const OP_PUSHBYTES_3 = 0x03; //  3 -- not an actual opcode, but used in documentation --> pushes the next 3 bytes onto the stack.
 | ||||
| export const OP_PUSHDATA1 = 0x4c;   // 76 -- The next byte contains the number of bytes to be pushed onto the stack.
 | ||||
| export const OP_PUSHDATA2 = 0x4d;   // 77 -- The next two bytes contain the number of bytes to be pushed onto the stack in little endian order.
 | ||||
| export const OP_PUSHDATA4 = 0x4e;   // 78 -- The next four bytes contain the number of bytes to be pushed onto the stack in little endian order.
 | ||||
| export const OP_ENDIF = 0x68;       // 104 -- Ends an if/else block.
 | ||||
| 
 | ||||
| export const OP_1NEGATE = 0x4f;            // 79 -- The number -1 is pushed onto the stack.
 | ||||
| export const OP_RESERVED = 0x50;           // 80 -- Transaction is invalid unless occuring in an unexecuted OP_IF branch
 | ||||
| export const OP_PUSHNUM_1 = 0x51;          // 81 -- also known as OP_1
 | ||||
| export const OP_PUSHNUM_2 = 0x52;          // 82 -- also known as OP_2
 | ||||
| export const OP_PUSHNUM_3 = 0x53;          // 83 -- also known as OP_3
 | ||||
| export const OP_PUSHNUM_4 = 0x54;          // 84 -- also known as OP_4
 | ||||
| export const OP_PUSHNUM_5 = 0x55;          // 85 -- also known as OP_5
 | ||||
| export const OP_PUSHNUM_6 = 0x56;          // 86 -- also known as OP_6
 | ||||
| export const OP_PUSHNUM_7 = 0x57;          // 87 -- also known as OP_7
 | ||||
| export const OP_PUSHNUM_8 = 0x58;          // 88 -- also known as OP_8
 | ||||
| export const OP_PUSHNUM_9 = 0x59;          // 89 -- also known as OP_9
 | ||||
| export const OP_PUSHNUM_10 = 0x5a;         // 90 -- also known as OP_10
 | ||||
| export const OP_PUSHNUM_11 = 0x5b;         // 91 -- also known as OP_11
 | ||||
| export const OP_PUSHNUM_12 = 0x5c;         // 92 -- also known as OP_12
 | ||||
| export const OP_PUSHNUM_13 = 0x5d;         // 93 -- also known as OP_13
 | ||||
| export const OP_PUSHNUM_14 = 0x5e;         // 94 -- also known as OP_14
 | ||||
| export const OP_PUSHNUM_15 = 0x5f;         // 95 -- also known as OP_15
 | ||||
| export const OP_PUSHNUM_16 = 0x60;         // 96 -- also known as OP_16
 | ||||
| 
 | ||||
| export const OP_RETURN = 0x6a;             // 106 -- a standard way of attaching extra data to transactions is to add a zero-value output with a scriptPubKey consisting of OP_RETURN followed by data
 | ||||
| 
 | ||||
| //////////////////////////// Helper ///////////////////////////////
 | ||||
| 
 | ||||
| /** | ||||
|  * Inscriptions may include fields before an optional body. Each field consists of two data pushes, a tag and a value. | ||||
|  * Currently, there are six defined fields: | ||||
|  */ | ||||
| export const knownFields = { | ||||
|   // content_type, with a tag of 1, whose value is the MIME type of the body.
 | ||||
|   content_type: 0x01, | ||||
| 
 | ||||
|   // pointer, with a tag of 2, see pointer docs: https://docs.ordinals.com/inscriptions/pointer.html
 | ||||
|   pointer: 0x02, | ||||
| 
 | ||||
|   // parent, with a tag of 3, see provenance docs: https://docs.ordinals.com/inscriptions/provenance.html
 | ||||
|   parent: 0x03, | ||||
| 
 | ||||
|   // metadata, with a tag of 5, see metadata docs: https://docs.ordinals.com/inscriptions/metadata.html
 | ||||
|   metadata: 0x05, | ||||
| 
 | ||||
|   // metaprotocol, with a tag of 7, whose value is the metaprotocol identifier.
 | ||||
|   metaprotocol: 0x07, | ||||
| 
 | ||||
|   // content_encoding, with a tag of 9, whose value is the encoding of the body.
 | ||||
|   content_encoding: 0x09, | ||||
| 
 | ||||
|   // delegate, with a tag of 11, see delegate docs: https://docs.ordinals.com/inscriptions/delegate.html
 | ||||
|   delegate: 0xb | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Retrieves the value for a given field from an array of field objects. | ||||
|  * It returns the value of the first object where the tag matches the specified field. | ||||
|  * | ||||
|  * @param fields - An array of objects containing tag and value properties. | ||||
|  * @param field - The field number to search for. | ||||
|  * @returns The value associated with the first matching field, or undefined if no match is found. | ||||
|  */ | ||||
| export function getKnownFieldValue(fields: { tag: number; value: Uint8Array }[], field: number): Uint8Array | undefined { | ||||
|   const knownField = fields.find(x => | ||||
|     x.tag === field); | ||||
| 
 | ||||
|   if (knownField === undefined) { | ||||
|     return undefined; | ||||
|   } | ||||
| 
 | ||||
|   return knownField.value; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Retrieves the values for a given field from an array of field objects. | ||||
|  * It returns the values of all objects where the tag matches the specified field. | ||||
|  * | ||||
|  * @param fields - An array of objects containing tag and value properties. | ||||
|  * @param field - The field number to search for. | ||||
|  * @returns An array of Uint8Array values associated with the matching fields. If no matches are found, an empty array is returned. | ||||
|  */ | ||||
| export function getKnownFieldValues(fields: { tag: number; value: Uint8Array }[], field: number): Uint8Array[] { | ||||
|   const knownFields = fields.filter(x => | ||||
|     x.tag === field | ||||
|   ); | ||||
| 
 | ||||
|   return knownFields.map(field => field.value); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Searches for the next position of the ordinal inscription mark (0063036f7264) | ||||
|  * within the raw transaction data, starting from a given position. | ||||
|  * | ||||
|  * This function looks for a specific sequence of 6 bytes that represents the start of an ordinal inscription. | ||||
|  * If the sequence is found, the function returns the index immediately following the inscription mark. | ||||
|  * If the sequence is not found, the function returns -1, indicating no inscription mark was found. | ||||
|  * | ||||
|  * Note: This function uses a simple hardcoded approach based on the fixed length of the inscription mark. | ||||
|  * | ||||
|  * @returns The position immediately after the inscription mark, or -1 if not found. | ||||
|  */ | ||||
| export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): number { | ||||
| 
 | ||||
|   // OP_FALSE
 | ||||
|   // OP_IF
 | ||||
|   // OP_PUSHBYTES_3: This pushes the next 3 bytes onto the stack.
 | ||||
|   // 0x6f, 0x72, 0x64: These bytes translate to the ASCII string "ord"
 | ||||
|   const inscriptionMark = new Uint8Array([OP_FALSE, OP_IF, OP_PUSHBYTES_3, 0x6f, 0x72, 0x64]); | ||||
| 
 | ||||
|   for (let index = startPosition; index <= raw.length - 6; index++) { | ||||
|     if (raw[index] === inscriptionMark[0] && | ||||
|       raw[index + 1] === inscriptionMark[1] && | ||||
|       raw[index + 2] === inscriptionMark[2] && | ||||
|       raw[index + 3] === inscriptionMark[3] && | ||||
|       raw[index + 4] === inscriptionMark[4] && | ||||
|       raw[index + 5] === inscriptionMark[5]) { | ||||
|       return index + 6; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return -1; | ||||
| } | ||||
| 
 | ||||
| /////////////////////////////// Reader ///////////////////////////////
 | ||||
| 
 | ||||
| /** | ||||
|  * Reads a specified number of bytes from a Uint8Array starting from a given pointer. | ||||
|  * | ||||
|  * @param raw - The Uint8Array from which bytes are to be read. | ||||
|  * @param pointer - The position in the array from where to start reading. | ||||
|  * @param n - The number of bytes to read. | ||||
|  * @returns A tuple containing the read bytes as Uint8Array and the updated pointer position. | ||||
|  */ | ||||
| export function readBytes(raw: Uint8Array, pointer: number, n: number): [Uint8Array, number] { | ||||
|   const slice = raw.slice(pointer, pointer + n); | ||||
|   return [slice, pointer + n]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Reads data based on the Bitcoin script push opcode starting from a specified pointer in the raw data. | ||||
|  * Handles different opcodes and direct push (where the opcode itself signifies the number of bytes to push). | ||||
|  * | ||||
|  * @param raw - The raw transaction data as a Uint8Array. | ||||
|  * @param pointer - The current position in the raw data array. | ||||
|  * @returns A tuple containing the read data as Uint8Array and the updated pointer position. | ||||
|  */ | ||||
| export function readPushdata(raw: Uint8Array, pointer: number): [Uint8Array, number] { | ||||
| 
 | ||||
|   let [opcodeSlice, newPointer] = readBytes(raw, pointer, 1); | ||||
|   const opcode = opcodeSlice[0]; | ||||
| 
 | ||||
|   // Handle the special case of OP_0 (0x00) which pushes an empty array (interpreted as zero)
 | ||||
|   // fixes #18
 | ||||
|   if (opcode === OP_0) { | ||||
|     return [new Uint8Array(), newPointer]; | ||||
|   } | ||||
| 
 | ||||
|   // Handle the special case of OP_1NEGATE (-1)
 | ||||
|   if (opcode === OP_1NEGATE) { | ||||
|     // OP_1NEGATE pushes the value -1 onto the stack, represented as 0x81 in Bitcoin Script
 | ||||
|     return [new Uint8Array([0x81]), newPointer]; | ||||
|   } | ||||
| 
 | ||||
|   // Handle minimal push numbers OP_PUSHNUM_1 (0x51) to OP_PUSHNUM_16 (0x60)
 | ||||
|   // which are used to push the values 0x01 (decimal 1) through 0x10 (decimal 16) onto the stack.
 | ||||
|   // To get the value, we can subtract OP_RESERVED (0x50) from the opcode to get the value to be pushed.
 | ||||
|   if (opcode >= OP_PUSHNUM_1 && opcode <= OP_PUSHNUM_16) { | ||||
|     // Convert opcode to corresponding byte value
 | ||||
|     const byteValue = opcode - OP_RESERVED; | ||||
|     return [Uint8Array.from([byteValue]), newPointer]; | ||||
|   } | ||||
| 
 | ||||
|   // Handle direct push of 1 to 75 bytes (OP_PUSHBYTES_1 to OP_PUSHBYTES_75)
 | ||||
|   if (1 <= opcode && opcode <= 75) { | ||||
|     return readBytes(raw, newPointer, opcode); | ||||
|   } | ||||
| 
 | ||||
|   let numBytes: number; | ||||
|   switch (opcode) { | ||||
|     case OP_PUSHDATA1: numBytes = 1; break; | ||||
|     case OP_PUSHDATA2: numBytes = 2; break; | ||||
|     case OP_PUSHDATA4: numBytes = 4; break; | ||||
|     default: | ||||
|       throw new Error(`Invalid push opcode ${opcode} at position ${pointer}`); | ||||
|   } | ||||
| 
 | ||||
|   let [dataSizeArray, nextPointer] = readBytes(raw, newPointer, numBytes); | ||||
|   let dataSize = littleEndianBytesToNumber(dataSizeArray); | ||||
|   return readBytes(raw, nextPointer, dataSize); | ||||
| } | ||||
| 
 | ||||
| //////////////////////////// Conversion ////////////////////////////
 | ||||
| 
 | ||||
| /** | ||||
|  * Converts a Uint8Array containing UTF-8 encoded data to a normal a UTF-16 encoded string. | ||||
|  * | ||||
|  * @param bytes - The Uint8Array containing UTF-8 encoded data. | ||||
|  * @returns The corresponding UTF-16 encoded JavaScript string. | ||||
|  */ | ||||
| export function bytesToUnicodeString(bytes: Uint8Array): string { | ||||
|   const decoder = new TextDecoder('utf-8'); | ||||
|   return decoder.decode(bytes); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Convert a Uint8Array to a string by treating each byte as a character code. | ||||
|  * It avoids interpreting bytes as UTF-8 encoded sequences. | ||||
|  * --> Again: it ignores UTF-8 encoding, which is necessary for binary content! | ||||
|  * | ||||
|  * Note: This method is different from just using `String.fromCharCode(...combinedData)` which can | ||||
|  * cause a "Maximum call stack size exceeded" error for large arrays due to the limitation of | ||||
|  * the spread operator in JavaScript. (previously the parser broke here, because of large content) | ||||
|  * | ||||
|  * @param bytes - The byte array to convert. | ||||
|  * @returns The resulting string where each byte value is treated as a direct character code. | ||||
|  */ | ||||
| export function bytesToBinaryString(bytes: Uint8Array): string { | ||||
|   let resultStr = ''; | ||||
|   for (let i = 0; i < bytes.length; i++) { | ||||
|     resultStr += String.fromCharCode(bytes[i]); | ||||
|   } | ||||
|   return resultStr; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Converts a hexadecimal string to a Uint8Array. | ||||
|  * | ||||
|  * @param hex - A string of hexadecimal characters. | ||||
|  * @returns A Uint8Array representing the hex string. | ||||
|  */ | ||||
| export function hexToBytes(hex: string): Uint8Array { | ||||
|   const bytes = new Uint8Array(hex.length / 2); | ||||
|   for (let i = 0, j = 0; i < hex.length; i += 2, j++) { | ||||
|     bytes[j] = parseInt(hex.slice(i, i + 2), 16); | ||||
|   } | ||||
|   return bytes; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Converts a Uint8Array to a hexadecimal string. | ||||
|  * | ||||
|  * @param bytes - A Uint8Array to convert. | ||||
|  * @returns A string of hexadecimal characters representing the byte array. | ||||
|  */ | ||||
| export function bytesToHex(bytes: Uint8Array): string { | ||||
|   if (!bytes) { | ||||
|     return null; | ||||
|   } | ||||
|   return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Converts a little-endian byte array to a JavaScript number. | ||||
|  * | ||||
|  * This function interprets the provided bytes in little-endian format, where the least significant byte comes first. | ||||
|  * It constructs an integer value representing the number encoded by the bytes. | ||||
|  * | ||||
|  * @param byteArray - An array containing the bytes in little-endian format. | ||||
|  * @returns The number represented by the byte array. | ||||
|  */ | ||||
| export function littleEndianBytesToNumber(byteArray: Uint8Array): number { | ||||
|   let number = 0; | ||||
|   for (let i = 0; i < byteArray.length; i++) { | ||||
|     // Extract each byte from byteArray, shift it to the left by 8 * i bits, and combine it with number.
 | ||||
|     // The shifting accounts for the little-endian format where the least significant byte comes first.
 | ||||
|     number |= byteArray[i] << (8 * i); | ||||
|   } | ||||
|   return number; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Concatenates multiple Uint8Array objects into a single Uint8Array. | ||||
|  * | ||||
|  * @param arrays - An array of Uint8Array objects to concatenate. | ||||
|  * @returns A new Uint8Array containing the concatenated results of the input arrays. | ||||
|  */ | ||||
| export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array { | ||||
|   if (arrays.length === 0) { | ||||
|       return new Uint8Array(); | ||||
|   } | ||||
| 
 | ||||
|   const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); | ||||
|   const result = new Uint8Array(totalLength); | ||||
|   let offset = 0; | ||||
| 
 | ||||
|   for (const array of arrays) { | ||||
|       result.set(array, offset); | ||||
|       offset += array.length; | ||||
|   } | ||||
| 
 | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| ////////////////////////////// Inscription ///////////////////////////
 | ||||
| 
 | ||||
| export interface Inscription { | ||||
|   body?: Uint8Array; | ||||
|   is_cropped?: boolean; | ||||
|   body_length?: number; | ||||
|   content_type?: Uint8Array; | ||||
|   content_type_str?: string; | ||||
|   delegate_txid?: string; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Extracts fields from the raw data until OP_0 is encountered. | ||||
|  * | ||||
|  * @param raw - The raw data to read. | ||||
|  * @param pointer - The current pointer where the reading starts. | ||||
|  * @returns An array of fields and the updated pointer position. | ||||
|  */ | ||||
| export function extractFields(raw: Uint8Array, pointer: number): [{ tag: number; value: Uint8Array }[], number] { | ||||
| 
 | ||||
|   const fields: { tag: number; value: Uint8Array }[] = []; | ||||
|   let newPointer = pointer; | ||||
|   let slice: Uint8Array; | ||||
| 
 | ||||
|   while (newPointer < raw.length && | ||||
|     // normal inscription - content follows now
 | ||||
|     (raw[newPointer] !== OP_0) && | ||||
|     // delegate - inscription has no further content and ends directly here
 | ||||
|     (raw[newPointer] !== OP_ENDIF) | ||||
|   ) { | ||||
| 
 | ||||
|     // tags are encoded by ord as single-byte data pushes, but are accepted by ord as either single-byte pushes, or as OP_NUM data pushes.
 | ||||
|     // tags greater than or equal to 256 should be encoded as little endian integers with trailing zeros omitted.
 | ||||
|     // see: https://github.com/ordinals/ord/issues/2505
 | ||||
|     [slice, newPointer] = readPushdata(raw, newPointer); | ||||
|     const tag = slice.length === 1 ? slice[0] : littleEndianBytesToNumber(slice); | ||||
| 
 | ||||
|     [slice, newPointer] = readPushdata(raw, newPointer); | ||||
|     const value = slice; | ||||
| 
 | ||||
|     fields.push({ tag, value }); | ||||
|   } | ||||
| 
 | ||||
|   return [fields, newPointer]; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Extracts inscription data starting from the current pointer. | ||||
|  * @param raw - The raw data to read. | ||||
|  * @param pointer - The current pointer where the reading starts. | ||||
|  * @returns The parsed inscription or nullx | ||||
|  */ | ||||
| export function extractInscriptionData(raw: Uint8Array, pointer: number): Inscription | null { | ||||
| 
 | ||||
|   try { | ||||
| 
 | ||||
|     let fields: { tag: number; value: Uint8Array }[]; | ||||
|     let newPointer: number; | ||||
|     let slice: Uint8Array; | ||||
| 
 | ||||
|     [fields, newPointer] = extractFields(raw, pointer); | ||||
| 
 | ||||
|     // Now we are at the beginning of the body
 | ||||
|     // (or at the end of the raw data if there's no body)
 | ||||
|     if (newPointer < raw.length && raw[newPointer] === OP_0) { | ||||
|       newPointer++; // Skip OP_0
 | ||||
|     } | ||||
| 
 | ||||
|     // Collect body data until OP_ENDIF
 | ||||
|     const data: Uint8Array[] = []; | ||||
|     while (newPointer < raw.length && raw[newPointer] !== OP_ENDIF) { | ||||
|       [slice, newPointer] = readPushdata(raw, newPointer); | ||||
|       data.push(slice); | ||||
|     } | ||||
| 
 | ||||
|     const combinedLengthOfAllArrays = data.reduce((acc, curr) => acc + curr.length, 0); | ||||
|     let combinedData = new Uint8Array(combinedLengthOfAllArrays); | ||||
| 
 | ||||
|     // Copy all segments from data into combinedData, forming a single contiguous Uint8Array
 | ||||
|     let idx = 0; | ||||
|     for (const segment of data) { | ||||
|       combinedData.set(segment, idx); | ||||
|       idx += segment.length; | ||||
|     } | ||||
| 
 | ||||
|     const contentTypeRaw = getKnownFieldValue(fields, knownFields.content_type); | ||||
|     let contentType: string; | ||||
| 
 | ||||
|     if (!contentTypeRaw) { | ||||
|       contentType = 'undefined'; | ||||
|     } else { | ||||
|       contentType = bytesToUnicodeString(contentTypeRaw); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       content_type_str: contentType, | ||||
|       body: combinedData.slice(0, 100_000), // Limit body to 100 kB for now
 | ||||
|       is_cropped: combinedData.length > 100_000, | ||||
|       body_length: combinedData.length, | ||||
|       delegate_txid: getKnownFieldValue(fields, knownFields.delegate) ? bytesToHex(getKnownFieldValue(fields, knownFields.delegate).reverse()) : null | ||||
|     }; | ||||
| 
 | ||||
|   } catch (ex) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										255
									
								
								frontend/src/app/shared/ord/rune.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								frontend/src/app/shared/ord/rune.utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| 
 | ||||
| export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; | ||||
| 
 | ||||
| export class RuneId { | ||||
|   block: number; | ||||
|   index: number; | ||||
| 
 | ||||
|   constructor(block: number, index: number) { | ||||
|     this.block = block; | ||||
|     this.index = index; | ||||
|   } | ||||
| 
 | ||||
|   toString(): string { | ||||
|     return `${this.block}:${this.index}`; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type Etching = { | ||||
|   divisibility?: number; | ||||
|   premine?: bigint; | ||||
|   symbol?: string; | ||||
|   terms?: { | ||||
|     cap?: bigint; | ||||
|     amount?: bigint; | ||||
|     offset?: { | ||||
|       start?: bigint; | ||||
|       end?: bigint; | ||||
|     }; | ||||
|     height?: { | ||||
|       start?: bigint; | ||||
|       end?: bigint; | ||||
|     }; | ||||
|   }; | ||||
|   turbo?: boolean; | ||||
|   name?: string; | ||||
|   spacedName?: string; | ||||
|   supply?: bigint; | ||||
| }; | ||||
| 
 | ||||
| export type Edict = { | ||||
|   id: RuneId; | ||||
|   amount: bigint; | ||||
|   output: number; | ||||
| }; | ||||
| 
 | ||||
| export type Runestone = { | ||||
|   mint?: RuneId; | ||||
|   pointer?: number; | ||||
|   edicts?: Edict[]; | ||||
|   etching?: Etching; | ||||
| }; | ||||
| 
 | ||||
| type Message = { | ||||
|   fields: Record<number, bigint[]>; | ||||
|   edicts: Edict[]; | ||||
| } | ||||
| 
 | ||||
| export const UNCOMMON_GOODS: Etching = { | ||||
|   divisibility: 0, | ||||
|   premine: 0n, | ||||
|   symbol: '⧉', | ||||
|   terms: { | ||||
|     cap: U128_MAX_BIGINT, | ||||
|     amount: 1n, | ||||
|     offset: { | ||||
|       start: 0n, | ||||
|       end: 0n, | ||||
|     }, | ||||
|     height: { | ||||
|       start: 840000n, | ||||
|       end: 1050000n, | ||||
|     }, | ||||
|   }, | ||||
|   turbo: false, | ||||
|   name: 'UNCOMMONGOODS', | ||||
|   spacedName: 'UNCOMMON•GOODS', | ||||
|   supply: U128_MAX_BIGINT, | ||||
| }; | ||||
| 
 | ||||
| enum Tag { | ||||
|   Body = 0, | ||||
|   Flags = 2, | ||||
|   Rune = 4, | ||||
|   Premine = 6, | ||||
|   Cap = 8, | ||||
|   Amount = 10, | ||||
|   HeightStart = 12, | ||||
|   HeightEnd = 14, | ||||
|   OffsetStart = 16, | ||||
|   OffsetEnd = 18, | ||||
|   Mint = 20, | ||||
|   Pointer = 22, | ||||
|   Cenotaph = 126, | ||||
| 
 | ||||
|   Divisibility = 1, | ||||
|   Spacers = 3, | ||||
|   Symbol = 5, | ||||
|   Nop = 127, | ||||
| } | ||||
| 
 | ||||
| const Flag = { | ||||
|   ETCHING: 1n, | ||||
|   TERMS: 1n << 1n, | ||||
|   TURBO: 1n << 2n, | ||||
|   CENOTAPH: 1n << 127n, | ||||
| }; | ||||
| 
 | ||||
| function hexToBytes(hex: string): Uint8Array { | ||||
|   return new Uint8Array(hex.match(/.{2}/g).map((byte) => parseInt(byte, 16))); | ||||
| } | ||||
| 
 | ||||
| function decodeLEB128(bytes: Uint8Array): bigint[] { | ||||
|   const integers: bigint[] = []; | ||||
|   let index = 0; | ||||
|   while (index < bytes.length) { | ||||
|     let value = BigInt(0); | ||||
|     let shift = 0; | ||||
|     let byte: number; | ||||
|     do { | ||||
|       byte = bytes[index++]; | ||||
|       value |= BigInt(byte & 0x7f) << BigInt(shift); | ||||
|       shift += 7; | ||||
|     } while (byte & 0x80); | ||||
|     integers.push(value); | ||||
|   } | ||||
|   return integers; | ||||
| } | ||||
| 
 | ||||
| function integersToMessage(integers: bigint[]): Message { | ||||
|   const message = { | ||||
|     fields: {}, | ||||
|     edicts: [], | ||||
|   }; | ||||
|   let inBody = false; | ||||
|   while (integers.length) { | ||||
|     if (!inBody) { | ||||
|       // The integers are interpreted as a sequence of tag/value pairs, with duplicate tags appending their value to the field value.
 | ||||
|       const tag: Tag = Number(integers.shift()); | ||||
|       if (tag === Tag.Body) { | ||||
|         inBody = true; | ||||
|       } else { | ||||
|         const value = integers.shift(); | ||||
|         if (message.fields[tag]) { | ||||
|           message.fields[tag].push(value); | ||||
|         } else { | ||||
|           message.fields[tag] = [value]; | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       // If a tag with value zero is encountered, all following integers are interpreted as a series of four-integer edicts, each consisting of a rune ID block height, rune ID transaction index, amount, and output.
 | ||||
|       const height = integers.shift(); | ||||
|       const txIndex = integers.shift(); | ||||
|       const amount = integers.shift(); | ||||
|       const output = integers.shift(); | ||||
|       message.edicts.push({ | ||||
|         id: new RuneId(Number(height), Number(txIndex)), | ||||
|         amount, | ||||
|         output, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|   return message; | ||||
| } | ||||
| 
 | ||||
| function parseRuneName(rune: bigint): string { | ||||
|   let name = ''; | ||||
|   rune += 1n; | ||||
|   while (rune > 0n) { | ||||
|     name = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((rune - 1n) % 26n)] + name; | ||||
|     rune = (rune - 1n) / 26n; | ||||
|   } | ||||
|   return name; | ||||
| } | ||||
| 
 | ||||
| function spaceRuneName(name: string, spacers: bigint): string { | ||||
|   let i = 0; | ||||
|   let spacedName = ''; | ||||
|   while (spacers > 0n || i < name.length) { | ||||
|     spacedName += name[i]; | ||||
|     if (spacers & 1n) { | ||||
|       spacedName += '•'; | ||||
|     } | ||||
|     if (spacers > 0n) { | ||||
|       spacers >>= 1n; | ||||
|     } | ||||
|     i++; | ||||
|   } | ||||
|   return spacedName; | ||||
| } | ||||
| 
 | ||||
| function messageToRunestone(message: Message): Runestone { | ||||
|   let etching: Etching | undefined; | ||||
|   let mint: RuneId | undefined; | ||||
|   let pointer: number | undefined; | ||||
| 
 | ||||
|   const flags = message.fields[Tag.Flags]?.[0] || 0n; | ||||
|   if (flags & Flag.ETCHING) { | ||||
|     const hasTerms = (flags & Flag.TERMS) > 0n; | ||||
|     const isTurbo = (flags & Flag.TURBO) > 0n; | ||||
|     const name = parseRuneName(message.fields[Tag.Rune]?.[0] ?? 0n); | ||||
|     etching = { | ||||
|       divisibility: Number(message.fields[Tag.Divisibility]?.[0] ?? 0n), | ||||
|       premine: message.fields[Tag.Premine]?.[0], | ||||
|       symbol: message.fields[Tag.Symbol]?.[0] ? String.fromCodePoint(Number(message.fields[Tag.Symbol][0])) : '¤', | ||||
|       terms: hasTerms ? { | ||||
|         cap: message.fields[Tag.Cap]?.[0], | ||||
|         amount: message.fields[Tag.Amount]?.[0], | ||||
|         offset: { | ||||
|           start: message.fields[Tag.OffsetStart]?.[0], | ||||
|           end: message.fields[Tag.OffsetEnd]?.[0], | ||||
|         }, | ||||
|         height: { | ||||
|           start: message.fields[Tag.HeightStart]?.[0], | ||||
|           end: message.fields[Tag.HeightEnd]?.[0], | ||||
|         }, | ||||
|       } : undefined, | ||||
|       turbo: isTurbo, | ||||
|       name, | ||||
|       spacedName: spaceRuneName(name, message.fields[Tag.Spacers]?.[0] ?? 0n), | ||||
|     }; | ||||
|     etching.supply = ( | ||||
|       (etching.terms?.cap ?? 0n) * (etching.terms?.amount ?? 0n) | ||||
|     ) + (etching.premine ?? 0n); | ||||
|   } | ||||
|   const mintField = message.fields[Tag.Mint]; | ||||
|   if (mintField) { | ||||
|     mint = new RuneId(Number(mintField[0]), Number(mintField[1])); | ||||
|   } | ||||
|   const pointerField = message.fields[Tag.Pointer]; | ||||
|   if (pointerField) { | ||||
|     pointer = Number(pointerField[0]); | ||||
|   } | ||||
|   return { | ||||
|     mint, | ||||
|     pointer, | ||||
|     edicts: message.edicts, | ||||
|     etching, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function decipherRunestone(tx: Transaction): Runestone | void { | ||||
|   const payload = tx.vout.find((vout) => vout.scriptpubkey.startsWith('6a5d'))?.scriptpubkey_asm.replace(/OP_\w+|\s/g, ''); | ||||
|   if (!payload) { | ||||
|     return; | ||||
|   } | ||||
|   try { | ||||
|     const integers = decodeLEB128(hexToBytes(payload)); | ||||
|     const message = integersToMessage(integers); | ||||
|     return messageToRunestone(message); | ||||
|   } catch (error) { | ||||
|     console.error(error); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| @ -102,6 +102,7 @@ import { AccelerationsListComponent } from '../components/acceleration/accelerat | ||||
| import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; | ||||
| import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; | ||||
| import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component'; | ||||
| import { OrdDataComponent } from '../components/ord-data/ord-data.component'; | ||||
| 
 | ||||
| import { BlockViewComponent } from '../components/block-view/block-view.component'; | ||||
| import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; | ||||
| @ -229,6 +230,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AccelerationStatsComponent, | ||||
|     PendingStatsComponent, | ||||
|     AccelerationSparklesComponent, | ||||
|     OrdDataComponent, | ||||
|     HttpErrorComponent, | ||||
|     TwitterWidgetComponent, | ||||
|     FaucetComponent, | ||||
| @ -361,6 +363,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AccelerationStatsComponent, | ||||
|     PendingStatsComponent, | ||||
|     AccelerationSparklesComponent, | ||||
|     OrdDataComponent, | ||||
|     HttpErrorComponent, | ||||
|     TwitterWidgetComponent, | ||||
|     TwitterLogin, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user