Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sdk-coin-rune): add native transaction fees for wallet recovery #5351

Merged
merged 1 commit into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion modules/sdk-coin-rune/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export const testnetValidatorAddressRegex = /^(sthor)1(['qpzry9x8gf2tvdw0s3jn54k
export const TESTNET_ADDRESS_PREFIX = 'sthor';

export const GAS_LIMIT = 200000;
export const GAS_AMOUNT = '150000';
export const GAS_AMOUNT = '0'; // Gas amount should be zero for RUNE transactions, as fees (0.02 RUNE) is cut from sender balance directly in the transaction
export const RUNE_FEES = '2000000'; // https://dev.thorchain.org/concepts/fees.html#thorchain-native-rune
export const ROOT_PATH = 'm/0';
128 changes: 125 additions & 3 deletions modules/sdk-coin-rune/src/rune.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { CosmosCoin, CosmosKeyPair, GasAmountDetails } from '@bitgo/abstract-cosmos';
import { BaseCoin, BitGoBase, Environments, TransactionType, VerifyTransactionOptions } from '@bitgo/sdk-core';
import {
CosmosCoin,
CosmosKeyPair,
CosmosLikeCoinRecoveryOutput,
CosmosTransaction,
FeeData,
GasAmountDetails,
RecoveryOptions,
SendMessage,
} from '@bitgo/abstract-cosmos';
import {
BaseCoin,
BitGoBase,
Ecdsa,
ECDSAUtils,
Environments,
TransactionType,
VerifyTransactionOptions,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, BaseUnit, coins } from '@bitgo/statics';
import { KeyPair, TransactionBuilderFactory } from './lib';
import { GAS_AMOUNT, GAS_LIMIT } from './lib/constants';
import { GAS_AMOUNT, GAS_LIMIT, RUNE_FEES, ROOT_PATH } from './lib/constants';
import { RuneUtils } from './lib/utils';
import { BigNumber } from 'bignumber.js';
const bech32 = require('bech32-buffer');
import * as _ from 'lodash';
import { Coin } from '@cosmjs/stargate';
import { createHash } from 'crypto';

export class Rune extends CosmosCoin {
protected readonly _utils: RuneUtils;
Expand Down Expand Up @@ -106,4 +125,107 @@
}
return true;
}

getNativeRuneTxnFees(): string {
return RUNE_FEES;
}

/**
* This function is overridden from CosmosCoin class' recover function due to the difference in fees handling in thorchain
* @param {RecoveryOptions} params parameters needed to construct and
* (maybe) sign the transaction
*
* @returns {CosmosLikeCoinRecoveryOutput} the serialized transaction hex string and index
* of the address being swept
*/
async recover(params: RecoveryOptions): Promise<CosmosLikeCoinRecoveryOutput> {
// Step 1: Check if params contains the required parameters

if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
throw new Error('invalid recoveryDestination');
}

if (!params.userKey) {
throw new Error('missing userKey');
}

if (!params.backupKey) {
throw new Error('missing backupKey');
}

if (!params.walletPassphrase) {
throw new Error('missing wallet passphrase');
}

// Step 2: Fetch the bitgo key from params
const userKey = params.userKey.replace(/\s/g, '');
const backupKey = params.backupKey.replace(/\s/g, '');

const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
userKey,
backupKey,
params.walletPassphrase
); // baseAddress is not extracted
// Step 3: Instantiate the ECDSA signer and fetch the address details
const MPC = new Ecdsa();
const chainId = await this.getChainId();
const publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66);
const senderAddress = this.getAddressFromPublicKey(publicKey);

// Step 4: Fetch account details such as accountNo, balance and check for sufficient funds once gasAmount has been deducted
const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress);
const balance = new BigNumber(await this.getAccountBalance(senderAddress));
const gasBudget: FeeData = {
amount: [{ denom: this.getDenomination(), amount: this.getGasAmountDetails().gasAmount }],
gasLimit: this.getGasAmountDetails().gasLimit,
};
const actualBalance = balance.minus(this.getNativeRuneTxnFees());

if (actualBalance.isLessThanOrEqualTo(0)) {
throw new Error('Did not have enough funds to recover');
}

// Step 5: Once sufficient funds are present, construct the recover tx messsage
const amount: Coin[] = [
{
denom: this.getDenomination(),
amount: actualBalance.toFixed(),
},
];
const sendMessage: SendMessage[] = [
{
fromAddress: senderAddress,
toAddress: params.recoveryDestination,
amount: amount,
},
];

// Step 6: Build the unsigned tx using the constructed message
const txnBuilder = this.getBuilder().getTransferBuilder();
txnBuilder
.messages(sendMessage)
.gasBudget(gasBudget)
.publicKey(publicKey)
.sequence(Number(sequenceNo))
.accountNumber(Number(accountNumber))
.chainId(chainId);
const unsignedTransaction = (await txnBuilder.build()) as CosmosTransaction;
let serializedTx = unsignedTransaction.toBroadcastFormat();
Dismissed Show dismissed Hide dismissed
const signableHex = unsignedTransaction.signablePayload.toString('hex');

// Step 7: Sign the tx
const message = unsignedTransaction.signablePayload;
const messageHash = createHash('sha256').update(message).digest();

const signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain);

const signableBuffer = Buffer.from(signableHex, 'hex');
MPC.verify(signableBuffer, signature, this.getHashFunction());
const cosmosKeyPair = this.getKeyPair(publicKey);
txnBuilder.addSignature({ pub: cosmosKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex'));
const signedTransaction = await txnBuilder.build();
serializedTx = signedTransaction.toBroadcastFormat();

return { serializedTx: serializedTx };
}
}
5 changes: 2 additions & 3 deletions modules/sdk-coin-rune/test/unit/rune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const bech32 = require('bech32-buffer');
import should = require('should');
import { beforeEach } from 'mocha';
import { CosmosTransaction, SendMessage } from '@bitgo/abstract-cosmos';
import { GAS_AMOUNT } from '../../src/lib/constants';

describe('Rune', function () {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -272,7 +271,7 @@ describe('Rune', function () {
describe('Recover transaction: success path', () => {
const sandBox = sinon.createSandbox();
const coin = coins.get('tthorchain:rune');
const testBalance = '1500000';
const testBalance = '15000000';
const testAccountNumber = '123';
const testSequenceNumber = '0';
const testChainId = 'thorchain-stagenet-2';
Expand Down Expand Up @@ -312,7 +311,7 @@ describe('Rune', function () {
const truneTxnJson = truneTxn.toJson();
const sendMessage = truneTxnJson.sendMessages[0].value as SendMessage;
const balance = new BigNumber(testBalance);
const actualBalance = balance.minus(new BigNumber(GAS_AMOUNT));
const actualBalance = balance.minus('2000000'); // native rune transaction fees
should.equal(sendMessage.amount[0].amount, actualBalance.toFixed());
});
});
Expand Down
Loading