Skip to content

Commit

Permalink
fix: improves receipt handling for transactions with multiple signatu…
Browse files Browse the repository at this point in the history
…res (#339)

We discovered that certain transactions e.g. AccountCreateTransactions with a threshold key would fail to return a receipt when executeWithSigner was called on the transaction.

Under the hood, call was being executed in the DAppSigner and attempting to run the query through the wallet. Because these queries are free, we should utilize getReceipt instead to ensure consistency and reliability for these complex transactions.

Additionally, we should avoid guessing and running both a query and transaction when call is used by the Hedera SDK. This would return the following incorrect error

Signed-off-by: Michael Kantor <[email protected]>
  • Loading branch information
kantorcodes authored Nov 25, 2024
1 parent e10fa05 commit 646a2ef
Show file tree
Hide file tree
Showing 8 changed files with 494 additions and 57 deletions.
146 changes: 137 additions & 9 deletions demos/react-dapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
PublicKey,
TransactionId,
TransferTransaction,
AccountCreateTransaction,
KeyList,
} from '@hashgraph/sdk'
import { SessionTypes, SignClientTypes } from '@walletconnect/types'

Expand All @@ -26,7 +28,9 @@ import {
transactionToBase64String,
SignAndExecuteQueryParams,
ExecuteTransactionParams,
} from '../../../dist/src/index'
base64StringToUint8Array,
verifySignerSignature,
} from '../../../dist'

import React, { useEffect, useMemo, useState } from 'react'
import Modal from './components/Modal'
Expand Down Expand Up @@ -56,6 +60,7 @@ const App: React.FC = () => {
const [amount, setAmount] = useState('')
const [message, setMessage] = useState('')
const [publicKey, setPublicKey] = useState('')
const [signMethod, setSignMethod] = useState<'connector' | 'signer'>('connector')
const [selectedTransactionMethod, setSelectedTransactionMethod] = useState(
'hedera_executeTransaction',
)
Expand All @@ -65,6 +70,10 @@ const App: React.FC = () => {
const [isModalLoading, setIsModalLoading] = useState<boolean>(false)
const [modalData, setModalData] = useState<any>(null)

// Multi-signature account states
const [publicKeyInputs, setPublicKeyInputs] = useState<string[]>([''])
const [threshold, setThreshold] = useState<number>(1)

useEffect(() => {
const state = JSON.parse(localStorage.getItem('hedera-wc-demos-saved-state') || '{}')
if (state) {
Expand Down Expand Up @@ -183,6 +192,31 @@ const App: React.FC = () => {
})
}

const handleSignMessageThroughSigner = async () => {
modalWrapper(async () => {
if (!selectedSigner) throw new Error('Selected signer is required')
const params: SignMessageParams = {
signerAccountId: 'hedera:testnet:' + selectedSigner.getAccountId().toString(),
message,
}

const buffered = btoa(params.message)
const base64 = base64StringToUint8Array(buffered)

const signResult = await (selectedSigner as DAppSigner).sign(
[base64]
)
const accountPublicKey = PublicKey.fromString(publicKey)
const verifiedResult = verifySignerSignature(params.message, signResult[0], accountPublicKey)
console.log('SignatureMap: ', signResult)
console.log('Verified: ', verifiedResult)
return {
signatureMap: signResult,
verified: verifiedResult,
}
})
}

// 4. hedera_signAndExecuteQuery
const handleExecuteQuery = () => {
modalWrapper(async () => {
Expand Down Expand Up @@ -239,6 +273,36 @@ const App: React.FC = () => {
return { transaction: transactionSigned }
}

// Create multi-signature account
const handleCreateMultisigAccount = async () => {
// Fetch public keys from mirror node for each account
const fetchPublicKey = async (accountId: string) => {
const response = await fetch(
`https://testnet.mirrornode.hedera.com/api/v1/accounts/${accountId}`
)
const data = await response.json()
return data.key.key
}

const publicKeys = await Promise.all(
publicKeyInputs.filter(id => id).map(accountId => fetchPublicKey(accountId))
)

console.log('Public keys: ', publicKeys)

const transaction = new AccountCreateTransaction()
.setKey(new KeyList(publicKeys.map((key) => PublicKey.fromString(key)), threshold))
.setInitialBalance(new Hbar(0))
.setAccountMemo('Multisig Account')

const frozen = await transaction.freezeWithSigner(selectedSigner!)
const result = await frozen.executeWithSigner(selectedSigner!)
console.log('Result: transaction completed', result)
const receipt = await result.getReceiptWithSigner(selectedSigner!)
console.log('Receipt: ', receipt)
return receipt;
}

/**
* Session management methods
*/
Expand Down Expand Up @@ -454,13 +518,17 @@ const App: React.FC = () => {
<div>
<fieldset>
<legend>3. hedera_signMessage</legend>
<AccountSelector
accounts={signers.map((signer) => signer.getAccountId())}
selectedAccount={selectedSigner?.getAccountId() || null}
onSelect={(accountId) =>
setSelectedSigner(dAppConnector?.getSigner(accountId)!)
}
/>
<label>
Sign Method:
<select
value={signMethod}
onChange={(e) => setSignMethod(e.target.value as 'connector' | 'signer')}
className="mb-2"
>
<option value="connector">Sign with Connector</option>
<option value="signer">Sign with Signer</option>
</select>
</label>
<label>
Message:
<input value={message} onChange={(e) => setMessage(e.target.value)} required />
Expand All @@ -475,7 +543,10 @@ const App: React.FC = () => {
</label>
<p>The public key for the account is used to verify the signed message</p>
</fieldset>
<button disabled={disableButtons} onClick={handleSignMessage}>
<button
disabled={disableButtons}
onClick={signMethod === 'connector' ? handleSignMessage : handleSignMessageThroughSigner}
>
Submit to wallet
</button>
</div>
Expand Down Expand Up @@ -600,6 +671,63 @@ const App: React.FC = () => {
<button onClick={handleClearData}>Clear saved data</button>
</div>
</section>
<section>
<div>
<fieldset>
<legend>Create Multi-signature Account</legend>
<AccountSelector
accounts={signers.map((signer) => signer.getAccountId())}
selectedAccount={selectedSigner?.getAccountId() || null}
onSelect={(accountId) =>
setSelectedSigner(dAppConnector?.getSigner(accountId)!)
}
/>
{publicKeyInputs.map((input, index) => (
<div key={index}>
<label>
Account ID {index + 1}:
<input
value={input}
onChange={(e) => {
const newInputs = [...publicKeyInputs]
newInputs[index] = e.target.value
setPublicKeyInputs(newInputs)
}}
placeholder="Enter Account ID"
/>
</label>
{index === publicKeyInputs.length - 1 && (
<button onClick={() => setPublicKeyInputs([...publicKeyInputs, ''])}>
Add Another Account
</button>
)}
</div>
))}
<label>
Threshold:
<input
type="number"
min="1"
max={publicKeyInputs.filter(Boolean).length}
value={threshold}
onChange={(e) => setThreshold(parseInt(e.target.value))}
/>
</label>
</fieldset>
<button
disabled={disableButtons || !publicKeyInputs[0] || threshold < 1}
onClick={() => {
modalWrapper(async () => {
if (!dAppConnector) throw new Error('DAppConnector is required')
if (!selectedSigner) throw new Error('Selected signer is required')
return handleCreateMultisigAccount()
})
}}
>
Create Multisig Account
</button>
</div>
</section>
<Modal title="Send Request" isOpen={isModalOpen} onClose={() => setModalOpen(false)}>
{isModalLoading ? (
<div className="loading">
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hashgraph/hedera-wallet-connect",
"version": "1.3.6",
"version": "1.3.7",
"description": "A library to facilitate integrating Hedera with WalletConnect",
"repository": {
"type": "git",
Expand Down Expand Up @@ -55,6 +55,7 @@
"dev:ts-demo": "rimraf dist && npm run build && concurrently --raw \"npm run watch\" \"node scripts/demos/typescript/dev.mjs\"",
"dev:react-demo": "rimraf dist && npm run build && concurrently --raw \"npm run watch\" \"node scripts/demos/react/dev.mjs\"",
"test": "jest",
"test:watch": "jest --watch",
"test:connect": "jest --testMatch '**/DAppConnector.test.ts' --verbose",
"test:signer": "jest --testMatch '**/DAppSigner.test.ts' --verbose",
"prepublishOnly": "rm -Rf dist && npm run build",
Expand Down
Loading

0 comments on commit 646a2ef

Please sign in to comment.