diff --git a/package.json b/package.json index a12922e4..04f0969c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "clean": "polkadot-dev-clean-build", "demo:identicon": "webpack-serve --config packages/ui-identicon/webpack.config.js --content packages/ui-identicon --port 3000", "postinstall": "polkadot-dev-yarn-only", - "test": "jest --coverage" + "test": "jest --coverage", + "test:one": "jest" }, "devDependencies": { "@babel/core": "^7.5.5", diff --git a/packages/react-qr/src/Display.tsx b/packages/react-qr/src/Display.tsx index 9fadeeec..96f584f1 100644 --- a/packages/react-qr/src/Display.tsx +++ b/packages/react-qr/src/Display.tsx @@ -7,16 +7,14 @@ import { BaseProps } from './types'; import React from 'react'; import qrcode from 'qrcode-generator'; import styled from 'styled-components'; -import { u8aConcat } from '@polkadot/util'; import { xxhashAsHex } from '@polkadot/util-crypto'; -import { createSize } from './constants'; -import { encodeNumber, decodeString } from './util'; +import { createFrames, createImgSize, decodeString } from './util'; interface Props extends BaseProps { size?: number; + skipEncoding?: boolean; value: Uint8Array; - withMulti?: boolean; } interface State { @@ -28,8 +26,6 @@ interface State { } const FRAME_DELAY = 2100; -const FRAME_SIZE = 1716; -const MULTIPART = new Uint8Array([0]); function getDataUrl (value: string): string { const qr = qrcode(0, 'M'); @@ -40,26 +36,6 @@ function getDataUrl (value: string): string { 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 { public state: State = { frames: [], @@ -69,16 +45,16 @@ class Display extends React.PureComponent { valueHash: null }; - public static getDerivedStateFromProps ({ value, withMulti = true }: Props, prevState: State): Pick | null { + public static getDerivedStateFromProps ({ value, skipEncoding = false }: Props, prevState: State): Pick | null { const valueHash = xxhashAsHex(value); if (valueHash === prevState.valueHash) { return null; } - const frames: string[] = withMulti - ? createFrames(value) - : [decodeString(value)]; + const frames: string[] = skipEncoding + ? [decodeString(value)] + : createFrames(value); return { frames, @@ -113,7 +89,7 @@ class Display extends React.PureComponent { return (
{ public state: State = { data: null, @@ -30,10 +26,7 @@ export default class DisplayExtrinsic extends React.PureComponent }; public static getDerivedStateFromProps ({ address }: Props, prevState: State): State | null { - const data = u8aConcat( - PREFIX, - encodeString(address) - ); + const data = createAddressPayload(address); const dataHash = xxhashAsHex(data); if (dataHash === prevState.dataHash) { @@ -54,6 +47,7 @@ export default class DisplayExtrinsic extends React.PureComponent return ( diff --git a/packages/react-qr/src/DisplayPayload.tsx b/packages/react-qr/src/DisplayPayload.tsx index 613c9432..f7c7631d 100644 --- a/packages/react-qr/src/DisplayPayload.tsx +++ b/packages/react-qr/src/DisplayPayload.tsx @@ -5,9 +5,9 @@ import { BaseProps } from './types'; import React from 'react'; -import { u8aConcat } from '@polkadot/util'; -import { decodeAddress, xxhashAsHex } from '@polkadot/util-crypto'; +import { xxhashAsHex } from '@polkadot/util-crypto'; +import { createSignPayload } from './util'; import QrDisplay from './Display'; interface Props extends BaseProps { @@ -20,10 +20,6 @@ interface State { 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 { public state: State = { data: null, @@ -31,13 +27,7 @@ export default class DisplayPayload extends React.PureComponent { }; public static getDerivedStateFromProps ({ address, payload }: Props, prevState: State): State | null { - const data = u8aConcat( - SUBSTRATE, - CRYPTO_SR25519, - SIGN_TX, - decodeAddress(address), - payload - ); + const data = createSignPayload(address, payload); const dataHash = xxhashAsHex(data); if (dataHash === prevState.dataHash) { diff --git a/packages/react-qr/src/Scan.tsx b/packages/react-qr/src/Scan.tsx index cecb6d83..71e0da76 100644 --- a/packages/react-qr/src/Scan.tsx +++ b/packages/react-qr/src/Scan.tsx @@ -8,7 +8,7 @@ import React from 'react'; import Reader from 'react-qr-reader'; import styled from 'styled-components'; -import { createSize } from './constants'; +import { createImgSize } from './util'; interface Props extends BaseProps { delay?: number; @@ -29,7 +29,7 @@ class Scan extends React.PureComponent { return (
{ - const height = `${size}px`; - - return { - height, - width: height - }; -} +const FRAME_SIZE = 1716; +const SUBSTRATE_ID = new Uint8Array([0x53]); +const CRYPTO_SR25519 = new Uint8Array([0x01]); +const CMD_SIGN_TX = new Uint8Array([0x00]); export { ADDRESS_PREFIX, - DEFAULT_SIZE, - createSize + CMD_SIGN_TX, + CRYPTO_SR25519, + DEFAULT_IMG_SIZE, + FRAME_SIZE, + SUBSTRATE_ID }; diff --git a/packages/react-qr/src/util.spec.ts b/packages/react-qr/src/util.spec.ts new file mode 100644 index 00000000..be5aafb6 --- /dev/null +++ b/packages/react-qr/src/util.spec.ts @@ -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 + ]); + }); + }); +}); diff --git a/packages/react-qr/src/util.ts b/packages/react-qr/src/util.ts index 459b0976..72693731 100644 --- a/packages/react-qr/src/util.ts +++ b/packages/react-qr/src/util.ts @@ -2,8 +2,15 @@ // This software may be modified and distributed under the terms // 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 { - return new Uint8Array([value >> 8, value & 256]); + return new Uint8Array([value >> 8, value & 0xff]); } export function encodeString (value: string): Uint8Array { @@ -21,3 +28,49 @@ export function decodeString (value: Uint8Array): string { 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 { + const height = `${size}px`; + + return { + height, + width: height + }; +}