Skip to content

Commit 194ef8e

Browse files
committed
Enhance signTransaction endpoint with additional validation and witness management
- Normalized transaction hash to lowercase for consistency. - Introduced witness summaries for better tracking of signatures and public keys. - Improved transaction hash validation and error handling for provided transaction hashes. - Refactored transaction state management and updated transaction JSON structure to include multisig details. - Enhanced error responses for better clarity on transaction submission issues.
1 parent 8e197e6 commit 194ef8e

File tree

1 file changed

+151
-12
lines changed

1 file changed

+151
-12
lines changed

src/pages/api/v1/signTransaction.ts

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export default async function handler(
221221
.json({ error: "Signature public key does not match address" });
222222
}
223223

224-
const txHashHex = calculateTxHash(parsedStoredTx.to_hex());
224+
const txHashHex = calculateTxHash(parsedStoredTx.to_hex()).toLowerCase();
225225
const txHashBytes = Buffer.from(txHashHex, "hex");
226226
const isSignatureValid = witnessPublicKey.verify(txHashBytes, witnessSignature);
227227

@@ -254,6 +254,26 @@ export default async function handler(
254254
}
255255
const txHexForUpdate = updatedTx.to_hex();
256256

257+
const witnessSummaries: {
258+
keyHashHex: string;
259+
publicKeyBech32: string;
260+
signatureHex: string;
261+
}[] = [];
262+
const witnessSetForExport = csl.Vkeywitnesses.from_bytes(
263+
vkeyWitnesses.to_bytes(),
264+
);
265+
const witnessCountForExport = witnessSetForExport.len();
266+
for (let i = 0; i < witnessCountForExport; i++) {
267+
const witness = witnessSetForExport.get(i);
268+
witnessSummaries.push({
269+
keyHashHex: toHex(
270+
witness.vkey().public_key().hash().to_bytes(),
271+
).toLowerCase(),
272+
publicKeyBech32: witness.vkey().public_key().to_bech32(),
273+
signatureHex: toHex(witness.signature().to_bytes()).toLowerCase(),
274+
});
275+
}
276+
257277
const shouldAttemptBroadcast = coerceBoolean(rawBroadcast, true);
258278

259279
const threshold = (() => {
@@ -274,19 +294,61 @@ export default async function handler(
274294
? rawTxHash.trim()
275295
: undefined;
276296

297+
let normalizedProvidedTxHash: string | undefined;
298+
if (providedTxHash) {
299+
try {
300+
normalizedProvidedTxHash = normalizeHex(providedTxHash, "txHash");
301+
} catch (error: unknown) {
302+
console.error("Invalid txHash payload", toError(error));
303+
return res.status(400).json({ error: "Invalid txHash format" });
304+
}
305+
306+
if (normalizedProvidedTxHash.length !== 64) {
307+
return res.status(400).json({ error: "Invalid txHash length" });
308+
}
309+
310+
if (normalizedProvidedTxHash !== txHashHex) {
311+
return res
312+
.status(400)
313+
.json({ error: "Provided txHash does not match transaction body" });
314+
}
315+
}
316+
277317
let nextState = transaction.state;
278-
let finalTxHash = providedTxHash ?? transaction.txHash ?? undefined;
318+
let finalTxHash = normalizedProvidedTxHash ?? transaction.txHash ?? undefined;
279319
let submissionError: string | undefined;
280320

321+
const resolveNetworkId = async (): Promise<number> => {
322+
const primarySignerAddress = wallet.signersAddresses.find(
323+
(candidate) => typeof candidate === "string" && candidate.trim() !== "",
324+
);
325+
if (primarySignerAddress) {
326+
return addressToNetwork(primarySignerAddress);
327+
}
328+
329+
const walletRecord = await db.wallet.findUnique({
330+
where: { id: walletId },
331+
select: { signersAddresses: true },
332+
});
333+
334+
const fallbackAddress = walletRecord?.signersAddresses?.find(
335+
(candidate) => typeof candidate === "string" && candidate.trim() !== "",
336+
);
337+
if (fallbackAddress) {
338+
return addressToNetwork(fallbackAddress);
339+
}
340+
341+
return addressToNetwork(address);
342+
};
343+
281344
if (
282345
shouldAttemptBroadcast &&
283346
threshold > 0 &&
284347
updatedSignedAddresses.length >= threshold &&
285-
!providedTxHash
348+
!normalizedProvidedTxHash
286349
) {
287350
try {
288-
const networkSource = wallet.signersAddresses[0] ?? address;
289-
const network = addressToNetwork(networkSource);
351+
const network = await resolveNetworkId();
290352
const provider = getProvider(network);
291353
const submittedHash = await provider.submitTx(txHexForUpdate);
292354
finalTxHash = submittedHash;
@@ -301,7 +363,8 @@ export default async function handler(
301363
}
302364
}
303365

304-
if (providedTxHash) {
366+
if (normalizedProvidedTxHash) {
367+
finalTxHash = txHashHex;
305368
nextState = 1;
306369
}
307370

@@ -311,20 +374,96 @@ export default async function handler(
311374
nextState = 0;
312375
}
313376

314-
const updatedTransaction = await caller.transaction.updateTransaction({
315-
transactionId,
377+
let txJsonForUpdate = transaction.txJson;
378+
try {
379+
const parsedTxJson = JSON.parse(
380+
transaction.txJson,
381+
) as Record<string, unknown>;
382+
const enrichedTxJson = {
383+
...parsedTxJson,
384+
multisig: {
385+
state: nextState,
386+
submitted: nextState === 1,
387+
signedAddresses: updatedSignedAddresses,
388+
rejectedAddresses: transaction.rejectedAddresses,
389+
witnesses: witnessSummaries,
390+
txHash: (finalTxHash ?? txHashHex).toLowerCase(),
391+
bodyHash: txHashHex,
392+
submissionError: submissionError ?? null,
393+
},
394+
};
395+
txJsonForUpdate = JSON.stringify(enrichedTxJson);
396+
} catch (error: unknown) {
397+
const err = toError(error);
398+
console.warn("Unable to update txJson snapshot", {
399+
transactionId,
400+
error: err,
401+
});
402+
}
403+
404+
const updateData: {
405+
signedAddresses: { set: string[] };
406+
rejectedAddresses: { set: string[] };
407+
txCbor: string;
408+
txJson: string;
409+
state: number;
410+
txHash?: string;
411+
} = {
412+
signedAddresses: { set: updatedSignedAddresses },
413+
rejectedAddresses: { set: transaction.rejectedAddresses },
316414
txCbor: txHexForUpdate,
317-
signedAddresses: updatedSignedAddresses,
318-
rejectedAddresses: transaction.rejectedAddresses,
415+
txJson: txJsonForUpdate,
319416
state: nextState,
320-
...(finalTxHash ? { txHash: finalTxHash } : {}),
417+
};
418+
419+
if (finalTxHash) {
420+
updateData.txHash = finalTxHash;
421+
}
422+
423+
const updateResult = await db.transaction.updateMany({
424+
where: {
425+
id: transactionId,
426+
signedAddresses: { equals: transaction.signedAddresses },
427+
rejectedAddresses: { equals: transaction.rejectedAddresses },
428+
txCbor: transaction.txCbor ?? "",
429+
txJson: transaction.txJson,
430+
},
431+
data: updateData,
321432
});
322433

434+
if (updateResult.count === 0) {
435+
const latest = await db.transaction.findUnique({
436+
where: { id: transactionId },
437+
});
438+
439+
return res.status(409).json({
440+
error: "Transaction was updated by another signer. Please refresh and try again.",
441+
...(latest ? { transaction: latest } : {}),
442+
});
443+
}
444+
445+
const updatedTransaction = await db.transaction.findUnique({
446+
where: { id: transactionId },
447+
});
448+
449+
if (!updatedTransaction) {
450+
return res.status(500).json({ error: "Failed to load updated transaction state" });
451+
}
452+
453+
if (submissionError) {
454+
return res.status(502).json({
455+
error: "Transaction witness recorded, but submission to network failed",
456+
transaction: updatedTransaction,
457+
submitted: false,
458+
txHash: finalTxHash,
459+
submissionError,
460+
});
461+
}
462+
323463
return res.status(200).json({
324464
transaction: updatedTransaction,
325465
submitted: nextState === 1,
326466
txHash: finalTxHash,
327-
...(submissionError ? { submissionError } : {}),
328467
});
329468
} catch (error: unknown) {
330469
const err = toError(error);

0 commit comments

Comments
 (0)