util.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. // Copyright 2017-2020 @polkadot/app-claims authors & contributors
  2. // This software may be modified and distributed under the terms
  3. // of the Apache-2.0 license. See the LICENSE file for details.
  4. import { EthereumAddress, EcdsaSignature, StatementKind } from '@polkadot/types/interfaces';
  5. import secp256k1 from 'secp256k1/elliptic';
  6. import { registry } from '@polkadot/react-api';
  7. import { assert, hexToU8a, stringToU8a, u8aToBuffer, u8aConcat } from '@polkadot/util';
  8. import { keccakAsHex, keccakAsU8a } from '@polkadot/util-crypto';
  9. interface RecoveredSignature {
  10. error: Error | null;
  11. ethereumAddress: EthereumAddress | null;
  12. signature: EcdsaSignature | null;
  13. }
  14. interface SignatureParts {
  15. recovery: number;
  16. signature: Buffer;
  17. }
  18. // converts an Ethereum address to a checksum representation
  19. export function addrToChecksum (_address: string): string {
  20. const address = _address.toLowerCase();
  21. const hash = keccakAsHex(address.substr(2)).substr(2);
  22. let result = '0x';
  23. for (let n = 0; n < 40; n++) {
  24. result = `${result}${
  25. parseInt(hash[n], 16) > 7
  26. ? address[n + 2].toUpperCase()
  27. : address[n + 2]
  28. }`;
  29. }
  30. return result;
  31. }
  32. // convert a give public key to an Ethereum address (the last 20 bytes of an _exapnded_ key keccack)
  33. export function publicToAddr (publicKey: Uint8Array): string {
  34. return addrToChecksum(`0x${keccakAsHex(publicKey).slice(-40)}`);
  35. }
  36. // hash a message for use in signature recovery, adding the standard Ethereum header
  37. export function hashMessage (message: string): Buffer {
  38. const expanded = stringToU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`);
  39. const hashed = keccakAsU8a(expanded);
  40. return u8aToBuffer(hashed);
  41. }
  42. // split is 65-byte signature into the r, s (combined) and recovery number (derived from v)
  43. export function sigToParts (_signature: string): SignatureParts {
  44. const signature = hexToU8a(_signature);
  45. assert(signature.length === 65, `Invalid signature length, expected 65 found ${signature.length}`);
  46. let v = signature[64];
  47. if (v < 27) {
  48. v += 27;
  49. }
  50. const recovery = v - 27;
  51. assert(recovery === 0 || recovery === 1, 'Invalid signature v value');
  52. return {
  53. recovery,
  54. signature: u8aToBuffer(signature.slice(0, 64))
  55. };
  56. }
  57. // recover an address from a given message and a recover/signature combination
  58. export function recoverAddress (message: string, { recovery, signature }: SignatureParts): string {
  59. const msgHash = hashMessage(message);
  60. const senderPubKey = secp256k1.recover(msgHash, signature, recovery);
  61. return publicToAddr(
  62. secp256k1.publicKeyConvert(senderPubKey, false).slice(1)
  63. );
  64. }
  65. // recover an address from a signature JSON (as supplied by e.g. MyCrypto)
  66. export function recoverFromJSON (signatureJson: string | null): RecoveredSignature {
  67. try {
  68. const { msg, sig } = JSON.parse(signatureJson || '{}') as Record<string, string>;
  69. if (!msg || !sig) {
  70. throw new Error('Invalid signature object');
  71. }
  72. const parts = sigToParts(sig);
  73. return {
  74. error: null,
  75. ethereumAddress: registry.createType('EthereumAddress', recoverAddress(msg, parts)),
  76. signature: registry.createType('EcdsaSignature', u8aConcat(parts.signature, new Uint8Array([parts.recovery])))
  77. };
  78. } catch (error) {
  79. console.error(error);
  80. return {
  81. error: error as Error,
  82. ethereumAddress: null,
  83. signature: null
  84. };
  85. }
  86. }
  87. export interface Statement {
  88. sentence: string;
  89. url: string;
  90. }
  91. function getPolkadot (kind?: StatementKind | null): Statement | undefined {
  92. if (!kind) {
  93. return undefined;
  94. }
  95. const url = kind.isRegular
  96. ? 'https://statement.polkadot.network/regular.html'
  97. : 'https://statement.polkadot.network/saft.html';
  98. const hash = kind.isRegular
  99. ? 'Qmc1XYqT6S39WNp2UeiRUrZichUWUPpGEThDE6dAb3f6Ny'
  100. : 'QmXEkMahfhHJPzT3RjkXiZVFi77ZeVeuxtAjhojGRNYckz';
  101. return {
  102. sentence: `I hereby agree to the terms of the statement whose SHA-256 multihash is ${hash}. (This may be found at the URL: ${url})`,
  103. url
  104. };
  105. }
  106. export function getStatement (network: string, kind?: StatementKind | null): Statement | undefined {
  107. switch (network) {
  108. case 'Polkadot':
  109. case 'Polkadot CC1':
  110. return getPolkadot(kind);
  111. default:
  112. return undefined;
  113. }
  114. }