@hashgraph/hedera-wallet-connect
Version:
A library to facilitate integrating Hedera with WalletConnect
559 lines (558 loc) • 24.7 kB
JavaScript
import { CoreHelperUtil } from '@reown/appkit';
import { isReownName } from '@reown/appkit-common';
import { AdapterBlueprint, WcHelpersUtil } from '@reown/appkit-controllers';
import { LedgerId } from '@hiero-ledger/sdk';
import { BrowserProvider, Contract, formatUnits, hexlify, isHexString, JsonRpcSigner, parseUnits, toUtf8Bytes, } from 'ethers';
import { HederaConnector } from './connectors';
import { hederaNamespace, getAccountBalance, HederaChainDefinition } from './utils';
import { createLogger } from '../lib/shared/logger';
export class HederaAdapter extends AdapterBlueprint {
constructor(params) {
var _a, _b;
if (params.namespace !== hederaNamespace && params.namespace !== 'eip155') {
throw new Error('Namespace must be "hedera" or "eip155"');
}
if (params.namespace == 'eip155') {
console.warn('HederaAdapter with namespace "eip155" is deprecated and will be removed in the next major version. ' +
'Use WagmiAdapter from @reown/appkit-adapter-wagmi for EVM wallet connectivity instead.');
if ((_a = params.networks) === null || _a === void 0 ? void 0 : _a.some((n) => n.chainNamespace != 'eip155')) {
throw new Error('Invalid networks for eip155 namespace');
}
}
else {
if ((_b = params.networks) === null || _b === void 0 ? void 0 : _b.some((n) => n.chainNamespace != hederaNamespace)) {
throw new Error('Invalid networks for hedera namespace');
}
}
super(Object.assign({}, params));
this.logger = createLogger('HederaAdapter');
this.injectedProviders = new Map();
this.activeInjectedProvider = null;
this.injectedListenersSet = false;
this.getCaipNetworks = (namespace) => {
var _a;
const targetNamespace = namespace || this.namespace;
// If the caller explicitly provided networks, respect them instead of
// returning all Hedera networks regardless of configuration.
if ((_a = params.networks) === null || _a === void 0 ? void 0 : _a.length) {
return params.networks.filter((n) => !targetNamespace || n.chainNamespace === targetNamespace);
}
if (targetNamespace === 'eip155') {
return [HederaChainDefinition.EVM.Mainnet, HederaChainDefinition.EVM.Testnet];
}
else if (targetNamespace === hederaNamespace) {
return [HederaChainDefinition.Native.Mainnet, HederaChainDefinition.Native.Testnet];
}
else {
return [
HederaChainDefinition.EVM.Mainnet,
HederaChainDefinition.EVM.Testnet,
HederaChainDefinition.Native.Mainnet,
HederaChainDefinition.Native.Testnet,
];
}
};
}
async setUniversalProvider(universalProvider) {
this.addConnector(new HederaConnector({
provider: universalProvider,
caipNetworks: this.getCaipNetworks() || [],
namespace: this.namespace,
}));
}
async connect(params) {
this.logger.debug('connect called with params:', params);
const type = params.type;
if (type === 'ANNOUNCED' || type === 'INJECTED') {
return this.connectInjected(params);
}
return this.connectViaWalletConnect(params);
}
async connectViaWalletConnect(params) {
const connector = this.getWalletConnectConnector();
if (connector && 'connectWalletConnect' in connector) {
this.logger.debug('Calling HederaConnector.connectWalletConnect');
await connector.connectWalletConnect();
}
else {
this.logger.warn('HederaConnector not found or connectWalletConnect method missing');
}
this.activeInjectedProvider = null;
return {
id: 'WALLET_CONNECT',
type: 'WALLET_CONNECT',
chainId: Number(params.chainId),
provider: this.provider,
address: '',
};
}
async connectInjected(params) {
var _a, _b, _c, _d, _e;
const id = params.id;
const type = params.type;
const injectedProvider = this.injectedProviders.get(id) ||
params.provider;
if (!injectedProvider) {
throw new Error(`Injected provider not found for id: ${id}`);
}
// Hedera-native wallets (e.g. Kabila) announce via EIP-6963 for discovery but
// cannot fulfill EIP-1193 RPC calls — they require WalletConnect + HIP-820.
if (injectedProvider.isWalletConnectOnly) {
this.logger.debug(`connectInjected: "${id}" is WalletConnect-only, falling back to WC`);
return this.connectViaWalletConnect(params);
}
this.logger.debug(`connectInjected: requesting accounts from "${id}"`);
let accounts;
try {
accounts = (await injectedProvider.request({
method: 'eth_requestAccounts',
}));
}
catch (error) {
if ((_a = error === null || error === void 0 ? void 0 : error.message) === null || _a === void 0 ? void 0 : _a.includes('already pending')) {
// User has a pending MetaMask request — surface this so they can action it.
this.logger.warn('A wallet_requestPermissions request is already pending. ' +
'Open the wallet extension and approve or reject the pending request.');
throw error;
}
// Any other rejection (e.g. wallet announced via EIP-6963 but doesn't support
// EIP-1193) — fall back to WalletConnect pairing instead of surfacing an error.
this.logger.warn(`connectInjected: "${id}" rejected eth_requestAccounts (${error === null || error === void 0 ? void 0 : error.message}), falling back to WC`);
return this.connectViaWalletConnect(params);
}
if (!accounts || accounts.length === 0) {
throw new Error('No accounts returned from injected provider');
}
let chainIdHex = (await injectedProvider.request({
method: 'eth_chainId',
}));
let chainId = parseInt(chainIdHex, 16);
const configuredNetworks = this.getCaipNetworks();
const isChainSupported = configuredNetworks.some((n) => Number(n.id) === chainId);
if (!isChainSupported && configuredNetworks.length > 0) {
const targetNetwork = configuredNetworks[0];
const targetChainIdHex = `0x${Number(targetNetwork.id).toString(16)}`;
this.logger.debug(`connectInjected: wallet is on chain ${chainId}, switching to ${targetNetwork.name} (${targetNetwork.id})`);
try {
await injectedProvider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: targetChainIdHex }],
});
}
catch (switchError) {
// 4902: chain not added to wallet yet
if ((switchError === null || switchError === void 0 ? void 0 : switchError.code) === 4902 || ((_c = (_b = switchError === null || switchError === void 0 ? void 0 : switchError.data) === null || _b === void 0 ? void 0 : _b.originalError) === null || _c === void 0 ? void 0 : _c.code) === 4902) {
await injectedProvider.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: targetChainIdHex,
chainName: targetNetwork.name,
nativeCurrency: targetNetwork.nativeCurrency,
rpcUrls: [targetNetwork.rpcUrls.default.http[0]],
blockExplorerUrls: ((_e = (_d = targetNetwork.blockExplorers) === null || _d === void 0 ? void 0 : _d.default) === null || _e === void 0 ? void 0 : _e.url)
? [targetNetwork.blockExplorers.default.url]
: undefined,
},
],
});
}
else {
throw switchError;
}
}
chainIdHex = (await injectedProvider.request({
method: 'eth_chainId',
}));
chainId = parseInt(chainIdHex, 16);
}
this.activeInjectedProvider = injectedProvider;
if (typeof window !== 'undefined') {
window.localStorage.removeItem(HederaAdapter.INJECTED_DISCONNECT_KEY);
}
this.logger.debug(`connectInjected: connected to ${accounts[0]} on chain ${chainId}`);
const connector = this.connectors.find((c) => c.id === id);
this.emit('accountChanged', {
address: accounts[0],
chainId,
connector: connector,
});
this.setupInjectedListeners(injectedProvider, id);
return {
id,
type: type,
provider: injectedProvider,
address: accounts[0],
chainId,
};
}
setupInjectedListeners(provider, connectorId) {
var _a, _b;
if (this.injectedListenersSet) {
return;
}
this.injectedListenersSet = true;
const connector = this.connectors.find((c) => c.id === connectorId);
const onAccountsChanged = (accounts) => {
const addrs = accounts;
if (addrs.length === 0) {
this.activeInjectedProvider = null;
this.emit('disconnect');
}
else {
this.emit('accountChanged', {
address: addrs[0],
connector: connector,
});
}
};
const onChainChanged = (chainId) => {
var _a;
const newChainId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId;
this.emit('switchNetwork', {
address: ((_a = this.connectors.find((c) => c.id === connectorId)) === null || _a === void 0 ? void 0 : _a.address) || '',
chainId: newChainId,
});
};
(_a = provider.on) === null || _a === void 0 ? void 0 : _a.call(provider, 'accountsChanged', onAccountsChanged);
(_b = provider.on) === null || _b === void 0 ? void 0 : _b.call(provider, 'chainChanged', onChainChanged);
}
async disconnect(_params) {
if (this.activeInjectedProvider) {
this.activeInjectedProvider = null;
this.injectedListenersSet = false;
if (typeof window !== 'undefined') {
window.localStorage.setItem(HederaAdapter.INJECTED_DISCONNECT_KEY, 'true');
}
return { connections: [] };
}
try {
const connector = this.getWalletConnectConnector();
await connector.disconnect();
}
catch (error) {
this.logger.warn('disconnect - error', error);
}
return { connections: [] };
}
async getAccounts({ namespace, }) {
var _a, _b, _c, _d;
if (this.activeInjectedProvider) {
const accounts = (await this.activeInjectedProvider.request({
method: 'eth_accounts',
}));
return {
accounts: accounts.map((address) => CoreHelperUtil.createAccount('eip155', address, 'eoa')),
};
}
const provider = this.provider;
const addresses = (((_d = (_c = (_b = (_a = provider === null || provider === void 0 ? void 0 : provider.session) === null || _a === void 0 ? void 0 : _a.namespaces) === null || _b === void 0 ? void 0 : _b[namespace]) === null || _c === void 0 ? void 0 : _c.accounts) === null || _d === void 0 ? void 0 : _d.map((account) => {
const [, , address] = account.split(':');
return address;
}).filter((address, index, self) => self.indexOf(address) === index)) || []);
return {
accounts: addresses.map((address) => CoreHelperUtil.createAccount(namespace, address, 'eoa')),
};
}
async syncConnectors() {
if (this.namespace !== 'eip155' || typeof window === 'undefined') {
return;
}
const handleAnnouncement = (event) => {
const e = event;
const { info, provider } = e.detail;
if (!(info === null || info === void 0 ? void 0 : info.rdns) || this.injectedProviders.has(info.rdns)) {
return;
}
this.injectedProviders.set(info.rdns, provider);
this.addConnector({
id: info.rdns,
type: 'ANNOUNCED',
name: info.name,
info: { uuid: info.uuid, name: info.name, icon: info.icon, rdns: info.rdns },
provider,
chain: 'eip155',
chains: this.getCaipNetworks(),
});
this.logger.debug(`EIP-6963: Discovered wallet "${info.name}" (${info.rdns})`);
};
window.addEventListener('eip6963:announceProvider', handleAnnouncement);
window.dispatchEvent(new Event('eip6963:requestProvider'));
}
async syncConnections(_params) {
return Promise.resolve();
}
async getBalance(params) {
const { address, caipNetwork } = params;
if (!caipNetwork) {
return Promise.resolve({
balance: '0',
decimals: 0,
symbol: '',
});
}
const accountBalance = await getAccountBalance(caipNetwork.testnet ? LedgerId.TESTNET : LedgerId.MAINNET, address);
return Promise.resolve({
balance: accountBalance
? formatUnits(accountBalance.hbars.toTinybars().toString(), 8).toString()
: '0',
decimals: caipNetwork.nativeCurrency.decimals,
symbol: caipNetwork.nativeCurrency.symbol,
});
}
async signMessage(params) {
const { provider, message, address } = params;
if (this.activeInjectedProvider) {
const hexMessage = isHexString(message) ? message : hexlify(toUtf8Bytes(message));
const signature = (await this.activeInjectedProvider.request({
method: 'personal_sign',
params: [hexMessage, address],
}));
return { signature };
}
if (!provider) {
throw new Error('Provider is undefined');
}
const hederaProvider = provider;
let signature = '';
if (this.namespace === hederaNamespace) {
const response = await hederaProvider.hedera_signMessage({
signerAccountId: address,
message,
});
signature = response.signatureMap;
}
else {
signature = await hederaProvider.eth_signMessage(message, address);
}
return { signature };
}
/**
* @deprecated This method is only used with the eip155 namespace, which is deprecated.
* Use `WagmiAdapter` from `@reown/appkit-adapter-wagmi` for EVM operations instead.
*/
async estimateGas(params) {
const { caipNetwork, address } = params;
if (this.namespace !== 'eip155') {
throw new Error('Namespace is not eip155');
}
if (this.activeInjectedProvider) {
const browserProvider = new BrowserProvider(this.activeInjectedProvider, Number(caipNetwork === null || caipNetwork === void 0 ? void 0 : caipNetwork.id));
const signer = new JsonRpcSigner(browserProvider, address);
const gas = await signer.estimateGas({
from: address,
to: params.to,
data: params.data,
type: 0,
});
return { gas };
}
const { provider } = params;
if (!provider) {
throw new Error('Provider is undefined');
}
const hederaProvider = provider;
const result = await hederaProvider.eth_estimateGas({
data: params.data,
to: params.to,
address: address,
}, address, Number(caipNetwork === null || caipNetwork === void 0 ? void 0 : caipNetwork.id));
return { gas: result };
}
/**
* @deprecated This method is only used with the eip155 namespace, which is deprecated.
* Use `WagmiAdapter` from `@reown/appkit-adapter-wagmi` for EVM operations instead.
*/
async sendTransaction(params) {
var _a, _b;
if (this.namespace !== 'eip155') {
throw new Error('Namespace is not eip155');
}
if (this.activeInjectedProvider) {
const browserProvider = new BrowserProvider(this.activeInjectedProvider, Number((_a = params.caipNetwork) === null || _a === void 0 ? void 0 : _a.id));
const signer = new JsonRpcSigner(browserProvider, params.address);
const txResponse = await signer.sendTransaction({
to: params.to,
value: params.value,
data: params.data,
gasLimit: params.gas,
gasPrice: params.gasPrice,
type: 0,
});
const txReceipt = await txResponse.wait();
return { hash: (txReceipt === null || txReceipt === void 0 ? void 0 : txReceipt.hash) || null };
}
if (!params.provider) {
throw new Error('Provider is undefined');
}
const hederaProvider = params.provider;
const tx = await hederaProvider.eth_sendTransaction({
value: params.value,
to: params.to,
data: params.data,
gas: params.gas,
gasPrice: params.gasPrice,
address: params.address,
}, params.address, Number((_b = params.caipNetwork) === null || _b === void 0 ? void 0 : _b.id));
return { hash: tx };
}
/**
* @deprecated This method is only used with the eip155 namespace, which is deprecated.
* Use `WagmiAdapter` from `@reown/appkit-adapter-wagmi` for EVM operations instead.
*/
async writeContract(params) {
if (this.namespace !== 'eip155') {
throw new Error('Namespace is not eip155');
}
const { caipNetwork, caipAddress, abi, tokenAddress, method, args } = params;
let browserProvider;
if (this.activeInjectedProvider) {
browserProvider = new BrowserProvider(this.activeInjectedProvider, Number(caipNetwork === null || caipNetwork === void 0 ? void 0 : caipNetwork.id));
}
else {
if (!params.provider) {
throw new Error('Provider is undefined');
}
browserProvider = new BrowserProvider(params.provider, Number(caipNetwork === null || caipNetwork === void 0 ? void 0 : caipNetwork.id));
}
const signer = new JsonRpcSigner(browserProvider, caipAddress);
const contract = new Contract(tokenAddress, abi, signer);
if (!contract || !method) {
throw new Error('Contract method is undefined');
}
const contractMethod = contract[method];
if (contractMethod) {
const result = await contractMethod(...args);
return { hash: result };
}
else
throw new Error('Contract method is undefined');
}
/**
* @deprecated This method is only used with the eip155 namespace, which is deprecated.
* Use `WagmiAdapter` from `@reown/appkit-adapter-wagmi` for EVM operations instead.
*/
async getEnsAddress(params) {
if (this.namespace !== 'eip155') {
throw new Error('Namespace is not eip155');
}
const { name, caipNetwork } = params;
if (caipNetwork) {
if (isReownName(name)) {
return {
address: (await WcHelpersUtil.resolveReownName(name)) || false,
};
}
}
return { address: false };
}
parseUnits(params) {
return parseUnits(params.value, params.decimals);
}
formatUnits(params) {
return formatUnits(params.value, params.decimals);
}
/**
* @deprecated This method is only used with the eip155 namespace, which is deprecated.
* Use `WagmiAdapter` from `@reown/appkit-adapter-wagmi` for EVM operations instead.
*/
async getCapabilities(params) {
var _a, _b;
if (this.namespace !== 'eip155') {
throw new Error('Namespace is not eip155');
}
const provider = this.provider;
if (!provider) {
throw new Error('Provider is undefined');
}
const walletCapabilitiesString = (_b = (_a = provider.session) === null || _a === void 0 ? void 0 : _a.sessionProperties) === null || _b === void 0 ? void 0 : _b['capabilities'];
if (walletCapabilitiesString) {
try {
const walletCapabilities = JSON.parse(walletCapabilitiesString);
const accountCapabilities = walletCapabilities[params];
if (accountCapabilities) {
return accountCapabilities;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (error) {
throw new Error('Error parsing wallet capabilities');
}
}
return await provider.request({
method: 'wallet_getCapabilities',
params: [params],
});
}
async getProfile() {
return Promise.resolve({ profileImage: '', profileName: '' });
}
async grantPermissions() {
return Promise.resolve({});
}
async revokePermissions() {
return Promise.resolve('0x');
}
async syncConnection(params) {
const wasDisconnected = typeof window !== 'undefined' &&
window.localStorage.getItem(HederaAdapter.INJECTED_DISCONNECT_KEY) === 'true';
const injectedProvider = !wasDisconnected && (this.activeInjectedProvider || this.injectedProviders.get(params.id));
if (injectedProvider) {
// eth_accounts (not eth_requestAccounts) to avoid triggering a popup
const accounts = (await injectedProvider.request({
method: 'eth_accounts',
}));
if (accounts && accounts.length > 0) {
const chainIdHex = (await injectedProvider.request({
method: 'eth_chainId',
}));
const chainId = parseInt(chainIdHex, 16);
this.activeInjectedProvider = injectedProvider;
this.setupInjectedListeners(injectedProvider, params.id);
return {
id: params.id,
type: 'ANNOUNCED',
provider: injectedProvider,
address: accounts[0],
chainId,
};
}
}
return {
id: 'WALLET_CONNECT',
type: 'WALLET_CONNECT',
chainId: params.chainId,
provider: this.provider,
address: '',
};
}
async switchNetwork(params) {
const { caipNetwork } = params;
if (this.activeInjectedProvider) {
const chainIdHex = `0x${Number(caipNetwork.id).toString(16)}`;
await this.activeInjectedProvider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }],
});
return;
}
const connector = this.getWalletConnectConnector();
connector.provider.setDefaultChain(caipNetwork.caipNetworkId);
}
getWalletConnectConnector() {
const connector = this.connectors.find((c) => c.type == 'WALLET_CONNECT');
if (!connector) {
throw new Error('WalletConnectConnector not found');
}
return connector;
}
getWalletConnectProvider() {
const connector = this.connectors.find((c) => c.type === 'WALLET_CONNECT');
const provider = connector === null || connector === void 0 ? void 0 : connector.provider;
return provider;
}
async walletGetAssets(_params) {
return Promise.resolve({});
}
}
HederaAdapter.INJECTED_DISCONNECT_KEY = '@hwc/injected-disconnected';