UNPKG

@hashgraph/hedera-wallet-connect

Version:

A library to facilitate integrating Hedera with WalletConnect

319 lines (318 loc) 14.3 kB
/* * * Hedera Wallet Connect * * Copyright (C) 2023 Hedera Hashgraph, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import { AccountBalance, AccountId, AccountInfo, LedgerId, SignerSignature, Transaction, TransactionRecord, Client, PublicKey, TransactionId, TransactionResponse, Query, AccountRecordsQuery, AccountInfoQuery, AccountBalanceQuery, TransactionReceiptQuery, TransactionReceipt, TransactionRecordQuery, } from '@hashgraph/sdk'; import { proto } from '@hashgraph/proto'; import { HederaJsonRpcMethod, base64StringToSignatureMap, base64StringToUint8Array, ledgerIdToCAIPChainId, queryToBase64String, transactionBodyToBase64String, transactionToBase64String, transactionToTransactionBody, extensionOpen, Uint8ArrayToBase64String, Uint8ArrayToString, } from '../shared'; import { DefaultLogger } from '../shared/logger'; import { SessionNotFoundError } from './SessionNotFoundError'; const clients = {}; export class DAppSigner { constructor(accountId, signClient, topic, ledgerId = LedgerId.MAINNET, extensionId, logLevel = 'debug') { this.accountId = accountId; this.signClient = signClient; this.topic = topic; this.ledgerId = ledgerId; this.extensionId = extensionId; this.logger = new DefaultLogger(logLevel); } /** * Sets the logging level for the DAppSigner * @param level - The logging level to set */ setLogLevel(level) { if (this.logger instanceof DefaultLogger) { this.logger.setLogLevel(level); } } _getHederaClient() { const ledgerIdString = this.ledgerId.toString(); if (!clients[ledgerIdString]) { clients[ledgerIdString] = Client.forName(ledgerIdString); } return clients[ledgerIdString]; } get _signerAccountId() { return `${ledgerIdToCAIPChainId(this.ledgerId)}:${this.accountId.toString()}`; } _getRandomNodes(numberOfNodes) { const allNodes = Object.values(this._getHederaClient().network).map((o) => typeof o === 'string' ? AccountId.fromString(o) : o); // shuffle nodes for (let i = allNodes.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [allNodes[i], allNodes[j]] = [allNodes[j], allNodes[i]]; } return allNodes.slice(0, numberOfNodes); } request(request) { var _a, _b; // Avoid a wallet call if the session is no longer valid if (!((_b = (_a = this === null || this === void 0 ? void 0 : this.signClient) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.get(this.topic))) { this.logger.error('Session no longer exists, signer will be removed. Please reconnect to the wallet.'); // Notify DAppConnector to remove this signer this.signClient.emit({ topic: this.topic, event: { name: 'session_delete', data: { topic: this.topic }, }, chainId: ledgerIdToCAIPChainId(this.ledgerId), }); throw new SessionNotFoundError('Session no longer exists. Please reconnect to the wallet.'); } if (this.extensionId) extensionOpen(this.extensionId); return this.signClient.request({ topic: this.topic, request, chainId: ledgerIdToCAIPChainId(this.ledgerId), }); } getAccountId() { return this.accountId; } getAccountKey() { throw new Error('Method not implemented.'); } getLedgerId() { return this.ledgerId; } getNetwork() { return this._getHederaClient().network; } getMirrorNetwork() { return this._getHederaClient().mirrorNetwork; } getAccountBalance() { return this.call(new AccountBalanceQuery().setAccountId(this.accountId)); } getAccountInfo() { return this.call(new AccountInfoQuery().setAccountId(this.accountId)); } getAccountRecords() { return this.call(new AccountRecordsQuery().setAccountId(this.accountId)); } getMetadata() { return this.signClient.metadata; } async sign(data, signOptions = { encoding: 'utf-8', }) { try { const messageToSign = signOptions.encoding === 'base64' ? Uint8ArrayToBase64String(data[0]) : Uint8ArrayToString(data[0]); const { signatureMap } = await this.request({ method: HederaJsonRpcMethod.SignMessage, params: { signerAccountId: this._signerAccountId, message: messageToSign, }, }); const sigmap = base64StringToSignatureMap(signatureMap); const signerSignature = new SignerSignature({ accountId: this.getAccountId(), publicKey: PublicKey.fromBytes(sigmap.sigPair[0].pubKeyPrefix), signature: sigmap.sigPair[0].ed25519 || sigmap.sigPair[0].ECDSASecp256k1, }); this.logger.debug('Data signed successfully'); return [signerSignature]; } catch (error) { this.logger.error('Error signing data:', error); throw error; } } async checkTransaction(transaction) { throw new Error('Method not implemented.'); } async populateTransaction(transaction) { return transaction .setNodeAccountIds(this._getRandomNodes(10)) // allow retrying on up to 10 nodes .setTransactionId(TransactionId.generate(this.getAccountId())); } /** * Prepares a transaction object for signing using a single node account id. * If the transaction object does not already have a node account id, * generate a random node account id using the Hedera SDK client * * @param transaction - Any instance of a class that extends `Transaction` * @returns transaction - `Transaction` object with signature */ async signTransaction(transaction) { let nodeAccountId; if (!transaction.nodeAccountIds || transaction.nodeAccountIds.length === 0) nodeAccountId = this._getRandomNodes(1)[0]; else nodeAccountId = transaction.nodeAccountIds[0]; const transactionBody = transactionToTransactionBody(transaction, nodeAccountId); if (!transactionBody) throw new Error('Failed to serialize transaction body'); const transactionBodyBase64 = transactionBodyToBase64String(transactionBody); const { signatureMap } = await this.request({ method: HederaJsonRpcMethod.SignTransaction, params: { signerAccountId: this._signerAccountId, transactionBody: transactionBodyBase64, }, }); const sigMap = base64StringToSignatureMap(signatureMap); const bodyBytes = base64StringToUint8Array(transactionBodyBase64); const bytes = proto.Transaction.encode({ bodyBytes, sigMap }).finish(); return Transaction.fromBytes(bytes); } async _tryExecuteTransactionRequest(request) { try { const requestToBytes = request.toBytes(); this.logger.debug('Creating transaction from bytes', requestToBytes, request); const transaction = Transaction.fromBytes(requestToBytes); this.logger.debug('Executing transaction request', transaction); const result = await this.request({ method: HederaJsonRpcMethod.SignAndExecuteTransaction, params: { signerAccountId: this._signerAccountId, transactionList: transactionToBase64String(transaction), }, }); this.logger.debug('Transaction request completed successfully'); return { result: TransactionResponse.fromJSON(result) }; } catch (error) { this.logger.error('Error executing transaction request:', error); return { error }; } } async _parseQueryResponse(query, base64EncodedQueryResponse) { if (query instanceof AccountRecordsQuery) { const base64EncodedQueryResponseSplit = base64EncodedQueryResponse.split(','); const data = base64EncodedQueryResponseSplit.map((o) => base64StringToUint8Array(o)); return data.map((o) => TransactionRecord.fromBytes(o)); } const data = base64StringToUint8Array(base64EncodedQueryResponse); if (query instanceof AccountBalanceQuery) { return AccountBalance.fromBytes(data); } else if (query instanceof AccountInfoQuery) { return AccountInfo.fromBytes(data); } else if (query instanceof TransactionReceiptQuery) { return TransactionReceipt.fromBytes(data); } else if (query instanceof TransactionRecordQuery) { return TransactionRecord.fromBytes(data); } else { throw new Error('Unsupported query type'); } } /** * Executes a free receipt query without signing a transaction. * Enables the DApp to fetch the receipt of a transaction without making a new request * to the wallet. * @param request - The query to execute * @returns The result of the query */ async executeReceiptQueryFromRequest(request) { try { const isMainnet = this.ledgerId === LedgerId.MAINNET; const client = isMainnet ? Client.forMainnet() : Client.forTestnet(); const receipt = TransactionReceiptQuery.fromBytes(request.toBytes()); const result = await receipt.execute(client); return { result }; } catch (error) { return { error }; } } async _tryExecuteQueryRequest(request) { try { const isReceiptQuery = request instanceof TransactionReceiptQuery; if (isReceiptQuery) { this.logger.debug('Attempting to execute free receipt query', request); const result = await this.executeReceiptQueryFromRequest(request); if (!(result === null || result === void 0 ? void 0 : result.error)) { return { result: result.result }; } this.logger.error('Error executing free receipt query. Sending to wallet.', result.error); } /** * Note, should we be converting these to specific query types? * Left alone to avoid changing the API for other requests. */ const query = isReceiptQuery ? TransactionReceiptQuery.fromBytes(request.toBytes()) : Query.fromBytes(request.toBytes()); this.logger.debug('Executing query request', query, queryToBase64String(query), isReceiptQuery); const result = await this.request({ method: HederaJsonRpcMethod.SignAndExecuteQuery, params: { signerAccountId: this._signerAccountId, query: queryToBase64String(query), }, }); this.logger.debug('Query request completed successfully', result); return { result: this._parseQueryResponse(query, result.response) }; } catch (error) { this.logger.error('Error executing query request:', error); return { error }; } } async call(request) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const isReceiptQuery = request instanceof TransactionReceiptQuery; let txResult = undefined; // a receipt query is a free query and we should not execute a transaction. if (!isReceiptQuery) { txResult = await this._tryExecuteTransactionRequest(request); if (txResult.result) { return txResult.result; } } const queryResult = await this._tryExecuteQueryRequest(request); if (queryResult.result) { return queryResult.result; } // TODO: make this error more usable if (isReceiptQuery) { throw new Error('Error executing receipt query: \n' + JSON.stringify({ queryError: { name: (_a = queryResult.error) === null || _a === void 0 ? void 0 : _a.name, message: (_b = queryResult.error) === null || _b === void 0 ? void 0 : _b.message, stack: (_c = queryResult.error) === null || _c === void 0 ? void 0 : _c.stack, }, })); } throw new Error('Error executing transaction or query: \n' + JSON.stringify({ txError: { name: (_d = txResult === null || txResult === void 0 ? void 0 : txResult.error) === null || _d === void 0 ? void 0 : _d.name, message: (_e = txResult === null || txResult === void 0 ? void 0 : txResult.error) === null || _e === void 0 ? void 0 : _e.message, stack: (_f = txResult === null || txResult === void 0 ? void 0 : txResult.error) === null || _f === void 0 ? void 0 : _f.stack, }, queryError: { name: (_g = queryResult.error) === null || _g === void 0 ? void 0 : _g.name, message: (_h = queryResult.error) === null || _h === void 0 ? void 0 : _h.message, stack: (_j = queryResult.error) === null || _j === void 0 ? void 0 : _j.stack, }, }, null, 2)); } }