@@ -5,18 +5,17 @@ import { verifyJwt } from "@/lib/verifyJwt";
55import { createCaller } from "@/server/api/root" ;
66import { db } from "@/server/db" ;
77import { getProvider } from "@/utils/get-provider" ;
8- import { paymentKeyHash } from "@/utils/multisigSDK" ;
9- import { csl } from "@meshsdk/core-csl" ;
10-
11- const HEX_REGEX = / ^ [ 0 - 9 a - f A - F ] + $ / ;
12-
13- const isHexString = ( value : unknown ) : value is string =>
14- typeof value === "string" && value . length > 0 && HEX_REGEX . test ( value ) ;
15-
16- const resolveNetworkFromAddress = ( bech32Address : string ) : 0 | 1 =>
17- bech32Address . startsWith ( "addr_test" ) || bech32Address . startsWith ( "stake_test" )
18- ? 0
19- : 1 ;
8+ import { addressToNetwork } from "@/utils/multisigSDK" ;
9+
10+ function coerceBoolean ( value : unknown , fallback = false ) : boolean {
11+ if ( typeof value === "boolean" ) return value ;
12+ if ( typeof value === "string" ) {
13+ const normalized = value . trim ( ) . toLowerCase ( ) ;
14+ if ( normalized === "true" ) return true ;
15+ if ( normalized === "false" ) return false ;
16+ }
17+ return fallback ;
18+ }
2019
2120export default async function handler (
2221 req : NextApiRequest ,
@@ -34,7 +33,9 @@ export default async function handler(
3433 }
3534
3635 const authHeader = req . headers . authorization ;
37- const token = authHeader ?. startsWith ( "Bearer " ) ? authHeader . slice ( 7 ) : null ;
36+ const token = authHeader ?. startsWith ( "Bearer " )
37+ ? authHeader . slice ( 7 )
38+ : null ;
3839
3940 if ( ! token ) {
4041 return res . status ( 401 ) . json ( { error : "Unauthorized - Missing token" } ) ;
@@ -50,42 +51,49 @@ export default async function handler(
5051 expires : new Date ( Date . now ( ) + 60 * 60 * 1000 ) . toISOString ( ) ,
5152 } as const ;
5253
53- const caller = createCaller ( { db, session } ) ;
54-
55- const body =
56- typeof req . body === "object" && req . body !== null
57- ? ( req . body as Record < string , unknown > )
58- : { } ;
59-
60- const { walletId, transactionId, address, signedTx } = body ;
61-
62- if ( typeof walletId !== "string" || walletId . trim ( ) . length === 0 ) {
54+ type SignTransactionRequestBody = {
55+ walletId ?: unknown ;
56+ transactionId ?: unknown ;
57+ address ?: unknown ;
58+ txCbor ?: unknown ;
59+ broadcast ?: unknown ;
60+ txHash ?: unknown ;
61+ } ;
62+
63+ const {
64+ walletId,
65+ transactionId,
66+ address,
67+ txCbor,
68+ broadcast : rawBroadcast ,
69+ txHash : rawTxHash ,
70+ } = ( req . body ?? { } ) as SignTransactionRequestBody ;
71+
72+ if ( typeof walletId !== "string" || walletId . trim ( ) === "" ) {
6373 return res . status ( 400 ) . json ( { error : "Missing or invalid walletId" } ) ;
6474 }
6575
66- if ( typeof transactionId !== "string" || transactionId . trim ( ) . length === 0 ) {
76+ if ( typeof transactionId !== "string" || transactionId . trim ( ) === "" ) {
6777 return res
6878 . status ( 400 )
6979 . json ( { error : "Missing or invalid transactionId" } ) ;
7080 }
7181
72- if ( typeof address !== "string" || address . trim ( ) . length === 0 ) {
82+ if ( typeof address !== "string" || address . trim ( ) === "" ) {
7383 return res . status ( 400 ) . json ( { error : "Missing or invalid address" } ) ;
7484 }
7585
76- if ( ! isHexString ( signedTx ) ) {
77- return res . status ( 400 ) . json ( { error : "Missing or invalid signedTx" } ) ;
78- }
79-
80- if ( signedTx . length % 2 !== 0 ) {
81- return res . status ( 400 ) . json ( { error : "Missing or invalid signedTx" } ) ;
86+ if ( typeof txCbor !== "string" || txCbor . trim ( ) === "" ) {
87+ return res . status ( 400 ) . json ( { error : "Missing or invalid txCbor" } ) ;
8288 }
8389
8490 if ( payload . address !== address ) {
8591 return res . status ( 403 ) . json ( { error : "Address mismatch" } ) ;
8692 }
8793
8894 try {
95+ const caller = createCaller ( { db, session } ) ;
96+
8997 const wallet = await caller . wallet . getWallet ( { walletId, address } ) ;
9098 if ( ! wallet ) {
9199 return res . status ( 404 ) . json ( { error : "Wallet not found" } ) ;
@@ -114,111 +122,91 @@ export default async function handler(
114122 if ( transaction . signedAddresses . includes ( address ) ) {
115123 return res
116124 . status ( 409 )
117- . json ( { error : "Address already signed this transaction" } ) ;
125+ . json ( { error : "Address has already signed this transaction" } ) ;
118126 }
119127
120128 if ( transaction . rejectedAddresses . includes ( address ) ) {
121129 return res
122130 . status ( 409 )
123- . json ( { error : "Address has rejected this transaction" } ) ;
124- }
125-
126- let signerKeyHash : string ;
127- try {
128- signerKeyHash = paymentKeyHash ( address ) . toLowerCase ( ) ;
129- } catch ( deriveError ) {
130- console . error ( "Failed to derive payment key hash" , {
131- message : ( deriveError as Error ) ?. message ,
132- } ) ;
133- return res . status ( 400 ) . json ( { error : "Unable to derive signer key" } ) ;
131+ . json ( { error : "Address has already rejected this transaction" } ) ;
134132 }
135133
136- let signerWitnessFound = false ;
137- try {
138- const tx = csl . Transaction . from_hex ( signedTx ) ;
139- const vkeys = tx . witness_set ( ) ?. vkeys ( ) ;
140-
141- if ( vkeys ) {
142- for ( let i = 0 ; i < vkeys . len ( ) ; i += 1 ) {
143- const witness = vkeys . get ( i ) ;
144- const witnessKeyHash = Buffer . from (
145- witness . vkey ( ) . public_key ( ) . hash ( ) . to_bytes ( ) ,
146- )
147- . toString ( "hex" )
148- . toLowerCase ( ) ;
149-
150- if ( witnessKeyHash === signerKeyHash ) {
151- signerWitnessFound = true ;
152- break ;
153- }
154- }
134+ const updatedSignedAddresses = [
135+ ...transaction . signedAddresses ,
136+ address ,
137+ ] ;
138+
139+ const shouldAttemptBroadcast = coerceBoolean ( rawBroadcast , true ) ;
140+
141+ const threshold = ( ( ) => {
142+ switch ( wallet . type ) {
143+ case "atLeast" :
144+ return wallet . numRequiredSigners ?? wallet . signersAddresses . length ;
145+ case "all" :
146+ return wallet . signersAddresses . length ;
147+ case "any" :
148+ return 1 ;
149+ default :
150+ return wallet . numRequiredSigners ?? 1 ;
155151 }
156- } catch ( decodeError ) {
157- console . error ( "Failed to inspect transaction witnesses" , {
158- message : ( decodeError as Error ) ?. message ,
159- stack : ( decodeError as Error ) ?. stack ,
160- } ) ;
161- return res . status ( 400 ) . json ( { error : "Invalid signedTx payload" } ) ;
162- }
163-
164- if ( ! signerWitnessFound ) {
165- return res . status ( 400 ) . json ( {
166- error : "Signed transaction does not include caller signature" ,
167- } ) ;
168- }
169-
170- const updatedSignedAddresses = [ ...transaction . signedAddresses , address ] ;
171- const updatedRejectedAddresses = [ ...transaction . rejectedAddresses ] ;
172-
173- const totalSigners = wallet . signersAddresses . length ;
174- const requiredSigners = wallet . numRequiredSigners ?? undefined ;
175-
176- let thresholdReached = false ;
177- switch ( wallet . type ) {
178- case "any" :
179- thresholdReached = true ;
180- break ;
181- case "all" :
182- thresholdReached = updatedSignedAddresses . length >= totalSigners ;
183- break ;
184- case "atLeast" :
185- thresholdReached =
186- typeof requiredSigners === "number" &&
187- updatedSignedAddresses . length >= requiredSigners ;
188- break ;
189- default :
190- thresholdReached = false ;
191- }
192-
193- let finalTxHash : string | undefined ;
194- if ( thresholdReached && ! finalTxHash ) {
152+ } ) ( ) ;
153+
154+ const providedTxHash =
155+ typeof rawTxHash === "string" && rawTxHash . trim ( ) !== ""
156+ ? rawTxHash . trim ( )
157+ : undefined ;
158+
159+ let nextState = transaction . state ;
160+ let finalTxHash = providedTxHash ?? transaction . txHash ?? undefined ;
161+ let submissionError : string | undefined ;
162+
163+ if (
164+ shouldAttemptBroadcast &&
165+ threshold > 0 &&
166+ updatedSignedAddresses . length >= threshold &&
167+ ! providedTxHash
168+ ) {
195169 try {
196- const network = resolveNetworkFromAddress ( address ) ;
197- const blockchainProvider = getProvider ( network ) ;
198- finalTxHash = await blockchainProvider . submitTx ( signedTx ) ;
199- } catch ( submitError ) {
200- console . error ( "Failed to submit transaction" , {
201- message : ( submitError as Error ) ?. message ,
202- stack : ( submitError as Error ) ?. stack ,
170+ const networkSource = wallet . signersAddresses [ 0 ] ?? address ;
171+ const network = addressToNetwork ( networkSource ) ;
172+ const provider = getProvider ( network ) ;
173+ const submittedHash = await provider . submitTx ( txCbor ) ;
174+ finalTxHash = submittedHash ;
175+ nextState = 1 ;
176+ } catch ( error ) {
177+ console . error ( "Error submitting signed transaction" , {
178+ transactionId,
179+ error,
203180 } ) ;
204- return res . status ( 502 ) . json ( { error : "Failed to submit transaction" } ) ;
181+ submissionError = ( error as Error ) ?. message ?? "Failed to submit transaction" ;
205182 }
206183 }
207184
208- const nextState = finalTxHash ? 1 : 0 ;
185+ if ( providedTxHash ) {
186+ nextState = 1 ;
187+ }
188+
189+ // Ensure we do not downgrade a completed transaction back to pending
190+ if ( transaction . state === 1 ) {
191+ nextState = 1 ;
192+ } else if ( nextState !== 1 ) {
193+ nextState = 0 ;
194+ }
209195
210196 const updatedTransaction = await caller . transaction . updateTransaction ( {
211197 transactionId,
212- txCbor : signedTx ,
198+ txCbor,
213199 signedAddresses : updatedSignedAddresses ,
214- rejectedAddresses : updatedRejectedAddresses ,
200+ rejectedAddresses : transaction . rejectedAddresses ,
215201 state : nextState ,
216- txHash : finalTxHash ,
202+ ... ( finalTxHash ? { txHash : finalTxHash } : { } ) ,
217203 } ) ;
218204
219205 return res . status ( 200 ) . json ( {
220206 transaction : updatedTransaction ,
221- thresholdReached,
207+ submitted : nextState === 1 ,
208+ txHash : finalTxHash ,
209+ ...( submissionError ? { submissionError } : { } ) ,
222210 } ) ;
223211 } catch ( error ) {
224212 console . error ( "Error in signTransaction handler" , {
@@ -228,4 +216,3 @@ export default async function handler(
228216 return res . status ( 500 ) . json ( { error : "Internal Server Error" } ) ;
229217 }
230218}
231-
0 commit comments