@hashgraph/hedera-wallet-connect
Version:
A library to facilitate integrating Hedera with WalletConnect
319 lines (318 loc) • 14.3 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, 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));
}
}