├── .cursor ├── mcp.json └── rules │ ├── database-admin.mdc │ ├── frontend-developer.mdc │ ├── frontend-tester.mdc │ ├── manager.mdc │ └── ux-designer.mdc ├── .cursorignore ├── .cursorrules ├── .dockerignore ├── .firebaserc ├── .github └── workflows │ └── test_and_deploy.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile.backend ├── Dockerfile.backend.fly ├── Dockerfile.frontend ├── Dockerfile.pm2 ├── Dockerfile.postgres ├── FUNDING.json ├── LICENSE ├── Makefile ├── README.md ├── _headers ├── _redirects ├── ads.txt ├── babel.config.js ├── build-prod-images-amd64.sh ├── build-prod-images-arm64.sh ├── buildx-amd64-push.sh ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── fly.pgbouncer.toml ├── fly.pm2.toml ├── fly.postgres.toml ├── fly.redis.toml ├── fly.soketi.toml ├── fly.toml ├── generate-env-files.sh ├── index.html ├── jest.config.js ├── jsconfig.json ├── package.json ├── pm2-server ├── .eslintrc.js ├── app.js ├── ecosystem.config.js ├── index.js ├── lib │ └── pm2.js ├── package.json ├── tests │ ├── app.test.js │ ├── lib │ │ └── pm2.test.js │ └── mocks │ │ └── lib │ │ └── pm2.js └── yarn.lock ├── postgresql.conf ├── public ├── 404.html ├── _headers ├── _redirects ├── ads.txt ├── favicon.ico └── index.old.html ├── run ├── .eslintrc.js ├── .sequelizerc ├── api │ ├── accounts.js │ ├── addresses.js │ ├── blocks.js │ ├── caddy.js │ ├── contracts.js │ ├── demo.js │ ├── domains.js │ ├── erc721Collections.js │ ├── erc721Tokens.js │ ├── explorers.js │ ├── external.js │ ├── faucets.js │ ├── gas.js │ ├── index.js │ ├── marketing.js │ ├── modules │ │ └── tokens.js │ ├── pusher.js │ ├── search.js │ ├── setup.js │ ├── stats.js │ ├── status.js │ ├── stripe.js │ ├── transactionTraceSteps.js │ ├── transactions.js │ ├── users.js │ ├── v2Dexes.js │ └── workspaces.js ├── app.js ├── config │ └── database.js ├── index.js ├── instrument.js ├── jobs │ ├── batchBlockDelete.js │ ├── batchBlockSync.js │ ├── batchContractDelete.js │ ├── blockSync.js │ ├── blockSyncMonitoring.js │ ├── deleteWorkspace.js │ ├── enforceDataRetentionForWorkspace.js │ ├── explorerSyncCheck.js │ ├── increaseStripeBillingQuota.js │ ├── index.js │ ├── integrityCheck.js │ ├── integrityCheckStarter.js │ ├── processBlock.js │ ├── processContract.js │ ├── processExplorerV2Dex.js │ ├── processExplorerV2DexPair.js │ ├── processStripeSubscription.js │ ├── processTokenTransfer.js │ ├── processTransactionError.js │ ├── processTransactionTrace.js │ ├── processUser.js │ ├── queueMonitoring.js │ ├── receiptSync.js │ ├── reloadErc721Token.js │ ├── removeExpiredExplorers.js │ ├── removeStalledBlock.js │ ├── reprocessWorkspaceTransactionErrors.js │ ├── reprocessWorkspaceTransactionTraces.js │ ├── rpcHealthCheck.js │ ├── rpcHealthCheckStarter.js │ ├── sendResetPasswordEmail.js │ ├── setupV2DexPoolReserves.js │ ├── updateApproximatedRecord.js │ ├── updateExplorerSyncingProcess.js │ └── workspaceReset.js ├── lib │ ├── abi.js │ ├── abis │ │ ├── IUniswapV2Factory.json │ │ ├── IUniswapV2Pair.json │ │ ├── IUniswapV2Router02.json │ │ ├── erc1155.json │ │ ├── erc20.json │ │ ├── erc721.json │ │ ├── erc721Enumerable.json │ │ ├── erc721Metadata.json │ │ └── selectors.json │ ├── analytics.js │ ├── codeRunner.js │ ├── contract.js │ ├── counter.js │ ├── crypto.js │ ├── env.js │ ├── errors.js │ ├── firebase.js │ ├── flags.js │ ├── lock.js │ ├── logger.js │ ├── opsgenie.js │ ├── pm2.js │ ├── processContractVerification.js │ ├── pusher.js │ ├── queue.js │ ├── rateLimiter.js │ ├── redis.js │ ├── rpc.js │ ├── stripe.js │ ├── trace.js │ ├── transactions.js │ ├── utils.js │ ├── verifierParams.js │ └── yasold │ │ ├── bytecodes.js │ │ └── index.js ├── middlewares │ ├── auth.js │ ├── browserSync.js │ ├── bullboard.js │ ├── passportLocalStrategy.js │ ├── passportTokenStrategy.js │ ├── quicknode.js │ ├── secret.js │ ├── selfHosted.js │ ├── strategies │ │ ├── local.js │ │ └── token.js │ ├── stripe.js │ ├── webhook.js │ └── workspaceAuth.js ├── migrations │ ├── 20220407103950-create-user.js │ ├── 20220407104329-create-workspace.js │ ├── 20220407105903-create-block.js │ ├── 20220409151411-create-transaction.js │ ├── 20220409201615-create-transaction-receipt.js │ ├── 20220410115956-create-transaction-logs.js │ ├── 20220412124846-create-contract.js │ ├── 20220412215918-create-token-transfer.js │ ├── 20220413080504-create-balance-change.js │ ├── 20220413165300-add-tracing-to-workspaces.js │ ├── 20220414190045-create-transaction-trace-step.js │ ├── 20220415140322-add-change-transaction-log-data-type.js │ ├── 20220416102751-change-method-label-type.js │ ├── 20220421140019-allow-transaction-block-id-null.js │ ├── 20220426194342-change-difficulty.js │ ├── 20220428080007-add-ws-tx-index.js │ ├── 20220505073301-add-unique-index-balance-changes.js │ ├── 20220505135013-change-trace-field-type.js │ ├── 20220510133908-add-creates-to-tx.js │ ├── 20220511100249-add-contractAddress-to-receipt.js │ ├── 20220512211017-add-unique-index-token-transfers.js │ ├── 20220514162144-add-workspaceId-traceSteps.js │ ├── 20220516161542-add-imported-contract.js │ ├── 20220517071014-create-rpc-account.js │ ├── 20220517180358-add-proxy-to-contracts.js │ ├── 20220523070839-make-tx-nonce-optional.js │ ├── 20220523071221-make-block-nonce-optional.js │ ├── 20220523154752-make-transactions-v-string.js │ ├── 20220523154953-make-transactions-v-string.js │ ├── 20220526143718-create-explorer.js │ ├── 20220621093442-add-defualt-byzantium.js │ ├── 20220621132201-add-default-byzantium.js │ ├── 20220621132533-allow-null-status.js │ ├── 20220802085753-add-more-workspaceid.js │ ├── 20220807182605-change-ts-columns.js │ ├── 20220822083328-add-idx-to-transaction-steps.js │ ├── 20220822200917-add-isremote-to-ws.js │ ├── 20220824081456-add-erc721-fields-to-contracts.js │ ├── 20220824105125-create-erc-721-tokens.js │ ├── 20220824132313-add-totalsupply-to-contracts.js │ ├── 20220828150106-add-tokenIndex-to-tokentranfers.js │ ├── 20220907163435-update-token-transfer-constraint.js │ ├── 20220921084056-remove-api-enabled-field.js │ ├── 20220929134109-add-data-retention-ws.js │ ├── 20221002162242-add-default-retention-to-users.js │ ├── 20221013122700-add-total-supply.js │ ├── 20221101213654-add-ast-to-contracts.js │ ├── 20221114130137-add-bytecode-to-contracts.js │ ├── 20221115092621-add-asm-to-contracts.js │ ├── 20221201105014-add-configurable-storage.js │ ├── 20221206123650-make-erc721-configurable.js │ ├── 20221213145059-make-longer-erc-uri.js │ ├── 20230105135846-drop-token-transfer-constraints.js │ ├── 20230105170826-drop-token-balance-change-constraint.js │ ├── 20230109075637-add-token-transfer-logid.js │ ├── 20230111131428-add-index-on-token-transfer.js │ ├── 20230112154337-add-processed-to-token-transfers.js │ ├── 20230113095232-index-log-topics.js │ ├── 20230202115320-make-ids-mandatory.js │ ├── 20230217173301-allow-flexible-logs.js │ ├── 20230224155517-change-tx-log-data.js │ ├── 20230225141236-add-user-pwd-field.js │ ├── 20230227235434-add-custom-fields.js │ ├── 20230320203058-add-browser-sync.js │ ├── 20230402165603-add-block-transactions-status.js │ ├── 20230408002038-add-integrity-checks-to-workspaces.js │ ├── 20230408193424-create-integrity-checks.js │ ├── 20230415000132-create-rpc-healthchecks.js │ ├── 20230417222305-add-enabled-status-to-workspace.js │ ├── 20230518124738-create-contract-code.js │ ├── 20230615155732-add-value-to-trace-steps.js │ ├── 20230616002839-create-plans-table.js │ ├── 20230710131539-create-transaction-quotas.js │ ├── 20230710210306-add-crypto-payment.js │ ├── 20230711212929-create-explorer-domains.js │ ├── 20230811204104-make-domain-name-unique.js │ ├── 20230812142020-add-trial-stuff.js │ ├── 20230817110015-create-mv-dashboard.js │ ├── 20230819062040-make-less-tx-fields-mandatory.js │ ├── 20230819212413-allow-null-index.js │ ├── 20230823074521-add-demo-plan-to-user.js │ ├── 20230824125643-drop-custom-fields-on-workspace-delete.js │ ├── 20230922090307-add-failed-attemps.js │ ├── 20231005194411-add-demo-on-explorers.js │ ├── 20231031142245-make-stripeid-non-mandatory.js │ ├── 20231116103046-make-explorers-unique.js │ ├── 20231118175350-make-chainid-explorer-string.js │ ├── 20231123131211-add-enforce-quota-to-explorers.js │ ├── 20231226134450-add-pending-deletion-workspace.js │ ├── 20231228145503-make-parsed-error-text.js │ ├── 20231231123911-make-mv-indexes-unique.js │ ├── 20240102140645-create-mv-tokens.js │ ├── 20240111132749-create-blocks-hypertable.js │ ├── 20240116140522-add-pollingInterval-to-ws.js │ ├── 20240117094745-add-emitMissed-to-ws.js │ ├── 20240222134704-drop-mv.js │ ├── 20240303212123-add-qn-fields.js │ ├── 20240310215023-add-l1-settlement-explorer.js │ ├── 20240312183723-create-quota-credit.js │ ├── 20240321122741-make-miner-optional.js │ ├── 20240527114054-add-rate-limiter.js │ ├── 20240529123458-add-faucet-tables.js │ ├── 20240618111931-create-explorer-dex.js │ ├── 20240625120001-default-gas-price.js │ ├── 20240701131043-dex-reserve-tables.js │ ├── 20240812145655-add-tsdb-aggregate.js │ ├── 20240813204549-add-creation-hash.js │ ├── 20240814121027-add-vairous-indexes.js │ ├── 20240814202739-replace-tx-contract-creation.js │ ├── 20240815224652-default-verif-library.js │ ├── 20240916134457-idx-hashed-bytecode.js │ ├── 20240917195925-change-default-polling-interval.js │ ├── 20241014231525-make-constraint-deferrable.js │ ├── 20241014234136-make-blocks-constraint-deferrable.js │ ├── 20241016095610-make-blockid-not-null.js │ ├── 20241120153036-add-skip-integrity-check.js │ ├── 20241121121032-optional-hashed-bytecode.js │ ├── 20241124043517-tbc-constraint.js │ ├── 20241203232535-contract-verif-constraints.js │ ├── 20241217210214-make-token-constraint-deferrable.js │ ├── 20241218140422-make-transactions-constraint-deferrable.js │ ├── 20250125193354-make-vrs-fields-optional.js │ ├── 20250208102954-create-block-event.js │ ├── 20250216141527-add-gas-page-setting.js │ ├── 20250306170351-make-workspace-name-contraint-partial.js │ └── 20250319080551-index-for-internal-txs.js ├── models │ ├── ContractSource.js │ ├── ContractVerification.js │ ├── account.js │ ├── block.js │ ├── blockevent.js │ ├── contract.js │ ├── customfield.js │ ├── erc721token.js │ ├── explorer.js │ ├── explorerdomain.js │ ├── explorerfaucet.js │ ├── explorerv2dex.js │ ├── faucetdrip.js │ ├── index.js │ ├── integritycheck.js │ ├── rpchealtcheck.js │ ├── stripeplan.js │ ├── stripequotaextension.js │ ├── stripesubscription.js │ ├── tokenbalancechange.js │ ├── tokenbalancechangeevent.js │ ├── tokentransfer.js │ ├── tokentransferevent.js │ ├── transaction.js │ ├── transactionevent.js │ ├── transactionlogs.js │ ├── transactionreceipt.js │ ├── transactiontracestep.js │ ├── user.js │ ├── v2dexpair.js │ ├── v2dexpoolreserve.js │ └── workspace.js ├── package-lock.json ├── package.json ├── queues.js ├── scheduler.js ├── seeders │ └── 20240607120000-seed-stripe-plan-all-capabilities.js ├── tests │ ├── api │ │ ├── accounts.test.js │ │ ├── addresses.test.js │ │ ├── blocks.test.js │ │ ├── caddy.test.js │ │ ├── contracts.test.js │ │ ├── demo.test.js │ │ ├── domains.test.js │ │ ├── erc721Collections.test.js │ │ ├── erc721Tokens.test.js │ │ ├── explorers.test.js │ │ ├── faucets.test.js │ │ ├── gas.test.js │ │ ├── marketing.test.js │ │ ├── modules │ │ │ └── tokens.test.js │ │ ├── pusher.test.js │ │ ├── search.test.js │ │ ├── setup.test.js │ │ ├── stats.test.js │ │ ├── status.test.js │ │ ├── stripe.test.js │ │ ├── transactionTraceSteps.test.js │ │ ├── transactions.test.js │ │ ├── users.test.js │ │ ├── v2Dexes.test.js │ │ └── workspaces.test.js │ ├── fixtures │ │ ├── ABI.json │ │ ├── ABIProp.json │ │ ├── AmalfiContract.json │ │ ├── AmalfiDepositTransaction.json │ │ ├── DSProxyContract.json │ │ ├── ERC20_ABI.json │ │ ├── ERC721TransferReceipt.json │ │ ├── ERC721_ABI.json │ │ ├── LogNoteEventProp.json │ │ ├── LogProp.json │ │ ├── StripePaymentSucceededWebhookBody.json │ │ ├── StripeSubscription.json │ │ ├── SyncEvent.json │ │ ├── Trace.json │ │ ├── Transaction.json │ │ ├── TransactionReceipt.json │ │ └── TransactionTransfer.json │ ├── jobs │ │ ├── batchBlockDelete.test.js │ │ ├── batchBlockSync.test.js │ │ ├── batchContractDelete.test.js │ │ ├── blockSync.test.js │ │ ├── blockSyncMonitoring.test.js │ │ ├── deleteWorkspace.test.js │ │ ├── enforceDataRetentionForWorkspace.test.js │ │ ├── explorerSyncCheck.test.js │ │ ├── increaseStripeBillingQuota.test.js │ │ ├── integrityCheck.test.js │ │ ├── integrityCheckStarter.test.js │ │ ├── processBlock.test.js │ │ ├── processContract.test.js │ │ ├── processExplorerV2Dex.test.js │ │ ├── processExplorerV2DexPair.test.js │ │ ├── processTokenTransfer.test.js │ │ ├── processTransactionError.test.js │ │ ├── processTransactionTrace.test.js │ │ ├── processUser.test.js │ │ ├── receiptSync.test.js │ │ ├── reloadErc721Token.test.js │ │ ├── removeExpiredExplorers.test.js │ │ ├── removeStalledBlock.test.js │ │ ├── rpcHealthCheck.test.js │ │ ├── rpcHealthCheckStarter.test.js │ │ ├── sendResetPasswordEmail.test.js │ │ ├── setupV2DexPoolReserves.test.js │ │ ├── updateApproximatedRecord.test.js │ │ ├── updateExplorerSyncingProcess.test.js │ │ └── workspaceReset.test.js │ ├── lib │ │ ├── __snapshots__ │ │ │ ├── abi.test.js.snap │ │ │ └── trace.test.js.snap │ │ ├── abi.test.js │ │ ├── contract.test.js │ │ ├── crypto.test.js │ │ ├── firebase.test.js │ │ ├── pm2.test.js │ │ ├── processContractVerification.test.js │ │ ├── queue.test.js │ │ ├── rpc.test.js │ │ ├── stripe.test.js │ │ ├── trace.test.js │ │ ├── transactions.test.js │ │ └── utils.test.js │ ├── middlewares │ │ ├── auth.test.js │ │ ├── browserSync.test.js │ │ ├── quicknode.test.js │ │ ├── secret.test.js │ │ ├── strategies │ │ │ ├── local.test.js │ │ │ └── token.test.js │ │ ├── webhook.test.js │ │ └── workspaceAuth.test.js │ ├── mocks │ │ ├── lib │ │ │ ├── abi.js │ │ │ ├── analytics.js │ │ │ ├── axios.js │ │ │ ├── codeRunner.js │ │ │ ├── crypto.js │ │ │ ├── env.js │ │ │ ├── ethers.js │ │ │ ├── firebase-admin.js │ │ │ ├── firebase.js │ │ │ ├── flags.js │ │ │ ├── lock.js │ │ │ ├── logger.js │ │ │ ├── opsgenie.js │ │ │ ├── pm2.js │ │ │ ├── processContractVerification.js │ │ │ ├── pusher.js │ │ │ ├── queue.js │ │ │ ├── rateLimiter.js │ │ │ ├── rpc.js │ │ │ ├── sendgrid.js │ │ │ ├── stripe.js │ │ │ ├── stripeLib.js │ │ │ ├── trace.js │ │ │ ├── transactions.js │ │ │ ├── utils.js │ │ │ └── yasold.js │ │ ├── middlewares │ │ │ ├── auth.js │ │ │ ├── browserSync.js │ │ │ ├── passportLocalStrategy.js │ │ │ ├── quicknode.js │ │ │ ├── secret.js │ │ │ ├── selfHosted.js │ │ │ ├── taskAuth.js │ │ │ └── workspaceAuth.js │ │ ├── models │ │ │ ├── Block.js │ │ │ ├── Contract.js │ │ │ ├── Explorer.js │ │ │ ├── ExplorerDomain │ │ │ ├── ExplorerFaucet.js │ │ │ ├── ExplorerV2Dex.js │ │ │ ├── StripePlan.js │ │ │ ├── StripeSubscription.js │ │ │ ├── TokenTransfer.js │ │ │ ├── Transaction.js │ │ │ ├── TransactionLog.js │ │ │ ├── TransactionReceipt.js │ │ │ ├── User.js │ │ │ ├── V2DexPair.js │ │ │ ├── V2DexPoolReserve.js │ │ │ ├── Workspace.js │ │ │ └── index.js │ │ └── queues.js │ ├── setupJestMocks.js │ └── webhooks │ │ ├── quicknode.test.js │ │ └── stripe.test.js ├── webhooks │ ├── index.js │ ├── quicknode.js │ └── stripe.js └── workers │ ├── highPriority.js │ ├── lowPriority.js │ ├── mediumPriority.js │ ├── priorities.js │ └── processHistoricalBlocks.js ├── src ├── .eslintrc.js ├── App.vue ├── Demo.vue ├── Embedded.vue ├── SSO.vue ├── SetupRoot.vue ├── abis │ ├── IUniswapV2Pair.json │ ├── IUniswapV2Router02.json │ ├── erc20.json │ ├── erc721.json │ ├── erc721Enumerable.json │ ├── erc721Metadata.json │ └── selectors.json ├── assets │ ├── analytics.webp │ ├── contract.webp │ ├── demo_compressed.mp4 │ ├── gallery.webp │ ├── metamask-fox.svg │ ├── styles │ │ └── theme.css │ ├── transactions.webp │ ├── transfers.webp │ └── verification.webp ├── bus.js ├── components │ ├── Account.vue │ ├── Accounts.vue │ ├── AdBanner.vue │ ├── AddAccountModal.vue │ ├── Address.vue │ ├── AddressAnalytics.vue │ ├── AddressAssets.vue │ ├── AddressERC20TokenTransfer.vue │ ├── AddressHeader.vue │ ├── AddressTokenAssets.vue │ ├── AddressTokenTransfers.vue │ ├── AddressTraceSteps.vue │ ├── AddressTransactionsList.vue │ ├── Auth.vue │ ├── Billing.vue │ ├── Block.vue │ ├── BlockList.vue │ ├── BlockOverview.vue │ ├── BlockTransactionList.vue │ ├── Blocks.vue │ ├── BrowserSyncExplainerModal.vue │ ├── CompactTokenTransfers.vue │ ├── CompactTransactionTokenTransfers.vue │ ├── ContractCallOptions.vue │ ├── ContractCode.vue │ ├── ContractCodeEditor.vue │ ├── ContractDetails.vue │ ├── ContractFileExplorer.vue │ ├── ContractInteraction.vue │ ├── ContractLogs.vue │ ├── ContractReadMethod.vue │ ├── ContractReadWrite.vue │ ├── ContractVerification.vue │ ├── ContractVerificationInfo.vue │ ├── ContractWriteMethod.vue │ ├── Contracts.vue │ ├── CreateExplorerDexModal.vue │ ├── CreateExplorerFaucetModal.vue │ ├── CreateExplorerModal.vue │ ├── CreateWorkspace.vue │ ├── CreateWorkspaceModal.vue │ ├── CustomField.vue │ ├── DateRangeSelector.vue │ ├── DemoExplorerMigrationModal.vue │ ├── DemoExplorerSetup.vue │ ├── DemoExplorerSetupEmbedded.vue │ ├── DexTokenSelectionModal.vue │ ├── ERC20Contract.vue │ ├── ERC20ContractAnalytics.vue │ ├── ERC20TokenHolders.vue │ ├── ERC721Collection.vue │ ├── ERC721Collections.vue │ ├── ERC721Gallery.vue │ ├── ERC721Token.vue │ ├── ERC721TokenCard.vue │ ├── ERC721TokenTransferModal.vue │ ├── ERC721TokenTransfers.vue │ ├── ExpandableText.vue │ ├── Explorer.vue │ ├── ExplorerAnalytics.vue │ ├── ExplorerBilling.vue │ ├── ExplorerBranding.vue │ ├── ExplorerBridge.vue │ ├── ExplorerDangerZone.vue │ ├── ExplorerDex.vue │ ├── ExplorerDexParametersModal.vue │ ├── ExplorerDexSettings.vue │ ├── ExplorerDexSettingsDangerZone.vue │ ├── ExplorerDomain.vue │ ├── ExplorerDomainDNSInfoModal.vue │ ├── ExplorerDomainsList.vue │ ├── ExplorerFaucet.vue │ ├── ExplorerFaucetAnalytics.vue │ ├── ExplorerFaucetPrivateKeyExportModal.vue │ ├── ExplorerFaucetSettings.vue │ ├── ExplorerFaucetSettingsDangerZone.vue │ ├── ExplorerFaucetTransactionHistory.vue │ ├── ExplorerGeneral.vue │ ├── ExplorerPlanCard.vue │ ├── ExplorerPlanSelector.vue │ ├── ExplorerQuotaManagementModal.vue │ ├── ExplorerSettings.vue │ ├── ExplorerStatus.vue │ ├── ExplorerSync.vue │ ├── Explorers.vue │ ├── FormattedSolVar.vue │ ├── GasConsumers.vue │ ├── GasSpender.vue │ ├── GasTracker.vue │ ├── HashLink.vue │ ├── ImportArtifactModal.vue │ ├── ImportContractModal.vue │ ├── InternalTransactions.vue │ ├── LineChart.vue │ ├── MainNavBar.vue │ ├── MigrateExplorerModal.vue │ ├── MultiLineChart.vue │ ├── NFTGallery.vue │ ├── NewExplorerDomainModal.vue │ ├── NewExplorerLink.vue │ ├── OnboardingModal.vue │ ├── Overview.vue │ ├── OverviewCharts.vue │ ├── OverviewStats.vue │ ├── PrivateExplorerFooter.vue │ ├── PublicExplorerFooter.vue │ ├── RemoveContractConfirmationModal.vue │ ├── RpcConnector.vue │ ├── SearchBar.vue │ ├── SelfHostedSetup.vue │ ├── SelfHostedSetupDone.vue │ ├── SelfHostedSetupExplorer.vue │ ├── SelfHostedSetupUser.vue │ ├── Settings.vue │ ├── StatNumber.vue │ ├── ThemeToggle.vue │ ├── TokenBalanceCard.vue │ ├── TokenBalances.vue │ ├── TokenContract.vue │ ├── TokenHeader.vue │ ├── TokenTransfers.vue │ ├── Tokens.vue │ ├── TokensBalanceDiff.vue │ ├── TopERC20Tokens.vue │ ├── TopNFT.vue │ ├── TraceStep.vue │ ├── TraceStepsTable.vue │ ├── Transaction.vue │ ├── TransactionEvent.vue │ ├── TransactionEventLogDetails.vue │ ├── TransactionEventRawInfo.vue │ ├── TransactionFunctionCall.vue │ ├── TransactionInternalTxns.vue │ ├── TransactionLogs.vue │ ├── TransactionOverview.vue │ ├── TransactionReceiptDetail.vue │ ├── TransactionState.vue │ ├── TransactionTraceEmbedded.vue │ ├── Transactions.vue │ ├── TransactionsList.vue │ ├── UnlockAccountModal.vue │ ├── UpdateExplorerPlanModal.vue │ ├── UpgradeLink.vue │ ├── VerifiedContracts.vue │ ├── WalletConnector.vue │ ├── WalletConnectorMirror.vue │ ├── WorkspaceList.vue │ ├── WorkspaceNFTTransfer.vue │ ├── WorkspaceTokenTransfer.vue │ └── base │ │ └── BaseChipGroup.vue ├── composables │ └── useContrastingColor.js ├── config │ └── firebase.js ├── filters │ ├── FromWei.js │ └── dt.js ├── lib │ ├── abi.js │ ├── contract.js │ ├── metamask.js │ ├── rpc.js │ ├── sample.json │ ├── trace.js │ └── utils.js ├── main.js ├── mixins │ └── user.js ├── plugins │ ├── bus.js │ ├── demoRouter.js │ ├── embeddedRouter.js │ ├── firebase.js │ ├── iconify.js │ ├── posthog.js │ ├── pusher.js │ ├── router.js │ ├── server.js │ ├── setupRouter.js │ ├── ssoRouter.js │ └── vuetify.js ├── stores │ ├── currentWorkspace.js │ ├── customisation.js │ ├── env.js │ ├── explorer.js │ ├── user.js │ └── walletStore.js ├── styles │ ├── main.scss │ ├── theme-transitions.scss │ └── theme-variables.scss └── workers │ ├── api.js │ └── blockSyncer.worker.js ├── tests ├── .eslintrc.js ├── setup.js └── unit │ ├── SSO.spec.js │ ├── __snapshots__ │ └── SSO.spec.js.snap │ ├── components │ ├── Account.spec.js │ ├── Accounts.spec.js │ ├── AdBanner.spec.js │ ├── AddAccountModal.spec.js │ ├── Address.spec.js │ ├── AddressAnalytics.spec.js │ ├── AddressAssets.spec.js │ ├── AddressERC20TokenTransfer.spec.js │ ├── AddressHeader.spec.js │ ├── AddressTokenAssets.spec.js │ ├── AddressTokenTransfers.spec.js │ ├── AddressTraceSteps.spec.js │ ├── AddressTransactionsList.spec.js │ ├── Auth.spec.js │ ├── Billing.spec.js │ ├── Block.spec.js │ ├── BlockList.spec.js │ ├── BlockOverview.spec.js │ ├── BlockTransactionList.spec.js │ ├── Blocks.spec.js │ ├── CompactTokenTransfers.spec.js │ ├── CompactTransactionTokenTransfer.spec.js │ ├── ContractCallOptions.spec.js │ ├── ContractCode.spec.js │ ├── ContractCodeEditor.spec.js │ ├── ContractDetails.spec.js │ ├── ContractFileExplorer.spec.js │ ├── ContractInteraction.spec.js │ ├── ContractLogs.spec.js │ ├── ContractReadMethod.spec.js │ ├── ContractReadWrite.spec.js │ ├── ContractVerification.spec.js │ ├── ContractVerificationInfo.spec.js │ ├── ContractWriteMethod.spec.js │ ├── Contracts.spec.js │ ├── CreateExplorerDexModal.spec.js │ ├── CreateExplorerFaucetModal.spec.js │ ├── CreateExplorerModal.spec.js │ ├── CreateWorkspace.spec.js │ ├── CreateWorkspaceModal.spec.js │ ├── DateRangeSelector.spec.js │ ├── DemoExplorerSetup.spec.js │ ├── DemoExplorerSetupEmbedded.spec.js │ ├── DexTokenSelectionModal.spec.js │ ├── ERC20Contract.spec.js │ ├── ERC20ContractAnalytics.spec.js │ ├── ERC20TokenHolders.spec.js │ ├── ERC721Collection.spec.js │ ├── ERC721Collections.spec.js │ ├── ERC721Gallery.spec.js │ ├── ERC721Token.spec.js │ ├── ERC721TokenCard.spec.js │ ├── ERC721TokenTransferModal.spec.js │ ├── ERC721TokenTransfers.spec.js │ ├── ExpandableText.spec.js │ ├── Explorer.spec.js │ ├── ExplorerAnalytics.spec.js │ ├── ExplorerBilling.spec.js │ ├── ExplorerBranding.spec.js │ ├── ExplorerDangerZone.spec.js │ ├── ExplorerDex.spec.js │ ├── ExplorerDexParametersModal.spec.js │ ├── ExplorerDexSettings.spec.js │ ├── ExplorerDexSettingsDangerZone.spec.js │ ├── ExplorerDomain.spec.js │ ├── ExplorerDomainDNSInfoModal.spec.js │ ├── ExplorerDomainsList.spec.js │ ├── ExplorerFaucet.spec.js │ ├── ExplorerFaucetAnalytics.spec.js │ ├── ExplorerFaucetPrivateKeyExportModal.spec.js │ ├── ExplorerFaucetSettings.spec.js │ ├── ExplorerFaucetSettingsDangerZone.spec.js │ ├── ExplorerFaucetTransactionHistory.spec.js │ ├── ExplorerGeneral.spec.js │ ├── ExplorerPlanCard.spec.js │ ├── ExplorerPlanSelector.spec.js │ ├── ExplorerQuotaManagementModal.spec.js │ ├── ExplorerSettings.spec.js │ ├── ExplorerStatus.spec.js │ ├── ExplorerSync.spec.js │ ├── FormattedSolVar.spec.js │ ├── GasConsumers.spec.js │ ├── GasSpender.spec.js │ ├── GasTracker.spec.js │ ├── HashLink.spec.js │ ├── ImportArtifactModal.spec.js │ ├── ImportContractModal.spec.js │ ├── InternalTransactions.spec.js │ ├── MainNavBar.spec.js │ ├── MigrateExplorerModal.spec.js │ ├── NFTGallery.spec.js │ ├── NewExplorerDomainModal.spec.js │ ├── NewExplorerLink.spec.js │ ├── OnboardingModal.spec.js │ ├── Overview.spec.js │ ├── OverviewCharts.spec.js │ ├── OverviewStats.spec.js │ ├── PrivateExplorerFooter.spec.js │ ├── PublicExplorerFooter.spec.js │ ├── RemoveContractConfirmationModal.spec.js │ ├── RpcConnector.spec.js │ ├── SearchBar.spec.js │ ├── SelfHostedSetup.spec.js │ ├── SelfHostedSetupDone.spec.js │ ├── SelfHostedSetupExplorer.spec.js │ ├── SelfHostedSetupUser.spec.js │ ├── Settings.spec.js │ ├── StatNumber.spec.js │ ├── ThemeToggle.spec.js │ ├── TokenBalanceCard.spec.js │ ├── TokenBalances.spec.js │ ├── TokenContract.spec.js │ ├── TokenHeader.spec.js │ ├── TokenTransfers.spec.js │ ├── Tokens.spec.js │ ├── TokensBalanceDiff.spec.js │ ├── TopERC20Tokens.spec.js │ ├── TopNFT.spec.js │ ├── TraceStep.spec.js │ ├── TraceStepsTable.spec.js │ ├── Transaction.spec.js │ ├── TransactionEvent.spec.js │ ├── TransactionEventLogDetails.spec.js │ ├── TransactionEventRawInfo.spec.js │ ├── TransactionFunctionCall.spec.js │ ├── TransactionInternalTxns.spec.js │ ├── TransactionLogs.spec.js │ ├── TransactionOverview.spec.js │ ├── TransactionReceiptDetail.spec.js │ ├── TransactionState.spec.js │ ├── Transactions.spec.js │ ├── TransactionsList.spec.js │ ├── UnlockAccountModal.spec.js │ ├── UpdateExplorerPlanModal.spec.js │ ├── UpgradeLink.spec.js │ ├── VerifiedContracts.spec.js │ ├── WalletConnector.spec.js │ ├── WorkspaceList.spec.js │ ├── WorkspaceNFTTransfer.spec.js │ ├── WorkspaceTokenTransfer.spec.js │ ├── __snapshots__ │ │ ├── Account.spec.js.snap │ │ ├── Accounts.spec.js.snap │ │ ├── AdBanner.spec.js.snap │ │ ├── AddAccountModal.spec.js.snap │ │ ├── Address.spec.js.snap │ │ ├── AddressAnalytics.spec.js.snap │ │ ├── AddressAssets.spec.js.snap │ │ ├── AddressERC20TokenTransfer.spec.js.snap │ │ ├── AddressHeader.spec.js.snap │ │ ├── AddressTokenAssets.spec.js.snap │ │ ├── AddressTokenTransfers.spec.js.snap │ │ ├── AddressTraceSteps.spec.js.snap │ │ ├── AddressTransactionsList.spec.js.snap │ │ ├── Auth.spec.js.snap │ │ ├── Billing.spec.js.snap │ │ ├── Block.spec.js.snap │ │ ├── BlockList.spec.js.snap │ │ ├── BlockOverview.spec.js.snap │ │ ├── BlockTransactionList.spec.js.snap │ │ ├── Blocks.spec.js.snap │ │ ├── CompactTokenTransfers.spec.js.snap │ │ ├── CompactTransactionTokenTransfer.spec.js.snap │ │ ├── ContractCallOptions.spec.js.snap │ │ ├── ContractCode.spec.js.snap │ │ ├── ContractCodeEditor.spec.js.snap │ │ ├── ContractDetails.spec.js.snap │ │ ├── ContractFileExplorer.spec.js.snap │ │ ├── ContractInteraction.spec.js.snap │ │ ├── ContractLogs.spec.js.snap │ │ ├── ContractReadMethod.spec.js.snap │ │ ├── ContractReadWrite.spec.js.snap │ │ ├── ContractStorage.spec.js.snap │ │ ├── ContractVerification.spec.js.snap │ │ ├── ContractVerificationInfo.spec.js.snap │ │ ├── ContractWriteMethod.spec.js.snap │ │ ├── Contracts.spec.js.snap │ │ ├── CreateExplorerDexModal.spec.js.snap │ │ ├── CreateExplorerFaucetModal.spec.js.snap │ │ ├── CreateExplorerModal.spec.js.snap │ │ ├── CreateWorkspace.spec.js.snap │ │ ├── CreateWorkspaceModal.spec.js.snap │ │ ├── DateRangeSelector.spec.js.snap │ │ ├── DemoExplorerSetup.spec.js.snap │ │ ├── DemoExplorerSetupEmbedded.spec.js.snap │ │ ├── DexTokenSelectionModal.spec.js.snap │ │ ├── ERC20Contract.spec.js.snap │ │ ├── ERC20ContractAnalytics.spec.js.snap │ │ ├── ERC20TokenHolders.spec.js.snap │ │ ├── ERC721Collection.spec.js.snap │ │ ├── ERC721Collections.spec.js.snap │ │ ├── ERC721Gallery.spec.js.snap │ │ ├── ERC721Token.spec.js.snap │ │ ├── ERC721TokenCard.spec.js.snap │ │ ├── ERC721TokenTransferModal.spec.js.snap │ │ ├── ERC721TokenTransfers.spec.js.snap │ │ ├── ExpandableText.spec.js.snap │ │ ├── Explorer.spec.js.snap │ │ ├── ExplorerAnalytics.spec.js.snap │ │ ├── ExplorerBilling.spec.js.snap │ │ ├── ExplorerBranding.spec.js.snap │ │ ├── ExplorerDangerZone.spec.js.snap │ │ ├── ExplorerDex.spec.js.snap │ │ ├── ExplorerDexParametersModal.spec.js.snap │ │ ├── ExplorerDexSettings.spec.js.snap │ │ ├── ExplorerDexSettingsDangerZone.spec.js.snap │ │ ├── ExplorerDomain.spec.js.snap │ │ ├── ExplorerDomainDNSInfoModal.spec.js.snap │ │ ├── ExplorerDomainsList.spec.js.snap │ │ ├── ExplorerFaucet.spec.js.snap │ │ ├── ExplorerFaucetAnalytics.spec.js.snap │ │ ├── ExplorerFaucetPrivateKeyExportModal.spec.js.snap │ │ ├── ExplorerFaucetSettings.spec.js.snap │ │ ├── ExplorerFaucetSettingsDangerZone.spec.js.snap │ │ ├── ExplorerFaucetTransactionHistory.spec.js.snap │ │ ├── ExplorerGeneral.spec.js.snap │ │ ├── ExplorerPlanCard.spec.js.snap │ │ ├── ExplorerPlanSelector.spec.js.snap │ │ ├── ExplorerQuotaManagementModal.spec.js.snap │ │ ├── ExplorerSettings.spec.js.snap │ │ ├── ExplorerStatus.spec.js.snap │ │ ├── ExplorerSync.spec.js.snap │ │ ├── Explorers.spec.js.snap │ │ ├── FormattedSolVar.spec.js.snap │ │ ├── GasConsumers.spec.js.snap │ │ ├── GasSpender.spec.js.snap │ │ ├── GasTracker.spec.js.snap │ │ ├── HashLink.spec.js.snap │ │ ├── ImportArtifactModal.spec.js.snap │ │ ├── ImportContractModal.spec.js.snap │ │ ├── InternalTransactions.spec.js.snap │ │ ├── MainNavBar.spec.js.snap │ │ ├── Metamask.spec.js.snap │ │ ├── MigrateExplorerModal.spec.js.snap │ │ ├── NFTGallery.spec.js.snap │ │ ├── NewExplorerDomainModal.spec.js.snap │ │ ├── NewExplorerLink.spec.js.snap │ │ ├── OnboardingModal.spec.js.snap │ │ ├── Overview.spec.js.snap │ │ ├── OverviewCharts.spec.js.snap │ │ ├── OverviewStats.spec.js.snap │ │ ├── PrivateExplorerFooter.spec.js.snap │ │ ├── PublicExplorerFooter.spec.js.snap │ │ ├── RemoveContractConfirmationModal.spec.js.snap │ │ ├── RpcConnector.spec.js.snap │ │ ├── SearchBar.spec.js.snap │ │ ├── SelfHostedSetup.spec.js.snap │ │ ├── SelfHostedSetupDone.spec.js.snap │ │ ├── SelfHostedSetupExplorer.spec.js.snap │ │ ├── SelfHostedSetupUser.spec.js.snap │ │ ├── Settings.spec.js.snap │ │ ├── StatNumber.spec.js.snap │ │ ├── StorageStructure.spec.js.snap │ │ ├── ThemeToggle.spec.js.snap │ │ ├── TokenBalanceCard.spec.js.snap │ │ ├── TokenBalances.spec.js.snap │ │ ├── TokenContract.spec.js.snap │ │ ├── TokenHeader.spec.js.snap │ │ ├── TokenTransfers.spec.js.snap │ │ ├── Tokens.spec.js.snap │ │ ├── TokensBalanceDiff.spec.js.snap │ │ ├── TopERC20Tokens.spec.js.snap │ │ ├── TopNFT.spec.js.snap │ │ ├── TraceStep.spec.js.snap │ │ ├── TraceStepsTable.spec.js.snap │ │ ├── Transaction.spec.js.snap │ │ ├── TransactionEvent.spec.js.snap │ │ ├── TransactionEventLogDetails.spec.js.snap │ │ ├── TransactionEventRawInfo.spec.js.snap │ │ ├── TransactionFunctionCall.spec.js.snap │ │ ├── TransactionInternalTxns.spec.js.snap │ │ ├── TransactionLogs.spec.js.snap │ │ ├── TransactionOverview.spec.js.snap │ │ ├── TransactionPicker.spec.js.snap │ │ ├── TransactionReceiptDetail.spec.js.snap │ │ ├── TransactionState.spec.js.snap │ │ ├── Transactions.spec.js.snap │ │ ├── TransactionsList.spec.js.snap │ │ ├── UnlockAccountModal.spec.js.snap │ │ ├── UpdateExplorerPlanModal.spec.js.snap │ │ ├── UpgradeLink.spec.js.snap │ │ ├── VerifiedContracts.spec.js.snap │ │ ├── WalletConnector.spec.js.snap │ │ ├── WorkspaceList.spec.js.snap │ │ ├── WorkspaceNFTTransfer.spec.js.snap │ │ └── WorkspaceTokenTransfer.spec.js.snap │ └── base │ │ ├── BaseChipGroup.spec.js │ │ └── __snapshots__ │ │ └── BaseChipGroup.spec.js.snap │ ├── composable │ └── useContrastingColor.spec.js │ ├── filters │ └── FromWei.spec.js │ ├── fixtures │ ├── ABIProp.json │ ├── AmalfiContract.json │ ├── AmalfiDepositTransaction.json │ ├── Block.json │ ├── DSProxyContract.json │ ├── DSProxyFactoryContract.json │ ├── DecodedStorageData.json │ ├── InstanceDecoderVariables.json │ ├── LogNoteEventProp.json │ ├── LogProp.json │ ├── NFTCollectibleABI.json │ ├── Storage.json │ ├── StorageProp.json │ ├── StripePaymentSucceededWebhookBody.json │ ├── TokenContract.json │ ├── Trace.json │ ├── TraceStepProp.json │ ├── TransactionProp.json │ └── USDCTransferTx.json │ ├── lib │ ├── __snapshots__ │ │ ├── abi.spec.js.snap │ │ ├── storage.spec.js.snap │ │ └── trace.spec.js.snap │ ├── abi.spec.js │ ├── contract.spec.js │ ├── trace.spec.js │ └── utils.spec.js │ └── mocks │ ├── db.js │ ├── ethereum.js │ ├── ethers.js │ ├── metamask.js │ ├── posthog.js │ ├── pusher.js │ ├── router.js │ ├── rpc.js │ ├── server.js │ ├── styles.js │ └── utils.js ├── vite.config.js ├── vitest.config.mjs ├── vue.config.js └── yarn.lock /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "postgres": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "@modelcontextprotocol/server-postgres", 8 | "postgresql://postgres:postgres@localhost:5433/ethernal" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ethernal-95a14" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile.backend: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | # Add Python and build dependencies 4 | RUN apk add --no-cache python3 make g++ gcc 5 | 6 | WORKDIR /app 7 | 8 | # Install dependencies first (better layer caching) 9 | COPY run/package*.json ./ 10 | RUN npm ci --only=production 11 | 12 | # Copy application files 13 | COPY run/api ./api 14 | COPY run/config ./config 15 | COPY run/jobs ./jobs 16 | COPY run/lib ./lib 17 | COPY run/middlewares ./middlewares 18 | COPY run/models ./models 19 | COPY run/webhooks ./webhooks 20 | COPY run/workers ./workers 21 | COPY run/migrations ./migrations 22 | COPY run/seeders ./seeders 23 | COPY run/.sequelizerc ./ 24 | COPY run/app.js . 25 | COPY run/index.js . 26 | COPY run/queues.js . 27 | COPY run/scheduler.js . 28 | COPY run/instrument.js . 29 | 30 | # Install sequelize-cli globally for migrations 31 | RUN npm install -g sequelize-cli && \ 32 | npm install -g nodemon 33 | 34 | EXPOSE 8888 35 | CMD ["node", "index.js"] -------------------------------------------------------------------------------- /Dockerfile.backend.fly: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | # Add Python and build dependencies 4 | RUN apk add --no-cache python3 make g++ gcc 5 | 6 | WORKDIR /app 7 | 8 | # Install dependencies first (better layer caching) 9 | COPY run/package*.json ./ 10 | RUN npm ci --only=production 11 | 12 | # Copy application files 13 | COPY run/api ./api 14 | COPY run/config ./config 15 | COPY run/jobs ./jobs 16 | COPY run/lib ./lib 17 | COPY run/middlewares ./middlewares 18 | COPY run/models ./models 19 | COPY run/webhooks ./webhooks 20 | COPY run/workers ./workers 21 | COPY run/migrations ./migrations 22 | COPY run/seeders ./seeders 23 | COPY run/.sequelizerc ./ 24 | COPY run/app.js . 25 | COPY run/index.js . 26 | COPY run/queues.js . 27 | COPY run/scheduler.js . 28 | COPY run/instrument.js . 29 | 30 | COPY ethernal-95a14-19f78a7e26cc.json ./ethernal-95a14-19f78a7e26cc.json 31 | 32 | # Install sequelize-cli globally for migrations 33 | RUN npm install -g sequelize-cli && \ 34 | npm install -g nodemon 35 | 36 | EXPOSE 8888 37 | CMD ["node", "index.js"] -------------------------------------------------------------------------------- /Dockerfile.pm2: -------------------------------------------------------------------------------- 1 | FROM node:18 AS base 2 | 3 | WORKDIR /app 4 | 5 | COPY pm2-server/package.json pm2-server/yarn.lock pm2-server/app.js pm2-server/index.js pm2-server/ecosystem.config.js ./ 6 | COPY pm2-server/lib/ ./lib 7 | RUN yarn global add ethernal-light pm2 8 | RUN pm2 install pm2-logrotate 9 | 10 | FROM base AS dev 11 | ENV PM2_HOME=/root/.pm2-dev 12 | RUN yarn 13 | CMD ["yarn", "run", "start:dev"] 14 | 15 | FROM base AS prod 16 | RUN yarn --prod 17 | CMD ["yarn", "run", "start"] 18 | -------------------------------------------------------------------------------- /Dockerfile.postgres: -------------------------------------------------------------------------------- 1 | FROM postgres:15 2 | 3 | # Add the TimescaleDB repo 4 | RUN apt-get update && apt-get install -y wget gnupg2 curl sudo \ 5 | && wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | apt-key add - \ 6 | && echo "deb https://packagecloud.io/timescale/timescaledb/debian/ buster main" > /etc/apt/sources.list.d/timescaledb.list \ 7 | && curl -s https://packagecloud.io/install/repositories/timescale/timescaledb/script.deb.sh | sudo bash \ 8 | && apt-get update \ 9 | && apt-get install -y sudo timescaledb-2-2.11.0-postgresql-15=2.11.0 10 | 11 | # Clean up APT when done. 12 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 13 | 14 | # Setup the entrypoint to init TimescaleDB 15 | ENTRYPOINT ["docker-entrypoint.sh"] 16 | 17 | EXPOSE 5432 18 | CMD ["postgres"] 19 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x1bF85ED48fcda98e2c7d08E4F2A8083fb18792AA" 5 | } 6 | }, 7 | "opRetro": { 8 | "projectId": "0x14eab8b10f729239d63df6c0cb85f666c3eb1f2dd440ebd003c1821f186a49da" 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ethernal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Document-Policy: js-profiling 3 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | /ingest/* https://app.posthog.com/:splat 200! 2 | 3 | /api/2/envelope/* https://sentry.tryethernal.com/api/2/envelope/:splat 200! 4 | /api/2/minidump/* https://sentry.tryethernal.com/api/2/minidump/:splat 200! 5 | /api/2/security/* https://sentry.tryethernal.com/api/2/security/:splat 200! 6 | /api/2/store/* https://sentry.tryethernal.com/api/2/store/:splat 200! 7 | 8 | /api module=contract action=checkverifystatus apikey=:apikey https://api.tryethernal.com/api/contracts/verificationStatus 200! 9 | /api module=contract action=getsourcecode apikey=:apikey https://api.tryethernal.com/api/contracts/sourceCode 200! 10 | /api module=contract action=getabi apikey=:apikey https://api.tryethernal.com/api/contracts/getabi 200! 11 | /api https://api.tryethernal.com/api 200! 12 | 13 | /api/* https://api.tryethernal.com/api/:splat 200! 14 | 15 | /* /index.html 200 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ] 4 | } 5 | -------------------------------------------------------------------------------- /buildx-amd64-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Build and push frontend image for amd64 5 | DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 -f Dockerfile.frontend -t antoinedc44/ethernal-frontend:latest --push . 6 | 7 | # Build and push backend image for amd64 8 | DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 -f Dockerfile.backend -t antoinedc44/ethernal-backend:latest --push . 9 | 10 | # Build and push pm2 image for amd64 11 | DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 -f Dockerfile.pm2 --target prod -t antoinedc44/ethernal-pm2:latest --push . 12 | 13 | echo "All amd64 images built and pushed successfully:" 14 | echo " antoinedc44/ethernal-frontend:latest" 15 | echo " antoinedc44/ethernal-backend:latest" 16 | echo " antoinedc44/ethernal-pm2:latest" -------------------------------------------------------------------------------- /fly.pgbouncer.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for ethernal-pgbouncer on 2022-11-25T23:17:08+01:00 2 | 3 | app = "ethernal-bouncer" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [build] 9 | image = "antoinedc44/docker-pgbouncer:latest" 10 | 11 | [env] 12 | 13 | [experimental] 14 | allowed_public_ports = [] 15 | auto_rollback = true 16 | private_network = true 17 | 18 | [[services]] 19 | internal_port = 5432 # Postgres instance 20 | protocol = "tcp" 21 | 22 | [services.concurrency] 23 | hard_limit = 300 24 | soft_limit = 200 25 | type = "connections" 26 | 27 | # Open port 10000 for plaintext connections. 28 | [[services.ports]] 29 | handlers = [] 30 | port = 5432 -------------------------------------------------------------------------------- /fly.pm2.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for ethernal-pm2 on 2023-08-11T11:17:21+02:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = "ethernal-pm2" 7 | primary_region = "ams" 8 | kill_signal = "SIGINT" 9 | kill_timeout = "5s" 10 | 11 | [build] 12 | dockerfile = "Dockerfile.pm2" 13 | 14 | [experimental] 15 | auto_rollback = true 16 | 17 | [http_service] 18 | internal_port = 8080 19 | auto_stop_machines = false 20 | auto_start_machines = false 21 | [[services.ports]] 22 | port = 80 23 | handlers = ["http"] 24 | force_https = true 25 | 26 | [[services.ports]] 27 | port = 443 28 | handlers = ["tls", "http"] 29 | [services.concurrency] 30 | type = "connections" 31 | hard_limit = 500 32 | soft_limit = 200 33 | 34 | [http_service.concurrency] 35 | type = "requests" 36 | soft_limit = 200 37 | hard_limit = 250 38 | -------------------------------------------------------------------------------- /fly.redis.toml: -------------------------------------------------------------------------------- 1 | app = "ethernal-redis" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [build] 7 | image = "redis:latest" 8 | 9 | [env] 10 | 11 | [experimental] 12 | allowed_public_ports = [] 13 | auto_rollback = true 14 | private_network = true 15 | 16 | [[services]] 17 | internal_port = 6379 # Redis instance 18 | protocol = "tcp" 19 | 20 | [services.concurrency] 21 | hard_limit = 1000 22 | soft_limit = 500 23 | type = "connections" 24 | 25 | # Open port 10000 for plaintext connections. 26 | [[services.ports]] 27 | handlers = [] 28 | port = 6379 -------------------------------------------------------------------------------- /fly.soketi.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for ethernal-pm2 on 2023-08-11T11:17:21+02:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = "ethernal-soketi" 7 | primary_region = "cdg" 8 | kill_signal = "SIGINT" 9 | kill_timeout = "5s" 10 | 11 | [experimental] 12 | auto_rollback = true 13 | 14 | [build] 15 | image = "quay.io/soketi/soketi:1.6.1-16-debian" 16 | 17 | [[services]] 18 | internal_port = 6001 19 | protocol = "tcp" 20 | 21 | [services.concurrency] 22 | hard_limit = 500 23 | soft_limit = 200 24 | 25 | [[services.ports]] 26 | handlers = ["http"] 27 | port = "6001" 28 | 29 | [[services.ports]] 30 | handlers = ["tls", "http"] 31 | port = "443" 32 | 33 | [[services.tcp_checks]] 34 | interval = 10000 35 | timeout = 2000 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | setupFiles: ["./tests/setup.js"], 4 | transformIgnorePatterns: ['/node_modules/(?!vuetify|vue)'], 5 | restoreMocks: true, 6 | clearMocks: true, 7 | resetMocks: true, 8 | moduleNameMapper: { 9 | '\\.(css|less)$': '/tests/unit/mocks/styles.js', 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "esnext", 5 | "baseUrl": "./src", 6 | "paths": { 7 | "@/*": ["src/components/*"] 8 | } 9 | }, 10 | "include": [ 11 | "src/**/*.vue", 12 | "src/**/*.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pm2-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | ], 11 | rules: { 12 | quotes: [2, "single"], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /pm2-server/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: "PM2 Server", 4 | script: "./index.js", 5 | env: { 6 | NODE_ENV: "development", 7 | }, 8 | env_production: { 9 | NODE_ENV: "production", 10 | } 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /pm2-server/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const app = require('./app.js'); 3 | 4 | const port = process.env.PORT || 9090; 5 | 6 | const triggerSync = () => { 7 | axios.post(`${process.env.ETHERNAL_HOST}/api/explorers/syncExplorers?secret=${process.env.ETHERNAL_SECRET}`) 8 | .then(({ data }) => console.log(data)) 9 | .catch((error) => { 10 | console.log(`Error when starting sync. Trying again in 1 second...`); 11 | console.log(error); 12 | setTimeout(triggerSync, 1000); 13 | }); 14 | }; 15 | 16 | app.listen(port, () => { 17 | console.log(`App is listening on port ${port}`); 18 | triggerSync(); 19 | }); 20 | -------------------------------------------------------------------------------- /pm2-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pm2-server", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "pm2-runtime start ecosystem.config.js --env production", 6 | "start:dev": "pm2-dev start ecosystem.config.js", 7 | "test": "jest", 8 | "test:handles": "jest --detectOpenHandles" 9 | }, 10 | "author": "Antoine de Chevigné", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.4.0", 14 | "body-parser": "^1.20.2", 15 | "express": "^4.18.2", 16 | "jest": "^29.6.1", 17 | "pm2": "^5.3.0", 18 | "supertest": "^6.3.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pm2-server/tests/mocks/lib/pm2.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/pm2', () => ({ 2 | list: jest.fn(), 3 | show: jest.fn(), 4 | stop: jest.fn(), 5 | reload: jest.fn(), 6 | restart: jest.fn(), 7 | delete: jest.fn(), 8 | start: jest.fn(), 9 | resume: jest.fn() 10 | })); 11 | -------------------------------------------------------------------------------- /postgresql.conf: -------------------------------------------------------------------------------- 1 | listen_addresses = '*' 2 | 3 | # Connection limits 4 | max_connections = 100 # PgBouncer handles concurrency 5 | 6 | # Memory settings (out of ~8 GB total) 7 | shared_buffers = 2GB 8 | work_mem = 16MB 9 | maintenance_work_mem = 256MB 10 | effective_cache_size = 6GB 11 | 12 | # WAL settings 13 | wal_buffers = 16MB 14 | checkpoint_completion_target = 0.9 15 | wal_writer_delay = 200ms 16 | 17 | # Background workers and parallelism 18 | max_worker_processes = 21 19 | max_parallel_workers = 2 20 | max_parallel_workers_per_gather = 1 21 | 22 | # Logging (optional, but useful) 23 | log_min_duration_statement = 500 # log slow queries 24 | log_connections = on 25 | log_disconnections = on 26 | log_lock_waits = on 27 | 28 | shared_preload_libraries = 'timescaledb' 29 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Document-Policy: js-profiling 3 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /ingest/* https://app.posthog.com/:splat 200! 2 | 3 | /api/2/envelope/* https://sentry.tryethernal.com/api/2/envelope/:splat 200! 4 | /api/2/minidump/* https://sentry.tryethernal.com/api/2/minidump/:splat 200! 5 | /api/2/security/* https://sentry.tryethernal.com/api/2/security/:splat 200! 6 | /api/2/store/* https://sentry.tryethernal.com/api/2/store/:splat 200! 7 | 8 | /api module=contract action=checkverifystatus apikey=:apikey https://api.tryethernal.com/api/contracts/verificationStatus 200! 9 | /api module=contract action=getsourcecode apikey=:apikey https://api.tryethernal.com/api/contracts/sourceCode 200! 10 | /api module=contract action=getabi apikey=:apikey https://api.tryethernal.com/api/contracts/getabi 200! 11 | /api https://api.tryethernal.com/api 200! 12 | 13 | /api/* https://api.tryethernal.com/api/:splat 200! 14 | 15 | /* /index.html 200 16 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/public/favicon.ico -------------------------------------------------------------------------------- /run/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | ], 11 | rules: { 12 | quotes: [2, "single"], 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 8 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /run/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('config', 'database.js') 5 | } -------------------------------------------------------------------------------- /run/api/external.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const axios = require('axios'); 4 | const { unmanagedError } = require('../lib/errors'); 5 | 6 | router.get('/compilers', async (req, res, next) => { 7 | try { 8 | const result = (await axios.get('https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/list.json')).data; 9 | 10 | res.status(200).json(result); 11 | } catch(error) { 12 | unmanagedError(error, req, next); 13 | } 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /run/api/pusher.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { pusher } = require('../lib/pusher'); 3 | const { managedError, unmanagedError } = require('../lib/errors'); 4 | const { isPusherEnabled } = require('../lib/flags'); 5 | const router = express.Router(); 6 | const workspaceAuthMiddleware = require('../middlewares/workspaceAuth'); 7 | 8 | const presenceMiddleware = async (req, res, next) => { 9 | try { 10 | if (isPusherEnabled()) 11 | next(); 12 | else 13 | return managedError(new Error('Pusher is not enabled'), req, res); 14 | } catch(error) { 15 | unmanagedError(error, req, next); 16 | } 17 | }; 18 | 19 | router.post('/authorization', [presenceMiddleware, workspaceAuthMiddleware], async (req, res, next) => { 20 | const socketId = req.body.socket_id; 21 | const channel = req.body.channel_name; 22 | 23 | try { 24 | const authResponse = pusher.authorizeChannel(socketId, channel); 25 | 26 | res.status(200).send(authResponse); 27 | } catch(error) { 28 | unmanagedError(error, req, next); 29 | } 30 | }); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /run/api/setup.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const db = require('../lib/firebase'); 4 | const selfHostedMiddleware = require('../middlewares/selfHosted'); 5 | const { managedError, unmanagedError } = require('../lib/errors'); 6 | 7 | router.post('/admin', selfHostedMiddleware, async (req, res, next) => { 8 | const data = req.body.data; 9 | try { 10 | const canSetupAdmin = await db.canSetupAdmin(); 11 | if (!canSetupAdmin) 12 | return managedError(new Error('Setup is not allowed'), req, res); 13 | 14 | try { 15 | const user = await db.createAdmin(data.email, data.password); 16 | res.status(200).json({ user }); 17 | } catch (error) { 18 | managedError(error, req, res); 19 | } 20 | } catch(error) { 21 | unmanagedError(error, req, next); 22 | } 23 | }); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /run/api/transactionTraceSteps.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const workspaceAuthMiddleware = require('../middlewares/workspaceAuth'); 3 | const router = express.Router(); 4 | const db = require('../lib/firebase'); 5 | const { unmanagedError } = require('../lib/errors'); 6 | 7 | /** 8 | * Retrieves all transaction trace steps (internal transactions) for a workspace 9 | * @param {string} workspace.id - The workspace id 10 | * @param {number} page - The page number 11 | * @param {number} itemsPerPage - The number of items per page 12 | * @returns {Promise} An array of transaction trace steps 13 | * @throws {Error} If an error occurs 14 | */ 15 | router.get('/', workspaceAuthMiddleware, async (req, res, next) => { 16 | const data = req.query; 17 | 18 | try { 19 | const items = await db.getWorkspaceTransactionTraceSteps(data.workspace.id, data.page, data.itemsPerPage); 20 | 21 | res.status(200).json({ items }); 22 | } catch(error) { 23 | unmanagedError(error, req, next); 24 | } 25 | }); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /run/config/database.js: -------------------------------------------------------------------------------- 1 | const logger = require('../lib/logger'); 2 | module.exports = { 3 | development: { 4 | "host": process.env.DB_HOST, 5 | "username": process.env.DB_USER, 6 | "database": "ethernal", 7 | "password": process.env.DB_PASSWORD, 8 | "port": process.env.DB_PORT, 9 | "dialect": "postgres", 10 | "logging": function(sql, sequelizeObject) { 11 | logger.debug(sql, { instance: sequelizeObject.instance }); 12 | }, 13 | benchmark: true 14 | }, 15 | production: { 16 | "username": process.env.DB_USER, 17 | "password": process.env.DB_PASSWORD, 18 | "database": process.env.DB_NAME, 19 | "host": process.env.DB_HOST, 20 | "port": process.env.DB_PORT, 21 | "dialect": "postgres", 22 | "logging": function(sql, sequelizeObject) { 23 | logger.debug(sql, { instance: sequelizeObject.instance }); 24 | }, 25 | "pool": { 26 | max: 400 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /run/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('./lib/logger'); 2 | const app = require('./app'); 3 | 4 | const port = parseInt(process.env.PORT) || 6000; 5 | app.listen(port, '::', () => { 6 | console.log(process.env.NODE_ENV == 'development' ? process.env : `App started on port ${port}`); 7 | logger.info(`Listening on port ${port}`); 8 | }); 9 | -------------------------------------------------------------------------------- /run/instrument.js: -------------------------------------------------------------------------------- 1 | const { getNodeEnv, getSentryDsn, getVersion } = require('./lib/env'); 2 | const logger = require('./lib/logger'); 3 | 4 | if (getSentryDsn()) { 5 | const Sentry = require('@sentry/node'); 6 | const { nodeProfilingIntegration } = require('@sentry/profiling-node'); 7 | 8 | Sentry.init({ 9 | dsn: getSentryDsn(), 10 | environment: getNodeEnv() || 'development', 11 | release: `ethernal@${getVersion()}`, 12 | integrations: [ 13 | nodeProfilingIntegration(), 14 | Sentry.postgresIntegration 15 | ], 16 | tracesSampleRate: 1.0, 17 | profilesSampleRate: 1.0 18 | }); 19 | 20 | logger.info('Started Sentry instrumentation'); 21 | } 22 | -------------------------------------------------------------------------------- /run/jobs/batchBlockDelete.js: -------------------------------------------------------------------------------- 1 | const { Workspace } = require('../models'); 2 | 3 | module.exports = async (job) => { 4 | const data = job.data; 5 | 6 | if (!data.workspaceId || !data.ids) 7 | throw new Error('Missing parameter'); 8 | 9 | const workspace = await Workspace.findByPk(data.workspaceId); 10 | if (!workspace) 11 | return 'Cannot find workspace'; 12 | 13 | return workspace.safeDestroyBlocks(data.ids); 14 | }; 15 | -------------------------------------------------------------------------------- /run/jobs/batchContractDelete.js: -------------------------------------------------------------------------------- 1 | const { Workspace } = require('../models'); 2 | 3 | module.exports = async (job) => { 4 | const data = job.data; 5 | 6 | if (!data.workspaceId || !data.ids) 7 | throw new Error('Missing parameter'); 8 | 9 | const workspace = await Workspace.findByPk(data.workspaceId); 10 | if (!workspace) 11 | return 'Cannot find workspace'; 12 | 13 | return workspace.safeDestroyContracts(data.ids); 14 | }; 15 | -------------------------------------------------------------------------------- /run/jobs/deleteWorkspace.js: -------------------------------------------------------------------------------- 1 | const { Workspace } = require('../models'); 2 | const { getMaxBlockForSyncReset, getMaxContractForReset } = require('../lib/env'); 3 | const { enqueue } = require('../lib/queue'); 4 | 5 | const RETRY_DELAY = 60 * 60 * 1000; 6 | 7 | module.exports = async job => { 8 | const data = job.data; 9 | 10 | const workspace = await Workspace.findByPk(data.workspaceId); 11 | 12 | if (!workspace) 13 | return 'Cannot find workspace'; 14 | 15 | if (!workspace.pendingDeletion) 16 | return 'This workspace has not been marked for deletion'; 17 | 18 | const blocks = await workspace.getBlocks({ limit: getMaxBlockForSyncReset() }); 19 | const contracts = await workspace.getContracts({ limit: getMaxContractForReset() }); 20 | 21 | if (blocks.length == getMaxBlockForSyncReset() || contracts.length == getMaxContractForReset()) { 22 | await enqueue('deleteWorkspace', `deleteWorkspace-${data.workspaceId}`, 23 | { workspaceId: data.workspaceId }, 24 | 1, 25 | null, 26 | RETRY_DELAY 27 | ); 28 | return 'Too many blocks/contracts for deletion'; 29 | } 30 | 31 | return workspace.safeDelete(); 32 | }; 33 | -------------------------------------------------------------------------------- /run/jobs/explorerSyncCheck.js: -------------------------------------------------------------------------------- 1 | const { Explorer } = require('../models'); 2 | const { bulkEnqueue } = require('../lib/queue'); 3 | 4 | module.exports = async () => { 5 | const explorers = await Explorer.findAll(); 6 | 7 | const jobs = explorers.map(e => ({ 8 | name: `updateExplorerSyncingProcess-${e.id}`, 9 | data: { explorerSlug: e.slug } 10 | })); 11 | 12 | await bulkEnqueue('updateExplorerSyncingProcess', jobs); 13 | 14 | return true; 15 | }; 16 | -------------------------------------------------------------------------------- /run/jobs/integrityCheckStarter.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const models = require('../models'); 3 | const { enqueue } = require('../lib/queue'); 4 | 5 | module.exports = async job => { 6 | const explorers = await models.Explorer.findAll({ 7 | where: { 8 | shouldSync: true, 9 | '$stripeSubscription.status$': 'active', 10 | '$stripeSubscription.stripePlan.slug$': { [Sequelize.Op.not]: 'demo' }, 11 | '$workspace.integrityCheckStartBlockNumber$': { [Sequelize.Op.not]: null }, 12 | '$workspace.skipIntegrityCheck$': false 13 | }, 14 | include: [ 15 | { model: models.StripeSubscription, as: 'stripeSubscription', include: { model: models.StripePlan, as: 'stripePlan' } }, 16 | { model: models.Workspace, as: 'workspace', include: 'rpcHealthCheck' } 17 | ] 18 | }); 19 | 20 | for (const explorer of explorers) 21 | await enqueue('integrityCheck', `integrityCheck-${explorer.workspaceId}`, { workspaceId: explorer.workspaceId }); 22 | 23 | return true; 24 | }; 25 | -------------------------------------------------------------------------------- /run/jobs/removeStalledBlock.js: -------------------------------------------------------------------------------- 1 | const { Block } = require('../models'); 2 | const { enqueue } = require('../lib/queue'); 3 | 4 | module.exports = async (job) => { 5 | const data = job.data; 6 | 7 | if (!data.blockId) 8 | return 'Missing parameter'; 9 | 10 | const block = await Block.findByPk(data.blockId, { 11 | include: ['transactions'] 12 | }); 13 | if (!block) 14 | return 'Could not find block'; 15 | 16 | const hasTransactionSyncing = block.transactions.length > 0 && block.transactions.filter(t => t.isSyncing).length > 0; 17 | if (hasTransactionSyncing) { 18 | await block.revertIfPartial(); 19 | return `Removed stalled block ${block.id} - Workspace ${block.workspaceId} - #${block.number}`; 20 | } 21 | else 22 | await enqueue('increaseStripeBillingQuota', `increaseStripeBillingQuota-${data.blockId}-${block.workspaceId}`, { blockId: data.blockId }); 23 | 24 | return true; 25 | }; 26 | -------------------------------------------------------------------------------- /run/jobs/reprocessWorkspaceTransactionErrors.js: -------------------------------------------------------------------------------- 1 | const models = require('../models'); 2 | const { bulkEnqueue } = require('../lib/queue'); 3 | 4 | const Workspace = models.Workspace; 5 | 6 | module.exports = async job => { 7 | const data = job.data; 8 | 9 | if (!data.workspaceId) 10 | throw new Error('Missing parameter.'); 11 | 12 | const workspace = await Workspace.findByPk(data.workspaceId); 13 | 14 | if (!workspace.public) 15 | return 'Not allowed on private workspaces'; 16 | 17 | const transactions = await workspace.getTransactions(); 18 | 19 | const batches = []; 20 | for (let i = 0; i < transactions.length; i++) { 21 | const transaction = transactions[i]; 22 | batches.push({ 23 | name: `processTransactionError-${data.workspaceId}-${transaction.hash}`, 24 | data: { transactionId: transaction.id } 25 | }); 26 | } 27 | 28 | await bulkEnqueue('processTransactionError', batches); 29 | 30 | return true; 31 | }; 32 | -------------------------------------------------------------------------------- /run/jobs/reprocessWorkspaceTransactionTraces.js: -------------------------------------------------------------------------------- 1 | const models = require('../models'); 2 | const { bulkEnqueue } = require('../lib/queue'); 3 | 4 | const Workspace = models.Workspace; 5 | 6 | module.exports = async job => { 7 | const data = job.data; 8 | 9 | if (!data.workspaceId) 10 | throw new Error('Missing parameter.'); 11 | 12 | const workspace = await Workspace.findByPk(data.workspaceId); 13 | 14 | if (!workspace.public) 15 | return 'Not allowed on private workspaces'; 16 | 17 | const transactions = await workspace.getTransactions(); 18 | 19 | const batches = []; 20 | for (let i = 0; i < transactions.length; i++) { 21 | const transaction = transactions[i]; 22 | batches.push({ 23 | name: `processTransactionTrace-${data.workspaceId}-${transaction.hash}`, 24 | data: { transactionId: transaction.id } 25 | }); 26 | } 27 | 28 | await bulkEnqueue('processTransactionTrace', batches); 29 | 30 | return true; 31 | }; 32 | -------------------------------------------------------------------------------- /run/jobs/rpcHealthCheckStarter.js: -------------------------------------------------------------------------------- 1 | const { Workspace, Explorer, StripeSubscription } = require('../models'); 2 | const { enqueue } = require('../lib/queue'); 3 | 4 | module.exports = async () => { 5 | const workspaces = await Workspace.findAll({ 6 | where: { 7 | rpcHealthCheckEnabled: true, 8 | public: true, 9 | pendingDeletion: false 10 | }, 11 | include: { 12 | model: Explorer, 13 | as: 'explorer', 14 | required: true, 15 | include: { 16 | model: StripeSubscription, 17 | as: 'stripeSubscription', 18 | required: true 19 | } 20 | } 21 | }); 22 | 23 | for (let i = 0; i < workspaces.length; i++) { 24 | const workspace = workspaces[i]; 25 | await enqueue('rpcHealthCheck', `rpcHealthCheck-${workspace.id}`, { workspaceId: workspace.id }); 26 | } 27 | 28 | return true; 29 | }; 30 | -------------------------------------------------------------------------------- /run/jobs/sendResetPasswordEmail.js: -------------------------------------------------------------------------------- 1 | const sgMail = require('@sendgrid/mail'); 2 | const { encode } = require('../lib/crypto'); 3 | const { getAppUrl, getSendgridApiKey, getSendgridSender } = require('../lib/env'); 4 | const { isSendgridEnabled } = require('../lib/flags'); 5 | 6 | module.exports = async (job) => { 7 | const { email, userId } = job.data; 8 | const jwt = encode({ userId }); 9 | const link = `${getAppUrl()}/auth?token=${jwt}`; 10 | 11 | if (!isSendgridEnabled()) 12 | throw new Error('Sendgrid has not been enabled.'); 13 | 14 | sgMail.setApiKey(getSendgridApiKey()); 15 | 16 | return sgMail.send({ 17 | to: email, 18 | from: getSendgridSender(), 19 | subject: 'Reset your password', 20 | text: `Click on this link to reset your password: ${link}`, 21 | html: `

Click on this link to reset your password: ${link}

` 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /run/lib/analytics.js: -------------------------------------------------------------------------------- 1 | const { PostHog } = require('posthog-node'); 2 | const { getPostHogApiKey, getPostHogApiHost } = require('./env'); 3 | 4 | class Analytics { 5 | constructor() { 6 | if (!getPostHogApiKey() || !getPostHogApiHost()) 7 | return; 8 | this.posthog = new PostHog(getPostHogApiKey(), { host: getPostHogApiHost() }); 9 | } 10 | 11 | track(distinctId, event, properties) { 12 | if (!this.posthog || !distinctId || !event) return; 13 | this.posthog && this.posthog.capture({ distinctId, event, properties }); 14 | } 15 | 16 | shutdown() { 17 | this.posthog && this.posthog.shutdown(); 18 | } 19 | } 20 | 21 | module.exports = Analytics; 22 | -------------------------------------------------------------------------------- /run/lib/counter.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { getCounterNamespace } = require('./env'); 3 | 4 | module.exports = { 5 | async countUp(key) { 6 | const response = await axios.get(`https://api.counterapi.dev/v1/${getCounterNamespace()}/${key}/up`); 7 | return response.data.count; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /run/lib/errors.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/node'); 2 | const logger = require('./logger'); 3 | 4 | const managedError = (error, req, res, status_code = 400, capture = true) => { 5 | logger.error(error.message, error, { ...req.params, ...req.query }); 6 | 7 | return res.status(status_code).send(error.message); 8 | }; 9 | 10 | const unmanagedError = (error, req, next) => { 11 | logger.error(error.message, error, { ...req.params, ...req.query }); 12 | 13 | Sentry.setContext('params', { ...req.params, ...req.query }); 14 | Sentry.setTags({ 15 | route: req.baseUrl + req.route.path, 16 | status_code: 500 17 | }); 18 | 19 | next(error); 20 | }; 21 | 22 | const managedWorkerError = (error, jobName, jobData, worker) => { 23 | logger.error(error.message, error, { jobName, worker, jobData }); 24 | Sentry.setContext('Job Data', jobData); 25 | return Sentry.captureException(error, { tags: { job: jobName, worker }}); 26 | }; 27 | 28 | module.exports = { managedError, unmanagedError, managedWorkerError }; 29 | -------------------------------------------------------------------------------- /run/lib/flags.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isSelfHosted: () => process.env.SELF_HOSTED, 3 | isPusherEnabled: () => !!process.env.SOKETI_DEFAULT_APP_ID && !!process.env.SOKETI_DEFAULT_APP_KEY && !!process.env.SOKETI_DEFAULT_APP_SECRET && !!process.env.SOKETI_HOST && !!process.env.SOKETI_PORT, 4 | isStripeEnabled: () => process.env.STRIPE_WEBHOOK_SECRET && process.env.STRIPE_SECRET_KEY, 5 | isSendgridEnabled: () => process.env.SENDGRID_API_KEY && process.env.SENDGRID_SENDER, 6 | isFirebaseAuthEnabled: () => !!process.env.ENABLE_FIREBASE_AUTH, 7 | isGoogleApiEnabled: () => !!process.env.GOOGLE_API_KEY, 8 | isApproximatedEnabled: () => process.env.APPROXIMATED_API_KEY && process.env.APPROXIMATED_TARGET_IP, 9 | isProductionEnvironment: () => process.env.NODE_ENV == 'production', 10 | isDevelopmentEnvironment: () => process.env.NODE_ENV == 'development', 11 | isDemoEnabled: () => !!process.env.DEMO_USER_ID, 12 | isQuicknodeEnabled: () => !!process.env.QUICKNODE_CREDENTIALS 13 | }; 14 | -------------------------------------------------------------------------------- /run/lib/lock.js: -------------------------------------------------------------------------------- 1 | const Mutex = require('redis-semaphore').Mutex 2 | const redis = require('../lib/redis'); 3 | 4 | class Lock { 5 | 6 | constructor(id, lockTimeout) { 7 | this.mutex = new Mutex(redis, id, { 8 | acquireAttemptsLimit: 1, 9 | lockTimeout 10 | }); 11 | } 12 | 13 | acquire() { 14 | return this.mutex.tryAcquire(); 15 | } 16 | 17 | release() { 18 | return this.mutex.release(); 19 | } 20 | } 21 | 22 | module.exports = Lock; 23 | -------------------------------------------------------------------------------- /run/lib/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | const { getLogLevel } = require('./env'); 3 | 4 | const logger = createLogger({ 5 | level: getLogLevel(), 6 | exitOnError: false, 7 | format: format.json(), 8 | transports: [] 9 | }); 10 | 11 | const transport = new transports.Console({ format: format.combine(format.colorize(), format.simple()) }); 12 | 13 | logger.add(transport); 14 | logger.exceptions.handle(transport); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /run/lib/opsgenie.js: -------------------------------------------------------------------------------- 1 | const { getNodeEnv, getOpsgenieApiKey } = require('./env'); 2 | const logger = require('./logger'); 3 | const axios = require('axios'); 4 | 5 | const createIncident = (message, description, priority = 'P1') => { 6 | if (getNodeEnv() === 'development' || !getOpsgenieApiKey()) { 7 | logger.info('Development environment (no OpsGenie API key) - skipping OpsGenie incident creation'); 8 | return logger.info({ message, description, priority }); 9 | } 10 | 11 | return axios({ 12 | method: 'POST', 13 | url: 'https://api.opsgenie.com/v2/alerts', 14 | headers: { 15 | 'Authorization': `GenieKey ${getOpsgenieApiKey()}`, 16 | }, 17 | data: { 18 | message, 19 | description, 20 | priority, 21 | tags: ['api'] 22 | } 23 | }); 24 | }; 25 | 26 | module.exports = { 27 | createIncident 28 | }; 29 | -------------------------------------------------------------------------------- /run/lib/pusher.js: -------------------------------------------------------------------------------- 1 | const Pusher = require('pusher'); 2 | const logger = require('./logger'); 3 | const { isPusherEnabled } = require('./flags'); 4 | const { getSoketiDefaultAppId, getSoketiDefaultAppKey, getSoketiDefaultAppSecret, getSoketiHost, getSoketiPort, getSoketiScheme, getSoketiUseTLS } = require('./env'); 5 | 6 | const pusher = isPusherEnabled() ? 7 | new Pusher({ 8 | appId: getSoketiDefaultAppId(), 9 | key: getSoketiDefaultAppKey(), 10 | secret: getSoketiDefaultAppSecret(), 11 | host: getSoketiHost(), 12 | port: getSoketiPort(), 13 | scheme: getSoketiScheme(), 14 | useTLS: getSoketiUseTLS() 15 | }) : 16 | { trigger: () => new Promise(resolve => resolve()) }; 17 | 18 | module.exports = { 19 | pusher, 20 | trigger: (channel, event, data) => { 21 | if (isPusherEnabled()) { 22 | pusher.trigger(channel, event, data) 23 | .catch(error => { 24 | logger.error(error.message, { location: 'lib.pusher', error, channel, event, data }); 25 | }); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /run/lib/rateLimiter.js: -------------------------------------------------------------------------------- 1 | const { RedisRateLimiter } = require('rolling-rate-limiter'); 2 | const redis = require('../lib/redis'); 3 | 4 | class RateLimiter { 5 | 6 | constructor(id, interval, maxInInterval) { 7 | this.id = id; 8 | this.limiter = new RedisRateLimiter({ 9 | client: redis, 10 | namespace: 'rate-limiter', 11 | interval, maxInInterval 12 | }); 13 | } 14 | 15 | limit() { 16 | return this.limiter.limit(this.id); 17 | } 18 | 19 | wouldLimit() { 20 | return this.limiter.wouldLimitWithInfo(this.id); 21 | } 22 | } 23 | 24 | module.exports = RateLimiter; 25 | -------------------------------------------------------------------------------- /run/lib/redis.js: -------------------------------------------------------------------------------- 1 | const { getRedisUrl, getRedisFamily } = require('./env'); 2 | const Redis = require('ioredis'); 3 | 4 | module.exports = new Redis(getRedisUrl(), { 5 | maxRetriesPerRequest: null, 6 | family: getRedisFamily() || 4, 7 | }); 8 | -------------------------------------------------------------------------------- /run/middlewares/bullboard.js: -------------------------------------------------------------------------------- 1 | /* 2 | This middleware sets up a basic auth mechanism to access the BullMQ UI at /bull 3 | */ 4 | 5 | const auth = require('basic-auth'); 6 | const { getBullboardUsername, getBullboardPassword } = require('../lib/env'); 7 | 8 | module.exports = (req, res, next) => { 9 | const user = auth(req); 10 | 11 | if (user && user.name == getBullboardUsername() && user.pass == getBullboardPassword()) { 12 | next(); 13 | } 14 | else { 15 | res.set({ 'WWW-Authenticate': 'Basic realm="bullBoard"' }).sendStatus(401); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /run/middlewares/passportLocalStrategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | This sets up the middleware used for email/password auth 3 | The actually code is in strategies/local.js to make it easier to unit test 4 | */ 5 | 6 | const passport = require('passport'); 7 | const LocalStrategy = require('passport-local'); 8 | 9 | const strategy = require('./strategies/local'); 10 | 11 | const localStrategy = new LocalStrategy( 12 | { usernameField: 'email' }, 13 | strategy 14 | ); 15 | 16 | passport.use(localStrategy); 17 | 18 | module.exports = (req, res, next) => { 19 | return passport.authenticate('local', { session: false }, (err, user, info) => { 20 | if (err || !user) 21 | return res.status(400).send('Invalid email or password.'); 22 | 23 | req.user = user; 24 | 25 | next(); 26 | })(req, res, next); 27 | }; 28 | -------------------------------------------------------------------------------- /run/middlewares/passportTokenStrategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | This sets up the middleware used for token auth 3 | The actually code is in strategies/token.js to make it easier to unit test 4 | */ 5 | 6 | const passport = require('passport'); 7 | const CustomStrategy = require('passport-custom'); 8 | const tokenStrategy = require('./strategies/token'); 9 | 10 | const strategy = new CustomStrategy(tokenStrategy); 11 | 12 | passport.use('token', strategy); 13 | 14 | module.exports = passport.authenticate('token', { session: false }); 15 | -------------------------------------------------------------------------------- /run/middlewares/quicknode.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks that it has been sent by Quiknode backend & that 3 | Quiknode integration is enabled 4 | */ 5 | 6 | const logger = require('../lib/logger'); 7 | const { isQuicknodeEnabled } = require('../lib/flags'); 8 | const { getQuicknodeCredentials } = require('../lib/env'); 9 | 10 | module.exports = async (req, res, next) => { 11 | try { 12 | if (!isQuicknodeEnabled()) 13 | return res.sendStatus(404); 14 | 15 | if (!req.headers.authorization || req.headers.authorization != `Basic ${getQuicknodeCredentials()}`) 16 | return res.sendStatus(401); 17 | 18 | next(); 19 | } catch(error) { 20 | logger.error(error.message, { location: 'middleware.quiknode', error: error }); 21 | res.status(401).send(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /run/middlewares/secret.js: -------------------------------------------------------------------------------- 1 | /* 2 | This middleware checks that the secret is present in the query 3 | */ 4 | 5 | const logger = require('../lib/logger'); 6 | const { getSecret } = require('../lib/env'); 7 | 8 | module.exports = (req, res, next) => { 9 | try { 10 | if (req.query.secret == getSecret()) 11 | next(); 12 | else 13 | throw new Error('Invalid secret') 14 | } catch(error) { 15 | logger.error(error.message, { location: 'middleware.secret', error: error }); 16 | res.status(401).send(error.message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /run/middlewares/selfHosted.js: -------------------------------------------------------------------------------- 1 | /* 2 | This middleware checks if the instance is self-hosted 3 | */ 4 | 5 | const logger = require('../lib/logger'); 6 | const { isSelfHosted } = require('../lib/flags'); 7 | 8 | module.exports = (req, res, next) => { 9 | try { 10 | if (isSelfHosted()) 11 | next(); 12 | else 13 | throw new Error('This feature is only available on self-hosted instances'); 14 | } catch(error) { 15 | logger.error(error.message, { location: 'middleware.selfHosted', error: error }); 16 | res.status(401).send(error.message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /run/middlewares/strategies/local.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple auth strategy that takes an email & password as an input, 3 | checks that it matches the stored hashed password and call the 4 | callback function. 5 | We use the firebase hashing function for backward compatibility 6 | with Firebase Auth 7 | */ 8 | 9 | const { firebaseVerify } = require('../../lib/crypto'); 10 | const db = require('../../lib/firebase'); 11 | 12 | module.exports = async (email, password, cb) => { 13 | const user = await db.getUserByEmail(email); 14 | 15 | if (!user) 16 | return cb(null, false, { message: 'Invalid email or password.' }); 17 | 18 | const isPasswordValid = await firebaseVerify(password, user.passwordSalt, user.passwordHash); 19 | 20 | if (!isPasswordValid) 21 | return cb(null, false, { message: 'Invalid email or password.' }); 22 | 23 | return cb(null, user); 24 | }; 25 | -------------------------------------------------------------------------------- /run/middlewares/stripe.js: -------------------------------------------------------------------------------- 1 | /* 2 | This middleware checks that Stripe is enabled. 3 | If not, a 404 is sent 4 | */ 5 | 6 | 7 | const logger = require('../lib/logger'); 8 | const { isStripeEnabled } = require('../lib/flags'); 9 | 10 | module.exports = async (req, res, next) => { 11 | try { 12 | if (isStripeEnabled()) 13 | next(); 14 | else 15 | res.sendStatus(404); 16 | } catch(error) { 17 | logger.error(error.message, { location: 'middleware.stripe', error: error }); 18 | res.status(401).send(error); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /run/migrations/20220413165300-add-tracing-to-workspaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'tracing', { 6 | type: Sequelize.DataTypes.STRING 7 | }); 8 | }, 9 | 10 | async down (queryInterface, Sequelize) { 11 | await queryInterface.removeColumn('workspaces', 'tracing'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /run/migrations/20220415140322-add-change-transaction-log-data-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transaction_logs', 'data', { 6 | type: Sequelize.TEXT 7 | }); 8 | }, 9 | 10 | async down (queryInterface, Sequelize) { 11 | await queryInterface.changeColumn('transaction_logs', 'data', { 12 | type: Sequelize.STRING 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220416102751-change-method-label-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transactions', 'methodLabel', { 6 | type: Sequelize.TEXT 7 | }); 8 | }, 9 | 10 | async down (queryInterface, Sequelize) { 11 | await queryInterface.changeColumn('transactions', 'methodLabel', { 12 | type: Sequelize.STRING 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220421140019-allow-transaction-block-id-null.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transactions', 'blockId', { 6 | type: Sequelize.INTEGER, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.changeColumn('transactions', 'blockId', { 13 | type: Sequelize.INTEGER, 14 | allowNull: false, 15 | references: { 16 | key: 'id', 17 | model: { 18 | tableName: 'blocks' 19 | } 20 | }, 21 | onDelete: 'CASCADE' 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /run/migrations/20220426194342-change-difficulty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('blocks', 'difficulty', { 6 | type: Sequelize.STRING 7 | }); 8 | }, 9 | 10 | async down (queryInterface, Sequelize) { 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /run/migrations/20220428080007-add-ws-tx-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addIndex( 6 | 'transactions', 7 | { 8 | fields: ['workspaceId', 'hash'], 9 | name: 'transactions_workspaceId_hash_idx', 10 | } 11 | ); 12 | }, 13 | 14 | async down (queryInterface, Sequelize) { 15 | await queryInterface.sequelize.query(` 16 | DROP INDEX "transactions_workspaceId_hash_idx" 17 | `); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /run/migrations/20220505073301-add-unique-index-balance-changes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.sequelize.query(` 6 | ALTER TABLE ONLY token_balance_changes 7 | ADD CONSTRAINT unique_transactionId_token_address_token_balance_changes 8 | UNIQUE ("transactionId", "token", "address"); 9 | `); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.sequelize.query(` 14 | ALTER TABLE ONLY token_balance_changes 15 | DROP CONSTRAINT unique_transactionId_token_address_token_balance_changes 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /run/migrations/20220505135013-change-trace-field-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transaction_trace_steps', 'input', { 6 | type: Sequelize.TEXT 7 | }); 8 | await queryInterface.changeColumn('transaction_trace_steps', 'returnData', { 9 | type: Sequelize.TEXT 10 | }); 11 | }, 12 | 13 | async down (queryInterface, Sequelize) { 14 | await queryInterface.changeColumn('transaction_trace_steps', 'input', { 15 | type: Sequelize.STRING 16 | }); 17 | await queryInterface.changeColumn('transaction_trace_steps', 'returnData', { 18 | type: Sequelize.STRING 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /run/migrations/20220510133908-add-creates-to-tx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('transactions', 'creates', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('transactions', 'creates'); 13 | } 14 | }; -------------------------------------------------------------------------------- /run/migrations/20220511100249-add-contractAddress-to-receipt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('transaction_receipts', 'contractAddress', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('transaction_receipts', 'contractAddress'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220512211017-add-unique-index-token-transfers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.sequelize.query(` 6 | ALTER TABLE ONLY token_transfers 7 | ADD CONSTRAINT unique_dst_src_token_transactionId_token_transfers 8 | UNIQUE ("dst", "src", "token", "transactionId"); 9 | `); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.sequelize.query(` 14 | ALTER TABLE ONLY token_transfers 15 | DROP CONSTRAINT unique_dst_src_token_transactionId_token_transfers 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /run/migrations/20220514162144-add-workspaceId-traceSteps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('transaction_trace_steps', 'workspaceId', { 6 | type: Sequelize.INTEGER, 7 | allowNull: false, 8 | references: { 9 | key: 'id', 10 | model: { 11 | tableName: 'workspaces' 12 | } 13 | }, 14 | }); 15 | }, 16 | 17 | async down (queryInterface, Sequelize) { 18 | await queryInterface.removeColumn('transactions', 'workspaceId'); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /run/migrations/20220516161542-add-imported-contract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'imported', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('contracts', 'imported'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220517180358-add-proxy-to-contracts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'proxy', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'proxy'); 13 | } 14 | }; -------------------------------------------------------------------------------- /run/migrations/20220523070839-make-tx-nonce-optional.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transactions', 'nonce', { 6 | type: Sequelize.INTEGER, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.changeColumn('transactions', 'nonce', { 13 | type: Sequelize.INTEGER, 14 | allowNull: false 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /run/migrations/20220523071221-make-block-nonce-optional.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('blocks', 'nonce', { 6 | type: Sequelize.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.changeColumn('blocks', 'nonce', { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /run/migrations/20220523154752-make-transactions-v-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transactions', 'v', { 6 | type: Sequelize.STRING, 7 | allowNull: false 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220523154953-make-transactions-v-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transactions', 'v', { 6 | type: Sequelize.STRING, 7 | allowNull: false 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220621093442-add-defualt-byzantium.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transaction_receipts', 'byzantium', { 6 | type: Sequelize.BOOLEAN, 7 | allowNull: false, 8 | default: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220621132201-add-default-byzantium.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transaction_receipts', 'byzantium', { 6 | type: Sequelize.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220621132533-allow-null-status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('transaction_receipts', 'status', { 6 | type: Sequelize.BOOLEAN, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220822083328-add-idx-to-transaction-steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addIndex( 6 | 'transaction_trace_steps', 7 | { 8 | fields: ['transactionId', 'workspaceId'], 9 | name: 'transaction_trace_steps_transactionId_workspaceId_idx', 10 | } 11 | ); 12 | }, 13 | 14 | async down (queryInterface, Sequelize) { 15 | await queryInterface.sequelize.query(` 16 | DROP INDEX "transaction_trace_steps_transactionId_workspaceId_idx" 17 | `); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /run/migrations/20220822200917-add-isremote-to-ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'isRemote', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('workspaces', 'isRemote'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220824132313-add-totalsupply-to-contracts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'tokenTotalSupply', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'tokenTotalSupply'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220828150106-add-tokenIndex-to-tokentranfers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('token_transfers', 'tokenId', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('token_transfers', 'tokenId'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20220921084056-remove-api-enabled-field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.removeColumn('workspaces', 'apiEnabled'); 6 | }, 7 | 8 | async down (queryInterface, Sequelize) { 9 | await queryInterface.addColumn('workspaces', 'apiEnabled', { 10 | type: Sequelize.DataTypes.BOOLEAN, 11 | allowNull: false, 12 | defaultValue: false 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20220929134109-add-data-retention-ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'dataRetentionLimit', { 6 | type: Sequelize.DataTypes.INTEGER, 7 | allowNull: false, 8 | defaultValue: 0 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('workspaces', 'dataRetentionLimit'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20221002162242-add-default-retention-to-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('users', 'defaultDataRetentionLimit', { 6 | type: Sequelize.DataTypes.INTEGER, 7 | allowNull: false, 8 | defaultValue: 7 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('users', 'defaultDataRetentionLimit'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20221013122700-add-total-supply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('explorers', 'totalSupply', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('explorers', 'totalSupply'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20221101213654-add-ast-to-contracts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'ast', { 6 | type: Sequelize.DataTypes.JSON, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'ast'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20221114130137-add-bytecode-to-contracts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'bytecode', { 6 | type: Sequelize.DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'bytecode'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20221115092621-add-asm-to-contracts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'asm', { 6 | type: Sequelize.DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'asm'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20221201105014-add-configurable-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | await queryInterface.addColumn('workspaces', 'storageEnabled', { 8 | type: Sequelize.DataTypes.BOOLEAN, 9 | allowNull: false, 10 | defaultValue: true 11 | }, { transaction }); 12 | 13 | await queryInterface.sequelize.query(` 14 | UPDATE workspaces 15 | SET "storageEnabled" = false 16 | WHERE public = true 17 | `, { transaction }); 18 | 19 | await transaction.commit(); 20 | } catch(error) { 21 | console.log(error) 22 | await transaction.rollback(); 23 | throw error; 24 | } 25 | }, 26 | 27 | async down (queryInterface, Sequelize) { 28 | await queryInterface.removeColumn('workspaces', 'storageEnabled'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /run/migrations/20221206123650-make-erc721-configurable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | await queryInterface.addColumn('workspaces', 'erc721LoadingEnabled', { 8 | type: Sequelize.DataTypes.BOOLEAN, 9 | allowNull: false, 10 | defaultValue: true 11 | }, { transaction }); 12 | 13 | await queryInterface.sequelize.query(` 14 | UPDATE workspaces 15 | SET "erc721LoadingEnabled" = false 16 | WHERE public = true 17 | `, { transaction }); 18 | 19 | await transaction.commit(); 20 | } catch(error) { 21 | console.log(error) 22 | await transaction.rollback(); 23 | throw error; 24 | } 25 | }, 26 | 27 | async down (queryInterface, Sequelize) { 28 | await queryInterface.removeColumn('workspaces', 'erc721LoadingEnabled'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /run/migrations/20221213145059-make-longer-erc-uri.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('erc_721_tokens', 'URI', { 6 | type: Sequelize.TEXT 7 | }); 8 | }, 9 | 10 | async down (queryInterface, Sequelize) { 11 | await queryInterface.changeColumn('erc_721_tokens', 'URI', { 12 | type: Sequelize.STRING 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20230105135846-drop-token-transfer-constraints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.sequelize.query(` 7 | ALTER TABLE ONLY token_transfers 8 | DROP CONSTRAINT unique_dst_src_token_transactionId_tokenId_token_transfers 9 | `); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20230105170826-drop-token-balance-change-constraint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.sequelize.query(` 6 | ALTER TABLE ONLY token_balance_changes 7 | DROP CONSTRAINT unique_transactionId_token_address_token_balance_changes 8 | `); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /run/migrations/20230111131428-add-index-on-token-transfer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.sequelize.query(` 7 | CREATE INDEX "token_transfers_transaction_logs_transactionLogId_idx" 8 | ON token_transfers("transactionLogId"); 9 | `); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.sequelize.query(` 14 | DROP INDEX "token_transfers_transaction_logs_transactionLogId_idx"; 15 | `); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /run/migrations/20230112154337-add-processed-to-token-transfers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.addColumn('token_transfers', 'processed', { 7 | type: Sequelize.DataTypes.BOOLEAN, 8 | allowNull: false, 9 | defaultValue: false 10 | }); 11 | }, 12 | 13 | async down (queryInterface, Sequelize) { 14 | await queryInterface.removeColumn('token_transfers', 'processed'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20230113095232-index-log-topics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.sequelize.query(` 7 | CREATE INDEX transaction_logs_topics_idx 8 | ON transaction_logs 9 | USING GIN (topics); 10 | `); 11 | }, 12 | 13 | async down (queryInterface, Sequelize) { 14 | await queryInterface.sequelize.query(` 15 | DROP INDEX transaction_logs_topics_idx; 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /run/migrations/20230224155517-change-tx-log-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE "transaction_logs" 10 | ALTER COLUMN "data" TYPE TEXT; 11 | `, { transaction }); 12 | 13 | await transaction.commit(); 14 | } catch(error) { 15 | console.log(error); 16 | await transaction.rollback(); 17 | } 18 | }, 19 | 20 | async down (queryInterface, Sequelize) { 21 | 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /run/migrations/20230225141236-add-user-pwd-field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | await queryInterface.addColumn('users', 'passwordHash', { 8 | type: Sequelize.DataTypes.STRING, 9 | allowNull: true 10 | }); 11 | await queryInterface.addColumn('users', 'passwordSalt', { 12 | type: Sequelize.DataTypes.STRING, 13 | allowNull: true 14 | }); 15 | await transaction.commit(); 16 | } catch(error) { 17 | console.log(error) 18 | await transaction.rollback(); 19 | throw error; 20 | } 21 | }, 22 | 23 | async down (queryInterface, Sequelize) { 24 | const transaction = await queryInterface.sequelize.transaction(); 25 | try { 26 | await queryInterface.removeColumn('users', 'passwordHash'); 27 | await queryInterface.removeColumn('users', 'passwordSalt'); 28 | await transaction.commit(); 29 | } catch(error) { 30 | console.log(error) 31 | await transaction.rollback(); 32 | throw error; 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /run/migrations/20230320203058-add-browser-sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'browserSyncEnabled', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: true 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('workspaces', 'browserSyncEnabled'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20230408002038-add-integrity-checks-to-workspaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'integrityCheckStartBlockNumber', { 6 | type: Sequelize.DataTypes.INTEGER, 7 | allowNull: true, 8 | defaultValue: null 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('workspaces', 'integrityCheckStartBlockNumber'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20230417222305-add-enabled-status-to-workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'statusPageEnabled', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('workspaces', 'statusPageEnabled'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20230615155732-add-value-to-trace-steps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('transaction_trace_steps', 'value', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('transaction_trace_steps', 'value'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20230710131539-create-transaction-quotas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.addColumn('stripe_subscriptions', 'transactionQuota', { 5 | type: Sequelize.DataTypes.INTEGER, 6 | allowNull: false, 7 | defaultValue: 0 8 | }); 9 | }, 10 | async down(queryInterface, Sequelize) { 11 | await queryInterface.removeColumn('stripe_subscriptions', 'transactionQuota'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /run/migrations/20230710210306-add-crypto-payment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('users', 'cryptoPaymentEnabled', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('users', 'cryptoPaymentEnabled'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20230811204104-make-domain-name-unique.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.sequelize.query(` 6 | ALTER TABLE explorer_domains 7 | ADD CONSTRAINT explorer_domains_unique_domain 8 | UNIQUE (domain); 9 | `); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.sequelize.query(` 14 | ALTER TABLE explorer_domains 15 | DROP CONSTRAINT IF EXISTS explorer_domains_unique_domain; 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /run/migrations/20230819212413-allow-null-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | /** 7 | * Add altering commands here. 8 | * 9 | * Example: 10 | * await queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | }, 13 | 14 | async down (queryInterface, Sequelize) { 15 | /** 16 | * Add reverting commands here. 17 | * 18 | * Example: 19 | * await queryInterface.dropTable('users'); 20 | */ 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /run/migrations/20230823074521-add-demo-plan-to-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('users', 'canUseDemoPlan', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('users', 'canUseDemoPlan'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20231005194411-add-demo-on-explorers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('explorers', 'isDemo', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('explorers', 'isDemo'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20231031142245-make-stripeid-non-mandatory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.changeColumn('stripe_plans', 'stripePriceId', { 7 | type: Sequelize.STRING, 8 | allowNull: true 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.changeColumn('stripe_plans', 'stripePriceId', { 14 | type: Sequelize.STRING, 15 | allowNull: false 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /run/migrations/20231116103046-make-explorers-unique.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE explorers 10 | ADD CONSTRAINT "explorers_workspaceId_unique" 11 | UNIQUE ("workspaceId"); 12 | `, { transaction }); 13 | 14 | await transaction.commit(); 15 | } catch(error) { 16 | console.log(error) 17 | await transaction.rollback(); 18 | throw error; 19 | } 20 | }, 21 | 22 | async down (queryInterface, Sequelize) { 23 | const transaction = await queryInterface.sequelize.transaction(); 24 | try { 25 | await queryInterface.sequelize.query(` 26 | ALTER TABLE explorer_domains 27 | DROP CONSTRAINT IF EXISTS "explorers_workspaceId_unique"; 28 | `, { transaction }); 29 | 30 | await transaction.commit(); 31 | } catch(error) { 32 | console.log(error) 33 | await transaction.rollback(); 34 | throw error; 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /run/migrations/20231118175350-make-chainid-explorer-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | await queryInterface.changeColumn('explorers', 'chainId', { 8 | type: Sequelize.STRING, 9 | allowNull: false 10 | }, { transaction }); 11 | 12 | await queryInterface.changeColumn('transactions', 'chainId', { 13 | type: Sequelize.INTEGER, 14 | allowNull: true 15 | }, { transaction }); 16 | await transaction.commit(); 17 | } catch(error) { 18 | await transaction.rollback(); 19 | throw error; 20 | } 21 | }, 22 | 23 | async down (queryInterface, Sequelize) { 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /run/migrations/20231123131211-add-enforce-quota-to-explorers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.addColumn('explorers', 'shouldEnforceQuota', { 7 | type: Sequelize.DataTypes.BOOLEAN, 8 | allowNull: false, 9 | defaultValue: true 10 | }); 11 | }, 12 | 13 | async down (queryInterface, Sequelize) { 14 | await queryInterface.removeColumn('explorers', 'shouldEnforceQuota'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20231226134450-add-pending-deletion-workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.addColumn('workspaces', 'pendingDeletion', { 7 | type: Sequelize.DataTypes.BOOLEAN, 8 | allowNull: false, 9 | defaultValue: false 10 | }); 11 | }, 12 | 13 | async down (queryInterface) { 14 | await queryInterface.removeColumn('workspaces', 'pendingDeletion'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20231228145503-make-parsed-error-text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.changeColumn('transactions', 'parsedError', { 7 | type: Sequelize.TEXT, 8 | allowNull: true 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | /** 14 | * Add reverting commands here. 15 | * 16 | * Example: 17 | * await queryInterface.dropTable('users'); 18 | */ 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /run/migrations/20240116140522-add-pollingInterval-to-ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('workspaces', 'pollingInterval', { 6 | type: Sequelize.DataTypes.INTEGER, 7 | allowNull: false, 8 | defaultValue: 1000 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('workspaces', 'pollingInterval'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20240321122741-make-miner-optional.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('blocks', 'miner', { 6 | type: Sequelize.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.changeColumn('blocks', 'miner', { 13 | type: Sequelize.STRING, 14 | allowNull: false 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /run/migrations/20240625120001-default-gas-price.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.changeColumn('transactions', 'gasPrice', { 7 | type: Sequelize.STRING, 8 | allowNull: false, 9 | defaultValue: '0' 10 | }); 11 | }, 12 | 13 | async down (queryInterface, Sequelize) { 14 | /** 15 | * Add reverting commands here. 16 | * 17 | * Example: 18 | * await queryInterface.dropTable('users'); 19 | */ 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /run/migrations/20240813204549-add-creation-hash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('contracts', 'creationTransactionHash', { 6 | type: Sequelize.DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.removeColumn('contracts', 'creationTransactionHash'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /run/migrations/20240814121027-add-vairous-indexes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.addIndex( 9 | 'explorer_domains', 10 | { 11 | fields: ['explorerId'], 12 | name: 'explorer_domains_explorerId_idx', 13 | } 14 | ); 15 | 16 | await transaction.commit(); 17 | } catch(error) { 18 | console.log(error); 19 | await transaction.rollback(); 20 | throw error; 21 | } 22 | }, 23 | async down(queryInterface, Sequelize) { 24 | const transaction = await queryInterface.sequelize.transaction(); 25 | try { 26 | await queryInterface.sequelize.query(` 27 | DROP INDEX "explorer_domains_explorerId_idx" 28 | `); 29 | 30 | await transaction.commit(); 31 | } catch(error) { 32 | console.log(error); 33 | await transaction.rollback(); 34 | throw error; 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /run/migrations/20240815224652-default-verif-library.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.sequelize.query(` 7 | ALTER TABLE contract_verifications ALTER COLUMN libraries SET DEFAULT '{}'; 8 | `); 9 | }, 10 | 11 | async down (queryInterface, Sequelize) { 12 | await queryInterface.sequelize.query(` 13 | ALTER TABLE contract_verifications ALTER COLUMN libraries DROP DEFAULT; 14 | `); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20240916134457-idx-hashed-bytecode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addIndex( 6 | 'contracts', 7 | { 8 | fields: ['workspaceId', 'hashedBytecode'], 9 | name: 'contracts_workspaceId_hashedBytecode_idx', 10 | } 11 | ); 12 | }, 13 | 14 | async down (queryInterface, Sequelize) { 15 | await queryInterface.sequelize.query(` 16 | DROP INDEX "contracts_workspaceId_hashedBytecode_idx" 17 | `); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /run/migrations/20240917195925-change-default-polling-interval.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('workspaces', 'pollingInterval', { 6 | type: Sequelize.INTEGER, 7 | allowNull: false, 8 | defaultValue: 4000 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.changeColumn('workspaces', 'pollingInterval', { 14 | type: Sequelize.INTEGER, 15 | allowNull: false, 16 | defaultValue: 1000 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /run/migrations/20241014231525-make-constraint-deferrable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE contracts DROP CONSTRAINT "contracts_transactionId_fkey"; 10 | `); 11 | await queryInterface.sequelize.query(` 12 | ALTER TABLE contracts 13 | ADD CONSTRAINT "contracts_transactionId_fkey" 14 | FOREIGN KEY ("transactionId") REFERENCES "transactions" ("id") DEFERRABLE INITIALLY DEFERRED; 15 | `); 16 | 17 | await transaction.commit(); 18 | } catch(error) { 19 | console.log(error); 20 | await transaction.rollback(); 21 | throw error; 22 | } 23 | }, 24 | async down(queryInterface, Sequelize) { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /run/migrations/20241014234136-make-blocks-constraint-deferrable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE transactions DROP CONSTRAINT "fk_blocknumber_workspaceid_blocks_number_workspaceid"; 10 | `); 11 | await queryInterface.sequelize.query(` 12 | ALTER TABLE transactions 13 | ADD CONSTRAINT "fk_blocknumber_workspaceid_blocks_number_workspaceid" 14 | FOREIGN KEY ("blockNumber", "workspaceId") REFERENCES blocks(number, "workspaceId") DEFERRABLE INITIALLY DEFERRED; 15 | `); 16 | 17 | await transaction.commit(); 18 | } catch(error) { 19 | console.log(error); 20 | await transaction.rollback(); 21 | throw error; 22 | } 23 | }, 24 | async down(queryInterface, Sequelize) { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /run/migrations/20241016095610-make-blockid-not-null.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE transactions ALTER COLUMN "blockId" SET NOT NULL; 10 | `); 11 | 12 | await transaction.commit(); 13 | } catch(error) { 14 | console.log(error); 15 | await transaction.rollback(); 16 | throw error; 17 | } 18 | }, 19 | async down(queryInterface, Sequelize) { 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /run/migrations/20241120153036-add-skip-integrity-check.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.addColumn('workspaces', 'skipIntegrityCheck', { 9 | type: Sequelize.DataTypes.BOOLEAN, 10 | allowNull: false, 11 | defaultValue: false 12 | }, { transaction }); 13 | 14 | await transaction.commit(); 15 | } catch(error) { 16 | console.log(error); 17 | await transaction.rollback(); 18 | throw error; 19 | } 20 | }, 21 | async down(queryInterface, Sequelize) { 22 | const transaction = await queryInterface.sequelize.transaction(); 23 | try { 24 | await queryInterface.removeColumn('workspaces', 'skipIntegrityCheck', { transaction }); 25 | 26 | await transaction.commit(); 27 | } catch(error) { 28 | console.log(error); 29 | await transaction.rollback(); 30 | throw error; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /run/migrations/20241121121032-optional-hashed-bytecode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface) { 6 | await queryInterface.sequelize.query('ALTER TABLE transaction_trace_steps ALTER COLUMN "contractHashedBytecode" DROP NOT NULL;'); 7 | }, 8 | 9 | async down (queryInterface) { 10 | await queryInterface.sequelize.query('ALTER TABLE transaction_trace_steps ALTER COLUMN "contractHashedBytecode" SET NOT NULL;'); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /run/migrations/20241124043517-tbc-constraint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.addConstraint('token_balance_changes', { 7 | type: 'unique', 8 | fields: ['transactionId', 'token', 'address'], 9 | name: 'token_balance_changes_txid_token_address_unique' 10 | }); 11 | }, 12 | 13 | async down (queryInterface) { 14 | await queryInterface.removeConstraint('token_balance_changes', 'token_balance_changes_txid_token_address_unique'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20241203232535-contract-verif-constraints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface, Sequelize) { 6 | await queryInterface.addConstraint('contract_verifications', { 7 | fields: ['contractId'], 8 | type: 'unique', 9 | name: 'unique_contract_verification_contractId' 10 | }); 11 | }, 12 | 13 | async down (queryInterface) { 14 | await queryInterface.removeConstraint('contract_verifications', 'unique_contract_verification_contractId'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /run/migrations/20241218140422-make-transactions-constraint-deferrable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | const transaction = await queryInterface.sequelize.transaction(); 7 | try { 8 | await queryInterface.sequelize.query(` 9 | ALTER TABLE token_transfers DROP CONSTRAINT "token_transfers_transactionId_fkey"; 10 | `); 11 | await queryInterface.sequelize.query(` 12 | ALTER TABLE token_transfers 13 | ADD CONSTRAINT "token_transfers_transactionId_fkey" 14 | FOREIGN KEY ("transactionId") REFERENCES "transactions" ("id") DEFERRABLE INITIALLY IMMEDIATE; 15 | `); 16 | 17 | await transaction.commit(); 18 | } catch(error) { 19 | console.log(error); 20 | await transaction.rollback(); 21 | throw error; 22 | } 23 | }, 24 | async down(queryInterface, Sequelize) { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /run/migrations/20250216141527-add-gas-page-setting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up (queryInterface, Sequelize) { 5 | await queryInterface.addColumn('explorers', 'gasAnalyticsEnabled', { 6 | type: Sequelize.DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: true 9 | }); 10 | }, 11 | 12 | async down (queryInterface, Sequelize) { 13 | await queryInterface.removeColumn('explorers', 'gasAnalyticsEnabled'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /run/migrations/20250306170351-make-workspace-name-contraint-partial.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | // Drop the existing constraint 6 | await queryInterface.removeConstraint( 7 | 'workspaces', 8 | 'workspaces_name_userId_is_unique' 9 | ); 10 | 11 | // Create a partial unique index using a raw query 12 | // Sequelize doesn't directly support partial indexes, so we use a raw query 13 | await queryInterface.sequelize.query(` 14 | CREATE UNIQUE INDEX "workspaces_name_userId_is_unique" 15 | ON "workspaces"("name", "userId") 16 | WHERE "pendingDeletion" = false; 17 | `); 18 | }, 19 | 20 | down: async (queryInterface, Sequelize) => { 21 | // Drop the partial index 22 | await queryInterface.sequelize.query(` 23 | DROP INDEX IF EXISTS "workspaces_name_userId_is_unique"; 24 | `); 25 | 26 | // Recreate the original constraint 27 | await queryInterface.addConstraint('workspaces', { 28 | fields: ['name', 'userId'], 29 | type: 'unique', 30 | name: 'workspaces_name_userId_is_unique' 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /run/models/ContractSource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | 6 | module.exports = (sequelize, DataTypes) => { 7 | class ContractSource extends Model { 8 | /** 9 | * Helper method for defining associations. 10 | * This method is not a part of Sequelize lifecycle. 11 | * The `models/index` file will call this method automatically. 12 | */ 13 | static associate(models) { 14 | ContractSource.belongsTo(models.Workspace, { foreignKey: 'workspaceId', as: 'workspace' }); 15 | ContractSource.belongsTo(models.Contract, { foreignKey: 'contractId', as: 'contract' }); 16 | ContractSource.belongsTo(models.ContractVerification, { foreignKey: 'contractVerificationId', as: 'verification' }); 17 | } 18 | } 19 | ContractSource.init({ 20 | workspaceId: DataTypes.INTEGER, 21 | contractId: DataTypes.INTEGER, 22 | contractVerificationId: DataTypes.INTEGER, 23 | fileName: DataTypes.STRING, 24 | content: DataTypes.TEXT, 25 | createdAt: DataTypes.DATE, 26 | updatedAt: DataTypes.DATE 27 | }, { 28 | sequelize, 29 | modelName: 'ContractSource', 30 | tableName: 'contract_sources', 31 | }); 32 | return ContractSource; 33 | }; 34 | -------------------------------------------------------------------------------- /run/models/customfield.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | 6 | module.exports = (sequelize, DataTypes) => { 7 | class CustomField extends Model { 8 | /** 9 | * Helper method for defining associations. 10 | * This method is not a part of Sequelize lifecycle. 11 | * The `models/index` file will call this method automatically. 12 | */ 13 | static associate(models) { 14 | CustomField.belongsTo(models.Workspace, { foreignKey: 'workspaceId', as: 'workspace' }); 15 | } 16 | } 17 | CustomField.init({ 18 | workspaceId: DataTypes.INTEGER, 19 | function: DataTypes.TEXT, 20 | location: DataTypes.STRING, 21 | }, { 22 | sequelize, 23 | modelName: 'CustomField', 24 | tableName: 'custom_fields', 25 | }); 26 | return CustomField; 27 | }; 28 | -------------------------------------------------------------------------------- /run/models/faucetdrip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class FaucetDrip extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | FaucetDrip.belongsTo(models.ExplorerFaucet, { foreignKey: 'explorerFaucetId', as: 'faucet' }); 14 | } 15 | } 16 | FaucetDrip.init({ 17 | explorerFaucetId: DataTypes.INTEGER, 18 | address: { 19 | type: DataTypes.STRING, 20 | set(value) { 21 | this.setDataValue('address', value.toLowerCase()); 22 | } 23 | }, 24 | amount: DataTypes.STRING, 25 | transactionHash: DataTypes.STRING, 26 | createdAt: DataTypes.DATE, 27 | updatedAt: DataTypes.DATE 28 | }, { 29 | sequelize, 30 | modelName: 'FaucetDrip', 31 | tableName: 'faucet_drips' 32 | }); 33 | return FaucetDrip; 34 | }; -------------------------------------------------------------------------------- /run/models/stripeplan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | 6 | module.exports = (sequelize, DataTypes) => { 7 | class StripePlan extends Model { 8 | /** 9 | * Helper method for defining associations. 10 | * This method is not a part of Sequelize lifecycle. 11 | * The `models/index` file will call this method automatically. 12 | */ 13 | static associate(models) { 14 | StripePlan.hasMany(models.StripeSubscription, { foreignKey: 'stripePlanId', as: 'stripeSubscriptions' }); 15 | } 16 | } 17 | StripePlan.init({ 18 | slug: DataTypes.STRING, 19 | name: DataTypes.STRING, 20 | stripePriceId: DataTypes.STRING, 21 | capabilities: DataTypes.JSON, 22 | price: DataTypes.INTEGER, 23 | public: DataTypes.BOOLEAN, 24 | createdAt: DataTypes.DATE, 25 | updatedAt: DataTypes.DATE, 26 | }, { 27 | sequelize, 28 | modelName: 'StripePlan', 29 | tableName: 'stripe_plans' 30 | }); 31 | return StripePlan; 32 | }; 33 | -------------------------------------------------------------------------------- /run/models/stripequotaextension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | 6 | module.exports = (sequelize, DataTypes) => { 7 | class StripeQuotaExtension extends Model { 8 | /** 9 | * Helper method for defining associations. 10 | * This method is not a part of Sequelize lifecycle. 11 | * The `models/index` file will call this method automatically. 12 | */ 13 | static associate(models) { 14 | StripeQuotaExtension.belongsTo(models.StripeSubscription, { foreignKey: 'stripeSubscriptionId', as: 'stripeSubscription' }); 15 | StripeQuotaExtension.belongsTo(models.StripePlan, { foreignKey: 'stripePlanId', as: 'stripePlan' }); 16 | } 17 | } 18 | StripeQuotaExtension.init({ 19 | stripeSubscriptionId: DataTypes.INTEGER, 20 | stripePlanId: DataTypes.INTEGER, 21 | stripeId: DataTypes.STRING, 22 | quota: DataTypes.INTEGER, 23 | createdAt: DataTypes.DATE, 24 | updatedAt: DataTypes.DATE, 25 | }, { 26 | sequelize, 27 | modelName: 'StripeQuotaExtension', 28 | tableName: 'stripe_quota_extensions' 29 | }); 30 | return StripeQuotaExtension; 31 | }; 32 | -------------------------------------------------------------------------------- /run/seeders/20240607120000-seed-stripe-plan-all-capabilities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up (queryInterface) { 6 | const now = new Date(); 7 | await queryInterface.bulkInsert('stripe_plans', [ 8 | { 9 | slug: 'self-hosted', 10 | name: 'Self-Hosted', 11 | stripePriceId: null, 12 | capabilities: JSON.stringify({ 13 | description: 'Self hosted plan with all capabilities', 14 | dataRetention: 0, 15 | skipBilling: true, 16 | customStartingBlock: true, 17 | customDomain: true, 18 | branding: true, 19 | nativeToken: true, 20 | totalSupply: true, 21 | txLimit: 0 22 | }), 23 | price: 0, 24 | public: true, 25 | createdAt: now, 26 | updatedAt: now 27 | } 28 | ], {}); 29 | }, 30 | 31 | async down (queryInterface) { 32 | await queryInterface.bulkDelete('stripe_plans', { slug: 'self-hosted' }, {}); 33 | } 34 | }; -------------------------------------------------------------------------------- /run/tests/api/pusher.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | require('../mocks/models'); 3 | require('../mocks/lib/analytics'); 4 | require('../mocks/lib/flags'); 5 | require('../mocks/middlewares/workspaceAuth'); 6 | require('../mocks/lib/pusher'); 7 | 8 | const supertest = require('supertest'); 9 | const app = require('../../app'); 10 | const request = supertest(app); 11 | 12 | const BASE_URL = '/api/pusher'; 13 | 14 | describe(`GET ${BASE_URL}/authorization`, () => { 15 | 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('Should return a 200 with a response', (done) => { 21 | request.post(`${BASE_URL}/authorization`) 22 | .send({ socket_id: 1, channel_name: 'hello' }) 23 | .expect(200) 24 | .then(({ text }) => { 25 | expect(text).toEqual('1234'); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /run/tests/api/transactionTraceSteps.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | require('../mocks/lib/firebase'); 3 | require('../mocks/models'); 4 | require('../mocks/middlewares/workspaceAuth'); 5 | 6 | const db = require('../../lib/firebase'); 7 | 8 | const supertest = require('supertest'); 9 | const app = require('../../app'); 10 | const request = supertest(app); 11 | 12 | const BASE_URL = '/api/transactionTraceSteps'; 13 | 14 | describe(`GET ${BASE_URL}`, () => { 15 | 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('Should return a 200 with a response', (done) => { 21 | jest.spyOn(db, 'getWorkspaceTransactionTraceSteps').mockResolvedValueOnce([{ hash: '0x1234' }]); 22 | request.get(`${BASE_URL}`) 23 | .send({ workspace: 'My Workspace', page: 1, itemsPerPage: 10 }) 24 | .expect(200) 25 | .then(({ body }) => { 26 | expect(body).toEqual({ items: [{ hash: '0x1234' }] }); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /run/tests/fixtures/LogProp.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockHash":"0x02cfea90863e2b298f07b8981cfb0ae04c66ebbfa895338030dde272cb3ddc32", 3 | "address":"0xc00e94Cb662C3520282E6f5717214004A7f26888", 4 | "logIndex":0, 5 | "data":"0x0000000000000000000000000000000000000000000000000000000000000001", 6 | "topics":[ 7 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 8 | "0x000000000000000000000000e93381fb4c4f14bda253907b18fad305d799241a", 9 | "0x000000000000000000000000c00e94cb662c3520282e6f5717214004a7f26888" 10 | ], 11 | "transactionIndex":0, 12 | "transactionHash":"0xb750fb9dd193bb4a46ea5426837c469815d2494abd68a94b1c2c190f3569c5b8", 13 | "blockNumber":12695880 14 | } -------------------------------------------------------------------------------- /run/tests/fixtures/SyncEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactionIndex": 0, 3 | "blockNumber": 3913550, 4 | "transactionHash": "0xecbf8a3609a01d30577e32217857e2b32014206350a13337abb553100d61ea31", 5 | "address": "0x19394644257EE69295546168892Ba989fdc0CE7c", 6 | "topics": ["0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"], 7 | "data": "0x00000000000000000000000000000000000000000000d3b04b1ec1b13824de6c00000000000000000000000000000000000000000010bf83030cde83718f58c2", 8 | "logIndex": 3, 9 | "blockHash": "0x50e6cc205c776c1e590f2394b02aa7e9790540ac6bf9e24863803d78072f247f" 10 | } -------------------------------------------------------------------------------- /run/tests/fixtures/Transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockHash":"0x02cfea90863e2b298f07b8981cfb0ae04c66ebbfa895338030dde272cb3ddc32", 3 | "data":"0xa9059cbb000000000000000000000000c00e94cb662c3520282e6f5717214004a7f268880000000000000000000000000000000000000000000000000000000000000001", 4 | "transactionIndex":0, 5 | "confirmations":1, 6 | "nonce":2196963, 7 | "gasLimit":"123455", 8 | "r":"0x4ba8b20554c3997fcc63e94d68f15ff88e7b8f62828677cfe16dc376df65e8b8", 9 | "s":"0x766f5cd25f9e0acc31f0ef296eda16f3439c663b9866ef891d58636b46c794a7", 10 | "chainId":1, 11 | "v":38, 12 | "blockNumber":12695880, 13 | "from":"0xe93381fb4c4f14bda253907b18fad305d799241a", 14 | "to":"0xc00e94cb662c3520282e6f5717214004a7f26888", 15 | "value":"0", 16 | "hash":"0xb750fb9dd193bb4a46ea5426837c469815d2494abd68a94b1c2c190f3569c5b8", 17 | "gasPrice":"23", 18 | "timestamp":1624524964 19 | } 20 | -------------------------------------------------------------------------------- /run/tests/fixtures/TransactionTransfer.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockHash":"0x02cfea90863e2b298f07b8981cfb0ae04c66ebbfa895338030dde272cb3ddc32", 3 | "data":"0xa9059cbb0000000000000000000000000f71271b3611f99b6867b95eda4d203f0a91397200000000000000000000000000000000000000000000007b53f79e378741b620", 4 | "transactionIndex":0, 5 | "confirmations":1, 6 | "nonce":2196963, 7 | "gasLimit":"123455", 8 | "r":"0x4ba8b20554c3997fcc63e94d68f15ff88e7b8f62828677cfe16dc376df65e8b8", 9 | "s":"0x766f5cd25f9e0acc31f0ef296eda16f3439c663b9866ef891d58636b46c794a7", 10 | "chainId":1, 11 | "v":38, 12 | "blockNumber":12695880, 13 | "from":"0xe93381fb4c4f14bda253907b18fad305d799241a", 14 | "to":"0xc00e94cb662c3520282e6f5717214004a7f26888", 15 | "value":"0", 16 | "hash":"0xb750fb9dd193bb4a46ea5426837c469815d2494abd68a94b1c2c190f3569c5b8", 17 | "gasPrice":"23", 18 | "timestamp":1624524964 19 | } 20 | -------------------------------------------------------------------------------- /run/tests/jobs/batchBlockDelete.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | const { Workspace } = require('../mocks/models'); 3 | 4 | const batchBlockDelete = require('../../jobs/batchBlockDelete'); 5 | 6 | beforeEach(() => jest.clearAllMocks()); 7 | 8 | describe('batchBlockDelete', () => { 9 | it('Should return an error if workspace is not found', (done) => { 10 | jest.spyOn(Workspace, 'findByPk').mockResolvedValueOnce(null); 11 | batchBlockDelete({ data: { workspaceId: 1, ids: [1] }}) 12 | .then(res => { 13 | expect(res).toEqual('Cannot find workspace'); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('Should call the destroy function', (done) => { 19 | const safeDestroyBlocks = jest.fn(); 20 | jest.spyOn(Workspace, 'findByPk').mockResolvedValueOnce({ safeDestroyBlocks }); 21 | batchBlockDelete({ data: { workspaceId: 1, ids: [1] }}) 22 | .then(() => { 23 | expect(safeDestroyBlocks).toHaveBeenCalledWith([1]); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /run/tests/jobs/batchContractDelete.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | const { Workspace } = require('../mocks/models'); 3 | 4 | const batchContractDelete = require('../../jobs/batchContractDelete'); 5 | 6 | beforeEach(() => jest.clearAllMocks()); 7 | 8 | describe('batchContractDelete', () => { 9 | it('Should return an error if workspace is not found', (done) => { 10 | jest.spyOn(Workspace, 'findByPk').mockResolvedValueOnce(null); 11 | batchContractDelete({ data: { workspaceId: 1, ids: [1] }}) 12 | .then(res => { 13 | expect(res).toEqual('Cannot find workspace'); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('Should call the destroy function', (done) => { 19 | const safeDestroyContracts = jest.fn(); 20 | jest.spyOn(Workspace, 'findByPk').mockResolvedValueOnce({ safeDestroyContracts }); 21 | batchContractDelete({ data: { workspaceId: 1, ids: [1] }}) 22 | .then(() => { 23 | expect(safeDestroyContracts).toHaveBeenCalledWith([1]); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /run/tests/jobs/explorerSyncCheck.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | const { Explorer } = require('../mocks/models'); 3 | 4 | const { bulkEnqueue } = require('../../lib/queue'); 5 | const explorerSyncCheck = require('../../jobs/explorerSyncCheck'); 6 | 7 | beforeEach(() => jest.clearAllMocks()); 8 | 9 | describe('explorerSyncCheck', () => { 10 | it('Should enqueue all explorers', (done) => { 11 | jest.spyOn(Explorer, 'findAll').mockResolvedValue([ 12 | { id: 1, slug: 'explorer-1' }, 13 | { id: 2, slug: 'explorer-2' } 14 | ]); 15 | 16 | explorerSyncCheck() 17 | .then(res => { 18 | expect(bulkEnqueue).toHaveBeenCalledWith('updateExplorerSyncingProcess', [ 19 | { name: 'updateExplorerSyncingProcess-1', data: { explorerSlug: 'explorer-1' }}, 20 | { name: 'updateExplorerSyncingProcess-2', data: { explorerSlug: 'explorer-2' }} 21 | ]); 22 | expect(res).toEqual(true); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /run/tests/jobs/integrityCheckStarter.test.js: -------------------------------------------------------------------------------- 1 | const { Explorer } = require('../mocks/models'); 2 | require('../mocks/lib/queue'); 3 | 4 | const { enqueue } = require('../../lib/queue'); 5 | const integrityCheckStarter = require('../../jobs/integrityCheckStarter'); 6 | 7 | beforeEach(() => jest.clearAllMocks()); 8 | 9 | describe('integrityCheckStarter', () => { 10 | it('Should enqueue integrity check processes if integrityCheck start number is set', async () => { 11 | jest.spyOn(Explorer, 'findAll').mockResolvedValueOnce([ 12 | { 13 | id: 2, 14 | workspaceId: 2, 15 | workspace: { 16 | integrityCheckStarterBlockNumber: 5 17 | } 18 | } 19 | ]); 20 | 21 | await integrityCheckStarter({}); 22 | 23 | expect(enqueue).toHaveBeenNthCalledWith(1, 'integrityCheck', 'integrityCheck-2', { 24 | workspaceId: 2, 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /run/tests/jobs/rpcHealthCheckStarter.test.js: -------------------------------------------------------------------------------- 1 | const { Workspace } = require('../mocks/models'); 2 | require('../mocks/lib/queue'); 3 | 4 | const { enqueue } = require('../../lib/queue'); 5 | const rpcHealthCheckStarter = require('../../jobs/rpcHealthCheckStarter'); 6 | 7 | beforeEach(() => jest.clearAllMocks()); 8 | 9 | describe('rpcHealthCheckStarter', () => { 10 | it('Should enqueue integrity check processes if integrityCheck start number is set', (done) => { 11 | jest.spyOn(Workspace, 'findAll').mockResolvedValueOnce([{ id: 2 }]); 12 | 13 | rpcHealthCheckStarter() 14 | .then(() => { 15 | expect(enqueue).toHaveBeenNthCalledWith(1, 'rpcHealthCheck', 'rpcHealthCheck-2', { 16 | workspaceId: 2, 17 | }); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /run/tests/jobs/sendResetPasswordEmail.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/crypto'); 2 | require('../mocks/lib/flags'); 3 | require('../mocks/lib/sendgrid'); 4 | 5 | const flags = require('../../lib/flags'); 6 | const sgMail = require('@sendgrid/mail'); 7 | 8 | const sendResetPasswordEmail = require('../../jobs/sendResetPasswordEmail'); 9 | 10 | beforeEach(() => jest.clearAllMocks()); 11 | 12 | describe('batchBlockSync', () => { 13 | it('Should send a reset password email', (done) => { 14 | sendResetPasswordEmail({ data: { email: 'antoine@tryethernal.com' }}) 15 | .then(() => { 16 | expect(sgMail.send).toHaveBeenCalled(); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('Should raise an error if Sendgrid is not enabled', (done) => { 22 | jest.spyOn(flags, 'isSendgridEnabled').mockReturnValueOnce(false); 23 | sendResetPasswordEmail({ data: { email: 'antoine@tryethernal.com' }}) 24 | .catch(error => { 25 | expect(error.message).toEqual('Sendgrid has not been enabled.'); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /run/tests/lib/__snapshots__/trace.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`parseTrace Should return the processed trace 1`] = ` 4 | [ 5 | { 6 | "address": "0xe54f3adc9ff833a025a89945c733233ce190f60e", 7 | "contractHashedBytecode": "0xdbe576b4818846aa77e82f4ed5fa78f92766b141f282d36703886d196df39322", 8 | "depth": 0, 9 | "input": "0x70a082310000000000000000000000002d481eeb2ba97955cd081cf218f453a817259ab1", 10 | "op": "STATICCALL", 11 | "returnData": "0x0000000000000000000000000000000000000001431e0fae6d7217caa0000000", 12 | }, 13 | ] 14 | `; 15 | -------------------------------------------------------------------------------- /run/tests/lib/contract.test.js: -------------------------------------------------------------------------------- 1 | const { isErc20 } = require('../../lib/contract'); 2 | const DSProxyContract = require('../fixtures/DSProxyContract'); 3 | const ERC20 = require('../fixtures/ERC20_ABI'); 4 | const ERC721 = require('../fixtures/ERC721_ABI'); 5 | 6 | describe('isErc20', () => { 7 | it('Should return true if the abi is erc20', () => { 8 | expect(isErc20(ERC20)).toBe(true); 9 | }); 10 | 11 | it('Should return false if the abi is not erc20', () => { 12 | expect(isErc20(DSProxyContract.abi)).toBe(false); 13 | }); 14 | }); 15 | 16 | describe('isErc721', () => { 17 | it('Should return true if the abi is erc721', () => { 18 | expect(isErc20(ERC20)).toBe(true); 19 | }); 20 | 21 | it('Should return false if the abi is not erc721', () => { 22 | expect(isErc20(DSProxyContract.abi)).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /run/tests/lib/queue.test.js: -------------------------------------------------------------------------------- 1 | // We do this to cancel the global mock in setupJestMock 2 | jest.mock('../../lib/queue', () => ({ 3 | ...jest.requireActual('../../lib/queue') 4 | })) 5 | require('../mocks/queues'); 6 | 7 | const { bulkEnqueue } = require('../../lib/queue'); 8 | const queues = require('../../queues'); 9 | 10 | describe('bulkEnqueue', () => { 11 | it('Enqueue 5 batches', async () => { 12 | const jobData = []; 13 | for (let i = 0; i < 10000; i++) 14 | jobData.push({ name: `job${i}`, data: { i }}); 15 | 16 | await bulkEnqueue('test', jobData); 17 | expect(queues['test'].addBulk).toHaveBeenCalledTimes(5); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /run/tests/lib/trace.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/models'); 2 | require('../mocks/lib/firebase'); 3 | 4 | const { parseTrace } = require('../../lib/trace'); 5 | const Trace = require('../fixtures/Trace.json'); 6 | 7 | afterEach(() => jest.clearAllMocks()); 8 | 9 | describe('parseTrace', () => { 10 | let from, provider; 11 | 12 | beforeEach(async () => { 13 | from = '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f'; 14 | provider = { 15 | getCode: () => { 16 | return new Promise((resolve) => resolve('0xabcd')); 17 | } 18 | }; 19 | }); 20 | 21 | it('Should return the processed trace', async () => { 22 | const result = await parseTrace(from, Trace, provider); 23 | expect(result).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /run/tests/middlewares/secret.test.js: -------------------------------------------------------------------------------- 1 | const secret = require('../../middlewares/secret'); 2 | 3 | describe('authMiddleware', () => { 4 | process.env.SECRET = '123'; 5 | const send = jest.fn(); 6 | const res = { 7 | status: jest.fn(() => ({ 8 | send: send 9 | })) 10 | }; 11 | 12 | it('Should allow access when using the right secret', async () => { 13 | const next = jest.fn(); 14 | const req = { 15 | query: { 16 | secret: '123' 17 | } 18 | }; 19 | 20 | await secret(req, res, next); 21 | 22 | expect(next).toHaveBeenCalled(); 23 | }); 24 | 25 | it('Should throw an error if using wrong secret', async () => { 26 | const next = jest.fn(); 27 | const req = { 28 | query: { 29 | secret: '1234' 30 | } 31 | }; 32 | 33 | await secret(req, res, next); 34 | 35 | expect(res.status).toHaveBeenCalledWith(401); 36 | expect(send).toHaveBeenCalledWith('Invalid secret'); 37 | expect(next).not.toHaveBeenCalled(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/abi.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/abi', () => ({ 2 | getV2PoolReserves: jest.fn().mockReturnValue({ reserve0: '10000', reserve1: '20000' }), 3 | getTokenTransfer: jest.fn(), 4 | getTransactionMethodDetails: jest.fn() 5 | })); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/analytics.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/analytics', () => { 2 | return jest.fn().mockImplementation(() => ({ 3 | track: jest.fn(), 4 | shutdown: jest.fn() 5 | })); 6 | }); 7 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/axios.js: -------------------------------------------------------------------------------- 1 | jest.mock('axios', () => ({ 2 | get: jest.fn(), 3 | post: jest.fn(), 4 | delete: jest.fn() 5 | })); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/codeRunner.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/codeRunner', () => ({ 2 | transactionFn: jest.fn() 3 | })); 4 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/crypto.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/crypto', () => ({ 2 | encode: jest.fn().mockReturnValue('1234'), 3 | decode: jest.fn().mockReturnValue('1234'), 4 | decrypt: jest.fn().mockReturnValue('1234'), 5 | encrypt: jest.fn().mockReturnValue('1234'), 6 | firebaseHash: jest.fn().mockReturnValue('1234'), 7 | firebaseVerify: jest.fn().mockReturnValue(true) 8 | })); 9 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/firebase-admin.js: -------------------------------------------------------------------------------- 1 | jest.mock('firebase-admin/auth', () => ({ 2 | getAuth: jest.fn(() => { 3 | return { 4 | verifyIdToken: jest.fn().mockResolvedValue({ user_id: '123' }), 5 | getUser: jest.fn().mockResolvedValue({ email: 'antoine@tryethernal.com' }), 6 | updateUser: jest.fn().mockResolvedValue(), 7 | listUsers: jest.fn().mockResolvedValue({ users: [{ email: 'antoine@tryethernal.com', passwordSalt: 'salt', passwordHash: 'hash' }]}), 8 | createUser: jest.fn() 9 | }; 10 | }) 11 | })); 12 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/firebase.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/firebase', () => { 2 | const actual = jest.requireActual('../../../lib/firebase'); 3 | const mocks = {}; 4 | Object.keys(actual).forEach(k => mocks[k] = jest.fn()); 5 | return mocks; 6 | }); 7 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/flags.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/flags', () => ({ 2 | isSelfHosted: jest.fn(() => true), 3 | isStripeEnabled: jest.fn(() => true), 4 | isPusherEnabled: jest.fn(() => true), 5 | isSendgridEnabled: jest.fn(() => true), 6 | isFirebaseAuthEnabled: jest.fn(() => true), 7 | isGoogleApiEnabled: jest.fn(() => true), 8 | isApproximatedEnabled: jest.fn(() => true), 9 | isDemoEnabled: jest.fn(() => true), 10 | isQuicknodeEnabled: jest.fn(() => true) 11 | })); 12 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/lock.js: -------------------------------------------------------------------------------- 1 | require('./env'); 2 | jest.mock('ioredis'); 3 | jest.mock('redis-semaphore', () => { 4 | return { 5 | Mutex: jest.fn() 6 | } 7 | }); 8 | jest.mock('../../../lib/lock', () => { 9 | return jest.fn().mockImplementation(() => ({ 10 | acquire: jest.fn().mockResolvedValue(true), 11 | release: jest.fn().mockResolvedValue(true) 12 | })) 13 | }); 14 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/logger.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/logger'); 2 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/opsgenie.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/opsgenie', () => ({ 2 | createIncident: jest.fn().mockResolvedValue('OK') 3 | })); 4 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/pm2.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/pm2'); 2 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/processContractVerification.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/processContractVerification'); 2 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/pusher.js: -------------------------------------------------------------------------------- 1 | jest.mock('pusher', () => { 2 | return function() { 3 | return { 4 | trigger: jest.fn().mockResolvedValue(true), 5 | authorizeChannel: jest.fn(() => '1234') 6 | } 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/queue.js: -------------------------------------------------------------------------------- 1 | jest.mock('bullmq'); 2 | jest.mock('../../../lib/queue', () => ({ 3 | enqueue: jest.fn().mockResolvedValue(true), 4 | bulkEnqueue: jest.fn().mockResolvedValue(true) 5 | })); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/rateLimiter.js: -------------------------------------------------------------------------------- 1 | require('./rpc'); 2 | jest.mock('ioredis'); 3 | jest.mock('rolling-rate-limiter', () => { 4 | return { 5 | RedisRateLimiter: jest.fn().mockImplementation(() => ({ 6 | limit: jest.fn(), 7 | wouldLimitWithInfo: jest.fn() 8 | })) 9 | } 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/sendgrid.js: -------------------------------------------------------------------------------- 1 | jest.mock('@sendgrid/mail', () => ({ 2 | setApiKey: jest.fn(), 3 | send: jest.fn().mockResolvedValue([{ statusCode: 202 }]) 4 | })); 5 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/stripe.js: -------------------------------------------------------------------------------- 1 | jest.mock('stripe', () => { 2 | const StripeSubscription = require('../../fixtures/StripeSubscription'); 3 | 4 | return jest.fn(() => { 5 | return { 6 | customers: { 7 | create: jest.fn().mockResolvedValue({ id: '1234' }), 8 | retrieve: jest.fn() 9 | }, 10 | webhooks: { 11 | constructEvent: jest.fn().mockReturnValue({ type: 'invoice.payment_succeeded', data: { object: {}}}) 12 | }, 13 | paymentIntents: { 14 | retrieve: () => { 15 | return new Promise((resolve) => resolve({ payment_method: 'car_123' })); 16 | } 17 | }, 18 | subscriptions: { 19 | retrieve: () => { 20 | return new Promise((resolve) => resolve(StripeSubscription)) 21 | } 22 | } 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/stripeLib.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/stripe', () => ({ 2 | handleStripeSubscriptionUpdate: jest.fn(), 3 | handleStripeSubscriptionDeletion: jest.fn(), 4 | handleStripePaymentSucceeded: jest.fn() 5 | })); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/trace.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/trace', () => ({ 2 | parseTrace: jest.fn().mockResolvedValue([{ op: 'CALL' }, { op: 'CALLSTATIC' }]), 3 | processTrace: jest.fn() 4 | })); 5 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/transactions.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/transactions', () => ({ 2 | processTransactions: jest.fn().mockResolvedValue() 3 | })); 4 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/utils.js: -------------------------------------------------------------------------------- 1 | require('./rateLimiter') 2 | jest.mock('../../../lib/utils', () => { 3 | const actual = jest.requireActual('../../../lib/utils'); 4 | return { 5 | getEnv: jest.fn().mockReturnValue('test'), 6 | getFunctionSignatureForTransaction: jest.fn(), 7 | sanitize: actual.sanitize, 8 | withTimeout: jest.fn(cb => new Promise(resolve => resolve(cb))), 9 | processRawRpcObject: jest.fn(data => data), 10 | validateBNString: jest.fn().mockReturnValue(true), 11 | sleep: jest.fn().mockResolvedValue() 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /run/tests/mocks/lib/yasold.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../lib/yasold', () => ({ 2 | decompileToText: jest.fn().mockReturnValue('asm') 3 | })); 4 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/auth', () => { 2 | return jest.fn((req, res, next) => { 3 | req.body.data = { 4 | ...(req.body.data || {}), 5 | uid: '123', 6 | user: { id: 1 } 7 | }; 8 | next(); 9 | }) 10 | }); 11 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/browserSync.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/browserSync', () => { 2 | return (req, res, next) => { 3 | next(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/passportLocalStrategy.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/passportLocalStrategy', () => { 2 | return (req, res, next) => { 3 | req.user = { id : 1 }; 4 | next(); 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/quicknode.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/quicknode', () => { 2 | return (req, res, next) => { 3 | next(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/secret.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/auth', () => { 2 | return (req, res, next) => { 3 | next(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/selfHosted.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/selfHosted', () => { 2 | return (req, res, next) => { 3 | next(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/taskAuth.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../middlewares/taskAuth', () => { 2 | return (req, res, next) => { 3 | next(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /run/tests/mocks/middlewares/workspaceAuth.js: -------------------------------------------------------------------------------- 1 | require('../lib/firebase'); 2 | jest.mock('../../../middlewares/workspaceAuth', () => { 3 | return jest.fn((req, res, next) => { 4 | req.query.firebaseUserId = '123'; 5 | req.query.workspace = { id: 1, name: 'My Workspace' } 6 | next(); 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /run/tests/mocks/models/Block.js: -------------------------------------------------------------------------------- 1 | const Block = { 2 | findByPk: jest.fn(), 3 | findOne: jest.fn(), 4 | rawAttributes: { number: 'number' } 5 | }; 6 | 7 | module.exports = { Block }; 8 | -------------------------------------------------------------------------------- /run/tests/mocks/models/Contract.js: -------------------------------------------------------------------------------- 1 | const Contract = { 2 | findOne: jest.fn(), 3 | findByPk: jest.fn() 4 | }; 5 | 6 | module.exports = { Contract }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/Explorer.js: -------------------------------------------------------------------------------- 1 | const explorer = { 2 | name: 'Ethernal', 3 | slug: 'ethernal', 4 | toJSON: () => ({ name: 'Ethernal', slug: 'ethernal' }) 5 | }; 6 | 7 | const Explorer = { 8 | findAndCountAll: jest.fn(), 9 | findAll: jest.fn(), 10 | findOne: jest.fn(), 11 | findByPk: jest.fn(), 12 | findBySlug: jest.fn().mockResolvedValue(explorer), 13 | findByDomain: jest.fn().mockResolvedValue(explorer), 14 | safeCreateExplorer: jest.fn().mockResolvedValue(explorer), 15 | }; 16 | 17 | module.exports = { 18 | Explorer: Explorer, 19 | explorer: explorer 20 | }; 21 | -------------------------------------------------------------------------------- /run/tests/mocks/models/ExplorerDomain: -------------------------------------------------------------------------------- 1 | const ExplorerDomain = { 2 | findByPk: jest.fn(), 3 | findOne: jest.fn() 4 | }; 5 | 6 | module.exports = { ExplorerDomain }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/ExplorerFaucet.js: -------------------------------------------------------------------------------- 1 | const ExplorerFaucet = { 2 | findOne: jest.fn(), 3 | findByPk: jest.fn() 4 | }; 5 | 6 | module.exports = { ExplorerFaucet }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/ExplorerV2Dex.js: -------------------------------------------------------------------------------- 1 | const ExplorerV2Dex = { 2 | findByPk: jest.fn(), 3 | findOne: jest.fn() 4 | }; 5 | 6 | module.exports = { ExplorerV2Dex }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/StripePlan.js: -------------------------------------------------------------------------------- 1 | const StripePlan = { 2 | findAll: jest.fn(), 3 | findOne: jest.fn() 4 | }; 5 | 6 | module.exports = { StripePlan }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/StripeSubscription.js: -------------------------------------------------------------------------------- 1 | const StripeSubscription = { 2 | findByPk: jest.fn(), 3 | findAll: jest.fn() 4 | }; 5 | 6 | module.exports = { StripeSubscription }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/TokenTransfer.js: -------------------------------------------------------------------------------- 1 | const TokenTransfer = { 2 | findByPk: jest.fn() 3 | }; 4 | 5 | module.exports = { TokenTransfer }; 6 | -------------------------------------------------------------------------------- /run/tests/mocks/models/Transaction.js: -------------------------------------------------------------------------------- 1 | const Transaction = { 2 | findByPk: jest.fn(), 3 | findOne: jest.fn() 4 | }; 5 | 6 | module.exports = { Transaction }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/TransactionLog.js: -------------------------------------------------------------------------------- 1 | const TransactionLog = { 2 | findAll: jest.fn() 3 | }; 4 | 5 | module.exports = { TransactionLog }; 6 | -------------------------------------------------------------------------------- /run/tests/mocks/models/TransactionReceipt.js: -------------------------------------------------------------------------------- 1 | const TransactionReceipt = { 2 | rawAttributes: { blockNumber: 1 } 3 | } 4 | 5 | module.exports = { TransactionReceipt }; 6 | -------------------------------------------------------------------------------- /run/tests/mocks/models/User.js: -------------------------------------------------------------------------------- 1 | const { workspace } = require('./Workspace'); 2 | 3 | const user = { 4 | id: 1, 5 | workspaces: [workspace], 6 | isPremium: true, 7 | safeCreateWorkspace: jest.fn().mockResolvedValue({ 8 | id: 1, 9 | name: 'My Workspace', 10 | toJSON: () => ({ id: 1, name: 'My Workspace' }) 11 | }), 12 | getWorkspaceByName: jest.fn().mockResolvedValue({ id: 1, name: 'My Workspace' }), 13 | getWorkspaces: jest.fn().mockResolvedValue([workspace]), 14 | update: jest.fn(), 15 | toJSON: jest.fn().mockResolvedValue({ id: 1, workspaces: [workspace] }), 16 | findByPk: jest.fn() 17 | }; 18 | 19 | const User = { 20 | findOne: jest.fn(), 21 | findByAuthIdWithWorkspace: jest.fn().mockResolvedValue(user), 22 | findByPk: jest.fn().mockResolvedValue(user), 23 | findByAuthId: jest.fn().mockResolvedValue(user), 24 | findByStripeCustomerId: jest.fn().mockResolvedValue(user) 25 | }; 26 | 27 | module.exports = { 28 | User: User, 29 | user: user 30 | }; 31 | -------------------------------------------------------------------------------- /run/tests/mocks/models/V2DexPair.js: -------------------------------------------------------------------------------- 1 | const V2DexPair = { 2 | findOne: jest.fn(), 3 | findByPk: jest.fn() 4 | }; 5 | 6 | module.exports = { V2DexPair }; 7 | -------------------------------------------------------------------------------- /run/tests/mocks/models/V2DexPoolReserve.js: -------------------------------------------------------------------------------- 1 | const V2DexPoolReserve = { 2 | bulkCreate: jest.fn() 3 | }; 4 | 5 | module.exports = { V2DexPoolReserve }; 6 | -------------------------------------------------------------------------------- /run/tests/mocks/queues.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../queues', () => ({ 2 | test: { 3 | addBulk: jest.fn() 4 | } 5 | })); 6 | -------------------------------------------------------------------------------- /run/tests/setupJestMocks.js: -------------------------------------------------------------------------------- 1 | jest.mock('ioredis'); 2 | jest.mock('@sentry/node'); 3 | require('./mocks/lib/queue'); 4 | -------------------------------------------------------------------------------- /run/tests/webhooks/stripe.test.js: -------------------------------------------------------------------------------- 1 | require('../mocks/lib/queue'); 2 | require('../mocks/models'); 3 | require('../mocks/lib/stripe'); 4 | require('../mocks/lib/env'); 5 | require('../mocks/lib/flags'); 6 | require('../mocks/lib/stripeLib'); 7 | require('../mocks/lib/firebase'); 8 | 9 | const stripeLib = require('../../lib/stripe'); 10 | 11 | const supertest = require('supertest'); 12 | const app = require('../../app'); 13 | const request = supertest(app); 14 | 15 | const BASE_URL = '/webhooks/stripe'; 16 | 17 | describe(`POST ${BASE_URL}`, () => { 18 | beforeEach(() => jest.clearAllMocks()); 19 | 20 | it('Should return a 200 status and call the payment succeded handler', (done) => { 21 | request.post(BASE_URL) 22 | .set('stripe-signature', '123') 23 | .expect(200) 24 | .then(() => { 25 | expect(stripeLib.handleStripePaymentSucceeded).toHaveBeenCalledWith({}); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /run/webhooks/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { isStripeEnabled, isQuicknodeEnabled } = require('../lib/flags'); 4 | 5 | if (isStripeEnabled()) { 6 | const stripe = require('./stripe'); 7 | router.use('/stripe', stripe); 8 | } 9 | 10 | if (isQuicknodeEnabled()) { 11 | const quicknode = require('./quicknode'); 12 | router.use('/quicknode', quicknode); 13 | } 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /run/workers/highPriority.js: -------------------------------------------------------------------------------- 1 | require('../instrument'); 2 | const Sentry = require('@sentry/node'); 3 | const { initializeApp } = require('firebase-admin/app'); 4 | initializeApp(); 5 | const { Worker, MetricsTime } = require('bullmq'); 6 | const connection = require('../lib/redis'); 7 | const jobs = require('../jobs'); 8 | const logger = require('../lib/logger'); 9 | const priorities = require('./priorities.js'); 10 | const { managedWorkerError } = require('../lib/errors'); 11 | 12 | priorities['high'].forEach(jobName => { 13 | const worker = new Worker( 14 | jobName, 15 | job => { 16 | return Sentry.startSpan( 17 | { name: jobName }, () => { 18 | return jobs[jobName](job) 19 | } 20 | ) 21 | }, 22 | { 23 | concurrency: 50, 24 | connection, 25 | metrics: { 26 | maxDataPoints: MetricsTime.ONE_WEEK * 2, 27 | } 28 | } 29 | ); 30 | worker.on('failed', (job, error) => managedWorkerError(error, jobName, job.data, 'highPriority')); 31 | 32 | logger.info(`Started worker "${jobName}" - Priority: high`); 33 | }); 34 | -------------------------------------------------------------------------------- /run/workers/lowPriority.js: -------------------------------------------------------------------------------- 1 | require('../instrument'); 2 | const Sentry = require('@sentry/node'); 3 | const { Worker, MetricsTime } = require('bullmq'); 4 | const connection = require('../lib/redis'); 5 | const jobs = require('../jobs'); 6 | const logger = require('../lib/logger'); 7 | const priorities = require('./priorities.js'); 8 | const { managedWorkerError } = require('../lib/errors'); 9 | 10 | priorities['low'].forEach(jobName => { 11 | const worker = new Worker( 12 | jobName, 13 | job => { 14 | return Sentry.startSpan( 15 | { name: jobName }, () => { 16 | return jobs[jobName](job) 17 | } 18 | ) 19 | }, 20 | { 21 | concurrency: 10, 22 | maxStalledCount: 5, 23 | connection, 24 | metrics: { 25 | maxDataPoints: MetricsTime.ONE_WEEK * 2, 26 | } 27 | } 28 | ); 29 | worker.on('failed', (job, error) => managedWorkerError(error, jobName, job.data, 'lowPriority')); 30 | 31 | logger.info(`Started worker "${jobName}" - Priority: low`); 32 | }); 33 | -------------------------------------------------------------------------------- /run/workers/mediumPriority.js: -------------------------------------------------------------------------------- 1 | require('../instrument'); 2 | const Sentry = require('@sentry/node'); 3 | const { Worker, MetricsTime } = require('bullmq'); 4 | const connection = require('../lib/redis'); 5 | const jobs = require('../jobs'); 6 | const logger = require('../lib/logger'); 7 | const priorities = require('./priorities.js'); 8 | const { managedWorkerError } = require('../lib/errors'); 9 | 10 | priorities['medium'].forEach(jobName => { 11 | const worker = new Worker( 12 | jobName, 13 | job => { 14 | return Sentry.startSpan( 15 | { name: jobName }, () => { 16 | return jobs[jobName](job) 17 | } 18 | ) 19 | }, 20 | { 21 | maxStalledCount: 5, 22 | lockDuration: 300000, 23 | concurrency: 50, 24 | connection, 25 | metrics: { 26 | maxDataPoints: MetricsTime.ONE_WEEK * 2, 27 | } 28 | } 29 | ); 30 | worker.on('failed', (job, error) => managedWorkerError(error, jobName, job.data, 'mediumPriority')); 31 | 32 | logger.info(`Started worker "${jobName}" - Priority: medium`); 33 | }); 34 | -------------------------------------------------------------------------------- /run/workers/processHistoricalBlocks.js: -------------------------------------------------------------------------------- 1 | require('../instrument'); 2 | const Sentry = require('@sentry/node'); 3 | const { Worker, MetricsTime } = require('bullmq'); 4 | const connection = require('../lib/redis'); 5 | const jobs = require('../jobs'); 6 | const { managedWorkerError } = require('../lib/errors'); 7 | const { getHistoricalBlocksProcessingConcurrency } = require('../lib/env'); 8 | 9 | const worker = new Worker( 10 | 'processHistoricalBlocks', 11 | job => { 12 | return Sentry.startSpan( 13 | { name: 'processHistoricalBlocks' }, () => { 14 | return jobs['processBlock'](job) 15 | } 16 | ) 17 | }, 18 | { 19 | concurrency: getHistoricalBlocksProcessingConcurrency(), 20 | connection, 21 | metrics: { 22 | maxDataPoints: MetricsTime.ONE_WEEK * 2, 23 | } 24 | } 25 | ); 26 | worker.on('failed', (job, error) => managedWorkerError(error, 'processHistoricalBlocks', job.data, 'highPriority')); 27 | 28 | module.exports = worker; 29 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:vue/base', 4 | 'plugin:vuetify/base' 5 | ], 6 | rules: { 7 | 'vue/multi-word-component-names': 'off' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Demo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/Embedded.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/SSO.vue: -------------------------------------------------------------------------------- 1 | 11 | 41 | -------------------------------------------------------------------------------- /src/SetupRoot.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/abis/selectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "erc20": { 3 | "functions": [ 4 | "0x18160ddd", 5 | "0x70a08231", 6 | "0xa9059cbb", 7 | "0xdd62ed3e", 8 | "0x095ea7b3", 9 | "0x23b872dd" 10 | ], 11 | "events": [ 12 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 13 | "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925" 14 | ], 15 | "errors": [] 16 | }, 17 | 18 | "erc721": { 19 | "functions": [ 20 | "0x70a08231", 21 | "0x6352211e", 22 | "0xb88d4fde", 23 | "0x42842e0e", 24 | "0x23b872dd", 25 | "0x095ea7b3", 26 | "0xa22cb465", 27 | "0x081812fc", 28 | "0xe985e9c5" 29 | ], 30 | "events": [ 31 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 32 | "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", 33 | "0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31" 34 | ], 35 | "errors": [] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/analytics.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/analytics.webp -------------------------------------------------------------------------------- /src/assets/contract.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/contract.webp -------------------------------------------------------------------------------- /src/assets/demo_compressed.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/demo_compressed.mp4 -------------------------------------------------------------------------------- /src/assets/gallery.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/gallery.webp -------------------------------------------------------------------------------- /src/assets/styles/theme.css: -------------------------------------------------------------------------------- 1 | /* Core Theme Classes */ 2 | .theme-background { 3 | background-color: var(--app-bar-background) !important; 4 | } 5 | 6 | /* Base Theme Colors */ 7 | :root { 8 | --app-background: var(--card-background); 9 | --text-color-primary: var(--text-primary); 10 | --text-color-secondary: var(--text-secondary); 11 | --border-color-theme: var(--border-color); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/transactions.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/transactions.webp -------------------------------------------------------------------------------- /src/assets/transfers.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/transfers.webp -------------------------------------------------------------------------------- /src/assets/verification.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryethernal/ethernal/9e58b3c7e19fe4ee6caa8936c3432ce69bd8aca2/src/assets/verification.webp -------------------------------------------------------------------------------- /src/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export const bus = new Vue(); 4 | -------------------------------------------------------------------------------- /src/components/AddressTransactionsList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/components/BlockTransactionList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /src/components/UpgradeLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 18 | 23 | -------------------------------------------------------------------------------- /src/components/WalletConnectorMirror.vue: -------------------------------------------------------------------------------- 1 | 11 | 29 | -------------------------------------------------------------------------------- /src/components/base/BaseChipGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/composables/useContrastingColor.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useTheme } from 'vuetify'; 3 | import { getBestContrastingColor } from '@/lib/utils'; 4 | 5 | export function useContrastingColor(backgroundColor = null) { 6 | const theme = useTheme(); 7 | 8 | const contrastingColor = computed(() => { 9 | return getBestContrastingColor(backgroundColor || theme.current.value.colors.background, theme.current.value.colors); 10 | }); 11 | 12 | return { 13 | contrastingColor 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/config/firebase.js: -------------------------------------------------------------------------------- 1 | export const FIREBASE_CONFIG = { 2 | apiKey: process.env.VUE_APP_FIREBASE_API_KEY, 3 | authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN, 4 | databaseURL: process.env.VUE_APP_FIREBASE_DATABASE_URL, 5 | projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID, 6 | storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET, 7 | messagingSenderId: process.env.VUE_APP_MESSAGING_SENDER_ID, 8 | appId: process.env.VUE_APP_FIREBASE_APP_ID 9 | }; 10 | -------------------------------------------------------------------------------- /src/filters/dt.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | export default { 4 | shortDate(timestamp) { 5 | if (!timestamp) return ''; 6 | return DateTime 7 | .fromISO(timestamp) 8 | .toFormat('LL/dd h:mm:ss a'); 9 | }, 10 | 11 | fromNow(timestamp) { 12 | if (!timestamp) return ''; 13 | 14 | const relative = DateTime 15 | .fromISO(timestamp) 16 | .toRelative({ round: true }); 17 | 18 | return relative.match(/seconds/) ? 'a few seconds ago' : relative; 19 | }, 20 | 21 | format(timestamp, format) { 22 | if (!timestamp) return ''; 23 | return DateTime 24 | .fromISO(timestamp) 25 | .toFormat(format); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/metamask.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers'); 2 | 3 | export const sendTransaction = ({ ethereum, address, abi, signature, params, options }) => { 4 | return new Promise((resolve, reject) => { 5 | const provider = new ethers.providers.Web3Provider(ethereum, 'any'); 6 | const signer = provider.getSigner(); 7 | const contract = new ethers.Contract(address, abi, signer); 8 | contract.populateTransaction[signature](...params, options) 9 | .then(transaction => { 10 | const params = { 11 | ...transaction, 12 | value: transaction.value.toHexString() 13 | }; 14 | window.ethereum.request({ 15 | method: 'eth_sendTransaction', 16 | params: [params] 17 | }) 18 | .then(resolve) 19 | .catch(reject); 20 | }) 21 | .catch(reject); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/mixins/user.js: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/stores/user'; 2 | import { useCurrentWorkspaceStore } from '@/stores/currentWorkspace' 3 | 4 | export default { 5 | computed: { 6 | isUserAdmin() { 7 | const userStore = useUserStore(); 8 | const currentWorkspaceStore = useCurrentWorkspaceStore(); 9 | 10 | return userStore.id && userStore.id === currentWorkspaceStore.userId; 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/plugins/bus.js: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | 3 | const eventBus = reactive({ events: {} }); 4 | 5 | eventBus.on = (event, callback) => { 6 | eventBus.events[event] = eventBus.events[event] || []; 7 | eventBus.events[event].push(callback); 8 | }; 9 | 10 | eventBus.emit = (event, data) => { 11 | if (eventBus.events[event]) { 12 | eventBus.events[event].forEach(callback => callback(data)); 13 | } 14 | }; 15 | 16 | export default eventBus; 17 | -------------------------------------------------------------------------------- /src/plugins/demoRouter.js: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from 'vue-router'; 2 | import DemoExplorerSetup from '../components/DemoExplorerSetup.vue'; 3 | import DemoExplorerSetupEmbedded from '../components/DemoExplorerSetupEmbedded.vue'; 4 | 5 | const routes = [ 6 | { path: '/demo', component: DemoExplorerSetup }, 7 | { path: '/demoEmbedded', component: DemoExplorerSetupEmbedded }, 8 | { path: '/:pathMatch(.*)*', name: 'not-found', component: DemoExplorerSetup } 9 | ]; 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(), 13 | routes: routes 14 | }); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /src/plugins/embeddedRouter.js: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from 'vue-router'; 2 | import DemoExplorerSetupEmbedded from '../components/DemoExplorerSetupEmbedded.vue'; 3 | import TransactionTraceEmbedded from '../components/TransactionTraceEmbedded.vue'; 4 | 5 | const routes = [ 6 | { path: '/embedded/demoSetup', component: DemoExplorerSetupEmbedded }, 7 | { path: '/embedded/transactionTrace/:hash', props: true, component: TransactionTraceEmbedded }, 8 | { path: '/:pathMatch(.*)*', name: 'not-found', component: DemoExplorerSetupEmbedded } 9 | ]; 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(), 13 | routes: routes 14 | }); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /src/plugins/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import 'firebase/database'; 4 | import 'firebase/functions'; 5 | 6 | import { FIREBASE_CONFIG } from '../config/firebase.js'; 7 | firebase.initializeApp(FIREBASE_CONFIG); 8 | 9 | const _auth = firebase.auth; 10 | 11 | export const dbPlugin = { 12 | install(app) { 13 | const $db = { 14 | getIdToken: function() { 15 | if (!_auth().currentUser) return new Promise(resolve => resolve(null)); 16 | return _auth().currentUser.getIdToken(); 17 | } 18 | }; 19 | 20 | app.config.globalProperties.$db = $db; 21 | } 22 | }; 23 | 24 | if (process.env.NODE_ENV == 'development') { 25 | _auth().useEmulator(process.env.VUE_APP_AUTH_HOST); 26 | } 27 | 28 | export const auth = _auth; 29 | -------------------------------------------------------------------------------- /src/plugins/iconify.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue'; 2 | import { Icon } from '@iconify/vue'; 3 | 4 | export const iconify = (set) => ({ 5 | component: (props) => 6 | h(Icon, { 7 | icon: `${set}:${props.icon}`, 8 | disabled: props.disabled, 9 | }) 10 | }); 11 | -------------------------------------------------------------------------------- /src/plugins/posthog.js: -------------------------------------------------------------------------------- 1 | import posthog from "posthog-js"; 2 | import { useEnvStore } from '../stores/env'; 3 | 4 | export default { 5 | install(app) { 6 | const envStore = useEnvStore(); 7 | const $posthog = envStore.hasAnalyticsEnabled ? 8 | posthog.init( 9 | envStore.postHogApiKey, { api_host: envStore.postHogApiHost } 10 | ) : 11 | { capture: () => {}, reset: () => {}, identify: () => {} } 12 | 13 | app.config.globalProperties.$posthog = $posthog; 14 | app.provide('$posthog', $posthog); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/plugins/setupRouter.js: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from 'vue-router'; 2 | import SelfHostedSetup from '../components/SelfHostedSetup.vue'; 3 | 4 | const routes = [ 5 | { path: '/', component: SelfHostedSetup }, 6 | { path: '/setup', component: SelfHostedSetup }, 7 | { path: '/:pathMatch(.*)*', name: 'not-found', component: SelfHostedSetup } 8 | ]; 9 | 10 | const setupRouter = createRouter({ 11 | history: createWebHistory(), 12 | routes 13 | }); 14 | 15 | export default setupRouter; 16 | -------------------------------------------------------------------------------- /src/plugins/ssoRouter.js: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from 'vue-router'; 2 | import SSO from '../SSO.vue'; 3 | 4 | const routes = [ 5 | { path: '/sso', component: SSO }, 6 | { path: '/:pathMatch(.*)*', name: 'not-found', component: SSO } 7 | ]; 8 | 9 | const router = createRouter({ 10 | history: createWebHistory(), 11 | routes: routes 12 | }); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/stores/customisation.js: -------------------------------------------------------------------------------- 1 | import createWorkerBox from 'workerboxjs'; 2 | import { defineStore } from 'pinia'; 3 | 4 | export const useCustomisationStore = defineStore('customisation', { 5 | state: () => ({ 6 | packages: {}, 7 | functions: {}, 8 | worker: null 9 | }), 10 | actions: { 11 | async updateCustomisations(customisations) { 12 | for (const [alias, name] of Object.entries(customisations.packages)) { 13 | this.packages[alias] = await import(`https://cdn.skypack.dev/${name}?min`); 14 | } 15 | for (const [alias, fn] of Object.entries(customisations.functions)) { 16 | this.functions[alias] = fn; 17 | } 18 | this.worker = await createWorkerBox(); 19 | }, 20 | alternateLink(address) { 21 | if (!this.functions.alternateLink || !this.worker) return new Promise(resolve => resolve(null)); 22 | 23 | return this.worker.run(this.functions.alternateLink, 24 | { 25 | address, 26 | swisstronikUtils: this.packages.swisstronikUtils 27 | } 28 | ); 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/stores/explorer.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useExplorerStore = defineStore('explorer', { 4 | state: () => ({ 5 | id: null, 6 | slug: null, 7 | name: null, 8 | rpcServer: null, 9 | token: 'ETH', 10 | l1Explorer: null, 11 | domain: null, 12 | domains: [], 13 | themes: {}, 14 | admin: {}, 15 | workspace: {}, 16 | faucet: null, 17 | v2Dex: null, 18 | gasAnalyticsEnabled: null, 19 | isDemo: false, 20 | totalSupply: null, 21 | adsEnabled: null 22 | }), 23 | 24 | actions: { 25 | updateExplorer(explorer) { 26 | if (!explorer) 27 | this.$reset(); 28 | 29 | this.$patch(explorer); 30 | } 31 | }, 32 | 33 | getters: { 34 | mainDomain() { 35 | return this.domains.length ? this.domains[0] : this.domain; 36 | } 37 | } 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /src/styles/theme-transitions.scss: -------------------------------------------------------------------------------- 1 | body { 2 | transition: background-color 0.3s ease, color 0.1s ease; 3 | } 4 | 5 | // Common Vuetify components theme transitions 6 | .v-app-bar, 7 | .v-card, 8 | .v-sheet, 9 | .v-list, 10 | .v-navigation-drawer, 11 | .v-toolbar, 12 | .v-footer, 13 | .v-tabs, 14 | .v-btn, 15 | .v-menu, 16 | .v-dialog, 17 | .v-expansion-panel { 18 | transition: background-color 0.3s ease, 19 | border-color 0.3s ease, 20 | color 0.1s ease; 21 | 22 | // Handle nested text elements 23 | .v-list-item__title, 24 | .v-list-item__subtitle, 25 | .v-card__title, 26 | .v-card__subtitle, 27 | .v-card__text, 28 | .v-toolbar__title, 29 | .v-tab, 30 | .v-btn__content { 31 | transition: color 0.1s ease; 32 | } 33 | } 34 | 35 | // Form components 36 | .v-text-field, 37 | .v-select, 38 | .v-textarea, 39 | .v-input { 40 | transition: background-color 0.3s ease, 41 | border-color 0.3s ease, 42 | color 0.1s ease; 43 | 44 | .v-label, 45 | input, 46 | textarea { 47 | transition: color 0.1s ease; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/styles/theme-variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // Light theme defaults 3 | --app-background: #FFFFFF; 4 | --text-primary: rgba(0, 0, 0, 0.87); 5 | --text-secondary: rgba(0, 0, 0, 0.6); 6 | --border-color: rgba(0, 0, 0, 0.12); 7 | --card-background: #FFFFFF; 8 | --app-bar-background: #FFFFFF; 9 | --hover-background: rgba(0, 0, 0, 0.04); 10 | --divider-color: rgba(0, 0, 0, 0.12); 11 | } 12 | 13 | .v-theme--dark { 14 | // Dark theme overrides 15 | --app-background: #121212; 16 | --text-primary: rgba(255, 255, 255, 0.87); 17 | --text-secondary: rgba(255, 255, 255, 0.6); 18 | --border-color: rgba(255, 255, 255, 0.12); 19 | --card-background: #1E1E1E; 20 | --app-bar-background: #1E1E1E; 21 | --hover-background: rgba(255, 255, 255, 0.08); 22 | --divider-color: rgba(255, 255, 255, 0.12); 23 | } 24 | 25 | // Force background color in dark mode 26 | .v-theme--dark .v-app-bar, 27 | .v-theme--dark .v-navigation-drawer { 28 | background-color: var(--app-bar-background) !important; 29 | } 30 | 31 | // Force text colors in dark mode 32 | .v-theme--dark .v-app-bar, 33 | .v-theme--dark .v-navigation-drawer { 34 | color: var(--text-primary) !important; 35 | } 36 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true 6 | }, 7 | globals: { 8 | mount: 'readonly', 9 | createTestingPinia: 'readonly', 10 | flushPromises: 'readonly', 11 | server: 'readonly', 12 | pusher: 'readonly', 13 | fromWei: 'readonly', 14 | vi: 'readonly' 15 | } 16 | }; -------------------------------------------------------------------------------- /tests/unit/SSO.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import SSO from '@/SSO.vue'; 4 | 5 | describe('SSO.vue', () => { 6 | it('Should show a link', async () => { 7 | vi.spyOn(server, 'getCurrentUser') 8 | .mockResolvedValue({ data: { user: { id: 1 }}}); 9 | 10 | const wrapper = mount(SSO, { 11 | global: { 12 | mocks: { 13 | $route: { query: { explorerId: 1 }}, 14 | }, 15 | stubs: ['Explorer'] 16 | } 17 | }); 18 | await flushPromises(); 19 | 20 | expect(wrapper.html()).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/SSO.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`SSO.vue > Should show a link 1`] = ` 4 | "
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 |
" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/unit/components/Account.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import Account from '@/components/Account.vue'; 4 | 5 | describe('Account.vue', () => { 6 | it('Should load the account tab', async () => { 7 | const wrapper = mount(Account, { 8 | global: { 9 | plugins: [createTestingPinia({ initialState: { user: { apiToken: '1234' } } })] 10 | } 11 | }); 12 | await flushPromises(); 13 | 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/components/Accounts.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import Accounts from '@/components/Accounts.vue'; 4 | 5 | describe('Accounts.vue', () => { 6 | it('Should load stored accounts', async () => { 7 | vi.spyOn(server, 'getAccounts') 8 | .mockResolvedValue({ data: { total: 1, items: [{ address: '0x123' }]}}); 9 | vi.spyOn(server, 'getAccountBalance') 10 | .mockResolvedValue('1000'); 11 | 12 | const wrapper = mount(Accounts, { 13 | global: { 14 | stubs: ['Hash-Link'], 15 | plugins: [createTestingPinia()], 16 | } 17 | }); 18 | await flushPromises(); 19 | 20 | expect(wrapper.html()).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/components/AddressAssets.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import AddressAssets from '@/components/AddressAssets.vue'; 4 | 5 | const stubs = ['AddressTokenAssets', 'NFTGallery']; 6 | 7 | describe('AddressAssets.vue', () => { 8 | it('Should load address assets', async () => { 9 | const wrapper = mount(AddressAssets, { 10 | props: { 11 | address: '0x1234567890123456789012345678901234567890' 12 | }, 13 | global: { 14 | stubs 15 | } 16 | }); 17 | await flushPromises(); 18 | 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/components/AddressTokenAssets.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import AddressTokenAssets from '@/components/AddressTokenAssets.vue'; 4 | 5 | const stubs = ['HashLink']; 6 | 7 | describe('AddressTokenAssets.vue', () => { 8 | it('Should load address token assets', async () => { 9 | vi.spyOn(server, 'getTokenBalances').mockResolvedValueOnce({ 10 | data: [{ 11 | tokenContract: { 12 | address: '0x1234567890123456789012345678901234567890', 13 | tokenSymbol: 'ETH', 14 | tokenDecimals: 18, 15 | tokenName: 'Ether' 16 | }, 17 | token: '0x1234567890123456789012345678901234567890', 18 | currentBalance: '1000000000000000000' 19 | }] 20 | }); 21 | 22 | const wrapper = mount(AddressTokenAssets, { 23 | props: { 24 | address: '0x1234567890123456789012345678901234567890' 25 | }, 26 | global: { 27 | stubs 28 | } 29 | }); 30 | await flushPromises(); 31 | 32 | expect(wrapper.html()).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/components/AddressTransactionsList.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import AddressTransactionsList from '@/components/AddressTransactionsList.vue'; 4 | 5 | describe('AddressTransactionsList.vue', () => { 6 | it('Should display the list', async () => { 7 | const wrapper = mount(AddressTransactionsList, { 8 | props: { 9 | address: '0x123' 10 | }, 11 | global: { 12 | stubs: ['Hash-Link', 'Transactions-List'] 13 | } 14 | }); 15 | 16 | await flushPromises(); 17 | expect(wrapper.html()).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/unit/components/BlockTransactionList.spec.js: -------------------------------------------------------------------------------- 1 | import BlockTransactionList from '@/components/BlockTransactionList.vue'; 2 | 3 | describe('BlockTransactionList', () => { 4 | it('should render block transactions list', () => { 5 | const wrapper = mount(BlockTransactionList, { 6 | props: { 7 | blockNumber: '12345678' 8 | }, 9 | global: { 10 | stubs: ['Transactions-List'] 11 | } 12 | }); 13 | 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/components/Blocks.spec.js: -------------------------------------------------------------------------------- 1 | import Blocks from '@/components/Blocks.vue'; 2 | 3 | describe('Blocks.vue', () => { 4 | it('Should show the blocks list', async () => { 5 | vi.spyOn(server, 'getLast24hGasUtilisationRatio').mockResolvedValue({ data: { gasUtilisationRatio24h: 0.5 } }); 6 | vi.spyOn(server, 'getLast24hTotalGasUsed').mockResolvedValue({ data: { totalGasUsed: 1000000 } }); 7 | vi.spyOn(server, 'getLast24hBurntFees').mockResolvedValue({ data: { burntFees: 10000000000000000000 } }); 8 | 9 | const wrapper = mount(Blocks, { 10 | global: { 11 | stubs: ['Block-List', 'Stat-Number'] 12 | } 13 | }); 14 | await new Promise(process.nextTick); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/components/CreateExplorerDexModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | import CreateExplorerDexModal from '@/components/CreateExplorerDexModal.vue'; 3 | 4 | describe('CreateExplorerDexModal.vue', () => { 5 | it('Should show dex creation modal', async () => { 6 | const wrapper = mount(CreateExplorerDexModal, { 7 | global: { 8 | plugins: [createTestingPinia({ initialState: { explorer: { id: 1 } } })] 9 | } 10 | }); 11 | 12 | wrapper.vm.open(); 13 | 14 | await flushPromises(); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/components/CreateExplorerFaucetModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import CreateExplorerFaucetModal from '@/components/CreateExplorerFaucetModal.vue'; 4 | 5 | describe('CreateExplorerFaucetModal.vue', () => { 6 | it('Should show faucet creation modal', async () => { 7 | const wrapper = mount(CreateExplorerFaucetModal); 8 | 9 | wrapper.vm.open({ explorerId: 1, token: 'ETL' }); 10 | await flushPromises(); 11 | 12 | expect(wrapper.html()).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/components/CreateWorkspaceModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import CreateWorkspaceModal from '@/components/CreateWorkspaceModal.vue'; 4 | 5 | describe('CreateWorkspaceModal.vue', () => { 6 | it('Should let the user create a new workspace', async () => { 7 | const wrapper = mount(CreateWorkspaceModal, { 8 | global: { 9 | stubs: ['Create-Workspace'] 10 | } 11 | }); 12 | 13 | await wrapper.setData({ dialog: true, resolve: vi.fn() }); 14 | 15 | await flushPromises(); 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/components/DemoExplorerSetup.spec.js: -------------------------------------------------------------------------------- 1 | import { h } from "vue"; 2 | import { VApp } from "vuetify/components"; 3 | import DemoExplorerSetup from '@/components/DemoExplorerSetup.vue'; 4 | 5 | describe('DemoExplorerSetup.vue', () => { 6 | it('Should display demo explorer setup page', async () => { 7 | vi.spyOn(server, 'getCurrentUser').mockResolvedValue({ data: { id: 1 }}); 8 | const wrapper = mount(VApp, { 9 | slots: { 10 | default: h(DemoExplorerSetup) 11 | } 12 | }); 13 | 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/components/DemoExplorerSetupEmbedded.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import { h } from "vue"; 3 | import { VApp } from "vuetify/components"; 4 | import DemoExplorerSetupEmbedded from '@/components/DemoExplorerSetupEmbedded.vue'; 5 | 6 | describe('DemoExplorerSetupEmbedded.vue', () => { 7 | it('Should display embeddable setup', async () => { 8 | vi.spyOn(server, 'getCurrentUser').mockResolvedValue({ data: { id: 1 }}); 9 | const wrapper = mount(VApp, { 10 | slots: { 11 | default: h(DemoExplorerSetupEmbedded) 12 | } 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | 18 | it('Should display success message with domain', async () => { 19 | vi.spyOn(server, 'getCurrentUser').mockResolvedValue({ data: { id: 1 }}); 20 | const wrapper = mount(VApp, { 21 | slots: { 22 | default: h(DemoExplorerSetupEmbedded, { 23 | domain: 'my.explorer.com' 24 | }) 25 | } 26 | }); 27 | 28 | expect(wrapper.html()).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/components/ERC721Collections.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises' 2 | 3 | import ERC721Collections from '@/components/ERC721Collections.vue'; 4 | 5 | describe('ERC721Collections.vue', () => { 6 | it('Should load & display erc721 contracts', async () => { 7 | vi.spyOn(server, 'getContracts') 8 | .mockResolvedValue({ data: { total: 1, items: [{ address: '0x123', tokenName: 'Ethernal', tokenSymbol: 'ETL', tokenTotalSupply: 100, patterns: ['erc721'] }]}}); 9 | 10 | const wrapper = mount(ERC721Collections, { 11 | global: { 12 | stubs: ['Base-Chip-Group', 'Token-Header', 'ERC20-Token-Holders', 'ERC721-Token-Transfers', 'Contract-Details', 'NFT-Gallery'] 13 | } 14 | }); 15 | await flushPromises(); 16 | 17 | expect(wrapper.html()).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/unit/components/Explorer.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | vi.mock('vue-router', () => ({ 4 | useRouter: vi.fn(), 5 | useRoute: vi.fn(() => ({ query: { tab: 'general' } })) 6 | })); 7 | 8 | import Explorer from '@/components/Explorer.vue'; 9 | 10 | describe('Explorer.vue', () => { 11 | it('Should display explorer sections', async() => { 12 | const wrapper = mount(Explorer, { 13 | global: { 14 | stubs: ['Explorer-General', 'Explorer-Faucet-Settings'] 15 | }, 16 | props: { 17 | id: 1 18 | } 19 | }); 20 | await flushPromises(); 21 | 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerDexParametersModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import ExplorerDexParametersModal from '@/components/ExplorerDexParametersModal.vue'; 4 | 5 | describe('ExplorerDexParametersModal.vue', () => { 6 | it('Should display dex parameters', async () => { 7 | const wrapper = mount(ExplorerDexParametersModal, { 8 | data() { 9 | return { 10 | dialog: true, 11 | resolve: vi.fn().mockResolvedValue(), 12 | transactionTimeout: 20 * 60 * 60, 13 | slippageToleranceInBps: 0.5 * 100 14 | }; 15 | } 16 | }); 17 | await flushPromises(); 18 | 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerDexSettingsDangerZone.spec.js: -------------------------------------------------------------------------------- 1 | import ExplorerDexSettingsDangerZone from '@/components/ExplorerDexSettingsDangerZone.vue'; 2 | 3 | describe('ExplorerDexSettingsDangerZone.vue', () => { 4 | it('Should display danger zone', async () => { 5 | const wrapper = mount(ExplorerDexSettingsDangerZone, { 6 | props: { v2DexId: 1 } 7 | }); 8 | 9 | expect(wrapper.html()).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerDomainDNSInfoModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import ExplorerDomainDNSInfoModal from '@/components/ExplorerDomainDNSInfoModal.vue'; 4 | 5 | describe('ExplorerDomainDNSInfoModal.vue', () => { 6 | it('Should display DNS info', async () => { 7 | const wrapper = mount(ExplorerDomainDNSInfoModal, { 8 | data() { 9 | return { 10 | dialog: true, 11 | resolve: vi.fn().mockResolvedValue(), 12 | domain: 'ethernal.com', 13 | dnsStatus: { 14 | apx_hit: true, 15 | dns_pointed_at: '37.16.1.34', 16 | last_monitored_humanized: '1 minute ago', 17 | is_resolving: true, 18 | has_ssl: false 19 | } 20 | }; 21 | } 22 | }); 23 | await flushPromises(); 24 | 25 | expect(wrapper.html()).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerFaucetAnalytics.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import ExplorerFaucetAnalytics from '@/components/ExplorerFaucetAnalytics.vue'; 4 | 5 | describe('ExplorerFaucetAnalytics.vue', () => { 6 | const stubs = ['Line-Chart']; 7 | 8 | it('Should display faucet analytics', async () => { 9 | vi.spyOn(server, 'getFaucetRequestVolume').mockResolvedValue({ data: [{ date: 1, count: 1 }, { date: 2, count: 1 }]}); 10 | vi.spyOn(server, 'getFaucetTokenVolume').mockResolvedValue({ data: [{ date: 1, amount: '10000000000000000000' }, { date: 1, amount: '20000000000000000000' }]}); 11 | 12 | const wrapper = mount(ExplorerFaucetAnalytics, { 13 | global: { 14 | stubs, 15 | plugins: [createTestingPinia({ 16 | initialState: { 17 | explorer: { 18 | token: 'ETL' 19 | } 20 | } 21 | })] 22 | } 23 | }); 24 | await flushPromises(); 25 | 26 | expect(wrapper.html()).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerFaucetPrivateKeyExportModal.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import ExplorerFaucetPrivateKeyExportModal from '@/components/ExplorerFaucetPrivateKeyExportModal.vue'; 4 | 5 | describe('ExplorerFaucetPrivateKeyExportModal.vue', () => { 6 | it('Should display private key', async () => { 7 | vi.spyOn(server, 'getFaucetPrivateKey').mockResolvedValue({ data: { privateKey: '0x123' }}); 8 | 9 | const wrapper = mount(ExplorerFaucetPrivateKeyExportModal); 10 | wrapper.vm.open({ faucetId: 1 }); 11 | await flushPromises(); 12 | 13 | expect(wrapper.html()).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/components/ExplorerFaucetSettingsDangerZone.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import ExplorerFaucetSettingsDangerZone from '@/components/ExplorerFaucetSettingsDangerZone.vue'; 4 | 5 | describe('ExplorerFaucetSettingsDangerZone.vue', () => { 6 | const stubs = ['Explorer-Faucet-Private-Key-Export-Modal']; 7 | 8 | it('Should display danger zone', async () => { 9 | const wrapper = mount(ExplorerFaucetSettingsDangerZone, { 10 | stubs 11 | }); 12 | await flushPromises(); 13 | 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/components/NewExplorerLink.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import NewExplorerLink from '@/components/NewExplorerLink.vue'; 4 | 5 | describe('NewExplorerLink.vue', () => { 6 | it('Should display a new link form', async () => { 7 | const wrapper = mount(NewExplorerLink, { 8 | global: { 9 | stubs: ['v-autocomplete'] 10 | } 11 | }); 12 | await flushPromises(); 13 | 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | 17 | it('Should display an existing link form', async () => { 18 | vi.spyOn(server, 'searchIcon').mockResolvedValueOnce({ data: { name: 'twitter' }}); 19 | const wrapper = mount(NewExplorerLink, { 20 | props: { 21 | url: 'http://twitter.com', 22 | name: 'twitter', 23 | icon: 'mdi-twitter', 24 | uid: 1234 25 | }, 26 | global: { 27 | stubs: ['v-autocomplete'] 28 | } 29 | }); 30 | await flushPromises(); 31 | 32 | expect(wrapper.html()).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/components/OnboardingModal.spec.js: -------------------------------------------------------------------------------- 1 | import OnboardingModal from '@/components/OnboardingModal.vue'; 2 | 3 | describe('OnboardingModal.vue', () => { 4 | it('Should let the user create a new workspace', async () => { 5 | vi.spyOn(server, 'initRpcServer').mockResolvedValue(true); 6 | vi.spyOn(server, 'getRpcAccounts') 7 | .mockResolvedValue(['0x123', '0x456']); 8 | 9 | vi.spyOn(server, 'createWorkspace').mockResolvedValue({ data: { 10 | workspace: { 11 | rpcServer: 'https://127.0.0.1', 12 | networkId: 1, 13 | settings: { 14 | gasLimit: 1234567 15 | }, 16 | }, 17 | name: 'Hardhat' 18 | }}); 19 | 20 | const wrapper = mount(OnboardingModal, { 21 | global: { 22 | stubs: ['Create-Workspace'] 23 | } 24 | }); 25 | 26 | await wrapper.setData({ dialog: true }); 27 | 28 | expect(wrapper.html()).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/components/SelfHostedSetupDone.spec.js: -------------------------------------------------------------------------------- 1 | import SelfHostedSetupDone from '@/components/SelfHostedSetupDone.vue'; 2 | 3 | const stubs = []; 4 | 5 | describe('SelfHostedSetupDone.vue', () => { 6 | it('shows explorer domain if present', async () => { 7 | const wrapper = mount(SelfHostedSetupDone, { 8 | props: { 9 | explorer: { id: 1, domains: [{ domain: 'myexplorer.com' }] } 10 | }, 11 | global: { 12 | stubs 13 | } 14 | }); 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | 18 | it('shows slug-based url if no domain', async () => { 19 | const wrapper = mount(SelfHostedSetupDone, { 20 | props: { 21 | explorer: { id: 2, slug: 'slug' } 22 | }, 23 | global: { 24 | stubs 25 | } 26 | }); 27 | expect(wrapper.html()).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/components/Tokens.spec.js: -------------------------------------------------------------------------------- 1 | import Tokens from '@/components/Tokens.vue'; 2 | 3 | describe('Tokens.vue', () => { 4 | it('Should display token contracts', async () => { 5 | vi.spyOn(server, 'getContracts') 6 | .mockResolvedValue({ 7 | data: { 8 | items: [ 9 | { timestamp: '1636557049', address: '0x123', contractName: 'Ethernal Token', tokenName: 'Ethernal', tokenSymbol: 'ETL', tokenDecimals: 18, patterns: ['erc20'] }, 10 | { timestamp: '1636557049', address: '0x124', contractName: 'USD Coin', tokenName: 'USDC', tokenSymbol: 'USDC', tokenDecimals: 6, patterns: ['erc20', 'proxy'] } 11 | ], 12 | total: 2 13 | } 14 | }); 15 | 16 | const wrapper = mount(Tokens, { 17 | global: { 18 | stubs: ['Hash-Link'] 19 | } 20 | }); 21 | await new Promise(process.nextTick); 22 | 23 | expect(wrapper.html()).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/components/Transactions.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import Transactions from '@/components/Transactions.vue'; 4 | 5 | describe('Transactions.vue', () => { 6 | it('Should display the list', async () => { 7 | vi.spyOn(server, 'getTxCount24h').mockResolvedValue(100); 8 | vi.spyOn(server, 'getLast24hTransactionFee').mockResolvedValue(100); 9 | vi.spyOn(server, 'getLast24hAverageTransactionFee').mockResolvedValue(100); 10 | 11 | const wrapper = mount(Transactions, { 12 | props: { address: '0x123' }, 13 | global: { 14 | stubs: ['Transactions-List', 'Stat-Number'] 15 | } 16 | }); 17 | await flushPromises(); 18 | 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/components/UpdateExplorerPlanModal.spec.js: -------------------------------------------------------------------------------- 1 | import UpdateExplorerPlanModal from '@/components/UpdateExplorerPlanModal.vue'; 2 | 3 | describe('UpdateExplorerPlanModal.vue', () => { 4 | it('Should display plan selector', () => { 5 | const wrapper = mount(UpdateExplorerPlanModal, { 6 | global: { 7 | stubs: ['Explorer-Plan-Selector'], 8 | plugins: [createTestingPinia({ 9 | initialState: { 10 | user: { 11 | cryptoPaymentEnabled: false, 12 | canTrial: true 13 | } 14 | } 15 | })] 16 | }, 17 | data() { 18 | return { 19 | dialog: true, 20 | options: { 21 | currentPlanSlug: '100', 22 | pendingCancelation: false 23 | } 24 | } 25 | } 26 | }); 27 | 28 | expect(wrapper.html()).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/components/UpgradeLink.spec.js: -------------------------------------------------------------------------------- 1 | import UpgradeLink from '@/components/UpgradeLink.vue'; 2 | 3 | describe('UpgradeLink.vue', () => { 4 | it('Should display a link to the billing page', () => { 5 | const wrapper = mount(UpgradeLink); 6 | expect(wrapper.html()).toMatchSnapshot(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/unit/components/WorkspaceList.spec.js: -------------------------------------------------------------------------------- 1 | import flushPromises from 'flush-promises'; 2 | 3 | import WorkspaceList from '@/components/WorkspaceList.vue'; 4 | 5 | describe('WorkspaceList.vue', () => { 6 | it('Should display workspaces list', async () => { 7 | vi.spyOn(server, 'getWorkspaces').mockResolvedValue({ data: [ 8 | { id: 1, name: 'workspace 1', rpcServer: 'http://localhost:8545' }, 9 | { id: 2, name: 'workspace 2', rpcServer: 'http://localhost:8545' } 10 | ]}); 11 | 12 | const wrapper = mount(WorkspaceList, { 13 | global: { 14 | stubs: ['Create-Workspace-Modal'], 15 | plugins: [createTestingPinia({ 16 | initialState: { 17 | currentWorkspace: { id: 1 } 18 | } 19 | })], 20 | } 21 | }); 22 | await flushPromises(); 23 | expect(wrapper.html()).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/AdBanner.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AdBanner.vue > Should show the component 1`] = ` 4 | "
5 |
6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/AddressERC20TokenTransfer.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AddressERC20TokenTransfer.vue > Should load address erc20 token transfers 1`] = `""`; 4 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/AddressTokenTransfers.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AddressTokenTransfers.vue > Should load address token transfers 1`] = ` 4 | "
5 | 6 |
7 | " 8 | `; 9 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/AddressTraceSteps.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AddressTraceSteps.vue > Should load address trace steps 1`] = `""`; 4 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/AddressTransactionsList.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`AddressTransactionsList.vue > Should display the list 1`] = `""`; 4 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/CompactTransactionTokenTransfer.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CompactTransactionTokenTransfers.vue > Should fetch and display token transfers 1`] = ` 4 | "
5 | 6 |
" 7 | `; 8 | 9 | exports[`CompactTransactionTokenTransfers.vue > Should show loading state 1`] = ` 10 | "
11 | 12 |
" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/ExplorerPlanSelector.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ExplorerPlanSelector.vue > Should display plans 1`] = ` 4 | "
5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
" 15 | `; 16 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/StorageStructure.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`StorageStructure.vue Should display the correct info 1`] = ` 4 |
5 |
6 | 7 |
8 | mapping(address => uint256) _deposits: 9 | 10 |
11 |
12 | 13 |
14 | 0x56713b5deeb3438c5fcd203a134d01609bD5463C; 15 | 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/UpgradeLink.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`UpgradeLink.vue > Should display a link to the billing page 1`] = `"Upgrade"`; 4 | -------------------------------------------------------------------------------- /tests/unit/components/base/BaseChipGroup.spec.js: -------------------------------------------------------------------------------- 1 | import BaseChipGroup from '@/components/base/BaseChipGroup.vue'; 2 | 3 | describe('BaseChipGroup.vue', () => { 4 | it('Should render with default props', async () => { 5 | const wrapper = mount(BaseChipGroup); 6 | 7 | expect(wrapper.html()).toMatchSnapshot(); 8 | }); 9 | 10 | it('Should render with custom background color', async () => { 11 | const wrapper = mount(BaseChipGroup, { 12 | props: { 13 | backgroundColor: '#FF0000' 14 | } 15 | }); 16 | 17 | expect(wrapper.html()).toMatchSnapshot(); 18 | }); 19 | 20 | it('Should pass attributes to v-chip-group', async () => { 21 | const wrapper = mount(BaseChipGroup, { 22 | props: { 23 | mandatory: true, 24 | column: true 25 | } 26 | }); 27 | 28 | expect(wrapper.html()).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/unit/components/base/__snapshots__/BaseChipGroup.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`BaseChipGroup.vue > Should pass attributes to v-chip-group 1`] = ` 4 | "
5 | 6 |
7 |
8 |
9 | 10 |
" 11 | `; 12 | 13 | exports[`BaseChipGroup.vue > Should render with custom background color 1`] = ` 14 | "
15 | 16 |
17 |
18 |
19 | 20 |
" 21 | `; 22 | 23 | exports[`BaseChipGroup.vue > Should render with default props 1`] = ` 24 | "
25 | 26 |
27 |
28 |
29 | 30 |
" 31 | `; 32 | -------------------------------------------------------------------------------- /tests/unit/filters/FromWei.spec.js: -------------------------------------------------------------------------------- 1 | import FromWei from '@/filters/FromWei.js'; 2 | 3 | describe('FromWei', () => { 4 | it('Not do anything when no conversion unit specified', () => { 5 | const result = FromWei(1000000000000000000); 6 | expect(result).toEqual(1000000000000000000); 7 | }); 8 | 9 | it('Convert to given unit & format', () => { 10 | const result = FromWei(1000000000000000000, 'gwei', 'gwei'); 11 | expect(result).toEqual('1,000,000,000 gwei'); 12 | }); 13 | 14 | it('Should display the correct native token', () => { 15 | const result = FromWei(1000000000000000000, 'ether', 'Matic'); 16 | expect(result).toEqual('1 Matic'); 17 | }); 18 | 19 | it('Convert to given unit & format even with a native token passed', () => { 20 | const result = FromWei(1000000000000000000, 'gwei', 'Matic'); 21 | expect(result).toEqual('1,000,000,000 Matic'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/unit/fixtures/Block.json: -------------------------------------------------------------------------------- 1 | { 2 | "number": "1", 3 | "gasLimit": "1000000000", 4 | "timestamp": "1621548462", 5 | "hash": "0xb750fb9dd193bb4a46ea5426837c469815d2494abd68a94b1c2c190f3569c5b8" 6 | } -------------------------------------------------------------------------------- /tests/unit/fixtures/DecodedStorageData.json: -------------------------------------------------------------------------------- 1 | { 2 | "_owner":"0x2D481eeb2bA97955CD081Cf218f453A817259AB1", 3 | "_feeBasisPoints":"10", 4 | "_deposits":{ 5 | 6 | }, 7 | "_maturities":{ 8 | 9 | }, 10 | "stuff":{ 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /tests/unit/fixtures/LogProp.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockHash":"0x02cfea90863e2b298f07b8981cfb0ae04c66ebbfa895338030dde272cb3ddc32", 3 | "address":"0xc00e94Cb662C3520282E6f5717214004A7f26888", 4 | "logIndex":0, 5 | "data":"0x0000000000000000000000000000000000000000000000000000000000000001", 6 | "topics":[ 7 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 8 | "0x000000000000000000000000e93381fb4c4f14bda253907b18fad305d799241a", 9 | "0x000000000000000000000000c00e94cb662c3520282e6f5717214004a7f26888" 10 | ], 11 | "transactionIndex":0, 12 | "transactionHash":"0xb750fb9dd193bb4a46ea5426837c469815d2494abd68a94b1c2c190f3569c5b8", 13 | "blockNumber":12695880 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/fixtures/StorageProp.json: -------------------------------------------------------------------------------- 1 | { 2 | "path":[ 3 | "_deposits" 4 | ], 5 | "label":"mapping(address => uint256) _deposits:", 6 | "key":"_deposits", 7 | "children":[ 8 | { 9 | "path":[ 10 | "_deposits", 11 | "0x56713b5deeb3438c5fcd203a134d01609bD5463C" 12 | ], 13 | "children":null, 14 | "label":"0x56713b5deeb3438c5fcd203a134d01609bD5463C;", 15 | "key":"0x56713b5deeb3438c5fcd203a134d01609bD5463C" 16 | } 17 | ], 18 | "index":1 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/fixtures/TokenContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Token", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "available", 9 | "type": "uint256" 10 | }, 11 | { 12 | "internalType": "uint256", 13 | "name": "required", 14 | "type": "uint256" 15 | } 16 | ], 17 | "name": "InsufficientBalance", 18 | "type": "error" 19 | }, 20 | { 21 | "inputs": [ 22 | { 23 | "internalType": "address", 24 | "name": "to", 25 | "type": "address" 26 | }, 27 | { 28 | "internalType": "uint256", 29 | "name": "amount", 30 | "type": "uint256" 31 | } 32 | ], 33 | "name": "transfer", 34 | "outputs": [], 35 | "stateMutability": "pure", 36 | "type": "function", 37 | "constant": true 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/lib/__snapshots__/trace.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`parseTrace > Should return the processed trace 1`] = ` 4 | [ 5 | { 6 | "address": "0xBd919DCf1410ffc22fd6D0Fa0Fd88450a39B7A7C", 7 | "contractHashedBytecode": "0xdbe576b4818846aa77e82f4ed5fa78f92766b141f282d36703886d196df39322", 8 | "depth": 0, 9 | "op": "CREATE2", 10 | }, 11 | { 12 | "address": "0xbd919dcf1410ffc22fd6d0fa0fd88450a39b7a7c", 13 | "contractHashedBytecode": "0xdbe576b4818846aa77e82f4ed5fa78f92766b141f282d36703886d196df39322", 14 | "depth": 0, 15 | "input": "0x485cc9550000000000000000000000004d787c7d124721cd22a5f124bbb06d965fde04ef000000000000000000000000acfe4511ce883c14c4ea40563f176c3c09b4c47c", 16 | "op": "CALL", 17 | "returnData": "", 18 | "value": "0", 19 | }, 20 | ] 21 | `; 22 | -------------------------------------------------------------------------------- /tests/unit/lib/trace.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTrace} from '@/lib/trace.js'; 2 | import Trace from '../fixtures/Trace.json' 3 | 4 | describe('parseTrace', () => { 5 | let from, provider; 6 | 7 | beforeEach(async () => { 8 | from = '0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f'; 9 | provider = { 10 | getCode: () => { 11 | return new Promise((resolve) => resolve('0xabcd')); 12 | } 13 | }; 14 | }); 15 | 16 | it('Should return the processed trace', async () => { 17 | const result = await parseTrace(from, Trace, provider); 18 | expect(result).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/unit/mocks/db.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../src/plugins/firebase', () => ({ 2 | auth: jest.fn(), 3 | dbPlugin: { 4 | install(Vue) { 5 | Vue.prototype.db = { 6 | contractStorage: jest.fn(), 7 | getIdToken: jest.fn().mockResolvedValue('123') 8 | } 9 | } 10 | } 11 | })); 12 | -------------------------------------------------------------------------------- /tests/unit/mocks/ethereum.js: -------------------------------------------------------------------------------- 1 | const ethereum = { 2 | eth_requestAccounts: vi.fn(() => ['0x1234']), 3 | eth_chainId: vi.fn(() => '0x1'), 4 | accountsChanged: vi.fn(() => ['0x1235']), 5 | chainChanged: vi.fn(() => 2), 6 | request: vi.fn(), 7 | on: vi.fn() 8 | }; 9 | 10 | export default ethereum; 11 | -------------------------------------------------------------------------------- /tests/unit/mocks/metamask.js: -------------------------------------------------------------------------------- 1 | vi.mock('@/lib/metamask', () => ({ 2 | sendTransaction: vi.fn().mockResolvedValue('0x1234') 3 | })); 4 | -------------------------------------------------------------------------------- /tests/unit/mocks/posthog.js: -------------------------------------------------------------------------------- 1 | vi.mock('@/plugins/posthog', () => ({ 2 | posthogPlugin: { 3 | install(Vue) { 4 | Vue.prototype.$posthog = { 5 | identify: jest.fn(), 6 | capture: jest.fn(), 7 | reset: jest.fn() 8 | } 9 | } 10 | } 11 | })); 12 | -------------------------------------------------------------------------------- /tests/unit/mocks/pusher.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../src/plugins/pusher', () => ({ 2 | pusherPlugin: { 3 | install(Vue) { 4 | Vue.prototype.pusher = { 5 | onUpdatedAccount: jest.fn(), 6 | onNewFailedTransactions: jest.fn(), 7 | onNewProcessableTransactions: jest.fn(), 8 | onNewBlock: jest.fn(), 9 | onNewContract: jest.fn(), 10 | onNewContractLog: jest.fn(), 11 | onNewTransaction: jest.fn(), 12 | onNewToken: jest.fn(), 13 | onNewNft: jest.fn(), 14 | onUserUpdated: jest.fn(), 15 | onDestroyedContract: jest.fn(), 16 | onNewBlockEvent: jest.fn() 17 | } 18 | } 19 | } 20 | })); 21 | -------------------------------------------------------------------------------- /tests/unit/mocks/router.js: -------------------------------------------------------------------------------- 1 | vi.mock('@/plugins/router', () => { 2 | const VueRouter = require('vue-router'); 3 | return new VueRouter({ 4 | mode: 'history', 5 | routes: [] 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/unit/mocks/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /tests/unit/mocks/utils.js: -------------------------------------------------------------------------------- 1 | vi.mock('@/lib/utils', async () => { 2 | const actual = await vi.importActual('@/lib/utils'); 3 | return { 4 | ...actual, 5 | debounce: fn => fn 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | test: { 8 | env: false, 9 | environment: 'jsdom', 10 | globals: true, 11 | setupFiles: ['./tests/setup.js'], 12 | server: { 13 | deps: { 14 | inline: ['vuetify', '@web3-onboard/wagmi'] 15 | } 16 | } 17 | }, 18 | resolve: { 19 | alias: { 20 | '@': path.resolve(__dirname, './src') 21 | } 22 | } 23 | }); 24 | --------------------------------------------------------------------------------