diff --git a/boltzr/Cargo.toml b/boltzr/Cargo.toml index 6c1fdb99..78c41a2a 100644 --- a/boltzr/Cargo.toml +++ b/boltzr/Cargo.toml @@ -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" diff --git a/boltzr/src/api/rescue.rs b/boltzr/src/api/rescue.rs index cd1d38bd..20f00735 100644 --- a/boltzr/src/api/rescue.rs +++ b/boltzr/src/api/rescue.rs @@ -46,17 +46,25 @@ impl<'de> Deserialize<'de> for XpubDeserialize { #[derive(Deserialize)] pub struct RescueParams { xpub: XpubDeserialize, + #[serde(rename = "derivationPath")] + derivation_path: Option, } pub async fn swap_rescue( Extension(state): Extension>>, - Json(RescueParams { xpub }): Json, + Json(RescueParams { + xpub, + derivation_path, + }): Json, ) -> anyhow::Result 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()) } diff --git a/boltzr/src/api/ws/status.rs b/boltzr/src/api/ws/status.rs index 75832453..a3ed38a1 100644 --- a/boltzr/src/api/ws/status.rs +++ b/boltzr/src/api/ws/status.rs @@ -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; } }; diff --git a/boltzr/src/db/models/chain_swap.rs b/boltzr/src/db/models/chain_swap.rs index f356d3d7..4681adea 100644 --- a/boltzr/src/db/models/chain_swap.rs +++ b/boltzr/src/db/models/chain_swap.rs @@ -13,6 +13,7 @@ pub struct ChainSwap { pub pair: String, pub orderSide: i32, pub status: String, + pub preimageHash: String, pub createdAt: chrono::NaiveDateTime, } diff --git a/boltzr/src/db/models/swap.rs b/boltzr/src/db/models/swap.rs index 92119353..04d27dc6 100644 --- a/boltzr/src/db/models/swap.rs +++ b/boltzr/src/db/models/swap.rs @@ -13,6 +13,7 @@ pub struct Swap { pub orderSide: i32, pub status: String, pub failureReason: Option, + pub preimageHash: String, pub invoice: Option, pub keyIndex: Option, pub refundPublicKey: Option, diff --git a/boltzr/src/db/schema.rs b/boltzr/src/db/schema.rs index 1cb20477..0948ec58 100644 --- a/boltzr/src/db/schema.rs +++ b/boltzr/src/db/schema.rs @@ -27,6 +27,7 @@ diesel::table! { orderSide -> Integer, status -> Text, failureReason -> Nullable, + preimageHash -> Text, invoice -> Nullable, keyIndex -> Nullable, refundPublicKey -> Nullable, @@ -58,6 +59,7 @@ diesel::table! { pair -> Text, orderSide -> Integer, status -> Text, + preimageHash -> Text, createdAt -> Timestamptz, } } diff --git a/boltzr/src/service/rescue.rs b/boltzr/src/service/rescue.rs index 4bfb2313..f2c05942 100644 --- a/boltzr/src/service/rescue.rs +++ b/boltzr/src/service/rescue.rs @@ -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")] @@ -64,8 +66,6 @@ pub struct RescuableSwap { pub created_at: u64, } -// TODO: database indexes - pub struct SwapRescue { currencies: Currencies, swap_helper: Arc, @@ -86,13 +86,22 @@ impl SwapRescue { } #[instrument(name = "SwapRescue::rescue_xpub", skip_all)] - pub fn rescue_xpub(&self, xpub: &Xpub) -> Result> { + pub fn rescue_xpub( + &self, + xpub: &Xpub, + derivation_path: Option, + ) -> Result> { 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(); @@ -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::>(); let swaps = self.swap_helper.get_all_nullable(Box::new( @@ -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, }) @@ -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, }) }) @@ -294,6 +305,7 @@ impl SwapRescue { fn derive_keys( secp: &Secp256k1, xpub: &Xpub, + derivation_path: &str, start: u64, end: u64, ) -> Result> { @@ -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)?; @@ -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()), @@ -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 { @@ -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], @@ -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(), @@ -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( @@ -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!( @@ -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()); diff --git a/lib/api/v2/routers/SwapRouter.ts b/lib/api/v2/routers/SwapRouter.ts index 251da579..f232a629 100644 --- a/lib/api/v2/routers/SwapRouter.ts +++ b/lib/api/v2/routers/SwapRouter.ts @@ -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; }; diff --git a/lib/db/models/ChainSwapData.ts b/lib/db/models/ChainSwapData.ts index 0ad782a2..d1f6667a 100644 --- a/lib/db/models/ChainSwapData.ts +++ b/lib/db/models/ChainSwapData.ts @@ -89,6 +89,10 @@ class ChainSwapData extends Model implements ChainSwapDataType { unique: false, fields: ['transactionId'], }, + { + unique: false, + fields: ['theirPublicKey'], + }, ], }, ); diff --git a/lib/db/models/Swap.ts b/lib/db/models/Swap.ts index ccbb35ae..3ef25390 100644 --- a/lib/db/models/Swap.ts +++ b/lib/db/models/Swap.ts @@ -177,6 +177,10 @@ class Swap extends Model implements SwapType { unique: false, fields: ['lockupTransactionId'], }, + { + unique: false, + fields: ['refundPublicKey'], + }, ], }, ); diff --git a/swagger-spec.json b/swagger-spec.json index a316a55a..5da3bf74 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -2157,6 +2157,39 @@ } } } + }, + "/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" + } + } + } + } + } + } + } } }, "components": { @@ -3235,6 +3268,109 @@ } } } + }, + "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" + } + } } } },