123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- /*
- * This file is part of the storage node for the Joystream project.
- * Copyright (C) 2019 Joystream Contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- 'use strict'
- const path = require('path')
- const fs = require('fs')
- const debug = require('debug')('joystream:runtime:identities')
- const { Keyring } = require('@polkadot/keyring')
- const utilCrypto = require('@polkadot/util-crypto')
- /*
- * Add identity management to the substrate API.
- *
- * This loosely groups: accounts, key management, and membership.
- */
- class IdentitiesApi {
- static async create(base, { accountFile, passphrase, canPromptForPassphrase }) {
- const ret = new IdentitiesApi()
- ret.base = base
- await ret.init(accountFile, passphrase, canPromptForPassphrase)
- return ret
- }
- async init(accountFile, passphrase, canPromptForPassphrase) {
- debug('Init')
- // Creatre keyring
- this.keyring = new Keyring()
- this.canPromptForPassphrase = canPromptForPassphrase || false
- // Load account file, if possible.
- try {
- this.key = await this.loadUnlock(accountFile, passphrase)
- } catch (err) {
- debug('Error loading account file:', err.message)
- }
- }
- /*
- * Load a key file and unlock it if necessary.
- */
- async loadUnlock(accountFile, passphrase) {
- const fullname = path.resolve(accountFile)
- debug('Initializing key from', fullname)
- const key = this.keyring.addFromJson(require(fullname))
- await this.tryUnlock(key, passphrase)
- debug('Successfully initialized with address', key.address)
- return key
- }
- /*
- * Try to unlock a key if it isn't already unlocked.
- * passphrase should be supplied as argument.
- */
- async tryUnlock(key, passphrase) {
- if (!key.isLocked) {
- debug('Key is not locked, not attempting to unlock')
- return
- }
- // First try with an empty passphrase - for convenience
- try {
- key.decodePkcs8('')
- if (passphrase) {
- debug('Key was not encrypted, supplied passphrase was ignored')
- }
- return
- } catch (err) {
- // pass
- }
- // Then with supplied passphrase
- try {
- debug('Decrypting with supplied passphrase')
- key.decodePkcs8(passphrase)
- return
- } catch (err) {
- // pass
- }
- // If that didn't work, ask for a passphrase if appropriate
- if (this.canPromptForPassphrase) {
- passphrase = await this.askForPassphrase(key.address)
- key.decodePkcs8(passphrase)
- return
- }
- throw new Error('invalid passphrase supplied')
- }
- /*
- * Ask for a passphrase
- */
- /* eslint-disable class-methods-use-this */
- // Disable lint because the method used by a mocking library.
- askForPassphrase(address) {
- // Query for passphrase
- const prompt = require('password-prompt')
- return prompt(`Enter passphrase for ${address}: `, { required: false })
- }
- /*
- * Return true if the account is a root account of a member
- */
- async isMember(accountId) {
- const memberIds = await this.memberIdsOf(accountId) // return array of member ids
- return memberIds.length > 0 // true if at least one member id exists for the acccount
- }
- /*
- * Return all the member IDs of an account by the root account id
- */
- async memberIdsOf(accountId) {
- const decoded = this.keyring.decodeAddress(accountId)
- return this.base.api.query.members.memberIdsByRootAccountId(decoded)
- }
- /*
- * Return all the member IDs of an account by the controller account id
- */
- async memberIdsOfController(accountId) {
- const decoded = this.keyring.decodeAddress(accountId)
- return this.base.api.query.members.memberIdsByControllerAccountId(decoded)
- }
- /*
- * Return the first member ID of an account, or undefined if not a member root account.
- */
- async firstMemberIdOf(accountId) {
- const decoded = this.keyring.decodeAddress(accountId)
- const ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded)
- return ids[0]
- }
- /*
- * Export a key pair to JSON. Will ask for a passphrase.
- */
- async exportKeyPair(accountId) {
- const passphrase = await this.askForPassphrase(accountId)
- // Produce JSON output
- return this.keyring.toJson(accountId, passphrase)
- }
- /*
- * Export a key pair and write it to a JSON file with the account ID as the
- * name.
- */
- async writeKeyPairExport(accountId, prefix) {
- // Generate JSON
- const data = await this.exportKeyPair(accountId)
- // Write JSON
- let filename = `${data.address}.json`
- if (prefix) {
- const path = require('path')
- filename = path.resolve(prefix, filename)
- }
- fs.writeFileSync(filename, JSON.stringify(data), {
- encoding: 'utf8',
- mode: 0o600,
- })
- return filename
- }
- /*
- * Register account id with userInfo as a new member
- * using default policy 0, returns new member id
- */
- async registerMember(accountId, userInfo) {
- const tx = this.base.api.tx.members.buyMembership(0, userInfo.handle, userInfo.avatarUri, userInfo.about)
- return this.base.signAndSendThenGetEventResult(accountId, tx, {
- module: 'members',
- event: 'MemberRegistered',
- type: 'MemberId',
- index: 0,
- })
- }
- /*
- * Injects a keypair and sets it as the default identity
- */
- useKeyPair(keyPair) {
- this.key = this.keyring.addPair(keyPair)
- }
- /*
- * Create a new role key. If no name is given,
- * default to 'storage'.
- */
- async createNewRoleKey(name) {
- name = name || 'storage-provider'
- // Generate new key pair
- const keyPair = utilCrypto.naclKeypairFromRandom()
- // Encode to an address.
- const addr = this.keyring.encodeAddress(keyPair.publicKey)
- debug('Generated new key pair with address', addr)
- // Add to key wring. We set the meta to identify the account as
- // a role key.
- const meta = {
- name: `${name} role account`,
- }
- const createPair = require('@polkadot/keyring/pair').default
- const pair = createPair('ed25519', keyPair, meta)
- this.keyring.addPair(pair)
- return pair
- }
- getSudoAccount() {
- return this.base.api.query.sudo.key()
- }
- }
- module.exports = {
- IdentitiesApi,
- }
|