From c2638e08308283f9a32b4efe78520472276d991f Mon Sep 17 00:00:00 2001 From: Lokesh Chandra Date: Fri, 3 Oct 2025 18:14:21 +0530 Subject: [PATCH] feat(express): migrate coinSignTx to typed routes Ticket: WP-5422 --- modules/express/package.json | 2 +- modules/express/src/clientRoutes.ts | 4 +- modules/express/src/typedRoutes/api/index.ts | 4 + .../src/typedRoutes/api/v2/coinSignTx.ts | 150 +++++++ .../test/unit/typedRoutes/coinSignTx.ts | 374 ++++++++++++++++++ modules/sdk-core/package.json | 2 +- 6 files changed, 532 insertions(+), 4 deletions(-) create mode 100644 modules/express/src/typedRoutes/api/v2/coinSignTx.ts create mode 100644 modules/express/test/unit/typedRoutes/coinSignTx.ts diff --git a/modules/express/package.json b/modules/express/package.json index a0c37ad41d..1e2b7094ee 100644 --- a/modules/express/package.json +++ b/modules/express/package.json @@ -58,7 +58,7 @@ "superagent": "^9.0.1" }, "devDependencies": { - "@bitgo/public-types": "5.22.0", + "@bitgo/public-types": "5.29.0", "@bitgo/sdk-lib-mpc": "^10.7.0", "@bitgo/sdk-test": "^9.1.2", "@types/argparse": "^1.0.36", diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index f6433eb73b..ebbeb8dc03 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -724,7 +724,7 @@ async function handleV2SignTxWallet(req: express.Request) { * handle sign transaction * @param req */ -async function handleV2SignTx(req: express.Request) { +async function handleV2SignTx(req: ExpressApiRouteRequest<'express.v2.coin.signtx', 'post'>) { const bitgo = req.bitgo; const coin = bitgo.coin(req.params.coin); try { @@ -1632,7 +1632,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void { router.post('express.ofc.signPayload', [prepareBitGo(config), typedPromiseWrapper(handleV2OFCSignPayload)]); // sign transaction - app.post('/api/v2/:coin/signtx', parseBody, prepareBitGo(config), promiseWrapper(handleV2SignTx)); + router.post('express.v2.coin.signtx', [prepareBitGo(config), typedPromiseWrapper(handleV2SignTx)]); app.post('/api/v2/:coin/wallet/:id/signtx', parseBody, prepareBitGo(config), promiseWrapper(handleV2SignTxWallet)); app.post( '/api/v2/:coin/wallet/:id/signtxtss', diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index b4db56f42d..2d7da518f6 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -26,6 +26,7 @@ import { PostCreateAddress } from './v2/createAddress'; import { PutFanoutUnspents } from './v1/fanoutUnspents'; import { PostOfcSignPayload } from './v2/ofcSignPayload'; import { PostWalletRecoverToken } from './v2/walletRecoverToken'; +import { PostCoinSignTx } from './v2/coinSignTx'; export const ExpressApi = apiSpec({ 'express.ping': { @@ -100,6 +101,9 @@ export const ExpressApi = apiSpec({ 'express.v2.wallet.recovertoken': { post: PostWalletRecoverToken, }, + 'express.v2.coin.signtx': { + post: PostCoinSignTx, + }, }); export type ExpressApi = typeof ExpressApi; diff --git a/modules/express/src/typedRoutes/api/v2/coinSignTx.ts b/modules/express/src/typedRoutes/api/v2/coinSignTx.ts new file mode 100644 index 0000000000..b6e15079e2 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/coinSignTx.ts @@ -0,0 +1,150 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { TransactionRequest as TxRequestResponse } from '@bitgo/public-types'; +import { BitgoExpressError } from '../../schemas/error'; + +/** + * Request parameters for signing a transaction + */ +export const CoinSignTxParams = { + /** The coin type */ + coin: t.string, +} as const; + +/** + * Transaction prebuild information + */ +export const TransactionPrebuild = t.partial({ + /** Transaction in hex format */ + txHex: t.string, + /** Transaction in base64 format (for some coins) */ + txBase64: t.string, + /** Transaction in JSON format (for some coins) */ + txInfo: t.any, + /** Wallet ID for the transaction */ + walletId: t.string, + /** Next contract sequence ID (for ETH) */ + nextContractSequenceId: t.number, + /** Whether this is a batch transaction (for ETH) */ + isBatch: t.boolean, + /** EIP1559 transaction parameters (for ETH) */ + eip1559: t.any, + /** Hop transaction data (for ETH) */ + hopTransaction: t.any, + /** Backup key nonce (for ETH) */ + backupKeyNonce: t.any, + /** Recipients of the transaction */ + recipients: t.any, +}); + +/** + * Request body for signing a transaction + */ +export const CoinSignTxBody = { + /** Private key for signing */ + prv: optional(t.string), + /** Transaction prebuild data */ + txPrebuild: optional(TransactionPrebuild), + /** Whether this is the last signature in a multi-sig tx */ + isLastSignature: optional(t.boolean), + /** Gas limit for ETH transactions */ + gasLimit: optional(t.union([t.string, t.number])), + /** Gas price for ETH transactions */ + gasPrice: optional(t.union([t.string, t.number])), + /** Transaction expiration time */ + expireTime: optional(t.number), + /** Sequence ID for transactions */ + sequenceId: optional(t.number), + /** Public keys for multi-signature transactions */ + pubKeys: optional(t.array(t.string)), + /** For EVM cross-chain recovery */ + isEvmBasedCrossChainRecovery: optional(t.boolean), + /** Recipients of the transaction */ + recipients: optional(t.any), + /** Custodian transaction ID */ + custodianTransactionId: optional(t.string), + /** Signing step for MuSig2 */ + signingStep: optional(t.union([t.literal('signerNonce'), t.literal('signerSignature'), t.literal('cosignerNonce')])), + /** Allow non-segwit signing without previous transaction */ + allowNonSegwitSigningWithoutPrevTx: optional(t.boolean), +} as const; + +/** + * Response for a fully signed transaction + */ +export const FullySignedTransactionResponse = t.type({ + /** Transaction in hex format */ + txHex: t.string, +}); + +/** + * Response for a half-signed account transaction + */ +export const HalfSignedAccountTransactionResponse = t.type({ + halfSigned: t.partial({ + txHex: optional(t.string), + payload: optional(t.string), + txBase64: optional(t.string), + }), +}); + +/** + * Response for a half-signed UTXO transaction + */ +export const HalfSignedUtxoTransactionResponse = t.type({ + txHex: t.string, +}); + +/** + * Response for a transaction request + */ +export const SignedTransactionRequestResponse = t.type({ + txRequestId: t.string, +}); + +/** + * Response for signing a transaction + * + * Uses TxRequestResponse (TransactionRequest) from @bitgo/public-types for TSS transaction requests + * (supports both Lite and Full versions) + */ +export const CoinSignTxResponse = { + /** Successfully signed transaction */ + 200: t.union([ + FullySignedTransactionResponse, + HalfSignedAccountTransactionResponse, + HalfSignedUtxoTransactionResponse, + SignedTransactionRequestResponse, + TxRequestResponse, + ]), + /** Error response */ + 400: BitgoExpressError, +}; + +/** + * Sign a transaction for a specific coin + * + * This endpoint signs a transaction for a specific coin type. + * The request body is passed directly to coin.signTransaction() and varies by coin. + * Common fields include: + * - txPrebuild: Contains transaction data like txHex or txBase64 + * - prv: Private key for signing + * - isLastSignature: Whether this is the last signature in a multi-sig tx + * - gasLimit: Gas limit for ETH transactions + * - gasPrice: Gas price for ETH transactions + * - expireTime: Transaction expiration time + * - sequenceId: Sequence ID for transactions + * - pubKeys: Public keys for multi-signature transactions + * - isEvmBasedCrossChainRecovery: For EVM cross-chain recovery + * + * @operationId express.v2.coin.signtx + */ +export const PostCoinSignTx = httpRoute({ + path: '/api/v2/:coin/signtx', + method: 'POST', + request: httpRequest({ + params: CoinSignTxParams, + body: CoinSignTxBody, + }), + response: CoinSignTxResponse, +}); diff --git a/modules/express/test/unit/typedRoutes/coinSignTx.ts b/modules/express/test/unit/typedRoutes/coinSignTx.ts new file mode 100644 index 0000000000..b81faed62d --- /dev/null +++ b/modules/express/test/unit/typedRoutes/coinSignTx.ts @@ -0,0 +1,374 @@ +import * as assert from 'assert'; +import * as t from 'io-ts'; +import { TransactionRequest as TxRequestResponse } from '@bitgo/public-types'; +import { + CoinSignTxParams, + TransactionPrebuild, + CoinSignTxBody, + FullySignedTransactionResponse, + HalfSignedAccountTransactionResponse, + HalfSignedUtxoTransactionResponse, + SignedTransactionRequestResponse, + PostCoinSignTx, +} from '../../../src/typedRoutes/api/v2/coinSignTx'; +import { assertDecode } from './common'; + +describe('CoinSignTx codec tests', function () { + describe('CoinSignTxParams', function () { + it('should validate params with required coin', function () { + const validParams = { + coin: 'btc', + }; + + const decoded = assertDecode(t.type(CoinSignTxParams), validParams); + assert.strictEqual(decoded.coin, validParams.coin); + }); + + it('should reject params with missing coin', function () { + const invalidParams = {}; + + assert.throws(() => { + assertDecode(t.type(CoinSignTxParams), invalidParams); + }); + }); + + it('should reject params with non-string coin', function () { + const invalidParams = { + coin: 123, // number instead of string + }; + + assert.throws(() => { + assertDecode(t.type(CoinSignTxParams), invalidParams); + }); + }); + }); + + describe('TransactionPrebuild', function () { + it('should validate prebuild with all fields', function () { + const validPrebuild = { + txHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + txBase64: + 'AQAAAAFz2JT3Xvjk8jKcYcMrKR8tPMRm5+/Q6J2sMgtz7QDpAAAAAAD+////AoCWmAAAAAAAGXapFJA29QPQaHHwR3Uriuhw2A6tHkPgiKwAAAAAAAEBH9cQ2QAAAAAAAXapFCf/zr8zPrMftHGIRsOt0Cf+wdOyiKwA', + txInfo: { + inputs: [{ address: '1abc', value: 100000 }], + outputs: [{ address: '1xyz', value: 95000 }], + }, + walletId: '5a1341e7c8421dc90710673b3166bbd5', + nextContractSequenceId: 123, + isBatch: true, + eip1559: { + maxPriorityFeePerGas: '10000000000', + maxFeePerGas: '20000000000', + }, + hopTransaction: { + txHex: '0x123456', + gasPrice: '20000000000', + }, + backupKeyNonce: 42, + recipients: [ + { address: '1abc', amount: 100000 }, + { address: '1xyz', amount: 95000 }, + ], + }; + + const decoded = assertDecode(TransactionPrebuild, validPrebuild); + assert.strictEqual(decoded.txHex, validPrebuild.txHex); + assert.strictEqual(decoded.txBase64, validPrebuild.txBase64); + assert.deepStrictEqual(decoded.txInfo, validPrebuild.txInfo); + assert.strictEqual(decoded.walletId, validPrebuild.walletId); + assert.strictEqual(decoded.nextContractSequenceId, validPrebuild.nextContractSequenceId); + assert.strictEqual(decoded.isBatch, validPrebuild.isBatch); + assert.deepStrictEqual(decoded.eip1559, validPrebuild.eip1559); + assert.deepStrictEqual(decoded.hopTransaction, validPrebuild.hopTransaction); + assert.strictEqual(decoded.backupKeyNonce, validPrebuild.backupKeyNonce); + assert.deepStrictEqual(decoded.recipients, validPrebuild.recipients); + }); + + it('should validate empty prebuild', function () { + const validPrebuild = {}; + const decoded = assertDecode(TransactionPrebuild, validPrebuild); + assert.deepStrictEqual(decoded, {}); + }); + + it('should reject prebuild with invalid field types', function () { + const invalidPrebuild = { + txHex: 123, // number instead of string + isBatch: 'true', // string instead of boolean + nextContractSequenceId: '123', // string instead of number + }; + + assert.throws(() => { + assertDecode(TransactionPrebuild, invalidPrebuild); + }); + }); + }); + + describe('CoinSignTxBody', function () { + it('should validate body with all fields', function () { + const validBody = { + prv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2', + txPrebuild: { + txHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + }, + isLastSignature: true, + gasLimit: 21000, + gasPrice: '20000000000', + expireTime: 1633046400000, + sequenceId: 42, + pubKeys: [ + '03a247b2c6826c3f833c6e164a3be1b124bf5f6de0d837a143a4d81e427a43a26f', + '02d3a8e9a42b89168a54f09476d40b8d60f5d553f6dcc8e5bf3e8b2733cff25c92', + ], + isEvmBasedCrossChainRecovery: true, + recipients: [ + { address: '1abc', amount: 100000 }, + { address: '1xyz', amount: 95000 }, + ], + custodianTransactionId: 'custodian-tx-123456', + signingStep: 'signerNonce', + allowNonSegwitSigningWithoutPrevTx: true, + }; + + const decoded = assertDecode(t.partial(CoinSignTxBody), validBody); + assert.strictEqual(decoded.prv, validBody.prv); + assert.deepStrictEqual(decoded.txPrebuild, validBody.txPrebuild); + assert.strictEqual(decoded.isLastSignature, validBody.isLastSignature); + assert.strictEqual(decoded.gasLimit, validBody.gasLimit); + assert.strictEqual(decoded.gasPrice, validBody.gasPrice); + assert.strictEqual(decoded.expireTime, validBody.expireTime); + assert.strictEqual(decoded.sequenceId, validBody.sequenceId); + assert.deepStrictEqual(decoded.pubKeys, validBody.pubKeys); + assert.strictEqual(decoded.isEvmBasedCrossChainRecovery, validBody.isEvmBasedCrossChainRecovery); + assert.deepStrictEqual(decoded.recipients, validBody.recipients); + assert.strictEqual(decoded.custodianTransactionId, validBody.custodianTransactionId); + assert.strictEqual(decoded.signingStep, validBody.signingStep); + assert.strictEqual(decoded.allowNonSegwitSigningWithoutPrevTx, validBody.allowNonSegwitSigningWithoutPrevTx); + }); + + it('should validate empty body', function () { + const validBody = {}; + const decoded = assertDecode(t.partial(CoinSignTxBody), validBody); + assert.deepStrictEqual(decoded, {}); + }); + + it('should validate body with gasLimit and gasPrice as different types', function () { + const validBody = { + gasLimit: 21000, // as number + gasPrice: '20000000000', // as string + }; + + const decoded = assertDecode(t.partial(CoinSignTxBody), validBody); + assert.strictEqual(decoded.gasLimit, validBody.gasLimit); + assert.strictEqual(decoded.gasPrice, validBody.gasPrice); + }); + + it('should reject body with invalid field types', function () { + const invalidBody = { + prv: 123, // number instead of string + isLastSignature: 'true', // string instead of boolean + expireTime: '1633046400000', // string instead of number + signingStep: 'invalidStep', // not one of the allowed values + }; + + assert.throws(() => { + assertDecode(t.partial(CoinSignTxBody), invalidBody); + }); + }); + }); + + describe('Response Types', function () { + describe('FullySignedTransactionResponse', function () { + it('should validate response with required txHex', function () { + const validResponse = { + txHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + }; + + const decoded = assertDecode(FullySignedTransactionResponse, validResponse); + assert.strictEqual(decoded.txHex, validResponse.txHex); + }); + + it('should reject response with missing txHex', function () { + const invalidResponse = {}; + + assert.throws(() => { + assertDecode(FullySignedTransactionResponse, invalidResponse); + }); + }); + }); + + describe('HalfSignedAccountTransactionResponse', function () { + it('should validate response with all halfSigned fields', function () { + const validResponse = { + halfSigned: { + txHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + payload: '{"serializedTx":"0x123456","signature":"0xabcdef"}', + txBase64: + 'AQAAAAFz2JT3Xvjk8jKcYcMrKR8tPMRm5+/Q6J2sMgtz7QDpAAAAAAD+////AoCWmAAAAAAAGXapFJA29QPQaHHwR3Uriuhw2A6tHkPgiKwAAAAAAAEBH9cQ2QAAAAAAAXapFCf/zr8zPrMftHGIRsOt0Cf+wdOyiKwA', + }, + }; + + const decoded = assertDecode(HalfSignedAccountTransactionResponse, validResponse); + assert.strictEqual(decoded.halfSigned.txHex, validResponse.halfSigned.txHex); + assert.strictEqual(decoded.halfSigned.payload, validResponse.halfSigned.payload); + assert.strictEqual(decoded.halfSigned.txBase64, validResponse.halfSigned.txBase64); + }); + + it('should validate response with empty halfSigned', function () { + const validResponse = { + halfSigned: {}, + }; + + const decoded = assertDecode(HalfSignedAccountTransactionResponse, validResponse); + assert.deepStrictEqual(decoded.halfSigned, {}); + }); + }); + + describe('HalfSignedUtxoTransactionResponse', function () { + it('should validate response with required txHex', function () { + const validResponse = { + txHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + }; + + const decoded = assertDecode(HalfSignedUtxoTransactionResponse, validResponse); + assert.strictEqual(decoded.txHex, validResponse.txHex); + }); + + it('should reject response with missing txHex', function () { + const invalidResponse = {}; + + assert.throws(() => { + assertDecode(HalfSignedUtxoTransactionResponse, invalidResponse); + }); + }); + }); + + describe('SignedTransactionRequestResponse', function () { + it('should validate response with required txRequestId', function () { + const validResponse = { + txRequestId: '5a1341e7c8421dc90710673b3166bbd5', + }; + + const decoded = assertDecode(SignedTransactionRequestResponse, validResponse); + assert.strictEqual(decoded.txRequestId, validResponse.txRequestId); + }); + + it('should reject response with missing txRequestId', function () { + const invalidResponse = {}; + + assert.throws(() => { + assertDecode(SignedTransactionRequestResponse, invalidResponse); + }); + }); + }); + + describe('TxRequestResponse', function () { + it('should validate response with all required fields', function () { + const validResponse = { + txRequestId: '5a1341e7c8421dc90710673b3166bbd5', + walletId: '5a1341e7c8421dc90710673b3166bbd5', + version: 1, + state: 'pendingApproval', + date: '2023-01-01T00:00:00.000Z', + createdDate: '2023-01-01T00:00:00.000Z', + userId: '5a1341e7c8421dc90710673b3166bbd5', + initiatedBy: '5a1341e7c8421dc90710673b3166bbd5', + updatedBy: '5a1341e7c8421dc90710673b3166bbd5', + intents: [], + latest: true, + }; + + const decoded = assertDecode(TxRequestResponse, validResponse); + assert.strictEqual(decoded.txRequestId, validResponse.txRequestId); + assert.strictEqual(decoded.walletId, validResponse.walletId); + assert.strictEqual(decoded.version, validResponse.version); + assert.strictEqual(decoded.state, validResponse.state); + assert.strictEqual(decoded.userId, validResponse.userId); + assert.strictEqual(decoded.initiatedBy, validResponse.initiatedBy); + assert.strictEqual(decoded.updatedBy, validResponse.updatedBy); + assert.deepStrictEqual(decoded.intents, validResponse.intents); + assert.strictEqual(decoded.latest, validResponse.latest); + }); + + it('should validate response with optional fields (Lite version)', function () { + const validResponse = { + txRequestId: '5a1341e7c8421dc90710673b3166bbd5', + walletId: '5a1341e7c8421dc90710673b3166bbd5', + walletType: 'hot', + version: 1, + state: 'pendingApproval', + date: '2023-01-01T00:00:00.000Z', + createdDate: '2023-01-01T00:00:00.000Z', + userId: '5a1341e7c8421dc90710673b3166bbd5', + initiatedBy: '5a1341e7c8421dc90710673b3166bbd5', + updatedBy: '5a1341e7c8421dc90710673b3166bbd5', + intents: [], + enterpriseId: '5a1341e7c8421dc90710673b3166bbd5', + intent: {}, + pendingApprovalId: '5a1341e7c8421dc90710673b3166bbd5', + policiesChecked: true, + signatureShares: [ + { + from: 'user', + to: 'bitgo', + share: 'abc123', + }, + ], + commitmentShares: [ + { + from: 'user', + to: 'bitgo', + share: 'abc123', + type: 'commitment', + }, + ], + txHashes: ['hash1', 'hash2'], + unsignedTxs: [ + { + serializedTxHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + signableHex: + '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', + derivationPath: "m/44'/0'/0'/0/0", + }, + ], + apiVersion: 'lite', + latest: true, + }; + + const decoded = assertDecode(TxRequestResponse, validResponse); + assert.strictEqual(decoded.txRequestId, validResponse.txRequestId); + assert.strictEqual(decoded.walletId, validResponse.walletId); + assert.strictEqual(decoded.version, validResponse.version); + assert.strictEqual(decoded.state, validResponse.state); + assert.strictEqual(decoded.userId, validResponse.userId); + assert.strictEqual(decoded.initiatedBy, validResponse.initiatedBy); + assert.strictEqual(decoded.updatedBy, validResponse.updatedBy); + assert.strictEqual(decoded.latest, validResponse.latest); + }); + }); + }); + + describe('PostCoinSignTx route definition', function () { + it('should have the correct path', function () { + assert.strictEqual(PostCoinSignTx.path, '/api/v2/:coin/signtx'); + }); + + it('should have the correct HTTP method', function () { + assert.strictEqual(PostCoinSignTx.method, 'POST'); + }); + + it('should have the correct request configuration', function () { + assert.ok(PostCoinSignTx.request); + }); + + it('should have the correct response types', function () { + assert.ok(PostCoinSignTx.response[200]); + assert.ok(PostCoinSignTx.response[400]); + }); + }); +}); diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index e39cc7b497..a562bdd228 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -40,7 +40,7 @@ ] }, "dependencies": { - "@bitgo/public-types": "5.22.0", + "@bitgo/public-types": "5.29.0", "@bitgo/sdk-lib-mpc": "^10.7.0", "@bitgo/secp256k1": "^1.5.0", "@bitgo/sjcl": "^1.0.1",