Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions modules/bitgo/test/v2/unit/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,4 +706,75 @@ describe('Pending Approvals:', () => {
});
});
});

describe('message-sign approvals (transactionRequestFull)', () => {
const buildMessageSignPendingApprovalData = (intentType: string): PendingApprovalData => ({
id: 'pa-msg',
info: {
type: Type.TRANSACTION_REQUEST_FULL,
transactionRequestFull: {
txRequestId: 'txreq-msg-1',
intent: { intentType },
},
} as PendingApprovalInfo,
state: State.PENDING,
creator: 'test',
txRequestId: 'txreq-msg-1',
});

['signMessage', 'signTypedStructuredData'].forEach((intentType) => {
it(`should approve without the post-approve auto-sign for a ${intentType} approval`, async () => {
const ethBaseCoin = bitgo.coin('teth');
const paData = buildMessageSignPendingApprovalData(intentType);
const pendingApproval = new PendingApproval(bitgo, ethBaseCoin, paData, wallet);

const paScope = nock(bgUrl)
.put(`/api/v2/teth/pendingapprovals/pa-msg`, { state: 'approved', otp: undefined })
.reply(200, { ...paData, state: 'approved' });
const recreateTransactionTssStub = sandbox.stub(PendingApproval.prototype, 'recreateAndSignTSSTransaction');

await pendingApproval.approve({ walletPassphrase: 'pass' });

recreateTransactionTssStub.notCalled.should.be.true();
paScope.isDone().should.be.true();
});
});

it('should keep the post-approve auto-sign for transaction approvals', async () => {
const ethBaseCoin = bitgo.coin('teth');
const paData = buildMessageSignPendingApprovalData('payment');
const pendingApproval = new PendingApproval(bitgo, ethBaseCoin, paData, wallet);

const paScope = nock(bgUrl)
.put(`/api/v2/teth/pendingapprovals/pa-msg`, { state: 'approved', otp: undefined })
.reply(200, { ...paData, state: 'approved' });
const recreateTransactionTssStub = sandbox.stub(PendingApproval.prototype, 'recreateAndSignTSSTransaction');

await pendingApproval.approve({ walletPassphrase: 'pass' });

recreateTransactionTssStub.calledOnce.should.be.true();
paScope.isDone().should.be.true();
});

it('should detect a message-sign request from messages when intent is absent', async () => {
const ethBaseCoin = bitgo.coin('teth');
const paData = buildMessageSignPendingApprovalData('payment');
const full = paData.info.transactionRequestFull;
if (full) {
delete full.intent;
full.messages = [{ messageRaw: 'hello' }];
}
const pendingApproval = new PendingApproval(bitgo, ethBaseCoin, paData, wallet);

const paScope = nock(bgUrl)
.put(`/api/v2/teth/pendingapprovals/pa-msg`, { state: 'approved', otp: undefined })
.reply(200, { ...paData, state: 'approved' });
const recreateTransactionTssStub = sandbox.stub(PendingApproval.prototype, 'recreateAndSignTSSTransaction');

await pendingApproval.approve({ walletPassphrase: 'pass' });

recreateTransactionTssStub.notCalled.should.be.true();
paScope.isDone().should.be.true();
});
});
});
16 changes: 16 additions & 0 deletions modules/sdk-core/src/bitgo/pendingApproval/iPendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,24 @@ export interface PendingApprovalInfo {
};
sourceWallet?: string;
};
/**
* Present on transactionRequestFull pending approvals. intent.intentType is
* MESSAGE_SIGNING_INTENT_TYPES for message-sign approvals, which have no
* unsigned transaction.
*/
transactionRequestFull?: {
txRequestId?: string;
intent?: {
intentType?: string;
[index: string]: unknown;
};
[index: string]: unknown;
};
}

/** Intent types whose txRequest carries a message to sign rather than a transaction. */
export const MESSAGE_SIGNING_INTENT_TYPES: ReadonlySet<string> = new Set(['signMessage', 'signTypedStructuredData']);

export interface PendingApprovalData {
id: string;
wallet?: string;
Expand Down
24 changes: 23 additions & 1 deletion modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as _ from 'lodash';
import * as utxolib from '@bitgo/utxo-lib';
import type { AttestationPayload } from '@bitgo/public-types';
import { MESSAGE_SIGNING_INTENT_TYPES } from './iPendingApproval';
import { IBaseCoin } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
import {
Expand Down Expand Up @@ -172,7 +173,10 @@ export class PendingApproval implements IPendingApproval {
}

/**
* Sets this PendingApproval to an approved state
* Sets this PendingApproval to an approved state.
* For transaction approvals on TSS wallets, this also attempts to recreate
* and sign the transaction. For message-sign approvals, signing is deferred:
* the message must be signed separately via signAndSendMessageTxRequest.
*/
async approve(params: ApproveOptions = {}): Promise<any> {
params.previewPendingTxs = true;
Expand Down Expand Up @@ -489,12 +493,30 @@ export class PendingApproval implements IPendingApproval {
reqId
);
} else if (this.canRecreateTransaction(params) && this.baseCoin.supportsTss()) {
if (this.isMessageSigningRequest()) {
return;
}
await this.recreateAndSignTSSTransaction(params, reqId);
}
}
}
}

/**
* True when the approval's intent is a message-sign intent.
*/
private isMessageSigningRequest(): boolean {
const info = this.info()?.transactionRequestFull;
if (info?.intent?.intentType && MESSAGE_SIGNING_INTENT_TYPES.has(info.intent.intentType)) {
return true;
}
// Fallback when intent is absent: a messages array means the request is message-only.
if (info && 'messages' in info && Array.isArray(info.messages) && info.messages.length > 0) {
return true;
}
return false;
}

/**
* Helper function to ensure that self.wallet is set
*/
Expand Down
18 changes: 15 additions & 3 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,11 +590,13 @@ export class EcdsaUtils extends BaseEcdsaUtils {

if (requestType === RequestType.tx) {
assert(
txRequest.transactions || (txRequest as TxRequest).unsignedTxs,
(txRequest.transactions && txRequest.transactions.length > 0) ||
(txRequest.unsignedTxs && txRequest.unsignedTxs.length > 0),
'Unable to find transactions in txRequest'
);
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
assert(unsignedTx, 'Missing unsignedTx in txRequest');
derivationPath = unsignedTx.derivationPath;
} else if (requestType === RequestType.message) {
// TODO BG-67299 Message signing with derivation path
Expand Down Expand Up @@ -641,9 +643,14 @@ export class EcdsaUtils extends BaseEcdsaUtils {
const txRequest: TxRequest = tssParams.txRequest;
let signablePayload;
if (requestType === RequestType.tx) {
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
assert(
(txRequest.transactions && txRequest.transactions.length > 0) ||
(txRequest.unsignedTxs && txRequest.unsignedTxs.length > 0),
'Unable to find transactions in txRequest'
);
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
assert(unsignedTx, 'Missing unsignedTx in txRequest');
signablePayload = Buffer.from(unsignedTx.signableHex, 'hex');
} else if (requestType === RequestType.message) {
signablePayload = (params.tssParams as TSSParamsForMessage).bufferToSign;
Expand Down Expand Up @@ -799,9 +806,14 @@ export class EcdsaUtils extends BaseEcdsaUtils {
let derivationPath = '';

if (requestType === RequestType.tx) {
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
assert(
(txRequest.transactions && txRequest.transactions.length > 0) ||
(txRequest.unsignedTxs && txRequest.unsignedTxs.length > 0),
'Unable to find transactions in txRequest'
);
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
assert(unsignedTx, 'Missing unsignedTx in txRequest');

if (
'recipientSource' in params &&
Expand Down
Loading