diff --git a/sdk/apps/modal-example/package.json b/sdk/apps/modal-example/package.json index 0f5dbf66..0ee89184 100644 --- a/sdk/apps/modal-example/package.json +++ b/sdk/apps/modal-example/package.json @@ -17,12 +17,13 @@ "dependencies": { "@mysten/sui.js": "^0.42.0", "@mysten/wallet-adapter-wallet-standard": "^0.8.0", - "@nightlylabs/wallet-selector-solana": "0.2.7", + "@nightlylabs/wallet-selector-solana": "0.3.0", "@nightlylabs/nightly-connect-solana": "0.0.29", "@nightlylabs/nightly-connect-sui": "0.0.29", "@nightlylabs/wallet-selector-sui": "0.2.7", "@nightlylabs/nightly-connect-polkadot": "0.0.14", - "@nightlylabs/wallet-selector-polkadot": "0.2.0", + "@nightlylabs/wallet-selector-polkadot": "0.2.2", + "@nightlylabs/wallet-selector-base": "^0.4.0", "@polkadot/extension-inject": "^0.46.5", "@polkadot/api": "^10.10.1", "@solana/web3.js": "^1.77.2", @@ -38,4 +39,4 @@ "engines": { "node": ">=16.8" } -} \ No newline at end of file +} diff --git a/sdk/apps/modal-example/src/routes/aleph.tsx b/sdk/apps/modal-example/src/routes/aleph.tsx index ed5ea9e6..4bda427a 100644 --- a/sdk/apps/modal-example/src/routes/aleph.tsx +++ b/sdk/apps/modal-example/src/routes/aleph.tsx @@ -29,7 +29,6 @@ export default function Polkadot() { ) adapter.canEagerConnect().then((canEagerConnect: boolean) => { - console.log('canEagerConnect', canEagerConnect) setEager(canEagerConnect) }) setAdapter(adapter) @@ -67,12 +66,16 @@ export default function Polkadot() { fallback={ diff --git a/sdk/apps/modal-example/src/routes/alephCustom.tsx b/sdk/apps/modal-example/src/routes/alephCustom.tsx index b411a4fa..8c8c3b8f 100644 --- a/sdk/apps/modal-example/src/routes/alephCustom.tsx +++ b/sdk/apps/modal-example/src/routes/alephCustom.tsx @@ -29,7 +29,7 @@ export default function Polkadot() { }, network: 'AlephZero' }, - true, // change this to false to test disabling eager connect + {}, // change this to false to test disabling eager connect document.getElementById('modalAnchor'), { variablesOverride: { @@ -97,12 +97,16 @@ export default function Polkadot() { fallback={ diff --git a/sdk/apps/modal-example/src/routes/alephInitOnConnect.tsx b/sdk/apps/modal-example/src/routes/alephInitOnConnect.tsx index cec5003e..c64db8d8 100644 --- a/sdk/apps/modal-example/src/routes/alephInitOnConnect.tsx +++ b/sdk/apps/modal-example/src/routes/alephInitOnConnect.tsx @@ -13,7 +13,7 @@ export default function Polkadot() { const provider = new WsProvider('wss://ws.test.azero.dev/') onMount(async () => { - const adapter = NightlyConnectAdapter.buildWithInitOnConnect( + const adapter = NightlyConnectAdapter.buildLazy( { appMetadata: { name: 'NC TEST AlephZero', @@ -23,10 +23,12 @@ export default function Polkadot() { }, network: 'AlephZero' }, - true, // change this to false to test disabling eager connect + { initOnConnect: true }, // change this to false to test disabling eager connect document.getElementById('modalAnchor') ) - + adapter.on('connect', (a) => { + console.log('adapter connected', a) + }) setAdapter(adapter) ApiPromise.create({ @@ -45,12 +47,16 @@ export default function Polkadot() { fallback={ diff --git a/sdk/apps/modal-example/src/routes/externalModal.tsx b/sdk/apps/modal-example/src/routes/externalModal.tsx new file mode 100644 index 00000000..d3f903b7 --- /dev/null +++ b/sdk/apps/modal-example/src/routes/externalModal.tsx @@ -0,0 +1,155 @@ +import { createEffect, createSignal, onMount, Show } from 'solid-js' +import { Title } from 'solid-start' +import { NightlyConnectAdapter } from '@nightlylabs/wallet-selector-solana' +import { Connection, PublicKey, SystemProgram, Transaction as SolanaTx } from '@solana/web3.js' +import toast from 'solid-toast' +import { AppInitData, NightlyConnectSelectorModal } from '@nightlylabs/wallet-selector-base' +import { SOLANA_NETWORK } from '@nightlylabs/nightly-connect-solana' + +const connection = new Connection('https://api.devnet.solana.com') + +export default function SolanaExternalModal() { + const [adapter, setAdapter] = createSignal() + const [modal, setModal] = createSignal() + const [eager, setEager] = createSignal(false) + const [publicKey, setPublicKey] = createSignal() + onMount(async () => { + const appInitData: AppInitData = { + appMetadata: { + name: 'NCTestSolana', + description: 'Nightly Connect Test', + icon: 'https://docs.nightly.app/img/logo.png', + additionalInfo: 'Courtesy of Nightly Connect team' + }, + persistent: true + } + + const adapter = await NightlyConnectAdapter.build( + appInitData, + { disableModal: true }, + document.getElementById('modalAnchor') + ) + + const modal = new NightlyConnectSelectorModal( + adapter.walletsList, + appInitData.url ?? 'https://nc2.nightly.app', + { + name: SOLANA_NETWORK, + icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' + }, + document.getElementById('modalAnchor') + ) + + setModal(modal) + + adapter.on('connect', (pk) => { + modal.closeModal() + setPublicKey(pk) + }) + + adapter.on('disconnect', () => { + modal.walletsList = adapter.walletsList + setPublicKey(undefined) + }) + + adapter.canEagerConnect().then((canEagerConnect) => { + setEager(canEagerConnect) + }) + + setAdapter(adapter) + }) + + createEffect(() => { + if (eager()) { + adapter() + ?.connect() + .then( + () => { + console.log('connect resolved successfully') + }, + () => { + console.log('connect rejected') + } + ) + } + }) + + return ( +
+ Solana with External Modal Example +
+ { + if (adapter()?.connecting) { + console.log('Cannot connect while connecting') + return + } + + if (adapter()?.connected) { + return + } + + modal()?.openModal(adapter()?.sessionId ?? undefined, async (walletName) => { + try { + modal()?.setStandardWalletConnectProgress(true) + await adapter()?.connectToWallet(walletName) + } catch (err) { + modal()?.setStandardWalletConnectProgress(false) + console.log('error') + modal()?.closeModal() + } + }) + }}> + Connect + + }> +

Current public key: {publicKey()!.toString()}

+ + + +
+
+ ) +} diff --git a/sdk/apps/modal-example/src/routes/index.tsx b/sdk/apps/modal-example/src/routes/index.tsx index 1c9bce56..0f052fba 100644 --- a/sdk/apps/modal-example/src/routes/index.tsx +++ b/sdk/apps/modal-example/src/routes/index.tsx @@ -14,6 +14,9 @@ export default function Home() { + + + diff --git a/sdk/apps/modal-example/src/routes/solana.tsx b/sdk/apps/modal-example/src/routes/solana.tsx index 4831f50b..7220aeb7 100644 --- a/sdk/apps/modal-example/src/routes/solana.tsx +++ b/sdk/apps/modal-example/src/routes/solana.tsx @@ -20,7 +20,7 @@ export default function Solana() { additionalInfo: 'Courtesy of Nightly Connect team' } }, - true, + {}, document.getElementById('modalAnchor') ).then((adapter) => { adapter.on('connect', (pk) => { diff --git a/sdk/apps/modal-example/src/routes/solanaInitOnConnect.tsx b/sdk/apps/modal-example/src/routes/solanaInitOnConnect.tsx index d89e360f..7803ca4e 100644 --- a/sdk/apps/modal-example/src/routes/solanaInitOnConnect.tsx +++ b/sdk/apps/modal-example/src/routes/solanaInitOnConnect.tsx @@ -10,7 +10,7 @@ export default function SolanaLazy() { const [adapter, setAdapter] = createSignal() const [publicKey, setPublicKey] = createSignal() onMount(() => { - const adapter = NightlyConnectAdapter.buildWithInitOnConnect( + const adapter = NightlyConnectAdapter.buildLazy( { appMetadata: { name: 'NCTestSolana', @@ -20,7 +20,7 @@ export default function SolanaLazy() { }, url: 'https://nc2.nightly.app' }, - true, + { initOnConnect: true }, document.getElementById('modalAnchor') ) diff --git a/sdk/apps/modal-example/src/routes/solanaLazy.tsx b/sdk/apps/modal-example/src/routes/solanaLazy.tsx index 7772d83b..6209f1c0 100644 --- a/sdk/apps/modal-example/src/routes/solanaLazy.tsx +++ b/sdk/apps/modal-example/src/routes/solanaLazy.tsx @@ -21,7 +21,7 @@ export default function SolanaLazy() { }, url: 'https://nc2.nightly.app' }, - true, + {}, document.getElementById('modalAnchor') ) diff --git a/sdk/packages/modal/package.json b/sdk/packages/modal/package.json index b994bca5..e4f0ff2b 100644 --- a/sdk/packages/modal/package.json +++ b/sdk/packages/modal/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/wallet-selector-modal", - "version": "0.2.0", + "version": "0.2.1", "type": "module", "exports": { ".": { @@ -55,4 +55,4 @@ "typescript": "^5.0.2", "vite": "^4.3.9" } -} \ No newline at end of file +} diff --git a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts index 53f1a457..6f376ae7 100644 --- a/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts +++ b/sdk/packages/modal/src/components/nightly-desktop-main/nightly-desktop-main.stories.ts @@ -154,7 +154,7 @@ export const Error: Story = (args: NightlyModalArgs) => { if (!args.sessionId) setTimeout(() => { - updateArgs({ timeoutError: true }) + updateArgs({ timeoutError: 'error' }) }, 5000) return html` = {} - @property({ type: Boolean }) - timeoutError = false + @property({ type: String }) + timeoutError = '' @state() copyMessage = 'Copy' @@ -113,19 +113,20 @@ export class NightlyDesktopMain extends LitElement { - ${!this.isSessionIdImmediatelyDefined && - html`
- Loading -

Generating QR code...

-
`} + ${!this.isSessionIdImmediatelyDefined + ? html`
+ Loading +

Generating QR code...

+
` + : html``}
- ${!this.isSessionIdImmediatelyDefined && - html`
- - Loading -

Generating QR code...

-
`} + ${!this.isSessionIdImmediatelyDefined + ? html`
+ + Loading +

Generating QR code...

+
` + : html``}
boolean, - recentWalletName?: string -) => { - const { get } = getWallets() - const windowWallets = get() - - const walletsData: Record = {} - - presetList.forEach((wallet) => { - walletsData[wallet.name] = { - ...wallet, - recent: recentWalletName === wallet.name - } - }) - - windowWallets.filter(walletsFilterCb).forEach((wallet) => { - walletsData[wallet.name] = { - ...(walletsData?.[wallet.name] ?? { - name: wallet.name, - icon: wallet.icon, - link: '', - deeplink: null, - recent: recentWalletName === wallet.name, - walletType: 'hybrid' - }), - detected: true, - standardWallet: wallet - } - }) - - return Object.values(walletsData) -} diff --git a/sdk/packages/selector-base/src/index.ts b/sdk/packages/selector-base/src/index.ts index 62a90dd8..e31ac429 100644 --- a/sdk/packages/selector-base/src/index.ts +++ b/sdk/packages/selector-base/src/index.ts @@ -1,7 +1,6 @@ -export * from './detection' export * from './persistence' export * from './utils' export * from './types' export * from './logoBase64' export * from './modal' -export { type XMLOptions } from '@nightlylabs/wallet-selector-modal' \ No newline at end of file +export { type XMLOptions } from '@nightlylabs/wallet-selector-modal' diff --git a/sdk/packages/selector-base/src/modal.ts b/sdk/packages/selector-base/src/modal.ts index 44daf443..1b3aa243 100644 --- a/sdk/packages/selector-base/src/modal.ts +++ b/sdk/packages/selector-base/src/modal.ts @@ -1,6 +1,9 @@ -import { type XMLOptions, type NightlySelector } from '@nightlylabs/wallet-selector-modal' +import { + type XMLOptions, + type NightlySelector, + WalletSelectorItem +} from '@nightlylabs/wallet-selector-modal' import { type IWalletListItem, type NetworkData } from './types' -import { isMobileBrowser } from './utils' export class NightlyConnectSelectorModal { _modal: NightlySelector | undefined @@ -40,12 +43,13 @@ export class NightlyConnectSelectorModal { } set walletsList(list: IWalletListItem[]) { - const filtered = list.filter((w) => - isMobileBrowser() ? w.walletType !== 'extension' : w.walletType !== 'mobile' - ) - this._walletsList = filtered + this._walletsList = list if (this._modal) { - this._modal.selectorItems = filtered + this._modal.selectorItems = list.map((item) => ({ + ...item, + icon: item.image.default, + link: item.homepage + })) as WalletSelectorItem[] } } @@ -53,6 +57,10 @@ export class NightlyConnectSelectorModal { if (this._modal && id) this._modal.sessionId = id } + set timeoutError(error: string) { + if (this._modal && error) this._modal.timeoutError = error + } + createSelectorElement = ( variablesOverride?: object, stylesOverride?: string, @@ -65,7 +73,11 @@ export class NightlyConnectSelectorModal { this._modal.relay = this._relay this._modal.chainIcon = this._networkData.icon this._modal.chainName = this._networkData.name - this._modal.selectorItems = this.walletsList + this._modal.selectorItems = this.walletsList.map((item) => ({ + ...item, + icon: item.image.default, + link: item.homepage + })) as WalletSelectorItem[] }) } diff --git a/sdk/packages/selector-base/src/types.ts b/sdk/packages/selector-base/src/types.ts index 0a14a253..39676310 100644 --- a/sdk/packages/selector-base/src/types.ts +++ b/sdk/packages/selector-base/src/types.ts @@ -2,6 +2,8 @@ import { type AppBaseInitialize } from '@nightlylabs/nightly-connect-base' import { type Deeplink } from '@nightlylabs/nightly-connect-base/dist/types/bindings/Deeplink' import { type Wallet } from '@wallet-standard/core' import { type WalletType } from '../../../bindings/WalletType' +import { WalletMetadata } from '../../../bindings/WalletMetadata' +export { type WalletMetadata } from '../../../bindings/WalletMetadata' export interface Adapter { connect: () => Promise @@ -17,7 +19,11 @@ export interface MetadataWallet { walletType: WalletType } -export interface IWalletListItem extends MetadataWallet { +export interface IWalletListItem + extends Pick< + WalletMetadata, + 'name' | 'slug' | 'walletType' | 'mobile' | 'desktop' | 'image' | 'homepage' + > { recent?: boolean detected?: boolean standardWallet?: Wallet diff --git a/sdk/packages/selector-polkadot/package.json b/sdk/packages/selector-polkadot/package.json index 014cd889..b1feef76 100644 --- a/sdk/packages/selector-polkadot/package.json +++ b/sdk/packages/selector-polkadot/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/wallet-selector-polkadot", - "version": "0.2.1", + "version": "0.2.2", "description": "", "type": "module", "exports": { @@ -25,10 +25,11 @@ "license": "ISC", "dependencies": { "@nightlylabs/nightly-connect-polkadot": "^0.0.15", - "@nightlylabs/wallet-selector-base": "^0.3.1", - "@polkadot/extension-inject": "0.46.5", + "@nightlylabs/wallet-selector-base": "^0.4.0", "@polkadot/api": "10.10.1", - "@wallet-standard/core": "^1.0.3" + "@polkadot/extension-inject": "0.46.5", + "@wallet-standard/core": "^1.0.3", + "eventemitter3": "^5.0.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/sdk/packages/selector-polkadot/src/adapter.ts b/sdk/packages/selector-polkadot/src/adapter.ts index 99b5a253..4ec9cb49 100644 --- a/sdk/packages/selector-polkadot/src/adapter.ts +++ b/sdk/packages/selector-polkadot/src/adapter.ts @@ -1,13 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { - AppPolkadot, - AppPolkadotInitialize, - WalletMetadata -} from '@nightlylabs/nightly-connect-polkadot' +import { AppPolkadot, AppPolkadotInitialize } from '@nightlylabs/nightly-connect-polkadot' import { ConnectionOptions, ConnectionType, + IWalletListItem, NightlyConnectSelectorModal, + WalletMetadata, XMLOptions, clearRecentWalletForNetwork, clearSessionIdForNetwork, @@ -21,13 +19,24 @@ import { } from '@nightlylabs/wallet-selector-base' import { type Signer as InjectedSigner } from '@polkadot/api/types' -import { type Injected } from '@polkadot/extension-inject/types' +import { InjectedAccount, type Injected } from '@polkadot/extension-inject/types' import { IPolkadotWalletListItem, getPolkadotWalletsList } from './detection' import { networkToData, SupportedNetworks } from './utils' +import EventEmitter from 'eventemitter3' + export type AppSelectorInitialize = Omit & { network: SupportedNetworks } -export class NightlyConnectAdapter implements Injected { + +type NightlyConnectAdapterEvents = { + connect(publicKey: InjectedAccount[]): void + disconnect(): void +} + +export class NightlyConnectAdapter + extends EventEmitter + implements Injected +{ name = 'Nightly Connect' url = 'https://nightly.app' icon = logoBase64 @@ -50,6 +59,7 @@ export class NightlyConnectAdapter implements Injected { private _loading: boolean constructor(appInitData: AppSelectorInitialize, connectionOptions?: ConnectionOptions) { + super() this._connecting = false this._connected = false this._appInitData = appInitData @@ -181,7 +191,26 @@ export class NightlyConnectAdapter implements Injected { metadataWallets, getRecentWalletForNetwork(adapter.network)?.walletName ?? undefined ) + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(adapter.network, { + walletName: adapter._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + if (!adapter._app || adapter._app.accounts.activeAccounts.length <= 0) { + adapter._connected = false + // If user does not pass any accounts, we should disconnect + adapter.disconnect() + return + } + adapter._connected = true + adapter.emit('connect', await adapter.accounts.get()) + } catch { + adapter.disconnect() + } + }) return adapter } @@ -212,7 +241,7 @@ export class NightlyConnectAdapter implements Injected { if (!adapter._connectionOptions.disableModal) { adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, + adapter.walletsList as IWalletListItem[], appInitData.url ?? 'https://nc2.nightly.app', networkToData(adapter.network), anchorRef, @@ -235,6 +264,26 @@ export class NightlyConnectAdapter implements Injected { ) adapter._loading = false + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(adapter.network, { + walletName: adapter._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!adapter._app || adapter._app.accounts.activeAccounts.length <= 0) { + adapter._connected = false + // If user does not pass any accounts, we should disconnect + adapter.disconnect() + return + } + adapter._connected = true + adapter.emit('connect', await adapter.accounts.get()) + } catch { + adapter.disconnect() + } + }) }) .catch(() => { adapter._loading = false @@ -309,21 +358,21 @@ export class NightlyConnectAdapter implements Injected { throw new Error('Wallet not found') } - if (wallet.deeplink === null) { + if (wallet.mobile === null) { throw new Error('Deeplink not found') } // If we have a native deeplink, we should use it - if (wallet.deeplink.native !== null) { + if (wallet.mobile.native !== null) { this._app.connectDeeplink({ walletName: wallet.name, - url: wallet.deeplink.native + url: wallet.mobile.native }) this._chosenMobileWalletName = walletName triggerConnect( - wallet.deeplink.native, + wallet.mobile.native, this._app.sessionId, this._appInitData.url ?? 'https://nc2.nightly.app' ) @@ -331,16 +380,16 @@ export class NightlyConnectAdapter implements Injected { } // If we have a universal deeplink, we should use it - if (wallet.deeplink.universal !== null) { + if (wallet.mobile.universal !== null) { this._app.connectDeeplink({ walletName: wallet.name, - url: wallet.deeplink.universal + url: wallet.mobile.universal }) this._chosenMobileWalletName = walletName triggerConnect( - wallet.deeplink.universal, + wallet.mobile.universal, this._app.sessionId, this._appInitData.url ?? 'https://nc2.nightly.app' ) @@ -348,8 +397,8 @@ export class NightlyConnectAdapter implements Injected { } // Fallback to redirecting to app browser // aka browser inside the app - if (!wallet.deeplink.redirectToAppBrowser) { - const redirectToAppBrowser = wallet.deeplink.redirectToAppBrowser + if (!wallet.mobile.redirectToAppBrowser) { + const redirectToAppBrowser = wallet.mobile.redirectToAppBrowser if (redirectToAppBrowser !== null && redirectToAppBrowser.indexOf('{{url}}') > -1) { const url = redirectToAppBrowser.replace( '{{url}}', @@ -407,6 +456,7 @@ export class NightlyConnectAdapter implements Injected { this._connected = true this._connecting = false + this.emit('connect', await this.accounts.get()) persistRecentWalletForNetwork(this.network, { walletName, @@ -458,6 +508,7 @@ export class NightlyConnectAdapter implements Injected { try { this._connected = true this._connecting = false + this.emit('connect', await this.accounts.get()) resolve() return } catch (error) { @@ -484,11 +535,33 @@ export class NightlyConnectAdapter implements Injected { metadataWallets, getRecentWalletForNetwork(this.network)?.walletName ?? undefined ) + + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(this.network, { + walletName: this._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!this._app || this._app.accounts.activeAccounts.length <= 0) { + this._connected = false + // If user does not pass any accounts, we should disconnect + this.disconnect() + return + } + this._connected = true + this.emit('connect', await this.accounts.get()) + } catch { + this.disconnect() + } + }) this._loading = false }) .catch(() => { this._loading = false - throw new Error('Failed to initialize adapter') + reject('Failed to initialize adapter') + return }) } // Interval that checks if app has connected @@ -513,8 +586,12 @@ export class NightlyConnectAdapter implements Injected { ) { this.connectToMobileWallet(walletName) } else { - await this.connectToStandardWallet(walletName) - resolve() + try { + await this.connectToStandardWallet(walletName) + resolve() + } catch (error) { + reject(error) + } } }) @@ -526,26 +603,20 @@ export class NightlyConnectAdapter implements Injected { // Clear interval if app is connected clearInterval(loadingInterval) if (this._modal) this._modal.sessionId = this._app.sessionId - this._app.on('userConnected', () => { + // We already have hook for userConnected + // This is just for resolving promise + this._app.on('userConnected', async () => { try { - persistRecentWalletForNetwork(this.network, { - walletName: this._chosenMobileWalletName || '', - walletType: ConnectionType.Nightly - }) - if (!this._app || this._app.accounts.activeAccounts.length <= 0) { - this._connecting = false - this._connected = false - // If user does not pass any accounts, we should disconnect - this.disconnect() - return + reject(new Error('No accounts found')) } this._connected = true - this._connecting = false this._modal?.closeModal() resolve() - } catch { - this.disconnect() + } catch (error) { + reject(error) + } finally { + this._connecting = false } }) return @@ -555,7 +626,7 @@ export class NightlyConnectAdapter implements Injected { if (checks > 500) { clearInterval(loadingInterval) // reject(new Error('Connecting takes too long')) - // TODO we need to have a way to show error on modal + if (this._modal) this._modal.timeoutError = 'Connecting is taking too long' } }, 10) } @@ -563,8 +634,6 @@ export class NightlyConnectAdapter implements Injected { } catch (error: any) { this._connecting = false reject(error) - } finally { - this._connecting = false } } @@ -599,11 +668,12 @@ export class NightlyConnectAdapter implements Injected { getRecentWalletForNetwork(this.network)?.walletName ?? undefined ) if (this._modal) { - this._modal.walletsList = this.walletsList + this._modal.walletsList = this.walletsList as IWalletListItem[] } this._connected = false } finally { this._connecting = false + this.emit('disconnect') } } } diff --git a/sdk/packages/selector-polkadot/src/detection.ts b/sdk/packages/selector-polkadot/src/detection.ts index 829a04aa..30615deb 100644 --- a/sdk/packages/selector-polkadot/src/detection.ts +++ b/sdk/packages/selector-polkadot/src/detection.ts @@ -1,8 +1,8 @@ -import { type Injected, type InjectedExtension } from '@polkadot/extension-inject/types' -import { type WalletIcon } from '@wallet-standard/core' +import { Injected, InjectedExtension } from '@polkadot/extension-inject/types' +import { WalletIcon } from '@wallet-standard/core' import { appToIcon } from './tempIcons' -import { IWalletListItem } from '@nightlylabs/wallet-selector-base' -import { WalletMetadata } from '@nightlylabs/nightly-connect-polkadot' +import { WalletMetadata } from '@nightlylabs/wallet-selector-base' + export interface PolkadotWalletInjected { // Default Polkadot standard connect?: (origin: string) => Promise // Is this even used ? @@ -32,7 +32,13 @@ export const getPolkadotWallets = (): PolkadotWalletInjected[] => { } } -export interface IPolkadotWalletListItem extends Omit { +export interface IPolkadotWalletListItem + extends Pick< + WalletMetadata, + 'name' | 'slug' | 'walletType' | 'mobile' | 'desktop' | 'image' | 'homepage' + > { + recent?: boolean + detected?: boolean injectedWallet?: PolkadotWalletInjected } @@ -43,12 +49,7 @@ export const getPolkadotWalletsList = (presetList: WalletMetadata[], recentWalle presetList.forEach((wallet) => { walletsData[wallet.slug.toLocaleLowerCase()] = { - slug: wallet.slug, - name: wallet.name, - icon: wallet.image.default, - deeplink: wallet.mobile, - link: wallet.homepage, - walletType: wallet.walletType, + ...wallet, recent: recentWalletName === wallet.name } }) @@ -57,48 +58,42 @@ export const getPolkadotWalletsList = (presetList: WalletMetadata[], recentWalle // by namespace if (walletsData[wallet.slug.toLocaleLowerCase()]) { walletsData[wallet.slug.toLocaleLowerCase()] = { - ...(walletsData?.[wallet.slug.toLocaleLowerCase()] ?? { - name: wallet.name, - icon: wallet.icon, - link: '', - deeplink: null, - recent: recentWalletName === wallet.name, - walletType: 'hybrid' - }), + ...walletsData?.[wallet.slug.toLocaleLowerCase()], + recent: recentWalletName === wallet.name, detected: true, - injectedWallet: wallet + injectedWallet: wallet, + walletType: 'hybrid' } - continue } // Check if wallet is already in the list // by name - if (walletsData[wallet.name.toLocaleLowerCase()]) { + else if (walletsData[wallet.name.toLocaleLowerCase()]) { walletsData[wallet.name.toLocaleLowerCase()] = { - ...(walletsData?.[wallet.name.toLocaleLowerCase()] ?? { - name: wallet.name, - icon: wallet.icon, - link: '', - deeplink: null, - recent: recentWalletName === wallet.name, - walletType: 'hybrid' - }), + ...walletsData[wallet.name.toLocaleLowerCase()], + recent: recentWalletName === wallet.name, detected: true, - injectedWallet: wallet + injectedWallet: wallet, + walletType: 'hybrid' + } + } else + walletsData[wallet.name.toLocaleLowerCase()] = { + slug: wallet.name, + name: wallet.name, + image: { + default: wallet.icon as string, + sm: wallet.icon as string, + md: wallet.icon as string, + lg: wallet.icon as string + }, + desktop: null, + mobile: null, + recent: recentWalletName === wallet.name, + detected: true, + injectedWallet: wallet, + walletType: 'hybrid', + homepage: 'https://nightly.app/download' } - continue - } - walletsData[wallet.name.toLocaleLowerCase()] = { - slug: wallet.name, - name: wallet.name, - icon: wallet.icon as string, - link: '', - deeplink: null, - recent: recentWalletName === wallet.name, - detected: true, - injectedWallet: wallet, - walletType: 'hybrid' - } } return Object.values(walletsData) diff --git a/sdk/packages/selector-solana/package.json b/sdk/packages/selector-solana/package.json index bed9c9cb..a63cdc6b 100644 --- a/sdk/packages/selector-solana/package.json +++ b/sdk/packages/selector-solana/package.json @@ -1,6 +1,6 @@ { "name": "@nightlylabs/wallet-selector-solana", - "version": "0.2.7", + "version": "0.3.0", "description": "", "type": "module", "exports": { @@ -25,7 +25,7 @@ "license": "ISC", "dependencies": { "@nightlylabs/nightly-connect-solana": "^0.0.29", - "@nightlylabs/wallet-selector-base": "^0.2.4", + "@nightlylabs/wallet-selector-base": "^0.4.0", "@solana/wallet-adapter-base": "^0.9.22", "@solana/wallet-standard": "^1.0.2", "@solana/web3.js": "^1.77.2", @@ -41,4 +41,4 @@ "tslib": "^2.5.3", "typescript": "^5.1.3" } -} \ No newline at end of file +} diff --git a/sdk/packages/selector-solana/src/adapter.ts b/sdk/packages/selector-solana/src/adapter.ts index 18f1e848..911c0c62 100644 --- a/sdk/packages/selector-solana/src/adapter.ts +++ b/sdk/packages/selector-solana/src/adapter.ts @@ -1,26 +1,27 @@ import { AppSolana, SOLANA_NETWORK } from '@nightlylabs/nightly-connect-solana' import { AppInitData, - clearRecentStandardWalletForNetwork, + clearRecentWalletForNetwork, clearSessionIdForNetwork, - getRecentStandardWalletForNetwork, - getWalletsList, + getRecentWalletForNetwork, isMobileBrowser, - IWalletListItem, logoBase64, - MetadataWallet, NightlyConnectSelectorModal, - persistRecentStandardWalletForNetwork, - persistStandardConnectForNetwork, - isStandardConnectedForNetwork, + persistRecentWalletForNetwork, triggerConnect, - persistStandardDisconnectForNetwork, sleep, - XMLOptions + XMLOptions, + ConnectionType, + ConnectionOptions, + defaultConnectionOptions, + WalletMetadata, + IWalletListItem } from '@nightlylabs/wallet-selector-base' import { BaseMessageSignerWalletAdapter, WalletAdapterCompatibleStandardWallet, + WalletAdapterEvents, + WalletError, WalletName, WalletNotConnectedError, WalletNotReadyError, @@ -32,7 +33,12 @@ import { } from '@solana/wallet-adapter-base' import { StandardWalletAdapter } from '@solana/wallet-standard' import { PublicKey, Transaction, TransactionVersion, VersionedTransaction } from '@solana/web3.js' -import { solanaWalletsFilter } from './detection' +import { getSolanaWalletsList } from './detection' +import { StandardEventsChangeProperties } from '@wallet-standard/core' + +type NightlyConnectAdapterEvents = WalletAdapterEvents & { + change(properties: StandardEventsChangeProperties): void +} export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { name = 'Nightly Connect' as WalletName<'Nightly Connect'> @@ -55,31 +61,31 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { private _modal: NightlyConnectSelectorModal | undefined private _appInitData: AppInitData - private _eagerConnectForStandardWallets: boolean - private _metadataWallets: MetadataWallet[] = [] + private _metadataWallets: WalletMetadata[] = [] private _walletsList: IWalletListItem[] = [] private _chosenMobileWalletName: string | undefined private _loading: boolean - private _initOnConnect: boolean + private _connectionOptions: ConnectionOptions = defaultConnectionOptions - constructor( - appInitData: AppInitData, - eagerConnectForStandardWallets?: boolean, - initOnConnect = false - ) { + constructor(appInitData: AppInitData, connectionOptions?: ConnectionOptions) { super() this._connecting = false this._connected = false this._publicKey = null this._appInitData = appInitData - this._eagerConnectForStandardWallets = !!eagerConnectForStandardWallets + if (appInitData.persistent !== false) this._appInitData.persistent = true + this._appSessionActive = false this._loading = false - this._initOnConnect = initOnConnect + this._connectionOptions = { ...this._connectionOptions, ...connectionOptions } + // If not persistent, clear session id + if (!this._appInitData.persistent) { + clearSessionIdForNetwork(SOLANA_NETWORK) + } } get connecting() { @@ -109,26 +115,23 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { } } + get sessionId() { + return this._app?.sessionId + } + + get qrCode() { + return this._modal?.qrCode + } + public static initApp = async ( appInitData: AppInitData - ): Promise<[AppSolana, MetadataWallet[]]> => { + ): Promise<[AppSolana, WalletMetadata[]]> => { try { return await Promise.all([ AppSolana.build(appInitData), AppSolana.getWalletsMetadata( `${appInitData.url ?? 'https://nc2.nightly.app'}/get_wallets_metadata` ) - .then((list) => - list.map((wallet) => ({ - slug: wallet.slug, - name: wallet.name, - icon: wallet.image.default, - deeplink: wallet.mobile, - link: wallet.homepage, - walletType: wallet.walletType - })) - ) - .catch(() => [] as MetadataWallet[]) ]) } catch { clearSessionIdForNetwork(SOLANA_NETWORK) @@ -137,24 +140,40 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { AppSolana.getWalletsMetadata( `${appInitData.url ?? 'https://nc2.nightly.app'}/get_wallets_metadata` ) - .then((list) => - list.map((wallet) => ({ - slug: wallet.slug, - name: wallet.name, - icon: wallet.image.default, - deeplink: wallet.mobile, - link: wallet.homepage, - walletType: wallet.walletType - })) - ) - .catch(() => [] as MetadataWallet[]) ]) } } + on( + event: T, + fn: NightlyConnectAdapterEvents[T] extends (...args: infer Args) => void + ? (...args: Args) => void + : never, + context?: any + ): this { + if (event === 'change') { + // TODO implement on change listener + return this + } else { + return super.on(event, fn, context) + } + } + + emit( + event: T, + ...args: [publicKey: PublicKey] | [] | [error: WalletError] | [readyState: WalletReadyState] + ): boolean { + if (event === 'change') { + // TODO implement change event emitter + } else { + super.emit(event, ...args) + } + return true + } + public static build = async ( appInitData: AppInitData, - eagerConnectForStandardWallets?: boolean, + connectionOptions?: ConnectionOptions, anchorRef?: HTMLElement | null, uiOverrides?: { variablesOverride?: object @@ -162,48 +181,69 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { qrConfigOverride?: Partial } ) => { - const adapter = new NightlyConnectAdapter(appInitData, eagerConnectForStandardWallets) + const adapter = new NightlyConnectAdapter(appInitData, connectionOptions) if (adapter._readyState === WalletReadyState.Unsupported) { return adapter } - adapter.walletsList = getWalletsList( + adapter.walletsList = getSolanaWalletsList( [], - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined ) - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - { - name: SOLANA_NETWORK, - icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' - }, - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride - ) + if (!adapter._connectionOptions.disableModal) + adapter._modal = new NightlyConnectSelectorModal( + adapter.walletsList, + appInitData.url ?? 'https://nc2.nightly.app', + { + name: SOLANA_NETWORK, + icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' + }, + anchorRef, + uiOverrides?.variablesOverride, + uiOverrides?.stylesOverride, + uiOverrides?.qrConfigOverride + ) const [app, metadataWallets] = await NightlyConnectAdapter.initApp(appInitData) adapter._app = app adapter._metadataWallets = metadataWallets - adapter.walletsList = getWalletsList( + adapter.walletsList = getSolanaWalletsList( metadataWallets, - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined ) + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(SOLANA_NETWORK, { + walletName: adapter._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!adapter._app || adapter._app.connectedPublicKeys.length <= 0) { + adapter._connected = false + // If user does not pass any accounts, we should disconnect + adapter.disconnect() + return + } + adapter._publicKey = adapter._app.connectedPublicKeys[0] + adapter._connected = true + adapter.emit('connect', adapter._publicKey) + } catch { + adapter.disconnect() + } + }) + return adapter } public static buildLazy = ( appInitData: AppInitData, - eagerConnectForStandardWallets?: boolean, + connectionOptions?: ConnectionOptions, anchorRef?: HTMLElement | null, uiOverrides?: { variablesOverride?: object @@ -211,226 +251,247 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { qrConfigOverride?: Partial } ) => { - const adapter = new NightlyConnectAdapter(appInitData, eagerConnectForStandardWallets) + const adapter = new NightlyConnectAdapter(appInitData, connectionOptions) if (adapter._readyState === WalletReadyState.Unsupported) { return adapter } - adapter.walletsList = getWalletsList( + adapter.walletsList = getSolanaWalletsList( [], - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined - ) - - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - { - name: SOLANA_NETWORK, - icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' - }, - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined ) - adapter._loading = true - - NightlyConnectAdapter.initApp(appInitData).then(([app, metadataWallets]) => { - adapter._app = app + // Fetch wallets from registry + adapter.fetchWalletsFromRegistry().then((metadataWallets) => { adapter._metadataWallets = metadataWallets - adapter.walletsList = getWalletsList( + adapter.walletsList = getSolanaWalletsList( metadataWallets, - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined ) - - adapter._loading = false }) - return adapter - } + if (!adapter._connectionOptions.disableModal) + adapter._modal = new NightlyConnectSelectorModal( + adapter.walletsList, + appInitData.url ?? 'https://nc2.nightly.app', + { + name: SOLANA_NETWORK, + icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' + }, + anchorRef, + uiOverrides?.variablesOverride, + uiOverrides?.stylesOverride, + uiOverrides?.qrConfigOverride + ) - public static buildWithInitOnConnect = ( - appInitData: AppInitData, - eagerConnectForStandardWallets?: boolean, - anchorRef?: HTMLElement | null, - uiOverrides?: { - variablesOverride?: object - stylesOverride?: string - qrConfigOverride?: Partial - } - ) => { - const adapter = new NightlyConnectAdapter(appInitData, eagerConnectForStandardWallets, true) + // If init on connect is not enabled, we should initialize app + if (!adapter._connectionOptions.initOnConnect) { + adapter._loading = true - if (adapter._readyState === WalletReadyState.Unsupported) { - return adapter - } + NightlyConnectAdapter.initApp(appInitData) + .then(([app, metadataWallets]) => { + adapter._app = app + adapter._metadataWallets = metadataWallets - adapter.walletsList = getWalletsList( - [], - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined - ) + adapter.walletsList = getSolanaWalletsList( + metadataWallets, + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined + ) - adapter._modal = new NightlyConnectSelectorModal( - adapter.walletsList, - appInitData.url ?? 'https://nc2.nightly.app', - { - name: SOLANA_NETWORK, - icon: 'https://assets.coingecko.com/coins/images/4128/small/solana.png' - }, - anchorRef, - uiOverrides?.variablesOverride, - uiOverrides?.stylesOverride, - uiOverrides?.qrConfigOverride - ) + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(SOLANA_NETWORK, { + walletName: adapter._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!adapter._app || adapter._app.connectedPublicKeys.length <= 0) { + adapter._connected = false + // If user does not pass any accounts, we should disconnect + adapter.disconnect() + return + } + adapter._publicKey = adapter._app.connectedPublicKeys[0] + adapter._connected = true + adapter.emit('connect', adapter._publicKey) + } catch { + adapter.disconnect() + } + }) + + adapter._loading = false + }) + .catch(() => { + adapter._loading = false + throw new Error('Failed to initialize adapter') + }) + } return adapter } + // Checks if we can restore user session canEagerConnect = async () => { - // utility for case if somebody wants to fire connect asap, but doesn't want to show modal if e. g. there was no user connected on the device yet - if (this._loading) { - for (let i = 0; i < 200; i++) { - await sleep(10) + // If eager connect is disabled, we can't eager connect + if (this._connectionOptions.disableEagerConnect) return false - if (!this._loading) { - break - } - } - } - - if (this._loading) { - false - } + // Get recent wallet for network + const recentWallet = getRecentWalletForNetwork(SOLANA_NETWORK) - if (this._app && this._app.hasBeenRestored() && this._app.connectedPublicKeys.length > 0) { - return true - } + // If there is no recent wallet, we can't eager connect + if (recentWallet === null) return false + // If we use wallet standard, we can eager connect if ( - this._eagerConnectForStandardWallets && - getRecentStandardWalletForNetwork(SOLANA_NETWORK) !== null && - isStandardConnectedForNetwork(SOLANA_NETWORK) + recentWallet.walletName !== null && + recentWallet.walletType === ConnectionType.WalletStandard ) { return true } + // If we use nightly connect we need to make sure app is restored + if (recentWallet.walletType === ConnectionType.Nightly) { + if (this._connectionOptions.initOnConnect) { + return false + } + // Wait for app to be restored + if (this._loading) { + for (let i = 0; i < 2000; i++) { + await sleep(10) + if (!this._loading) { + break + } + } + } + + if (this._loading) { + return false + } + + // If app is restored and has connected public keys, we can eager connect + + if (this._app && this._app.hasBeenRestored() && this._app.connectedPublicKeys.length > 0) { + return true + } + } return false } - eagerConnectDeeplink = () => { - if (isMobileBrowser() && this._app) { - const mobileWalletName = getRecentStandardWalletForNetwork(SOLANA_NETWORK) - const wallet = this.walletsList.find((w) => w.name === mobileWalletName) + connectToMobileWallet = (walletName: string) => { + try { + if (this._modal) { + this._modal.setStandardWalletConnectProgress(true) + } + + const wallet = this.walletsList.find((w) => w.name === walletName) + + if (!this._app) { + throw new Error('Wallet not ready') + } if (typeof wallet === 'undefined') { - return + throw new Error('Wallet not found') } - if (wallet.deeplink === null) { - return + if (wallet.mobile === null) { + throw new Error('Deeplink not found') } - if (wallet.deeplink.native !== null) { + + // If we have a native deeplink, we should use it + if (wallet.mobile.native !== null) { this._app.connectDeeplink({ walletName: wallet.name, - url: wallet.deeplink.native + url: wallet.mobile.native }) + + this._chosenMobileWalletName = walletName + + triggerConnect( + wallet.mobile.native, + this._app.sessionId, + this._appInitData.url ?? 'https://nc2.nightly.app' + ) return } - if (wallet.deeplink.universal !== null) { + + // If we have a universal deeplink, we should use it + if (wallet.mobile.universal !== null) { this._app.connectDeeplink({ walletName: wallet.name, - url: wallet.deeplink.universal + url: wallet.mobile.universal }) - return - } - } - } - - connectToMobileWallet = (walletName: string) => { - if (this._modal) { - this._modal.setStandardWalletConnectProgress(true) - } - - const wallet = this.walletsList.find((w) => w.name === walletName) - - if (!this._app || typeof wallet === 'undefined') { - return - } - - if (wallet.deeplink === null) { - return - } - - if (wallet.deeplink.native !== null) { - this._app.connectDeeplink({ - walletName: wallet.name, - url: wallet.deeplink.native - }) - this._chosenMobileWalletName = walletName + this._chosenMobileWalletName = walletName - triggerConnect( - wallet.deeplink.native, - this._app.sessionId, - this._appInitData.url ?? 'https://nc2.nightly.app' - ) - return - } - - if (wallet.deeplink.universal !== null) { - this._app.connectDeeplink({ - walletName: wallet.name, - url: wallet.deeplink.universal - }) - - this._chosenMobileWalletName = walletName - - triggerConnect( - wallet.deeplink.universal, - this._app.sessionId, - this._appInitData.url ?? 'https://nc2.nightly.app' - ) - return + triggerConnect( + wallet.mobile.universal, + this._app.sessionId, + this._appInitData.url ?? 'https://nc2.nightly.app' + ) + return + } + } catch (err) { + clearRecentWalletForNetwork(SOLANA_NETWORK) + if (this._modal) { + this._modal.setStandardWalletConnectProgress(false) + } + throw err } } - connectToStandardWallet = async (walletName: string, onSuccess: () => void) => { + // Generic connect to standard wallet + connectToStandardWallet = async (walletName: string) => { try { if (this._modal) { this._modal.setStandardWalletConnectProgress(true) } - const wallet = this.walletsList.find((w) => w.name === walletName) - if (typeof wallet?.standardWallet === 'undefined') { + const wallet = this.walletsList.find((w) => w.name === walletName)?.standardWallet + if (typeof wallet === 'undefined') { + if (this._modal) { + this._modal.setStandardWalletConnectProgress(false) + } throw new Error('Wallet not found') } const adapter = new StandardWalletAdapter({ - wallet: wallet.standardWallet as WalletAdapterCompatibleStandardWallet + wallet: wallet as WalletAdapterCompatibleStandardWallet }) await adapter.connect() - persistRecentStandardWalletForNetwork(walletName, SOLANA_NETWORK) - persistStandardConnectForNetwork(SOLANA_NETWORK) + this._innerStandardAdapter = adapter this._publicKey = adapter.publicKey + this._connected = true this._connecting = false this.emit('connect', this._publicKey!) + + persistRecentWalletForNetwork(SOLANA_NETWORK, { + walletName, + walletType: ConnectionType.WalletStandard + }) + this._modal?.closeModal() - onSuccess() - } catch { + } catch (err) { // clear recent wallet - persistStandardDisconnectForNetwork(SOLANA_NETWORK) + clearRecentWalletForNetwork(SOLANA_NETWORK) if (this._modal) { this._modal.setStandardWalletConnectProgress(false) } + + throw err + } + } + + connectToWallet = async (walletName: string) => { + if (isMobileBrowser() && !this.walletsList.find((w) => w.name === walletName)?.standardWallet) { + this.connectToMobileWallet(walletName) + } else { + await this.connectToStandardWallet(walletName) } } @@ -438,132 +499,158 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { new Promise((resolve, reject) => { const innerConnect = async () => { try { - if (this.connected || this.connecting) { + if (this.connecting) { + reject('Cannot connect while connecting') + return + } + + if (this.connected) { resolve() return } if (this._readyState !== WalletReadyState.Loadable) throw new WalletNotReadyError() - if (this._initOnConnect) { - this._connecting = true - - if (!this._app) { - try { - const [app, metadataWallets] = await NightlyConnectAdapter.initApp( - this._appInitData - ) - - this._app = app - this._metadataWallets = metadataWallets - - this.walletsList = getWalletsList( - metadataWallets, - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined - ) - } catch { - if (!this._app) { - this._connecting = false - throw new WalletNotReadyError() - } - } + const recentWallet = getRecentWalletForNetwork(SOLANA_NETWORK) + if (!this._connectionOptions.disableEagerConnect && recentWallet !== null) { + // Eager connect standard if possible + if (recentWallet.walletType === ConnectionType.WalletStandard) { + await this.connectToStandardWallet(recentWallet.walletName) + resolve() + return } - } else { - if (this._loading) { - // we do it to ensure proper connect flow in case if adapter is lazily built, but e. g. solana wallets selector uses its own eager connect - for (let i = 0; i < 200; i++) { - await sleep(10) - if (!this._loading) { - break + // Eager connect remote if possible + if (recentWallet.walletType === ConnectionType.Nightly) { + if (this._app?.hasBeenRestored() && this._app.connectedPublicKeys.length > 0) { + // Try to eager connect if session is restored + try { + this._publicKey = this._app.connectedPublicKeys[0] + this._connected = true + this._connecting = false + this._appSessionActive = true + this.emit('connect', this._publicKey) + resolve() + return + } catch (error) { + // If we fail because of whatever reason + // Reset session since it might be corrupted + const [app] = await NightlyConnectAdapter.initApp(this._appInitData) + this._app = app } } - - if (this._loading) { - throw new WalletNotReadyError() - } - } - - if (!this._app) { - throw new WalletNotReadyError() } - - this._connecting = true } - if (this._app.hasBeenRestored() && this._app.connectedPublicKeys.length > 0) { - // Try to eager connect if session is restored - try { - this.eagerConnectDeeplink() - this._publicKey = this._app.connectedPublicKeys[0] - this._connected = true - this._connecting = false - this._appSessionActive = true - this.emit('connect', this._publicKey) - resolve() - return - } catch (error) { - // If we fail because of whatever reason - // Reset session since it might be corrupted - const [app] = await NightlyConnectAdapter.initApp(this._appInitData) - this._app = app - } + if (this._connectionOptions.disableModal) { + reject('Modal is disabled') + return } - const recentName = getRecentStandardWalletForNetwork(SOLANA_NETWORK) - if ( - this._eagerConnectForStandardWallets && - recentName !== null && - isStandardConnectedForNetwork(SOLANA_NETWORK) - ) { - await this.connectToStandardWallet(recentName, resolve) - - if (this._connected) { - return - } + if (this._connectionOptions.initOnConnect) { + this._loading = true + NightlyConnectAdapter.initApp(this._appInitData) + .then(([app, metadataWallets]) => { + this._app = app + this._metadataWallets = metadataWallets + this.walletsList = getSolanaWalletsList( + metadataWallets, + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined + ) + // Add event listener for userConnected + app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(SOLANA_NETWORK, { + walletName: this._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!this._app || this._app.connectedPublicKeys.length <= 0) { + this._connected = false + // If user does not pass any accounts, we should disconnect + this.disconnect() + return + } + this._publicKey = this._app.connectedPublicKeys[0] + this._connected = true + this.emit('connect', this._publicKey) + } catch { + this.disconnect() + } + }) + this._loading = false + }) + .catch(() => { + this._loading = false + throw new Error('Failed to initialize adapter') + }) } - this._app.on('userConnected', (e) => { - try { - if (this._chosenMobileWalletName) { - persistRecentStandardWalletForNetwork(this._chosenMobileWalletName, SOLANA_NETWORK) - } else { - clearRecentStandardWalletForNetwork(SOLANA_NETWORK) - } - this._publicKey = new PublicKey(e.publicKeys[0]) - this._connected = true - this._connecting = false - this._appSessionActive = true - this.emit('connect', this._publicKey) - this._modal?.closeModal() - resolve() - } catch { - this.disconnect() - } - }) + // Interval that checks if app has connected + let loadingInterval: NodeJS.Timeout + if (this._modal) { + this._connecting = true this._modal.onClose = () => { + clearInterval(loadingInterval) if (this._connecting) { this._connecting = false - const error = new WalletWindowClosedError() - this.emit('error', error) reject(error) } } - this._modal.openModal(this._app.sessionId, (walletName) => { + this._modal.openModal(this._app?.sessionId ?? undefined, async (walletName) => { + // If we are on mobile and wallet is not injected, we should connect to mobile wallet if ( isMobileBrowser() && !this.walletsList.find((w) => w.name === walletName)?.standardWallet ) { this.connectToMobileWallet(walletName) } else { - this.connectToStandardWallet(walletName, resolve) + try { + await this.connectToStandardWallet(walletName) + resolve() + } catch (error) { + reject(error) + } } }) + + // loop until app is connected or we timeout + let checks = 0 + loadingInterval = setInterval(async (): Promise => { + checks++ + if (this._app) { + // Clear interval if app is connected + clearInterval(loadingInterval) + if (this._modal) this._modal.sessionId = this._app.sessionId + + this._app.on('userConnected', async () => { + try { + if (!this._app || this._app.connectedPublicKeys.length <= 0) { + reject(new Error('No accounts found')) + } + this._connected = true + this._modal?.closeModal() + resolve() + } catch (error) { + reject(error) + } finally { + this._connecting = false + } + }) + return + } + // timeout after 5 seconds + if (checks > 500) { + clearInterval(loadingInterval) + // reject(new Error('Connecting takes too long')) + if (this._modal) this._modal.timeoutError = 'Connecting is taking too long' + } + }, 10) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._connecting = false @@ -576,6 +663,22 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { innerConnect() }) + fetchWalletsFromRegistry: () => Promise = async () => { + this._metadataWallets = await AppSolana.getWalletsMetadata( + `${this._appInitData.url ?? 'https://nc2.nightly.app'}/get_wallets_metadata` + ) + return this._metadataWallets + } + + fetchAllWallets = async () => { + const metadataWallets = await this.fetchWalletsFromRegistry() + this.walletsList = getSolanaWalletsList( + metadataWallets, + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined + ) + return this.walletsList + } + disconnect = async () => { if (this.connected) { if (this._appSessionActive) { @@ -584,6 +687,27 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { this._loading = true try { this._app = await AppSolana.build(this._appInitData) + // Add event listener for userConnected + this._app.on('userConnected', async () => { + try { + persistRecentWalletForNetwork(SOLANA_NETWORK, { + walletName: this._chosenMobileWalletName || '', + walletType: ConnectionType.Nightly + }) + + if (!this._app || this._app.connectedPublicKeys.length <= 0) { + this._connected = false + // If user does not pass any accounts, we should disconnect + this.disconnect() + return + } + this._publicKey = this._app.connectedPublicKeys[0] + this._connected = true + this.emit('connect', this._publicKey) + } catch { + this.disconnect() + } + }) } catch (err) { console.log(err) } finally { @@ -593,12 +717,11 @@ export class NightlyConnectAdapter extends BaseMessageSignerWalletAdapter { if (this._innerStandardAdapter) { await this._innerStandardAdapter.disconnect() this._innerStandardAdapter = undefined - persistStandardDisconnectForNetwork(SOLANA_NETWORK) + clearRecentWalletForNetwork(SOLANA_NETWORK) } - this.walletsList = getWalletsList( + this.walletsList = getSolanaWalletsList( this._metadataWallets, - solanaWalletsFilter, - getRecentStandardWalletForNetwork(SOLANA_NETWORK) ?? undefined + getRecentWalletForNetwork(SOLANA_NETWORK)?.walletName ?? undefined ) this._publicKey = null this._connected = false diff --git a/sdk/packages/selector-solana/src/detection.ts b/sdk/packages/selector-solana/src/detection.ts index 423a9b50..8ea2a19c 100644 --- a/sdk/packages/selector-solana/src/detection.ts +++ b/sdk/packages/selector-solana/src/detection.ts @@ -1,5 +1,52 @@ -import { type Wallet } from '@wallet-standard/core' +import { getWallets, Wallet } from '@wallet-standard/core' import { isWalletAdapterCompatibleStandardWallet } from '@solana/wallet-adapter-base' +import { IWalletListItem, WalletMetadata } from '@nightlylabs/wallet-selector-base' export const solanaWalletsFilter = (wallet: Wallet) => isWalletAdapterCompatibleStandardWallet(wallet) + +export const getSolanaWalletsList = (presetList: WalletMetadata[], recentWalletName?: string) => { + const { get } = getWallets() + const windowWallets = get() + + const walletsData: Record = {} + + presetList.forEach((wallet) => { + walletsData[wallet.name] = { + ...wallet, + recent: recentWalletName === wallet.name + } + }) + + windowWallets.filter(solanaWalletsFilter).forEach((wallet) => { + if (walletsData[wallet.name]) { + walletsData[wallet.name] = { + ...walletsData[wallet.name], + recent: recentWalletName === wallet.name, + detected: true, + standardWallet: wallet, + walletType: 'hybrid' + } + } else { + walletsData[wallet.name] = { + name: wallet.name, + image: { + default: wallet.icon as string, + lg: wallet.icon as string, + md: wallet.icon as string, + sm: wallet.icon as string + }, + desktop: null, + homepage: 'https://nightly.app/download', // Fall back to nightly.app + mobile: null, + slug: wallet.name, + recent: recentWalletName === wallet.name, + walletType: 'hybrid', + detected: true, + standardWallet: wallet + } + } + }) + + return Object.values(walletsData) +} diff --git a/sdk/pnpm-lock.yaml b/sdk/pnpm-lock.yaml index 2af1fe5a..8aad36e0 100644 --- a/sdk/pnpm-lock.yaml +++ b/sdk/pnpm-lock.yaml @@ -140,11 +140,14 @@ importers: '@nightlylabs/nightly-connect-sui': specifier: 0.0.29 version: link:../sui + '@nightlylabs/wallet-selector-base': + specifier: ^0.4.0 + version: link:../../packages/selector-base '@nightlylabs/wallet-selector-polkadot': - specifier: 0.2.0 - version: 0.2.0(@polkadot/util@12.5.1) + specifier: 0.2.2 + version: link:../../packages/selector-polkadot '@nightlylabs/wallet-selector-solana': - specifier: 0.2.7 + specifier: 0.3.0 version: link:../../packages/selector-solana '@nightlylabs/wallet-selector-sui': specifier: 0.2.7 @@ -592,7 +595,7 @@ importers: specifier: 0.0.27 version: link:../../apps/base '@nightlylabs/wallet-selector-modal': - specifier: 0.2.0 + specifier: 0.2.1 version: link:../modal '@wallet-standard/core': specifier: ^1.0.3 @@ -632,7 +635,7 @@ importers: specifier: ^0.0.15 version: link:../../apps/polkadot '@nightlylabs/wallet-selector-base': - specifier: ^0.3.1 + specifier: ^0.4.0 version: link:../selector-base '@polkadot/api': specifier: 10.10.1 @@ -643,6 +646,9 @@ importers: '@wallet-standard/core': specifier: ^1.0.3 version: 1.0.3 + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 devDependencies: '@rollup/plugin-commonjs': specifier: ^25.0.0 @@ -675,8 +681,8 @@ importers: specifier: ^0.0.29 version: link:../../apps/solana '@nightlylabs/wallet-selector-base': - specifier: ^0.2.4 - version: 0.2.4 + specifier: ^0.4.0 + version: link:../selector-base '@solana/wallet-adapter-base': specifier: ^0.9.22 version: 0.9.22(@solana/web3.js@1.77.2) @@ -5090,24 +5096,6 @@ packages: - utf-8-validate dev: false - /@nightlylabs/nightly-connect-polkadot@0.0.15: - resolution: {integrity: sha512-WCsumvHwhPipbxPQoswKCwHykwJ48Dffwb9hCf7zjCgEysIBCnA6Dzj/2G80drLqYYpS285nMa8z+3NaXVu2dA==} - dependencies: - '@nightlylabs/nightly-connect-base': 0.0.27 - '@polkadot/api': 10.10.1 - '@polkadot/extension-inject': 0.46.5(@polkadot/api@10.10.1)(@polkadot/util@12.5.1) - '@polkadot/types': 10.10.1 - '@polkadot/util': 12.5.1 - '@polkadot/util-crypto': 12.5.1(@polkadot/util@12.5.1) - eventemitter3: 5.0.1 - uuid: 9.0.0 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - dev: false - /@nightlylabs/nightly-connect-solana@0.0.28: resolution: {integrity: sha512-8PBkmuXzWZNPqu6SGT2tsGK4DgD3yswQsUVb3L+GgFGCdQI7eUqyHd2ofWFWzEgj4a1XuixA29ZcSyw20ajgzw==} dependencies: @@ -5158,21 +5146,6 @@ packages: - utf-8-validate dev: false - /@nightlylabs/wallet-selector-base@0.3.1: - resolution: {integrity: sha512-m2hdNkOrQNS52xXYSvko8YvbI60miCU9AHO8HHKfGXiuYgUjAmQSeTm1wLP8LZI4+Mygqbr8Oq9na+HMEgq2XA==} - dependencies: - '@nightlylabs/nightly-connect-base': 0.0.27 - '@nightlylabs/wallet-selector-modal': 0.2.0 - '@wallet-standard/core': 1.0.3 - isomorphic-localstorage: 1.0.2 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - ts-node - - utf-8-validate - dev: false - /@nightlylabs/wallet-selector-modal@0.1.2: resolution: {integrity: sha512-vxy9S2dEf3NARW6LDq2ZKpWMlk5JJFIuwUfSxkuJlgUg2OVSlnDS7vdho3h4DmluRU5GM9vVhaXUGHAVp5sDQg==} dependencies: @@ -5187,37 +5160,6 @@ packages: - ts-node dev: false - /@nightlylabs/wallet-selector-modal@0.2.0: - resolution: {integrity: sha512-BdEk3FhL65z/X0N9ygPjk7uQvV0GGHTWSwXBVob/l48Nok4ikFcV30Dtxk6iSSbErDZ8U4zV3/78cK+m+4lt8A==} - dependencies: - '@nightlylabs/qr-code': 2.0.4 - autoprefixer: 10.4.14(postcss@8.4.24) - lit: 2.7.2 - postcss: 8.4.24 - postcss-lit: 1.1.0(postcss@8.4.24) - tailwindcss: 3.3.2 - transitivePeerDependencies: - - supports-color - - ts-node - dev: false - - /@nightlylabs/wallet-selector-polkadot@0.2.0(@polkadot/util@12.5.1): - resolution: {integrity: sha512-T4C6J+RVBif8H742LdeZfIRZyFZV79CAWL6/w3keQmyi8ZmTfmmxdVaXkSE5Va11j3xt2A298mugbbujluW6bw==} - dependencies: - '@nightlylabs/nightly-connect-polkadot': 0.0.15 - '@nightlylabs/wallet-selector-base': 0.3.1 - '@polkadot/api': 10.10.1 - '@polkadot/extension-inject': 0.46.5(@polkadot/api@10.10.1)(@polkadot/util@12.5.1) - '@wallet-standard/core': 1.0.3 - transitivePeerDependencies: - - '@polkadot/util' - - bufferutil - - encoding - - supports-color - - ts-node - - utf-8-validate - dev: false - /@nightlylabs/wallet-selector-solana@0.2.6(bs58@4.0.1)(react@18.2.0): resolution: {integrity: sha512-cVTKk+c6tGv4GeSQMlUaZ2si4A6ySKj41emkGJ8OtuwmtzwUym4Xuh3chXZYgGrMQgvPrX5+erIR4oq2GmGIPg==} dependencies: