From 8d2054d3489ab5c29955afb7f085c98127665017 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Thu, 17 Jul 2025 18:52:51 -0400 Subject: [PATCH] feat(mbe): allow fillNonce sendman tx's Ticket: WP-5234 --- masterBitgoExpress.json | 222 ++++++++++++++-------- src/__tests__/api/master/sendMany.test.ts | 75 ++++++++ src/api/master/routers/masterApiSpec.ts | 4 +- 3 files changed, 218 insertions(+), 83 deletions(-) diff --git a/masterBitgoExpress.json b/masterBitgoExpress.json index 186dfc2..045f791 100644 --- a/masterBitgoExpress.json +++ b/masterBitgoExpress.json @@ -100,19 +100,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -193,7 +181,9 @@ "description": "Bad Request", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, @@ -202,19 +192,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -355,7 +333,9 @@ "description": "Bad Request", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, @@ -364,19 +344,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -417,6 +385,7 @@ "type": "string", "enum": [ "transfer", + "fillNonce", "acceleration", "accountSet", "enabletoken", @@ -532,11 +501,13 @@ }, "custodianTransactionId": { "type": "string" + }, + "nonce": { + "type": "string" } }, "required": [ - "source", - "recipients" + "source" ] } } @@ -556,19 +527,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -642,19 +601,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -723,19 +670,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "details": { - "type": "string" - } - }, - "required": [ - "error", - "details" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -939,6 +874,113 @@ } } }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/{coin}/wallet/recoveryconsolidations": { + "post": { + "parameters": [ + { + "name": "coin", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userPub": { + "type": "string" + }, + "backupPub": { + "type": "string" + }, + "bitgoPub": { + "type": "string" + }, + "multisigType": { + "type": "string", + "enum": [ + "onchain", + "tss" + ] + }, + "commonKeychain": { + "type": "string" + }, + "tokenContractAddress": { + "type": "string" + }, + "startingScanIndex": { + "type": "number" + }, + "endingScanIndex": { + "type": "number" + }, + "apiKey": { + "type": "string" + }, + "durableNonces": { + "type": "object", + "properties": { + "publicKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "secretKey": { + "type": "string" + } + }, + "required": [ + "publicKeys", + "secretKey" + ] + } + }, + "required": [ + "multisigType" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {} + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -1122,6 +1164,22 @@ "status", "timestamp" ] + }, + "ErrorResponse": { + "title": "ErrorResponse", + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "details": { + "type": "string" + } + }, + "required": [ + "error", + "details" + ] } } } diff --git a/src/__tests__/api/master/sendMany.test.ts b/src/__tests__/api/master/sendMany.test.ts index d6e273f..4c32410 100644 --- a/src/__tests__/api/master/sendMany.test.ts +++ b/src/__tests__/api/master/sendMany.test.ts @@ -394,6 +394,81 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => { sinon.assert.calledOnce(multisigTypeStub); }); + it('should be able to sign a fill nonce transaction', async () => { + // Mock wallet get request for TSS wallet + const walletGetNock = nock(bitgoApiUrl) + .get(`/api/v2/${coin}/wallet/${walletId}`) + .matchHeader('any', () => true) + .reply(200, { + id: walletId, + type: 'cold', + subType: 'onPrem', + keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + multisigType: 'tss', + }); + + // Mock keychain get request for TSS keychain + const keychainGetNock = nock(bitgoApiUrl) + .get(`/api/v2/${coin}/key/user-key-id`) + .matchHeader('any', () => true) + .reply(200, { + id: 'user-key-id', + pub: 'xpub_user', + commonKeychain: 'test-common-keychain', + source: 'user', + type: 'tss', + }); + + const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ + txRequest: { + txRequestId: 'test-tx-request-id', + state: 'signed', + apiVersion: 'full', + pendingApprovalId: 'test-pending-approval-id', + transactions: [ + { + state: 'signed', + unsignedTx: { + derivationPath: 'm/0', + signableHex: 'testMessage', + serializedTxHex: 'testSerializedTxHex', + }, + signatureShares: [], + signedTx: { + id: 'test-tx-id', + tx: 'signed-transaction', + }, + }, + ], + }, + txid: 'test-tx-id', + tx: 'signed-transaction', + }); + + // Mock multisigType to return 'tss' + const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); + + const response = await agent + .post(`/api/${coin}/wallet/${walletId}/sendMany`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + type: 'fillNonce', + nonce: '2', + source: 'user', + pubkey: 'xpub_user', + }); + + response.status.should.equal(200); + response.body.should.have.property('txRequest'); + response.body.should.have.property('txid', 'test-tx-id'); + response.body.should.have.property('tx', 'signed-transaction'); + + walletGetNock.done(); + keychainGetNock.done(); + sinon.assert.calledOnce(sendManyStub); + sinon.assert.calledOnce(multisigTypeStub); + }); + it('should fail when backup key is used for ECDSA TSS signing', async () => { // Mock wallet get request for TSS wallet const walletGetNock = nock(bitgoApiUrl) diff --git a/src/api/master/routers/masterApiSpec.ts b/src/api/master/routers/masterApiSpec.ts index 2451b97..fb16d10 100644 --- a/src/api/master/routers/masterApiSpec.ts +++ b/src/api/master/routers/masterApiSpec.ts @@ -116,6 +116,7 @@ export const SendManyRequest = { type: t.union([ t.undefined, t.literal('transfer'), + t.literal('fillNonce'), t.literal('acceleration'), t.literal('accountSet'), t.literal('enabletoken'), @@ -126,7 +127,7 @@ export const SendManyRequest = { ]), commonKeychain: t.union([t.undefined, t.string]), source: t.union([t.literal('user'), t.literal('backup')]), - recipients: t.array(t.any), + recipients: t.union([t.undefined, t.array(t.any)]), numBlocks: t.union([t.undefined, t.number]), feeRate: t.union([t.undefined, t.number]), feeMultiplier: t.union([t.undefined, t.number]), @@ -153,6 +154,7 @@ export const SendManyRequest = { eip1559: t.union([t.undefined, t.any]), gasLimit: t.union([t.undefined, t.number]), custodianTransactionId: t.union([t.undefined, t.string]), + nonce: t.union([t.undefined, t.string]), }; export const SendManyResponse: HttpResponse = {