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

Feat: New vault swap encoding RPC & bouncer test #5384

Merged
merged 18 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
47 changes: 1 addition & 46 deletions api/bin/chainflip-broker-api/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use cf_utilities::{
health::{self, HealthCheckOptions},
rpc::NumberOrHex,
task_scope::{task_scope, Scope},
try_parse_number_or_hex,
};
use chainflip_api::{
self,
primitives::{AccountRole, Affiliates, Asset, BasisPoints, CcmChannelMetadata, DcaParameters},
settings::StateChain,
AccountId32, AddressString, BrokerApi, OperatorApi, RefundParameters, StateChainApi,
SwapDepositAddress, SwapPayload, WithdrawFeesDetail,
SwapDepositAddress, WithdrawFeesDetail,
};
use clap::Parser;
use futures::FutureExt;
Expand Down Expand Up @@ -86,20 +84,6 @@ pub trait Rpc {
asset: Asset,
destination_address: AddressString,
) -> RpcResult<WithdrawFeesDetail>;

#[method(name = "request_swap_parameter_encoding", aliases = ["broker_requestSwapParameterEncoding"])]
async fn request_swap_parameter_encoding(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to keep this in the broker api, and pass it through to the custom rpc.

&self,
source_asset: Asset,
destination_asset: Asset,
destination_address: AddressString,
broker_commission: BasisPoints,
min_output_amount: NumberOrHex,
retry_duration: u32,
boost_fee: Option<BasisPoints>,
affiliate_fees: Option<Affiliates<AccountId32>>,
dca_parameters: Option<DcaParameters>,
) -> RpcResult<SwapPayload>;
}

pub struct RpcServerImpl {
Expand Down Expand Up @@ -165,35 +149,6 @@ impl RpcServer for RpcServerImpl {
) -> RpcResult<WithdrawFeesDetail> {
Ok(self.api.broker_api().withdraw_fees(asset, destination_address).await?)
}

async fn request_swap_parameter_encoding(
&self,
source_asset: Asset,
destination_asset: Asset,
destination_address: AddressString,
broker_commission: BasisPoints,
min_output_amount: NumberOrHex,
retry_duration: u32,
boost_fee: Option<BasisPoints>,
affiliate_fees: Option<Affiliates<AccountId32>>,
dca_parameters: Option<DcaParameters>,
) -> RpcResult<SwapPayload> {
Ok(self
.api
.broker_api()
.request_swap_parameter_encoding(
source_asset,
destination_asset,
destination_address,
broker_commission,
try_parse_number_or_hex(min_output_amount)?,
retry_duration,
boost_fee,
affiliate_fees,
dca_parameters,
)
.await?)
}
}

#[derive(Parser, Debug, Clone, Default)]
Expand Down
82 changes: 1 addition & 81 deletions api/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use cf_chains::{
address::{try_from_encoded_address, EncodedAddress},
btc::vault_swap_encoding::{
encode_swap_params_in_nulldata_utxo, SharedCfParameters, UtxoEncodedData,
},
dot::PolkadotAccountId,
evm::to_evm_address,
sol::SolAddress,
CcmChannelMetadata, ChannelRefundParametersEncoded, ChannelRefundParametersGeneric,
ForeignChain, ForeignChainAddress,
};
pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer};
use cf_primitives::{AssetAmount, BlockNumber, DcaParameters, NetworkEnvironment};
use cf_primitives::{DcaParameters, NetworkEnvironment};
use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME;
use pallet_cf_governance::ExecutionMode;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -111,11 +108,6 @@ pub async fn request_block(
.ok_or_else(|| anyhow!("unknown block hash"))
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SwapPayload {
Bitcoin { nulldata_utxo: Bytes },
}

pub struct StateChainApi {
pub state_chain_client: Arc<StateChainClient>,
}
Expand Down Expand Up @@ -510,78 +502,6 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st
self.simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {})
.await
}

async fn request_swap_parameter_encoding(
&self,
source_asset: Asset,
destination_asset: Asset,
destination_address: AddressString,
broker_commission: BasisPoints,
min_output_amount: AssetAmount,
retry_duration: BlockNumber,
boost_fee: Option<BasisPoints>,
affiliate_fees: Option<Affiliates<AccountId32>>,
dca_parameters: Option<DcaParameters>,
) -> Result<SwapPayload> {
// Check if safe mode is active
let block_hash = self.base_rpc_api().latest_finalized_block_hash().await?;
let safe_mode = self
.storage_value::<pallet_cf_environment::RuntimeSafeMode<state_chain_runtime::Runtime>>(
block_hash,
)
.await?;
if !safe_mode.swapping.swaps_enabled {
bail!("Safe mode is active. Swaps are disabled.");
}

// Validate params
frame_support::ensure!(
broker_commission == 0 && affiliate_fees.map_or(true, |fees| fees.is_empty()),
anyhow!("Broker/Affi fees are not yet supported for vault swaps. Request a deposit address or remove the broker fees.")
);
self.base_rpc_api()
.validate_refund_params(retry_duration, Some(block_hash))
.await?;
if let Some(params) = dca_parameters.as_ref() {
self.base_rpc_api()
.validate_dca_params(
params.number_of_chunks,
params.chunk_interval,
Some(block_hash),
)
.await?;
}

// Encode swap
match ForeignChain::from(source_asset) {
ForeignChain::Bitcoin => {
let params = UtxoEncodedData {
output_asset: destination_asset,
output_address: destination_address
.try_parse_to_encoded_address(destination_asset.into())?,
parameters: SharedCfParameters {
retry_duration: retry_duration.try_into()?,
min_output_amount,
number_of_chunks: dca_parameters
.as_ref()
.map(|params| params.number_of_chunks)
.unwrap_or(1)
.try_into()?,
chunk_interval: dca_parameters
.as_ref()
.map(|params| params.chunk_interval)
.unwrap_or(2)
.try_into()?,
boost_fee: boost_fee.unwrap_or_default().try_into()?,
},
};
Ok(SwapPayload::Bitcoin {
nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(),
})
},
_ => bail!("Unsupported input asset"),
}
}
}

#[async_trait]
Expand Down
39 changes: 8 additions & 31 deletions bouncer/commands/create_raw_btc_tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,26 @@
// Constructs a very simple Raw BTC transaction. Can be used for manual testing a raw broadcast for example.
// Usage: ./commands/create_raw_btc_tx.ts <bitcoin_address> <btc_amount>

import Client from 'bitcoin-core';
import { BTC_ENDPOINT, btcClient, selectInputs } from '../shared/send_btc';

const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332';
console.log(`BTC_ENDPOINT is set to '${BTC_ENDPOINT}'`);

const client = new Client({
host: BTC_ENDPOINT.split(':')[1].slice(2),
port: Number(BTC_ENDPOINT.split(':')[2]),
username: 'flip',
password: 'flip',
wallet: 'whale',
});
console.log(`Btc endpoint is set to '${BTC_ENDPOINT}'`);

const createRawTransaction = async (toAddress: string, amountInBtc: number | string) => {
try {
// Inputs and outputs
const feeInBtc = 0.00001;

// List unspent UTXOs
const utxos = await client.listUnspent();

const utxo = utxos.find((u) => u.amount >= Number(amountInBtc) + feeInBtc);
if (!utxo) throw new Error('Insufficient funds');

// Prepare the transaction inputs and outputs
const inputs = [
{
txid: utxo.txid,
vout: utxo.vout,
},
];

const changeAmount = utxo.amount - Number(amountInBtc) - feeInBtc;
const changeAddress = await client.getNewAddress();
const { inputs, change } = await selectInputs(Number(amountInBtc) + feeInBtc);
const changeAddress = await btcClient.getNewAddress();
const outputs = {
[toAddress]: amountInBtc,
[changeAddress]: changeAmount,
[changeAddress]: change,
};

// Create the raw transaction
const rawTx = await client.createRawTransaction(inputs, outputs);
const rawTx = await btcClient.createRawTransaction(inputs, outputs);

// Sign the raw transaction
const signedTx = await client.signRawTransactionWithWallet(rawTx);
const signedTx = await btcClient.signRawTransactionWithWallet(rawTx);

// Here's your raw signed transaction
console.log('Raw signed transaction:', signedTx.hex);
Expand Down
2 changes: 2 additions & 0 deletions bouncer/commands/run_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { testDeltaBasedIngress } from '../tests/delta_based_ingress';
import { testCancelOrdersBatch } from '../tests/create_and_delete_multiple_orders';
import { depositChannelCreation } from '../tests/request_swap_deposit_address_with_affiliates';
import { testDCASwaps } from '../tests/DCA_test';
import { testBtcVaultSwap } from '../tests/btc_vault_swap';

async function main() {
const testName = process.argv[2];
Expand Down Expand Up @@ -67,6 +68,7 @@ async function main() {
testDeltaBasedIngress,
testCancelOrdersBatch,
depositChannelCreation,
testBtcVaultSwap,
];

// Help message
Expand Down
59 changes: 42 additions & 17 deletions bouncer/shared/send_btc.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
import Client from 'bitcoin-core';
import { sleep, btcClientMutex } from './utils';

export async function sendBtc(address: string, amount: number | string) {
const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332';
const client = new Client({
host: BTC_ENDPOINT.split(':')[1].slice(2),
port: Number(BTC_ENDPOINT.split(':')[2]),
username: 'flip',
password: 'flip',
wallet: 'whale',
});
export const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332';

// Btc client has a limit on the number of concurrent requests
const txid = await btcClientMutex.runExclusive(async () =>
client.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1),
);
export const btcClient = new Client({
host: BTC_ENDPOINT.split(':')[1].slice(2),
port: Number(BTC_ENDPOINT.split(':')[2]),
username: 'flip',
password: 'flip',
wallet: 'whale',
});

for (let i = 0; i < 50; i++) {
const transactionDetails = await client.getTransaction(txid);
export async function selectInputs(amount: number) {
// List unspent UTXOs
const utxos = await btcClient.listUnspent();

// Find a UTXO with enough funds
const utxo = utxos.find((u) => u.amount >= amount);
if (!utxo) throw new Error('Insufficient funds');
// TODO: be able to select more than one UTXO

const change = utxo.amount - amount;

// Prepare the transaction inputs and outputs
const inputs = [
{
txid: utxo.txid,
vout: utxo.vout,
},
];

return { inputs, change };
}

const confirmations = transactionDetails.confirmations;
export async function waitForBtcTransaction(txid: string, confirmations = 1) {
for (let i = 0; i < 50; i++) {
const transactionDetails = await btcClient.getTransaction(txid);

if (confirmations < 1) {
if (transactionDetails.confirmations < confirmations) {
await sleep(1000);
} else {
return;
}
}
}

export async function sendBtc(address: string, amount: number | string) {
// Btc client has a limit on the number of concurrent requests
const txid = (await btcClientMutex.runExclusive(async () =>
btcClient.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1),
)) as string;

await waitForBtcTransaction(txid);
}
Loading
Loading