UNPKG

@hashgraph/hedera-wallet-connect

Version:

A library to facilitate integrating Hedera with WalletConnect

345 lines (344 loc) 16.2 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, 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()}`; } 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.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) { var _a, _b; // Ensure transaction is frozen with node account IDs before signing // This is required so the transaction can be executed later by any client if (!transaction.isFrozen()) { transaction.freezeWith(this._getHederaClient()); } // Extract the first node account ID from the frozen transaction to preserve it in the transaction body const nodeAccountId = (_b = (_a = transaction.nodeAccountIds) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null; 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); // Get the original transaction bytes to preserve the full transaction structure // including all node account IDs const originalTransactionBytes = transaction.toBytes(); const originalTransactionList = proto.TransactionList.decode(originalTransactionBytes); // Add the signature to all transactions in the list // Each transaction in the list corresponds to a different node const signedTransactionList = originalTransactionList.transactionList.map((tx) => { // Check if the transaction has signedTransactionBytes (frozen transactions) if (tx.signedTransactionBytes) { // Decode the SignedTransaction to access the bodyBytes and existing sigMap const signedTx = proto.SignedTransaction.decode(tx.signedTransactionBytes); const existingSigMap = signedTx.sigMap || proto.SignatureMap.create({}); // Merge the new signatures with existing signatures const mergedSigPairs = [...(existingSigMap.sigPair || []), ...(sigMap.sigPair || [])]; // Create updated SignedTransaction with merged signatures const updatedSignedTx = proto.SignedTransaction.encode({ bodyBytes: signedTx.bodyBytes, sigMap: proto.SignatureMap.create({ sigPair: mergedSigPairs, }), }).finish(); return { signedTransactionBytes: updatedSignedTx, }; } else { // Transaction has bodyBytes and sigMap at the top level (not frozen) const existingSigMap = tx.sigMap || proto.SignatureMap.create({}); // Merge the new signatures with existing signatures const mergedSigPairs = [...(existingSigMap.sigPair || []), ...(sigMap.sigPair || [])]; return Object.assign(Object.assign({}, tx), { sigMap: Object.assign(Object.assign({}, existingSigMap), { sigPair: mergedSigPairs }) }); } }); // Encode the signed transaction list back to bytes const signedBytes = proto.TransactionList.encode({ transactionList: signedTransactionList, }).finish(); return Transaction.fromBytes(signedBytes); } 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; } 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)); } }