Merge pull request #847 from shocknet/wizard-update

Wizard update
This commit is contained in:
Justin (shocknet) 2025-10-09 22:36:01 -04:00 committed by GitHub
commit 24fa94824a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1608 additions and 804 deletions

View file

@ -15,6 +15,7 @@ message ConfigRequest {
string relay_url = 2;
bool automate_liquidity = 3;
bool push_backups_to_nostr = 4;
string avatar_url = 5;
}
message AdminConnectInfoResponse {
string nprofile = 1;
@ -37,4 +38,10 @@ message ServiceStateResponse {
bool watchdog_ok = 6;
string http_url = 7;
string nprofile = 8;
string source_name = 9;
string relay_url = 10;
bool automate_liquidity = 11;
bool push_backups_to_nostr = 12;
string avatar_url = 13;
string app_id = 14;
}

View file

@ -24,20 +24,6 @@ The nostr server will send back a message response, and inside the body there wi
## HTTP Methods
### These are the http methods the client implements to communicate with the API
- WizardState
- auth type: __Guest__
- http method: __get__
- http route: __/wizard/state__
- This methods has an __empty__ __request__ body
- output: [StateResponse](#StateResponse)
- WizardConfig
- auth type: __Guest__
- http method: __post__
- http route: __/wizard/config__
- input: [ConfigRequest](#ConfigRequest)
- This methods has an __empty__ __response__ body
- GetAdminConnectInfo
- auth type: __Guest__
- http method: __get__
@ -52,40 +38,61 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [ServiceStateResponse](#ServiceStateResponse)
- WizardConfig
- auth type: __Guest__
- http method: __post__
- http route: __/wizard/config__
- input: [ConfigRequest](#ConfigRequest)
- This methods has an __empty__ __response__ body
- WizardState
- auth type: __Guest__
- http method: __get__
- http route: __/wizard/state__
- This methods has an __empty__ __request__ body
- output: [StateResponse](#StateResponse)
# INPUTS AND OUTPUTS
## Messages
### The content of requests and response from the methods
### StateResponse
- __config_sent__: _boolean_
- __admin_linked__: _boolean_
### AdminConnectInfoResponse
- __connect_info__: _[AdminConnectInfoResponse_connect_info](#AdminConnectInfoResponse_connect_info)_
- __nprofile__: _string_
### ConfigRequest
- __source_name__: _string_
- __relay_url__: _string_
- __automate_liquidity__: _boolean_
- __avatar_url__: _string_
- __push_backups_to_nostr__: _boolean_
### AdminConnectInfoResponse
- __nprofile__: _string_
- __connect_info__: _AdminConnectInfoResponse_connect_info_
### ServiceStateResponse
- __http_url__: _string_
- __nprofile__: _string_
- __provider_name__: _string_
- __relays__: ARRAY of: _string_
- __admin_npub__: _string_
- __relay_connected__: _boolean_
- __lnd_state__: _[LndState](#LndState)_
- __watchdog_ok__: _boolean_
- __relay_url__: _string_
- __source_name__: _string_
### Empty
### ServiceStateResponse
- __admin_npub__: _string_
- __app_id__: _string_
- __automate_liquidity__: _boolean_
- __avatar_url__: _string_
- __http_url__: _string_
- __lnd_state__: _[LndState](#LndState)_
- __nprofile__: _string_
- __provider_name__: _string_
- __push_backups_to_nostr__: _boolean_
- __relay_connected__: _boolean_
- __relay_url__: _string_
- __relays__: ARRAY of: _string_
- __source_name__: _string_
- __watchdog_ok__: _boolean_
### StateResponse
- __admin_linked__: _boolean_
- __config_sent__: _boolean_
## Enums
### The enumerators used in the messages
### LndState
- __OFFLINE__
- __SYNCING__
- __ONLINE__
- __SYNCING__

View file

@ -1,61 +1,5 @@
([]*main.Method) (len=4 cap=4) {
(*main.Method)(0xc00022a280)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
name: (string) (len=11) "WizardState",
out: (main.MethodMessage) {
name: (string) (len=13) "StateResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009a9c0)({
authType: (*main.supportedAuth)(0xc0003c9aa0)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
route: (string) (len=13) "/wizard/state",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a2d0)({
in: (main.MethodMessage) {
name: (string) (len=13) "ConfigRequest",
hasZeroFields: (bool) false
},
name: (string) (len=12) "WizardConfig",
out: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
opts: (*main.methodOptions)(0xc00009ab40)({
authType: (*main.supportedAuth)(0xc0003c9b60)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
}),
method: (string) (len=4) "post",
route: (main.decodedRoute) {
route: (string) (len=14) "/wizard/config",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a640)({
(*main.Method)(0xc0002b00f0)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
@ -65,12 +9,11 @@
name: (string) (len=24) "AdminConnectInfoResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009acc0)({
authType: (*main.supportedAuth)(0xc0003c9c20)({
opts: (*main.methodOptions)(0xc0006907e0)({
authType: (*main.supportedAuth)(0xc000347380)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
context: ([]*main.authContext) <nil>
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
@ -83,7 +26,7 @@
}),
serverStream: (bool) false
}),
(*main.Method)(0xc00022a690)({
(*main.Method)(0xc0002b0140)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
@ -93,12 +36,11 @@
name: (string) (len=20) "ServiceStateResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc00009ae40)({
authType: (*main.supportedAuth)(0xc0003c9ce0)({
opts: (*main.methodOptions)(0xc000690960)({
authType: (*main.supportedAuth)(0xc000347440)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: (map[string]string) {
}
context: ([]*main.authContext) <nil>
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
@ -110,53 +52,101 @@
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc0002b00a0)({
in: (main.MethodMessage) {
name: (string) (len=13) "ConfigRequest",
hasZeroFields: (bool) false
},
name: (string) (len=12) "WizardConfig",
out: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
opts: (*main.methodOptions)(0xc000690660)({
authType: (*main.supportedAuth)(0xc0003472c0)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: ([]*main.authContext) <nil>
}),
method: (string) (len=4) "post",
route: (main.decodedRoute) {
route: (string) (len=14) "/wizard/config",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
}),
(*main.Method)(0xc0002b0050)({
in: (main.MethodMessage) {
name: (string) (len=5) "Empty",
hasZeroFields: (bool) true
},
name: (string) (len=11) "WizardState",
out: (main.MethodMessage) {
name: (string) (len=13) "StateResponse",
hasZeroFields: (bool) false
},
opts: (*main.methodOptions)(0xc0006904e0)({
authType: (*main.supportedAuth)(0xc000347200)({
id: (string) (len=5) "guest",
name: (string) (len=5) "Guest",
context: ([]*main.authContext) <nil>
}),
method: (string) (len=3) "get",
route: (main.decodedRoute) {
route: (string) (len=13) "/wizard/state",
params: ([]string) <nil>
},
query: ([]string) <nil>,
nostr: (bool) false,
batch: (bool) false
}),
serverStream: (bool) false
})
}
([]*main.Enum) (len=1 cap=1) {
(*main.Enum)(0xc0003c9680)({
(*main.Enum)(0xc000443d40)({
name: (string) (len=8) "LndState",
values: ([]main.EnumValue) (len=3 cap=4) {
(main.EnumValue) {
number: (int64) 0,
name: (string) (len=7) "OFFLINE"
},
(main.EnumValue) {
number: (int64) 1,
name: (string) (len=7) "SYNCING"
},
(main.EnumValue) {
number: (int64) 2,
name: (string) (len=6) "ONLINE"
},
(main.EnumValue) {
number: (int64) 1,
name: (string) (len=7) "SYNCING"
}
}
})
}
(map[string]*main.Message) (len=5) {
(string) (len=5) "Empty": (*main.Message)(0xc0003c94a0)({
fullName: (string) (len=5) "Empty",
name: (string) (len=5) "Empty",
fields: (map[string]*main.Field) {
}
}),
(string) (len=13) "StateResponse": (*main.Message)(0xc0003c9500)({
fullName: (string) (len=13) "StateResponse",
name: (string) (len=13) "StateResponse",
fields: (map[string]*main.Field) (len=2) {
(string) (len=11) "config_sent": (*main.Field)(0xc0003ee440)({
name: (string) (len=11) "config_sent",
kind: (string) (len=4) "bool",
([]*main.SortableMessage) (len=5 cap=8) {
(*main.SortableMessage)(0xc0003475c0)({
fullName: (string) (len=24) "AdminConnectInfoResponse",
name: (string) (len=24) "AdminConnectInfoResponse",
fields: ([]*main.Field) (len=2 cap=2) {
(*main.Field)(0xc000347480)({
name: (string) (len=12) "connect_info",
kind: (string) (len=37) "AdminConnectInfoResponse_connect_info",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isMessage: (bool) true,
isOptional: (bool) false,
oneOfName: (string) ""
oneOfName: (string) (len=12) "connect_info"
}),
(string) (len=12) "admin_linked": (*main.Field)(0xc0003ee480)({
name: (string) (len=12) "admin_linked",
kind: (string) (len=4) "bool",
(*main.Field)(0xc000346bc0)({
name: (string) (len=8) "nprofile",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
@ -166,21 +156,11 @@
})
}
}),
(string) (len=13) "ConfigRequest": (*main.Message)(0xc0003c9560)({
(*main.SortableMessage)(0xc000347580)({
fullName: (string) (len=13) "ConfigRequest",
name: (string) (len=13) "ConfigRequest",
fields: (map[string]*main.Field) (len=4) {
(string) (len=9) "relay_url": (*main.Field)(0xc0003ee500)({
name: (string) (len=9) "relay_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=18) "automate_liquidity": (*main.Field)(0xc0003ee540)({
fields: ([]*main.Field) (len=5 cap=8) {
(*main.Field)(0xc000346b00)({
name: (string) (len=18) "automate_liquidity",
kind: (string) (len=4) "bool",
isMap: (bool) false,
@ -190,7 +170,17 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=21) "push_backups_to_nostr": (*main.Field)(0xc0003ee580)({
(*main.Field)(0xc000346b80)({
name: (string) (len=10) "avatar_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346b40)({
name: (string) (len=21) "push_backups_to_nostr",
kind: (string) (len=4) "bool",
isMap: (bool) false,
@ -200,7 +190,17 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=11) "source_name": (*main.Field)(0xc0003ee4c0)({
(*main.Field)(0xc000346ac0)({
name: (string) (len=9) "relay_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346a80)({
name: (string) (len=11) "source_name",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -212,37 +212,16 @@
})
}
}),
(string) (len=24) "AdminConnectInfoResponse": (*main.Message)(0xc0003c95c0)({
fullName: (string) (len=24) "AdminConnectInfoResponse",
name: (string) (len=24) "AdminConnectInfoResponse",
fields: (map[string]*main.Field) (len=2) {
(string) (len=8) "nprofile": (*main.Field)(0xc0003ee5c0)({
name: (string) (len=8) "nprofile",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=37) "AdminConnectInfoResponse_connect_info": (*main.Field)(0xc0003eea80)({
name: (string) (len=12) "connect_info",
kind: (string) (len=37) "AdminConnectInfoResponse_connect_info",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
})
}
(*main.SortableMessage)(0xc0003474c0)({
fullName: (string) (len=5) "Empty",
name: (string) (len=5) "Empty",
fields: ([]*main.Field) <nil>
}),
(string) (len=20) "ServiceStateResponse": (*main.Message)(0xc0003c9620)({
(*main.SortableMessage)(0xc000347640)({
fullName: (string) (len=20) "ServiceStateResponse",
name: (string) (len=20) "ServiceStateResponse",
fields: (map[string]*main.Field) (len=8) {
(string) (len=10) "admin_npub": (*main.Field)(0xc0003ee700)({
fields: ([]*main.Field) (len=14 cap=16) {
(*main.Field)(0xc000346d00)({
name: (string) (len=10) "admin_npub",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -252,8 +231,18 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=15) "relay_connected": (*main.Field)(0xc0003ee740)({
name: (string) (len=15) "relay_connected",
(*main.Field)(0xc000346fc0)({
name: (string) (len=6) "app_id",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346f00)({
name: (string) (len=18) "automate_liquidity",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
@ -262,19 +251,9 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=9) "lnd_state": (*main.Field)(0xc0003ee780)({
name: (string) (len=9) "lnd_state",
kind: (string) (len=8) "LndState",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) true,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=11) "watchdog_ok": (*main.Field)(0xc0003ee7c0)({
name: (string) (len=11) "watchdog_ok",
kind: (string) (len=4) "bool",
(*main.Field)(0xc000346f80)({
name: (string) (len=10) "avatar_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
@ -282,7 +261,7 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=8) "http_url": (*main.Field)(0xc0003ee800)({
(*main.Field)(0xc000346e00)({
name: (string) (len=8) "http_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -292,7 +271,17 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=8) "nprofile": (*main.Field)(0xc0003ee840)({
(*main.Field)(0xc000346d80)({
name: (string) (len=9) "lnd_state",
kind: (string) (len=8) "LndState",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) true,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346e40)({
name: (string) (len=8) "nprofile",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -302,7 +291,7 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=13) "provider_name": (*main.Field)(0xc0003ee680)({
(*main.Field)(0xc000346c80)({
name: (string) (len=13) "provider_name",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -312,7 +301,37 @@
isOptional: (bool) false,
oneOfName: (string) ""
}),
(string) (len=6) "relays": (*main.Field)(0xc0003ee6c0)({
(*main.Field)(0xc000346f40)({
name: (string) (len=21) "push_backups_to_nostr",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346d40)({
name: (string) (len=15) "relay_connected",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346ec0)({
name: (string) (len=9) "relay_url",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346cc0)({
name: (string) (len=6) "relays",
kind: (string) (len=6) "string",
isMap: (bool) false,
@ -321,39 +340,88 @@
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346e80)({
name: (string) (len=11) "source_name",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346dc0)({
name: (string) (len=11) "watchdog_ok",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
})
}
}),
(*main.SortableMessage)(0xc000347500)({
fullName: (string) (len=13) "StateResponse",
name: (string) (len=13) "StateResponse",
fields: ([]*main.Field) (len=2 cap=2) {
(*main.Field)(0xc000346a40)({
name: (string) (len=12) "admin_linked",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
}),
(*main.Field)(0xc000346a00)({
name: (string) (len=11) "config_sent",
kind: (string) (len=4) "bool",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) ""
})
}
})
}
(map[string][]*main.Field) (len=1) {
(string) (len=37) "AdminConnectInfoResponse_connect_info": ([]*main.Field) (len=2 cap=2) {
(*main.Field)(0xc0003ee600)({
name: (string) (len=11) "admin_token",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
}),
(*main.Field)(0xc0003ee640)({
name: (string) (len=13) "enrolled_npub",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
})
}
([]*main.OneOf) (len=1 cap=1) {
(*main.OneOf)(0xc0000e31a0)({
kind: (string) (len=37) "AdminConnectInfoResponse_connect_info",
fields: ([]*main.Field) (len=2 cap=2) {
(*main.Field)(0xc000346c00)({
name: (string) (len=11) "admin_token",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
}),
(*main.Field)(0xc000346c40)({
name: (string) (len=13) "enrolled_npub",
kind: (string) (len=6) "string",
isMap: (bool) false,
isArray: (bool) false,
isEnum: (bool) false,
isMessage: (bool) false,
isOptional: (bool) false,
oneOfName: (string) (len=12) "connect_info"
})
}
})
}
parsing file: wizard_structs 5
parsing file: wizard_methods 2
-> [{guest Guest map[]}]
-> [{guest Guest []}]
([]interface {}) <nil>

View file

@ -0,0 +1,151 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
package lightning_pub
import (
"net/url"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
func doPostRequest(url string, body []byte, auth string) ([]byte, error) {
bodyReader := bytes.NewReader(body)
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("authorization", auth)
return doRequest(req)
}
func doGetRequest(url string, auth string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("authorization", auth)
return doRequest(req)
}
func doRequest(req *http.Request) ([]byte, error) {
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return resBody, nil
}
type ClientParams struct {
BaseURL string
RetrieveGuestAuth func() (string, error)
}
type Client struct {
GetAdminConnectInfo func() (*AdminConnectInfoResponse, error)
GetServiceState func() (*ServiceStateResponse, error)
WizardConfig func(req ConfigRequest) error
WizardState func() (*StateResponse, error)
}
func NewClient(params ClientParams) *Client {
return &Client{
GetAdminConnectInfo: func() (*AdminConnectInfoResponse, error) {
auth, err := params.RetrieveGuestAuth()
if err != nil {
return nil, err
}
finalRoute := "/wizard/admin_connect_info"
resBody, err := doGetRequest(params.BaseURL+finalRoute, auth)
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := AdminConnectInfoResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetServiceState: func() (*ServiceStateResponse, error) {
auth, err := params.RetrieveGuestAuth()
if err != nil {
return nil, err
}
finalRoute := "/wizard/service_state"
resBody, err := doGetRequest(params.BaseURL+finalRoute, auth)
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := ServiceStateResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
WizardConfig: func(req ConfigRequest) error {
auth, err := params.RetrieveGuestAuth()
if err != nil {
return err
}
finalRoute := "/wizard/config"
body, err := json.Marshal(req)
if err != nil {
return err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return err
}
if result.Status == "ERROR" {
return fmt.Errorf(result.Reason)
}
return nil
},
WizardState: func() (*StateResponse, error) {
auth, err := params.RetrieveGuestAuth()
if err != nil {
return nil, err
}
finalRoute := "/wizard/state"
resBody, err := doGetRequest(params.BaseURL+finalRoute, auth)
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := StateResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
}
}

View file

@ -0,0 +1,63 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
package lightning_pub
type ResultError struct {
Status string `json:"status"`
Reason string `json:"reason"`
}
type GuestContext struct {
}
type LndState string
const (
OFFLINE LndState = "OFFLINE"
ONLINE LndState = "ONLINE"
SYNCING LndState = "SYNCING"
)
type AdminConnectInfoResponse struct {
Connect_info *AdminConnectInfoResponse_connect_info `json:"connect_info"`
Nprofile string `json:"nprofile"`
}
type ConfigRequest struct {
Automate_liquidity bool `json:"automate_liquidity"`
Avatar_url string `json:"avatar_url"`
Push_backups_to_nostr bool `json:"push_backups_to_nostr"`
Relay_url string `json:"relay_url"`
Source_name string `json:"source_name"`
}
type Empty struct {
}
type ServiceStateResponse struct {
Admin_npub string `json:"admin_npub"`
App_id string `json:"app_id"`
Automate_liquidity bool `json:"automate_liquidity"`
Avatar_url string `json:"avatar_url"`
Http_url string `json:"http_url"`
Lnd_state LndState `json:"lnd_state"`
Nprofile string `json:"nprofile"`
Provider_name string `json:"provider_name"`
Push_backups_to_nostr bool `json:"push_backups_to_nostr"`
Relay_connected bool `json:"relay_connected"`
Relay_url string `json:"relay_url"`
Relays []string `json:"relays"`
Source_name string `json:"source_name"`
Watchdog_ok bool `json:"watchdog_ok"`
}
type StateResponse struct {
Admin_linked bool `json:"admin_linked"`
Config_sent bool `json:"config_sent"`
}
type AdminConnectInfoResponse_connect_info_type string
const (
ADMIN_TOKEN AdminConnectInfoResponse_connect_info_type = "admin_token"
ENROLLED_NPUB AdminConnectInfoResponse_connect_info_type = "enrolled_npub"
)
type AdminConnectInfoResponse_connect_info struct {
Type AdminConnectInfoResponse_connect_info_type `json:"type"`
Admin_token *string `json:"admin_token"`
Enrolled_npub *string `json:"enrolled_npub"`
}

View file

@ -29,47 +29,6 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
app.use(json())
app.use(urlencoded({ extended: true }))
if (opts.logMethod) app.use((req, _, next) => { console.log(req.method, req.path); if (opts.logBody) console.log(req.body); next() })
if (!opts.allowNotImplementedMethods && !methods.WizardState) throw new Error('method: WizardState is not implemented')
app.get('/wizard/state', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardState', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardState) throw new Error('method: WizardState is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.WizardState({rpcName:'WizardState', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
app.post('/wizard/config', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardConfig', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.ConfigRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.WizardConfig({rpcName:'WizardConfig', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetAdminConnectInfo) throw new Error('method: GetAdminConnectInfo is not implemented')
app.get('/wizard/admin_connect_info', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminConnectInfo', batch: false, nostr: false, batchSize: 0}
@ -108,6 +67,47 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
app.post('/wizard/config', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardConfig', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardConfig) throw new Error('method: WizardConfig is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.ConfigRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.WizardConfig({rpcName:'WizardConfig', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.WizardState) throw new Error('method: WizardState is not implemented')
app.get('/wizard/state', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'WizardState', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.WizardState) throw new Error('method: WizardState is not implemented')
const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.WizardState({rpcName:'WizardState', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (opts.staticFiles) {
app.use(express.static(opts.staticFiles))
app.get('*', function (_, res) { res.sendFile('index.html', { root: opts.staticFiles })})

View file

@ -12,31 +12,6 @@ export type ClientParams = {
checkResult?: true
}
export default (params: ClientParams) => ({
WizardState: async (): Promise<ResultError | ({ status: 'OK' }& Types.StateResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/state'
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.StateResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
WizardConfig: async (request: Types.ConfigRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/config'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminConnectInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.AdminConnectInfoResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
@ -65,4 +40,29 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
WizardConfig: async (request: Types.ConfigRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/config'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
WizardState: async (): Promise<ResultError | ({ status: 'OK' }& Types.StateResponse)> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/wizard/state'
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.StateResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
})

View file

@ -6,33 +6,33 @@ export type RequestStats = { startMs:number, start:bigint, parse: bigint, guard:
export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: string }
export type GuestContext = {
}
export type GuestMethodInputs = WizardState_Input | WizardConfig_Input | GetAdminConnectInfo_Input | GetServiceState_Input
export type GuestMethodOutputs = WizardState_Output | WizardConfig_Output | GetAdminConnectInfo_Output | GetServiceState_Output
export type GuestMethodInputs = GetAdminConnectInfo_Input | GetServiceState_Input | WizardConfig_Input | WizardState_Input
export type GuestMethodOutputs = GetAdminConnectInfo_Output | GetServiceState_Output | WizardConfig_Output | WizardState_Output
export type AuthContext = GuestContext
export type WizardState_Input = {rpcName:'WizardState'}
export type WizardState_Output = ResultError | ({ status: 'OK' } & StateResponse)
export type WizardConfig_Input = {rpcName:'WizardConfig', req: ConfigRequest}
export type WizardConfig_Output = ResultError | { status: 'OK' }
export type GetAdminConnectInfo_Input = {rpcName:'GetAdminConnectInfo'}
export type GetAdminConnectInfo_Output = ResultError | ({ status: 'OK' } & AdminConnectInfoResponse)
export type GetServiceState_Input = {rpcName:'GetServiceState'}
export type GetServiceState_Output = ResultError | ({ status: 'OK' } & ServiceStateResponse)
export type WizardConfig_Input = {rpcName:'WizardConfig', req: ConfigRequest}
export type WizardConfig_Output = ResultError | { status: 'OK' }
export type WizardState_Input = {rpcName:'WizardState'}
export type WizardState_Output = ResultError | ({ status: 'OK' } & StateResponse)
export type ServerMethods = {
WizardState?: (req: WizardState_Input & {ctx: GuestContext }) => Promise<StateResponse>
WizardConfig?: (req: WizardConfig_Input & {ctx: GuestContext }) => Promise<void>
GetAdminConnectInfo?: (req: GetAdminConnectInfo_Input & {ctx: GuestContext }) => Promise<AdminConnectInfoResponse>
GetServiceState?: (req: GetServiceState_Input & {ctx: GuestContext }) => Promise<ServiceStateResponse>
WizardConfig?: (req: WizardConfig_Input & {ctx: GuestContext }) => Promise<void>
WizardState?: (req: WizardState_Input & {ctx: GuestContext }) => Promise<StateResponse>
}
export enum LndState {
OFFLINE = 'OFFLINE',
SYNCING = 'SYNCING',
ONLINE = 'ONLINE',
SYNCING = 'SYNCING',
}
export const enumCheckLndState = (e?: LndState): boolean => {
for (const v in LndState) if (e === v) return true
@ -43,6 +43,68 @@ export type OptionsBaseMessage = {
allOptionalsAreSet?: true
}
export type AdminConnectInfoResponse = {
connect_info: AdminConnectInfoResponse_connect_info
nprofile: string
}
export const AdminConnectInfoResponseOptionalFields: [] = []
export type AdminConnectInfoResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
connect_info_Options?: AdminConnectInfoResponse_connect_infoOptions
nprofile_CustomCheck?: (v: string) => boolean
}
export const AdminConnectInfoResponseValidate = (o?: AdminConnectInfoResponse, opts: AdminConnectInfoResponseOptions = {}, path: string = 'AdminConnectInfoResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
const connect_infoErr = AdminConnectInfoResponse_connect_infoValidate(o.connect_info, opts.connect_info_Options, `${path}.connect_info`)
if (connect_infoErr !== null) return connect_infoErr
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
return null
}
export type ConfigRequest = {
automate_liquidity: boolean
avatar_url: string
push_backups_to_nostr: boolean
relay_url: string
source_name: string
}
export const ConfigRequestOptionalFields: [] = []
export type ConfigRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
automate_liquidity_CustomCheck?: (v: boolean) => boolean
avatar_url_CustomCheck?: (v: string) => boolean
push_backups_to_nostr_CustomCheck?: (v: boolean) => boolean
relay_url_CustomCheck?: (v: string) => boolean
source_name_CustomCheck?: (v: string) => boolean
}
export const ConfigRequestValidate = (o?: ConfigRequest, opts: ConfigRequestOptions = {}, path: string = 'ConfigRequest::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.automate_liquidity !== 'boolean') return new Error(`${path}.automate_liquidity: is not a boolean`)
if (opts.automate_liquidity_CustomCheck && !opts.automate_liquidity_CustomCheck(o.automate_liquidity)) return new Error(`${path}.automate_liquidity: custom check failed`)
if (typeof o.avatar_url !== 'string') return new Error(`${path}.avatar_url: is not a string`)
if (opts.avatar_url_CustomCheck && !opts.avatar_url_CustomCheck(o.avatar_url)) return new Error(`${path}.avatar_url: custom check failed`)
if (typeof o.push_backups_to_nostr !== 'boolean') return new Error(`${path}.push_backups_to_nostr: is not a boolean`)
if (opts.push_backups_to_nostr_CustomCheck && !opts.push_backups_to_nostr_CustomCheck(o.push_backups_to_nostr)) return new Error(`${path}.push_backups_to_nostr: custom check failed`)
if (typeof o.relay_url !== 'string') return new Error(`${path}.relay_url: is not a string`)
if (opts.relay_url_CustomCheck && !opts.relay_url_CustomCheck(o.relay_url)) return new Error(`${path}.relay_url: custom check failed`)
if (typeof o.source_name !== 'string') return new Error(`${path}.source_name: is not a string`)
if (opts.source_name_CustomCheck && !opts.source_name_CustomCheck(o.source_name)) return new Error(`${path}.source_name: custom check failed`)
return null
}
export type Empty = {
}
export const EmptyOptionalFields: [] = []
@ -56,138 +118,111 @@ export const EmptyValidate = (o?: Empty, opts: EmptyOptions = {}, path: string =
return null
}
export type StateResponse = {
config_sent: boolean
admin_linked: boolean
}
export const StateResponseOptionalFields: [] = []
export type StateResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
config_sent_CustomCheck?: (v: boolean) => boolean
admin_linked_CustomCheck?: (v: boolean) => boolean
}
export const StateResponseValidate = (o?: StateResponse, opts: StateResponseOptions = {}, path: string = 'StateResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.config_sent !== 'boolean') return new Error(`${path}.config_sent: is not a boolean`)
if (opts.config_sent_CustomCheck && !opts.config_sent_CustomCheck(o.config_sent)) return new Error(`${path}.config_sent: custom check failed`)
if (typeof o.admin_linked !== 'boolean') return new Error(`${path}.admin_linked: is not a boolean`)
if (opts.admin_linked_CustomCheck && !opts.admin_linked_CustomCheck(o.admin_linked)) return new Error(`${path}.admin_linked: custom check failed`)
return null
}
export type ConfigRequest = {
source_name: string
relay_url: string
automate_liquidity: boolean
push_backups_to_nostr: boolean
}
export const ConfigRequestOptionalFields: [] = []
export type ConfigRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
push_backups_to_nostr_CustomCheck?: (v: boolean) => boolean
source_name_CustomCheck?: (v: string) => boolean
relay_url_CustomCheck?: (v: string) => boolean
automate_liquidity_CustomCheck?: (v: boolean) => boolean
}
export const ConfigRequestValidate = (o?: ConfigRequest, opts: ConfigRequestOptions = {}, path: string = 'ConfigRequest::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.source_name !== 'string') return new Error(`${path}.source_name: is not a string`)
if (opts.source_name_CustomCheck && !opts.source_name_CustomCheck(o.source_name)) return new Error(`${path}.source_name: custom check failed`)
if (typeof o.relay_url !== 'string') return new Error(`${path}.relay_url: is not a string`)
if (opts.relay_url_CustomCheck && !opts.relay_url_CustomCheck(o.relay_url)) return new Error(`${path}.relay_url: custom check failed`)
if (typeof o.automate_liquidity !== 'boolean') return new Error(`${path}.automate_liquidity: is not a boolean`)
if (opts.automate_liquidity_CustomCheck && !opts.automate_liquidity_CustomCheck(o.automate_liquidity)) return new Error(`${path}.automate_liquidity: custom check failed`)
if (typeof o.push_backups_to_nostr !== 'boolean') return new Error(`${path}.push_backups_to_nostr: is not a boolean`)
if (opts.push_backups_to_nostr_CustomCheck && !opts.push_backups_to_nostr_CustomCheck(o.push_backups_to_nostr)) return new Error(`${path}.push_backups_to_nostr: custom check failed`)
return null
}
export type AdminConnectInfoResponse = {
nprofile: string
connect_info: AdminConnectInfoResponse_connect_info
}
export const AdminConnectInfoResponseOptionalFields: [] = []
export type AdminConnectInfoResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
nprofile_CustomCheck?: (v: string) => boolean
connect_info_CustomCheck?: (v: AdminConnectInfoResponse_connect_info) => boolean
}
export const AdminConnectInfoResponseValidate = (o?: AdminConnectInfoResponse, opts: AdminConnectInfoResponseOptions = {}, path: string = 'AdminConnectInfoResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
const connect_infoErr = AdminConnectInfoResponse_connect_infoValidate(o.connect_info,{}, `${path}.connect_info`)
if (connect_infoErr !== null) return connect_infoErr
return null
}
export type ServiceStateResponse = {
admin_npub: string
app_id: string
automate_liquidity: boolean
avatar_url: string
http_url: string
lnd_state: LndState
nprofile: string
provider_name: string
relays: string[]
admin_npub: string
push_backups_to_nostr: boolean
relay_connected: boolean
lnd_state: LndState
relay_url: string
relays: string[]
source_name: string
watchdog_ok: boolean
}
export const ServiceStateResponseOptionalFields: [] = []
export type ServiceStateResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
admin_npub_CustomCheck?: (v: string) => boolean
app_id_CustomCheck?: (v: string) => boolean
automate_liquidity_CustomCheck?: (v: boolean) => boolean
avatar_url_CustomCheck?: (v: string) => boolean
http_url_CustomCheck?: (v: string) => boolean
lnd_state_CustomCheck?: (v: LndState) => boolean
nprofile_CustomCheck?: (v: string) => boolean
provider_name_CustomCheck?: (v: string) => boolean
relays_CustomCheck?: (v: string[]) => boolean
admin_npub_CustomCheck?: (v: string) => boolean
push_backups_to_nostr_CustomCheck?: (v: boolean) => boolean
relay_connected_CustomCheck?: (v: boolean) => boolean
lnd_state_CustomCheck?: (v: LndState) => boolean
relay_url_CustomCheck?: (v: string) => boolean
relays_CustomCheck?: (v: string[]) => boolean
source_name_CustomCheck?: (v: string) => boolean
watchdog_ok_CustomCheck?: (v: boolean) => boolean
}
export const ServiceStateResponseValidate = (o?: ServiceStateResponse, opts: ServiceStateResponseOptions = {}, path: string = 'ServiceStateResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.admin_npub !== 'string') return new Error(`${path}.admin_npub: is not a string`)
if (opts.admin_npub_CustomCheck && !opts.admin_npub_CustomCheck(o.admin_npub)) return new Error(`${path}.admin_npub: custom check failed`)
if (typeof o.app_id !== 'string') return new Error(`${path}.app_id: is not a string`)
if (opts.app_id_CustomCheck && !opts.app_id_CustomCheck(o.app_id)) return new Error(`${path}.app_id: custom check failed`)
if (typeof o.automate_liquidity !== 'boolean') return new Error(`${path}.automate_liquidity: is not a boolean`)
if (opts.automate_liquidity_CustomCheck && !opts.automate_liquidity_CustomCheck(o.automate_liquidity)) return new Error(`${path}.automate_liquidity: custom check failed`)
if (typeof o.avatar_url !== 'string') return new Error(`${path}.avatar_url: is not a string`)
if (opts.avatar_url_CustomCheck && !opts.avatar_url_CustomCheck(o.avatar_url)) return new Error(`${path}.avatar_url: custom check failed`)
if (typeof o.http_url !== 'string') return new Error(`${path}.http_url: is not a string`)
if (opts.http_url_CustomCheck && !opts.http_url_CustomCheck(o.http_url)) return new Error(`${path}.http_url: custom check failed`)
if (!enumCheckLndState(o.lnd_state)) return new Error(`${path}.lnd_state: is not a valid LndState`)
if (opts.lnd_state_CustomCheck && !opts.lnd_state_CustomCheck(o.lnd_state)) return new Error(`${path}.lnd_state: custom check failed`)
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
if (typeof o.provider_name !== 'string') return new Error(`${path}.provider_name: is not a string`)
if (opts.provider_name_CustomCheck && !opts.provider_name_CustomCheck(o.provider_name)) return new Error(`${path}.provider_name: custom check failed`)
if (typeof o.push_backups_to_nostr !== 'boolean') return new Error(`${path}.push_backups_to_nostr: is not a boolean`)
if (opts.push_backups_to_nostr_CustomCheck && !opts.push_backups_to_nostr_CustomCheck(o.push_backups_to_nostr)) return new Error(`${path}.push_backups_to_nostr: custom check failed`)
if (typeof o.relay_connected !== 'boolean') return new Error(`${path}.relay_connected: is not a boolean`)
if (opts.relay_connected_CustomCheck && !opts.relay_connected_CustomCheck(o.relay_connected)) return new Error(`${path}.relay_connected: custom check failed`)
if (typeof o.relay_url !== 'string') return new Error(`${path}.relay_url: is not a string`)
if (opts.relay_url_CustomCheck && !opts.relay_url_CustomCheck(o.relay_url)) return new Error(`${path}.relay_url: custom check failed`)
if (!Array.isArray(o.relays)) return new Error(`${path}.relays: is not an array`)
for (let index = 0; index < o.relays.length; index++) {
if (typeof o.relays[index] !== 'string') return new Error(`${path}.relays[${index}]: is not a string`)
}
if (opts.relays_CustomCheck && !opts.relays_CustomCheck(o.relays)) return new Error(`${path}.relays: custom check failed`)
if (typeof o.admin_npub !== 'string') return new Error(`${path}.admin_npub: is not a string`)
if (opts.admin_npub_CustomCheck && !opts.admin_npub_CustomCheck(o.admin_npub)) return new Error(`${path}.admin_npub: custom check failed`)
if (typeof o.relay_connected !== 'boolean') return new Error(`${path}.relay_connected: is not a boolean`)
if (opts.relay_connected_CustomCheck && !opts.relay_connected_CustomCheck(o.relay_connected)) return new Error(`${path}.relay_connected: custom check failed`)
if (!enumCheckLndState(o.lnd_state)) return new Error(`${path}.lnd_state: is not a valid LndState`)
if (opts.lnd_state_CustomCheck && !opts.lnd_state_CustomCheck(o.lnd_state)) return new Error(`${path}.lnd_state: custom check failed`)
if (typeof o.source_name !== 'string') return new Error(`${path}.source_name: is not a string`)
if (opts.source_name_CustomCheck && !opts.source_name_CustomCheck(o.source_name)) return new Error(`${path}.source_name: custom check failed`)
if (typeof o.watchdog_ok !== 'boolean') return new Error(`${path}.watchdog_ok: is not a boolean`)
if (opts.watchdog_ok_CustomCheck && !opts.watchdog_ok_CustomCheck(o.watchdog_ok)) return new Error(`${path}.watchdog_ok: custom check failed`)
if (typeof o.http_url !== 'string') return new Error(`${path}.http_url: is not a string`)
if (opts.http_url_CustomCheck && !opts.http_url_CustomCheck(o.http_url)) return new Error(`${path}.http_url: custom check failed`)
return null
}
if (typeof o.nprofile !== 'string') return new Error(`${path}.nprofile: is not a string`)
if (opts.nprofile_CustomCheck && !opts.nprofile_CustomCheck(o.nprofile)) return new Error(`${path}.nprofile: custom check failed`)
export type StateResponse = {
admin_linked: boolean
config_sent: boolean
}
export const StateResponseOptionalFields: [] = []
export type StateResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
admin_linked_CustomCheck?: (v: boolean) => boolean
config_sent_CustomCheck?: (v: boolean) => boolean
}
export const StateResponseValidate = (o?: StateResponse, opts: StateResponseOptions = {}, path: string = 'StateResponse::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.provider_name !== 'string') return new Error(`${path}.provider_name: is not a string`)
if (opts.provider_name_CustomCheck && !opts.provider_name_CustomCheck(o.provider_name)) return new Error(`${path}.provider_name: custom check failed`)
if (typeof o.admin_linked !== 'boolean') return new Error(`${path}.admin_linked: is not a boolean`)
if (opts.admin_linked_CustomCheck && !opts.admin_linked_CustomCheck(o.admin_linked)) return new Error(`${path}.admin_linked: custom check failed`)
if (typeof o.config_sent !== 'boolean') return new Error(`${path}.config_sent: is not a boolean`)
if (opts.config_sent_CustomCheck && !opts.config_sent_CustomCheck(o.config_sent)) return new Error(`${path}.config_sent: custom check failed`)
return null
}
@ -196,19 +231,34 @@ export enum AdminConnectInfoResponse_connect_info_type {
ADMIN_TOKEN = 'admin_token',
ENROLLED_NPUB = 'enrolled_npub',
}
export const enumCheckAdminConnectInfoResponse_connect_info_type = (e?: AdminConnectInfoResponse_connect_info_type): boolean => {
for (const v in AdminConnectInfoResponse_connect_info_type) if (e === v) return true
return false
}
export type AdminConnectInfoResponse_connect_info =
{type:AdminConnectInfoResponse_connect_info_type.ADMIN_TOKEN, admin_token:string}|
{type:AdminConnectInfoResponse_connect_info_type.ENROLLED_NPUB, enrolled_npub:string}
export const AdminConnectInfoResponse_connect_infoValidate = (o?: AdminConnectInfoResponse_connect_info, opts = {}, path: string = 'AdminConnectInfoResponse_connect_info::root.'): Error | null => {
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
switch (o.type) {
case 'admin_token':
if (typeof o.admin_token !== 'string') return new Error(`${path}.admin_token: is not a string`)
break
case 'enrolled_npub':
if (typeof o.enrolled_npub !== 'string') return new Error(`${path}.enrolled_npub: is not a string`)
break
}
return new Error(path + ': unknown type'+ o.type)
export type AdminConnectInfoResponse_connect_infoOptions = {
admin_token_CustomCheck?: (v: string) => boolean
enrolled_npub_CustomCheck?: (v: string) => boolean
}
export const AdminConnectInfoResponse_connect_infoValidate = (o?: AdminConnectInfoResponse_connect_info, opts:AdminConnectInfoResponse_connect_infoOptions = {}, path: string = 'AdminConnectInfoResponse_connect_info::root.'): Error | null => {
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
const stringType: string = o.type
switch (o.type) {
case AdminConnectInfoResponse_connect_info_type.ADMIN_TOKEN:
if (typeof o.admin_token !== 'string') return new Error(`${path}.admin_token: is not a string`)
if (opts.admin_token_CustomCheck && !opts.admin_token_CustomCheck(o.admin_token)) return new Error(`${path}.admin_token: custom check failed`)
break
case AdminConnectInfoResponse_connect_info_type.ENROLLED_NPUB:
if (typeof o.enrolled_npub !== 'string') return new Error(`${path}.enrolled_npub: is not a string`)
if (opts.enrolled_npub_CustomCheck && !opts.enrolled_npub_CustomCheck(o.enrolled_npub)) return new Error(`${path}.enrolled_npub: custom check failed`)
break
default:
return new Error(path + ': unknown type '+ stringType)
}
return null
}

View file

@ -11,7 +11,7 @@ log() {
echo -e "$(echo "$message" | sed 's/\\e\[[0-9;]*m//g')" >> "$TMP_LOG_FILE"
}
SCRIPT_VERSION="0.2.1"
SCRIPT_VERSION="0.2.2"
REPO="shocknet/Lightning.Pub"
BRANCH="master"

View file

@ -95,7 +95,11 @@ install_lnd() {
# Check for and add default settings only if the keys are missing.
grep -q "^bitcoin.mainnet=" $USER_HOME/.lnd/lnd.conf || echo "bitcoin.mainnet=true" >> $USER_HOME/.lnd/lnd.conf
grep -q "^bitcoin.node=" $USER_HOME/.lnd/lnd.conf || echo "bitcoin.node=neutrino" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=neutrino.shock.network" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=neutrino.shock.network" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=neutrino.shock.network" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=asia.blixtwallet.com" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=asia.blixtwallet.com" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=europe.blixtwallet.com" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=europe.blixtwallet.com" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=btcd.lnolymp.us" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=btcd.lnolymp.us" >> $USER_HOME/.lnd/lnd.conf
grep -q "^neutrino.addpeer=btcd-mainnet.lightning.computer" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=btcd-mainnet.lightning.computer" >> $USER_HOME/.lnd/lnd.conf
grep -q "^fee.url=" $USER_HOME/.lnd/lnd.conf || echo "fee.url=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json" >> $USER_HOME/.lnd/lnd.conf
chmod 600 $USER_HOME/.lnd/lnd.conf

View file

@ -80,7 +80,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
}, event.startAtNano, event.startAtMs)
})
return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args), Ping: () => nostr.Ping() }
// Mark nostr connected/ready after initial subscription tick
mainHandler.adminManager.setNostrConnected(true)
return { Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop }, Send: (...args) => nostr.Send(...args), Ping: () => nostr.Ping() }
}

View file

@ -22,6 +22,7 @@ export class AdminManager {
interval: NodeJS.Timer
appNprofile: string
lnd: LND
nostrConnected: boolean = false
constructor(mainSettings: MainSettings, storage: Storage) {
this.storage = storage
this.dataDir = mainSettings.storageSettings.dataDir
@ -42,6 +43,14 @@ export class AdminManager {
this.lnd = lnd
}
setNostrConnected = (connected: boolean) => {
this.nostrConnected = connected
}
GetNostrConnected = () => {
return this.nostrConnected
}
setAppNprofile = (nprofile: string) => {
this.appNprofile = nprofile
const enrollToken = this.ReadAdminEnrollToken()

View file

@ -99,7 +99,7 @@ export default class {
StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => {
this.UpdateBeacon(app, { type: 'service', name: app.name })
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: (app as any).avatar_url })
})
}
@ -386,7 +386,7 @@ export default class {
})
}
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {
async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string }) {
if (!app.nostr_public_key) {
getLogger({ appName: app.name })("cannot update beacon, public key not set")
return

View file

@ -27,6 +27,9 @@ export class Application {
@Column({ nullable: true, unique: true })
nostr_public_key?: string
@Column({ nullable: true })
avatar_url?: string
@CreateDateColumn()
created_at: Date

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ApplicationAvatarUrl1761000001000 implements MigrationInterface {
name = 'ApplicationAvatarUrl1761000001000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "application" ADD COLUMN "avatar_url" varchar`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// SQLite has limited ALTER TABLE support; we can recreate table if needed, but for now just keep it simple
// No-op: dropping a column is non-trivial; leave column in place on downgrade
}
}

View file

@ -25,12 +25,13 @@ import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something
import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js'
import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000]
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {

View file

@ -40,19 +40,63 @@ export class Wizard {
}
GetServiceState = async (): Promise<WizardTypes.ServiceStateResponse> => {
const apps = await this.storage.applicationStorage.GetApplications()
const appNamesList = apps.map(app => app.name).join(', ')
return {
admin_npub: this.adminManager.GetAdminNpub(),
http_url: this.settings.serviceUrl,
lnd_state: WizardTypes.LndState.OFFLINE,
nprofile: this.nprofile,
provider_name: appNamesList,
relay_connected: false,
relays: this.relays,
watchdog_ok: false
try {
const apps = await this.storage.applicationStorage.GetApplications()
const appNamesList = apps.map(app => app.name).join(', ')
const relays = this.settings.nostrRelaySettings ? this.settings.nostrRelaySettings.relays : [];
const relayUrl = (relays && relays.length > 0) ? relays[0] : '';
const defaultApp = apps.find(a => a.name === this.settings.defaultAppName) || apps[0]
// Determine LND state and watchdog
let lndState: WizardTypes.LndState = WizardTypes.LndState.OFFLINE
let watchdogOk = false
try {
const info = await this.adminManager.LndGetInfo()
const online = info.synced_to_chain && info.synced_to_graph
lndState = online ? WizardTypes.LndState.ONLINE : WizardTypes.LndState.SYNCING
watchdogOk = !info.watchdog_barking
} catch {
lndState = WizardTypes.LndState.OFFLINE
watchdogOk = false
}
return {
admin_npub: this.adminManager.GetAdminNpub(),
http_url: this.settings.serviceUrl,
lnd_state: lndState,
nprofile: this.nprofile,
provider_name: defaultApp?.name || appNamesList,
relay_connected: this.adminManager.GetNostrConnected(),
relays: this.relays,
watchdog_ok: watchdogOk,
source_name: defaultApp?.name || this.settings.defaultAppName || appNamesList,
relay_url: relayUrl,
automate_liquidity: this.settings.liquiditySettings.liquidityProviderPub !== 'null',
push_backups_to_nostr: this.settings.pushBackupsToNostr,
avatar_url: defaultApp?.avatar_url || '',
app_id: defaultApp?.app_id || ''
}
} catch (e) {
this.log(`Error in GetServiceState: ${(e as Error).message}`)
// Return a default/error state that is still valid JSON to prevent client-side parse errors
return {
admin_npub: '',
http_url: '',
lnd_state: WizardTypes.LndState.OFFLINE,
nprofile: '',
provider_name: 'Error loading state',
relay_connected: false,
relays: [],
watchdog_ok: false,
source_name: 'Error',
relay_url: '',
automate_liquidity: false,
push_backups_to_nostr: false,
avatar_url: '',
app_id: ''
}
}
}
WizardState = async (): Promise<WizardTypes.StateResponse> => {
return {
config_sent: this.pendingConfig !== null,
@ -118,10 +162,30 @@ export class Wizard {
relay_url_CustomCheck: relay => relay !== '',
})
if (err != null) { throw new Error(err.message) }
if (this.IsInitialized() || this.pendingConfig !== null) {
throw new Error("already initialized")
}
const pendingConfig = { sourceName: req.source_name, relayUrl: req.relay_url, automateLiquidity: req.automate_liquidity, pushBackupsToNostr: req.push_backups_to_nostr }
// Persist app name/avatar to DB regardless (idempotent behavior)
try {
const appsList = await this.storage.applicationStorage.GetApplications()
const defaultNames = ['wallet', 'wallet-test', this.settings.defaultAppName]
const existingDefaultApp = appsList.find(app => defaultNames.includes(app.name)) || appsList[0]
if (existingDefaultApp) {
await this.storage.applicationStorage.UpdateApplication(existingDefaultApp, { name: req.source_name, avatar_url: (req as any).avatar_url || existingDefaultApp.avatar_url })
}
} catch (e) {
this.log(`Error updating app info: ${(e as Error).message}`)
}
// If already initialized, treat as idempotent update for env and exit
if (this.IsInitialized()) {
this.updateEnvFile(pendingConfig)
return
}
// First-time configuration flow
if (this.pendingConfig !== null) {
throw new Error("already initializing")
}
this.updateEnvFile(pendingConfig)
this.configQueue.forEach(q => q.res(true))
this.configQueue = []
@ -153,10 +217,16 @@ export class Wizard {
}
const automateLiquidityIndex = envFileContent.findIndex(line => line.startsWith('LIQUIDITY_PROVIDER_PUB'))
if (automateLiquidityIndex === -1) {
toMerge.push(`LIQUIDITY_PROVIDER_PUB=${pendingConfig.automateLiquidity ? defaultProviderPub : ""}`)
if (pendingConfig.automateLiquidity) {
if (automateLiquidityIndex !== -1) {
envFileContent.splice(automateLiquidityIndex, 1)
}
} else {
envFileContent[automateLiquidityIndex] = `LIQUIDITY_PROVIDER_PUB=null`
if (automateLiquidityIndex === -1) {
toMerge.push(`LIQUIDITY_PROVIDER_PUB=null`)
} else {
envFileContent[automateLiquidityIndex] = `LIQUIDITY_PROVIDER_PUB=null`
}
}
const pushBackupsToNostrIndex = envFileContent.findIndex(line => line.startsWith('PUSH_BACKUPS_TO_NOSTR'))

View file

@ -1,143 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/backup.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<link rel="icon" type="image/png" href="img/pub_logo.png" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main>
<section class="setup-header">
<button class="icon-button back-button" onclick="history.back()">
<img src="img/back.svg" alt="" />
</button>
<h2>Choose a Recovery Method</h2>
<p class="header-title">
<span style="font-weight: bold">New Node! 🎉</span> It's important
to backup your keys.
</p>
</section>
<div class="line"></div>
<section class="setup-content">
<div class="description-box">
<div class="description">
In addition to your seed phrase, you also need channel details to recover funds should your node experience a
hardware failure.
</div>
<br />
<div class="description">
It's important always to have the latest version of this file. Fortunately, it's small enough to automatically
store on the Nostr relay.
</div>
</div>
<div class="warning-text">
If you did not choose the developers relay, be sure your relay has
adequate storage policies to hold NIP78 events.
</div>
<div class="checkbox-container">
<div class="checkbox" style="margin-top: 12px">
<input type="checkbox" id="backup" />
<div class="checkbox-shape"></div>
<label for="backup">
Encrypted Backup to Nostr Relay
</label>
</div>
</div>
<div class="checkbox-container">
<div class="checkbox manual-checkbox" style="margin-top: 12px">
<input type="checkbox" id="manual-backup" />
<div class="checkbox-shape"></div>
<label for="manual-backup">
DO NOT store on relay (Manual Backups)
</label>
</div>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button hidden-button" style="margin-top: 60px;" id="next-button">
Next
</button>
</section>
</main>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/backup.js"></script>
<script>
const sendConfig = async () => {
const req = {
source_name: localStorage.getItem("wizard/nodeName"),
relay_url: localStorage.getItem("wizard/relayUrl"),
automate_liquidity: localStorage.getItem("wizard/liquidity") === 'automate',
push_backups_to_nostr: localStorage.getItem("wizard/backup") === 'backup',
}
const res = await fetch("/wizard/config", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(req)
})
if (res.status !== 200) {
document.getElementById('errorText').innerText = "failed to start service"
return
}
const j = await res.json()
if (j.status !== 'OK') {
document.getElementById('errorText').innerText = "failed to start service" + j.reason
return
}
location.href = 'connect.html'
}
document.getElementById("next-button").onclick = (e) => {
const backup = document.getElementById('backup').checked
const manual = document.getElementById('manual-backup').checked
if (!backup && !manual) {
document.getElementById('errorText').innerText = 'Please select an option'
return
}
if (backup && manual) {
document.getElementById('errorText').innerText = 'Please select only one option'
return
}
if (backup) {
localStorage.setItem('wizard/backup', 'backup')
} else {
localStorage.setItem('wizard/backup', 'manual')
}
sendConfig()
}
</script>
</body>
</html>

View file

@ -19,7 +19,7 @@
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="26px" alt="Lightning Pub logo" />
</header>
<main>
@ -48,7 +48,7 @@
<div style="text-align: center; color: #c434e0;" id="click-text">Click to reveal</div>
</div>
<div id="qrcode"></div>
<div style="color: #a3a3a3; font-size: 11px;">
<div style="color: #a3a3a3; font-size: 11px; word-break: break-all; overflow-wrap: anywhere; max-width: 220px; margin: 0 auto;">
<div id="connectString"></div>
</div>
</div>
@ -104,13 +104,20 @@
height: 157,
// correctLevel : QRCode.CorrectLevel.H
});
document.getElementById('connectString').innerHTML = connectString
document.getElementById('connectString').innerText = connectString
}
}
try {
fetchInfo()
} catch (e) { console.log({ e }) }
// Continue to status
const btn = document.createElement('button')
btn.className = 'push-button'
btn.style.marginTop = '20px'
btn.innerText = 'Continue'
btn.onclick = () => { location.href = 'status.html' }
document.querySelector('.setup-content').appendChild(btn)
</script>
</body>

View file

@ -13,9 +13,8 @@
display: block;
position: relative;
width: 253px;
height: 241px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
min-height: 241px;
height: auto;
margin-top: 10px;
padding: 10px;
transition: background-color 0.5s;
@ -26,6 +25,60 @@
transition: background-color 0.3s;
}
/* prevent overlap: hide connect string until revealed; click surface on box */
.qrcode-box #connectString { display: none; margin-top: 6px; }
.qrcode-box.revealed #connectString { display: block; }
.qrcode-box.revealed #connectString {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
pointer-events: auto !important;
position: relative; /* Ensure z-index is respected */
z-index: 10; /* Bring to the front */
}
.qrcode-box { cursor: pointer; }
.qrcode-box.revealed { cursor: default; }
/* QR viewport and veil overlay */
.qrcode-viewport {
position: relative;
width: 100%;
height: 175px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-top: 6px;
}
.qrcode-viewport #qrcode {
position: absolute;
width: 157px;
height: 157px;
}
/* Remove global QR blur after reveal */
.qrcode-box.revealed #qrcode { filter: none !important; }
.qrcode-viewport .qr-veil {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
background: rgba(0,0,0,0.25);
color: #c434e0;
font-size: 12px;
pointer-events: none; /* allow clicks to pass through if needed */
}
.qrcode-box.revealed .qr-veil { display: none; }
.qrcode-viewport.revealed .qr-veil { display: none; }
.qrcode-box.revealed .qrcode-viewport { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; pointer-events: none; }
/* hide helper text when revealed to avoid overlap */
.qrcode-box.revealed #click-text { display: none; }
.qrcode-box-clicked::before {
content: "";
position: absolute;

View file

@ -133,6 +133,10 @@ a {
display: none;
}
.checkbox input[type="radio"] {
display: none;
}
/* Create a new box */
.checkbox label {
padding-left: 32px;
@ -157,6 +161,31 @@ a {
z-index: -1;
}
.checkbox input[type="radio"] + label::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
border: 1px solid #a3a3a3;
border-radius: 50%;
background-color: transparent;
}
.checkbox input[type="radio"]:checked + label::after {
content: '';
position: absolute;
left: 5px;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #a012c7;
}
/* Display a checkmark when the checkbox is checked */
.checkbox input[type="checkbox"]:checked + .checkbox-shape::before {
content: "✓";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

@ -8,6 +8,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/liquidity.css" />
<link rel="stylesheet" href="css/backup.css" />
<link rel="stylesheet" href="css/connect.css" />
<link rel="stylesheet" href="css/status.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
@ -17,45 +21,310 @@
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="26px" alt="Lightning Pub logo" />
</header>
<main>
<section class="setup-header">
<h2>Setup your Pub</h2>
<p class="header-title">
</p>
<section id="page-node">
<div class="setup-header">
<h2>Setup your Pub</h2>
<p class="header-title">
</p>
</div>
<div class="line"></div>
<div class="setup-content">
<div class="input-group">
<span>Give this node a name that wallet users will see:</span>
<input type="text" placeholder="Nodey McNodeFace" value="" style="width: 100%" id="nodeName" />
</div>
<div class="input-group" style="margin-top: 20px">
<span>Avatar (shown in wallet):</span>
<div style="display:flex; align-items:center; gap:12px; width:100%;">
<img id="avatarPreview" src="" alt="avatar" style="width:56px;height:56px;border-radius:6px;object-fit:cover;box-shadow:0 0 2px rgba(0,0,0,1);" />
<input type="text" placeholder="" style="flex:1" id="avatarUrl" />
</div>
</div>
<div class="input-group" style="margin-top: 28px">
<span>If you want to use a specific Nostr relay, enter it now:</span>
<div style="display:flex; flex-direction: column; gap:10px; width:100%;">
<input type="text" placeholder="wss://relay.lightning.pub" style="width: 100%" id="relayUrl" />
<div class="checkbox" style="margin-top: 0">
<input type="checkbox" id="customCheckbox" />
<div class="checkbox-shape"></div>
<label for="customCheckbox">
Use the default managed relay service and auto-pay 1000 sats
per month to support developers
</label>
</div>
</div>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="liquidityBtn">
Next
</button>
</div>
</section>
<div class="line"></div>
<section class="setup-content">
<div class="input-group">
<span>Give this node a name that wallet users will see:</span>
<input type="text" placeholder="Nodey McNodeFace" value="" style="width: 100%" id="nodeName" />
<section id="page-liquidity" style="display: none;">
<div class="setup-header">
<button class="icon-button back-button" id="back-to-node">
<img src="img/back.svg" alt="" />
</button>
<h2>Manage Node Liquidity</h2>
<p class="header-title">
How do you want to manage Lightning channels?
</p>
</div>
<div class="input-group" style="margin-top: 38px">
<span>If you want to use a specific Nostr relay, enter it now:</span>
<input type="text" placeholder="wss://relay.lightning.pub" style="width: 100%" id="relayUrl" />
<div class="line"></div>
<div class="setup-content">
<div class="checkbox" style="margin-top: 60px">
<input type="radio" id="automate" name="service" data-group="service" />
<label for="automate" class="automate">
Use Automation Service
<div class="question-box">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
<div class="question-content" id="question-content">
Automation helps reduce the fees you pay by trusting peers temporarily until your node balance is sufficient
to open a balanced Lightning channel.
<button class="icon-button close-button" id="close-question">
<img src="img/close.svg" alt="" />
</button>
<a href="https://docs.shock.network/" target="_blank" class="marked question-more">Learn More</a>
</div>
</label>
</div>
<div class="checkbox" style="margin-top: 30px">
<input type="radio" id="manual" name="service" data-group="service" />
<label for="manual">Manage my channels manually</label>
</div>
<div>
<p id="errorTextLiquidity" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="backupBtn">
Next
</button>
</div>
</section>
<section id="page-backup" style="display: none;">
<div class="setup-header">
<button class="icon-button back-button" id="back-to-liquidity">
<img src="img/back.svg" alt="" />
</button>
<h2>Choose a Recovery Method</h2>
<p class="header-title">
<span style="font-weight: bold">New Node! 🎉</span> It's important
to backup your keys.
</p>
</div>
<div class="checkbox" style="margin-top: 12px">
<input type="checkbox" id="customCheckbox" />
<div class="checkbox-shape"></div>
<label for="customCheckbox">
Use the default managed relay service and auto-pay 1000 sats
per month to support developers
</label>
</div>
<div class="line"></div>
<div>
<div class="setup-content">
<div class="description-box">
<div class="description">
In addition to your seed phrase, you also need channel details to recover funds should your node experience a
hardware failure.
</div>
<br />
<div class="description">
It's important always to have the latest version of this file. Fortunately, it's small enough to automatically
store on the Nostr relay.
</div>
</div>
<div class="warning-text">
If you did not choose the developers relay, be sure your relay has
adequate storage policies to hold NIP78 events.
</div>
<div class="checkbox-container">
<div class="checkbox" style="margin-top: 12px">
<input type="radio" id="backup" name="backup-option" />
<label for="backup">
Encrypted Backup to Nostr Relay
</label>
</div>
</div>
<div class="checkbox-container">
<div class="checkbox manual-checkbox" style="margin-top: 12px">
<input type="radio" id="manual-backup" name="backup-option" />
<label for="manual-backup">
DO NOT store on relay (Manual Backups)
</label>
</div>
</div>
<div>
<p id="errorTextBackup" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px;" id="next-button">
Finish Setup
</button>
</div>
</section>
<section id="page-connect" style="display: none;">
<div class="setup-header">
<button class="icon-button back-button" id="back-to-backup">
<img src="img/back.svg" alt="" />
</button>
<h2>Connect</h2>
<p class="header-title">You can now manage your node remotely</p>
</div>
<div class="line"></div>
<div class="setup-content">
<div style="font-size: 13px; margin-top: 5px;">Scan the QR or Copy-Paste the string to establish the connection.</div>
<div style="display: flex; justify-content: center;">
<div class="qrcode-box" id="codebox" aria-label="Pairing QR and string">
<div style="font-size: 11px;">
<div style="text-align: center; color: #a3a3a3;">Code contains a one-time pairing secret</div>
<div style="text-align: center; color: #c434e0;" id="click-text">Click to reveal</div>
</div>
<div class="qrcode-viewport">
<div id="qrcode"></div>
<div class="qr-veil"><span>Click to reveal</span></div>
</div>
<div style="color: #a3a3a3; font-size: 11px; word-break: break-all; overflow-wrap: anywhere; max-width: 220px; margin: 0 auto;">
<div id="connectString"></div>
</div>
</div>
</div>
<button class="push-button" style="margin-top: 20px" id="to-status">OK, I've connected my wallet</button>
</div>
</section>
<section id="page-status" style="display: none;">
<div class="setup-header">
<button class="icon-button back-button" id="back-to-connect">
<img src="img/back.svg" alt="" />
</button>
<h2>Node Status</h2>
<p class="header-title"></p>
</div>
<div class="line" style="width: 100%;"></div>
<section class="node-status">
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="liquidityBtn">
Next
</button>
<div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Public Node Name:</div>
<div class="fc-grey editable-content">
<div class="show-nodey" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nodey" placeholder="" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nodey">Cancel</button>
<button class="small-btn" id="save-show-nodey">Save</button>
</div>
</div>
<div id="show-nodey-text">Nodey McNodeFace</div>
<div class="question-box">
<button class="icon-button" id="show-nodey">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Nostr Relay:</div>
<div class="fc-grey editable-content">
<div class="show-nostr" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-nostr" placeholder="wss://relay.lightning.pub" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-nostr">Cancel</button>
<button class="small-btn" id="save-show-nostr">Save</button>
</div>
</div>
<div id="show-nostr-text">wss://relay.lightning.pub</div>
<div class="question-box">
<button class="icon-button" id="show-nostr">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div>Administrator:</div>
<div id="adminNpub" style="line-break: anywhere;">Loading...</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Avatar:</div>
<div class="fc-grey editable-content">
<div class="show-avatar" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-avatar" placeholder="" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-avatar">Cancel</button>
<button class="small-btn" id="save-show-avatar">Save</button>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<img id="avatarImg" src="" alt="avatar" style="width:48px;height:48px;border-radius:6px;object-fit:cover;box-shadow:0 0 2px rgba(0,0,0,1);" />
<div class="question-box">
<button class="icon-button" id="show-avatar">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
</div>
</div>
<div style="display: flex; justify-content: end;padding-right: 12px;">
<div class="marked" id="show-reset" style="text-decoration: underline; margin-top: 5px;position: relative; display: none;">
Reset
<div class="watchdog-status">
<a href="https://docs.shock.network/pub/watchdog" target="_blank">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</a>
</div>
</div>
</div>
<div id="reset-box">
<div style="width: 100%;height: 100%;position: relative;">
<button class="icon-button close-button" id="close-reset-box">
<img src="img/close.svg" alt="">
</button>
<div class="reset-box-content" id="reset-content"></div>
<div class="continue-button-container">
<div class="continue-button" id="">Continue</div>
</div>
</div>
</div>
<div style="margin-top: 40px;">
<div class="status-element">
<div>Relay Status:</div>
<div id="relayStatus"><span class="yellow-dot">&#9679;</span> Loading...</div>
</div>
<div class="status-element">
<div>Lightning Status:</div>
<div id="lndStatus"><span class="yellow-dot">&#9679;</span> Loading...</div>
</div>
<div class="status-element">
<div style="position: relative;">Watchdog Status:
<div class="watchdog-status">
<a href="https://docs.shock.network/pub/watchdog" target="_blank">
<button class="icon-button" id="show-question"><img src="img/question.svg" /></button>
</a>
</div>
</div>
<div id="watchdog-status"><span class="green-dot">&#9679;</span> Loading...</div>
</div>
</div>
<div style="margin-top: 20px;">
<div style="font-size: 13px; text-align: left;">Guest Invitation Link:</div>
<a href="#" target="_blank" style="font-size: 11px;line-break: anywhere;" id="inviteLinkHttp" class="invite-link"></a>
</div>
</section>
</section>
</main>
@ -71,43 +340,10 @@
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script>
document.getElementById("liquidityBtn").onclick = (e) => {
const nodeName = document.getElementById("nodeName").value;
const relayUrl = document.getElementById("relayUrl").value;
const checked = document.getElementById("customCheckbox").checked;
if (!nodeName) {
document.getElementById("errorText").innerText = "Please enter a node name";
return;
}
if (!checked && !relayUrl) {
document.getElementById("errorText").innerText = "Please enter a relay URL or check the default relay box";
return;
}
localStorage.setItem("wizard/nodeName", nodeName);
if (checked) {
localStorage.setItem("wizard/relayUrl", "wss://relay.lightning.pub");
} else {
localStorage.setItem("wizard/relayUrl", relayUrl);
}
location.href = 'liquidity.html'
}
fetch("/wizard/state").then((res) => {
if (res.status === 200) {
res.json().then((data) => {
if (data.admin_linked) {
location.href = 'status.html'
} else if (data.config_sent) {
location.href = 'connect.html'
} else {
console.log("ready to initialize")
}
});
}
});
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
<script src="js/status.js"></script>
<script src="js/wizard.js"></script>
</body>
</html>

View file

@ -1,23 +0,0 @@
$(() => {
let backup; let manual_backup;
$("#backup").click(() => {
backup = $("#backup").prop("checked");
$('#manual-backup').prop("checked",false);
const nextButton = $("#next-button");
if (backup) {
nextButton.removeClass("hidden-button");
} else {
nextButton.addClass("hidden-button");
}
});
$("#manual-backup").click(()=>{
manual_backup = $('#manual-backup').prop("checked");
$("#backup").prop("checked",false);
const nextButton = $("#next-button");
if(manual_backup) {
nextButton.removeClass("hidden-button");
} else {
nextButton.addClass("hidden-button");
};
});
});

View file

@ -1,19 +0,0 @@
$(() => {
$("#show-question").click(() => {
$("#question-content").show();
});
$("#close-question").click(() => {
$("#question-content").hide();
});
$("#automate").click(() => {
$('[data-group="service"]').prop("checked", false);
$("#automate").prop("checked", true);
});
$("#manual").click(() => {
$('[data-group="service"]').prop("checked", false);
$("#manual").prop("checked", true);
});
});

View file

@ -1,4 +1,27 @@
$(() => {
const postConfig = async (updates) => {
try {
const stateRes = await fetch('/wizard/service_state')
if (stateRes.status !== 200) return false
const s = await stateRes.json()
const body = {
source_name: updates.source_name ?? (s.source_name || s.provider_name || ''),
relay_url: updates.relay_url ?? (s.relay_url || (s.relays && s.relays[0]) || ''),
automate_liquidity: s.automate_liquidity || false,
push_backups_to_nostr: s.push_backups_to_nostr || false,
avatar_url: s.avatar_url || ''
}
const res = await fetch('/wizard/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (res.status !== 200) return false
const j = await res.json().catch(() => ({}))
if (j && j.status && j.status !== 'OK') return false
return true
} catch { return false }
}
$("#show-reset").click(() => {
$("#reset-content").text('Reset the administrator account if you lost access via the Dashboard.');
$("#reset-box").show();
@ -11,6 +34,11 @@ $(() => {
$("#reset-box").show();
$('.continue-button').attr('id', 'set-show-nostr');
});
$("#show-avatar").click(() => {
$('.show-avatar').show()
$('#show-avatar-text').hide()
$('input[name="show-avatar"]').focus();
});
$("#show-nodey").click(() => {
$('.show-nodey').show()
$('#show-nodey-text').hide()
@ -18,9 +46,13 @@ $(() => {
});
$("#save-show-nodey").click(() => {
var targetInputVal = $('input[name="show-nodey"]').val()
$('#show-nodey-text').text(targetInputVal)
$('.show-nodey').hide()
$('#show-nodey-text').show()
postConfig({ source_name: targetInputVal }).then(ok => {
if (ok) {
$('#show-nodey-text').text(targetInputVal)
}
$('.show-nodey').hide()
$('#show-nodey-text').show()
})
})
$("#cancel-show-nodey").click(() => {
$('.show-nodey').hide()
@ -38,9 +70,28 @@ $(() => {
});
$("#save-show-nostr").click(() => {
var targetInputVal = $('input[name="show-nostr"]').val()
$('#show-nostr-text').text(targetInputVal)
$('.show-nostr').hide()
$('#show-nostr-text').show()
postConfig({ relay_url: targetInputVal }).then(ok => {
if (ok) {
$('#show-nostr-text').text(targetInputVal)
}
$('.show-nostr').hide()
$('#show-nostr-text').show()
})
})
$("#save-show-avatar").click(() => {
var targetInputVal = $('input[name="show-avatar"]').val()
postConfig({ avatar_url: targetInputVal }).then(ok => {
if (ok) {
$('#show-avatar-text').text(targetInputVal || '—')
if (targetInputVal) { $('#avatarImg').attr('src', targetInputVal) }
}
$('.show-avatar').hide()
$('#show-avatar-text').show()
})
})
$("#cancel-show-avatar").click(() => {
$('.show-avatar').hide()
$('#show-avatar-text').show()
})
$("#cancel-show-nostr").click(() => {
$('.show-nostr').hide()

241
static/js/wizard.js Normal file
View file

@ -0,0 +1,241 @@
$(() => {
// Page sections
const pages = {
node: $('#page-node'),
liquidity: $('#page-liquidity'),
backup: $('#page-backup'),
connect: $('#page-connect'),
status: $('#page-status')
};
// Inputs
const nodeNameInput = $("#nodeName");
const relayUrlInput = $("#relayUrl");
const avatarUrlInput = $("#avatarUrl");
const avatarPreview = $("#avatarPreview");
const customCheckbox = $("#customCheckbox");
const automateLiquidityRadio = $("#automate");
const manualLiquidityRadio = $("#manual");
const backupNostrRadio = $("#backup");
const manualBackupRadio = $("#manual-backup");
// Buttons
const toLiquidityBtn = $("#liquidityBtn");
const toBackupBtn = $("#backupBtn");
const toStatusBtn = $("#to-status");
const finishBtn = $("#next-button");
const backToNodeBtn = $("#back-to-node");
const backToLiquidityBtn = $("#back-to-liquidity");
// Error text
const errorTextNode = $("#errorText");
const errorTextLiquidity = $("#errorTextLiquidity");
const errorTextBackup = $("#errorTextBackup");
// Liquidity question mark
$("#show-question").click(() => $("#question-content").show());
$("#close-question").click(() => $("#question-content").hide());
const showPage = (pageToShow) => {
Object.values(pages).forEach(page => page.hide());
pageToShow.show();
};
const populateStatus = async () => {
try {
const res = await fetch('/wizard/service_state');
if (res.status !== 200) return;
const s = await res.json();
const name = s.source_name || s.provider_name || '';
const relay = s.relay_url || (s.relays && s.relays[0]) || '';
const admin = s.admin_npub || '';
const avatar = s.avatar_url || (s.app_id ? `https://robohash.org/${encodeURIComponent(s.app_id)}.png?size=128x128&set=set3` : '');
const lndState = s.lnd_state; // 0 OFFLINE, 1 SYNCING, 2 ONLINE (per enum)
const watchdogOk = !!s.watchdog_ok;
const relayConnected = !!s.relay_connected;
$('#show-nodey-text').text(name || '—');
$('#show-nostr-text').text(relay || '—');
$('#adminNpub').text(admin || '—');
if (avatar) { $('#avatarImg').attr('src', avatar); }
const mkDot = (ok) => ok ? '<span class="green-dot">&#9679;</span>' : '<span class="yellow-dot">&#9679;</span>';
const lndTxt = lndState === 2 ? 'Online' : (lndState === 1 ? 'Syncing' : 'Offline');
$('#lndStatus').html(`${mkDot(lndState === 2)} ${lndTxt}`);
$('#watchdog-status').html(`${mkDot(watchdogOk)} ${watchdogOk ? 'OK' : 'Alert'}`);
$('#relayStatus').html(`${mkDot(relayConnected)} ${relayConnected ? 'Connected' : 'Disconnected'}`);
} catch { /* noop */ }
};
// Navigation
toLiquidityBtn.click(() => {
const nodeName = nodeNameInput.val();
const relayUrl = relayUrlInput.val();
const useDefaultRelay = customCheckbox.prop('checked');
if (!nodeName) {
errorTextNode.text("Please enter a node name");
return;
}
if (!useDefaultRelay && !relayUrl) {
errorTextNode.text("Please enter a relay URL or check the default relay box");
return;
}
errorTextNode.text("");
showPage(pages.liquidity);
});
toBackupBtn.click(() => {
if (!automateLiquidityRadio.prop('checked') && !manualLiquidityRadio.prop('checked')) {
errorTextLiquidity.text('Please select an option');
return;
}
errorTextLiquidity.text("");
showPage(pages.backup);
});
backToNodeBtn.click(() => showPage(pages.node));
backToLiquidityBtn.click(() => showPage(pages.liquidity));
// Final submission
finishBtn.click(async () => {
if (!backupNostrRadio.prop('checked') && !manualBackupRadio.prop('checked')) {
errorTextBackup.text('Please select an option');
return;
}
errorTextBackup.text("");
const relayUrl = customCheckbox.prop('checked') ? 'wss://relay.lightning.pub' : relayUrlInput.val();
const req = {
source_name: nodeNameInput.val(),
relay_url: relayUrl,
automate_liquidity: automateLiquidityRadio.prop('checked'),
push_backups_to_nostr: backupNostrRadio.prop('checked'),
avatar_url: avatarUrlInput.val()
};
try {
const res = await fetch("/wizard/config", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req)
});
if (!res.ok) {
const j = await res.json();
throw new Error(j.reason || "Failed to start service");
}
// Move to in-page connect step
showPage(pages.connect);
// fetch and prepare connect info
(async () => {
const res = await fetch('/wizard/admin_connect_info');
if (res.status !== 200) return;
const j = await res.json();
if (j.connect_info && j.connect_info.enrolled_npub) {
showPage(pages.status);
await populateStatus();
return;
}
const connectString = j.nprofile + ':' + j.connect_info.admin_token;
const qrElement = document.getElementById('qrcode');
const codebox = $('#codebox');
const clickText = $('#click-text');
const cs = $('#connectString');
// Reset visual state
codebox.removeClass('revealed');
cs.text('');
codebox.find('.qr-veil').show();
clickText.show();
if (qrElement) {
while (qrElement.firstChild) qrElement.removeChild(qrElement.firstChild);
// Pre-generate QR behind veil to entice reveal
new QRCode(qrElement, { text: connectString, colorDark: '#000000', colorLight: '#ffffff', width: 157, height: 157 });
}
// Reveal on click: show string below and remove veil/heading
codebox.off('click').on('click', (e) => {
if (!codebox.hasClass('revealed')) {
e.preventDefault();
e.stopPropagation();
cs.text(connectString);
codebox.addClass('revealed');
// Remove the veil from the DOM entirely to kill the blur
codebox.find('.qr-veil').remove();
clickText.hide();
// Unbind to allow text selection and normal behavior after reveal
codebox.off('click');
// Force text to be selectable on top
cs.css({
'user-select': 'text',
'-webkit-user-select': 'text',
'pointer-events': 'auto'
});
}
});
})();
} catch (err) {
errorTextBackup.text(err.message);
}
});
// Navigate from connect to status
toStatusBtn && toStatusBtn.click(async () => {
showPage(pages.status);
await populateStatus();
})
const syncRelayState = () => {
relayUrlInput.prop('disabled', customCheckbox.prop('checked'));
if (customCheckbox.prop('checked')) {
relayUrlInput.val('');
}
};
customCheckbox.on('change', syncRelayState);
relayUrlInput.on('input', () => {
if (relayUrlInput.val()) {
customCheckbox.prop('checked', false);
syncRelayState();
}
});
// Initial state load (no redirects; SPA only)
console.log('Wizard script version: REVEAL_FIX_3 activated');
fetch("/wizard/service_state").then(res => res.json()).then(state => {
nodeNameInput.val(state.source_name);
if (state.relay_url === 'wss://relay.lightning.pub') {
customCheckbox.prop('checked', true);
} else {
relayUrlInput.val(state.relay_url);
}
const robo = state.app_id ? `https://robohash.org/${encodeURIComponent(state.app_id)}.png?size=128x128&set=set3` : ''
if (state.avatar_url) {
avatarUrlInput.val(state.avatar_url);
avatarPreview.attr('src', state.avatar_url)
} else if (robo) {
avatarPreview.attr('src', robo)
}
if (robo) {
avatarUrlInput.attr('placeholder', robo)
}
syncRelayState();
if (state.automate_liquidity) {
automateLiquidityRadio.prop('checked', true);
} else {
manualLiquidityRadio.prop('checked', true);
}
if (state.push_backups_to_nostr) {
backupNostrRadio.prop('checked', true);
} else {
manualBackupRadio.prop('checked', true);
}
});
});

View file

@ -1,109 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/liquidity.css" />
<!-- HTML Meta Tags -->
<title>Lightning.Pub</title>
<meta name="description" content="Lightning for Everyone" />
<link rel="icon" type="image/png" href="img/pub_logo.png" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
</header>
<main>
<section class="setup-header">
<button class="icon-button back-button" onclick="history.back()">
<img src="img/back.svg" alt="" />
</button>
<h2>Manage Node Liquidity</h2>
<p class="header-title">
How do you want to manage Lightning channels?
</p>
</section>
<div class="line"></div>
<section class="setup-content">
<div class="checkbox" style="margin-top: 60px">
<input type="checkbox" id="automate" data-group="service" />
<div class="checkbox-shape"></div>
<label for="automate" class="automate">
Use Automation Service
<div class="question-box">
<button class="icon-button" id="show-question">
<img src="img/question.svg" />
</button>
</div>
<div class="question-content" id="question-content">
Automation helps reduce the fees you pay by trusting peers temporarily until your node balance is sufficient
to open a balanced Lightning channel.
<button class="icon-button close-button" id="close-question">
<img src="img/close.svg" alt="" />
</button>
<a href="https://docs.shock.network/" target="_blank" class="marked question-more">Learn More</a>
</div>
</label>
</div>
<div class="checkbox" style="margin-top: 30px">
<input type="checkbox" id="manual" data-group="service" />
<div class="checkbox-shape"></div>
<label for="manual">Manage my channels manually</label>
</div>
<div>
<p id="errorText" style="color:red"></p>
</div>
<button class="push-button" style="margin-top: 60px" id="backupBtn">
Next
</button>
</section>
</main>
<footer>
<div class="footer-text">
<div>By proceeding you acknowledge that this is</div>
<div>bleeding-edge software, and agree to the providers</div>
<div>
<span style="color: #c434e0">terms</span> regarding any services
herein.
</div>
</div>
<div class="line"></div>
<a href="https://docs.shock.network" class="marked need-help">Need Help?</a>
</footer>
<script src="js/liquidity.js"></script>
<script>
document.getElementById("backupBtn").onclick = (e) => {
const automate = document.getElementById('automate').checked
const manual = document.getElementById('manual').checked
if (!automate && !manual) {
document.getElementById('errorText').innerText = 'Please select an option'
return
}
if (automate && manual) {
document.getElementById('errorText').innerText = 'Please select only one option'
return
}
if (automate) {
localStorage.setItem('wizard/liquidity', 'automate')
} else {
localStorage.setItem('wizard/liquidity', 'manual')
}
location.href = 'backup.html'
}
</script>
</body>
</html>

View file

@ -19,7 +19,7 @@
<body>
<header>
<img src="img/pub_logo.png" width="38px" height="auto" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="33px" alt="Lightning Pub logo" />
<img src="img/LightningPub.png" height="26px" alt="Lightning Pub logo" />
</header>
<main>
@ -69,6 +69,26 @@
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div style="text-align: left;">Avatar:</div>
<div class="fc-grey editable-content">
<div class="show-avatar" style="display: flex; flex-direction: column; display: none;">
<input type="text" value="" name="show-avatar" placeholder="" />
<div style="display: flex;justify-content: end;">
<button class="small-btn" id="cancel-show-avatar">Cancel</button>
<button class="small-btn" id="save-show-avatar">Save</button>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<img id="avatarImg" src="" alt="avatar" style="width:48px;height:48px;border-radius:6px;object-fit:cover;box-shadow:0 0 2px rgba(0,0,0,1);" />
<div class="question-box">
<button class="icon-button" id="show-avatar">
<img src="img/pencil.svg" style="cursor: pointer;" />
</button>
</div>
</div>
</div>
</div>
<div class="status-element" style="margin-top: 15px;">
<div>Administrator:</div>
<div id="adminNpub" style="line-break: anywhere;">
@ -157,20 +177,29 @@
}
const j = await res.json()
console.log(j)
if (j.status !== 'OK') {
document.getElementById('errorText').innerText = "failed to get state info" + j.reason
return
}
document.getElementById("show-nodey-text").innerHTML = j.provider_name
document.getElementById("show-nostr-text").innerHTML = j.relays[0]
document.getElementById("adminNpub").innerText = j.admin_npub
document.getElementById("relayStatus").innerHTML = `<span class="${j.relay_connected ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.relay_connected ? 'Connected' : 'Disconnected'}`
document.getElementById("lndStatus").innerHTML = `<span class="${j.lnd_state === 'ONLINE' ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.lnd_state}`
// Map fields from service state
const name = j.source_name || j.provider_name || 'Unknown'
const relayUrl = j.relay_url || (j.relays && j.relays[0]) || ''
const admin = j.admin_npub || ''
// lnd_state may arrive as enum number or string; normalize to text
const lndText = (typeof j.lnd_state === 'string')
? j.lnd_state
: (j.lnd_state === 2 ? 'ONLINE' : (j.lnd_state === 1 ? 'SYNCING' : 'OFFLINE'))
const lndDot = (lndText === 'ONLINE') ? 'green-dot' : (lndText === 'SYNCING' ? 'yellow-dot' : 'red-dot')
// use actual relay_connected flag from backend
const relayConnected = !!j.relay_connected
const avatar = j.avatar_url || (j.app_id ? `https://robohash.org/${encodeURIComponent(j.app_id)}.png?size=128x128&set=set3` : '')
document.getElementById("show-nodey-text").innerHTML = name
document.getElementById("show-nostr-text").innerHTML = relayUrl || '—'
if (avatar) { document.getElementById("avatarImg").src = avatar }
document.getElementById("adminNpub").innerText = admin
document.getElementById("relayStatus").innerHTML = `<span class="${relayConnected ? 'green-dot' : 'red-dot'}">&#9679;</span> ${relayConnected ? 'Connected' : 'Disconnected'}`
document.getElementById("lndStatus").innerHTML = `<span class="${lndDot}">&#9679;</span> ${lndText}`
document.getElementById("watchdog-status").innerHTML = `<span class="${j.watchdog_ok ? 'green-dot' : 'red-dot'}">&#9679;</span> ${j.watchdog_ok ? 'No Alerts' : 'ALERT!!'}`
document.getElementById("inviteLinkHttp").href = `https://my.shockwallet.app/#/sources?addSource=${j.nprofile}`
document.getElementById("inviteLinkHttp").innerHTML = `https://my.shockwallet.app/#/sources?addSource=${j.nprofile}`
document.querySelector('input[name="show-nodey"]').placeholder = j.provider_name;
document.title = j.provider_name;
document.querySelector('input[name="show-nodey"]').placeholder = name;
document.title = name;
}
try {
fetchInfo()