feat(signing-bitgo): add BitGo TSS MPC signing driver#2163
Conversation
57bc6a9 to
c990ec3
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new BitGo-backed signing provider to the Wallet Gateway, enabling asynchronous Canton transaction signing via BitGo TSS MPC custodial wallets, and wires it into the existing signing-provider framework.
Changes:
- Introduces
@canton-network/core-signing-bitgo(BitGo API handler +SigningDriverInterfaceimplementation) with tests, scripts, and README. - Extends signing-provider configuration to include
SigningProvider.BITGO. - Wires BitGo env vars and driver registration into
wallet-gateway/remote.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds BitGo SDK (and related) dependencies and bumps various packages. |
| wallet-gateway/remote/src/init.ts | Registers BitGo signing driver when BITGO_ACCESS_TOKEN is set. |
| wallet-gateway/remote/src/env.ts | Adds BitGo-related environment variable accessors. |
| wallet-gateway/remote/package.json | Adds workspace dependency on @canton-network/core-signing-bitgo. |
| core/signing-lib/src/config/schema.ts | Adds BITGO = 'bitgo' to SigningProvider enum. |
| core/signing-bitgo/vitest.config.ts | Adds Vitest config + coverage thresholds for the new package. |
| core/signing-bitgo/tsup.config.ts | Adds tsup build config for the new package. |
| core/signing-bitgo/tsconfig.json | Adds TypeScript config for the new package. |
| core/signing-bitgo/src/index.ts | Implements BitGoSigningDriver (controller methods, config get/set). |
| core/signing-bitgo/src/index.test.ts | Adds unit tests for BitGoSigningDriver. |
| core/signing-bitgo/src/bitgo.ts | Implements BitGoHandler (BitGo REST calls, tx request formatting). |
| core/signing-bitgo/src/bitgo.test.ts | Adds unit tests for BitGoHandler including state/signature extraction. |
| core/signing-bitgo/scripts/test-sign.ts | Adds a manual submit/check script for signing requests. |
| core/signing-bitgo/scripts/test-connectivity.ts | Adds a manual connectivity script (getKeys/createKey). |
| core/signing-bitgo/README.md | Documents usage, configuration, and operational behavior. |
| core/signing-bitgo/package.json | Defines the new package’s build/test setup and dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
core/signing-bitgo/README.md:26
- The README’s coin auto-detection description doesn’t match the implementation (bitgo.ts defaults to tcanton only when baseUrl includes "bitgo-test.com", otherwise canton). This can mislead users configuring a proxy/non-test URL.
| `coin` | No | Canton coin identifier. Auto-detected: `canton` for `*.bitgo.com`, `tcanton` otherwise. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
core/signing-bitgo/src/bitgo.ts:273
- formatTxRequest can return status='signed' without a signature when txReq.state maps to signed (e.g. delivered/signed) but messages[0].txHash is absent/undefined. This is inconsistent with the driver’s own intent to avoid "signed-without-signature" and can cause the gateway to stop polling without a usable signature. Consider always attempting signature extraction whenever the mapped status is 'signed', and downgrade to 'pending' if extraction fails.
const mappedStatus: SigningStatus =
messageState === 'signed'
? 'signed'
: (BITGO_STATE_TO_CANTON[txReq.state] ?? 'pending')
// Only extract when signed status comes from message-level state (txRequest still in
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
core/signing-bitgo/src/bitgo.ts:275
- formatTxRequest can return status 'signed' without a signature when txReq.state maps to 'signed' (e.g. delivered/signed) but messages[0].txHash is missing/empty. The current fallback-to-pending guard only triggers when messageState === 'signed', so callers may observe a signed tx with no signature.
// Prefer message-level state: for Canton message signing, the crypto is complete once
// messages[0].state === 'signed', even if the txRequest is still in 'pendingDelivery'.
const messageState = txReq.messages?.[0]?.state
const mappedStatus: SigningStatus =
messageState === 'signed'
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
core/signing-bitgo/src/bitgo.ts:284
formatTxRequestcan returnstatus: 'signed'without asignaturewhen BitGo reports a terminal txRequest state (delivered/signed) butmessages[0].txHashis missing or cannot be parsed. This contradicts the nearby intent comment (“prevents signed-without-signature from reaching the gateway”) and will cause runtime errors in the gateway (it treats signed-without-signature as fatal).
// Only extract when signed status comes from message-level state (txRequest still in
// pendingDelivery). When the txRequest itself is terminal (delivered/signed), trust it.
const signedFromMessage =
messageState === 'signed' && mappedStatus === 'signed'
const signedData = signedFromMessage
wallet-gateway/remote/src/init.ts:334
- The BitGo driver is registered here, but the gateway still rejects
SigningProvider.BITGOin core flows:TransactionService.sign()has a provider switch that does not include BITGO (wallet-gateway/remote/src/ledger/transaction-service.ts:70-107), andWalletAllocationServicesimilarly lacks BITGO (wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts:108-173). As a result, wallets configured withsigningProviderId: 'bitgo'will throw "Unsupported signing provider" despite the driver being available.
if (Env.BITGO_ACCESS_TOKEN()) {
drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
accessToken: Env.BITGO_ACCESS_TOKEN()!,
baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
enterpriseId: Env.BITGO_ENTERPRISE_ID(),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
core/signing-bitgo/src/bitgo.ts:286
formatTxRequestmay returnstatus: 'signed'without asignaturewhen the txRequest is terminal (state: delivered/signed) butmessages[0].state/txHashis missing (or not parseable). Since BitGo’s signature is carried inmessages[0].txHash(no dedicated signature field), this can surface a signed transaction that the gateway can’t execute. Consider extracting signed data whenevermappedStatus === 'signed', and falling back topendingif extraction fails (same protection you already have for message-level signed).
const signedFromMessage =
messageState === 'signed' && mappedStatus === 'signed'
const signedData = signedFromMessage
? this.extractSignedData(txReq.messages?.[0]?.txHash)
: {}
wallet-gateway/remote/src/init.ts:334
- The driver is registered here, but core gateway flows still treat
SigningProvider.BITGOas unsupported. For example,wallet-gateway/remote/src/ledger/transaction-service.tsthrows in thedefaultbranch ofsign()/execute()when the provider isn’t one of the existing cases (BITGO isn’t included), andwallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.tssimilarly rejects unknown providers increateWallet()/allocateParty(). As-is, selectingbitgowill fail even though the driver is instantiated.
if (Env.BITGO_ACCESS_TOKEN()) {
drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
accessToken: Env.BITGO_ACCESS_TOKEN()!,
baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
enterpriseId: Env.BITGO_ENTERPRISE_ID(),
Signed-off-by: Ravi Hegde <ravihegde348@bitgo.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
wallet-gateway/remote/src/init.ts:336
- BitGo driver is registered here, but Wallet Gateway logic still treats SigningProvider.BITGO as unsupported in key flows. For example, TransactionService.sign() throws on default for unknown providers (wallet-gateway/remote/src/ledger/transaction-service.ts:70-107) and WalletAllocationService.createWallet()/allocateParty() also throw for unsupported providers (wallet-gateway/remote/src/ledger/wallet-allocation/wallet-allocation-service.ts:108-173, 181-236). As a result, setting a wallet’s signingProviderId to "bitgo" will fail even though the driver is configured.
if (Env.BITGO_ACCESS_TOKEN()) {
drivers[SigningProvider.BITGO] = new BitGoSigningProvider({
accessToken: Env.BITGO_ACCESS_TOKEN()!,
baseUrl: Env.BITGO_API_URL('https://app.bitgo.com'),
enterpriseId: Env.BITGO_ENTERPRISE_ID(),
coin: Env.BITGO_COIN(),
})
core/signing-bitgo/src/bitgo.ts:280
- formatTxRequest can return status="signed" without a signature when the txRequest state maps to signed (e.g. delivered/signed) but messages[0].state/txHash is missing. The current signed-without-signature safeguard only applies when messageState === 'signed', not when signed status comes from txReq.state mapping.
const mappedStatus: SigningStatus =
messageState === 'signed'
? 'signed'
: (BITGO_STATE_TO_CANTON[txReq.state] ?? 'pending')
// Only extract when signed status comes from message-level state (txRequest still in
Summary
Adds a new signing driver (@canton-network/core-signing-bitgo) that integrates BitGo's TSS MPC custodial wallets as a Canton signing provider.
Changes
Design notes for reviewers
Known limitations
Test plan