Skip to content

Commit 4ee1a9c

Browse files
committed
feat: create txrequest for message signing
Ticket: BG-54920
1 parent e19fbc0 commit 4ee1a9c

File tree

6 files changed

+118
-29
lines changed

6 files changed

+118
-29
lines changed

modules/bitgo/test/v2/unit/wallet.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,53 +1980,71 @@ describe('V2 Wallet:', function () {
19801980
};
19811981
let signTxRequestForMessage;
19821982
const messageSigningCoins = ['teth', 'tpolygon'];
1983+
const message = 'test';
19831984

19841985
beforeEach(async function () {
19851986
signTxRequestForMessage = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'signTxRequestForMessage');
19861987
signTxRequestForMessage.resolves(txRequestForMessageSigning);
1987-
// TODO(BG-59686): this is not doing anything if we don't check the return value
1988-
signTxRequestForMessage.calledOnceWithExactly({ txRequest: txRequestForMessageSigning, prv: 'secretKey', reqId });
1988+
});
1989+
1990+
afterEach(async function () {
1991+
sinon.restore();
19891992
});
19901993

19911994
it('should throw error for unsupported coins', async function () {
19921995

19931996
await tssWallet.signMessage({
19941997
reqId,
1995-
messagePrebuild: { message: 'test' },
1998+
messagePrebuild: { message },
19961999
prv: 'secretKey',
19972000
}).should.be.rejectedWith('Message signing not supported for Testnet Solana');
19982001
});
19992002

20002003
messageSigningCoins.map((coinName) => {
20012004
tssEthWallet = new Wallet(bitgo, bitgo.coin(coinName), ethWalletData);
2005+
const txRequestId = txRequestForMessageSigning.txRequestId;
2006+
20022007
it('should sign message', async function () {
2008+
const signMessageTssSpy = sinon.spy(tssEthWallet, 'signMessageTss' as any);
2009+
nock(bgUrl)
2010+
.get(`/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${txRequestForMessageSigning.txRequestId}&latest=true`)
2011+
.reply(200, { txRequests: [txRequestForMessageSigning] });
20032012

20042013
const signMessage = await tssEthWallet.signMessage({
20052014
reqId,
2006-
messagePrebuild: { message: 'test', txRequestId: 'id' },
2015+
messagePrebuild: { message, txRequestId },
20072016
prv: 'secretKey',
20082017
});
2009-
signMessage.should.deepEqual({ txRequestId: 'id' } );
2018+
signMessage.should.deepEqual({ txRequestId } );
2019+
const actualArg = signMessageTssSpy.getCalls()[0].args[0];
2020+
actualArg.messagePrebuild.message.should.equal(`\u0019Ethereum Signed Message:\\n${message.length}${message}`);
20102021
});
20112022

2012-
it('should fail to sign message without txRequestId', async function() {
2013-
await tssEthWallet.signMessage({
2023+
it('should sign message when txRequestId not provided', async function () {
2024+
const signMessageTssSpy = sinon.spy(tssEthWallet, 'signMessageTss' as any);
2025+
nock(bgUrl)
2026+
.post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`)
2027+
.reply(200, txRequestForMessageSigning);
2028+
2029+
const signMessage = await tssEthWallet.signMessage({
20142030
reqId,
2015-
messagePrebuild: { message: '' },
2031+
messagePrebuild: { message },
20162032
prv: 'secretKey',
2017-
}).should.be.rejectedWith('txRequestId required to sign message with TSS');
2033+
});
2034+
signMessage.should.deepEqual({ txRequestId } );
2035+
const actualArg = signMessageTssSpy.getCalls()[0].args[0];
2036+
actualArg.messagePrebuild.message.should.equal(`\u0019Ethereum Signed Message:\\n${message.length}${message}`);
20182037
});
20192038

20202039
it('should fail to sign message with empty prv', async function () {
20212040
await tssEthWallet.signMessage({
20222041
reqId,
2023-
messagePrebuild: { message: 'test', txRequestId: 'id' },
2042+
messagePrebuild: { message, txRequestId },
20242043
prv: '',
20252044
}).should.be.rejectedWith('prv required to sign message with TSS');
20262045
});
20272046
});
20282047

2029-
20302048
});
20312049

20322050
describe('Send Many', function () {

modules/sdk-coin-eth/src/eth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,16 @@ export class Eth extends BaseCoin {
17391739
return true;
17401740
}
17411741

1742+
/**
1743+
* Transform message to accommodate specific blockchain requirements.
1744+
* @param message the message to prepare
1745+
* @return string the prepared message.
1746+
*/
1747+
prepareMessage(message: string): string {
1748+
const prefix = `\u0019Ethereum Signed Message:\\n${message.length}`;
1749+
return prefix.concat(message);
1750+
}
1751+
17421752
private isETHAddress(address: string): boolean {
17431753
return !!address.match(/0x[a-fA-F0-9]{40}/);
17441754
}

modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ export interface TokenEnablementConfig {
346346
supportsMultipleTokenEnablements: boolean;
347347
}
348348

349+
export interface MessagePrep {
350+
prepareMessage(message: string): string;
351+
}
352+
349353
export type MPCAlgorithm = 'ecdsa' | 'eddsa';
350354

351355
export interface IBaseCoin {

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
TxRequest,
1919
TxRequestVersion,
2020
BackupKeyShare,
21+
IntentOptionsBase,
22+
PopulatedIntentBase,
2123
} from './baseTypes';
2224
import { SignShare, YShare, GShare } from '../../../account-lib/mpc/tss/eddsa/types';
2325

@@ -187,6 +189,40 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
187189
return unsignedTx;
188190
}
189191

192+
/**
193+
* Create a tx request from params for message signing
194+
*
195+
* @param params
196+
* @param apiVersion
197+
* @param preview
198+
*/
199+
async createTxRequestWithIntentForMessageSigning(
200+
params: IntentOptionsBase,
201+
apiVersion: TxRequestVersion = 'full',
202+
preview?: boolean
203+
): Promise<TxRequest> {
204+
const intentOptions: PopulatedIntentBase = {
205+
intentType: params.intentType,
206+
sequenceId: params.sequenceId,
207+
comment: params.comment,
208+
memo: params.memo?.value,
209+
isTss: params.isTss,
210+
};
211+
const whitelistedParams = {
212+
intent: {
213+
...intentOptions,
214+
},
215+
apiVersion,
216+
preview,
217+
};
218+
const txRequest = (await this.bitgo
219+
.post(this.bitgo.url(`/wallet/${this.wallet.id()}/txrequests`, 2))
220+
.send(whitelistedParams)
221+
.result()) as TxRequest;
222+
223+
return txRequest;
224+
}
225+
190226
/**
191227
* Call delete signature shares for a txRequest, the endpoint delete the signatures and return them
192228
*

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,27 @@ export interface TokenTransferRecipientParams {
5656
tokenId?: string;
5757
decimalPlaces?: number;
5858
}
59-
export interface PrebuildTransactionWithIntentOptions {
59+
export interface IntentOptionsBase {
6060
reqId: IRequestTracer;
6161
intentType: string;
6262
sequenceId?: string;
63+
isTss?: boolean;
64+
comment?: string;
65+
memo?: Memo;
66+
}
67+
export interface PrebuildTransactionWithIntentOptions extends IntentOptionsBase {
6368
recipients?: {
6469
address: string;
6570
amount: string | number;
6671
tokenName?: string;
6772
tokenData?: TokenTransferRecipientParams;
6873
}[];
69-
comment?: string;
70-
memo?: Memo;
7174
tokenName?: string;
7275
enableTokens?: TokenEnablement[];
7376
nonce?: string;
7477
selfSend?: boolean;
7578
feeOptions?: FeeOption | EIP1559FeeOptions;
7679
hopParams?: HopParams;
77-
isTss?: boolean;
7880
lowFeeTxid?: string;
7981
}
8082
export interface IntentRecipient {
@@ -87,20 +89,23 @@ export interface IntentRecipient {
8789
};
8890
tokenData?: TokenTransferRecipientParams;
8991
}
90-
export interface PopulatedIntent {
92+
export interface PopulatedIntentBase {
9193
intentType: string;
92-
recipients?: IntentRecipient[];
9394
sequenceId?: string;
9495
comment?: string;
95-
nonce?: string;
9696
memo?: string;
97+
isTss?: boolean;
98+
}
99+
100+
export interface PopulatedIntent extends PopulatedIntentBase {
101+
recipients?: IntentRecipient[];
102+
nonce?: string;
97103
token?: string;
98104
enableTokens?: TokenEnablement[];
99105
// ETH & ETH-like params
100106
selfSend?: boolean;
101107
feeOptions?: FeeOption | EIP1559FeeOptions;
102108
hopParams?: HopParams;
103-
isTss?: boolean;
104109
txid?: string;
105110
}
106111

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @prettier
33
*/
4-
4+
import assert from 'assert';
55
import { BigNumber } from 'bignumber.js';
66
import * as _ from 'lodash';
77
import * as common from '../../common';
@@ -22,7 +22,7 @@ import { drawKeycard } from '../internal/keycard';
2222
import { Keychain } from '../keychain';
2323
import { IPendingApproval, PendingApproval } from '../pendingApproval';
2424
import { TradingAccount } from '../trading/tradingAccount';
25-
import { inferAddressType, RequestTracer, TxRequest, EddsaUnsignedTransaction } from '../utils';
25+
import { inferAddressType, RequestTracer, TxRequest, EddsaUnsignedTransaction, IntentOptionsBase } from '../utils';
2626
import {
2727
AccelerateTransactionOptions,
2828
AddressesOptions,
@@ -74,6 +74,7 @@ import { StakingWallet } from '../staking/stakingWallet';
7474
import { Lightning } from '../lightning';
7575
import EddsaUtils from '../utils/tss/eddsa';
7676
import { EcdsaUtils } from '../utils/tss/ecdsa';
77+
import { getTxRequest } from '../tss';
7778
const debug = require('debug')('bitgo:v2:wallet');
7879

7980
type ManageUnspents = 'consolidate' | 'fanout';
@@ -1577,6 +1578,13 @@ export class Wallet implements IWallet {
15771578
if (!this.baseCoin.supportsMessageSigning()) {
15781579
throw new Error(`Message signing not supported for ${this.baseCoin.getFullName()}`);
15791580
}
1581+
if (!params.messagePrebuild) {
1582+
throw new Error('messagePrebuild required to sign message');
1583+
}
1584+
if (_.isFunction((this.baseCoin as any).prepareMessage)) {
1585+
assert(params.messagePrebuild);
1586+
params.messagePrebuild.message = (this.baseCoin as any).prepareMessage(params.messagePrebuild.message);
1587+
}
15801588
const presign = { ...params, walletData: this._wallet, tssUtils: this.tssUtils };
15811589
if (this._wallet.multisigType !== 'tss') {
15821590
throw new Error('Message signing only supported for TSS wallets');
@@ -2665,21 +2673,29 @@ export class Wallet implements IWallet {
26652673
* @param params signing options
26662674
*/
26672675
private async signMessageTss(params: WalletSignMessageOptions = {}): Promise<SignedMessage> {
2668-
if (!params.messagePrebuild) {
2669-
throw new Error('messagePrebuild required to sign message with TSS');
2670-
}
2671-
2672-
if (!params.messagePrebuild.txRequestId) {
2673-
throw new Error('txRequestId required to sign message with TSS');
2674-
}
2676+
assert(params.reqId);
26752677

26762678
if (!params.prv) {
26772679
throw new Error('prv required to sign message with TSS');
26782680
}
26792681

26802682
try {
2683+
let txRequest;
2684+
assert(params.messagePrebuild);
2685+
if (!params.messagePrebuild.txRequestId) {
2686+
const intentOption: IntentOptionsBase = {
2687+
reqId: params.reqId,
2688+
intentType: 'signmessage',
2689+
isTss: true,
2690+
};
2691+
txRequest = await this.tssUtils!.createTxRequestWithIntentForMessageSigning(intentOption);
2692+
params.messagePrebuild.txRequestId = txRequest.txRequestId;
2693+
} else {
2694+
assert(params.messagePrebuild.txRequestId);
2695+
txRequest = await getTxRequest(this.bitgo, this.id(), params.messagePrebuild.txRequestId);
2696+
}
26812697
const signedMessageRequest = await this.tssUtils!.signTxRequestForMessage({
2682-
txRequest: params.messagePrebuild.txRequestId,
2698+
txRequest,
26832699
prv: params.prv,
26842700
reqId: params.reqId || new RequestTracer(),
26852701
});

0 commit comments

Comments
 (0)