Rename project from Nexa to Osias
Renames the npm package (nexa-extension -> osias-extension), the extension manifest name and Firefox gecko id ([email protected]), the internal runtime message-type namespace and storage keys ('nexa:...' -> 'osias:...'), the NexaMessage type, and all remaining Nexa branding in source comments, UI copy, and CLAUDE.md to match the new osias.trade domain and the osias-trade GitHub org. No wire-protocol changes — purely internal naming plus user-facing strings. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tn7uDYjTuZEbLPwpEiSCUw
This commit is contained in:
@@ -22,7 +22,7 @@ There is no lint/test setup yet — add one when the project needs it rather tha
|
|||||||
|
|
||||||
## What this project is
|
## What this project is
|
||||||
|
|
||||||
Nexa is a blockchain-powered trading companion focused on the emotional/behavioral side of trading, not the trades themselves. Trades are already visible on-chain, so Nexa observes wallet activity rather than requiring manual logging. This repo is the **open-source frontend**: a browser extension. The backend (wallet/RPC listening via Solana/Helius, loss detection, auth, WebSocket push) is a separate private service and is out of scope here — treat it as an external API.
|
Osias is a blockchain-powered trading companion focused on the emotional/behavioral side of trading, not the trades themselves. Trades are already visible on-chain, so Osias observes wallet activity rather than requiring manual logging. This repo is the **open-source frontend**: a browser extension. The backend (wallet/RPC listening via Solana/Helius, loss detection, auth, WebSocket push) is a separate private service and is out of scope here — treat it as an external API.
|
||||||
|
|
||||||
License: Apache 2.0. The backend stays closed-source; this frontend repo is the OSS component.
|
License: Apache 2.0. The backend stays closed-source; this frontend repo is the OSS component.
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ Note the naming mismatch with the wire protocol below: the frontend's internal `
|
|||||||
- `src/background/wallet-auth.ts` — `handleWalletConnected()` runs the REST auth flow (`POST /auth/nonce` → Phantom signature → `POST /auth/verify` → session token) once a content script reports a connected wallet. Also `requestWalletReconnect()` (silent reconnect after the backend invalidates a session), `requestWalletDisconnect()` (sign-out), `requestAccountSwitch()` (explicit account switch) — all three just message whichever tabs are on a supported site; `wallet-connect.ts` in the content script does the actual work.
|
- `src/background/wallet-auth.ts` — `handleWalletConnected()` runs the REST auth flow (`POST /auth/nonce` → Phantom signature → `POST /auth/verify` → session token) once a content script reports a connected wallet. Also `requestWalletReconnect()` (silent reconnect after the backend invalidates a session), `requestWalletDisconnect()` (sign-out), `requestAccountSwitch()` (explicit account switch) — all three just message whichever tabs are on a supported site; `wallet-connect.ts` in the content script does the actual work.
|
||||||
- `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`. `connectWsClient()` returns a controller with `reconnectNow()` so the background script can short-circuit the backoff wait right after a fresh token arrives. Auth failures (`4001` close, `auth_expired`/`session_revoked` errors) do **not** auto-retry with backoff — they call `onAuthExpired()` instead, since retrying with a known-bad token can't succeed; only real disconnects (network drop, backgrounded browser) use the protocol's suggested backoff schedule.
|
- `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`. `connectWsClient()` returns a controller with `reconnectNow()` so the background script can short-circuit the backoff wait right after a fresh token arrives. Auth failures (`4001` close, `auth_expired`/`session_revoked` errors) do **not** auto-retry with backoff — they call `onAuthExpired()` instead, since retrying with a known-bad token can't succeed; only real disconnects (network drop, backgrounded browser) use the protocol's suggested backoff schedule.
|
||||||
- `src/background/connection-status.ts` — separate from lock state; the popup surfaces this (connecting/connected/disconnected/auth-error) alongside the lock state so a broken connection isn't silently indistinguishable from "unlocked".
|
- `src/background/connection-status.ts` — separate from lock state; the popup surfaces this (connecting/connected/disconnected/auth-error) alongside the lock state so a broken connection isn't silently indistinguishable from "unlocked".
|
||||||
- `src/background/settings-client.ts` — `GET`/`PATCH /me/settings` (per-user loss-detection thresholds; see backend/CLAUDE.md, "Account settings"), same direct-fetch-with-bearer-token shape as `wallet-auth.ts`. Popup -> background dispatch is `nexa:get-settings`/`nexa:update-settings` in `background.ts`, responses shaped `{ settings } | { error }` (`SettingsResult` in `shared/messaging.ts`). The popup's "Loss thresholds" panel (`entrypoints/popup/App.tsx`) is the only place percent fields (`near_full_exit_fraction` and both loss thresholds) get converted between the backend's fractional wire representation and the whole-number percentages shown in the form — same "convert only at the seam" rule as `LockScope`'s kebab/snake split below.
|
- `src/background/settings-client.ts` — `GET`/`PATCH /me/settings` (per-user loss-detection thresholds; see backend/CLAUDE.md, "Account settings"), same direct-fetch-with-bearer-token shape as `wallet-auth.ts`. Popup -> background dispatch is `osias:get-settings`/`osias:update-settings` in `background.ts`, responses shaped `{ settings } | { error }` (`SettingsResult` in `shared/messaging.ts`). The popup's "Loss thresholds" panel (`entrypoints/popup/App.tsx`) is the only place percent fields (`near_full_exit_fraction` and both loss thresholds) get converted between the backend's fractional wire representation and the whole-number percentages shown in the form — same "convert only at the seam" rule as `LockScope`'s kebab/snake split below.
|
||||||
|
|
||||||
### Wallet auth (Phantom) — implemented
|
### Wallet auth (Phantom) — implemented
|
||||||
|
|
||||||
@@ -101,20 +101,20 @@ Real wallet signing, not a stub keypair. Phantom (and any wallet injecting a com
|
|||||||
|
|
||||||
- `src/entrypoints/wallet-bridge.content.ts` + `src/content-scripts/wallet-bridge/inject.ts` — a **second, `world: 'MAIN'`** content script on axiom.trade (Manifest V3 native main-world injection — no `web_accessible_resources`/dynamic `<script>` tag needed). This is the only file that touches `window.solana`/`window.phantom.solana` directly: `connect()` and `signMessage()`. Talks back to the isolated world via `window.postMessage` (channel-tagged messages, matched by request id) — **not `CustomEvent`**, see the callout below.
|
- `src/entrypoints/wallet-bridge.content.ts` + `src/content-scripts/wallet-bridge/inject.ts` — a **second, `world: 'MAIN'`** content script on axiom.trade (Manifest V3 native main-world injection — no `web_accessible_resources`/dynamic `<script>` tag needed). This is the only file that touches `window.solana`/`window.phantom.solana` directly: `connect()` and `signMessage()`. Talks back to the isolated world via `window.postMessage` (channel-tagged messages, matched by request id) — **not `CustomEvent`**, see the callout below.
|
||||||
- `src/content-scripts/wallet-bridge/relay.ts` — isolated-world side of that bridge: `callWallet(action, payload)` posts the request message and returns a promise that resolves/rejects on the matching result message, with a 30s timeout.
|
- `src/content-scripts/wallet-bridge/relay.ts` — isolated-world side of that bridge: `callWallet(action, payload)` posts the request message and returns a promise that resolves/rejects on the matching result message, with a 30s timeout.
|
||||||
- `src/content-scripts/wallet-bridge/banner.ts` — minimal plain-DOM "Connect your wallet to Nexa" prompt injected onto the page (bottom-right, fixed position) when a wallet isn't already trusted for this origin. No framework, kept deliberately tiny since it's living on someone else's page.
|
- `src/content-scripts/wallet-bridge/banner.ts` — minimal plain-DOM "Connect your wallet to Osias" prompt injected onto the page (bottom-right, fixed position) when a wallet isn't already trusted for this origin. No framework, kept deliberately tiny since it's living on someone else's page.
|
||||||
- `src/content-scripts/wallet-connect.ts` — orchestrates the above, wired into `content-index.ts` (runs independently of site-adapter matching): on load, tries `connect({ onlyIfTrusted: true })` silently (succeeds with no user interaction if the user already approved this origin in Phantom before); on failure, shows the banner and only calls plain `connect()` from the banner button's own click handler, since **a real user gesture is required for Phantom to show its approval popup on a first-ever connect** — this is why the banner exists in the page rather than the extension popup (a click in the popup's UI doesn't count as a gesture on the axiom.trade page by the time it reaches the wallet, since it crosses an extension-messaging boundary asynchronously). Once connected, reports `{ walletAddress }` to the background via `nexa:wallet-connected`. Also handles, all from the background: `nexa:wallet-sign-request` (sign a nonce), `nexa:request-wallet-connect` (retry the silent connect, e.g. after a session was invalidated), `nexa:wallet-disconnect-request` (sign-out) and `nexa:switch-account-request` (explicit account switch) — see the sign-out/switch-account bullet below for those two.
|
- `src/content-scripts/wallet-connect.ts` — orchestrates the above, wired into `content-index.ts` (runs independently of site-adapter matching): on load, tries `connect({ onlyIfTrusted: true })` silently (succeeds with no user interaction if the user already approved this origin in Phantom before); on failure, shows the banner and only calls plain `connect()` from the banner button's own click handler, since **a real user gesture is required for Phantom to show its approval popup on a first-ever connect** — this is why the banner exists in the page rather than the extension popup (a click in the popup's UI doesn't count as a gesture on the axiom.trade page by the time it reaches the wallet, since it crosses an extension-messaging boundary asynchronously). Once connected, reports `{ walletAddress }` to the background via `osias:wallet-connected`. Also handles, all from the background: `osias:wallet-sign-request` (sign a nonce), `osias:request-wallet-connect` (retry the silent connect, e.g. after a session was invalidated), `osias:wallet-disconnect-request` (sign-out) and `osias:switch-account-request` (explicit account switch) — see the sign-out/switch-account bullet below for those two.
|
||||||
- **Known gap**: this only works while an axiom.trade tab is open — there's no wallet connection path from the popup alone. That's intentional for now (matches the "only live while on a supported site" framing in backend/CLAUDE.md's RPC-subscription note), not an oversight.
|
- **Known gap**: this only works while an axiom.trade tab is open — there's no wallet connection path from the popup alone. That's intentional for now (matches the "only live while on a supported site" framing in backend/CLAUDE.md's RPC-subscription note), not an oversight.
|
||||||
- **Sign out and account switching (implemented) — both explicit user actions, not automatic.** An earlier version of this also wired Phantom's own `accountChanged` provider event to trigger re-auth automatically, but calling `connect()` ourselves also fires that same event — so a normal silent reconnect raced its own event-triggered handler and produced *two* competing "wallet connected" reports, each independently asking Phantom to sign a nonce (a popup every time). Removed entirely; `wallet-bridge/inject.ts` does not listen for any Phantom provider events, only responds to our own explicit calls. See its doc comment for the full story if this is ever reconsidered.
|
- **Sign out and account switching (implemented) — both explicit user actions, not automatic.** An earlier version of this also wired Phantom's own `accountChanged` provider event to trigger re-auth automatically, but calling `connect()` ourselves also fires that same event — so a normal silent reconnect raced its own event-triggered handler and produced *two* competing "wallet connected" reports, each independently asking Phantom to sign a nonce (a popup every time). Removed entirely; `wallet-bridge/inject.ts` does not listen for any Phantom provider events, only responds to our own explicit calls. See its doc comment for the full story if this is ever reconsidered.
|
||||||
- "Sign out" (popup button → `nexa:sign-out` → `background.ts`'s `signOut()`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects.
|
- "Sign out" (popup button → `osias:sign-out` → `background.ts`'s `signOut()`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects.
|
||||||
- "Switch account" (popup button → `nexa:switch-account` → `wallet-connect.ts`'s `switchAccount()`) explicitly disconnects then immediately reconnects, so Phantom shows its connect approval UI for whichever account is currently active there. Not fully verified whether Phantom requires a fresh user gesture for this non-`onlyIfTrusted` connect call relayed from the popup (vs. a direct page click) — if it does, `switchAccount()` falls back to the on-page banner so the user can complete it with a real click.
|
- "Switch account" (popup button → `osias:switch-account` → `wallet-connect.ts`'s `switchAccount()`) explicitly disconnects then immediately reconnects, so Phantom shows its connect approval UI for whichever account is currently active there. Not fully verified whether Phantom requires a fresh user gesture for this non-`onlyIfTrusted` connect call relayed from the popup (vs. a direct page click) — if it does, `switchAccount()` falls back to the on-page banner so the user can complete it with a real click.
|
||||||
- `background.ts`'s `nexa:wallet-connected` handler distinguishes "already signed in as this wallet" (no-op) from "signed in as a *different* wallet" (re-authenticate) by comparing the reported wallet address against `backend-client.ts`'s stored `{ token, walletAddress }` pair (`storeSession()`, not just a bare token) — a bare "do we have a token" check can't tell those apart and was the reason the account-switch case needed fixing in the first place.
|
- `background.ts`'s `osias:wallet-connected` handler distinguishes "already signed in as this wallet" (no-op) from "signed in as a *different* wallet" (re-authenticate) by comparing the reported wallet address against `backend-client.ts`'s stored `{ token, walletAddress }` pair (`storeSession()`, not just a bare token) — a bare "do we have a token" check can't tell those apart and was the reason the account-switch case needed fixing in the first place.
|
||||||
- **Firefox gotcha (hit during dev, now fixed)**: the isolated↔main-world bridge originally used `CustomEvent`s dispatched on `window`. That works on Chromium but throws `Uncaught Error: Permission denied to access property "id"` on Firefox — a `CustomEvent.detail` object created in one world can't have its properties read from the other (an Xray-wrapper security restriction specific to Firefox's extension model). Fixed by switching to `window.postMessage` for this bridge, which structured-clones its payload across the boundary correctly on both browsers — the same technique Phantom's own inpage↔content-script bridge uses. If you're extending this bridge, don't reach for `CustomEvent` again for isolated↔main-world data; `postMessage` (with a `channel` field to disambiguate from the page's own postMessage traffic, and an `event.source === window` check) is the pattern here.
|
- **Firefox gotcha (hit during dev, now fixed)**: the isolated↔main-world bridge originally used `CustomEvent`s dispatched on `window`. That works on Chromium but throws `Uncaught Error: Permission denied to access property "id"` on Firefox — a `CustomEvent.detail` object created in one world can't have its properties read from the other (an Xray-wrapper security restriction specific to Firefox's extension model). Fixed by switching to `window.postMessage` for this bridge, which structured-clones its payload across the boundary correctly on both browsers — the same technique Phantom's own inpage↔content-script bridge uses. If you're extending this bridge, don't reach for `CustomEvent` again for isolated↔main-world data; `postMessage` (with a `channel` field to disambiguate from the page's own postMessage traffic, and an `event.source === window` check) is the pattern here.
|
||||||
- **Verified working end-to-end against real Phantom** on Zen: connect → sign → `/auth/verify` → session token stored, confirmed via the `[nexa/wallet-*]` debug logs. `world: 'MAIN'` also needs Firefox 128+; confirmed fine on Zen's base version.
|
- **Verified working end-to-end against real Phantom** on Zen: connect → sign → `/auth/verify` → session token stored, confirmed via the `[osias/wallet-*]` debug logs. `world: 'MAIN'` also needs Firefox 128+; confirmed fine on Zen's base version.
|
||||||
- **A content script reports `wallet-connected` on every page load** (it always tries a silent `onlyIfTrusted` connect first), and `signMessage()` shows a fresh Phantom approval popup every single time it's called, unlike `connect()`, which is silent once trusted — so the re-auth gate on that message matters a lot for not spamming signature prompts. An earlier version of the gate only checked "is there any token stored," which correctly avoided re-signing on ordinary page loads but broke account switching (see above — it's now a wallet-address comparison instead).
|
- **A content script reports `wallet-connected` on every page load** (it always tries a silent `onlyIfTrusted` connect first), and `signMessage()` shows a fresh Phantom approval popup every single time it's called, unlike `connect()`, which is silent once trusted — so the re-auth gate on that message matters a lot for not spamming signature prompts. An earlier version of the gate only checked "is there any token stored," which correctly avoided re-signing on ordinary page loads but broke account switching (see above — it's now a wallet-address comparison instead).
|
||||||
|
|
||||||
## WebSocket protocol (backend wire contract)
|
## WebSocket protocol (backend wire contract)
|
||||||
|
|
||||||
Single source of truth for the wire protocol between the Nexa backend (Rust/Axum) and this extension's background script. Backend and frontend repos both implement against this section — not against each other's internal types. If either side needs to change a message shape, this doc changes first.
|
Single source of truth for the wire protocol between the Osias backend (Rust/Axum) and this extension's background script. Backend and frontend repos both implement against this section — not against each other's internal types. If either side needs to change a message shape, this doc changes first.
|
||||||
|
|
||||||
**Transport**
|
**Transport**
|
||||||
- WebSocket, endpoint `/ws` on the backend.
|
- WebSocket, endpoint `/ws` on the backend.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "nexa-extension",
|
"name": "osias-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nexa-extension",
|
"name": "osias-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "nexa-extension",
|
"name": "osias-extension",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Nexa browser extension frontend — your blockchain powered agent to help with your trading emotions.",
|
"description": "Osias browser extension frontend — your blockchain powered agent to help with your trading emotions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
|
|
||||||
const SESSION_STORAGE_KEY = 'nexa:session';
|
const SESSION_STORAGE_KEY = 'osias:session';
|
||||||
|
|
||||||
interface StoredSession {
|
interface StoredSession {
|
||||||
token: string;
|
token: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import { DEFAULT_CONNECTION_STATUS, type ConnectionStatus } from '@/shared/types';
|
import { DEFAULT_CONNECTION_STATUS, type ConnectionStatus } from '@/shared/types';
|
||||||
import type { NexaMessage } from '@/shared/messaging';
|
import type { OsiasMessage } from '@/shared/messaging';
|
||||||
|
|
||||||
/** In-memory only (not persisted) — always reflects the live WS connection, not a stale guess. */
|
/** In-memory only (not persisted) — always reflects the live WS connection, not a stale guess. */
|
||||||
let currentStatus: ConnectionStatus = DEFAULT_CONNECTION_STATUS;
|
let currentStatus: ConnectionStatus = DEFAULT_CONNECTION_STATUS;
|
||||||
@@ -13,6 +13,6 @@ export async function setConnectionStatus(next: ConnectionStatus): Promise<void>
|
|||||||
if (next === currentStatus) return;
|
if (next === currentStatus) return;
|
||||||
currentStatus = next;
|
currentStatus = next;
|
||||||
|
|
||||||
const message: NexaMessage = { type: 'nexa:connection-status-changed', status: next };
|
const message: OsiasMessage = { type: 'osias:connection-status-changed', status: next };
|
||||||
await browser.runtime.sendMessage(message).catch(() => undefined);
|
await browser.runtime.sendMessage(message).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
|
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
|
||||||
import { LOCK_STATE_STORAGE_KEY, type NexaMessage } from '@/shared/messaging';
|
import { LOCK_STATE_STORAGE_KEY, type OsiasMessage } from '@/shared/messaging';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single isolated seam for lock state. Nothing else in the extension reads
|
* Single isolated seam for lock state. Nothing else in the extension reads
|
||||||
@@ -38,7 +38,7 @@ export async function applyLockState(next: LockState): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function broadcastLockState(state: LockState): Promise<void> {
|
async function broadcastLockState(state: LockState): Promise<void> {
|
||||||
const message: NexaMessage = { type: 'nexa:lock-state-changed', state };
|
const message: OsiasMessage = { type: 'osias:lock-state-changed', state };
|
||||||
|
|
||||||
const tabs = await browser.tabs.query({});
|
const tabs = await browser.tabs.query({});
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import { BACKEND_HTTP_URL } from '@/shared/config';
|
import { BACKEND_HTTP_URL } from '@/shared/config';
|
||||||
import type { NexaMessage, WalletSignResult } from '@/shared/messaging';
|
import type { OsiasMessage, WalletSignResult } from '@/shared/messaging';
|
||||||
import { storeSession } from './backend-client';
|
import { storeSession } from './backend-client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +13,7 @@ import { storeSession } from './backend-client';
|
|||||||
* ws-client's own retry loop to keep trying getSessionToken()).
|
* ws-client's own retry loop to keep trying getSessionToken()).
|
||||||
*/
|
*/
|
||||||
export async function handleWalletConnected(tabId: number | undefined, walletAddress: string): Promise<void> {
|
export async function handleWalletConnected(tabId: number | undefined, walletAddress: string): Promise<void> {
|
||||||
console.debug('[nexa/wallet-auth] wallet connected', walletAddress, 'tab', tabId);
|
console.debug('[osias/wallet-auth] wallet connected', walletAddress, 'tab', tabId);
|
||||||
if (tabId == null) throw new Error('wallet-connected message had no source tab');
|
if (tabId == null) throw new Error('wallet-connected message had no source tab');
|
||||||
|
|
||||||
const nonceRes = await fetch(`${BACKEND_HTTP_URL}/auth/nonce`, {
|
const nonceRes = await fetch(`${BACKEND_HTTP_URL}/auth/nonce`, {
|
||||||
@@ -23,14 +23,14 @@ export async function handleWalletConnected(tabId: number | undefined, walletAdd
|
|||||||
});
|
});
|
||||||
if (!nonceRes.ok) throw new Error(`nonce request failed: ${nonceRes.status}`);
|
if (!nonceRes.ok) throw new Error(`nonce request failed: ${nonceRes.status}`);
|
||||||
const { nonce } = (await nonceRes.json()) as { nonce: string };
|
const { nonce } = (await nonceRes.json()) as { nonce: string };
|
||||||
console.debug('[nexa/wallet-auth] got nonce, asking tab to sign');
|
console.debug('[osias/wallet-auth] got nonce, asking tab to sign');
|
||||||
|
|
||||||
const signResult = (await browser.tabs.sendMessage(tabId, {
|
const signResult = (await browser.tabs.sendMessage(tabId, {
|
||||||
type: 'nexa:wallet-sign-request',
|
type: 'osias:wallet-sign-request',
|
||||||
nonce,
|
nonce,
|
||||||
} satisfies NexaMessage)) as WalletSignResult;
|
} satisfies OsiasMessage)) as WalletSignResult;
|
||||||
if ('error' in signResult) throw new Error(signResult.error);
|
if ('error' in signResult) throw new Error(signResult.error);
|
||||||
console.debug('[nexa/wallet-auth] got signature, verifying');
|
console.debug('[osias/wallet-auth] got signature, verifying');
|
||||||
|
|
||||||
const verifyRes = await fetch(`${BACKEND_HTTP_URL}/auth/verify`, {
|
const verifyRes = await fetch(`${BACKEND_HTTP_URL}/auth/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -41,10 +41,10 @@ export async function handleWalletConnected(tabId: number | undefined, walletAdd
|
|||||||
const { session_token: sessionToken } = (await verifyRes.json()) as { session_token: string };
|
const { session_token: sessionToken } = (await verifyRes.json()) as { session_token: string };
|
||||||
|
|
||||||
await storeSession(sessionToken, walletAddress);
|
await storeSession(sessionToken, walletAddress);
|
||||||
console.debug('[nexa/wallet-auth] session token stored');
|
console.debug('[osias/wallet-auth] session token stored');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function messageAllSupportedTabs(message: NexaMessage): Promise<void> {
|
async function messageAllSupportedTabs(message: OsiasMessage): Promise<void> {
|
||||||
const tabs = await browser.tabs.query({ url: 'https://axiom.trade/*' });
|
const tabs = await browser.tabs.query({ url: 'https://axiom.trade/*' });
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
tabs
|
tabs
|
||||||
@@ -55,15 +55,15 @@ async function messageAllSupportedTabs(message: NexaMessage): Promise<void> {
|
|||||||
|
|
||||||
/** Prompts any open supported-site tab to attempt a silent (onlyIfTrusted) reconnect — used after the session is invalidated server-side. */
|
/** Prompts any open supported-site tab to attempt a silent (onlyIfTrusted) reconnect — used after the session is invalidated server-side. */
|
||||||
export async function requestWalletReconnect(): Promise<void> {
|
export async function requestWalletReconnect(): Promise<void> {
|
||||||
await messageAllSupportedTabs({ type: 'nexa:request-wallet-connect' });
|
await messageAllSupportedTabs({ type: 'osias:request-wallet-connect' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign-out: tells any open supported-site tab to disconnect from Phantom (revokes this origin's trust, so the next silent connect correctly fails until the user reconnects). */
|
/** Sign-out: tells any open supported-site tab to disconnect from Phantom (revokes this origin's trust, so the next silent connect correctly fails until the user reconnects). */
|
||||||
export async function requestWalletDisconnect(): Promise<void> {
|
export async function requestWalletDisconnect(): Promise<void> {
|
||||||
await messageAllSupportedTabs({ type: 'nexa:wallet-disconnect-request' });
|
await messageAllSupportedTabs({ type: 'osias:wallet-disconnect-request' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Explicit "use a different account" — a deliberate user action (popup button), not something inferred from a wallet-provider event. See wallet-connect.ts's switchAccount(). */
|
/** Explicit "use a different account" — a deliberate user action (popup button), not something inferred from a wallet-provider event. See wallet-connect.ts's switchAccount(). */
|
||||||
export async function requestAccountSwitch(): Promise<void> {
|
export async function requestAccountSwitch(): Promise<void> {
|
||||||
await messageAllSupportedTabs({ type: 'nexa:switch-account-request' });
|
await messageAllSupportedTabs({ type: 'osias:switch-account-request' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { observeForElements } from './observer';
|
|||||||
import { initWalletConnect } from './wallet-connect';
|
import { initWalletConnect } from './wallet-connect';
|
||||||
import { showLockToast, hideLockToast } from '@/notifications/toast';
|
import { showLockToast, hideLockToast } from '@/notifications/toast';
|
||||||
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
|
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
|
||||||
import type { NexaMessage } from '@/shared/messaging';
|
import type { OsiasMessage } from '@/shared/messaging';
|
||||||
|
|
||||||
// Registering a new site = adding it here. Nothing below this line needs to
|
// Registering a new site = adding it here. Nothing below this line needs to
|
||||||
// change to support another adapter.
|
// change to support another adapter.
|
||||||
@@ -97,12 +97,12 @@ export function initContentScript(): void {
|
|||||||
const handle = observeForElements(document.body, () => adapter.findBuyElements(document.body), reconcile);
|
const handle = observeForElements(document.body, () => adapter.findBuyElements(document.body), reconcile);
|
||||||
|
|
||||||
browser.runtime
|
browser.runtime
|
||||||
.sendMessage({ type: 'nexa:get-lock-state' } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:get-lock-state' } satisfies OsiasMessage)
|
||||||
.then((state: LockState) => applyLockState(state))
|
.then((state: LockState) => applyLockState(state))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message: NexaMessage) => {
|
browser.runtime.onMessage.addListener((message: OsiasMessage) => {
|
||||||
if (message?.type === 'nexa:lock-state-changed') {
|
if (message?.type === 'osias:lock-state-changed') {
|
||||||
applyLockState(message.state);
|
applyLockState(message.state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const blurDisable: Intervention = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.dataset.nexaOverlay = 'true';
|
overlay.dataset.osiasOverlay = 'true';
|
||||||
overlay.style.cssText = OVERLAY_STYLE;
|
overlay.style.cssText = OVERLAY_STYLE;
|
||||||
overlay.innerHTML = LOCK_ICON_SVG;
|
overlay.innerHTML = LOCK_ICON_SVG;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const fullBlock: Intervention = {
|
|||||||
id: 'full-block',
|
id: 'full-block',
|
||||||
|
|
||||||
apply(_container, _reason, _onLockedInteraction) {
|
apply(_container, _reason, _onLockedInteraction) {
|
||||||
console.warn('[nexa] full-block intervention is a v1 stub and is not yet implemented.');
|
console.warn('[osias] full-block intervention is a v1 stub and is not yet implemented.');
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(_container) {},
|
remove(_container) {},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function showConnectBanner(onConnect: () => void): void {
|
|||||||
if (bannerEl) return;
|
if (bannerEl) return;
|
||||||
|
|
||||||
bannerEl = document.createElement('div');
|
bannerEl = document.createElement('div');
|
||||||
bannerEl.id = 'nexa-connect-banner';
|
bannerEl.id = 'osias-connect-banner';
|
||||||
Object.assign(bannerEl.style, {
|
Object.assign(bannerEl.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: '16px',
|
bottom: '16px',
|
||||||
@@ -24,7 +24,7 @@ export function showConnectBanner(onConnect: () => void): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.textContent = 'Connect your wallet to Nexa';
|
label.textContent = 'Connect your wallet to Osias';
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import bs58 from 'bs58';
|
|||||||
* separate, explicit action instead (see wallet-connect.ts's `switchAccount`).
|
* separate, explicit action instead (see wallet-connect.ts's `switchAccount`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CALL_CHANNEL = 'nexa:wallet-call';
|
const CALL_CHANNEL = 'osias:wallet-call';
|
||||||
const RESULT_CHANNEL = 'nexa:wallet-result';
|
const RESULT_CHANNEL = 'osias:wallet-result';
|
||||||
|
|
||||||
interface WalletCallMessage {
|
interface WalletCallMessage {
|
||||||
channel: typeof CALL_CHANNEL;
|
channel: typeof CALL_CHANNEL;
|
||||||
@@ -53,7 +53,7 @@ function respond(id: string, ok: boolean, dataOrError: unknown): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initWalletBridgeInjected(): void {
|
export function initWalletBridgeInjected(): void {
|
||||||
console.debug('[nexa/wallet-bridge] injected script active on', window.location.href);
|
console.debug('[osias/wallet-bridge] injected script active on', window.location.href);
|
||||||
|
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (event.source !== window) return; // ignore iframes/other windows
|
if (event.source !== window) return; // ignore iframes/other windows
|
||||||
@@ -61,7 +61,7 @@ export function initWalletBridgeInjected(): void {
|
|||||||
if (message?.channel !== CALL_CHANNEL) return; // not ours — the page may postMessage for its own reasons
|
if (message?.channel !== CALL_CHANNEL) return; // not ours — the page may postMessage for its own reasons
|
||||||
|
|
||||||
const { id, action, payload } = message as WalletCallMessage;
|
const { id, action, payload } = message as WalletCallMessage;
|
||||||
console.debug('[nexa/wallet-bridge] call', action, payload);
|
console.debug('[osias/wallet-bridge] call', action, payload);
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
@@ -71,7 +71,7 @@ export function initWalletBridgeInjected(): void {
|
|||||||
if (action === 'connect') {
|
if (action === 'connect') {
|
||||||
const { onlyIfTrusted } = payload as { onlyIfTrusted?: boolean };
|
const { onlyIfTrusted } = payload as { onlyIfTrusted?: boolean };
|
||||||
const result = await provider.connect(onlyIfTrusted ? { onlyIfTrusted: true } : undefined);
|
const result = await provider.connect(onlyIfTrusted ? { onlyIfTrusted: true } : undefined);
|
||||||
console.debug('[nexa/wallet-bridge] connect ok', result.publicKey.toString());
|
console.debug('[osias/wallet-bridge] connect ok', result.publicKey.toString());
|
||||||
respond(id, true, { walletAddress: result.publicKey.toString() });
|
respond(id, true, { walletAddress: result.publicKey.toString() });
|
||||||
} else if (action === 'signMessage') {
|
} else if (action === 'signMessage') {
|
||||||
const { message: nonce } = payload as { message: string };
|
const { message: nonce } = payload as { message: string };
|
||||||
@@ -84,7 +84,7 @@ export function initWalletBridgeInjected(): void {
|
|||||||
throw new Error(`Unknown wallet action: ${String(action)}`);
|
throw new Error(`Unknown wallet action: ${String(action)}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug('[nexa/wallet-bridge] call failed:', err);
|
console.debug('[osias/wallet-bridge] call failed:', err);
|
||||||
respond(id, false, err instanceof Error ? err.message : String(err));
|
respond(id, false, err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
* Firefox and Chromium. See wallet-bridge/inject.ts for the other side.
|
* Firefox and Chromium. See wallet-bridge/inject.ts for the other side.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CALL_CHANNEL = 'nexa:wallet-call';
|
const CALL_CHANNEL = 'osias:wallet-call';
|
||||||
const RESULT_CHANNEL = 'nexa:wallet-result';
|
const RESULT_CHANNEL = 'osias:wallet-result';
|
||||||
|
|
||||||
interface WalletResultMessage {
|
interface WalletResultMessage {
|
||||||
channel: typeof RESULT_CHANNEL;
|
channel: typeof RESULT_CHANNEL;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import type { NexaMessage } from '@/shared/messaging';
|
import type { OsiasMessage } from '@/shared/messaging';
|
||||||
import { callWallet } from './wallet-bridge/relay';
|
import { callWallet } from './wallet-bridge/relay';
|
||||||
import { hideConnectBanner, setConnectBannerError, showConnectBanner } from './wallet-bridge/banner';
|
import { hideConnectBanner, setConnectBannerError, showConnectBanner } from './wallet-bridge/banner';
|
||||||
|
|
||||||
@@ -12,30 +12,30 @@ interface SignResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function reportConnected(walletAddress: string): Promise<void> {
|
async function reportConnected(walletAddress: string): Promise<void> {
|
||||||
console.debug('[nexa/wallet-connect] connected', walletAddress);
|
console.debug('[osias/wallet-connect] connected', walletAddress);
|
||||||
hideConnectBanner();
|
hideConnectBanner();
|
||||||
await browser.runtime
|
await browser.runtime
|
||||||
.sendMessage({ type: 'nexa:wallet-connected', walletAddress } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:wallet-connected', walletAddress } satisfies OsiasMessage)
|
||||||
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err));
|
.catch((err) => console.debug('[osias/wallet-connect] failed to notify background:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign-out or a failed switch — clears the backend session and re-shows the connect prompt. */
|
/** Sign-out or a failed switch — clears the backend session and re-shows the connect prompt. */
|
||||||
async function reportDisconnected(): Promise<void> {
|
async function reportDisconnected(): Promise<void> {
|
||||||
console.debug('[nexa/wallet-connect] disconnected');
|
console.debug('[osias/wallet-connect] disconnected');
|
||||||
await browser.runtime
|
await browser.runtime
|
||||||
.sendMessage({ type: 'nexa:wallet-disconnected' } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:wallet-disconnected' } satisfies OsiasMessage)
|
||||||
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err));
|
.catch((err) => console.debug('[osias/wallet-connect] failed to notify background:', err));
|
||||||
showConnectBanner(() => void connectWithGesture());
|
showConnectBanner(() => void connectWithGesture());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Real user gesture (banner button click) — required for Phantom to show its connect approval popup on a first-ever connect. */
|
/** Real user gesture (banner button click) — required for Phantom to show its connect approval popup on a first-ever connect. */
|
||||||
async function connectWithGesture(): Promise<void> {
|
async function connectWithGesture(): Promise<void> {
|
||||||
console.debug('[nexa/wallet-connect] banner clicked, calling connect()');
|
console.debug('[osias/wallet-connect] banner clicked, calling connect()');
|
||||||
try {
|
try {
|
||||||
const { walletAddress } = await callWallet<ConnectResult>('connect', {});
|
const { walletAddress } = await callWallet<ConnectResult>('connect', {});
|
||||||
await reportConnected(walletAddress);
|
await reportConnected(walletAddress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug('[nexa/wallet-connect] connect() failed:', err);
|
console.debug('[osias/wallet-connect] connect() failed:', err);
|
||||||
setConnectBannerError(err instanceof Error ? err.message : String(err));
|
setConnectBannerError(err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,12 +46,12 @@ async function connectWithGesture(): Promise<void> {
|
|||||||
* connect. Falls back to the on-page banner (a real click) only when it isn't.
|
* connect. Falls back to the on-page banner (a real click) only when it isn't.
|
||||||
*/
|
*/
|
||||||
async function attemptSilentConnect(): Promise<void> {
|
async function attemptSilentConnect(): Promise<void> {
|
||||||
console.debug('[nexa/wallet-connect] attempting silent connect');
|
console.debug('[osias/wallet-connect] attempting silent connect');
|
||||||
try {
|
try {
|
||||||
const { walletAddress } = await callWallet<ConnectResult>('connect', { onlyIfTrusted: true });
|
const { walletAddress } = await callWallet<ConnectResult>('connect', { onlyIfTrusted: true });
|
||||||
await reportConnected(walletAddress);
|
await reportConnected(walletAddress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug('[nexa/wallet-connect] silent connect failed, showing banner:', err);
|
console.debug('[osias/wallet-connect] silent connect failed, showing banner:', err);
|
||||||
showConnectBanner(() => void connectWithGesture());
|
showConnectBanner(() => void connectWithGesture());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,46 +66,46 @@ async function attemptSilentConnect(): Promise<void> {
|
|||||||
* to the same on-page banner as a first-time connect would.
|
* to the same on-page banner as a first-time connect would.
|
||||||
*/
|
*/
|
||||||
async function switchAccount(): Promise<void> {
|
async function switchAccount(): Promise<void> {
|
||||||
console.debug('[nexa/wallet-connect] switch account requested');
|
console.debug('[osias/wallet-connect] switch account requested');
|
||||||
try {
|
try {
|
||||||
await callWallet('disconnect', {}).catch(() => undefined); // best-effort — fine if already disconnected
|
await callWallet('disconnect', {}).catch(() => undefined); // best-effort — fine if already disconnected
|
||||||
const { walletAddress } = await callWallet<ConnectResult>('connect', {});
|
const { walletAddress } = await callWallet<ConnectResult>('connect', {});
|
||||||
await reportConnected(walletAddress);
|
await reportConnected(walletAddress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug('[nexa/wallet-connect] switch account failed:', err);
|
console.debug('[osias/wallet-connect] switch account failed:', err);
|
||||||
showConnectBanner(() => void connectWithGesture());
|
showConnectBanner(() => void connectWithGesture());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wires wallet connect/sign into the page. Independent of any site adapter — runs regardless of whether a buy-button adapter matched. */
|
/** Wires wallet connect/sign into the page. Independent of any site adapter — runs regardless of whether a buy-button adapter matched. */
|
||||||
export function initWalletConnect(): void {
|
export function initWalletConnect(): void {
|
||||||
console.debug('[nexa/wallet-connect] init on', window.location.href);
|
console.debug('[osias/wallet-connect] init on', window.location.href);
|
||||||
void attemptSilentConnect();
|
void attemptSilentConnect();
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message: NexaMessage, _sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message: OsiasMessage, _sender, sendResponse) => {
|
||||||
if (message?.type === 'nexa:wallet-sign-request') {
|
if (message?.type === 'osias:wallet-sign-request') {
|
||||||
callWallet<SignResult>('signMessage', { message: message.nonce })
|
callWallet<SignResult>('signMessage', { message: message.nonce })
|
||||||
.then((result) => sendResponse(result))
|
.then((result) => sendResponse(result))
|
||||||
.catch((err) => sendResponse({ error: err instanceof Error ? err.message : String(err) }));
|
.catch((err) => sendResponse({ error: err instanceof Error ? err.message : String(err) }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message?.type === 'nexa:request-wallet-connect') {
|
if (message?.type === 'osias:request-wallet-connect') {
|
||||||
void attemptSilentConnect();
|
void attemptSilentConnect();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message?.type === 'nexa:wallet-disconnect-request') {
|
if (message?.type === 'osias:wallet-disconnect-request') {
|
||||||
// Sign-out, initiated from the popup (see background/wallet-auth.ts).
|
// Sign-out, initiated from the popup (see background/wallet-auth.ts).
|
||||||
// Phantom's disconnect() revokes this origin's trust, so the next
|
// Phantom's disconnect() revokes this origin's trust, so the next
|
||||||
// onlyIfTrusted attempt correctly fails until the user reconnects.
|
// onlyIfTrusted attempt correctly fails until the user reconnects.
|
||||||
callWallet('disconnect', {})
|
callWallet('disconnect', {})
|
||||||
.catch((err) => console.debug('[nexa/wallet-connect] disconnect() failed:', err))
|
.catch((err) => console.debug('[osias/wallet-connect] disconnect() failed:', err))
|
||||||
.finally(() => void reportDisconnected());
|
.finally(() => void reportDisconnected());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message?.type === 'nexa:switch-account-request') {
|
if (message?.type === 'osias:switch-account-request') {
|
||||||
void switchAccount();
|
void switchAccount();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@/background/wallet-auth';
|
} from '@/background/wallet-auth';
|
||||||
import { getSettings, updateSettings } from '@/background/settings-client';
|
import { getSettings, updateSettings } from '@/background/settings-client';
|
||||||
import { connectWsClient } from '@/background/ws-client';
|
import { connectWsClient } from '@/background/ws-client';
|
||||||
import type { NexaMessage, SettingsResult } from '@/shared/messaging';
|
import type { OsiasMessage, SettingsResult } from '@/shared/messaging';
|
||||||
|
|
||||||
export default defineBackground(() => {
|
export default defineBackground(() => {
|
||||||
const ws = connectWsClient({
|
const ws = connectWsClient({
|
||||||
@@ -27,23 +27,23 @@ export default defineBackground(() => {
|
|||||||
await requestWalletDisconnect();
|
await requestWalletDisconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message: NexaMessage, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message: OsiasMessage, sender, sendResponse) => {
|
||||||
switch (message?.type) {
|
switch (message?.type) {
|
||||||
case 'nexa:get-lock-state':
|
case 'osias:get-lock-state':
|
||||||
getLockState().then(sendResponse);
|
getLockState().then(sendResponse);
|
||||||
return true; // keep the message channel open for the async response
|
return true; // keep the message channel open for the async response
|
||||||
|
|
||||||
case 'nexa:get-connection-status':
|
case 'osias:get-connection-status':
|
||||||
sendResponse(getConnectionStatus());
|
sendResponse(getConnectionStatus());
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:open-popup':
|
case 'osias:open-popup':
|
||||||
// Best-effort: not all browsers/contexts allow programmatic popup
|
// Best-effort: not all browsers/contexts allow programmatic popup
|
||||||
// opening outside a direct user gesture on the action icon.
|
// opening outside a direct user gesture on the action icon.
|
||||||
browser.action.openPopup().catch(() => undefined);
|
browser.action.openPopup().catch(() => undefined);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:wallet-connected':
|
case 'osias:wallet-connected':
|
||||||
// A content script reports this on every page load (it always tries
|
// A content script reports this on every page load (it always tries
|
||||||
// a silent onlyIfTrusted connect first), and after an explicit
|
// a silent onlyIfTrusted connect first), and after an explicit
|
||||||
// account switch. Only re-authenticate (which means asking Phantom
|
// account switch. Only re-authenticate (which means asking Phantom
|
||||||
@@ -55,30 +55,30 @@ export default defineBackground(() => {
|
|||||||
if (session?.walletAddress === message.walletAddress) return;
|
if (session?.walletAddress === message.walletAddress) return;
|
||||||
return handleWalletConnected(sender.tab?.id, message.walletAddress).then(() => ws.reconnectNow());
|
return handleWalletConnected(sender.tab?.id, message.walletAddress).then(() => ws.reconnectNow());
|
||||||
})
|
})
|
||||||
.catch((err) => console.debug('[nexa/background] wallet auth failed:', err)); // ws-client's own retry loop keeps trying regardless
|
.catch((err) => console.debug('[osias/background] wallet auth failed:', err)); // ws-client's own retry loop keeps trying regardless
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:wallet-disconnected':
|
case 'osias:wallet-disconnected':
|
||||||
// Disconnected directly in Phantom's UI (not via our own sign-out
|
// Disconnected directly in Phantom's UI (not via our own sign-out
|
||||||
// flow, which already clears/disconnects itself) — treat the same way.
|
// flow, which already clears/disconnects itself) — treat the same way.
|
||||||
void clearSessionToken().then(() => ws.disconnect());
|
void clearSessionToken().then(() => ws.disconnect());
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:sign-out':
|
case 'osias:sign-out':
|
||||||
void signOut();
|
void signOut();
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:switch-account':
|
case 'osias:switch-account':
|
||||||
void requestAccountSwitch();
|
void requestAccountSwitch();
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 'nexa:get-settings':
|
case 'osias:get-settings':
|
||||||
getSettings()
|
getSettings()
|
||||||
.then((settings) => sendResponse({ settings } satisfies SettingsResult))
|
.then((settings) => sendResponse({ settings } satisfies SettingsResult))
|
||||||
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
|
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
|
||||||
return true; // keep the message channel open for the async response
|
return true; // keep the message channel open for the async response
|
||||||
|
|
||||||
case 'nexa:update-settings':
|
case 'osias:update-settings':
|
||||||
updateSettings(message.settings)
|
updateSettings(message.settings)
|
||||||
.then((settings) => sendResponse({ settings } satisfies SettingsResult))
|
.then((settings) => sendResponse({ settings } satisfies SettingsResult))
|
||||||
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
|
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type LockState,
|
type LockState,
|
||||||
type ThresholdSettings,
|
type ThresholdSettings,
|
||||||
} from '@/shared/types';
|
} from '@/shared/types';
|
||||||
import type { NexaMessage, SettingsResult } from '@/shared/messaging';
|
import type { OsiasMessage, SettingsResult } from '@/shared/messaging';
|
||||||
|
|
||||||
const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
|
const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
|
||||||
connecting: 'Connecting…',
|
connecting: 'Connecting…',
|
||||||
@@ -23,17 +23,17 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
browser.runtime
|
browser.runtime
|
||||||
.sendMessage({ type: 'nexa:get-lock-state' } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:get-lock-state' } satisfies OsiasMessage)
|
||||||
.then((state: LockState | undefined) => setLockState(state ?? DEFAULT_LOCK_STATE));
|
.then((state: LockState | undefined) => setLockState(state ?? DEFAULT_LOCK_STATE));
|
||||||
|
|
||||||
browser.runtime
|
browser.runtime
|
||||||
.sendMessage({ type: 'nexa:get-connection-status' } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:get-connection-status' } satisfies OsiasMessage)
|
||||||
.then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS));
|
.then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS));
|
||||||
|
|
||||||
const listener = (message: NexaMessage) => {
|
const listener = (message: OsiasMessage) => {
|
||||||
if (message?.type === 'nexa:lock-state-changed') {
|
if (message?.type === 'osias:lock-state-changed') {
|
||||||
setLockState(message.state);
|
setLockState(message.state);
|
||||||
} else if (message?.type === 'nexa:connection-status-changed') {
|
} else if (message?.type === 'osias:connection-status-changed') {
|
||||||
setConnectionStatus(message.status);
|
setConnectionStatus(message.status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -42,17 +42,17 @@ export function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function signOut(): void {
|
function signOut(): void {
|
||||||
void browser.runtime.sendMessage({ type: 'nexa:sign-out' } satisfies NexaMessage);
|
void browser.runtime.sendMessage({ type: 'osias:sign-out' } satisfies OsiasMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchAccount(): void {
|
function switchAccount(): void {
|
||||||
void browser.runtime.sendMessage({ type: 'nexa:switch-account' } satisfies NexaMessage);
|
void browser.runtime.sendMessage({ type: 'osias:switch-account' } satisfies OsiasMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<h1>Nexa</h1>
|
<h1>Osias</h1>
|
||||||
<p className="subtitle">Trading companion</p>
|
<p className="subtitle">Trading companion</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ function ThresholdsPanel() {
|
|||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
browser.runtime
|
browser.runtime
|
||||||
.sendMessage({ type: 'nexa:get-settings' } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:get-settings' } satisfies OsiasMessage)
|
||||||
.then((result: SettingsResult | undefined) => {
|
.then((result: SettingsResult | undefined) => {
|
||||||
if (result && 'settings' in result) {
|
if (result && 'settings' in result) {
|
||||||
setForm(toForm(result.settings));
|
setForm(toForm(result.settings));
|
||||||
@@ -160,7 +160,7 @@ function ThresholdsPanel() {
|
|||||||
setStatus('saving');
|
setStatus('saving');
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
browser.runtime
|
browser.runtime
|
||||||
.sendMessage({ type: 'nexa:update-settings', settings: fromForm(form) } satisfies NexaMessage)
|
.sendMessage({ type: 'osias:update-settings', settings: fromForm(form) } satisfies OsiasMessage)
|
||||||
.then((result: SettingsResult | undefined) => {
|
.then((result: SettingsResult | undefined) => {
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
if (result && 'settings' in result) {
|
if (result && 'settings' in result) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Nexa</title>
|
<title>Osias</title>
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+17
-17
@@ -1,8 +1,8 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import type { NexaMessage } from '@/shared/messaging';
|
import type { OsiasMessage } from '@/shared/messaging';
|
||||||
|
|
||||||
const TOAST_ID = 'nexa-lock-toast';
|
const TOAST_ID = 'osias-lock-toast';
|
||||||
const STYLE_ID = 'nexa-lock-toast-styles';
|
const STYLE_ID = 'osias-lock-toast-styles';
|
||||||
const AUTO_HIDE_MS = 5000;
|
const AUTO_HIDE_MS = 5000;
|
||||||
|
|
||||||
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
@@ -30,18 +30,18 @@ function ensureStyles(): void {
|
|||||||
font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
#${TOAST_ID} .nexa-toast-icon { flex: none; margin-top: 1px; }
|
#${TOAST_ID} .osias-toast-icon { flex: none; margin-top: 1px; }
|
||||||
#${TOAST_ID} .nexa-toast-body { flex: 1; }
|
#${TOAST_ID} .osias-toast-body { flex: 1; }
|
||||||
#${TOAST_ID} .nexa-toast-title { font-weight: 600; margin-bottom: 2px; }
|
#${TOAST_ID} .osias-toast-title { font-weight: 600; margin-bottom: 2px; }
|
||||||
#${TOAST_ID} .nexa-toast-reason { opacity: 0.85; }
|
#${TOAST_ID} .osias-toast-reason { opacity: 0.85; }
|
||||||
#${TOAST_ID} .nexa-toast-action {
|
#${TOAST_ID} .osias-toast-action {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #7ab8ff;
|
color: #7ab8ff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#${TOAST_ID} .nexa-toast-action:hover { text-decoration: underline; }
|
#${TOAST_ID} .osias-toast-action:hover { text-decoration: underline; }
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
@@ -55,18 +55,18 @@ function ensureToast(): HTMLElement {
|
|||||||
el = document.createElement('div');
|
el = document.createElement('div');
|
||||||
el.id = TOAST_ID;
|
el.id = TOAST_ID;
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="nexa-toast-icon">🔒</div>
|
<div class="osias-toast-icon">🔒</div>
|
||||||
<div class="nexa-toast-body">
|
<div class="osias-toast-body">
|
||||||
<div class="nexa-toast-title">Nexa: buying locked</div>
|
<div class="osias-toast-title">Osias: buying locked</div>
|
||||||
<div class="nexa-toast-reason"></div>
|
<div class="osias-toast-reason"></div>
|
||||||
<div class="nexa-toast-action">View details</div>
|
<div class="osias-toast-action">View details</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
// No manual close button — the toast only goes away on its own (auto-hide
|
// No manual close button — the toast only goes away on its own (auto-hide
|
||||||
// timer below) or when the lock state actually changes; clicking it never
|
// timer below) or when the lock state actually changes; clicking it never
|
||||||
// unlocks anything.
|
// unlocks anything.
|
||||||
el.querySelector('.nexa-toast-action')?.addEventListener('click', () => {
|
el.querySelector('.osias-toast-action')?.addEventListener('click', () => {
|
||||||
const message: NexaMessage = { type: 'nexa:open-popup' };
|
const message: OsiasMessage = { type: 'osias:open-popup' };
|
||||||
browser.runtime.sendMessage(message).catch(() => undefined);
|
browser.runtime.sendMessage(message).catch(() => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ function ensureToast(): HTMLElement {
|
|||||||
|
|
||||||
export function showLockToast(reason: string | undefined): void {
|
export function showLockToast(reason: string | undefined): void {
|
||||||
const el = ensureToast();
|
const el = ensureToast();
|
||||||
const reasonEl = el.querySelector('.nexa-toast-reason');
|
const reasonEl = el.querySelector('.osias-toast-reason');
|
||||||
if (reasonEl) {
|
if (reasonEl) {
|
||||||
reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.';
|
reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Nexa backend location. `host_permissions` in wxt.config.ts must be kept in
|
* Osias backend location. `host_permissions` in wxt.config.ts must be kept in
|
||||||
* sync with this host.
|
* sync with this host.
|
||||||
*/
|
*/
|
||||||
export const BACKEND_HTTP_URL = 'https://api.osias.trade';
|
export const BACKEND_HTTP_URL = 'https://api.osias.trade';
|
||||||
|
|||||||
+19
-19
@@ -1,7 +1,7 @@
|
|||||||
import type { ConnectionStatus, LockState, ThresholdSettings } from './types';
|
import type { ConnectionStatus, LockState, ThresholdSettings } from './types';
|
||||||
|
|
||||||
/** browser.storage.local key backing the lock state seam in the background script. */
|
/** browser.storage.local key backing the lock state seam in the background script. */
|
||||||
export const LOCK_STATE_STORAGE_KEY = 'nexa:lockState';
|
export const LOCK_STATE_STORAGE_KEY = 'osias:lockState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typed message contracts between background <-> content scripts <-> popup.
|
* Typed message contracts between background <-> content scripts <-> popup.
|
||||||
@@ -12,30 +12,30 @@ export const LOCK_STATE_STORAGE_KEY = 'nexa:lockState';
|
|||||||
* in ../../CLAUDE.md) — only the WS client (ws-client.ts) may apply a new
|
* in ../../CLAUDE.md) — only the WS client (ws-client.ts) may apply a new
|
||||||
* lock state, and only because the server told it to.
|
* lock state, and only because the server told it to.
|
||||||
*/
|
*/
|
||||||
export type NexaMessage =
|
export type OsiasMessage =
|
||||||
| { type: 'nexa:get-lock-state' }
|
| { type: 'osias:get-lock-state' }
|
||||||
| { type: 'nexa:lock-state-changed'; state: LockState }
|
| { type: 'osias:lock-state-changed'; state: LockState }
|
||||||
| { type: 'nexa:get-connection-status' }
|
| { type: 'osias:get-connection-status' }
|
||||||
| { type: 'nexa:connection-status-changed'; status: ConnectionStatus }
|
| { type: 'osias:connection-status-changed'; status: ConnectionStatus }
|
||||||
| { type: 'nexa:open-popup' }
|
| { type: 'osias:open-popup' }
|
||||||
// Wallet auth (content script <-> background). See
|
// Wallet auth (content script <-> background). See
|
||||||
// content-scripts/wallet-connect.ts and background/wallet-auth.ts.
|
// content-scripts/wallet-connect.ts and background/wallet-auth.ts.
|
||||||
| { type: 'nexa:wallet-connected'; walletAddress: string }
|
| { type: 'osias:wallet-connected'; walletAddress: string }
|
||||||
| { type: 'nexa:wallet-disconnected' }
|
| { type: 'osias:wallet-disconnected' }
|
||||||
| { type: 'nexa:wallet-sign-request'; nonce: string }
|
| { type: 'osias:wallet-sign-request'; nonce: string }
|
||||||
| { type: 'nexa:request-wallet-connect' }
|
| { type: 'osias:request-wallet-connect' }
|
||||||
| { type: 'nexa:wallet-disconnect-request' }
|
| { type: 'osias:wallet-disconnect-request' }
|
||||||
| { type: 'nexa:switch-account-request' }
|
| { type: 'osias:switch-account-request' }
|
||||||
// Sign-out / switch account (popup -> background). See entrypoints/popup/App.tsx.
|
// Sign-out / switch account (popup -> background). See entrypoints/popup/App.tsx.
|
||||||
| { type: 'nexa:sign-out' }
|
| { type: 'osias:sign-out' }
|
||||||
| { type: 'nexa:switch-account' }
|
| { type: 'osias:switch-account' }
|
||||||
// Loss-threshold settings (popup -> background -> GET/PATCH /me/settings).
|
// Loss-threshold settings (popup -> background -> GET/PATCH /me/settings).
|
||||||
// See background/settings-client.ts and entrypoints/popup/App.tsx.
|
// See background/settings-client.ts and entrypoints/popup/App.tsx.
|
||||||
| { type: 'nexa:get-settings' }
|
| { type: 'osias:get-settings' }
|
||||||
| { type: 'nexa:update-settings'; settings: Partial<ThresholdSettings> };
|
| { type: 'osias:update-settings'; settings: Partial<ThresholdSettings> };
|
||||||
|
|
||||||
/** Response shape for 'nexa:get-settings'/'nexa:update-settings', returned via sendResponse. */
|
/** Response shape for 'osias:get-settings'/'osias:update-settings', returned via sendResponse. */
|
||||||
export type SettingsResult = { settings: ThresholdSettings } | { error: string };
|
export type SettingsResult = { settings: ThresholdSettings } | { error: string };
|
||||||
|
|
||||||
/** Response shape for 'nexa:wallet-sign-request', returned via sendResponse (not a dispatched NexaMessage). */
|
/** Response shape for 'osias:wallet-sign-request', returned via sendResponse (not a dispatched OsiasMessage). */
|
||||||
export type WalletSignResult = { signature: string } | { error: string };
|
export type WalletSignResult = { signature: string } | { error: string };
|
||||||
|
|||||||
+3
-3
@@ -8,15 +8,15 @@ export default defineConfig({
|
|||||||
// Target Manifest V3 on both Chromium and Firefox (modern Firefox / Zen support it).
|
// Target Manifest V3 on both Chromium and Firefox (modern Firefox / Zen support it).
|
||||||
manifestVersion: 3,
|
manifestVersion: 3,
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Nexa',
|
name: 'Osias',
|
||||||
description: 'Your blockchain powered agent to help with your trading emotions.',
|
description: 'Your blockchain powered agent to help with your trading emotions.',
|
||||||
permissions: ['storage'],
|
permissions: ['storage'],
|
||||||
// axiom.trade: the site adapter target. The other entry is the Nexa backend.
|
// axiom.trade: the site adapter target. The other entry is the Osias backend.
|
||||||
host_permissions: ['https://axiom.trade/*', BACKEND_HTTP_URL + "/*"],
|
host_permissions: ['https://axiom.trade/*', BACKEND_HTTP_URL + "/*"],
|
||||||
browser_specific_settings: {
|
browser_specific_settings: {
|
||||||
gecko: {
|
gecko: {
|
||||||
// Placeholder id for local/dev builds; replace before publishing to AMO.
|
// Placeholder id for local/dev builds; replace before publishing to AMO.
|
||||||
id: 'nexa-extension@nexa-sol.dev',
|
id: 'osias-extension@osias.trade',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user