Skip to content

Commit 1bef6f8

Browse files
committed
Enhance signTransaction endpoint with additional mocks and Swagger documentation
- Added mock implementations for database transaction updates and console error/warn handling in tests. - Updated the signTransaction API documentation in Swagger to include detailed request and response structures. - Improved test coverage for transaction updates and error handling scenarios.
1 parent 7790dfa commit 1bef6f8

File tree

2 files changed

+196
-18
lines changed

2 files changed

+196
-18
lines changed

src/__tests__/signTransaction.test.ts

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,17 @@ jest.mock(
3737
);
3838

3939
const dbTransactionFindUniqueMock = jest.fn<(args: unknown) => Promise<unknown>>();
40+
const dbTransactionUpdateManyMock = jest.fn<
41+
(args: {
42+
where: unknown;
43+
data: unknown;
44+
}) => Promise<{ count: number }>
45+
>();
4046

4147
const dbMock = {
4248
transaction: {
4349
findUnique: dbTransactionFindUniqueMock,
50+
updateMany: dbTransactionUpdateManyMock,
4451
},
4552
};
4653

@@ -94,6 +101,10 @@ class MockEd25519Signature {
94101
static from_hex(hex: string) {
95102
return new MockEd25519Signature(hex);
96103
}
104+
105+
to_bytes() {
106+
return Buffer.from(this.hex, 'hex');
107+
}
97108
}
98109

99110
class MockPublicKey {
@@ -112,6 +123,10 @@ class MockPublicKey {
112123
verify() {
113124
return true;
114125
}
126+
127+
to_bech32() {
128+
return `mock_bech32_${this.hex.slice(0, 8)}`;
129+
}
115130
}
116131

117132
class MockVkey {
@@ -139,6 +154,10 @@ class MockVkeywitness {
139154
vkey() {
140155
return this.vkeyInstance;
141156
}
157+
158+
signature() {
159+
return this.signatureInstance;
160+
}
142161
}
143162

144163
class MockVkeywitnesses {
@@ -284,6 +303,13 @@ jest.mock(
284303
{ virtual: true },
285304
);
286305

306+
const consoleErrorSpy = jest
307+
.spyOn(console, 'error')
308+
.mockImplementation(() => undefined);
309+
const consoleWarnSpy = jest
310+
.spyOn(console, 'warn')
311+
.mockImplementation(() => undefined);
312+
287313
type ResponseMock = NextApiResponse & { statusCode?: number };
288314

289315
function createMockResponse(): ResponseMock {
@@ -306,7 +332,6 @@ function createMockResponse(): ResponseMock {
306332
}
307333

308334
const walletGetWalletMock = jest.fn<(args: unknown) => Promise<unknown>>();
309-
const transactionUpdateMock = jest.fn<(args: unknown) => Promise<unknown>>();
310335

311336
let handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void | NextApiResponse>;
312337

@@ -319,8 +344,8 @@ beforeEach(() => {
319344
MockVkeywitnesses.reset();
320345

321346
walletGetWalletMock.mockReset();
322-
transactionUpdateMock.mockReset();
323347
dbTransactionFindUniqueMock.mockReset();
348+
dbTransactionUpdateManyMock.mockReset();
324349
getProviderMock.mockReset();
325350
addressToNetworkMock.mockReset();
326351
resolvePaymentKeyHashMock.mockReset();
@@ -340,10 +365,19 @@ beforeEach(() => {
340365

341366
createCallerMock.mockReturnValue({
342367
wallet: { getWallet: walletGetWalletMock },
343-
transaction: { updateTransaction: transactionUpdateMock },
344368
});
345369
});
346370

371+
afterEach(() => {
372+
consoleErrorSpy.mockClear();
373+
consoleWarnSpy.mockClear();
374+
});
375+
376+
afterAll(() => {
377+
consoleErrorSpy.mockRestore();
378+
consoleWarnSpy.mockRestore();
379+
});
380+
347381
describe('signTransaction API route', () => {
348382
it('updates transaction when payload is valid', async () => {
349383
const address = 'addr_test1qpl3w9v4l5qhxk778exampleaddress';
@@ -369,19 +403,27 @@ describe('signTransaction API route', () => {
369403
rejectedAddresses: [] as string[],
370404
txCbor: 'stored-tx-hex',
371405
txHash: null as string | null,
406+
txJson: '{}',
372407
};
373408

374-
dbTransactionFindUniqueMock.mockResolvedValue(transactionRecord);
375-
376409
const updatedTransaction = {
377410
...transactionRecord,
378411
signedAddresses: [address],
379412
txCbor: 'updated-tx-hex',
380413
state: 1,
381414
txHash: 'provided-hash',
415+
txJson: '{"multisig":{"state":1}}',
382416
};
383417

384-
transactionUpdateMock.mockResolvedValue(updatedTransaction);
418+
dbTransactionFindUniqueMock
419+
.mockResolvedValueOnce(transactionRecord)
420+
.mockResolvedValueOnce(updatedTransaction);
421+
422+
dbTransactionUpdateManyMock.mockResolvedValue({ count: 1 });
423+
424+
const submitTxMock = jest.fn<(txHex: string) => Promise<string>>();
425+
submitTxMock.mockResolvedValue('provided-hash');
426+
getProviderMock.mockReturnValue({ submitTx: submitTxMock });
385427

386428
const req = {
387429
method: 'POST',
@@ -411,17 +453,33 @@ describe('signTransaction API route', () => {
411453
}),
412454
});
413455
expect(walletGetWalletMock).toHaveBeenCalledWith({ walletId, address });
414-
expect(dbTransactionFindUniqueMock).toHaveBeenCalledWith({ where: { id: transactionId } });
415-
expect(res.status).toHaveBeenCalledWith(200);
416-
expect(getProviderMock).not.toHaveBeenCalled();
417-
expect(transactionUpdateMock).toHaveBeenCalledWith({
418-
transactionId,
419-
txCbor: 'updated-tx-hex',
420-
signedAddresses: [address],
421-
rejectedAddresses: [],
422-
state: 1,
423-
txHash: 'provided-hash',
456+
expect(dbTransactionFindUniqueMock).toHaveBeenNthCalledWith(1, {
457+
where: { id: transactionId },
424458
});
459+
expect(getProviderMock).toHaveBeenCalledWith(0);
460+
expect(submitTxMock).toHaveBeenCalledWith('updated-tx-hex');
461+
expect(dbTransactionUpdateManyMock).toHaveBeenCalledWith({
462+
where: {
463+
id: transactionId,
464+
signedAddresses: { equals: [] },
465+
rejectedAddresses: { equals: [] },
466+
txCbor: 'stored-tx-hex',
467+
txJson: '{}',
468+
},
469+
data: expect.objectContaining({
470+
signedAddresses: { set: [address] },
471+
rejectedAddresses: { set: [] },
472+
txCbor: 'updated-tx-hex',
473+
state: 1,
474+
txHash: 'provided-hash',
475+
txJson: expect.any(String),
476+
}),
477+
});
478+
expect(dbTransactionFindUniqueMock).toHaveBeenNthCalledWith(2, {
479+
where: { id: transactionId },
480+
});
481+
expect(consoleErrorSpy).not.toHaveBeenCalled();
482+
expect(res.status).toHaveBeenCalledWith(200);
425483
expect(res.json).toHaveBeenCalledWith({
426484
transaction: updatedTransaction,
427485
submitted: true,
@@ -587,7 +645,7 @@ describe('signTransaction API route', () => {
587645
expect(res.json).toHaveBeenCalledWith({
588646
error: 'Address has already signed this transaction',
589647
});
590-
expect(transactionUpdateMock).not.toHaveBeenCalled();
648+
expect(dbTransactionUpdateManyMock).not.toHaveBeenCalled();
591649
expect(getProviderMock).not.toHaveBeenCalled();
592650
});
593651

@@ -633,7 +691,7 @@ describe('signTransaction API route', () => {
633691
expect(res.json).toHaveBeenCalledWith({
634692
error: 'Stored transaction is missing txCbor',
635693
});
636-
expect(transactionUpdateMock).not.toHaveBeenCalled();
694+
expect(dbTransactionUpdateManyMock).not.toHaveBeenCalled();
637695
});
638696

639697
});

src/utils/swagger.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,126 @@ export const swaggerSpec = swaggerJSDoc({
259259
},
260260
},
261261
},
262+
"/api/v1/signTransaction": {
263+
post: {
264+
tags: ["V1"],
265+
summary: "Sign an existing transaction",
266+
description:
267+
"Records a witness for an existing multisig transaction and optionally submits it if the signing threshold is met.",
268+
requestBody: {
269+
required: true,
270+
content: {
271+
"application/json": {
272+
schema: {
273+
type: "object",
274+
properties: {
275+
walletId: { type: "string" },
276+
transactionId: { type: "string" },
277+
address: { type: "string" },
278+
signature: { type: "string" },
279+
key: { type: "string" },
280+
broadcast: { type: "boolean" },
281+
},
282+
required: [
283+
"walletId",
284+
"transactionId",
285+
"address",
286+
"signature",
287+
"key",
288+
],
289+
},
290+
},
291+
},
292+
},
293+
responses: {
294+
200: {
295+
description:
296+
"Witness stored. Includes updated transaction and submission status.",
297+
content: {
298+
"application/json": {
299+
schema: {
300+
type: "object",
301+
properties: {
302+
transaction: {
303+
type: "object",
304+
properties: {
305+
id: { type: "string" },
306+
walletId: { type: "string" },
307+
txJson: { type: "string" },
308+
txCbor: { type: "string" },
309+
signedAddresses: {
310+
type: "array",
311+
items: { type: "string" },
312+
},
313+
rejectedAddresses: {
314+
type: "array",
315+
items: { type: "string" },
316+
},
317+
description: { type: "string" },
318+
state: { type: "number" },
319+
txHash: { type: "string" },
320+
createdAt: { type: "string" },
321+
updatedAt: { type: "string" },
322+
},
323+
},
324+
submitted: { type: "boolean" },
325+
txHash: { type: "string", nullable: true },
326+
},
327+
},
328+
},
329+
},
330+
},
331+
401: { description: "Unauthorized or invalid signature" },
332+
403: { description: "Forbidden due to address mismatch or access" },
333+
404: { description: "Wallet or transaction not found" },
334+
409: {
335+
description:
336+
"Transaction already finalized or conflicting update detected",
337+
},
338+
502: {
339+
description:
340+
"Witness stored but submission to the network failed",
341+
content: {
342+
"application/json": {
343+
schema: {
344+
type: "object",
345+
properties: {
346+
error: { type: "string" },
347+
transaction: {
348+
type: "object",
349+
properties: {
350+
id: { type: "string" },
351+
walletId: { type: "string" },
352+
txJson: { type: "string" },
353+
txCbor: { type: "string" },
354+
signedAddresses: {
355+
type: "array",
356+
items: { type: "string" },
357+
},
358+
rejectedAddresses: {
359+
type: "array",
360+
items: { type: "string" },
361+
},
362+
description: { type: "string" },
363+
state: { type: "number" },
364+
txHash: { type: "string" },
365+
createdAt: { type: "string" },
366+
updatedAt: { type: "string" },
367+
},
368+
},
369+
submitted: { type: "boolean" },
370+
txHash: { type: "string", nullable: true },
371+
submissionError: { type: "string" },
372+
},
373+
},
374+
},
375+
},
376+
},
377+
405: { description: "Method not allowed" },
378+
500: { description: "Internal server error" },
379+
},
380+
},
381+
},
262382
"/api/v1/submitDatum": {
263383
post: {
264384
tags: ["V1"],

0 commit comments

Comments
 (0)