1
- import { BaseCoin , BitGoBase , common , MPCAlgorithm , MultisigType , multisigTypes } from '@bitgo/sdk-core' ;
1
+ import {
2
+ BaseCoin ,
3
+ BitGoBase ,
4
+ common ,
5
+ Ecdsa ,
6
+ MPCAlgorithm ,
7
+ MultisigType ,
8
+ multisigTypes ,
9
+ UnsignedTransactionTss ,
10
+ } from '@bitgo/sdk-core' ;
2
11
import { BaseCoin as StaticsBaseCoin , coins } from '@bitgo/statics' ;
3
12
import {
4
13
AbstractEthLikeNewCoins ,
14
+ KeyPair ,
5
15
recoveryBlockchainExplorerQuery ,
6
- VerifyEthTransactionOptions ,
16
+ ReplayProtectionOptions ,
17
+ UnsignedSweepTxMPCv2 ,
18
+ RecoverOptions ,
19
+ OfflineVaultTxInfo ,
20
+ ETHTransactionType ,
21
+ Transaction as EthTransaction ,
22
+ optionalDeps ,
7
23
} from '@bitgo/abstract-eth' ;
8
24
import { TransactionBuilder } from './lib' ;
25
+ import { getDerivationPath } from '@bitgo/sdk-lib-mpc' ;
26
+ import EthereumCommon from '@ethereumjs/common' ;
9
27
10
28
export class Bsc extends AbstractEthLikeNewCoins {
11
29
protected constructor ( bitgo : BitGoBase , staticsCoin ?: Readonly < StaticsBaseCoin > ) {
@@ -16,6 +34,180 @@ export class Bsc extends AbstractEthLikeNewCoins {
16
34
return new Bsc ( bitgo , staticsCoin ) ;
17
35
}
18
36
37
+ /**
38
+ * Builds an unsigned sweep transaction for TSS specific to BSC
39
+ * This implementation ensures that only legacy transactions are supported for BSC.
40
+ * @param params - Recovery options
41
+ * @returns {Promise<OfflineVaultTxInfo | UnsignedSweepTxMPCv2> }
42
+ */
43
+ protected async buildUnsignedSweepTxnTSS ( params : RecoverOptions ) : Promise < OfflineVaultTxInfo | UnsignedSweepTxMPCv2 > {
44
+ const bscParams : RecoverOptions = { ...params } ;
45
+
46
+ if ( ! bscParams . replayProtectionOptions ) {
47
+ bscParams . replayProtectionOptions = {
48
+ chain : this . getChain ( ) . includes ( 't' ) ? '97' : '56' ,
49
+ hardfork : 'petersburg' ,
50
+ } ;
51
+ } else if ( ! bscParams . replayProtectionOptions . hardfork ) {
52
+ bscParams . replayProtectionOptions . hardfork = 'petersburg' ;
53
+ }
54
+
55
+ if ( ! bscParams . gasLimit || ! bscParams . gasPrice ) {
56
+ throw new Error ( 'gasLimit and gasPrice are required for BSC legacy transactions' ) ;
57
+ }
58
+
59
+ if ( ! bscParams . backupKey ) {
60
+ throw new Error ( 'backupKey is required for TSS recovery' ) ;
61
+ }
62
+ if ( ! bscParams . recoveryDestination ) {
63
+ throw new Error ( 'Recipient address (recoveryDestination) is required for TSS recovery' ) ;
64
+ }
65
+
66
+ const { gasLimit, gasPrice } = await this . getGasValues ( params ) ;
67
+
68
+ const derivationPath = bscParams . derivationSeed ? getDerivationPath ( bscParams . derivationSeed ) : 'm/0' ;
69
+ const MPC = new Ecdsa ( ) ;
70
+ const derivedCommonKeyChain = MPC . deriveUnhardened ( bscParams . backupKey as string , derivationPath ) ;
71
+ const backupKeyPair = new KeyPair ( { pub : derivedCommonKeyChain . slice ( 0 , 66 ) } ) ;
72
+ const baseAddress = backupKeyPair . getAddress ( ) ;
73
+
74
+ const { txInfo, tx, nonce } = await this . buildTssRecoveryTxn ( baseAddress , gasPrice , gasLimit , bscParams ) ;
75
+
76
+ return this . buildTxRequestForOfflineVaultMPCv2bsc (
77
+ txInfo ,
78
+ tx ,
79
+ derivationPath ,
80
+ nonce ,
81
+ gasPrice ,
82
+ gasLimit ,
83
+ bscParams . replayProtectionOptions ,
84
+ derivedCommonKeyChain
85
+ ) ;
86
+ }
87
+ /**
88
+ * This transforms the unsigned transaction information into a format the BitGo offline vault expects
89
+ * Specific to BSC which only supports legacy transactions
90
+ * @param {any } txInfo
91
+ * @param {any } tx
92
+ * @param {string } derivationPath
93
+ * @param {number } nonce
94
+ * @param {string } gasPrice
95
+ * @param {string } gasLimit
96
+ * @param {undefined } _eip1559Params
97
+ * @param {ReplayProtectionOptions } replayProtectionOptions
98
+ * @param {string } commonKeyChain
99
+ * @returns {UnsignedSweepTxMPCv2 }
100
+ */
101
+ private buildTxRequestForOfflineVaultMPCv2bsc (
102
+ txInfo : any ,
103
+ tx : any ,
104
+ derivationPath : string ,
105
+ nonce : any ,
106
+ gasPrice : Buffer ,
107
+ gasLimit : number ,
108
+ replayProtectionOptions : ReplayProtectionOptions | undefined ,
109
+ commonKeyChain : string
110
+ ) : OfflineVaultTxInfo | UnsignedSweepTxMPCv2 {
111
+ if ( ! tx . to ) {
112
+ throw new Error ( 'BSC tx must have a `to` address' ) ;
113
+ }
114
+
115
+ console . log ( 'tx' , tx ) ;
116
+ const fee = gasLimit * Number ( optionalDeps . ethUtil . bufferToInt ( gasPrice ) . toFixed ( ) ) ;
117
+ const txNonce = tx . nonce ;
118
+ const txGasPrice = tx . gasPrice ;
119
+ const txGasLimit = tx . gasLimit ;
120
+ const txTo = tx . to ? tx . to . toBuffer ( ) : Buffer . alloc ( 0 ) ;
121
+ const txValue = tx . value ;
122
+ const txData = tx . data && tx . data . length > 0 ? tx . data : Buffer . alloc ( 0 ) ;
123
+ const txFrom = tx . from ? tx . from . toBuffer ( ) : Buffer . alloc ( 0 ) ;
124
+
125
+ const txChainId = this . getChain ( ) . includes ( 't' ) ? 97 : 56 ;
126
+
127
+ let signableHex : string ;
128
+ let serializedTxHex : string ;
129
+
130
+ try {
131
+ const txDataObj = {
132
+ nonce : txNonce ,
133
+ gasPrice : txGasPrice ,
134
+ gasLimit : txGasLimit ,
135
+ to : txTo ,
136
+ value : txValue ,
137
+ data : txData ,
138
+ chainId : txChainId . toString ( ) ,
139
+ _type : ETHTransactionType . LEGACY ,
140
+ from : txFrom ,
141
+ } ;
142
+
143
+ const common = EthereumCommon . forCustomChain (
144
+ 'mainnet' ,
145
+ {
146
+ name : 'bsc' ,
147
+ networkId : txChainId ,
148
+ chainId : txChainId ,
149
+ } ,
150
+ 'petersburg'
151
+ ) ;
152
+
153
+ const bscTx = new EthTransaction ( coins . get ( this . getChain ( ) ) , common , txDataObj ) ;
154
+ const serializedTx = bscTx . toBroadcastFormat ( ) ;
155
+ // Remove '0x' prefix if present
156
+ serializedTxHex = serializedTx . startsWith ( '0x' ) ? serializedTx . slice ( 2 ) : serializedTx ;
157
+ signableHex = serializedTxHex ;
158
+ } catch ( e ) {
159
+ throw new Error ( `Failed to encode transaction: ${ e . message } ` ) ;
160
+ }
161
+
162
+ if ( ! replayProtectionOptions ) {
163
+ replayProtectionOptions = {
164
+ chain : txChainId . toString ( ) ,
165
+ hardfork : 'petersburg' ,
166
+ } ;
167
+ } else if ( ! replayProtectionOptions . chain ) {
168
+ replayProtectionOptions . chain = txChainId . toString ( ) ;
169
+ }
170
+
171
+ const unsignedTx : UnsignedTransactionTss = {
172
+ serializedTxHex : serializedTxHex ,
173
+ signableHex : signableHex ,
174
+ derivationPath : derivationPath ,
175
+ feeInfo : {
176
+ fee : fee ,
177
+ feeString : fee . toString ( ) ,
178
+ } ,
179
+ parsedTx : {
180
+ spendAmount : txInfo . recipient . amount ,
181
+ outputs : [
182
+ {
183
+ coinName : this . getChain ( ) ,
184
+ address : txInfo . recipient . address ,
185
+ valueString : txInfo . recipient . amount ,
186
+ } ,
187
+ ] ,
188
+ } ,
189
+ coinSpecific : {
190
+ commonKeyChain : commonKeyChain ,
191
+ } ,
192
+ replayProtectionOptions : replayProtectionOptions ,
193
+ } ;
194
+
195
+ return {
196
+ txRequests : [
197
+ {
198
+ walletCoin : this . getChain ( ) ,
199
+ transactions : [
200
+ {
201
+ unsignedTx : unsignedTx ,
202
+ nonce : nonce ,
203
+ signatureShares : [ ] ,
204
+ } ,
205
+ ] ,
206
+ } ,
207
+ ] ,
208
+ } ;
209
+ }
210
+
19
211
protected getTransactionBuilder ( ) : TransactionBuilder {
20
212
return new TransactionBuilder ( coins . get ( this . getBaseChain ( ) ) ) ;
21
213
}
@@ -40,8 +232,11 @@ export class Bsc extends AbstractEthLikeNewCoins {
40
232
return 'ecdsa' ;
41
233
}
42
234
43
- async recoveryBlockchainExplorerQuery ( query : Record < string , string > ) : Promise < Record < string , unknown > > {
44
- const apiToken = common . Environments [ this . bitgo . getEnv ( ) ] . bscscanApiToken ;
235
+ async recoveryBlockchainExplorerQuery (
236
+ query : Record < string , string > ,
237
+ apiKey ?: string
238
+ ) : Promise < Record < string , unknown > > {
239
+ const apiToken = apiKey || common . Environments [ this . bitgo . getEnv ( ) ] . bscscanApiToken ;
45
240
const explorerUrl = common . Environments [ this . bitgo . getEnv ( ) ] . bscscanBaseUrl ;
46
241
return await recoveryBlockchainExplorerQuery ( query , explorerUrl as string , apiToken ) ;
47
242
}
@@ -55,7 +250,7 @@ export class Bsc extends AbstractEthLikeNewCoins {
55
250
* @param {Wallet } params.wallet - Wallet object to obtain keys to verify against
56
251
* @returns {boolean }
57
252
*/
58
- async verifyTssTransaction ( params : VerifyEthTransactionOptions ) : Promise < boolean > {
253
+ async verifyTssTransaction ( params : { txParams : any ; txPrebuild : any ; wallet : any } ) : Promise < boolean > {
59
254
const { txParams, txPrebuild, wallet } = params ;
60
255
if (
61
256
! txParams ?. recipients &&
0 commit comments