-
Notifications
You must be signed in to change notification settings - Fork 302
feat: implement BitGo signing in SDK #8624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ded3b89
c5a372d
5045e63
7c16a2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -403,6 +403,15 @@ function getWalletPwFromEnv(walletId: string): string { | |
| return walletPw; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the wallet passphrase from the environment, or undefined if not set. | ||
| * Unlike getWalletPwFromEnv, this does not throw when the env variable is absent. | ||
| * Use this when the passphrase is optional (e.g. KMS-backed wallets). | ||
| */ | ||
| function findWalletPwFromEnv(walletId: string): string | undefined { | ||
| return process.env[`WALLET_${walletId}_PASSPHRASE`]; | ||
| } | ||
|
|
||
| async function getEncryptedPrivKey(path: string, walletId: string): Promise<string> { | ||
| const privKeyFile = await fs.readFile(path, { encoding: 'utf8' }); | ||
| const encryptedPrivKey = JSON.parse(privKeyFile); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to zahin's thread: collapse
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will update it in a follow up PR |
||
|
|
@@ -629,7 +638,9 @@ export async function handleV2OFCSignPayload( | |
| throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404); | ||
| } | ||
|
|
||
| const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id()); | ||
| // Prefer the passphrase from the request body; fall back to the env var. | ||
| // If neither is present, pass undefined — signPayload() routes to KMS internally. | ||
| const walletPassphrase = bodyWalletPassphrase ?? findWalletPwFromEnv(wallet.id()); | ||
| const tradingAccount = wallet.toTradingAccount(); | ||
| const stringifiedPayload = typeof payload === 'string' ? payload : JSON.stringify(payload); | ||
| const signature = await tradingAccount.signPayload({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,51 @@ export class TradingAccount implements ITradingAccount { | |
| } | ||
|
|
||
| /** | ||
| * Signs an arbitrary payload with the user key on this trading account | ||
| * Signs an arbitrary payload. Use the user key if passphrase is provided, or the BitGo key if not. | ||
| * @param params | ||
| * @param params.payload arbitrary payload object (string | Record<string, unknown>) | ||
| * @param params.walletPassphrase passphrase on this trading account, used to unlock the account user key | ||
| * @returns hex-encoded signature of the payload | ||
| */ | ||
| async signPayload(params: SignPayloadParameters): Promise<string> { | ||
| // if no passphrase is provided, attempt to sign using the wallet's bitgo key remotely | ||
| if (!params.walletPassphrase) { | ||
| return this.signPayloadByBitGoKey(params); | ||
| } | ||
|
alextse-bg marked this conversation as resolved.
|
||
| // if a passphrase is provided, we must be trying to sign using the user private key - decrypt and sign locally | ||
| return this.signPayloadByUserKey(params); | ||
| } | ||
|
|
||
| /** | ||
| * Signs the payload of a trading account via the trading account BitGo key | ||
| * @param params | ||
| * @private | ||
| */ | ||
| private async signPayloadByBitGoKey(params: Omit<SignPayloadParameters, 'walletPassphrase'>): Promise<string> { | ||
| const walletData = this.wallet.toJSON(); | ||
| if (walletData.userKeySigningRequired) { | ||
| throw new Error( | ||
| 'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.' | ||
| ); | ||
| } | ||
| if (walletData.keys.length < 2) { | ||
| throw new Error( | ||
| 'Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.' | ||
| ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when |
||
| } | ||
|
alextse-bg marked this conversation as resolved.
|
||
|
|
||
| const url = this.wallet.url('/tx/sign'); | ||
| const { signature } = await this.wallet.bitgo.post(url).send(params.payload).result(); | ||
|
|
||
| return signature; | ||
| } | ||
|
|
||
| /** | ||
| * Signs the payload of a trading account locally by fetching the user's encrypted private key and decrypt using passphrase | ||
| * @param params | ||
| * @private | ||
| */ | ||
| private async signPayloadByUserKey(params: SignPayloadParameters): Promise<string> { | ||
| const key = (await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[0] })) as any; | ||
| const prv = this.wallet.bitgo.decrypt({ | ||
| input: key.encryptedPrv, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -909,6 +909,7 @@ export interface WalletData { | |
| evmKeyRingReferenceWalletId?: string; | ||
| isParent?: boolean; | ||
| enabledChildChains?: string[]; | ||
| userKeySigningRequired?: string; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| export interface RecoverTokenOptions { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { | |
| SignTransactionOptions as BaseSignTransactionOptions, | ||
| SignedTransaction, | ||
| ITransactionRecipient, | ||
| Wallet, | ||
| } from '../'; | ||
| import { isBolt11Invoice } from '../lightning'; | ||
|
|
||
|
|
@@ -18,7 +19,8 @@ export interface SignTransactionOptions extends BaseSignTransactionOptions { | |
| txPrebuild: { | ||
| payload: string; | ||
| }; | ||
| prv: string; | ||
| prv?: string; | ||
| wallet?: Wallet; | ||
| } | ||
|
|
||
| export { OfcTokenConfig }; | ||
|
|
@@ -107,15 +109,25 @@ export class OfcToken extends Ofc { | |
| } | ||
|
|
||
| /** | ||
| * Assemble keychain and half-sign prebuilt transaction | ||
| * Signs a half-signed OFC transaction. | ||
| * Signs the transaction remotely using the BitGo key if prv is not provided. | ||
| * @param params | ||
| * @returns {Promise<SignedTransaction>} | ||
| */ | ||
| async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> { | ||
| const txPrebuild = params.txPrebuild; | ||
| const payload = txPrebuild.payload; | ||
| const signatureBuffer = (await this.signMessage(params, payload)) as any; | ||
| const signature: string = signatureBuffer.toString('hex'); | ||
|
|
||
| let signature: string; | ||
| if (params.wallet) { | ||
| signature = await params.wallet.toTradingAccount().signPayload({ payload, walletPassphrase: params.prv }); | ||
| } else if (params.prv) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const signatureBuffer = (await this.signMessage({ prv: params.prv }, payload)) as any; | ||
| signature = signatureBuffer.toString('hex'); | ||
| } else { | ||
| throw new Error('You must pass in either one of wallet or prv'); | ||
| } | ||
|
|
||
| return { halfSigned: { payload, signature } } as any; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.