@@ -6,6 +6,8 @@ import { createCaller } from "@/server/api/root";
66import { db } from "@/server/db" ;
77import { getProvider } from "@/utils/get-provider" ;
88import { addressToNetwork } from "@/utils/multisigSDK" ;
9+ import { resolvePaymentKeyHash } from "@meshsdk/core" ;
10+ import { csl , calculateTxHash } from "@meshsdk/core-csl" ;
911
1012function coerceBoolean ( value : unknown , fallback = false ) : boolean {
1113 if ( typeof value === "boolean" ) return value ;
@@ -17,6 +19,22 @@ function coerceBoolean(value: unknown, fallback = false): boolean {
1719 return fallback ;
1820}
1921
22+ function normalizeHex ( value : string , context : string ) : string {
23+ const trimmed = value . trim ( ) . toLowerCase ( ) . replace ( / ^ 0 x / , "" ) ;
24+ if ( trimmed . length === 0 || trimmed . length % 2 !== 0 || ! / ^ [ 0 - 9 a - f ] + $ / . test ( trimmed ) ) {
25+ throw new Error ( `Invalid ${ context } hex string` ) ;
26+ }
27+ return trimmed ;
28+ }
29+
30+ function toHex ( bytes : Uint8Array ) : string {
31+ return Buffer . from ( bytes ) . toString ( "hex" ) ;
32+ }
33+
34+ function toError ( error : unknown ) : Error {
35+ return error instanceof Error ? error : new Error ( String ( error ) ) ;
36+ }
37+
2038export default async function handler (
2139 req : NextApiRequest ,
2240 res : NextApiResponse ,
@@ -55,7 +73,8 @@ export default async function handler(
5573 walletId ?: unknown ;
5674 transactionId ?: unknown ;
5775 address ?: unknown ;
58- txCbor ?: unknown ;
76+ signature ?: unknown ;
77+ key ?: unknown ;
5978 broadcast ?: unknown ;
6079 txHash ?: unknown ;
6180 } ;
@@ -64,7 +83,8 @@ export default async function handler(
6483 walletId,
6584 transactionId,
6685 address,
67- txCbor,
86+ signature,
87+ key,
6888 broadcast : rawBroadcast ,
6989 txHash : rawTxHash ,
7090 } = ( req . body ?? { } ) as SignTransactionRequestBody ;
@@ -83,8 +103,12 @@ export default async function handler(
83103 return res . status ( 400 ) . json ( { error : "Missing or invalid address" } ) ;
84104 }
85105
86- if ( typeof txCbor !== "string" || txCbor . trim ( ) === "" ) {
87- return res . status ( 400 ) . json ( { error : "Missing or invalid txCbor" } ) ;
106+ if ( typeof signature !== "string" || signature . trim ( ) === "" ) {
107+ return res . status ( 400 ) . json ( { error : "Missing or invalid signature" } ) ;
108+ }
109+
110+ if ( typeof key !== "string" || key . trim ( ) === "" ) {
111+ return res . status ( 400 ) . json ( { error : "Missing or invalid key" } ) ;
88112 }
89113
90114 if ( payload . address !== address ) {
@@ -131,10 +155,104 @@ export default async function handler(
131155 . json ( { error : "Address has already rejected this transaction" } ) ;
132156 }
133157
134- const updatedSignedAddresses = [
135- ...transaction . signedAddresses ,
136- address ,
137- ] ;
158+ const updatedSignedAddresses = Array . from (
159+ new Set ( [ ...transaction . signedAddresses , address ] ) ,
160+ ) ;
161+
162+ const storedTxHex = transaction . txCbor ?. trim ( ) ;
163+ if ( ! storedTxHex ) {
164+ return res . status ( 500 ) . json ( { error : "Stored transaction is missing txCbor" } ) ;
165+ }
166+
167+ let parsedStoredTx : ReturnType < typeof csl . Transaction . from_hex > ;
168+ try {
169+ parsedStoredTx = csl . Transaction . from_hex ( storedTxHex ) ;
170+ } catch ( error : unknown ) {
171+ console . error ( "Failed to parse stored transaction" , toError ( error ) ) ;
172+ return res . status ( 500 ) . json ( { error : "Invalid stored transaction data" } ) ;
173+ }
174+
175+ const txBodyClone = csl . TransactionBody . from_bytes (
176+ parsedStoredTx . body ( ) . to_bytes ( ) ,
177+ ) ;
178+ const witnessSetClone = csl . TransactionWitnessSet . from_bytes (
179+ parsedStoredTx . witness_set ( ) . to_bytes ( ) ,
180+ ) ;
181+
182+ let vkeyWitnesses = witnessSetClone . vkeys ( ) ;
183+ if ( ! vkeyWitnesses ) {
184+ vkeyWitnesses = csl . Vkeywitnesses . new ( ) ;
185+ witnessSetClone . set_vkeys ( vkeyWitnesses ) ;
186+ } else {
187+ vkeyWitnesses = csl . Vkeywitnesses . from_bytes ( vkeyWitnesses . to_bytes ( ) ) ;
188+ witnessSetClone . set_vkeys ( vkeyWitnesses ) ;
189+ }
190+
191+ const signatureHex = normalizeHex ( signature , "signature" ) ;
192+ const keyHex = normalizeHex ( key , "key" ) ;
193+
194+ let witnessPublicKey : csl . PublicKey ;
195+ let witnessSignature : csl . Ed25519Signature ;
196+ let witnessToAdd : csl . Vkeywitness ;
197+
198+ try {
199+ witnessPublicKey = csl . PublicKey . from_hex ( keyHex ) ;
200+ witnessSignature = csl . Ed25519Signature . from_hex ( signatureHex ) ;
201+ const vkey = csl . Vkey . new ( witnessPublicKey ) ;
202+ witnessToAdd = csl . Vkeywitness . new ( vkey , witnessSignature ) ;
203+ } catch ( error : unknown ) {
204+ console . error ( "Invalid signature payload" , toError ( error ) ) ;
205+ return res . status ( 400 ) . json ( { error : "Invalid signature payload" } ) ;
206+ }
207+
208+ const witnessKeyHash = toHex ( witnessPublicKey . hash ( ) . to_bytes ( ) ) . toLowerCase ( ) ;
209+
210+ let addressKeyHash : string ;
211+ try {
212+ addressKeyHash = resolvePaymentKeyHash ( address ) . toLowerCase ( ) ;
213+ } catch ( error : unknown ) {
214+ console . error ( "Unable to resolve payment key hash" , toError ( error ) ) ;
215+ return res . status ( 400 ) . json ( { error : "Invalid address format" } ) ;
216+ }
217+
218+ if ( addressKeyHash !== witnessKeyHash ) {
219+ return res
220+ . status ( 403 )
221+ . json ( { error : "Signature public key does not match address" } ) ;
222+ }
223+
224+ const txHashHex = calculateTxHash ( parsedStoredTx . to_hex ( ) ) ;
225+ const txHashBytes = Buffer . from ( txHashHex , "hex" ) ;
226+ const isSignatureValid = witnessPublicKey . verify ( txHashBytes , witnessSignature ) ;
227+
228+ if ( ! isSignatureValid ) {
229+ return res . status ( 401 ) . json ( { error : "Invalid signature for transaction" } ) ;
230+ }
231+
232+ const existingWitnessCount = vkeyWitnesses . len ( ) ;
233+ for ( let i = 0 ; i < existingWitnessCount ; i ++ ) {
234+ const existingWitness = vkeyWitnesses . get ( i ) ;
235+ const existingKeyHash = toHex (
236+ existingWitness . vkey ( ) . public_key ( ) . hash ( ) . to_bytes ( ) ,
237+ ) . toLowerCase ( ) ;
238+ if ( existingKeyHash === witnessKeyHash ) {
239+ return res
240+ . status ( 409 )
241+ . json ( { error : "Witness for this address already exists" } ) ;
242+ }
243+ }
244+
245+ vkeyWitnesses . add ( witnessToAdd ) ;
246+
247+ const updatedTx = csl . Transaction . new (
248+ txBodyClone ,
249+ witnessSetClone ,
250+ parsedStoredTx . auxiliary_data ( ) ,
251+ ) ;
252+ if ( ! parsedStoredTx . is_valid ( ) ) {
253+ updatedTx . set_is_valid ( false ) ;
254+ }
255+ const txHexForUpdate = updatedTx . to_hex ( ) ;
138256
139257 const shouldAttemptBroadcast = coerceBoolean ( rawBroadcast , true ) ;
140258
@@ -170,23 +288,23 @@ export default async function handler(
170288 const networkSource = wallet . signersAddresses [ 0 ] ?? address ;
171289 const network = addressToNetwork ( networkSource ) ;
172290 const provider = getProvider ( network ) ;
173- const submittedHash = await provider . submitTx ( txCbor ) ;
291+ const submittedHash = await provider . submitTx ( txHexForUpdate ) ;
174292 finalTxHash = submittedHash ;
175293 nextState = 1 ;
176- } catch ( error ) {
294+ } catch ( error : unknown ) {
295+ const err = toError ( error ) ;
177296 console . error ( "Error submitting signed transaction" , {
178297 transactionId,
179- error,
298+ error : err ,
180299 } ) ;
181- submissionError = ( error as Error ) ? .message ?? "Failed to submit transaction" ;
300+ submissionError = err . message ?? "Failed to submit transaction" ;
182301 }
183302 }
184303
185304 if ( providedTxHash ) {
186305 nextState = 1 ;
187306 }
188307
189- // Ensure we do not downgrade a completed transaction back to pending
190308 if ( transaction . state === 1 ) {
191309 nextState = 1 ;
192310 } else if ( nextState !== 1 ) {
@@ -195,7 +313,7 @@ export default async function handler(
195313
196314 const updatedTransaction = await caller . transaction . updateTransaction ( {
197315 transactionId,
198- txCbor,
316+ txCbor : txHexForUpdate ,
199317 signedAddresses : updatedSignedAddresses ,
200318 rejectedAddresses : transaction . rejectedAddresses ,
201319 state : nextState ,
@@ -208,10 +326,11 @@ export default async function handler(
208326 txHash : finalTxHash ,
209327 ...( submissionError ? { submissionError } : { } ) ,
210328 } ) ;
211- } catch ( error ) {
329+ } catch ( error : unknown ) {
330+ const err = toError ( error ) ;
212331 console . error ( "Error in signTransaction handler" , {
213- message : ( error as Error ) ? .message ,
214- stack : ( error as Error ) ? .stack ,
332+ message : err . message ,
333+ stack : err . stack ,
215334 } ) ;
216335 return res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
217336 }
0 commit comments