Skip to content

Commit

Permalink
Merge pull request #836 from BoltzExchange/document-rescue
Browse files Browse the repository at this point in the history
docs: add rescue endpoint to swagger
  • Loading branch information
michael1011 authored Mar 3, 2025
2 parents 806ffa6 + 116bf9f commit 94692d3
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 10 deletions.
1 change: 1 addition & 0 deletions boltzr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ diesel = { version = "2.2.7", default-features = false, features = [
"r2d2",
"chrono",
"serde_json",
"32-column-tables",
] }
strum_macros = "0.27.1"
strum = "0.27.1"
Expand Down
12 changes: 10 additions & 2 deletions boltzr/src/api/rescue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,25 @@ impl<'de> Deserialize<'de> for XpubDeserialize {
#[derive(Deserialize)]
pub struct RescueParams {
xpub: XpubDeserialize,
#[serde(rename = "derivationPath")]
derivation_path: Option<String>,
}

pub async fn swap_rescue<S, M>(
Extension(state): Extension<Arc<ServerState<S, M>>>,
Json(RescueParams { xpub }): Json<RescueParams>,
Json(RescueParams {
xpub,
derivation_path,
}): Json<RescueParams>,
) -> anyhow::Result<impl IntoResponse, AxumError>
where
S: SwapInfos + Send + Sync + Clone + 'static,
M: SwapManager + Send + Sync + 'static,
{
let res = state.service.swap_rescue.rescue_xpub(&xpub.0)?;
let res = state
.service
.swap_rescue
.rescue_xpub(&xpub.0, derivation_path)?;
Ok((StatusCode::OK, Json(res)).into_response())
}

Expand Down
2 changes: 1 addition & 1 deletion boltzr/src/api/ws/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ where
let ws_stream = match accept_async(stream).await {
Ok(stream) => stream,
Err(err) => {
error!("Could not accept WebSocket connection: {}", err);
debug!("Could not accept WebSocket connection: {}", err);
return;
}
};
Expand Down
1 change: 1 addition & 0 deletions boltzr/src/db/models/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct ChainSwap {
pub pair: String,
pub orderSide: i32,
pub status: String,
pub preimageHash: String,
pub createdAt: chrono::NaiveDateTime,
}

Expand Down
1 change: 1 addition & 0 deletions boltzr/src/db/models/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct Swap {
pub orderSide: i32,
pub status: String,
pub failureReason: Option<String>,
pub preimageHash: String,
pub invoice: Option<String>,
pub keyIndex: Option<i32>,
pub refundPublicKey: Option<String>,
Expand Down
2 changes: 2 additions & 0 deletions boltzr/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ diesel::table! {
orderSide -> Integer,
status -> Text,
failureReason -> Nullable<Text>,
preimageHash -> Text,
invoice -> Nullable<Text>,
keyIndex -> Nullable<Integer>,
refundPublicKey -> Nullable<Text>,
Expand Down Expand Up @@ -58,6 +59,7 @@ diesel::table! {
pair -> Text,
orderSide -> Integer,
status -> Text,
preimageHash -> Text,
createdAt -> Timestamptz,
}
}
Expand Down
59 changes: 52 additions & 7 deletions boltzr/src/service/rescue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct RescuableSwap {
pub symbol: String,
#[serde(rename = "keyIndex")]
pub key_index: u64,
#[serde(rename = "preimageHash")]
pub preimage_hash: String,
#[serde(rename = "timeoutBlockHeight")]
pub timeout_block_height: u64,
#[serde(rename = "serverPublicKey")]
Expand All @@ -64,8 +66,6 @@ pub struct RescuableSwap {
pub created_at: u64,
}

// TODO: database indexes

pub struct SwapRescue {
currencies: Currencies,
swap_helper: Arc<dyn SwapHelper + Sync + Send>,
Expand All @@ -86,13 +86,22 @@ impl SwapRescue {
}

#[instrument(name = "SwapRescue::rescue_xpub", skip_all)]
pub fn rescue_xpub(&self, xpub: &Xpub) -> Result<Vec<RescuableSwap>> {
pub fn rescue_xpub(
&self,
xpub: &Xpub,
derivation_path: Option<String>,
) -> Result<Vec<RescuableSwap>> {
debug!(
"Scanning for rescuable swaps for {}",
xpub.identifier().to_string()
);

let secp = Secp256k1::default();
let derivation_path = if let Some(path) = &derivation_path {
path
} else {
DERIVATION_PATH
};

let mut rescuable = Vec::new();

Expand All @@ -106,7 +115,7 @@ impl SwapRescue {
xpub.identifier().to_string()
);

let keys_map = Self::derive_keys(&secp, xpub, from, to)?;
let keys_map = Self::derive_keys(&secp, xpub, derivation_path, from, to)?;
let keys = keys_map.keys().map(|k| Some(k.clone())).collect::<Vec<_>>();

let swaps = self.swap_helper.get_all_nullable(Box::new(
Expand Down Expand Up @@ -171,6 +180,7 @@ impl SwapRescue {
s.lockupTransactionVout,
),
status: s.status,
preimage_hash: s.preimageHash,
lockup_address: s.lockupAddress,
created_at: s.createdAt.and_utc().timestamp() as u64,
})
Expand Down Expand Up @@ -228,6 +238,7 @@ impl SwapRescue {
),
lockup_address: s.receiving().lockupAddress.clone(),
status: s.swap.status,
preimage_hash: s.swap.preimageHash,
created_at: s.swap.createdAt.and_utc().timestamp() as u64,
})
})
Expand Down Expand Up @@ -294,6 +305,7 @@ impl SwapRescue {
fn derive_keys<C: secp256k1::Verification>(
secp: &Secp256k1<C>,
xpub: &Xpub,
derivation_path: &str,
start: u64,
end: u64,
) -> Result<HashMap<String, u64>> {
Expand All @@ -303,7 +315,7 @@ impl SwapRescue {
let key = xpub
.derive_pub(
secp,
&DerivationPath::from_str(&format!("{}/{}", DERIVATION_PATH, i))?,
&DerivationPath::from_str(&format!("{}/{}", derivation_path, i))?,
)
.map(|derived| derived.public_key)?;

Expand Down Expand Up @@ -350,6 +362,7 @@ mod test {
status: "invoice.failedToPay".to_string(),
keyIndex: Some(1),
timeoutBlockHeight: 321,
preimageHash: "101a17e334bcaba40cbf8e3580b73d263c3b94ed65e86ff81317f95fe1346dd8".to_string(),
refundPublicKey: Some("025964821780625d20ba1af21a45b203a96dcc5986c75c2d43bdc873d224810b0c".to_string()),
lockupAddress: "el1qqwgersfg6zwpr0htqwg6rt7zwvz5ypec9q2zn2d2s526uevt4hdtyf8jqgtak7aummc7te0rj0ke4v7ygj60s7a07pe3nz6a6".to_string(),
redeemScript: Some(tree.to_string()),
Expand All @@ -365,6 +378,7 @@ mod test {
pair: "L-BTC/BTC".to_string(),
orderSide: 1,
status: "transaction.failed".to_string(),
preimageHash: "966ea2be5351178cf96b1ae2b5b41e57bcc3d42ebcb3ef5e3bb2647641d34414".to_string(),
createdAt: chrono::NaiveDateTime::from_str("2025-01-01T23:57:21").unwrap(),
},
vec![ChainSwapData {
Expand Down Expand Up @@ -428,7 +442,7 @@ mod test {
)])),
);
let xpub = Xpub::from_str("xpub661MyMwAqRbcGXPykvqCkK3sspTv2iwWTYpY9gBewku5Noj96ov1EqnKMDzGN9yPsncpRoUymJ7zpJ7HQiEtEC9Af2n3DmVu36TSV4oaiym").unwrap();
let res = rescue.rescue_xpub(&xpub).unwrap();
let res = rescue.rescue_xpub(&xpub, None).unwrap();
assert_eq!(res.len(), 2);
assert_eq!(
res[0],
Expand All @@ -438,6 +452,7 @@ mod test {
status: swap.status,
symbol: crate::chain::elements_client::SYMBOL.to_string(),
key_index: 0,
preimage_hash: swap.preimageHash,
timeout_block_height: swap.timeoutBlockHeight as u64,
server_public_key:
"03f80e5650435fb598bb07257d50af378d4f7ddf8f2f78181f8b29abb0b05ecb47".to_string(),
Expand Down Expand Up @@ -465,6 +480,7 @@ mod test {
status: chain_swap.swap.status,
symbol: crate::chain::elements_client::SYMBOL.to_string(),
key_index: 11,
preimage_hash: chain_swap.swap.preimageHash,
server_public_key:
"02609b800f905a8bfba6763a5f0d9bdca4192648b006aeeb22598ea0b9004cf6c9".to_string(),
blinding_key: Some(
Expand Down Expand Up @@ -579,7 +595,14 @@ mod test {
#[test]
fn test_derive_keys() {
let xpub = Xpub::from_str("xpub661MyMwAqRbcGXPykvqCkK3sspTv2iwWTYpY9gBewku5Noj96ov1EqnKMDzGN9yPsncpRoUymJ7zpJ7HQiEtEC9Af2n3DmVu36TSV4oaiym").unwrap();
let keys = SwapRescue::derive_keys(&Secp256k1::verification_only(), &xpub, 0, 10).unwrap();
let keys = SwapRescue::derive_keys(
&Secp256k1::verification_only(),
&xpub,
DERIVATION_PATH,
0,
10,
)
.unwrap();

assert_eq!(keys.len(), 10);
assert_eq!(
Expand All @@ -596,6 +619,28 @@ mod test {
);
}

#[test]
fn test_derive_keys_custom_path() {
let xpub = Xpub::from_str("xpub661MyMwAqRbcGXPykvqCkK3sspTv2iwWTYpY9gBewku5Noj96ov1EqnKMDzGN9yPsncpRoUymJ7zpJ7HQiEtEC9Af2n3DmVu36TSV4oaiym").unwrap();
let keys =
SwapRescue::derive_keys(&Secp256k1::verification_only(), &xpub, "m/45/1/0/0", 0, 10)
.unwrap();

assert_eq!(keys.len(), 10);
assert_eq!(
*keys
.get("0331369109fbd305f2fdd1a0babc5a6bc629bed7aa987b4472526c2be520ed3457")
.unwrap(),
0
);
assert_eq!(
*keys
.get("035d6f0b7f7cde3c1db252aec0262721c1858effc2cc806db4eca4d2f2928f1bc0")
.unwrap(),
1
);
}

#[test]
fn test_parse_tree() {
assert!(SwapRescue::parse_tree("{\"claimLeaf\":{\"version\":192,\"output\":\"82012088a91433ca578b1dde9cb32e4b6a2c05fe74520911b66e8820884ff511cc5061a90f07e553de127095df5d438b2bda23db4159c5f32df5e1f9ac\"},\"refundLeaf\":{\"version\":192,\"output\":\"205bbdfe5d1bf863f65c5271d4cd6621c44048b89e80aa79301fe671d98bed598aad026001b1\"}}").err().is_none());
Expand Down
93 changes: 93 additions & 0 deletions lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,99 @@ class SwapRouter extends RouterBase {
*/
router.get('/:id', this.handleError(this.getSwapStatus));

// From the sidecar

/**
* @openapi
* components:
* schemas:
* RescueRequest:
* type: object
* required: ["xpub"]
* properties:
* xpub:
* type: string
* description: XPUB from which the refund keys were derived
* derivationPath:
* type: string
* description: Derivation path to use for the rescue. Defaults to m/44/0/0/0
*
* RescuableSwap:
* type: object
* required: ["id", "type", "status", "symbol", "keyIndex", "preimageHash", "timeoutBlockHeight", "serverPublicKey", "tree", "lockupAddress", "createdAt"]
* properties:
* id:
* type: string
* description: ID of the Swap
* type:
* type: string
* enum: ["submarine", "chain"]
* description: Type of the Swap
* status:
* type: string
* description: Status of the Swap
* symbol:
* type: string
* description: Symbol of the chain on which the funds locked for the swap can be rescued
* keyIndex:
* type: number
* description: Derivation index for the refund key used in the swap
* preimageHash:
* type: string
* description: Preimage hash of the swap
* timeoutBlockHeight:
* type: number
* description: Block height at which the rescuable onchain HTLCs will time out
* serverPublicKey:
* type: string
* description: Public key of the server
* blindingKey:
* type: string
* description: Blinding key of the lockup address. Only set when the chain is Liquid
* tree:
* $ref: '#/components/schemas/SwapTree'
* lockupAddress:
* type: string
* description: Lockup address of the Swap
* transaction:
* type: object
* description: Lockup transaction of the Swap
* required: ["id", "hex"]
* properties:
* id:
* type: string
* description: ID of the transaction
* hex:
* type: string
* description: The transaction encoded as HEX
* createdAt:
* type: number
* description: UNIX timestamp of the creation of the Swap
*/

/**
* @openapi
* /swap/rescue:
* post:
* tags: [Swap]
* description: Rescue swaps that were created with an XPUB
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RescueRequest'
* responses:
* '200':
* description: List of swaps that can be rescued
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/RescuableSwap'
*/

return router;
};

Expand Down
4 changes: 4 additions & 0 deletions lib/db/models/ChainSwapData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class ChainSwapData extends Model implements ChainSwapDataType {
unique: false,
fields: ['transactionId'],
},
{
unique: false,
fields: ['theirPublicKey'],
},
],
},
);
Expand Down
4 changes: 4 additions & 0 deletions lib/db/models/Swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ class Swap extends Model implements SwapType {
unique: false,
fields: ['lockupTransactionId'],
},
{
unique: false,
fields: ['refundPublicKey'],
},
],
},
);
Expand Down
Loading

0 comments on commit 94692d3

Please sign in to comment.