|
1 | | -import { CosmosCoin, CosmosKeyPair, GasAmountDetails } from '@bitgo/abstract-cosmos'; |
2 | | -import { BaseCoin, BitGoBase, Environments, TransactionType, VerifyTransactionOptions } from '@bitgo/sdk-core'; |
| 1 | +import { |
| 2 | + CosmosCoin, |
| 3 | + CosmosKeyPair, |
| 4 | + CosmosLikeCoinRecoveryOutput, |
| 5 | + CosmosTransaction, |
| 6 | + FeeData, |
| 7 | + GasAmountDetails, |
| 8 | + RecoveryOptions, |
| 9 | + SendMessage, |
| 10 | +} from '@bitgo/abstract-cosmos'; |
| 11 | +import { |
| 12 | + BaseCoin, |
| 13 | + BitGoBase, |
| 14 | + Ecdsa, |
| 15 | + ECDSAUtils, |
| 16 | + Environments, |
| 17 | + TransactionType, |
| 18 | + VerifyTransactionOptions, |
| 19 | +} from '@bitgo/sdk-core'; |
3 | 20 | import { BaseCoin as StaticsBaseCoin, BaseUnit, coins } from '@bitgo/statics'; |
4 | 21 | import { KeyPair, TransactionBuilderFactory } from './lib'; |
5 | | -import { GAS_AMOUNT, GAS_LIMIT } from './lib/constants'; |
| 22 | +import { GAS_AMOUNT, GAS_LIMIT, RUNE_FEES, ROOT_PATH } from './lib/constants'; |
6 | 23 | import { RuneUtils } from './lib/utils'; |
7 | 24 | import { BigNumber } from 'bignumber.js'; |
8 | 25 | const bech32 = require('bech32-buffer'); |
9 | 26 | import * as _ from 'lodash'; |
| 27 | +import { Coin } from '@cosmjs/stargate'; |
| 28 | +import { createHash } from 'crypto'; |
10 | 29 |
|
11 | 30 | export class Rune extends CosmosCoin { |
12 | 31 | protected readonly _utils: RuneUtils; |
@@ -106,4 +125,107 @@ export class Rune extends CosmosCoin { |
106 | 125 | } |
107 | 126 | return true; |
108 | 127 | } |
| 128 | + |
| 129 | + getNativeRuneTxnFees(): string { |
| 130 | + return RUNE_FEES; |
| 131 | + } |
| 132 | + |
| 133 | + /** |
| 134 | + * This function is overridden from CosmosCoin class' recover function due to the difference in fees handling in thorchain |
| 135 | + * @param {RecoveryOptions} params parameters needed to construct and |
| 136 | + * (maybe) sign the transaction |
| 137 | + * |
| 138 | + * @returns {CosmosLikeCoinRecoveryOutput} the serialized transaction hex string and index |
| 139 | + * of the address being swept |
| 140 | + */ |
| 141 | + async recover(params: RecoveryOptions): Promise<CosmosLikeCoinRecoveryOutput> { |
| 142 | + // Step 1: Check if params contains the required parameters |
| 143 | + |
| 144 | + if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) { |
| 145 | + throw new Error('invalid recoveryDestination'); |
| 146 | + } |
| 147 | + |
| 148 | + if (!params.userKey) { |
| 149 | + throw new Error('missing userKey'); |
| 150 | + } |
| 151 | + |
| 152 | + if (!params.backupKey) { |
| 153 | + throw new Error('missing backupKey'); |
| 154 | + } |
| 155 | + |
| 156 | + if (!params.walletPassphrase) { |
| 157 | + throw new Error('missing wallet passphrase'); |
| 158 | + } |
| 159 | + |
| 160 | + // Step 2: Fetch the bitgo key from params |
| 161 | + const userKey = params.userKey.replace(/\s/g, ''); |
| 162 | + const backupKey = params.backupKey.replace(/\s/g, ''); |
| 163 | + |
| 164 | + const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( |
| 165 | + userKey, |
| 166 | + backupKey, |
| 167 | + params.walletPassphrase |
| 168 | + ); // baseAddress is not extracted |
| 169 | + // Step 3: Instantiate the ECDSA signer and fetch the address details |
| 170 | + const MPC = new Ecdsa(); |
| 171 | + const chainId = await this.getChainId(); |
| 172 | + const publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66); |
| 173 | + const senderAddress = this.getAddressFromPublicKey(publicKey); |
| 174 | + |
| 175 | + // Step 4: Fetch account details such as accountNo, balance and check for sufficient funds once gasAmount has been deducted |
| 176 | + const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress); |
| 177 | + const balance = new BigNumber(await this.getAccountBalance(senderAddress)); |
| 178 | + const gasBudget: FeeData = { |
| 179 | + amount: [{ denom: this.getDenomination(), amount: this.getGasAmountDetails().gasAmount }], |
| 180 | + gasLimit: this.getGasAmountDetails().gasLimit, |
| 181 | + }; |
| 182 | + const actualBalance = balance.minus(this.getNativeRuneTxnFees()); |
| 183 | + |
| 184 | + if (actualBalance.isLessThanOrEqualTo(0)) { |
| 185 | + throw new Error('Did not have enough funds to recover'); |
| 186 | + } |
| 187 | + |
| 188 | + // Step 5: Once sufficient funds are present, construct the recover tx messsage |
| 189 | + const amount: Coin[] = [ |
| 190 | + { |
| 191 | + denom: this.getDenomination(), |
| 192 | + amount: actualBalance.toFixed(), |
| 193 | + }, |
| 194 | + ]; |
| 195 | + const sendMessage: SendMessage[] = [ |
| 196 | + { |
| 197 | + fromAddress: senderAddress, |
| 198 | + toAddress: params.recoveryDestination, |
| 199 | + amount: amount, |
| 200 | + }, |
| 201 | + ]; |
| 202 | + |
| 203 | + // Step 6: Build the unsigned tx using the constructed message |
| 204 | + const txnBuilder = this.getBuilder().getTransferBuilder(); |
| 205 | + txnBuilder |
| 206 | + .messages(sendMessage) |
| 207 | + .gasBudget(gasBudget) |
| 208 | + .publicKey(publicKey) |
| 209 | + .sequence(Number(sequenceNo)) |
| 210 | + .accountNumber(Number(accountNumber)) |
| 211 | + .chainId(chainId); |
| 212 | + const unsignedTransaction = (await txnBuilder.build()) as CosmosTransaction; |
| 213 | + let serializedTx = unsignedTransaction.toBroadcastFormat(); |
| 214 | + const signableHex = unsignedTransaction.signablePayload.toString('hex'); |
| 215 | + |
| 216 | + // Step 7: Sign the tx |
| 217 | + const message = unsignedTransaction.signablePayload; |
| 218 | + const messageHash = createHash('sha256').update(message).digest(); |
| 219 | + |
| 220 | + const signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain); |
| 221 | + |
| 222 | + const signableBuffer = Buffer.from(signableHex, 'hex'); |
| 223 | + MPC.verify(signableBuffer, signature, this.getHashFunction()); |
| 224 | + const cosmosKeyPair = this.getKeyPair(publicKey); |
| 225 | + txnBuilder.addSignature({ pub: cosmosKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex')); |
| 226 | + const signedTransaction = await txnBuilder.build(); |
| 227 | + serializedTx = signedTransaction.toBroadcastFormat(); |
| 228 | + |
| 229 | + return { serializedTx: serializedTx }; |
| 230 | + } |
109 | 231 | } |
0 commit comments