diff --git a/lib/2wp-utils.js b/lib/2wp-utils.js index 9a915188..5c36ded3 100644 --- a/lib/2wp-utils.js +++ b/lib/2wp-utils.js @@ -7,10 +7,11 @@ const { waitForRskMempoolToGetNewTxs, waitAndUpdateBridge } = require('./rsk-utils'); -const { retryWithCheck } = require('./utils'); +const { retryWithCheck, ensure0x } = require('./utils'); const { waitForBitcoinTxToBeInMempool, waitForBitcoinMempoolToGetTxs } = require('./btc-utils'); const { getBridge } = require('./precompiled-abi-forks-util'); const { getBridgeState } = require('@rsksmart/bridge-state-data-parser'); +const { getDerivedRSKAddressInformation } = require('@rsksmart/btc-rsk-derivation'); const btcEthUnitConverter = require('@rsksmart/btc-eth-unit-converter'); const peginVerifier = require('pegin-address-verificator'); @@ -198,7 +199,7 @@ const ensurePeginIsRegistered = async (rskTxHelper, peginBtcTxHash, expectedUtxo * @param {RskTransactionHelper} rskTxHelper * @param {BtcTransactionHelper} btcTxHelper * @param {number} amountInBtc - * @returns {string} the pegin tx hash + * @returns {Promise} the pegin tx hash */ const donateToBridge = async (rskTxHelper, btcTxHelper, donatingBtcAddressInformation, amountInBtc) => { const data = []; @@ -227,6 +228,59 @@ const disableWhitelisting = async (rskTxHelper, btcTxHelper, blockDelay = 1) => } }; +/** + * Creates a btc sender and rsk recipient information (private keys and addresses) and funds the btc sender address with the specified amount. + * @param {RskTransactionHelper} rskTxHelper to make transactions to the rsk network. + * @param {BtcTransactionHelper} btcTxHelper to make transactions to the bitcoin network. + * @param {string} type the btc address type to generate. Defaults to 'legacy'. + * @param {number} initialAmountToFundInBtc the initial amount to fund the btc sender address. Defaults to 1. + * @returns {Promise<{btcSenderAddressInfo: {address: string, privateKey: string}, rskRecipientRskAddressInfo: {address: string, privateKey: string}>}} + */ +const createSenderRecipientInfo = async (rskTxHelper, btcTxHelper, type = 'legacy', initialAmountToFundInBtc = 1) => { + const btcSenderAddressInfo = await btcTxHelper.generateBtcAddress(type); + const rskRecipientRskAddressInfo = getDerivedRSKAddressInformation(btcSenderAddressInfo.privateKey, btcTxHelper.btcConfig.network); + await rskTxHelper.importAccount(rskRecipientRskAddressInfo.privateKey); + await rskTxHelper.unlockAccount(rskRecipientRskAddressInfo.address); + initialAmountToFundInBtc && await btcTxHelper.fundAddress(btcSenderAddressInfo.address, initialAmountToFundInBtc); + return { + btcSenderAddressInfo, + rskRecipientRskAddressInfo + }; +}; + +/** + * Creates a pegin_btc event with the specified parameters. + * @param {Object} partialExpectedEvent an object with some pegin_btc event default values. + * @param {string} rskRecipientRskAddress the rsk address that receives the funds expected to be in the event. + * @param {string} btcPeginTxHash the pegin btc tx hash expected to be in the event. + * @param {number} peginValueInSatoshis the pegin value in satoshis expected to be in the event. + * @param {string} protocolVersion the pegin protocol version expected to be in the event. Defaults to '0'. + * @returns {BridgeEvent} + */ +const createExpectedPeginBtcEvent = (partialExpectedEvent, rskRecipientRskAddress, btcPeginTxHash, peginValueInSatoshis, protocolVersion = '0') => { + const expectedEvent = { + ...partialExpectedEvent, + arguments: { + receiver: ensure0x(rskRecipientRskAddress), + btcTxHash: ensure0x(btcPeginTxHash), + amount: `${peginValueInSatoshis}`, + protocolVersion, + }, + } + return expectedEvent; +}; + +/** + * Gets the Bridge state and sums the utxos amount + * @param {RskTransactionHelper} rskTxHelper to make transactions to the rsk network + * @returns {Promise} the sum of the utxos in the Bridge + */ +const getBridgeUtxosBalance = async (rskTxHelper) => { + const bridgeState = await getBridgeState(rskTxHelper.getClient()); + const utxosSum = bridgeState.activeFederationUtxos.reduce((sum, utxo) => sum + utxo.valueInSatoshis, 0); + return utxosSum; +}; + module.exports = { sendTxToBridge, assertRefundUtxosSameAsPeginUtxos, @@ -240,4 +294,7 @@ module.exports = { mineForPeginRegistration, MIN_PEGOUT_VALUE_IN_RBTC, disableWhitelisting, + createSenderRecipientInfo, + createExpectedPeginBtcEvent, + getBridgeUtxosBalance, }; diff --git a/lib/assertions/2wp.js b/lib/assertions/2wp.js index f6fa6be7..7a9081fe 100644 --- a/lib/assertions/2wp.js +++ b/lib/assertions/2wp.js @@ -2,6 +2,7 @@ const expect = require('chai').expect; var {wait, removePrefix0x} = require('../utils'); var bitcoin = require('peglib').bitcoin; var rsk = require('peglib').rsk; + const {MAX_ESTIMATED_FEE_PER_PEGOUT, FEE_DIFFERENCE_PER_PEGOUT} = require('../constants'); const {encodeOutpointValuesAsMap, decodeOutpointValues} = require("../varint"); @@ -140,5 +141,5 @@ module.exports = { assertLock: assertLock(btcClient, rskClient, pegClient), }), assertCallToPegoutBatchingBridgeMethods, - assertRejectedPeginEvent + assertRejectedPeginEvent, }; diff --git a/lib/btc-utils.js b/lib/btc-utils.js index 19556ea9..0e97fe9b 100644 --- a/lib/btc-utils.js +++ b/lib/btc-utils.js @@ -3,6 +3,7 @@ const merkleLib = require('merkle-lib'); const pmtBuilder = require('@rsksmart/pmt-builder'); const { retryWithCheck } = require('./utils'); const { getLogger } = require('../logger'); +const { btcToSatoshis } = require('@rsksmart/btc-eth-unit-converter'); const logger = getLogger(); @@ -181,10 +182,15 @@ const waitForBitcoinMempoolToGetTxs = async (btcTxHelper, maxAttempts = 3, check return bitcoinMempoolHasTx; } + const getBtcAddressBalanceInSatoshis = async (btcTxHelper, btcAddress) => { + return Number(btcToSatoshis(await btcTxHelper.getAddressBalance(btcAddress))); + }; + module.exports = { publicKeyToCompressed, fundAddressAndGetData, getBitcoinTransactionsInMempool, waitForBitcoinTxToBeInMempool, - waitForBitcoinMempoolToGetTxs + waitForBitcoinMempoolToGetTxs, + getBtcAddressBalanceInSatoshis, } diff --git a/lib/constants.js b/lib/constants.js index 193f6fd5..32ab0936 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -55,7 +55,14 @@ const PEGOUT_EVENTS = { BATCH_PEGOUT_CREATED: "batch_pegout_created", PEGOUT_TRANSACTION_CREATED: "pegout_transaction_created", PEGOUT_CONFIRMED: "pegout_confirmed" -} +}; + +const PEGIN_EVENTS = { + PEGIN_BTC: { + name: "pegin_btc", + signature: '0x44cdc782a38244afd68336ab92a0b39f864d6c0b2a50fa1da58cafc93cd2ae5a' + } +}; module.exports = { KEY_TYPE_BTC, @@ -77,4 +84,5 @@ module.exports = { PEGOUT_EVENTS, FUNDS_MIGRATION_AGE_SINCE_ACTIVATION_BEGIN, FUNDS_MIGRATION_AGE_SINCE_ACTIVATION_END, + PEGIN_EVENTS, }; diff --git a/lib/tests/2wp.js b/lib/tests/2wp.js new file mode 100644 index 00000000..d866bb87 --- /dev/null +++ b/lib/tests/2wp.js @@ -0,0 +1,104 @@ +const expect = require('chai').expect; +const { getBridge } = require('../precompiled-abi-forks-util'); +const { getBtcClient } = require('../btc-client-provider'); +const { getRskTransactionHelpers, getRskTransactionHelper } = require('../rsk-tx-helper-provider'); +const { satoshisToBtc, btcToSatoshis, satoshisToWeis } = require('@rsksmart/btc-eth-unit-converter'); +const { findEventInBlock } = require('../rsk-utils'); +const { PEGIN_EVENTS } = require("../constants"); +const { sendPegin, + ensurePeginIsRegistered, + createSenderRecipientInfo, + createExpectedPeginBtcEvent, + BRIDGE_ADDRESS, + getBridgeUtxosBalance, +} = require('../2wp-utils'); +const { ensure0x } = require('../utils'); +const { getBtcAddressBalanceInSatoshis } = require('../btc-utils'); + +let btcTxHelper; +let rskTxHelper; +let rskTxHelpers; +let bridge; +let federationAddress; +let minimumPeginValueInSatoshis; +let minimumPeginValueInBtc; +let btcFeeInSatoshis; + +const execute = (description, getRskHost) => { + + describe(description, () => { + + before(async () => { + + rskTxHelpers = getRskTransactionHelpers(); + btcTxHelper = getBtcClient(); + rskTxHelper = getRskTransactionHelper(getRskHost()); + bridge = getBridge(rskTxHelper.getClient()); + + federationAddress = await bridge.methods.getFederationAddress().call(); + minimumPeginValueInSatoshis = Number(await bridge.methods.getMinimumLockTxValue().call()); + minimumPeginValueInBtc = Number(satoshisToBtc(minimumPeginValueInSatoshis)); + btcFeeInSatoshis = btcToSatoshis(await btcTxHelper.getFee()); + + await btcTxHelper.importAddress(federationAddress, 'federation'); + + }); + + it('should do a basic legacy pegin with the exact minimum value', async () => { + + // Arrange + + const initialBridgeBalance = Number(await rskTxHelper.getBalance(BRIDGE_ADDRESS)); + const initialBridgeUtxosBalance = await getBridgeUtxosBalance(rskTxHelper); + const initialFederationAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, federationAddress); + const senderRecipientInfo = await createSenderRecipientInfo(rskTxHelper, btcTxHelper); + const initialSenderAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo.btcSenderAddressInfo.address); + const peginValueInSatoshis = minimumPeginValueInSatoshis; + + // Act + + const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis)); + await ensurePeginIsRegistered(rskTxHelper, btcPeginTxHash); + + // Assert + + // The btc pegin tx is already marked as processed by the bridge + const isBtcTxHashAlreadyProcessed = await bridge.methods.isBtcTxHashAlreadyProcessed(btcPeginTxHash).call(); + expect(isBtcTxHashAlreadyProcessed).to.be.true; + + // The pegin_btc event is emitted with the expected values + const recipient1RskAddressChecksumed = rskTxHelper.getClient().utils.toChecksumAddress(ensure0x(senderRecipientInfo.rskRecipientRskAddressInfo.address)); + const expectedEvent = createExpectedPeginBtcEvent(PEGIN_EVENTS.PEGIN_BTC, recipient1RskAddressChecksumed, btcPeginTxHash, peginValueInSatoshis); + const btcTxHashProcessedHeight = Number(await bridge.methods.getBtcTxHashProcessedHeight(btcPeginTxHash).call()); + const peginBtcEvent = await findEventInBlock(rskTxHelper, expectedEvent.name, btcTxHashProcessedHeight); + expect(peginBtcEvent).to.be.deep.equal(expectedEvent); + + // The federation balance is increased by the pegin value + const finalFederationAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, federationAddress); + expect(finalFederationAddressBalanceInSatoshis).to.be.equal(initialFederationAddressBalanceInSatoshis + peginValueInSatoshis); + + // The sender address balance is decreased by the pegin value and the btc fee + const finalSenderAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo.btcSenderAddressInfo.address); + expect(finalSenderAddressBalanceInSatoshis).to.be.equal(initialSenderAddressBalanceInSatoshis - peginValueInSatoshis - btcFeeInSatoshis); + + // The recipient rsk address balance is increased by the pegin value + const finalRskRecipientBalance = Number(await rskTxHelper.getBalance(senderRecipientInfo.rskRecipientRskAddressInfo.address)); + expect(finalRskRecipientBalance).to.be.equal(Number(satoshisToWeis(peginValueInSatoshis))); + + // After the successful pegin, the Bridge balance should be reduced by the pegin value + const finalBridgeBalance = Number(await rskTxHelper.getBalance(BRIDGE_ADDRESS)); + expect(finalBridgeBalance).to.be.equal(initialBridgeBalance - satoshisToWeis(peginValueInSatoshis)); + + // After the successful pegin, the Bridge utxos sum should be incremented by the pegin value + const finalBridgeUtxosBalance = await getBridgeUtxosBalance(rskTxHelper); + expect(finalBridgeUtxosBalance).to.be.equal(initialBridgeUtxosBalance + peginValueInSatoshis); + + }); + + }); + +} + +module.exports = { + execute, +}; diff --git a/tests/01_01_01-2wp.js b/tests/01_01_01-2wp.js new file mode 100644 index 00000000..5e39c8c5 --- /dev/null +++ b/tests/01_01_01-2wp.js @@ -0,0 +1,3 @@ +const twoWpTests = require('../lib/tests/2wp'); + +twoWpTests.execute('BTC <=> RSK 2WP', () => Runners.hosts.federate.host);