Toy vanity address generator (#31)

* Basic vanity address generator

* Node operations moved to cli completely

* Cleanups

* cli progress
This commit is contained in:
Jaco Greeff 2018-05-09 12:39:55 +02:00 committed by GitHub
parent 45937acddd
commit 4cec0ec5ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 680 additions and 21 deletions

View File

@ -10,6 +10,7 @@
"build:apps": "cd packages/apps && react-scripts build",
"check": "eslint packages && flow check",
"deploy:ghpages": "gh-pages -d packages/apps/build",
"generator": "node packages/app-vanitygen/build/generator/cli.js",
"start": "cd packages/apps && react-scripts start",
"test": "echo 'no tests executed'"
},

View File

@ -50,6 +50,8 @@ function addressFromSeed (seed: string): string {
}
class Creator extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);

View File

@ -30,6 +30,8 @@ type State = {
}
class Editor extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);

View File

@ -0,0 +1,15 @@
ISC License (ISC)
Copyright 2017-2018 Jaco Greeff <jacogr@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1 @@
# @polkadot/app-vanitygen

View File

@ -0,0 +1,18 @@
{
"name": "@polkadot/app-vanitygen",
"version": "0.2.1",
"main": "index.js",
"repository": "https://github.com/polkadot-js/apps.git",
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "ISC",
"scripts": {
"build": "polkadot-dev-build-babel",
"check": "stylelint 'src/**/*.css' && eslint src && flow check",
"test": "jest --coverage"
},
"devDependencies": {
"@polkadot/ui-react-app": "^0.2.1",
"chalk": "^2.4.1",
"yargs": "10.1.2"
}
}

View File

@ -0,0 +1,51 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { BareProps } from '@polkadot/ui-react-app/types';
import React from 'react';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import u8aToHex from '@polkadot/util/u8a/toHex';
type Props = BareProps & {
address: string;
count: number;
offset: number;
onRemove: (address: string) => void,
seed: Uint8Array;
}
export default function Match ({ address, className, count, offset, onRemove, seed, style }: Props): React$Node {
const _onRemove = (): void =>
onRemove(address);
return (
<div
className={['vanity--Match', className].join(' ')}
style={style}
>
<div className='vanity--Match-item'>
<IdentityIcon
className='vanity--Match-icon'
size={48}
value={address}
/>
<div className='vanity--Match-addr'>
<span className='no'>{address.slice(0, offset)}</span><span className='yes'>{address.slice(offset, count + offset)}</span><span className='no'>{address.slice(count + offset)}</span>
</div>
<div className='vanity--Match-seed'>
{u8aToHex(seed)}
</div>
<Button
className='vanity--Match-remove'
icon='close'
negative
onClick={_onRemove}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,47 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Generator$Calculation, Generator$Options } from './types';
module.exports = function calculate (test: Array<string>, _address: string, { atOffset = -1, withCase = false }: Generator$Options): Generator$Calculation {
const address = withCase
? _address
: _address.toLowerCase();
const calculateAt = (atOffset: number): Generator$Calculation => {
return {
count: test.reduce((count, c, index) => {
if (index === count) {
count += (c === '?' || c === address.charAt(index + atOffset)) ? 1 : 0;
}
return count;
}, 0),
offset: atOffset
};
};
if (atOffset > 0) {
return calculateAt(atOffset);
}
const maxOffset = address.length - test.length - 1;
let bestCount = 0;
let bestOffset = 1;
for (let index = 1; index < maxOffset; index++) {
const { count, offset } = calculateAt(index);
if (count > bestCount) {
bestCount = count;
bestOffset = offset;
}
}
return {
count: bestCount,
offset: bestOffset
};
};

View File

@ -0,0 +1,65 @@
#!/usr/bin/env node
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
const yargs = require('yargs');
const chalk = require('chalk');
const u8aToHex = require('@polkadot/util/u8a/toHex');
const generator = require('./index.js');
const { match } = yargs
.option('match', {
alias: 'm',
default: 'EEEEE'
})
.argv;
const INDICATORS = ['|', '/', '-', '\\'];
const options = {
match,
runs: 100
};
const startAt = Date.now();
let best = { count: -1 };
let total = 0;
let indicator = -1;
function showProgress () {
const elapsed = Date.now() - startAt;
indicator++;
if (indicator === INDICATORS.length) {
indicator = 0;
}
process.stdout.write(`\r[${INDICATORS[indicator]}] ${total} keys in ${(elapsed / 1000).toFixed(2)}s, ${(elapsed / total).toFixed(3)}ms/key`);
}
function showBest () {
const { address, count, offset, seed } = best;
console.log(`\r::: ${address.slice(0, offset)}${chalk.cyan(address.slice(offset, count + offset))}${address.slice(count + offset)} <= ${u8aToHex(seed)} (count=${count}, offset=${offset})`);
}
while (true) {
const nextBest = generator(options).found.reduce((best, match) => {
if ((match.count > best.count) || ((match.count === best.count) && (match.offset < best.offset))) {
return match;
}
return best;
}, best);
total += options.runs;
if (nextBest.address !== best.address) {
best = nextBest;
showBest();
}
showProgress();
}

View File

@ -0,0 +1,27 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Generator$Match, Generator$Options } from './types';
const keypairFromSeed = require('@polkadot/util-crypto/nacl/keypair/fromSeed');
const randomBytes = require('@polkadot/util-crypto/random/asU8a');
const addressEncode = require('@polkadot/util-keyring/address/encode');
const calculate = require('./calculate');
module.exports = function generator (test: Array<string>, options: Generator$Options): Generator$Match {
const seed = randomBytes();
const address = addressEncode(
keypairFromSeed(seed).publicKey
);
const { count, offset } = calculate(test, address, options);
return {
address,
count,
offset,
seed
};
};

View File

@ -0,0 +1,26 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Generator$Matches, Generator$Result, Generator$Options } from './types';
const generate = require('./generate');
module.exports = function generator (options: Generator$Options): Generator$Result {
const { match, runs = 10, withCase = false } = options;
const test = withCase
? match.split('')
: match.toLowerCase().split('');
const startAt = Date.now();
const found: Generator$Matches = [];
while (found.length !== runs) {
found.push(generate(test, options));
}
return {
elapsed: Date.now() - startAt,
found
};
};

View File

@ -0,0 +1,32 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Generator$Match } from './types';
function numberSort (a: number, b: number): number {
if (a > b) {
return -1;
} else if (a < b) {
return 1;
}
return 0;
}
module.exports = function sort (a: Generator$Match, b: Generator$Match): number {
const countResult = numberSort(a.count, b.count);
if (countResult !== 0) {
return countResult;
}
const positionResult = numberSort(b.offset, a.offset);
if (positionResult !== 0) {
return positionResult;
}
return a.address.localeCompare(b.address);
};

View File

@ -0,0 +1,28 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
export type Generator$Calculation = {
count: number,
offset: number
};
export type Generator$Match = Generator$Calculation & {
address: string,
seed: Uint8Array
};
export type Generator$Matches = Array<Generator$Match>;
export type Generator$Options = {
atOffset?: number,
match: string,
runs?: number,
withCase?: boolean
};
export type Generator$Result = {
elapsed: number,
found: Generator$Matches
}

View File

@ -0,0 +1,65 @@
/* Copyright 2017-2018 Jaco Greeff
/* This software may be modified and distributed under the terms
/* of the ISC license. See the LICENSE file for details. */
.vanity--App {
padding: 1em 2em;
}
.vanity--App-matches {
padding: 1em 0;
}
.vanity--App-stats {
padding: 1em 0 0 0;
opacity: 0.45;
text-align: center;
}
.vanity--Match {
text-align: center;
}
.vanity--Match:hover {
background: #f9f9f9;
}
.vanity--Match-item {
display: inline-block;
font-family: monospace;
margin: 0.5em auto;
padding: 0.5em;
position: relative;
}
.vanity--Match-icon {
position: absolute;
top: 0;
left: 1rem;
}
.vanity--Match-addr,
.vanity--Match-seed {
padding: 0 5rem;
}
.vanity--Match-addr {
font-size: 1.5rem;
}
.vanity--Match-addr .no {
}
.vanity--Match-addr .yes {
color: red;
}
.vanity--Match-remove {
position: absolute;
top: 0;
right: 1rem;
}
.vanity--Match-seed {
opacity: 0.45;
}

View File

@ -0,0 +1,237 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { I18nProps } from '@polkadot/ui-react-app/types';
import type { Generator$Matches, Generator$Result } from './generator/types';
import './index.css';
import React from 'react';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import Input from 'semantic-ui-react/dist/es/elements/Input';
import Label from 'semantic-ui-react/dist/es/elements/Label';
import Match from './Match';
import generator from './generator';
import generatorSort from './generator/sort';
import translate from './translate';
type Props = I18nProps & {};
type State = {
elapsed: number,
isMatchValid: boolean,
isRunning: boolean,
keyCount: 0,
keyTime: 0,
match: string,
matches: Generator$Matches,
startAt: number
}
const DEFAULT_MATCH = 'S?me';
const MATCH_REGEX = /[1-9A-Za-z?]*$/;
class App extends React.PureComponent<Props, State> {
results: Array<Generator$Result>;
state: State;
constructor (props: Props) {
super(props);
this.results = [];
this.state = {
elapsed: 0,
isMatchValid: true,
isRunning: false,
keyCount: 0,
keyTime: 0,
match: DEFAULT_MATCH,
matches: [],
startAt: 0
};
}
render (): React$Node {
const { className, style, t } = this.props;
const { elapsed, isMatchValid, isRunning, match, matches, keyCount } = this.state;
return (
<div
className={['vanity--App', className].join(' ')}
style={style}
>
<div className='ui--form'>
<div className='medium'>
<Label>{t('vanity.matching', {
defaultValue: 'generate address containing characters'
})}</Label>
<Input
disabled={isRunning}
error={!isMatchValid}
onChange={this.onChangeMatch}
value={match}
/>
</div>
<div className='small'>
<Label>{t('vanity.case', {
defaultValue: 'case sensitivity'
})}</Label>
<div className='ui dropdown selection disabled'>
{t('vanity.case.off', {
defaultValue: 'No'
})}
</div>
</div>
<div className='small'>
<Label>{t('vanity.offset', {
defaultValue: 'exact offset'
})}</Label>
<div className='ui dropdown selection disabled'>
{t('vanity.offset.off', {
defaultValue: 'No'
})}
</div>
</div>
</div>
<div className='ui--form-buttons'>
<Button
disabled={!isMatchValid}
negative={isRunning}
primary
onClick={this.toggleStart}
>
{
isRunning
? t('vanity.stop', { defaultValue: 'Stop' })
: t('vanity.start', { defaultValue: 'Start' })
}
</Button>
</div>
{
keyCount
? (
<div className='vanity--App-stats'>
{t('vanity.stats', {
defaultValue: 'Generated {{count}} keys in {{elapsed}}s ({{avg}}ms/key)',
replace: {
avg: (elapsed / keyCount).toFixed(3),
count: keyCount,
elapsed: (elapsed / 1000).toFixed(2)
}
})}
</div>
)
: null
}
<div className='vanity--App-matches'>
{matches.map((match) => (
<Match
{...match}
key={match.address}
onRemove={this.onRemove}
/>
))}
</div>
</div>
);
}
checkMatches (): void {
const results = this.results;
this.results = [];
if (results.length === 0) {
return;
}
this.setState(
(prevState: State) => {
let newKeyCount = prevState.keyCount;
let newKeyTime = prevState.keyTime;
const matches = results
.reduce((result, { elapsed, found }) => {
newKeyCount += found.length;
newKeyTime += elapsed;
return result.concat(found);
}, prevState.matches)
.sort(generatorSort)
.slice(0, 25);
const elapsed = Date.now() - prevState.startAt;
return {
elapsed,
matches,
keyCount: newKeyCount,
keyTime: newKeyTime
};
}
);
}
executeGeneration = (): void => {
if (!this.state.isRunning) {
this.checkMatches();
return;
}
setTimeout(() => {
if (this.results.length === 5) {
this.checkMatches();
}
this.results.push(
generator({
match: this.state.match,
runs: 10
})
);
this.executeGeneration();
}, 0);
}
// eslint-disable-next-line no-unused-vars
onChangeMatch = (event: SyntheticEvent<*>, { value }): void => {
const isMatchValid = MATCH_REGEX.test(value) && value.length < 31;
this.setState({
isMatchValid,
match: value
});
}
onRemove = (address: string): void => {
this.setState(
(prevState: State) => ({
matches: prevState.matches.filter((item) =>
item.address !== address
)
})
);
}
toggleStart = (): void => {
this.setState(
(prevState: State) => {
const { isRunning, keyCount, keyTime, startAt } = prevState;
return {
isRunning: !isRunning,
keyCount: !isRunning ? 0 : keyCount,
keyTime: !isRunning ? 0 : keyTime,
startAt: !isRunning ? Date.now() : startAt
};
},
this.executeGeneration
);
}
}
export default translate(App);

View File

@ -0,0 +1,8 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import { translate } from 'react-i18next';
export default translate(['vanitygen', 'ui']);

View File

@ -29,7 +29,7 @@ function SideBar ({ className, style, t }: Props): React$Node {
vertical
>
{
routing.routes.map((route) => (
routing.routes.map((route, index) => (
route
? (
<Item
@ -39,7 +39,12 @@ function SideBar ({ className, style, t }: Props): React$Node {
{...route}
/>
)
: <Divider hidden />
: (
<Divider
hidden
key={index}
/>
)
))
}
<Divider hidden />

View File

@ -12,11 +12,11 @@ import extrinsics from './extrinsics';
// import home from './home';
// import settings from './settings';
import storage from './storage';
// import vanitygen from './vanitygen';
import vanitygen from './vanitygen';
export default ({
default: 'explorer',
routes: [].concat(
explorer, extrinsics, storage, null, accounts
explorer, extrinsics, storage, null, accounts, vanitygen
)
}: Routing);

View File

@ -0,0 +1,21 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Routes } from '../types';
import Vanity from '@polkadot/app-vanitygen/src';
export default ([
{
component: Vanity,
i18n: {
defaultValue: 'Vanity'
},
icon: 'hand lizard',
isExact: false,
isHidden: false,
name: 'vanitygen'
}
]: Routes);

View File

@ -2426,6 +2426,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
character-entities-html4@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50"
@ -10789,6 +10797,23 @@ yargs-parser@^9.0.2:
dependencies:
camelcase "^4.1.0"
yargs@10.1.2, yargs@^10.0.3:
version "10.1.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"
dependencies:
cliui "^4.0.0"
decamelize "^1.1.1"
find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
yargs-parser "^8.1.0"
yargs@6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
@ -10807,23 +10832,6 @@ yargs@6.6.0:
y18n "^3.2.1"
yargs-parser "^4.2.0"
yargs@^10.0.3:
version "10.1.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"
dependencies:
cliui "^4.0.0"
decamelize "^1.1.1"
find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
yargs-parser "^8.1.0"
yargs@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"