Skip to content

Commit e40f377

Browse files
authored
Merge pull request #5351 from BitGo/coin-2417_remove_fees
fix(sdk-coin-rune): add native transaction fees for wallet recovery
2 parents 49dae73 + 140f0ce commit e40f377

File tree

3 files changed

+130
-7
lines changed

3 files changed

+130
-7
lines changed

modules/sdk-coin-rune/src/lib/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ export const testnetValidatorAddressRegex = /^(sthor)1(['qpzry9x8gf2tvdw0s3jn54k
99
export const TESTNET_ADDRESS_PREFIX = 'sthor';
1010

1111
export const GAS_LIMIT = 200000;
12-
export const GAS_AMOUNT = '150000';
12+
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
13+
export const RUNE_FEES = '2000000'; // https://dev.thorchain.org/concepts/fees.html#thorchain-native-rune
14+
export const ROOT_PATH = 'm/0';

modules/sdk-coin-rune/src/rune.ts

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
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';
320
import { BaseCoin as StaticsBaseCoin, BaseUnit, coins } from '@bitgo/statics';
421
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';
623
import { RuneUtils } from './lib/utils';
724
import { BigNumber } from 'bignumber.js';
825
const bech32 = require('bech32-buffer');
926
import * as _ from 'lodash';
27+
import { Coin } from '@cosmjs/stargate';
28+
import { createHash } from 'crypto';
1029

1130
export class Rune extends CosmosCoin {
1231
protected readonly _utils: RuneUtils;
@@ -106,4 +125,107 @@ export class Rune extends CosmosCoin {
106125
}
107126
return true;
108127
}
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+
}
109231
}

modules/sdk-coin-rune/test/unit/rune.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const bech32 = require('bech32-buffer');
1111
import should = require('should');
1212
import { beforeEach } from 'mocha';
1313
import { CosmosTransaction, SendMessage } from '@bitgo/abstract-cosmos';
14-
import { GAS_AMOUNT } from '../../src/lib/constants';
1514

1615
describe('Rune', function () {
1716
let bitgo: TestBitGoAPI;
@@ -272,7 +271,7 @@ describe('Rune', function () {
272271
describe('Recover transaction: success path', () => {
273272
const sandBox = sinon.createSandbox();
274273
const coin = coins.get('tthorchain:rune');
275-
const testBalance = '1500000';
274+
const testBalance = '15000000';
276275
const testAccountNumber = '123';
277276
const testSequenceNumber = '0';
278277
const testChainId = 'thorchain-stagenet-2';
@@ -312,7 +311,7 @@ describe('Rune', function () {
312311
const truneTxnJson = truneTxn.toJson();
313312
const sendMessage = truneTxnJson.sendMessages[0].value as SendMessage;
314313
const balance = new BigNumber(testBalance);
315-
const actualBalance = balance.minus(new BigNumber(GAS_AMOUNT));
314+
const actualBalance = balance.minus('2000000'); // native rune transaction fees
316315
should.equal(sendMessage.amount[0].amount, actualBalance.toFixed());
317316
});
318317
});

0 commit comments

Comments
 (0)