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"
}
}