walletlink-e
Version:
WalletLink JavaScript SDK
498 lines (497 loc) • 20.3 kB
JavaScript
"use strict";
// Copyright (c) 2018-2020 WalletLink.org <https://www.walletlink.org/>
// Copyright (c) 2018-2020 Coinbase, Inc. <https://www.coinbase.com/>
// Licensed under the Apache License, version 2.0
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletLinkProvider = void 0;
const bn_js_1 = __importDefault(require("bn.js"));
const util_1 = require("../util");
const eth_eip712_util_1 = __importDefault(require("../vendor-js/eth-eip712-util"));
const FilterPolyfill_1 = require("./FilterPolyfill");
const JSONRPC_1 = require("./JSONRPC");
const Web3Provider_1 = require("./Web3Provider");
const LOCAL_STORAGE_ADDRESSES_KEY = "Addresses";
class WalletLinkProvider {
constructor(options) {
this._filterPolyfill = new FilterPolyfill_1.FilterPolyfill(this);
this._addresses = [];
this._send = this.send;
this._sendAsync = this.sendAsync;
if (!options.relay) {
throw new Error("realy must be provided");
}
if (!options.jsonRpcUrl) {
throw new Error("jsonRpcUrl must be provided");
}
this._relay = options.relay;
this._chainId = util_1.ensureIntNumber(options.chainId || 1);
this._jsonRpcUrl = options.jsonRpcUrl;
const cahedAddresses = this._relay.getStorageItem(LOCAL_STORAGE_ADDRESSES_KEY);
if (cahedAddresses) {
const addresses = cahedAddresses.split(" ");
if (addresses[0] !== "") {
this._addresses = addresses;
}
}
}
get selectedAddress() {
return this._addresses[0] || undefined;
}
get networkVersion() {
return this._chainId.toString(10);
}
get isWalletLink() {
return true;
}
get host() {
return this._jsonRpcUrl;
}
get connected() {
return true;
}
isConnected() {
return true;
}
async enable() {
if (this._addresses.length > 0) {
return this._addresses;
}
return await this._send(JSONRPC_1.JSONRPCMethod.eth_requestAccounts);
}
close() {
this._relay.resetAndReload();
}
send(requestOrMethod, callbackOrParams) {
// send<T>(method, params): Promise<T>
if (typeof requestOrMethod === "string") {
const method = requestOrMethod;
const params = Array.isArray(callbackOrParams)
? callbackOrParams
: callbackOrParams !== undefined
? [callbackOrParams]
: [];
const request = {
jsonrpc: "2.0",
id: 0,
method,
params
};
return this._sendRequestAsync(request).then(res => res.result);
}
// send(JSONRPCRequest | JSONRPCRequest[], callback): void
if (typeof callbackOrParams === "function") {
const request = requestOrMethod;
const callback = callbackOrParams;
return this._sendAsync(request, callback);
}
// send(JSONRPCRequest[]): JSONRPCResponse[]
if (Array.isArray(requestOrMethod)) {
const requests = requestOrMethod;
return requests.map(r => this._sendRequest(r));
}
// send(JSONRPCRequest): JSONRPCResponse
const req = requestOrMethod;
return this._sendRequest(req);
}
sendAsync(request, callback) {
if (typeof callback !== "function") {
throw new Error("callback is required");
}
// send(JSONRPCRequest[], callback): void
if (Array.isArray(request)) {
const arrayCb = callback;
this._sendMultipleRequestsAsync(request)
.then(responses => arrayCb(null, responses))
.catch(err => arrayCb(err, null));
return;
}
// send(JSONRPCRequest, callback): void
const cb = callback;
this._sendRequestAsync(request)
.then(response => cb(null, response))
.catch(err => cb(err, null));
}
async scanQRCode(match) {
const res = await this._relay.scanQRCode(util_1.ensureRegExpString(match));
if (typeof res.result !== "string") {
throw new Error("result was not a string");
}
return res.result;
}
async arbitraryRequest(data) {
const res = await this._relay.arbitraryRequest(data);
if (typeof res.result !== "string") {
throw new Error("result was not a string");
}
return res.result;
}
supportsSubscriptions() {
return false;
}
subscribe() {
throw new Error("Subscriptions are not supported");
}
unsubscribe() {
throw new Error("Subscriptions are not supported");
}
disconnect() {
return true;
}
_sendRequest(request) {
const response = {
jsonrpc: "2.0",
id: request.id
};
const { method } = request;
response.result = this._handleSynchronousMethods(request);
if (response.result === undefined) {
throw new Error(`WalletLink does not support calling ${method} synchronously without ` +
`a callback. Please provide a callback parameter to call ${method} ` +
`asynchronously.`);
}
return response;
}
_setAddresses(addresses) {
if (!Array.isArray(addresses)) {
throw new Error("addresses is not an array");
}
this._addresses = addresses.map(address => util_1.ensureAddressString(address));
this._relay.setStorageItem(LOCAL_STORAGE_ADDRESSES_KEY, addresses.join(" "));
window.dispatchEvent(new CustomEvent("walletlink:addresses", { detail: this._addresses }));
}
_sendRequestAsync(request) {
return new Promise((resolve, reject) => {
try {
const syncResult = this._handleSynchronousMethods(request);
if (syncResult !== undefined) {
return resolve({
jsonrpc: "2.0",
id: request.id,
result: syncResult
});
}
const filterPromise = this._handleAsynchronousFilterMethods(request);
if (filterPromise !== undefined) {
filterPromise
.then(res => resolve(Object.assign(Object.assign({}, res), { id: request.id })))
.catch(err => reject(err));
return;
}
}
catch (err) {
return reject(err);
}
this._handleAsynchronousMethods(request)
.then(res => resolve(Object.assign(Object.assign({}, res), { id: request.id })))
.catch(err => reject(err));
});
}
_sendMultipleRequestsAsync(requests) {
return Promise.all(requests.map(r => this._sendRequestAsync(r)));
}
_handleSynchronousMethods(request) {
const { method } = request;
const params = request.params || [];
switch (method) {
case JSONRPC_1.JSONRPCMethod.eth_accounts:
return this._eth_accounts();
case JSONRPC_1.JSONRPCMethod.eth_coinbase:
return this._eth_coinbase();
case JSONRPC_1.JSONRPCMethod.eth_uninstallFilter:
return this._eth_uninstallFilter(params);
case JSONRPC_1.JSONRPCMethod.net_version:
return this._net_version();
default:
return undefined;
}
}
_handleAsynchronousMethods(request) {
const { method } = request;
const params = request.params || [];
switch (method) {
case JSONRPC_1.JSONRPCMethod.eth_requestAccounts:
return this._eth_requestAccounts();
case JSONRPC_1.JSONRPCMethod.eth_sign:
return this._eth_sign(params);
case JSONRPC_1.JSONRPCMethod.eth_ecRecover:
return this._eth_ecRecover(params);
case JSONRPC_1.JSONRPCMethod.personal_sign:
return this._personal_sign(params);
case JSONRPC_1.JSONRPCMethod.personal_ecRecover:
return this._personal_ecRecover(params);
case JSONRPC_1.JSONRPCMethod.eth_signTransaction:
return this._eth_signTransaction(params);
case JSONRPC_1.JSONRPCMethod.eth_sendRawTransaction:
return this._eth_sendRawTransaction(params);
case JSONRPC_1.JSONRPCMethod.eth_sendTransaction:
return this._eth_sendTransaction(params);
case JSONRPC_1.JSONRPCMethod.eth_signTypedData_v1:
return this._eth_signTypedData_v1(params);
case JSONRPC_1.JSONRPCMethod.eth_signTypedData_v2:
return this._throwUnsupportedMethodError();
case JSONRPC_1.JSONRPCMethod.eth_signTypedData_v3:
return this._eth_signTypedData_v3(params);
case JSONRPC_1.JSONRPCMethod.eth_signTypedData_v4:
case JSONRPC_1.JSONRPCMethod.eth_signTypedData:
return this._eth_signTypedData_v4(params);
case JSONRPC_1.JSONRPCMethod.walletlink_arbitrary:
return this._walletlink_arbitrary(params);
}
return window
.fetch(this._jsonRpcUrl, {
method: "POST",
body: JSON.stringify(request),
mode: "cors",
headers: { "Content-Type": "application/json" }
})
.then(res => res.json())
.then(json => {
if (!json) {
throw new Web3Provider_1.ProviderError("unexpected response");
}
const response = json;
const { error } = response;
if (error) {
throw new Web3Provider_1.ProviderError(error.message || "RPC Error", error.code, error.data);
}
return response;
});
}
_handleAsynchronousFilterMethods(request) {
const { method } = request;
const params = request.params || [];
switch (method) {
case JSONRPC_1.JSONRPCMethod.eth_newFilter:
return this._eth_newFilter(params);
case JSONRPC_1.JSONRPCMethod.eth_newBlockFilter:
return this._eth_newBlockFilter();
case JSONRPC_1.JSONRPCMethod.eth_newPendingTransactionFilter:
return this._eth_newPendingTransactionFilter();
case JSONRPC_1.JSONRPCMethod.eth_getFilterChanges:
return this._eth_getFilterChanges(params);
case JSONRPC_1.JSONRPCMethod.eth_getFilterLogs:
return this._eth_getFilterLogs(params);
}
return undefined;
}
_isKnownAddress(addressString) {
try {
const address = util_1.ensureAddressString(addressString);
return this._addresses.includes(address);
}
catch (_a) { }
return false;
}
_ensureKnownAddress(addressString) {
if (!this._isKnownAddress(addressString)) {
throw new Error("Unknown Ethereum address");
}
}
_prepareTransactionParams(tx) {
const fromAddress = tx.from
? util_1.ensureAddressString(tx.from)
: this.selectedAddress;
if (!fromAddress) {
throw new Error("Ethereum address is unavailable");
}
this._ensureKnownAddress(fromAddress);
const toAddress = tx.to ? util_1.ensureAddressString(tx.to) : null;
const weiValue = tx.value != null ? util_1.ensureBN(tx.value) : new bn_js_1.default(0);
const data = tx.data ? util_1.ensureBuffer(tx.data) : Buffer.alloc(0);
const nonce = tx.nonce != null ? util_1.ensureIntNumber(tx.nonce) : null;
const gasPriceInWei = tx.gasPrice != null ? util_1.ensureBN(tx.gasPrice) : null;
const gasLimit = tx.gas != null ? util_1.ensureBN(tx.gas) : null;
const chainId = this._chainId;
return {
fromAddress,
toAddress,
weiValue,
data,
nonce,
gasPriceInWei,
gasLimit,
chainId
};
}
_requireAuthorization() {
if (this._addresses.length === 0) {
throw new Web3Provider_1.ProviderError("Unauthorized", Web3Provider_1.ProviderErrorCode.UNAUTHORIZED);
}
}
_throwUnsupportedMethodError() {
throw new Web3Provider_1.ProviderError("Unsupported method", Web3Provider_1.ProviderErrorCode.UNSUPPORTED_METHOD);
}
async _signEthereumMessage(message, address, addPrefix, typedDataJson) {
this._ensureKnownAddress(address);
try {
const res = await this._relay.signEthereumMessage(message, address, addPrefix, typedDataJson);
return { jsonrpc: "2.0", id: 0, result: res.result };
}
catch (err) {
if (typeof err.message === "string" &&
err.message.match(/(denied|rejected)/i)) {
throw new Web3Provider_1.ProviderError("User denied message signature", Web3Provider_1.ProviderErrorCode.USER_DENIED_REQUEST_SIGNATURE);
}
throw err;
}
}
async _ethereumAddressFromSignedMessage(message, signature, addPrefix) {
const res = await this._relay.ethereumAddressFromSignedMessage(message, signature, addPrefix);
return { jsonrpc: "2.0", id: 0, result: res.result };
}
_eth_accounts() {
return this._addresses;
}
_eth_coinbase() {
return this.selectedAddress || null;
}
_net_version() {
return this._chainId.toString(10);
}
async _eth_requestAccounts() {
if (this._addresses.length > 0) {
return Promise.resolve({
jsonrpc: "2.0",
id: 0,
result: this._addresses
});
}
let res;
try {
res = await this._relay.requestEthereumAccounts();
}
catch (err) {
if (typeof err.message === "string" &&
err.message.match(/(denied|rejected)/i)) {
throw new Web3Provider_1.ProviderError("User denied account authorization", Web3Provider_1.ProviderErrorCode.USER_DENIED_REQUEST_ACCOUNTS);
}
throw err;
}
if (!res.result) {
throw new Error("accounts received is empty");
}
this._setAddresses(res.result);
return { jsonrpc: "2.0", id: 0, result: this._addresses };
}
_eth_sign(params) {
this._requireAuthorization();
const address = util_1.ensureAddressString(params[0]);
const message = util_1.ensureBuffer(params[1]);
return this._signEthereumMessage(message, address, false);
}
_eth_ecRecover(params) {
const message = util_1.ensureBuffer(params[0]);
const signature = util_1.ensureBuffer(params[1]);
return this._ethereumAddressFromSignedMessage(message, signature, false);
}
_personal_sign(params) {
this._requireAuthorization();
const message = util_1.ensureBuffer(params[0]);
const address = util_1.ensureAddressString(params[1]);
return this._signEthereumMessage(message, address, true);
}
_personal_ecRecover(params) {
const message = util_1.ensureBuffer(params[0]);
const signature = util_1.ensureBuffer(params[1]);
return this._ethereumAddressFromSignedMessage(message, signature, true);
}
async _eth_signTransaction(params) {
this._requireAuthorization();
const tx = this._prepareTransactionParams(params[0] || {});
try {
const res = await this._relay.signEthereumTransaction(tx);
return { jsonrpc: "2.0", id: 0, result: res.result };
}
catch (err) {
if (typeof err.message === "string" &&
err.message.match(/(denied|rejected)/i)) {
throw new Web3Provider_1.ProviderError("User denied transaction signature", Web3Provider_1.ProviderErrorCode.USER_DENIED_REQUEST_SIGNATURE);
}
throw err;
}
}
async _eth_sendRawTransaction(params) {
const signedTransaction = util_1.ensureBuffer(params[0]);
const res = await this._relay.submitEthereumTransaction(signedTransaction, this._chainId);
return { jsonrpc: "2.0", id: 0, result: res.result };
}
async _eth_sendTransaction(params) {
this._requireAuthorization();
const tx = this._prepareTransactionParams(params[0] || {});
try {
const res = await this._relay.signAndSubmitEthereumTransaction(tx);
return { jsonrpc: "2.0", id: 0, result: res.result };
}
catch (err) {
if (typeof err.message === "string" &&
err.message.match(/(denied|rejected)/i)) {
throw new Web3Provider_1.ProviderError("User denied transaction signature", Web3Provider_1.ProviderErrorCode.USER_DENIED_REQUEST_SIGNATURE);
}
throw err;
}
}
async _eth_signTypedData_v1(params) {
this._requireAuthorization();
const typedData = util_1.ensureParsedJSONObject(params[0]);
const address = util_1.ensureAddressString(params[1]);
this._ensureKnownAddress(address);
const message = eth_eip712_util_1.default.hashForSignTypedDataLegacy({ data: typedData });
const typedDataJSON = JSON.stringify(typedData, null, 2);
return this._signEthereumMessage(message, address, false, typedDataJSON);
}
async _eth_signTypedData_v3(params) {
this._requireAuthorization();
const address = util_1.ensureAddressString(params[0]);
const typedData = util_1.ensureParsedJSONObject(params[1]);
this._ensureKnownAddress(address);
const message = eth_eip712_util_1.default.hashForSignTypedData_v3({ data: typedData });
const typedDataJSON = JSON.stringify(typedData, null, 2);
return this._signEthereumMessage(message, address, false, typedDataJSON);
}
async _eth_signTypedData_v4(params) {
this._requireAuthorization();
const address = util_1.ensureAddressString(params[0]);
const typedData = util_1.ensureParsedJSONObject(params[1]);
this._ensureKnownAddress(address);
const message = eth_eip712_util_1.default.hashForSignTypedData_v4({ data: typedData });
const typedDataJSON = JSON.stringify(typedData, null, 2);
return this._signEthereumMessage(message, address, false, typedDataJSON);
}
async _walletlink_arbitrary(params) {
const data = params[0];
if (typeof data !== "string") {
throw new Error("parameter must be a string");
}
const result = await this.arbitraryRequest(data);
return { jsonrpc: "2.0", id: 0, result };
}
_eth_uninstallFilter(params) {
const filterId = util_1.ensureHexString(params[0]);
return this._filterPolyfill.uninstallFilter(filterId);
}
async _eth_newFilter(params) {
const param = params[0];
const filterId = await this._filterPolyfill.newFilter(param);
return { jsonrpc: "2.0", id: 0, result: filterId };
}
async _eth_newBlockFilter() {
const filterId = await this._filterPolyfill.newBlockFilter();
return { jsonrpc: "2.0", id: 0, result: filterId };
}
async _eth_newPendingTransactionFilter() {
const filterId = await this._filterPolyfill.newPendingTransactionFilter();
return { jsonrpc: "2.0", id: 0, result: filterId };
}
_eth_getFilterChanges(params) {
const filterId = util_1.ensureHexString(params[0]);
return this._filterPolyfill.getFilterChanges(filterId);
}
_eth_getFilterLogs(params) {
const filterId = util_1.ensureHexString(params[0]);
return this._filterPolyfill.getFilterLogs(filterId);
}
}
exports.WalletLinkProvider = WalletLinkProvider;