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
8 changes: 7 additions & 1 deletion docs/ai/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,20 @@ The **currency** selector does **not** appear in the user menu — it is always

## WalletConnect

OISY connects to external dApps over WalletConnect (Reown WalletKit). When a dApp proposes a session, OISY advertises one namespace per chain family for which the signed-in user has a loaded address, so a single connection can span Ethereum, Solana, and Bitcoin at once.
OISY connects to external dApps over WalletConnect (Reown WalletKit). When a dApp proposes a session, OISY advertises one namespace per chain family for which the signed-in user has a loaded address, so each connection can span Ethereum, Solana, and Bitcoin at once. Multiple dApp connections can be open simultaneously (see [Multiple simultaneous connections](#multiple-simultaneous-connections)).

- **Ethereum (`eip155`)** — supports `eth_sendTransaction`, `eth_sign`, `personal_sign`, `eth_signTypedData_v4`, and `eth_signTypedData` (legacy).
- **Solana (`solana`)** — supports `solana_signTransaction`, `solana_signAndSendTransaction`, and `solana_signMessage`, advertised for the mainnet and devnet addresses that are present (including the legacy CAIP-10 namespaces for compatibility).
- **Bitcoin (`bip122`)** — supports `getAccountAddresses`, `signMessage`, and `signPsbt`. The namespace is advertised whenever any BTC address (mainnet, testnet, or regtest) is loaded, with one `bip122:<genesis>` chain and matching `bip122:<genesis>:<address>` account per present network, and the `bip122_addressesChanged` event.

`signPsbt` is **sign-only**: OISY signs the PSBT the dApp provides and returns it, but does not broadcast the resulting transaction itself. Broadcasting is deferred to the dApp (and the `sendTransfer` method is intentionally not offered) so OISY never broadcasts a transaction it cannot fully account for — see the spec's broadcast-atomicity rationale.

### Multiple simultaneous connections

OISY supports **several dApp connections at once**, each independently manageable. Connecting a new dApp leaves the already-connected ones in place, and the "Connected Apps" list shows every live session. Each row's close button disconnects only that dApp; a "Disconnect all" control tears every connection down in one tap. When a dApp ends its own session, only that entry is removed and the others keep working. Incoming sign/send requests route to the correct session by topic across all open connections. Previously connected sessions are restored after a page refresh.

Requests are still handled **one at a time**: while a request from one dApp is under review, a request arriving from another is rejected with a "request skipped" notice. Session proposals are likewise reviewed one at a time — the user adds connections sequentially (scan/paste → review → approve, then repeat).

---

## Bitcoin
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import WalletConnectActions from '$lib/components/wallet-connect/WalletConnectActions.svelte';
import WalletConnectDomainVerification from '$lib/components/wallet-connect/WalletConnectDomainVerification.svelte';
import { isBusy } from '$lib/derived/busy.derived';
import { resetListener } from '$lib/services/wallet-connect.services';
import { resetListenerIfNoSessions, syncSessions } from '$lib/services/wallet-connect.services';
import { busy } from '$lib/stores/busy.store';
import { i18n } from '$lib/stores/i18n.store';
import { modalStore } from '$lib/stores/modal.store';
Expand Down Expand Up @@ -49,7 +49,8 @@
const close = () => modalStore.close();

const resetAndClose = () => {
resetListener();
// Rejecting a proposal must not drop dApps already connected — keep the listener if any remain.
resetListenerIfNoSessions();
close();
};

Expand Down Expand Up @@ -83,14 +84,17 @@
try {
await callback(proposal);

// Reflect the newly approved (or removed) session in the reactive sessions store.
syncSessions();

toast?.();
} catch (err: unknown) {
toastsError({
msg: { text: $i18n.wallet_connect.error.unexpected },
err
});

resetListener();
resetListenerIfNoSessions();
}

busy.stop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
onSessionProposal,
onSessionRequest
} from '$lib/services/wallet-connect-handlers.services';
import { disconnectListener, resetListener } from '$lib/services/wallet-connect.services';
import {
disconnectListener,
resetListener,
resetListenerIfNoSessions,
syncSessions
} from '$lib/services/wallet-connect.services';
import { i18n } from '$lib/stores/i18n.store';
import { initialLoading } from '$lib/stores/loader.store';
import { modalStore } from '$lib/stores/modal.store';
Expand Down Expand Up @@ -134,7 +139,8 @@
onSessionDelete({
listener: newListener,
callback: () => {
resetListener();
// Only one session ended — keep the listener alive if other dApps remain connected.
resetListenerIfNoSessions();
}
}),
onSessionRequest: (sessionRequest: WalletKitTypes.SessionRequest) =>
Expand All @@ -147,6 +153,9 @@
// We have no active sessions, we can disconnect the listener.
if (Object.keys(sessions).length === 0) {
await disconnectListener();
} else {
// Seed the reactive sessions store with the restored sessions.
syncSessions();
}
} catch (err: unknown) {
toastsError({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { Modal } from '@dfinity/gix-components';
import { isNullish, nonNullish } from '@dfinity/utils';
import { nonNullish } from '@dfinity/utils';
import type { SessionTypes } from '@walletconnect/types';
import { onMount } from 'svelte';
import { CAIP10_CHAINS } from '$env/caip10-chains.env';
import { SUPPORTED_EVM_NETWORKS } from '$env/networks/networks-evm/networks.evm.env';
import { SUPPORTED_ETHEREUM_NETWORKS } from '$env/networks/networks.eth.env';
Expand All @@ -15,11 +16,16 @@
import ExternalLink from '$lib/components/ui/ExternalLink.svelte';
import LogoButton from '$lib/components/ui/LogoButton.svelte';
import OverlappedLogos from '$lib/components/ui/OverlappedLogos.svelte';
import { disconnectListener } from '$lib/services/wallet-connect.services';
import {
disconnectListener,
disconnectSession,
syncSessions
} from '$lib/services/wallet-connect.services';
import { i18n } from '$lib/stores/i18n.store';
import { modalStore } from '$lib/stores/modal.store';
import { toastsShow } from '$lib/stores/toasts.store';
import { walletConnectListenerStore } from '$lib/stores/wallet-connect.store';
import { walletConnectSessionsStore } from '$lib/stores/wallet-connect.store';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
import { SolanaNetworks } from '$sol/types/network';

const chainIconMap: Record<string, string> = {
Expand All @@ -35,25 +41,35 @@
}, {})
};

let listener = $derived($walletConnectListenerStore);
let sessions = $derived($walletConnectSessionsStore);

let sessions = $derived.by((): SessionTypes.Struct[] => {
if (isNullish(listener)) {
return [];
}

return Object.values(listener.getActiveSessions());
});

const disconnect = async () => {
await disconnectListener();
// Reflect the live WalletKit sessions whenever the modal is opened, independent of which add /
// remove path last ran.
onMount(syncSessions);

const showDisconnectedToast = () =>
toastsShow({
text: $i18n.wallet_connect.info.disconnected,
level: 'info',
duration: 2000
});

// Disconnect a single dApp by topic; the list updates in place and the other dApps stay connected.
// The service catches its own errors, so only surface the success toast when it actually succeeded.
const disconnectOne = async (topic: string) => {
const { success } = await disconnectSession(topic);

if (success) {
showDisconnectedToast();
}
};

// Tear down every connection at once, preserving the previous one-tap teardown behaviour.
const disconnectAll = async () => {
await disconnectListener();

showDisconnectedToast();

modalStore.close();
};

Expand Down Expand Up @@ -120,7 +136,17 @@
{/snippet}

{#snippet action()}
<Button colorStyle="error" onclick={disconnect} paddingSmall transparent>
<Button
ariaLabel={replacePlaceholders($i18n.wallet_connect.text.disconnect_app, {
$name: name
})}
colorStyle="error"
onclick={() => disconnectOne(session.topic)}
paddingSmall
testId={`wallet-connect-disconnect-session-${session.topic}`}
transparent
type="button"
>
<IconLinkOff />
</Button>
{/snippet}
Expand All @@ -131,6 +157,16 @@

{#snippet toolbar()}
<ButtonGroup>
{#if sessions.length > 0}
<Button
colorStyle="error"
onclick={disconnectAll}
testId="wallet-connect-disconnect-all"
type="button"
>
{$i18n.wallet_connect.text.disconnect_all}
</Button>
{/if}
<ButtonCloseModal />
</ButtonGroup>
{/snippet}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "التطبيقات المتصلة",
"no_connected_apps": "لا توجد تطبيقات متصلة حاليًا.",
"disconnect_all": "قطع اتصال الكل",
"disconnect_app": "قطع الاتصال بـ $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Připojené aplikace",
"no_connected_apps": "Aktuálně nejsou připojeny žádné aplikace.",
"disconnect_all": "Odpojit vše",
"disconnect_app": "Odpojit $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Verbundene Apps",
"no_connected_apps": "Derzeit sind keine Apps verbunden.",
"disconnect_all": "Alle trennen",
"disconnect_app": "$name trennen",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "This request asks the wallet to broadcast the transaction, which is not supported. For your security, it cannot be approved. Request the transaction with \"broadcast: false\" to receive the signed PSBT and broadcast it yourself.",
"connected_apps": "Connected Apps",
"no_connected_apps": "No apps are currently connected.",
"disconnect_all": "Disconnect all",
"disconnect_app": "Disconnect $name",
"wallet_connect": "WalletConnect"
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Aplicaciones conectadas",
"no_connected_apps": "Actualmente no hay aplicaciones conectadas.",
"disconnect_all": "Desconectar todas",
"disconnect_app": "Desconectar $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Applications connectées",
"no_connected_apps": "Aucune application n'est actuellement connectée.",
"disconnect_all": "Tout déconnecter",
"disconnect_app": "Déconnecter $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "जुड़े हुए ऐप्स",
"no_connected_apps": "वर्तमान में कोई ऐप्स कनेक्ट नहीं हैं।",
"disconnect_all": "सभी डिस्कनेक्ट करें",
"disconnect_app": "$name को डिस्कनेक्ट करें",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "App connesse",
"no_connected_apps": "Attualmente non ci sono app connesse.",
"disconnect_all": "Disconnetti tutte",
"disconnect_app": "Disconnetti $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "接続済みアプリ",
"no_connected_apps": "現在接続されているアプリはありません。",
"disconnect_all": "すべて切断",
"disconnect_app": "$name を切断",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "연결된 앱",
"no_connected_apps": "현재 연결된 앱이 없습니다.",
"disconnect_all": "모두 연결 해제",
"disconnect_app": "$name 연결 해제",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Połączone aplikacje",
"no_connected_apps": "Obecnie żadne aplikacje nie są połączone.",
"disconnect_all": "Rozłącz wszystkie",
"disconnect_app": "Rozłącz $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Aplicativos conectados",
"no_connected_apps": "Nenhum aplicativo está conectado no momento.",
"disconnect_all": "Desconectar todos",
"disconnect_app": "Desconectar $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Подключённые приложения",
"no_connected_apps": "В настоящее время нет подключённых приложений.",
"disconnect_all": "Отключить все",
"disconnect_app": "Отключить $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "Ứng dụng đã kết nối",
"no_connected_apps": "Hiện không có ứng dụng nào được kết nối.",
"disconnect_all": "Ngắt kết nối tất cả",
"disconnect_app": "Ngắt kết nối $name",
"wallet_connect": ""
},
"alt": {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,8 @@
"psbt_broadcast_unsupported_note": "",
"connected_apps": "已连接的应用",
"no_connected_apps": "当前没有已连接的应用。",
"disconnect_all": "断开所有连接",
"disconnect_app": "断开与 $name 的连接",
"wallet_connect": ""
},
"alt": {
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/src/lib/providers/wallet-connect.providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,15 @@ export class WalletConnectClient extends WalletConnectListener {
getActiveSessions = (): Record<string, SessionTypes.Struct> =>
this.#walletKit.getActiveSessions();

// Disconnect a single session by topic. Unlike `disconnect`, this leaves other active sessions
// and all pairings untouched, so the remaining connected dApps keep working.
disconnectSession = async (topic: string): Promise<void> => {
await this.#walletKit.disconnectSession({
topic,
reason: getSdkError('USER_DISCONNECTED')
});
};

disconnect = async () => {
const disconnectPairings = async () => {
const pairings = this.#walletKit.engine.signClient.core.pairing.pairings.values;
Expand Down
Loading
Loading