From 8ba7bfe949c422ae1709902b08464c7859fe49f4 Mon Sep 17 00:00:00 2001 From: "S. Santos" Date: Tue, 16 Jul 2024 06:10:22 -0300 Subject: [PATCH] Add lightning latch functions to nodeJS client --- clients/apps/nodejs/config/default.json | 7 +- clients/apps/nodejs/config/regtest.json | 2 +- clients/apps/nodejs/config/signet.json | 2 +- clients/apps/nodejs/index.js | 42 ++++++- clients/libs/nodejs/index.js | 54 ++++++++- clients/libs/nodejs/lightning-latch.js | 138 +++++++++++++++++++++++ clients/libs/rust/src/lightning_latch.rs | 18 ++- 7 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 clients/libs/nodejs/lightning-latch.js diff --git a/clients/apps/nodejs/config/default.json b/clients/apps/nodejs/config/default.json index 477042c8..26caa8a0 100644 --- a/clients/apps/nodejs/config/default.json +++ b/clients/apps/nodejs/config/default.json @@ -1,16 +1,11 @@ { "statechainEntity": "http://0.0.0.0:8000", - "__statechainEntity": "http://j23wevaeducxuy3zahd6bpn4x76cymwz2j3bdixv7ow4awjrg5p6jaid.onion", - "_statechainEntity": "http://45.76.136.11:8500/", - "__electrumServer": "tcp://0.0.0.0:50001", "electrumServer": "tcp://0.0.0.0:50001", "electrumType": "electrs", - "_electrumServer": "tcp://0.0.0.0:50001", "network": "regtest", "feeRateTolerance": 5, "databaseFile": "wallet.db", "confirmationTarget": 2, - "_torProxy": "socks5h://localhost:9050", "torProxy": null, "maxFeeRate": 1 -} \ No newline at end of file +} diff --git a/clients/apps/nodejs/config/regtest.json b/clients/apps/nodejs/config/regtest.json index 477042c8..4e432016 100644 --- a/clients/apps/nodejs/config/regtest.json +++ b/clients/apps/nodejs/config/regtest.json @@ -13,4 +13,4 @@ "_torProxy": "socks5h://localhost:9050", "torProxy": null, "maxFeeRate": 1 -} \ No newline at end of file +} diff --git a/clients/apps/nodejs/config/signet.json b/clients/apps/nodejs/config/signet.json index aeac5a9b..3920ddaa 100644 --- a/clients/apps/nodejs/config/signet.json +++ b/clients/apps/nodejs/config/signet.json @@ -13,4 +13,4 @@ "_torProxy": "socks5h://localhost:9050", "torProxy": null, "maxFeeRate": 1 -} \ No newline at end of file +} diff --git a/clients/apps/nodejs/index.js b/clients/apps/nodejs/index.js index cd321957..ecae30cb 100644 --- a/clients/apps/nodejs/index.js +++ b/clients/apps/nodejs/index.js @@ -134,7 +134,7 @@ async function main() { let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_name); receivedStatechainIds = [...receivedStatechainIds, ...transferReceiveResult.receivedStatechainIds]; - if (!transferReceiveResult.isThereBatchLocked) { + if (transferReceiveResult.isThereBatchLocked) { console.log("Statecoin batch still locked. Waiting until expiration or unlock."); await sleep(5000); } else { @@ -144,6 +144,44 @@ async function main() { console.log(JSON.stringify(receivedStatechainIds, null, 2)); }); + + program.command('payment-hash') + .description('Get a payment hash for lightning latch') + .argument('', 'name of the wallet') + .argument('', 'statechain id of the coin') + .action(async (wallet_name, statechain_id) => { + + let res = await mercurynodejslib.paymentHash(clientConfig, wallet_name, statechain_id); + + console.log(JSON.stringify(res, null, 2)); + }); + + program.command('confirm-pending-invoice') + .description('Confirm a pending invoice for lightning latch') + .argument('', 'name of the wallet') + .argument('', 'statechain id of the coin') + .action(async (wallet_name, statechain_id) => { + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_name, statechain_id); + + let res = { + message: 'Invoice confirmed' + }; + + console.log(JSON.stringify(res, null, 2)); + }); + + program.command('retrieve-pre-image') + .description('Confirm a pending invoice for lightning latch') + .argument('', 'name of the wallet') + .argument('', 'statechain id of the coin') + .argument('', 'transfer batch id') + .action(async (wallet_name, statechain_id, batch_id) => { + + let res = await mercurynodejslib.retrievePreImage(clientConfig, wallet_name, statechain_id, batch_id); + + console.log(JSON.stringify(res, null, 2)); + }); program.parse(); @@ -152,5 +190,3 @@ async function main() { (async () => { await main(); })(); - - diff --git a/clients/libs/nodejs/index.js b/clients/libs/nodejs/index.js index 69303c7d..00a5a240 100644 --- a/clients/libs/nodejs/index.js +++ b/clients/libs/nodejs/index.js @@ -7,6 +7,7 @@ const withdraw = require('./withdraw'); const transfer_receive = require('./transfer_receive'); const transfer_send = require('./transfer_send'); const coin_status = require('./coin_status'); +const lightningLatch = require('./lightning-latch'); const sqlite3 = require('sqlite3').verbose(); @@ -186,6 +187,52 @@ const transferReceive = async (clientConfig, walletName) => { return transferReceiveResult; } +const paymentHash = async (clientConfig, walletName, statechainId) => { + + const db = await getDatabase(clientConfig); + + const electrumClient = await getElectrumClient(clientConfig); + + await coin_status.updateCoins(clientConfig, electrumClient, db, walletName); + + let paymentHash = await lightningLatch.createPreImage(clientConfig, db, walletName, statechainId); + + electrumClient.close(); + db.close(); + + return paymentHash; +} + +const confirmPendingInvoice = async (clientConfig, walletName, statechainId) => { + + const db = await getDatabase(clientConfig); + + const electrumClient = await getElectrumClient(clientConfig); + + await coin_status.updateCoins(clientConfig, electrumClient, db, walletName); + + await lightningLatch.confirmPendingInvoice(clientConfig, db, walletName, statechainId); + + electrumClient.close(); + db.close(); +} + +const retrievePreImage = async (clientConfig, walletName, statechainId, batchId) => { + + const db = await getDatabase(clientConfig); + + const electrumClient = await getElectrumClient(clientConfig); + + await coin_status.updateCoins(clientConfig, electrumClient, db, walletName); + + let preImage = await lightningLatch.retrievePreImage(clientConfig, db, walletName, statechainId, batchId); + + electrumClient.close(); + db.close(); + + return preImage; +} + module.exports = { createWallet, newToken, @@ -196,5 +243,8 @@ module.exports = { withdrawCoin, newTransferAddress, transferSend, - transferReceive -}; \ No newline at end of file + transferReceive, + paymentHash, + confirmPendingInvoice, + retrievePreImage +}; diff --git a/clients/libs/nodejs/lightning-latch.js b/clients/libs/nodejs/lightning-latch.js new file mode 100644 index 00000000..b8d03700 --- /dev/null +++ b/clients/libs/nodejs/lightning-latch.js @@ -0,0 +1,138 @@ +const sqlite_manager = require('./sqlite_manager'); +const { v4: uuidv4 } = require('uuid'); +const axios = require('axios').default; +const { SocksProxyAgent } = require('socks-proxy-agent'); +const { CoinStatus } = require('./coin_enum'); + +const createPreImage = async (clientConfig, db, walletName, statechainId) => { + + const batchId = uuidv4(); + + let wallet = await sqlite_manager.getWallet(db, walletName); + + let coinsWithStatechainId = wallet.coins.filter(c => { + return c.statechain_id === statechainId + }); + + if (!coinsWithStatechainId || coinsWithStatechainId.length === 0) { + throw new Error(`There is no coin for the statechain id ${statechainId}`); + } + + // If the user sends to himself, he will have two coins with same statechain_id + // In this case, we need to find the one with the lowest locktime + // Sort the coins by locktime in ascending order and pick the first one + let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; + + if (coin.status != CoinStatus.CONFIRMED && coin.status != CoinStatus.IN_TRANSFER) { + throw new Error(`Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is ${coin.status}`); + } + + if (coin.locktime == null) { + throw new Error("Coin.locktime is null"); + } + + let paymentHashPayload = { + statechain_id: statechainId, + auth_sig: coin.signed_statechain_id, + batch_id: batchId + }; + + let paymentHash = await sendPaymentHash(clientConfig, paymentHashPayload); + + return { + hash: paymentHash, + batchId: batchId + }; +} + +const sendPaymentHash = async (clientConfig, paymentHashPayload) => { + + const url = `${clientConfig.statechainEntity}/transfer/paymenthash`; + const torProxy = clientConfig.torProxy; + + let socksAgent = undefined; + + if (torProxy) { + socksAgent = { httpAgent: new SocksProxyAgent(torProxy) }; + } + + let response = await axios.post(url, paymentHashPayload, socksAgent); + + return { hash: response?.data?.hash }; +} + +const confirmPendingInvoice = async (clientConfig, db, walletName, statechainId) => { + + let wallet = await sqlite_manager.getWallet(db, walletName); + + let coinsWithStatechainId = wallet.coins.filter(c => { + return c.statechain_id === statechainId + }); + + if (!coinsWithStatechainId || coinsWithStatechainId.length === 0) { + throw new Error(`There is no coin for the statechain id ${statechainId}`); + } + + // If the user sends to himself, he will have two coins with same statechain_id + // In this case, we need to find the one with the lowest locktime + // Sort the coins by locktime in ascending order and pick the first one + let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; + + const transferUnlockRequestPayload = { + statechain_id: statechainId, + auth_sig: coin.signed_statechain_id, + auth_pub_key: null + }; + + const url = `${clientConfig.statechainEntity}/transfer/unlock`; + const torProxy = clientConfig.torProxy; + + let socksAgent = undefined; + + if (torProxy) { + socksAgent = { httpAgent: new SocksProxyAgent(torProxy) }; + } + + // If there is an http error an exception will be thrown + await axios.post(url, transferUnlockRequestPayload, socksAgent); +} + +const retrievePreImage = async (clientConfig, db, walletName, statechainId, batchId) => { + + let wallet = await sqlite_manager.getWallet(db, walletName); + + let coinsWithStatechainId = wallet.coins.filter(c => { + return c.statechain_id === statechainId + }); + + if (!coinsWithStatechainId || coinsWithStatechainId.length === 0) { + throw new Error(`There is no coin for the statechain id ${statechainId}`); + } + + // If the user sends to himself, he will have two coins with same statechain_id + // In this case, we need to find the one with the lowest locktime + // Sort the coins by locktime in ascending order and pick the first one + let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; + + const transferPreimageRequestPayload = { + statechain_id: statechainId, + auth_sig: coin.signed_statechain_id, + previous_user_auth_key: coin.auth_pubkey, + batch_id: batchId, + }; + + const url = `${clientConfig.statechainEntity}/transfer/transfer_preimage`; + const torProxy = clientConfig.torProxy; + + let socksAgent = undefined; + + if (torProxy) { + socksAgent = { httpAgent: new SocksProxyAgent(torProxy) }; + } + + let response = await axios.post(url, transferPreimageRequestPayload, socksAgent); + + return { preimage: response?.data?.preimage }; +} + +module.exports = { createPreImage, confirmPendingInvoice, retrievePreImage }; diff --git a/clients/libs/rust/src/lightning_latch.rs b/clients/libs/rust/src/lightning_latch.rs index b1dce65b..b1f98f0c 100644 --- a/clients/libs/rust/src/lightning_latch.rs +++ b/clients/libs/rust/src/lightning_latch.rs @@ -1,12 +1,12 @@ use crate::{client_config::ClientConfig, sqlite_manager::get_wallet}; use anyhow::{anyhow, Result}; -use mercurylib::transfer::sender::{PaymentHashRequestPayload, PaymentHashResponsePayload, TransferPreimageRequestPayload, TransferPreimageResponsePayload}; +use mercurylib::{transfer::sender::{PaymentHashRequestPayload, PaymentHashResponsePayload, TransferPreimageRequestPayload, TransferPreimageResponsePayload}, wallet::CoinStatus}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct CreatePreImageResponse { - pub pre_image: String, + pub hash: String, pub batch_id: String, } @@ -30,6 +30,18 @@ pub async fn create_pre_image( let coin = coin.unwrap(); + if coin.amount.is_none() { + return Err(anyhow::anyhow!("coin.amount is None")); + } + + if coin.status != CoinStatus::CONFIRMED && coin.status != CoinStatus::IN_TRANSFER { + return Err(anyhow::anyhow!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", coin.status)); + } + + if coin.locktime.is_none() { + return Err(anyhow::anyhow!("coin.locktime is None")); + } + let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap(); let payment_hash_payload = PaymentHashRequestPayload { @@ -56,7 +68,7 @@ pub async fn create_pre_image( let payment_hash_response_payload: PaymentHashResponsePayload = serde_json::from_str(value.as_str())?; Ok(CreatePreImageResponse { - pre_image: payment_hash_response_payload.hash, + hash: payment_hash_response_payload.hash, batch_id, }) }