chore(wallet) WalletConnect quick prototype environment for integration

Add GO helper to:
- loads WalletConnect SDK bundle
- bootstraps status-go user session
- provides a way to call status-go API from webview
- forwards status-go signals to webview

Updates: #12551
This commit is contained in:
Stefan 2023-10-26 23:16:19 +03:00 committed by Stefan Dunca
parent 53d19b0e5e
commit d4e15fe932
5 changed files with 3672 additions and 0 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
statusgo "github.com/status-im/status-go/mobile"
"github.com/status-im/status-go/multiaccounts"
)
func loginToAccount(hashedPassword, userFolder, nodeConfigJson string) error {
absUserFolder, err := filepath.Abs(userFolder)
if err != nil {
return err
}
accountsJson := statusgo.OpenAccounts(absUserFolder)
accounts := make([]multiaccounts.Account, 0)
err = getApiResponse(accountsJson, &accounts)
if err != nil {
return err
}
if len(accounts) == 0 {
return fmt.Errorf("no accounts found")
}
account := accounts[0]
keystorePath := filepath.Join(filepath.Join(absUserFolder, "keystore/"), account.KeyUID)
initKeystoreJson := statusgo.InitKeystore(keystorePath)
apiResponse := statusgo.APIResponse{}
err = getApiResponse(initKeystoreJson, &apiResponse)
if err != nil {
return err
}
//serialize account of type multiaccounts.Account
accountJson, err := json.Marshal(account)
if err != nil {
return err
}
loginJson := statusgo.LoginWithConfig(string(accountJson), hashedPassword, nodeConfigJson)
err = getApiResponse(loginJson, &apiResponse)
if err != nil {
return err
}
return nil
}
type jsonrpcMessage struct {
Version string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
}
type jsonrpcRequest struct {
jsonrpcMessage
ChainID uint64 `json:"chainId"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
func callPrivateMethod(method string, params interface{}) string {
var paramsJson json.RawMessage
var err error
if params != nil {
paramsJson, err = json.Marshal(params)
if err != nil {
return ""
}
}
msg := jsonrpcRequest{
jsonrpcMessage: jsonrpcMessage{
Version: "2.0",
},
Method: method,
Params: paramsJson,
}
msgJson, err := json.Marshal(msg)
if err != nil {
return ""
}
return statusgo.CallPrivateRPC(string(msgJson))
}
type Config struct {
HashedPassword string `json:"hashedPassword"`
NodeConfigFile string `json:"nodeConfigFile"`
}
func processConfigArgs() (config *Config, nodeConfigJson string, userFolder string, err error) {
var configFilePath string
flag.StringVar(&configFilePath, "config", "", "path to json config file")
flag.StringVar(&userFolder, "dataDir", "../../../Status/data", "path to json config file")
flag.Parse()
if configFilePath == "" {
flag.Usage()
return
}
config = &Config{}
// parse config file
configFile, err := os.Open(configFilePath)
if err != nil {
panic(err)
}
defer configFile.Close()
jsonParser := json.NewDecoder(configFile)
if err = jsonParser.Decode(&config); err != nil {
panic(err)
}
// Read config.NodeConfigFile json file and store it as string
nodeConfigFile, err := os.Open(config.NodeConfigFile)
if err != nil {
panic(err)
}
defer nodeConfigFile.Close()
nodeConfigData, err := io.ReadAll(nodeConfigFile)
if err == nil {
nodeConfigJson = string(nodeConfigData)
}
return
}
func getApiResponse[T any](responseJson string, res T) error {
apiResponse := statusgo.APIResponse{}
err := json.Unmarshal([]byte(responseJson), &apiResponse)
if err == nil {
if apiResponse.Error != "" {
return fmt.Errorf("API error: %s", apiResponse.Error)
}
}
typeOfT := reflect.TypeOf(res)
kindOfT := typeOfT.Kind()
// Check for valid types: pointer, slice, map
if kindOfT != reflect.Ptr && kindOfT != reflect.Slice && kindOfT != reflect.Map {
return fmt.Errorf("type T must be a pointer, slice, or map")
}
if err := json.Unmarshal([]byte(responseJson), &res); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
return nil
}

View File

@ -0,0 +1,207 @@
<!DOCTYPE html>
<html>
<head>
<title>Wallet Connect status-go test</title>
</head>
<body>
<input
type="text"
id="pairLinkInput"
placeholder="Insert pair link"
disabled
/>
<div id="buttonRow">
<button id="pairButton" disabled>Pair</button>
<button id="authButton" disabled>Auth</button>
<button id="acceptButton" style="display: none">Accept</button>
<button id="rejectButton" style="display: none">Reject</button>
</div>
<div id="statusRow">
skd: <span id="statusText">-</span> ; status-go
<span id="statusGoStatusText">-</span>
</div>
<textarea id="echoTextArea" rows="10" cols="80"></textarea>
<script
src="bundle.js"
type="module"
onload="sdkLoaded()"
onerror="sdkFailLoading()"
></script>
<script>
function statusGoReady() {}
</script>
<script>
function sdkLoaded() {
if (window.wc === undefined) {
goEcho(`FAILED missing "window.wc" SDK`);
setStatus(`FAILED missing "window.wc" SDK`);
} else {
window.getConfiguration().then(
(conf) => {
window.wc.init(conf.projectId).then(
(wc) => {
pairLinkInput.disabled = false;
setStatus("initialized");
},
(err) => {
setStatus(`SDK error: ${JSON.stringify(err)}`, "red");
}
);
},
(err) => {
goEcho(`SDK getConfiguration error: ${JSON.stringify(err)}`);
}
);
}
const pairLinkInput = document.getElementById("pairLinkInput");
const pairButton = document.getElementById("pairButton");
const authButton = document.getElementById("authButton");
const acceptButton = document.getElementById("acceptButton");
const rejectButton = document.getElementById("rejectButton");
pairLinkInput.addEventListener("input", function () {
pairButton.disabled = !(pairLinkInput.value.length > 0);
/*authButton.disabled = !(
pairLinkInput.value.length > 0
);*/
});
pairButton.addEventListener("click", function () {
setStatus("Pairing...");
try {
wc.pair(pairLinkInput.value)
.then((sessionProposal) => {
setStatus(`Wait user pair`);
setDetails(
`Pair ID: ${sessionProposal.id} ; Topic: ${sessionProposal.params.pairingTopic}`
);
acceptButton.addEventListener("click", function () {
window.wc.approveSession(sessionProposal).then(
() => {
goEcho(`Session ${sessionProposal.id} approved`);
setStatus(
`Session ${sessionProposal.id} approved`
);
acceptButton.style.display = "none";
rejectButton.style.display = "none";
},
(err) => {
goEcho(
`Session ${sessionProposal.id} approve error: ${err.message}`
);
setStatus(
`Session ${sessionProposal.id} approve error: ${err.message}`
);
}
);
});
rejectButton.addEventListener("click", function () {
try {
window.wc.rejectSession(sessionProposal.id).then(
() => {
goEcho(`Session ${sessionProposal.id} rejected`);
setStatus(
`Session ${sessionProposal.id} rejected`
);
acceptButton.style.display = "none";
rejectButton.style.display = "none";
},
(err) => {
goEcho(
`Session ${sessionProposal.id} reject error`
);
setStatus(
`Session ${sessionProposal.id} reject error`
);
}
);
} catch (err) {
goEcho(
`Session ${sessionProposal.id} reject error: ${err.message}`
);
setStatus(
`Session ${sessionProposal.id} reject error: ${err.message}`
);
}
});
acceptButton.style.display = "inline";
rejectButton.style.display = "inline";
})
.catch((error) => {
goEcho(`Pairing error ${JSON.stringify(error)}`);
setStatus(`Pairing error: ${error.message}`, "red");
});
} catch (err) {
goEcho(`Pairing error ${JSON.stringify(err)}`);
setStatus(`Pairing error: ${err.message}`, "red");
}
});
authButton.addEventListener("click", function () {
setStatus("Authenticating...");
window.auth();
});
window.wc.registerForSessionRequest(event => {
setStatus(`Session topic ${event.topic}`);
})
}
function goEcho(message) {
window.echo(message);
}
function setStatusForElement(element, message, color) {
const statusText = document.getElementById(element);
statusText.textContent = message;
if (color === undefined) color = "green";
statusText.style.color = color;
}
function setStatus(message, color) {
setStatusForElement("statusText", message, color);
}
function setGoStatus(message, color) {
setStatusForElement("statusGoStatusText", message, color);
}
function setDetails(message) {
const echoTextArea = document.getElementById("echoTextArea");
echoTextArea.value = message;
}
function sdkFailLoading() {
setStatus("FAILED loading SDK", "red");
}
async function processGoEvents() {
while (true) {
try {
const event = await window.popNextEvent();
switch (event.name) {
case "nodeReady":
setGoStatus("ready");
statusGoReady();
break;
case "tokensAvailable":
setDetails(`${JSON.stringify(event.payload)}\n`);
break;
default:
await new Promise((resolve) => setTimeout(resolve, 100));
break;
}
} catch (err) {
goEcho(`GO event error: ${err.message}`);
}
}
}
processGoEvents();
</script>
</body>
</html>

View File

@ -0,0 +1,102 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
webview "github.com/webview/webview_go"
statusgo "github.com/status-im/status-go/mobile"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/signal"
)
type PairResult struct {
SessionProposal string `json:"sessionProposal"`
}
type Configuration struct {
ProjectId string `json:"projectId"`
}
type GoEvent struct {
Name string `json:"name"`
Payload string `json:"payload"`
}
var eventQueue chan GoEvent = make(chan GoEvent, 10000)
func signalHandler(jsonEvent string) {
// parse signal.Envelope from jsonEvent
envelope := signal.Envelope{}
err := json.Unmarshal([]byte(jsonEvent), &envelope)
if err != nil {
// check for error in json
apiResponse := statusgo.APIResponse{}
err = json.Unmarshal([]byte(jsonEvent), &apiResponse)
if err != nil {
fmt.Println("@dd Error parsing the event: ", err)
return
}
}
if envelope.Type == signal.EventNodeReady {
eventQueue <- GoEvent{Name: "nodeReady", Payload: ""}
}
}
func main() {
signal.SetDefaultNodeNotificationHandler(signalHandler)
config, nodeConfigJson, userFolder, err := processConfigArgs()
if err != nil {
panic(err)
}
// Login to first account
err = loginToAccount(config.HashedPassword, userFolder, nodeConfigJson)
if err != nil {
panic(err)
}
// Start WebView
w := webview.New(true)
defer w.Destroy()
w.SetTitle("WC status-go test")
w.SetSize(480, 320, webview.HintNone)
w.Bind("getConfiguration", func() Configuration {
projectID := os.Getenv("WALLET_CONNECT_PROJECT_ID")
return Configuration{ProjectId: projectID}
})
w.Bind("echo", func(message string) bool {
fmt.Println("@dd WebView:", message)
return true
})
// Setup go to webview event queue
w.Bind("popNextEvent", func() GoEvent {
select {
case event := <-eventQueue:
return event
default:
return GoEvent{Name: "", Payload: ""}
}
})
// Start a local server to serve the files
http.HandleFunc("/bundle.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "../../../ui/app/AppLayouts/Wallet/views/walletconnect/sdk/generated/bundle.js")
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
})
go http.ListenAndServe(":8080", nil)
w.Navigate("http://localhost:8080")
w.Run()
}