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:
parent
53d19b0e5e
commit
d4e15fe932
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
|
@ -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()
|
||||||
|
}
|
Loading…
Reference in New Issue