Skip to content

Commit

Permalink
Merge pull request Joystream#22 from jfinkhaeuser/chain-integration
Browse files Browse the repository at this point in the history
Become storage provider workflow
  • Loading branch information
jfinkhaeuser authored Apr 9, 2019
2 parents 157da95 + 38f9763 commit 7941179
Show file tree
Hide file tree
Showing 15 changed files with 1,691 additions and 21 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ yarn-error.log*
# Node modules except our own
node_modules/*
!node_modules/joystream

# Generated JS files
lib/joystream/substrate/types/*.js
!lib/joystream/substrate/types/index.js
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This project uses [yarn](https://yarnpkg.com/) as Node package manager.
$ yarn run build
```

The command will run `yarn install` and perform post-install fixes.
The command will run `yarn install`, perform post-install fixes and build
TypeScript files.

To make the `js_storage` script available globally, run:

Expand Down Expand Up @@ -40,6 +41,16 @@ Run linter:
$ yarn run lint
```

TypeScript files are used for type declarations for interaction with the chain.
They are taken from [the app](https://github.com/Joystream/apps), and renamed
to suit our use:

`apps/packages/joy-members/src/types.ts` becomes `lib/joystream/types/members.ts`,
etc. The only thing adjusted in the files are the imports of other joystream
types.

`lib/joystream/types/index.js` is manually maintained, but other JavaScript
files in that folder are generated.

Command-Line
------------
Expand Down
40 changes: 39 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const cli = meow(`
list Output a list of storage entries. If an argument is given,
it is interpreted as a repo ID, and the contents of the
repo are listed instead.
signup Sign up as a storage provider. Requires that you provide
a JSON account file of an account that is a member, and has
sufficient balance for staking as a storage provider.
Writes a new account file that should be used to run the
storage node.
Options:
--config=PATH, -c PATH Configuration file path. Defaults to
Expand All @@ -40,7 +45,7 @@ const cli = meow(`
protocol. Defaults to 3030.
--sync-period Number of milliseconds to wait between synchronization
runs. Defaults to 30,000 (30s).
--key Private key to run the storage node under.
--key Private key to run the storage node under. FIXME should become file
--storage=PATH, -s PATH Storage path to use.
--storage-type=TYPE One of "fs", "hyperdrive". Defaults to "hyperdrive".
`, {
Expand Down Expand Up @@ -226,6 +231,34 @@ function list_repo(store, repo_id)
});
}

async function run_signup(account_file)
{
const { RolesApi, ROLE_STORAGE } = require('joystream/substrate/roles');
const api = await RolesApi.create(account_file);
const member_address = api.key.address();

// Check if account works
const check = await api.checkAccountForStaking(member_address);
if (check) {
console.log('Account is working for staking, proceeding.');
}

// Create a role key
const role_key = await api.createRoleKey(member_address);
const role_address = role_key.address();
console.log('Generated ', role_address);
const filename = await api.writeKeyPairExport(role_address);
console.log('Identity stored in', filename);

// Ok, transfer for staking.
await api.transferForStaking(member_address, role_address);
console.log('Funds transferred.');

// Now apply for the role
await api.applyForRole(role_address, ROLE_STORAGE, member_address);
console.log('Role application sent.');
}

// Simple CLI commands
var command = cli.input[0];
if (!command) {
Expand Down Expand Up @@ -279,6 +312,11 @@ const commands = {
list_repos(store);
}
},
'signup': () => {
const account_file = cli.input[1];
const ret = run_signup(account_file);
ret.catch(console.error).finally(_ => process.exit());
},
};

if (commands.hasOwnProperty(command)) {
Expand Down
66 changes: 66 additions & 0 deletions lib/joystream/substrate/balances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const debug = require('debug')('joystream:substrate:balances');

const { IdentitiesApi } = require('joystream/substrate/identities');

// TODO replace this with the one defined in the role
const DEFAULT_STAKING_AMOUNT = 3000;

class BalancesApi extends IdentitiesApi
{
static async create(account_file)
{
const ret = new BalancesApi();
await ret.init(account_file);
return ret;
}

async init(account_file)
{
debug('Init');

// Super init
await super.init(account_file);
}

async hasBalanceForRoleStaking(accountId)
{
return await this.hasMinimumBalanceOf(accountId, DEFAULT_STAKING_AMOUNT);
}

async hasMinimumBalanceOf(accountId, min)
{
const balance = await this.freeBalance(accountId);
return balance.cmpn(min) >= 0;
}

async freeBalance(accountId)
{
const decoded = this.keyring.decodeAddress(accountId, true);
return await this.api.query.balances.freeBalance(decoded);
}

async transfer(from, to, amount)
{
const decode = require('@polkadot/keyring/address/decode').default;
const to_decoded = decode(to, true);

const from_key = this.keyring.getPair(from);
if (from_key.isLocked()) {
throw new Error('Must unlock key before using it to sign!');
}

return await this.api.tx.balances.transfer(to_decoded, amount)
.signAndSend(from_key);
}

async transferForStaking(from, to)
{
return await this.transfer(from, to, DEFAULT_STAKING_AMOUNT + 100 /*FIXME */);
}
}

module.exports = {
BalancesApi: BalancesApi,
}
31 changes: 31 additions & 0 deletions lib/joystream/substrate/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

const debug = require('debug')('joystream:substrate:base');

const { registerJoystreamTypes } = require('joystream/substrate/types');
const { ApiPromise } = require('@polkadot/api');

class SubstrateApi
{
static async create()
{
const ret = new SubstrateApi();
await ret.init();
return ret;
}

async init()
{
debug('Init');

// Register joystream types
registerJoystreamTypes();

// Create the API instrance
this.api = await ApiPromise.create();
}
}

module.exports = {
SubstrateApi: SubstrateApi,
}
124 changes: 124 additions & 0 deletions lib/joystream/substrate/identities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use strict';

const path = require('path');
const fs = require('fs');
const readline = require('readline');

const debug = require('debug')('joystream:substrate:identities');

const { Keyring } = require('@polkadot/keyring');
const { Null } = require('@polkadot/types/primitive');
const util_crypto = require('@polkadot/util-crypto');

const { _ } = require('lodash');

const { SubstrateApi } = require('joystream/substrate/base');

class IdentitiesApi extends SubstrateApi
{
static async create(account_file)
{
const ret = new IdentitiesApi();
await ret.init(account_file);
return ret;
}

async init(account_file)
{
debug('Init');

// Super init
await super.init();

// Creatre keyring
this.keyring = new Keyring();

// Load account file, if possible.
const fullname = path.resolve(account_file);
debug('Initializing key from', fullname);
this.key = this.keyring.addFromJson(require(fullname), true);
if (this.key.isLocked()) {
const passphrase = await this.askForPassphrase(this.key.address());
this.key.decodePkcs8(passphrase);
}
debug('Successfully initialized with address', this.key.address());
}

async askForPassphrase(address)
{
// Query for passphrase
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const question = (str) => new Promise(resolve => rl.question(str, resolve));
const passphrase = await question(`Enter passphrase for ${address}: `);
rl.close();
return passphrase;
}

async isMember(accountId)
{
const memberId = await this.memberIdOf(accountId);
return !_.isEqual(memberId.raw, new Null());
}

async memberIdOf(accountId)
{
const decoded = this.keyring.decodeAddress(accountId, true);
return await this.api.query.membership.memberIdByAccountId(decoded);
}

async createRoleKey(accountId, role)
{
role = role || 'storage';

// Generate new key pair
const keyPair = util_crypto.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: `${role} role account for ${accountId}`,
};

const createPair = require('@polkadot/keyring/pair').default;
const pair = createPair('ed25519', keyPair, meta);

this.keyring.addPair(pair);

return pair;
}

async exportKeyPair(accountId)
{
const passphrase = await this.askForPassphrase(accountId);

// Produce JSON output
return this.keyring.toJson(accountId, passphrase);
}

async writeKeyPairExport(accountId)
{
// Generate JSON
const data = await this.exportKeyPair(accountId);

// Write JSON
const filename = `${data.address}.json`;
fs.writeFileSync(filename, JSON.stringify(data), {
encoding: 'utf8',
mode: 0o600,
});

return filename;
}
}

module.exports = {
IdentitiesApi: IdentitiesApi,
}
Loading

0 comments on commit 7941179

Please sign in to comment.