Initialization
Installation
You can use npm or yarn to install the Liquality SDK
- npm
- Yarn
npm install @liquality/wallet-sdk
yarn add @liquality/wallet-sdk
Setup API keys
Liquality SDK relies on API keys from different providers we use to fetch blockchain data.
Before you can utilize all of the different functions Liquality SDK has to offer, you should export the sdk.setup()
function which you can easily use in all of your files and components.
Example of a file where you export the setup function
import { setup } from "@liquality/wallet-sdk";
export function setupSDK() {
setup({
alchemyApiKey: "YOUR_API_KEY_HERE",
etherscanApiKey: "YOUR_API_KEY_HERE",
infuraProjectId: "-",
pocketNetworkApplicationID: "-",
quorum: 1,
slowGasPriceMultiplier: 1,
averageGasPriceMultiplier: 1.5,
fastGasPriceMultiplier: 2,
gasLimitMargin: 2000,
gelatoApiKey: "", //needed for gasless transactions
});
}
Example of how to use it in a React component
import { setupSDK } from "../setupSDK";
import { NftService } from "@liquality/wallet-sdk";
import React, { useState } from "react";
export default function Collectibles() {
setupSDK();
const [nfts, setNfts] = useState([]);
const fetchNfts = async (address: string, chainId: string) => {
const nfts: any = await NftService.getNfts(address, +chainId);
console.log(JSON.stringify(nfts));
setNfts(nfts);
}...
The setup
function is only needed for some of the SDK functions.
Service Worker
- The seamless login experience with social single-sign-on is dependent on the service worker which need to be registered in the project.
A service worker is a script that runs in the browser and operates separately from the DOM. It comes with several built-in network features. The Liquality SDK requires a service worker specific to the baseUrl to capture authentication redirects for the social single sign on.
For instance, if the baseUrl is set to http://localhost:3000/serviceworker
, then the user
will be redirected to the http://localhost:3000/serviceworker/redirect
page after logging in,
where the service worker will capture the results and send them back to the original window where
the login was initiated.
The service worker setup is only needed for the create and login wallet functions of the SDK.
Service Worker Setup
To set up a service worker with React, you need to create a sw.js file in the public folder and register it in the index.html file. There is more information on how to do this in these outside resources.
- React see this guide
- Angular see this guide
- Vue see this guide
The serviceworker code:
/* eslint-disable */
function getScope() {
return self.registration.scope;
}
self.addEventListener("message", function (event) {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener("fetch", function (event) {
try {
const url = new URL(event.request.url);
if (url.pathname.includes("redirect") && url.href.includes(getScope())) {
event.respondWith(
new Response(
new Blob(
[
`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Redirect</title>
<style>
* {
box-sizing: border-box;
}
html,
body {
background: #fcfcfc;
height: 100%;
padding: 0;
margin: 0;
}
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
h1.title {
font-size: 14px;
color: #0f1222;
font-family: "Roboto", sans-serif !important;
margin: 0;
text-align: center;
}
.spinner .beat {
background-color: #0364ff;
height: 12px;
width: 12px;
margin: 24px 2px 10px;
border-radius: 100%;
-webkit-animation: beatStretchDelay 0.7s infinite linear;
animation: beatStretchDelay 0.7s infinite linear;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
display: inline-block;
}
.spinner .beat-odd {
animation-delay: 0s;
}
.spinner .beat-even {
animation-delay: 0.35s;
}
@-webkit-keyframes beatStretchDelay {
50% {
-webkit-transform: scale(0.75);
transform: scale(0.75);
-webkit-opacity: 0.2;
opacity: 0.2;
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-opacity: 1;
opacity: 1;
}
}
@keyframes beatStretchDelay {
50% {
-webkit-transform: scale(0.75);
transform: scale(0.75);
-webkit-opacity: 0.2;
opacity: 0.2;
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
-webkit-opacity: 1;
opacity: 1;
}
}
@media (min-width: 768px) {
h1.title {
font-size: 14px;
}
p.info {
font-size: 28px;
}
.spinner .beat {
height: 12px;
width: 12px;
}
}
</style>
</head>
<body>
<div id="message" class="container">
<div class="spinner content" id="spinner">
<div class="beat beat-odd"></div>
<div class="beat beat-even"></div>
<div class="beat beat-odd"></div>
</div>
<h1 class="title content" id="closeText" style="display: none;">You can close this window now</h1>
</div>
<script
src="https://scripts.toruswallet.io/broadcastChannel_5_0_2.js"
integrity="Bu0bRAeSlh2jpBuUxKk5ivkdotaHH37cQ2XiV20EmFJmghb41D0f8xME/M1WZxFC"
></script>
<script>
function storageAvailable(type) {
var storage;
try {
storage = window[type];
var x = "__storage_test__";
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e &&
// everything except Firefox
(e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === "QuotaExceededError" ||
// Firefox
e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
}
function showCloseText() {
var closeText = document.getElementById("closeText");
var spinner = document.getElementById("spinner");
if (closeText) {
closeText.style.display = "block";
}
if (spinner) {
spinner.style.display = "none";
}
}
var isLocalStorageAvailable = storageAvailable("localStorage");
var isSessionStorageAvailable = storageAvailable("sessionStorage");
// set theme
let theme = "light";
if (isLocalStorageAvailable) {
var torusTheme = localStorage.getItem("torus-theme");
if (torusTheme) {
theme = torusTheme.split("-")[0];
}
}
if (theme === "dark") {
document.querySelector("body").style.backgroundColor = "#24252A";
}
var bc;
var broadcastChannelOptions = {
// type: 'localstorage', // (optional) enforce a type, oneOf['native', 'idb', 'localstorage', 'node'
webWorkerSupport: false, // (optional) set this to false if you know that your channel will never be used in a WebWorker (increase performance)
};
var instanceParams = {};
var preopenInstanceId = new URL(window.location.href).searchParams.get("preopenInstanceId");
if (!preopenInstanceId) {
document.getElementById("message").style.visibility = "visible";
// in general oauth redirect
try {
var url = new URL(location.href);
var hash = url.hash.substr(1);
var hashParams = {};
if (hash) {
hashParams = hash.split("&").reduce(function (result, item) {
var parts = item.split("=");
result[parts[0]] = parts[1];
return result;
}, {});
}
var queryParams = {};
for (var key of url.searchParams.keys()) {
queryParams[key] = url.searchParams.get(key);
}
var error = "";
try {
if (Object.keys(hashParams).length > 0 && hashParams.state) {
instanceParams = JSON.parse(window.atob(decodeURIComponent(decodeURIComponent(hashParams.state)))) || {};
if (hashParams.error) error = hashParams.error;
} else if (Object.keys(queryParams).length > 0 && queryParams.state) {
instanceParams = JSON.parse(window.atob(decodeURIComponent(decodeURIComponent(queryParams.state)))) || {};
if (queryParams.error) error = queryParams.error;
}
} catch (e) {
console.error(e);
}
if (instanceParams.redirectToOpener) {
// communicate to window.opener
window.opener.postMessage(
{
channel: "redirect_channel_" + instanceParams.instanceId,
data: {
instanceParams: instanceParams,
hashParams: hashParams,
queryParams: queryParams,
},
error: error,
},
"http://localhost:3000"
);
} else {
// communicate via broadcast channel
bc = new broadcastChannelLib.BroadcastChannel("redirect_channel_" + instanceParams.instanceId, broadcastChannelOptions);
bc.addEventListener("message", function (ev) {
if (ev.success) {
bc.close();
console.log("posted", {
queryParams,
instanceParams,
hashParams,
});
} else {
window.close();
showCloseText();
}
});
bc.postMessage({
data: {
instanceParams: instanceParams,
hashParams: hashParams,
queryParams: queryParams,
},
error: error,
}).then(function () {
setTimeout(function () {
window.location.href = url.origin + location.search + location.hash;
}, 5000);
});
}
} catch (err) {
console.error(err, "service worker error in redirect");
bc && bc.close();
window.close();
showCloseText();
}
} else {
// in preopen, awaiting redirect
try {
bc = new broadcastChannelLib.BroadcastChannel("preopen_channel_" + preopenInstanceId, broadcastChannelOptions);
bc.onmessage = function (ev) {
var { preopenInstanceId: oldId, payload, message } = ev.data;
if (oldId === preopenInstanceId && payload && payload.url) {
window.location.href = payload.url;
} else if (oldId === preopenInstanceId && message === "setup_complete") {
bc.postMessage({
data: {
preopenInstanceId: preopenInstanceId,
message: "popup_loaded",
},
});
}
if (ev.error && ev.error !== "") {
console.error(ev.error);
bc.close();
}
};
} catch (err) {
console.error(err, "service worker error in preopen");
bc && bc.close();
window.close();
showCloseText();
}
}
</script>
</body>
</html>
${""}
`,
],
{ type: "text/html" }
)
)
);
}
} catch (error) {
console.log("Error caught in sw.js", error);
}
});
Production Environment
If you are hosting your application in production enviornment, you also need to create a environment variable called PUBLIC_URL
at the web hosting platform you use. See below example of how it would look like in Vercel:
Webpack and Dependency Issues
If you are setting up a new web3 project from scratch, chances are likely you will have some dependency issues that need polyfilling. This is because web3.js and ether.js that work with the Liquality SDK under the hood have certain dependencies which are not present within the browser environment by webpack 5. That is why you need to polyfill certain modules in your project to override and enable their usage.
React Polyfill Solution
If you are using create-react-app
version >= 5 you may run into issues building and the project complaining about certain modules. You can resolve it by polyfilling.
- Install
react-app-rewired
and the required dependencies in your project
- npm
- Yarn
npm install --save-dev react-app-rewired crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url buffer process
yarn add --dev react-app-rewired crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url buffer process
Then create a file called config-overrides.js
in the root folder with the following content:
const webpack = require("webpack");
module.exports = function override(config) {
const fallback = config.resolve.fallback || {};
Object.assign(fallback, {
crypto: require.resolve("crypto-browserify"),
stream: require.resolve("stream-browserify"),
assert: require.resolve("assert"),
http: require.resolve("stream-http"),
https: require.resolve("https-browserify"),
os: require.resolve("os-browserify"),
url: require.resolve("url"),
});
config.resolve.fallback = fallback;
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: "process/browser",
Buffer: ["buffer", "Buffer"],
}),
]);
config.ignoreWarnings = [/Failed to parse source map/];
config.module.rules.push({
test: /\.(js|mjs|jsx)$/,
enforce: "pre",
loader: require.resolve("source-map-loader"),
resolve: {
fullySpecified: false,
},
});
return config;
};
If you're using craco
or similar config files then similar changes need to be done to the craco.config.js
file
- Within the
package.json
file in the root project, you also need to change the scripts field for start, build and test. Instead ofreact-scripts
replace it withreact-app-rewired
- before
- after
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
Vue Polyfill Solution
- Install required dependencies in your project
- npm
- Yarn
npm install --save node-polyfill-webpack-plugin
yarn add node-polyfill-webpack-plugin
Then add the following lines to vue.config.js
const { defineConfig } = require("@vue/cli-service");
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
configureWebpack: {
plugins: [new NodePolyfillPlugin()],
optimization: {
splitChunks: {
chunks: "all",
},
},
},
});
Angular Polyfill Solution
- Install required dependencies in your project
- npm
- Yarn
npm install --save-dev crypto-browserify stream-browserify assert stream-http https-browserify os-browserify process buffer url
yarn add --dev crypto-browserify stream-browserify assert stream-http https-browserify os-browserify process buffer url
- Then add the following
paths
in thetsconfig.json
incompilerOptions
. That way webpack can access the correct dependencies.
{
"compilerOptions": {
"paths" : {
"crypto": ["./node_modules/crypto-browserify"],
"stream": ["./node_modules/stream-browserify"],
"assert": ["./node_modules/assert"],
"http": ["./node_modules/stream-http"],
"https": ["./node_modules/https-browserify"],
"os": ["./node_modules/os-browserify"],
"process": ["./node_modules/process"],
}
}
}
- Add the following lines to
polyfills.ts
file:
import { Buffer } from "buffer";
(window as any).global = window;
global.Buffer = Buffer;
global.process = {
env: { DEBUG: undefined },
version: "",
nextTick: require("next-tick"),
} as any;