@broxus/tvm-connect
Version:
TypeScript SDK for connecting to Nekoton-compatible wallets using a unified interface.
652 lines (651 loc) • 24.4 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TvmConnectService = void 0;
const js_core_1 = require("@broxus/js-core");
const js_utils_1 = require("@broxus/js-utils");
const everscale_inpage_provider_1 = require("everscale-inpage-provider");
const mobx_1 = require("mobx");
const misc_1 = require("../misc");
const utils_1 = require("../utils");
class TvmConnectService extends js_core_1.AbstractStore {
params;
name = 'TvmWalletService';
constructor(params) {
super();
this.params = params;
const recentMeta = (0, utils_1.getRecentConnectionMeta)(params?.recentMetaStorageKey);
const networks = (!params?.networks || params.networks.length === 0) ? (0, misc_1.getPredefinedNetworks)() : params.networks;
// eslint-disable-next-line max-len
const providers = (!params?.providers || params.providers.length === 0) ? (0, misc_1.getPredefinedProviders)() : params.providers;
this.setData(() => ({ networks, providers }));
const defaultNetworkId = params?.defaultNetworkId ?? recentMeta?.chainId;
const providerId = params?.providerId || (0, misc_1.getPredefinedProviderId)(recentMeta);
const hasProvider = providers.some(provider => provider.id === providerId);
const isNetworkSupported = networks.some(network => network.chainId === defaultNetworkId?.toString());
let chainId;
if (isNetworkSupported || (params?.allowUnsupportedNetworks ?? true)) {
chainId = Number(defaultNetworkId);
}
this.setState(() => ({
chainId,
isSyncing: undefined,
providerId: hasProvider ? providerId : undefined,
}));
(0, mobx_1.makeObservable)(this);
if (params?.autoInit ?? true) {
this._init().catch(reason => {
(0, js_utils_1.error)(`${this.constructor.name} init failed with an error`, reason);
});
}
}
/**
* Define current provider connector
* @returns {NekotonConnector|undefined}
*/
get connector() {
return this.providers?.find(config => config.id === this.providerId)?.connector;
}
/**
* Initialize the wallet connection manually
* @returns {Promise<ProviderRpcClient | undefined>}
*/
async init() {
const provider = await this.connector?.init();
await this._init();
return provider;
}
/**
* Manually connect to the wallet
* @returns {Promise<void>}
*/
async connect(networkIdOParams, options) {
if (this.isConnecting || this.isDisconnecting) {
return;
}
try {
const defaultNetworkId = typeof networkIdOParams === 'number' ? networkIdOParams : networkIdOParams?.networkId
?? this.params?.defaultNetworkId
?? this.network?.chainId
?? js_core_1.TvmChains.EverscaleMainnet;
const network = this.networks.find(_network => _network.chainId === defaultNetworkId?.toString());
// eslint-disable-next-line no-nested-ternary
const networkParams = typeof networkIdOParams === 'object'
? networkIdOParams
: network ? (0, utils_1.toChainParams)(network) : undefined;
await this.connector?.connect(networkParams, options);
}
catch (e) {
(0, js_utils_1.error)('Tvm Wallet connect error', e);
throw e;
}
}
/**
* Manually disconnect from the wallet
* @param {DisconnectOptions} [options]
* @returns {Promise<void>}
*/
async disconnect(options) {
if ((this.isConnecting || this.isDisconnecting) && !options?.force) {
return;
}
try {
await this.connector?.disconnect(options);
this.setData('contractState', undefined);
}
catch (e) {
(0, js_utils_1.error)(`${this.connector?.constructor.name || 'Wallet'} disconnect failed with an error`, e);
}
}
/**
* Add custom token asset to the TVM Wallet
* @param {Address | string} address
* @param {AssetType} [type]
*/
async addAsset(address, type = 'tip3_token') {
return this.connector?.addAsset(address, type);
}
/**
* Add new network to the wallet.
* Supported since version `0.3.40` of the `everscale-inpage-provider`
* @param {AddNetwork} network
* @param {boolean} switchNetwork
* @returns {Promise<Network | null>}
*/
async addNetwork(network, switchNetwork) {
if (!this.connector) {
throw new Error('Provider is not defined');
}
return this.connector.addNetwork(network, switchNetwork);
}
/**
* Switch to network by the given networkId or network params.
* Supported since version `0.3.40` of the `everscale-inpage-provider`
* @param {number | AddNetwork} networkIdOParams
* @returns {Promise<Network | null>}
*/
async switchNetwork(networkIdOParams) {
if (!this.connector) {
throw new Error('Provider is not defined');
}
return this.connector.switchNetwork(networkIdOParams);
}
/**
* Change current account in the wallet.
* @returns {Promise<void>}
*/
async changeAccount() {
if (!this.provider) {
throw new Error('Provider is not defined');
}
await this.provider.changeAccount();
}
/**
* An independent RPC connection that allows you to receive data from the blockchain without
* being able to send transactions.
*
* This connection does not require `accountInteraction` permissions.
* @returns {ProviderRpcClient}
*/
get connection() {
const params = this.params?.connectionParams;
return params?.factory?.(this.network) ?? new everscale_inpage_provider_1.ProviderRpcClient({
provider: params?.provider,
});
}
/**
* A Provider that requires a `basic` and `accountInteraction` permissions for the ability to
* send transactions.
* @returns {ProviderRpcClient|undefined}
*/
get provider() {
return this.connector?.provider;
}
/**
* Returns computed wallet contract state
* @returns {TvmConnectServiceData["contractState"]}
*/
get contract() {
return this._data.contractState;
}
/**
* The list of the supported networks
* @returns {TvmConnectServiceData["networks"]}
*/
get networks() {
return this._data.networks ?? [];
}
/**
* The list of the supported providers
* @returns {TvmConnectServiceData["providers"]}
*/
get providers() {
return this._data.providers ?? [];
}
/**
* Returns current network chain id
* @returns {TvmConnectServiceState["chainId"]}
*/
get chainId() {
return this._state.chainId;
}
/**
* Returns `true` if wallet contract is updating
* @returns {TvmConnectServiceState["isSyncing"]}
*/
get isSyncing() {
return this._state.isSyncing;
}
/**
* A unique identifier of the connected wallet (provider)
* @returns {TvmConnectServiceState["providerId"]}
*/
get providerId() {
return this._state.providerId;
}
/**
* Returns computed wallet address value
* @returns {Address|undefined}
*/
get address() {
return this.account?.address;
}
/**
* Returns computed wallet normalized balance value
* @returns {FullContractState["balance"]}
*/
get balance() {
return this.contract?.balance ?? '0';
}
/**
* Returns network native currency
* @returns {NativeCurrency<Address>}
*/
get currency() {
return {
decimals: this.network?.currency.decimals ?? js_core_1.DEFAULT_NATIVE_CURRENCY_DECIMALS,
icon: this.network?.currency.icon,
name: this.network?.currency.name,
symbol: this.network?.currency.symbol || 'EVER',
wrappedCurrencyAddress: this.network?.currency.wrappedCurrencyAddress
? (0, js_core_1.resolveTvmAddress)(this.network.currency.wrappedCurrencyAddress)
: undefined,
};
}
/**
* Whether provider is available.
*
* That means extension is installed and activated, else `false`
* @returns {boolean}
*/
get hasProvider() {
return this.connector?.provider != null;
}
/**
* Returns `true` if wallet is connected
* @returns {boolean}
*/
get isConnected() {
return this.address != null;
}
/**
* Returns `true` if installed wallet has outdated version
*/
get isOutdated() {
const providerConfig = this.params?.providers?.find(config => config.id === this.providerId);
if (this.connector?.version === undefined || providerConfig?.minVersion === undefined) {
return false;
}
const [currentMajorVersion = '0', currentMinorVersion = '0', currentPatchVersion = '0'] = this.connector.version.split('.');
const [minMajorVersion, minMinorVersion, minPatchVersion] = providerConfig.minVersion.split('.');
return (currentMajorVersion < minMajorVersion
|| (currentMajorVersion <= minMajorVersion && currentMinorVersion < minMinorVersion)
|| (currentMajorVersion <= minMajorVersion
&& currentMinorVersion <= minMinorVersion
&& currentPatchVersion < minPatchVersion));
}
/**
* Returns `true` if connection to RPC is initialized and connected
* @returns {boolean}
*/
get isReady() {
return (!this.isInitializing
&& !this.isConnecting
&& !this.isDisconnecting
&& this.isInitialized === true
&& this.isConnected);
}
/**
* Checks for supported network
* @returns {boolean}
*/
get isUnsupportedNetwork() {
if (this.chainId == null) {
return false;
}
return !(this.networks?.map(network => network.chainId)?.includes(this.chainId?.toString()) ?? false);
}
/**
* Returns current network configuration
* @returns {TvmNetworkConfig|undefined}
*/
get network() {
const chainId = this.chainId?.toString() ?? this.params?.defaultNetworkId?.toString();
return this.networks?.find(network => network.type === 'tvm' && network.chainId === chainId);
}
/**
* Returns details about current connected provider
* @returns {TvmWalletProviderInfo | undefined}
*/
get providerInfo() {
return this.providers?.find(config => config.id === this.providerId)?.info ?? this.connector?.info;
}
/**
* Returns computed account
* @returns {NekotonConnector["account"]}
*/
get account() {
return this.connector?.account;
}
/**
* Returns `true` if wallet is connecting
* @returns {NekotonConnector["isConnecting"]}
*/
get isConnecting() {
return this.connector?.isConnecting;
}
/**
* Returns `true` if wallet is disconnecting
* @returns {NekotonConnector["isDisconnecting"]}
*/
get isDisconnecting() {
return this.connector?.isDisconnecting;
}
/**
* Returns `true` if wallet is initialized
* @returns {NekotonConnector["isInitialized"]}
*/
get isInitialized() {
return this.connector?.isInitialized;
}
/**
* Returns `true` if wallet is initializing
* @returns {NekotonConnector["isInitializing"]}
*/
get isInitializing() {
return this.connector?.isInitializing;
}
/**
* Synchronizes the contract state of the current account.
*
* If `force` is not set, it will only sync if not already syncing.
*
* @param {Forceable} [options] - Options to control the sync behavior.
* @returns {Promise<void>}
*/
async syncContractState(options) {
if (!this.provider || (!options?.force && this.isSyncing)) {
return;
}
try {
if (this.account?.address) {
this.setState('isSyncing', this.isSyncing === undefined);
const state = await (0, js_core_1.getFullContractState)(this.provider, this.account.address, { force: options?.force });
this.setData('contractState', state);
}
}
catch (e) {
(0, js_utils_1.groupCollapsed)(`%c${this.constructor.name}%c get account full contract state error`, js_core_1.errorLabelStyle, js_core_1.inheritTextStyle);
(0, js_utils_1.error)(e);
(0, js_utils_1.groupEnd)();
throw e;
}
finally {
this.setState('isSyncing', false);
}
}
/**
* Initializes the reactive logic for synchronizing the state of the contract and network.
* Sets up disposers for reacting to address changes and network chain ID updates.
*
* @return {Promise<void>}
*/
async _init() {
this._accountDisposer = (0, mobx_1.reaction)(() => this.address, async (address, prevAddress) => {
if (!(0, js_core_1.isAddressesEquals)(address, prevAddress)) {
this.setState('isSyncing', undefined);
}
if (address) {
try {
await this._watch();
}
catch (e) {
await this._unwatch();
}
await this.syncContractState({ force: !this.isSyncing });
}
}, { delay: 100, equals: mobx_1.comparer.shallow });
this._networkDisposer = (0, mobx_1.reaction)(() => this.connector?.chainId, async (chainId, prevChainId) => {
const isSupported = this.networks?.some(network => network.chainId === chainId?.toString());
if (isSupported || (this.params?.allowUnsupportedNetworks ?? true)) {
if (chainId !== prevChainId || !prevChainId) {
this.setState('chainId', chainId);
this.setState('isSyncing', undefined);
if (this.address) {
try {
await this._watch();
}
catch (e) {
await this._unwatch();
}
await this.syncContractState({ force: true });
}
}
}
}, { delay: 100 });
}
/**
* Subscribe to current account address contract state updates
* and update the local data while updates is coming.
* @returns {Promise<void>}
* @protected
*/
async _watch() {
if (!this.provider || !this.address) {
await this._unwatch();
return;
}
await this._unwatch();
this._contractSubscriber = new this.provider.Subscriber();
await this._contractSubscriber.states(this.address).delayed(stream => {
(0, js_utils_1.debug)(`%c${this.constructor.name}%c subscribes to the user wallet [%c${(0, js_utils_1.sliceAddress)(this.address?.toString())}%c] contract updates`, js_core_1.successLabelStyle, js_core_1.inheritTextStyle, js_core_1.successTextStyle, js_core_1.inheritTextStyle);
return stream.on(async (e) => {
(0, js_utils_1.debug)(`%c${this.constructor.name}%c %ccontractStateChanged%c event was captured for current connected wallet %c${(0, js_utils_1.sliceAddress)(this.address?.toString())}`, js_core_1.successLabelStyle, js_core_1.inheritTextStyle, js_core_1.successTextStyle, js_core_1.inheritTextStyle, js_core_1.successTextStyle, e);
if ((0, js_core_1.isAddressesEquals)(this.address, e.address)) {
await this.syncContractState({ force: !this.isSyncing });
}
else {
await this._contractSubscriber?.unsubscribe();
this._contractSubscriber = undefined;
}
});
});
}
/**
* Unsubscribe from current account address contract state updates.
*
* @returns {Promise<void>}
* @protected
*/
async _unwatch() {
if (!this.provider || !this.address) {
return;
}
try {
await this._contractSubscriber?.unsubscribe();
(0, js_utils_1.debug)(`%c${this.constructor.name}%c unsubscribe from the user wallet [%c${(0, js_utils_1.sliceAddress)(this.address?.toString())}%c] contract updates`, js_core_1.successLabelStyle, js_core_1.inheritTextStyle, js_core_1.successTextStyle, js_core_1.inheritTextStyle);
this._contractSubscriber = undefined;
}
catch (e) {
this._contractSubscriber = undefined;
}
}
_accountDisposer;
_contractSubscriber;
_networkDisposer;
}
exports.TvmConnectService = TvmConnectService;
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "connector", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "init", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "connect", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "disconnect", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "addAsset", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Boolean]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "addNetwork", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "switchNetwork", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "changeAccount", null);
__decorate([
mobx_1.computed,
__metadata("design:type", everscale_inpage_provider_1.ProviderRpcClient),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "connection", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "provider", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "contract", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "networks", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "providers", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "chainId", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isSyncing", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "providerId", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "address", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "balance", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "currency", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "hasProvider", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isConnected", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isOutdated", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isReady", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isUnsupportedNetwork", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "network", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "providerInfo", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "account", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isConnecting", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isDisconnecting", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isInitialized", null);
__decorate([
mobx_1.computed,
__metadata("design:type", Object),
__metadata("design:paramtypes", [])
], TvmConnectService.prototype, "isInitializing", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "syncContractState", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "_init", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "_watch", null);
__decorate([
mobx_1.action.bound,
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TvmConnectService.prototype, "_unwatch", null);