Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-waves-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@status-im/trpc-webext': patch
---

First version of @status-im/trpc-webext
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
layout node
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for direnv

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ web-build/

# Contentlayer
.contentlayer

/.lsp/
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./packages/components",
"./packages/status-js",
"./packages/wallet",
"./packages/trpc-webext",
"./apps/connector",
"./apps/portfolio",
"./apps/wallet",
Expand Down
6 changes: 3 additions & 3 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
},
"dependencies": {
"@status-im/wallet": "workspace:*",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"@trpc/next": "10.45.2",
"@trpc/client": "11.3.0",
"@trpc/server": "11.3.0",
"@trpc/next": "11.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.0",
Expand Down
5 changes: 3 additions & 2 deletions apps/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@
"@status-im/components": "workspace:*",
"@status-im/icons": "workspace:*",
"@status-im/wallet": "workspace:*",
"@status-im/trpc-webext": "workspace:*",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-router": "^1.109.2",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"@trpc/client": "11.3.0",
"@trpc/server": "11.3.0",
"@trustwallet/wallet-core": "^4.3.6",
"@wxt-dev/storage": "^1.1.0",
"@zxcvbn-ts/core": "^3.0.4",
Expand Down
31 changes: 22 additions & 9 deletions apps/wallet/src/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
// import { Cardano } from '@cardano-sdk/core'
// import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto'
// import { AddressType, InMemoryKeyAgent } from '@cardano-sdk/key-management'
import { createTRPCProxyClient } from '@trpc/client'
import { createWebExtHandler, webExtensionLink } from '@status-im/trpc-webext'
import { createTRPCClient } from '@trpc/client'
import { initTRPC } from '@trpc/server'
import superjson from 'superjson'
import { createChromeHandler } from 'trpc-chrome/adapter'
import { browser } from 'wxt/browser'
import { z } from 'zod'

import * as bitcoin from './bitcoin/bitcoin'
import { chromeLinkWithRetries } from './chromeLink'
import * as ethereum from './ethereum/ethereum'
import { getKeystore } from './keystore'
import * as solana from './solana/solana'
import {
getWalletCore,
// type WalletCore
} from './wallet'
import { runtimePortToClientContextType } from './webext'

const createContext = async () => {
import type { CreateWebExtContextOptions } from '@status-im/trpc-webext/adapter'

const createContext = async (webextOpts?: CreateWebExtContextOptions) => {
const keyStore = await getKeystore()
const walletCore = await getWalletCore()

return {
...webextOpts,
contextType: runtimePortToClientContextType(webextOpts?.req),
keyStore,
walletCore,
}
Expand Down Expand Up @@ -598,8 +603,11 @@ const apiRouter = router({
export type APIRouter = typeof apiRouter

export async function createAPI() {
// @ts-expect-error: fixme!:
createChromeHandler({ router: apiRouter, createContext })
createWebExtHandler({
router: apiRouter,
createContext,
runtime: browser.runtime,
})

const ctx = await createContext()
const api = createCallerFactory(apiRouter)(ctx)
Expand All @@ -608,8 +616,13 @@ export async function createAPI() {
}

export function createAPIClient() {
return createTRPCProxyClient<APIRouter>({
links: [chromeLinkWithRetries()],
transformer: superjson,
return createTRPCClient<APIRouter>({
links: [
webExtensionLink({
runtime: browser.runtime,
timeoutMS: 30000,
transformer: superjson,
}),
],
})
}
15 changes: 15 additions & 0 deletions apps/wallet/src/data/webext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Runtime } from 'wxt/browser'

export type TRPCClientContextType = 'POPUP' | 'SIDE_PANEL' | 'PAGE' | 'TAB'

export function runtimePortToClientContextType(
port?: Runtime.Port,
): TRPCClientContextType | undefined {
const { origin } = globalThis.location
if (!port) return
if (port.sender?.url?.startsWith(`${origin}/sidepanel.html`))
return 'SIDE_PANEL'
if (port.sender?.url?.startsWith(`${origin}/popup.html`)) return 'POPUP'
if (port.sender?.url?.startsWith(`${origin}/page.html`)) return 'PAGE'
return 'TAB'
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"packages/icons",
"packages/components",
"packages/wallet",
"packages/trpc-webext",
"apps/connector",
"apps/portfolio",
"apps/wallet",
Expand Down
5 changes: 5 additions & 0 deletions packages/trpc-webext/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"arrowParens": "avoid"
}
1 change: 1 addition & 0 deletions packages/trpc-webext/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @status-im/trpc-webext
110 changes: 110 additions & 0 deletions packages/trpc-webext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# @status-im/trpc-webext

A tRPC adapter for web extensions that enables type-safe communication between different extension contexts (background, content scripts, popup, etc.).

## Installation

```sh
pnpm add @status-im/trpc-webext
```

## Basic Usage

### 1. Create your tRPC router (typically in background script)

```typescript
import { initTRPC } from '@trpc/server'
import { createWebExtHandler } from '@status-im/trpc-webext/adapter'
import { browser } from 'webextension-polyfill'
import superjson from 'superjson'

// Initialize tRPC
const t = initTRPC.context<Context>().create({
transformer: superjson,
isServer: false,
allowOutsideOfServer: true,
})

// Define your router
const appRouter = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return =Hello ${input.name}!=
}),
})

// Create context function
const createContext = async (opts) => {
return {
// Add your context data here
userId: 'user Alice',
}
}

// Set up the handler in background script
createWebExtHandler({
router: appRouter,
createContext,
runtime: browser.runtime,
})

export type AppRouter = typeof appRouter
```

### 2. Create a client (in popup, content script, etc.)

```typescript
import { createTRPCClient } from '@trpc/client'
import { webExtensionLink } from '@status-im/trpc-webext/link'
import { browser } from 'webextension-polyfill'
import superjson from 'superjson'
import type { AppRouter } from './background'

const client = createTRPCClient<AppRouter>({
links: [
webExtensionLink({
runtime: browser.runtime,
transformer: superjson, // same transformer as the server
timeoutMS: 30000, // optional, defaults to 10000ms
}),
],
})

// Use the client
async function example() {
const result = await client.greeting.query({ name: 'World' })
console.log(result) // "Hello World!"
}
```

## Key Features

- **Type Safety**: Full TypeScript support with end-to-end type safety
- **Real-time Communication**: Support for subscriptions using observables
- **Multiple Contexts**: Works across all web extension contexts (background, popup, content scripts, options page, etc.)
- **Data Transformation**: Built-in support for data transformers like SuperJSON
- **Error Handling**: Proper error propagation and handling
- **Connection Management**: Automatic cleanup of connections and subscriptions

## Configuration Options

### `createWebExtHandler` options:

- `router`: Your tRPC router
- `createContext`: Function to create request context
- `runtime`: Browser runtime (e.g., `browser.runtime`)
- `onError`: Optional error handler

### `webExtensionLink` options:

- `runtime`: Browser runtime (e.g., `browser.runtime`)
- `transformer`: Data transformer (e.g., SuperJSON)
- `timeoutMS`: Request timeout in milliseconds (default: 10000)

## Notes

- The handler should be set up in your background script
- Clients can be created in any extension context
- Make sure to use the same transformer on both ends
- Subscriptions are automatically cleaned up when ports disconnect
9 changes: 9 additions & 0 deletions packages/trpc-webext/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import configs from '@status-im/eslint-config'

/** @type {import('eslint').Linter.Config[]} */
export default [
...configs,
{
files: ['**/*.ts', '**/*.mts', '**/*.mjs', '**/*.tsx'],
},
]
55 changes: 55 additions & 0 deletions packages/trpc-webext/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@status-im/trpc-webext",
"description": "description",
"version": "0.0.0",
"license": "MIT",
"keywords": [
"trpc",
"extension",
"webext",
"webextension"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./adapter": {
"types": "./dist/adapter/index.d.ts",
"import": "./dist/adapter/index.js",
"require": "./dist/adapter/index.js"
},
"./link": {
"types": "./dist/link/index.d.ts",
"import": "./dist/link/index.js",
"require": "./dist/link/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "tsc -w",
"build": "tsc",
"lint": "eslint src",
"format": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest --watch"
},
"peerDependencies": {
"@trpc/client": "^11.0.0",
"@trpc/server": "^11.0.0"
},
"devDependencies": {
"@types/webextension-polyfill": "^0.12.3",
"zod": "^3.23.8"
},
"publishConfig": {
"access": "public"
}
}
Loading