Basic QR tests (+ number encoding fix) (#168)

* Basic QR tests (+ number encoding fix)

* skipEncoding for Address display

* Fixup comments
This commit is contained in:
Jaco Greeff
2019-07-29 17:40:08 +02:00
committed by GitHub
parent 0ad431ba87
commit f7bd11a293
8 changed files with 148 additions and 69 deletions
+2 -1
View File
@@ -19,7 +19,8 @@
"clean": "polkadot-dev-clean-build", "clean": "polkadot-dev-clean-build",
"demo:identicon": "webpack-serve --config packages/ui-identicon/webpack.config.js --content packages/ui-identicon --port 3000", "demo:identicon": "webpack-serve --config packages/ui-identicon/webpack.config.js --content packages/ui-identicon --port 3000",
"postinstall": "polkadot-dev-yarn-only", "postinstall": "polkadot-dev-yarn-only",
"test": "jest --coverage" "test": "jest --coverage",
"test:one": "jest"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
+7 -31
View File
@@ -7,16 +7,14 @@ import { BaseProps } from './types';
import React from 'react'; import React from 'react';
import qrcode from 'qrcode-generator'; import qrcode from 'qrcode-generator';
import styled from 'styled-components'; import styled from 'styled-components';
import { u8aConcat } from '@polkadot/util';
import { xxhashAsHex } from '@polkadot/util-crypto'; import { xxhashAsHex } from '@polkadot/util-crypto';
import { createSize } from './constants'; import { createFrames, createImgSize, decodeString } from './util';
import { encodeNumber, decodeString } from './util';
interface Props extends BaseProps { interface Props extends BaseProps {
size?: number; size?: number;
skipEncoding?: boolean;
value: Uint8Array; value: Uint8Array;
withMulti?: boolean;
} }
interface State { interface State {
@@ -28,8 +26,6 @@ interface State {
} }
const FRAME_DELAY = 2100; const FRAME_DELAY = 2100;
const FRAME_SIZE = 1716;
const MULTIPART = new Uint8Array([0]);
function getDataUrl (value: string): string { function getDataUrl (value: string): string {
const qr = qrcode(0, 'M'); const qr = qrcode(0, 'M');
@@ -40,26 +36,6 @@ function getDataUrl (value: string): string {
return qr.createDataURL(16, 0); return qr.createDataURL(16, 0);
} }
function createFrames (input: Uint8Array): string[] {
const frames = [];
let idx = 0;
while (idx < input.length) {
frames.push(input.subarray(idx, idx + FRAME_SIZE));
idx += FRAME_SIZE;
}
return frames.map((frame, index: number): string =>
decodeString(u8aConcat(
MULTIPART,
encodeNumber(frames.length),
encodeNumber(index),
frame
))
);
}
class Display extends React.PureComponent<Props, State> { class Display extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
frames: [], frames: [],
@@ -69,16 +45,16 @@ class Display extends React.PureComponent<Props, State> {
valueHash: null valueHash: null
}; };
public static getDerivedStateFromProps ({ value, withMulti = true }: Props, prevState: State): Pick<State, never> | null { public static getDerivedStateFromProps ({ value, skipEncoding = false }: Props, prevState: State): Pick<State, never> | null {
const valueHash = xxhashAsHex(value); const valueHash = xxhashAsHex(value);
if (valueHash === prevState.valueHash) { if (valueHash === prevState.valueHash) {
return null; return null;
} }
const frames: string[] = withMulti const frames: string[] = skipEncoding
? createFrames(value) ? [decodeString(value)]
: [decodeString(value)]; : createFrames(value);
return { return {
frames, frames,
@@ -113,7 +89,7 @@ class Display extends React.PureComponent<Props, State> {
return ( return (
<div <div
className={className} className={className}
style={createSize(size)} style={createImgSize(size)}
> >
<div <div
className='ui--qr-Display' className='ui--qr-Display'
+3 -9
View File
@@ -5,11 +5,9 @@
import { BaseProps } from './types'; import { BaseProps } from './types';
import React from 'react'; import React from 'react';
import { u8aConcat } from '@polkadot/util';
import { xxhashAsHex } from '@polkadot/util-crypto'; import { xxhashAsHex } from '@polkadot/util-crypto';
import { ADDRESS_PREFIX } from './constants'; import { createAddressPayload } from './util';
import { encodeString } from './util';
import QrDisplay from './Display'; import QrDisplay from './Display';
interface Props extends BaseProps { interface Props extends BaseProps {
@@ -21,8 +19,6 @@ interface State {
dataHash: string | null; dataHash: string | null;
} }
const PREFIX = encodeString(ADDRESS_PREFIX);
export default class DisplayExtrinsic extends React.PureComponent<Props, State> { export default class DisplayExtrinsic extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
data: null, data: null,
@@ -30,10 +26,7 @@ export default class DisplayExtrinsic extends React.PureComponent<Props, State>
}; };
public static getDerivedStateFromProps ({ address }: Props, prevState: State): State | null { public static getDerivedStateFromProps ({ address }: Props, prevState: State): State | null {
const data = u8aConcat( const data = createAddressPayload(address);
PREFIX,
encodeString(address)
);
const dataHash = xxhashAsHex(data); const dataHash = xxhashAsHex(data);
if (dataHash === prevState.dataHash) { if (dataHash === prevState.dataHash) {
@@ -54,6 +47,7 @@ export default class DisplayExtrinsic extends React.PureComponent<Props, State>
return ( return (
<QrDisplay <QrDisplay
className={className} className={className}
skipEncoding={true}
style={style} style={style}
value={data} value={data}
/> />
+3 -13
View File
@@ -5,9 +5,9 @@
import { BaseProps } from './types'; import { BaseProps } from './types';
import React from 'react'; import React from 'react';
import { u8aConcat } from '@polkadot/util'; import { xxhashAsHex } from '@polkadot/util-crypto';
import { decodeAddress, xxhashAsHex } from '@polkadot/util-crypto';
import { createSignPayload } from './util';
import QrDisplay from './Display'; import QrDisplay from './Display';
interface Props extends BaseProps { interface Props extends BaseProps {
@@ -20,10 +20,6 @@ interface State {
dataHash: string | null; dataHash: string | null;
} }
const SUBSTRATE = new Uint8Array([0x53]);
const CRYPTO_SR25519 = new Uint8Array([0x01]);
const SIGN_TX = new Uint8Array([0x00]);
export default class DisplayPayload extends React.PureComponent<Props, State> { export default class DisplayPayload extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
data: null, data: null,
@@ -31,13 +27,7 @@ export default class DisplayPayload extends React.PureComponent<Props, State> {
}; };
public static getDerivedStateFromProps ({ address, payload }: Props, prevState: State): State | null { public static getDerivedStateFromProps ({ address, payload }: Props, prevState: State): State | null {
const data = u8aConcat( const data = createSignPayload(address, payload);
SUBSTRATE,
CRYPTO_SR25519,
SIGN_TX,
decodeAddress(address),
payload
);
const dataHash = xxhashAsHex(data); const dataHash = xxhashAsHex(data);
if (dataHash === prevState.dataHash) { if (dataHash === prevState.dataHash) {
+2 -2
View File
@@ -8,7 +8,7 @@ import React from 'react';
import Reader from 'react-qr-reader'; import Reader from 'react-qr-reader';
import styled from 'styled-components'; import styled from 'styled-components';
import { createSize } from './constants'; import { createImgSize } from './util';
interface Props extends BaseProps { interface Props extends BaseProps {
delay?: number; delay?: number;
@@ -29,7 +29,7 @@ class Scan extends React.PureComponent<Props> {
return ( return (
<div <div
className={className} className={className}
style={createSize(size)} style={createImgSize(size)}
> >
<Reader <Reader
className='ui--qr-Scan' className='ui--qr-Scan'
+10 -12
View File
@@ -2,20 +2,18 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
const DEFAULT_SIZE = 300; const DEFAULT_IMG_SIZE = 300;
const ADDRESS_PREFIX = 'substrate:'; const ADDRESS_PREFIX = 'substrate:';
const FRAME_SIZE = 1716;
function createSize (size: number = DEFAULT_SIZE): Record<string, string> { const SUBSTRATE_ID = new Uint8Array([0x53]);
const height = `${size}px`; const CRYPTO_SR25519 = new Uint8Array([0x01]);
const CMD_SIGN_TX = new Uint8Array([0x00]);
return {
height,
width: height
};
}
export { export {
ADDRESS_PREFIX, ADDRESS_PREFIX,
DEFAULT_SIZE, CMD_SIGN_TX,
createSize CRYPTO_SR25519,
DEFAULT_IMG_SIZE,
FRAME_SIZE,
SUBSTRATE_ID
}; };
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2017-2019 @polkadot/react-qr 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 { u8aToHex, u8aToString } from '@polkadot/util';
import { createAddressPayload, createSignPayload, createFrames, encodeNumber, encodeString } from './util';
describe('util', (): void => {
describe('encodeNumber', (): void => {
it('encodes 1 correctly', (): void => {
expect(
encodeNumber(1)
).toEqual(new Uint8Array([0, 1]));
});
it('encodes 257 correctly', (): void => {
expect(
encodeNumber(257)
).toEqual(new Uint8Array([1, 1]));
});
});
describe('createAddressPayload', (): void => {
it('encodes an address properly', (): void => {
expect(
u8aToString(
createAddressPayload('5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX')
)
).toEqual('substrate:5GKhfyctwmW5LQdGaHTyU9qq2yDtggdJo719bj5ZUxnVGtmX');
});
});
describe('createSignPayload', (): void => {
it('encodes a payload properly', (): void => {
expect(
u8aToHex(
createSignPayload('5HbgaJEuVN5qGbkhgtuDQANivSWwHXWsC2erP1SQUXgciTVq', 'THIS IS SPARTA!')
)
).toEqual(
'0x' + // prefix
'53' + // substrate
'01' + // sr25519
'00' + // sign tx
'f4cd755672a8f9542ca9da4fbf2182e79135d94304002e6a09ffc96fef6e6c4c' + // publickey
'544849532049532053504152544121' // THIS IS SPARTA!
);
});
});
describe('createFrames', (): void => {
it('encodes frames properly', (): void => {
expect(
createFrames(
createSignPayload('5HbgaJEuVN5qGbkhgtuDQANivSWwHXWsC2erP1SQUXgciTVq', '0x12345678')
).map((str): string => u8aToHex(encodeString(str)))
).toEqual([
'0x' +
'00' + // multipart
'0001' + // length
'0000' + // index
'530100' + // payload info, substrate + sr25519 + signtx
'f4cd755672a8f9542ca9da4fbf2182e79135d94304002e6a09ffc96fef6e6c4c' + // publicKey
'12345678' // data
]);
});
});
});
+54 -1
View File
@@ -2,8 +2,15 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { u8aConcat, u8aToU8a } from '@polkadot/util';
import { decodeAddress } from '@polkadot/util-crypto';
import { ADDRESS_PREFIX, CMD_SIGN_TX, CRYPTO_SR25519, DEFAULT_IMG_SIZE, FRAME_SIZE, SUBSTRATE_ID } from './constants';
const MULTIPART = new Uint8Array([0]);
export function encodeNumber (value: number): Uint8Array { export function encodeNumber (value: number): Uint8Array {
return new Uint8Array([value >> 8, value & 256]); return new Uint8Array([value >> 8, value & 0xff]);
} }
export function encodeString (value: string): Uint8Array { export function encodeString (value: string): Uint8Array {
@@ -21,3 +28,49 @@ export function decodeString (value: Uint8Array): string {
return str + String.fromCharCode(code); return str + String.fromCharCode(code);
}, ''); }, '');
} }
export function createAddressPayload (address: string): Uint8Array {
return u8aConcat(
encodeString(ADDRESS_PREFIX),
encodeString(address)
);
}
export function createSignPayload (address: string, payload: string | Uint8Array): Uint8Array {
return u8aConcat(
SUBSTRATE_ID,
CRYPTO_SR25519,
CMD_SIGN_TX,
decodeAddress(address),
u8aToU8a(payload)
);
}
export function createFrames (input: Uint8Array): string[] {
const frames = [];
let idx = 0;
while (idx < input.length) {
frames.push(input.subarray(idx, idx + FRAME_SIZE));
idx += FRAME_SIZE;
}
return frames.map((frame, index: number): string =>
decodeString(u8aConcat(
MULTIPART,
encodeNumber(frames.length),
encodeNumber(index),
frame
))
);
}
export function createImgSize (size: number = DEFAULT_IMG_SIZE): Record<string, string> {
const height = `${size}px`;
return {
height,
width: height
};
}