Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format send result to work with ethers #8

Merged
merged 7 commits into from
Aug 23, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions MetaMaskConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ class MetaMaskConnector {
constructor(options) {
this.config = Object.assign({}, { port: DEFAULT_PORT }, options);
}

async start() {
this._app = express();
this._app.use(express.static(path.resolve(__dirname, 'client')));
this._wss = await this._runServer();
await this._initialize();
}

stop() {
return new Promise(resolve => {
this._wss.close(() => {
Expand All @@ -25,6 +27,7 @@ class MetaMaskConnector {
});
});
}

_runServer() {
return new Promise((resolve, reject) => {
this._server = this._app.listen(this.config.port, 'localhost', err => {
Expand All @@ -33,6 +36,7 @@ class MetaMaskConnector {
});
});
}

_initialize() {
return new Promise(resolve => {
this._wss.on('connection', ws => {
Expand All @@ -49,9 +53,11 @@ class MetaMaskConnector {
});
});
}

ready() {
return this._ws && this._ws.readyState === WebSocket.OPEN;
}

static handleMessage(msg) {
let message;
try {
Expand All @@ -62,6 +68,7 @@ class MetaMaskConnector {
const { action, requestId, payload } = message;
return MetaMaskConnector.handleAction(action, requestId, payload);
}

static handleAction(action, requestId, payload) {
if (action === 'error') {
throw new Error(payload);
Expand All @@ -72,6 +79,7 @@ class MetaMaskConnector {
responsePayload: payload,
};
}

send(action, requestId, payload, requiredAction) {
return new Promise(resolve => {
const onMsg = msg => {
Expand All @@ -93,6 +101,7 @@ class MetaMaskConnector {
this._ws.send(msg);
});
}

getProvider() {
return new RemoteMetaMaskProvider(this);
}
Expand Down
105 changes: 69 additions & 36 deletions RemoteMetaMaskProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@ class RemoteMetaMaskProvider {
this._connector = connector;
this._callbacks = new Map();
}
_getAsyncMethod(method) {
// Sync methods don't work with MetaMask
this.syncMethods = [

// Generate a request id to track callbacks from async methods
static generateRequestId() {
const s4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}

// Get the associated async method for the given sync method (MetaMask does
// not work with sync methods)
static getAsyncMethod(method) {
const syncMethods = [
'version_node',
'version_network',
'version_ethereum',
Expand All @@ -20,13 +31,17 @@ class RemoteMetaMaskProvider {
'eth_accounts',
'eth_blockNumber',
];
const idx = this.syncMethods.indexOf(method);

// Translate the defined sync methods
const idx = syncMethods.indexOf(method);
if (idx >= 0) {
return this.syncMethods[idx].replace(
return syncMethods[idx].replace(
/(.+)_([a-z])(.+)/,
(str, p1, p2, p3) => `${p1}_get${p2.toUpperCase()}${p3}`,
);
}

// Translate other sync methods
const translateMethod = {
net_version: 'version_getNetwork',
eth_getLogs: 'eth_filter',
Expand All @@ -35,60 +50,78 @@ class RemoteMetaMaskProvider {
if (Object.prototype.hasOwnProperty.call(translateMethod, method)) {
return translateMethod[method];
}

return method;
}
_guid() {
this.s4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
return `
${this.s4()}
${this.s4()}
-
${this.s4()}
-
${this.s4()}
-
${this.s4()}
-
${this.s4()}
${this.s4()}
${this.s4()}
`.replace(/[\n\r]+ */g, '');

// When connected to a remote network, the return values for "gasPrice" and
// "value" are strings, so we will need to properly format them for ethers.
// Ideally we would use the big number type from bn.js or bignumber.js but
// ethers does not support any big number type other than it's own.
static formatResult(_result) {
const result = _result;

// Format "gasPrice"
if (result && typeof result.gasPrice === 'string') {
result.gasPrice = Number(result.gasPrice);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather see parseInt(x, 10) here as it is more explicit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

// Format "value"
if (result && typeof result.value === 'string') {
result.value = Number(result.value);
}

return result;
}

// Define send method
send(_payload, _callback) {
if (!this._connector.ready()) {
return _callback(
new Error("Can't send. Not connected to a MetaMask socket."),
new Error('Unable to send. Not connected to a MetaMask socket.'),
);
}
// Because requests are handled across a WebSocket they need to be
// associated with their callback with an ID which is returned with the
// response.
const requestId = this._guid();
const payload = _payload;

// Because requests are handled across a WebSocket, their callbacks need to
// be associated with an ID which is returned with the response.
const requestId = RemoteMetaMaskProvider.generateRequestId();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We like to use this.constructor.xxx instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// Set the callback using the requestId
this._callbacks.set(requestId, _callback);
payload.method = this._getAsyncMethod(payload.method);

// Set the payload to allow reassignment
const payload = _payload;

// Get the async method (Metamask does not support sync methods)
payload.method = RemoteMetaMaskProvider.getAsyncMethod(payload.method);

return this._connector
.send('execute', requestId, payload, 'executed')
.then(({ requestId: responseRequestId, result }) => {
const requestCallback = this._callbacks.get(responseRequestId);
if (!this._callbacks.has(responseRequestId)) {
return; // A response for this request was already handled
}
this._callbacks.delete(responseRequestId);

// Exit if a response for this request was already handled
if (!this._callbacks.has(responseRequestId)) return;

// Throw error if send error
if (result && result.error) {
requestCallback(new Error(result.error));
}

// Handle request callback
requestCallback(null, {
id: payload.id,
jsonrpc: '2.0',
result,
result: RemoteMetaMaskProvider.formatResult(result),
});

// Delete the callback after the request has been handled
this._callbacks.delete(responseRequestId);
})
.catch(err => _callback(err));
}

// Define async send method
sendAsync(payload, callback) {
this.send(payload, callback);
}
Expand Down
11 changes: 11 additions & 0 deletions client/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ul {
list-style: none;
margin: 0;
padding: 2rem;
}

li {
line-height: 1.5;
padding: 1rem;
word-break: break-all;
}
1 change: 1 addition & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="/index.css">
<title>MetaMask Connector</title>
</head>
<body>
Expand Down
17 changes: 13 additions & 4 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
/* global web3:true */
/* global window:true */

(async function client(w) {
(async w => {
const addLog = msg => {
const logEntry = document.createElement('li');
logEntry.innerText = `${new Date().toString()} - ${msg}`;
logEntry.innerText = `${new Date().toString()}\n${msg}`;
document.querySelector('#messages').appendChild(logEntry);
};

const checkUnlocked = () =>
new Promise((resolve, reject) => {
web3.eth.getAccounts((err, accounts) => {
if (err) return reject(err);
return resolve(accounts && !!accounts[0]);
});
});

const execute = (requestId, method, params) =>
new Promise((resolve, reject) => {
const splitMethod = method.split('_');
Expand All @@ -25,7 +27,10 @@
if (err) {
return reject(err);
}
addLog(`Result from ${requestId} ${method}: ${JSON.stringify(result)}`);
addLog(
`Request ID: ${requestId}
Result from ${method}: ${JSON.stringify(result)}`,
);
return resolve(result);
});
try {
Expand All @@ -34,10 +39,12 @@
reject(e);
}
});

async function executeAction(requestId, { method, params }, reply) {
let result;
addLog(
`Request ${requestId} Calling ${method} with ${JSON.stringify(params)}`,
`Request ID: ${requestId}
Calling ${method}: ${JSON.stringify(params)}`,
);
try {
result = await execute(requestId, method, params);
Expand All @@ -48,6 +55,7 @@
}
return reply('executed', requestId, result);
}

if (!w.web3) {
return addLog('MetaMask not found!');
}
Expand All @@ -71,5 +79,6 @@
}
return true;
};

return true;
})(window);