1
- import { AuditDecryptedKeyParams , BaseCoin , BitGoBase } from '@bitgo/sdk-core' ;
2
- import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics' ;
3
- import { SubstrateCoin } from '@bitgo/abstract-substrate' ;
1
+ import {
2
+ AuditDecryptedKeyParams ,
3
+ BaseCoin ,
4
+ BitGoBase ,
5
+ EDDSAMethods ,
6
+ EDDSAMethodTypes ,
7
+ MPCRecoveryOptions ,
8
+ MPCSweepTxs ,
9
+ MPCTx ,
10
+ MPCUnsignedTx ,
11
+ RecoveryTxRequest ,
12
+ Environments ,
13
+ MPCSweepRecoveryOptions ,
14
+ MPCTxs ,
15
+ } from '@bitgo/sdk-core' ;
16
+ import { ApiPromise , WsProvider } from '@polkadot/api' ;
17
+ import { BaseCoin as StaticsBaseCoin , coins , SubstrateSpecNameType } from '@bitgo/statics' ;
18
+ import { KeyPair as SubstrateKeyPair , SubstrateCoin , Transaction , Interface } from '@bitgo/abstract-substrate' ;
4
19
import { BatchStakingBuilder } from './lib/batchStakingBuilder' ;
5
20
import { BondExtraBuilder } from './lib/bondExtraBuilder' ;
6
21
import { POLYX_ADDRESS_FORMAT } from './lib/constants' ;
22
+ import { getDerivationPath } from '@bitgo/sdk-lib-mpc' ;
23
+ import BigNumber from 'bignumber.js' ;
24
+ import { TransactionBuilderFactory , TransferBuilder } from './lib' ;
7
25
8
26
export class Polyx extends SubstrateCoin {
9
27
protected readonly _staticsCoin : Readonly < StaticsBaseCoin > ;
@@ -17,10 +35,17 @@ export class Polyx extends SubstrateCoin {
17
35
this . _staticsCoin = staticsCoin ;
18
36
}
19
37
38
+ protected static nodeApiInitialized = false ;
39
+ protected static API : ApiPromise ;
40
+
20
41
static createInstance ( bitgo : BitGoBase , staticsCoin ?: Readonly < StaticsBaseCoin > ) : BaseCoin {
21
42
return new Polyx ( bitgo , staticsCoin ) ;
22
43
}
23
44
45
+ getBuilder ( ) : TransactionBuilderFactory {
46
+ return new TransactionBuilderFactory ( coins . get ( this . getChain ( ) ) ) ;
47
+ }
48
+
24
49
/**
25
50
* Factor between the coin's base unit and its smallest subdivison
26
51
*/
@@ -58,4 +83,264 @@ export class Polyx extends SubstrateCoin {
58
83
protected getAddressFormat ( ) : number {
59
84
return POLYX_ADDRESS_FORMAT ;
60
85
}
86
+
87
+ protected async getInitializedNodeAPI ( ) : Promise < ApiPromise > {
88
+ if ( ! Polyx . nodeApiInitialized ) {
89
+ const wsProvider = new WsProvider ( Environments [ this . bitgo . getEnv ( ) ] . polymeshNodeUrls ) ;
90
+ Polyx . API = await ApiPromise . create ( { provider : wsProvider } ) ;
91
+ Polyx . nodeApiInitialized = true ;
92
+ }
93
+ return Polyx . API ;
94
+ }
95
+
96
+ protected async getAccountInfo ( walletAddr : string ) : Promise < { nonce : number ; freeBalance : number } > {
97
+ const api = await this . getInitializedNodeAPI ( ) ;
98
+ const { nonce, data : balance } = await api . query . system . account ( walletAddr ) ;
99
+
100
+ return { nonce : nonce . toNumber ( ) , freeBalance : balance . free . toNumber ( ) } ;
101
+ }
102
+
103
+ protected async getFee ( destAddr : string , srcAddr : string , amount : number ) : Promise < number > {
104
+ const api = await this . getInitializedNodeAPI ( ) ;
105
+ const info = await api . tx . balances . transfer ( destAddr , amount ) . paymentInfo ( srcAddr ) ;
106
+ return info . partialFee . toNumber ( ) ;
107
+ }
108
+
109
+ protected async getHeaderInfo ( ) : Promise < { headerNumber : number ; headerHash : string } > {
110
+ const api = await this . getInitializedNodeAPI ( ) ;
111
+ const { number, hash } = await api . rpc . chain . getHeader ( ) ;
112
+ return { headerNumber : number . toNumber ( ) , headerHash : hash . toString ( ) } ;
113
+ }
114
+
115
+ protected async getMaterial ( ) : Promise < Interface . Material > {
116
+ const api = await this . getInitializedNodeAPI ( ) ;
117
+ return {
118
+ genesisHash : api . genesisHash . toString ( ) ,
119
+ chainName : api . runtimeChain . toString ( ) ,
120
+ specName : api . runtimeVersion . specName . toString ( ) as SubstrateSpecNameType ,
121
+ specVersion : api . runtimeVersion . specVersion . toNumber ( ) ,
122
+ txVersion : api . runtimeVersion . transactionVersion . toNumber ( ) ,
123
+ metadata : api . runtimeMetadata . toHex ( ) ,
124
+ } ;
125
+ }
126
+
127
+ /**
128
+ * Builds a funds recovery transaction without BitGo
129
+ * @param {MPCRecoveryOptions } params parameters needed to construct and
130
+ * (maybe) sign the transaction
131
+ *
132
+ * @returns {MPCTx } the serialized transaction hex string and index
133
+ * of the address being swept
134
+ */
135
+ async recover ( params : MPCRecoveryOptions ) : Promise < MPCTx | MPCSweepTxs > {
136
+ if ( ! params . bitgoKey ) {
137
+ throw new Error ( 'Missing bitgoKey' ) ;
138
+ }
139
+
140
+ if ( ! params . recoveryDestination || ! this . isValidAddress ( params . recoveryDestination ) ) {
141
+ throw new Error ( 'Invalid recovery destination address' ) ;
142
+ }
143
+
144
+ const bitgoKey = params . bitgoKey . replace ( / \s / g, '' ) ;
145
+ const isUnsignedSweep = ! params . userKey && ! params . backupKey && ! params . walletPassphrase ;
146
+
147
+ const MPC = await EDDSAMethods . getInitializedMpcInstance ( ) ;
148
+
149
+ const index = params . index || 0 ;
150
+ const currPath = params . seed ? getDerivationPath ( params . seed ) + `/${ index } ` : `m/${ index } ` ;
151
+ const accountId = MPC . deriveUnhardened ( bitgoKey , currPath ) . slice ( 0 , 64 ) ;
152
+ const senderAddr = this . getAddressFromPublicKey ( accountId ) ;
153
+
154
+ const { nonce, freeBalance } = await this . getAccountInfo ( senderAddr ) ;
155
+
156
+ const destAddr = params . recoveryDestination ;
157
+ const amount = freeBalance ;
158
+ const partialFee = await this . getFee ( destAddr , senderAddr , amount ) ;
159
+ const paddedFee = new BigNumber ( partialFee ) . times ( 10 ) . toNumber ( ) ;
160
+ const amountToSend = new BigNumber ( amount ) . minus ( new BigNumber ( paddedFee ) ) ;
161
+
162
+ const value = new BigNumber ( freeBalance ) . minus ( new BigNumber ( partialFee ) ) ;
163
+ if ( value . isLessThanOrEqualTo ( 0 ) ) {
164
+ throw new Error ( 'Did not find address with funds to recover' ) ;
165
+ }
166
+
167
+ const { headerNumber, headerHash } = await this . getHeaderInfo ( ) ;
168
+ const material = await this . getMaterial ( ) ;
169
+ const validityWindow = { firstValid : headerNumber , maxDuration : this . MAX_VALIDITY_DURATION } ;
170
+
171
+ const txBuilder = this . getBuilder ( ) . getTransferBuilder ( ) . material ( material ) as TransferBuilder ;
172
+
173
+ txBuilder
174
+ . amount ( amountToSend . toString ( ) )
175
+ . to ( { address : params . recoveryDestination } )
176
+ . sender ( { address : senderAddr } )
177
+ . memo ( '0' )
178
+ . validity ( validityWindow )
179
+ . referenceBlock ( headerHash )
180
+ . sequenceId ( { name : 'Nonce' , keyword : 'nonce' , value : nonce } )
181
+ . fee ( { amount : 0 , type : 'tip' } ) ;
182
+
183
+ const unsignedTransaction = ( await txBuilder . build ( ) ) as Transaction ;
184
+
185
+ let serializedTx = unsignedTransaction . toBroadcastFormat ( ) ;
186
+ if ( ! isUnsignedSweep ) {
187
+ if ( ! params . userKey ) {
188
+ throw new Error ( 'missing userKey' ) ;
189
+ }
190
+ if ( ! params . backupKey ) {
191
+ throw new Error ( 'missing backupKey' ) ;
192
+ }
193
+ if ( ! params . walletPassphrase ) {
194
+ throw new Error ( 'missing wallet passphrase' ) ;
195
+ }
196
+
197
+ const userKey = params . userKey . replace ( / \s / g, '' ) ;
198
+ const backupKey = params . backupKey . replace ( / \s / g, '' ) ;
199
+
200
+ // Decrypt private keys from KeyCard values
201
+ let userPrv ;
202
+ try {
203
+ userPrv = this . bitgo . decrypt ( {
204
+ input : userKey ,
205
+ password : params . walletPassphrase ,
206
+ } ) ;
207
+ } catch ( e ) {
208
+ throw new Error ( `Error decrypting user keychain: ${ e . message } ` ) ;
209
+ }
210
+ const userSigningMaterial = JSON . parse ( userPrv ) as EDDSAMethodTypes . UserSigningMaterial ;
211
+
212
+ let backupPrv ;
213
+ try {
214
+ backupPrv = this . bitgo . decrypt ( {
215
+ input : backupKey ,
216
+ password : params . walletPassphrase ,
217
+ } ) ;
218
+ } catch ( e ) {
219
+ throw new Error ( `Error decrypting backup keychain: ${ e . message } ` ) ;
220
+ }
221
+ const backupSigningMaterial = JSON . parse ( backupPrv ) as EDDSAMethodTypes . BackupSigningMaterial ;
222
+
223
+ // add signature
224
+ const signatureHex = await EDDSAMethods . getTSSSignature (
225
+ userSigningMaterial ,
226
+ backupSigningMaterial ,
227
+ currPath ,
228
+ unsignedTransaction
229
+ ) ;
230
+
231
+ const substrateKeyPair = new SubstrateKeyPair ( { pub : accountId } ) ;
232
+ txBuilder . addSignature ( { pub : substrateKeyPair . getKeys ( ) . pub } , signatureHex ) ;
233
+ const signedTransaction = await txBuilder . build ( ) ;
234
+ serializedTx = signedTransaction . toBroadcastFormat ( ) ;
235
+ } else {
236
+ const walletCoin = this . getChain ( ) ;
237
+ const inputs = [
238
+ {
239
+ address : unsignedTransaction . inputs [ 0 ] . address ,
240
+ valueString : amountToSend . toString ( ) ,
241
+ value : amountToSend . toNumber ( ) ,
242
+ } ,
243
+ ] ;
244
+ const outputs = [
245
+ {
246
+ address : unsignedTransaction . outputs [ 0 ] . address ,
247
+ valueString : amountToSend . toString ( ) ,
248
+ coinName : walletCoin ,
249
+ } ,
250
+ ] ;
251
+ const spendAmount = amountToSend . toString ( ) ;
252
+ const parsedTx = { inputs : inputs , outputs : outputs , spendAmount : spendAmount , type : '' } ;
253
+ const feeInfo = { fee : 0 , feeString : '0' } ;
254
+ const transaction : MPCTx = {
255
+ serializedTx : serializedTx ,
256
+ scanIndex : index ,
257
+ coin : walletCoin ,
258
+ signableHex : unsignedTransaction . signablePayload . toString ( 'hex' ) ,
259
+ derivationPath : currPath ,
260
+ parsedTx : parsedTx ,
261
+ feeInfo : feeInfo ,
262
+ coinSpecific : { ...validityWindow , commonKeychain : bitgoKey } ,
263
+ } ;
264
+
265
+ const unsignedTx : MPCUnsignedTx = { unsignedTx : transaction , signatureShares : [ ] } ;
266
+ const transactions : MPCUnsignedTx [ ] = [ unsignedTx ] ;
267
+ const txRequest : RecoveryTxRequest = {
268
+ transactions : transactions ,
269
+ walletCoin : walletCoin ,
270
+ } ;
271
+ const txRequests : MPCSweepTxs = { txRequests : [ txRequest ] } ;
272
+ return txRequests ;
273
+ }
274
+
275
+ const transaction : MPCTx = { serializedTx : serializedTx , scanIndex : index } ;
276
+ return transaction ;
277
+ }
278
+
279
+ /** inherited doc */
280
+ async createBroadcastableSweepTransaction ( params : MPCSweepRecoveryOptions ) : Promise < MPCTxs > {
281
+ const req = params . signatureShares ;
282
+ const broadcastableTransactions : MPCTx [ ] = [ ] ;
283
+ let lastScanIndex = 0 ;
284
+
285
+ for ( let i = 0 ; i < req . length ; i ++ ) {
286
+ const MPC = await EDDSAMethods . getInitializedMpcInstance ( ) ;
287
+ const transaction = req [ i ] . txRequest . transactions [ 0 ] . unsignedTx ;
288
+ if ( ! req [ i ] . ovc || ! req [ i ] . ovc [ 0 ] . eddsaSignature ) {
289
+ throw new Error ( 'Missing signature(s)' ) ;
290
+ }
291
+ const signature = req [ i ] . ovc [ 0 ] . eddsaSignature ;
292
+ if ( ! transaction . signableHex ) {
293
+ throw new Error ( 'Missing signable hex' ) ;
294
+ }
295
+ const messageBuffer = Buffer . from ( transaction . signableHex ! , 'hex' ) ;
296
+ const result = MPC . verify ( messageBuffer , signature ) ;
297
+ if ( ! result ) {
298
+ throw new Error ( 'Invalid signature' ) ;
299
+ }
300
+ const signatureHex = Buffer . concat ( [ Buffer . from ( signature . R , 'hex' ) , Buffer . from ( signature . sigma , 'hex' ) ] ) ;
301
+ if (
302
+ ! transaction . coinSpecific ||
303
+ ! transaction . coinSpecific ?. firstValid ||
304
+ ! transaction . coinSpecific ?. maxDuration
305
+ ) {
306
+ throw new Error ( 'missing validity window' ) ;
307
+ }
308
+ const validityWindow = {
309
+ firstValid : transaction . coinSpecific ?. firstValid ,
310
+ maxDuration : transaction . coinSpecific ?. maxDuration ,
311
+ } ;
312
+ const material = await this . getMaterial ( ) ;
313
+ if ( ! transaction . coinSpecific ?. commonKeychain ) {
314
+ throw new Error ( 'Missing common keychain' ) ;
315
+ }
316
+ const commonKeychain = transaction . coinSpecific ! . commonKeychain ! as string ;
317
+ if ( ! transaction . derivationPath ) {
318
+ throw new Error ( 'Missing derivation path' ) ;
319
+ }
320
+ const derivationPath = transaction . derivationPath as string ;
321
+ const accountId = MPC . deriveUnhardened ( commonKeychain , derivationPath ) . slice ( 0 , 64 ) ;
322
+ const senderAddr = this . getAddressFromPublicKey ( accountId ) ;
323
+
324
+ const txnBuilder = this . getBuilder ( )
325
+ . material ( material )
326
+ . from ( transaction . serializedTx as string )
327
+ . sender ( { address : senderAddr } )
328
+ . validity ( validityWindow ) ;
329
+
330
+ const substrateKeyPair = new SubstrateKeyPair ( { pub : accountId } ) ;
331
+ txnBuilder . addSignature ( { pub : substrateKeyPair . getKeys ( ) . pub } , signatureHex ) ;
332
+ const signedTransaction = await txnBuilder . build ( ) ;
333
+ const serializedTx = signedTransaction . toBroadcastFormat ( ) ;
334
+
335
+ broadcastableTransactions . push ( {
336
+ serializedTx : serializedTx ,
337
+ scanIndex : transaction . scanIndex ,
338
+ } ) ;
339
+
340
+ if ( i === req . length - 1 && transaction . coinSpecific ! . lastScanIndex ) {
341
+ lastScanIndex = transaction . coinSpecific ! . lastScanIndex as number ;
342
+ }
343
+ }
344
+ return { transactions : broadcastableTransactions , lastScanIndex } ;
345
+ }
61
346
}
0 commit comments