@@ -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