UNPKG

@hashgraph/hedera-wallet-connect

Version:

A library to facilitate integrating Hedera with WalletConnect

302 lines (301 loc) 12.6 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 { Buffer } from 'buffer'; import { Core } from '@walletconnect/core'; import { Web3Wallet } from '@walletconnect/web3wallet'; import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils'; import { Wallet as HederaWallet, Client, AccountId } from '@hashgraph/sdk'; import { HederaChainId, HederaSessionEvent, HederaJsonRpcMethod, base64StringToQuery, Uint8ArrayToBase64String, stringToSignerMessage, signatureMapToBase64String, signerSignaturesToSignatureMap, base64StringToTransaction, getHederaError, } from '../shared'; import { proto } from '@hashgraph/proto'; import Provider from './provider'; export { default as WalletProvider } from './provider'; /* * * @see {@link https://github.com/WalletConnect/walletconnect-monorepo/blob/v2.0/packages/web3wallet/src/client.ts} */ export class HederaWeb3Wallet extends Web3Wallet { /* * Set default values for chains, methods, events */ constructor(opts, chains = Object.values(HederaChainId), methods = Object.values(HederaJsonRpcMethod), sessionEvents = Object.values(HederaSessionEvent)) { super(opts); this.chains = chains; this.methods = methods; this.sessionEvents = sessionEvents; } // wrapper to reduce needing to instantiate Core object on client, also add hedera sensible defaults static async create(projectId, metadata, chains, methods, sessionEvents) { const wallet = new HederaWeb3Wallet({ core: new Core({ projectId }), metadata }, chains, methods, sessionEvents); //https://github.com/WalletConnect/walletconnect-monorepo/blob/14f54684c3d89a5986a68f4dd700a79a958f1604/packages/web3wallet/src/client.ts#L178 wallet.logger.trace(`Initialized`); try { await wallet.engine.init(); wallet.logger.info(`Web3Wallet Initialization Success`); } catch (error) { wallet.logger.info(`Web3Wallet Initialization Failure`); wallet.logger.error(error.message); throw error; } return wallet; } /* * Hedera Wallet Signer */ getHederaWallet(chainId, accountId, privateKey, _provider) { const network = chainId.split(':')[1]; const client = Client.forName(network); const provider = _provider !== null && _provider !== void 0 ? _provider : new Provider(client); return new HederaWallet(accountId, privateKey, provider); } /* * Session proposal */ async buildAndApproveSession(accounts, { id, params }) { // filter to get unique chains const chains = accounts .map((account) => account.split(':').slice(0, 2).join(':')) .filter((x, i, a) => a.indexOf(x) == i); return await this.approveSession({ id, namespaces: buildApprovedNamespaces({ proposal: params, supportedNamespaces: { hedera: { chains, methods: this.methods, events: this.sessionEvents, accounts, }, }, }), }); } /* * Session Requests */ validateParam(name, value, expectedType) { if (expectedType === 'array' && Array.isArray(value)) return; if (typeof value === expectedType) return; throw getHederaError('INVALID_PARAMS', `Invalid paramameter value for ${name}, expected ${expectedType} but got ${typeof value}`); } parseSessionRequest(event, // optional arg to throw error if request is invalid, call with shouldThrow = false when calling from rejectSessionRequest as we only need id and top to send reject response shouldThrow = true) { const { id, topic } = event; const { request: { method, params }, chainId, } = event.params; let body; // get account id from optional second param for transactions and queries or from transaction id // this allows for the case where the requested signer is not the payer, but defaults to the payer if a second param is not provided let signerAccountId; // First test for valid params for each method // then convert params to a body that the respective function expects try { switch (method) { case HederaJsonRpcMethod.GetNodeAddresses: { // 1 if (params) throw getHederaError('INVALID_PARAMS'); break; } case HederaJsonRpcMethod.ExecuteTransaction: { // 2 const { transactionList } = params; this.validateParam('transactionList', transactionList, 'string'); body = base64StringToTransaction(transactionList); break; } case HederaJsonRpcMethod.SignMessage: { // 3 const { signerAccountId: _accountId, message } = params; this.validateParam('signerAccountId', _accountId, 'string'); this.validateParam('message', message, 'string'); signerAccountId = AccountId.fromString(_accountId.replace(chainId + ':', '')); body = message; break; } case HederaJsonRpcMethod.SignAndExecuteQuery: { // 4 const { signerAccountId: _accountId, query } = params; this.validateParam('signerAccountId', _accountId, 'string'); this.validateParam('query', query, 'string'); signerAccountId = AccountId.fromString(_accountId.replace(chainId + ':', '')); body = base64StringToQuery(query); break; } case HederaJsonRpcMethod.SignAndExecuteTransaction: { // 5 const { signerAccountId: _accountId, transactionList } = params; this.validateParam('signerAccountId', _accountId, 'string'); this.validateParam('transactionList', transactionList, 'string'); signerAccountId = AccountId.fromString(_accountId.replace(chainId + ':', '')); body = base64StringToTransaction(transactionList); break; } case HederaJsonRpcMethod.SignTransaction: { // 6 const { signerAccountId: _accountId, transactionBody } = params; this.validateParam('signerAccountId', _accountId, 'string'); this.validateParam('transactionBody', transactionBody, 'string'); signerAccountId = AccountId.fromString(_accountId.replace(chainId + ':', '')); body = Buffer.from(transactionBody, 'base64'); break; } default: throw getSdkError('INVALID_METHOD'); } // error parsing request params } catch (e) { if (shouldThrow) throw e; } return { method: method, chainId: chainId, id, topic, body, accountId: signerAccountId, }; } async executeSessionRequest(event, hederaWallet) { const { method, id, topic, body } = this.parseSessionRequest(event); return await this[method](id, topic, body, hederaWallet); } // https://docs.walletconnect.com/web3wallet/wallet-usage#responding-to-session-requests async rejectSessionRequest(event, error) { const { id, topic } = this.parseSessionRequest(event, false); return await this.respondSessionRequest({ topic, response: { id, error, jsonrpc: '2.0' }, }); } /* * JSON RPC Methods */ // 1. hedera_getNodeAddresses async hedera_getNodeAddresses(id, topic, _, // ignore this param to be consistent call signature with other functions signer) { const nodesAccountIds = signer.getNetwork(); const nodes = Object.values(nodesAccountIds).map((nodeAccountId) => nodeAccountId.toString()); const response = { topic, response: { jsonrpc: '2.0', id, result: { nodes, }, }, }; return await this.respondSessionRequest(response); } // 2. hedera_executeTransaction async hedera_executeTransaction(id, topic, body, signer) { const response = { topic, response: { id, result: (await signer.call(body)).toJSON(), jsonrpc: '2.0', }, }; return await this.respondSessionRequest(response); } // 3. hedera_signMessage async hedera_signMessage(id, topic, body, signer) { // signer takes an array of Uint8Arrays though spec allows for 1 message to be signed const signerSignatures = await signer.sign(stringToSignerMessage(body)); const _signatureMap = proto.SignatureMap.create(signerSignaturesToSignatureMap(signerSignatures)); const signatureMap = signatureMapToBase64String(_signatureMap); const response = { topic, response: { jsonrpc: '2.0', id, result: { signatureMap, }, }, }; return await this.respondSessionRequest(response); } // 4. hedera_signAndExecuteQuery async hedera_signAndExecuteQuery(id, topic, body, signer) { /* * Can be used with return values the have a toBytes method implemented * For example: * https://github.com/hashgraph/hedera-sdk-js/blob/c4438cbaa38074d8bfc934dba84e3b430344ed89/src/account/AccountInfo.js#L402 */ const queryResult = await body.executeWithSigner(signer); let queryResponse = ''; if (Array.isArray(queryResult)) { queryResponse = queryResult.map((qr) => Uint8ArrayToBase64String(qr.toBytes())).join(','); } else { queryResponse = Uint8ArrayToBase64String(queryResult.toBytes()); } const response = { topic, response: { jsonrpc: '2.0', id, result: { response: queryResponse, }, }, }; return await this.respondSessionRequest(response); } // 5. hedera_signAndExecuteTransaction async hedera_signAndExecuteTransaction(id, topic, body, signer) { const signedTransaction = await signer.signTransaction(body); const response = { topic, response: { id, result: (await signer.call(signedTransaction)).toJSON(), jsonrpc: '2.0', }, }; return await this.respondSessionRequest(response); } // 6. hedera_signTransaction async hedera_signTransaction(id, topic, body, signer) { const signerSignatures = await signer.sign([body]); const _signatureMap = proto.SignatureMap.create(signerSignaturesToSignatureMap(signerSignatures)); const signatureMap = signatureMapToBase64String(_signatureMap); const response = { topic, response: { jsonrpc: '2.0', id, result: { signatureMap, }, }, }; return await this.respondSessionRequest(response); } } export default HederaWeb3Wallet;