diff --git a/README.md b/README.md index e7f8832..62adf3b 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ curl -X POST -H "Content-type: application/json" \ The node currently exposes the following APIs: - `/address` (POST) - `/assetbalance` (POST) +- `/assetidfromhexbytes` (POST) +- `/assetidtohexbytes` (POST) - `/assetmetadata` (POST) - `/backup` (POST) - `/btcbalance` (POST) diff --git a/openapi.yaml b/openapi.yaml index c152fdc..cdc0758 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -61,6 +61,44 @@ paths: application/json: schema: $ref: '#/components/schemas/AssetBalanceResponse' + /assetidfromhexbytes: + post: + tags: + - RGB + summary: Convert hex-encoded bytes to RGB asset ID + description: Convert asset ID bytes in hexadecimal format to the corresponding baid64 string form + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssetIdFromHexBytesRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AssetIdFromHexBytesResponse' + /assetidtohexbytes: + post: + tags: + - RGB + summary: Convert RGB asset ID to hex-encoded bytes + description: Convert RGB asset ID in baid64 string form to its bytes in hexadecimal format + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssetIdToHexBytesRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AssetIdToHexBytesResponse' /assetmetadata: post: tags: @@ -986,6 +1024,30 @@ components: offchain_inbound: type: integer example: 0 + AssetIdFromHexBytesRequest: + type: object + properties: + hex_bytes: + type: string + example: 08991be186708d18b3dac93f3c044f8a8c2d558235717004602a8ec137ef459f + AssetIdFromHexBytesResponse: + type: object + properties: + asset_id: + type: string + example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8 + AssetIdToHexBytesRequest: + type: object + properties: + asset_id: + type: string + example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8 + AssetIdToHexBytesResponse: + type: object + properties: + hex_bytes: + type: string + example: 08991be186708d18b3dac93f3c044f8a8c2d558235717004602a8ec137ef459f AssetMetadataRequest: type: object properties: diff --git a/src/error.rs b/src/error.rs index 221ddb7..ccaf309 100644 --- a/src/error.rs +++ b/src/error.rs @@ -111,6 +111,9 @@ pub enum APIError { #[error("Invalid asset ID: {0}")] InvalidAssetID(String), + #[error("Invalid hex bytes")] + InvalidAssetIDBytes, + #[error("Invalid attachments: {0}")] InvalidAttachments(String), @@ -129,6 +132,9 @@ pub enum APIError { #[error("Invalid fee rate: {0}")] InvalidFeeRate(String), + #[error("Invalid hex string: {0}")] + InvalidHexString(String), + #[error("Invalid indexer: {0}")] InvalidIndexer(String), @@ -405,12 +411,14 @@ impl IntoResponse for APIError { | APIError::InvalidAnnounceAddresses(_) | APIError::InvalidAnnounceAlias(_) | APIError::InvalidAssetID(_) + | APIError::InvalidAssetIDBytes | APIError::InvalidAttachments(_) | APIError::InvalidBackupPath | APIError::InvalidChannelID | APIError::InvalidDetails(_) | APIError::InvalidEstimationBlocks | APIError::InvalidFeeRate(_) + | APIError::InvalidHexString(_) | APIError::InvalidInvoice(_) | APIError::InvalidMediaDigest | APIError::InvalidName(_) diff --git a/src/main.rs b/src/main.rs index ff0e7ec..dc32cee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,15 +39,15 @@ use crate::args::LdkUserInfo; use crate::error::AppError; use crate::ldk::stop_ldk; use crate::routes::{ - address, asset_balance, asset_metadata, backup, btc_balance, change_password, - check_indexer_url, check_proxy_endpoint, close_channel, connect_peer, create_utxos, - decode_ln_invoice, decode_rgb_invoice, disconnect_peer, estimate_fee, fail_transfers, - get_asset_media, get_channel_id, get_payment, get_swap, init, invoice_status, issue_asset_cfa, - issue_asset_nia, issue_asset_uda, keysend, list_assets, list_channels, list_payments, - list_peers, list_swaps, list_transactions, list_transfers, list_unspents, ln_invoice, lock, - maker_execute, maker_init, network_info, node_info, open_channel, post_asset_media, - refresh_transfers, restore, rgb_invoice, send_asset, send_btc, send_onion_message, - send_payment, shutdown, sign_message, sync, taker, unlock, + address, asset_balance, asset_id_from_hex_bytes, asset_id_to_hex_bytes, asset_metadata, backup, + btc_balance, change_password, check_indexer_url, check_proxy_endpoint, close_channel, + connect_peer, create_utxos, decode_ln_invoice, decode_rgb_invoice, disconnect_peer, + estimate_fee, fail_transfers, get_asset_media, get_channel_id, get_payment, get_swap, init, + invoice_status, issue_asset_cfa, issue_asset_nia, issue_asset_uda, keysend, list_assets, + list_channels, list_payments, list_peers, list_swaps, list_transactions, list_transfers, + list_unspents, ln_invoice, lock, maker_execute, maker_init, network_info, node_info, + open_channel, post_asset_media, refresh_transfers, restore, rgb_invoice, send_asset, send_btc, + send_onion_message, send_payment, shutdown, sign_message, sync, taker, unlock, }; use crate::utils::{start_daemon, AppState, LOGS_DIR}; @@ -103,6 +103,8 @@ pub(crate) async fn app(args: LdkUserInfo) -> Result<(Router, Arc), Ap .layer(DefaultBodyLimit::disable()) .route("/address", post(address)) .route("/assetbalance", post(asset_balance)) + .route("/assetidfromhexbytes", post(asset_id_from_hex_bytes)) + .route("/assetidtohexbytes", post(asset_id_to_hex_bytes)) .route("/assetmetadata", post(asset_metadata)) .route("/backup", post(backup)) .route("/btcbalance", post(btc_balance)) diff --git a/src/routes.rs b/src/routes.rs index e00c46e..166ecc6 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -141,6 +141,26 @@ impl From for AssetBalanceResponse { } } +#[derive(Deserialize, Serialize)] +pub(crate) struct AssetIdToHexBytesRequest { + pub(crate) asset_id: String, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct AssetIdToHexBytesResponse { + pub(crate) hex_bytes: String, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct AssetIdFromHexBytesRequest { + pub(crate) hex_bytes: String, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct AssetIdFromHexBytesResponse { + pub(crate) asset_id: String, +} + #[derive(Deserialize, Serialize)] pub(crate) struct AssetMetadataRequest { pub(crate) asset_id: String, @@ -1224,6 +1244,29 @@ pub(crate) async fn asset_balance( })) } +pub(crate) async fn asset_id_from_hex_bytes( + WithRejection(Json(payload), _): WithRejection, APIError>, +) -> Result, APIError> { + let hex_bytes = hex_str_to_vec(&payload.hex_bytes) + .ok_or_else(|| APIError::InvalidHexString(payload.hex_bytes))?; + + let contract_id = + ContractId::copy_from_slice(&hex_bytes).map_err(|_| APIError::InvalidAssetIDBytes)?; + let asset_id = contract_id.to_string(); + + Ok(Json(AssetIdFromHexBytesResponse { asset_id })) +} + +pub(crate) async fn asset_id_to_hex_bytes( + WithRejection(Json(payload), _): WithRejection, APIError>, +) -> Result, APIError> { + let contract_id = ContractId::from_str(&payload.asset_id) + .map_err(|_| APIError::InvalidAssetID(payload.asset_id))?; + let hex_bytes = hex_str(&contract_id.to_byte_array()); + + Ok(Json(AssetIdToHexBytesResponse { hex_bytes })) +} + pub(crate) async fn asset_metadata( State(state): State>, WithRejection(Json(payload), _): WithRejection, APIError>, diff --git a/src/test/asset_id_hex_bytes.rs b/src/test/asset_id_hex_bytes.rs new file mode 100644 index 0000000..3e5d531 --- /dev/null +++ b/src/test/asset_id_hex_bytes.rs @@ -0,0 +1,36 @@ +use super::*; + +const TEST_DIR_BASE: &str = "tmp/asset_id_hex_bytes/"; + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn success() { + initialize(); + + let test_dir_node1 = format!("{TEST_DIR_BASE}node1"); + let (node1_addr, _) = start_node(&test_dir_node1, NODE1_PEER_PORT, false).await; + + fund_and_create_utxos(node1_addr, None).await; + + // issue assets + let asset_cfa = issue_asset_cfa(node1_addr, None).await; + let asset_nia = issue_asset_nia(node1_addr).await; + let asset_uda = issue_asset_uda(node1_addr, None).await; + + // check + let cfa_decoded_result = asset_id_to_hex_bytes(node1_addr, asset_cfa.asset_id.clone()).await; + let nia_decoded_result = asset_id_to_hex_bytes(node1_addr, asset_nia.asset_id.clone()).await; + let uda_decoded_result = asset_id_to_hex_bytes(node1_addr, asset_uda.asset_id.clone()).await; + + let cfa_encoded_result = + asset_id_from_hex_bytes(node1_addr, cfa_decoded_result.hex_bytes.clone()).await; + let nia_encoded_result = + asset_id_from_hex_bytes(node1_addr, nia_decoded_result.hex_bytes.clone()).await; + let uda_encoded_result = + asset_id_from_hex_bytes(node1_addr, uda_decoded_result.hex_bytes.clone()).await; + + assert_eq!(cfa_encoded_result.asset_id, asset_cfa.asset_id); + assert_eq!(nia_encoded_result.asset_id, asset_nia.asset_id); + assert_eq!(uda_encoded_result.asset_id, asset_uda.asset_id); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index fb72334..74037d6 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -18,25 +18,26 @@ use tracing_test::traced_test; use crate::error::APIErrorResponse; use crate::ldk::FEE_RATE; use crate::routes::{ - AddressResponse, AssetBalanceRequest, AssetBalanceResponse, AssetCFA, AssetNIA, AssetUDA, - BackupRequest, BtcBalanceRequest, BtcBalanceResponse, ChangePasswordRequest, Channel, - CloseChannelRequest, ConnectPeerRequest, CreateUtxosRequest, DecodeLNInvoiceRequest, - DecodeLNInvoiceResponse, DecodeRGBInvoiceRequest, DecodeRGBInvoiceResponse, - DisconnectPeerRequest, EmptyResponse, FailTransfersRequest, FailTransfersResponse, - GetAssetMediaRequest, GetAssetMediaResponse, GetChannelIdRequest, GetChannelIdResponse, - GetPaymentRequest, GetPaymentResponse, GetSwapRequest, GetSwapResponse, HTLCStatus, - InitRequest, InitResponse, InvoiceStatus, InvoiceStatusRequest, InvoiceStatusResponse, - IssueAssetCFARequest, IssueAssetCFAResponse, IssueAssetNIARequest, IssueAssetNIAResponse, - IssueAssetUDARequest, IssueAssetUDAResponse, KeysendRequest, KeysendResponse, LNInvoiceRequest, - LNInvoiceResponse, ListAssetsRequest, ListAssetsResponse, ListChannelsResponse, - ListPaymentsResponse, ListPeersResponse, ListSwapsResponse, ListTransactionsRequest, - ListTransactionsResponse, ListTransfersRequest, ListTransfersResponse, ListUnspentsRequest, - ListUnspentsResponse, MakerExecuteRequest, MakerInitRequest, MakerInitResponse, - NetworkInfoResponse, NodeInfoResponse, OpenChannelRequest, OpenChannelResponse, Payment, Peer, - PostAssetMediaResponse, RefreshRequest, RestoreRequest, RgbInvoiceRequest, RgbInvoiceResponse, - SendAssetRequest, SendAssetResponse, SendBtcRequest, SendBtcResponse, SendPaymentRequest, - SendPaymentResponse, Swap, SwapStatus, TakerRequest, Transaction, Transfer, UnlockRequest, - Unspent, + AddressResponse, AssetBalanceRequest, AssetBalanceResponse, AssetCFA, + AssetIdFromHexBytesRequest, AssetIdFromHexBytesResponse, AssetIdToHexBytesRequest, + AssetIdToHexBytesResponse, AssetNIA, AssetUDA, BackupRequest, BtcBalanceRequest, + BtcBalanceResponse, ChangePasswordRequest, Channel, CloseChannelRequest, ConnectPeerRequest, + CreateUtxosRequest, DecodeLNInvoiceRequest, DecodeLNInvoiceResponse, DecodeRGBInvoiceRequest, + DecodeRGBInvoiceResponse, DisconnectPeerRequest, EmptyResponse, FailTransfersRequest, + FailTransfersResponse, GetAssetMediaRequest, GetAssetMediaResponse, GetChannelIdRequest, + GetChannelIdResponse, GetPaymentRequest, GetPaymentResponse, GetSwapRequest, GetSwapResponse, + HTLCStatus, InitRequest, InitResponse, InvoiceStatus, InvoiceStatusRequest, + InvoiceStatusResponse, IssueAssetCFARequest, IssueAssetCFAResponse, IssueAssetNIARequest, + IssueAssetNIAResponse, IssueAssetUDARequest, IssueAssetUDAResponse, KeysendRequest, + KeysendResponse, LNInvoiceRequest, LNInvoiceResponse, ListAssetsRequest, ListAssetsResponse, + ListChannelsResponse, ListPaymentsResponse, ListPeersResponse, ListSwapsResponse, + ListTransactionsRequest, ListTransactionsResponse, ListTransfersRequest, ListTransfersResponse, + ListUnspentsRequest, ListUnspentsResponse, MakerExecuteRequest, MakerInitRequest, + MakerInitResponse, NetworkInfoResponse, NodeInfoResponse, OpenChannelRequest, + OpenChannelResponse, Payment, Peer, PostAssetMediaResponse, RefreshRequest, RestoreRequest, + RgbInvoiceRequest, RgbInvoiceResponse, SendAssetRequest, SendAssetResponse, SendBtcRequest, + SendBtcResponse, SendPaymentRequest, SendPaymentResponse, Swap, SwapStatus, TakerRequest, + Transaction, Transfer, UnlockRequest, Unspent, }; use crate::utils::{hex_str_to_vec, ELECTRUM_URL_REGTEST, PROXY_ENDPOINT_LOCAL}; @@ -228,6 +229,44 @@ async fn asset_balance_spendable(node_address: SocketAddr, asset_id: &str) -> u6 asset_balance(node_address, asset_id).await.spendable } +async fn asset_id_from_hex_bytes( + node_address: SocketAddr, + hex_bytes: String, +) -> AssetIdFromHexBytesResponse { + println!("converting hex bytes {hex_bytes} to asset ID for node {node_address}"); + let payload = AssetIdFromHexBytesRequest { hex_bytes }; + let res = reqwest::Client::new() + .post(format!("http://{}/assetidfromhexbytes", node_address)) + .json(&payload) + .send() + .await + .unwrap(); + _check_response_is_ok(res) + .await + .json::() + .await + .unwrap() +} + +async fn asset_id_to_hex_bytes( + node_address: SocketAddr, + asset_id: String, +) -> AssetIdToHexBytesResponse { + println!("converting asset ID {asset_id} to hex bytes for node {node_address}"); + let payload = AssetIdToHexBytesRequest { asset_id }; + let res = reqwest::Client::new() + .post(format!("http://{}/assetidtohexbytes", node_address)) + .json(&payload) + .send() + .await + .unwrap(); + _check_response_is_ok(res) + .await + .json::() + .await + .unwrap() +} + async fn backup(node_address: SocketAddr, backup_path: &str, password: &str) { println!("performing backup for node {node_address} on {backup_path}"); let payload = BackupRequest { @@ -1658,6 +1697,7 @@ pub fn mock_fee(fee: u32) -> u32 { } } +mod asset_id_hex_bytes; mod backup_and_restore; mod close_coop_nobtc_acceptor; mod close_coop_other_side;