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' ;
419import { BatchStakingBuilder } from './lib/batchStakingBuilder' ;
520import { BondExtraBuilder } from './lib/bondExtraBuilder' ;
621import { 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' ;
725
826export class Polyx extends SubstrateCoin {
927 protected readonly _staticsCoin : Readonly < StaticsBaseCoin > ;
@@ -17,10 +35,17 @@ export class Polyx extends SubstrateCoin {
1735 this . _staticsCoin = staticsCoin ;
1836 }
1937
38+ protected static nodeApiInitialized = false ;
39+ protected static API : ApiPromise ;
40+
2041 static createInstance ( bitgo : BitGoBase , staticsCoin ?: Readonly < StaticsBaseCoin > ) : BaseCoin {
2142 return new Polyx ( bitgo , staticsCoin ) ;
2243 }
2344
45+ getBuilder ( ) : TransactionBuilderFactory {
46+ return new TransactionBuilderFactory ( coins . get ( this . getChain ( ) ) ) ;
47+ }
48+
2449 /**
2550 * Factor between the coin's base unit and its smallest subdivison
2651 */
@@ -58,4 +83,264 @@ export class Polyx extends SubstrateCoin {
5883 protected getAddressFormat ( ) : number {
5984 return POLYX_ADDRESS_FORMAT ;
6085 }
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+ }
61346}
0 commit comments