UNPKG

@hashgraph/hedera-wallet-connect

Version:

A library to facilitate integrating Hedera with WalletConnect

462 lines (461 loc) 19.1 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 { AccountId, Transaction, LedgerId, Query, } from '@hashgraph/sdk'; import { proto } from '@hashgraph/proto'; /** * Converts `Transaction` to a Base64-string. * * Converts a transaction to bytes and then encodes it as a Base64-string. Allow incomplete transaction (HIP-745). * @param transaction - Any instance of a class that extends `Transaction` * @returns Base64 encoded representation of the input `Transaction` object */ export function transactionToBase64String(transaction) { const transactionBytes = transaction.toBytes(); return Buffer.from(transactionBytes).toString('base64'); } /** * Recreates a `Transaction` from a base64 encoded string. * * Decodes the string to a buffer, * then passes to `Transaction.fromBytes`. For greater flexibility, this function uses the base * `Transaction` class, but takes an optional type parameter if the type of transaction is known, * allowing stronger typeing. * @param transactionBytes - a base64 encoded string * @returns `Transaction` * @example * ```ts * const txn1 = base64StringToTransaction(bytesString) * const txn2 = base64StringToTransaction<TransferTransaction>(bytesString) * // txn1 type: Transaction * // txn2 type: TransferTransaction * ``` */ export function base64StringToTransaction(transactionBytes) { const decoded = Buffer.from(transactionBytes, 'base64'); return Transaction.fromBytes(decoded); } /** * @param transaction - a base64 encoded string of proto.TransactionBody.encode().finish() * @param nodeAccountId - an optional `AccountId` to set the node account ID for the transaction * @returns `string` * */ export function transactionToTransactionBody(transaction, nodeAccountId = null) { // This is a private function, though provides the capabilities to construct a proto.TransactionBody //@ts-ignore return transaction._makeTransactionBody(nodeAccountId); } export function transactionBodyToBase64String(transactionBody) { return Uint8ArrayToBase64String(proto.TransactionBody.encode(transactionBody).finish()); } /** * @param transactionList - a proto.TransactionList object * @returns `string` * */ export function transactionListToBase64String(transactionList) { const encoded = proto.TransactionList.encode(transactionList).finish(); return Uint8ArrayToBase64String(encoded); } /** * Extracts the first signature from a proto.SignatureMap object. * @param signatureMap - a proto.SignatureMap object * @returns `Uint8Array` * */ export const extractFirstSignature = (signatureMap) => { var _a; const firstPair = (_a = signatureMap === null || signatureMap === void 0 ? void 0 : signatureMap.sigPair) === null || _a === void 0 ? void 0 : _a[0]; const firstSignature = (firstPair === null || firstPair === void 0 ? void 0 : firstPair.ed25519) || (firstPair === null || firstPair === void 0 ? void 0 : firstPair.ECDSASecp256k1) || (firstPair === null || firstPair === void 0 ? void 0 : firstPair.ECDSA_384); if (!firstSignature) { throw new Error('No signatures found in response'); } return firstSignature; }; /** * Decodes base64 encoded proto.TransactionBody bytes to a `proto.TransactionBody` object. * * @param transactionBody - a base64 encoded string of proto.TransactionBody.encode().finish() * @returns `Transaction` * * */ export function base64StringToTransactionBody(transactionBody) { const bytes = Buffer.from(transactionBody, 'base64'); return proto.TransactionBody.decode(bytes); } /** * Converts a `proto.SignatureMap` to a base64 encoded string. * * First converts the `proto.SignatureMap` object to a JSON. * Then encodes the JSON to a base64 encoded string. * @param signatureMap - The `proto.SignatureMap` object to be converted * @returns Base64-encoded string representation of the input `proto.SignatureMap` */ export function signatureMapToBase64String(signatureMap) { const encoded = proto.SignatureMap.encode(signatureMap).finish(); return Uint8ArrayToBase64String(encoded); } /** * Converts a Base64-encoded string to a `proto.SignatureMap`. * @param base64string - Base64-encoded string * @returns `proto.SignatureMap` */ export function base64StringToSignatureMap(base64string) { const encoded = Buffer.from(base64string, 'base64'); return proto.SignatureMap.decode(encoded); } /** * Encodes the binary data represented by the `Uint8Array` to a Base64 string. * @param binary - The `Uint8Array` containing binary data to be converted * @returns Base64-encoded string representation of the input `Uint8Array` */ export function Uint8ArrayToBase64String(binary) { return Buffer.from(binary).toString('base64'); } /** * Encodes the binary data represented by the `Uint8Array` to a UTF-8 string. * @param binary - The `Uint8Array` containing binary data to be converted * @returns UTF-8 string representation of the input `Uint8Array` */ export function Uint8ArrayToString(binary) { return Buffer.from(binary).toString('utf-8'); } /** * Converts a Base64-encoded string to a `Uint8Array`. * @param base64string - Base64-encoded string to be converted * @returns A `Uint8Array` representing the decoded binary data */ export function base64StringToUint8Array(base64string) { const encoded = Buffer.from(base64string, 'base64'); return new Uint8Array(encoded); } /** * Converts a `Query` object to a Base64-encoded string. * First utilizes the `toBytes` method of the `Query` instance to obtain its binary `Uint8Array` representation. * Then encodes the binary `Uint8Array` to a Base64 string representation. * @param query - A `Query` object to be converted * @returns Base64 encoded representation of the input `Query` object */ export function queryToBase64String(query) { const queryBytes = query.toBytes(); return Buffer.from(queryBytes).toString('base64'); } /** * Recreates a `Query` from a Base64-encoded string. First decodes the string to a buffer, * then passes to `Query.fromBytes`. For greater flexibility, this function uses the base * `Query` class, but takes an optional type parameter if the type of query is known, * allowing stronger typeing. * @param bytesString - Base64-encoded string * @returns `Query<T>` * @example * ```ts * const query1 = base64StringToQuery(bytesString) * const query2 = base64StringToQuery<AccountInfoQuery>(bytesString) * // query1 type: Query<any> * // query2 type: AccountInfoQuery * ``` */ export function base64StringToQuery(bytesString) { const decoded = Buffer.from(bytesString, 'base64'); return Query.fromBytes(decoded); } export function prefixMessageToSign(message) { return '\x19Hedera Signed Message:\n' + message.length + message; } /** * Incorporates additional data (salt) into the message to alter the output signature. * This alteration ensures that passing a transaction here for signing will yield an invalid signature, * as the additional data modifies the signature text. * * @param message - A plain text string * @returns An array of Uint8Array containing the prepared message for signing */ export function stringToSignerMessage(message) { return [Buffer.from(prefixMessageToSign(message))]; } /** * This implementation expects a plain text string, which is prefixed and then signed by a wallet. * Because the spec calls for 1 message to be signed and 1 signer, this function expects a single * signature and used the first item in the sigPair array. * * @param message - A plain text string * @param base64SignatureMap - A base64 encoded proto.SignatureMap object * @param publicKey - A PublicKey object use to verify the signature * @returns boolean - whether or not the first signature in the sigPair is valid for the message and public key */ export function verifyMessageSignature(message, base64SignatureMap, publicKey) { const signatureMap = base64StringToSignatureMap(base64SignatureMap); const signature = signatureMap.sigPair[0].ed25519 || signatureMap.sigPair[0].ECDSASecp256k1; if (!signature) throw new Error('Signature not found in signature map'); return publicKey.verify(Buffer.from(prefixMessageToSign(message)), signature); } /** * This implementation expects a plain text string, which is prefixed and then signed by a wallet. * Because the spec calls for 1 message to be signed and 1 signer, this function expects a single * signature and used the first item in the sigPair array. * * @param message - A plain text string * @param signerSignature - A SignerSignature object * @param publicKey - A PublicKey object use to verify the signature * @returns boolean - whether or not the first signature in the sigPair is valid for the message and public key */ export function verifySignerSignature(message, signerSignature, publicKey) { const signature = signerSignature.signature; if (!signature) throw new Error('Signature not found in signature map'); return publicKey.verify(Buffer.from(prefixMessageToSign(message)), signature); } /** * * https://github.com/hashgraph/hedera-sdk-js/blob/c78512b1d43eedf1d8bf2926a5b7ed3368fc39d1/src/PublicKey.js#L258 * a signature pair is a protobuf object with a signature and a public key, it is the responsibility of a dApp to ensure the public key matches the account id * @param signerSignatures - An array of `SignerSignature` objects * @returns `proto.SignatureMap` object */ export function signerSignaturesToSignatureMap(signerSignatures) { const signatureMap = proto.SignatureMap.create({ sigPair: signerSignatures.map((s) => s.publicKey._toProtobufSignature(s.signature)), }); return signatureMap; } /** * A mapping of `LedgerId` to EIP chain id and CAIP-2 network name. * * Structure: [`LedgerId`, `number` (EIP155 chain id), `string` (CAIP-2 chain id)][] * * @see {@link https://namespaces.chainagnostic.org/hedera/README | Hedera Namespaces} * @see {@link https://hips.hedera.com/hip/hip-30 | CAIP Identifiers for the Hedera Network (HIP-30)} */ export const LEDGER_ID_MAPPINGS = [ [LedgerId.MAINNET, 295, 'hedera:mainnet'], [LedgerId.TESTNET, 296, 'hedera:testnet'], [LedgerId.PREVIEWNET, 297, 'hedera:previewnet'], [LedgerId.LOCAL_NODE, 298, 'hedera:devnet'], ]; const DEFAULT_LEDGER_ID = LedgerId.LOCAL_NODE; const DEFAULT_EIP = LEDGER_ID_MAPPINGS[3][1]; const DEFAULT_CAIP = LEDGER_ID_MAPPINGS[3][2]; /** * Converts an EIP chain id to a LedgerId object. * * If no mapping is found, returns `LedgerId.LOCAL_NODE`. * * @param chainId - The EIP chain ID (number) to be converted * @returns A `LedgerId` corresponding to the provided chain ID * @example * ```ts * const localnodeLedgerId = EIPChainIdToLedgerId(298) * console.log(localnodeLedgerId) // LedgerId.LOCAL_NODE * const mainnetLedgerId = EIPChainIdToLedgerId(295) * console.log(mainnetLedgerId) // LedgerId.MAINNET * ``` */ export function EIPChainIdToLedgerId(chainId) { for (let i = 0; i < LEDGER_ID_MAPPINGS.length; i++) { const [ledgerId, chainId_] = LEDGER_ID_MAPPINGS[i]; if (chainId === chainId_) { return ledgerId; } } return DEFAULT_LEDGER_ID; } /** * Converts a LedgerId object to an EIP chain id. * * If no mapping is found, returns the EIP chain id for `LedgerId.LOCAL_NODE`. * * @param ledgerId - The `LedgerId` object to be converted * @returns A `number` representing the EIP chain id for the provided `LedgerId` * @example * ```ts * const previewnetChainId = ledgerIdToEIPChainId(LedgerId.PREVIEWNET) * console.log(previewnetChainId) // 297 * const testnetChainId = ledgerIdToEIPChainId(LedgerId.TESTNET) * console.log(testnetChainId) // 296 * ``` */ export function ledgerIdToEIPChainId(ledgerId) { for (let i = 0; i < LEDGER_ID_MAPPINGS.length; i++) { const [ledgerId_, chainId] = LEDGER_ID_MAPPINGS[i]; if (ledgerId === ledgerId_) { return chainId; } } return DEFAULT_EIP; } /** * Converts a network name to an EIP chain id. * If no mapping is found, returns the EIP chain id for `LedgerId.LOCAL_NODE`. * * @param networkName - The network name (string) to be converted * @returns A `number` representing the EIP chain id for the provided network name * @example * ```ts * const mainnetChainId = networkNameToEIPChainId('mainnet') * console.log(mainnetChainId) // 295 * const testnetChainId = networkNameToEIPChainId('testnet') * console.log(mainnetChainId) // 296 * ``` */ export function networkNameToEIPChainId(networkName) { const ledgerId = LedgerId.fromString(networkName.toLowerCase()); return ledgerIdToEIPChainId(ledgerId); } /** * Converts a CAIP chain id to a LedgerId object. * * If no mapping is found, returns `LedgerId.LOCAL_NODE`. * * @param chainId - The CAIP chain ID (string) to be converted * @returns A `LedgerId` corresponding to the provided CAIP chain ID * @example * ```ts * const previewnetLedgerId = CAIPChainIdToLedgerId(HederaChainId.Previewnet) * console.log(previewnetLedgerId) // LedgerId.PREVIEWNET * const testnetLedgerId = CAIPChainIdToLedgerId(HederaChainId.Testnet) * console.log(testnetLedgerId) // LedgerId.TESTNET * ``` */ export function CAIPChainIdToLedgerId(chainId) { for (let i = 0; i < LEDGER_ID_MAPPINGS.length; i++) { const [ledgerId, _, chainId_] = LEDGER_ID_MAPPINGS[i]; if (chainId === chainId_) { return ledgerId; } } return DEFAULT_LEDGER_ID; } /** * Converts a LedgerId object to a CAIP chain id. * * If no mapping is found, returns the CAIP chain id for `LedgerId.LOCAL_NODE`. * * @param ledgerId - The `LedgerId` object to be converted * @returns A `string` representing the CAIP chain id for the provided `LedgerId` * @example * ```ts * const mainnetChainId = ledgerIdToCAIPChainId(HederaChainId.Mainnet) * console.log(mainnetChainId) // LedgerId.PREVIEWNET * const testnetChainId = ledgerIdToCAIPChainId(HederaChainId.Testnet) * console.log(testnetChainId) // LedgerId.TESTNET * ``` */ export function ledgerIdToCAIPChainId(ledgerId) { for (let i = 0; i < LEDGER_ID_MAPPINGS.length; i++) { const [ledgerId_, _, chainId] = LEDGER_ID_MAPPINGS[i]; if (ledgerId.toString() === ledgerId_.toString()) { return chainId; } } return DEFAULT_CAIP; } /** * Converts a network name to a CAIP chain id. * * If no mapping is found, returns the CAIP chain id for `LedgerId.LOCAL_NODE`. * * @param networkName - The network name (string) to be converted * @returns A `string` representing the CAIP chain id for the provided network name * @example * ```ts * const previewnetChainId = networkNameToCAIPChainId('previewnet') * console.log(previewnetChainId) // HederaChainId.Previewnet * const devnetChainId = networkNameToCAIPChainId('devnet') * console.log(devnetChainId) // HederaChainId.Devnet * ``` */ export function networkNameToCAIPChainId(networkName) { const ledgerId = LedgerId.fromString(networkName.toLowerCase()); const chainId = ledgerIdToCAIPChainId(ledgerId); return chainId; } /** * Create a `ProposalTypes.RequiredNamespaces` object for a given ledgerId. * * @param ledgerId - The `LedgerId` for which the namespaces are created * @param methods - An array of strings representing methods * @param events - An array of strings representing events * @returns A `ProposalTypes.RequiredNamespaces` object */ export const networkNamespaces = (ledgerId, methods, events) => ({ hedera: { chains: [ledgerIdToCAIPChainId(ledgerId)], methods, events, }, }); /** * Get the account and ledger from a `SessionTypes.Struct` object. * * @param session - The `SessionTypes.Struct` object containing namespaces * @returns `ProposalTypes.RequiredNamespaces` - an array of objects containing network (LedgerId) and account (AccountId) */ export const accountAndLedgerFromSession = (session) => { const hederaNamespace = session.namespaces.hedera; if (!hederaNamespace) throw new Error('No hedera namespace found'); return hederaNamespace.accounts.map((account) => { const [chain, network, acc] = account.split(':'); return { network: CAIPChainIdToLedgerId(chain + ':' + network), account: AccountId.fromString(acc), }; }); }; /** * Adds an additional signature to an already-signed transaction. * Uses proto-level manipulation to preserve existing signatures. */ export async function addSignatureToTransaction(transaction, privateKey) { const originalBytes = transaction.toBytes(); const originalList = proto.TransactionList.decode(originalBytes); const firstTransaction = originalList.transactionList[0]; let bodyBytes; if (firstTransaction.signedTransactionBytes) { const signedTx = proto.SignedTransaction.decode(firstTransaction.signedTransactionBytes); bodyBytes = signedTx.bodyBytes; } else { bodyBytes = firstTransaction.bodyBytes; } const signature = await privateKey.sign(bodyBytes); const publicKey = privateKey.publicKey; const signedTransactionList = originalList.transactionList.map((tx) => { const newSigPair = publicKey._toProtobufSignature(signature); if (tx.signedTransactionBytes) { const signedTx = proto.SignedTransaction.decode(tx.signedTransactionBytes); const existingSigMap = signedTx.sigMap || proto.SignatureMap.create({}); const mergedSigPairs = [...(existingSigMap.sigPair || []), newSigPair]; const updatedSignedTx = proto.SignedTransaction.encode({ bodyBytes: signedTx.bodyBytes, sigMap: proto.SignatureMap.create({ sigPair: mergedSigPairs }), }).finish(); return { signedTransactionBytes: updatedSignedTx }; } else { const existingSigMap = tx.sigMap || proto.SignatureMap.create({}); const mergedSigPairs = [...(existingSigMap.sigPair || []), newSigPair]; return Object.assign(Object.assign({}, tx), { sigMap: Object.assign(Object.assign({}, existingSigMap), { sigPair: mergedSigPairs }) }); } }); const signedBytes = proto.TransactionList.encode({ transactionList: signedTransactionList, }).finish(); return Transaction.fromBytes(signedBytes); }