feat: sign eddsa recovery transaction#65
Conversation
de845fb to
cbe8005
Compare
cbe8005 to
3a3ea85
Compare
There was a problem hiding this comment.
Pull Request Overview
This PR adds support for EdDSA recovery transactions, introducing a new TSS recovery flow alongside the existing multi-signature recovery mechanism. The changes enable signing recovery transactions for EdDSA-based coins (Solana, Near, Sui, Ada, Dot) through a new MPC endpoint.
- Implements EdDSA recovery transaction signing functionality with TSS keyshare support
- Adds new API endpoint
/api/{coin}/mpc/recoveryfor EdDSA recovery operations - Restructures existing recovery API to support both TSS and traditional multi-signature recovery modes
Reviewed Changes
Copilot reviewed 12 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/coinUtils.ts | Adds utility functions for Solana coin detection and EdDSA algorithm checking |
| src/enclavedBitgoExpress/routers/enclavedApiSpec.ts | Defines new MPC recovery endpoint with request/response types and handler routing |
| src/api/master/routers/masterApiSpec.ts | Updates recovery wallet API structure to support both TSS and multi-signature recovery parameters |
| src/api/master/handlers/recoveryWallet.ts | Implements dual recovery flow logic handling both EdDSA TSS and traditional multi-signature recovery |
| src/api/master/handlers/recoverEddsaWallets.ts | New module for coin-specific EdDSA wallet recovery with dynamic imports |
| src/api/master/clients/enclavedExpressClient.ts | Adds MPC recovery client method with transaction format handling |
| src/api/enclaved/mpcFinalize.ts | Updates signing material structure for user vs backup key differentiation |
| src/api/enclaved/handlers/signEddsaRecoveryTransaction.ts | New handler implementing EdDSA signature generation with TSS support |
| src/tests/api/master/recoveryWallet.test.ts | Updates test structure to match new API parameter organization |
| src/tests/api/master/musigRecovery.test.ts | Updates test cases and adds TSS recovery validation test |
| package.json | Adds EdDSA coin SDK dependencies and version resolution |
| masterBitgoExpress.json | Updates OpenAPI specification for new recovery API structure |
| recoveryDestination, | ||
| apiKey: params.apiKey, | ||
| }); | ||
| console.log('Unsigned sweep tx'); |
There was a problem hiding this comment.
Console.log statements should not be used in production code. Consider using a proper logging framework or removing this debug output.
| apiKey: params.apiKey, | ||
| }); | ||
| console.log('Unsigned sweep tx'); | ||
| console.log(JSON.stringify(unsignedSweepPrebuildTx, null, 2)); |
There was a problem hiding this comment.
Console.log statements should not be used in production code. Consider using a proper logging framework or removing this debug output.
|
|
||
| return { txBuilder, publicKey }; | ||
| } catch (error) { | ||
| console.error(error); |
There was a problem hiding this comment.
Console.error should be replaced with a proper logging framework for consistency with the rest of the codebase.
| console.error(error); | |
| logger.error(error); |
| throw new Error('Invalid user public key format'); | ||
| } else if (!sdkCoin.isValidPub(backupPub)) { | ||
| throw new Error('Invalid backup public'); | ||
| throw new Error('Invalid backup public key format'); |
There was a problem hiding this comment.
The error message should be 'Invalid backup public key format' to be consistent with the user key error message format.
| userPub: string; | ||
| backupPub: string; | ||
| apiKey: string; | ||
| coinSpecificParams: any; |
There was a problem hiding this comment.
Using 'any' type for coinSpecificParams reduces type safety. Consider defining a proper interface or union type for coin-specific parameters.
| coinSpecificParams: any; | |
| coinSpecificParams: CoinSpecificParams; |
| coinFamily: CoinFamily, | ||
| signableHex: string, | ||
| accountId: string, | ||
| ): Promise<{ txBuilder: any; publicKey: string }> { |
There was a problem hiding this comment.
Using 'any' type for txBuilder reduces type safety. Consider defining a proper interface or union type for transaction builders.
pranavjain97
left a comment
There was a problem hiding this comment.
Nice work! Some comments
| 'v1.mpc.recovery': { | ||
| post: httpRoute({ | ||
| method: 'POST', | ||
| path: `/api/{coin}/mpc/recovery`, | ||
| request: httpRequest({ | ||
| params: { | ||
| coin: t.string, | ||
| }, | ||
| body: RecoveryEdDSARequest, |
There was a problem hiding this comment.
why is the body specific to EDDSA?
| }), | ||
| }; | ||
|
|
||
| const RecoveryEdDSARequest = { |
There was a problem hiding this comment.
woudn't it be similar for ECDSA?
| const { recoveryDestination, userKey } = commonRecoveryParams; | ||
| try { | ||
| const unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, { | ||
| bitgoKey: userKey, |
There was a problem hiding this comment.
I would change userKey to call it commonKeychain so it isnt misleading
There was a problem hiding this comment.
lol that's how they decided to name the field on the sdk. Would just mean more transforming of the parameters. I can do it in a follow up.
| if ('txRequests' in tx && Array.isArray(tx.txRequests)) { | ||
| // MPCTxs format | ||
| const firstRequest = tx.txRequests[0]; | ||
| if (firstRequest && firstRequest.transactions && firstRequest.transactions[0]) { | ||
| const firstTx = firstRequest.transactions[0]; | ||
| txRequest.signableHex = firstTx.unsignedTx?.serializedTx || ''; | ||
| txRequest.derivationPath = firstTx.unsignedTx?.derivationPath || ''; | ||
| } | ||
| } else if ('signableHex' in tx) { | ||
| // MPCTx format | ||
| txRequest.signableHex = tx.signableHex || ''; | ||
| txRequest.derivationPath = tx.derivationPath || ''; | ||
| } else if (Array.isArray(tx) && tx.length > 0) { | ||
| // MPCSweepTxs format | ||
| const firstTx = tx[0]; | ||
| if (firstTx && firstTx.transactions && firstTx.transactions[0]) { | ||
| const transaction = firstTx.transactions[0]; | ||
| txRequest.signableHex = transaction.unsignedTx?.signableHex || ''; | ||
| txRequest.derivationPath = transaction.unsignedTx?.derivationPath || ''; | ||
| } | ||
| } |
There was a problem hiding this comment.
Could you add comments in which scenario are which formats used? I'm surprised it differs so much just between MPC.
There was a problem hiding this comment.
Let's move this to a helper. Like prepareUnsignedSweepTxRequest
There was a problem hiding this comment.
The client is already too big; we need to start modularizing it.
There was a problem hiding this comment.
can do it in a follow up.
| txRequest.signableHex = tx.signableHex || ''; | ||
| txRequest.derivationPath = tx.derivationPath || ''; | ||
| } else if (Array.isArray(tx) && tx.length > 0) { | ||
| // MPCSweepTxs format |
There was a problem hiding this comment.
arent the above two sweeps as well?
There was a problem hiding this comment.
they are, just different transaction formats that are returned by recovery
| const hdTree = await Ed25519Bip32HdTree.initialize(); | ||
| const MPC = await Eddsa.initialize(hdTree); | ||
|
|
||
| const accountId = MPC.deriveUnhardened(request.commonKeychain, request.derivationPath).slice( |
There was a problem hiding this comment.
what is this accountId?
There was a problem hiding this comment.
https://github.com/BitGo/BitGoJS/blob/52c248404bdfadf4b285f46e9585909446824f33/modules/sdk-coin-sol/src/sol.ts#L742 something like a public key I guess?
| accountId, | ||
| ); | ||
|
|
||
| const txBuilder = builder; |
There was a problem hiding this comment.
nit: prolly not needed
ac800dc to
ff6ecf8
Compare
ff6ecf8 to
88eac6b
Compare
No description provided.