Subscribe to ACTIVE_ACCOUNT_SET
Advanced Example
Beacon provides developers the ability to subscribe to its internal state, as shown on the dedicated page. Since version 4.2.0, subscribing to ACTIVE_ACCOUNT_SET
has become mandatory. This page provides a custom example with user validation by requesting the wallet to sign a payload.
Before Starting
Be aware that calling one of the functions listed below will also trigger ACTIVE_ACCOUNT_SET
. Be careful when calling such functions inside the handler to avoid causing your dApp to enter an endless loop.
List of functions:
requestPermissions
setActiveAccount
clearActiveAccount
disconnect
removeAccount
removeAllAccounts
destroy
Example
After initializing your dAppClient
instance, you need to subscribe to ACTIVE_ACCOUNT_SET
as shown below:
- Beacon
- Taquito
const dAppClient = new DAppClient({
name: "Beacon Docs",
});
dAppClient.subscribeToEvent(BeaconEvent.ACTIVE_ACCOUNT_SET, (account) => {
console.log(`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `, account);
});
const Tezos = new TezosToolkit("https://mainnet.api.tez.ie");
const wallet = new BeaconWallet({ name: "Beacon Docs" });
wallet.client.subscribeToEvent(BeaconEvent.ACTIVE_ACCOUNT_SET, (account) => {
console.log(`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `, account);
});
The handler should be as concise as possible. Ideally, it should just globally update your active account on your dApp. However, in some cases, adding extra lines of code may be necessary.
Adding requestSignPayload
Sometimes requestPermissions
may not be enough, and you want to ensure the user who has synced with the wallet is authorized. A common way to accomplish this is by sending a sign_payload request to the wallet.
- Beacon
- Taquito
dAppClient.subscribeToEvent(BeaconEvent.ACTIVE_ACCOUNT_SET, async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account) {
return;
}
try {
await dAppClient.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
});
wallet.client.subscribeToEvent(
BeaconEvent.ACTIVE_ACCOUNT_SET,
async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account) {
return;
}
try {
await wallet.client.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
},
);
Note:
ACTIVE_ACCOUNT_SET
gets triggered both when setting a new account and resetting the current one. Make sure not to send a sign_payload request without an account.
Multi-tab Synchronization
While the example above works for single-page dApps, it may become problematic in a multi-tab setup. Beacon emits an event to keep each tab synced with the internal state. Therefore, if your dApp needs multiple tabs support, the above approach may cause issues. Each tab will send a sign_payload request to the wallet, which is not intended and may lead to request rejection if a certain threshold is reached.
To address this, we need to implement multi-tab synchronization. There are multiple ways to achieve this; for simplicity, we use broadcast-channel
.
The following example is designed to be as simple as possible to help developers kickstart synchronization in their dApp. Please note that not all edge cases are covered.
Step 1: Install broadcast-channel
Run the following command:
- NPM
- Yarn
npm install --save broadcast-channel
yarn add broadcast-channel
Step 2: Set Up the Channel
The main idea is to elect a tab as the Leader so that only this tab will send a request to the wallet. First, set up a channel.
const channel = new BroadcastChannel("beacon-channel"); // "beacon-channel" is an example, you can choose any name you want
const elector = createLeaderElection(channel);
// Auxiliary variable needed for handling beforeUnload.
// Closing a tab causes the elector to be killed immediately
let wasLeader: boolean = false;
Check if a leader already exists, otherwise request leadership. We also need to handle the case in which the Leader tab gets closed and therefore we need to transfer the leadership to another tab.
elector.hasLeader().then(async (hasLeader) => {
if (!hasLeader) {
await elector.awaitLeadership();
wasLeader = elector.isLeader;
}
});
// NOTE: If you are using a JS framework, do not call window.onbeforeunload directly.
// Refer to your framework's guidelines for handling this scenario.
window.onbeforeunload = async () => {
if (wasLeader) {
await elector.die();
channel.postMessage("LEADER_DEAD");
}
};
channel.onmessage = async (message: any) => {
if (message === "LEADER_DEAD") {
await elector.awaitLeadership();
}
};
Step 3: Update the Handler
Now, inside the handler, check whether the current tab has the leadership. If not, do not send a sign_payload
request.
- Beacon
- Taquito
dAppClient.subscribeToEvent(BeaconEvent.ACTIVE_ACCOUNT_SET, async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account || !elector.isLeader) {
return;
}
try {
await dAppClient.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
});
wallet.client.subscribeToEvent(
BeaconEvent.ACTIVE_ACCOUNT_SET,
async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account || !elector.isLeader) {
return;
}
try {
await wallet.client.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
},
);
Conclusion
The end result should look like this:
- Beacon
- Taquito
import { DAppClient, BeaconEvent } from "@airgap/beacon-dapp";
import { NetworkType } from "@airgap/beacon-types";
import { BroadcastChannel, createLeaderElection } from "broadcast-channel";
const channel = new BroadcastChannel("beacon-test");
const elector = createLeaderElection(channel);
let wasLeader: boolean = false;
elector.hasLeader().then(async (hasLeader) => {
if (!hasLeader) {
await elector.awaitLeadership();
wasLeader = elector.isLeader;
}
});
window.onbeforeunload = async () => {
if (wasLeader) {
await elector.die();
channel.postMessage("LEADER_DEAD");
}
};
channel.onmessage = async (message: any) => {
if (message === "LEADER_DEAD") {
await elector.awaitLeadership();
}
};
const dAppClient = new DAppClient({
name: "Beacon Docs",
network: {
type: NetworkType.GHOSTNET,
},
});
dAppClient.subscribeToEvent(BeaconEvent.ACTIVE_ACCOUNT_SET, async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account || !elector.isLeader) {
return;
}
try {
await dAppClient.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
});
dAppClient.requestPermissions();
import { TezosToolkit } from "@taquito/taquito";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { BeaconEvent } from "@airgap/beacon-dapp";
import { BroadcastChannel, createLeaderElection } from "broadcast-channel";
const channel = new BroadcastChannel("beacon-test");
const elector = createLeaderElection(channel);
let wasLeader: boolean = false;
elector.hasLeader().then(async (hasLeader) => {
if (!hasLeader) {
await elector.awaitLeadership();
wasLeader = elector.isLeader;
}
});
window.onbeforeunload = async () => {
if (wasLeader) {
await elector.die();
channel.postMessage("LEADER_DEAD");
}
};
channel.onmessage = async (message: any) => {
if (message === "LEADER_DEAD") {
await elector.awaitLeadership();
}
};
const Tezos = new TezosToolkit("https://mainnet.api.tez.ie");
const wallet = new BeaconWallet({ name: "Beacon Docs" });
Tezos.setWalletProvider(wallet);
wallet.client.subscribeToEvent(
BeaconEvent.ACTIVE_ACCOUNT_SET,
async (account) => {
console.log(
`${BeaconEvent.ACTIVE_ACCOUNT_SET} triggered: `,
account?.address,
);
if (!account || !elector.isLeader) {
return;
}
try {
await wallet.client.requestSignPayload({
payload:
"05010000004254657a6f73205369676e6564204d6573736167653a207465737455726c20323032332d30322d30385431303a33363a31382e3435345a2048656c6f20576f726c64",
});
} catch (err: any) {
// The request was rejected
// handle disconnection
}
},
);
wallet.client.requestPermissions();
References
- BroadcastChannel: Documentation and usage examples for the
broadcast-channel
library.