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

docs: add rescue endpoint to swagger #836

Merged
merged 2 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading