Skip to content

Commit 9515b8e

Browse files
committed
feat(sdk-core): add skipTssRecipientVerification opt-out flag
Ticket: WCN-151
1 parent 0bc8911 commit 9515b8e

16 files changed

Lines changed: 223 additions & 54 deletions

File tree

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,7 +3105,15 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31053105
);
31063106
};
31073107

3108+
if (!wallet || !txPrebuild) {
3109+
throw new Error('missing params');
3110+
}
3111+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
3112+
throw new Error('tx cannot be both a batch and hop transaction');
3113+
}
3114+
31083115
if (
3116+
!params.verification?.skipTssRecipientVerification &&
31093117
!txParams?.recipients &&
31103118
!(
31113119
txParams.prebuildTx?.consolidateId ||
@@ -3117,14 +3125,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31173125
) {
31183126
throw new Error('missing txParams');
31193127
}
3120-
if (!wallet || !txPrebuild) {
3121-
throw new Error('missing params');
3122-
}
3123-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
3124-
throw new Error('tx cannot be both a batch and hop transaction');
3125-
}
31263128

3127-
if (txParams.type && ['transfer'].includes(txParams.type)) {
3129+
if (!params.verification?.skipTssRecipientVerification && txParams.type && ['transfer'].includes(txParams.type)) {
31283130
if (txParams.recipients && txParams.recipients.length === 1) {
31293131
const recipients = txParams.recipients;
31303132
const expectedAmount = recipients[0].amount.toString();

modules/express/src/typedRoutes/api/v2/prebuildAndSignTransaction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ export const VerificationOptions = t.partial({
287287
verifyTokenEnablement: t.boolean,
288288
/** Verify consolidation to base address */
289289
consolidationToBaseAddress: t.boolean,
290+
/** Skip TSS recipient verification during signing */
291+
skipTssRecipientVerification: t.boolean,
290292
});
291293

292294
/**

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@ export class Bsc extends AbstractEthLikeNewCoins {
7474
*/
7575
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
7676
const { txParams, txPrebuild, wallet } = params;
77+
if (!wallet || !txPrebuild) {
78+
throw new Error(`missing params`);
79+
}
80+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
81+
throw new Error(`tx cannot be both a batch and hop transaction`);
82+
}
83+
7784
if (
85+
!params.verification?.skipTssRecipientVerification &&
7886
!txParams?.recipients &&
7987
!(
8088
txParams.prebuildTx?.consolidateId ||
@@ -85,12 +93,6 @@ export class Bsc extends AbstractEthLikeNewCoins {
8593
) {
8694
throw new Error(`missing txParams`);
8795
}
88-
if (!wallet || !txPrebuild) {
89-
throw new Error(`missing params`);
90-
}
91-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
92-
throw new Error(`tx cannot be both a batch and hop transaction`);
93-
}
9496

9597
return true;
9698
}

modules/sdk-coin-bsc/src/bscToken.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,15 @@ export class BscToken extends EthLikeToken {
5454
*/
5555
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
5656
const { txParams, txPrebuild, wallet } = params;
57+
if (!wallet || !txPrebuild) {
58+
throw new Error(`missing params`);
59+
}
60+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
61+
throw new Error(`tx cannot be both a batch and hop transaction`);
62+
}
63+
5764
if (
65+
!params.verification?.skipTssRecipientVerification &&
5866
!txParams?.recipients &&
5967
!(
6068
txParams.prebuildTx?.consolidateId ||
@@ -65,12 +73,6 @@ export class BscToken extends EthLikeToken {
6573
) {
6674
throw new Error(`missing txParams`);
6775
}
68-
if (!wallet || !txPrebuild) {
69-
throw new Error(`missing params`);
70-
}
71-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
72-
throw new Error(`tx cannot be both a batch and hop transaction`);
73-
}
7476

7577
return true;
7678
}

modules/sdk-coin-bsc/test/unit/bsc.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'should';
2+
import assert from 'assert';
23

34
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
45
import { BitGoAPI } from '@bitgo/sdk-api';
@@ -39,4 +40,123 @@ describe('Native BNB', function () {
3940
tbsc.allowsAccountConsolidations().should.equal(true);
4041
});
4142
});
43+
44+
describe('verifyTssTransaction', function () {
45+
let bsc: Bsc;
46+
47+
before(function () {
48+
bsc = bitgo.coin('bsc') as Bsc;
49+
});
50+
51+
const baseTxPrebuild = { txHex: '0xdeadbeef' } as any;
52+
const baseWallet = {} as any;
53+
54+
it('returns true immediately when skipTssRecipientVerification is true', async function () {
55+
const result = await bsc.verifyTssTransaction({
56+
txParams: {} as any,
57+
txPrebuild: baseTxPrebuild,
58+
wallet: baseWallet,
59+
verification: { skipTssRecipientVerification: true },
60+
});
61+
assert.strictEqual(result, true);
62+
});
63+
64+
it('throws when no recipients and skipTssRecipientVerification is false', async function () {
65+
await assert.rejects(
66+
() =>
67+
bsc.verifyTssTransaction({
68+
txParams: {} as any,
69+
txPrebuild: baseTxPrebuild,
70+
wallet: baseWallet,
71+
verification: { skipTssRecipientVerification: false },
72+
}),
73+
/missing txParams/
74+
);
75+
});
76+
77+
it('throws when no recipients and verification is not set', async function () {
78+
await assert.rejects(
79+
() =>
80+
bsc.verifyTssTransaction({
81+
txParams: {} as any,
82+
txPrebuild: baseTxPrebuild,
83+
wallet: baseWallet,
84+
}),
85+
/missing txParams/
86+
);
87+
});
88+
89+
it('returns true for exempt type without skipTssRecipientVerification', async function () {
90+
const result = await bsc.verifyTssTransaction({
91+
txParams: { type: 'delegate' } as any,
92+
txPrebuild: baseTxPrebuild,
93+
wallet: baseWallet,
94+
});
95+
assert.strictEqual(result, true);
96+
});
97+
98+
it('returns true when recipients are present without flag', async function () {
99+
const result = await bsc.verifyTssTransaction({
100+
txParams: { recipients: [{ address: '0xabc', amount: '100' }] } as any,
101+
txPrebuild: baseTxPrebuild,
102+
wallet: baseWallet,
103+
});
104+
assert.strictEqual(result, true);
105+
});
106+
107+
it('still throws missing params when wallet is missing even with skipTssRecipientVerification', async function () {
108+
await assert.rejects(
109+
() =>
110+
bsc.verifyTssTransaction({
111+
txParams: {} as any,
112+
txPrebuild: baseTxPrebuild,
113+
wallet: undefined as any,
114+
verification: { skipTssRecipientVerification: true },
115+
}),
116+
/missing params/
117+
);
118+
});
119+
120+
it('still throws missing params when txPrebuild is missing even with skipTssRecipientVerification', async function () {
121+
await assert.rejects(
122+
() =>
123+
bsc.verifyTssTransaction({
124+
txParams: {} as any,
125+
txPrebuild: undefined as any,
126+
wallet: baseWallet,
127+
verification: { skipTssRecipientVerification: true },
128+
}),
129+
/missing params/
130+
);
131+
});
132+
133+
it('still throws hop error even with skipTssRecipientVerification', async function () {
134+
await assert.rejects(
135+
() =>
136+
bsc.verifyTssTransaction({
137+
txParams: {
138+
hop: true,
139+
recipients: [
140+
{ address: '0xabc', amount: '100' },
141+
{ address: '0xdef', amount: '200' },
142+
],
143+
} as any,
144+
txPrebuild: baseTxPrebuild,
145+
wallet: baseWallet,
146+
verification: { skipTssRecipientVerification: true },
147+
}),
148+
/tx cannot be both a batch and hop transaction/
149+
);
150+
});
151+
152+
it('skips recipient check but passes other validation with skipTssRecipientVerification', async function () {
153+
const result = await bsc.verifyTssTransaction({
154+
txParams: {} as any,
155+
txPrebuild: baseTxPrebuild,
156+
wallet: baseWallet,
157+
verification: { skipTssRecipientVerification: true },
158+
});
159+
assert.strictEqual(result, true);
160+
});
161+
});
42162
});

modules/sdk-coin-evm/src/evmCoin.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,17 @@ export class EvmCoin extends AbstractEthLikeNewCoins {
113113
private async verifyLegacyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
114114
const { txParams, txPrebuild, wallet } = params;
115115

116-
// Basic validation for legacy transactions only
116+
if (!wallet || !txPrebuild) {
117+
throw new Error(`missing params`);
118+
}
119+
120+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
121+
throw new Error(`tx cannot be both a batch and hop transaction`);
122+
}
123+
124+
// Only enforce recipient presence when skipTssRecipientVerification is not set
117125
if (
126+
!params.verification?.skipTssRecipientVerification &&
118127
!txParams?.recipients &&
119128
!(
120129
txParams.prebuildTx?.consolidateId ||
@@ -126,14 +135,6 @@ export class EvmCoin extends AbstractEthLikeNewCoins {
126135
throw new Error(`missing txParams`);
127136
}
128137

129-
if (!wallet || !txPrebuild) {
130-
throw new Error(`missing params`);
131-
}
132-
133-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
134-
throw new Error(`tx cannot be both a batch and hop transaction`);
135-
}
136-
137138
// If validation passes, consider it verified
138139
return true;
139140
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,15 @@ export class Xdc extends AbstractEthLikeNewCoins {
6666
*/
6767
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
6868
const { txParams, txPrebuild, wallet } = params;
69+
if (!wallet || !txPrebuild) {
70+
throw new Error(`missing params`);
71+
}
72+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
73+
throw new Error(`tx cannot be both a batch and hop transaction`);
74+
}
75+
6976
if (
77+
!params.verification?.skipTssRecipientVerification &&
7078
!txParams?.recipients &&
7179
!(
7280
txParams.prebuildTx?.consolidateId ||
@@ -77,12 +85,6 @@ export class Xdc extends AbstractEthLikeNewCoins {
7785
) {
7886
throw new Error(`missing txParams`);
7987
}
80-
if (!wallet || !txPrebuild) {
81-
throw new Error(`missing params`);
82-
}
83-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
84-
throw new Error(`tx cannot be both a batch and hop transaction`);
85-
}
8688

8789
return true;
8890
}

modules/sdk-coin-xdc/src/xdcToken.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ export class XdcToken extends EthLikeToken {
7676
*/
7777
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
7878
const { txParams, txPrebuild, wallet } = params;
79+
if (!wallet || !txPrebuild) {
80+
throw new Error(`missing params`);
81+
}
82+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
83+
throw new Error(`tx cannot be both a batch and hop transaction`);
84+
}
85+
7986
if (
87+
!params.verification?.skipTssRecipientVerification &&
8088
!txParams?.recipients &&
8189
!(
8290
txParams.prebuildTx?.consolidateId ||
@@ -87,12 +95,6 @@ export class XdcToken extends EthLikeToken {
8795
) {
8896
throw new Error(`missing txParams`);
8997
}
90-
if (!wallet || !txPrebuild) {
91-
throw new Error(`missing params`);
92-
}
93-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
94-
throw new Error(`tx cannot be both a batch and hop transaction`);
95-
}
9698

9799
return true;
98100
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ export interface VerificationOptions {
238238
verifyTokenEnablement?: boolean;
239239
// Verify transaction is consolidating to wallet's base address
240240
consolidationToBaseAddress?: boolean;
241+
// Skip TSS recipient verification during signing (safety-net opt-out for callers
242+
// whose transaction type legitimately carries no explicit recipients).
243+
skipTssRecipientVerification?: boolean;
241244
}
242245

243246
export interface VerifyTransactionOptions {

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Key, SerializedKeyPair } from 'openpgp';
22
import { EncryptionVersion, IEncryptionSession, IRequestTracer } from '../../../api';
3-
import { type ITransactionRecipient, KeychainsTriplet, ParsedTransaction, TransactionParams } from '../../baseCoin';
3+
import {
4+
type ITransactionRecipient,
5+
KeychainsTriplet,
6+
ParsedTransaction,
7+
TransactionParams,
8+
VerificationOptions,
9+
} from '../../baseCoin';
410
import { ApiKeyShare, Keychain, WebauthnKeyEncryptionInfo } from '../../keychain';
511
import { ApiVersion, Memo, WalletType } from '../../wallet';
612
import { EDDSA, GShare, Signature, SignShare } from '../../../account-lib/mpc/tss';
@@ -679,6 +685,7 @@ export type TssSignTxExplicitRecipientParams = {
679685
apiVersion?: ApiVersion;
680686
recipientSource: typeof TssTxRecipientSource.Explicit;
681687
txParams: TransactionParamsWithMandatoryRecipients;
688+
verification?: VerificationOptions;
682689
};
683690

684691
export type TssSignTxResolvedRecipientParams = {
@@ -687,6 +694,7 @@ export type TssSignTxResolvedRecipientParams = {
687694
apiVersion?: ApiVersion;
688695
recipientSource?: typeof TssTxRecipientSource.Resolved;
689696
txParams?: TransactionParams;
697+
verification?: VerificationOptions;
690698
};
691699

692700
/**

0 commit comments

Comments
 (0)