diff --git a/.github/workflows/deploy-staging-sync-service.yml b/.github/workflows/deploy-staging-sync-service.yml new file mode 100644 index 0000000000..a8793877c9 --- /dev/null +++ b/.github/workflows/deploy-staging-sync-service.yml @@ -0,0 +1,67 @@ +name: Deploy staging sync service + +on: + workflow_dispatch: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: '1.2.4' + + - name: Build code + run: | + make sync-service.build + + + - name: Copy files to server + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "apps/sync-service/build/*" + target: /home/deployer/staging_sync_service + strip_components: 2 + rm: true + debug: true + + - name: Deploy staging sync service to server + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + script_stop: true + script: | + chmod -R o+rX /home/deployer/staging_sync_service + rm -rf /tmp/staging_sync_service + mv /home/deployer/staging_sync_service /tmp + sudo -u staging_sync_service bash -c ' + rm -rf /home/staging_sync_service/build + cp -r /tmp/staging_sync_service/* /home/staging_sync_service/build + + cat > /home/staging_sync_service/build/.env <<-EOF + NODE_ENV=production + APP_PORT=49534 + SETTINGS_DB_URL=settings.sqlite + EOF + # cf. https://github.com/appleboy/drone-ssh/issues/175 + sed -i '/DRONE_SSH_PREV_COMMAND_EXIT_CODE/d' /home/staging_sync_service/build/.env + cd /home/staging_sync_service/build + bun migrate.es.js + ' + rm -rf /tmp/staging_sync_service + sudo /bin/systemctl restart staging-sync.service diff --git a/.github/workflows/deploy-sync-service.yml b/.github/workflows/deploy-sync-service.yml new file mode 100644 index 0000000000..d5864afe29 --- /dev/null +++ b/.github/workflows/deploy-sync-service.yml @@ -0,0 +1,67 @@ +name: Deploy sync service + +on: + workflow_dispatch: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: '1.2.4' + + - name: Build code + run: | + make sync-service.build + + + - name: Copy files to server + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "apps/sync-service/build/*" + target: /home/deployer/sync_service + strip_components: 2 + rm: true + debug: true + + - name: Deploy sync service to server + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + script_stop: true + script: | + chmod -R o+rX /home/deployer/sync_service + rm -rf /tmp/sync_service + mv /home/deployer/sync_service /tmp + sudo -u sync_service bash -c ' + rm -rf /home/sync_service/build + cp -r /tmp/sync_service/* /home/sync_service/build + + cat > /home/sync_service/build/.env <<-EOF + NODE_ENV=production + APP_PORT=49535 + SETTINGS_DB_URL=settings.sqlite + EOF + # cf. https://github.com/appleboy/drone-ssh/issues/175 + sed -i '/DRONE_SSH_PREV_COMMAND_EXIT_CODE/d' /home/sync_service/build/.env + cd /home/sync_service/build + bun migrate.es.js + ' + rm -rf /tmp/sync_service + sudo /bin/systemctl restart sync.service diff --git a/Makefile b/Makefile index 221a427958..63cb3cb683 100644 --- a/Makefile +++ b/Makefile @@ -415,6 +415,10 @@ monitor-service.build: setup shared.build submitter.build cd apps/monitor-service && make build .PHONY: monitor-service.build +sync-service.build: setup shared.build + cd apps/sync-service && make build +.PHONY: sync-service.build + # ================================================================================================== ##@ Docs diff --git a/apps/iframe/.env.example b/apps/iframe/.env.example index 620c6bde58..01a778f9f9 100644 --- a/apps/iframe/.env.example +++ b/apps/iframe/.env.example @@ -88,6 +88,11 @@ VITE_FAUCET_ENDPOINT=https://faucet.testnet.happy.tech/faucet # Safe to publicize, this gets bundled in the client code served by the wallet. VITE_TURNSTILE_SITEKEY=0x4AAAAAABRnNdBbR6oFMviC +######################################################################################################################## +# SYNC SERVICE + +VITE_SYNC_SERVICE_URL=https://sync-staging.happy.tech + ######################################################################################################################## # DEV UTILS diff --git a/apps/iframe/package.json b/apps/iframe/package.json index cf0ad82c37..b347367ab1 100644 --- a/apps/iframe/package.json +++ b/apps/iframe/package.json @@ -12,6 +12,7 @@ "@happy.tech/common": "workspace:*", "@happy.tech/contracts": "workspace:0.2.0", "@happy.tech/wallet-common": "workspace:*", + "@legendapp/state": "^3.0.0-beta.30", "@metamask/safe-event-emitter": "^3.1.1", "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.56.2", diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx index 77895080fd..00cf083e48 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx @@ -9,11 +9,10 @@ enum TokenMenuActions { } interface RemoveTokensMenuProps { - user: Address token: Address } -const RemoveTokenMenu = ({ user, token }: RemoveTokensMenuProps) => { +const RemoveTokenMenu = ({ token }: RemoveTokensMenuProps) => { return ( @@ -31,7 +30,7 @@ const RemoveTokenMenu = ({ user, token }: RemoveTokensMenuProps) => { asChild className="text-primary dark:text-content cursor-pointer bg-primary/20 hover:bg-primary/30 dark:bg-primary/10 dark:hover:bg-primary/20 rounded-md p-1.5" value={TokenMenuActions.StopTracking} - onClick={() => removeWatchedAsset(user, token)} + onClick={() => removeWatchedAsset(token)} > {TokenMenuActions.StopTracking} diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx index b0b4051c96..fccb0d5eff 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx @@ -1,7 +1,8 @@ +import { observer } from "@legendapp/state/react" import { CoinsIcon } from "@phosphor-icons/react" import { useAtomValue } from "jotai" import { userAtom } from "#src/state/user" -import { watchedAssetsAtom } from "#src/state/watchedAssets" +import { getWatchedAssets } from "#src/state/watchedAssets" import { UserNotFoundWarning } from "../UserNotFoundWarning" import { TriggerImportTokensDialog } from "./ImportTokensDialog" import { WatchedAsset } from "./WatchedAsset" @@ -9,12 +10,11 @@ import { WatchedAsset } from "./WatchedAsset" /** * Displays all watched assets registered by the connected user. */ -export const TokenView = () => { +export const TokenView = observer(() => { const user = useAtomValue(userAtom) - const watchedAssets = useAtomValue(watchedAssetsAtom) + const userAssets = getWatchedAssets() if (!user) return - const userAssets = watchedAssets[user.address] return (
    @@ -35,4 +35,4 @@ export const TokenView = () => {
) -} +}) diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx index 5691b6ac84..00211dae79 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx @@ -84,7 +84,7 @@ export const WatchedAsset = ({ user, asset }: WatchedAssetProps) => { - + ) } diff --git a/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx b/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx index ae7c614c5a..00f2bea78e 100644 --- a/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx @@ -1,7 +1,8 @@ import { useCollapsible } from "@ark-ui/react" import { Button } from "#src/components/primitives/button/Button" import { InlineDrawer } from "#src/components/primitives/collapsible/InlineDrawer" -import { type AppPermissions, clearAppPermissions } from "#src/state/permissions" +import { clearAppPermissions } from "#src/state/permissions" +import type { AppPermissions } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" interface ClearAllDappsPermissionsProps { diff --git a/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx b/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx index ced5f39b68..e93637de65 100644 --- a/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx @@ -1,7 +1,7 @@ import { CaretRightIcon } from "@phosphor-icons/react" import { Link } from "@tanstack/react-router" import { useState } from "react" -import type { AppPermissions } from "#src/state/permissions" +import type { AppPermissions } from "#src/state/permissions/types" import { getAppURL } from "#src/utils/appURL" interface ListItemProps { diff --git a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx index 3e25c18cfb..c3ea209ddb 100644 --- a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx @@ -4,7 +4,8 @@ import { Switch } from "#src/components/primitives/toggle-switch/Switch" import { PermissionName } from "#src/constants/permissions" import { type PermissionDescriptionIndex, permissionDescriptions } from "#src/constants/requestLabels" import { useLocalPermissionChanges } from "#src/hooks/useLocalPermissionChanges" -import type { AppPermissions, PermissionsRequest, WalletPermission } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" +import type { AppPermissions, WalletPermission } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" import { SessionKeyCheckbox } from "./SessionKeyCheckbox" diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index 9ca7231d4b..552b23db0c 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,16 +1,31 @@ -import { entries } from "@happy.tech/common" -import { useAtomValue } from "jotai" -import { useAccount } from "wagmi" -import { type AppPermissions, permissionsMapAtom } from "#src/state/permissions" +import { use$ } from "@legendapp/state/react" +import { permissionsMapLegend } from "#src/state/permissions/observable" +import type { AppPermissions } from "#src/state/permissions/types" import { type AppURL, isWallet } from "#src/utils/appURL" export function useAppsWithPermissions(): [AppURL, AppPermissions][] { - const permissionsMap = useAtomValue(permissionsMapAtom) - const account = useAccount() + const appsWithPermissions = () => { + const permissions = permissionsMapLegend.get() + return Object.values(permissions) + .filter((permission) => !isWallet(permission.invoker)) + .reduce( + (acc, permission) => { + const existing = acc.find(([app]) => app === permission.invoker) + if (existing) { + existing[1][permission.parentCapability] = permission + } else { + acc.push([ + permission.invoker, + { + [permission.parentCapability]: permission, + }, + ]) + } + return acc + }, + [] as [AppURL, AppPermissions][], + ) + } - // TODO: the default here should include the wallet app, but currently its empty - // adding a permission to an unrelated app will cause the wallet to _also_ be - // granted the default permissions and will then show up here - return entries(permissionsMap[account?.address ?? "0x0"] ?? {}) // - .filter(([app]) => !isWallet(app)) + return use$(() => appsWithPermissions()) } diff --git a/apps/iframe/src/hooks/useCachedPermissions.ts b/apps/iframe/src/hooks/useCachedPermissions.ts index 93f08c5c9e..098f8bc9b1 100644 --- a/apps/iframe/src/hooks/useCachedPermissions.ts +++ b/apps/iframe/src/hooks/useCachedPermissions.ts @@ -1,7 +1,8 @@ import { useAtomValue } from "jotai" import { useState } from "react" -import { getAppPermissions, getAppPermissionsPure, permissionsMapAtom } from "#src/state/permissions" -import type { AppPermissions } from "#src/state/permissions" +import { getAppPermissions, getAppPermissionsPure } from "#src/state/permissions" +import { permissionsMapLegend } from "#src/state/permissions/observable" +import type { AppPermissions } from "#src/state/permissions/types" import { userAtom } from "#src/state/user" import type { AppURL } from "#src/utils/appURL" import { canonicalCaveatKey, mergeCaveats } from "#src/utils/caveats" @@ -11,8 +12,8 @@ export function useCachedPermissions(appURL: AppURL): { permissions: AppPermissi // and can be toggle back on while we don't navigate away. const [cachedPermissions, setCachedPermissions] = useState(structuredClone(getAppPermissions(appURL))) const user = useAtomValue(userAtom) - const permissionsMap = useAtomValue(permissionsMapAtom) - const reactivePermissions = getAppPermissionsPure(user, appURL, permissionsMap) + const permissionsMap = permissionsMapLegend.get() + const reactivePermissions = getAppPermissionsPure(user, appURL, Object.values(permissionsMap)) /** * flag to track if any update has occurred. If se, we will set state diff --git a/apps/iframe/src/hooks/useHasPermissions.ts b/apps/iframe/src/hooks/useHasPermissions.ts index 9c33a9affc..2ed54b8c31 100644 --- a/apps/iframe/src/hooks/useHasPermissions.ts +++ b/apps/iframe/src/hooks/useHasPermissions.ts @@ -1,13 +1,8 @@ -import { useAtomValue } from "jotai" -import { useMemo } from "react" -import { type PermissionsRequest, atomForPermissionsCheck } from "../state/permissions" +import { use$ } from "@legendapp/state/react" +import { hasPermissions } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" import { type AppURL, getAppURL } from "../utils/appURL" export function useHasPermissions(permissionsRequest: PermissionsRequest, app: AppURL = getAppURL()) { - // This must be memoized to avoid an infinite render loop. - const permissionsAtom = useMemo( - () => atomForPermissionsCheck(permissionsRequest, app), // - [permissionsRequest, app], - ) - return useAtomValue(permissionsAtom) + return use$(() => hasPermissions(app, permissionsRequest)) } diff --git a/apps/iframe/src/hooks/useLocalPermissionChanges.ts b/apps/iframe/src/hooks/useLocalPermissionChanges.ts index fca153dc4f..0228d1ec75 100644 --- a/apps/iframe/src/hooks/useLocalPermissionChanges.ts +++ b/apps/iframe/src/hooks/useLocalPermissionChanges.ts @@ -6,7 +6,7 @@ import { PermissionName } from "#src/constants/permissions" import { revokeSessionKeys } from "#src/requests/utils/sessionKeys" import { revokedSessionKeys } from "#src/state/interfaceState" import { grantPermissions, hasPermissions, permissionRequestEntries, revokePermissions } from "#src/state/permissions" -import type { PermissionsRequest } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" import { mergeCaveats } from "#src/utils/caveats" import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" diff --git a/apps/iframe/src/listeners/atoms.ts b/apps/iframe/src/listeners/atoms.ts index 6197b8a507..2e82c4a70f 100644 --- a/apps/iframe/src/listeners/atoms.ts +++ b/apps/iframe/src/listeners/atoms.ts @@ -2,7 +2,7 @@ import { Msgs } from "@happy.tech/wallet-common" import { getDefaultStore } from "jotai/vanilla" import { http, createPublicClient } from "viem" import { mainnet } from "viem/chains" -import { permissionsMapAtom } from "#src/state/permissions" +import { permissionsMapLegend } from "#src/state/permissions/observable" import { appMessageBus } from "../services/eventBus" import { authStateAtom } from "../state/authState" import { userAtom } from "../state/user" @@ -64,7 +64,7 @@ if (store.get(userAtom)) emitUserUpdate(store.get(userAtom)) * @emits {@link Msgs.UserChanged} (optional) * @emits {@link Msgs.ProviderEvent} (optional) */ -store.sub(permissionsMapAtom, () => { +permissionsMapLegend.onChange(() => { emitUserUpdate(store.get(userAtom)) }) diff --git a/apps/iframe/src/requests/handlers/approved.ts b/apps/iframe/src/requests/handlers/approved.ts index 609cae2b62..5d36840080 100644 --- a/apps/iframe/src/requests/handlers/approved.ts +++ b/apps/iframe/src/requests/handlers/approved.ts @@ -72,7 +72,7 @@ export async function dispatchApprovedRequest(request: PopupMsgs[Msgs.PopupAppro case "wallet_watchAsset": { const params = checkedWatchedAsset(request.payload.params) - return addWatchedAsset(user.address, params) + return addWatchedAsset(params) } case HappyMethodNames.LOAD_ABI: { diff --git a/apps/iframe/src/requests/handlers/injected.ts b/apps/iframe/src/requests/handlers/injected.ts index 64d22eef29..34b233c459 100644 --- a/apps/iframe/src/requests/handlers/injected.ts +++ b/apps/iframe/src/requests/handlers/injected.ts @@ -224,7 +224,7 @@ export async function dispatchInjectedRequest(request: ProviderMsgsFromApp[Msgs. case "wallet_watchAsset": { checkUser(user) const params = checkedWatchedAsset(request.payload.params) - return addWatchedAsset(user.address, params) + return addWatchedAsset(params) } case HappyMethodNames.LOAD_ABI: { diff --git a/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts b/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts index a66e1d20e7..e449bbc709 100644 --- a/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts +++ b/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts @@ -36,14 +36,12 @@ describe("walletClient wallet_watchAsset", () => { }) await dispatchApprovedRequest(request) - const userAssets = getWatchedAssets() - const assetsForAddress = userAssets[user.address] - expect(assetsForAddress.length).toBe(1) + expect(getWatchedAssets().length).toBe(1) // add the same token a second time, shouldn't add a new token but also returns true // since this isn't an error case const reAddTokenReq = await dispatchApprovedRequest(request) - expect(assetsForAddress.length).toBe(1) + expect(getWatchedAssets().length).toBe(1) expect(reAddTokenReq).toBe(true) }) }) diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions/index.ts similarity index 62% rename from apps/iframe/src/state/permissions.ts rename to apps/iframe/src/state/permissions/index.ts index 39547f868e..e025dc20d0 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions/index.ts @@ -1,130 +1,14 @@ -import { createUUID } from "@happy.tech/common" -import type { Address, UUID } from "@happy.tech/common" +import type { Address } from "@happy.tech/common" import type { HappyUser } from "@happy.tech/wallet-common" -import { type Atom, atom, getDefaultStore } from "jotai" -import { atomFamily, atomWithStorage, createJSONStorage } from "jotai/utils" import { PermissionName } from "#src/constants/permissions" +import { revokedSessionKeys } from "#src/state/interfaceState" +import { getUser } from "#src/state/user" +import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "#src/utils/appURL" +import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" +import { emitUserUpdate } from "#src/utils/emitUserUpdate" import { permissionsLogger } from "#src/utils/logger" -import { StorageKey } from "../services/storage" -import { type AppURL, getAppURL, getWalletURL, isApp, isStandaloneWallet } from "../utils/appURL" -import { checkIfCaveatsMatch } from "../utils/checkIfCaveatsMatch" -import { emitUserUpdate } from "../utils/emitUserUpdate" -import { revokedSessionKeys } from "./interfaceState" -import { getUser, userAtom } from "./user" - -// STORE INSTANTIATION -const store = getDefaultStore() - -// In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. -// These permissions are scoped per app and per account. -// -// The system is not widely adopted and mostly wallet only handles the `eth_accounts` permission, -// which defines whether an app can get the user's account(s) and subsequently make other requests -// (some of which will require confirmations, like `eth_sendTransaction`, some of which won't like -// `eth_call`). -// -// Like other wallets, we only handle the `eth_accounts` permission, but we support processing -// all incoming permission requests. -// -// References: -// https://eips.ethereum.org/EIPS/eip-2255 - -/** - * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions - * for that user on that app. - */ -export type PermissionsMap = Record> - -/** - * Maps permissions names to permission objects. - * EIP-2255 specifies that permissions names must be EIP-1193 request names (e.g. `eth_accounts`). - * However, we type this as a string in case we want to extend the permission system to other - * names that do not map to a request (or are custom requests). - */ -export type AppPermissions = Record - -/** - * Permission object for a specific permission. - * - * This type is copied from Viem (eip1193.ts) - */ -export type WalletPermission = { - // The app to which the permission is granted. - invoker: AppURL - // This is the EIP-1193 request that this permission is mapped to. - parentCapability: PermissionName | (string & {}) - caveats: WalletPermissionCaveat[] - date: number - // Not in the EIP, but Viem wants this. - id: UUID -} - -/** - * A caveat is a specific specific restrictions applied to the permitted request. - */ -type WalletPermissionCaveat = { - type: string - value: unknown -} - -/** - * A request for one or more permissions. - */ -export type PermissionRequestObject = { - [requestName: string]: { [caveatName: string]: unknown } -} - -/** - * A refinement of {@link PermissionRequestObject} for requesting session keys. - */ -export type SessionKeyRequest = { - [PermissionName.SessionKey]: { target: Address } -} - -/** - * A permissions specifier, which can be either a single EIP-1193 request name, or a {@link - * PermissionRequestObject}. - */ -export type PermissionsRequest = string | PermissionRequestObject - -/** - * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions - * for that user on that app. - */ -export const permissionsMapAtom = atomWithStorage(StorageKey.UserPermissions, {}, createJSONStorage(), { - getOnInit: true, -}) - -type PermissionCheckParams = { - permissionsRequest: PermissionsRequest - app: AppURL -} - -const _atomForPermissionsCheck: (params: PermissionCheckParams) => Atom = // - atomFamily(({ permissionsRequest, app }) => { - return atom((get) => { - const user = get(userAtom) - if (!user) return false - // This call *might* be required to record the dependency, which occurs via - // `getDefaultStore().get` during `hasPermissions`. - get(permissionsMapAtom) - return hasPermissions(app, permissionsRequest) - }) - }) - -/** - * A function that returns a new atom that subscribes to a check on the specified permissions. - * - * The atom is cached, but not automatically garbage-collected. If this is called with a changing - * set of permissions, it is necessary to call `atomForPermissionsCheck.remove(oldPermissions)` - * when changing the permissions! - */ -export function atomForPermissionsCheck( - permissionsRequest: PermissionsRequest, // - app: AppURL = getAppURL(), -): Atom { - return _atomForPermissionsCheck({ permissionsRequest, app }) -} +import { permissionsMapLegend } from "./observable" +import type { AppPermissions, PermissionsRequest, WalletPermission, WalletPermissionCaveat } from "./types" // === GET ALL PERMISSIONS ======================================================================================= @@ -133,45 +17,52 @@ export function atomForPermissionsCheck( */ export function getAppPermissions(app: AppURL): AppPermissions { const user = getUser() - const permissionsMap = store.get(permissionsMapAtom) - return getAppPermissionsPure(user, app, permissionsMap) + const permissionsMap = permissionsMapLegend.get() + return getAppPermissionsPure(user, app, Object.values(permissionsMap)) } export function getAppPermissionsPure( user: HappyUser | undefined, app: AppURL, - permissionsMap: PermissionsMap, + permissions: WalletPermission[], ): AppPermissions { if (!user) { // This should never happen and requires investigating if it does! permissionsLogger.warn("No user found, returning empty permissions.") return {} } - const appPermissions = permissionsMap[user.address]?.[app] - if (appPermissions) return appPermissions - - // Permissions don't exist, create them. - - const baseAppPermissions: AppPermissions = - app === getWalletURL() - ? { - // The iframe is always granted the `eth_accounts` permission. - eth_accounts: { - invoker: app, - parentCapability: "eth_accounts", - caveats: [], - date: Date.now(), - id: createUUID(), - }, - } - : {} - - // It's not required to set the permissionsAtom here because the permissions don't actually - // change (so nothing dependent on the atom needs to update). We just write them to avoid - // rerunning the above logic on each lookup. - permissionsMap[user.address] ??= {} - permissionsMap[user.address][app] = baseAppPermissions - - return baseAppPermissions + + const appPermissions = permissions.filter((p) => p.invoker === app && p.user === user.address) + + if (appPermissions.length > 0) { + const appPermissionsObject = appPermissions.reduce((acc, p) => { + acc[p.parentCapability] = p + return acc + }, {} as AppPermissions) + return appPermissionsObject + } + if (app === getWalletURL()) { + // Permissions don't exist, create them. + // The iframe is always granted the `eth_accounts` permission. + const permissionId = `${user.address}-${app}-eth_accounts` + const permission: WalletPermission = { + type: "WalletPermissions", + user: user.address, + invoker: app, + parentCapability: "eth_accounts", + caveats: [], + date: Date.now(), + id: permissionId, + updatedAt: Date.now(), + createdAt: Date.now(), + deleted: false, + } + permissionsMapLegend[permissionId].set(permission) + return { + eth_accounts: permission, + } + } + + return {} } // === WRITE ALL PERMISSIONS ======================================================================================= @@ -189,25 +80,29 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { } const permissionArray = Object.values(appPermissions) - if (!permissionArray.length) { clearAppPermissions(app) return } - store.set(permissionsMapAtom, (prev: PermissionsMap) => { - if (!permissionArray.every((a) => a.invoker === app)) { - // No all permissions supplied are scoped to the app. - // This should never happen! - console.warn("Invalid permission update requested, not setting permissions.") - return prev - } + if (!permissionArray.every((a) => a.invoker === app)) { + // No all permissions supplied are scoped to the app. + // This should never happen! + console.warn("Invalid permission update requested, not setting permissions.") + return + } - return { - ...prev, - [user.address]: { ...prev[user.address], [app]: appPermissions }, + const currentPermissions = getAppPermissions(app) + + for (const permission of Object.values(currentPermissions)) { + if (!permissionArray.some((p) => p.id === permission.id)) { + permissionsMapLegend[permission.id].delete() } - }) + } + + for (const permission of permissionArray) { + permissionsMapLegend[permission.id].set(permission) + } } // === CLEAR PERMISSIONS =========================================================================== @@ -218,10 +113,13 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { export function clearPermissions(): void { const user = getUser() if (!user) return - store.set(permissionsMapAtom, (prev) => { - const { [user.address]: _, ...rest } = prev - return rest - }) + + const permissions = permissionsMapLegend.get() + for (const permission of Object.values(permissions)) { + if (permission.user === user.address) { + permissionsMapLegend[permission.id].delete() + } + } } /** @@ -230,23 +128,20 @@ export function clearPermissions(): void { export function clearAppPermissions(app: AppURL): void { const user = getUser() if (!user) return - // Register session keys for onchain deregistrations. Object.values(getAppPermissions(app)) .filter((p: WalletPermission) => p.parentCapability === PermissionName.SessionKey) .flatMap((p) => p.caveats) .forEach((c) => revokedSessionKeys.add(c.value as Address)) - // Remove app permissions from storage - store.set(permissionsMapAtom, (prev) => { - const { - [user.address]: { [app]: _, ...otherApps }, - ...otherUsers - } = prev - return { ...otherUsers, [user.address]: otherApps } - }) -} + const permissions = permissionsMapLegend.get() + for (const permission of Object.values(permissions)) { + if (permission.invoker === app && permission.user === user.address) { + permissionsMapLegend[permission.id].delete() + } + } +} type PermissionRequestEntry = { name: string caveats: WalletPermissionCaveat[] @@ -292,9 +187,12 @@ export function permissionRequestEntries(permissions: PermissionsRequest): Permi * ``` */ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequest): WalletPermission[] { - const grantedPermissions = [] + const grantedPermissions: WalletPermission[] = [] const appPermissions = getAppPermissions(app) + const user = getUser() + if (!user) return [] + for (const { name, caveats: newCaveats } of permissionRequestEntries(permissionRequest)) { // If permission exists, merge new caveats with existing ones if (appPermissions[name]) { @@ -311,12 +209,18 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ grantedPermissions.push(appPermissions[name]) } else { - const grantedPermission = { + const id = `${user.address}-${app}-${name}` + const grantedPermission: WalletPermission = { caveats: newCaveats, invoker: app, parentCapability: name, date: Date.now(), - id: createUUID(), + id, + updatedAt: Date.now(), + createdAt: Date.now(), + type: "WalletPermissions", + user: user.address, + deleted: false, } grantedPermissions.push(grantedPermission) @@ -369,7 +273,6 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ */ export function revokePermissions(app: AppURL, permissionsRequest: PermissionsRequest): void { const appPermissions = getAppPermissions(app) - for (const { name, caveats } of permissionRequestEntries(permissionsRequest)) { // Permission is not granted, nothing to do. if (!appPermissions[name]) continue diff --git a/apps/iframe/src/state/permissions/observable.ts b/apps/iframe/src/state/permissions/observable.ts new file mode 100644 index 0000000000..c470ea4364 --- /dev/null +++ b/apps/iframe/src/state/permissions/observable.ts @@ -0,0 +1,84 @@ +import { observable } from "@legendapp/state" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" +import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { deploymentVar } from "#src/env.ts" +import { getUser } from "../user" +import type { WalletPermission } from "./types" + +const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") + +export const permissionsMapLegend = observable( + syncedCrud({ + list: async ({ lastSync }) => { + const user = getUser() + if (!user) return [] + + const response = await fetch( + `${SYNC_SERVICE_URL}/api/v1/settings/list?type=WalletPermissions&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + ) + const data = await response.json() + + return data.data as WalletPermission[] + }, + create: async (data: WalletPermission) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + update: async (data: WalletPermission) => { + const user = getUser() + if (!user) return + + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + type: "WalletPermissions", + user: user.address, + }), + }) + await response.json() + }, + subscribe: ({ refresh }) => { + const user = getUser() + if (!user) return + const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("config.changed", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) + refresh() + }) + + return () => eventSource.close() + }, + delete: async ({ id }) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + await response.json() + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: "config-legend", + retrySync: true, // Retry sync after reload + }, + initial: {}, + fieldCreatedAt: "createdAt", + fieldUpdatedAt: "updatedAt", + fieldDeleted: "deleted", + changesSince: "last-sync", + updatePartial: true, + }), +) diff --git a/apps/iframe/src/state/permissions.spec.ts b/apps/iframe/src/state/permissions/permissions.spec.ts similarity index 99% rename from apps/iframe/src/state/permissions.spec.ts rename to apps/iframe/src/state/permissions/permissions.spec.ts index 2aa84cf12f..3f93356610 100644 --- a/apps/iframe/src/state/permissions.spec.ts +++ b/apps/iframe/src/state/permissions/permissions.spec.ts @@ -9,8 +9,8 @@ import { hasPermissions, revokePermissions, } from "#src/state/permissions" +import { setUser } from "#src/state/user" import { disablePermissionWarnings } from "#src/testing/utils" -import { setUser } from "../state/user" const { appURL, walletURL, appURLMock } = await vi // .hoisted(async () => await import("#src/testing/cross_origin.mocks")) diff --git a/apps/iframe/src/state/permissions/types.ts b/apps/iframe/src/state/permissions/types.ts new file mode 100644 index 0000000000..0b82427b5a --- /dev/null +++ b/apps/iframe/src/state/permissions/types.ts @@ -0,0 +1,82 @@ +import type { Address } from "@happy.tech/common" +import { PermissionName } from "#src/constants/permissions" +import type { AppURL } from "#src/utils/appURL" + +// In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. +// These permissions are scoped per app and per account. +// +// The system is not widely adopted and mostly wallet only handles the `eth_accounts` permission, +// which defines whether an app can get the user's account(s) and subsequently make other requests +// (some of which will require confirmations, like `eth_sendTransaction`, some of which won't like +// `eth_call`). +// +// Like other wallets, we only handle the `eth_accounts` permission, but we support processing +// all incoming permission requests. +// +// References: +// https://eips.ethereum.org/EIPS/eip-2255 + +/** + * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions + * for that user on that app. + */ +export type PermissionsMap = Record> + +/** + * Maps permissions names to permission objects. + * EIP-2255 specifies that permissions names must be EIP-1193 request names (e.g. `eth_accounts`). + * However, we type this as a string in case we want to extend the permission system to other + * names that do not map to a request (or are custom requests). + */ +export type AppPermissions = Record + +/** + * Permission object for a specific permission. + * + * This type is copied from Viem (eip1193.ts) + */ +export type WalletPermission = { + type: "WalletPermissions" + // The user to which the permission is granted. + user: Address + // The app to which the permission is granted. + invoker: AppURL + // This is the EIP-1193 request that this permission is mapped to. + parentCapability: PermissionName | (string & {}) + caveats: WalletPermissionCaveat[] + date: number + // Not in the EIP, but Viem wants this. + id: string + // Required by the sync service. + updatedAt: number + createdAt: number + deleted: boolean +} + +/** + * A caveat is a specific specific restrictions applied to the permitted request. + */ +export type WalletPermissionCaveat = { + type: string + value: unknown +} + +/** + * A request for one or more permissions. + */ +export type PermissionRequestObject = { + [requestName: string]: { [caveatName: string]: unknown } +} + +/** + * A refinement of {@link PermissionRequestObject} for requesting session keys. + */ +export type SessionKeyRequest = { + [PermissionName.SessionKey]: { target: Address } +} + +/** + * A permissions specifier, which can be either a single EIP-1193 request name, or a {@link + * PermissionRequestObject}. + */ +export type PermissionsRequest = string | PermissionRequestObject diff --git a/apps/iframe/src/state/watchedAssets.ts b/apps/iframe/src/state/watchedAssets.ts deleted file mode 100644 index 514dec66d0..0000000000 --- a/apps/iframe/src/state/watchedAssets.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Address } from "@happy.tech/common" -import { getDefaultStore } from "jotai" -import { atomWithStorage } from "jotai/utils" -import type { WatchAssetParameters } from "viem" -import { StorageKey } from "#src/services/storage" - -export type UserWatchedAssetsRecord = Record - -// === Atom Definition ================================================================================== - -/** - * Atom to manage watched assets mapped to user's address, using localStorage. - */ -export const watchedAssetsAtom = atomWithStorage(StorageKey.WatchedAssets, {}, undefined, { - getOnInit: true, -}) - -// Store Instantiation -const store = getDefaultStore() - -// === State Accessors ================================================================================== - -/** - * Retrieves the current list of watched assets from the Jotai store. - */ -export function getWatchedAssets(): UserWatchedAssetsRecord { - return store.get(watchedAssetsAtom) -} - -// === State Mutators =================================================================================== - -/** - * Adds a new asset to the store under the provided address. - * If the asset does not already exist for the address, it is added. - * Does nothing if the asset is already in the list. - */ -export function addWatchedAsset(userAddress: Address, newAsset: WatchAssetParameters): boolean { - store.set(watchedAssetsAtom, (prevAssets) => { - const assetsForAddress = prevAssets[userAddress] || [] - const assetExists = assetsForAddress.some((asset) => asset.options.address === newAsset.options.address) - - return assetExists - ? prevAssets - : { - ...prevAssets, - [userAddress]: assetsForAddress.concat(newAsset), - } - }) - - return true -} - -/** - * Removes a specific asset from the watched assets list by its contract address for a specific user. - * Returns `true` if the asset was found and removed, or `false` if it was not in the list. - */ -export function removeWatchedAsset(userAddress: Address, assetAddress: Address): boolean { - let assetRemoved = false - store.set(watchedAssetsAtom, (prevAssets) => { - const assetsForAddress = prevAssets[userAddress] || [] - const updatedAssets = assetsForAddress.filter((asset) => asset.options.address !== assetAddress) - assetRemoved = updatedAssets.length < assetsForAddress.length - - if (updatedAssets.length === 0) { - const { [userAddress]: _, ...remainingAssets } = prevAssets - return remainingAssets - } - - return { - ...prevAssets, - [userAddress]: updatedAssets, - } - }) - return assetRemoved -} diff --git a/apps/iframe/src/state/watchedAssets/index.ts b/apps/iframe/src/state/watchedAssets/index.ts new file mode 100644 index 0000000000..0db50b583b --- /dev/null +++ b/apps/iframe/src/state/watchedAssets/index.ts @@ -0,0 +1,48 @@ +import type { Address } from "@happy.tech/common" +import type { WatchAssetParameters } from "viem" +import { getUser } from "#src/state/user" +import { watchedAssetsMapLegend } from "./observable" +import type { WatchedAsset } from "./types" + +// === State Accessors ================================================================================== + +/** + * Retrieves the current list of watched assets from the Jotai store. + */ +export function getWatchedAssets(): WatchedAsset[] { + return Object.values(watchedAssetsMapLegend.get()) +} + +// === State Mutators =================================================================================== + +/** + * Adds a new asset to the store under the provided address. + * If the asset does not already exist for the address, it is added. + * Does nothing if the asset is already in the list. + */ +export function addWatchedAsset(newAsset: WatchAssetParameters): boolean { + const user = getUser() + if (!user) return false + + const asset: WatchedAsset = { + ...newAsset, + user: user.address, + id: `${user.address}-${newAsset.options.address}`, + createdAt: Date.now(), + updatedAt: Date.now(), + deleted: false, + } + watchedAssetsMapLegend[asset.id].set(asset) + return true +} + +/** + * Removes a specific asset from the watched assets list by its contract address for a specific user. + * Returns `true` if the asset was found and removed, or `false` if it was not in the list. + */ +export function removeWatchedAsset(assetAddress: Address): boolean { + const asset = Object.values(watchedAssetsMapLegend.get()).find((asset) => asset.options.address === assetAddress) + if (!asset) return false + watchedAssetsMapLegend[asset.id].delete() + return true +} diff --git a/apps/iframe/src/state/watchedAssets/observable.ts b/apps/iframe/src/state/watchedAssets/observable.ts new file mode 100644 index 0000000000..a020bf365b --- /dev/null +++ b/apps/iframe/src/state/watchedAssets/observable.ts @@ -0,0 +1,85 @@ +import { observable } from "@legendapp/state" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" +import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { deploymentVar } from "#src/env.ts" +import { getUser } from "../user" +import type { WatchedAsset } from "./types" + +const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") + +export const watchedAssetsMapLegend = observable( + syncedCrud({ + list: async ({ lastSync }) => { + const user = getUser() + if (!user) return [] + + const response = await fetch( + `${SYNC_SERVICE_URL}/api/v1/settings/list?type=ERC20&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + ) + const data = await response.json() + + return data.data as WatchedAsset[] + }, + create: async (data: WatchedAsset) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + update: async (data: WatchedAsset) => { + const user = getUser() + if (!user) return + + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + type: "ERC20", + user: user.address, + }), + }) + await response.json() + }, + subscribe: ({ refresh }) => { + const user = getUser() + if (!user) return () => {} + + console.log("Subscribing to updates for user", user.address) + + const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("config.changed", () => { + refresh() + }) + + return () => eventSource.close() + }, + delete: async ({ id }) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + await response.json() + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: "watched-assets-legend", + retrySync: true, // Retry sync after reload + }, + initial: {}, + fieldCreatedAt: "createdAt", + fieldUpdatedAt: "updatedAt", + fieldDeleted: "deleted", + changesSince: "last-sync", + updatePartial: true, + }), +) diff --git a/apps/iframe/src/state/watchedAssets/types.ts b/apps/iframe/src/state/watchedAssets/types.ts new file mode 100644 index 0000000000..1eecd8a961 --- /dev/null +++ b/apps/iframe/src/state/watchedAssets/types.ts @@ -0,0 +1,10 @@ +import type { Address } from "@happy.tech/common" +import type { WatchAssetParameters } from "viem" + +export type WatchedAsset = WatchAssetParameters & { + user: Address + id: string + createdAt: number + updatedAt: number + deleted: boolean +} diff --git a/apps/sync-service/.env.example b/apps/sync-service/.env.example new file mode 100644 index 0000000000..c6ab1df781 --- /dev/null +++ b/apps/sync-service/.env.example @@ -0,0 +1,3 @@ +NODE_ENV=development +APP_PORT=3000 +SETTINGS_DB_URL=settings.sqlite \ No newline at end of file diff --git a/apps/sync-service/Makefile b/apps/sync-service/Makefile new file mode 100644 index 0000000000..6fe7c145dd --- /dev/null +++ b/apps/sync-service/Makefile @@ -0,0 +1,14 @@ +SRC_ROOT_DIR := src + +include ../../makefiles/lib.mk +include ../../makefiles/formatting.mk +include ../../makefiles/bundling.mk +include ../../makefiles/help.mk + +dev: ## Starts the settings service + bun run --hot src/index.ts +.PHONY: dev + +migrate: ## Runs pending migrations + bun run src/migrate.ts +.PHONY: migrate \ No newline at end of file diff --git a/apps/sync-service/build.config.ts b/apps/sync-service/build.config.ts new file mode 100644 index 0000000000..1ee0717c15 --- /dev/null +++ b/apps/sync-service/build.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@happy.tech/happybuild" + +export default defineConfig({ + exports: [".", "./migrate"], + bunConfig: { + minify: false, + target: "node", + external: ["better-sqlite3"], + }, +}) diff --git a/apps/sync-service/package.json b/apps/sync-service/package.json new file mode 100644 index 0000000000..d56a834698 --- /dev/null +++ b/apps/sync-service/package.json @@ -0,0 +1,27 @@ +{ + "name": "@happy.tech/sync-service", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./dist/index.es.js", + "module": "./dist/index.es.js", + "exports": { + ".": "./dist/index.es.js", + "./migrate": "./dist/migrate.es.js" + }, + "dependencies": { + "@happy.tech/common": "workspace:1.0.0", + "@hono/node-server": "^1.13.8", + "@scalar/hono-api-reference": "^0.5.175", + "hono": "^4.7.2", + "hono-openapi": "^0.4.4", + "neverthrow": "^8.1.0", + "zod": "^3.23.8", + "zod-openapi": "^4.2.3" + }, + "devDependencies": { + "@happy.tech/happybuild": "workspace:1.0.0", + "typescript": "^5.6.2", + "hono-openapi": "^0.4.4" + } +} diff --git a/apps/sync-service/src/db/driver.ts b/apps/sync-service/src/db/driver.ts new file mode 100644 index 0000000000..72da91b149 --- /dev/null +++ b/apps/sync-service/src/db/driver.ts @@ -0,0 +1,13 @@ +import { Database as BunDatabase } from "bun:sqlite" +import { Kysely, ParseJSONResultsPlugin } from "kysely" +import { BunSqliteDialect } from "kysely-bun-sqlite" +import type { Database } from "./types" + +import { env } from "../env" + +const dbPath = env.SETTINGS_DB_URL || ":memory:" + +export const db = new Kysely({ + dialect: new BunSqliteDialect({ database: new BunDatabase(dbPath) }), + plugins: [new ParseJSONResultsPlugin()], +}) diff --git a/apps/sync-service/src/db/migrations/Migration20250515123000.ts b/apps/sync-service/src/db/migrations/Migration20250515123000.ts new file mode 100644 index 0000000000..8b4fc215e8 --- /dev/null +++ b/apps/sync-service/src/db/migrations/Migration20250515123000.ts @@ -0,0 +1,19 @@ +import type { Kysely } from "kysely" +import type { Database } from "../types" + +export async function up(db: Kysely) { + await db.schema + .createTable("walletPermissions") + .addColumn("user", "text", (col) => col.notNull()) + .addColumn("invoker", "text", (col) => col.notNull()) + .addColumn("parentCapability", "text", (col) => col.notNull()) + .addColumn("caveats", "jsonb", (col) => col.notNull()) + .addColumn("date", "integer", (col) => col.notNull()) + .addColumn("id", "text", (col) => col.notNull().primaryKey()) + .addColumn("updatedAt", "integer", (col) => col.notNull()) + .addColumn("createdAt", "integer", (col) => col.notNull()) + .addColumn("deleted", "boolean", (col) => col.notNull()) + .execute() +} + +export const migration20250515123000 = { up } diff --git a/apps/sync-service/src/db/migrations/Migration20250623143000.ts b/apps/sync-service/src/db/migrations/Migration20250623143000.ts new file mode 100644 index 0000000000..027f72c763 --- /dev/null +++ b/apps/sync-service/src/db/migrations/Migration20250623143000.ts @@ -0,0 +1,35 @@ +import type { Kysely } from "kysely" +import type { Database } from "../types" + +export type WatchAssetParams = { + /** Token type. */ + type: "ERC20" + options: { + /** The address of the token contract */ + address: string + /** A ticker symbol or shorthand, up to 11 characters */ + symbol: string + /** The number of token decimals */ + decimals: number + /** A string url of the token logo */ + image?: string | undefined + } +} + +export async function up(db: Kysely) { + await db.schema + .createTable("watchedAssets") + .addColumn("user", "text", (col) => col.notNull()) + .addColumn("type", "text", (col) => col.notNull()) + .addColumn("address", "text", (col) => col.notNull()) + .addColumn("symbol", "text", (col) => col.notNull()) + .addColumn("decimals", "integer", (col) => col.notNull()) + .addColumn("image", "text") + .addColumn("id", "text", (col) => col.notNull().primaryKey()) + .addColumn("updatedAt", "integer", (col) => col.notNull()) + .addColumn("createdAt", "integer", (col) => col.notNull()) + .addColumn("deleted", "boolean", (col) => col.notNull()) + .execute() +} + +export const migration20250623143000 = { up } diff --git a/apps/sync-service/src/db/migrations/index.ts b/apps/sync-service/src/db/migrations/index.ts new file mode 100644 index 0000000000..fbb32073b9 --- /dev/null +++ b/apps/sync-service/src/db/migrations/index.ts @@ -0,0 +1,7 @@ +import { migration20250515123000 } from "./Migration20250515123000" +import { migration20250623143000 } from "./Migration20250623143000" + +export const migrations = { + "20250515123000": migration20250515123000, + "20250623143000": migration20250623143000, +} diff --git a/apps/sync-service/src/db/types.ts b/apps/sync-service/src/db/types.ts new file mode 100644 index 0000000000..7024ee3f6b --- /dev/null +++ b/apps/sync-service/src/db/types.ts @@ -0,0 +1,52 @@ +import type { Hex } from "@happy.tech/common" +import type { HTTPString } from "@happy.tech/common" +import type { ColumnType } from "kysely" + +export type AppURL = HTTPString & { _brand: "AppHTTPString" } + +/** + * A caveat is a specific specific restrictions applied to the permitted request. + */ +type WalletPermissionCaveat = { + type: string + value: string +} + +/** + * Permission object for a specific permission. + * + * This type is copied from Viem (eip1193.ts) but we add a user field. + */ +export type WalletPermissionRow = { + // The user to which the permission is granted. + user: Hex + // The app to which the permission is granted. + invoker: AppURL + // This is the EIP-1193 request that this permission is mapped to. + parentCapability: string + caveats: ColumnType + date: number + // Not in the EIP, but Viem wants this. + id: string + updatedAt: number + createdAt: number + deleted: ColumnType +} + +export type WatchAssetRow = { + user: Hex + type: string + address: Hex + symbol: string + decimals: number + image: string + id: string + updatedAt: number + createdAt: number + deleted: ColumnType +} + +export interface Database { + walletPermissions: WalletPermissionRow + watchedAssets: WatchAssetRow +} diff --git a/apps/sync-service/src/dtos.ts b/apps/sync-service/src/dtos.ts new file mode 100644 index 0000000000..1384af5f0a --- /dev/null +++ b/apps/sync-service/src/dtos.ts @@ -0,0 +1,99 @@ +import { isAddress } from "@happy.tech/common" +import { checksum } from "ox/Address" +import { z } from "zod" +import { isAppUrl } from "./utils/isAppUrl" + +export const walletPermission = z.object({ + type: z.literal("WalletPermissions").openapi({ + example: "WalletPermissions", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + invoker: z.string().refine(isAppUrl).openapi({ example: "https://app.happy.tech" }), + parentCapability: z.string().openapi({ example: "eth_accounts" }), + caveats: z.array( + z.object({ + type: z.string().openapi({ example: "target" }), + value: z.string().openapi({ example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }), + }), + ), + date: z.number().openapi({ example: 1715702400 }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + createdAt: z.number().openapi({ example: 1715702400 }), + deleted: z.boolean().openapi({ example: false }), +}) + +export type WalletPermission = z.infer + +export const walletPermissionUpdate = walletPermission.partial().extend({ + type: z.literal("WalletPermissions").openapi({ + example: "WalletPermissions", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), +}) + +export type WalletPermissionUpdate = z.infer + +export const configChangedEvent = z.object({ + event: z.enum(["config.changed"]), + data: z.object({ + destination: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + resourceId: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), +}) + +export type ConfigChangedEvent = z.infer + +export const watchAsset = z.object({ + type: z.literal("ERC20").openapi({ + example: "ERC20", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + options: z.object({ + address: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + symbol: z.string().openapi({ example: "ETH" }), + decimals: z.number().openapi({ example: 18 }), + image: z.string().optional().openapi({ example: "https://example.com/logo.png" }), + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + createdAt: z.number().openapi({ example: 1715702400 }), + deleted: z.boolean().openapi({ example: false }), +}) + +export type WatchAsset = z.infer + +export const watchAssetUpdate = watchAsset.partial().extend({ + type: z.literal("ERC20").openapi({ + example: "ERC20", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), +}) + +export type WatchAssetUpdate = z.infer diff --git a/apps/sync-service/src/env.ts b/apps/sync-service/src/env.ts new file mode 100644 index 0000000000..6aa45a7646 --- /dev/null +++ b/apps/sync-service/src/env.ts @@ -0,0 +1,13 @@ +import { z } from "zod" + +const envSchema = z.object({ + NODE_ENV: z.enum(["development", "production", "staging"]), + APP_PORT: z.string().transform((s) => Number(s)), + SETTINGS_DB_URL: z.string().trim(), + LOG_LEVEL: z.preprocess( + (level) => level && String(level).toUpperCase(), + z.enum(["OFF", "TRACE", "INFO", "WARN", "ERROR"]).default("INFO"), + ), +}) + +export const env = envSchema.parse(process.env) diff --git a/apps/sync-service/src/errors.ts b/apps/sync-service/src/errors.ts new file mode 100644 index 0000000000..11db85717a --- /dev/null +++ b/apps/sync-service/src/errors.ts @@ -0,0 +1,11 @@ +import type { ContentfulStatusCode } from "hono/utils/http-status" + +export abstract class HappySettingsError extends Error { + public readonly statusCode: ContentfulStatusCode + + constructor(statusCode: ContentfulStatusCode, message?: string, options?: ErrorOptions) { + super(message, options) + this.name = this.constructor.name + this.statusCode = statusCode + } +} diff --git a/apps/sync-service/src/handlers/createConfig/createConfig.ts b/apps/sync-service/src/handlers/createConfig/createConfig.ts new file mode 100644 index 0000000000..83e00d6828 --- /dev/null +++ b/apps/sync-service/src/handlers/createConfig/createConfig.ts @@ -0,0 +1,26 @@ +import { createUUID } from "@happy.tech/common" +import { type Result, ok } from "neverthrow" +import { savePermission } from "../../repositories/permissionsRepository" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" +import { notifyUpdates } from "../../services/notifyUpdates" +import type { CreateConfigInput } from "./types" + +export async function createConfig(input: CreateConfigInput): Promise> { + if (input.type === "WalletPermissions") { + await savePermission(input) + } else if (input.type === "ERC20") { + await saveWatchedAsset(input) + } + + notifyUpdates({ + event: "config.changed", + data: { + destination: input.user, + resourceId: input.id, + updatedAt: input.updatedAt, + }, + id: createUUID(), + }) + + return ok(undefined) +} diff --git a/apps/sync-service/src/handlers/createConfig/index.ts b/apps/sync-service/src/handlers/createConfig/index.ts new file mode 100644 index 0000000000..cb792f3b13 --- /dev/null +++ b/apps/sync-service/src/handlers/createConfig/index.ts @@ -0,0 +1,2 @@ +export { createConfig } from "./createConfig" +export { createConfigValidation, createConfigDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/createConfig/types.ts b/apps/sync-service/src/handlers/createConfig/types.ts new file mode 100644 index 0000000000..ded661974c --- /dev/null +++ b/apps/sync-service/src/handlers/createConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type CreateConfigInput = z.infer +export type CreateConfigOutput = z.infer diff --git a/apps/sync-service/src/handlers/createConfig/validation.ts b/apps/sync-service/src/handlers/createConfig/validation.ts new file mode 100644 index 0000000000..344fe75c6c --- /dev/null +++ b/apps/sync-service/src/handlers/createConfig/validation.ts @@ -0,0 +1,31 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { walletPermission, watchAsset } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission, typeof watchAsset]> = + z.discriminatedUnion("type", [walletPermission, watchAsset]) + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const createConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Create a new config", + responses: { + 201: { + description: "Config created", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const createConfigValidation = zv("json", inputSchema) diff --git a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts new file mode 100644 index 0000000000..66e35800e3 --- /dev/null +++ b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts @@ -0,0 +1,34 @@ +import { type Address, createUUID } from "@happy.tech/common" +import { type Result, err, ok } from "neverthrow" +import { deletePermission, getPermission } from "../../repositories/permissionsRepository" +import { deleteWatchedAsset, getWatchedAsset } from "../../repositories/watchAssetsRepository" +import { notifyUpdates } from "../../services/notifyUpdates" +import type { DeleteConfigInput } from "./types" + +export async function deleteConfig(input: DeleteConfigInput): Promise> { + const permission = await getPermission(input.id) + const watchedAsset = await getWatchedAsset(input.id) + + let user: Address + if (permission) { + await deletePermission(input.id) + user = permission.user + } else if (watchedAsset) { + await deleteWatchedAsset(input.id) + user = watchedAsset.user + } else { + return err(new Error("Config not found")) + } + + notifyUpdates({ + event: "config.changed", + data: { + destination: user, + resourceId: input.id, + updatedAt: Date.now(), + }, + id: createUUID(), + }) + + return ok(undefined) +} diff --git a/apps/sync-service/src/handlers/deleteConfig/index.ts b/apps/sync-service/src/handlers/deleteConfig/index.ts new file mode 100644 index 0000000000..70a85d4d95 --- /dev/null +++ b/apps/sync-service/src/handlers/deleteConfig/index.ts @@ -0,0 +1,2 @@ +export { deleteConfig } from "./deleteConfig" +export { deleteConfigValidation, deleteConfigDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/deleteConfig/types.ts b/apps/sync-service/src/handlers/deleteConfig/types.ts new file mode 100644 index 0000000000..08f309b034 --- /dev/null +++ b/apps/sync-service/src/handlers/deleteConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type DeleteConfigInput = z.infer +export type DeleteConfigOutput = z.infer diff --git a/apps/sync-service/src/handlers/deleteConfig/validation.ts b/apps/sync-service/src/handlers/deleteConfig/validation.ts new file mode 100644 index 0000000000..e3341f73b6 --- /dev/null +++ b/apps/sync-service/src/handlers/deleteConfig/validation.ts @@ -0,0 +1,35 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { isProduction } from "../../utils/isProduction" + +export const deleteConfigSchema = z + .object({ + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + }) + .strict() + +export const inputSchema = deleteConfigSchema + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const deleteConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Delete config", + responses: { + 200: { + description: "Config deleted", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const deleteConfigValidation = zv("json", inputSchema) diff --git a/apps/sync-service/src/handlers/listConfig/index.ts b/apps/sync-service/src/handlers/listConfig/index.ts new file mode 100644 index 0000000000..fc44eb2f67 --- /dev/null +++ b/apps/sync-service/src/handlers/listConfig/index.ts @@ -0,0 +1,2 @@ +export { listConfig } from "./listConfig" +export { listConfigValidation, listConfigDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/listConfig/listConfig.ts b/apps/sync-service/src/handlers/listConfig/listConfig.ts new file mode 100644 index 0000000000..91b47c985d --- /dev/null +++ b/apps/sync-service/src/handlers/listConfig/listConfig.ts @@ -0,0 +1,19 @@ +import { type Result, ok } from "neverthrow" +import type { WalletPermission, WatchAsset } from "../../dtos" +import { listPermissions } from "../../repositories/permissionsRepository" +import { listWatchedAssets } from "../../repositories/watchAssetsRepository" +import type { ListConfigInput } from "./types" + +export async function listConfig(input: ListConfigInput): Promise> { + const config: (WalletPermission | WatchAsset)[] = [] + if (input.type === "WalletPermissions" || input.type === undefined) { + const permissions = await listPermissions(input.user, input.lastUpdated) + config.push(...permissions) + } + if (input.type === "ERC20" || input.type === undefined) { + const watchedAssets = await listWatchedAssets(input.user, input.lastUpdated) + config.push(...watchedAssets) + } + + return ok(config) +} diff --git a/apps/sync-service/src/handlers/listConfig/types.ts b/apps/sync-service/src/handlers/listConfig/types.ts new file mode 100644 index 0000000000..c8033128ce --- /dev/null +++ b/apps/sync-service/src/handlers/listConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type ListConfigInput = z.infer +export type ListConfigOutput = z.infer diff --git a/apps/sync-service/src/handlers/listConfig/validation.ts b/apps/sync-service/src/handlers/listConfig/validation.ts new file mode 100644 index 0000000000..767a4ef6dd --- /dev/null +++ b/apps/sync-service/src/handlers/listConfig/validation.ts @@ -0,0 +1,51 @@ +import { isAddress } from "@happy.tech/common" +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { checksum } from "ox/Address" +import { z } from "zod" +import { walletPermission, watchAsset } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const listConfigSchema = z + .object({ + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + lastUpdated: z + .string() + .optional() + .transform((val) => (val ? Number.parseInt(val) : undefined)) + .openapi({ + example: "1715702400", + type: "number", + }), + type: z.enum(["WalletPermissions", "ERC20"]).optional(), + }) + .strict() + +export const inputSchema = listConfigSchema + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), + data: z.array(z.discriminatedUnion("type", [walletPermission, watchAsset])), +}) + +export const listConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "List configs", + responses: { + 200: { + description: "Configs listed", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const listConfigValidation = zv("query", inputSchema) diff --git a/apps/sync-service/src/handlers/subscribe/index.ts b/apps/sync-service/src/handlers/subscribe/index.ts new file mode 100644 index 0000000000..26760afc10 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/index.ts @@ -0,0 +1,2 @@ +export { subscribe } from "./subscribe" +export { subscribeValidation, subscribeDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/subscribe/subscribe.ts b/apps/sync-service/src/handlers/subscribe/subscribe.ts new file mode 100644 index 0000000000..cbc422ede7 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/subscribe.ts @@ -0,0 +1,16 @@ +import { promiseWithResolvers } from "@happy.tech/common" +import type { SSEStreamingApi } from "hono/streaming" +import { saveStream } from "../../services/notifyUpdates" +import type { SubscribeInput } from "./types" + +export async function subscribe(input: SubscribeInput, stream: SSEStreamingApi) { + const { promise, reject } = promiseWithResolvers() + + stream.onAbort(() => { + reject(undefined) + }) + + saveStream(input.user, stream) + + await promise +} diff --git a/apps/sync-service/src/handlers/subscribe/types.ts b/apps/sync-service/src/handlers/subscribe/types.ts new file mode 100644 index 0000000000..f198260bae --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/types.ts @@ -0,0 +1,4 @@ +import type { z } from "zod" +import type { inputSchema } from "./validation" + +export type SubscribeInput = z.infer diff --git a/apps/sync-service/src/handlers/subscribe/validation.ts b/apps/sync-service/src/handlers/subscribe/validation.ts new file mode 100644 index 0000000000..46ffe0f981 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/validation.ts @@ -0,0 +1,24 @@ +import { isAddress } from "@happy.tech/common" +import { describeRoute } from "hono-openapi" +import { validator as zv } from "hono-openapi/zod" +import { checksum } from "ox/Address" +import { z } from "zod" +import { isProduction } from "../../utils/isProduction" + +export const subscribeSchema = z + .object({ + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + }) + .strict() + +export const inputSchema = subscribeSchema + +export const subscribeDescription = describeRoute({ + validateResponse: !isProduction, + description: "Subscribe to config updates", +}) + +export const subscribeValidation = zv("query", inputSchema) diff --git a/apps/sync-service/src/handlers/updateConfig/index.ts b/apps/sync-service/src/handlers/updateConfig/index.ts new file mode 100644 index 0000000000..a1ed789e59 --- /dev/null +++ b/apps/sync-service/src/handlers/updateConfig/index.ts @@ -0,0 +1,2 @@ +export { updateConfig } from "./updateConfig" +export { updateConfigValidation, updateConfigDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/updateConfig/types.ts b/apps/sync-service/src/handlers/updateConfig/types.ts new file mode 100644 index 0000000000..1bbb59c205 --- /dev/null +++ b/apps/sync-service/src/handlers/updateConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type UpdateConfigInput = z.infer +export type UpdateConfigOutput = z.infer diff --git a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts new file mode 100644 index 0000000000..6ae2753217 --- /dev/null +++ b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts @@ -0,0 +1,25 @@ +import { type Result, ok } from "neverthrow" +import { savePermission } from "../../repositories/permissionsRepository" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" +import { notifyUpdates } from "../../services/notifyUpdates" +import type { UpdateConfigInput } from "./types" + +export async function updateConfig(input: UpdateConfigInput): Promise> { + if (input.type === "WalletPermissions") { + await savePermission(input) + } else if (input.type === "ERC20") { + await saveWatchedAsset(input) + } + + notifyUpdates({ + event: "config.changed", + data: { + destination: input.user, + resourceId: input.id, + updatedAt: Date.now(), + }, + id: input.id, + }) + + return ok(undefined) +} diff --git a/apps/sync-service/src/handlers/updateConfig/validation.ts b/apps/sync-service/src/handlers/updateConfig/validation.ts new file mode 100644 index 0000000000..971ddab2d8 --- /dev/null +++ b/apps/sync-service/src/handlers/updateConfig/validation.ts @@ -0,0 +1,31 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { walletPermissionUpdate, watchAssetUpdate } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate, typeof watchAssetUpdate]> = + z.discriminatedUnion("type", [walletPermissionUpdate, watchAssetUpdate]) + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const updateConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Update config", + responses: { + 200: { + description: "Config updated", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const updateConfigValidation = zv("json", inputSchema) diff --git a/apps/sync-service/src/index.ts b/apps/sync-service/src/index.ts new file mode 100644 index 0000000000..350b0475d6 --- /dev/null +++ b/apps/sync-service/src/index.ts @@ -0,0 +1,11 @@ +import { env } from "./env" +import { app } from "./server" +import type { AppType } from "./server" + +export type { AppType } + +export default { + port: env.APP_PORT, + fetch: app.fetch, + idleTimeout: 0, +} diff --git a/apps/sync-service/src/migrate.ts b/apps/sync-service/src/migrate.ts new file mode 100644 index 0000000000..b7e7f4444c --- /dev/null +++ b/apps/sync-service/src/migrate.ts @@ -0,0 +1,38 @@ +import { type Migration, type MigrationProvider, Migrator } from "kysely" +import { db } from "./db/driver" +import { migrations } from "./db/migrations" + +class ObjectMigrationProvider implements MigrationProvider { + constructor(private migrations: Record) {} + + async getMigrations(): Promise> { + return this.migrations + } +} + +async function migrateToLatest() { + const migrator = new Migrator({ + db, + provider: new ObjectMigrationProvider(migrations), + }) + + const { error, results } = await migrator.migrateToLatest() + + results?.forEach((it) => { + if (it.status === "Success") { + console.log(`migration "${it.migrationName}" was executed successfully`) + } else if (it.status === "Error") { + console.error(`failed to execute migration "${it.migrationName}"`) + } + }) + + if (error) { + console.error("failed to migrate") + console.error(error) + process.exit(1) + } + + await db.destroy() +} + +migrateToLatest() diff --git a/apps/sync-service/src/repositories/permissionsRepository.ts b/apps/sync-service/src/repositories/permissionsRepository.ts new file mode 100644 index 0000000000..1b18b1235c --- /dev/null +++ b/apps/sync-service/src/repositories/permissionsRepository.ts @@ -0,0 +1,61 @@ +import type { Hex } from "@happy.tech/common" +import type { Insertable, Selectable } from "kysely" +import { db } from "../db/driver" +import type { WalletPermissionRow } from "../db/types" +import type { WalletPermission, WalletPermissionUpdate } from "../dtos" + +function fromDtoToDbUpdate(permission: WalletPermissionUpdate): Partial> { + const { type, caveats, ...rest } = permission + return { + ...rest, + ...(caveats && { caveats: JSON.stringify(caveats) }), + updatedAt: Date.now(), + } +} + +function fromDbToDto(permission: Selectable): WalletPermission { + return { + type: "WalletPermissions", + ...permission, + deleted: permission.deleted === 1, + } +} + +export function getPermission(id: string) { + return db.selectFrom("walletPermissions").where("id", "=", id).selectAll().executeTakeFirst() +} + +export async function listPermissions(user: Hex, lastUpdated?: number): Promise { + const result = await db + .selectFrom("walletPermissions") + .where("user", "=", user) + .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) + .selectAll() + .execute() + + return result.map(fromDbToDto) +} + +export async function savePermission(permission: WalletPermissionUpdate) { + const existing = await getPermission(permission.id) + if (existing) { + return await db + .updateTable("walletPermissions") + .set(fromDtoToDbUpdate(permission)) + .where("id", "=", permission.id) + .execute() + } + + return await db + .insertInto("walletPermissions") + .values(fromDtoToDbUpdate(permission) as Insertable) + .execute() +} + +export async function deletePermission(id: string) { + return await db + .updateTable("walletPermissions") + .set({ deleted: true, updatedAt: Date.now() }) + .where("id", "=", id) + .execute() +} diff --git a/apps/sync-service/src/repositories/watchAssetsRepository.ts b/apps/sync-service/src/repositories/watchAssetsRepository.ts new file mode 100644 index 0000000000..6582d216f0 --- /dev/null +++ b/apps/sync-service/src/repositories/watchAssetsRepository.ts @@ -0,0 +1,69 @@ +import type { Hex } from "@happy.tech/common" +import type { Insertable, Selectable } from "kysely" +import { db } from "../db/driver" +import type { WatchAssetRow } from "../db/types" +import type { WatchAsset, WatchAssetUpdate } from "../dtos" + +function fromDtoToDbUpdate(watchedAsset: WatchAssetUpdate): Partial> { + const { options, ...rest } = watchedAsset + return { + ...rest, + ...(options ?? {}), + updatedAt: Date.now(), + } +} + +function fromDbToDto(watchedAsset: Selectable): WatchAsset { + return { + type: "ERC20", + options: { + symbol: watchedAsset.symbol, + address: watchedAsset.address, + decimals: watchedAsset.decimals, + image: watchedAsset.image ?? undefined, + }, + user: watchedAsset.user, + id: watchedAsset.id, + updatedAt: watchedAsset.updatedAt, + createdAt: watchedAsset.createdAt, + deleted: watchedAsset.deleted === 1, + } +} + +export function getWatchedAsset(id: string) { + return db.selectFrom("watchedAssets").where("id", "=", id).selectAll().executeTakeFirst() +} + +export async function listWatchedAssets(user: Hex, lastUpdated?: number): Promise { + const result = await db + .selectFrom("watchedAssets") + .where("user", "=", user) + .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) + .selectAll() + .execute() + return result.map(fromDbToDto) +} + +export async function saveWatchedAsset(watchedAsset: WatchAssetUpdate) { + const existing = await getWatchedAsset(watchedAsset.id) + if (existing) { + return await db + .updateTable("watchedAssets") + .set(fromDtoToDbUpdate(watchedAsset)) + .where("id", "=", watchedAsset.id) + .execute() + } + + return await db + .insertInto("watchedAssets") + .values(fromDtoToDbUpdate(watchedAsset) as Insertable) + .execute() +} + +export async function deleteWatchedAsset(id: string) { + return await db + .updateTable("watchedAssets") + .set({ deleted: true, updatedAt: Date.now() }) + .where("id", "=", id) + .execute() +} diff --git a/apps/sync-service/src/server/configRoute.ts b/apps/sync-service/src/server/configRoute.ts new file mode 100644 index 0000000000..f19b5dd54a --- /dev/null +++ b/apps/sync-service/src/server/configRoute.ts @@ -0,0 +1,49 @@ +import { Hono } from "hono" +import { streamSSE } from "hono/streaming" +import { createConfig } from "../handlers/createConfig/createConfig" +import { createConfigDescription, createConfigValidation } from "../handlers/createConfig/validation" +import { deleteConfig } from "../handlers/deleteConfig/deleteConfig" +import { deleteConfigDescription, deleteConfigValidation } from "../handlers/deleteConfig/validation" +import { listConfig, listConfigDescription, listConfigValidation } from "../handlers/listConfig" +import { subscribe } from "../handlers/subscribe/subscribe" +import { subscribeDescription, subscribeValidation } from "../handlers/subscribe/validation" +import { updateConfig } from "../handlers/updateConfig/updateConfig" +import { updateConfigDescription, updateConfigValidation } from "../handlers/updateConfig/validation" +import { makeResponse } from "./makeResponse" + +export default new Hono() + .post("/create", createConfigDescription, createConfigValidation, async (c) => { + const input = c.req.valid("json") + const result = await createConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) + .get("/list", listConfigDescription, listConfigValidation, async (c) => { + const input = c.req.valid("query") + const output = await listConfig(input) + + const [response, code] = makeResponse(output) + return c.json(response, code) + }) + .put("/update", updateConfigDescription, updateConfigValidation, async (c) => { + const input = c.req.valid("json") + const result = await updateConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) + .delete("/delete", deleteConfigDescription, deleteConfigValidation, async (c) => { + const input = c.req.valid("json") + const result = await deleteConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) + .get("/subscribe", subscribeDescription, subscribeValidation, async (c) => { + c.header("Access-Control-Allow-Origin", "*") + const input = c.req.valid("query") + return streamSSE(c, async (s) => { + await subscribe(input, s) + }) + }) diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts new file mode 100644 index 0000000000..bab1f8c5ac --- /dev/null +++ b/apps/sync-service/src/server/index.ts @@ -0,0 +1,101 @@ +import "zod-openapi/extend" +import { apiReference } from "@scalar/hono-api-reference" +import { Hono } from "hono" +import { openAPISpecs } from "hono-openapi" +import { cors } from "hono/cors" +import { HTTPException } from "hono/http-exception" +import { logger as loggerMiddleware } from "hono/logger" +import { prettyJSON as prettyJSONMiddleware } from "hono/pretty-json" +import { requestId as requestIdMiddleware } from "hono/request-id" +import { timing as timingMiddleware } from "hono/timing" +import { ZodError } from "zod" +import pkg from "../../package.json" assert { type: "json" } +import { env } from "../env" +import { isProduction } from "../utils/isProduction" +import { logger } from "../utils/logger" +import { logJSONResponseMiddleware } from "../utils/logger" +import configRoute from "./configRoute" + +const app = new Hono() + +// Middleware setup +app.use( + "*", + cors({ + origin: "*", + }), +) +app.use("*", timingMiddleware()) +app.use("*", logJSONResponseMiddleware) +app.use("*", prettyJSONMiddleware()) +app.use("*", requestIdMiddleware()) +app.use("*", loggerMiddleware()) + +// Routes setup +app.get("/", (c) => c.text("Welcome to the Settings Service!")) + +// OpenAPI documentation +app.get( + "/docs/openapi.json", + openAPISpecs(app, { + documentation: { + info: { title: "Settings", version: pkg.version, description: "Settings API" }, + servers: [ + ...(env.NODE_ENV === "development" + ? [ + { + url: `http://localhost:${env.APP_PORT}`, + description: "Local", + }, + ] + : []), + { url: "https://sync-staging.happy.tech", description: "Staging" }, + { url: "https://sync.happy.tech", description: "Production" }, + ], + }, + }), +) + +// API Reference UI +app.get( + "/docs", + apiReference({ + pageTitle: "Settings API Reference - HappyChain", + theme: "kepler", + spec: { url: "/docs/openapi.json" }, + showSidebar: true, + hideSearch: false, + }), +) + +app.notFound((c) => c.text("These aren't the droids you're looking for", 404)) +app.onError(async (err, c) => { + // re-format input validation errors + if (err instanceof HTTPException && err.cause instanceof ZodError) { + const error = err.cause.issues.map((i) => ({ path: i.path.join("."), message: i.message })) + return c.json({ error, requestId: c.get("requestId"), url: c.req.url }, 422) + } + + logger.warn({ requestId: c.get("requestId"), url: c.req.url }, err) + + // standard hono exceptions + // https://hono.dev/docs/api/exception#handling-httpexception + if (err instanceof HTTPException) return err.getResponse() + + // Unhandled Exceptions - should not occur + return c.json( + { + error: isProduction + ? `Something Happened, file a report with this key to find out more: ${c.get("requestId")}` + : err.message, + requestId: c.get("requestId"), + url: c.req.url, + }, + 500, + ) +}) + +app.route("/api/v1/settings", configRoute) + +export type AppType = typeof app +export { app } diff --git a/apps/sync-service/src/server/makeResponse.ts b/apps/sync-service/src/server/makeResponse.ts new file mode 100644 index 0000000000..bf7d472209 --- /dev/null +++ b/apps/sync-service/src/server/makeResponse.ts @@ -0,0 +1,45 @@ +import type { ContentfulStatusCode } from "hono/utils/http-status" +import type { Result } from "neverthrow" +import { HappySettingsError } from "../errors" + +type ResponseBodySuccess = { + success: true + message?: string + data?: TOk +} + +type ResponseBodyError = { + success: false + message: string +} + +type ResponseBody = ResponseBodySuccess | ResponseBodyError + +export function makeResponse(output: Result): [ResponseBody, ContentfulStatusCode] { + if (output.isOk()) + return [ + { + success: true, + ...(output.value !== undefined ? { data: output.value } : {}), + }, + 200, + ] as const + + if (output.error instanceof HappySettingsError) { + return [ + { + success: false, + message: output.error.message, + }, + output.error.statusCode, + ] as const + } + + return [ + { + success: false, + message: "Unexpected error", + }, + 500, + ] as const +} diff --git a/apps/sync-service/src/services/notifyUpdates.ts b/apps/sync-service/src/services/notifyUpdates.ts new file mode 100644 index 0000000000..daccbb543d --- /dev/null +++ b/apps/sync-service/src/services/notifyUpdates.ts @@ -0,0 +1,33 @@ +import type { Address } from "@happy.tech/common" +import type { SSEStreamingApi } from "hono/streaming" +import type { ConfigChangedEvent } from "../dtos" + +const streams = new Map() + +export function notifyUpdates(event: ConfigChangedEvent) { + const userStreams = streams.get(event.data.destination) + if (!userStreams) { + return + } + + for (const stream of userStreams) { + stream.writeSSE({ + data: JSON.stringify(event.data), + event: event.event, + id: event.id, + }) + } +} + +export function getStream(address: Address) { + return streams.get(address) +} + +export function saveStream(address: Address, stream: SSEStreamingApi) { + const userStreams = streams.get(address) + if (!userStreams) { + streams.set(address, [stream]) + } else { + userStreams.push(stream) + } +} diff --git a/apps/sync-service/src/utils/isAppUrl.ts b/apps/sync-service/src/utils/isAppUrl.ts new file mode 100644 index 0000000000..e1182adbf6 --- /dev/null +++ b/apps/sync-service/src/utils/isAppUrl.ts @@ -0,0 +1,10 @@ +import type { AppURL } from "../db/types" + +export function isAppUrl(urlString: string): urlString is AppURL { + try { + const url = new URL(urlString) + return url.protocol === "http:" || url.protocol === "https:" + } catch { + return false + } +} diff --git a/apps/sync-service/src/utils/isProduction.ts b/apps/sync-service/src/utils/isProduction.ts new file mode 100644 index 0000000000..9c9b3ec318 --- /dev/null +++ b/apps/sync-service/src/utils/isProduction.ts @@ -0,0 +1,3 @@ +import { env } from "../env" + +export const isProduction = ["staging", "production"].includes(env.NODE_ENV) diff --git a/apps/sync-service/src/utils/isUUID.ts b/apps/sync-service/src/utils/isUUID.ts new file mode 100644 index 0000000000..7043584976 --- /dev/null +++ b/apps/sync-service/src/utils/isUUID.ts @@ -0,0 +1,6 @@ +import type { UUID } from "@happy.tech/common" +import { validate, version } from "uuid" + +export function isUUID(str: string): str is UUID { + return validate(str) && version(str) === 4 +} diff --git a/apps/sync-service/src/utils/logger.ts b/apps/sync-service/src/utils/logger.ts new file mode 100644 index 0000000000..05ba4280ab --- /dev/null +++ b/apps/sync-service/src/utils/logger.ts @@ -0,0 +1,28 @@ +import { LogLevel, Logger, logLevel } from "@happy.tech/common" +import { createMiddleware } from "hono/factory" +import { env } from "../env" + +const defaultLogLevel = logLevel(env.LOG_LEVEL) +Logger.instance.setLogLevel(defaultLogLevel) + +export const logger = Logger.create("SettingsService") +const responseLogger = Logger.create("Response", { + level: LogLevel.TRACE, +}) + +export const logJSONResponseMiddleware = createMiddleware(async (c, next) => { + await next() + + if (LogLevel.TRACE > responseLogger.logLevel) return + if (!c.req.path.startsWith("/api")) return + if (c.req.path.includes("/api/v1/settings/subscribe")) return + try { + responseLogger.trace(c.res.status, await c.res.clone().json()) + } catch (e) { + responseLogger.error("failed to parse response:", { + error: (e as Error)?.message, + requestId: c.get("requestId"), + url: c.req.url, + }) + } +}) diff --git a/apps/sync-service/tsconfig.build.json b/apps/sync-service/tsconfig.build.json new file mode 100644 index 0000000000..7a86a94de4 --- /dev/null +++ b/apps/sync-service/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../support/configs/tsconfig.base.json", "../../support/configs/tsconfig.types.json"], + "include": ["src", "./package.json"] +} diff --git a/apps/sync-service/tsconfig.json b/apps/sync-service/tsconfig.json new file mode 100644 index 0000000000..fe56c15ad1 --- /dev/null +++ b/apps/sync-service/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../support/configs/tsconfig.base.json"], + "include": ["*.ts", "src", "./package.json"] +} diff --git a/bun.lock b/bun.lock index 5495b015b4..f56bfbc624 100644 --- a/bun.lock +++ b/bun.lock @@ -79,6 +79,7 @@ "@happy.tech/common": "workspace:*", "@happy.tech/contracts": "workspace:0.2.0", "@happy.tech/wallet-common": "workspace:*", + "@legendapp/state": "^3.0.0-beta.30", "@metamask/safe-event-emitter": "^3.1.1", "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.56.2", @@ -213,6 +214,25 @@ "typescript": "^5.6.2", }, }, + "apps/sync-service": { + "name": "@happy.tech/sync-service", + "version": "0.1.0", + "dependencies": { + "@happy.tech/common": "workspace:1.0.0", + "@hono/node-server": "^1.13.8", + "@scalar/hono-api-reference": "^0.5.175", + "hono": "^4.7.2", + "hono-openapi": "^0.4.4", + "neverthrow": "^8.1.0", + "zod": "^3.23.8", + "zod-openapi": "^4.2.3", + }, + "devDependencies": { + "@happy.tech/happybuild": "workspace:1.0.0", + "hono-openapi": "^0.4.4", + "typescript": "^5.6.2", + }, + }, "contracts": { "name": "@happy.tech/contracts", "version": "0.2.0", @@ -410,6 +430,7 @@ "version": "1.0.0", "dependencies": { "@opentelemetry/api": "^1.9.0", + "uuid": "^11.1.0", }, "devDependencies": { "@happy.tech/configs": "workspace:*", @@ -939,6 +960,8 @@ "@happy.tech/submitter": ["@happy.tech/submitter@workspace:apps/submitter"], + "@happy.tech/sync-service": ["@happy.tech/sync-service@workspace:apps/sync-service"], + "@happy.tech/testing": ["@happy.tech/testing@workspace:support/testing"], "@happy.tech/txm": ["@happy.tech/txm@workspace:packages/txm"], @@ -995,12 +1018,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@kevincharm/bls-bn254": ["@kevincharm/bls-bn254@2.0.0", "", { "peerDependencies": { "ethers": "^6.8.0", "mcl-wasm": "^1.4.0" } }, "sha512-Y6Jk8oE6Re4v3rDkb51mF3rHfDakxCTGptVr/LX2iwtbA7qu3DLNBNgdBU3heYFA8t22JiX3BgnMTbBgm3AZMA=="], + "@legendapp/state": ["@legendapp/state@3.0.0-beta.31", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "expo-sqlite": "^15.0.0" }, "optionalPeers": ["expo-sqlite"] }, "sha512-ejQHeBk3DEHOeF/j4/nX0W6uXiGQBOqQ5Ftfk10ReDpGzeC/GxF3Or31LG5qPcp3ZZweUBiVyqZqEtQefX4eKA=="], + "@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="], "@lezer/css": ["@lezer/css@1.2.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg=="], @@ -1421,45 +1444,45 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.0", "", { "os": "android", "cpu": "arm" }, "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ=="], + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -1571,27 +1594,27 @@ "@solidity-parser/parser": ["@solidity-parser/parser@0.20.1", "", {}, "sha512-58I2sRpzaQUN+jJmWbHfbWf9AKfzqCI8JAdFB0vbyY+u8tBRcuTt9LxzasvR0LGQpcRv97eyV7l61FQ3Ib7zVw=="], - "@swc/core": ["@swc/core@1.12.6", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.6", "@swc/core-darwin-x64": "1.12.6", "@swc/core-linux-arm-gnueabihf": "1.12.6", "@swc/core-linux-arm64-gnu": "1.12.6", "@swc/core-linux-arm64-musl": "1.12.6", "@swc/core-linux-x64-gnu": "1.12.6", "@swc/core-linux-x64-musl": "1.12.6", "@swc/core-win32-arm64-msvc": "1.12.6", "@swc/core-win32-ia32-msvc": "1.12.6", "@swc/core-win32-x64-msvc": "1.12.6" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-TEpta6Gi02X1b2yDIzBOIr7dFprvq9jD8RbEVI2OcMrwklbCUx0Dz9TrAnklSOwRvYvH5JjCx8ht9E94oWiG7A=="], + "@swc/core": ["@swc/core@1.12.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.7", "@swc/core-darwin-x64": "1.12.7", "@swc/core-linux-arm-gnueabihf": "1.12.7", "@swc/core-linux-arm64-gnu": "1.12.7", "@swc/core-linux-arm64-musl": "1.12.7", "@swc/core-linux-x64-gnu": "1.12.7", "@swc/core-linux-x64-musl": "1.12.7", "@swc/core-win32-arm64-msvc": "1.12.7", "@swc/core-win32-ia32-msvc": "1.12.7", "@swc/core-win32-x64-msvc": "1.12.7" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA=="], - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yLiw+XzG+MilfFh0ON7qt67bfIr7UxB9JprhYReVOmLTBDmDVQSC3T4/vIuc+GwlX08ydnHy0ud4lIjTNW4uWg=="], + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA=="], - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-qwg8ux5x5Gd1LmSUtL4s9mbyfzAjr5M6OtjO281dKHwc/GYiSc4j1urb2jNSo9FcMkfT78oAOW2L6HQiWv+j1A=="], + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-jN6LhFfGOpm4DY2mXPgwH4aa9GLOwublwMVFFZ/bGnHYYCRitLZs9+JWBbyWs7MyGcA246Ew+EREx36KVEAxjA=="], - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.6", "", { "os": "linux", "cpu": "arm" }, "sha512-pnkqH59JXBZu+MedaykMAC2or7tlUKeya7GKjzub+hkwxBP0ywWoFd+QYEdzp7QSziOt1VIHc4Wb9iZ2EfnzmA=="], + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.7", "", { "os": "linux", "cpu": "arm" }, "sha512-rHn8XXi7G2StEtZRAeJ6c7nhJPDnqsHXmeNrAaYwk8Tvpa6ZYG2nT9E1OQNXj1/dfbSFTjdiA8M8ZvGYBlpBoA=="], - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-h8+Ltx0NSEzIFHetkOYoQ+UQ59unYLuJ4wF6kCpxzS4HskRLjcngr1HgN0F/PRpptnrmJUPVQmfms/vjN8ndAQ=="], + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw=="], - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-GZu3MnB/5qtBxKEH46hgVDaplEe4mp3ZmQ1O2UpFCv/u/Ji3Gar5w5g2wHCZoT5AOouAhP1bh7IAEqjG/fbVfg=="], + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-jxyINtBezpxd3eIUDiDXv7UQ87YWlPsM9KumOwJk09FkFSO4oYxV2RT+Wu+Nt5tVWue4N0MdXT/p7SQsDEk4YA=="], - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-WwJLQFzMW9ufVjM6k3le4HUgBFNunyt2oghjcgn2YjnKj0Ka2LrrBHCxfS7lgFSCQh/shib2wIlKXUnlTEWQJw=="], + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.7", "", { "os": "linux", "cpu": "x64" }, "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw=="], - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-rVGPNpI/sm8VVAhnB09Z/23OJP3ymouv6F4z4aYDbq/2JIwxqgpnl8gtMYP+Jw3XqabaFNjQmPiL15TvKCQaxQ=="], + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.7", "", { "os": "linux", "cpu": "x64" }, "sha512-zy7JWfQtQItgMfUjSbbcS3DZqQUn2d9VuV0LSGpJxtTXwgzhRpF1S2Sj7cU9hGpbM27Y8RJ4DeFb3qbAufjbrw=="], - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-EKDJ1+8vaIlJGMl2yvd2HklV4GNbpKKwNQcUQid6j91tFYz4/aByw+9vh/sDVG7ZNqdmdywSnLRo317UTt0zFg=="], + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-52PeF0tyX04ZFD8nibNhy/GjMFOZWTEWPmIB3wpD1vIJ1po+smtBnEdRRll5WIXITKoiND8AeHlBNBPqcsdcwA=="], - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-jnULikZkR2fpZgFUQs7NsNIztavM1JdX+8Y+8FsfChBvMvziKxXtvUPGjeVJ8nzU1wgMnaeilJX9vrwuDGkA0Q=="], + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-WzQwkNMuhB1qQShT9uUgz/mX2j7NIEPExEtzvGsBT7TlZ9j1kGZ8NJcZH/fwOFcSJL4W7DnkL7nAhx6DBlSPaA=="], - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.6", "", { "os": "win32", "cpu": "x64" }, "sha512-jL2Dcdcc/QZiQnwByP1uIE4k/mTlapzUng7owtLD2tSBBi1d+jPIdXIefdv+nccYJKRA+lKG3rRB6Tk9GrC7Kg=="], + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.7", "", { "os": "win32", "cpu": "x64" }, "sha512-R52ivBi2lgjl+Bd3XCPum0YfgbZq/W1AUExITysddP9ErsNSwnreYyNB3exEijiazWGcqHEas2ChiuMOP7NYrA=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], @@ -1643,15 +1666,15 @@ "@tanstack/react-store": ["@tanstack/react-store@0.7.1", "", { "dependencies": { "@tanstack/store": "0.7.1", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA=="], - "@tanstack/router-cli": ["@tanstack/router-cli@1.121.34", "", { "dependencies": { "@tanstack/router-generator": "^1.121.34", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-OkBZ+n58wcXU45m1b0f58Uce0mkEoNav+ZeHwrPQWfvw3TQdc9cmH57Kss01YgeOBgCbTaPyAiPJltPl05LjmA=="], + "@tanstack/router-cli": ["@tanstack/router-cli@1.121.37", "", { "dependencies": { "@tanstack/router-generator": "^1.121.37", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-Jc/YIBPBGgKt10wqquWMR3dntbUWSlhXaGCYFRb31SM+zRl+NSyOIhEO5zAm0oP4l605yyejs9gGj8BC9idn0Q=="], "@tanstack/router-core": ["@tanstack/router-core@1.121.34", "", { "dependencies": { "@tanstack/history": "1.121.34", "@tanstack/store": "^0.7.0", "tiny-invariant": "^1.3.3" } }, "sha512-CRH9dC8uLfFOKUGTbtOcMPv+weNVt2xs+me34KLX0Yja2yHG99oAUCBwamXsVQPpfjLFPYeJuKyo98+Mg+Ppeg=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.121.34", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.5" }, "peerDependencies": { "@tanstack/router-core": "^1.121.34", "csstype": "^3.0.10", "tiny-invariant": "^1.3.3" }, "optionalPeers": ["csstype"] }, "sha512-WAFYxJ7qViKxqkFmf+VsrtMT4TfYqdfWTBRhVU/6qi0k/+7TO2EHjl8/aGBhg6q0/IwO9wyGvcbDhJxm0DwWag=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.121.34", "", { "dependencies": { "@tanstack/router-core": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-JmxlhK8f7LIxHV8BAHikeiYGfwM9p5nxbEMpujNgTmC0dBwSyes+Zm0DzEL0EotVXZy+CyI/9bVa7z+9nWvqlA=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.121.37", "", { "dependencies": { "@tanstack/router-core": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-d7IqEDf962uJFNPMWXfPr+kUpS3Cv72azZhBNMMVmZUox/h3VDGgQ6OUnWXHwnno4xqDoS/mx9huTUnItoewaw=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.121.34", "", { "dependencies": { "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/router-core": "^1.121.34", "@tanstack/router-generator": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "babel-dead-code-elimination": "^1.0.10", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.121.34", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-ZmX/tkdd/ZKLdr17ewKJTTBGkXQDeOfQKSCuuEW5IjiNfWjT5gx8rQDvcYUSRcZdpUZ0LvDBxJUI74oHQ3sAiw=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.121.37", "", { "dependencies": { "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/router-core": "^1.121.34", "@tanstack/router-generator": "^1.121.37", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "babel-dead-code-elimination": "^1.0.10", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.121.34", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-zrolQ1J53xDUdxdO6MLfvnpVINnkIfOnEDVeX3kwHKBGQ5zyGdbolVcVVrJIRYQS0SJoWesn8cf8j+z+u8nZtg=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.121.21", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2" } }, "sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A=="], @@ -1789,7 +1812,7 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/lodash": ["@types/lodash@4.17.18", "", {}, "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g=="], + "@types/lodash": ["@types/lodash@4.17.19", "", {}, "sha512-NYqRyg/hIQrYPT9lbOeYc3kIRabJDn/k4qQHIXUpx88CBDww2fD15Sg5kbXlW86zm2XEW4g0QxkTI3/Kfkc7xQ=="], "@types/lru-cache": ["@types/lru-cache@5.1.1", "", {}, "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="], @@ -1803,7 +1826,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="], + "@types/node": ["@types/node@22.15.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw=="], "@types/pbkdf2": ["@types/pbkdf2@3.1.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew=="], @@ -1895,11 +1918,11 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "@volar/language-core": ["@volar/language-core@2.4.14", "", { "dependencies": { "@volar/source-map": "2.4.14" } }, "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w=="], + "@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], - "@volar/source-map": ["@volar/source-map@2.4.14", "", {}, "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ=="], + "@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], - "@volar/typescript": ["@volar/typescript@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw=="], + "@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], "@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="], @@ -2301,7 +2324,7 @@ "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], - "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], + "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], @@ -2341,7 +2364,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001724", "", {}, "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="], "cbor": ["cbor@10.0.3", "", { "dependencies": { "nofilter": "^3.0.2" } }, "sha512-72Jnj81xMsqepqdcSdf2+fflz/UDsThOHy5hj2MW5F5xzHL8Oa0KQ6I6V9CwVUPxg5pf+W9xp6W2KilaRXWWtw=="], @@ -2609,7 +2632,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.172", "", {}, "sha512-fnKW9dGgmBfsebbYognQSv0CGGLFH1a5iV9EDYTBwmAQn+whbzHbLFlC+3XbHc8xaNtpO0etm8LOcRXs1qMRkQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.176", "", {}, "sha512-2nDK9orkm7M9ZZkjO3PjbEd3VUulQLyg5T9O3enJdFvUg46Hzd4DUvTvAuEgbdHYXyFsiG4A5sO9IzToMH1cDg=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -2633,7 +2656,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -2701,7 +2724,7 @@ "eslint-plugin-n": ["eslint-plugin-n@17.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "@typescript-eslint/utils": "^8.26.1", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "ignore": "^5.3.2", "minimatch": "^9.0.5", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.0", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw=="], "eslint-plugin-promise": ["eslint-plugin-promise@7.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA=="], @@ -3019,7 +3042,7 @@ "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], - "hono": ["hono@4.8.2", "", {}, "sha512-hM+1RIn9PK1I6SiTNS6/y7O1mvg88awYLFEuEtoiMtRyT3SD2iu9pSFgbBXT3b1Ua4IwzvSTLvwO0SEhDxCi4w=="], + "hono": ["hono@4.8.3", "", {}, "sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ=="], "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], @@ -3581,7 +3604,7 @@ "node-jq": ["node-jq@6.0.1", "", { "dependencies": { "is-valid-path": "^0.1.1", "strip-final-newline": "^2.0.0", "tar": "^7.4.0", "tempy": "^3.1.0", "zod": "^3.23.8" }, "bin": { "node-jq": "bin/jq" } }, "sha512-jt1H7i2c/BZUkid7O8uK4KWw5wDZgpsSHq8WAVv8SpedToUOpA6kAgoLnoAHmorAUGJTfICZlniKkMaEl28Uyw=="], - "node-mock-http": ["node-mock-http@1.0.0", "", {}, "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ=="], + "node-mock-http": ["node-mock-http@1.0.1", "", {}, "sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -3713,7 +3736,7 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], "pbkdf2": ["pbkdf2@3.1.3", "", { "dependencies": { "create-hash": "~1.1.3", "create-hmac": "^1.1.7", "ripemd160": "=2.0.1", "safe-buffer": "^5.2.1", "sha.js": "^2.4.11", "to-buffer": "^1.2.0" } }, "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA=="], @@ -3985,7 +4008,7 @@ "rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="], - "rollup": ["rollup@4.44.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.0", "@rollup/rollup-android-arm64": "4.44.0", "@rollup/rollup-darwin-arm64": "4.44.0", "@rollup/rollup-darwin-x64": "4.44.0", "@rollup/rollup-freebsd-arm64": "4.44.0", "@rollup/rollup-freebsd-x64": "4.44.0", "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", "@rollup/rollup-linux-arm-musleabihf": "4.44.0", "@rollup/rollup-linux-arm64-gnu": "4.44.0", "@rollup/rollup-linux-arm64-musl": "4.44.0", "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-musl": "4.44.0", "@rollup/rollup-linux-s390x-gnu": "4.44.0", "@rollup/rollup-linux-x64-gnu": "4.44.0", "@rollup/rollup-linux-x64-musl": "4.44.0", "@rollup/rollup-win32-arm64-msvc": "4.44.0", "@rollup/rollup-win32-ia32-msvc": "4.44.0", "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA=="], + "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -4393,7 +4416,7 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], @@ -4403,7 +4426,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], @@ -4611,6 +4634,10 @@ "@ethersproject/providers/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "@firebase/component/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@firebase/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@happy.tech/txm/kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -4655,6 +4682,10 @@ "@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], + "@metamask/sdk/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@metamask/sdk-communication-layer/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@microsoft/api-extractor/minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="], @@ -4669,18 +4700,8 @@ "@nomiclabs/hardhat-etherscan/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@opentelemetry/exporter-logs-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/otlp-grpc-exporter-base/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - "@radix-ui/react-use-is-hydrated/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - "@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], "@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.0", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg=="], @@ -4781,9 +4802,7 @@ "@tailwindcss/vite/tailwindcss": ["tailwindcss@4.0.7", "", {}, "sha512-yH5bPPyapavo7L+547h3c4jcBXcrKwybQRjwdEIVAd9iXRvy/3T1CC6XSQEgZtRySjKfqvo3Cc0ZF1DTheuIdA=="], - "@tanstack/react-store/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - - "@tanstack/router-generator/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "@tanstack/router-generator/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "@tanstack/router-generator/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -4799,7 +4818,7 @@ "@tkey/tss/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], - "@toruslabs/eslint-config-typescript/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "@toruslabs/eslint-config-typescript/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "@toruslabs/metadata-helpers/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], @@ -4879,6 +4898,8 @@ "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "boxen/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], @@ -4951,7 +4972,7 @@ "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - "eslint-plugin-prettier/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "eslint-plugin-prettier/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "eslint-plugin-tsdoc/@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.0", "", {}, "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA=="], @@ -4981,6 +5002,8 @@ "ethereumjs-wallet/aes-js": ["aes-js@3.1.2", "", {}, "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="], + "ethereumjs-wallet/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "ethers/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], "ethers/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], @@ -4995,6 +5018,8 @@ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "extension-port-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "find-replace/array-back": ["array-back@1.0.4", "", { "dependencies": { "typical": "^2.6.0" } }, "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw=="], @@ -5033,6 +5058,8 @@ "hardhat/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "hardhat/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "hardhat/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "hardhat-deploy/ethers": ["ethers@5.8.0", "", { "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.8.0", "@ethersproject/address": "5.8.0", "@ethersproject/base64": "5.8.0", "@ethersproject/basex": "5.8.0", "@ethersproject/bignumber": "5.8.0", "@ethersproject/bytes": "5.8.0", "@ethersproject/constants": "5.8.0", "@ethersproject/contracts": "5.8.0", "@ethersproject/hash": "5.8.0", "@ethersproject/hdnode": "5.8.0", "@ethersproject/json-wallets": "5.8.0", "@ethersproject/keccak256": "5.8.0", "@ethersproject/logger": "5.8.0", "@ethersproject/networks": "5.8.0", "@ethersproject/pbkdf2": "5.8.0", "@ethersproject/properties": "5.8.0", "@ethersproject/providers": "5.8.0", "@ethersproject/random": "5.8.0", "@ethersproject/rlp": "5.8.0", "@ethersproject/sha2": "5.8.0", "@ethersproject/signing-key": "5.8.0", "@ethersproject/solidity": "5.8.0", "@ethersproject/strings": "5.8.0", "@ethersproject/transactions": "5.8.0", "@ethersproject/units": "5.8.0", "@ethersproject/wallet": "5.8.0", "@ethersproject/web": "5.8.0", "@ethersproject/wordlists": "5.8.0" } }, "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg=="], @@ -5103,8 +5130,6 @@ "mcl-wasm/@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="], - "md5.js/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "mdast-util-directive/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "mdast-util-directive/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], @@ -5181,14 +5206,14 @@ "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "radix-vue/@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="], - - "radix-vue/@internationalized/number": ["@internationalized/number@3.6.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw=="], - "radix-vue/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], "read-yaml-file/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5197,8 +5222,6 @@ "recast/esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "recast/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "recursive-readdir/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "rehype-autolink-headings/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], @@ -5211,8 +5234,6 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "sc-istanbul/glob": ["glob@5.0.15", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA=="], "sc-istanbul/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5305,6 +5326,10 @@ "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "use-callback-ref/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "valtio/proxy-compare": ["proxy-compare@2.6.0", "", {}, "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="], "valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], @@ -5323,6 +5348,8 @@ "vocs/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "web3-eth-abi/abitype": ["abitype@0.7.1", "", { "peerDependencies": { "typescript": ">=4.9.4", "zod": "^3 >=3.19.1" }, "optionalPeers": ["zod"] }, "sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ=="], "web3-eth-accounts/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], @@ -5559,8 +5586,12 @@ "eslint/optionator/type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "eth-json-rpc-filters/async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "extension-port-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "ghost-testrpc/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "ghost-testrpc/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -5879,8 +5910,6 @@ "mocha/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "pbkdf2/create-hash/ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], diff --git a/support/common/lib/utils/uuid.ts b/support/common/lib/utils/uuid.ts index 6424ba02c9..80849d80f6 100644 --- a/support/common/lib/utils/uuid.ts +++ b/support/common/lib/utils/uuid.ts @@ -1,5 +1,11 @@ +import { validate, version } from "uuid" + export type UUID = ReturnType & { _brand: "uuid" } export function createUUID(): UUID { return crypto.randomUUID() as UUID } + +export function isUUID(str: string): str is UUID { + return validate(str) && version(str) === 4 +} diff --git a/support/common/package.json b/support/common/package.json index c12d92fbdf..4ff2ebb4b1 100644 --- a/support/common/package.json +++ b/support/common/package.json @@ -34,6 +34,7 @@ "vite-plugin-node-polyfills": "^0.22.0" }, "dependencies": { - "@opentelemetry/api": "^1.9.0" + "@opentelemetry/api": "^1.9.0", + "uuid": "^11.1.0" } }