@hashgraph/hedera-wallet-connect
Version:
A library to facilitate integrating Hedera with WalletConnect
345 lines (344 loc) • 16.2 kB
JavaScript
/*
*
* 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));
}
}