@tezos-x/octez.connect-dapp
Version:
Use this package on your dApp to instanciate a DAppClient object and communicate to wallets.
2,618 lines • 144 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DAppClient = void 0;
const bs58check_1 = __importDefault(require("bs58check"));
const octez_connect_types_1 = require("@tezos-x/octez.connect-types");
const octez_connect_core_1 = require("@tezos-x/octez.connect-core");
const octez_connect_blockchain_tezos_1 = require("@tezos-x/octez.connect-blockchain-tezos");
const octez_connect_utils_1 = require("@tezos-x/octez.connect-utils");
const octez_connect_transport_postmessage_1 = require("@tezos-x/octez.connect-transport-postmessage");
const octez_connect_ui_1 = require("@tezos-x/octez.connect-ui");
const octez_connect_transport_walletconnect_1 = require("@tezos-x/octez.connect-transport-walletconnect");
const beacon_message_events_1 = require("../beacon-message-events");
const events_1 = require("../events");
const tzkt_blockexplorer_1 = require("../utils/tzkt-blockexplorer");
const DappPostMessageTransport_1 = require("../transports/DappPostMessageTransport");
const DappP2PTransport_1 = require("../transports/DappP2PTransport");
const DappWalletConnectTransport_1 = require("../transports/DappWalletConnectTransport");
const logger = new octez_connect_core_1.Logger('DAppClient');
// Surfaced when a permission response yields zero account records.
const EMPTY_PERMISSION_ACCOUNTS_MESSAGE = 'Wallet permission response did not include any accounts. Please re-pair the wallet and try again.';
/**
* @publicapi
*
* The DAppClient has to be used in decentralized applications. It handles all the logic related to connecting to beacon-compatible
* wallets and sending requests.
*
* @category DApp
*/
class DAppClient extends octez_connect_core_1.Client {
constructor(config) {
var _a, _b, _c, _d, _e, _f, _g, _h;
super(Object.assign({ storage: config && config.storage ? config.storage : new octez_connect_core_1.LocalStorage() }, config));
this.events = new events_1.BeaconEventHandler();
/**
* WalletConnect is enabled by default (as in 4.8.x) using the shared
* DEFAULT_WALLETCONNECT_PROJECT_ID unless `walletConnectOptions` overrides it.
* Set `disableWalletConnect: true` to opt out. When false, no WC transport is
* built, listened to, or offered for pairing.
*/
this.isWalletConnectEnabled = true;
this.isGetActiveAccountHandled = false;
this.openRequestsOtherTabs = new Set();
/**
* A map of requests that are currently "open", meaning we have sent them to a wallet and are still awaiting a response.
*/
this.openRequests = new Map();
/**
* The currently active account. For all requests that are associated to a specific request (operation request, signing request),
* the active account is used to determine the network and destination wallet
*/
this._activeAccount = new octez_connect_utils_1.ExposedPromise();
/**
* The currently active peer. This is used to address a peer in case the active account is not set. (Eg. for permission requests)
*/
this._activePeer = new octez_connect_utils_1.ExposedPromise();
this.isInitPending = false;
this.beaconIDB = new octez_connect_core_1.IndexedDBStorage('beacon', ['bug_report', 'metrics']);
this.debounceSetActiveAccount = false;
this.multiTabChannel = new octez_connect_core_1.MultiTabChannel('octez.connect-sdk-channel', this.onBCMessageHandler.bind(this), this.onElectedLeaderhandler.bind(this));
this.description = config.description;
// WalletConnect is enabled BY DEFAULT (as in 4.8.x) with the shared
// ecosystem projectId, so dApps don't have to register with WalletConnect
// Cloud to offer WC wallets (e.g. Kukai Mobile) — the free tier covers the
// ecosystem's volume. `walletConnectOptions` overrides the projectId/relay,
// `disableWalletConnect: true` opts out entirely (e.g. environments where
// the WC provider cannot run, see #32).
this.isWalletConnectEnabled = !config.disableWalletConnect;
this.wcProjectId = this.isWalletConnectEnabled
? ((_a = config.walletConnectOptions) === null || _a === void 0 ? void 0 : _a.projectId) || octez_connect_core_1.DEFAULT_WALLETCONNECT_PROJECT_ID
: undefined;
this.wcRelayUrl = this.isWalletConnectEnabled ? (_b = config.walletConnectOptions) === null || _b === void 0 ? void 0 : _b.relayUrl : undefined;
this.featuredWallets = config.featuredWallets;
this.events = new events_1.BeaconEventHandler(config.eventHandlers, (_c = config.disableDefaultEvents) !== null && _c !== void 0 ? _c : false);
this.blockExplorer = (_d = config.blockExplorer) !== null && _d !== void 0 ? _d : new tzkt_blockexplorer_1.TzktBlockExplorer();
this.network = (_e = config.network) !== null && _e !== void 0 ? _e : { type: (_f = config.preferredNetwork) !== null && _f !== void 0 ? _f : octez_connect_types_1.NetworkType.MAINNET };
(0, octez_connect_ui_1.setColorMode)((_g = config.colorMode) !== null && _g !== void 0 ? _g : octez_connect_types_1.ColorMode.LIGHT);
this.termsAndConditionsUrl = config.termsAndConditionsUrl;
this.privacyPolicyUrl = config.privacyPolicyUrl;
this.errorMessages = (_h = config.errorMessages) !== null && _h !== void 0 ? _h : {};
this.appMetadataManager = new octez_connect_core_1.AppMetadataManager(this.storage);
this.storageValidator = new octez_connect_core_1.StorageValidator(this.storage);
this.enableAppSwitching =
config.enableAppSwitching === undefined ? true : Boolean(config.enableAppSwitching);
this.enableMetrics = config.enableMetrics ? true : false;
this.requiredMinimumVersion = (0, octez_connect_core_1.resolveRequiredMinimumVersion)(config.requiredMinimumVersion);
// Tezos is the default chain: the wrapped-only pipeline routes every
// request/response through the registry handler, so registration is no
// longer a consumer obligation. addBlockchain stays public — a later
// registration under 'tezos' overrides this default.
this.addBlockchain(new octez_connect_blockchain_tezos_1.TezosBlockchain());
// Subscribe to storage changes and update the active account if it changes on other tabs
this.storage.subscribeToStorageChanged((event) => __awaiter(this, void 0, void 0, function* () {
if (event.eventType === 'storageCleared') {
this.setActiveAccount(undefined);
return;
}
if (event.eventType === 'entryModified') {
if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT)) {
const accountIdentifier = event.newValue;
if (!accountIdentifier || accountIdentifier === 'undefined') {
this.setActiveAccount(undefined);
}
else {
const account = yield this.getAccount(accountIdentifier);
this.setActiveAccount(account);
}
return;
}
if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ENABLE_METRICS)) {
this.enableMetrics = Boolean(yield this.storage.get(octez_connect_types_1.StorageKey.ENABLE_METRICS));
return;
}
if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED)) {
this._keyPair = new octez_connect_utils_1.ExposedPromise();
this._beaconId = new octez_connect_utils_1.ExposedPromise();
yield this.initSDK();
return;
}
}
}));
this.activeAccountLoaded = this.storage
.get(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT)
.then((activeAccountIdentifier) => __awaiter(this, void 0, void 0, function* () {
if (activeAccountIdentifier) {
const account = yield this.accountManager.getAccount(activeAccountIdentifier);
yield this.setActiveAccount(account);
return account;
}
else {
yield this.setActiveAccount(undefined);
return undefined;
}
}))
.catch((storageError) => __awaiter(this, void 0, void 0, function* () {
logger.error(storageError);
yield this.resetInvalidState(false);
this.events.emit(events_1.BeaconEvent.INVALID_ACCOUNT_DEACTIVATED);
return undefined;
}));
this.handleResponse = (wireMessage, connectionInfo) => __awaiter(this, void 0, void 0, function* () {
var _a;
// Negotiated wire: a flat arrival is the legacy v2 dialect (from a
// v4.8.x wallet answering a flat request) and is already the shape the
// pipeline consumes. Wrapped arrivals carry Tezos payloads that are
// normalized back to those same flat shapes — the wire dialect stays
// invisible to integrators. Non-Tezos wrapped payloads keep the
// pass-through of the generic permissionRequest/request API.
const isWrapped = (0, octez_connect_core_1.usesWrappedMessages)(wireMessage.version);
// Issue #33: a V3-versioned message can arrive without its wrapped payload.
// Drop it safely instead of dereferencing an undefined payload, which would
// throw an unhandled rejection inside the transport subscription callback.
if (isWrapped && !wireMessage.message) {
logger.log('handleResponse', 'Received wrapped message with undefined payload; dropping', wireMessage);
return;
}
const normalized = isWrapped
? this.normalizeWrappedTezosMessage(wireMessage)
: wireMessage;
const message = normalized !== null && normalized !== void 0 ? normalized : wireMessage;
const typedMessage = normalized !== null && normalized !== void 0 ? normalized : wireMessage.message;
let appMetadata = normalized
? typedMessage.appMetadata
: (_a = typedMessage.blockchainData) === null || _a === void 0 ? void 0 : _a.appMetadata;
if (!appMetadata && !normalized) {
const storedMetadata = yield Promise.all([
this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_P2P_PEERS_DAPP),
this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_WALLETCONNECT_PEERS_DAPP),
this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP)
]);
for (const peers of storedMetadata) {
const peer = peers.find((peer) => peer.senderId === message.senderId);
if (!peer) {
continue;
}
const wallet = yield this.getWalletInfo();
appMetadata = {
name: peer.name,
senderId: peer.senderId,
icon: wallet.icon
};
break;
}
}
if (this.openRequestsOtherTabs.has(message.id)) {
// Relay the ORIGINAL wrapped envelope, not the normalized flat
// message: the receiving tab funnels this straight back into its own
// handleResponse, whose wrapped-only contract would drop a flat
// arrival (version '4' with no `.message` payload) and leave the
// other tab's request pending forever.
this.multiTabChannel.postMessage({
type: 'RESPONSE',
data: {
message: wireMessage,
connectionInfo
},
id: message.id
});
if (typedMessage.type !== octez_connect_types_1.BeaconMessageType.Acknowledge) {
this.openRequestsOtherTabs.delete(message.id);
}
return;
}
const openRequest = this.openRequests.get(message.id);
logger.log('### openRequest ###', openRequest);
logger.log('handleResponse', 'Received message', message, connectionInfo);
logger.log('### message ###', JSON.stringify(message));
logger.log('### connectionInfo ###', connectionInfo);
const handleDisconnect = () => __awaiter(this, void 0, void 0, function* () {
var _a;
this.analytics.track('event', 'DAppClient', 'Disconnect received from Wallet');
const relevantTransport = connectionInfo.origin === octez_connect_types_1.Origin.P2P
? this.p2pTransport
: connectionInfo.origin === octez_connect_types_1.Origin.WALLETCONNECT
? this.walletConnectTransport
: ((_a = this.postMessageTransport) !== null && _a !== void 0 ? _a : (yield this.transport));
if (relevantTransport) {
const peers = yield relevantTransport.getPeers();
const peer = peers.find((peerEl) => peerEl.senderId === message.senderId);
if (peer) {
yield relevantTransport.removePeer(peer);
}
}
yield this.removeAccountsForPeerIds([message.senderId]);
yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
// Reset transport state so next requestPermissions() shows pairing modal
this.postMessageTransport = undefined;
this.p2pTransport = undefined;
this.walletConnectTransport = undefined;
yield this.setTransport(undefined);
yield this.setActivePeer(undefined);
});
if (openRequest && typedMessage.type === octez_connect_types_1.BeaconMessageType.Acknowledge) {
this.analytics.track('event', 'DAppClient', 'Acknowledge received from Wallet');
logger.log('handleResponse', `acknowledge message received for ${message.id}`);
this.events
.emit(events_1.BeaconEvent.ACKNOWLEDGE_RECEIVED, {
message: typedMessage,
extraInfo: {},
walletInfo: yield this.getWalletInfo()
})
.catch(console.error);
}
else if (openRequest) {
// Define valid response types that should resolve a request
const validResponseTypes = [
octez_connect_types_1.BeaconMessageType.PermissionResponse,
octez_connect_types_1.BeaconMessageType.OperationResponse,
octez_connect_types_1.BeaconMessageType.SignPayloadResponse,
octez_connect_types_1.BeaconMessageType.BroadcastResponse,
octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeResponse,
octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeResponse,
octez_connect_types_1.BeaconMessageType.BlockchainResponse,
octez_connect_types_1.BeaconMessageType.Error
];
// Only process if it's a valid response type
if (validResponseTypes.includes(typedMessage.type)) {
if (typedMessage.type === octez_connect_types_1.BeaconMessageType.PermissionResponse && appMetadata) {
yield this.appMetadataManager.addAppMetadata(appMetadata);
}
if (typedMessage.type === octez_connect_types_1.BeaconMessageType.Error) {
openRequest.reject(typedMessage);
}
else {
openRequest.resolve({ message, connectionInfo });
}
this.openRequests.delete(typedMessage.id);
}
else {
// Log unexpected message types but don't resolve the request
logger.warn('handleResponse', `Received unexpected message type "${typedMessage.type}" for request ${message.id}. Expected a response type, not a request type.`);
}
}
else {
if (typedMessage.type === octez_connect_types_1.BeaconMessageType.Disconnect) {
yield handleDisconnect();
}
else if (typedMessage.type === octez_connect_types_1.BeaconMessageType.ChangeAccountRequest) {
yield this.onNewAccount(typedMessage, connectionInfo);
}
}
if (this._transport.isResolved()) {
const transport = yield this.transport;
if (transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport &&
!this.openRequests.has('session_update')) {
this.openRequests.set('session_update', new octez_connect_utils_1.ExposedPromise());
}
}
});
this.storageValidator
.validate()
.then((isValid) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const account = yield this.activeAccountLoaded;
if (!isValid) {
const info = yield this.getWalletInfo(undefined, account, false);
info.type =
info.type === 'extension' && (account === null || account === void 0 ? void 0 : account.origin.type) === octez_connect_types_1.Origin.P2P ? 'mobile' : info.type;
yield this.storage.set(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET, {
icon: (_a = info.icon) !== null && _a !== void 0 ? _a : '',
key: info.name,
type: (_b = info.type) !== null && _b !== void 0 ? _b : 'web',
name: info.name,
url: info.deeplink
});
const nowValid = yield this.storageValidator.validate();
if (!nowValid) {
this.resetInvalidState(false);
}
}
if (account && account.origin.type !== 'p2p') {
this.init();
}
}))
.catch((err) => logger.error(err.message));
this.sendMetrics(`enable-metrics?${this.addQueryParam('version', octez_connect_core_1.SDK_VERSION)}`, undefined, (res) => {
if (!res.ok) {
res.status === 426
? console.error('Metrics are no longer supported for this version, please upgrade.')
: console.warn('Network error encountered. Metrics sharing have been automatically disabled.');
}
this.enableMetrics = res.ok;
this.storage.set(octez_connect_types_1.StorageKey.ENABLE_METRICS, res.ok);
}, () => {
this.enableMetrics = false;
this.storage.set(octez_connect_types_1.StorageKey.ENABLE_METRICS, false);
});
this.initUserID().catch((err) => logger.error(err.message));
}
checkIfBCLeaderExists() {
return __awaiter(this, void 0, void 0, function* () {
// broadcast channel does not work on mobile
if ((0, octez_connect_ui_1.isMobileOS)(window)) {
return true;
}
const hasLeader = yield this.multiTabChannel.hasLeader();
if (hasLeader) {
return this.multiTabChannel.isLeader();
}
yield this.multiTabChannel.getLeadership();
return this.multiTabChannel.isLeader();
});
}
onElectedLeaderhandler() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._transport.isResolved()) {
return;
}
const tranport = yield this.transport;
if (tranport.type !== octez_connect_types_1.TransportType.WALLETCONNECT) {
return;
}
if (tranport.connectionStatus === octez_connect_types_1.TransportStatus.CONNECTED) {
return;
}
yield tranport.connect();
});
}
onBCMessageHandler(message) {
return __awaiter(this, void 0, void 0, function* () {
switch (message.type) {
case octez_connect_types_1.BeaconMessageType.PermissionRequest:
case octez_connect_types_1.BeaconMessageType.OperationRequest:
case octez_connect_types_1.BeaconMessageType.SignPayloadRequest:
case octez_connect_types_1.BeaconMessageType.BroadcastRequest:
case octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeRequest:
case octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeRequest:
this.prepareRequest(message);
break;
case octez_connect_types_1.BeaconMessageType.BlockchainRequest:
this.prepareRequest(message, true);
break;
case 'RESPONSE':
this.handleResponse(message.data.message, message.data.connectionInfo);
break;
case 'DISCONNECT':
this._transport.isResolved() && this.disconnect();
break;
default:
logger.error('onBCMessageHandler', 'message type not recognized', message);
}
});
}
prepareRequest(message_1) {
return __awaiter(this, arguments, void 0, function* (message, isV3 = false) {
if (!this.multiTabChannel.isLeader()) {
return;
}
// block until the transport is ready
const transport = (yield this._transport.promise);
yield transport.waitForResolution();
this.openRequestsOtherTabs.add(message.id);
isV3
? this.makeRequestV3(message.data, message.id)
: this.makeRequest(message.data, false, message.id);
});
}
createStateSnapshot() {
return __awaiter(this, void 0, void 0, function* () {
if (!localStorage || !this.enableMetrics) {
return;
}
const keys = Object.values(octez_connect_types_1.StorageKey).filter((key) => !key.includes('wc@2') && !key.includes('secret') && !key.includes('account'));
try {
for (const key of keys) {
yield this.beaconIDB.set(key, this.storage.getPrefixedKey(key));
}
}
catch (err) {
logger.error('createStateSnapshot', err.message);
}
});
}
initUserID() {
return __awaiter(this, void 0, void 0, function* () {
const id = yield this.storage.get(octez_connect_types_1.StorageKey.USER_ID);
if (id) {
this.userId = id;
return;
}
this.userId = yield (0, octez_connect_utils_1.generateGUID)();
this.storage.set(octez_connect_types_1.StorageKey.USER_ID, this.userId);
});
}
initInternalTransports() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const seed = yield this.storage.get(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED);
if (!seed) {
throw new Error('Secret seed not found');
}
const keyPair = yield (0, octez_connect_utils_1.getKeypairFromSeed)(seed);
if (this.postMessageTransport || this.p2pTransport || this.walletConnectTransport) {
return;
}
this.postMessageTransport = new DappPostMessageTransport_1.DappPostMessageTransport(this.name, keyPair, this.storage);
yield this.addListener(this.postMessageTransport);
this.p2pTransport = new DappP2PTransport_1.DappP2PTransport(this.name, keyPair, this.storage, this.matrixNodes, this.iconUrl, this.appUrl);
yield this.addListener(this.p2pTransport);
// WalletConnect is on by default; skip building/listening the transport
// only when explicitly disabled via disableWalletConnect (#32).
if (this.isWalletConnectEnabled) {
const wcOptions = {
projectId: this.wcProjectId,
relayUrl: this.wcRelayUrl,
metadata: {
name: this.name,
description: (_a = this.description) !== null && _a !== void 0 ? _a : '',
url: (_b = this.appUrl) !== null && _b !== void 0 ? _b : '',
icons: this.iconUrl ? [this.iconUrl] : []
}
};
this.walletConnectTransport = new DappWalletConnectTransport_1.DappWalletConnectTransport(this.name, keyPair, this.storage, {
network: this.network.type,
opts: wcOptions
}, this.checkIfBCLeaderExists.bind(this));
// Apply proposal networks requested before the transport existed
// (requestPermissions runs before init creates the transport).
if (this.pendingWcProposalNetworks) {
this.walletConnectTransport.setProposalNetworks(this.pendingWcProposalNetworks);
this.pendingWcProposalNetworks = undefined;
}
this.initEvents();
yield this.addListener(this.walletConnectTransport);
}
});
}
initEvents() {
if (!this.walletConnectTransport) {
return;
}
this.walletConnectTransport.setEventHandler("CLOSE_ALERT" /* ClientEvents.CLOSE_ALERT */, this.hideUI.bind(this, ['alert', 'toast']));
this.walletConnectTransport.setEventHandler("RESET_STATE" /* ClientEvents.RESET_STATE */, this.channelClosedHandler.bind(this));
this.walletConnectTransport.setEventHandler("WC_ACK_NOTIFICATION" /* ClientEvents.WC_ACK_NOTIFICATION */, this.wcToastHandler.bind(this));
this.walletConnectTransport.setEventHandler("ON_RELAYER_ERROR" /* ClientEvents.ON_RELAYER_ERROR */, this.onRelayerError.bind(this));
}
onRelayerError() {
return __awaiter(this, void 0, void 0, function* () {
yield this.resetInvalidState(false);
const error = new octez_connect_core_1.UnknownBeaconError();
yield this.emitEventWithErrorContext(events_1.BeaconEvent.RELAYER_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () { return errorContext; }));
});
}
wcToastHandler(status) {
return __awaiter(this, void 0, void 0, function* () {
const walletInfo = yield (() => __awaiter(this, void 0, void 0, function* () {
try {
return yield this.getWalletInfo();
}
catch (_a) {
return { name: 'wallet' };
}
}))();
yield this.events.emit(events_1.BeaconEvent.HIDE_UI, ['alert']);
if (status === 'pending') {
this.events.emit(events_1.BeaconEvent.ACKNOWLEDGE_RECEIVED, {
message: {},
extraInfo: {},
walletInfo
});
}
else {
const error = (0, octez_connect_core_1.getError)(octez_connect_types_1.BeaconErrorType.ABORTED_ERROR, undefined);
yield this.emitEventWithErrorContext(events_1.BeaconEvent.PERMISSION_REQUEST_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
errorResponse: { errorType: octez_connect_types_1.BeaconErrorType.ABORTED_ERROR },
walletInfo,
errorContext
});
}));
}
});
}
channelClosedHandler(type) {
return __awaiter(this, void 0, void 0, function* () {
const transport = yield this.transport;
if (transport.type !== type) {
return;
}
yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
this.setActiveAccount(undefined);
yield this.disconnect();
});
}
/**
* Destroy the instance.
*
* WARNING: Call `destroy` whenever you no longer need dAppClient
* as it frees internal subscriptions to the transport and therefore the instance may no longer work properly.
* If you wish to disconnect your dApp, use `disconnect` instead.
*/
destroy() {
const _super = Object.create(null, {
destroy: { get: () => super.destroy }
});
return __awaiter(this, void 0, void 0, function* () {
yield this.createStateSnapshot();
yield _super.destroy.call(this);
});
}
init(transport, substratePairing) {
const _super = Object.create(null, {
init: { get: () => super.init }
});
return __awaiter(this, void 0, void 0, function* () {
if (this._initPromise) {
return this._initPromise;
}
try {
yield this.activeAccountLoaded;
}
catch (_a) {
//
}
this._initPromise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
this._initPromiseReject = reject;
if (transport) {
yield this.addListener(transport);
resolve(yield _super.init.call(this, transport));
}
else if (this._transport.isSettled()) {
yield (yield this.transport).connect();
resolve(yield _super.init.call(this, yield this.transport));
}
else {
const activeAccount = yield this.getActiveAccount();
const stopListening = () => {
if (this.postMessageTransport) {
this.postMessageTransport.stopListeningForNewPeers().catch(console.error);
}
if (this.p2pTransport) {
this.p2pTransport.stopListeningForNewPeers().catch(console.error);
}
if (this.walletConnectTransport) {
this.walletConnectTransport.stopListeningForNewPeers().catch(console.error);
}
};
yield this.initInternalTransports();
if (!this.postMessageTransport ||
!this.p2pTransport ||
(this.isWalletConnectEnabled && !this.walletConnectTransport)) {
return;
}
this.postMessageTransport.connect().then().catch(console.error);
if (activeAccount && activeAccount.origin) {
const origin = activeAccount.origin.type;
// Select the transport that matches the active account
if (origin === octez_connect_types_1.Origin.EXTENSION) {
resolve(yield _super.init.call(this, this.postMessageTransport));
}
else if (origin === octez_connect_types_1.Origin.P2P) {
resolve(yield _super.init.call(this, this.p2pTransport));
}
else if (origin === octez_connect_types_1.Origin.WALLETCONNECT && this.walletConnectTransport) {
resolve(yield _super.init.call(this, this.walletConnectTransport));
}
else {
// The persisted active account was paired over WalletConnect but WC
// has since been disabled (`disableWalletConnect`, #32) or failed to
// initialize, so there is no matching transport to
// restore. Without this branch none of the conditions above call
// resolve() and the init promise created above never settles, so
// init() (and every later call awaiting it) hangs forever. Resolve on
// the always-available P2P transport so the SDK stays usable and the
// stale WC account can be re-paired.
logger.warn('init', 'Active account was paired over WalletConnect but WC is disabled; falling back to the P2P transport');
resolve(yield _super.init.call(this, this.p2pTransport));
}
}
else {
const p2pTransport = this.p2pTransport;
const postMessageTransport = this.postMessageTransport;
const walletConnectTransport = this.walletConnectTransport;
postMessageTransport
.listenForNewPeer((peer) => {
logger.log('init', 'postmessage transport peer connected', peer);
this.analytics.track('event', 'DAppClient', 'Extension connected', {
peerName: peer.name
});
this.events
.emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
.catch((emitError) => console.warn(emitError));
this.setActivePeer(peer).catch(console.error);
this.setTransport(this.postMessageTransport).catch(console.error);
stopListening();
resolve(octez_connect_types_1.TransportType.POST_MESSAGE);
})
.catch(console.error);
p2pTransport
.listenForNewPeer((peer) => {
logger.log('init', 'p2p transport peer connected', peer);
this.analytics.track('event', 'DAppClient', 'octez.connect Wallet connected', {
peerName: peer.name
});
this.events
.emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
.catch((emitError) => console.warn(emitError));
this.setActivePeer(peer).catch(console.error);
this.setTransport(this.p2pTransport).catch(console.error);
stopListening();
resolve(octez_connect_types_1.TransportType.P2P);
})
.catch(console.error);
walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.listenForNewPeer((peer) => {
logger.log('init', 'walletconnect transport peer connected', peer);
this.analytics.track('event', 'DAppClient', 'WalletConnect Wallet connected', {
peerName: peer.name
});
this.events
.emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
.catch((emitError) => console.warn(emitError));
this.setActivePeer(peer).catch(console.error);
this.setTransport(this.walletConnectTransport).catch(console.error);
stopListening();
resolve(octez_connect_types_1.TransportType.WALLETCONNECT);
}).catch(console.error);
octez_connect_transport_postmessage_1.PostMessageTransport.getAvailableExtensions()
.then((extensions) => __awaiter(this, void 0, void 0, function* () {
this.analytics.track('event', 'DAppClient', 'Extensions detected', { extensions });
}))
.catch((error) => {
this._initPromise = undefined;
this._initPromiseReject = undefined;
console.error(error);
});
const abortHandler = () => __awaiter(this, void 0, void 0, function* () {
logger.log('init', 'ABORTED');
this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'abort'));
yield Promise.all([
postMessageTransport.disconnect(),
// p2pTransport.disconnect(), do not abort connection manually
walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.disconnect()
]);
this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
this._activeAccount.isResolved() && this.clearActiveAccount();
this.events.emit(events_1.BeaconEvent.PAIR_ABORTED).catch((emitError) => console.warn(emitError));
if (this._initPromiseReject) {
this._initPromiseReject(new octez_connect_core_1.AbortedBeaconError());
}
this._initPromise = undefined;
this._initPromiseReject = undefined;
});
const serializer = new octez_connect_core_1.Serializer();
const p2pPeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
try {
yield p2pTransport.connect();
}
catch (err) {
logger.error(err);
yield this.hideUI(['alert']); // hide pairing alert
const error = new octez_connect_core_1.UnknownBeaconError();
setTimeout(() => {
this.emitEventWithErrorContext(events_1.BeaconEvent.GENERIC_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
message: err.message,
errorContext
});
})).catch((emitError) => console.warn(emitError));
}, 1000);
abortHandler();
resolve('');
return;
}
resolve(yield serializer.serialize(yield p2pTransport.getPairingRequestInfo()));
}));
const walletConnectPeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
// When WC is disabled there is no transport to pair through (#32).
resolve(walletConnectTransport
? (yield walletConnectTransport.getPairingRequestInfo()).uri
: '');
}));
const postmessagePeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
resolve(yield serializer.serialize(yield postMessageTransport.getPairingRequestInfo()));
}));
this.events
.emit(events_1.BeaconEvent.PAIR_INIT, {
p2pPeerInfo,
postmessagePeerInfo,
walletConnectPeerInfo,
networkType: this.network.type,
abortedHandler: abortHandler.bind(this),
analytics: this.analytics,
featuredWallets: this.featuredWallets,
termsAndConditionsUrl: this.termsAndConditionsUrl,
privacyPolicyUrl: this.privacyPolicyUrl,
substratePairing
})
.catch((emitError) => console.warn(emitError));
}
}
}));
return this._initPromise;
});
}
/**
* Returns the active account
*/
getActiveAccount() {
return __awaiter(this, void 0, void 0, function* () {
return this._activeAccount.promise;
});
}
isInvalidState(account) {
return __awaiter(this, void 0, void 0, function* () {
const activeAccount = yield this._activeAccount.promise;
return !activeAccount
? false
: (activeAccount === null || activeAccount === void 0 ? void 0 : activeAccount.address) !== (account === null || account === void 0 ? void 0 : account.address) && !this.isGetActiveAccountHandled;
});
}
resetInvalidState() {
return __awaiter(this, arguments, void 0, function* (emit = true) {
var _a, _b;
this.accountManager.removeAllAccounts();
this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(undefined);
this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, undefined);
emit && this.events.emit(events_1.BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
!emit && this.hideUI(['alert']);
yield Promise.all([
(_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
(_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
]);
this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
yield this.setActivePeer(undefined);
yield this.setTransport(undefined);
this._initPromise = undefined;
});
}
/**
* Sets the active account
*
* @param account The account that will be set as the active account
*/
setActiveAccount(account) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isGetActiveAccountHandled) {
console.warn(`An active account has been received, but no active subscription was found for BeaconEvent.ACTIVE_ACCOUNT_SET.
For more information, visit: https://octez-connect.tezos.com/guides/migration-guide`);
}
if (account && this._activeAccount.isSettled() && (yield this.isInvalidState(account))) {
const tranport = yield this.transport;
if (tranport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport && tranport.wasDisconnectedByWallet()) {
yield this.resetInvalidState();
return;
}
}
// when I'm resetting the activeAccount
if (!account && this._activeAccount.isResolved() && (yield this.getActiveAccount())) {
const transport = yield this.transport;
const activeAccount = yield this.getActiveAccount();
if (!transport || !activeAccount) {
return;
}
if (!this.debounceSetActiveAccount && transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport) {
this.debounceSetActiveAccount = true;
this._initPromise = undefined;
this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
if (this.multiTabChannel.isLeader() || (0, octez_connect_ui_1.isMobileOS)(window)) {
yield transport.disconnect();
this.openRequestsOtherTabs.clear();
}
else {
this.multiTabChannel.postMessage({
type: 'DISCONNECT'
});
}
Array.from(this.openRequests.entries())
.filter(([id, _promise]) => id !== 'session_update')
.forEach(([id, promise]) => {
promise.reject({
type: octez_connect_types_1.BeaconMessageType.Error,
errorType: octez_connect_types_1.BeaconErrorType.ABORTED_ERROR,
id,
senderId: '',
// SDK-synthesized rejection consumed in-process (never hits the
// wire); stamped with the wrapped baseline for consistency.
version: (0, octez_connect_core_1.negotiateEnvelopeVersion)(undefined)
});
});
this.openRequests.clear();
this.debounceSetActiveAccount = false;
}
}
if (this._activeAccount.isSettled()) {
// If the promise has already been resolved we need to create a new one.
this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(account);
}
else {
this._activeAccount.resolve(account);
}
if (!this.isGetActiveAccountHandled && this._transport.isResolved()) {
const transport = yield this.transport;
if (transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport && transport.wasDisconnectedByWallet()) {
yield this.resetInvalidState();
return;
}
}
if (account) {
const origin = account.origin.type;
yield this.initInternalTransports();
// Select the transport that matches the active account
if (origin === octez_connect_types_1.Origin.EXTENSION) {
yield this.setTransport(this.postMessageTransport);
}
else if (origin === octez_connect_types_1.Origin.P2P) {
yield this.setTransport(this.p2pTransport);
}
else if (origin === octez_connect_types_1.Origin.WALLETCONNECT && this.walletConnectTransport) {
yield this.setTransport(this.walletConnectTransport);
this.walletConnectTransport.forceUpdate('INIT');
}
else if (origin === octez_connect_types_1.Origin.WALLETCONNECT) {
// WalletConnect is opt-out/disabled (#32) but this persisted account was
// paired over WC. There is no WC transport to bind; setting the transport
// to `undefined` would leave the client unable to send any request. Fall
// back to the always-available P2P transport so the SDK stays usable
// (the stale WC account can be re-paired).
logger.warn('setActiveAccount', 'Active account was paired over WalletConnect but WC is disabled; falling back to the P2P transport');
yield this.setTransport(this.p2pTransport);
}
if (this._transport.isResolved()) {
const transport = yield this.transport;
if (transport.connectionStatus === octez_connect_types_1.TransportStatus.NOT_CONNECTED) {
yield transport.connect();
}
}
const peer = yield this.getPeer(account);
yield this.setActivePeer(peer);
}
else {
yield this.setActivePeer(undefined);
yield this.setTransport(undefined);
}
yield this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, account ? account.accountIdentifier : undefined);
yield this.events.emit(events_1.BeaconEvent.ACTIVE_ACCOUNT_SET, account);
return;
});
}
/**
* Clear the active account
*/
clearActiveAccount() {
return this.setActiveAccount();
}
setColorMode(colorMode) {
return __awaiter(this, void 0, void 0, function* () {
return (0, octez_connect_ui_1.setColorMode)(colorMode);
});
}
getColorMode() {
return __awaiter(this, void 0, void 0, function* () {
return (0, octez_connect_ui_1.getColorMode)();
});
}
/**
* @deprecated
*
* Use getOwnAppMetadata instead
*/
getAppMetadata() {
return __awaiter(this, void 0, void 0, function* () {
return this.getOwnAppMetadata();
});
}
showPrepare() {
return __awaiter(this, void 0, void 0, function* () {
const walletInfo = yield (() => __awaiter(this, void 0, void 0, function* () {
try {
return yield this.getWalletInfo();
}
catch (_a) {
return undefined;
}
}))();
yield this.events.emit(events_1.BeaconEvent.SHOW_PREPARE, { walletInfo });
});
}
hideUI(elements) {
return __awaiter(this, void 0, void 0, function* () {
yield this.events.emit(events_1.BeaconEvent.HIDE_UI, elements);
});
}
tryToAppSwitch() {
return __awaiter(this, void 0, void 0, function* () {
if (!(0, octez_connect_ui_1.isMobileOS)(window) || !this.enableAppSwitching) {
return;
}
const wallet = yield this.getWalletInfo();
if (wallet.type !== 'mobile' || !wallet.deeplink) {
return;
}
const link = (0, octez_connect_ui_1.isIOS)(window) ? wallet.deeplink : `${wallet.deeplink}wc?uri=`;
if (!(link === null || link === void 0 ? void 0 : link.length)) {
return;
}
window.location = link;
});
}
addQueryParam(paramName, paramValue) {
return `${paramName}=${paramValue}`;
}
buildPayload(action, status) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
const wallet = yield this.storage.get(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET);
const transport = this._activeAccount.isResolved()
? ((_b = (_a = (yield this.getActiveAccount())) === null || _a === void 0 ? void 0 : _a.origin.type) !== null && _b !== void 0 ? _b : 'UNKNOWN')
: 'UNKNOWN';
return {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: this.userId,
os: (0, octez_connect_ui_1.currentOS)(),
walletName: (_c = wallet === null || wallet === void 0 ? void 0 : wallet.name) !== null && _c !== void 0 ? _c : 'init',
walletType: (_d = wallet === null || wallet === void 0 ? void 0 : wallet.type) !== null && _d !== void 0 ? _d : 'init',
sdkVersion: octez_connect_core_1.SDK_VERSION,
transport,
time: new Date(),
action,
status
})
};
});
}
updateMetricsStorage(payload) {
return __awaiter(this, void 0, void 0, function* () {
const queue = yield this.beaconIDB.getAllKeys('metrics');
if (queue.length >= 1000) {
const key = queue.shift();
this.beaconIDB.delete(key.toString(), 'metrics');
}
this.beaconIDB.set(String(Date.now()), payload, 'metrics');
});
}
sendMetrics(uri, options, thenHandler, catchHandler) {
if (!this.enableMetrics && uri === 'performance-metrics/save') {
options && this.updateMetricsStorage(options.body);
}
if (!this.enableMetrics) {
return;
}
fetch(`${octez_connect_core_1.BACKEND_URL}/${uri}`, options)
.then((res) => thenHandler && thenHandler(res))
.catch((err) => {
console.warn('Network error encountered. Metrics sharing have been automatically disabled.');
logger.error(err.message);
this.enableMetrics = false; // in the event of a network error, stop sending metrics
catchHandler && catchHandler(err);
});
}
checkMakeRequest() {
return __awaiter(this, void 0, void 0, function* () {
const isResolved = this._transport.isResolved();
const isWCInstance = isResolved && (yield this.transport) instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport;
yield this.multiTabChannel.init();
const isLeader = this.multiTabChannel.isLeader();
return !isResolved || !isWCInstance || isLeader || (0, octez_connect_ui_1.isMobileOS)(window);
});
}
/**
* Will remove the account from the local storage and set a new active account if necessary.
*
* @param accountIdentifier ID of the account
*/
removeAccount(accountIdentifier) {
const _super = Object.create(null, {
removeAccount: { get: () => super.removeAccount }
});
return __awaiter(this, void 0, void 0, function* () {
const removeAccountResult = _super.removeAccount.call(this, accountIdentifier);
const activeAccount = yield this.getActiveAccount();
if (activeAccount && activeAccount.accountIdentifier === accountIdentifier) {
yield this.setActiveAccount(undefined);
}
return removeAccountResult;
});
}
/**
* Remove all accounts and set active account to undefined
*/
removeAllAccounts() {
const _super = Object.create(null, {
removeAllAccounts: { get: () => super.removeAllAccounts }
});
return __awaiter(this, void 0, void 0, function* () {
yield _super.removeAllAccounts.call(this);
yield this.setActiveAccount(undefined);
});
}
/**
* Removes a peer and all the accounts that have been connected through that peer
*
* @param peer Peer to be removed
*/
removePeer(peer_1) {
return __awaiter(this, arguments, void 0, function* (peer, sendDisconnectToPeer = false) {
const transport = yield this.transport;
const removePeerResult = transport.removePeer(peer);
yield this.removeAccountsForPeers([peer]);
if (sendDisconnectToPeer) {
yield this.sendDisconnectToPeer(peer, transport);
}
return removePeerResult;
});
}
/**
* Remove all peers and all accounts that have been connected through those peers
*/
removeAllPeers() {
return __awaiter(this, arguments, void 0, function* (sendDisconnectToPeers = false) {
const transport = yield this.transport;
const peers = yield transport.getPeers();
const removePeerResult = transport.removeAllPeers();
yield this.removeAccountsForPeers(peers);
if (sendDisconnectToPeers) {
const disconnectPromises = peers.map((peer) => this.sendDisconnectToPeer(peer, transport));
yield Promise.all(disconnectPromises);
}
return removePeerResult;
});
}
/**
* Allows the user to subscribe to specific events that are fired in the SDK
*
* @param internalEvent The event to subscribe to
* @param eventCallback The callback that will be called when the event occurs
*/
subscribeToEvent(internalEvent, eventCallback) {
return __awaiter(this, void 0, void 0, function* () {
if (internalEvent === events_1.BeaconEvent.ACTIVE_ACCOUNT_SET) {
this.isGetActiveAccountHandled = true;
}
yield this.events.on(internalEvent, eventCallback);
});
}
/**
* Check if we have permissions to send the specific message type to the active account.
* If no active account is set, only permission requests are allowed.
*
* @param type The type of the message
*/
checkPermissions(type) {
return __awaiter(this, void 0, void 0, function* () {
if ([
octez_connect_types_1.BeaconMessageType.PermissionRequest,
octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeRequest,
octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeRequest
].includes(type)) {
return true;
}
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw yield this.sendInternalError('No active account set!');
}
const permissions = activeAccount.scopes;
switch (type) {
case octez_connect_types_1.BeaconMessageType.OperationRequest:
return permissions.includes(octez_connect_types_1.PermissionScope.OPERATION_REQUEST);
case octez_connect_types_1.BeaconMessageType.SignPayloadRequest:
return permissions.includes(octez_connect_types_1.PermissionScope.SIGN);
// TODO: ENCRYPTION
// case BeaconMessageType.EncryptPayloadRequest:
// return permissions.includes(PermissionScope.ENCRYPT)
case octez_connect_types_1.BeaconMessageType.BroadcastRequest:
return true;
default:
return false;
}
});
}
sendNotification(title, message, payload, protocolIdentifier) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const activeAccount = yield this.getActiveAccount();
if (!activeAccount ||
(activeAccount &&
!activeAccount.scopes.includes(octez_connect_types_1.PermissionScope.NOTIFICATION) &&
!activeAccount.notification)) {
throw new Error('notification permissions not given');
}
if (!((_a = activeAccount.notification) === null || _a === void 0 ? void 0 : _a.token)) {
throw new Error('No AccessToken');
}
const url = (_b = activeAccount.notification) === null || _b === void 0 ? void 0 : _b.apiUrl;
if (!url) {
throw new Error('No Push URL set');
}
return this.sendNotificationWithAccessToken({
url,
recipient: activeAccount.address,
title,
body: message,
payload,
protocolIdentifier,
accessToken: (_c = activeAccount.notification) === null || _c === void 0 ? void 0 : _c.token
});
});
}
addBlockchain(chain) {
super.addBlockchain(chain);
chain.getWalletLists().then((walletLists) => {
(0, octez_connect_ui_1.setDesktopList)(walletLists.desktopList);
(0, octez_connect_ui_1.setExtensionList)(walletLists.extensionList);
(0, octez_connect_ui_1.setWebList)(walletLists.webList);
(0, octez_connect_ui_1.setiOSList)(walletLists.iOSList);
});
}
permissionRequest(input) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
logger.log('permissionRequest', input);
const blockchain = this.blockchains.get(input.blockchainIdentifier);
if (!blockchain) {
throw new Error(`Blockchain "${input.blockchainIdentifier}" not supported by dAppClient`);
}
const request = Object.assign(Object.assign({}, input), { type: octez_connect_types_1.BeaconMessageType.PermissionRequest, blockchainData: Object.assign(Object.assign({}, input.blockchainData), { appMetadata: yield this.getOwnAppMetadata() }) });
logger.log('REQUESTION PERMIMISSION V3', 'xxx', request);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
const logId = `makeRequestV3 ${Date.now()}`;
logger.time(true, logId);
const { message: response, connectionInfo } = yield this.requireResponse(this.makeRequestV3(request), request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
logger.log('RESPONSE V3', response, connectionInfo);
// The version stamp lives on the wrapper envelope, not the inner payload;
// reading response.message.version would always miss and collapse to '0',
// misrouting v4 responses when no peer is persisted yet.
const walletPeerV3 = yield this.getPeer();
const walletPeerVersionV3 = (_a = (0, octez_connect_core_1.effectivePeerVersion)(walletPeerV3)) !== null && _a !== void 0 ? _a : response.version;
const partialAccountInfos = yield blockchain.getAccountInfosFromPermissionResponse(response.message, walletPeerVersionV3);
const blockchainDataScopes = response.message.blockchainData.scopes;
const accountInfos = partialAccountInfos.map((p) => ({
accountIdentifier: p.accountId,
senderId: response.senderId,
origin: {
type: connectionInfo.origin,
id: connectionInfo.id
},
address: p.address,
publicKey: p.publicKey,
network: p.network,
scopes: blockchainDataScopes !== null && blockchainDataScopes !== void 0 ? blockchainDataScopes : p.scopes,
connectedAt: new Date().getTime(),
chainData: response.message.blockchainData
}));
yield this.accountManager.addAccounts(accountInfos);
const accountInfo = accountInfos[0];
if (!accountInfo) {
throw new Error(EMPTY_PERMISSION_ACCOUNTS_MESSAGE);
}
yield this.setActiveAccount(accountInfo);
yield blockchain.handleResponse({
request,
account: accountInfo,
output: response,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
yield this.notifySuccess(request, {
account: accountInfo,
output: {
address: accountInfo.address,
network: { type: 'substrate' },
scopes: []
},
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
// return output
return response.message;
});
}
request(input) {
return __awaiter(this, void 0, void 0, function* () {
logger.log('request', input);
const blockchain = this.blockchains.get(input.blockchainIdentifier);
if (!blockchain) {
throw new Error(`Blockchain "${blockchain}" not supported by dAppClient`);
}
yield blockchain.validateRequest(input);
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw yield this.sendInternalError('No active account!');
}
const request = Object.assign(Object.assign({}, input), { type: octez_connect_types_1.BeaconMessageType.BlockchainRequest, accountId: activeAccount.accountIdentifier });
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'start'));
const logId = `makeRequestV3 ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequestV3(request)
: this.makeRequestBC(request);
const { message: response, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
yield blockchain.handleResponse({
request,
account: activeAccount,
output: response,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
yield this.notifySuccess(request, {
walletInfo: yield this.getWalletInfo()
});
return response.message;
});
}
/**
* Send a permission request to the DApp. This should be done as the first step. The wallet will respond
* with an publicKey and permissions that were given. The account returned will be set as the "activeAccount"
* and will be used for the following requests.
*
* @param input The message details we need to prepare the PermissionRequest message.
*/
requestPermissions(input) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (input === null || input === void 0 ? void 0 : input.network) {
throw new Error('[BEACON] the "network" property is no longer accepted in input. Please provide it when instantiating DAppClient.');
}
const request = {
appMetadata: yield this.getOwnAppMetadata(),
type: octez_connect_types_1.BeaconMessageType.PermissionRequest,
network: this.network,
scopes: input && input.scopes
? input.scopes
: [octez_connect_types_1.PermissionScope.OPERATION_REQUEST, octez_connect_types_1.PermissionScope.SIGN]
};
// Dedupe by the normalized CAIP-2 chainId so equivalent input entries
// ('NetX…' and 'tezos:NetX…') collapse to one canonical wire entry.
if ((input === null || input === void 0 ? void 0 : input.networks) && input.networks.length > 0) {
const dedupedNetworks = Array.from(new Map(input.networks.map((n) => {
const chainId = (0, octez_connect_core_1.normalizeTezosCaip2)(n.chainId);
return [chainId, Object.assign(Object.assign({}, n), { chainId })];
})).values());
request.networks = dedupedNetworks;
// Multi-network over WalletConnect travels via the session proposal,
// not the wire payload (WC peers have no beacon version handshake).
// Map each requested chain id to its named network for the proposal;
// ids without a static genesis mapping are skipped here (the
// NetworksUnsupportedBeaconError guard above reports them when the
// dApp opted into requiredMinimumVersion '4').
const proposalNetworkTypes = [];
for (const { chainId } of dedupedNetworks) {
const networkType = (0, octez_connect_core_1.networkTypeFromTezosCaip2)(chainId);
if (networkType === undefined) {
logger.debug('requestPermissions', `No static network mapping for "${chainId}"; excluded from the WalletConnect proposal`);
continue;
}
proposalNetworkTypes.push(networkType);
}
if (this.walletConnectTransport) {
this.walletConnectTransport.setProposalNetworks(proposalNetworkTypes);
}
else {
// The WalletConnect transport is created lazily inside init() (run
// by makeRequest below, before the pairing proposal is built); stash
// the networks and apply them right after creation.
this.pendingWcProposalNetworks = proposalNetworkTypes;
}
}
this.analytics.track('event', 'DAppClient', 'Permission requested');
this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest()) || !(yield this.getActiveAccount())
? this.makeRequest(request, undefined, undefined)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'success'));
logger.log('requestPermissions', '######## MESSAGE #######');
logger.log('requestPermissions', message);
// Capability-aware peer version (legacy wallets echo the dApp's version
// at pairing — see effectivePeerVersion); fall back to the response
// envelope version when no peer is persisted yet.
const walletPeer = yield this.getPeer();
const walletPeerVersion = (_a = (0, octez_connect_core_1.effectivePeerVersion)(walletPeer)) !== null && _a !== void 0 ? _a : message.version;
// Gate the freshly-paired wallet against the dApp's required minimum (a
// no-op under the default, permissive minimum). Throws
// VersionUnsupportedBeaconError when the wallet is too old.
this.assertWalletVersionMeetsMinimum(walletPeerVersion);
const isV4Session = (0, octez_connect_core_1.isMultiNetworkVersion)(walletPeerVersion);
const messageAccounts = message.accounts;
const multiNetworkAccounts = isV4Session &&
messageAccounts &&
typeof messageAccounts === 'object' &&
!Array.isArray(messageAccounts)
? messageAccounts
: null;
let accountInfo;
if (isV4Session) {
const requestedNetworks = ((_b = request.networks) !== null && _b !== void 0 ? _b : []).map((n) => (0, octez_connect_core_1.normalizeTezosCaip2)(n.chainId));
const servedChainIds = multiNetworkAccounts
? Object.keys(multiNetworkAccounts).map(octez_connect_core_1.normalizeTezosCaip2)
: [];
const isMinimumVersionV4OrHigher = (0, octez_connect_core_1.isMultiNetworkVersion)(this.requiredMinimumVersion);
// Reject silently-degraded responses: a v4 session with >=2 networks
// requested but no accounts fanout returned.
if (requestedNetworks.length >= 2 && isMinimumVersionV4OrHigher) {
const missing = requestedNetworks.filter((c) => !servedChainIds.includes(c));
if (missing.length > 0) {
throw new octez_connect_core_1.NetworksUnsupportedBeaconError({
requestedNetworks,
unsupportedNetworks: missing
});
}
}
}
if (isV4Session && multiNetworkAccounts) {
const builtAccounts = yield this.buildAccountInfosFromV4Fanout(message, multiNetworkAccounts, walletPeerVersion !== null && walletPeerVersion !== void 0 ? walletPeerVersion : '0', connectionInfo);
yield this.accountManager.addAccounts(builtAccounts);
accountInfo = builtAccounts[0];
if (!accountInfo) {
throw new Error(EMPTY_PERMISSION_ACCOUNTS_MESSAGE);
}
yield this.setActiveAccount(accountInfo);
logger.log('requestPermissions', '######## MULTI-NETWORK ACCOUNTS #######', builtAccounts.length);
}
else {
accountInfo = yield this.onNewAccount(message, connectionInfo);
logger.log('requestPermissions', '######## ACCOUNT INFO #######');
logger.log('requestPermissions', JSON.stringify(accountInfo));
yield this.accountManager.addAccount(accountInfo);
}
if (!accountInfo) {
throw new Error(EMPTY_PERMISSION_ACCOUNTS_MESSAGE);
}
const output = Object.assign(Object.assign({}, message), { walletKey: accountInfo.walletKey, address: accountInfo.address, accountInfo });
yield this.notifySuccess(request, {
account: accountInfo,
output,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
this.analytics.track('event', 'DAppClient', 'Permission received', {
address: accountInfo.address
});
return output;
});
}
/**
* Send a proof of event request to the wallet. The wallet will either accept or decline the challenge.
* If it is accepted, the challenge will be stored, meaning that even if the user refresh the page, the DAppClient will keep checking if the challenge has been fulfilled.
* Once the challenge is stored, a challenge stored message will be sent to the wallet.
* It's **highly recommended** to run a proof of event challenge to check the identity of an abstracted account
*
* @param input The message details we need to prepare the ProofOfEventChallenge message.
*/
requestProofOfEventChallenge(input) {
return __awaiter(this, void 0, void 0, function* () {
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw new Error('Please request permissions before doing a proof of event challenge');
}
if (activeAccount.walletType !== 'abstracted_account' &&
activeAccount.verificationType !== 'proof_of_event') {
throw new Error('This wallet is not an abstracted account and thus cannot perform proof of event');
}
const request = {
type: octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeRequest,
contractAddress: activeAccount.address,
payload: input.payload
};
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'start'));
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequest(request)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
this.analytics.track('event', 'DAppClient', `Proof of event challenge ${message.isAccepted ? 'accepted' : 'refused'}`, { address: activeAccount.address });
yield this.notifySuccess(request, {
account: activeAccount,
output: message,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
return message;
});
}
/**
* Send a simulated proof of event request to the wallet. The wallet will either accept or decline the challenge.
* It's the same than `requestProofOfEventChallenge` but rather than executing operations on the blockchain to prove the identity,
* The wallet will return a list of operations that you'll be able to run on your side to verify the identity of the abstracted account
* It's **highly recommended** to run a proof of event challenge to check the identity of an abstracted account
*
* @param input The message details we need to prepare the SimulatedProofOfEventChallenge message.
*/
requestSimulatedProofOfEventChallenge(input) {
return __awaiter(this, void 0, void 0, function* () {
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw new Error('Please request permissions before doing a proof of event challenge');
}
if (activeAccount.walletType !== 'abstracted_account' &&
activeAccount.verificationType !== 'proof_of_event') {
throw new Error('This wallet is not an abstracted account and thus cannot perform a simulated proof of event');
}
const request = Object.assign({ type: octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeRequest, contractAddress: activeAccount.address }, input);
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequest(request)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.analytics.track('event', 'DAppClient', `Simulated proof of event challenge ${!message.errorMessage ? 'accepted' : 'refused'}`, { address: activeAccount.address });
yield this.notifySuccess(request, {
account: activeAccount,
output: message,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
return message;
});
}
/**
* This method will send a "SignPayloadRequest" to the wallet. This method is meant to be used to sign
* arbitrary data (eg. a string). It will return the signature in the format of "edsig..."
*
* @param input The message details we need to prepare the SignPayloadRequest message.
*/
requestSignPayload(input) {
return __awaiter(this, void 0, void 0, function* () {
if (!input.payload) {
throw yield this.sendInternalError('Payload must be provided');
}
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw yield this.sendInternalError('No active account!');
}
const payload = input.payload;
if (typeof payload !== 'string') {
throw new Error('Payload must be a string');
}
const signingType = (() => {
switch (input.signingType) {
case octez_connect_types_1.SigningType.OPERATION:
if (!payload.startsWith('03')) {
throw new Error('When using signing type "OPERATION", the payload must start with prefix "03"');
}
return octez_connect_types_1.SigningType.OPERATION;
case octez_connect_types_1.SigningType.MICHELINE:
if (!payload.startsWith('05')) {
throw new Error('When using signing type "MICHELINE", the payload must start with prefix "05"');
}
return octez_connect_types_1.SigningType.MICHELINE;
case octez_connect_types_1.SigningType.RAW:
default:
return octez_connect_types_1.SigningType.RAW;
}
})();
this.analytics.track('event', 'DAppClient', 'Signature requested');
const request = {
type: octez_connect_types_1.BeaconMessageType.SignPayloadRequest,
signingType,
payload,
sourceAddress: input.sourceAddress || activeAccount.address
};
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'start'));
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequest(request)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
yield this.notifySuccess(request, {
account: activeAccount,
output: message,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
this.analytics.track('event', 'DAppClient', 'Signature response');
return message;
});
}
/**
* This method will send an "EncryptPayloadRequest" to the wallet. This method is meant to be used to encrypt or decrypt
* arbitrary data (eg. a string). It will return the encrypted or decrypted payload
*
* @param input The message details we need to prepare the EncryptPayloadRequest message.
*/
// TODO: ENCRYPTION
// public async requestEncryptPayload(
// input: RequestEncryptPayloadInput
// ): Promise<EncryptPayloadResponseOutput> {
// if (!input.payload) {
// throw await this.sendInternalError('Payload must be provided')
// }
// const activeAccount: AccountInfo | undefined = await this.getActiveAccount()
// if (!activeAccount) {
// throw await this.sendInternalError('No active account!')
// }
// const payload = input.payload
// if (typeof payload !== 'string') {
// throw new Error('Payload must be a string')
// }
// if (typeof input.encryptionCryptoOperation === 'undefined') {
// throw new Error('encryptionCryptoOperation must be defined')
// }
// if (typeof input.encryptionType === 'undefined') {
// throw new Error('encryptionType must be defined')
// }
// const request: EncryptPayloadRequestInput = {
// type: BeaconMessageType.EncryptPayloadRequest,
// cryptoOperation: input.encryptionCryptoOperation,
// encryptionType: input.encryptionType,
// payload,
// sourceAddress: input.sourceAddress || activeAccount.address
// }
// const { message, connectionInfo } = await this.makeRequest<
// EncryptPayloadRequest,
// EncryptPayloadResponse
// >(request).catch(async (requestError: ErrorResponse) => {
// throw await this.handleRequestError(request, requestError)
// })
// await this.notifySuccess(request, {
// account: activeAccount,
// output: message,
// connectionContext: connectionInfo,
// walletInfo: await this.getWalletInfo()
// })
// return message
// }
/**
* This method sends an OperationRequest to the wallet. This method should be used for all kinds of operations,
* eg. transaction or delegation. Not all properties have to be provided. Data like "counter" and fees will be
* fetched and calculated by the wallet (but they can still be provided if required).
*
* @param input The message details we need to prepare the OperationRequest message.
*/
requestOperation(input) {
return __awaiter(this, void 0, void 0, function* () {
if (!input.operationDetails) {
throw yield this.sendInternalError('Operation details must be provided');
}
const activeAccount = yield this.getActiveAccount();
if (!activeAccount) {
throw yield this.sendInternalError('No active account!');
}
const resolvedNetwork = yield this.resolveOperationNetwork(input.network, activeAccount);
const request = {
type: octez_connect_types_1.BeaconMessageType.OperationRequest,
network: resolvedNetwork,
operationDetails: input.operationDetails,
sourceAddress: activeAccount.address || ''
};
this.analytics.track('event', 'DAppClient', 'Operation requested');
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'start'));
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequest(request)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
yield this.notifySuccess(request, {
account: activeAccount,
output: message,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
this.analytics.track('event', 'DAppClient', 'Operation response');
return message;
});
}
/**
* Sends a "BroadcastRequest" to the wallet. This method can be used to inject an already signed transaction
* to the network.
*
* @param input The message details we need to prepare the BroadcastRequest message.
*/
requestBroadcast(input) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (!input.signedTransaction) {
throw yield this.sendInternalError('Signed transaction must be provided');
}
// Add error message for deprecation of network
// TODO: Remove when we remove deprecated preferredNetwork
if (input.network !== undefined && this.network.type !== ((_a = input.network) === null || _a === void 0 ? void 0 : _a.type)) {
console.error('[BEACON] The network specified in the DAppClient constructor does not match the network set in the broadcast request. Please set the network in the constructor. Setting it during the Broadcast Request is deprecated.');
}
const request = {
type: octez_connect_types_1.BeaconMessageType.BroadcastRequest,
network: this.network,
signedTransaction: input.signedTransaction
};
this.analytics.track('event', 'DAppClient', 'Broadcast requested');
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'start'));
const logId = `makeRequest ${Date.now()}`;
logger.time(true, logId);
const res = (yield this.checkMakeRequest())
? this.makeRequest(request)
: this.makeRequestBC(request);
const { message, connectionInfo } = yield this.requireResponse(res, request, logId);
logger.time(false, logId);
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
yield this.notifySuccess(request, {
network: this.network,
output: message,
blockExplorer: this.blockExplorer,
connectionContext: connectionInfo,
walletInfo: yield this.getWalletInfo()
});
this.analytics.track('event', 'DAppClient', 'Broadcast response');
return message;
});
}
setActivePeer(peer) {
return __awaiter(this, void 0, void 0, function* () {
if (this._activePeer.isSettled()) {
// If the promise has already been resolved we need to create a new one.
this._activePeer = octez_connect_utils_1.ExposedPromise.resolve(peer);
}
else {
this._activePeer.resolve(peer);
}
if (!peer) {
return;
}
yield this.initInternalTransports();
if (peer.type === 'postmessage-pairing-response') {
yield this.setTransport(this.postMessageTransport);
}
else if (peer.type === 'p2p-pairing-response') {
yield this.setTransport(this.p2pTransport);
}
});
}
/**
* A "setter" for when the transport needs to be changed.
*/
setTransport(transport) {
const _super = Object.create(null, {
setTransport: { get: () => super.setTransport }
});
return __awaiter(this, void 0, void 0, function* () {
if (!transport) {
this._initPromise = undefined;
}
const result = _super.setTransport.call(this, transport);
const event = transport ? Object.assign({}, transport) : undefined;
// remove keyPair, to prevent dApps from accidentaly leaking the privateKey
if (event) {
event.client = Object.assign(Object.assign({}, event.client), { keyPair: undefined });
}
yield this.events.emit(events_1.BeaconEvent.ACTIVE_TRANSPORT_SET, event);
return result;
});
}
/**
* This method will emit an internal error message.
*
* @param errorMessage The error message to send.
*/
sendInternalError(errorMessage) {
return __awaiter(this, void 0, void 0, function* () {
const error = new octez_connect_core_1.UnknownBeaconError();
yield this.emitEventWithErrorContext(events_1.BeaconEvent.INTERNAL_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
text: errorMessage,
errorContext
});
}));
throw new Error(errorMessage);
});
}
emitEventWithErrorContext(event, error, buildPayload, ...additionalArgs) {
return __awaiter(this, void 0, void 0, function* () {
const transport = this._transport.isResolved() ? yield this.transport : undefined;
const errorContext = yield (0, octez_connect_core_1.buildErrorContext)(error, this.storage, transport === null || transport === void 0 ? void 0 : transport.type);
const payload = yield buildPayload(errorContext);
yield this.events.emit(event, payload, ...additionalArgs);
});
}
/**
* This method will remove all accounts associated with a specific peer.
*
* @param peersToRemove An array of peers for which accounts should be removed
*/
removeAccountsForPeers(peersToRemove) {
return __awaiter(this, void 0, void 0, function* () {
const peerIdsToRemove = peersToRemove.map((peer) => peer.senderId);
return this.removeAccountsForPeerIds(peerIdsToRemove);
});
}
removeAccountsForPeerIds(peerIds) {
return __awaiter(this, void 0, void 0, function* () {
const accounts = yield this.accountManager.getAccounts();
// Remove all accounts with origin of the specified peer
const accountsToRemove = accounts.filter((account) => peerIds.includes(account.senderId));
const accountIdentifiersToRemove = accountsToRemove.map((accountInfo) => accountInfo.accountIdentifier);
yield this.accountManager.removeAccounts(accountIdentifiersToRemove);
// Check if one of the accounts that was removed was the active account and if yes, set it to undefined
const activeAccount = yield this.getActiveAccount();
if (activeAccount) {
if (accountIdentifiersToRemove.includes(activeAccount.accountIdentifier)) {
yield this.setActiveAccount(undefined);
}
}
});
}
requireResponse(responsePromise, request, logId) {
return __awaiter(this, void 0, void 0, function* () {
try {
return (yield responsePromise);
}
catch (requestError) {
const error = requestError;
if (error instanceof octez_connect_core_1.AbortedBeaconError) {
this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'));
logger.time(false, logId);
throw error;
}
const errorResponse = error;
errorResponse.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
: this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
logger.time(false, logId);
throw yield this.handleRequestError(request, errorResponse);
}
});
}
/**
* This message handles errors that we receive from the wallet.
*
* @param request The request we sent
* @param beaconError The error we received
*/
handleRequestError(request, beaconError) {
return __awaiter(this, void 0, void 0, function* () {
logger.error('handleRequestError', 'error response', beaconError);
if (beaconError.errorType) {
const buttons = [];
if (beaconError.errorType === octez_connect_types_1.BeaconErrorType.NO_PRIVATE_KEY_FOUND_ERROR) {
const actionCallback = () => __awaiter(this, void 0, void 0, function* () {
const operationRequest = request;
// if the account we requested is not available, we remove it locally
let accountInfo;
if (operationRequest.sourceAddress && operationRequest.network) {
const networkForId = typeof operationRequest.network === 'string'
? (0, octez_connect_core_1.networkFromTezosCaip2)((0, octez_connect_core_1.normalizeTezosCaip2)(operationRequest.network))
: operationRequest.network;
const accountIdentifier = yield (0, octez_connect_core_1.getAccountIdentifier)(operationRequest.sourceAddress, networkForId);
accountInfo = yield this.getAccount(accountIdentifier);
if (accountInfo) {
yield this.removeAccount(accountInfo.accountIdentifier);
}
}
});
buttons.push({ text: 'Remove account', actionCallback });
}
const peer = yield this.getPeer();
const activeAccount = yield this.getActiveAccount();
// If we sent a permission request, received an error and there is no active account, we need to reset the DAppClient.
// This most likely means that the user rejected the first permission request after pairing a wallet, so we "forget" the paired wallet to allow the user to pair again.
if (request.type === octez_connect_types_1.BeaconMessageType.PermissionRequest &&
(yield this.getActiveAccount()) === undefined) {
this._initPromise = undefined;
this.postMessageTransport = undefined;
this.p2pTransport = undefined;
this.walletConnectTransport = undefined;
yield this.setTransport();
yield this.setActivePeer();
}
const error = (0, octez_connect_core_1.getError)(beaconError.errorType, beaconError.errorData);
yield this.emitEventWithErrorContext(beacon_message_events_1.messageEvents[request.type].error, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
errorResponse: beaconError,
walletInfo: yield this.getWalletInfo(peer, activeAccount),
errorMessages: this.errorMessages,
errorContext
});
}), buttons).catch((emitError) => logger.error('handleRequestError', emitError));
throw (0, octez_connect_core_1.getError)(beaconError.errorType, beaconError.errorData);
}
throw beaconError;
});
}
/**
* This message will send an event when we receive a successful response to one of the requests we sent.
*
* @param request The request we sent
* @param response The response we received
*/
notifySuccess(request, response) {
return __awaiter(this, void 0, void 0, function* () {
this.events
.emit(beacon_message_events_1.messageEvents[request.type].success, response)
.catch((emitError) => console.warn(emitError));
});
}
getWalletInfoFromStorage() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.storage.get(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET);
});
}
updateStorageWallet(walletInfo) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const wallet = yield this.storage.get(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET);
if (!wallet) {
return;
}
wallet.name = walletInfo.name;
wallet.icon = (_a = walletInfo.icon) !== null && _a !== void 0 ? _a : wallet.icon;
this.storage.set(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET, wallet);
});
}
getWalletInfo(peer_1, account_1) {
return __awaiter(this, arguments, void 0, function* (peer, account, readFromStorage = true) {
var _a, _b, _c, _d, _e, _f;
const selectedAccount = account ? account : yield this.getActiveAccount();
const selectedPeer = peer ? peer : yield this.getPeer(selectedAccount);
let walletInfo;
if (selectedAccount) {
walletInfo = yield this.appMetadataManager.getAppMetadata(selectedAccount.senderId);
}
let storageWallet;
if (readFromStorage) {
storageWallet = yield this.getWalletInfoFromStorage();
}
if (!walletInfo) {
walletInfo = {
name: (_b = (_a = selectedPeer === null || selectedPeer === void 0 ? void 0 : selectedPeer.name) !== null && _a !== void 0 ? _a : storageWallet === null || storageWallet === void 0 ? void 0 : storageWallet.key) !== null && _b !== void 0 ? _b : '',
icon: (_c = selectedPeer === null || selectedPeer === void 0 ? void 0 : selectedPeer.icon) !== null && _c !== void 0 ? _c : storageWallet === null || storageWallet === void 0 ? void 0 : storageWallet.icon,
type: storageWallet === null || storageWallet === void 0 ? void 0 : storageWallet.type
};
this.updateStorageWallet(walletInfo);
}
const lowerCaseCompare = (str1, str2) => {
if (str1 && str2) {
return str1.toLowerCase() === str2.toLowerCase();
}
return false;
};
const getOrgName = (name) => name.split(/[_\s]+/)[0];
const apps = [
...(0, octez_connect_ui_1.getiOSList)(),
...(0, octez_connect_ui_1.getWebList)(),
...(0, octez_connect_ui_1.getDesktopList)(),
...(0, octez_connect_ui_1.getExtensionList)()
].filter((app) => { var _a; return lowerCaseCompare(getOrgName(app.key), getOrgName((_a = walletInfo === null || walletInfo === void 0 ? void 0 : walletInfo.name) !== null && _a !== void 0 ? _a : 'wallet')); });
// TODO: Remove once all wallets send the icon?
const mobile = apps.find((app) => app.universalLink || app.key.includes('ios') || app.key.includes('mobile'));
const browser = apps.find((app) => app.links);
const desktop = apps.find((app) => app.downloadLink);
const extension = apps.find((app) => app.id);
const appTypeMap = {
extension: { app: extension, type: 'extension' },
desktop: { app: desktop, type: 'desktop' },
mobile: { app: mobile, type: 'mobile' },
web: { app: browser, type: 'web' }
};
const defaultType = () => {
if ((0, octez_connect_ui_1.isBrowser)(window) && browser) {
return { app: browser, type: 'web' };
}
if ((0, octez_connect_ui_1.isDesktop)(window) && desktop) {
return { app: desktop, type: 'desktop' };
}
if ((0, octez_connect_ui_1.isBrowser)(window) && extension) {
return { app: extension, type: 'extension' };
}
if (mobile) {
return { app: mobile, type: 'mobile' };
}
return { app: undefined, type: undefined };
};
const { app, type } = storageWallet ? appTypeMap[storageWallet.type] : defaultType();
if (app) {
let deeplink;
if (app.hasOwnProperty('links')) {
deeplink = app.links[(_d = selectedAccount === null || selectedAccount === void 0 ? void 0 : selectedAccount.network.type) !== null && _d !== void 0 ? _d : this.network.type];
}
else if (app.hasOwnProperty('deepLink')) {
deeplink = app.deepLink;
}
return {
name: (_e = app === null || app === void 0 ? void 0 : app.name) !== null && _e !== void 0 ? _e : walletInfo.name,
icon: (_f = app === null || app === void 0 ? void 0 : app.logo) !== null && _f !== void 0 ? _f : walletInfo.icon,
deeplink,
type: type
};
}
return walletInfo;
});
}
// Throws VersionUnsupportedBeaconError when a wallet that reported a version
// is below requiredMinimumVersion (or reports a malformed one). Callers pass
// the EFFECTIVE version (see effectivePeerVersion): a declared version is
// only trusted alongside the v5 `protocolVersion` pairing marker; a
// marker-less wallet that declared one counts as '2' (legacy wallets echo
// the dApp's version, so the raw peer.version may read deceptively high).
// A peer that never reported a version (WalletConnect pairings, legacy
// pairings predating versioning) is treated as unknown and allowed through,
// so raising the minimum never retroactively breaks an already-paired
// session on a pure read.
assertWalletVersionMeetsMinimum(walletVersion) {
if (walletVersion === undefined) {
return;
}
const min = this.requiredMinimumVersion;
let cmp;
try {
cmp = (0, octez_connect_core_1.compareBeaconVersion)(walletVersion, min);
}
catch (_a) {
// A present-but-malformed version cannot be shown to meet the minimum.
throw new octez_connect_core_1.VersionUnsupportedBeaconError(min, walletVersion);
}
if (cmp < 0) {
throw new octez_connect_core_1.VersionUnsupportedBeaconError(min, walletVersion);
}
}
// Distinct CAIP-2 chain ids across all session accounts. Reads the account
// store, so callers should avoid it on paths that can decide without it.
getSessionChainIds() {
return __awaiter(this, void 0, void 0, function* () {
const allAccounts = yield this.accountManager.getAccounts();
return Array.from(new Set(allAccounts
.map((a) => { var _a; return (_a = a.network) === null || _a === void 0 ? void 0 : _a.chainId; })
.filter((c) => typeof c === 'string' && c.length > 0)));
});
}
// Resolves the network for an outgoing operation. Returns the supplied
// CAIP-2 string (validated and confirmed in-session), or the active
// account's Network when there is no ambiguity. Throws on a multi-network
// session when no network is supplied.
resolveOperationNetwork(inputNetwork, activeAccount) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (inputNetwork) {
if (!(0, octez_connect_core_1.isValidTezosCaip2)(inputNetwork)) {
throw new octez_connect_core_1.NetworksUnsupportedBeaconError({
requestedNetworks: [inputNetwork],
unsupportedNetworks: [inputNetwork],
customMessage: `Malformed CAIP-2 string: "${inputNetwork}". Expected format: tezos:<NetID>.`
});
}
// Fast path: the active account is already on the requested network, so
// it is trivially in-session — skip the account-store read.
if (inputNetwork === ((_a = activeAccount.network) === null || _a === void 0 ? void 0 : _a.chainId)) {
return inputNetwork;
}
const knownChainIds = yield this.getSessionChainIds();
// Tolerate pre-multi-network sessions with no recorded chain ids: such a
// wallet (v2/v3) expects a Network object, not a bare CAIP-2 string it
// cannot interpret, so return the active account's Network rather than the
// string. (There is exactly one network in a session with no chain ids.)
if (knownChainIds.length === 0) {
return activeAccount.network || this.network;
}
if (!knownChainIds.includes(inputNetwork)) {
throw new octez_connect_core_1.NetworksUnsupportedBeaconError({
requestedNetworks: [inputNetwork],
unsupportedNetworks: [inputNetwork]
});
}
return inputNetwork;
}
// No explicit network: a session spanning more than one chain is ambiguous.
const sessionChainIds = yield this.getSessionChainIds();
if (sessionChainIds.length > 1) {
throw new octez_connect_core_1.NetworksUnsupportedBeaconError({
requestedNetworks: [],
unsupportedNetworks: []
});
}
return activeAccount.network || this.network;
});
}
getPeer(account) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
let peer;
if (account) {
logger.log('getPeer', 'We have an account', account);
const postMessagePeers = (_b = (yield ((_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.getPeers()))) !== null && _b !== void 0 ? _b : [];
const p2pPeers = (_d = (yield ((_c = this.p2pTransport) === null || _c === void 0 ? void 0 : _c.getPeers()))) !== null && _d !== void 0 ? _d : [];
const walletConnectPeers = (_f = (yield ((_e = this.walletConnectTransport) === null || _e === void 0 ? void 0 : _e.getPeers()))) !== null && _f !== void 0 ? _f : [];
const peers = [...postMessagePeers, ...p2pPeers, ...walletConnectPeers];
logger.log('getPeer', 'Found peers', peers, account);
peer = peers.find((peerEl) => peerEl.senderId === account.senderId);
if (!peer) {
// We could not find an exact match for a sender, so we most likely received it over a relay
peer = peers.find((peerEl) => peerEl.id === account.origin.id);
}
}
else {
peer = yield this._activePeer.promise;
logger.log('getPeer', 'Active peer', peer);
}
// NB: getPeer() is a pure read used during session resumption, error
// handling and wallet-info lookups, so it must not throw on an
// under-minimum peer (that would wipe a resumed session or mask the real
// wallet error). The version gate is enforced at the pairing response and
// at request-send time instead — see assertWalletVersionMeetsMinimum.
return peer;
});
}
// Build the wrapped Tezos wire message for a flat request input. The v4
// multi-network `networks` field is stripped for peers negotiated below
// v4 — a v3 peer must never see v4 payload fields.
wrapTezosRequest(flat, envelope, account) {
var _a;
if (flat.type === octez_connect_types_1.BeaconMessageType.PermissionRequest) {
const blockchainData = Object.assign({}, flat);
delete blockchainData.type;
if (!(0, octez_connect_core_1.isMultiNetworkVersion)(envelope.version)) {
delete blockchainData.networks;
}
return (0, octez_connect_core_1.wrapBeaconMessage)(envelope, {
blockchainIdentifier: 'tezos',
type: octez_connect_types_1.BeaconMessageType.PermissionRequest,
blockchainData
});
}
const payloadMeta = DAppClient.FLAT_REQUEST_PAYLOAD_TYPES[flat.type];
if (!payloadMeta) {
throw new Error(`Cannot send a "${flat.type}" message: not a Tezos request type`);
}
const payload = Object.assign({}, flat);
delete payload.type;
return (0, octez_connect_core_1.wrapBeaconMessage)(envelope, {
blockchainIdentifier: 'tezos',
type: octez_connect_types_1.BeaconMessageType.BlockchainRequest,
accountId: (_a = account === null || account === void 0 ? void 0 : account.accountIdentifier) !== null && _a !== void 0 ? _a : '',
blockchainData: Object.assign({ type: payloadMeta.type, scope: payloadMeta.scope }, payload)
});
}
// Normalize a wrapped Tezos wire message back to the flat shape the
// pre-fork pipeline (request* methods, onNewAccount, integrator events)
// consumes. Returns undefined for non-Tezos payloads, which keep the
// wrapped pass-through of the generic permissionRequest/request API.
normalizeWrappedTezosMessage(wrapper) {
var _a, _b, _c, _d, _e, _f;
const inner = wrapper.message;
const envelope = { id: wrapper.id, version: wrapper.version, senderId: wrapper.senderId };
// Chain-agnostic control messages.
if (inner.type === octez_connect_types_1.BeaconMessageType.Acknowledge || inner.type === octez_connect_types_1.BeaconMessageType.Disconnect) {
return Object.assign(Object.assign({}, envelope), { type: inner.type });
}
if (inner.type === octez_connect_types_1.BeaconMessageType.Error) {
return Object.assign(Object.assign({}, envelope), { type: octez_connect_types_1.BeaconMessageType.Error, errorType: (_b = (_a = inner.error) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : octez_connect_types_1.BeaconErrorType.UNKNOWN_ERROR, errorData: (_c = inner.error) === null || _c === void 0 ? void 0 : _c.data, description: inner.description });
}
if (inner.blockchainIdentifier === undefined ||
!DAppClient.TEZOS_IDENTIFIERS.includes(inner.blockchainIdentifier)) {
return undefined;
}
if (inner.type === octez_connect_types_1.BeaconMessageType.PermissionResponse ||
inner.type === octez_connect_types_1.BeaconMessageType.ChangeAccountRequest) {
return Object.assign(Object.assign(Object.assign({}, envelope), { type: inner.type }), ((_d = inner.blockchainData) !== null && _d !== void 0 ? _d : {}));
}
if (inner.type === octez_connect_types_1.BeaconMessageType.BlockchainResponse) {
const payloadType = (_e = inner.blockchainData) === null || _e === void 0 ? void 0 : _e.type;
const flatType = typeof payloadType === 'string'
? DAppClient.TEZOS_PAYLOAD_TO_FLAT_TYPE[payloadType]
: undefined;
if (!flatType) {
return undefined;
}
const payload = Object.assign({}, ((_f = inner.blockchainData) !== null && _f !== void 0 ? _f : {}));
delete payload.type;
return Object.assign(Object.assign(Object.assign({}, envelope), { type: flatType }), payload);
}
return undefined;
}
makeRequest(requestInput, skipResponse, otherTabMessageId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e;
const messageId = otherTabMessageId !== null && otherTabMessageId !== void 0 ? otherTabMessageId : (yield (0, octez_connect_utils_1.generateGUID)());
if (this._initPromise && this.isInitPending) {
if (this._initPromiseReject) {
this._initPromiseReject(new octez_connect_core_1.AbortedBeaconError());
this._initPromiseReject = undefined;
}
yield Promise.all([
(_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
(_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
]);
this._initPromise = undefined;
this.hideUI(['toast']);
}
logger.log('makeRequest', 'starting');
this.isInitPending = true;
yield this.init();
this.isInitPending = false;
logger.log('makeRequest', 'after init');
if (yield this.addRequestAndCheckIfRateLimited()) {
this.events
.emit(events_1.BeaconEvent.LOCAL_RATE_LIMIT_REACHED)
.catch((emitError) => console.warn(emitError));
throw new Error('rate limit reached');
}
if (!(yield this.checkPermissions(requestInput.type))) {
this.events.emit(events_1.BeaconEvent.NO_PERMISSIONS).catch((emitError) => console.warn(emitError));
throw new Error('No permissions to send this request to wallet!');
}
if (!this.beaconId) {
throw yield this.sendInternalError('octez.connect ID not defined');
}
const account = yield this.getActiveAccount();
const peer = yield this.getPeer(account);
// Enforce the dApp's required minimum before a request leaves the SDK.
// No-op under the default (permissive) minimum; otherwise rejects an
// under-minimum wallet with VersionUnsupportedBeaconError.
this.assertWalletVersionMeetsMinimum((0, octez_connect_core_1.effectivePeerVersion)(peer));
// The wire dialect is negotiated per peer — min(effective peer version,
// BEACON_VERSION), floor '2'. The effective version is capability-aware:
// legacy wallets ECHO the dApp's version in their pairing response, so
// peer.version alone would misread a 4.8.x wallet as v4; only peers that
// sent the v5 `protocolVersion` marker are trusted. A wrapped-capable
// peer receives the flat requestInput (the unchanged public shape)
// mapped onto the wrapped Tezos payload; a legacy peer receives the flat
// v2 message it has always spoken. Integrators never see either dialect.
const negotiatedVersion = (0, octez_connect_core_1.negotiateEnvelopeVersion)((0, octez_connect_core_1.effectivePeerVersion)(peer));
const requestSenderId = yield (0, octez_connect_core_1.getSenderId)(yield this.beaconId);
let request;
if ((0, octez_connect_core_1.usesWrappedMessages)(negotiatedVersion)) {
const wrapped = this.wrapTezosRequest(requestInput, { id: messageId, version: negotiatedVersion, senderId: requestSenderId }, account);
yield ((_c = this.blockchains.get('tezos')) === null || _c === void 0 ? void 0 : _c.validateRequest(wrapped.message));
request = wrapped;
}
else {
request = Object.assign({ id: messageId, version: negotiatedVersion, senderId: requestSenderId }, requestInput);
}
let exposed;
if (!skipResponse) {
exposed = new octez_connect_utils_1.ExposedPromise();
this.addOpenRequest(request.id, exposed);
}
const payload = yield new octez_connect_core_1.Serializer(this.getPeerProtocolVersion(peer)).serialize(request);
const walletInfo = yield this.getWalletInfo(peer, account);
logger.log('makeRequest', 'sending message', request);
try {
// Hook for performance measurement
if ((_d = window.__beaconPerf) === null || _d === void 0 ? void 0 : _d.onBeforeSend) {
window.__beaconPerf.onBeforeSend();
}
;
(yield this.transport).send(payload, peer);
if (requestInput.type !== octez_connect_types_1.BeaconMessageType.PermissionRequest ||
(this._activeAccount.isResolved() && (yield this._activeAccount.promise))) {
this.tryToAppSwitch();
}
}
catch (sendError) {
const error = new octez_connect_core_1.UnknownBeaconError();
yield this.emitEventWithErrorContext(events_1.BeaconEvent.INTERNAL_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
errorContext,
buttons: [
{
text: 'Reset Connection',
actionCallback: () => __awaiter(this, void 0, void 0, function* () {
(0, octez_connect_ui_1.closeToast)();
this.disconnect();
})
}
]
});
}));
throw sendError;
}
if (!otherTabMessageId) {
this.events
.emit(beacon_message_events_1.messageEvents[requestInput.type].sent, {
walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_e = walletInfo.name) !== null && _e !== void 0 ? _e : 'Wallet' }),
extraInfo: {
resetCallback: () => __awaiter(this, void 0, void 0, function* () {
this.disconnect();
})
}
})
.catch((emitError) => console.warn(emitError));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return exposed === null || exposed === void 0 ? void 0 : exposed.promise; // TODO: fix type
});
}
/**
* This method handles sending of requests to the DApp. It makes sure that the DAppClient is initialized and connected
* to the transport. After that rate limits and permissions will be checked, an ID is attached and the request is sent
* to the DApp over the transport.
*
* @param requestInput The BeaconMessage to be sent to the wallet
* @param account The account that the message will be sent to
*/
makeRequestV3(requestInput, otherTabMessageId) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
if (this._initPromise && this.isInitPending) {
if (this._initPromiseReject) {
this._initPromiseReject(new octez_connect_core_1.AbortedBeaconError());
this._initPromiseReject = undefined;
}
yield Promise.all([
(_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
(_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
]);
this._initPromise = undefined;
this.hideUI(['toast']);
}
const messageId = otherTabMessageId !== null && otherTabMessageId !== void 0 ? otherTabMessageId : (yield (0, octez_connect_utils_1.generateGUID)());
logger.log('makeRequest', 'starting');
this.isInitPending = true;
yield this.init(undefined, true);
this.isInitPending = false;
logger.log('makeRequest', 'after init');
if (yield this.addRequestAndCheckIfRateLimited()) {
this.events
.emit(events_1.BeaconEvent.LOCAL_RATE_LIMIT_REACHED)
.catch((emitError) => console.warn(emitError));
throw new Error('rate limit reached');
}
if (!this.beaconId) {
throw yield this.sendInternalError('octez.connect ID not defined');
}
const account = yield this.getActiveAccount();
const peer = yield this.getPeer(account);
// Enforce the dApp's required minimum before a request leaves the SDK.
this.assertWalletVersionMeetsMinimum((0, octez_connect_core_1.effectivePeerVersion)(peer));
// The envelope carries the version negotiated against the effective
// (capability-aware) peer version — see effectivePeerVersion: legacy
// wallets echo the dApp's version, so only peers that sent the v5
// `protocolVersion` marker are trusted with the v4 dialect. Unlike
// makeRequest, this path is wrapper-only (the v3 blockchainData shape
// has no flat equivalent), so it floors at '3' instead of '2': a legacy
// wallet routes a version-'3' wrapper through its v3 branch, whereas a
// '2'-stamped wrapper would be misrouted as flat and silently dropped.
const negotiatedVersion = (0, octez_connect_core_1.negotiateEnvelopeVersion)((0, octez_connect_core_1.effectivePeerVersion)(peer));
const request = {
id: messageId,
version: (0, octez_connect_core_1.usesWrappedMessages)(negotiatedVersion)
? negotiatedVersion
: String(octez_connect_core_1.MESSAGE_WRAPPED_FROM_VERSION),
senderId: yield (0, octez_connect_core_1.getSenderId)(yield this.beaconId),
message: requestInput
};
const exposed = new octez_connect_utils_1.ExposedPromise();
this.addOpenRequest(request.id, exposed);
const payload = yield new octez_connect_core_1.Serializer(this.getPeerProtocolVersion(peer)).serialize(request);
const walletInfo = yield this.getWalletInfo(peer, account);
logger.log('makeRequest', 'sending message', request);
try {
// Hook for performance measurement
if ((_c = window.__beaconPerf) === null || _c === void 0 ? void 0 : _c.onBeforeSend) {
window.__beaconPerf.onBeforeSend();
}
;
(yield this.transport).send(payload, peer);
if (request.message.type !== octez_connect_types_1.BeaconMessageType.PermissionRequest ||
(this._activeAccount.isResolved() && (yield this._activeAccount.promise))) {
this.tryToAppSwitch();
}
}
catch (sendError) {
const error = new octez_connect_core_1.UnknownBeaconError();
yield this.emitEventWithErrorContext(events_1.BeaconEvent.INTERNAL_ERROR, error, (errorContext) => __awaiter(this, void 0, void 0, function* () {
return ({
text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
errorContext,
buttons: [
{
text: 'Reset Connection',
actionCallback: () => __awaiter(this, void 0, void 0, function* () {
(0, octez_connect_ui_1.closeToast)();
this.disconnect();
})
}
]
});
}));
throw sendError;
}
const index = requestInput.type;
this.events
.emit(beacon_message_events_1.messageEvents[index].sent, {
walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_d = walletInfo.name) !== null && _d !== void 0 ? _d : 'Wallet' }),
extraInfo: {
resetCallback: () => __awaiter(this, void 0, void 0, function* () {
this.disconnect();
})
}
})
.catch((emitError) => console.warn(emitError));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return exposed.promise; // TODO: fix type
});
}
makeRequestBC(request) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._transport.isResolved()) {
return;
}
const transport = yield this.transport;
if (transport.type !== octez_connect_types_1.TransportType.WALLETCONNECT) {
return;
}
if (yield this.addRequestAndCheckIfRateLimited()) {
this.events
.emit(events_1.BeaconEvent.LOCAL_RATE_LIMIT_REACHED)
.catch((emitError) => console.warn(emitError));
throw new Error('rate limit reached');
}
const id = yield (0, octez_connect_utils_1.generateGUID)();
this.multiTabChannel.postMessage({
type: request.type,
data: request,
id
});
if (request.type !== octez_connect_types_1.BeaconMessageType.PermissionRequest ||
(this._activeAccount.isResolved() && (yield this._activeAccount.promise))) {
this.tryToAppSwitch();
}
this.events
.emit(beacon_message_events_1.messageEvents[octez_connect_types_1.BeaconMessageType.PermissionRequest].sent, {
walletInfo: yield this.getWalletInfo(),
extraInfo: {
resetCallback: () => this.disconnect()
}
})
.catch((emitError) => console.warn(emitError));
const exposed = new octez_connect_utils_1.ExposedPromise();
this.addOpenRequest(id, exposed);
return exposed.promise;
});
}
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._transport.isResolved()) {
throw new Error('No transport available.');
}
const transport = yield this.transport;
if (transport.connectionStatus === octez_connect_types_1.TransportStatus.NOT_CONNECTED) {
throw new Error('Not connected.');
}
yield this.createStateSnapshot();
this.sendMetrics('performance-metrics/save', yield this.buildPayload('disconnect', 'start'));
const peers = yield transport.getPeers();
if (peers.length > 0) {
const shouldNotifyPeers = !(transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport);
yield this.removeAllPeers(shouldNotifyPeers);
}
yield this.clearActiveAccount();
if (!(transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport)) {
try {
yield transport.disconnect();
}
catch (disconnectError) {
const message = disconnectError instanceof Error ? disconnectError.message : String(disconnectError);
if (typeof message === 'string' && message.includes('Syncing stopped manually')) {
logger.log('disconnect', 'Matrix sync stopped manually');
}
else {
throw disconnectError;
}
}
}
this.postMessageTransport = undefined;
this.p2pTransport = undefined;
this.walletConnectTransport = undefined;
yield this.setTransport();
this._initPromise = undefined;
this._initPromiseReject = undefined;
this.isInitPending = false;
this.sendMetrics('performance-metrics/save', yield this.buildPayload('disconnect', 'success'));
});
}
/**
* Adds a requests to the "openRequests" set so we know what messages have already been answered/handled.
*
* @param id The ID of the message
* @param promise A promise that resolves once the response for that specific message is received
*/
addOpenRequest(id, promise) {
logger.log('addOpenRequest', this.name, `adding request ${id} and waiting for answer`);
this.openRequests.set(id, promise);
}
sendNotificationWithAccessToken(notification) {
return __awaiter(this, void 0, void 0, function* () {
const { url, recipient, title, body, payload, protocolIdentifier, accessToken } = notification;
const timestamp = new Date().toISOString();
const keypair = yield this.keyPair;
const rawPublicKey = keypair.publicKey;
const prefix = Buffer.from(new Uint8Array([13, 15, 37, 217]));
const publicKey = bs58check_1.default.encode(Buffer.concat([prefix, Buffer.from(rawPublicKey)]));
const constructedString = [
'Tezos Signed Message: ',
recipient,
title,
body,
timestamp,
payload
].join(' ');
const bytes = (0, octez_connect_utils_1.toHex)(constructedString);
const payloadBytes = `05` + `01${bytes.length.toString(16).padStart(8, '0')}${bytes}`;
const signature = yield (0, octez_connect_utils_1.signMessage)(payloadBytes, {
secretKey: Buffer.from(keypair.secretKey)
});
const notificationResponse = yield fetch(`${url}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient,
title,
body,
timestamp,
payload,
accessToken,
protocolIdentifier,
sender: {
name: this.name,
publicKey,
signature
}
})
});
if (!notificationResponse.ok) {
throw new Error(`sendNotification failed: ${notificationResponse.status} ${notificationResponse.statusText}`);
}
return notificationResponse.json();
});
}
// Delegate the per-network unpacking to the Tezos blockchain plug-in (the
// single owner of the v4 fanout shape) and enrich each returned record with
// the envelope-level AccountInfo metadata that the parser doesn't see.
buildAccountInfosFromV4Fanout(message, fanout, peerVersion, connectionInfo) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
// Keyed by the CAIP-2 namespace `'tezos'`, which is also the wire-format
// `blockchainIdentifier` carried by PermissionRequestV3/Response. The
// dApp must have registered `new TezosBlockchain()` via `addBlockchain`
// before requesting permissions on the v4 path.
const tezosBlockchain = this.blockchains.get('tezos');
if (!tezosBlockchain) {
throw new Error('Tezos blockchain handler is required to parse v4 permission responses');
}
// Normalize the legacy `pubkey`/`pubKey` aliases and prefix any raw key
// before handing the fanout to the parser. The parser keeps publicKey
// strings verbatim, so prefixing here keeps stored records canonical.
const envelopePublicKey = (_a = message.publicKey) !== null && _a !== void 0 ? _a : message.pubkey;
const prefixedEnvelopePk = envelopePublicKey ? (0, octez_connect_utils_1.prefixPublicKey)(envelopePublicKey) : undefined;
const prefixedFanout = Object.fromEntries(Object.entries(fanout).map(([cid, raw]) => [
cid,
Object.assign(Object.assign({}, raw), { publicKey: (raw === null || raw === void 0 ? void 0 : raw.publicKey) ? (0, octez_connect_utils_1.prefixPublicKey)(raw.publicKey) : undefined })
]));
const synthesizedResponse = {
blockchainIdentifier: 'tezos',
type: octez_connect_types_1.BeaconMessageType.PermissionResponse,
blockchainData: Object.assign(Object.assign({}, message), { publicKey: prefixedEnvelopePk, accounts: prefixedFanout })
};
const partials = yield tezosBlockchain.getAccountInfosFromPermissionResponse(synthesizedResponse, peerVersion);
const walletKey = (_b = (yield this.storage.get(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET))) === null || _b === void 0 ? void 0 : _b.key;
return Promise.all(partials.map((p) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!p.publicKey && !p.address) {
throw new Error('PublicKey or Address must be defined for multi-network account');
}
const address = p.address || (yield (0, octez_connect_utils_1.getAddressFromPublicKey)(p.publicKey));
// The parser keys by the address it saw; re-derive accountId if we
// synthesized the address from a publicKey above.
const accountIdentifier = p.address || !p.network ? p.accountId : yield (0, octez_connect_core_1.getAccountIdentifier)(address, p.network);
return Object.assign({ accountIdentifier, senderId: message.senderId, origin: {
type: connectionInfo.origin,
id: connectionInfo.id
}, walletKey,
address, publicKey: p.publicKey || undefined,
// The v4 fanout parser always sets a Network (networkFromTezosCaip2);
// the `?? this.network` fallback keeps the type `Network` (required by
// AccountInfo) without an `as AccountInfo` assertion.
network: (_a = p.network) !== null && _a !== void 0 ? _a : this.network, scopes: message.scopes, threshold: message.threshold, notification: message.notification, connectedAt: new Date().getTime(), walletType: (_b = message.walletType) !== null && _b !== void 0 ? _b : 'implicit', verificationType: message.verificationType }, (message.verificationType === 'proof_of_event'
? { hasVerifiedChallenge: false }
: {}));
})));
});
}
onNewAccount(message, connectionInfo) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
// TODO: Migration code. Remove sometime after 1.0.0 release.
const tempPK = message.publicKey || message.pubkey || message.pubKey;
const publicKey = tempPK ? (0, octez_connect_utils_1.prefixPublicKey)(tempPK) : undefined;
if (!publicKey && !message.address) {
throw new Error('PublicKey or Address must be defined');
}
const address = (_a = message.address) !== null && _a !== void 0 ? _a : (yield (0, octez_connect_utils_1.getAddressFromPublicKey)(publicKey));
if (!(0, octez_connect_utils_1.isValidAddress)(address)) {
throw new Error(`Invalid address: "${address}"`);
}
if (message.walletType === 'abstracted_account' &&
address.substring(0, 3) !== octez_connect_utils_1.CONTRACT_PREFIX) {
throw new Error(`Invalid abstracted account address "${address}", it should be a ${octez_connect_utils_1.CONTRACT_PREFIX} address`);
}
logger.log('######## MESSAGE #######');
logger.log('onNewAccount', message);
const walletKey = (_b = (yield this.storage.get(octez_connect_types_1.StorageKey.LAST_SELECTED_WALLET))) === null || _b === void 0 ? void 0 : _b.key;
const accountInfo = Object.assign({ accountIdentifier: yield (0, octez_connect_core_1.getAccountIdentifier)(address, message.network), senderId: message.senderId, origin: {
type: connectionInfo.origin,
id: connectionInfo.id
}, walletKey,
address,
publicKey, network: message.network, scopes: message.scopes, threshold: message.threshold, notification: message.notification, connectedAt: new Date().getTime(), walletType: (_c = message.walletType) !== null && _c !== void 0 ? _c : 'implicit', verificationType: message.verificationType }, (message.verificationType === 'proof_of_event' ? { hasVerifiedChallenge: false } : {}));
logger.log('accountInfo', '######## ACCOUNT INFO #######');
logger.log('accountInfo', accountInfo);
yield this.accountManager.addAccount(accountInfo);
yield this.setActiveAccount(accountInfo);
return accountInfo;
});
}
}
exports.DAppClient = DAppClient;
/**
* This method handles sending of requests to the DApp. It makes sure that the DAppClient is initialized and connected
* to the transport. After that rate limits and permissions will be checked, an ID is attached and the request is sent
* to the DApp over the transport.
*
* @param requestInput The BeaconMessage to be sent to the wallet
* @param account The account that the message will be sent to
* @param skipResponse If true, the function return as soon as the message is sent
*/
// Maps each flat Tezos request type to its wrapped blockchainData
// discriminator + scope (the pre-fork flat wire strings, kept verbatim;
// mirrored by the wallet-side flat-output normalization).
DAppClient.FLAT_REQUEST_PAYLOAD_TYPES = {
[octez_connect_types_1.BeaconMessageType.OperationRequest]: { type: 'operation_request', scope: 'operation_request' },
[octez_connect_types_1.BeaconMessageType.SignPayloadRequest]: { type: 'sign_payload_request', scope: 'sign' },
[octez_connect_types_1.BeaconMessageType.BroadcastRequest]: { type: 'broadcast_request', scope: 'broadcast' },
[octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeRequest]: {
type: 'proof_of_event_challenge_request',
scope: 'proof_of_event'
},
[octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeRequest]: {
type: 'simulated_proof_of_event_challenge_request',
scope: 'proof_of_event'
}
};
// Wrapped Tezos response blockchainData discriminators → the flat
// BeaconMessageType the request* methods (and integrators) consume.
DAppClient.TEZOS_PAYLOAD_TO_FLAT_TYPE = {
operation_response: octez_connect_types_1.BeaconMessageType.OperationResponse,
sign_payload_response: octez_connect_types_1.BeaconMessageType.SignPayloadResponse,
broadcast_response: octez_connect_types_1.BeaconMessageType.BroadcastResponse,
proof_of_event_challenge_response: octez_connect_types_1.BeaconMessageType.ProofOfEventChallengeResponse,
simulated_proof_of_event_challenge_response: octez_connect_types_1.BeaconMessageType.SimulatedProofOfEventChallengeResponse
};
DAppClient.TEZOS_IDENTIFIERS = ['tezos', 'xtz'];
//# sourceMappingURL=DAppClient.js.map