Skip to content

Commit 152afd9

Browse files
committed
Enhance signTransaction endpoint with improved validation and error handling
- Introduced new parameters for signature and key, replacing the previous txCbor parameter. - Added functions for hex normalization and error handling. - Enhanced transaction processing logic, including signature verification and witness management. - Updated transaction submission logic to use the correct transaction hex format. - Improved error logging for better debugging.
1 parent b215bd0 commit 152afd9

File tree

1 file changed

+136
-17
lines changed

1 file changed

+136
-17
lines changed

src/pages/api/v1/signTransaction.ts

Lines changed: 136 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { createCaller } from "@/server/api/root";
66
import { db } from "@/server/db";
77
import { getProvider } from "@/utils/get-provider";
88
import { addressToNetwork } from "@/utils/multisigSDK";
9+
import { resolvePaymentKeyHash } from "@meshsdk/core";
10+
import { csl, calculateTxHash } from "@meshsdk/core-csl";
911

1012
function 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(/^0x/, "");
24+
if (trimmed.length === 0 || trimmed.length % 2 !== 0 || !/^[0-9a-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+
2038
export 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

Comments
 (0)