identities.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict';
  19. const path = require('path');
  20. const fs = require('fs');
  21. // const readline = require('readline');
  22. const debug = require('debug')('joystream:runtime:identities');
  23. const { Keyring } = require('@polkadot/keyring');
  24. // const { Null } = require('@polkadot/types/primitive');
  25. const util_crypto = require('@polkadot/util-crypto');
  26. // const { _ } = require('lodash');
  27. /*
  28. * Add identity management to the substrate API.
  29. *
  30. * This loosely groups: accounts, key management, and membership.
  31. */
  32. class IdentitiesApi
  33. {
  34. static async create(base, {account_file, passphrase, canPromptForPassphrase})
  35. {
  36. const ret = new IdentitiesApi();
  37. ret.base = base;
  38. await ret.init(account_file, passphrase, canPromptForPassphrase);
  39. return ret;
  40. }
  41. async init(account_file, passphrase, canPromptForPassphrase)
  42. {
  43. debug('Init');
  44. // Creatre keyring
  45. this.keyring = new Keyring();
  46. this.canPromptForPassphrase = canPromptForPassphrase || false;
  47. // Load account file, if possible.
  48. try {
  49. this.key = await this.loadUnlock(account_file, passphrase);
  50. } catch (err) {
  51. debug('Error loading account file:', err.message);
  52. }
  53. }
  54. /*
  55. * Load a key file and unlock it if necessary.
  56. */
  57. async loadUnlock(account_file, passphrase)
  58. {
  59. const fullname = path.resolve(account_file);
  60. debug('Initializing key from', fullname);
  61. const key = this.keyring.addFromJson(require(fullname));
  62. await this.tryUnlock(key, passphrase);
  63. debug('Successfully initialized with address', key.address);
  64. return key;
  65. }
  66. /*
  67. * Try to unlock a key if it isn't already unlocked.
  68. * passphrase should be supplied as argument.
  69. */
  70. async tryUnlock(key, passphrase)
  71. {
  72. if (!key.isLocked) {
  73. debug('Key is not locked, not attempting to unlock')
  74. return;
  75. }
  76. // First try with an empty passphrase - for convenience
  77. try {
  78. key.decodePkcs8('');
  79. if (passphrase) {
  80. debug('Key was not encrypted, supplied passphrase was ignored');
  81. }
  82. return;
  83. } catch (err) {
  84. // pass
  85. }
  86. // Then with supplied passphrase
  87. try {
  88. debug('Decrypting with supplied passphrase');
  89. key.decodePkcs8(passphrase);
  90. return;
  91. } catch (err) {
  92. // pass
  93. }
  94. // If that didn't work, ask for a passphrase if appropriate
  95. if (this.canPromptForPassphrase) {
  96. passphrase = await this.askForPassphrase(key.address);
  97. key.decodePkcs8(passphrase);
  98. return
  99. }
  100. throw new Error('invalid passphrase supplied');
  101. }
  102. /*
  103. * Ask for a passphrase
  104. */
  105. askForPassphrase(address)
  106. {
  107. // Query for passphrase
  108. const prompt = require('password-prompt');
  109. return prompt(`Enter passphrase for ${address}: `, { required: false });
  110. }
  111. /*
  112. * Return true if the account is a member
  113. */
  114. async isMember(accountId)
  115. {
  116. const memberIds = await this.memberIdsOf(accountId); // return array of member ids
  117. return memberIds.length > 0 // true if at least one member id exists for the acccount
  118. }
  119. /*
  120. * Return the member IDs of an account
  121. */
  122. async memberIdsOf(accountId)
  123. {
  124. const decoded = this.keyring.decodeAddress(accountId);
  125. return await this.base.api.query.members.memberIdsByRootAccountId(decoded);
  126. }
  127. /*
  128. * Return the first member ID of an account, or undefined if not a member.
  129. */
  130. async firstMemberIdOf(accountId)
  131. {
  132. const decoded = this.keyring.decodeAddress(accountId);
  133. let ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded);
  134. return ids[0]
  135. }
  136. /*
  137. * Export a key pair to JSON. Will ask for a passphrase.
  138. */
  139. async exportKeyPair(accountId)
  140. {
  141. const passphrase = await this.askForPassphrase(accountId);
  142. // Produce JSON output
  143. return this.keyring.toJson(accountId, passphrase);
  144. }
  145. /*
  146. * Export a key pair and write it to a JSON file with the account ID as the
  147. * name.
  148. */
  149. async writeKeyPairExport(accountId, prefix)
  150. {
  151. // Generate JSON
  152. const data = await this.exportKeyPair(accountId);
  153. // Write JSON
  154. var filename = `${data.address}.json`;
  155. if (prefix) {
  156. const path = require('path');
  157. filename = path.resolve(prefix, filename);
  158. }
  159. fs.writeFileSync(filename, JSON.stringify(data), {
  160. encoding: 'utf8',
  161. mode: 0o600,
  162. });
  163. return filename;
  164. }
  165. async registerMember (accountId, userInfo) {
  166. const subscribed = [['members', 'MemberRegistered']]
  167. const tx = this.base.api.tx.members.buyMembership(0, userInfo)
  168. return new Promise(async (resolve, reject) => {
  169. try {
  170. await this.base.signAndSend(accountId, tx, 1, subscribed, (events) => {
  171. events.forEach((event) => {
  172. resolve(event[1].MemberId)
  173. })
  174. })
  175. } catch (err) {
  176. reject(err)
  177. }
  178. })
  179. }
  180. }
  181. module.exports = {
  182. IdentitiesApi: IdentitiesApi,
  183. }