diff --git a/book.toml b/book.toml index e5be345..7d2b9c4 100644 --- a/book.toml +++ b/book.toml @@ -6,6 +6,7 @@ src = "src" title = "Breez SDK" [output.html] +additional-js = ["tabs.js"] git-repository-url = "https://github.com/breez/breez-sdk-docs" edit-url-template = "https://github.com/breez/breez-sdk-docs/edit/main/{path}" diff --git a/src/guide/getting_started.md b/src/guide/getting_started.md index 9e0b572..322612a 100644 --- a/src/guide/getting_started.md +++ b/src/guide/getting_started.md @@ -15,6 +15,11 @@ Connecting to a node requires a seed (your master key) and credentials. The seed Breez SDK is available in several platforms. Follow the [Installing](install.md) page for instructions on how to install on your platform. + +
Rust
+
+ +The first step is to register a new node. In order to do that a seed is needed. ## Registering a new node ```rust,no_run let seed = ; @@ -62,4 +67,138 @@ if let Some(node_state) = sdk.node_info()? { let balance_onchain = node_state.onchain_balance_msat; } ``` +
+
Swift
+
+ +The first step is to register a new node +## Registering a new node +```swift +do { + let seed = try mnemonicToSeed(phrase: "") + let inviteCode = "" + + // register_node takes either greenlight credentials (certifate & key) or invite code. + // At this example we are using the invite code option. + let credentials = try registerNode(network: Network.bitcoin, seed: seed, registerCredentials: nil, inviteCode: inviteCode) +} catch { + // handle error +} +``` + +## Recovering an existing node +```swift +do { + let seed = try mnemonicToSeed(phrase: "") + let credentials = try recoverNode(network: Network.bitcoin, seed: seed) +} catch { + // handle error +} +``` + +Once the credentials are retrieved they should be saved in a secured storage. +The next step is to initialize the SDK and start the node: + +## Initializing the SDK +```swift + +// SDK events listener +class SDKListener: EventListener { + func onEvent(e: BreezEvent) { + print("received event ", e) + } +} + +// Create the default config +var config = defaultConfig(envType: EnvironmentType.production) + +// Customize the config object according to your needs +config.apiKey = "your API key"; +config.workingDir = "path to an existing directory"; + +do { + let sdk = try initServices(config: config, seed: seed, creds: credentials, listener: SDKListener()); + try sdk.start(); +} catch{ + // handle error +} +``` + +At any point we can fetch our balance from the Greenlight node: + +```swift +do { + let nodeInfo = try sdk.nodeInfo() + let lnBalance = nodeInfo?.channelsBalanceMsat + let onchainBalance = nodeInfo?.onchainBalanceMsat +} catch { + // handle error +} +``` + +
+
React Native
+
+ +The first step is to register a new node +## Registering a new node +```typescript +try { + const seed = await mnemonicToSeed(""); + const invite_code = ""; + + // register_node takes either greenlight credentials (certifate & key) or invite code. + // At this example we are using the invite code option. + const credentials = await registerNode(Network.BITCOIN, seed, inviteCode); +} catch (error) { + console.log(error) +} +``` + +## Recovering an existing node +```typescript + const seed = await mnemonicToSeed(""); + const credentials = await recoverNode(Network.BITCOIN, seed); +``` + +Once the credentials are retrieved they should be saved in a secured storage. +The next step is to initialize the SDK and start the node: + +## Initializing the SDK +```typescript + +// SDK events listener +addEventListener((type, data) => { + console.log(`received event ${type}`); +}) + +// Create the default config +let config = defaultConfig(EnvironmentType.PRODUCTION) + +// Customize the config object according to your needs +config.apiKey = "your API key"; +config.workingDir = "path to an existing directory"; + +try { + const sdkServices = await initServices(config, credentials.deviceKey, credentials.deviceCert, seed); + await start(); +} catch (error) { + console.log(error); +} +``` + +At any point we can fetch our balance from the Greenlight node: + +```typescript +try { + const nodeInfo = await nodeInfo(); + const lnBalance = nodeInfo.channelsBalanceMsat; + const onchainBalance = nodeInfo.onchainBalanceMsat; +} catch (error) { + console.log(error); +} +``` +
+
+ You are now ready to receive a Lightning [payment](payments.md). diff --git a/src/guide/lnurl_auth.md b/src/guide/lnurl_auth.md index 0a7ea4a..807ca30 100644 --- a/src/guide/lnurl_auth.md +++ b/src/guide/lnurl_auth.md @@ -1,6 +1,9 @@ # LNURL-Auth ## Usage + +
Rust
+
```rust,no_run // Endpoint can also be of the form: @@ -21,7 +24,57 @@ if let Ok(LnUrlAuth{data: ad}) = parse(lnurl_auth_url).await { } } ``` +
+
Swift
+
+```swift +// Endpoint can also be of the form: +// keyauth://domain.com/auth?key=val +let lnurlAuthUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgeqgntfgu" + +do { + let inputType = try parseInput(s: lnurlAuthUrl) + if case .lnUrlAuth(let data) = inputType { + let result = try sdk.lnurlAuth(reqData: data) + switch result { + case .ok: + print("Successfully authenticated") + case .errorStatus(let data): + print("Failed to authenticate") + } + } +} catch { + // handle error +} +``` + +
+
React Native
+
+ +```typescript +// Endpoint can also be of the form: +// keyauth://domain.com/auth?key=val +let lnurlAuthUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgeqgntfgu"; + +try { + const input = await parseInput(lnurlAuthUrl) + if (input.type === InputType.LNURL_AUTH) { + const result = await lnurlAuth(input.data) + if (result.status === "ok") { + print("Successfully authenticated") + } else { + print("Failed to authenticate") + } + } +} catch (error) { + console.log(error) +} +``` + +
+ ## Supported Specs diff --git a/src/guide/lnurl_pay.md b/src/guide/lnurl_pay.md index 1019d92..85b088b 100644 --- a/src/guide/lnurl_pay.md +++ b/src/guide/lnurl_pay.md @@ -2,6 +2,10 @@ ## Usage + +
Rust
+
+ ```rust,no_run // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val @@ -17,6 +21,47 @@ if let Ok(LnUrlPay{data: pd}) = parse(lnurl_pay_url).await { } ``` +
+
Swift
+
+ +```swift +// Endpoint can also be of the form: +// lnurlp://domain.com/lnurl-pay?key=val +// lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf +let lnurlPayUrl = "lightning@address.com"; +do { + let inputType = try parseInput(s: lnurlPayUrl) + if case .lnUrlPay(let data) = inputType { + let amountSats = data.minSendable; + try sdk.payLnurl(reqData: data, amountSats: amountSats, comment: "comment") + } +} catch { + // handle error +} +``` +
+
React Native
+
+ +```typescript +// Endpoint can also be of the form: +// lnurlp://domain.com/lnurl-pay?key=val +// lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf +let lnurlPayUrl = "lightning@address.com"; + +try { + const input = await parseInput(lnurlAuthUrl) + if (input.type === InputType.LNURL_PAY) { + const amountSats = input.minSendable; + const result = await payLnurl(input.data, amountSats, "comment") + } +} catch (error) { + console.log(error) +} +``` +
+ ## Supported Specs diff --git a/src/guide/lnurl_withdraw.md b/src/guide/lnurl_withdraw.md index 869e833..aaff69b 100644 --- a/src/guide/lnurl_withdraw.md +++ b/src/guide/lnurl_withdraw.md @@ -3,6 +3,10 @@ ## Usage + +
Rust
+
+ ```rust,no_run // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val @@ -16,7 +20,48 @@ if let Ok(LnUrlWithdraw{data: wd}) = parse(lnurl_withdraw_url).await { sdk.lnurl_withdraw(wd, amount_msat, Some(description)).await?; } ``` +
+
Swift
+
+```swift +// Endpoint can also be of the form: +// lnurlw://domain.com/lnurl-withdraw?key=val +let lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk" + +do { + let inputType = try parseInput(s: lnurlWithdrawUrl) + if case .lnUrlWithdraw(let data) = inputType { + let amountSat = data.minWithdrawable + let description = "Test withdraw" + try sdk.withdrawLnurl(reqData: data, amountSats: amountSat, description: "comment") + } +} catch { + // handle error +} + +``` +
+
React Native
+
+ +```typescript +// Endpoint can also be of the form: +// lnurlw://domain.com/lnurl-withdraw?key=val +let lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; + +try { + const input = await parseInput(lnurlAuthUrl) + if (input.type === InputType.LNURL_WITHDRAW) { + const amountSats = input.minWithdrawable; + const result = await withdrawLnurl(input.data, amountSats, "comment") + } +} catch (error) { + console.log(error) +} +``` +
+ ## Supported Specs diff --git a/src/guide/payments.md b/src/guide/payments.md index 96faf60..70aacd5 100644 --- a/src/guide/payments.md +++ b/src/guide/payments.md @@ -1,5 +1,8 @@ # Sending and receiving Lightning payments + +
Rust
+
## Receiving Lightning Payments Breez SDK doesn't require you to open a channel and set up your inbound liquidity. Breez SDK automatically connects your node to the LSP peer and you can now receive payments: @@ -20,3 +23,73 @@ let node_id = "..."; sdk.send_payment(node_id.into(), Some(3000)).await?; ``` +
+
Swift
+
+## Receiving Lightning Payments +Breez SDK doesn't require you to open a channel and set up your inbound liquidity. +Breez SDK automatically connects your node to the LSP peer and you can now receive payments: + +```swift +do { + let invoice = try sdk.receivePayment(amountSats: 3000, description: "Invoice for 3000 sats") +} catch { + // handle error +} +``` + +## Sending Lightning Payments +```swift +let bolt11 = "..."; +do { + let payment = try sdk.sendPayment(bolt11: bolt11, amountSats: 3000) +} catch { + // handle error +} +``` + +## Sending Spontaneous Lightning Payments +```swift +let nodeId = "..."; +do { + let payment = try sdk.sendSpontaneousPayment(nodeId: nodeId, amountSats: 3000) +} catch { + // handle error +} +``` +
+
React Native
+
+## Receiving Lightning Payments +Breez SDK doesn't require you to open a channel and set up your inbound liquidity. +Breez SDK automatically connects your node to the LSP peer and you can now receive payments: + +```typescript +try { + const invoice = await receivePayment(3000, "Invoice for 3000 sats") +} catch (error) { + console.log(error) +} +``` + +## Sending Lightning Payments +```typescript +const bolt11 = "..."; +try { + const payment = await sendPayment(bolt11, 3000) +} catch (error) { + console.log(error) +} +``` + +## Sending Spontaneous Lightning Payments +```typescript +const nodeId = "..."; +try { + const payment = await sendSpontaneousPayment(nodeId, 3000) +} catch (error) { + console.log(error) +} +``` +
+
\ No newline at end of file diff --git a/src/guide/recieve_onchain.md b/src/guide/recieve_onchain.md index 7c9fb04..c0569eb 100644 --- a/src/guide/recieve_onchain.md +++ b/src/guide/recieve_onchain.md @@ -1,10 +1,14 @@ # Receiving an on-chain transaction (swap-in) There are cases when you have funds in some bitcoin address and you would like to send those to your lightning node. + +
Rust
+
+ ```rust,no_run let swap_info = sdk.receive_onchain().await?; -// Send your funds to the bellow bitcoin address +// Send your funds to the below bitcoin address let address = swap_info.bitcoin_address; ``` @@ -29,6 +33,114 @@ Once you have a refundable swap in hand, use the follwing code to execute a refu ```rust,no_run let destination_address = "...".into() -let sat_per_byte = -sdk.refund(refundable.bitcoin_address, destination_address, sat_per_byte).await? +let sat_per_vbyte = +sdk.refund(refundable.bitcoin_address, destination_address, sat_per_vbyte).await? ``` +
+
Swift
+
+ +```swift +do { + let swapInfo = try sdk.receiveOnchain() + + // Send your funds to the bellow bitcoin address + let address = swapInfo.bitcoinAddress; +} catch { + // handle error +} +``` + +Once you've sent the funds to the above address, the SDK will monitor this address for unspent confirmed outputs and use a trustless submarine swap to receive these into your Lightning node. You can always monitor the status of the current in-progress swap using the following code: + +```swift +do { + let swapInfo = try sdk.inProgressSwap() +} catch { + // handle error +} +``` + +The process of receiving funds via an on-chain address is trustless and uses a submarine swap. This means there are two ways to spend the sent funds: + +1. Either by a preimage that is exposed when the Lightning payment is completed - this is the positive case where the swap was successful. +2. Or by your node when the swap didn't complete within a certain timeout - this is the negative case where your node will execute a refund. + +In order to execute a refund, you need to supply an on-chain address to where the refunded amount will be sent. The following code will retrieve the refundable swaps: + +```swift +do { + let refundables = try sdk.listRefundables() +} catch { + // handle error +} +``` + +Once you have a refundable swap in hand, use the follwing code to execute a refund: + +```swift +let destinationAddress = "..." +let satPerVbyte = + +do { + try sdk.refund( + swapAddress: "", + toAddress: destinationAddress, + satPerVbyte: satPerVbyte) +} catch { + // handle error +} +``` +
+
React Native
+
+ +```typescript +try { + const swapInfo = await receiveOnchain(); + + // Send your funds to the below bitcoin address + const address = swapInfo.bitcoinAddress; +} catch (error) { + console.log(error) +} +``` + +Once you've sent the funds to the above address, the SDK will monitor this address for unspent confirmed outputs and use a trustless submarine swap to receive these into your Lightning node. You can always monitor the status of the current in-progress swap using the following code: + +```typescript +try { + const swapInfo = await inProgressSwap() +} catch (error) { + console.log(error) +} +``` + +The process of receiving funds via an on-chain address is trustless and uses a submarine swap. This means there are two ways to spend the sent funds: + +1. Either by a preimage that is exposed when the Lightning payment is completed - this is the positive case where the swap was successful. +2. Or by your node when the swap didn't complete within a certain timeout - this is the negative case where your node will execute a refund. + +In order to execute a refund, you need to supply an on-chain address to where the refunded amount will be sent. The following code will retrieve the refundable swaps: + +```typescript +try { + const refundables = await listRefundables() +} catch (error) { + console.log(error) +} +``` + +Once you have a refundable swap in hand, use the follwing code to execute a refund: + +```typescript +const destinationAddress = "..." +const satPerVbyte = +try { + const result = await refund(refundable.bitcoinAddress, destinationAddress, satPerVbyte) +} catch (error) { + console.log(error) +} +``` +
+
\ No newline at end of file diff --git a/src/guide/send_onchain.md b/src/guide/send_onchain.md index 340cd03..12367ee 100644 --- a/src/guide/send_onchain.md +++ b/src/guide/send_onchain.md @@ -4,6 +4,10 @@ You can send funds from the Breez SDK wallet to an on-chain address as follows. First, fetch the current reverse swap fees: + +
Rust
+
+ ```rust,no_run let current_fees = sdk.fetch_reverse_swap_fees().await?; @@ -43,7 +47,115 @@ for rs in sdk.in_progress_reverse_swaps().await? { info!("Reverse swap {} in progress, status is {}", rs.id, rs.breez_status); } ``` +
+
Swift
+
+```swift +try { + let currentFees = try sdk.fetchReverseSwapFees() + + println("Percentage fee for the reverse swap service: \(currentFees.feesPercentage)") + println("Estimated miner fees in sats for locking up funds: \(currentFees.feesLockup)") + println("Estimated miner fees in sats for claiming funds: \(currentFees.feesClaim)") +} catch { + // handle error +} +``` + +The reverse swap will involve two on-chain transactions, for which the mining fees can only be estimated. They will happen +automatically once the process is started, but the last two values above are these estimates to help you get a picture +of the total costs. + +Fetching the fees also tells you what is the range of amounts you can send: + +```swift +println("Minimum amount, in sats: \(current_fees.min)") +println("Maximum amount, in sats: \(current_fees.max)") +``` + +Once you checked the fees are acceptable, you can start the reverse swap: + +```swift +let destinationAddress = "bc1.." +let amountSat = currentFees.min +let satPerVbyte = +try { + try sdk.sendOnchain(amountSat: amountSat, onchainRecipientAddress: destinationAddress, pairHash: currentFees.feesHash, satPerVbyte: satPerVbyte) +} catch { + // handle error +} +``` + +Starting the reverse swap will trigger a HODL invoice payment, which will only be settled if the entire swap completes. +This means you will see an outgoing pending payment in your list of payments, which locks those funds until the invoice +is either settled or cancelled. This will happen automatically at the end of the reverse swap. + +You can check its status with: + +```swift +for rs in sdk.inProgressReverseSwaps() { + println("Reverse swap \(rs.id) in progress, status is \(rs.breezStatus)") +} +``` +
+
React Native
+
+ +```typescript +try { + const currentFees = await fetchReverseSwapFees() + + console.log(`Percentage fee for the reverse swap service: ${currentFees.feesPercentage}`); + console.log(`Estimated miner fees in sats for locking up funds: ${currentFees.feesLockup}`); + console.log(`Estimated miner fees in sats for claiming funds: ${currentFees.feesClaim}`); +} catch (error) { + console.log(error) +} +``` + +The reverse swap will involve two on-chain transactions, for which the mining fees can only be estimated. They will happen +automatically once the process is started, but the last two values above are these estimates to help you get a picture +of the total costs. + +Fetching the fees also tells you what is the range of amounts you can send: + +```typescript +console.log(`Minimum amount, in sats: ${current_fees.min}`); +console.log(`Maximum amount, in sats: ${current_fees.max}`); +``` + +Once you checked the fees are acceptable, you can start the reverse swap: + +```typescript +const destinationAddress = "bc1.."; +const amountSat = currentFees.min; +const satPerVbyte = +try { + const reverseSwapInfo = sendOnchain(amountSat, destinationAddress, currentFees.feesHash, satPerVbyte) +} catch (error) { + console.log(error) +} +``` + +Starting the reverse swap will trigger a HODL invoice payment, which will only be settled if the entire swap completes. +This means you will see an outgoing pending payment in your list of payments, which locks those funds until the invoice +is either settled or cancelled. This will happen automatically at the end of the reverse swap. + +You can check its status with: + +```typescript +try { + const swaps = await inProgressReverseSwaps() + for (const swap in swaps) { + println(`Reverse swap ${swap.id} in progress, status is ${swap.breezStatus}`); + } +} catch (error) { + console.log(error) +} +``` +
+
If the reverse swap is successful, you'll get the on-chain payment on your destination address and the HODL invoice will change from pending to settled. diff --git a/tabs.js b/tabs.js new file mode 100644 index 0000000..a804acf --- /dev/null +++ b/tabs.js @@ -0,0 +1,147 @@ +(function () { + 'use strict'; + + let selected_ = null; + + customElements.define('custom-tabs', class extends HTMLElement { + + constructor() { + super(); // always call super() first in the ctor. + + // Create shadow DOM for the component. + let shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = ` + +
+ +
+
+ +
+ `; + } + + get selected() { + return selected_; + } + + set selected(idx) { + selected_ = idx; + this._selectTab(idx); + this.setAttribute('selected', idx); + } + + connectedCallback() { + this.setAttribute('role', 'tablist'); + + const tabsSlot = this.shadowRoot.querySelector('#tabsSlot'); + const panelsSlot = this.shadowRoot.querySelector('#panelsSlot'); + + this.tabs = tabsSlot.assignedNodes({ flatten: true }); + this.panels = panelsSlot.assignedNodes({ flatten: true }).filter(el => { + return el.nodeType === Node.ELEMENT_NODE; + }); + + // Save refer to we can remove listeners later. + this._boundOnTitleClick = this._onTitleClick.bind(this); + + tabsSlot.addEventListener('click', this._boundOnTitleClick); + + this.selected = this._findFirstSelectedTab() || this._findStoredSelectedTab() || 0; + } + + disconnectedCallback() { + const tabsSlot = this.shadowRoot.querySelector('#tabsSlot'); + tabsSlot.removeEventListener('click', this._boundOnTitleClick); + } + + _onTitleClick(e) { + if (e.target.slot === 'title') { + this.selected = this.tabs.indexOf(e.target); + e.target.focus(); + } + } + + _findFirstSelectedTab() { + let selectedIdx; + for (let [i, tab] of this.tabs.entries()) { + tab.setAttribute('role', 'tab'); + if (tab.hasAttribute('selected')) { + selectedIdx = i; + } + } + return selectedIdx; + } + + _findStoredSelectedTab() { + let selectedIdx; + if (this.getAttribute("category")) { + let selectedText; + try { selectedText = localStorage.getItem('mdbook-tabs-' + this.getAttribute("category")); } catch (e) { } + if (selectedText) { + for (let [i, tab] of this.tabs.entries()) { + if (tab.textContent === selectedText) { + selectedIdx = i; + break; + } + } + } + } + return selectedIdx; + } + + _selectTab(idx = null) { + for (let i = 0, tab; tab = this.tabs[i]; ++i) { + let select = i === idx; + tab.setAttribute('tabindex', select ? 0 : -1); + tab.setAttribute('aria-selected', select); + this.panels[i].setAttribute('aria-hidden', !select); + if (select && this.getAttribute("category") && tab.textContent) { + try { localStorage.setItem('mdbook-tabs-' + this.getAttribute("category"), tab.textContent); } catch (e) { } + } + } + } + + }); + +})(); \ No newline at end of file