Skip to content
Open
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
4 changes: 4 additions & 0 deletions modules/sdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## Unreleased

* **sdk-core:** Allow WalletConnect message and typed-data signing requests to carry `isWalletConnectTx: true`.

# [40.5.0](https://github.com/BitGo/BitGoJS/compare/%40bitgo%2Fsdk-core%4040.4.0...%40bitgo%2Fsdk-core%4040.5.0) (2026-11-03)

### Bug Fixes
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
isTss: params.isTss,
messageRaw: params.messageRaw,
messageEncoded: params.messageEncoded ?? '',
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};

return this.createTxRequestBase(intentOptions, apiVersion, preview, params.reqId);
Expand Down Expand Up @@ -498,6 +499,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
messageEncoded: params.messageEncoded ?? '',
signerAddress: params.signerAddress,
preparedTransaction: params.preparedTransaction,
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};

return this.buildSignMessageRequestBase(intent, apiVersion, params.reqId);
Expand All @@ -524,6 +526,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
isTss: params.isTss,
messageRaw: params.typedDataRaw,
messageEncoded: params.typedDataEncoded ?? '',
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};

return this.createTxRequestBase(intentOptions, apiVersion, preview, params.reqId);
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export interface UnwrapIntentParams {
}

export interface IntentOptionsForMessage extends IntentOptionsBase {
isWalletConnectTx?: true;
messageRaw: string;
messageEncoded?: string;
messageStandardType?: MessageStandardType;
Expand All @@ -333,6 +334,7 @@ export interface IntentOptionsForMessage extends IntentOptionsBase {
}

export interface IntentOptionsForTypedData extends IntentOptionsBase {
isWalletConnectTx?: true;
typedDataRaw: string;
typedDataEncoded?: string;
}
Expand Down Expand Up @@ -460,6 +462,7 @@ interface PopulatedIntentBase {
}

export interface PopulatedIntentForMessageSigning extends PopulatedIntentBase {
isWalletConnectTx?: true;
messageRaw: string;
messageEncoded: string;
custodianMessageId?: string;
Expand All @@ -470,6 +473,7 @@ export interface PopulatedIntentForMessageSigning extends PopulatedIntentBase {
}

export interface PopulatedIntentForTypedDataSigning extends PopulatedIntentBase {
isWalletConnectTx?: true;
messageRaw: string;
messageEncoded: string;
custodianMessageId?: string;
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,11 @@ export interface WalletSignTransactionOptions extends WalletSignBaseOptions {
}

interface WalletSignMessageBase extends WalletSignBaseOptions {
/**
* Identifies a new WalletConnect message signing request for classification, not authorization.
* When resuming an existing request by txRequestId, its original metadata is retained.
*/
isWalletConnectTx?: true;
walletPassphrase?: string;
custodianMessageId?: string;
customPaillierModulusGeneratingFunction?: CustomPaillierModulusGetterFunction;
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2632,6 +2632,7 @@ export class Wallet implements IWallet {
messageStandardType,
signerAddress: params.message.signerAddress,
preparedTransaction: params.message.preparedTransaction,
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};

if (!this.tssUtils) {
Expand Down Expand Up @@ -5510,6 +5511,7 @@ export class Wallet implements IWallet {
messageStandardType: params.message.messageStandardType,
signerAddress: params.message.signerAddress,
preparedTransaction: params.message.preparedTransaction,
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};
txRequest = await this.tssUtils!.buildSignMessageRequest(intentOption);
params.message.txRequestId = txRequest.txRequestId;
Expand Down Expand Up @@ -5596,6 +5598,7 @@ export class Wallet implements IWallet {
isTss: true,
typedDataRaw: params.typedData.typedDataRaw,
typedDataEncoded: params.typedData.typedDataEncoded!.toString('hex'),
...(params.isWalletConnectTx !== undefined && { isWalletConnectTx: params.isWalletConnectTx }),
};
txRequest = await this.tssUtils!.createTxRequestWithIntentForTypedDataSigning(intentOptions);
params.typedData.txRequestId = txRequest.txRequestId;
Expand Down
192 changes: 192 additions & 0 deletions modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { DklsUtils } from '@bitgo/sdk-lib-mpc';

import { BitGoBase, EcdsaMPCv2Utils, IBaseCoin, IWallet, RequestTracer, TxRequest } from '../../../../../src';
import BaseTssUtils from '../../../../../src/bitgo/utils/tss/baseTSSUtils';
import { Wallet } from '../../../../../src/bitgo/wallet/wallet';
import { MessageStandardType } from '../../../../../src/bitgo/utils/messageTypes';
import { SignTypedDataVersion } from '../../../../../src/bitgo/baseCoin/iBaseCoin';

type BitgoGpgKeyPair = openpgp.SerializedKeyPair<string> & { revocationCertificate: string };
type TxRequestTransaction = NonNullable<TxRequest['transactions']>[number];
Expand Down Expand Up @@ -108,6 +111,195 @@ describe('Base TSS Utils', function () {
ecdsaMPCv2Utils = new EcdsaMPCv2Utils(mockBitgo, mockCoin);
});

describe('message request WalletConnect marker', function () {
for (const isWalletConnectTx of [true, undefined] as const) {
for (const request of ['msgrequests', 'message txrequests', 'typed data txrequests'] as const) {
it(`forwards ${String(isWalletConnectTx)} to ${request}`, async function () {
const send = sinon.stub().returns({ result: sinon.stub().resolves(buildTxRequest()) });
mockBitgo.post = sinon.stub().returns({ send });
mockBitgo.url = sinon.stub().callsFake((url: string) => url);
mockBitgo.setRequestTracer = sinon.stub();
const wallet = { id: sinon.stub().returns('wallet-id') } as unknown as IWallet;
const coin = {} as IBaseCoin;
const utils = new TestBaseTssUtils(mockBitgo, coin, wallet);
const reqId = new RequestTracer();
const marker: { isWalletConnectTx?: true } = isWalletConnectTx === undefined ? {} : { isWalletConnectTx };

if (request === 'typed data txrequests') {
await utils.createTxRequestWithIntentForTypedDataSigning({
reqId,
intentType: 'signTypedStructuredData',
typedDataRaw: '{}',
...marker,
});
} else {
const options = { reqId, intentType: 'signMessage', messageRaw: 'hello', ...marker };
if (request === 'msgrequests') {
await utils.buildSignMessageRequest(options);
} else {
await utils.createTxRequestWithIntentForMessageSigning(options);
}
}

sinon.assert.calledOnce(send);
const body = send.firstCall.args[0];
assert.strictEqual(body.intent.isWalletConnectTx, isWalletConnectTx);
assert.strictEqual(
Object.prototype.hasOwnProperty.call(body.intent, 'isWalletConnectTx'),
isWalletConnectTx === true
);
assert.strictEqual(body.intent.messageRaw, request === 'typed data txrequests' ? '{}' : 'hello');
assert.strictEqual(
body.intent.intentType,
request === 'typed data txrequests' ? 'signTypedStructuredData' : 'signMessage'
);
assert.strictEqual(Object.prototype.hasOwnProperty.call(body.intent, 'txChannel'), false);
sinon.assert.calledWith(
mockBitgo.post as sinon.SinonStub,
`/wallet/wallet-id/${request === 'msgrequests' ? 'msgrequests' : 'txrequests'}`
);
});
}
}
});

describe('wallet message signing WalletConnect marker', function () {
for (const isWalletConnectTx of [true, undefined] as const) {
for (const { operation, coinName, messageStandardType } of [
{ operation: 'signMessage', coinName: 'teth', messageStandardType: MessageStandardType.EIP191 },
{ operation: 'signMessage', coinName: 'tsol', messageStandardType: MessageStandardType.SIMPLE },
{ operation: 'buildSignMessageRequest', coinName: 'teth', messageStandardType: MessageStandardType.EIP191 },
{ operation: 'buildSignMessageRequest', coinName: 'tsol', messageStandardType: MessageStandardType.SIMPLE },
{ operation: 'signTypedData', coinName: 'teth', messageStandardType: MessageStandardType.EIP712 },
] as const) {
it(`forwards ${String(isWalletConnectTx)} from ${coinName} ${operation}`, async function () {
const response = buildTxRequest({
apiVersion: 'full',
state: 'pendingApproval',
pendingApprovalId: 'approval-id',
});
const send = sinon.stub().returns({ result: sinon.stub().resolves(response) });
mockBitgo.post = sinon.stub().returns({ send });
mockBitgo.url = sinon.stub().callsFake((url: string) => url);
mockBitgo.setRequestTracer = sinon.stub();
const coin = {
supportsTss: () => false,
supportsMessageSigning: () => true,
supportsSigningTypedData: () => true,
keychains: () => ({ getKeysForSigning: async () => [{ commonKeychain: 'key' }] }),
} as unknown as IBaseCoin;
const wallet = new Wallet(mockBitgo, coin, { id: 'wallet-id', coin: coinName, multisigType: 'tss' });
Object.defineProperty(wallet, 'tssUtils', { value: new TestBaseTssUtils(mockBitgo, coin, wallet) });
sinon.stub(wallet, 'getUserPrv').resolves('prv');
const marker: { isWalletConnectTx?: true } = isWalletConnectTx === undefined ? {} : { isWalletConnectTx };
if (operation === 'signTypedData') {
const result = await wallet.signTypedData({
typedData: {
typedDataRaw: '{}',
typedDataEncoded: Buffer.from('abcd', 'hex'),
version: SignTypedDataVersion.V4,
},
...marker,
});
assert.deepStrictEqual(result, {
state: 'pendingApproval',
coin: coinName,
messageRaw: '{}',
txRequestId: 'tx-request-id',
pendingApprovalId: 'approval-id',
});
} else {
const options = {
message: { messageRaw: 'hello', messageStandardType },
...marker,
};
if (operation === 'signMessage') {
const result = await wallet.signMessage(options);
assert.deepStrictEqual(result, {
state: 'pendingApproval',
coin: coinName,
messageRaw: 'hello',
txRequestId: 'tx-request-id',
pendingApprovalId: 'approval-id',
});
} else {
await wallet.buildSignMessageRequest(options);
}
}
sinon.assert.calledOnce(send);
const intent = send.firstCall.args[0].intent;
assert.strictEqual(intent.isWalletConnectTx, isWalletConnectTx);
assert.strictEqual(
Object.prototype.hasOwnProperty.call(intent, 'isWalletConnectTx'),
isWalletConnectTx === true
);
assert.strictEqual(
intent.intentType,
operation === 'signTypedData' ? 'signTypedStructuredData' : 'signMessage'
);
assert.strictEqual(intent.messageRaw, operation === 'signTypedData' ? '{}' : 'hello');
assert.strictEqual(intent.messageEncoded, operation === 'signTypedData' ? 'abcd' : '');
if (operation !== 'signTypedData') {
assert.strictEqual(intent.messageStandardType, messageStandardType);
}
assert.strictEqual(Object.prototype.hasOwnProperty.call(intent, 'txChannel'), false);
sinon.assert.calledWith(
mockBitgo.post as sinon.SinonStub,
`/wallet/wallet-id/${operation === 'signTypedData' ? 'txrequests' : 'msgrequests'}`
);
});
}
}
});

describe('resuming message requests with WalletConnect metadata', function () {
for (const operation of ['signMessage', 'signTypedData'] as const) {
it(`reuses the existing request for ${operation}`, async function () {
const response = buildTxRequest({
apiVersion: 'full',
state: 'pendingApproval',
pendingApprovalId: 'approval-id',
});
const retry = sinon.stub().returns({ result: sinon.stub().resolves({ txRequests: [response] }) });
const query = sinon.stub().returns({ retry });
mockBitgo.get = sinon.stub().returns({ query });
mockBitgo.post = sinon.stub();
mockBitgo.url = sinon.stub().callsFake((url: string) => url);
mockBitgo.setRequestTracer = sinon.stub();
const coin = {
supportsTss: () => false,
supportsMessageSigning: () => true,
supportsSigningTypedData: () => true,
keychains: () => ({ getKeysForSigning: async () => [{ commonKeychain: 'key' }] }),
} as unknown as IBaseCoin;
const wallet = new Wallet(mockBitgo, coin, { id: 'wallet-id', coin: 'teth', multisigType: 'tss' });
Object.defineProperty(wallet, 'tssUtils', { value: new TestBaseTssUtils(mockBitgo, coin, wallet) });
sinon.stub(wallet, 'getUserPrv').resolves('prv');

if (operation === 'signMessage') {
await wallet.signMessage({
isWalletConnectTx: true,
message: { messageRaw: 'hello', messageStandardType: MessageStandardType.EIP191, txRequestId: 'existing' },
});
} else {
await wallet.signTypedData({
isWalletConnectTx: true,
typedData: {
typedDataRaw: '{}',
typedDataEncoded: Buffer.from('abcd', 'hex'),
version: SignTypedDataVersion.V4,
txRequestId: 'existing',
},
});
}

sinon.assert.calledOnce(mockBitgo.get as sinon.SinonStub);
sinon.assert.calledWith(query, { txRequestIds: 'existing', latest: 'true' });
sinon.assert.notCalled(mockBitgo.post as sinon.SinonStub);
});
}
});

describe('getSignableHexAndDerivationPath', function () {
it('extracts signableHex and derivationPath from a valid txRequest', function () {
const txRequest = buildTxRequest({
Expand Down
Loading