Skip to content

Commit

Permalink
Set up to automatically pop up Chrome extensions
Browse files Browse the repository at this point in the history
Added a new parameter to the `DAppConnector` constructor that allows
defining the extension IDs that automatically pop up when pairing or
sending requests to the wallet.

This contribution includes additional functions that allow for checking
whether a specific extension is installed and responding. The available
extensions are listed under the `extensions` property of the
`DAppConnector` class. To establish a session that will automatically
activate the extension, use the `connectExtension` method instead of the
`connect` method.

Signed-off-by: Fran Fernandez <fran@kabila.app>
  • Loading branch information
franfernandez20 committed Mar 19, 2024
1 parent d167d28 commit 1d59e10
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 2 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,16 @@ const dAppMetadata = {
icons: ['<Image url>'],
}

const extensionIds = ['<Extension ID>']

const dAppConnector = new DAppConnector(
dAppMetadata,
LedgerId.TESTNET,
projectId,
Object.values(HederaJsonRpcMethod),
[HederaSessionEvent.ChainChanged, HederaSessionEvent.AccountsChanged],
[HederaChainId.Testnet],
extensionIds,
)
await dAppConnector.init()
```
Expand Down Expand Up @@ -285,6 +288,68 @@ dAppConnector.disconnectSession(session.topic)
dAppConnector.disconnectAllSessions()
```

#### Extension popup

By default, it is not possible to directly pop up an extension with Wallet Connect. However, to
allow this possibility, the dAppConnector accepts a list of extension IDs. If you create the
AppConnector with an extension ID, it will automatically send a message to the extension to
detect if it is installed. In case the extension is installed, it will be added to the available
extensions and its data can be found at the extensions property of dAppConnector.

To connect an available extension, use the method `connectExtension(<extensionId>)`. This will
link the extension to the signer and session. Whenever you use the signer created for this
session, the extension will automatically open. You can find out if the extension is available
by checking the `extensions` property.

```javascript
const dAppConnector = new DAppConnector(
dAppMetadata,
LedgerId.TESTNET,
projectId,
Object.values(HederaJsonRpcMethod),
[HederaSessionEvent.ChainChanged, HederaSessionEvent.AccountsChanged],
[HederaChainId.Testnet],
['<Extension ID 1>, <Extension ID 2>'],
)

[...]

dAppConnector?.extensions?.forEach((extension) => {
console.log(extension)
})

const extension = dAppConnector?.extensions?.find((extension) => extension.name === '<Extension name>')
if (extension.available) {
await dAppConnector!.connectExtension(extension.id);
const signer = dAppConnector.getSigner(AccountId.fromString('0.0.12345'))

// This request will open the extension
const response = await signer.signAndExecuteTransaction(transaction)
}
```
Wallets that are compatible should be able to receive and respond to the following messages:
- `"hedera-extension-query-<extesnionId>"`: The extension is required to respond with
`"hedera-extension-response"` and provide the next set of data in the metadata property.
```javascript
let metadata = {
id: '<extesnionId>',
name: '<Wallet name>',
url: '<Wallet url>',
icon: '<Wallet con>',
description: '<Wallet url>',
}
```
- `"hedera-extension-open-<extensionId>"`: The extension needs to listen to this message and
automatically open.
- `"hedera-extension-connect-<extesnionId>"`: The extension must listen to this message and
utilize the `pairingString` property in order to establish a connection.
This communication protocol between the wallet and web dApps requires an intermediate script to
use the Chrome API. Refer to the
[Chrome Extensions documentation](https://developer.chrome.com/docs/extensions/develop/concepts/messaging)
## Demo & docs
This repository includes a vanilla html/css/javascript implementation with a dApp and wallet
Expand Down
4 changes: 4 additions & 0 deletions src/lib/dapp/DAppSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
transactionBodyToBase64String,
transactionToBase64String,
transactionToTransactionBody,
extensionOpen,
} from '../shared'

export class DAppSigner implements Signer {
Expand All @@ -60,6 +61,7 @@ export class DAppSigner implements Signer {
private readonly accountId: AccountId,
public readonly topic: string,
private readonly ledgerId: LedgerId = LedgerId.MAINNET,
public readonly extensionId?: string,
) {
this.signerAccountId = `${ledgerIdToCAIPChainId(ledgerId)}:${accountId.toString()}`
this.nodesAccountIds = [AccountId.fromString('0.0.3')]
Expand All @@ -79,6 +81,8 @@ export class DAppSigner implements Signer {
}

request<T>(request: { method: string; params: any }): Promise<T> {
if (this.extensionId) extensionOpen(this.extensionId)

return DAppSigner.signClient.request<T>({
topic: this.topic,
request,
Expand Down
71 changes: 69 additions & 2 deletions src/lib/dapp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import {
isSignAndExecuteTransactionParams,
isSignTransactionParams,
isSignAndExecuteQueryParams,
ExtensionData,
extensionConnect,
findExtensions,
} from '../shared'
import { DAppSigner } from './DAppSigner'
import { JsonRpcResult } from '@walletconnect/jsonrpc-types'
Expand All @@ -67,6 +70,8 @@ export class DAppConnector {
supportedEvents: string[] = []
supportedChains: string[] = []

extensions: ExtensionData[] = []

walletConnectClient: SignClient | undefined
walletConnectModal: WalletConnectModal
signers: DAppSigner[] = []
Expand All @@ -80,6 +85,7 @@ export class DAppConnector {
* @param methods - Array of supported methods for the DApp (optional).
* @param events - Array of supported events for the DApp (optional).
* @param events - Array of supported chains for the DApp (optional).
* @param extensions - Array of Chrome extensions enabled to be popup when sending requests (optional).
*/
constructor(
metadata: SignClientTypes.Metadata,
Expand All @@ -88,18 +94,45 @@ export class DAppConnector {
methods?: string[],
events?: string[],
chains?: string[],
extensionIds?: string[],
) {
this.dAppMetadata = metadata
this.network = network
this.projectId = projectId
this.supportedMethods = methods ?? Object.values(HederaJsonRpcMethod)
this.supportedEvents = events ?? []
this.supportedChains = chains ?? []
this.extensions =
extensionIds?.map((id) => ({
id,
available: false,
availableInIframe: false,
})) ?? []

this.walletConnectModal = new WalletConnectModal({
projectId: projectId,
chains: chains,
})

if (extensionIds?.length) {
findExtensions(
this.extensions.map((ext) => ext.id),
(metadata) => {
this.extensions = this.extensions.map((ext) => {
if (metadata.id === ext.id) {
return {
...ext,
available: true,
name: metadata.name,
url: metadata.url,
icon: metadata.icon,
}
}
return ext
})
},
)
}
}

get accountIds(): AccountId[] {
Expand Down Expand Up @@ -197,21 +230,53 @@ export class DAppConnector {
/**
* Initiates the WallecConnect connection flow using URI.
* @param pairingTopic - The pairing topic for the connection (optional).
* @param extensionId - The id for the extension used to connect (optional).
* @returns A Promise that resolves when the connection process is complete.
*/
public async connect(
launchCallback: (uri: string) => void,
pairingTopic?: string,
): Promise<void> {
extensionId?: string,
): Promise<SessionTypes.Struct> {
return this.abortableConnect(async () => {
const { uri, approval } = await this.connectURI(pairingTopic)
if (!uri) throw new Error('URI is not defined')
launchCallback(uri)
const session = await approval()
if (extensionId) {
const sessionProperties = {
...session.sessionProperties,
extensionId,
}
session.sessionProperties = sessionProperties
await this.walletConnectClient?.session.update(session.topic, {
sessionProperties,
})
}
await this.onSessionConnected(session)
return session
})
}

/**
* Initiates the WallecConnect connection flow sending a message to the extension.
* @param extensionId - The id for the extension used to connect.
* @param pairingTopic - The pairing topic for the connection (optional).
* @returns A Promise that resolves when the connection process is complete.
*/
public async connectExtension(
extensionId: string,
pairingTopic?: string,
): Promise<SessionTypes.Struct> {
return this.connect(
(uri) => {
extensionConnect(extensionId, uri)
},
pairingTopic,
extensionId,
)
}

private abortableConnect = async <T>(callback: () => Promise<T>): Promise<T> => {
return new Promise(async (resolve, reject) => {
const pairTimeoutMs = 480_000
Expand All @@ -222,6 +287,8 @@ export class DAppConnector {

try {
return resolve(await callback())
} catch (error) {
reject(error)
} finally {
clearTimeout(timeout)
}
Expand Down Expand Up @@ -282,7 +349,7 @@ export class DAppConnector {
const allNamespaceAccounts = accountAndLedgerFromSession(session)
return allNamespaceAccounts.map(
({ account, network }: { account: AccountId; network: LedgerId }) =>
new DAppSigner(account, session.topic, network),
new DAppSigner(account, session.topic, network, session.sessionProperties?.extensionId),
)
}

Expand Down
47 changes: 47 additions & 0 deletions src/lib/shared/extensionController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const EXTENSION_QUERY = 'hedera-extension-query-'
const EXTENSION_CONNECT = 'hedera-extension-connect-'
const EXTENSION_OPEN = 'hedera-extension-open-'
const EXTENSION_RESPONSE = 'hedera-extension-response'

export type ExtensionData = {
id: string
name?: string
icon?: string
url?: string
available: boolean
}

export const findExtensions = (
ids: string[],
onFound: (_metadata: ExtensionData) => void,
): void => {
if (typeof window === 'undefined') return

window.addEventListener(
'message',
(event) => {
if (event?.data?.type == EXTENSION_RESPONSE && event.data.metadata) {
onFound(event.data.metadata)
}
},
false,
)

setTimeout(() => {
ids.forEach((id) => {
extensionQuery(id)
})
}, 200)
}

export const extensionQuery = (id: string) => {
window.postMessage({ type: EXTENSION_QUERY + id }, '*')
}

export const extensionConnect = (id: string, pairingString: string) => {
window.postMessage({ type: EXTENSION_CONNECT + id, pairingString }, '*')
}

export const extensionOpen = (id: string) => {
window.postMessage({ type: EXTENSION_OPEN + id }, '*')
}
1 change: 1 addition & 0 deletions src/lib/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './events'
export * from './methods'
export * from './payloads'
export * from './utils'
export * from './extensionController'

0 comments on commit 1d59e10

Please sign in to comment.