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