ICON Workshop — ICONex Web Connect

In the past tutorials we have been invoking SCORE calls and making ICX transfers through hardcoded wallet files, this obviously isn’t ideal nor practical for DAPP users. Today I am going to show you how to connect to ICONex Chrome Extension from your web app, thereby enabling users to interact with your DAPP via their own wallets.

In this tutorial, since everything is frontend development, we’ll be working under one single html file, using the new ICON SDK for Javascript to invoke the core ICON Service methods.

Demo: https://dapps.icon.support/iconex-webconnect/

Create a new environment for our project,

# Create a new environment
$ mkdir iconex-webconnect && cd iconex-webconnect
$ touch index.html

ICON SDK for Javascript can be installed via npm or you can simply include the js file via CDN host.

<script src="https://cdn.jsdelivr.net/gh/icon-project/icon-sdk-js/build/icon-sdk-js.min.js"></script>

For this exercise we’ll use bootstrap 4 to stylize the page a little.

Create the bare-bone index.html

# index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ICONex Web Connect Demo</title>
<!-- bootstrap 4 style -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>

<!-- ICON SDK for Javascript -->
<script src="http://cdn.jsdelivr.net/gh/icon-project/icon-sdk-js@latest/build/icon-sdk-js.min.js"></script>
<!-- bootstrap 4 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>

</body>
</html>

CustomEvent

We need to use CustomEvent for dispatching and listening event. The type and payload of events are assigned to the detail field. type is a string of pre-defined type of events, and payload can be any type, that contains the body of your request or response. We’ll explain more by actual implementation in a bit.

We’ll start off by exploring the available types of events, they are

HAS_ACCOUNT:REQUEST_HAS_ACCOUNT, RESPONSE_HAS_ACCOUNT

HAS_ADDRESS:REQUEST_HAS_ADDRESS, RESPONSE_HAS_ADDRESS

ADDRESS:REQUEST_ADDRESS, RESPONSE_ADDRESS

JSON-RPC:REQUEST_JSON-RPC, RESPONSE_JSON-RPC

SIGNING:REQUEST_SIGNING, RESPONSE_SIGNING

HAS_ACCOUNT

This event will allow us to determine whether the client’s ICONex has any wallets. We’ll demonstrate here how to create a CustomEvent and dispatch that event with our request’s type, how to access type and payload data for the event handler to work with the data.

Access the page via a local webserver, clicking the button will invoke the event and return true if you have wallets in ICONex.

HAS_ADDRESS

Following a very similar implementation to HAS_ACCOUNT, we’ll register the onclick event to the button and dispatch a custom event to request for REQUEST_HAS_ADDRESS and add this to one of our event cases.

<!-- HAS_ADDRESS -->
<div class="my-3 p-3 bg-white rounded shadow-sm">
<h5 class="pb-2 mb-3 ml-3">HAS_ADDRESS</h5>
<div class="container">
<label for="formGroupExampleInput">ICX Address: </label>
<input id="request-has-address-data" type="text" class="form-control" placeholder="hx0000000000000000000000000000000000000000"><br>
<button id="request-has-address" type="button" class="btn btn-outline-primary">Is this address in my ICONex?</button>
</div>
<div id="response-has-address" class="mt-3 ml-3">> Result : </div>
</div>
...
var requestHasAddress = document.getElementById("request-has-address");
var requestHasAddressData = document.getElementById("request-has-address-data");
var responseHasAddress = document.getElementById("response-has-address");
...
case "RESPONSE_HAS_ADDRESS":
responseHasAddress.innerHTML = "> Result : " + payload.hasAddress + " (" + typeof payload.hasAddress + ")";
break;
default:
...
requestHasAddress.onclick = function () {
window.dispatchEvent(new CustomEvent('ICONEX_RELAY_REQUEST', {
detail: {
type: 'REQUEST_HAS_ADDRESS',
payload: requestHasAddressData.value || requestHasAddressData.placeholder
}
}))
};

ADDRESS

We’ll allow the user to select an active wallet they wish to use, in order to make requests, this is done via REQUEST_ADDRESS. The server will respond with RESPONSE_ADDRESS.

<!-- ADDRESS -->
<div class="my-3 p-3 bg-white rounded shadow-sm">
<h5 class="pb-2 mb-3 ml-3">ADDRESS</h5>
<div class="container">
<button id="request-address" type="button" class="btn btn-outline-primary">Which wallet to use for this session?</button>
</div>
<div id="response-address" class="mt-3 ml-3">> Result : </div>
</div>
...
var requestAddress = document.getElementById("request-address");
var responseAddress = document.getElementById("response-address");
...
case "RESPONSE_ADDRESS":
fromAddress = payload;
responseAddress.innerHTML = "> Selected ICX Address : " + payload;
break;
default:
...
requestAddress.onclick = function () {
window.dispatchEvent(new CustomEvent('ICONEX_RELAY_REQUEST', {
detail: {
type: 'REQUEST_ADDRESS'
}
}))
};

JSON-RPC

This allows us to make requests by calling ICON JSON-RPC API, note you’re required to select an ICX wallet from previous step, as we’ll be calling from the wallet you chose. We’ll demonstrate Custom, which you can use an arbitrary JSON method of your choice. ReadOnly, which calls a read-only method of a SCORE that does not cost anything. SendTransaction that calls a SCORE function (not read-only) and ICX Transfer which are transactions that’ll cost ICX so you’re required to confirm with wallet password.

var iconService = window['icon-sdk-js'];
var IconAmount = iconService.IconAmount;
var IconConverter = iconService.IconConverter;
var IconBuilder = iconService.IconBuilder;
var requestScore = document.getElementById("request-score");
var requestScoreForm = document.getElementById("request-score-form");
var responseScore = document.getElementById("response-score");
var jsonRpc0 = document.getElementById("json-rpc-0");
var jsonRpc1 = document.getElementById("json-rpc-1");
var jsonRpc2 = document.getElementById("json-rpc-2");
var jsonRpc3 = document.getElementById("json-rpc-3");
var scoreData = document.getElementById("score-data");
...
case "RESPONSE_ADDRESS":
fromAddress = payload;
responseAddress.innerHTML = "> Selected ICX Address : " + payload;
jsonRpc0.disabled = false;
jsonRpc1.disabled = false;
jsonRpc2.disabled = false;
jsonRpc3.disabled = false;
break;
case "RESPONSE_JSON-RPC":
responseScore.value = JSON.stringify(payload);
break;
case "CANCEL_JSON-RPC":
responseScore.value = null;
break;
default:
...
function setRequestScoreForm() {
var data = new FormData(requestScoreForm);
var type = '';
for (const entry of data) {
type = entry[1]
}
switch (type) {
case 'read-only':
var callBuilder = new IconBuilder.CallBuilder;
var readOnlyData = callBuilder
.from(fromAddress)
.to('cx43f59485bd34d0c7e9312835d65cb399f6d29651')
.method("hello")
.build();
scoreData.value = JSON.stringify({
"jsonrpc": "2.0",
"method": "icx_call",
"params": readOnlyData,
"id": 50889
});
break;

case 'send-transaction':
var callTransactionBuilder = new IconBuilder.CallTransactionBuilder;
var callTransactionData = callTransactionBuilder
.from(fromAddress)
.to("cxb20b5ff06ba50aef42c7832958af59f9ae0651e7")
.nid(IconConverter.toBigNumber(3))
.timestamp((new Date()).getTime() * 1000)
.stepLimit(IconConverter.toBigNumber(1000000))
.version(IconConverter.toBigNumber(3))
.method('createToken')
.params({
"price": IconConverter.toHex(10000),
"tokenType": IconConverter.toHex(2)
})
.build();
scoreData.value = JSON.stringify({
"jsonrpc": "2.0",
"method": "icx_sendTransaction",
"params": IconConverter.toRawTransaction(callTransactionData),
"id": 50889
});
break;

case 'icx-transfer':
var icxTransactionBuilder = new IconBuilder.IcxTransactionBuilder;
var icxTransferData = icxTransactionBuilder
.from(fromAddress)
.to("hx04d669879227bb24fc32312c408b0d5503362ef0")
.nid(IconConverter.toBigNumber(3))
.value(IconAmount.of(1, IconAmount.Unit.ICX).toLoop())
.timestamp((new Date()).getTime() * 1000)
.version(IconConverter.toBigNumber(3))
.stepLimit(IconConverter.toBigNumber(100000))
.build();
scoreData.value = JSON.stringify({
"jsonrpc": "2.0",
"method": "icx_sendTransaction",
"params": IconConverter.toRawTransaction(icxTransferData),
"id": 50889
});
break;
default:
}
}
...
requestScore.onclick = function () {
responseScore.value = null;

if (!scoreData.value) {
alert('Check the param data');
return
}

var parsed = JSON.parse(scoreData.value);
if (parsed.method === "icx_sendTransaction" && !fromAddress) {
alert('Select the ICX Address');
return
}

window.dispatchEvent(new CustomEvent('ICONEX_RELAY_REQUEST', {
detail: {
type: 'REQUEST_JSON-RPC',
payload: parsed
}
}))
};

You can experiment with one of the JSON-RPC calls, we’ll select ICX transfer.

Make the call and you should be prompted an ICONex window, enter your password

Make sure you’re selecting testnet YEOUIDO for the transfer, so you don’t actually spend any real ICX

The amount is set in the JSON-RPC call, IconAmount.of(1, IconAmount.Unit.ICX).toLoop() which is “0xde0b6b3a7640000” in hex.

You can check the transaction on the live tracker.

SIGNING

REQUEST_SIGNING sends a request to sign tx hash, so user confirmation is required, RESPONSE_SIGNING will return the signature.

Make sure a wallet is selected first before you sign, then request signing

You’ll be prompted to enter your password to confirm, once that’s done and submitted, you’ll get the signature back.

The entire code,

Demo: https://dapps.icon.support/iconex-webconnect/

That’s it for this ICONex web connect tutorial. This takes our DAPPs one step closer to being more practical and user friendly. We could apply this to tutorial series part 3 — ICON Dice Roll DAPP, so the users will not have to leave the site to their ICONex to make the bets, a much friendlier experience. We could also apply this to the workshop ICON Voting, where instead of hardcoding 3 wallets for 3 votes, we could have unlimited number of votes, as long there’s more unique wallet addresses.


Follow me on Twitter for most up-to-date ICON related content: https://www.twitter.com/2infiniti