├── .dockerignore ├── .env.example ├── .env.test ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── build-image-tag.yml │ ├── build.yml │ ├── lint.yml │ ├── main.yml │ ├── npm-publish.yml │ └── stale.yml ├── .gitignore ├── .prettierrc ├── .yarnrc ├── Dockerfile ├── LICENSE ├── README.md ├── biome.json ├── chain-overrides.example.json ├── contributing.md ├── docker-compose.yml ├── docs └── images │ └── engine-overview.webp ├── package.json ├── sdk ├── .babelrc ├── old_openapi.json ├── package.json ├── src │ ├── Engine.ts │ ├── core │ │ ├── ApiError.ts │ │ ├── ApiRequestOptions.ts │ │ ├── ApiResult.ts │ │ ├── BaseHttpRequest.ts │ │ ├── CancelablePromise.ts │ │ ├── FetchHttpRequest.ts │ │ ├── OpenAPI.ts │ │ └── request.ts │ ├── index.ts │ └── services │ │ ├── AccessTokensService.ts │ │ ├── AccountFactoryService.ts │ │ ├── AccountService.ts │ │ ├── BackendWalletService.ts │ │ ├── ChainService.ts │ │ ├── ConfigurationService.ts │ │ ├── ContractEventsService.ts │ │ ├── ContractMetadataService.ts │ │ ├── ContractRolesService.ts │ │ ├── ContractRoyaltiesService.ts │ │ ├── ContractService.ts │ │ ├── ContractSubscriptionsService.ts │ │ ├── DefaultService.ts │ │ ├── DeployService.ts │ │ ├── Erc1155Service.ts │ │ ├── Erc20Service.ts │ │ ├── Erc721Service.ts │ │ ├── KeypairService.ts │ │ ├── MarketplaceDirectListingsService.ts │ │ ├── MarketplaceEnglishAuctionsService.ts │ │ ├── MarketplaceOffersService.ts │ │ ├── PermissionsService.ts │ │ ├── RelayerService.ts │ │ ├── TransactionService.ts │ │ ├── WalletCredentialsService.ts │ │ ├── WalletSubscriptionsService.ts │ │ └── WebhooksService.ts ├── tsconfig.json └── yarn.lock ├── src ├── abitype.d.ts ├── index.ts ├── polyfill.ts ├── prisma │ ├── migrations │ │ ├── 20230913025746_init │ │ │ └── migration.sql │ │ ├── 20230913061811_triggers │ │ │ └── migration.sql │ │ ├── 20230922090743_user_ops │ │ │ └── migration.sql │ │ ├── 20230926004337_update_user_ops │ │ │ └── migration.sql │ │ ├── 20230927004337_transactions_cancelled_at_added │ │ │ └── migration.sql │ │ ├── 20230927221952_trigger_upd │ │ │ └── migration.sql │ │ ├── 20230929175313_cancelled_tx_table │ │ │ └── migration.sql │ │ ├── 20231003223429_encrypted_json │ │ │ └── migration.sql │ │ ├── 20231010225604_remove_cancelled_txs │ │ │ └── migration.sql │ │ ├── 20231016220744_configuration │ │ │ └── migration.sql │ │ ├── 20231016225111_wallets_configuration │ │ │ └── migration.sql │ │ ├── 20231016235340_chain_id_string │ │ │ └── migration.sql │ │ ├── 20231017214123_webhook_config │ │ │ └── migration.sql │ │ ├── 20231018202048_auth │ │ │ └── migration.sql │ │ ├── 20231018202837_one_config │ │ │ └── migration.sql │ │ ├── 20231019225104_auth_tokens │ │ │ └── migration.sql │ │ ├── 20231021091418_webhooks_tbl_config_update │ │ │ └── migration.sql │ │ ├── 20231025193415_labels │ │ │ └── migration.sql │ │ ├── 20231105235957_relayers │ │ │ └── migration.sql │ │ ├── 20231114220310_relayer_forwarders │ │ │ └── migration.sql │ │ ├── 20231123064817_added_onchain_status_flag_to_tx │ │ │ └── migration.sql │ │ ├── 20231203024522_groups │ │ │ └── migration.sql │ │ ├── 20231226100100_webhook_config_triggers │ │ │ └── migration.sql │ │ ├── 20240110194551_cors_to_configurations │ │ │ └── migration.sql │ │ ├── 20240116172315_clear_cache_cron_to_config │ │ │ └── migration.sql │ │ ├── 20240229013311_add_contract_event_subscriptions │ │ │ └── migration.sql │ │ ├── 20240313203128_add_transaction_receipts │ │ │ └── migration.sql │ │ ├── 20240323005129_add_idempotency_key_backward_compatible │ │ │ └── migration.sql │ │ ├── 20240327202800_add_keypairs │ │ │ └── migration.sql │ │ ├── 20240410015450_add_keypair_algorithm │ │ │ └── migration.sql │ │ ├── 20240411235927_add_keypair_label │ │ │ └── migration.sql │ │ ├── 20240510023239_add_webhook_id │ │ │ └── migration.sql │ │ ├── 20240511011737_set_on_delete_set_null │ │ │ └── migration.sql │ │ ├── 20240513204722_add_contract_sub_retry_delay_to_config │ │ │ └── migration.sql │ │ ├── 20240603232427_contract_subscription_filters │ │ │ └── migration.sql │ │ ├── 20240604034241_rename_contract_subscription_vars │ │ │ └── migration.sql │ │ ├── 20240611023057_add_contract_subscriptions_filter_functions │ │ │ └── migration.sql │ │ ├── 20240612015710_add_function_name │ │ │ └── migration.sql │ │ ├── 20240704112555_ip_allowlist │ │ │ └── migration.sql │ │ ├── 20240731212420_tx_add_composite_idx_1 │ │ │ └── migration.sql │ │ ├── 20240731213113_tx_add_composite_idx_2 │ │ │ └── migration.sql │ │ ├── 20240731213342_tx_add_composite_idx_3 │ │ │ └── migration.sql │ │ ├── 20240802175702_transaction_tbl_queued_at_idx │ │ │ └── migration.sql │ │ ├── 20241003051028_wallet_details_credentials │ │ │ └── migration.sql │ │ ├── 20241007003342_add_smart_backend_wallet_columns │ │ │ └── migration.sql │ │ ├── 20241022222921_add_entrypoint_address │ │ │ └── migration.sql │ │ ├── 20241031010103_add_mtls_configuration │ │ │ └── migration.sql │ │ ├── 20241105091733_chain_id_from_int_to_string │ │ │ └── migration.sql │ │ ├── 20241106225714_all_chain_ids_to_string │ │ │ └── migration.sql │ │ ├── 20250204201526_add_wallet_credentials │ │ │ └── migration.sql │ │ ├── 20250207135644_nullable_is_default │ │ │ └── migration.sql │ │ ├── 20250212235511_wallet_subscriptions │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── scripts │ ├── apply-migrations.ts │ ├── generate-sdk.ts │ └── setup-db.ts ├── server │ ├── index.ts │ ├── middleware │ │ ├── admin-routes.ts │ │ ├── auth.ts │ │ ├── cors.ts │ │ ├── engine-mode.ts │ │ ├── error.ts │ │ ├── logs.ts │ │ ├── open-api.ts │ │ ├── prometheus.ts │ │ ├── rate-limit.ts │ │ ├── security-headers.ts │ │ └── websocket.ts │ ├── routes │ │ ├── admin │ │ │ ├── nonces.ts │ │ │ └── transaction.ts │ │ ├── auth │ │ │ ├── access-tokens │ │ │ │ ├── create.ts │ │ │ │ ├── get-all.ts │ │ │ │ ├── revoke.ts │ │ │ │ └── update.ts │ │ │ ├── keypair │ │ │ │ ├── add.ts │ │ │ │ ├── list.ts │ │ │ │ └── remove.ts │ │ │ └── permissions │ │ │ │ ├── get-all.ts │ │ │ │ ├── grant.ts │ │ │ │ └── revoke.ts │ │ ├── backend-wallet │ │ │ ├── cancel-nonces.ts │ │ │ ├── create.ts │ │ │ ├── get-all.ts │ │ │ ├── get-balance.ts │ │ │ ├── get-nonce.ts │ │ │ ├── get-transactions-by-nonce.ts │ │ │ ├── get-transactions.ts │ │ │ ├── import.ts │ │ │ ├── remove.ts │ │ │ ├── reset-nonces.ts │ │ │ ├── send-transaction-batch-atomic.ts │ │ │ ├── send-transaction-batch.ts │ │ │ ├── send-transaction.ts │ │ │ ├── sign-message.ts │ │ │ ├── sign-transaction.ts │ │ │ ├── sign-typed-data.ts │ │ │ ├── simulate-transaction.ts │ │ │ ├── transfer.ts │ │ │ ├── update.ts │ │ │ └── withdraw.ts │ │ ├── chain │ │ │ ├── get-all.ts │ │ │ └── get.ts │ │ ├── configuration │ │ │ ├── auth │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── backend-wallet-balance │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── cache │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── chains │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── contract-subscriptions │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── cors │ │ │ │ ├── add.ts │ │ │ │ ├── get.ts │ │ │ │ ├── remove.ts │ │ │ │ └── set.ts │ │ │ ├── ip │ │ │ │ ├── get.ts │ │ │ │ └── set.ts │ │ │ ├── transactions │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ ├── wallet-subscriptions │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ │ └── wallets │ │ │ │ ├── get.ts │ │ │ │ └── update.ts │ │ ├── contract │ │ │ ├── events │ │ │ │ ├── get-all-events.ts │ │ │ │ ├── get-contract-event-logs.ts │ │ │ │ ├── get-event-logs-by-timestamp.ts │ │ │ │ ├── get-events.ts │ │ │ │ └── paginate-event-logs.ts │ │ │ ├── extensions │ │ │ │ ├── account-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── read │ │ │ │ │ │ ├── get-all-accounts.ts │ │ │ │ │ │ ├── get-associated-accounts.ts │ │ │ │ │ │ ├── is-account-deployed.ts │ │ │ │ │ │ └── predict-account-address.ts │ │ │ │ │ └── write │ │ │ │ │ │ └── create-account.ts │ │ │ │ ├── account │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── read │ │ │ │ │ │ ├── get-all-admins.ts │ │ │ │ │ │ └── get-all-sessions.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── grant-admin.ts │ │ │ │ │ │ ├── grant-session.ts │ │ │ │ │ │ ├── revoke-admin.ts │ │ │ │ │ │ ├── revoke-session.ts │ │ │ │ │ │ └── update-session.ts │ │ │ │ ├── erc1155 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── read │ │ │ │ │ │ ├── balance-of.ts │ │ │ │ │ │ ├── can-claim.ts │ │ │ │ │ │ ├── get-active-claim-conditions.ts │ │ │ │ │ │ ├── get-all-claim-conditions.ts │ │ │ │ │ │ ├── get-all.ts │ │ │ │ │ │ ├── get-claim-ineligibility-reasons.ts │ │ │ │ │ │ ├── get-claimer-proofs.ts │ │ │ │ │ │ ├── get-owned.ts │ │ │ │ │ │ ├── get.ts │ │ │ │ │ │ ├── is-approved.ts │ │ │ │ │ │ ├── signature-generate.ts │ │ │ │ │ │ ├── total-count.ts │ │ │ │ │ │ └── total-supply.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── airdrop.ts │ │ │ │ │ │ ├── burn-batch.ts │ │ │ │ │ │ ├── burn.ts │ │ │ │ │ │ ├── claim-to.ts │ │ │ │ │ │ ├── lazy-mint.ts │ │ │ │ │ │ ├── mint-additional-supply-to.ts │ │ │ │ │ │ ├── mint-batch-to.ts │ │ │ │ │ │ ├── mint-to.ts │ │ │ │ │ │ ├── set-approval-for-all.ts │ │ │ │ │ │ ├── set-batch-claim-conditions.ts │ │ │ │ │ │ ├── set-claim-conditions.ts │ │ │ │ │ │ ├── signature-mint.ts │ │ │ │ │ │ ├── transfer-from.ts │ │ │ │ │ │ ├── transfer.ts │ │ │ │ │ │ ├── update-claim-conditions.ts │ │ │ │ │ │ └── update-token-metadata.ts │ │ │ │ ├── erc20 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── read │ │ │ │ │ │ ├── allowance-of.ts │ │ │ │ │ │ ├── balance-of.ts │ │ │ │ │ │ ├── can-claim.ts │ │ │ │ │ │ ├── get-active-claim-conditions.ts │ │ │ │ │ │ ├── get-all-claim-conditions.ts │ │ │ │ │ │ ├── get-claim-ineligibility-reasons.ts │ │ │ │ │ │ ├── get-claimer-proofs.ts │ │ │ │ │ │ ├── get.ts │ │ │ │ │ │ ├── signature-generate.ts │ │ │ │ │ │ └── total-supply.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── burn-from.ts │ │ │ │ │ │ ├── burn.ts │ │ │ │ │ │ ├── claim-to.ts │ │ │ │ │ │ ├── mint-batch-to.ts │ │ │ │ │ │ ├── mint-to.ts │ │ │ │ │ │ ├── set-allowance.ts │ │ │ │ │ │ ├── set-claim-conditions.ts │ │ │ │ │ │ ├── signature-mint.ts │ │ │ │ │ │ ├── transfer-from.ts │ │ │ │ │ │ ├── transfer.ts │ │ │ │ │ │ └── update-claim-conditions.ts │ │ │ │ ├── erc721 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── read │ │ │ │ │ │ ├── balance-of.ts │ │ │ │ │ │ ├── can-claim.ts │ │ │ │ │ │ ├── get-active-claim-conditions.ts │ │ │ │ │ │ ├── get-all-claim-conditions.ts │ │ │ │ │ │ ├── get-all.ts │ │ │ │ │ │ ├── get-claim-ineligibility-reasons.ts │ │ │ │ │ │ ├── get-claimer-proofs.ts │ │ │ │ │ │ ├── get-owned.ts │ │ │ │ │ │ ├── get.ts │ │ │ │ │ │ ├── is-approved.ts │ │ │ │ │ │ ├── signature-generate.ts │ │ │ │ │ │ ├── signature-prepare.ts │ │ │ │ │ │ ├── total-claimed-supply.ts │ │ │ │ │ │ ├── total-count.ts │ │ │ │ │ │ └── total-unclaimed-supply.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── burn.ts │ │ │ │ │ │ ├── claim-to.ts │ │ │ │ │ │ ├── lazy-mint.ts │ │ │ │ │ │ ├── mint-batch-to.ts │ │ │ │ │ │ ├── mint-to.ts │ │ │ │ │ │ ├── set-approval-for-all.ts │ │ │ │ │ │ ├── set-approval-for-token.ts │ │ │ │ │ │ ├── set-claim-conditions.ts │ │ │ │ │ │ ├── signature-mint.ts │ │ │ │ │ │ ├── transfer-from.ts │ │ │ │ │ │ ├── transfer.ts │ │ │ │ │ │ ├── update-claim-conditions.ts │ │ │ │ │ │ └── update-token-metadata.ts │ │ │ │ └── marketplace-v3 │ │ │ │ │ ├── direct-listings │ │ │ │ │ ├── read │ │ │ │ │ │ ├── get-all-valid.ts │ │ │ │ │ │ ├── get-all.ts │ │ │ │ │ │ ├── get-listing.ts │ │ │ │ │ │ ├── get-total-count.ts │ │ │ │ │ │ ├── is-buyer-approved-for-listing.ts │ │ │ │ │ │ └── is-currency-approved-for-listing.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── approve-buyer-for-reserved-listing.ts │ │ │ │ │ │ ├── buy-from-listing.ts │ │ │ │ │ │ ├── cancel-listing.ts │ │ │ │ │ │ ├── create-listing.ts │ │ │ │ │ │ ├── revoke-buyer-approval-for-reserved-listing.ts │ │ │ │ │ │ ├── revoke-currency-approval-for-listing.ts │ │ │ │ │ │ └── update-listing.ts │ │ │ │ │ ├── english-auctions │ │ │ │ │ ├── read │ │ │ │ │ │ ├── get-all-valid.ts │ │ │ │ │ │ ├── get-all.ts │ │ │ │ │ │ ├── get-auction.ts │ │ │ │ │ │ ├── get-bid-buffer-bps.ts │ │ │ │ │ │ ├── get-minimum-next-bid.ts │ │ │ │ │ │ ├── get-total-count.ts │ │ │ │ │ │ ├── get-winner.ts │ │ │ │ │ │ ├── get-winning-bid.ts │ │ │ │ │ │ └── is-winning-bid.ts │ │ │ │ │ └── write │ │ │ │ │ │ ├── buyout-auction.ts │ │ │ │ │ │ ├── cancel-auction.ts │ │ │ │ │ │ ├── close-auction-for-bidder.ts │ │ │ │ │ │ ├── close-auction-for-seller.ts │ │ │ │ │ │ ├── create-auction.ts │ │ │ │ │ │ ├── execute-sale.ts │ │ │ │ │ │ └── make-bid.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── offers │ │ │ │ │ ├── read │ │ │ │ │ ├── get-all-valid.ts │ │ │ │ │ ├── get-all.ts │ │ │ │ │ ├── get-offer.ts │ │ │ │ │ └── get-total-count.ts │ │ │ │ │ └── write │ │ │ │ │ ├── accept-offer.ts │ │ │ │ │ ├── cancel-offer.ts │ │ │ │ │ └── make-offer.ts │ │ │ ├── metadata │ │ │ │ ├── abi.ts │ │ │ │ ├── events.ts │ │ │ │ ├── extensions.ts │ │ │ │ └── functions.ts │ │ │ ├── read │ │ │ │ ├── read-batch.ts │ │ │ │ └── read.ts │ │ │ ├── roles │ │ │ │ ├── read │ │ │ │ │ ├── get-all.ts │ │ │ │ │ └── get.ts │ │ │ │ └── write │ │ │ │ │ ├── grant.ts │ │ │ │ │ └── revoke.ts │ │ │ ├── royalties │ │ │ │ ├── read │ │ │ │ │ ├── get-default-royalty-info.ts │ │ │ │ │ └── get-token-royalty-info.ts │ │ │ │ └── write │ │ │ │ │ ├── set-default-royalty-info.ts │ │ │ │ │ └── set-token-royalty-info.ts │ │ │ ├── subscriptions │ │ │ │ ├── add-contract-subscription.ts │ │ │ │ ├── get-contract-indexed-block-range.ts │ │ │ │ ├── get-contract-subscriptions.ts │ │ │ │ ├── get-latest-block.ts │ │ │ │ └── remove-contract-subscription.ts │ │ │ ├── transactions │ │ │ │ ├── get-transaction-receipts-by-timestamp.ts │ │ │ │ ├── get-transaction-receipts.ts │ │ │ │ └── paginate-transaction-receipts.ts │ │ │ └── write │ │ │ │ └── write.ts │ │ ├── deploy │ │ │ ├── contract-types.ts │ │ │ ├── index.ts │ │ │ ├── prebuilt.ts │ │ │ ├── prebuilts │ │ │ │ ├── edition-drop.ts │ │ │ │ ├── edition.ts │ │ │ │ ├── marketplace-v3.ts │ │ │ │ ├── multiwrap.ts │ │ │ │ ├── nft-collection.ts │ │ │ │ ├── nft-drop.ts │ │ │ │ ├── pack.ts │ │ │ │ ├── signature-drop.ts │ │ │ │ ├── split.ts │ │ │ │ ├── token-drop.ts │ │ │ │ ├── token.ts │ │ │ │ └── vote.ts │ │ │ └── published.ts │ │ ├── home.ts │ │ ├── index.ts │ │ ├── relayer │ │ │ ├── create.ts │ │ │ ├── get-all.ts │ │ │ ├── index.ts │ │ │ ├── revoke.ts │ │ │ └── update.ts │ │ ├── system │ │ │ ├── health.ts │ │ │ └── queue.ts │ │ ├── transaction │ │ │ ├── blockchain │ │ │ │ ├── get-logs.ts │ │ │ │ ├── get-receipt.ts │ │ │ │ ├── get-user-op-receipt.ts │ │ │ │ ├── send-signed-tx.ts │ │ │ │ └── send-signed-user-op.ts │ │ │ ├── cancel.ts │ │ │ ├── get-all-deployed-contracts.ts │ │ │ ├── get-all.ts │ │ │ ├── retry-failed.ts │ │ │ ├── retry.ts │ │ │ ├── status.ts │ │ │ └── sync-retry.ts │ │ ├── wallet-credentials │ │ │ ├── create.ts │ │ │ ├── get-all.ts │ │ │ ├── get.ts │ │ │ └── update.ts │ │ ├── wallet-subscriptions │ │ │ ├── add.ts │ │ │ ├── delete.ts │ │ │ ├── get-all.ts │ │ │ └── update.ts │ │ └── webhooks │ │ │ ├── create.ts │ │ │ ├── events.ts │ │ │ ├── get-all.ts │ │ │ ├── revoke.ts │ │ │ └── test.ts │ ├── schemas │ │ ├── account │ │ │ └── index.ts │ │ ├── address.ts │ │ ├── chain │ │ │ └── index.ts │ │ ├── claim-conditions │ │ │ └── index.ts │ │ ├── contract-subscription.ts │ │ ├── contract │ │ │ └── index.ts │ │ ├── erc20 │ │ │ └── index.ts │ │ ├── event-log.ts │ │ ├── event.ts │ │ ├── http-headers │ │ │ └── thirdweb-sdk-version.ts │ │ ├── marketplace-v3 │ │ │ ├── direct-listing │ │ │ │ └── index.ts │ │ │ ├── english-auction │ │ │ │ └── index.ts │ │ │ └── offer │ │ │ │ └── index.ts │ │ ├── nft │ │ │ ├── index.ts │ │ │ └── v5.ts │ │ ├── number.ts │ │ ├── pagination.ts │ │ ├── prebuilts │ │ │ └── index.ts │ │ ├── shared-api-schemas.ts │ │ ├── transaction-receipt.ts │ │ ├── transaction │ │ │ ├── authorization.ts │ │ │ ├── index.ts │ │ │ └── raw-transaction-parms.ts │ │ ├── tx-overrides.ts │ │ ├── wallet-subscription.ts │ │ ├── wallet │ │ │ └── index.ts │ │ ├── webhook.ts │ │ └── websocket │ │ │ └── index.ts │ └── utils │ │ ├── abi.ts │ │ ├── chain.ts │ │ ├── convertor.ts │ │ ├── cors-urls.ts │ │ ├── marketplace-v3.ts │ │ ├── openapi.ts │ │ ├── storage │ │ └── local-storage.ts │ │ ├── transaction-overrides.ts │ │ ├── validator.ts │ │ └── wallets │ │ ├── aws-kms-arn.ts │ │ ├── circle │ │ └── index.ts │ │ ├── create-aws-kms-wallet.ts │ │ ├── create-gcp-kms-wallet.ts │ │ ├── create-local-wallet.ts │ │ ├── create-smart-wallet.ts │ │ ├── fetch-aws-kms-wallet-params.ts │ │ ├── fetch-gcp-kms-wallet-params.ts │ │ ├── gcp-kms-resource-path.ts │ │ ├── get-aws-kms-account.ts │ │ ├── get-gcp-kms-account.ts │ │ ├── get-local-wallet.ts │ │ ├── get-smart-wallet.ts │ │ ├── import-aws-kms-wallet.ts │ │ ├── import-gcp-kms-wallet.ts │ │ └── import-local-wallet.ts ├── shared │ ├── db │ │ ├── chain-indexers │ │ │ ├── get-chain-indexer.ts │ │ │ └── upsert-chain-indexer.ts │ │ ├── client.ts │ │ ├── configuration │ │ │ ├── get-configuration.ts │ │ │ └── update-configuration.ts │ │ ├── contract-event-logs │ │ │ ├── create-contract-event-logs.ts │ │ │ ├── delete-contract-event-logs.ts │ │ │ └── get-contract-event-logs.ts │ │ ├── contract-subscriptions │ │ │ ├── create-contract-subscription.ts │ │ │ ├── delete-contract-subscription.ts │ │ │ └── get-contract-subscriptions.ts │ │ ├── contract-transaction-receipts │ │ │ ├── create-contract-transaction-receipts.ts │ │ │ ├── delete-contract-transaction-receipts.ts │ │ │ └── get-contract-transaction-receipts.ts │ │ ├── keypair │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── insert.ts │ │ │ └── list.ts │ │ ├── permissions │ │ │ ├── delete-permissions.ts │ │ │ ├── get-permissions.ts │ │ │ └── update-permissions.ts │ │ ├── relayer │ │ │ └── get-relayer-by-id.ts │ │ ├── tokens │ │ │ ├── create-token.ts │ │ │ ├── get-access-tokens.ts │ │ │ ├── get-token.ts │ │ │ ├── revoke-token.ts │ │ │ └── update-token.ts │ │ ├── transactions │ │ │ ├── db.ts │ │ │ └── queue-tx.ts │ │ ├── wallet-credentials │ │ │ ├── create-wallet-credential.ts │ │ │ ├── get-all-wallet-credentials.ts │ │ │ ├── get-wallet-credential.ts │ │ │ └── update-wallet-credential.ts │ │ ├── wallet-subscriptions │ │ │ ├── create-wallet-subscription.ts │ │ │ ├── delete-wallet-subscription.ts │ │ │ ├── get-all-wallet-subscriptions.ts │ │ │ └── update-wallet-subscription.ts │ │ ├── wallets │ │ │ ├── create-wallet-details.ts │ │ │ ├── delete-wallet-details.ts │ │ │ ├── get-all-wallets.ts │ │ │ ├── get-wallet-details.ts │ │ │ ├── nonce-map.ts │ │ │ ├── update-wallet-details.ts │ │ │ └── wallet-nonce.ts │ │ └── webhooks │ │ │ ├── create-webhook.ts │ │ │ ├── get-all-webhooks.ts │ │ │ ├── get-webhook.ts │ │ │ └── revoke-webhook.ts │ ├── lib │ │ ├── cache │ │ │ └── swr.ts │ │ ├── chain │ │ │ └── chain-capabilities.ts │ │ └── transaction │ │ │ └── get-transaction-receipt.ts │ ├── schemas │ │ ├── auth.ts │ │ ├── config.ts │ │ ├── extension.ts │ │ ├── keypair.ts │ │ ├── prisma.ts │ │ ├── relayer.ts │ │ ├── wallet-subscription-conditions.ts │ │ ├── wallet.ts │ │ └── webhooks.ts │ └── utils │ │ ├── account.ts │ │ ├── auth.ts │ │ ├── block.ts │ │ ├── cache │ │ ├── access-token.ts │ │ ├── auth-wallet.ts │ │ ├── clear-cache.ts │ │ ├── get-config.ts │ │ ├── get-contract.ts │ │ ├── get-contractv5.ts │ │ ├── get-sdk.ts │ │ ├── get-smart-wallet-v5.ts │ │ ├── get-wallet.ts │ │ ├── get-webhook.ts │ │ └── keypair.ts │ │ ├── chain.ts │ │ ├── cron │ │ ├── clear-cache-cron.ts │ │ └── is-valid-cron.ts │ │ ├── crypto.ts │ │ ├── custom-auth-header.ts │ │ ├── date.ts │ │ ├── env.ts │ │ ├── error.ts │ │ ├── ethers.ts │ │ ├── indexer │ │ └── get-block-time.ts │ │ ├── logger.ts │ │ ├── math.ts │ │ ├── primitive-types.ts │ │ ├── prometheus.ts │ │ ├── redis │ │ ├── lock.ts │ │ └── redis.ts │ │ ├── sdk.ts │ │ ├── transaction │ │ ├── cancel-transaction.ts │ │ ├── insert-transaction.ts │ │ ├── queue-transation.ts │ │ ├── simulate-queued-transaction.ts │ │ ├── types.ts │ │ └── webhook.ts │ │ ├── usage.ts │ │ └── webhook.ts ├── tracer.ts └── worker │ ├── index.ts │ ├── indexers │ └── chain-indexer-registry.ts │ ├── listeners │ └── chain-indexer-listener.ts │ ├── queues │ ├── cancel-recycled-nonces-queue.ts │ ├── mine-transaction-queue.ts │ ├── nonce-health-check-queue.ts │ ├── nonce-resync-queue.ts │ ├── process-event-logs-queue.ts │ ├── process-transaction-receipts-queue.ts │ ├── prune-transactions-queue.ts │ ├── queues.ts │ ├── send-transaction-queue.ts │ ├── send-webhook-queue.ts │ └── wallet-subscription-queue.ts │ └── tasks │ ├── cancel-recycled-nonces-worker.ts │ ├── chain-indexer.ts │ ├── manage-chain-indexers.ts │ ├── mine-transaction-worker.ts │ ├── nonce-health-check-worker.ts │ ├── nonce-resync-worker.ts │ ├── process-event-logs-worker.ts │ ├── process-transaction-receipts-worker.ts │ ├── prune-transactions-worker.ts │ ├── send-transaction-worker.ts │ ├── send-webhook-worker.ts │ └── wallet-subscription-worker.ts ├── tests ├── e2e │ ├── .env.test.example │ ├── .gitignore │ ├── README.md │ ├── bun.lockb │ ├── config.ts │ ├── package.json │ ├── scripts │ │ └── counter.ts │ ├── tests │ │ ├── extensions.test.ts │ │ ├── load.test.ts │ │ ├── read.test.ts │ │ ├── routes │ │ │ ├── erc1155-transfer.test.ts │ │ │ ├── erc20-transfer.test.ts │ │ │ ├── erc721-transfer.test.ts │ │ │ ├── read.test.ts │ │ │ ├── sign-message.test.ts │ │ │ ├── sign-typed-data.test.ts │ │ │ ├── signature-prepare.test.ts │ │ │ └── write.test.ts │ │ ├── setup.ts │ │ ├── sign-transaction.test.ts │ │ ├── smart-backend-wallet │ │ │ ├── smart-aws-wallet.test.ts │ │ │ ├── smart-gcp-wallet.test.ts │ │ │ ├── smart-local-wallet-sdk-v4.test.ts │ │ │ └── smart-local-wallet.test.ts │ │ ├── smoke.test.ts │ │ ├── userop.test.ts │ │ ├── utils │ │ │ └── get-block-time.test.ts │ │ └── workers │ │ │ └── wallet-subscription-worker.test.ts │ ├── tsconfig.json │ └── utils │ │ ├── anvil.ts │ │ ├── engine.ts │ │ ├── statistics.ts │ │ ├── transactions.ts │ │ └── wallets.ts ├── shared │ ├── aws-kms.ts │ ├── chain.ts │ ├── client.ts │ ├── gcp-kms.ts │ └── typed-data.ts └── unit │ ├── auth.test.ts │ ├── aws-arn.test.ts │ ├── aws-kms.test.ts │ ├── chain.test.ts │ ├── gcp-kms.test.ts │ ├── gcp-resource-path.test.ts │ ├── math.test.ts │ ├── migrationV5.test.ts │ ├── schema.test.ts │ ├── send-transaction-worker.test.ts │ ├── swr.test.ts │ ├── validator.test.ts │ └── webhook.test.ts ├── tsconfig.json ├── vitest.config.ts ├── vitest.global-setup.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | .env 6 | .env* 7 | yarn-error.log 8 | .husky 9 | docker-compose*.yml 10 | Dockerfile* 11 | .DS_Store 12 | swagger.yml 13 | test 14 | .dist 15 | .github 16 | .gitignore 17 | .git 18 | .coverage 19 | dist 20 | docs 21 | .vscode 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # =====[ Required Configuration ]===== 2 | 3 | # Get your api secret key from the thirdweb dashboard - https://thirdweb.com/dashboard 4 | THIRDWEB_API_SECRET_KEY="" 5 | # The connection url for your running postgres instance, defaults to localhost postgres 6 | POSTGRES_CONNECTION_URL="postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" 7 | # The admin wallet that will be able to connect to Engine from the dashboard 8 | ADMIN_WALLET_ADDRESS="" 9 | # Encryption password for the wallet keys 10 | ENCRYPTION_PASSWORD="" 11 | # The connection url for your running redis instance, defaults to localhost redis 12 | REDIS_URL="redis://localhost:6379/0" 13 | 14 | # =====[ Optional Configuration ]===== 15 | 16 | # Optional configuration to override server host and port 17 | # PORT="3005" 18 | # HOST="0.0.0.0" 19 | 20 | # Optional configuration to enable https usage for localhost 21 | # ENABLE_HTTPS="false" 22 | # HTTPS_PASSPHRASE="..." 23 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | THIRDWEB_API_SECRET_KEY="test" 2 | POSTGRES_CONNECTION_URL="postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" 3 | ADMIN_WALLET_ADDRESS="test" 4 | ENCRYPTION_PASSWORD="test" 5 | ENABLE_KEYPAIR_AUTH="true" 6 | ENABLE_HTTPS="true" 7 | REDIS_URL="redis://127.0.0.1:6379/0" 8 | THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key" 9 | 10 | TEST_AWS_KMS_KEY_ID="UNIMPLEMENTED" 11 | TEST_AWS_KMS_ACCESS_KEY_ID="UNIMPLEMENTED" 12 | TEST_AWS_KMS_SECRET_ACCESS_KEY="UNIMPLEMENTED" 13 | TEST_AWS_KMS_REGION="UNIMPLEMENTED" 14 | 15 | TEST_GCP_KMS_RESOURCE_PATH="UNIMPLEMENTED" 16 | TEST_GCP_KMS_EMAIL="UNIMPLEMENTED" 17 | TEST_GCP_KMS_PK="UNIMPLEMENTED" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | - Linear: https://linear.app/thirdweb/issue/INF-171/ 4 | - Here is the problem being solved. 5 | - Here are the changes made. 6 | 7 | ## How this PR will be tested 8 | 9 | - [ ] Open the dashboard and click X. Result: A modal should appear. 10 | - [ ] Call the /foo/bar API. Result: Returns 200 with "baz" in the response body. 11 | 12 | ## Output 13 | 14 | (Example: Screenshot/GIF for UI changes, cURL output for API changes) 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-24.04 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | with: 12 | ref: ${{ github.ref }} 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 16 | with: 17 | node-version: "18" 18 | cache: "yarn" 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Run build 24 | run: yarn build 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-24.04 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | with: 12 | ref: ${{ github.ref }} 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 16 | with: 17 | node-version: "18" 18 | cache: "yarn" 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Run lint 24 | run: yarn lint 25 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | with: 13 | ref: ${{ github.ref }} 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 17 | with: 18 | node-version: "18" 19 | cache: "yarn" 20 | 21 | - name: Install dependencies 22 | run: yarn install 23 | 24 | - name: Install tsx 25 | run: npm install -g tsx 26 | 27 | - name: Install dependencies 28 | working-directory: ./sdk 29 | run: yarn install 30 | 31 | - name: Get version from package.json 32 | id: package_version 33 | run: echo "PACKAGE_VERSION=$(jq -r '.version' ./sdk/package.json)" >> $GITHUB_ENV 34 | 35 | - name: Use version 36 | run: echo "SDK version is $PACKAGE_VERSION" 37 | 38 | - name: Build 39 | run: yarn generate:sdk 40 | 41 | - name: Push to npm 42 | working-directory: ./sdk 43 | run: | 44 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 45 | npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '37 0 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v9 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-pr-message: 'This PR is stale because it has been open for 7 days with no activity. Remove stale label or comment or this PR will be closed in 3 days.' 25 | stale-pr-label: 'no-pr-activity' 26 | days-before-stale: 7 27 | days-before-close: 3 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true 2 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true, 7 | "defaultBranch": "dev" 8 | }, 9 | "formatter": { 10 | "enabled": true, 11 | "indentStyle": "space" 12 | }, 13 | "organizeImports": { 14 | "enabled": true 15 | }, 16 | "linter": { 17 | "enabled": true, 18 | "rules": { 19 | "correctness": { 20 | "noNewSymbol": "error", 21 | "noUnusedImports": "error", 22 | "noUnusedVariables": "error", 23 | "useArrayLiterals": "error" 24 | }, 25 | "complexity": { 26 | "noStaticOnlyClass": "off" 27 | } 28 | } 29 | }, 30 | "files": { 31 | "ignore": ["sdk"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /chain-overrides.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Localhost", 4 | "chain": "ETH", 5 | "rpc": ["http://localhost:8545"], 6 | "nativeCurrency": { 7 | "name": "Ether", 8 | "symbol": "ETH", 9 | "decimals": 18 10 | }, 11 | "chainId": 1337, 12 | "slug": "localhost" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | The thirdweb team welcomes contributions! 4 | 5 | ## Workflow 6 | 7 | For OSS contributions, we use a [Forking Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow). Develop locally on your own fork and submit a PR to the main repo when you're ready for your changes to be reviewed. 8 | 9 | 1. [Create a fork](https://github.com/thirdweb-dev/engine/fork) of this repository to your own GitHub account. 10 | 1. [Clone your fork](https://help.github.com/articles/cloning-a-repository/) to your local machine. 11 | 1. Create a new branch on your fork to start working on your changes: 12 | 13 | ```bash 14 | git checkout -b MY_BRANCH_NAME 15 | ``` 16 | 17 | 1. Install the dependencies: 18 | 19 | ```bash 20 | yarn install 21 | ``` 22 | 23 | 1. Make changes on your branch. 24 | 1. Make a pull request to the `thirdweb-dev/engine:main` branch. 25 | 26 | ## Test Your Changes 27 | 28 | Please run Engine locally to test your changes. 29 | 30 | ```bash 31 | yarn dev 32 | ``` 33 | 34 | You should be able to make requests to Engine locally and import it to the [thirdweb dashboard](https://thirdweb.com/dashboard/engine). 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | ################################### 2 | # USED FOR LOCAL DEVELOPMENT ONLY # 3 | ################################### 4 | 5 | services: 6 | postgres: 7 | container_name: postgres 8 | image: postgres:16.3 9 | restart: always 10 | env_file: 11 | - .env 12 | environment: 13 | - POSTGRES_HOST_AUTH_METHOD=trust 14 | volumes: 15 | - postgres_data:/var/lib/postgresql/data 16 | ports: 17 | - 5432:5432 18 | deploy: 19 | resources: 20 | limits: 21 | cpus: "2" 22 | memory: 2G 23 | reservations: 24 | cpus: "2" 25 | memory: 2G 26 | 27 | redis: 28 | container_name: redis 29 | image: redis:7.2 30 | restart: always 31 | ports: 32 | - 6379:6379 33 | deploy: 34 | resources: 35 | limits: 36 | cpus: "1" 37 | memory: 1024M 38 | volumes: 39 | - redis_data:/data 40 | 41 | engine: 42 | profiles: 43 | - engine 44 | build: 45 | dockerfile: Dockerfile 46 | context: . 47 | target: prod 48 | env_file: 49 | - .env 50 | ports: 51 | - 3005:3005 52 | depends_on: 53 | - postgres 54 | - redis 55 | deploy: 56 | resources: 57 | limits: 58 | cpus: "1" 59 | memory: 1024M 60 | # entrypoint: "yarn start:dev" 61 | # volumes: 62 | # - ./:/app 63 | # - node_modules:/app/node_modules 64 | 65 | volumes: 66 | postgres_data: 67 | redis_data: 68 | -------------------------------------------------------------------------------- /docs/images/engine-overview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/engine/fa4369ab34d8f168e83c0796ed12f8de4a889b95/docs/images/engine-overview.webp -------------------------------------------------------------------------------- /sdk/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thirdweb-dev/engine", 3 | "version": "0.0.19", 4 | "main": "dist/thirdweb-dev-engine.cjs.js", 5 | "module": "dist/thirdweb-dev-engine.esm.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "license": "MIT", 10 | "devDependencies": { 11 | "@babel/preset-typescript": "^7.23.2", 12 | "@preconstruct/cli": "^2.8.1" 13 | }, 14 | "dependencies": {}, 15 | "scripts": { 16 | "build": "preconstruct build" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sdk/src/core/ApiError.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | import type { ApiRequestOptions } from './ApiRequestOptions'; 6 | import type { ApiResult } from './ApiResult'; 7 | 8 | export class ApiError extends Error { 9 | public readonly url: string; 10 | public readonly status: number; 11 | public readonly statusText: string; 12 | public readonly body: any; 13 | public readonly request: ApiRequestOptions; 14 | 15 | constructor(request: ApiRequestOptions, response: ApiResult, message: string) { 16 | super(message); 17 | 18 | this.name = 'ApiError'; 19 | this.url = response.url; 20 | this.status = response.status; 21 | this.statusText = response.statusText; 22 | this.body = response.body; 23 | this.request = request; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sdk/src/core/ApiRequestOptions.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | export type ApiRequestOptions = { 6 | readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; 7 | readonly url: string; 8 | readonly path?: Record; 9 | readonly cookies?: Record; 10 | readonly headers?: Record; 11 | readonly query?: Record; 12 | readonly formData?: Record; 13 | readonly body?: any; 14 | readonly mediaType?: string; 15 | readonly responseHeader?: string; 16 | readonly errors?: Record; 17 | }; 18 | -------------------------------------------------------------------------------- /sdk/src/core/ApiResult.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | export type ApiResult = { 6 | readonly url: string; 7 | readonly ok: boolean; 8 | readonly status: number; 9 | readonly statusText: string; 10 | readonly body: any; 11 | }; 12 | -------------------------------------------------------------------------------- /sdk/src/core/BaseHttpRequest.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | import type { ApiRequestOptions } from './ApiRequestOptions'; 6 | import type { CancelablePromise } from './CancelablePromise'; 7 | import type { OpenAPIConfig } from './OpenAPI'; 8 | 9 | export abstract class BaseHttpRequest { 10 | 11 | constructor(public readonly config: OpenAPIConfig) {} 12 | 13 | public abstract request(options: ApiRequestOptions): CancelablePromise; 14 | } 15 | -------------------------------------------------------------------------------- /sdk/src/core/FetchHttpRequest.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | import type { ApiRequestOptions } from './ApiRequestOptions'; 6 | import { BaseHttpRequest } from './BaseHttpRequest'; 7 | import type { CancelablePromise } from './CancelablePromise'; 8 | import type { OpenAPIConfig } from './OpenAPI'; 9 | import { request as __request } from './request'; 10 | 11 | export class FetchHttpRequest extends BaseHttpRequest { 12 | 13 | constructor(config: OpenAPIConfig) { 14 | super(config); 15 | } 16 | 17 | /** 18 | * Request method 19 | * @param options The request options from the service 20 | * @returns CancelablePromise 21 | * @throws ApiError 22 | */ 23 | public override request(options: ApiRequestOptions): CancelablePromise { 24 | return __request(this.config, options); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sdk/src/core/OpenAPI.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | import type { ApiRequestOptions } from './ApiRequestOptions'; 6 | 7 | type Resolver = (options: ApiRequestOptions) => Promise; 8 | type Headers = Record; 9 | 10 | export type OpenAPIConfig = { 11 | BASE: string; 12 | VERSION: string; 13 | WITH_CREDENTIALS: boolean; 14 | CREDENTIALS: 'include' | 'omit' | 'same-origin'; 15 | TOKEN?: string | Resolver | undefined; 16 | USERNAME?: string | Resolver | undefined; 17 | PASSWORD?: string | Resolver | undefined; 18 | HEADERS?: Headers | Resolver | undefined; 19 | ENCODE_PATH?: ((path: string) => string) | undefined; 20 | }; 21 | 22 | export const OpenAPI: OpenAPIConfig = { 23 | BASE: '', 24 | VERSION: '1.0.0', 25 | WITH_CREDENTIALS: false, 26 | CREDENTIALS: 'include', 27 | TOKEN: undefined, 28 | USERNAME: undefined, 29 | PASSWORD: undefined, 30 | HEADERS: undefined, 31 | ENCODE_PATH: undefined, 32 | }; 33 | -------------------------------------------------------------------------------- /sdk/src/services/DefaultService.ts: -------------------------------------------------------------------------------- 1 | /* generated using openapi-typescript-codegen -- do no edit */ 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | import type { CancelablePromise } from '../core/CancelablePromise'; 6 | import type { BaseHttpRequest } from '../core/BaseHttpRequest'; 7 | 8 | export class DefaultService { 9 | 10 | constructor(public readonly httpRequest: BaseHttpRequest) {} 11 | 12 | /** 13 | * @returns any Default Response 14 | * @throws ApiError 15 | */ 16 | public getJson(): CancelablePromise { 17 | return this.httpRequest.request({ 18 | method: 'GET', 19 | url: '/json', 20 | }); 21 | } 22 | 23 | /** 24 | * @returns any Default Response 25 | * @throws ApiError 26 | */ 27 | public getOpenapiJson(): CancelablePromise { 28 | return this.httpRequest.request({ 29 | method: 'GET', 30 | url: '/openapi.json', 31 | }); 32 | } 33 | 34 | /** 35 | * @returns any Default Response 36 | * @throws ApiError 37 | */ 38 | public getJson1(): CancelablePromise { 39 | return this.httpRequest.request({ 40 | method: 'GET', 41 | url: '/json/', 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "isolatedModules": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/abitype.d.ts: -------------------------------------------------------------------------------- 1 | declare module "abitype" { 2 | export interface Config { 3 | AddressType: string; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "node:crypto"; 2 | 3 | if (typeof globalThis.crypto === "undefined") { 4 | // @ts-expect-error 5 | globalThis.crypto = crypto; 6 | } 7 | -------------------------------------------------------------------------------- /src/prisma/migrations/20230913061811_triggers/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION notify_transactions_insert() 2 | RETURNS TRIGGER 3 | LANGUAGE plpgsql 4 | AS $function$ 5 | BEGIN 6 | PERFORM pg_notify('new_transaction_data', row_to_json(NEW)::text); 7 | RETURN NEW; 8 | END; 9 | $function$; 10 | 11 | -- Trigger to return only specific column, to keep it under the size limit 12 | CREATE OR REPLACE FUNCTION notify_transactions_update() 13 | RETURNS TRIGGER 14 | LANGUAGE plpgsql 15 | AS $function$ 16 | BEGIN 17 | PERFORM pg_notify('updated_transaction_data', json_build_object( 18 | 'id', NEW.id 19 | )::text); 20 | RETURN NEW; 21 | END; 22 | $function$; 23 | 24 | CREATE OR REPLACE TRIGGER transactions_insert_trigger 25 | AFTER INSERT ON transactions 26 | FOR EACH ROW 27 | EXECUTE FUNCTION notify_transactions_insert(); 28 | 29 | CREATE OR REPLACE TRIGGER transactions_update_trigger 30 | AFTER UPDATE ON transactions 31 | FOR EACH ROW 32 | EXECUTE FUNCTION notify_transactions_update(); -------------------------------------------------------------------------------- /src/prisma/migrations/20230922090743_user_ops/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "wallet_nonce" DROP CONSTRAINT "wallet_nonce_address_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "transactions" ADD COLUMN "accountAddress" TEXT, 6 | ADD COLUMN "callData" TEXT, 7 | ADD COLUMN "callGasLimit" INTEGER, 8 | ADD COLUMN "initCode" TEXT, 9 | ADD COLUMN "paymasterAndData" TEXT, 10 | ADD COLUMN "preVerificationGas" INTEGER, 11 | ADD COLUMN "sender" TEXT, 12 | ADD COLUMN "signerAddress" TEXT, 13 | ADD COLUMN "target" TEXT, 14 | ADD COLUMN "userOpHash" TEXT, 15 | ADD COLUMN "verificationGasLimit" INTEGER, 16 | ALTER COLUMN "fromAddress" DROP NOT NULL; 17 | -------------------------------------------------------------------------------- /src/prisma/migrations/20230926004337_update_user_ops/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "transactions" 2 | ALTER COLUMN "callGasLimit" SET DATA TYPE TEXT, 3 | ALTER COLUMN "preVerificationGas" SET DATA TYPE TEXT, 4 | ALTER COLUMN "verificationGasLimit" SET DATA TYPE TEXT; -------------------------------------------------------------------------------- /src/prisma/migrations/20230927004337_transactions_cancelled_at_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "transactions" ADD COLUMN "cancelledAt" TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20230927221952_trigger_upd/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | CREATE OR REPLACE FUNCTION notify_transactions_insert() 3 | RETURNS TRIGGER 4 | LANGUAGE plpgsql 5 | AS $function$ 6 | BEGIN 7 | PERFORM pg_notify('new_transaction_data', json_build_object( 8 | 'id', NEW.id 9 | )::text); 10 | RETURN NEW; 11 | END; 12 | $function$; -------------------------------------------------------------------------------- /src/prisma/migrations/20230929175313_cancelled_tx_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "cancelled_transactions" ( 3 | "queueId" TEXT NOT NULL, 4 | "cancelledByWorkerAt" TIMESTAMP(3), 5 | 6 | CONSTRAINT "cancelled_transactions_pkey" PRIMARY KEY ("queueId") 7 | ); 8 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231003223429_encrypted_json/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "wallet_details" ADD COLUMN "encryptedJson" TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231010225604_remove_cancelled_txs/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `cancelled_transactions` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "cancelled_transactions"; 9 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231016220744_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "configuration" ( 3 | "id" TEXT NOT NULL, 4 | "minTxsToProcess" INTEGER NOT NULL, 5 | "maxTxsToProcess" INTEGER NOT NULL, 6 | "minedTxsCronSchedule" TEXT, 7 | "maxTxsToUpdate" INTEGER NOT NULL, 8 | "retryTxsCronSchedule" TEXT, 9 | "minEllapsedBlocksBeforeRetry" INTEGER NOT NULL, 10 | "maxFeePerGasForRetries" TEXT NOT NULL, 11 | "maxPriorityFeePerGasForRetries" TEXT NOT NULL, 12 | "maxRetriesPerTx" INTEGER NOT NULL, 13 | 14 | CONSTRAINT "configuration_pkey" PRIMARY KEY ("id") 15 | ); 16 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231016225111_wallets_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "awsAccessKeyId" TEXT, 3 | ADD COLUMN "awsRegion" TEXT, 4 | ADD COLUMN "awsSecretAccessKey" TEXT, 5 | ADD COLUMN "gcpApplicationCredentialEmail" TEXT, 6 | ADD COLUMN "gcpApplicationCredentialPrivateKey" TEXT, 7 | ADD COLUMN "gcpApplicationProjectId" TEXT, 8 | ADD COLUMN "gcpKmsKeyRingId" TEXT, 9 | ADD COLUMN "gcpKmsLocationId" TEXT; 10 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231016235340_chain_id_string/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `wallet_nonce` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "configuration" ADD COLUMN "chainOverrides" TEXT; 9 | 10 | -- AlterTable 11 | ALTER TABLE "transactions" ALTER COLUMN "chainId" SET DATA TYPE TEXT; 12 | 13 | -- AlterTable 14 | ALTER TABLE "wallet_nonce" DROP CONSTRAINT "wallet_nonce_pkey", 15 | ALTER COLUMN "chainId" SET DATA TYPE TEXT, 16 | ADD CONSTRAINT "wallet_nonce_pkey" PRIMARY KEY ("address", "chainId"); 17 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231017214123_webhook_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "webhookAuthBearerToken" TEXT, 3 | ADD COLUMN "webhookUrl" TEXT; 4 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231018202048_auth/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "authDomain" TEXT NOT NULL DEFAULT '', 3 | ADD COLUMN "authWalletEncryptedJson" TEXT NOT NULL DEFAULT ''; 4 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231018202837_one_config/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[id]` on the table `configuration` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "configuration" ALTER COLUMN "id" SET DEFAULT 'default'; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "configuration_id_key" ON "configuration"("id"); 12 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231019225104_auth_tokens/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "configuration_id_key"; 3 | 4 | -- CreateTable 5 | CREATE TABLE "permissions" ( 6 | "walletAddress" TEXT NOT NULL, 7 | "permissions" TEXT NOT NULL, 8 | 9 | CONSTRAINT "permissions_pkey" PRIMARY KEY ("walletAddress") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "tokens" ( 14 | "id" TEXT NOT NULL, 15 | "tokenMask" TEXT NOT NULL, 16 | "walletAddress" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "expiresAt" TIMESTAMP(3) NOT NULL, 19 | "revokedAt" TIMESTAMP(3), 20 | "isAccessToken" BOOLEAN NOT NULL, 21 | 22 | CONSTRAINT "tokens_pkey" PRIMARY KEY ("id") 23 | ); 24 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231021091418_webhooks_tbl_config_update/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `webhookAuthBearerToken` on the `configuration` table. All the data in the column will be lost. 5 | - You are about to drop the column `webhookUrl` on the `configuration` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "configuration" 10 | ADD COLUMN "minWalletBalance" TEXT NOT NULL DEFAULT '2000000000000000'; 11 | 12 | -- CreateTable 13 | CREATE TABLE "webhooks" ( 14 | "id" SERIAL NOT NULL, 15 | "name" TEXT NOT NULL, 16 | "url" TEXT NOT NULL, 17 | "secret" TEXT, 18 | "evenType" TEXT NOT NULL, 19 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "updatedAt" TIMESTAMP(3) NOT NULL, 21 | "revokedAt" TIMESTAMP(3), 22 | 23 | CONSTRAINT "webhooks_pkey" PRIMARY KEY ("id") 24 | ); 25 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231025193415_labels/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `secret` on table `webhooks` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "configuration" ALTER COLUMN "minWalletBalance" SET DEFAULT '20000000000000000'; 9 | 10 | -- AlterTable 11 | ALTER TABLE "permissions" ADD COLUMN "label" TEXT; 12 | 13 | -- AlterTable 14 | ALTER TABLE "tokens" ADD COLUMN "label" TEXT; 15 | 16 | -- AlterTable 17 | ALTER TABLE "wallet_details" ADD COLUMN "label" TEXT; 18 | 19 | -- AlterTable 20 | ALTER TABLE "webhooks" ALTER COLUMN "name" DROP NOT NULL, 21 | ALTER COLUMN "secret" SET NOT NULL; 22 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231105235957_relayers/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "relayers" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT, 5 | "chainId" TEXT NOT NULL, 6 | "backendWalletAddress" TEXT NOT NULL, 7 | "allowedContracts" TEXT, 8 | 9 | CONSTRAINT "relayers_pkey" PRIMARY KEY ("id") 10 | ); 11 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231114220310_relayer_forwarders/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "relayers" ADD COLUMN "allowedForwarders" TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231123064817_added_onchain_status_flag_to_tx/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "transactions" ADD COLUMN "onChainTxStatus" INTEGER; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20231203024522_groups/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "transactions" ADD COLUMN "groupId" TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240110194551_cors_to_configurations/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "accessControlAllowOrigin" TEXT NOT NULL DEFAULT 'https://thirdweb.com,https://embed.ipfscdn.io'; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240116172315_clear_cache_cron_to_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "clearCacheCronSchedule" TEXT NOT NULL DEFAULT '*/30 * * * * *'; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240313203128_add_transaction_receipts/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ALTER COLUMN "maxBlocksToIndex" SET DEFAULT 25; 3 | 4 | -- CreateTable 5 | CREATE TABLE "contract_transaction_receipts" ( 6 | "chainId" INTEGER NOT NULL, 7 | "blockNumber" INTEGER NOT NULL, 8 | "contractAddress" TEXT NOT NULL, 9 | "contractId" TEXT NOT NULL, 10 | "transactionHash" TEXT NOT NULL, 11 | "blockHash" TEXT NOT NULL, 12 | "timestamp" TIMESTAMP(3) NOT NULL, 13 | "data" TEXT NOT NULL, 14 | "to" TEXT NOT NULL, 15 | "from" TEXT NOT NULL, 16 | "value" TEXT NOT NULL, 17 | "transactionIndex" INTEGER NOT NULL, 18 | "gasUsed" TEXT NOT NULL, 19 | "effectiveGasPrice" TEXT NOT NULL, 20 | "status" INTEGER NOT NULL, 21 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | "updatedAt" TIMESTAMP(3) NOT NULL 23 | ); 24 | 25 | -- CreateIndex 26 | CREATE INDEX "contract_transaction_receipts_contractId_timestamp_idx" ON "contract_transaction_receipts"("contractId", "timestamp"); 27 | 28 | -- CreateIndex 29 | CREATE INDEX "contract_transaction_receipts_contractId_blockNumber_idx" ON "contract_transaction_receipts"("contractId", "blockNumber"); 30 | 31 | -- CreateIndex 32 | CREATE UNIQUE INDEX "contract_transaction_receipts_chainId_transactionHash_key" ON "contract_transaction_receipts"("chainId", "transactionHash"); 33 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240323005129_add_idempotency_key_backward_compatible/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[idempotencyKey]` on the table `transactions` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "transactions" ADD COLUMN "idempotencyKey" TEXT NOT NULL DEFAULT gen_random_uuid(); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "transactions_idempotencyKey_key" ON "transactions"("idempotencyKey"); 12 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240327202800_add_keypairs/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "keypairs" ( 3 | "hash" TEXT NOT NULL, 4 | "publicKey" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | 7 | CONSTRAINT "keypairs_pkey" PRIMARY KEY ("hash") 8 | ); 9 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240410015450_add_keypair_algorithm/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `algorithm` to the `keypairs` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "keypairs" ADD COLUMN "algorithm" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240411235927_add_keypair_label/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "keypairs" ADD COLUMN "label" TEXT, 3 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 4 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240510023239_add_webhook_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "contract_subscriptions" ADD COLUMN "webhookId" INTEGER; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "contract_subscriptions" ADD CONSTRAINT "contract_subscriptions_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240511011737_set_on_delete_set_null/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "contract_subscriptions" DROP CONSTRAINT "contract_subscriptions_webhookId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "contract_subscriptions" ADD CONSTRAINT "contract_subscriptions_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240513204722_add_contract_sub_retry_delay_to_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "contractSubscriptionsRetryDelaySeconds" TEXT NOT NULL DEFAULT '10'; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240603232427_contract_subscription_filters/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "contract_subscriptions" ADD COLUMN "filterEventLogs" TEXT[] DEFAULT ARRAY[]::TEXT[], 3 | ADD COLUMN "parseEventLogs" BOOLEAN NOT NULL DEFAULT true, 4 | ADD COLUMN "parseTransactionReceipts" BOOLEAN NOT NULL DEFAULT true; 5 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240604034241_rename_contract_subscription_vars/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `filterEventLogs` on the `contract_subscriptions` table. All the data in the column will be lost. 5 | - You are about to drop the column `parseEventLogs` on the `contract_subscriptions` table. All the data in the column will be lost. 6 | - You are about to drop the column `parseTransactionReceipts` on the `contract_subscriptions` table. All the data in the column will be lost. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "contract_subscriptions" DROP COLUMN "filterEventLogs", 11 | DROP COLUMN "parseEventLogs", 12 | DROP COLUMN "parseTransactionReceipts", 13 | ADD COLUMN "filterEvents" TEXT[] DEFAULT ARRAY[]::TEXT[], 14 | ADD COLUMN "processEventLogs" BOOLEAN NOT NULL DEFAULT true, 15 | ADD COLUMN "processTransactionReceipts" BOOLEAN NOT NULL DEFAULT true; 16 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240611023057_add_contract_subscriptions_filter_functions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "contract_subscriptions" ADD COLUMN "filterFunctions" TEXT[] DEFAULT ARRAY[]::TEXT[]; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240612015710_add_function_name/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "contract_transaction_receipts" ADD COLUMN "functionName" TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240704112555_ip_allowlist/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "ipAllowlist" TEXT[] DEFAULT ARRAY[]::TEXT[]; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240731212420_tx_add_composite_idx_1/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX CONCURRENTLY "transactions_sentAt_minedAt_cancelledAt_errorMessage_queued_idx" ON "transactions"("sentAt", "minedAt", "cancelledAt", "errorMessage", "queuedAt"); 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240731213113_tx_add_composite_idx_2/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX CONCURRENTLY "transactions_sentAt_accountAddress_userOpHash_minedAt_error_idx" ON "transactions"("sentAt", "accountAddress", "userOpHash", "minedAt", "errorMessage", "retryCount"); 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240731213342_tx_add_composite_idx_3/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX CONCURRENTLY "transactions_sentAt_transactionHash_accountAddress_minedAt__idx" ON "transactions"("sentAt", "transactionHash", "accountAddress", "minedAt", "errorMessage", "nonce"); 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20240802175702_transaction_tbl_queued_at_idx/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX CONCURRENTLY "transactions_queuedAt_idx" ON "transactions"("queuedAt"); 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241003051028_wallet_details_credentials/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "wallet_details" ADD COLUMN "awsKmsAccessKeyId" TEXT, 3 | ADD COLUMN "awsKmsSecretAccessKey" TEXT, 4 | ADD COLUMN "gcpApplicationCredentialEmail" TEXT, 5 | ADD COLUMN "gcpApplicationCredentialPrivateKey" TEXT; 6 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241007003342_add_smart_backend_wallet_columns/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "wallet_details" ADD COLUMN "accountFactoryAddress" TEXT, 3 | ADD COLUMN "accountSignerAddress" TEXT; 4 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241022222921_add_entrypoint_address/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "wallet_details" ADD COLUMN "entrypointAddress" TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "mtlsCertificateEncrypted" TEXT, 3 | ADD COLUMN "mtlsPrivateKeyEncrypted" TEXT; 4 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241105091733_chain_id_from_int_to_string/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "contract_subscriptions" ALTER COLUMN "chainId" SET DATA TYPE TEXT; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20241106225714_all_chain_ids_to_string/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `chain_indexers` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "chain_indexers" DROP CONSTRAINT "chain_indexers_pkey", 9 | ALTER COLUMN "chainId" SET DATA TYPE TEXT, 10 | ADD CONSTRAINT "chain_indexers_pkey" PRIMARY KEY ("chainId"); 11 | 12 | -- AlterTable 13 | ALTER TABLE "contract_event_logs" ALTER COLUMN "chainId" SET DATA TYPE TEXT; 14 | 15 | -- AlterTable 16 | ALTER TABLE "contract_transaction_receipts" ALTER COLUMN "chainId" SET DATA TYPE TEXT; 17 | -------------------------------------------------------------------------------- /src/prisma/migrations/20250204201526_add_wallet_credentials/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "walletProviderConfigs" JSONB NOT NULL DEFAULT '{}'; 3 | 4 | -- AlterTable 5 | ALTER TABLE "wallet_details" ADD COLUMN "credentialId" TEXT, 6 | ADD COLUMN "platformIdentifiers" JSONB; 7 | 8 | -- CreateTable 9 | CREATE TABLE "wallet_credentials" ( 10 | "id" TEXT NOT NULL, 11 | "type" TEXT NOT NULL, 12 | "label" TEXT NOT NULL, 13 | "data" JSONB NOT NULL, 14 | "isDefault" BOOLEAN NOT NULL DEFAULT false, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP(3) NOT NULL, 17 | "deletedAt" TIMESTAMP(3), 18 | 19 | CONSTRAINT "wallet_credentials_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "wallet_credentials_type_idx" ON "wallet_credentials"("type"); 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "wallet_credentials_type_is_default_key" ON "wallet_credentials"("type", "isDefault"); 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "wallet_details" ADD CONSTRAINT "wallet_details_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "wallet_credentials"("id") ON DELETE SET NULL ON UPDATE CASCADE; 30 | -------------------------------------------------------------------------------- /src/prisma/migrations/20250207135644_nullable_is_default/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "wallet_credentials" ALTER COLUMN "isDefault" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /src/prisma/migrations/20250212235511_wallet_subscriptions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "configuration" ADD COLUMN "walletSubscriptionsCronSchedule" TEXT; 3 | 4 | -- CreateTable 5 | CREATE TABLE "wallet_subscriptions" ( 6 | "id" TEXT NOT NULL, 7 | "chainId" TEXT NOT NULL, 8 | "walletAddress" TEXT NOT NULL, 9 | "conditions" JSONB[], 10 | "webhookId" INTEGER, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP(3) NOT NULL, 13 | "deletedAt" TIMESTAMP(3), 14 | 15 | CONSTRAINT "wallet_subscriptions_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateIndex 19 | CREATE INDEX "wallet_subscriptions_chainId_idx" ON "wallet_subscriptions"("chainId"); 20 | 21 | -- CreateIndex 22 | CREATE INDEX "wallet_subscriptions_walletAddress_idx" ON "wallet_subscriptions"("walletAddress"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "wallet_subscriptions" ADD CONSTRAINT "wallet_subscriptions_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE SET NULL ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /src/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /src/scripts/setup-db.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { prisma } from "../shared/db/client"; 3 | 4 | const main = async () => { 5 | const [{ exists: hasWalletsTable }]: [{ exists: boolean }] = 6 | await prisma.$queryRaw` 7 | SELECT EXISTS ( 8 | SELECT 1 9 | FROM pg_tables 10 | WHERE schemaname = 'public' 11 | AND tablename = 'wallets' 12 | ); 13 | `; 14 | 15 | const schema = 16 | process.env.NODE_ENV === "production" 17 | ? "./dist/prisma/schema.prisma" 18 | : "./src/prisma/schema.prisma"; 19 | 20 | if (hasWalletsTable) { 21 | execSync(`yarn prisma migrate reset --force --schema ${schema}`, { 22 | stdio: "inherit", 23 | }); 24 | } else { 25 | execSync(`yarn prisma migrate deploy --schema ${schema}`, { 26 | stdio: "inherit", 27 | }); 28 | } 29 | 30 | execSync(`yarn prisma generate --schema ${schema}`, { stdio: "inherit" }); 31 | }; 32 | 33 | main(); 34 | -------------------------------------------------------------------------------- /src/server/middleware/engine-mode.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import { env } from "../../shared/utils/env"; 3 | 4 | export function withEnforceEngineMode(server: FastifyInstance) { 5 | if (env.ENGINE_MODE === "sandbox") { 6 | server.addHook("onRequest", async (request, reply) => { 7 | if (request.method !== "GET") { 8 | return reply.status(405).send({ 9 | statusCode: 405, 10 | error: "Engine is in read-only mode. Only GET requests are allowed.", 11 | message: 12 | "Engine is in read-only mode. Only GET requests are allowed.", 13 | }); 14 | } 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/server/middleware/open-api.ts: -------------------------------------------------------------------------------- 1 | import swagger from "@fastify/swagger"; 2 | import type { FastifyInstance } from "fastify"; 3 | 4 | export const OPENAPI_ROUTES = ["/json", "/openapi.json", "/json/"]; 5 | 6 | export async function withOpenApi(server: FastifyInstance) { 7 | await server.register(swagger, { 8 | openapi: { 9 | info: { 10 | title: "thirdweb Engine", 11 | description: 12 | "Engine is an open-source, backend server that reads, writes, and deploys contracts at production scale.", 13 | version: "1.0.0", 14 | license: { 15 | name: "Apache 2.0", 16 | url: "http://www.apache.org/licenses/LICENSE-2.0.html", 17 | }, 18 | }, 19 | components: { 20 | securitySchemes: { 21 | bearerAuth: { 22 | type: "http", 23 | scheme: "bearer", 24 | bearerFormat: "JWT", 25 | description: "To authenticate server-side requests", 26 | }, 27 | }, 28 | }, 29 | security: [ 30 | { 31 | bearerAuth: [], 32 | }, 33 | ], 34 | }, 35 | }); 36 | 37 | for (const path of OPENAPI_ROUTES) { 38 | server.get(path, {}, async (_, res) => { 39 | res.send(server.swagger()); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/server/middleware/prometheus.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | import { env } from "../../shared/utils/env"; 3 | import { recordMetrics } from "../../shared/utils/prometheus"; 4 | 5 | export function withPrometheus(server: FastifyInstance) { 6 | if (!env.METRICS_ENABLED) { 7 | return; 8 | } 9 | 10 | server.addHook( 11 | "onResponse", 12 | async (req: FastifyRequest, res: FastifyReply) => { 13 | const { method } = req; 14 | const url = req.routeOptions.url; 15 | const { statusCode } = res; 16 | const duration = res.elapsedTime; 17 | 18 | recordMetrics({ 19 | event: "response_sent", 20 | params: { 21 | endpoint: url ?? "", 22 | statusCode: statusCode.toString(), 23 | duration: duration, 24 | method: method, 25 | }, 26 | }); 27 | }, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/server/middleware/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import { env } from "../../shared/utils/env"; 4 | import { redis } from "../../shared/utils/redis/redis"; 5 | import { createCustomError } from "./error"; 6 | import { OPENAPI_ROUTES } from "./open-api"; 7 | 8 | const SKIP_RATELIMIT_PATHS = ["/", ...OPENAPI_ROUTES]; 9 | 10 | export function withRateLimit(server: FastifyInstance) { 11 | server.addHook("onRequest", async (request, _reply) => { 12 | if (SKIP_RATELIMIT_PATHS.includes(request.url)) { 13 | return; 14 | } 15 | 16 | const epochTimeInMinutes = Math.floor(new Date().getTime() / (1000 * 60)); 17 | const key = `rate-limit:global:${epochTimeInMinutes}`; 18 | const count = await redis.incr(key); 19 | redis.expire(key, 2 * 60); 20 | 21 | if (count > env.GLOBAL_RATE_LIMIT_PER_MIN) { 22 | throw createCustomError( 23 | `Too many requests. Please reduce your calls to ${env.GLOBAL_RATE_LIMIT_PER_MIN} requests/minute or update the "GLOBAL_RATE_LIMIT_PER_MIN" env var.`, 24 | StatusCodes.TOO_MANY_REQUESTS, 25 | "TOO_MANY_REQUESTS", 26 | ); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/server/middleware/security-headers.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | 3 | export function withSecurityHeaders(server: FastifyInstance) { 4 | server.addHook("onSend", async (_request, reply, payload) => { 5 | reply.header( 6 | "Strict-Transport-Security", 7 | "max-age=31536000; includeSubDomains; preload", 8 | ); 9 | reply.header("Content-Security-Policy", "default-src 'self';"); 10 | reply.header("X-Frame-Options", "DENY"); 11 | reply.header("X-Content-Type-Options", "nosniff"); 12 | reply.header("Referrer-Policy", "no-referrer"); 13 | reply.header( 14 | "Permissions-Policy", 15 | "geolocation=(), camera=(), microphone=()", 16 | ); 17 | 18 | return payload; 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/server/middleware/websocket.ts: -------------------------------------------------------------------------------- 1 | import WebSocketPlugin from "@fastify/websocket"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { logger } from "../../shared/utils/logger"; 4 | 5 | export async function withWebSocket(server: FastifyInstance) { 6 | await server.register(WebSocketPlugin, { 7 | errorHandler: ( 8 | error, 9 | conn /* SocketStream */, 10 | _req /* FastifyRequest */, 11 | _reply /* FastifyReply */, 12 | ) => { 13 | logger({ 14 | service: "websocket", 15 | level: "error", 16 | message: `Websocket error: ${error}`, 17 | }); 18 | // Do stuff 19 | // destroy/close connection 20 | conn.destroy(error); 21 | }, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/server/routes/auth/access-tokens/get-all.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getAccessTokens } from "../../../../shared/db/tokens/get-access-tokens"; 5 | import { AddressSchema } from "../../../schemas/address"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | export const AccessTokenSchema = Type.Object({ 9 | id: Type.String(), 10 | tokenMask: Type.String(), 11 | walletAddress: AddressSchema, 12 | createdAt: Type.String(), 13 | expiresAt: Type.String(), 14 | label: Type.Union([Type.String(), Type.Null()]), 15 | }); 16 | 17 | const responseBodySchema = Type.Object({ 18 | result: Type.Array(AccessTokenSchema), 19 | }); 20 | 21 | export async function getAllAccessTokens(fastify: FastifyInstance) { 22 | fastify.route<{ 23 | Reply: Static; 24 | }>({ 25 | method: "GET", 26 | url: "/auth/access-tokens/get-all", 27 | schema: { 28 | summary: "Get all access tokens", 29 | description: "Get all access tokens", 30 | tags: ["Access Tokens"], 31 | operationId: "listAccessTokens", 32 | response: { 33 | ...standardResponseSchema, 34 | [StatusCodes.OK]: responseBodySchema, 35 | }, 36 | }, 37 | handler: async (_req, res) => { 38 | const accessTokens = await getAccessTokens(); 39 | res.status(StatusCodes.OK).send({ 40 | result: accessTokens.map((token) => ({ 41 | ...token, 42 | createdAt: token.createdAt.toISOString(), 43 | expiresAt: token.expiresAt.toISOString(), 44 | })), 45 | }); 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/server/routes/auth/access-tokens/revoke.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { revokeToken } from "../../../../shared/db/tokens/revoke-token"; 5 | import { accessTokenCache } from "../../../../shared/utils/cache/access-token"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | const requestBodySchema = Type.Object({ 9 | id: Type.String(), 10 | }); 11 | 12 | const responseBodySchema = Type.Object({ 13 | result: Type.Object({ 14 | success: Type.Boolean(), 15 | }), 16 | }); 17 | 18 | export async function revokeAccessToken(fastify: FastifyInstance) { 19 | fastify.route<{ 20 | Body: Static; 21 | Reply: Static; 22 | }>({ 23 | method: "POST", 24 | url: "/auth/access-tokens/revoke", 25 | schema: { 26 | summary: "Revoke an access token", 27 | description: "Revoke an access token", 28 | tags: ["Access Tokens"], 29 | operationId: "revokeAccessTokens", 30 | body: requestBodySchema, 31 | response: { 32 | ...standardResponseSchema, 33 | [StatusCodes.OK]: responseBodySchema, 34 | }, 35 | }, 36 | handler: async (req, res) => { 37 | await revokeToken({ id: req.body.id }); 38 | 39 | accessTokenCache.clear(); 40 | 41 | res.status(StatusCodes.OK).send({ 42 | result: { 43 | success: true, 44 | }, 45 | }); 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/server/routes/auth/access-tokens/update.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { updateToken } from "../../../../shared/db/tokens/update-token"; 5 | import { accessTokenCache } from "../../../../shared/utils/cache/access-token"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | const requestBodySchema = Type.Object({ 9 | id: Type.String(), 10 | label: Type.Optional(Type.String()), 11 | }); 12 | 13 | const responseBodySchema = Type.Object({ 14 | result: Type.Object({ 15 | success: Type.Boolean(), 16 | }), 17 | }); 18 | 19 | export async function updateAccessToken(fastify: FastifyInstance) { 20 | fastify.route<{ 21 | Body: Static; 22 | Reply: Static; 23 | }>({ 24 | method: "POST", 25 | url: "/auth/access-tokens/update", 26 | schema: { 27 | summary: "Update an access token", 28 | description: "Update an access token", 29 | tags: ["Access Tokens"], 30 | operationId: "updateAccessTokens", 31 | body: requestBodySchema, 32 | response: { 33 | ...standardResponseSchema, 34 | [StatusCodes.OK]: responseBodySchema, 35 | }, 36 | }, 37 | handler: async (req, res) => { 38 | const { id, label } = req.body; 39 | await updateToken({ id, label }); 40 | 41 | accessTokenCache.clear(); 42 | 43 | res.status(StatusCodes.OK).send({ 44 | result: { 45 | success: true, 46 | }, 47 | }); 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/server/routes/auth/keypair/list.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { listKeypairs } from "../../../../shared/db/keypair/list"; 5 | import { 6 | KeypairSchema, 7 | toKeypairSchema, 8 | } from "../../../../shared/schemas/keypair"; 9 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 10 | 11 | const responseBodySchema = Type.Object({ 12 | result: Type.Array(KeypairSchema), 13 | }); 14 | 15 | export async function listPublicKeys(fastify: FastifyInstance) { 16 | fastify.route<{ 17 | Reply: Static; 18 | }>({ 19 | method: "GET", 20 | url: "/auth/keypair/get-all", 21 | schema: { 22 | summary: "List public keys", 23 | description: "List the public keys configured with Engine", 24 | tags: ["Keypair"], 25 | operationId: "list", 26 | response: { 27 | ...standardResponseSchema, 28 | [StatusCodes.OK]: responseBodySchema, 29 | }, 30 | }, 31 | handler: async (_req, res) => { 32 | const keypairs = await listKeypairs(); 33 | 34 | res.status(StatusCodes.OK).send({ 35 | result: keypairs.map(toKeypairSchema), 36 | }); 37 | }, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/server/routes/auth/keypair/remove.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { deleteKeypair } from "../../../../shared/db/keypair/delete"; 5 | import { keypairCache } from "../../../../shared/utils/cache/keypair"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | const requestBodySchema = Type.Object({ 9 | hash: Type.String(), 10 | }); 11 | 12 | const responseBodySchema = Type.Object({ 13 | result: Type.Object({ 14 | success: Type.Boolean(), 15 | }), 16 | }); 17 | 18 | export async function removePublicKey(fastify: FastifyInstance) { 19 | fastify.route<{ 20 | Body: Static; 21 | Reply: Static; 22 | }>({ 23 | method: "POST", 24 | url: "/auth/keypair/remove", 25 | schema: { 26 | summary: "Remove public key", 27 | description: "Remove the public key for a keypair", 28 | tags: ["Keypair"], 29 | operationId: "remove", 30 | body: requestBodySchema, 31 | response: { 32 | ...standardResponseSchema, 33 | [StatusCodes.OK]: responseBodySchema, 34 | }, 35 | }, 36 | handler: async (req, res) => { 37 | const { hash } = req.body; 38 | 39 | await deleteKeypair({ hash }); 40 | keypairCache.clear(); 41 | 42 | res.status(StatusCodes.OK).send({ 43 | result: { success: true }, 44 | }); 45 | }, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/server/routes/auth/permissions/get-all.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { prisma } from "../../../../shared/db/client"; 5 | import { AddressSchema } from "../../../schemas/address"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | const responseBodySchema = Type.Object({ 9 | result: Type.Array( 10 | Type.Object({ 11 | walletAddress: AddressSchema, 12 | permissions: Type.String(), 13 | label: Type.Union([Type.String(), Type.Null()]), 14 | }), 15 | ), 16 | }); 17 | 18 | export async function getAllPermissions(fastify: FastifyInstance) { 19 | fastify.route<{ 20 | Reply: Static; 21 | }>({ 22 | method: "GET", 23 | url: "/auth/permissions/get-all", 24 | schema: { 25 | summary: "Get all permissions", 26 | description: "Get all users with their corresponding permissions", 27 | tags: ["Permissions"], 28 | operationId: "listAdmins", 29 | response: { 30 | ...standardResponseSchema, 31 | [StatusCodes.OK]: responseBodySchema, 32 | }, 33 | }, 34 | handler: async (_req, res) => { 35 | const permissions = await prisma.permissions.findMany(); 36 | res.status(StatusCodes.OK).send({ 37 | result: permissions, 38 | }); 39 | }, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/server/routes/auth/permissions/grant.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { updatePermissions } from "../../../../shared/db/permissions/update-permissions"; 5 | import { AddressSchema } from "../../../schemas/address"; 6 | import { permissionsSchema } from "../../../../shared/schemas/auth"; 7 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 8 | 9 | const requestBodySchema = Type.Object({ 10 | walletAddress: AddressSchema, 11 | permissions: permissionsSchema, 12 | label: Type.Optional(Type.String()), 13 | }); 14 | 15 | const responseBodySchema = Type.Object({ 16 | result: Type.Object({ 17 | success: Type.Boolean(), 18 | }), 19 | }); 20 | 21 | export async function grantPermissions(fastify: FastifyInstance) { 22 | fastify.route<{ 23 | Body: Static; 24 | Reply: Static; 25 | }>({ 26 | method: "POST", 27 | url: "/auth/permissions/grant", 28 | schema: { 29 | summary: "Grant permissions to user", 30 | description: "Grant permissions to a user", 31 | tags: ["Permissions"], 32 | operationId: "grantAdmin", 33 | body: requestBodySchema, 34 | response: { 35 | ...standardResponseSchema, 36 | [StatusCodes.OK]: responseBodySchema, 37 | }, 38 | }, 39 | handler: async (req, res) => { 40 | const { walletAddress, permissions, label } = req.body; 41 | await updatePermissions({ 42 | walletAddress, 43 | permissions, 44 | label, 45 | }); 46 | res.status(StatusCodes.OK).send({ 47 | result: { 48 | success: true, 49 | }, 50 | }); 51 | }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/server/routes/auth/permissions/revoke.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { deletePermissions } from "../../../../shared/db/permissions/delete-permissions"; 5 | import { AddressSchema } from "../../../schemas/address"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | const requestBodySchema = Type.Object({ 9 | walletAddress: AddressSchema, 10 | }); 11 | 12 | const responseBodySchema = Type.Object({ 13 | result: Type.Object({ 14 | success: Type.Boolean(), 15 | }), 16 | }); 17 | 18 | export async function revokePermissions(fastify: FastifyInstance) { 19 | fastify.route<{ 20 | Body: Static; 21 | Reply: Static; 22 | }>({ 23 | method: "POST", 24 | url: "/auth/permissions/revoke", 25 | schema: { 26 | summary: "Revoke permissions from user", 27 | description: "Revoke a user's permissions", 28 | tags: ["Permissions"], 29 | operationId: "revokeAdmin", 30 | body: requestBodySchema, 31 | response: { 32 | ...standardResponseSchema, 33 | [StatusCodes.OK]: responseBodySchema, 34 | }, 35 | }, 36 | handler: async (req, res) => { 37 | const { walletAddress } = req.body; 38 | await deletePermissions({ 39 | walletAddress, 40 | }); 41 | res.status(StatusCodes.OK).send({ 42 | result: { 43 | success: true, 44 | }, 45 | }); 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/server/routes/configuration/auth/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | export const responseBodySchema = Type.Object({ 8 | result: Type.Object({ 9 | authDomain: Type.String(), 10 | mtlsCertificate: Type.Union([Type.String(), Type.Null()]), 11 | // Do not return mtlsPrivateKey. 12 | }), 13 | }); 14 | 15 | export async function getAuthConfiguration(fastify: FastifyInstance) { 16 | fastify.route<{ 17 | Reply: Static; 18 | }>({ 19 | method: "GET", 20 | url: "/configuration/auth", 21 | schema: { 22 | summary: "Get auth configuration", 23 | description: "Get auth configuration", 24 | tags: ["Configuration"], 25 | operationId: "getAuthConfiguration", 26 | response: { 27 | ...standardResponseSchema, 28 | [StatusCodes.OK]: responseBodySchema, 29 | }, 30 | }, 31 | handler: async (_req, res) => { 32 | const { authDomain, mtlsCertificate } = await getConfig(); 33 | 34 | res.status(StatusCodes.OK).send({ 35 | result: { 36 | authDomain, 37 | mtlsCertificate, 38 | }, 39 | }); 40 | }, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/server/routes/configuration/backend-wallet-balance/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | export const responseBodySchema = Type.Object({ 8 | result: Type.Object({ 9 | minWalletBalance: Type.String({ 10 | description: "Minimum wallet balance in wei", 11 | }), 12 | }), 13 | }); 14 | 15 | export async function getBackendWalletBalanceConfiguration( 16 | fastify: FastifyInstance, 17 | ) { 18 | fastify.route<{ 19 | Reply: Static; 20 | }>({ 21 | method: "GET", 22 | url: "/configuration/backend-wallet-balance", 23 | schema: { 24 | summary: "Get wallet-balance configuration", 25 | description: "Get wallet-balance configuration", 26 | tags: ["Configuration"], 27 | operationId: "getBackendWalletBalanceConfiguration", 28 | response: { 29 | ...standardResponseSchema, 30 | [StatusCodes.OK]: responseBodySchema, 31 | }, 32 | }, 33 | handler: async (_req, res) => { 34 | const config = await getConfig(); 35 | res.status(StatusCodes.OK).send({ 36 | result: { 37 | minWalletBalance: config.minWalletBalance, 38 | }, 39 | }); 40 | }, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/server/routes/configuration/backend-wallet-balance/update.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { updateConfiguration } from "../../../../shared/db/configuration/update-configuration"; 5 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 6 | import { WeiAmountStringSchema } from "../../../schemas/number"; 7 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 8 | import { responseBodySchema } from "./get"; 9 | 10 | const requestBodySchema = Type.Partial( 11 | Type.Object({ 12 | minWalletBalance: { 13 | ...WeiAmountStringSchema, 14 | description: "Minimum wallet balance in wei", 15 | }, 16 | }), 17 | ); 18 | 19 | export async function updateBackendWalletBalanceConfiguration( 20 | fastify: FastifyInstance, 21 | ) { 22 | fastify.route<{ 23 | Body: Static; 24 | }>({ 25 | method: "POST", 26 | url: "/configuration/backend-wallet-balance", 27 | schema: { 28 | summary: "Update backend wallet balance configuration", 29 | description: "Update backend wallet balance configuration", 30 | tags: ["Configuration"], 31 | operationId: "updateBackendWalletBalanceConfiguration", 32 | body: requestBodySchema, 33 | response: { 34 | ...standardResponseSchema, 35 | [StatusCodes.OK]: responseBodySchema, 36 | }, 37 | }, 38 | handler: async (req, res) => { 39 | await updateConfiguration({ ...req.body }); 40 | const config = await getConfig(false); 41 | 42 | res.status(StatusCodes.OK).send({ 43 | result: { 44 | minWalletBalance: config.minWalletBalance, 45 | }, 46 | }); 47 | }, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/server/routes/configuration/cache/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | export const responseBodySchema = Type.Object({ 8 | result: Type.Object({ 9 | clearCacheCronSchedule: Type.String(), 10 | }), 11 | }); 12 | 13 | export async function getCacheConfiguration(fastify: FastifyInstance) { 14 | fastify.route<{ 15 | Reply: Static; 16 | }>({ 17 | method: "GET", 18 | url: "/configuration/cache", 19 | schema: { 20 | summary: "Get cache configuration", 21 | description: "Get cache configuration", 22 | tags: ["Configuration"], 23 | operationId: "getCacheConfiguration", 24 | response: { 25 | ...standardResponseSchema, 26 | [StatusCodes.OK]: responseBodySchema, 27 | }, 28 | }, 29 | handler: async (_req, res) => { 30 | const config = await getConfig(); 31 | res.status(StatusCodes.OK).send({ 32 | result: { 33 | clearCacheCronSchedule: config.clearCacheCronSchedule, 34 | }, 35 | }); 36 | }, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/server/routes/configuration/chains/get.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { chainResponseSchema } from "../../../schemas/chain"; 6 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 7 | 8 | export const responseBodySchema = Type.Object({ 9 | result: Type.Array(chainResponseSchema), 10 | }); 11 | 12 | export async function getChainsConfiguration(fastify: FastifyInstance) { 13 | fastify.route<{ 14 | Reply: Static; 15 | }>({ 16 | method: "GET", 17 | url: "/configuration/chains", 18 | schema: { 19 | summary: "Get chain overrides configuration", 20 | description: "Get chain overrides configuration", 21 | tags: ["Configuration"], 22 | operationId: "getChainsConfiguration", 23 | response: { 24 | ...standardResponseSchema, 25 | [StatusCodes.OK]: responseBodySchema, 26 | }, 27 | }, 28 | handler: async (_req, res) => { 29 | const config = await getConfig(); 30 | const result: Static[] = config.chainOverrides 31 | ? JSON.parse(config.chainOverrides) 32 | : []; 33 | 34 | res.status(StatusCodes.OK).send({ result }); 35 | }, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/server/routes/configuration/contract-subscriptions/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { 6 | contractSubscriptionConfigurationSchema, 7 | standardResponseSchema, 8 | } from "../../../schemas/shared-api-schemas"; 9 | 10 | const responseSchema = Type.Object({ 11 | result: contractSubscriptionConfigurationSchema, 12 | }); 13 | 14 | export async function getContractSubscriptionsConfiguration( 15 | fastify: FastifyInstance, 16 | ) { 17 | fastify.route<{ 18 | Reply: Static; 19 | }>({ 20 | method: "GET", 21 | url: "/configuration/contract-subscriptions", 22 | schema: { 23 | summary: "Get Contract Subscriptions configuration", 24 | description: "Get the configuration for Contract Subscriptions", 25 | tags: ["Configuration"], 26 | operationId: "getContractSubscriptionsConfiguration", 27 | response: { 28 | ...standardResponseSchema, 29 | [StatusCodes.OK]: responseSchema, 30 | }, 31 | }, 32 | handler: async (_req, res) => { 33 | const config = await getConfig(); 34 | res.status(StatusCodes.OK).send({ 35 | result: { 36 | maxBlocksToIndex: config.maxBlocksToIndex, 37 | contractSubscriptionsRequeryDelaySeconds: 38 | config.contractSubscriptionsRequeryDelaySeconds, 39 | }, 40 | }); 41 | }, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/server/routes/configuration/cors/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | import { mandatoryAllowedCorsUrls } from "../../../utils/cors-urls"; 7 | 8 | export const responseBodySchema = Type.Object({ 9 | result: Type.Array(Type.String()), 10 | }); 11 | 12 | export async function getCorsConfiguration(fastify: FastifyInstance) { 13 | fastify.route<{ 14 | Reply: Static; 15 | }>({ 16 | method: "GET", 17 | url: "/configuration/cors", 18 | schema: { 19 | summary: "Get CORS configuration", 20 | description: "Get CORS configuration", 21 | tags: ["Configuration"], 22 | operationId: "getCorsConfiguration", 23 | response: { 24 | ...standardResponseSchema, 25 | [StatusCodes.OK]: responseBodySchema, 26 | }, 27 | }, 28 | handler: async (_req, res) => { 29 | const config = await getConfig(false); 30 | 31 | // Omit required domains. 32 | const omitted = config.accessControlAllowOrigin 33 | .split(",") 34 | .filter((url) => !mandatoryAllowedCorsUrls.includes(url)); 35 | 36 | res.status(StatusCodes.OK).send({ 37 | result: omitted, 38 | }); 39 | }, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/server/routes/configuration/ip/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | export const responseBodySchema = Type.Object({ 8 | result: Type.Array(Type.String()), 9 | }); 10 | 11 | export async function getIpAllowlist(fastify: FastifyInstance) { 12 | fastify.route<{ 13 | Reply: Static; 14 | }>({ 15 | method: "GET", 16 | url: "/configuration/ip-allowlist", 17 | schema: { 18 | summary: "Get Allowed IP Addresses", 19 | description: "Get the list of allowed IP addresses", 20 | tags: ["Configuration"], 21 | operationId: "getIpAllowlist", 22 | response: { 23 | ...standardResponseSchema, 24 | [StatusCodes.OK]: responseBodySchema, 25 | }, 26 | }, 27 | handler: async (_req, res) => { 28 | const config = await getConfig(false); 29 | 30 | res.status(StatusCodes.OK).send({ 31 | result: config.ipAllowlist, 32 | }); 33 | }, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/server/routes/configuration/wallet-subscriptions/get.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getConfig } from "../../../../shared/utils/cache/get-config"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | const responseBodySchema = Type.Object({ 8 | result: Type.Object({ 9 | walletSubscriptionsCronSchedule: Type.String(), 10 | }), 11 | }); 12 | 13 | export async function getWalletSubscriptionsConfiguration( 14 | fastify: FastifyInstance, 15 | ) { 16 | fastify.route<{ 17 | Reply: Static; 18 | }>({ 19 | method: "GET", 20 | url: "/configuration/wallet-subscriptions", 21 | schema: { 22 | summary: "Get wallet subscriptions configuration", 23 | description: 24 | "Get wallet subscriptions configuration including cron schedule", 25 | tags: ["Configuration"], 26 | operationId: "getWalletSubscriptionsConfiguration", 27 | response: { 28 | ...standardResponseSchema, 29 | [StatusCodes.OK]: responseBodySchema, 30 | }, 31 | }, 32 | handler: async (_req, res) => { 33 | const config = await getConfig(false); 34 | res.status(StatusCodes.OK).send({ 35 | result: { 36 | walletSubscriptionsCronSchedule: 37 | config.walletSubscriptionsCronSchedule || "*/30 * * * * *", 38 | }, 39 | }); 40 | }, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/server/routes/contract/extensions/account-factory/index.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import { getAllAccounts } from "./read/get-all-accounts"; 3 | import { getAssociatedAccounts } from "./read/get-associated-accounts"; 4 | import { isAccountDeployed } from "./read/is-account-deployed"; 5 | import { predictAccountAddress } from "./read/predict-account-address"; 6 | import { createAccount } from "./write/create-account"; 7 | 8 | export const accountFactoryRoutes = async (fastify: FastifyInstance) => { 9 | // GET 10 | await fastify.register(getAllAccounts); 11 | await fastify.register(getAssociatedAccounts); 12 | await fastify.register(isAccountDeployed); 13 | await fastify.register(predictAccountAddress); 14 | 15 | // POST 16 | await fastify.register(createAccount); 17 | }; 18 | -------------------------------------------------------------------------------- /src/server/routes/contract/extensions/account/index.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import { getAllAdmins } from "./read/get-all-admins"; 3 | import { getAllSessions } from "./read/get-all-sessions"; 4 | import { grantAdmin } from "./write/grant-admin"; 5 | import { grantSession } from "./write/grant-session"; 6 | import { revokeAdmin } from "./write/revoke-admin"; 7 | import { revokeSession } from "./write/revoke-session"; 8 | import { updateSession } from "./write/update-session"; 9 | 10 | export const accountRoutes = async (fastify: FastifyInstance) => { 11 | // GET 12 | await fastify.register(getAllAdmins); 13 | await fastify.register(getAllSessions); 14 | 15 | // POST 16 | await fastify.register(grantAdmin); 17 | await fastify.register(revokeAdmin); 18 | await fastify.register(grantSession); 19 | await fastify.register(revokeSession); 20 | await fastify.register(updateSession); 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/routes/contract/extensions/account/read/get-all-admins.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getContract } from "../../../../../../shared/utils/cache/get-contract"; 5 | import { 6 | contractParamSchema, 7 | standardResponseSchema, 8 | } from "../../../../../schemas/shared-api-schemas"; 9 | import { getChainIdFromChain } from "../../../../../utils/chain"; 10 | 11 | const responseBodySchema = Type.Object({ 12 | result: Type.Array(Type.String(), { 13 | description: "The address of the admins on this account", 14 | }), 15 | }); 16 | 17 | export const getAllAdmins = async (fastify: FastifyInstance) => { 18 | fastify.route<{ 19 | Params: Static; 20 | Reply: Static; 21 | }>({ 22 | method: "GET", 23 | url: "/contract/:chain/:contractAddress/account/admins/get-all", 24 | schema: { 25 | summary: "Get all admins", 26 | description: "Get all admins for a smart account.", 27 | tags: ["Account"], 28 | operationId: "getAllAdmins", 29 | params: contractParamSchema, 30 | response: { 31 | ...standardResponseSchema, 32 | [StatusCodes.OK]: responseBodySchema, 33 | }, 34 | }, 35 | handler: async (request, reply) => { 36 | const { chain, contractAddress } = request.params; 37 | const chainId = await getChainIdFromChain(chain); 38 | 39 | const contract = await getContract({ 40 | chainId, 41 | contractAddress, 42 | }); 43 | const accountAddresses = await contract.account.getAllAdmins(); 44 | 45 | reply.status(StatusCodes.OK).send({ 46 | result: accountAddresses, 47 | }); 48 | }, 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/server/routes/contract/subscriptions/get-contract-subscriptions.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getAllContractSubscriptions } from "../../../../shared/db/contract-subscriptions/get-contract-subscriptions"; 5 | import { 6 | contractSubscriptionSchema, 7 | toContractSubscriptionSchema, 8 | } from "../../../schemas/contract-subscription"; 9 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 10 | 11 | const responseSchema = Type.Object({ 12 | result: Type.Array(contractSubscriptionSchema), 13 | }); 14 | 15 | responseSchema.example = { 16 | result: [ 17 | { 18 | chain: "ethereum", 19 | contractAddress: "0x....", 20 | webhook: { 21 | url: "https://...", 22 | }, 23 | }, 24 | ], 25 | }; 26 | 27 | export async function getContractSubscriptions(fastify: FastifyInstance) { 28 | fastify.route<{ 29 | Reply: Static; 30 | }>({ 31 | method: "GET", 32 | url: "/contract-subscriptions/get-all", 33 | schema: { 34 | summary: "Get contract subscriptions", 35 | description: "Get all contract subscriptions.", 36 | tags: ["Contract-Subscriptions"], 37 | operationId: "getContractSubscriptions", 38 | response: { 39 | ...standardResponseSchema, 40 | [StatusCodes.OK]: responseSchema, 41 | }, 42 | }, 43 | handler: async (_request, reply) => { 44 | const contractSubscriptions = await getAllContractSubscriptions(); 45 | 46 | reply.status(StatusCodes.OK).send({ 47 | result: contractSubscriptions.map(toContractSubscriptionSchema), 48 | }); 49 | }, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/server/routes/contract/subscriptions/remove-contract-subscription.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { deleteContractSubscription } from "../../../../shared/db/contract-subscriptions/delete-contract-subscription"; 5 | import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; 6 | 7 | const bodySchema = Type.Object({ 8 | contractSubscriptionId: Type.String({ 9 | description: "The ID for an existing contract subscription.", 10 | }), 11 | }); 12 | 13 | const responseSchema = Type.Object({ 14 | result: Type.Object({ 15 | status: Type.String(), 16 | }), 17 | }); 18 | 19 | responseSchema.example = { 20 | result: { 21 | status: "success", 22 | }, 23 | }; 24 | 25 | export async function removeContractSubscription(fastify: FastifyInstance) { 26 | fastify.route<{ 27 | Body: Static; 28 | Reply: Static; 29 | }>({ 30 | method: "POST", 31 | url: "/contract-subscriptions/remove", 32 | schema: { 33 | summary: "Remove contract subscription", 34 | description: "Remove an existing contract subscription", 35 | tags: ["Contract-Subscriptions"], 36 | operationId: "removeContractSubscription", 37 | body: bodySchema, 38 | response: { 39 | ...standardResponseSchema, 40 | [StatusCodes.OK]: responseSchema, 41 | }, 42 | }, 43 | handler: async (request, reply) => { 44 | const { contractSubscriptionId } = request.body; 45 | 46 | await deleteContractSubscription(contractSubscriptionId); 47 | 48 | reply.status(StatusCodes.OK).send({ 49 | result: { 50 | status: "success", 51 | }, 52 | }); 53 | }, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/server/routes/deploy/contract-types.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import { PREBUILT_CONTRACTS_MAP } from "@thirdweb-dev/sdk"; 3 | import type { FastifyInstance } from "fastify"; 4 | import { StatusCodes } from "http-status-codes"; 5 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 6 | 7 | // OUTPUT 8 | export const responseBodySchema = Type.Object({ 9 | result: Type.Array(Type.String()), 10 | }); 11 | 12 | export async function contractTypes(fastify: FastifyInstance) { 13 | fastify.route<{ 14 | Reply: Static; 15 | }>({ 16 | method: "GET", 17 | url: "/deploy/contract-types", 18 | schema: { 19 | summary: "Get contract types", 20 | description: "Get all prebuilt contract types.", 21 | tags: ["Deploy"], 22 | operationId: "contractTypes", 23 | response: { 24 | ...standardResponseSchema, 25 | [StatusCodes.OK]: responseBodySchema, 26 | }, 27 | }, 28 | handler: async (_request, reply) => { 29 | reply.status(StatusCodes.OK).send({ 30 | result: Object.keys(PREBUILT_CONTRACTS_MAP), 31 | }); 32 | }, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/server/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | 5 | const responseBodySchema = Type.Object({ 6 | message: Type.String(), 7 | }); 8 | 9 | export async function home(fastify: FastifyInstance) { 10 | fastify.route<{ 11 | Reply: Static; 12 | }>({ 13 | method: "GET", 14 | url: "/", 15 | schema: { 16 | hide: true, 17 | summary: "/", 18 | description: "Instructions to manage your Engine", 19 | tags: ["System"], 20 | operationId: "home", 21 | response: { 22 | [StatusCodes.OK]: responseBodySchema, 23 | }, 24 | }, 25 | handler: async (_, res) => { 26 | return res.status(StatusCodes.OK).send({ 27 | message: 28 | "Engine is set up successfully. Manage your Engine from https://thirdweb.com/dashboard/engine.", 29 | }); 30 | }, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/server/routes/relayer/revoke.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { prisma } from "../../../shared/db/client"; 5 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 6 | 7 | const requestBodySchema = Type.Object({ 8 | id: Type.String(), 9 | }); 10 | 11 | const responseBodySchema = Type.Object({ 12 | result: Type.Object({ 13 | success: Type.Boolean(), 14 | }), 15 | }); 16 | 17 | export async function revokeRelayer(fastify: FastifyInstance) { 18 | fastify.route<{ 19 | Body: Static; 20 | Reply: Static; 21 | }>({ 22 | method: "POST", 23 | url: "/relayer/revoke", 24 | schema: { 25 | summary: "Revoke a relayer", 26 | description: "Revoke a relayer", 27 | tags: ["Relayer"], 28 | operationId: "revokeRelayer", 29 | body: requestBodySchema, 30 | response: { 31 | ...standardResponseSchema, 32 | [StatusCodes.OK]: responseBodySchema, 33 | }, 34 | }, 35 | handler: async (req, res) => { 36 | const { id } = req.body; 37 | 38 | await prisma.relayers.delete({ 39 | where: { 40 | id, 41 | }, 42 | }); 43 | 44 | res.status(StatusCodes.OK).send({ 45 | result: { 46 | success: true, 47 | }, 48 | }); 49 | }, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/server/routes/wallet-subscriptions/delete.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { deleteWalletSubscription } from "../../../shared/db/wallet-subscriptions/delete-wallet-subscription"; 5 | import { 6 | walletSubscriptionSchema, 7 | toWalletSubscriptionSchema, 8 | } from "../../schemas/wallet-subscription"; 9 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 10 | 11 | const responseSchema = Type.Object({ 12 | result: walletSubscriptionSchema, 13 | }); 14 | 15 | const paramsSchema = Type.Object({ 16 | subscriptionId: Type.String({ 17 | description: "The ID of the wallet subscription to update.", 18 | }), 19 | }); 20 | 21 | 22 | export async function deleteWalletSubscriptionRoute(fastify: FastifyInstance) { 23 | fastify.route<{ 24 | Reply: Static; 25 | Params: Static; 26 | }>({ 27 | method: "DELETE", 28 | url: "/wallet-subscriptions/:subscriptionId", 29 | schema: { 30 | summary: "Delete wallet subscription", 31 | description: "Delete an existing wallet subscription.", 32 | tags: ["Wallet-Subscriptions"], 33 | operationId: "deleteWalletSubscription", 34 | params: paramsSchema, 35 | response: { 36 | ...standardResponseSchema, 37 | [StatusCodes.OK]: responseSchema, 38 | }, 39 | }, 40 | handler: async (request, reply) => { 41 | const { subscriptionId } = request.params; 42 | 43 | const subscription = await deleteWalletSubscription(subscriptionId); 44 | 45 | reply.status(StatusCodes.OK).send({ 46 | result: toWalletSubscriptionSchema(subscription), 47 | }); 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/server/routes/wallet-subscriptions/get-all.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getAllWalletSubscriptions } from "../../../shared/db/wallet-subscriptions/get-all-wallet-subscriptions"; 5 | import { 6 | walletSubscriptionSchema, 7 | toWalletSubscriptionSchema, 8 | } from "../../schemas/wallet-subscription"; 9 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 10 | import { PaginationSchema } from "../../schemas/pagination"; 11 | 12 | const responseSchema = Type.Object({ 13 | result: Type.Array(walletSubscriptionSchema), 14 | }); 15 | 16 | export async function getAllWalletSubscriptionsRoute(fastify: FastifyInstance) { 17 | fastify.route<{ 18 | Reply: Static; 19 | Params: Static; 20 | }>({ 21 | method: "GET", 22 | url: "/wallet-subscriptions/get-all", 23 | schema: { 24 | params: PaginationSchema, 25 | summary: "Get wallet subscriptions", 26 | description: "Get all wallet subscriptions.", 27 | tags: ["Wallet-Subscriptions"], 28 | operationId: "getAllWalletSubscriptions", 29 | response: { 30 | ...standardResponseSchema, 31 | [StatusCodes.OK]: responseSchema, 32 | }, 33 | }, 34 | handler: async (request, reply) => { 35 | const { page, limit } = request.params; 36 | 37 | const subscriptions = await getAllWalletSubscriptions({ 38 | page, 39 | limit, 40 | }); 41 | 42 | reply.status(StatusCodes.OK).send({ 43 | result: subscriptions.map(toWalletSubscriptionSchema), 44 | }); 45 | }, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/server/routes/webhooks/events.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { WebhooksEventTypes } from "../../../shared/schemas/webhooks"; 5 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 6 | 7 | export const responseBodySchema = Type.Object({ 8 | result: Type.Array(Type.Enum(WebhooksEventTypes)), 9 | }); 10 | 11 | export async function getWebhooksEventTypes(fastify: FastifyInstance) { 12 | fastify.route<{ 13 | Reply: Static; 14 | }>({ 15 | method: "GET", 16 | url: "/webhooks/event-types", 17 | schema: { 18 | summary: "Get webhooks event types", 19 | description: "Get the all the webhooks event types", 20 | tags: ["Webhooks"], 21 | operationId: "getEventTypes", 22 | response: { 23 | ...standardResponseSchema, 24 | [StatusCodes.OK]: responseBodySchema, 25 | }, 26 | }, 27 | handler: async (_req, res) => { 28 | const eventTypesArray = Object.values(WebhooksEventTypes); 29 | res.status(StatusCodes.OK).send({ 30 | result: eventTypesArray, 31 | }); 32 | }, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/server/routes/webhooks/get-all.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getAllWebhooks } from "../../../shared/db/webhooks/get-all-webhooks"; 5 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 6 | import { WebhookSchema, toWebhookSchema } from "../../schemas/webhook"; 7 | 8 | const responseBodySchema = Type.Object({ 9 | result: Type.Array(WebhookSchema), 10 | }); 11 | 12 | export async function getAllWebhooksData(fastify: FastifyInstance) { 13 | fastify.route<{ 14 | Reply: Static; 15 | }>({ 16 | method: "GET", 17 | url: "/webhooks/get-all", 18 | schema: { 19 | summary: "Get all webhooks configured", 20 | description: "Get all webhooks configuration data set up on Engine", 21 | tags: ["Webhooks"], 22 | operationId: "listWebhooks", 23 | response: { 24 | ...standardResponseSchema, 25 | [StatusCodes.OK]: responseBodySchema, 26 | }, 27 | }, 28 | handler: async (_req, res) => { 29 | const webhooks = await getAllWebhooks(); 30 | 31 | res.status(StatusCodes.OK).send({ 32 | result: webhooks.map(toWebhookSchema), 33 | }); 34 | }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/server/routes/webhooks/revoke.ts: -------------------------------------------------------------------------------- 1 | import { Type, type Static } from "@sinclair/typebox"; 2 | import type { FastifyInstance } from "fastify"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { getWebhook } from "../../../shared/db/webhooks/get-webhook"; 5 | import { deleteWebhook } from "../../../shared/db/webhooks/revoke-webhook"; 6 | import { createCustomError } from "../../middleware/error"; 7 | import { standardResponseSchema } from "../../schemas/shared-api-schemas"; 8 | 9 | const requestBodySchema = Type.Object({ 10 | id: Type.Integer({ minimum: 0 }), 11 | }); 12 | 13 | const responseBodySchema = Type.Object({ 14 | result: Type.Object({ 15 | success: Type.Boolean(), 16 | }), 17 | }); 18 | 19 | export async function revokeWebhook(fastify: FastifyInstance) { 20 | fastify.route<{ 21 | Body: Static; 22 | Reply: Static; 23 | }>({ 24 | method: "POST", 25 | url: "/webhooks/revoke", 26 | schema: { 27 | summary: "Revoke webhook", 28 | description: "Revoke a Webhook", 29 | tags: ["Webhooks"], 30 | operationId: "revoke", 31 | body: requestBodySchema, 32 | response: { 33 | ...standardResponseSchema, 34 | [StatusCodes.OK]: responseBodySchema, 35 | }, 36 | }, 37 | handler: async (req, res) => { 38 | const { id } = req.body; 39 | 40 | const webhook = await getWebhook(id); 41 | if (!webhook) { 42 | throw createCustomError( 43 | "Webhook not found.", 44 | StatusCodes.BAD_REQUEST, 45 | "BAD_REQUEST", 46 | ); 47 | } 48 | 49 | await deleteWebhook(id); 50 | 51 | res.status(StatusCodes.OK).send({ 52 | result: { 53 | success: true, 54 | }, 55 | }); 56 | }, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/server/schemas/account/index.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | import { AddressSchema } from "../address"; 3 | 4 | export const sessionSchema = Type.Object({ 5 | signerAddress: AddressSchema, 6 | startDate: Type.String(), 7 | expirationDate: Type.String(), 8 | nativeTokenLimitPerTransaction: Type.String(), 9 | approvedCallTargets: Type.Array(Type.String()), 10 | }); 11 | -------------------------------------------------------------------------------- /src/server/schemas/address.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | /** 4 | * An EVM address schema. Override the description like this: 5 | * 6 | * ```typescript 7 | * to: { 8 | * ...AddressSchema, 9 | * description: "The recipient wallet address.", 10 | * }, 11 | * ``` 12 | */ 13 | export const AddressSchema = Type.RegExp(/^0x[a-fA-F0-9]{40}$/, { 14 | description: "A contract or wallet address", 15 | examples: ["0x000000000000000000000000000000000000dead"], 16 | }); 17 | 18 | export const TransactionHashSchema = Type.RegExp(/^0x[a-fA-F0-9]{64}$/, { 19 | description: "A transaction hash", 20 | examples: [ 21 | "0x1f31b57601a6f90312fd5e57a2924bc8333477de579ee37b197a0681ab438431", 22 | ], 23 | }); 24 | 25 | export const HexSchema = Type.RegExp(/^0x[a-fA-F0-9]*$/, { 26 | description: "A valid hex string", 27 | examples: ["0x68656c6c6f20776f726c64"], 28 | }); 29 | -------------------------------------------------------------------------------- /src/server/schemas/chain/index.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | export const chainIdOrSlugSchema = Type.RegExp(/^[\w-]{1,50}$/, { 4 | description: `A chain ID ("137") or slug ("polygon-amoy-testnet"). Chain ID is preferred.`, 5 | examples: ["80002"], 6 | }); 7 | 8 | export const chainRequestQuerystringSchema = Type.Object({ 9 | chain: chainIdOrSlugSchema, 10 | }); 11 | 12 | export const chainResponseSchema = Type.Partial( 13 | Type.Object({ 14 | name: Type.String({ 15 | description: "Chain name", 16 | }), 17 | chain: Type.String({ 18 | description: "Chain name", 19 | }), 20 | rpc: Type.Array( 21 | Type.String({ 22 | description: "RPC URL", 23 | }), 24 | ), 25 | nativeCurrency: Type.Object({ 26 | name: Type.String({ 27 | description: "Native currency name", 28 | }), 29 | symbol: Type.String({ 30 | description: "Native currency symbol", 31 | }), 32 | decimals: Type.Number({ 33 | description: "Native currency decimals", 34 | }), 35 | }), 36 | shortName: Type.String({ 37 | description: "Chain short name", 38 | }), 39 | chainId: Type.Integer({ 40 | description: "Chain ID", 41 | }), 42 | testnet: Type.Boolean({ 43 | description: "Is testnet", 44 | }), 45 | slug: Type.String({ 46 | description: "Chain slug", 47 | }), 48 | }), 49 | ); 50 | -------------------------------------------------------------------------------- /src/server/schemas/contract-subscription.ts: -------------------------------------------------------------------------------- 1 | import type { ContractSubscriptions, Webhooks } from "@prisma/client"; 2 | import { Type, type Static } from "@sinclair/typebox"; 3 | import { AddressSchema } from "./address"; 4 | import { WebhookSchema, toWebhookSchema } from "./webhook"; 5 | 6 | export const contractSubscriptionSchema = Type.Object({ 7 | id: Type.String(), 8 | chainId: Type.Integer(), 9 | contractAddress: AddressSchema, 10 | webhook: Type.Optional(WebhookSchema), 11 | processEventLogs: Type.Boolean(), 12 | filterEvents: Type.Array(Type.String()), 13 | processTransactionReceipts: Type.Boolean(), 14 | filterFunctions: Type.Array(Type.String()), 15 | createdAt: Type.Unsafe({ 16 | type: "string", 17 | format: "date", 18 | }), 19 | }); 20 | 21 | export const toContractSubscriptionSchema = ( 22 | contractSubscription: ContractSubscriptions & { webhook: Webhooks | null }, 23 | ): Static => ({ 24 | id: contractSubscription.id, 25 | chainId: Number.parseInt(contractSubscription.chainId), 26 | contractAddress: contractSubscription.contractAddress, 27 | webhook: contractSubscription.webhook 28 | ? toWebhookSchema(contractSubscription.webhook) 29 | : undefined, 30 | processEventLogs: contractSubscription.processEventLogs, 31 | filterEvents: contractSubscription.filterEvents, 32 | processTransactionReceipts: contractSubscription.processTransactionReceipts, 33 | filterFunctions: contractSubscription.filterFunctions, 34 | createdAt: contractSubscription.createdAt, 35 | }); 36 | -------------------------------------------------------------------------------- /src/server/schemas/event-log.ts: -------------------------------------------------------------------------------- 1 | import type { ContractEventLogs } from "@prisma/client"; 2 | import { Type, type Static } from "@sinclair/typebox"; 3 | import { AddressSchema, TransactionHashSchema } from "./address"; 4 | 5 | export const eventLogSchema = Type.Object({ 6 | chainId: Type.Integer(), 7 | contractAddress: AddressSchema, 8 | blockNumber: Type.Integer(), 9 | transactionHash: TransactionHashSchema, 10 | topics: Type.Array(Type.String()), 11 | data: Type.String(), 12 | eventName: Type.Optional(Type.String()), 13 | decodedLog: Type.Any(), 14 | timestamp: Type.Integer(), 15 | transactionIndex: Type.Integer(), 16 | logIndex: Type.Integer(), 17 | }); 18 | 19 | export const toEventLogSchema = ( 20 | log: ContractEventLogs, 21 | ): Static => { 22 | const topics: string[] = []; 23 | for (const val of [ log.topic0, log.topic1, log.topic2, log.topic3 ]) { 24 | if (val) { 25 | topics.push(val); 26 | } 27 | } 28 | 29 | return { 30 | chainId: Number.parseInt(log.chainId), 31 | contractAddress: log.contractAddress, 32 | blockNumber: log.blockNumber, 33 | transactionHash: log.transactionHash, 34 | topics, 35 | data: log.data, 36 | eventName: log.eventName ?? undefined, 37 | decodedLog: log.decodedLog, 38 | timestamp: log.timestamp.getTime(), 39 | transactionIndex: log.transactionIndex, 40 | logIndex: log.logIndex, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/server/schemas/http-headers/thirdweb-sdk-version.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | export const thirdwebSdkVersionSchema = Type.Object({ 4 | "x-thirdweb-sdk-version": Type.Optional( 5 | Type.String({ 6 | description: `Override the thirdweb sdk version used. Example: "5" for v5 SDK compatibility.`, 7 | }), 8 | ), 9 | }); 10 | -------------------------------------------------------------------------------- /src/server/schemas/number.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | export const TokenAmountStringSchema = Type.RegExp(/^\d+(\.\d+)?$/, { 4 | description: 'An amount in native token (decimals allowed). Example: "0.1"', 5 | examples: ["0.1"], 6 | }); 7 | 8 | export const WeiAmountStringSchema = Type.RegExp(/^\d+$/, { 9 | description: 'An amount in wei (no decimals). Example: "50000000000"', 10 | examples: ["50000000000"], 11 | }); 12 | 13 | export const NumberStringSchema = Type.RegExp(/^\d+$/, { 14 | examples: ["42"], 15 | }); 16 | -------------------------------------------------------------------------------- /src/server/schemas/pagination.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | export const PaginationSchema = Type.Object({ 4 | page: Type.Integer({ 5 | description: "Specify the page number.", 6 | examples: [1], 7 | default: 1, 8 | minimum: 1, 9 | }), 10 | limit: Type.Integer({ 11 | description: "Specify the number of results to return per page.", 12 | examples: [100], 13 | default: 100, 14 | }), 15 | }); 16 | -------------------------------------------------------------------------------- /src/server/schemas/transaction-receipt.ts: -------------------------------------------------------------------------------- 1 | import type { ContractTransactionReceipts } from "@prisma/client"; 2 | import { Type, type Static } from "@sinclair/typebox"; 3 | import { AddressSchema, TransactionHashSchema } from "./address"; 4 | 5 | export const transactionReceiptSchema = Type.Object({ 6 | chainId: Type.Integer(), 7 | blockNumber: Type.Integer(), 8 | contractAddress: AddressSchema, 9 | transactionHash: TransactionHashSchema, 10 | blockHash: Type.String(), 11 | timestamp: Type.Integer(), 12 | data: Type.String(), 13 | value: Type.String(), 14 | 15 | to: Type.String(), 16 | from: Type.String(), 17 | transactionIndex: Type.Integer(), 18 | 19 | gasUsed: Type.String(), 20 | effectiveGasPrice: Type.String(), 21 | status: Type.Integer(), 22 | }); 23 | 24 | export const toTransactionReceiptSchema = ( 25 | transactionReceipt: ContractTransactionReceipts, 26 | ): Static => ({ 27 | chainId: Number.parseInt(transactionReceipt.chainId), 28 | blockNumber: transactionReceipt.blockNumber, 29 | contractAddress: transactionReceipt.contractAddress, 30 | transactionHash: transactionReceipt.transactionHash, 31 | blockHash: transactionReceipt.blockHash, 32 | timestamp: transactionReceipt.timestamp.getTime(), 33 | data: transactionReceipt.data, 34 | value: transactionReceipt.value, 35 | to: transactionReceipt.to, 36 | from: transactionReceipt.from, 37 | transactionIndex: transactionReceipt.transactionIndex, 38 | gasUsed: transactionReceipt.gasUsed, 39 | effectiveGasPrice: transactionReceipt.effectiveGasPrice, 40 | status: transactionReceipt.status, 41 | }); 42 | -------------------------------------------------------------------------------- /src/server/schemas/transaction/authorization.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import { AddressSchema } from "../address"; 3 | import { requiredAddress } from "../wallet"; 4 | import { requiredBigInt } from "../../../shared/utils/primitive-types"; 5 | 6 | export const authorizationSchema = Type.Object({ 7 | address: AddressSchema, 8 | chainId: Type.Integer(), 9 | nonce: Type.String(), 10 | r: Type.String(), 11 | s: Type.String(), 12 | yParity: Type.Number(), 13 | }); 14 | 15 | export const authorizationListSchema = Type.Optional( 16 | Type.Array(authorizationSchema), 17 | ); 18 | 19 | export const toParsedAuthorization = ( 20 | authorization: Static, 21 | ) => { 22 | return { 23 | address: requiredAddress(authorization.address, "[Authorization List]"), 24 | chainId: authorization.chainId, 25 | nonce: requiredBigInt(authorization.nonce, "[Authorization List] -> nonce"), 26 | r: requiredBigInt(authorization.r, "[Authorization List] -> r"), 27 | s: requiredBigInt(authorization.s, "[Authorization List] -> s"), 28 | yParity: authorization.yParity, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/server/schemas/transaction/raw-transaction-parms.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | import { AddressSchema, HexSchema } from "../address"; 3 | import { WeiAmountStringSchema } from "../number"; 4 | 5 | export const RawTransactionParamsSchema = Type.Object({ 6 | toAddress: Type.Optional(AddressSchema), 7 | data: HexSchema, 8 | value: WeiAmountStringSchema, 9 | }); 10 | -------------------------------------------------------------------------------- /src/server/schemas/wallet-subscription.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | import type { WalletSubscriptions, Webhooks } from "@prisma/client"; 3 | import { AddressSchema } from "./address"; 4 | import { 5 | WalletConditionsSchema, 6 | validateConditions, 7 | } from "../../shared/schemas/wallet-subscription-conditions"; 8 | 9 | type WalletSubscriptionWithWebhook = WalletSubscriptions & { 10 | webhook: Webhooks | null; 11 | }; 12 | 13 | export const walletSubscriptionSchema = Type.Object({ 14 | id: Type.String(), 15 | chainId: Type.String({ 16 | description: "The chain ID of the subscription.", 17 | }), 18 | walletAddress: AddressSchema, 19 | conditions: WalletConditionsSchema, 20 | webhook: Type.Optional( 21 | Type.Object({ 22 | url: Type.String(), 23 | }), 24 | ), 25 | createdAt: Type.String(), 26 | updatedAt: Type.String(), 27 | }); 28 | 29 | export type WalletSubscriptionSchema = typeof walletSubscriptionSchema; 30 | 31 | export function toWalletSubscriptionSchema( 32 | subscription: WalletSubscriptionWithWebhook, 33 | ) { 34 | return { 35 | id: subscription.id, 36 | chainId: subscription.chainId, 37 | walletAddress: subscription.walletAddress, 38 | conditions: validateConditions(subscription.conditions), 39 | webhook: 40 | subscription.webhookId && subscription.webhook 41 | ? { 42 | url: subscription.webhook.url, 43 | } 44 | : undefined, 45 | createdAt: subscription.createdAt.toISOString(), 46 | updatedAt: subscription.updatedAt.toISOString(), 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/server/schemas/webhook.ts: -------------------------------------------------------------------------------- 1 | import type { Webhooks } from "@prisma/client"; 2 | import { Type, type Static } from "@sinclair/typebox"; 3 | 4 | export const WebhookSchema = Type.Object({ 5 | id: Type.Integer(), 6 | url: Type.String(), 7 | name: Type.Union([Type.String(), Type.Null()]), 8 | secret: Type.Optional(Type.String()), 9 | eventType: Type.String(), 10 | active: Type.Boolean(), 11 | createdAt: Type.String(), 12 | }); 13 | 14 | export const toWebhookSchema = ( 15 | webhook: Webhooks, 16 | ): Static => ({ 17 | url: webhook.url, 18 | name: webhook.name, 19 | eventType: webhook.eventType, 20 | secret: webhook.secret, 21 | createdAt: webhook.createdAt.toISOString(), 22 | active: !webhook.revokedAt, 23 | id: webhook.id, 24 | }); 25 | -------------------------------------------------------------------------------- /src/server/schemas/websocket/index.ts: -------------------------------------------------------------------------------- 1 | // types.ts 2 | import type { WebSocket } from "ws"; 3 | 4 | export interface UserSubscription { 5 | socket: WebSocket; 6 | requestId: string; 7 | } 8 | 9 | export const subscriptionsData: UserSubscription[] = []; 10 | -------------------------------------------------------------------------------- /src/server/utils/abi.ts: -------------------------------------------------------------------------------- 1 | import type { Abi } from "thirdweb/utils"; 2 | import type { AbiSchemaType } from "../schemas/contract"; 3 | 4 | export function sanitizeAbi(abi: AbiSchemaType | undefined): Abi | undefined { 5 | if (!abi) { 6 | return undefined; 7 | } 8 | 9 | return abi.map((item) => { 10 | if (item.type === "function") { 11 | return { 12 | ...item, 13 | // older versions of engine allowed passing in empty inputs/outputs, but necesasry for abi validation 14 | inputs: item.inputs || [], 15 | outputs: item.outputs || [], 16 | }; 17 | } 18 | return item; 19 | }) as Abi; 20 | } 21 | 22 | export const sanitizeFunctionName = (val: string) => 23 | val.includes("(") && !val.startsWith("function ") ? `function ${val}` : val; 24 | -------------------------------------------------------------------------------- /src/server/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { getChainBySlugAsync } from "@thirdweb-dev/chains"; 2 | import { getChain } from "../../shared/utils/chain"; 3 | import { badChainError } from "../middleware/error"; 4 | 5 | /** 6 | * Given a valid chain name ('Polygon') or ID ('137'), return the numeric chain ID. 7 | * @throws if the chain is invalid or deprecated. 8 | */ 9 | export const getChainIdFromChain = async (input: string): Promise => { 10 | const chainId = Number.parseInt(input); 11 | if (!Number.isNaN(chainId)) { 12 | return (await getChain(chainId)).id; 13 | } 14 | 15 | try { 16 | const chainV4 = await getChainBySlugAsync(input.toLowerCase()); 17 | if (chainV4.status !== "deprecated") { 18 | return chainV4.chainId; 19 | } 20 | } catch {} 21 | 22 | throw badChainError(input); 23 | }; 24 | -------------------------------------------------------------------------------- /src/server/utils/convertor.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | 3 | const isHexBigNumber = (value: unknown) => { 4 | const isNonNullObject = typeof value === "object" && value !== null; 5 | const hasType = isNonNullObject && "type" in value; 6 | return hasType && value.type === "BigNumber" && "hex" in value; 7 | }; 8 | export const bigNumberReplacer = (value: unknown): unknown => { 9 | // if we find a BigNumber then make it into a string (since that is safe) 10 | if (BigNumber.isBigNumber(value) || isHexBigNumber(value)) { 11 | return BigNumber.from(value).toString(); 12 | } 13 | 14 | if (Array.isArray(value)) { 15 | return value.map(bigNumberReplacer); 16 | } 17 | 18 | if (typeof value === "bigint") { 19 | return value.toString(); 20 | } 21 | 22 | return value; 23 | }; 24 | -------------------------------------------------------------------------------- /src/server/utils/cors-urls.ts: -------------------------------------------------------------------------------- 1 | // URLs to check and add if missing 2 | export const mandatoryAllowedCorsUrls = [ 3 | "https://thirdweb.com", 4 | "https://embed.ipfscdn.io", 5 | ]; 6 | -------------------------------------------------------------------------------- /src/server/utils/marketplace-v3.ts: -------------------------------------------------------------------------------- 1 | import type { DirectListingV3, EnglishAuction, OfferV3 } from "@thirdweb-dev/sdk"; 2 | 3 | export const formatDirectListingV3Result = (listing: DirectListingV3) => { 4 | return { 5 | ...listing, 6 | currencyValuePerToken: { 7 | ...listing.currencyValuePerToken, 8 | value: listing.currencyValuePerToken.value.toString(), 9 | }, 10 | }; 11 | }; 12 | 13 | export const formatEnglishAuctionResult = (listing: EnglishAuction) => { 14 | return { 15 | ...listing, 16 | minimumBidCurrencyValue: { 17 | ...listing.minimumBidCurrencyValue, 18 | value: listing.minimumBidCurrencyValue.value.toString(), 19 | }, 20 | buyoutCurrencyValue: { 21 | ...listing.buyoutCurrencyValue, 22 | value: listing.buyoutCurrencyValue.value.toString(), 23 | }, 24 | }; 25 | }; 26 | 27 | export const formatOffersV3Result = (offer: OfferV3) => { 28 | return { 29 | ...offer, 30 | currencyValue: { 31 | ...offer.currencyValue, 32 | value: offer.currencyValue.value.toString(), 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/server/utils/openapi.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from "fastify"; 2 | import fs from "node:fs"; 3 | 4 | export const writeOpenApiToFile = (server: FastifyInstance) => { 5 | try { 6 | fs.writeFileSync( 7 | "./dist/openapi.json", 8 | JSON.stringify(server.swagger(), undefined, 2), 9 | ); 10 | } catch { 11 | // no-op 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/server/utils/transaction-overrides.ts: -------------------------------------------------------------------------------- 1 | import type { Static } from "@sinclair/typebox"; 2 | import { maybeBigInt } from "../../shared/utils/primitive-types"; 3 | import type { 4 | txOverridesSchema, 5 | txOverridesWithValueSchema, 6 | } from "../schemas/tx-overrides"; 7 | 8 | export const parseTransactionOverrides = ( 9 | overrides: 10 | | Static["txOverrides"] 11 | | Static["txOverrides"], 12 | ) => { 13 | if (!overrides) { 14 | return {}; 15 | } 16 | 17 | return { 18 | overrides: { 19 | gas: maybeBigInt(overrides.gas), 20 | gasPrice: maybeBigInt(overrides.gasPrice), 21 | maxFeePerGas: maybeBigInt(overrides.maxFeePerGas), 22 | maxPriorityFeePerGas: maybeBigInt(overrides.maxPriorityFeePerGas), 23 | }, 24 | timeoutSeconds: overrides.timeoutSeconds, 25 | // `value` may not be in the overrides object. 26 | value: "value" in overrides ? maybeBigInt(overrides.value) : undefined, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/server/utils/wallets/aws-kms-arn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Split an AWS KMS ARN into its parts. 3 | */ 4 | export function splitAwsKmsArn(arn: string) { 5 | // arn:aws:kms:::key/ 6 | const parts = arn.split(":"); 7 | if (parts.length < 6) { 8 | throw new Error("Invalid AWS KMS ARN"); 9 | } 10 | 11 | const keyId = parts[5].split("/")[1]; 12 | if (!keyId) { 13 | throw new Error("Invalid AWS KMS ARN"); 14 | } 15 | const keyIdExtension = parts.slice(6).join(":"); 16 | 17 | return { 18 | region: parts[3], 19 | accountId: parts[4], 20 | keyId: `${keyId}${keyIdExtension ? `:${keyIdExtension}` : ""}`, 21 | }; 22 | } 23 | 24 | /** 25 | * Get an AWS KMS ARN from its parts. 26 | */ 27 | export function getAwsKmsArn(options: { 28 | region: string; 29 | accountId: string; 30 | keyId: string; 31 | }) { 32 | return `arn:aws:kms:${options.region}:${options.accountId}:key/${options.keyId}`; 33 | } 34 | -------------------------------------------------------------------------------- /src/server/utils/wallets/fetch-aws-kms-wallet-params.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../../../shared/utils/cache/get-config"; 2 | 3 | export type AwsKmsWalletParams = { 4 | awsAccessKeyId: string; 5 | awsSecretAccessKey: string; 6 | 7 | awsRegion: string; 8 | }; 9 | 10 | export class FetchAwsKmsWalletParamsError extends Error {} 11 | 12 | /** 13 | * Fetches the AWS KMS wallet creation parameters from the configuration or overrides. 14 | * If any required parameter cannot be resolved from either the configuration or the overrides, an error is thrown. 15 | */ 16 | export async function fetchAwsKmsWalletParams( 17 | overrides: Partial, 18 | ): Promise { 19 | const config = await getConfig(); 20 | 21 | const awsAccessKeyId = 22 | overrides.awsAccessKeyId ?? config.walletConfiguration.aws?.awsAccessKeyId; 23 | 24 | if (!awsAccessKeyId) { 25 | throw new FetchAwsKmsWalletParamsError( 26 | "AWS access key ID is required for this wallet type. Could not find in configuration or params.", 27 | ); 28 | } 29 | 30 | const awsSecretAccessKey = 31 | overrides.awsSecretAccessKey ?? 32 | config.walletConfiguration.aws?.awsSecretAccessKey; 33 | 34 | if (!awsSecretAccessKey) { 35 | throw new FetchAwsKmsWalletParamsError( 36 | "AWS secretAccessKey is required for this wallet type. Could not find in configuration or params.", 37 | ); 38 | } 39 | 40 | const awsRegion = 41 | overrides.awsRegion ?? config.walletConfiguration.aws?.defaultAwsRegion; 42 | 43 | if (!awsRegion) { 44 | throw new FetchAwsKmsWalletParamsError( 45 | "AWS region is required for this wallet type. Could not find in configuration or params.", 46 | ); 47 | } 48 | 49 | return { 50 | awsAccessKeyId, 51 | awsSecretAccessKey, 52 | awsRegion, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/server/utils/wallets/gcp-kms-resource-path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Split a GCP KMS resource path into its parts. 3 | */ 4 | export function splitGcpKmsResourcePath(resourcePath: string) { 5 | const parts = resourcePath.split("/"); 6 | 7 | if (parts.length < 9) { 8 | throw new Error("Invalid GCP KMS resource path"); 9 | } 10 | 11 | return { 12 | projectId: parts[1], 13 | locationId: parts[3], 14 | keyRingId: parts[5], 15 | cryptoKeyId: parts[7], 16 | versionId: parts[9], 17 | }; 18 | } 19 | 20 | /** 21 | * Get a GCP KMS resource path from its parts. 22 | */ 23 | export function getGcpKmsResourcePath(options: { 24 | locationId: string; 25 | keyRingId: string; 26 | cryptoKeyId: string; 27 | versionId: string; 28 | projectId: string; 29 | }) { 30 | return `projects/${options.projectId}/locations/${options.locationId}/keyRings/${options.keyRingId}/cryptoKeys/${options.cryptoKeyId}/cryptoKeyVersions/${options.versionId}`; 31 | } 32 | -------------------------------------------------------------------------------- /src/server/utils/wallets/import-aws-kms-wallet.ts: -------------------------------------------------------------------------------- 1 | import { createWalletDetails } from "../../../shared/db/wallets/create-wallet-details"; 2 | import { WalletType } from "../../../shared/schemas/wallet"; 3 | import { thirdwebClient } from "../../../shared/utils/sdk"; 4 | import { splitAwsKmsArn } from "./aws-kms-arn"; 5 | import { getAwsKmsAccount } from "./get-aws-kms-account"; 6 | 7 | interface ImportAwsKmsWalletParams { 8 | awsKmsArn: string; 9 | crendentials: { 10 | accessKeyId: string; 11 | secretAccessKey: string; 12 | }; 13 | label?: string; 14 | } 15 | 16 | /** 17 | * Import an AWS KMS wallet, and store it into the database 18 | */ 19 | export const importAwsKmsWallet = async ({ 20 | crendentials, 21 | awsKmsArn, 22 | label, 23 | }: ImportAwsKmsWalletParams) => { 24 | const { keyId, region } = splitAwsKmsArn(awsKmsArn); 25 | const account = await getAwsKmsAccount({ 26 | client: thirdwebClient, 27 | keyId, 28 | config: { 29 | region, 30 | credentials: { 31 | accessKeyId: crendentials.accessKeyId, 32 | secretAccessKey: crendentials.secretAccessKey, 33 | }, 34 | }, 35 | }); 36 | 37 | const walletAddress = account.address; 38 | 39 | await createWalletDetails({ 40 | type: WalletType.awsKms, 41 | address: walletAddress, 42 | awsKmsArn, 43 | label, 44 | 45 | awsKmsAccessKeyId: crendentials.accessKeyId, 46 | awsKmsSecretAccessKey: crendentials.secretAccessKey, 47 | }); 48 | 49 | return walletAddress; 50 | }; 51 | -------------------------------------------------------------------------------- /src/server/utils/wallets/import-gcp-kms-wallet.ts: -------------------------------------------------------------------------------- 1 | import { createWalletDetails } from "../../../shared/db/wallets/create-wallet-details"; 2 | import { WalletType } from "../../../shared/schemas/wallet"; 3 | import { thirdwebClient } from "../../../shared/utils/sdk"; 4 | import { getGcpKmsAccount } from "./get-gcp-kms-account"; 5 | 6 | interface ImportGcpKmsWalletParams { 7 | gcpKmsResourcePath: string; 8 | label?: string; 9 | credentials: { 10 | email: string; 11 | privateKey: string; 12 | }; 13 | } 14 | 15 | /** 16 | * Import a GCP KMS wallet, and store it into the database 17 | * 18 | * If credentials.shouldStore is true, the GCP application credential email and private key will be stored 19 | * along with the wallet details, separately from the global configuration 20 | */ 21 | export const importGcpKmsWallet = async ({ 22 | label, 23 | gcpKmsResourcePath, 24 | credentials, 25 | }: ImportGcpKmsWalletParams) => { 26 | const account = await getGcpKmsAccount({ 27 | client: thirdwebClient, 28 | name: gcpKmsResourcePath, 29 | clientOptions: { 30 | credentials: { 31 | client_email: credentials.email, 32 | private_key: credentials.privateKey, 33 | }, 34 | }, 35 | }); 36 | 37 | const walletAddress = account.address; 38 | 39 | await createWalletDetails({ 40 | type: WalletType.gcpKms, 41 | address: walletAddress, 42 | label, 43 | gcpKmsResourcePath, 44 | 45 | gcpApplicationCredentialEmail: credentials.email, 46 | gcpApplicationCredentialPrivateKey: credentials.privateKey, 47 | }); 48 | 49 | return walletAddress; 50 | }; 51 | -------------------------------------------------------------------------------- /src/server/utils/wallets/import-local-wallet.ts: -------------------------------------------------------------------------------- 1 | import { LocalWallet } from "@thirdweb-dev/wallets"; 2 | import { env } from "../../../shared/utils/env"; 3 | import { LocalFileStorage } from "../storage/local-storage"; 4 | 5 | type ImportLocalWalletParams = 6 | | { 7 | method: "privateKey"; 8 | privateKey: string; 9 | label?: string; 10 | } 11 | | { 12 | method: "mnemonic"; 13 | mnemonic: string; 14 | label?: string; 15 | } 16 | | { 17 | method: "encryptedJson"; 18 | encryptedJson: string; 19 | password: string; 20 | label?: string; 21 | }; 22 | 23 | export const importLocalWallet = async ( 24 | options: ImportLocalWalletParams, 25 | ): Promise => { 26 | const wallet = new LocalWallet(); 27 | 28 | // TODO: Is there a case where we should enable encryption: true? 29 | let walletAddress: string; 30 | switch (options.method) { 31 | case "privateKey": 32 | walletAddress = await wallet.import({ 33 | privateKey: options.privateKey, 34 | encryption: false, 35 | }); 36 | break; 37 | case "mnemonic": 38 | walletAddress = await wallet.import({ 39 | mnemonic: options.mnemonic, 40 | encryption: false, 41 | }); 42 | break; 43 | case "encryptedJson": 44 | walletAddress = await wallet.import({ 45 | encryptedJson: options.encryptedJson, 46 | password: options.password, 47 | }); 48 | break; 49 | } 50 | 51 | // Creating wallet details gets handled by LocalFileStorage 52 | await wallet.save({ 53 | strategy: "encryptedJson", 54 | password: env.ENCRYPTION_PASSWORD, 55 | storage: new LocalFileStorage(walletAddress, options.label), 56 | }); 57 | 58 | return walletAddress; 59 | }; 60 | -------------------------------------------------------------------------------- /src/shared/db/chain-indexers/get-chain-indexer.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import type { PrismaTransaction } from "../../schemas/prisma"; 3 | import { getPrismaWithPostgresTx } from "../client"; 4 | 5 | interface GetLastIndexedBlockParams { 6 | chainId: number; 7 | pgtx?: PrismaTransaction; 8 | } 9 | 10 | export const getLastIndexedBlock = async ({ 11 | chainId, 12 | pgtx, 13 | }: GetLastIndexedBlockParams) => { 14 | const prisma = getPrismaWithPostgresTx(pgtx); 15 | 16 | const indexedChain = await prisma.chainIndexers.findUnique({ 17 | where: { 18 | chainId: chainId.toString(), 19 | }, 20 | }); 21 | 22 | if (indexedChain) { 23 | return indexedChain.lastIndexedBlock; 24 | } 25 | }; 26 | 27 | interface GetBlockForIndexingParams { 28 | chainId: number; 29 | pgtx?: PrismaTransaction; 30 | } 31 | 32 | export const getBlockForIndexing = async ({ 33 | chainId, 34 | pgtx, 35 | }: GetBlockForIndexingParams) => { 36 | const prisma = getPrismaWithPostgresTx(pgtx); 37 | 38 | const lastIndexedBlock = await prisma.$queryRaw< 39 | { lastIndexedBlock: number }[] 40 | >` 41 | SELECT 42 | "lastIndexedBlock" 43 | FROM 44 | "chain_indexers" 45 | WHERE 46 | "chainId"=${Prisma.sql`${chainId.toString()}`} 47 | FOR UPDATE NOWAIT 48 | `; 49 | return lastIndexedBlock[0].lastIndexedBlock; 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/db/chain-indexers/upsert-chain-indexer.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaTransaction } from "../../schemas/prisma"; 2 | import { getPrismaWithPostgresTx } from "../client"; 3 | 4 | interface UpsertChainIndexerParams { 5 | chainId: number; 6 | currentBlockNumber: number; 7 | pgtx?: PrismaTransaction; 8 | } 9 | 10 | export const upsertChainIndexer = async ({ 11 | chainId, 12 | currentBlockNumber, 13 | pgtx, 14 | }: UpsertChainIndexerParams) => { 15 | const prisma = getPrismaWithPostgresTx(pgtx); 16 | return prisma.chainIndexers.upsert({ 17 | where: { 18 | chainId: chainId.toString(), 19 | }, 20 | update: { 21 | chainId: chainId.toString(), 22 | lastIndexedBlock: currentBlockNumber, 23 | }, 24 | create: { 25 | chainId: chainId.toString(), 26 | lastIndexedBlock: currentBlockNumber, 27 | }, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/shared/db/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import pg, { type Knex } from "knex"; 3 | import type { PrismaTransaction } from "../schemas/prisma"; 4 | import { env } from "../utils/env"; 5 | 6 | export const prisma = new PrismaClient({ 7 | log: ["info"], 8 | }); 9 | 10 | export const getPrismaWithPostgresTx = (pgtx?: PrismaTransaction) => { 11 | return pgtx || prisma; 12 | }; 13 | 14 | export const knex = pg({ 15 | client: "pg", 16 | connection: { 17 | connectionString: env.POSTGRES_CONNECTION_URL, 18 | ssl: { 19 | rejectUnauthorized: false, 20 | }, 21 | }, 22 | acquireConnectionTimeout: 30000, 23 | } as Knex.Config); 24 | 25 | export const isDatabaseReachable = async () => { 26 | try { 27 | await prisma.walletDetails.findFirst(); 28 | return true; 29 | } catch { 30 | return false; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/db/contract-event-logs/create-contract-event-logs.ts: -------------------------------------------------------------------------------- 1 | import type { ContractEventLogs, Prisma } from "@prisma/client"; 2 | import type { PrismaTransaction } from "../../schemas/prisma"; 3 | import { getPrismaWithPostgresTx } from "../client"; 4 | 5 | export interface BulkInsertContractLogsParams { 6 | pgtx?: PrismaTransaction; 7 | logs: Prisma.ContractEventLogsCreateInput[]; 8 | } 9 | 10 | export const bulkInsertContractEventLogs = async ({ 11 | pgtx, 12 | logs, 13 | }: BulkInsertContractLogsParams): Promise => { 14 | const prisma = getPrismaWithPostgresTx(pgtx); 15 | return await prisma.contractEventLogs.createManyAndReturn({ 16 | data: logs, 17 | skipDuplicates: true, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/shared/db/contract-event-logs/delete-contract-event-logs.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface DeleteContractEventLogsParams { 4 | chainId: number; 5 | contractAddress: string; 6 | } 7 | 8 | export const deleteContractEventLogs = async ({ 9 | chainId, 10 | contractAddress, 11 | }: DeleteContractEventLogsParams) => { 12 | return prisma.contractEventLogs.deleteMany({ 13 | where: { 14 | chainId: chainId.toString(), 15 | contractAddress, 16 | }, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/db/contract-subscriptions/create-contract-subscription.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface CreateContractSubscriptionParams { 4 | chainId: number; 5 | contractAddress: string; 6 | webhookId?: number; 7 | processEventLogs: boolean; 8 | filterEvents: string[]; 9 | processTransactionReceipts: boolean; 10 | filterFunctions: string[]; 11 | } 12 | 13 | export const createContractSubscription = async ({ 14 | chainId, 15 | contractAddress, 16 | webhookId, 17 | processEventLogs, 18 | filterEvents, 19 | processTransactionReceipts, 20 | filterFunctions, 21 | }: CreateContractSubscriptionParams) => { 22 | return prisma.contractSubscriptions.create({ 23 | data: { 24 | chainId: chainId.toString(), 25 | contractAddress, 26 | webhookId, 27 | processEventLogs, 28 | filterEvents, 29 | processTransactionReceipts, 30 | filterFunctions, 31 | }, 32 | include: { 33 | webhook: true, 34 | }, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/shared/db/contract-subscriptions/delete-contract-subscription.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export const deleteContractSubscription = async (id: string) => { 4 | return prisma.contractSubscriptions.update({ 5 | where: { id }, 6 | data: { 7 | deletedAt: new Date(), 8 | }, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/db/contract-subscriptions/get-contract-subscriptions.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export interface GetContractSubscriptionsParams { 4 | chainId: number; 5 | contractAddress: string; 6 | } 7 | 8 | export const isContractSubscribed = async ({ 9 | chainId, 10 | contractAddress, 11 | }: GetContractSubscriptionsParams) => { 12 | const contractSubscription = await prisma.contractSubscriptions.findFirst({ 13 | where: { 14 | chainId: chainId.toString(), 15 | contractAddress, 16 | deletedAt: null, 17 | }, 18 | }); 19 | return contractSubscription !== null; 20 | }; 21 | 22 | export const getContractSubscriptionsByChainId = async ( 23 | chainId: number, 24 | includeWebhook = false, 25 | ) => { 26 | return await prisma.contractSubscriptions.findMany({ 27 | where: { 28 | chainId: chainId.toString(), 29 | deletedAt: null, 30 | }, 31 | include: { 32 | webhook: includeWebhook, 33 | }, 34 | }); 35 | }; 36 | 37 | export const getAllContractSubscriptions = async () => { 38 | return await prisma.contractSubscriptions.findMany({ 39 | where: { 40 | deletedAt: null, 41 | }, 42 | include: { 43 | webhook: true, 44 | }, 45 | }); 46 | }; 47 | 48 | export const getContractSubscriptionsUniqueChainIds = async () => { 49 | const uniqueChainIds = await prisma.contractSubscriptions.findMany({ 50 | distinct: ["chainId"], 51 | select: { 52 | chainId: true, 53 | }, 54 | where: { 55 | deletedAt: null, 56 | }, 57 | }); 58 | 59 | return uniqueChainIds.map((contract) => Number.parseInt(contract.chainId)); 60 | }; 61 | -------------------------------------------------------------------------------- /src/shared/db/contract-transaction-receipts/create-contract-transaction-receipts.ts: -------------------------------------------------------------------------------- 1 | import type { ContractTransactionReceipts, Prisma } from "@prisma/client"; 2 | import type { PrismaTransaction } from "../../schemas/prisma"; 3 | import { getPrismaWithPostgresTx } from "../client"; 4 | 5 | export interface BulkInsertContractLogsParams { 6 | pgtx?: PrismaTransaction; 7 | receipts: Prisma.ContractTransactionReceiptsCreateInput[]; 8 | } 9 | 10 | export const bulkInsertContractTransactionReceipts = async ({ 11 | pgtx, 12 | receipts, 13 | }: BulkInsertContractLogsParams): Promise => { 14 | const prisma = getPrismaWithPostgresTx(pgtx); 15 | return await prisma.contractTransactionReceipts.createManyAndReturn({ 16 | data: receipts, 17 | skipDuplicates: true, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/shared/db/contract-transaction-receipts/delete-contract-transaction-receipts.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface DeleteContractTransactionReceiptsParams { 4 | chainId: number; 5 | contractAddress: string; 6 | } 7 | 8 | export const deleteContractTransactionReceipts = async ({ 9 | chainId, 10 | contractAddress, 11 | }: DeleteContractTransactionReceiptsParams) => { 12 | return prisma.contractTransactionReceipts.deleteMany({ 13 | where: { 14 | chainId: chainId.toString(), 15 | contractAddress, 16 | }, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/db/keypair/delete.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import { prisma } from "../client"; 3 | 4 | export const deleteKeypair = async ({ 5 | hash, 6 | }: { 7 | hash: string; 8 | }): Promise => { 9 | return prisma.keypairs.delete({ 10 | where: { hash }, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/shared/db/keypair/get.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import { createHash } from "crypto"; 3 | import { prisma } from "../client"; 4 | 5 | export const getKeypairByHash = async ( 6 | hash: string, 7 | ): Promise => { 8 | return prisma.keypairs.findUnique({ 9 | where: { hash }, 10 | }); 11 | }; 12 | 13 | export const getKeypairByPublicKey = async ( 14 | publicKey: string, 15 | ): Promise => { 16 | const hash = createHash("sha256").update(publicKey).digest("hex"); 17 | return getKeypairByHash(hash); 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/db/keypair/insert.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import { createHash } from "node:crypto"; 3 | import type { KeypairAlgorithm } from "../../schemas/keypair"; 4 | import { prisma } from "../client"; 5 | 6 | export const insertKeypair = async ({ 7 | publicKey, 8 | algorithm, 9 | label, 10 | }: { 11 | publicKey: string; 12 | algorithm: KeypairAlgorithm; 13 | label?: string; 14 | }): Promise => { 15 | const hash = createHash("sha256").update(publicKey).digest("hex"); 16 | 17 | return prisma.keypairs.create({ 18 | data: { 19 | hash, 20 | publicKey, 21 | algorithm, 22 | label, 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/shared/db/keypair/list.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import { prisma } from "../client"; 3 | 4 | export const listKeypairs = async (): Promise => { 5 | return prisma.keypairs.findMany(); 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/db/permissions/delete-permissions.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface DeletePermissionsParams { 4 | walletAddress: string; 5 | } 6 | 7 | export const deletePermissions = async ({ 8 | walletAddress, 9 | }: DeletePermissionsParams) => { 10 | return prisma.permissions.delete({ 11 | where: { 12 | walletAddress: walletAddress.toLowerCase(), 13 | }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/shared/db/permissions/get-permissions.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from "../../schemas/auth"; 2 | import { env } from "../../utils/env"; 3 | import { prisma } from "../client"; 4 | 5 | interface GetPermissionsParams { 6 | walletAddress: string; 7 | } 8 | 9 | export const getPermissions = async ({ 10 | walletAddress, 11 | }: GetPermissionsParams) => { 12 | const permissions = await prisma.permissions.findUnique({ 13 | where: { 14 | walletAddress: walletAddress.toLowerCase(), 15 | }, 16 | }); 17 | 18 | // If the admin wallet isn't in the permissions table yet, add it 19 | if ( 20 | !permissions && 21 | walletAddress.toLowerCase() === env.ADMIN_WALLET_ADDRESS.toLowerCase() 22 | ) { 23 | return prisma.permissions.create({ 24 | data: { 25 | walletAddress: walletAddress.toLowerCase(), 26 | permissions: Permission.Admin, 27 | }, 28 | }); 29 | } 30 | 31 | return permissions; 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/db/permissions/update-permissions.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface CreatePermissionsParams { 4 | walletAddress: string; 5 | permissions: string; 6 | label?: string; 7 | } 8 | 9 | export const updatePermissions = async ({ 10 | walletAddress, 11 | permissions, 12 | label, 13 | }: CreatePermissionsParams) => { 14 | return prisma.permissions.upsert({ 15 | where: { 16 | walletAddress: walletAddress.toLowerCase(), 17 | }, 18 | create: { 19 | walletAddress: walletAddress.toLowerCase(), 20 | permissions, 21 | label, 22 | }, 23 | update: { 24 | permissions, 25 | label, 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/db/relayer/get-relayer-by-id.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface GetRelayerByIdParams { 4 | id: string; 5 | } 6 | 7 | export const getRelayerById = async ({ id }: GetRelayerByIdParams) => { 8 | const relayer = await prisma.relayers.findUnique({ 9 | where: { 10 | id, 11 | }, 12 | }); 13 | 14 | if (!relayer) { 15 | return null; 16 | } 17 | 18 | return { 19 | ...relayer, 20 | chainId: Number.parseInt(relayer.chainId), 21 | allowedContracts: relayer.allowedContracts 22 | ? (JSON.parse(relayer.allowedContracts).map((contractAddress: string) => 23 | contractAddress.toLowerCase(), 24 | ) as string[]) 25 | : null, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/shared/db/tokens/create-token.ts: -------------------------------------------------------------------------------- 1 | import { parseJWT } from "@thirdweb-dev/auth"; 2 | import { prisma } from "../client"; 3 | 4 | interface CreateTokenParams { 5 | jwt: string; 6 | isAccessToken: boolean; 7 | label?: string; 8 | } 9 | 10 | export const createToken = async ({ 11 | jwt, 12 | isAccessToken, 13 | label, 14 | }: CreateTokenParams) => { 15 | const { payload } = parseJWT(jwt); 16 | return prisma.tokens.create({ 17 | data: { 18 | id: payload.jti, 19 | tokenMask: `${jwt.slice(0, 10)}...${jwt.slice(-10)}`, 20 | walletAddress: payload.sub, 21 | expiresAt: new Date(payload.exp * 1000), 22 | isAccessToken, 23 | label, 24 | }, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/shared/db/tokens/get-access-tokens.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export const getAccessTokens = async () => { 4 | return prisma.tokens.findMany({ 5 | where: { 6 | isAccessToken: true, 7 | revokedAt: null, 8 | }, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/db/tokens/get-token.ts: -------------------------------------------------------------------------------- 1 | import { parseJWT } from "@thirdweb-dev/auth"; 2 | import { prisma } from "../client"; 3 | 4 | export const getToken = async (jwt: string) => { 5 | const { payload } = parseJWT(jwt); 6 | if (payload.jti) { 7 | return prisma.tokens.findUnique({ 8 | where: { 9 | id: payload.jti, 10 | }, 11 | }); 12 | } 13 | return null; 14 | }; 15 | -------------------------------------------------------------------------------- /src/shared/db/tokens/revoke-token.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface RevokeTokenParams { 4 | id: string; 5 | } 6 | 7 | export const revokeToken = async ({ id }: RevokeTokenParams) => { 8 | await prisma.tokens.update({ 9 | where: { 10 | id, 11 | }, 12 | data: { 13 | revokedAt: new Date(), 14 | }, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/shared/db/tokens/update-token.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface UpdateTokenParams { 4 | id: string; 5 | label?: string; 6 | } 7 | 8 | export const updateToken = async ({ id, label }: UpdateTokenParams) => { 9 | await prisma.tokens.update({ 10 | where: { 11 | id, 12 | }, 13 | data: { 14 | label, 15 | }, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/shared/db/wallet-credentials/create-wallet-credential.ts: -------------------------------------------------------------------------------- 1 | import { encrypt } from "../../utils/crypto"; 2 | import { prisma } from "../client"; 3 | import { getConfig } from "../../utils/cache/get-config"; 4 | import { WalletCredentialsError } from "./get-wallet-credential"; 5 | 6 | // will be expanded to be a discriminated union of all supported wallet types 7 | export type CreateWalletCredentialsParams = { 8 | type: "circle"; 9 | label: string; 10 | entitySecret: string; 11 | isDefault?: boolean; 12 | }; 13 | 14 | export const createWalletCredential = async ({ 15 | type, 16 | label, 17 | entitySecret, 18 | isDefault, 19 | }: CreateWalletCredentialsParams) => { 20 | const { walletConfiguration } = await getConfig(); 21 | switch (type) { 22 | case "circle": { 23 | const circleApiKey = walletConfiguration.circle?.apiKey; 24 | if (!circleApiKey) { 25 | throw new WalletCredentialsError("No Circle API Key Configured"); 26 | } 27 | // Create the wallet credentials 28 | const walletCredentials = await prisma.walletCredentials.create({ 29 | data: { 30 | type, 31 | label, 32 | isDefault: isDefault || null, 33 | data: { 34 | entitySecret: encrypt(entitySecret), 35 | }, 36 | }, 37 | }); 38 | return walletCredentials; 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/db/wallet-credentials/get-all-wallet-credentials.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | import type { PrismaTransaction } from "../../schemas/prisma"; 3 | 4 | interface GetAllWalletCredentialsParams { 5 | pgtx?: PrismaTransaction; 6 | page?: number; 7 | limit?: number; 8 | } 9 | 10 | export const getAllWalletCredentials = async ({ 11 | page = 1, 12 | limit = 10, 13 | }: GetAllWalletCredentialsParams) => { 14 | const credentials = await prisma.walletCredentials.findMany({ 15 | where: { 16 | deletedAt: null, 17 | }, 18 | skip: (page - 1) * limit, 19 | take: limit, 20 | select: { 21 | id: true, 22 | type: true, 23 | label: true, 24 | isDefault: true, 25 | createdAt: true, 26 | updatedAt: true, 27 | }, 28 | orderBy: { 29 | createdAt: "desc", 30 | }, 31 | }); 32 | 33 | return credentials; 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/db/wallet-credentials/update-wallet-credential.ts: -------------------------------------------------------------------------------- 1 | import { getWalletCredential } from "./get-wallet-credential"; 2 | import { encrypt } from "../../utils/crypto"; 3 | import { prisma } from "../client"; 4 | import { cirlceEntitySecretZodSchema } from "../../schemas/wallet"; 5 | 6 | interface UpdateWalletCredentialParams { 7 | id: string; 8 | label?: string; 9 | isDefault?: boolean; 10 | entitySecret?: string; 11 | } 12 | 13 | type UpdateData = { 14 | label?: string; 15 | isDefault: boolean | null; 16 | data?: { 17 | entitySecret: string; 18 | }; 19 | }; 20 | 21 | export const updateWalletCredential = async ({ 22 | id, 23 | label, 24 | isDefault, 25 | entitySecret, 26 | }: UpdateWalletCredentialParams) => { 27 | // First check if credential exists 28 | await getWalletCredential({ id }); 29 | 30 | // If entitySecret is provided, validate and encrypt it 31 | const data: UpdateData = { 32 | label, 33 | isDefault: isDefault || null, 34 | }; 35 | 36 | if (entitySecret) { 37 | // Validate the entity secret 38 | cirlceEntitySecretZodSchema.parse(entitySecret); 39 | 40 | // Only update data field if entitySecret is provided 41 | data.data = { 42 | entitySecret: encrypt(entitySecret), 43 | }; 44 | } 45 | 46 | // Update the credential 47 | const updatedCredential = await prisma.walletCredentials.update({ 48 | where: { 49 | id, 50 | }, 51 | data, 52 | }); 53 | 54 | return updatedCredential; 55 | }; 56 | -------------------------------------------------------------------------------- /src/shared/db/wallet-subscriptions/delete-wallet-subscription.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export async function deleteWalletSubscription(id: string) { 4 | return await prisma.walletSubscriptions.update({ 5 | where: { 6 | id, 7 | deletedAt: null, 8 | }, 9 | data: { 10 | deletedAt: new Date(), 11 | }, 12 | include: { 13 | webhook: true, 14 | }, 15 | }); 16 | } -------------------------------------------------------------------------------- /src/shared/db/wallet-subscriptions/get-all-wallet-subscriptions.ts: -------------------------------------------------------------------------------- 1 | import { validateConditions } from "../../schemas/wallet-subscription-conditions"; 2 | import { prisma } from "../client"; 3 | 4 | export async function getAllWalletSubscriptions(args?: { 5 | page?: number; 6 | limit?: number; 7 | }) { 8 | const { page, limit } = args || {}; 9 | const subscriptions = await prisma.walletSubscriptions.findMany({ 10 | where: { 11 | deletedAt: null, 12 | }, 13 | include: { 14 | webhook: true, 15 | }, 16 | skip: page && limit ? (page - 1) * limit : undefined, 17 | take: limit, 18 | orderBy: { 19 | updatedAt: "desc", 20 | }, 21 | }); 22 | 23 | return subscriptions.map((subscription) => ({ 24 | ...subscription, 25 | conditions: validateConditions(subscription.conditions), 26 | })); 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/db/wallet-subscriptions/update-wallet-subscription.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@prisma/client"; 2 | import { prisma } from "../client"; 3 | import type { WalletConditions } from "../../schemas/wallet-subscription-conditions"; 4 | import { validateConditions } from "../../schemas/wallet-subscription-conditions"; 5 | import { WebhooksEventTypes } from "../../schemas/webhooks"; 6 | import { getWebhook } from "../webhooks/get-webhook"; 7 | 8 | interface UpdateWalletSubscriptionParams { 9 | id: string; 10 | chainId?: string; 11 | walletAddress?: string; 12 | conditions?: WalletConditions; 13 | webhookId?: number | null; 14 | } 15 | 16 | export async function updateWalletSubscription({ 17 | id, 18 | chainId, 19 | walletAddress, 20 | conditions, 21 | webhookId, 22 | }: UpdateWalletSubscriptionParams) { 23 | if (webhookId) { 24 | const webhook = await getWebhook(webhookId); 25 | if (!webhook) { 26 | throw new Error("Webhook not found"); 27 | } 28 | if (webhook.revokedAt) { 29 | throw new Error("Webhook has been revoked"); 30 | } 31 | if (webhook.eventType !== WebhooksEventTypes.WALLET_SUBSCRIPTION) { 32 | throw new Error("Webhook is not a wallet subscription webhook"); 33 | } 34 | } 35 | 36 | return await prisma.walletSubscriptions.update({ 37 | where: { 38 | id, 39 | deletedAt: null, 40 | }, 41 | data: { 42 | ...(chainId && { chainId }), 43 | ...(walletAddress && { walletAddress: walletAddress.toLowerCase() }), 44 | ...(conditions && { 45 | conditions: validateConditions(conditions) as Prisma.InputJsonValue[], 46 | }), 47 | ...(webhookId !== undefined && { webhookId }), 48 | }, 49 | include: { 50 | webhook: true, 51 | }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/shared/db/wallets/delete-wallet-details.ts: -------------------------------------------------------------------------------- 1 | import type { Address } from "thirdweb"; 2 | import { prisma } from "../client"; 3 | 4 | export const deleteWalletDetails = async (walletAddress: Address) => { 5 | return prisma.walletDetails.delete({ 6 | where: { 7 | address: walletAddress.toLowerCase(), 8 | }, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/db/wallets/get-all-wallets.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaTransaction } from "../../schemas/prisma"; 2 | import { getPrismaWithPostgresTx } from "../client"; 3 | 4 | interface GetAllWalletsParams { 5 | pgtx?: PrismaTransaction; 6 | page: number; 7 | limit: number; 8 | } 9 | 10 | export const getAllWallets = async ({ 11 | pgtx, 12 | page, 13 | limit, 14 | }: GetAllWalletsParams) => { 15 | const prisma = getPrismaWithPostgresTx(pgtx); 16 | 17 | return prisma.walletDetails.findMany({ 18 | skip: (page - 1) * limit, 19 | take: limit, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/shared/db/wallets/update-wallet-details.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | interface UpdateWalletDetailsParams { 4 | address: string; 5 | label?: string; 6 | } 7 | 8 | export const updateWalletDetails = async ({ 9 | address, 10 | label, 11 | }: UpdateWalletDetailsParams) => { 12 | await prisma.walletDetails.update({ 13 | where: { 14 | address: address.toLowerCase(), 15 | }, 16 | data: { 17 | label, 18 | }, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/shared/db/webhooks/create-webhook.ts: -------------------------------------------------------------------------------- 1 | import type { Webhooks } from "@prisma/client"; 2 | import { createHash, randomBytes } from "node:crypto"; 3 | import type { WebhooksEventTypes } from "../../schemas/webhooks"; 4 | import { prisma } from "../client"; 5 | 6 | interface CreateWebhooksParams { 7 | url: string; 8 | name?: string; 9 | eventType: WebhooksEventTypes; 10 | } 11 | 12 | export const insertWebhook = async ({ 13 | url, 14 | name, 15 | eventType, 16 | }: CreateWebhooksParams): Promise => { 17 | // generate random bytes 18 | const bytes = randomBytes(4096); 19 | // hash the bytes to create the secret (this will not be stored by itself) 20 | const secret = createHash("sha512").update(bytes).digest("base64url"); 21 | 22 | return prisma.webhooks.create({ 23 | data: { 24 | url, 25 | name, 26 | eventType, 27 | secret, 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/shared/db/webhooks/get-all-webhooks.ts: -------------------------------------------------------------------------------- 1 | import type { Webhooks } from "@prisma/client"; 2 | import { prisma } from "../client"; 3 | 4 | export const getAllWebhooks = async (): Promise => { 5 | return await prisma.webhooks.findMany({ 6 | where: { 7 | revokedAt: null, 8 | }, 9 | orderBy: { 10 | id: "asc", 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/shared/db/webhooks/get-webhook.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export const getWebhook = async (id: number) => { 4 | return await prisma.webhooks.findUnique({ 5 | where: { id }, 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/shared/db/webhooks/revoke-webhook.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../client"; 2 | 3 | export const deleteWebhook = async (id: number) => { 4 | const now = new Date(); 5 | 6 | return prisma.webhooks.update({ 7 | where: { id }, 8 | data: { 9 | revokedAt: now, 10 | updatedAt: now, 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/shared/lib/chain/chain-capabilities.ts: -------------------------------------------------------------------------------- 1 | import { createSWRCache } from "../cache/swr"; 2 | 3 | const Services = [ 4 | "contracts", 5 | "connect-sdk", 6 | "engine", 7 | "account-abstraction", 8 | "pay", 9 | "rpc-edge", 10 | "chainsaw", 11 | "insight", 12 | ] as const; 13 | 14 | export type Service = (typeof Services)[number]; 15 | 16 | export type ChainCapabilities = Array<{ 17 | service: Service; 18 | enabled: boolean; 19 | }>; 20 | 21 | // Create cache with 2048 entries and 30 minute TTL 22 | const chainCapabilitiesCache = createSWRCache({ 23 | maxEntries: 2048, 24 | ttlMs: 1000 * 60 * 30, // 30 minutes 25 | }); 26 | 27 | /** 28 | * Get the capabilities of a chain (cached with stale-while-revalidate) 29 | */ 30 | export async function getChainCapabilities( 31 | chainId: number, 32 | ): Promise { 33 | return chainCapabilitiesCache.get(chainId, async () => { 34 | const response = await fetch( 35 | `https://api.thirdweb.com/v1/chains/${chainId}/services`, 36 | ); 37 | 38 | const data = await response.json(); 39 | 40 | if (data.error) { 41 | throw new Error(data.error); 42 | } 43 | 44 | return data.data.services as ChainCapabilities; 45 | }); 46 | } 47 | 48 | /** 49 | * Check if a chain supports a given service 50 | */ 51 | export async function doesChainSupportService( 52 | chainId: number, 53 | service: Service, 54 | ): Promise { 55 | const chainCapabilities = await getChainCapabilities(chainId); 56 | 57 | return chainCapabilities.some( 58 | (capability) => capability.service === service && capability.enabled, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/shared/schemas/auth.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@sinclair/typebox"; 2 | 3 | export enum Permission { 4 | Owner = "OWNER", 5 | Admin = "ADMIN", 6 | } 7 | 8 | export const permissionsSchema = Type.Union([ 9 | Type.Literal(Permission.Admin), 10 | Type.Literal(Permission.Owner), 11 | ]); 12 | -------------------------------------------------------------------------------- /src/shared/schemas/extension.ts: -------------------------------------------------------------------------------- 1 | export type ContractExtension = 2 | | "erc20" 3 | | "erc721" 4 | | "erc1155" 5 | | "marketplace-v3-direct-listings" 6 | | "marketplace-v3-english-auctions" 7 | | "marketplace-v3-offers" 8 | | "roles" 9 | | "none" 10 | | "withdraw" 11 | | "deploy-prebuilt" 12 | | "deploy-published" 13 | | "account-factory" 14 | | "account" 15 | | "relayer"; 16 | -------------------------------------------------------------------------------- /src/shared/schemas/keypair.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import { type Static, Type } from "@sinclair/typebox"; 3 | 4 | // https://github.com/auth0/node-jsonwebtoken#algorithms-supported 5 | const _supportedAlgorithms = [ 6 | // Symmetric algorithms are disabled to avoid storing keys in plaintext. 7 | // "HS256", 8 | // "HS384", 9 | // "HS512", 10 | "RS256", 11 | "RS384", 12 | "RS512", 13 | "ES256", 14 | "ES384", 15 | "ES512", 16 | "PS256", 17 | "PS384", 18 | "PS512", 19 | ] as const; 20 | 21 | export type KeypairAlgorithm = (typeof _supportedAlgorithms)[number]; 22 | 23 | export const KeypairAlgorithmSchema = Type.Union( 24 | _supportedAlgorithms.map((alg) => Type.Literal(alg)), 25 | ); 26 | 27 | export const KeypairSchema = Type.Object({ 28 | hash: Type.String({ 29 | description: "A unique identifier for the keypair", 30 | }), 31 | publicKey: Type.String({ 32 | description: "The public key", 33 | }), 34 | algorithm: Type.String({ 35 | description: "The keypair algorithm.", 36 | }), 37 | label: Type.Optional( 38 | Type.String({ 39 | description: "A description for the keypair.", 40 | }), 41 | ), 42 | createdAt: Type.Unsafe({ 43 | type: "string", 44 | format: "date", 45 | description: "When the keypair was added", 46 | }), 47 | updatedAt: Type.Unsafe({ 48 | type: "string", 49 | format: "date", 50 | description: "When the keypair was updated", 51 | }), 52 | }); 53 | 54 | export const toKeypairSchema = ( 55 | keypair: Keypairs, 56 | ): Static => ({ 57 | hash: keypair.hash, 58 | publicKey: keypair.publicKey, 59 | algorithm: keypair.algorithm, 60 | label: keypair.label ?? undefined, 61 | createdAt: keypair.createdAt, 62 | updatedAt: keypair.updatedAt, 63 | }); 64 | -------------------------------------------------------------------------------- /src/shared/schemas/prisma.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma, PrismaClient } from "@prisma/client"; 2 | import type { DefaultArgs } from "@prisma/client/runtime/library"; 3 | 4 | export type PrismaTransaction = Omit< 5 | PrismaClient, 6 | "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" 7 | >; 8 | -------------------------------------------------------------------------------- /src/shared/schemas/wallet.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export enum CircleWalletType { 4 | circle = "circle", 5 | 6 | // Smart wallets 7 | smartCircle = "smart:circle", 8 | } 9 | 10 | export enum LegacyWalletType { 11 | local = "local", 12 | awsKms = "aws-kms", 13 | gcpKms = "gcp-kms", 14 | 15 | // Smart wallets 16 | smartAwsKms = "smart:aws-kms", 17 | smartGcpKms = "smart:gcp-kms", 18 | smartLocal = "smart:local", 19 | } 20 | 21 | export enum WalletType { 22 | // Legacy wallet types 23 | local = "local", 24 | awsKms = "aws-kms", 25 | gcpKms = "gcp-kms", 26 | 27 | // Smart wallets 28 | smartAwsKms = "smart:aws-kms", 29 | smartGcpKms = "smart:gcp-kms", 30 | smartLocal = "smart:local", 31 | 32 | // New credential based wallet types 33 | circle = "circle", 34 | 35 | // Smart wallets 36 | smartCircle = "smart:circle", 37 | } 38 | 39 | export const cirlceEntitySecretZodSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, { 40 | message: "entitySecret must be a 32-byte hex string", 41 | }); 42 | -------------------------------------------------------------------------------- /src/shared/schemas/webhooks.ts: -------------------------------------------------------------------------------- 1 | import type { WalletCondition } from "./wallet-subscription-conditions"; 2 | 3 | export enum WebhooksEventTypes { 4 | QUEUED_TX = "queued_transaction", 5 | SENT_TX = "sent_transaction", 6 | MINED_TX = "mined_transaction", 7 | ERRORED_TX = "errored_transaction", 8 | CANCELLED_TX = "cancelled_transaction", 9 | ALL_TX = "all_transactions", 10 | BACKEND_WALLET_BALANCE = "backend_wallet_balance", 11 | AUTH = "auth", 12 | CONTRACT_SUBSCRIPTION = "contract_subscription", 13 | WALLET_SUBSCRIPTION = "wallet_subscription", 14 | } 15 | 16 | export type BackendWalletBalanceWebhookParams = { 17 | walletAddress: string; 18 | minimumBalance: string; 19 | currentBalance: string; 20 | chainId: number; 21 | message: string; 22 | }; 23 | export interface WalletSubscriptionWebhookParams { 24 | subscriptionId: string; 25 | chainId: string; 26 | walletAddress: string; 27 | condition: WalletCondition; 28 | currentValue: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { authenticateJWT } from "@thirdweb-dev/auth"; 2 | import { utils } from "ethers"; 3 | import { env } from "./env"; 4 | 5 | export const THIRDWEB_DASHBOARD_ISSUER = 6 | "0x016757dDf2Ab6a998a4729A80a091308d9059E17"; 7 | 8 | export const handleSiwe = async ( 9 | jwt: string, 10 | domain: string, 11 | issuer: string, 12 | ) => { 13 | try { 14 | return await authenticateJWT({ 15 | clientOptions: { 16 | secretKey: env.THIRDWEB_API_SECRET_KEY, 17 | }, 18 | // A stub implementation of a wallet that can only verify a signature. 19 | wallet: { 20 | type: "evm", 21 | getAddress: async () => issuer, 22 | verifySignature: async ( 23 | message: string, 24 | signature: string, 25 | address: string, 26 | ) => { 27 | const messageHash = utils.hashMessage(message); 28 | const messageHashBytes = utils.arrayify(messageHash); 29 | const recoveredAddress = utils.recoverAddress( 30 | messageHashBytes, 31 | signature, 32 | ); 33 | return recoveredAddress === address; 34 | }, 35 | signMessage: async (_: string) => "", 36 | }, 37 | jwt, 38 | options: { domain }, 39 | }); 40 | } catch { 41 | return null; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/shared/utils/block.ts: -------------------------------------------------------------------------------- 1 | import { eth_blockNumber, getRpcClient } from "thirdweb"; 2 | import { getChain } from "./chain"; 3 | import { redis } from "./redis/redis"; 4 | import { thirdwebClient } from "./sdk"; 5 | 6 | /** 7 | * Returns the latest block number. Falls back to the last known block number. 8 | * Only use if the precise block number is not required. 9 | * 10 | * @param chainId 11 | * @returns bigint - The latest block number. 12 | */ 13 | export const getBlockNumberish = async (chainId: number): Promise => { 14 | const rpcRequest = getRpcClient({ 15 | client: thirdwebClient, 16 | chain: await getChain(chainId), 17 | }); 18 | 19 | const key = `latestBlock:${chainId}`; 20 | try { 21 | const blockNumber = await eth_blockNumber(rpcRequest); 22 | // Non-blocking update to cache. 23 | redis.set(key, blockNumber.toString()).catch((_e) => {}); 24 | return blockNumber; 25 | } catch (_e) { 26 | const cached = await redis.get(key); 27 | if (cached) { 28 | return BigInt(cached); 29 | } 30 | 31 | throw new Error("Error getting latest block number."); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/shared/utils/cache/access-token.ts: -------------------------------------------------------------------------------- 1 | import type { Tokens } from "@prisma/client"; 2 | import LRUMap from "mnemonist/lru-map"; 3 | import { getToken } from "../../db/tokens/get-token"; 4 | import { env } from "../env"; 5 | 6 | // Cache an access token JWT to the token object, or null if not found. 7 | export const accessTokenCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); 8 | 9 | interface GetAccessTokenParams { 10 | jwt: string; 11 | } 12 | 13 | export const getAccessToken = async ({ 14 | jwt, 15 | }: GetAccessTokenParams): Promise => { 16 | const cached = accessTokenCache.get(jwt); 17 | if (cached) { 18 | return cached; 19 | } 20 | 21 | const accessToken = await getToken(jwt); 22 | accessTokenCache.set(jwt, accessToken); 23 | return accessToken; 24 | }; 25 | -------------------------------------------------------------------------------- /src/shared/utils/cache/auth-wallet.ts: -------------------------------------------------------------------------------- 1 | import { LocalWallet } from "@thirdweb-dev/wallets"; 2 | import { updateConfiguration } from "../../db/configuration/update-configuration"; 3 | import { env } from "../env"; 4 | import { logger } from "../logger"; 5 | import { getConfig } from "./get-config"; 6 | 7 | let authWallet: LocalWallet | undefined; 8 | 9 | export const getAuthWallet = async (): Promise => { 10 | if (!authWallet) { 11 | const config = await getConfig(); 12 | authWallet = new LocalWallet(); 13 | 14 | try { 15 | // First, we try to load the wallet with the encryption password 16 | await authWallet.import({ 17 | encryptedJson: config.authWalletEncryptedJson, 18 | password: env.ENCRYPTION_PASSWORD, 19 | }); 20 | } catch { 21 | // If that fails, we try to load the wallet with the secret key 22 | await authWallet.import({ 23 | encryptedJson: config.authWalletEncryptedJson, 24 | password: env.THIRDWEB_API_SECRET_KEY, 25 | }); 26 | 27 | // And then update the auth wallet to use encryption password instead 28 | const encryptedJson = await authWallet.export({ 29 | strategy: "encryptedJson", 30 | password: env.ENCRYPTION_PASSWORD, 31 | }); 32 | 33 | logger({ 34 | service: "server", 35 | level: "info", 36 | message: 37 | "[Encryption] Updating authWalletEncryptedJson to use ENCRYPTION_PASSWORD", 38 | }); 39 | 40 | await updateConfiguration({ 41 | authWalletEncryptedJson: encryptedJson, 42 | }); 43 | } 44 | } 45 | 46 | return authWallet; 47 | }; 48 | -------------------------------------------------------------------------------- /src/shared/utils/cache/clear-cache.ts: -------------------------------------------------------------------------------- 1 | import { accessTokenCache } from "./access-token"; 2 | import { invalidateConfig } from "./get-config"; 3 | import { sdkCache } from "./get-sdk"; 4 | import { walletsCache } from "./get-wallet"; 5 | import { webhookCache } from "./get-webhook"; 6 | import { keypairCache } from "./keypair"; 7 | 8 | export const clearCache = async (): Promise => { 9 | invalidateConfig(); 10 | webhookCache.clear(); 11 | sdkCache.clear(); 12 | walletsCache.clear(); 13 | accessTokenCache.clear(); 14 | keypairCache.clear(); 15 | }; 16 | -------------------------------------------------------------------------------- /src/shared/utils/cache/get-config.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration } from "../../db/configuration/get-configuration"; 2 | import type { ParsedConfig } from "../../schemas/config"; 3 | 4 | let _config: ParsedConfig | null = null; 5 | 6 | export const getConfig = async ( 7 | retrieveFromCache = true, 8 | ): Promise => { 9 | if (!_config || !retrieveFromCache) { 10 | _config = await getConfiguration(); 11 | } 12 | 13 | return _config; 14 | }; 15 | 16 | export const invalidateConfig = () => { 17 | _config = null; 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/utils/cache/get-contract.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type } from "@sinclair/typebox"; 2 | import { StatusCodes } from "http-status-codes"; 3 | import { createCustomError } from "../../../server/middleware/error"; 4 | import { abiSchema } from "../../../server/schemas/contract"; 5 | import { getSdk } from "./get-sdk"; 6 | import type { ThirdwebSDK } from "@thirdweb-dev/sdk"; 7 | 8 | const abiArraySchema = Type.Array(abiSchema); 9 | 10 | interface GetContractParams { 11 | chainId: number; 12 | walletAddress?: string; 13 | accountAddress?: string; 14 | contractAddress: string; 15 | abi?: Static; 16 | } 17 | 18 | export const getContract = async ({ 19 | chainId, 20 | walletAddress, 21 | contractAddress, 22 | accountAddress, 23 | abi, 24 | }: GetContractParams) => { 25 | let sdk: ThirdwebSDK; 26 | 27 | try { 28 | sdk = await getSdk({ chainId, walletAddress, accountAddress }); 29 | } catch (e) { 30 | throw createCustomError( 31 | `Could not get SDK: ${e}`, 32 | StatusCodes.BAD_REQUEST, 33 | "INVALID_CHAIN_OR_WALLET_TYPE_FOR_ROUTE", 34 | ); 35 | } 36 | 37 | try { 38 | if (abi) { 39 | return sdk.getContractFromAbi(contractAddress, abi); 40 | } 41 | // SDK already handles caching. 42 | return await sdk.getContract(contractAddress); 43 | } catch (e) { 44 | throw createCustomError( 45 | `Contract metadata could not be resolved: ${e}`, 46 | StatusCodes.BAD_REQUEST, 47 | "INVALID_CONTRACT", 48 | ); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/utils/cache/get-contractv5.ts: -------------------------------------------------------------------------------- 1 | import { type ThirdwebContract, getContract } from "thirdweb"; 2 | import type { Abi } from "thirdweb/utils"; 3 | import { thirdwebClient } from "../../utils/sdk"; 4 | import { getChain } from "../chain"; 5 | 6 | interface GetContractParams { 7 | chainId: number; 8 | contractAddress: string; 9 | abi?: Abi; 10 | } 11 | 12 | // Using new v5 SDK 13 | export const getContractV5 = async ({ 14 | chainId, 15 | contractAddress, 16 | abi, 17 | }: GetContractParams): Promise => { 18 | const definedChain = await getChain(chainId); 19 | 20 | // get a contract 21 | return getContract({ 22 | // the client you have created via `createThirdwebClient()` 23 | client: thirdwebClient, 24 | // the contract's address 25 | address: contractAddress, 26 | // the chain the contract is deployed on 27 | chain: definedChain, 28 | abi, 29 | }) as ThirdwebContract; // not using type inference here; 30 | }; 31 | -------------------------------------------------------------------------------- /src/shared/utils/cache/get-webhook.ts: -------------------------------------------------------------------------------- 1 | import type { Webhooks } from "@prisma/client"; 2 | import LRUMap from "mnemonist/lru-map"; 3 | import { getAllWebhooks } from "../../db/webhooks/get-all-webhooks"; 4 | import type { WebhooksEventTypes } from "../../schemas/webhooks"; 5 | import { env } from "../env"; 6 | 7 | export const webhookCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); 8 | 9 | export const getWebhooksByEventType = async ( 10 | eventType: WebhooksEventTypes, 11 | retrieveFromCache = true, 12 | ): Promise => { 13 | const cacheKey = eventType; 14 | 15 | if (retrieveFromCache && webhookCache.has(cacheKey)) { 16 | return webhookCache.get(cacheKey) as Webhooks[]; 17 | } 18 | 19 | const filteredWebhooks = (await getAllWebhooks()).filter( 20 | (webhook) => webhook.eventType === eventType, 21 | ); 22 | 23 | webhookCache.set(cacheKey, filteredWebhooks); 24 | return filteredWebhooks; 25 | }; 26 | -------------------------------------------------------------------------------- /src/shared/utils/cache/keypair.ts: -------------------------------------------------------------------------------- 1 | import type { Keypairs } from "@prisma/client"; 2 | import LRUMap from "mnemonist/lru-map"; 3 | import { getKeypairByHash, getKeypairByPublicKey } from "../../db/keypair/get"; 4 | import { env } from "../env"; 5 | 6 | // Cache a public key to the Keypair object, or null if not found. 7 | export const keypairCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); 8 | 9 | /** 10 | * Get a keypair by public key or hash. 11 | */ 12 | export const getKeypair = async (args: { 13 | publicKey?: string; 14 | publicKeyHash?: string; 15 | }): Promise => { 16 | const { publicKey, publicKeyHash } = args; 17 | 18 | const key = publicKey 19 | ? `public-key:${args.publicKey}` 20 | : publicKeyHash 21 | ? `public-key-hash:${args.publicKeyHash}` 22 | : null; 23 | 24 | if (!key) { 25 | throw new Error('Must provide "publicKey" or "publicKeyHash".'); 26 | } 27 | 28 | const cached = keypairCache.get(key); 29 | if (cached) { 30 | return cached; 31 | } 32 | 33 | const keypair = publicKey 34 | ? await getKeypairByPublicKey(publicKey) 35 | : publicKeyHash 36 | ? await getKeypairByHash(publicKeyHash) 37 | : null; 38 | 39 | keypairCache.set(key, keypair); 40 | return keypair; 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { defineChain, type Chain } from "thirdweb"; 2 | import { getConfig } from "./cache/get-config"; 3 | 4 | /** 5 | * Get the chain for thirdweb v5 SDK. Supports chain overrides. 6 | * @param chainId 7 | * @returns Chain 8 | */ 9 | export const getChain = async (chainId: number): Promise => { 10 | const config = await getConfig(); 11 | 12 | for (const override of config.chainOverridesParsed) { 13 | if (chainId === override.id) { 14 | // we need to call defineChain to ensure that the chain is registered in CUSTOM_CHAIN_MAP 15 | // even if we have a Chain type, we need to call defineChain to ensure that the chain is registered 16 | return defineChain(override); 17 | } 18 | } 19 | 20 | return defineChain(chainId); 21 | }; 22 | -------------------------------------------------------------------------------- /src/shared/utils/cron/clear-cache-cron.ts: -------------------------------------------------------------------------------- 1 | import { CronJob } from "cron"; 2 | import { clearCache } from "../cache/clear-cache"; 3 | import { getConfig } from "../cache/get-config"; 4 | 5 | let task: CronJob; 6 | 7 | export const clearCacheCron = async () => { 8 | const config = await getConfig(); 9 | 10 | if (!config.clearCacheCronSchedule) { 11 | return; 12 | } 13 | 14 | // Stop the existing task if it exists. 15 | if (task) { 16 | task.stop(); 17 | } 18 | 19 | task = new CronJob(config.clearCacheCronSchedule, async () => { 20 | await clearCache(); 21 | }); 22 | task.start(); 23 | }; 24 | -------------------------------------------------------------------------------- /src/shared/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | import crypto from "node:crypto"; 3 | import { env } from "./env"; 4 | 5 | export function encrypt(data: string): string { 6 | return CryptoJS.AES.encrypt(data, env.ENCRYPTION_PASSWORD).toString(); 7 | } 8 | 9 | export function decrypt(data: string, password: string) { 10 | return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8); 11 | } 12 | 13 | export function isWellFormedPublicKey(key: string) { 14 | try { 15 | crypto.createPublicKey(key); 16 | return true; 17 | } catch (_e) { 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/utils/date.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the milliseconds since a given past date. 3 | * Returns 0 if the date is in the future. 4 | * @param from A past date 5 | * @returns number Milliseconds since the `from` date. 6 | */ 7 | export const msSince = (from: Date) => { 8 | const ms = Date.now() - from.getTime(); 9 | return Math.max(ms, 0); 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { stringify } from "thirdweb/utils"; 3 | import { isEthersErrorCode } from "./ethers"; 4 | 5 | export const wrapError = (error: unknown, prefix: "RPC" | "Bundler") => 6 | new Error(`[${prefix}] ${prettifyError(error)}`); 7 | 8 | export const prettifyError = (error: unknown): string => 9 | error instanceof Error ? error.message : stringify(error); 10 | 11 | const _parseMessage = (error: unknown): string | null => { 12 | return error && typeof error === "object" && "message" in error 13 | ? (error.message as string).toLowerCase() 14 | : null; 15 | }; 16 | 17 | export const isNonceAlreadyUsedError = (error: unknown) => { 18 | const message = _parseMessage(error); 19 | 20 | if (message) { 21 | return ( 22 | message.includes("nonce too low") || message.includes("already known") 23 | ); 24 | } 25 | 26 | return isEthersErrorCode(error, ethers.errors.NONCE_EXPIRED); 27 | }; 28 | 29 | export const isReplacementGasFeeTooLow = (error: unknown) => { 30 | const message = _parseMessage(error); 31 | if (message) { 32 | return ( 33 | message.includes("replacement fee too low") || 34 | message.includes("replacement transaction underpriced") 35 | ); 36 | } 37 | return isEthersErrorCode(error, ethers.errors.REPLACEMENT_UNDERPRICED); 38 | }; 39 | 40 | export const isInsufficientFundsError = (error: unknown) => { 41 | const message = _parseMessage(error); 42 | if (message) { 43 | return message.includes("insufficient funds"); 44 | } 45 | return isEthersErrorCode(error, ethers.errors.INSUFFICIENT_FUNDS); 46 | }; 47 | -------------------------------------------------------------------------------- /src/shared/utils/ethers.ts: -------------------------------------------------------------------------------- 1 | // import { EthersError } from "@ethersproject/logger"; 2 | import { ethers } from "ethers"; 3 | 4 | // Copied from: https://github.com/ethers-io/ethers.js/blob/main/src.ts/utils/errors.ts#L156 5 | // EthersError in ethers isn't exported. 6 | export interface EthersError extends Error { 7 | /** 8 | * The string error code. 9 | */ 10 | code: ethers.errors; 11 | 12 | /** 13 | * A short message describing the error, with minimal additional 14 | * details. 15 | */ 16 | shortMessage: string; 17 | 18 | /** 19 | * Additional info regarding the error that may be useful. 20 | * 21 | * This is generally helpful mostly for human-based debugging. 22 | */ 23 | info?: Record; 24 | 25 | /** 26 | * Any related error. 27 | */ 28 | error?: Error; 29 | } 30 | 31 | export const ETHERS_ERROR_CODES = new Set(Object.values(ethers.errors)); 32 | 33 | /** 34 | * Returns an EthersError, or null if the error is not an ethers error. 35 | * @param error 36 | * @returns EthersError | null 37 | */ 38 | export const parseEthersError = (error: unknown): EthersError | null => { 39 | const isNonNullObject = error && typeof error === "object"; 40 | const hasCodeProperty = isNonNullObject && "code" in error; 41 | if (hasCodeProperty && ETHERS_ERROR_CODES.has(error.code as ethers.errors)) { 42 | return error as EthersError; 43 | } 44 | return null; 45 | }; 46 | 47 | export const isEthersErrorCode = (error: unknown, code: ethers.errors) => 48 | parseEthersError(error)?.code === code; 49 | -------------------------------------------------------------------------------- /src/shared/utils/indexer/get-block-time.ts: -------------------------------------------------------------------------------- 1 | import { eth_getBlockByNumber, getRpcClient } from "thirdweb"; 2 | import { getChain } from "../chain"; 3 | import { thirdwebClient } from "../sdk"; 4 | 5 | export const getBlockTimeSeconds = async ( 6 | chainId: number, 7 | blocksToEstimate: number, 8 | ) => { 9 | const chain = await getChain(chainId); 10 | const rpcRequest = getRpcClient({ 11 | client: thirdwebClient, 12 | chain, 13 | }); 14 | 15 | const latestBlock = await eth_getBlockByNumber(rpcRequest, { 16 | blockTag: "latest", 17 | includeTransactions: false, 18 | }); 19 | const referenceBlock = await eth_getBlockByNumber(rpcRequest, { 20 | blockNumber: latestBlock.number - BigInt(blocksToEstimate), 21 | includeTransactions: false, 22 | }); 23 | 24 | const diffSeconds = latestBlock.timestamp - referenceBlock.timestamp; 25 | return Number(diffSeconds) / (blocksToEstimate + 1); 26 | }; 27 | -------------------------------------------------------------------------------- /src/shared/utils/math.ts: -------------------------------------------------------------------------------- 1 | export const getPercentile = (arr: number[], percentile: number): number => { 2 | if (arr.length === 0) { 3 | return 0; 4 | } 5 | 6 | arr.sort((a, b) => a - b); 7 | const index = Math.floor((percentile / 100) * (arr.length - 1)); 8 | return arr[index]; 9 | }; 10 | 11 | export const BigIntMath = { 12 | min: (a: bigint, b: bigint) => (a < b ? a : b), 13 | max: (a: bigint, b: bigint) => (a > b ? a : b), 14 | }; 15 | -------------------------------------------------------------------------------- /src/shared/utils/redis/lock.ts: -------------------------------------------------------------------------------- 1 | import { redis } from "./redis"; 2 | 3 | // Add more locks here. 4 | type LockType = "lock:apply-migrations"; 5 | 6 | /** 7 | * Acquire a lock to prevent duplicate runs of a workflow. 8 | * 9 | * @param key string The lock identifier. 10 | * @param ttlSeconds number The number of seconds before the lock is automatically released. 11 | * @returns true if the lock was acquired. Else false. 12 | */ 13 | export const acquireLock = async ( 14 | key: LockType, 15 | ttlSeconds: number, 16 | ): Promise => { 17 | const result = await redis.set(key, Date.now(), "EX", ttlSeconds, "NX"); 18 | return result === "OK"; 19 | }; 20 | 21 | /** 22 | * Release a lock. 23 | * 24 | * @param key The lock identifier. 25 | * @returns true if the lock was active before releasing. 26 | */ 27 | export const releaseLock = async (key: LockType) => { 28 | const result = await redis.del(key); 29 | return result > 0; 30 | }; 31 | 32 | /** 33 | * Blocking polls a lock every second until it's released. 34 | * 35 | * @param key The lock identifier. 36 | */ 37 | export const waitForLock = async (key: LockType) => { 38 | while (await redis.get(key)) { 39 | await new Promise((resolve) => setTimeout(resolve, 1_000)); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/utils/redis/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | import { env } from "../env"; 3 | import { logger } from "../logger"; 4 | 5 | // ioredis has issues with batches over 100k+ (source: https://github.com/redis/ioredis/issues/801). 6 | export const MAX_REDIS_BATCH_SIZE = 50_000; 7 | 8 | export const redis = new Redis(env.REDIS_URL, { 9 | enableAutoPipelining: true, 10 | maxRetriesPerRequest: null, 11 | }); 12 | try { 13 | await redis.config("SET", "maxmemory", env.REDIS_MAXMEMORY); 14 | } catch (error) { 15 | logger({ 16 | level: "error", 17 | message: `Initializing Redis: ${error}`, 18 | service: "worker", 19 | }); 20 | } 21 | 22 | redis.on("error", (error) => () => { 23 | logger({ 24 | level: "error", 25 | message: `Redis error: ${error}`, 26 | service: "worker", 27 | }); 28 | }); 29 | redis.on("ready", () => { 30 | logger({ 31 | level: "debug", 32 | message: "Redis ready", 33 | service: "worker", 34 | }); 35 | }); 36 | 37 | export const isRedisReachable = async () => { 38 | try { 39 | await redis.ping(); 40 | return true; 41 | } catch { 42 | return false; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/shared/utils/sdk.ts: -------------------------------------------------------------------------------- 1 | import { sha256HexSync } from "@thirdweb-dev/crypto"; 2 | import { createThirdwebClient, hexToNumber, isHex } from "thirdweb"; 3 | import type { TransactionReceipt } from "thirdweb/transaction"; 4 | import { env } from "./env"; 5 | 6 | export const thirdwebClientId = sha256HexSync( 7 | env.THIRDWEB_API_SECRET_KEY, 8 | ).slice(0, 32); 9 | 10 | export const thirdwebClient = createThirdwebClient({ 11 | secretKey: env.THIRDWEB_API_SECRET_KEY, 12 | config: { 13 | rpc: { maxBatchSize: 50 }, 14 | }, 15 | }); 16 | 17 | /** 18 | * Helper functions to handle v4 -> v5 SDK migration. 19 | */ 20 | 21 | export const fromTransactionStatus = (status: "success" | "reverted") => 22 | status === "success" ? 1 : 0; 23 | 24 | export const fromTransactionType = (type: TransactionReceipt["type"]) => { 25 | if (type === "legacy") return 0; 26 | if (type === "eip1559") return 1; 27 | if (type === "eip2930") return 2; 28 | if (type === "eip4844") return 3; 29 | if (type === "eip7702") return 4; 30 | if (isHex(type)) return hexToNumber(type); 31 | throw new Error(`Unexpected transaction type ${type}`); 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/utils/transaction/cancel-transaction.ts: -------------------------------------------------------------------------------- 1 | import { type Address, toSerializableTransaction } from "thirdweb"; 2 | import { getAccount } from "../account"; 3 | import { getChain } from "../chain"; 4 | import { getChecksumAddress } from "../primitive-types"; 5 | import { thirdwebClient } from "../sdk"; 6 | 7 | interface CancellableTransaction { 8 | chainId: number; 9 | from: Address; 10 | nonce: number; 11 | } 12 | 13 | export const sendCancellationTransaction = async ( 14 | transaction: CancellableTransaction, 15 | ) => { 16 | const { chainId, from, nonce } = transaction; 17 | 18 | const chain = await getChain(chainId); 19 | const populatedTransaction = await toSerializableTransaction({ 20 | from: getChecksumAddress(from), 21 | transaction: { 22 | client: thirdwebClient, 23 | chain, 24 | to: from, 25 | data: "0x", 26 | value: 0n, 27 | nonce, 28 | }, 29 | }); 30 | 31 | // Set 2x current gas to prioritize this transaction over any pending one. 32 | // NOTE: This will not cancel a pending transaction set with higher gas. 33 | if (populatedTransaction.gasPrice) { 34 | populatedTransaction.gasPrice *= 2n; 35 | } 36 | if (populatedTransaction.maxFeePerGas) { 37 | populatedTransaction.maxFeePerGas *= 2n; 38 | } 39 | if (populatedTransaction.maxFeePerGas) { 40 | populatedTransaction.maxFeePerGas *= 2n; 41 | } 42 | 43 | const account = await getAccount({ chainId, from }); 44 | const { transactionHash } = await account.sendTransaction( 45 | populatedTransaction, 46 | ); 47 | return transactionHash; 48 | }; 49 | -------------------------------------------------------------------------------- /src/shared/utils/transaction/webhook.ts: -------------------------------------------------------------------------------- 1 | import { WebhooksEventTypes } from "../../schemas/webhooks"; 2 | import { SendWebhookQueue } from "../../../worker/queues/send-webhook-queue"; 3 | import type { AnyTransaction } from "./types"; 4 | 5 | export const enqueueTransactionWebhook = async ( 6 | transaction: AnyTransaction, 7 | ) => { 8 | const { queueId, status } = transaction; 9 | const type = 10 | status === "sent" 11 | ? WebhooksEventTypes.SENT_TX 12 | : status === "mined" 13 | ? WebhooksEventTypes.MINED_TX 14 | : status === "cancelled" 15 | ? WebhooksEventTypes.CANCELLED_TX 16 | : status === "errored" 17 | ? WebhooksEventTypes.ERRORED_TX 18 | : null; 19 | if (type) { 20 | await SendWebhookQueue.enqueueWebhook({ type, queueId }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/tracer.ts: -------------------------------------------------------------------------------- 1 | import tracer from "dd-trace"; 2 | import { env } from "./shared/utils/env"; 3 | 4 | if (env.DD_TRACER_ACTIVATED) { 5 | tracer.init(); // initialized in a different file to avoid hoisting. 6 | } 7 | 8 | export default tracer; 9 | -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { chainIndexerListener } from "./listeners/chain-indexer-listener"; 2 | import { initCancelRecycledNoncesWorker } from "./tasks/cancel-recycled-nonces-worker"; 3 | import { initMineTransactionWorker } from "./tasks/mine-transaction-worker"; 4 | import { initNonceHealthCheckWorker } from "./tasks/nonce-health-check-worker"; 5 | import { initNonceResyncWorker } from "./tasks/nonce-resync-worker"; 6 | import { initProcessEventLogsWorker } from "./tasks/process-event-logs-worker"; 7 | import { initProcessTransactionReceiptsWorker } from "./tasks/process-transaction-receipts-worker"; 8 | import { initPruneTransactionsWorker } from "./tasks/prune-transactions-worker"; 9 | import { initSendTransactionWorker } from "./tasks/send-transaction-worker"; 10 | import { initSendWebhookWorker } from "./tasks/send-webhook-worker"; 11 | import { initWalletSubscriptionWorker } from "./tasks/wallet-subscription-worker"; 12 | 13 | export const initWorker = async () => { 14 | initCancelRecycledNoncesWorker(); 15 | initProcessEventLogsWorker(); 16 | initProcessTransactionReceiptsWorker(); 17 | initPruneTransactionsWorker(); 18 | initSendTransactionWorker(); 19 | initMineTransactionWorker(); 20 | initSendWebhookWorker(); 21 | initNonceHealthCheckWorker(); 22 | 23 | await initNonceResyncWorker(); 24 | await initWalletSubscriptionWorker(); 25 | 26 | // Contract subscriptions. 27 | await chainIndexerListener(); 28 | }; 29 | -------------------------------------------------------------------------------- /src/worker/listeners/chain-indexer-listener.ts: -------------------------------------------------------------------------------- 1 | import { CronJob } from "cron"; 2 | import { getConfig } from "../../shared/utils/cache/get-config"; 3 | import { logger } from "../../shared/utils/logger"; 4 | import { manageChainIndexers } from "../tasks/manage-chain-indexers"; 5 | 6 | let processChainIndexerStarted = false; 7 | let task: CronJob; 8 | 9 | export const chainIndexerListener = async (): Promise => { 10 | const config = await getConfig(); 11 | if (!config.indexerListenerCronSchedule) { 12 | return; 13 | } 14 | 15 | // Stop the existing task if it exists. 16 | if (task) { 17 | task.stop(); 18 | } 19 | 20 | task = new CronJob(config.indexerListenerCronSchedule, async () => { 21 | if (!processChainIndexerStarted) { 22 | processChainIndexerStarted = true; 23 | await manageChainIndexers(); 24 | processChainIndexerStarted = false; 25 | } else { 26 | logger({ 27 | service: "worker", 28 | level: "warn", 29 | message: "manageChainIndexers already running, skipping", 30 | }); 31 | } 32 | }); 33 | task.start(); 34 | }; 35 | -------------------------------------------------------------------------------- /src/worker/queues/cancel-recycled-nonces-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import { redis } from "../../shared/utils/redis/redis"; 3 | import { defaultJobOptions } from "./queues"; 4 | 5 | export class CancelRecycledNoncesQueue { 6 | static q = new Queue("cancel-recycled-nonces", { 7 | connection: redis, 8 | defaultJobOptions, 9 | }); 10 | 11 | constructor() { 12 | CancelRecycledNoncesQueue.q.setGlobalConcurrency(1); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/worker/queues/mine-transaction-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import superjson from "superjson"; 3 | import { env } from "../../shared/utils/env"; 4 | import { redis } from "../../shared/utils/redis/redis"; 5 | import { defaultJobOptions } from "./queues"; 6 | 7 | export type MineTransactionData = { 8 | queueId: string; 9 | }; 10 | 11 | // Attempts are made every ~20 seconds. See backoffStrategy in initMineTransactionWorker(). 12 | const NUM_ATTEMPTS = env.EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS / 20; 13 | 14 | export class MineTransactionQueue { 15 | static q = new Queue("transactions-2-mine", { 16 | connection: redis, 17 | // Backoff strategy is defined on the worker (`BackeoffStrategy`) and when adding to the queue (`attempts`). 18 | defaultJobOptions, 19 | }); 20 | 21 | // There must be a worker to poll the result for every transaction hash, 22 | // even for the same queueId. This handles if any retried transactions succeed. 23 | static jobId = (data: MineTransactionData) => `mine.${data.queueId}`; 24 | 25 | static add = async (data: MineTransactionData) => { 26 | const serialized = superjson.stringify(data); 27 | const jobId = this.jobId(data); 28 | await this.q.add(jobId, serialized, { 29 | jobId, 30 | attempts: NUM_ATTEMPTS, 31 | backoff: { type: "custom" }, 32 | }); 33 | }; 34 | 35 | static length = async () => this.q.getWaitingCount(); 36 | } 37 | -------------------------------------------------------------------------------- /src/worker/queues/nonce-health-check-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import { redis } from "../../shared/utils/redis/redis"; 3 | import { defaultJobOptions } from "./queues"; 4 | 5 | export class NonceHealthCheckQueue { 6 | static q = new Queue("nonceHealthCheck", { 7 | connection: redis, 8 | defaultJobOptions, 9 | }); 10 | 11 | constructor() { 12 | NonceHealthCheckQueue.q.setGlobalConcurrency(1); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/worker/queues/nonce-resync-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import { redis } from "../../shared/utils/redis/redis"; 3 | import { defaultJobOptions } from "./queues"; 4 | 5 | export class NonceResyncQueue { 6 | static q = new Queue("nonce-resync-cron", { 7 | connection: redis, 8 | defaultJobOptions, 9 | }); 10 | 11 | constructor() { 12 | NonceResyncQueue.q.setGlobalConcurrency(1); 13 | 14 | // The cron job is defined in `initNonceResyncWorker` 15 | // because it requires an async call to query configuration. 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/worker/queues/prune-transactions-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import { redis } from "../../shared/utils/redis/redis"; 3 | import { defaultJobOptions } from "./queues"; 4 | 5 | export class PruneTransactionsQueue { 6 | static q = new Queue("prune-transactions", { 7 | connection: redis, 8 | defaultJobOptions, 9 | }); 10 | 11 | constructor() { 12 | PruneTransactionsQueue.q.setGlobalConcurrency(1); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/worker/queues/queues.ts: -------------------------------------------------------------------------------- 1 | import type { Job, JobsOptions, Worker } from "bullmq"; 2 | import { env } from "../../shared/utils/env"; 3 | import { logger } from "../../shared/utils/logger"; 4 | 5 | export const defaultJobOptions: JobsOptions = { 6 | // Does not retry by default. Queues must explicitly define their own retry count and backoff behavior. 7 | attempts: 0, 8 | removeOnComplete: { 9 | age: 7 * 24 * 60 * 60, 10 | count: env.QUEUE_COMPLETE_HISTORY_COUNT, 11 | }, 12 | removeOnFail: { 13 | age: 7 * 24 * 60 * 60, 14 | count: env.QUEUE_FAIL_HISTORY_COUNT, 15 | }, 16 | }; 17 | 18 | export const logWorkerExceptions = (worker: Worker) => { 19 | worker.on("failed", (job: Job | undefined, err: Error) => { 20 | if (!job) { 21 | return; 22 | } 23 | 24 | job.log(`Job failed: ${err.message}`); 25 | logger({ 26 | level: "error", 27 | message: `[${worker.name}] Job failed. jobId="${job.id}" data="${ 28 | job.data 29 | }", error="${err.message}" ${ 30 | env.NODE_ENV === "development" ? err.stack : "" 31 | }`, 32 | service: "worker", 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/worker/queues/send-transaction-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import superjson from "superjson"; 3 | import { redis } from "../../shared/utils/redis/redis"; 4 | import { defaultJobOptions } from "./queues"; 5 | 6 | export type SendTransactionData = { 7 | queueId: string; 8 | resendCount: number; 9 | }; 10 | 11 | export class SendTransactionQueue { 12 | static q = new Queue("transactions-1-send", { 13 | connection: redis, 14 | defaultJobOptions: { 15 | ...defaultJobOptions, 16 | attempts: 5, 17 | // Retries in: 1s, 2s, 4s, 8s, 16s 18 | backoff: { type: "exponential", delay: 1_000 }, 19 | }, 20 | }); 21 | 22 | // Allow enqueuing the same queueId for multiple retries. 23 | static jobId = (data: SendTransactionData) => 24 | `${data.queueId}.${data.resendCount}`; 25 | 26 | static add = async (data: SendTransactionData) => { 27 | const serialized = superjson.stringify(data); 28 | const jobId = this.jobId(data); 29 | await this.q.add(jobId, serialized, { jobId }); 30 | }; 31 | 32 | static remove = async (data: SendTransactionData) => { 33 | try { 34 | await this.q.remove(this.jobId(data)); 35 | } catch (_e) { 36 | // Job is currently running. 37 | } 38 | }; 39 | 40 | static length = async () => this.q.getWaitingCount(); 41 | } 42 | -------------------------------------------------------------------------------- /src/worker/queues/wallet-subscription-queue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import { redis } from "../../shared/utils/redis/redis"; 3 | import { defaultJobOptions } from "./queues"; 4 | 5 | export class WalletSubscriptionQueue { 6 | static q = new Queue("wallet-subscription", { 7 | connection: redis, 8 | defaultJobOptions, 9 | }); 10 | 11 | constructor() { 12 | WalletSubscriptionQueue.q.setGlobalConcurrency(1); 13 | } 14 | } -------------------------------------------------------------------------------- /src/worker/tasks/manage-chain-indexers.ts: -------------------------------------------------------------------------------- 1 | import { getContractSubscriptionsUniqueChainIds } from "../../shared/db/contract-subscriptions/get-contract-subscriptions"; 2 | import { 3 | INDEXER_REGISTRY, 4 | addChainIndexer, 5 | removeChainIndexer, 6 | } from "../indexers/chain-indexer-registry"; 7 | 8 | export const manageChainIndexers = async () => { 9 | const chainIdsToIndex = await getContractSubscriptionsUniqueChainIds(); 10 | 11 | for (const chainId of chainIdsToIndex) { 12 | if (!(chainId in INDEXER_REGISTRY)) { 13 | await addChainIndexer(chainId); 14 | } 15 | } 16 | 17 | for (const chainId in INDEXER_REGISTRY) { 18 | const chainIdNum = Number.parseInt(chainId); 19 | if (!chainIdsToIndex.includes(chainIdNum)) { 20 | await removeChainIndexer(chainIdNum); 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/worker/tasks/prune-transactions-worker.ts: -------------------------------------------------------------------------------- 1 | import { Worker, type Job, type Processor } from "bullmq"; 2 | import { TransactionDB } from "../../shared/db/transactions/db"; 3 | import { pruneNonceMaps } from "../../shared/db/wallets/nonce-map"; 4 | import { env } from "../../shared/utils/env"; 5 | import { redis } from "../../shared/utils/redis/redis"; 6 | import { PruneTransactionsQueue } from "../queues/prune-transactions-queue"; 7 | import { logWorkerExceptions } from "../queues/queues"; 8 | 9 | const handler: Processor = async (job: Job) => { 10 | const numTransactionsDeleted = 11 | await TransactionDB.pruneTransactionDetailsAndLists( 12 | env.TRANSACTION_HISTORY_COUNT, 13 | ); 14 | job.log(`Pruned ${numTransactionsDeleted} transaction details.`); 15 | 16 | const numNonceMapsDeleted = await pruneNonceMaps(); 17 | job.log(`Pruned ${numNonceMapsDeleted} nonce maps.`); 18 | }; 19 | 20 | // Must be explicitly called for the worker to run on this host. 21 | export const initPruneTransactionsWorker = () => { 22 | PruneTransactionsQueue.q.add("cron", "", { 23 | repeat: { pattern: "*/10 * * * *" }, 24 | jobId: "prune-transactions-cron", 25 | }); 26 | 27 | const _worker = new Worker(PruneTransactionsQueue.q.name, handler, { 28 | concurrency: 1, 29 | connection: redis, 30 | }); 31 | logWorkerExceptions(_worker); 32 | }; 33 | -------------------------------------------------------------------------------- /tests/e2e/.env.test.example: -------------------------------------------------------------------------------- 1 | THIRDWEB_API_SECRET_KEY="" 2 | ENGINE_ACCESS_TOKEN="" 3 | ENGINE_URL="http://127.0.0.1:3005" 4 | ANVIL_URL="" -------------------------------------------------------------------------------- /tests/e2e/README.md: -------------------------------------------------------------------------------- 1 | # engine e2e test suite 2 | ## Configuration 3 | 1. Create a `.env.test` file (use `.env.test.example` as a template) and fill in the necessary values. 4 | 2. Check `config.ts` to configure the test suite. 5 | 3. Run `bun test` within the directory to run the tests. 6 | 7 | Note: make sure `engine` is running, and `anvil` is installed if running the tests on a local environment. (You can get the latest version of `anvil` by installing [`foundry`](https://book.getfoundry.sh/getting-started/installation)) 8 | 9 | ## Running tests 10 | The test suite depends on a local SDK to run tests. To run the tests, you need to generate the SDK. To do this, run the following command from the root of the repository: 11 | 12 | ```bash 13 | yarn generate:sdk 14 | ``` 15 | Run all subsequent commands from the `test/e2e` directory. 16 | 17 | Some tests contains load tests which take a long time to run. To ensure they don't timeout, use the following command: 18 | 19 | ```bash 20 | bun test --timeout 300000 21 | ``` 22 | 23 | To run a specific test, use the following command: 24 | 25 | ```bash 26 | bun test tests/.test.ts 27 | ``` -------------------------------------------------------------------------------- /tests/e2e/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/engine/fa4369ab34d8f168e83c0796ed12f8de4a889b95/tests/e2e/bun.lockb -------------------------------------------------------------------------------- /tests/e2e/config.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | import { anvil, type Chain } from "thirdweb/chains"; 3 | 4 | assert(process.env.ENGINE_URL, "ENGINE_URL is required"); 5 | assert(process.env.ENGINE_ACCESS_TOKEN, "ENGINE_ACCESS_TOKEN is required"); 6 | 7 | export const CONFIG: Config = { 8 | ACCESS_TOKEN: process.env.ENGINE_ACCESS_TOKEN, 9 | URL: process.env.ENGINE_URL, 10 | USE_LOCAL_CHAIN: true, 11 | CHAIN: { 12 | ...anvil, 13 | rpc: "http://127.0.0.1:8545/1", 14 | }, 15 | TRANSACTION_COUNT: 500, 16 | TRANSACTIONS_PER_BATCH: 100, 17 | POLLING_INTERVAL: 1000, 18 | STAGGER_MAX: 500, 19 | }; 20 | 21 | type Config = { 22 | ACCESS_TOKEN: string; 23 | URL: string; 24 | TRANSACTION_COUNT: number; 25 | TRANSACTIONS_PER_BATCH: number; 26 | POLLING_INTERVAL: number; 27 | STAGGER_MAX: number; 28 | CHAIN: Chain; 29 | USE_LOCAL_CHAIN?: boolean; 30 | }; 31 | -------------------------------------------------------------------------------- /tests/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "module": "index.ts", 4 | "devDependencies": { 5 | "@types/bun": "latest", 6 | "tsup": "^8.2.3" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "type": "module", 12 | "dependencies": { 13 | "prool": "^0.0.16", 14 | "thirdweb": "5.90.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/e2e/scripts/counter.ts: -------------------------------------------------------------------------------- 1 | // Anvil chain outputs every RPC call to stdout 2 | // You can save the output to a file and then use this script to count the number of times a specific RPC call is made. 3 | 4 | import { argv } from "bun"; 5 | import { readFile } from "node:fs/promises"; 6 | import { join } from "node:path"; 7 | 8 | const file = join(__dirname, argv[2]); 9 | 10 | async function countLines(file: string) { 11 | const data = await readFile(file, "utf-8"); 12 | const lines = data.split("\n"); 13 | const statements = new Map(); 14 | 15 | for (const line of lines) { 16 | if (!line.trim()) { 17 | continue; 18 | } 19 | const statement = statements.get(line); 20 | if (statement !== undefined) { 21 | statements.set(line, statement + 1); 22 | } else { 23 | statements.set(line, 1); 24 | } 25 | } 26 | 27 | return statements; 28 | } 29 | 30 | countLines(file).then((lines) => { 31 | for (const [line, count] of lines) { 32 | console.log(`${line}: ${count}`); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /tests/e2e/tests/routes/sign-message.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { signMessage, toHex } from "thirdweb/utils"; 3 | import { ANVIL_PKEY_A } from "../../utils/wallets"; 4 | import { setup } from "../setup"; 5 | 6 | describe("signMessageRoute", () => { 7 | test("Sign a message (string)", async () => { 8 | const { engine, backendWallet } = await setup(); 9 | 10 | const res = await engine.backendWallet.signMessage(backendWallet, { 11 | message: "hello world", 12 | }); 13 | 14 | const expected = signMessage({ 15 | message: "hello world", 16 | privateKey: ANVIL_PKEY_A, 17 | }); 18 | 19 | expect(res.result).toEqual(expected); 20 | }); 21 | 22 | test("Sign a message (bytes)", async () => { 23 | const { engine, backendWallet } = await setup(); 24 | 25 | const res = await engine.backendWallet.signMessage(backendWallet, { 26 | message: toHex("hello world"), 27 | isBytes: true, 28 | }); 29 | 30 | const expected = signMessage({ 31 | message: "hello world", 32 | privateKey: ANVIL_PKEY_A, 33 | }); 34 | 35 | expect(res.result).toEqual(expected); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/e2e/tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { env, sleep } from "bun"; 2 | import { afterAll, beforeAll } from "bun:test"; 3 | import { 4 | createChain, 5 | getEngineBackendWallet, 6 | setupEngine, 7 | } from "../utils/engine"; 8 | 9 | import { createThirdwebClient, type Address } from "thirdweb"; 10 | import { CONFIG } from "../config"; 11 | import { startAnvil, stopAnvil } from "../utils/anvil"; 12 | 13 | type SetupResult = { 14 | engine: ReturnType; 15 | backendWallet: Address; 16 | thirdwebClient: ReturnType; 17 | }; 18 | 19 | let cachedSetup: SetupResult | null = null; 20 | 21 | export const setup = async (): Promise => { 22 | if (cachedSetup) { 23 | return cachedSetup; 24 | } 25 | 26 | const engine = setupEngine(); 27 | const backendWallet = await getEngineBackendWallet(engine); 28 | await engine.backendWallet.resetNonces({}); 29 | 30 | if (!env.THIRDWEB_API_SECRET_KEY) 31 | throw new Error("THIRDWEB_API_SECRET_KEY is not set"); 32 | 33 | const thirdwebClient = createThirdwebClient({ 34 | secretKey: env.THIRDWEB_API_SECRET_KEY, 35 | }); 36 | 37 | if (CONFIG.USE_LOCAL_CHAIN) { 38 | startAnvil(); 39 | 40 | await createChain(engine); 41 | await sleep(1000); // wait for chain to start producing blocks 42 | } 43 | 44 | cachedSetup = { engine, backendWallet, thirdwebClient }; 45 | return cachedSetup; 46 | }; 47 | 48 | // Run setup once before all tests 49 | beforeAll(async () => { 50 | await setup(); 51 | }); 52 | 53 | afterAll(async () => { 54 | await stopAnvil(); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/e2e/tests/smoke.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { CONFIG } from "../config"; 3 | import { pollTransactionStatus } from "../utils/transactions"; 4 | import { setup } from "./setup"; 5 | 6 | describe("Smoke Test", () => { 7 | test("Send NoOp Transaction", async () => { 8 | const { engine, backendWallet } = await setup(); 9 | 10 | const res = await engine.backendWallet.transfer( 11 | CONFIG.CHAIN.id.toString(), 12 | backendWallet, 13 | { 14 | amount: "0", 15 | to: backendWallet, 16 | }, 17 | ); 18 | 19 | const transactionStatus = await pollTransactionStatus( 20 | engine, 21 | res.result.queueId, 22 | true, 23 | ); 24 | 25 | expect(transactionStatus.minedAt).toBeDefined(); 26 | console.log("Transaction mined"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/e2e/tests/utils/get-block-time.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { polygon } from "thirdweb/chains"; 3 | import { getBlockTimeSeconds } from "../../../../src/utils/indexer/getBlockTime"; 4 | 5 | describe("getBlockTimeSeconds", () => { 6 | test("Returns roughly 2 seconds for Polygon", async () => { 7 | const result = await getBlockTimeSeconds(polygon.id, 100); 8 | // May be off slightly due to not having subsecond granularity. 9 | expect(Math.round(result)).toEqual(2); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/e2e/utils/anvil.ts: -------------------------------------------------------------------------------- 1 | import { createServer, type CreateServerReturnType } from "prool"; 2 | import { anvil } from "prool/instances"; 3 | 4 | let server: CreateServerReturnType | undefined; 5 | 6 | export const startAnvil = async () => { 7 | console.log("Starting Anvil server..."); 8 | const server = createServer({ 9 | instance: anvil(), 10 | port: 8545, 11 | }); 12 | await server.start(); 13 | }; 14 | 15 | export const stopAnvil = async () => { 16 | if (server) { 17 | console.log("Stopping Anvil server..."); 18 | await server.stop(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/e2e/utils/statistics.ts: -------------------------------------------------------------------------------- 1 | export const calculateStats = (times: number[]) => { 2 | const filteredTimes = times.filter((time) => time > 0); 3 | const sortedTimes = [...filteredTimes].sort((a, b) => a - b); 4 | 5 | return { 6 | avg: filteredTimes.reduce((a, b) => a + b, 0) / filteredTimes.length, 7 | min: Math.min(...filteredTimes), 8 | max: Math.max(...filteredTimes), 9 | p95: sortedTimes[Math.floor(sortedTimes.length * 0.95)], 10 | }; 11 | }; 12 | 13 | export const printStats = (minedTimes: number[], sentTimes: number[]) => { 14 | const minedStats = calculateStats(minedTimes); 15 | const sentStats = calculateStats(sentTimes); 16 | 17 | console.table({ 18 | Mined: minedStats, 19 | Sent: sentStats, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/e2e/utils/wallets.ts: -------------------------------------------------------------------------------- 1 | import { createThirdwebClient } from "thirdweb"; 2 | import { privateKeyToAccount } from "thirdweb/wallets"; 3 | 4 | export const ANVIL_PKEY_A = 5 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 6 | export const ANVIL_PKEY_B = 7 | "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; 8 | export const ANVIL_PKEY_C = 9 | "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; 10 | export const ANVIL_PKEY_D = 11 | "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6"; 12 | 13 | export const client = createThirdwebClient({ 14 | secretKey: process.env.THIRDWEB_API_SECRET_KEY as string, 15 | }); 16 | 17 | export const TEST_ACCOUNT_A = privateKeyToAccount({ 18 | client, 19 | privateKey: ANVIL_PKEY_A, 20 | }); 21 | 22 | export const TEST_ACCOUNT_B = privateKeyToAccount({ 23 | client, 24 | privateKey: ANVIL_PKEY_B, 25 | }); 26 | -------------------------------------------------------------------------------- /tests/shared/aws-kms.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "vitest"; 2 | 3 | const TEST_AWS_KMS_ACCESS_KEY_ID = process.env.TEST_AWS_KMS_ACCESS_KEY_ID; 4 | const TEST_AWS_KMS_SECRET_ACCESS_KEY = 5 | process.env.TEST_AWS_KMS_SECRET_ACCESS_KEY; 6 | const TEST_AWS_KMS_KEY_ID = process.env.TEST_AWS_KMS_KEY_ID; 7 | const TEST_AWS_KMS_REGION = process.env.TEST_AWS_KMS_REGION; 8 | 9 | assert(TEST_AWS_KMS_ACCESS_KEY_ID, "TEST_AWS_KMS_ACCESS_KEY_ID is required"); 10 | assert( 11 | TEST_AWS_KMS_SECRET_ACCESS_KEY, 12 | "TEST_AWS_KMS_SECRET_ACCESS_KEY is required", 13 | ); 14 | assert(TEST_AWS_KMS_KEY_ID, "TEST_AWS_KMS_KEY_ID is required"); 15 | assert(TEST_AWS_KMS_REGION, "TEST_AWS_KMS_REGION is required"); 16 | 17 | export const TEST_AWS_KMS_CONFIG = { 18 | accessKeyId: TEST_AWS_KMS_ACCESS_KEY_ID, 19 | secretAccessKey: TEST_AWS_KMS_SECRET_ACCESS_KEY, 20 | region: TEST_AWS_KMS_REGION, 21 | keyId: TEST_AWS_KMS_KEY_ID, 22 | }; 23 | -------------------------------------------------------------------------------- /tests/shared/chain.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "thirdweb"; 2 | import { anvil } from "thirdweb/chains"; 3 | import { createTestClient, http } from "viem"; 4 | 5 | export const ANVIL_CHAIN = defineChain({ 6 | ...anvil, 7 | rpc: "http://127.0.0.1:8645/1", 8 | }); 9 | 10 | export const anvilTestClient = createTestClient({ 11 | mode: "anvil", 12 | transport: http(ANVIL_CHAIN.rpc), 13 | }); 14 | -------------------------------------------------------------------------------- /tests/shared/client.ts: -------------------------------------------------------------------------------- 1 | import { createThirdwebClient } from "thirdweb"; 2 | 3 | const secretKey = process.env.TW_SECRET_KEY; 4 | if (!secretKey) throw new Error("TW_SECRET_KEY is required"); 5 | export const TEST_CLIENT = createThirdwebClient({ secretKey }); 6 | -------------------------------------------------------------------------------- /tests/shared/gcp-kms.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "vitest"; 2 | 3 | const TEST_GCP_KMS_RESOURCE_PATH = process.env.TEST_GCP_KMS_RESOURCE_PATH; 4 | const TEST_GCP_KMS_EMAIL = process.env.TEST_GCP_KMS_EMAIL; 5 | const TEST_GCP_KMS_PK = process.env.TEST_GCP_KMS_PK; 6 | 7 | assert(TEST_GCP_KMS_RESOURCE_PATH, "TEST_GCP_KMS_RESOURCE_PATH is required"); 8 | assert(TEST_GCP_KMS_EMAIL, "TEST_GCP_KMS_EMAIL is required"); 9 | assert(TEST_GCP_KMS_PK, "TEST_GCP_KMS_PK is required"); 10 | 11 | export const TEST_GCP_KMS_CONFIG = { 12 | resourcePath: TEST_GCP_KMS_RESOURCE_PATH, 13 | email: TEST_GCP_KMS_EMAIL, 14 | pk: TEST_GCP_KMS_PK, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/unit/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { isValidWebhookUrl } from "../../src/server/utils/validator"; 3 | 4 | describe("isValidWebhookUrl", () => { 5 | it("should return true for a valid HTTPS URL", () => { 6 | expect(isValidWebhookUrl("https://example.com")).toBe(true); 7 | }); 8 | 9 | it("should return false for an HTTP URL", () => { 10 | expect(isValidWebhookUrl("http://example.com")).toBe(false); 11 | }); 12 | 13 | it("should return false for a URL without protocol", () => { 14 | expect(isValidWebhookUrl("example.com")).toBe(false); 15 | }); 16 | 17 | it("should return false for an invalid URL", () => { 18 | expect(isValidWebhookUrl("invalid-url")).toBe(false); 19 | }); 20 | 21 | it("should return false for a URL with a different protocol", () => { 22 | expect(isValidWebhookUrl("ftp://example.com")).toBe(false); 23 | }); 24 | 25 | it("should return false for an empty string", () => { 26 | expect(isValidWebhookUrl("")).toBe(false); 27 | }); 28 | 29 | it("should return true for a http localhost", () => { 30 | expect(isValidWebhookUrl("http://localhost:3000")).toBe(true); 31 | expect(isValidWebhookUrl("http://0.0.0.0:3000")).toBe(true); 32 | expect(isValidWebhookUrl("http://user:pass@127.0.0.1:3000")).toBe(true); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "skipLibCheck": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true 14 | }, 15 | "ts-node": { 16 | "esm": true, 17 | "experimentalSpecifierResolution": "node" 18 | }, 19 | "include": [ 20 | "src/server/**/*.ts", 21 | "src/server/**/*.d.ts", 22 | "src/server/**/*.js", 23 | "src/utils/env.ts", 24 | "src/**/*.ts", 25 | "src/**/*.js", 26 | "src/**/*.d.ts" 27 | ], 28 | "exclude": ["node_modules", "tests/tests/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["tests/unit/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], 6 | exclude: ["tests/e2e/**/*"], 7 | globalSetup: ["./vitest.global-setup.ts"], 8 | mockReset: true, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /vitest.global-setup.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import path from "node:path"; 3 | 4 | config({ 5 | path: [path.resolve(".env.test.local"), path.resolve(".env.test")], 6 | }); 7 | 8 | // import { createServer } from "prool"; 9 | // import { anvil } from "prool/instances"; 10 | 11 | // export async function setup() { 12 | // const server = createServer({ 13 | // instance: anvil(), 14 | // port: 8645, // Choose an appropriate port 15 | // }); 16 | // await server.start(); 17 | // // Return a teardown function that will be called after all tests are complete 18 | // return async () => { 19 | // await server.stop(); 20 | // }; 21 | // } 22 | 23 | export async function setup() {} 24 | --------------------------------------------------------------------------------