Initial commit aka code from heaven

This commit is contained in:
Jaco Greeff
2019-05-20 12:40:44 +02:00
commit 0b6bcd2bb9
74 changed files with 17264 additions and 0 deletions

1
.babelrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('./babel.config.js');

6
.codeclimate.yml Normal file
View File

@@ -0,0 +1,6 @@
exclude_patterns:
- "**/*.spec.js"
- "**/*.spec.ts"
- "**/*.test.js"
- "**/*.test.ts"
- "docs/**/*.js"

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
root = true
[*]
indent_style=space
indent_size=2
tab_width=2
end_of_line=lf
charset=utf-8
trim_trailing_whitespace=true
max_line_length=120
insert_final_newline=true

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
build/
coverage/
node_modules/
tmp/
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.npmrc
lerna-debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
_book
docs/html

1
.npmignore Normal file
View File

@@ -0,0 +1 @@

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
10

14
.travis.yml Normal file
View File

@@ -0,0 +1,14 @@
language: node_js
node_js:
- "10"
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin::$(yarn global bin):$PATH
cache:
yarn: true
directories:
- node_modules
install:
- yarn
script:
- yarn polkadot-dev-build-travis

0
CHANGELOG.md Normal file
View File

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Contributing
## What?
Individuals making significant and valuable contributions are given commit-access to a project to contribute as they see fit.
A project is more like an open wiki than a standard guarded open source project.
## Rules
There are a few basic ground-rules for contributors (including the maintainer(s) of the project):
1. **No `--force` pushes** or modifying the Git history in any way. If you need to rebase, ensure you do it in your own repo.
2. **Non-master branches**, prefixed with a short name moniker (e.g. `<initials>-<feature>`) must be used for ongoing work.
3. **All modifications** must be made in a **pull-request** to solicit feedback from other contributors.
4. A pull-request *must not be merged until CI* has finished successfully.
#### Merging pull requests once CI is successful:
- A pull request with no large change to logic that is an urgent fix may be merged after a non-author contributor has reviewed it well.
- No PR should be merged until all reviews' comments are addressed.
#### Reviewing pull requests:
When reviewing a pull request, the end-goal is to suggest useful changes to the author. Reviews should finish with approval unless there are issues that would result in:
- Buggy behaviour.
- Undue maintenance burden.
- Breaking with house coding style.
- Pessimisation (i.e. reduction of speed as measured in the projects benchmarks).
- Feature reduction (i.e. it removes some aspect of functionality that a significant minority of users rely on).
- Uselessness (i.e. it does not strictly add a feature or fix a known issue).
#### Reviews may not be used as an effective veto for a PR because:
- There exists a somewhat cleaner/better/faster way of accomplishing the same feature/fix.
- It does not fit well with some other contributors' longer-term vision for the project.
## Releases
Declaring formal releases remains the prerogative of the project maintainer(s).
## Changes to this arrangement
This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
## Heritage
These contributing guidelines are modified from the "OPEN Open Source Project" guidelines for the Level project: [https://github.com/Level/community/blob/master/CONTRIBUTING.md](https://github.com/Level/community/blob/master/CONTRIBUTING.md)

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# @polkadot/extension
A very simple scaffolding browser extension that injects a [@polkadot/api](https://github.com/polkadot-js/api) Signer into a page, along with any associated accounts, allowing for use by the application. This is an extensible POC implementation of a Polkadot/Substrate browser signer.
As it stands, it does one thing: it _only_ manages accounts and allows the signing of transactions with those accounts. (It does not inject providers for use by dapps, not does it perform the function of a wallet where it submits txs to the network).
# Running
Currently is not packaged since it is under heavy development. To use -
1. Build via `yarn build` or `yarn watch`
2. Add the extension under `chrome://extensions/` (ensure you have the Development flag set) and "Load unpacked"
3. When visiting `http://localhost:3000` or `https://polkadot.js.org/apps/` it will inject (the menifest currently only lists these 2 endpoints)
Once added, you can create an account (via a generated seed) or import via an existing seed.
## Overview
It injects `injectedWeb3` into the global `window` object, exposing the following:
```js
// a version that identifies the actual injection version (future-use)
type Version = 0;
// an interface describing an account
interface Account {
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
}
// exposes accounts
interface Accounts {
readonly all: Array<Account>;
}
// a signer that communicates with the extension via sendMessage
interface Signer extends SignerInterface {
// no specific signer extensions
}
interface Injected {
readonly accounts: Accounts;
readonly signer: Signer;
readonly version: Version;
}
```
The app can use all or any of these, depending on needs. To instantiate the `@polkadot/api` with the provider (app does not have it's own) and the signer (allowing the extension to sign messages) can be done via -
```js
import { ApiPromise } from '@polkadot/api';
const { signer } = window.injectedWeb3;
const api = await Api.create({ signer });
```

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
extends: '@polkadot/dev/config/babel'
};

13
jest.config.js Normal file
View File

@@ -0,0 +1,13 @@
const config = require('@polkadot/dev/config/jest');
module.exports = Object.assign({}, config, {
moduleNameMapper: {
'@polkadot/extension-(ui)(.*)$': '<rootDir>/packages/extension-$1/src/$2',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
'\\.(css|less)$': 'empty/object'
},
modulePathIgnorePatterns: [
'<rootDir>/packages/extension/build',
'<rootDir>/packages/extension-ui/build'
]
});

13
lerna.json Normal file
View File

@@ -0,0 +1,13 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"allowBranch": "master"
}
},
"packages": [
"packages/*"
],
"version": "0.1.0"
}

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "@polkadot/extension",
"version": "0.1.0",
"description": "A sample signer extension for the @polkadot/api",
"repository": "https://github.com/polkadot-js/extension",
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "Apache-2",
"private": true,
"workspaces": [
"packages/*"
],
"engines": {
"node": ">=10.13.0",
"yarn": "^1.10.1"
},
"resolutions": {
"babel-core": "^7.0.0-bridge.0",
"typescript": "^3.4.5"
},
"scripts": {
"build": "NODE_ENV=production polkadot-dev-build-ts",
"check": "yarn lint",
"lint": "tslint --project . && tsc --noEmit --pretty",
"clean": "polkadot-dev-clean-build",
"postinstall": "polkadot-dev-yarn-only",
"test": "jest --coverage",
"watch": "cd packages/extension && webpack --config webpack.config.js --watch"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@polkadot/dev-react": "^0.30.0-beta.4",
"@polkadot/ts": "^0.1.56"
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "@polkadot/extension-ui",
"description": "A sample signer extension for the @polkadot/api",
"version": "0.1.0",
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "Apache-2",
"dependencies": {
"@polkadot/extension-inject": "^0.1.0",
"@polkadot/ui-identicon": "^0.39.0-beta.5",
"@types/react-router-dom": "^4.3.2",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.0"
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { Address, Button, Header, Tip } from '../components';
type Props = {
accounts: Array<KeyringJson>
};
export default function Accounts ({ accounts }: Props) {
return (
<div>
<Header label='accounts' />
{
(accounts.length === 0)
? <Tip header='add accounts' type='warn'>You currently don't have any accounts. Either create a new account or if you have an existing account you wish to use, import it with the seed phrase</Tip>
: accounts.map(({ address, meta: { name } }) => (
<Address
address={address}
key={address}
name={name}
/>
))
}
<Button
isFull
label='I want to create a new account with a new seed'
to='/account/create'
/>
<Button
isFull
label='I have a pre-existing seed, import the account'
to='/account/import'
/>
</div>
);
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState, useEffect } from 'react';
import { Address, Button, Header, Loading, TextArea } from '../components';
import { createAccount, createSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {};
export default function Create (props: Props) {
const [account, setAccount] = useState(null as null | { address: string, seed: string });
const [name, setName] = useState(null as string | null);
const [password, setPassword] = useState(null as string | null);
useEffect(() => {
createSeed()
.then(setAccount)
.catch(console.error);
}, []);
// FIXME Duplicated between here and Import.tsx
const _onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => {
window.location.hash = '/';
})
.catch(console.error);
}
};
return (
<div>
<Header label='create account' />
<Back />
<Loading>{account && (
<>
<TextArea
isReadOnly
label={`generated 12-word mnemonic seed`}
value={account.seed}
/>
<Name
isFocussed
onChange={setName}
/>
{name && <Password onChange={setPassword} />}
{name && password && (
<>
<Address
address={account.address}
name={name}
/>
<Button
isFull
label='Add the account with the generated seed'
onClick={_onCreate}
/>
</>
)}
</>
)}</Loading>
</div>
);
}

View File

@@ -0,0 +1,68 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import { Address, Button, Header, TextArea } from '../components';
import { createAccount, validateSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {};
export default function Import (props: Props) {
const [account, setAccount] = useState(null as null | { address: string, seed: string });
const [name, setName] = useState(null as string | null);
const [password, setPassword] = useState(null as string | null);
const _onChangeSeed = (seed: string): void => {
validateSeed(seed)
.then(setAccount)
.catch((error) => {
console.error(error);
setAccount(null);
});
};
// FIXME Duplicated between here and Create.tsx
const _onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => {
window.location.hash = '/';
})
.catch(console.error);
}
};
return (
<div>
<Header label='import account' />
<Back />
<TextArea
isError={!account}
isFocussed
label={`existing 12 or 24-word mnemonic seed`}
onChange={_onChangeSeed}
/>
{account && <Name onChange={setName} />}
{account && name && <Password onChange={setPassword} />}
{account && name && password && (
<>
<Address
address={account.address}
name={name}
/>
<Button
isFull
label='Add the account with the supplied seed'
onClick={_onCreate}
/>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,54 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
type Props = {
className?: string,
method: string,
nonce: string,
url: string
};
function Details ({ className, method, nonce, url }: Props) {
return (
<table className={className}>
<tbody>
<tr>
<td className='label'>from</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>nonce</td>
<td className='data'>{nonce}</td>
</tr>
<tr>
<td className='label'>method</td>
<td className='data'>{method}</td>
</tr>
</tbody>
</table>
);
}
export default styled(Details)`
border: 0;
display: block;
font-size: 0.75rem;
margin-left: 4.75rem;
td.data {
max-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
width: 100%;
}
td.label {
opacity: 0.5;
text-align: right;
}
`;

View File

@@ -0,0 +1,54 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { ActionBar, Address } from '../../components';
import { cancelRequest } from '../../messaging';
import Details from './Details';
import Unlock from './Unlock';
type Props = {
accounts: Array<KeyringJson>,
className?: string,
isFirst: boolean,
onAction: () => void,
request: MessageExtrinsicSign,
signId: number,
url: string
};
export default function Request ({ accounts, isFirst, onAction, request: { address, method, nonce }, signId, url }: Props) {
const account = accounts.find((account) => address === account.address);
const _onCancel = (): void => {
cancelRequest(signId)
.then(onAction)
.catch(console.error);
};
return (
<Address
address={address}
name={account ? account.meta.name : '<invalid account>'}
>
<Details
method={method}
nonce={nonce}
url={url}
/>
<ActionBar>
<a onClick={_onCancel}>Cancel</a>
</ActionBar>
<Unlock
isVisible={isFirst}
onAction={onAction}
signId={signId}
/>
</Address>
);
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import { Button, Input } from '../../components';
import { approveRequest } from '../../messaging';
type Props = {
signId: number,
isVisible: boolean,
onAction: () => void
};
export default function Unlock ({ isVisible, onAction, signId }: Props) {
const [password, setPassword] = useState('');
if (!isVisible) {
return null;
}
const _onSign = (): void => {
approveRequest(signId, password)
.then(onAction)
.catch(console.error);
};
return (
<>
<Input
isError={!password}
isFocussed
label='password for this account'
onChange={setPassword}
type='password'
/>
<Button
isFull
label='Sign the extrinsic'
onClick={_onSign}
/>
</>
);
}

View File

@@ -0,0 +1,36 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { Header } from '../../components';
import Request from './Request';
type Props = {
accounts: Array<KeyringJson>,
requests: Array<[number, MessageExtrinsicSign, string]>,
onAction: () => void
};
export default function Signing ({ accounts, onAction, requests }: Props) {
return (
<div>
<Header label='requests' />
{requests.map(([id, request, url], index) => (
<Request
accounts={accounts}
isFirst={index === 0}
key={id}
onAction={onAction}
request={request}
signId={id}
url={url}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,53 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
import { Loading } from '../components';
import { getAccounts, getRequests } from '../messaging';
import Accounts from './Accounts';
import Create from './Create';
import Import from './Import';
import Signing from './Signing';
type Props = {};
export default function Popup (props: Props) {
const [accounts, setAccounts] = useState(null as null | Array<KeyringJson>);
const [requests, setRequests] = useState(null as null | Array<[number, MessageExtrinsicSign, string]>);
const _onAction = (): void => {
getRequests()
.then(setRequests)
.catch(console.error);
};
useEffect((): void => {
Promise
.all([getAccounts(), getRequests()])
.then(([accounts, requests]) => {
setAccounts(accounts);
setRequests(requests);
})
.catch(console.error);
}, []);
return (
<Loading>{accounts && requests && (
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/import' component={Import} />
<Route exact path='/' component={() => {
return requests.length
? <Signing accounts={accounts} onAction={_onAction} requests={requests} />
: <Accounts accounts={accounts} />;
}} />
</Switch>
)}</Loading>
);
}

View File

@@ -0,0 +1,31 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
children: React.ReactNode;
className?: string;
};
function ActionBar ({ children, className }: Props) {
return (
<div className={className}>
{children}
</div>
);
}
export default styled(ActionBar)`
padding: ${defaults.boxPadding};
text-align: right;
a {
cursor: pointer;
margin-left: 0.75rem;
}
`;

View File

@@ -0,0 +1,70 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Identicon from '@polkadot/ui-identicon';
import Box from './Box';
import defaults from './defaults';
type Props = {
address?: string | null,
children?: React.ReactNode;
className?: string,
isHidden?: boolean,
name?: string | null,
theme?: 'polkadot' | 'substrate'
};
function Address ({ address, children, className, isHidden, name, theme = 'polkadot' }: Props) {
if (isHidden) {
return null;
}
return (
<Box className={className}>
<div>
<div className='name'>{name || '<unknown>'}</div>
<div className='address'>{address || '<unknown>'}</div>
<div className='children'>{children}</div>
<Identicon
className='icon'
size={64}
theme={theme}
value={address}
/>
</div>
</Box>
);
}
export default styled(Address)`
background: transparent;
padding: 1rem;
> div {
box-sizing: border-box;
padding: ${defaults.boxPadding};
position: relative;
.address,
.name {
padding-left: 4.75rem;
}
.address {
opacity: 0.5;
overflow: hidden;
text-overflow: ellipsis;
}
.icon {
left: 0;
position: absolute;
top: -0.75rem;
z-index: 1;
}
}
`;

View File

@@ -0,0 +1,31 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
children: React.ReactNode;
className?: string;
};
function Box ({ children, className }: Props) {
return (
<article className={className}>
{children}
</article>
);
}
export default styled(Box)`
background: ${defaults.box.info.background};
border-radius: ${defaults.borderRadius};
color: ${defaults.color};
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
margin: ${defaults.boxMargin};
padding: 0.75rem 1rem;
`;

View File

@@ -0,0 +1,84 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
className?: string;
children?: React.ReactNode,
isDisabled?: boolean,
isFull?: boolean,
label?: string,
onClick?: () => void,
to?: string
};
const DISABLED_OPACITY = '0.3';
function Button ({ children, className, isDisabled, label, onClick, to }: Props) {
const _onClick = (): void => {
if (isDisabled) {
return;
}
onClick && onClick();
if (to) {
window.location.hash = to;
}
};
return (
<div className={className}>
<button onClick={_onClick}>
{label}{children}
</button>
</div>
);
}
export default styled(Button)`
box-sizing: border-box;
display: ${({ isFull }) =>
isFull
? 'block'
: 'inline-block'
};
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
width: ${({ isFull }) =>
isFull
? '100%'
: 'auto'
};
button {
background: ${defaults.btnBg};
border: ${defaults.btnBorder};
border-radius: ${defaults.borderRadius};
color: ${defaults.btnColor};
cursor: pointer;
display: block;
font-size: ${defaults.fontSize};
opacity: ${({ isDisabled }) =>
isDisabled
? DISABLED_OPACITY
: '0.8'
};
padding: ${defaults.btnPadding};
text-align: center;
width: 100%;
&:hover {
opacity: ${({ isDisabled }) =>
isDisabled
? DISABLED_OPACITY
: '1.0'
};
}
}
`;

View File

@@ -0,0 +1,33 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
children?: React.ReactNode,
className?: string,
label?: string
};
function Header ({ children, className, label }: Props) {
return (
<h2 className={className}>
{label}{children}
</h2>
);
}
export default styled(Header)`
background: ${defaults.hdrBg};
box-sizing: border-box;
color: ${defaults.hdrColor};
font-family: ${defaults.fontFamily};
font-weight: normal;
margin: 0 -0.75rem;
padding: 0.5rem 1rem;
text-transform: uppercase;
`;

View File

@@ -0,0 +1,77 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Label from './Label';
import defaults from './defaults';
type Props = {
className?: string,
isError?: boolean,
isFocussed?: boolean,
isReadOnly?: boolean,
label: string,
onChange?: (value: string) => void,
type?: 'text' | 'password',
value?: string
};
function Input ({ className, label, isFocussed, isReadOnly, onChange, type = 'text', value }: Props) {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<input
autoFocus={isFocussed}
readOnly={isReadOnly}
onChange={_onChange}
type={type}
value={value}
/>
</Label>
);
}
export default styled(Input)`
input {
background: ${({ isError, isReadOnly }) =>
isError
? defaults.box.error.background
: isReadOnly
? '#eee'
: '#fff'
};
border-color: ${({ isError }) =>
isError
? defaults.box.error.border
: defaults.inputBorder
};
border-radius: ${defaults.borderRadius};
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: ${({ isError }) =>
isError
? defaults.box.error.border
: defaults.color
};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
padding: ${defaults.inputPadding};
width: 100%;
&:read-only {
box-shadow: none;
outline: none;
}
}
`;

View File

@@ -0,0 +1,39 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
children: React.ReactNode,
className?: string,
label: string
};
function Label ({ children, className, label }: Props) {
return (
<div className={className}>
<label>{label}</label>
{children}
</div>
);
}
export default styled(Label)`
box-sizing: border-box;
color: ${defaults.colorLabel};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
label {
display: block;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
`;

View File

@@ -0,0 +1,21 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
type Props = {
children?: React.ReactNode
};
export default function Loading ({ children }: Props) {
if (!children) {
return (
<div>... loading ...</div>
);
}
return (
<>{children}</>
);
}

View File

@@ -0,0 +1,76 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Label from './Label';
import defaults from './defaults';
type Props = {
className?: string,
isError?: boolean,
isFocussed?: boolean,
isReadOnly?: boolean,
label: string,
onChange?: (value: string) => void,
value?: string
};
function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value }: Props) {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<textarea
autoFocus={isFocussed}
onChange={_onChange}
readOnly={isReadOnly}
value={value}
/>
</Label>
);
}
export default styled(TextArea)`
textarea {
background: ${({ isError, isReadOnly }) =>
isError
? defaults.box.error.background
: isReadOnly
? '#eee'
: '#fff'
};
border-color: ${({ isError }) =>
isError
? defaults.box.error.border
: defaults.inputBorder
};
border-radius: ${defaults.borderRadius};
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: ${({ isError }) =>
isError
? defaults.box.error.border
: defaults.color
};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
padding: ${defaults.inputPadding};
resize: none;
width: 100%;
&:read-only {
box-shadow: none;
outline: none;
}
}
`;

View File

@@ -0,0 +1,57 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Color = {
background: string,
border: string,
color: string
};
type Type = keyof typeof defaults.box;
type Props = {
children: React.ReactNode,
className?: string,
header?: React.ReactNode,
type?: Type
};
function getColor ({ type }: Props): Color {
return defaults.box[type || 'info'] || defaults.box.info;
}
function Tip ({ children, className, header }: Props) {
return (
<article className={className}>
{header && <h3>{header}</h3>}
<div>{children}</div>
</article>
);
}
export default styled(Tip)`
background: ${(props) =>
getColor(props).background
};
border-left: 0.25rem solid ${(props) =>
getColor(props).border
};
color: ${(props) =>
getColor(props).color
};
margin: ${defaults.boxMargin};
padding: 1rem 1.25rem;
h3 {
color: ${(props) =>
getColor(props).border
};
font-weight: normal;
}
`;

View File

@@ -0,0 +1,48 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
type Props = {
children: React.ReactNode;
className?: string;
};
function View ({ children, className }: Props) {
return (
<main className={className}>
{children}
</main>
);
}
export default styled(View)`
color: ${defaults.color};
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
line-height: ${defaults.lineHeight};
a {
color: ${defaults.linkColor};
opacity: 0.9;
text-decoration: none;
&:hover {
color: ${defaults.linkColor};
opacity: 1.0;
}
&:visited {
color: ${defaults.linkColor};
}
}
h3 {
margin: 0 0 0.75rem;
text-transform: uppercase;
}
`;

View File

@@ -0,0 +1,51 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const TEXT_COLOR = '#4e4e4e';
const LABEL_COLOR = 'rgba(78, 78, 78, 0.75)';
const LINK_COLOR = '#3367d6';
const defaults: { [index: string]: any } = {
borderRadius: '0.25rem',
btnBg: '#fff',
btnBorder: `2px solid ${LINK_COLOR}`,
btnColor: LINK_COLOR, // '#fefefe',
btnPadding: '0.75rem 1rem',
boxMargin: '0.75rem 0',
boxPadding: '0 0.25rem',
color: TEXT_COLOR,
colorLabel: LABEL_COLOR,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSize: '1rem',
hdrBg: 'transparent',
hdrColor: '#ccc',
inputBorder: '#ccc',
inputPadding: '0.5rem 0.75rem',
lineHeight: '1.25',
linkColor: LINK_COLOR,
box: {
error: {
background: '#ffe6e6',
border: '#c00',
color: '#4d0000'
},
info: {
background: '#fafafa',
border: TEXT_COLOR,
color: 'inherit'
},
success: {
background: '#f3f5f7',
border: '#42b983',
color: 'inherit'
},
warn: {
background: 'rgba(255, 229, 100, .3)',
border: '#e7c000',
color: '#6b5900'
}
}
};
export default defaults;

View File

@@ -0,0 +1,17 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as ActionBar } from './ActionBar';
export { default as Address } from './Address';
export { default as Box } from './Box';
export { default as Button } from './Button';
export { default as Header } from './Header';
export { default as Input } from './Input';
export { default as Loading } from './Loading';
export { default as TextArea } from './TextArea';
export { default as Tip } from './Tip';
export { default as View } from './View';
export { default as defaults } from './defaults';
export { default as unicode } from './unicode';

View File

@@ -0,0 +1,14 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
// via http://www.amp-what.com/unicode/search/ (in hex)
const unicode = {
BACK: '\u21E6', // &#8678;
DOWN: '\u21E9', // &#8681;
FWD: '\u21E8', // &#8680;
UP: '\u21E7' // &#8679;
};
export default unicode;

View File

@@ -0,0 +1,28 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import { View } from './components';
export default function createView (Entry: React.ComponentType<any>, rootId: string = 'root'): void {
const rootElement = document.getElementById(rootId);
if (!rootElement) {
throw new Error(`Unable to find element with id '${rootId}'`);
}
ReactDOM.render(
<Suspense fallback='...'>
<View>
<HashRouter>
<Entry />
</HashRouter>
</View>
</Suspense>,
rootElement
);
}

View File

@@ -0,0 +1,6 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as createView } from './createView';
export { default as Popup } from './Popup';

View File

@@ -0,0 +1,51 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageTypes, MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { KeypairType } from '@polkadot/util-crypto/types';
let idCounter = 0;
function sendMessage (message: MessageTypes, request: any = {}): Promise<any> {
return new Promise((resolve, reject) => {
const id = ++idCounter;
chrome.runtime.sendMessage({ id, message, request }, ({ error, response }) => {
if (error) {
reject(new Error(error));
} else {
resolve(response);
}
});
});
}
export async function getAccounts (): Promise<Array<KeyringJson>> {
return sendMessage('accounts.list');
}
export async function getRequests (): Promise<Array<[number, MessageExtrinsicSign, string]>> {
return sendMessage('signing.requests');
}
export async function cancelRequest (id: number): Promise<boolean> {
return sendMessage('signing.cancel', { id });
}
export async function approveRequest (id: number, password: string): Promise<boolean> {
return sendMessage('signing.approve', { id, password });
}
export async function createAccount (name: string, password: string, suri: string, type?: KeypairType): Promise<boolean> {
return sendMessage('accounts.create', { name, password, suri, type });
}
export async function createSeed (length?: number, type?: KeypairType): Promise<{ address: string, seed: string }> {
return sendMessage('seed.create', { length, type });
}
export async function validateSeed (seed: string, type?: KeypairType): Promise<{ address: string, seed: string }> {
return sendMessage('seed.validate', { seed, type });
}

View File

@@ -0,0 +1,26 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { unicode } from '../components';
type Props = {
className?: string,
to?: string
};
function Back ({ className, to = '/' }: Props) {
return (
<div className={className}>
<Link to='/'>{unicode.BACK} Back</Link>
</div>
);
}
export default styled(Back)`
padding: 0.5rem;
`;

View File

@@ -0,0 +1,38 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useEffect, useState } from 'react';
import { Input } from '../components';
type Props = {
isFocussed?: boolean,
onChange: (name: string | null) => void
};
const MIN_LENGTH = 3;
export default function Name ({ isFocussed, onChange }: Props) {
const [name, setName] = useState('');
useEffect(() => {
onChange(
(name.length >= MIN_LENGTH)
? name
: null
);
}, [name]);
return (
<>
<Input
isError={name.length < MIN_LENGTH}
isFocussed={isFocussed}
label='a descriptive name for this account'
onChange={setName}
type='text'
/>
</>
);
}

View File

@@ -0,0 +1,47 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useEffect, useState } from 'react';
import { Input } from '../components';
type Props = {
isFocussed?: boolean,
onChange: (password: string | null) => void
};
const MIN_LENGTH = 6;
export default function Password ({ isFocussed, onChange }: Props) {
const [pass1, setPass1] = useState('');
const [pass2, setPass2] = useState('');
useEffect(() => {
onChange(
(pass1 && pass2 && (pass1.length >= MIN_LENGTH) && (pass1 === pass2))
? pass1
: null
);
}, [pass1, pass2]);
return (
<>
<Input
isError={pass1.length < MIN_LENGTH}
isFocussed={isFocussed}
label='a new password for this account'
onChange={setPass1}
type='password'
/>
{(pass1.length >= MIN_LENGTH) && (
<Input
isError={pass1 !== pass2}
label='repeat password for verification'
onChange={setPass2}
type='password'
/>
)}
</>
);
}

View File

@@ -0,0 +1,7 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as Back } from './Back';
export { default as Name } from './Name';
export { default as Password } from './Password';

View File

@@ -0,0 +1,19 @@
{
"name": "@polkadot/extension",
"description": "A sample signer extension for the @polkadot/api",
"version": "0.1.0",
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "Apache-2",
"dependencies": {
"@polkadot/api": "^0.79.0-beta.31",
"@polkadot/extension-inject": "^0.1.0",
"@polkadot/extension-ui": "^0.1.0",
"@polkadot/ui-keyring": "^0.39.0-beta.5"
},
"devDependencies": {
"@types/chrome": "^0.0.85",
"extract-loader": "^3.1.0",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,31 @@
{
"name": "polkadot{.js} extension",
"version": "0.0.1",
"manifest_version": 2,
"permissions": ["activeTab", "storage"],
"background": {
"scripts": ["background.js"],
"persistent": true
},
"browser_action": {
"default_title": "polkadot{.js}",
"default_popup": "popup.html"
},
"content_scripts": [{
"js": ["loader.js"],
"matches": [
"https://polkadot.js.org/apps/*",
"http://localhost:3000/*"
],
"run_at": "document_start"
}],
"icons": {
"16": "images/icon-16.png",
"32": "images/icon-32.png",
"48": "images/icon-48.png",
"64": "images/icon-64.png",
"128": "images/icon-128.png"
},
"web_accessible_resources": ["inject.js"],
"content_security_policy": "script-src 'self' 'unsafe-eval' 'wasm-eval'; object-src 'self'"
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>polkadot{.js}</title>
</head>
<body style="border-top:0.25rem solid #ccc; height:640px; margin:0; padding:0 0.75rem 0.75rem; width:480px;">
<div id="root"></div>
<script src='./popup.js'></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringStore } from '@polkadot/ui-keyring/types';
const lastError = (type: string): void => {
const error = chrome.runtime.lastError;
if (error) {
console.error(`ChromeStore.${type}:: chrome.runtime.lastError:`, error);
}
};
export default class ChromeStore implements KeyringStore {
all (cb: (key: string, value: any) => void): void {
chrome.storage.local.get(null, (result: { [index: string]: any }) => {
lastError('all');
Object.entries(result).forEach(([key, value]) =>
cb(key, value)
);
});
}
get (key: string, cb: (value: any) => void): void {
chrome.storage.local.get([key], (result: { [index: string]: any }) => {
lastError('get');
cb(result[key]);
});
}
remove (key: string, cb?: () => void): void {
chrome.storage.local.remove(key, () => {
lastError('remove');
cb && cb();
});
}
set (key: string, value: any, cb?: () => void): void {
// shortcut, don't save testing accounts in storage
if (key.indexOf('account:') === 0 && value.meta && value.meta.isTesting) {
cb && cb();
return;
}
chrome.storage.local.set({ [key]: value }, () => {
lastError('set');
cb && cb();
});
}
}

View File

@@ -0,0 +1,83 @@
// Copyright 2017-2019 @polkadot/types authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringPair } from '@polkadot/keyring/types';
import { ExtrinsicEra, Hash, IndexCompact, Struct, U8a } from '@polkadot/types';
import { blake2AsU8a } from '@polkadot/util-crypto';
// This is a copy of the signing in @polkadot/types with the method as a pure U8a
// (without metadata, we cannot reliably parse the Method, hence faling)
export default class SignaturePayloadRaw extends Struct {
protected _signature?: Uint8Array;
constructor (value?: any) {
super({
nonce: IndexCompact,
method: U8a,
era: ExtrinsicEra,
blockHash: Hash
}, value);
}
/**
* @description `true` if the payload refers to a valid signature
*/
get isSigned (): boolean {
return !!(this._signature && this._signature.length === 64);
}
/**
* @description The block [[Hash]] the signature applies to (mortal/immortal)
*/
get blockHash (): Hash {
return this.get('blockHash') as Hash;
}
/**
* @description The [[Method]] contained in the payload
*/
get method (): U8a {
return this.get('method') as U8a;
}
/**
* @description The [[ExtrinsicEra]]
*/
get era (): ExtrinsicEra {
return this.get('era') as ExtrinsicEra;
}
/**
* @description The [[IndexCompact]]
*/
get nonce (): IndexCompact {
return this.get('nonce') as IndexCompact;
}
/**
* @description The raw signature as a `Uint8Array`
*/
get signature (): Uint8Array {
if (!this.isSigned) {
throw new Error('Transaction is not signed');
}
return this._signature as Uint8Array;
}
/**
* @description Sign the payload with the keypair
*/
sign (signerPair: KeyringPair): Uint8Array {
const u8a = this.toU8a();
const encoded = u8a.length > 256
? blake2AsU8a(u8a)
: u8a;
this._signature = signerPair.sign(encoded);
return this._signature;
}
}

View File

@@ -0,0 +1,129 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { MessageTypes, MessageAccountCreate, MessageExtrinsicSign, MessageExtrinsicSignApprove, MessageExtrinsicSignCancel, MessageSeedCreate, MessageSeedCreate$Response, MessageSeedValidate, MessageSeedValidate$Response } from '../types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
import { assert, u8aToHex } from '@polkadot/util';
import RawPayload from '../RawPayload';
import SigningRequests from './SigningRequests';
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 24];
export default class Extension {
private signing: SigningRequests;
constructor (signing: SigningRequests) {
this.signing = signing;
}
private accountsCreate ({ name, password, suri, type }: MessageAccountCreate): boolean {
keyring.addUri(suri, password, { name }, type);
return true;
}
private accountsList (): Array<KeyringJson> {
return Object
.values(accountsObservable.subject.getValue())
.map(({ json }) => json);
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: MessageSeedCreate): MessageSeedCreate$Response {
const seed = mnemonicGenerate(length);
return {
address: keyring.createFromUri(seed, {}, type).address(),
seed
};
}
private seedValidate ({ seed, type }: MessageSeedValidate): MessageSeedValidate$Response {
assert(SEED_LENGTHS.includes(seed.split(' ').length), `Mnemonic needs to contain ${SEED_LENGTHS.join(', ')} words`);
assert(mnemonicValidate(seed), 'Not a valid mnemonic seed');
return {
address: keyring.createFromUri(seed, {}, type).address(),
seed
};
}
private signingApprove ({ id, password }: MessageExtrinsicSignApprove): boolean {
const queued = this.signing.get(id);
assert(queued, 'Unable to find request');
const { request: { address, blockHash, method, nonce }, resolve, reject } = queued;
const pair = keyring.getPair(address);
if (!pair) {
reject(new Error('Unable to find pair'));
return false;
}
pair.decodePkcs8(password);
const payload = new RawPayload({ blockHash, method, nonce });
const signature = u8aToHex(payload.sign(pair));
pair.lock();
resolve({
id,
signature
});
return true;
}
private signingCancel ({ id }: MessageExtrinsicSignCancel): boolean {
const queued = this.signing.get(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Cancelled'));
return true;
}
private signingRequests (): Array<[number, MessageExtrinsicSign, string]> {
return this.signing.requests;
}
async handle (type: MessageTypes, request: any): Promise<any> {
switch (type) {
case 'accounts.create':
return this.accountsCreate(request);
case 'accounts.list':
return this.accountsList();
case 'seed.create':
return this.seedCreate(request);
case 'seed.validate':
return this.seedValidate(request);
case 'signing.approve':
return this.signingApprove(request);
case 'signing.cancel':
return this.signingCancel(request);
case 'signing.requests':
return this.signingRequests();
default:
throw new Error(`Unable to handle message of type ${type}`);
}
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign, MessageExtrinsicSign$Response } from '../types';
type Request = {
id: number,
request: MessageExtrinsicSign,
resolve: (result: MessageExtrinsicSign$Response) => void,
reject: (error: Error) => void,
url: string
};
export default class SigningRequests {
private _requests: { [index: number]: Request } = {};
get hasRequests (): boolean {
return this.numRequests === 0;
}
get numRequests (): number {
return Object.keys(this._requests).length;
}
get requests (): Array<[number, MessageExtrinsicSign, string]> {
return Object
.values(this._requests)
.map(({ id, request, url }) => [id, request, url]);
}
private complete = (id: number, fn: Function) => {
return (result: MessageExtrinsicSign$Response | Error): void => {
delete this._requests[id];
this.updateIcon();
fn(result);
};
}
private updateIcon (): void {
const count = this.numRequests;
chrome.browserAction.setBadgeText({ text: count ? `${this.numRequests}` : '' });
}
get (id: number): Request {
return this._requests[id];
}
queue (id: number, request: MessageExtrinsicSign, url: string): Promise<MessageExtrinsicSign$Response> {
return new Promise((resolve, reject) => {
this._requests[id] = {
id,
request,
resolve: this.complete(id, resolve),
reject: this.complete(id, reject),
url
};
this.updateIcon();
});
}
}

View File

@@ -0,0 +1,52 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageTypes, MessageExtrinsicSign, MessageExtrinsicSign$Response } from '../types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert } from '@polkadot/util';
import SigningRequests from './SigningRequests';
let signCounter = 0;
export default class Tabs {
private signing: SigningRequests;
constructor (signing: SigningRequests) {
this.signing = signing;
}
private accountsList (): Array<{ address: string, name?: string }> {
return Object
.values(accountsObservable.subject.getValue())
.map(({ json: { address, meta: { name } } }) => ({
address, name
}));
}
private extrinsicSign (request: MessageExtrinsicSign, url: string): Promise<MessageExtrinsicSign$Response> {
const { address } = request;
const id = ++signCounter;
const pair = keyring.getPair(address);
assert(pair, 'Unable to find keypair');
return this.signing.queue(id, request, url);
}
async handle (type: MessageTypes, request: any, url: string = 'unknown'): Promise<any> {
switch (type) {
case 'accounts.list':
return this.accountsList();
case 'extrinsic.sign':
return this.extrinsicSign(request, url);
default:
throw new Error(`Unable to handle message of type ${type}`);
}
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageRequest } from '../types';
import Extension from './Extension';
import SigningRequests from './SigningRequests';
import Tabs from './Tabs';
const signing = new SigningRequests();
const extension = new Extension(signing);
const tabs = new Tabs(signing);
export default function handler ({ id, message, request }: MessageRequest, { tab }: chrome.runtime.MessageSender, sendResponse: (response?: any) => void): boolean {
const source = `${tab ? tab.url : 'extension'}: ${id}: ${message}`;
console.log(` [in] ${source}`); // :: ${JSON.stringify(request)}`);
const promise = tab
? tabs.handle(message, request, tab.url)
: extension.handle(message, request);
promise
.then((response) => {
console.log(`[out] ${source}`); // :: ${JSON.stringify(response)}`);
sendResponse({ id, response });
})
.catch((error) => {
console.log(`[err] ${source}:: ${error.message}`);
sendResponse({ id, error: error.message });
});
// return true to indicate we are sending the respionse async
return true;
}

View File

@@ -0,0 +1,27 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
// Runs in the extension background, handling all keyring access
import keyring from '@polkadot/ui-keyring';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import ChromeStore from './ChromeStore';
import handlers from './handlers';
chrome.browserAction.setBadgeBackgroundColor({ color: '#ff0000' });
chrome.runtime.onMessage.addListener(handlers);
cryptoWaitReady()
.then(() => {
console.log('crypto initialized');
// load all the keyring data
keyring.loadAll({ store: new ChromeStore(), type: 'sr25519' });
console.log('initialization completed');
})
.catch((error) => {
console.error('initialization failed', error);
});

View File

@@ -0,0 +1,67 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '@polkadot/util-crypto/types';
export type MessageTypes = 'accounts.create' | 'accounts.list' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests';
export type MessageRequest = {
id: number,
message: MessageTypes,
request: any
};
export type MessageResponse = {
error?: string,
id: number,
response?: any
};
export type MessageAccountCreate = {
name: string,
password: string,
suri: string,
type?: KeypairType
};
export type MessageExtrinsicSignApprove = {
id: number,
password: string
};
export type MessageExtrinsicSignCancel = {
id: number
};
export type MessageExtrinsicSign = {
address: string,
blockHash: string,
method: string,
nonce: string
};
export type MessageExtrinsicSign$Response = {
id: number,
signature: string
};
export type MessageSeedCreate$Response = {
address: string,
seed: string
};
export type MessageSeedCreate = {
length?: 12 | 24,
type?: KeypairType
};
export type MessageSeedValidate = {
seed: string,
type?: KeypairType
};
export type MessageSeedValidate$Response = {
address: string,
seed: string
};

View File

@@ -0,0 +1,10 @@
// Copyright 2019 @polkadot/extension-bg authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const events = {
request: 'PolkadotJsWebRequest',
response: 'PolkadotJsWebResponse'
};
export default events;

View File

@@ -0,0 +1,17 @@
// Copyright 2019 @polkadot/extension-inject authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Accounts as IAccounts, Account as IAccount, SendRequest } from './types';
let sendRequest: SendRequest;
export default class Accounts implements IAccounts {
constructor (_sendRequest: SendRequest) {
sendRequest = _sendRequest;
}
get (): Promise<Array<IAccount>> {
return sendRequest('accounts.list');
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2019 @polkadot/extension-inject authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Injected as IInjected, SendRequest, Version } from './types';
import Accounts from './Accounts';
import Signer from './Signer';
export default class Injected implements IInjected {
readonly accounts: Accounts;
readonly signer: Signer;
constructor (sendRequest: SendRequest) {
this.accounts = new Accounts(sendRequest);
this.signer = new Signer(sendRequest);
}
get version (): Version {
return 0;
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2019 @polkadot/extension-inject authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SignatureOptions } from '@polkadot/types/types';
import { Signer as ISigner, SendRequest } from './types';
import { Extrinsic, Hash } from '@polkadot/types';
import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
let sendRequest: SendRequest;
export default class Signer implements ISigner {
constructor (_sendRequest: SendRequest) {
sendRequest = _sendRequest;
}
async sign (extrinsic: Extrinsic, address: string, { blockHash, nonce }: SignatureOptions): Promise<number> {
// Bit of a hack - with this round-about way, we skip any keyring deps
const cleaned = JSON.parse(JSON.stringify({
address,
blockHash,
method: extrinsic.method.toHex(),
nonce
}));
const { id, signature } = await sendRequest('extrinsic.sign', cleaned);
extrinsic.addSignature(address as any, signature, nonce);
return id;
}
update (id: number, status: Hash | SubmittableResult): void {
// something
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { WindowInjected } from './types';
import events from '../events';
import Injected from './Injected';
const callbacks: { [index: number]: { resolve: (data: any) => void, reject: (error: Error) => void } } = {};
let idCounter = 0;
// setup a response listener (events created by the loader for extension responses)
document.addEventListener(events.response, (event) => {
const response = (event as CustomEvent).detail;
const promise = callbacks[response.id];
if (!promise) {
console.error(`Uknown response: ${JSON.stringify(response)}`);
return;
}
delete callbacks[response.id];
if (response.error) {
promise.reject(new Error(response.error));
} else {
promise.resolve(response.response);
}
});
(window as WindowInjected).injectedWeb3 = (window as WindowInjected).injectedWeb3 || {};
(window as WindowInjected).injectedWeb3['polkadot-js'] = new Injected(
(message: string, request: any = null): Promise<any> => {
return new Promise((resolve, reject) => {
const id = ++idCounter;
callbacks[id] = { resolve, reject };
document.dispatchEvent(
new CustomEvent(events.request, { detail: { id, message, request } })
);
});
}
);

View File

@@ -0,0 +1,36 @@
// Copyright 2019 @polkadot/extension-inject authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Signer as SignerInterface } from '@polkadot/api/types';
export type Version = 0;
export interface Account {
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
}
export interface Accounts {
get: () => Promise<Array<Account>>;
}
export interface Signer extends SignerInterface {
// no specific signer extensions
}
export interface Injected {
readonly accounts: Accounts;
readonly signer: Signer;
readonly version: Version;
}
export interface SendRequest {
(message: string, request?: any): Promise<any>;
}
export type WindowInjected = Window & {
injectedWeb3: {
[index: string]: Injected
}
};

View File

@@ -0,0 +1,39 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import events from './events';
function sendResponse (detail: any): void {
document.dispatchEvent(new CustomEvent(events.response, { detail }));
}
// Handle all custom events, passing messages to the extension
document.addEventListener(events.request, (event) => {
// get the request as part of the the event detailt
const request = (event as CustomEvent).detail;
// pass the detail as-is as a message to the extension
chrome.runtime.sendMessage(request, (response) => {
if (!response) {
// no data, send diconnected/unknown error
sendResponse({ id: request.id, error: 'Unknown response' });
} else {
// handle the response, passing as a custom event
sendResponse(response);
}
});
});
// inject our data injector
const script = document.createElement('script');
script.src = chrome.extension.getURL('inject.js');
script.onload = (): void => {
// remove the injecting tag when loaded
if (script.parentNode) {
script.parentNode.removeChild(script);
}
};
(document.head || document.documentElement).appendChild(script);

View File

@@ -0,0 +1,7 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { createView, Popup } from '@polkadot/extension-ui';
createView(Popup);

View File

@@ -0,0 +1,110 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { WebpackPluginServe } = require('webpack-plugin-serve');
const packages = [
'extension-ui'
];
const DEFAULT_THEME = 'polkadot';
function createWebpack ({ alias = {}, context }) {
const pkgJson = require(path.join(context, 'package.json'));
const ENV = process.env.NODE_ENV || 'development';
const isProd = ENV === 'production';
return {
context,
devtool: 'source-map',
entry: {
background: './src/background/index.ts',
inject: './src/inject/index.ts',
loader: './src/loader.ts',
popup: `./src/views/popup.ts`
},
mode: ENV,
output: {
chunkFilename: `[name].js`,
filename: `[name].js`,
globalObject: `(typeof self !== 'undefined' ? self : this)`,
path: path.join(context, 'build')
},
resolve: {
alias,
extensions: ['.js', '.jsx', '.ts', '.tsx']
},
module: {
rules: [
{
test: /\.(js|ts|tsx)$/,
exclude: /(node_modules)/,
use: [
require.resolve('thread-loader'),
{
loader: require.resolve('babel-loader'),
options: require('@polkadot/dev-react/config/babel')
}
]
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
use: [
{
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/[name].[ext]'
}
}
]
}
]
},
node: {
child_process: 'empty',
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty'
},
performance: {
hints: false
},
plugins: [
isProd
? null
: new WebpackPluginServe({
port: 3001,
static: path.join(process.cwd(), '/build')
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(ENV),
VERSION: JSON.stringify(pkgJson.version),
UI_MODE: JSON.stringify(process.env.UI_MODE || 'full'),
UI_THEME: JSON.stringify(process.env.UI_THEME || DEFAULT_THEME),
WS_URL: JSON.stringify(process.env.WS_URL)
}
}),
new CopyWebpackPlugin([{ from: 'public' }])
].filter((entry) => entry),
watch: !isProd
};
}
module.exports = createWebpack({
context: __dirname,
alias: packages.reduce((alias, pkg) => {
alias[`@polkadot/${pkg}`] = path.resolve(__dirname, `../${pkg}/src`);
return alias;
}, {})
});

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "./node_modules/@polkadot/dev/config/tsconfig",
"compilerOptions":
{
"baseUrl": ".",
"paths":
{
"@polkadot/extension": ["packages/extension/src"],
"@polkadot/extension/*": ["packages/extension/src/*"],
"@polkadot/extension-ui": ["packages/extension-ui/src"],
"@polkadot/extension-ui/*": ["packages/extension-ui/src/*"]
},
"typeRoots": [
"./node_modules/@polkadot/ts",
"./node_modules/@types"
]
}
}

9
tslint.json Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "@polkadot/dev/config/tslint",
"linterOptions": {
"exclude": "src/**/*.json"
},
"rules": {
"completed-docs": false
}
}

14636
yarn.lock Normal file

File diff suppressed because it is too large Load Diff