Browse Source

Merge branch 'babylon' into babylon-types

Leszek Wiesner 4 years ago
parent
commit
88776e1a1b
61 changed files with 20154 additions and 572 deletions
  1. 5 5
      Cargo.lock
  2. 1 1
      README.md
  3. 1 1
      cli/package.json
  4. 3 2
      devops/dockerfiles/node-and-runtime/Dockerfile
  5. 1 1
      node/Cargo.toml
  6. 6 6
      node/README.md
  7. 2 2
      node/src/chain_spec/mod.rs
  8. 3 0
      pioneer/packages/apps/public/Logo_Alexandrial.svg
  9. 97 10
      pioneer/packages/apps/public/index.html
  10. 1 0
      pioneer/packages/apps/public/locales/en/index.json
  11. 4 1
      pioneer/packages/apps/public/locales/en/joy-proposals.json
  12. 2 0
      pioneer/packages/apps/public/locales/en/translation.json
  13. 1 0
      pioneer/packages/apps/src/notLive.ts
  14. 3 1
      pioneer/packages/apps/webpack.base.config.js
  15. 2 0
      pioneer/packages/apps/webpack.config.js
  16. 5 1
      pioneer/packages/joy-media/src/DiscoveryProvider.tsx
  17. 42 20
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  18. 10 6
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  19. 26 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx
  20. 4 3
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx
  21. 14 8
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  22. 6 3
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  23. 4 1
      pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx
  24. 11 5
      pioneer/packages/joy-proposals/src/Proposal/discussion/ProposalDiscussion.tsx
  25. 37 3
      pioneer/packages/joy-proposals/src/index.tsx
  26. 157 12
      pioneer/packages/joy-utils/src/transport/proposals.ts
  27. 19301 0
      pioneer/packages/joy-utils/src/transport/static/historical-proposals.json
  28. 43 0
      pioneer/packages/joy-utils/src/types/proposals.ts
  29. 5 4
      pioneer/packages/page-accounts/src/Accounts/Account.tsx
  30. 6 5
      pioneer/packages/page-accounts/src/Accounts/PasswordInput.tsx
  31. 3 2
      pioneer/packages/page-accounts/src/Accounts/modals/Backup.tsx
  32. 7 6
      pioneer/packages/page-accounts/src/Accounts/modals/ChangePass.tsx
  33. 4 3
      pioneer/packages/page-accounts/src/Accounts/modals/Create.tsx
  34. 7 6
      pioneer/packages/page-accounts/src/Accounts/modals/Derive.tsx
  35. 3 2
      pioneer/packages/page-accounts/src/Accounts/modals/Import.tsx
  36. 1 1
      pioneer/packages/react-api/src/Api.tsx
  37. 4 4
      pioneer/packages/react-components/src/InputNumber.tsx
  38. 8 2
      pioneer/scripts/dev-build-ts.js
  39. 1 0
      runtime-modules/membership/src/lib.rs
  40. 1 1
      runtime-modules/membership/src/tests.rs
  41. 5 2
      runtime/CHANGELOG.md
  42. 1 1
      runtime/Cargo.toml
  43. 2 2
      runtime/src/lib.rs
  44. 1 1
      storage-node/packages/colossus/bin/cli.js
  45. 85 0
      storage-node/packages/colossus/lib/middleware/ipfs_proxy.js
  46. 108 65
      storage-node/packages/colossus/lib/sync.js
  47. 4 2
      storage-node/packages/colossus/package.json
  48. 14 82
      storage-node/packages/colossus/paths/asset/v0/{id}.js
  49. 1 1
      storage-node/packages/colossus/paths/discover/v0/{id}.js
  50. 1 29
      storage-node/packages/runtime-api/index.js
  51. 33 17
      storage-node/packages/storage/storage.js
  52. 7 34
      storage-node/packages/util/ranges.js
  53. 8 0
      storage-node/scripts/compose/devchain-and-ipfs-node/docker-compose.yaml
  54. 25 81
      testnets/joy-testnet-4.json
  55. 0 21
      testnets/nicaea-exported-state/chain_spec.json
  56. 0 0
      testnets/nicaea-exported-state/content.json
  57. 0 0
      testnets/nicaea-exported-state/forum.json
  58. 0 0
      testnets/nicaea-exported-state/members.json
  59. 0 81
      testnets/rome.json
  60. 14 21
      types/README.md
  61. 3 3
      yarn.lock

+ 5 - 5
Cargo.lock

@@ -711,12 +711,12 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "crossbeam-channel"
 name = "crossbeam-channel"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
 dependencies = [
 dependencies = [
- "cfg-if",
  "crossbeam-utils",
  "crossbeam-utils",
+ "maybe-uninit",
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -1993,7 +1993,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "joystream-node"
 name = "joystream-node"
-version = "3.1.0"
+version = "3.3.1"
 dependencies = [
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking",
  "frame-benchmarking-cli",
  "frame-benchmarking-cli",
@@ -2053,7 +2053,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "joystream-node-runtime"
 name = "joystream-node-runtime"
-version = "7.3.0"
+version = "7.5.1"
 dependencies = [
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking",
  "frame-executive",
  "frame-executive",

+ 1 - 1
README.md

@@ -94,7 +94,7 @@ You can also run your our own joystream-node:
 ```sh
 ```sh
 git checkout master
 git checkout master
 WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
 WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
-./target/release/joystream-node -- --pruning archive --chain testnets/rome.json
+./target/release/joystream-node -- --pruning archive --chain testnets/joy-testnet-4.json
 ```
 ```
 
 
 Wait for the node to sync to the latest block, then change pioneer settings "remote node" option to "Local Node", or follow the link below:
 Wait for the node to sync to the latest block, then change pioneer settings "remote node" option to "Local Node", or follow the link below:

+ 1 - 1
cli/package.json

@@ -15,7 +15,7 @@
     "@oclif/plugin-help": "^2.2.3",
     "@oclif/plugin-help": "^2.2.3",
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
-    "@polkadot/api": "^0.96.1",
+    "@polkadot/api": "1.26.1",
     "@types/inquirer": "^6.5.0",
     "@types/inquirer": "^6.5.0",
     "@types/proper-lockfile": "^4.1.1",
     "@types/proper-lockfile": "^4.1.1",
     "@types/slug": "^0.9.1",
     "@types/slug": "^0.9.1",

+ 3 - 2
devops/dockerfiles/node-and-runtime/Dockerfile

@@ -3,14 +3,15 @@ LABEL description="Compiles all workspace artifacts"
 WORKDIR /joystream
 WORKDIR /joystream
 COPY . /joystream
 COPY . /joystream
 
 
-# Build joystream-node and its dependencies - runtime
-RUN WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release -p joystream-node
+# Build all cargo crates
+RUN WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
 
 
 FROM debian:stretch
 FROM debian:stretch
 LABEL description="Joystream node"
 LABEL description="Joystream node"
 WORKDIR /joystream
 WORKDIR /joystream
 COPY --from=builder /joystream/target/release/joystream-node /joystream/node
 COPY --from=builder /joystream/target/release/joystream-node /joystream/node
 COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm /joystream/runtime.compact.wasm
 COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm /joystream/runtime.compact.wasm
+COPY --from=builder /joystream/target/release/chain-spec-builder /joystream/chain-spec-builder
 
 
 # confirm it works
 # confirm it works
 RUN /joystream/node --version
 RUN /joystream/node --version

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream contributors']
 build = 'build.rs'
 build = 'build.rs'
 edition = '2018'
 edition = '2018'
 name = 'joystream-node'
 name = 'joystream-node'
-version = '3.1.0'
+version = '3.3.1'
 default-run = "joystream-node"
 default-run = "joystream-node"
 
 
 [[bin]]
 [[bin]]

+ 6 - 6
node/README.md

@@ -49,7 +49,7 @@ this script will build and run a fresh new local development chain (purging exis
 Use the `--chain` argument, and specify the path to the genesis `chain.json` file for that public network. The JSON "chain spec" files for Joystream public networks can be found in [../testnets/](../testnets/).
 Use the `--chain` argument, and specify the path to the genesis `chain.json` file for that public network. The JSON "chain spec" files for Joystream public networks can be found in [../testnets/](../testnets/).
 
 
 ```bash
 ```bash
-./target/release/joystream-node --chain testnets/rome.json
+./target/release/joystream-node --chain testnets/joy-testnet-4.json
 ```
 ```
 
 
 ### Tests and code quality
 ### Tests and code quality
@@ -57,7 +57,7 @@ Use the `--chain` argument, and specify the path to the genesis `chain.json` fil
 Running unit tests:
 Running unit tests:
 
 
 ```bash
 ```bash
-cargo test --all
+cargo test --release --all
 ```
 ```
 
 
 Running full suite of checks, tests, formatting and linting:
 Running full suite of checks, tests, formatting and linting:
@@ -75,7 +75,7 @@ cargo fmt --all
 ### Integration tests
 ### Integration tests
 
 
 ```bash
 ```bash
-./scripts/run-test-chain.sh
+./scripts/run-dev-chain.sh
 yarn workspace joystream-testing test
 yarn workspace joystream-testing test
 ```
 ```
 
 
@@ -88,11 +88,11 @@ If you are building a tagged release from `master` branch and want to install th
 This will install the executable `joystream-node` to your `~/.cargo/bin` folder, which you would normally have in your `$PATH` environment.
 This will install the executable `joystream-node` to your `~/.cargo/bin` folder, which you would normally have in your `$PATH` environment.
 
 
 ```bash
 ```bash
-cargo install joystream-node --path node/
+WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo install joystream-node --path node/ --locked
 ```
 ```
 
 
-Now you can run and connect to the Rome testnet:
+Now you can run and connect to the testnet:
 
 
 ```bash
 ```bash
-joystream-node --chain testnets/rome.json
+joystream-node --chain testnets/joy-testnet-4.json
 ```
 ```

+ 2 - 2
node/src/chain_spec/mod.rs

@@ -357,9 +357,9 @@ pub fn testnet_genesis(
             fill_working_group_leader_opening_proposal_grace_period: cpcp
             fill_working_group_leader_opening_proposal_grace_period: cpcp
                 .fill_working_group_leader_opening_proposal_grace_period,
                 .fill_working_group_leader_opening_proposal_grace_period,
             set_working_group_mint_capacity_proposal_voting_period: cpcp
             set_working_group_mint_capacity_proposal_voting_period: cpcp
-                .set_content_working_group_mint_capacity_proposal_voting_period,
+                .set_working_group_mint_capacity_proposal_voting_period,
             set_working_group_mint_capacity_proposal_grace_period: cpcp
             set_working_group_mint_capacity_proposal_grace_period: cpcp
-                .set_content_working_group_mint_capacity_proposal_grace_period,
+                .set_working_group_mint_capacity_proposal_grace_period,
             decrease_working_group_leader_stake_proposal_voting_period: cpcp
             decrease_working_group_leader_stake_proposal_voting_period: cpcp
                 .decrease_working_group_leader_stake_proposal_voting_period,
                 .decrease_working_group_leader_stake_proposal_voting_period,
             decrease_working_group_leader_stake_proposal_grace_period: cpcp
             decrease_working_group_leader_stake_proposal_grace_period: cpcp

File diff suppressed because it is too large
+ 3 - 0
pioneer/packages/apps/public/Logo_Alexandrial.svg


+ 97 - 10
pioneer/packages/apps/public/index.html

@@ -18,17 +18,104 @@
       </script>
       </script>
     <% } %>
     <% } %>
     <script type="text/javascript" src="/env-config.js"></script>
     <script type="text/javascript" src="/env-config.js"></script>
+    <% if (!htmlWebpackPlugin.options.IS_LIVE) { %>
+      <meta name="viewport" content="width=device-width">
+      <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
+      <style>
+        body {
+          background: #000 !important;
+          color: #fff !important;
+          min-height: 100vh !important;
+          margin: 0 !important;
+          padding: 0 !important;
+          padding-top: 20vh !important;
+          box-sizing: border-box;
+          font-family: 'Inter', sans-serif !important;
+        }
+        header {
+          width: 100%;
+          background: #261EE4;
+        }
+        .info-flexbox {
+          display: flex;
+          width: 80%;
+          max-width: 800px;
+          margin: 0 auto;
+          align-items: flex-end;
+          padding: 20px;
+        }
+        h2 {
+          font-weight: normal !important;
+          margin-bottom: 20px;
+        }
+        .blog-link {
+          margin-left: auto;
+          color: #000 !important;
+          background: #6C6CFF;
+          padding: 10px 20px;
+          font-size: 16px;
+          text-decoration: none;
+          transition: background-color 0.2s;
+        }
+        .blog-link:hover {
+          background: #fff;
+        }
+        .box-content {
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          margin-top: 20px;
+        }
+        .logo-alexandria {
+          width: 30%;
+          flex-shrink: 0;
+          margin-right: 30px;
+        }
+        @media screen and (max-width: 800px) {
+          h2 {
+            font-size: 20px !important;
+          }
+        }
+        @media screen and (max-width: 500px) {
+          body {
+            padding: 50px 0 !important;
+          }
+          .info-flexbox {
+            flex-wrap: wrap;
+          }
+          .info-flexbox > * {
+            width: 100% !important;
+          }
+        }
+      </style>
+    <% } %>
   </head>
   </head>
   <body>
   <body>
-    <noscript>
-      You need to enable JavaScript to run this app.
-    </noscript>
-    <div id="root"></div>
-    <div id="tooltips"></div>
-    <script>
-      if (window.self !== window.top) {
-        window.top.location.href = window.location.href;
-      }
-    </script>
+    <% if (htmlWebpackPlugin.options.IS_LIVE) { %>
+      <noscript>
+        You need to enable JavaScript to run this app.
+      </noscript>
+      <div id="root"></div>
+      <div id="tooltips"></div>
+      <script>
+        if (window.self !== window.top) {
+          window.top.location.href = window.location.href;
+        }
+      </script>
+    <% } else { %>
+      <header>
+        <div class="info-flexbox">
+          <img src="./Logo_Alexandrial.svg" class="logo-alexandria">
+          <div class="box-content">
+            <h2>
+              Our new testnet - Alexandria will be launched
+              around&nbsp;09:00&nbsp;UTC on&nbsp;Monday the&nbsp;21st
+              of&nbsp;September&nbsp;2020.
+            </h2>
+            <a href="https://blog.joystream.org/announcing-alexandria/" target="_blank" class="blog-link">Find out more</a>
+          </div>
+        </div>
+      </header>
+    <% } %>
   </body>
   </body>
 </html>
 </html>

+ 1 - 0
pioneer/packages/apps/public/locales/en/index.json

@@ -24,6 +24,7 @@
   "joy-election.json",
   "joy-election.json",
   "joy-media.json",
   "joy-media.json",
   "joy-members.json",
   "joy-members.json",
+  "joy-proposals.json",
   "joy-roles.json",
   "joy-roles.json",
   "joy-utils.json",
   "joy-utils.json",
   "react-components.json",
   "react-components.json",

+ 4 - 1
pioneer/packages/apps/public/locales/en/joy-proposals.json

@@ -1 +1,4 @@
-{}
+{
+  "Current": "Current",
+  "Historical": "Historical"
+}

+ 2 - 0
pioneer/packages/apps/public/locales/en/translation.json

@@ -125,6 +125,7 @@
   "Create and backup account": "",
   "Create and backup account": "",
   "Crypto not detected": "",
   "Crypto not detected": "",
   "Cryptography used to create this signature. It is auto-detected on valid signatures.": "",
   "Cryptography used to create this signature. It is auto-detected on valid signatures.": "",
+  "Current": "",
   "Current account nonce: {{accountNonce}}": "",
   "Current account nonce: {{accountNonce}}": "",
   "Current prime member, default voting": "",
   "Current prime member, default voting": "",
   "Current society head, exempt": "",
   "Current society head, exempt": "",
@@ -184,6 +185,7 @@
   "Genesis Hash refers to initial state of the chain, it cannot be changed once the chain is launched": "",
   "Genesis Hash refers to initial state of the chain, it cannot be changed once the chain is launched": "",
   "Grandpa": "",
   "Grandpa": "",
   "Hash data": "",
   "Hash data": "",
+  "Historical": "",
   "I'm Online": "",
   "I'm Online": "",
   "If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.": "",
   "If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.": "",
   "If this proposal is passed, the changes will be applied via dispatch and the deposit returned.": "",
   "If this proposal is passed, the changes will be applied via dispatch and the deposit returned.": "",

+ 1 - 0
pioneer/packages/apps/src/notLive.ts

@@ -0,0 +1 @@
+// Empty entry point for webpack if not in live mode to speed up the build

+ 3 - 1
pioneer/packages/apps/webpack.base.config.js

@@ -27,6 +27,7 @@ function mapChunks (name, regs, inc) {
 function createWebpack (ENV, context) {
 function createWebpack (ENV, context) {
   const pkgJson = require(path.join(context, 'package.json'));
   const pkgJson = require(path.join(context, 'package.json'));
   const isProd = ENV === 'production';
   const isProd = ENV === 'production';
+  const isLive = !(process.env.IS_LIVE === 'false' || process.env.IS_LIVE === false);
   const hasPublic = fs.existsSync(path.join(context, 'public'));
   const hasPublic = fs.existsSync(path.join(context, 'public'));
   const plugins = hasPublic
   const plugins = hasPublic
     ? [new CopyWebpackPlugin({ patterns: [{ from: 'public' }] })]
     ? [new CopyWebpackPlugin({ patterns: [{ from: 'public' }] })]
@@ -53,7 +54,8 @@ function createWebpack (ENV, context) {
 
 
   return {
   return {
     context,
     context,
-    entry: ['@babel/polyfill', './src/index.tsx'],
+    // Make it quicker if we're not in a LIVE mode
+    entry: !isLive ? './src/notLive.ts' : ['@babel/polyfill', './src/index.tsx'],
     mode: ENV,
     mode: ENV,
     module: {
     module: {
       rules: [
       rules: [

+ 2 - 0
pioneer/packages/apps/webpack.config.js

@@ -11,6 +11,7 @@ const baseConfig = require('./webpack.base.config');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 
 
 const ENV = process.env.NODE_ENV || 'development';
 const ENV = process.env.NODE_ENV || 'development';
+const IS_LIVE = !(process.env.IS_LIVE === false || process.env.IS_LIVE === 'false');
 const context = __dirname;
 const context = __dirname;
 const hasPublic = fs.existsSync(path.join(context, 'public'));
 const hasPublic = fs.existsSync(path.join(context, 'public'));
 
 
@@ -21,6 +22,7 @@ module.exports = merge(
     plugins: [
     plugins: [
       new HtmlWebpackPlugin({
       new HtmlWebpackPlugin({
         IS_PROD: ENV === 'production',
         IS_PROD: ENV === 'production',
+        IS_LIVE,
         PAGE_TITLE: 'Joystream Network Portal',
         PAGE_TITLE: 'Joystream Network Portal',
         inject: true,
         inject: true,
         template: path.join(context, `${hasPublic ? 'public/' : ''}index.html`)
         template: path.join(context, `${hasPublic ? 'public/' : ''}index.html`)

+ 5 - 1
pioneer/packages/joy-media/src/DiscoveryProvider.tsx

@@ -44,7 +44,11 @@ type ProviderStats = {
 function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
 function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
   const stats = new Map<string, ProviderStats>();
   const stats = new Map<string, ProviderStats>();
 
 
-  const resolveAssetEndpoint = async (storageProvider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => {
+  const resolveAssetEndpoint = async (
+    storageProvider: StorageProviderId,
+    contentId?: string,
+    cancelToken?: CancelToken
+  ) => {
     const providerKey = storageProvider.toString();
     const providerKey = storageProvider.toString();
 
 
     let stat = stats.get(providerKey);
     let stat = stats.get(providerKey);

+ 42 - 20
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -33,6 +33,7 @@ type BodyProps = {
   proposerId: number | MemberId;
   proposerId: number | MemberId;
   isCancellable: boolean;
   isCancellable: boolean;
   cancellationFee: number;
   cancellationFee: number;
+  historical?: boolean;
 };
 };
 
 
 function ProposedAddress (props: { accountId?: AccountId }) {
 function ProposedAddress (props: { accountId?: AccountId }) {
@@ -97,7 +98,7 @@ class ParsedParam {
 }
 }
 
 
 // The methods for parsing params by Proposal type.
 // The methods for parsing params by Proposal type.
-const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>) => ParsedParam[]} = {
+const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>, historical?: boolean) => ParsedParam[]} = {
   Text: (content) => [
   Text: (content) => [
     new ParsedParam(
     new ParsedParam(
       'Content',
       'Content',
@@ -237,12 +238,14 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
     new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
   ],
   ],
-  BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [
+  BeginReviewWorkingGroupLeaderApplication: ([id, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
     new ParsedParam(
       'Opening id',
       'Opening id',
-      <Link to={`/working-groups/opportunities/storageProviders/${id.toString()}`}>#{id.toString()}</Link>
+      historical
+        ? `#${id.toString()}`
+        : <Link to={`/working-groups/opportunities/storageProviders/${id.toString()}`}>#{id.toString()}</Link>
     )
     )
   ],
   ],
   FillWorkingGroupLeaderOpening: ({
   FillWorkingGroupLeaderOpening: ({
@@ -250,46 +253,56 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     successful_application_id: succesfulApplicationId,
     successful_application_id: succesfulApplicationId,
     reward_policy: rewardPolicy,
     reward_policy: rewardPolicy,
     working_group: workingGroup
     working_group: workingGroup
-  }) => [
+  }, historical) => [
     new ParsedParam('Working group', workingGroup.type),
     new ParsedParam('Working group', workingGroup.type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
     new ParsedParam(
       'Opening id',
       'Opening id',
-      <Link to={`/working-groups/opportunities/storageProviders/${openingId.toString()}`}>#{openingId.toString()}</Link>),
+      historical
+        ? `#${openingId.toString()}`
+        : <Link to={`/working-groups/opportunities/storageProviders/${openingId.toString()}`}>#{openingId.toString()}</Link>),
     new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'),
     new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'),
     new ParsedParam(
     new ParsedParam(
       'Result',
       'Result',
-      <ApplicationsDetailsByOpening
-        openingId={openingId.toNumber()}
-        acceptedIds={[succesfulApplicationId.toNumber()]}
-        group={workingGroup.type}/>,
+      historical
+        ? `Accepted application ID: ${succesfulApplicationId.toNumber()}`
+        : <ApplicationsDetailsByOpening
+          openingId={openingId.toNumber()}
+          acceptedIds={[succesfulApplicationId.toNumber()]}
+          group={workingGroup.type}/>,
       true
       true
     )
     )
   ],
   ],
-  SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  SlashWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Slash amount', formatBalance(amount as Balance)),
     new ParsedParam('Slash amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
-  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Decrease amount', formatBalance(amount as Balance)),
     new ParsedParam('Decrease amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
-  SetWorkingGroupLeaderReward: ([leadId, amount, group]) => [
+  SetWorkingGroupLeaderReward: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('New reward amount', formatBalance(amount as Balance)),
     new ParsedParam('New reward amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
@@ -298,12 +311,19 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     rationale,
     rationale,
     worker_id: leadId,
     worker_id: leadId,
     slash
     slash
-  }) => {
+  },
+  historical) => {
     return [
     return [
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Rationale', bytesToString(rationale), true),
       new ParsedParam('Rationale', bytesToString(rationale), true),
       new ParsedParam('Slash stake', slash.isTrue ? 'YES' : 'NO'),
       new ParsedParam('Slash stake', slash.isTrue ? 'YES' : 'NO'),
-      new ParsedParam('Lead', <LeadInfoFromId group={workingGroup.type} leadId={leadId.toNumber()}/>, true)
+      new ParsedParam(
+        'Lead',
+        historical
+          ? `#${leadId.toNumber()}`
+          : <LeadInfoFromId group={workingGroup.type} leadId={leadId.toNumber()}/>,
+        true
+      )
     ];
     ];
   }
   }
 };
 };
@@ -364,14 +384,16 @@ export default function Body ({
   proposalId,
   proposalId,
   proposerId,
   proposerId,
   isCancellable,
   isCancellable,
-  cancellationFee
+  cancellationFee,
+  historical
 }: BodyProps) {
 }: BodyProps) {
   // Assert more generic type (since TypeScript cannot possibly know the value of "type" here yet)
   // Assert more generic type (since TypeScript cannot possibly know the value of "type" here yet)
-  const parseParams = paramParsers[type] as (params: SpecificProposalDetails<ProposalType>) => ParsedParam[];
+  const parseParams = paramParsers[type] as (params: SpecificProposalDetails<ProposalType>, historical?: boolean) => ParsedParam[];
   const parsedParams = parseParams(
   const parsedParams = parseParams(
     type === 'RuntimeUpgrade'
     type === 'RuntimeUpgrade'
       ? params as RuntimeUpgradeProposalDetails
       ? params as RuntimeUpgradeProposalDetails
-      : (params as ProposalDetails).asType(type)
+      : (params as ProposalDetails).asType(type),
+    historical
   );
   );
 
 
   return (
   return (

+ 10 - 6
pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -56,7 +56,7 @@ export type ExtendedProposalStatus = {
   executionFailReason: string | null;
   executionFailReason: string | null;
 }
 }
 
 
-export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNumber | undefined): ExtendedProposalStatus {
+export function getExtendedStatus (proposal: ParsedProposal, bestNumber?: BlockNumber): ExtendedProposalStatus {
   const basicStatus: BasicProposalStatus = proposal.status.type;
   const basicStatus: BasicProposalStatus = proposal.status.type;
   let expiresIn: number | null = null;
   let expiresIn: number | null = null;
 
 
@@ -120,6 +120,7 @@ type ProposalDetailsProps = MyAccountProps & {
   proposalId: ProposalId;
   proposalId: ProposalId;
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
   council?: Seat[];
   council?: Seat[];
+  historical?: boolean;
 };
 };
 
 
 function ProposalDetails ({
 function ProposalDetails ({
@@ -129,11 +130,12 @@ function ProposalDetails ({
   myMemberId,
   myMemberId,
   iAmMember,
   iAmMember,
   council,
   council,
-  bestNumber
+  bestNumber,
+  historical
 }: ProposalDetailsProps) {
 }: ProposalDetailsProps) {
   const iAmCouncilMember = Boolean(iAmMember && council && council.some((seat) => seat.member.toString() === myAddress));
   const iAmCouncilMember = Boolean(iAmMember && council && council.some((seat) => seat.member.toString() === myAddress));
   const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber());
   const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber());
-  const extendedStatus = getExtendedStatus(proposal, bestNumber);
+  const extendedStatus = getExtendedStatus(proposal, historical ? undefined : bestNumber);
   const isVotingPeriod = extendedStatus.periodStatus === 'Voting period';
   const isVotingPeriod = extendedStatus.periodStatus === 'Voting period';
 
 
   return (
   return (
@@ -150,21 +152,23 @@ function ProposalDetails ({
           proposerId={ proposal.proposerId }
           proposerId={ proposal.proposerId }
           isCancellable={ isVotingPeriod }
           isCancellable={ isVotingPeriod }
           cancellationFee={ proposal.cancellationFee }
           cancellationFee={ proposal.cancellationFee }
+          historical={historical}
         />
         />
         <ProposalDetailsVoting>
         <ProposalDetailsVoting>
-          { iAmCouncilMember && (
+          { (iAmCouncilMember && !historical) && (
             <VotingSection
             <VotingSection
               proposalId={proposalId}
               proposalId={proposalId}
               memberId={ myMemberId as MemberId }
               memberId={ myMemberId as MemberId }
               isVotingPeriod={ isVotingPeriod }/>
               isVotingPeriod={ isVotingPeriod }/>
           ) }
           ) }
-          <Votes proposal={proposal}/>
+          <Votes proposal={proposal} historical={historical}/>
         </ProposalDetailsVoting>
         </ProposalDetailsVoting>
       </ProposalDetailsMain>
       </ProposalDetailsMain>
       <ProposalDetailsDiscussion>
       <ProposalDetailsDiscussion>
         <ProposalDiscussion
         <ProposalDiscussion
           proposalId={proposalId}
           proposalId={proposalId}
-          memberId={ iAmMember ? myMemberId : undefined }/>
+          memberId={ iAmMember ? myMemberId : undefined }
+          historical={historical}/>
       </ProposalDetailsDiscussion>
       </ProposalDetailsDiscussion>
     </div>
     </div>
   );
   );

+ 26 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx

@@ -1,12 +1,37 @@
 import React from 'react';
 import React from 'react';
 import { RouteComponentProps } from 'react-router-dom';
 import { RouteComponentProps } from 'react-router-dom';
 import ProposalDetails from './ProposalDetails';
 import ProposalDetails from './ProposalDetails';
-import { useProposalSubscription } from '@polkadot/joy-utils/react/hooks';
+import { usePromise, useProposalSubscription, useTransport } from '@polkadot/joy-utils/react/hooks';
 import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { useApi } from '@polkadot/react-hooks';
 import { useApi } from '@polkadot/react-hooks';
+import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
 
 
 type RouteParams = { id?: string | undefined };
 type RouteParams = { id?: string | undefined };
 
 
+export function HistoricalProposalFromId (props: RouteComponentProps<RouteParams>) {
+  const {
+    match: {
+      params: { id }
+    }
+  } = props;
+  const { api } = useApi();
+
+  const transport = useTransport();
+  const [proposal, error, loading] = usePromise(
+    () => transport.proposals.historicalProposalById(api.createType('ProposalId', id)),
+    null as ParsedProposal | null
+  );
+
+  return (
+    <PromiseComponent
+      error={error}
+      loading={loading}
+      message={'Fetching proposal...'}>
+      <ProposalDetails proposal={ proposal } proposalId={ id } historical/>
+    </PromiseComponent>
+  );
+}
+
 export default function ProposalFromId (props: RouteComponentProps<RouteParams>) {
 export default function ProposalFromId (props: RouteComponentProps<RouteParams>) {
   const {
   const {
     match: {
     match: {

+ 4 - 3
pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx

@@ -25,15 +25,16 @@ const ProposalDesc = styled.div`
 export type ProposalPreviewProps = {
 export type ProposalPreviewProps = {
   proposal: ParsedProposal;
   proposal: ParsedProposal;
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
+  historical?: boolean;
 };
 };
 
 
-export default function ProposalPreview ({ proposal, bestNumber }: ProposalPreviewProps) {
-  const extendedStatus = getExtendedStatus(proposal, bestNumber);
+export default function ProposalPreview ({ proposal, bestNumber, historical }: ProposalPreviewProps) {
+  const extendedStatus = getExtendedStatus(proposal, historical ? undefined : bestNumber);
 
 
   return (
   return (
     <Card
     <Card
       fluid
       fluid
-      href={`#/proposals/${proposal.id.toString()}`}>
+      href={`#/proposals/${historical ? 'historical/' : ''}${proposal.id.toString()}`}>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <Card.Content>
       <Card.Content>
         <Card.Header>
         <Card.Header>

+ 14 - 8
pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -13,6 +13,7 @@ import { Dropdown } from '@polkadot/react-components';
 
 
 type ProposalPreviewListProps = {
 type ProposalPreviewListProps = {
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
+  historical?: boolean;
 };
 };
 
 
 const FilterContainer = styled.div`
 const FilterContainer = styled.div`
@@ -22,6 +23,7 @@ const FilterContainer = styled.div`
   margin-bottom: 1.75rem;
   margin-bottom: 1.75rem;
 `;
 `;
 const StyledDropdown = styled(Dropdown)`
 const StyledDropdown = styled(Dropdown)`
+  margin-left: auto;
   .dropdown {
   .dropdown {
     width: 200px;
     width: 200px;
   }
   }
@@ -30,15 +32,17 @@ const PaginationBox = styled.div`
   margin-bottom: 1em;
   margin-bottom: 1em;
 `;
 `;
 
 
-function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
+function ProposalPreviewList ({ bestNumber, historical }: ProposalPreviewListProps) {
   const { pathname } = useLocation();
   const { pathname } = useLocation();
   const transport = useTransport();
   const transport = useTransport();
   const [activeFilter, setActiveFilter] = useState<ProposalStatusFilter>('All');
   const [activeFilter, setActiveFilter] = useState<ProposalStatusFilter>('All');
   const [currentPage, setCurrentPage] = useState<number>(1);
   const [currentPage, setCurrentPage] = useState<number>(1);
   const [proposalsBatch, error, loading] = usePromise<ProposalsBatch | undefined>(
   const [proposalsBatch, error, loading] = usePromise<ProposalsBatch | undefined>(
-    () => transport.proposals.proposalsBatch(activeFilter, currentPage),
+    () => historical
+      ? transport.proposals.historicalProposalsBatch(activeFilter, currentPage)
+      : transport.proposals.proposalsBatch(activeFilter, currentPage),
     undefined,
     undefined,
-    [activeFilter, currentPage]
+    [activeFilter, currentPage, historical]
   );
   );
 
 
   const filterOptions = proposalStatusFilters.map((filter) => ({
   const filterOptions = proposalStatusFilters.map((filter) => ({
@@ -54,10 +58,12 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
   return (
   return (
     <Container className='Proposal' fluid>
     <Container className='Proposal' fluid>
       <FilterContainer>
       <FilterContainer>
-        <Button primary as={Link} to={`${pathname}/new`}>
-          <Icon name='add' />
-          New proposal
-        </Button>
+        { !historical && (
+          <Button primary as={Link} to={`${pathname}/new`}>
+            <Icon name='add' />
+            New proposal
+          </Button>
+        ) }
         <StyledDropdown
         <StyledDropdown
           label='Proposal state'
           label='Proposal state'
           options={filterOptions}
           options={filterOptions}
@@ -85,7 +91,7 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
             ? (
             ? (
               <Card.Group>
               <Card.Group>
                 {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => (
                 {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => (
-                  <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} />
+                  <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} historical={historical}/>
                 ))}
                 ))}
               </Card.Group>
               </Card.Group>
             )
             )

+ 6 - 3
pioneer/packages/joy-proposals/src/Proposal/Votes.tsx

@@ -10,14 +10,17 @@ import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseCompon
 
 
 type VotesProps = {
 type VotesProps = {
   proposal: ParsedProposal;
   proposal: ParsedProposal;
+  historical?: boolean;
 };
 };
 
 
-export default function Votes ({ proposal: { id, votingResults } }: VotesProps) {
+export default function Votes ({ proposal: { id, votingResults }, historical }: VotesProps) {
   const transport = useTransport();
   const transport = useTransport();
   const [votes, error, loading] = usePromise<ProposalVotes | null>(
   const [votes, error, loading] = usePromise<ProposalVotes | null>(
-    () => transport.proposals.votes(id),
+    () => historical
+      ? transport.proposals.historicalVotes(id)
+      : transport.proposals.votes(id),
     null,
     null,
-    [votingResults]
+    [votingResults, historical]
   );
   );
 
 
   return (
   return (

+ 4 - 1
pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx

@@ -45,17 +45,20 @@ type ProposalDiscussionPostProps = {
   post: ParsedPost;
   post: ParsedPost;
   memberId?: MemberId;
   memberId?: MemberId;
   refreshDiscussion: () => void;
   refreshDiscussion: () => void;
+  historical?: boolean;
 }
 }
 
 
 export default function DiscussionPost ({
 export default function DiscussionPost ({
   post,
   post,
   memberId,
   memberId,
-  refreshDiscussion
+  refreshDiscussion,
+  historical
 }: ProposalDiscussionPostProps) {
 }: ProposalDiscussionPostProps) {
   const { author, authorId, text, createdAt, editsCount } = post;
   const { author, authorId, text, createdAt, editsCount } = post;
   const [editing, setEditing] = useState(false);
   const [editing, setEditing] = useState(false);
   const constraints = useTransport().proposals.discussionContraints();
   const constraints = useTransport().proposals.discussionContraints();
   const canEdit = (
   const canEdit = (
+    !historical &&
     memberId &&
     memberId &&
     post.postId &&
     post.postId &&
     authorId.toNumber() === memberId.toNumber() &&
     authorId.toNumber() === memberId.toNumber() &&

+ 11 - 5
pioneer/packages/joy-proposals/src/Proposal/discussion/ProposalDiscussion.tsx

@@ -11,16 +11,21 @@ import { MemberId } from '@joystream/types/members';
 type ProposalDiscussionProps = {
 type ProposalDiscussionProps = {
   proposalId: ProposalId;
   proposalId: ProposalId;
   memberId?: MemberId;
   memberId?: MemberId;
+  historical?: boolean;
 };
 };
 
 
 export default function ProposalDiscussion ({
 export default function ProposalDiscussion ({
   proposalId,
   proposalId,
-  memberId
+  memberId,
+  historical
 }: ProposalDiscussionProps) {
 }: ProposalDiscussionProps) {
   const transport = useTransport();
   const transport = useTransport();
   const [discussion, error, loading, refreshDiscussion] = usePromise<ParsedDiscussion | null | undefined>(
   const [discussion, error, loading, refreshDiscussion] = usePromise<ParsedDiscussion | null | undefined>(
-    () => transport.proposals.discussion(proposalId),
-    undefined
+    () => historical
+      ? transport.proposals.historicalDiscussion(proposalId)
+      : transport.proposals.discussion(proposalId),
+    undefined,
+    [historical]
   );
   );
   const constraints = transport.proposals.discussionContraints();
   const constraints = transport.proposals.discussionContraints();
 
 
@@ -36,14 +41,15 @@ export default function ProposalDiscussion ({
                 key={post.postId ? post.postId.toNumber() : `k-${key}`}
                 key={post.postId ? post.postId.toNumber() : `k-${key}`}
                 post={post}
                 post={post}
                 memberId={memberId}
                 memberId={memberId}
-                refreshDiscussion={refreshDiscussion}/>
+                refreshDiscussion={refreshDiscussion}
+                historical={historical}/>
             ))
             ))
           )
           )
             : (
             : (
               <Header as='h4' style={{ margin: '1rem 0' }}>Nothing has been posted here yet!</Header>
               <Header as='h4' style={{ margin: '1rem 0' }}>Nothing has been posted here yet!</Header>
             )
             )
           }
           }
-          { memberId && (
+          { (memberId && !historical) && (
             <DiscussionPostForm
             <DiscussionPostForm
               threadId={discussion.threadId}
               threadId={discussion.threadId}
               memberId={memberId}
               memberId={memberId}

+ 37 - 3
pioneer/packages/joy-proposals/src/index.tsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import React from 'react';
 import { Route, Switch, RouteComponentProps } from 'react-router';
 import { Route, Switch, RouteComponentProps } from 'react-router';
+import Tabs from '@polkadot/react-components/Tabs';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 import styled from 'styled-components';
 import styled from 'styled-components';
-import { Breadcrumb } from 'semantic-ui-react';
+import { Breadcrumb, Message } from 'semantic-ui-react';
 
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
@@ -27,6 +28,7 @@ import { SignalForm,
   TerminateWorkingGroupLeaderForm } from './forms';
   TerminateWorkingGroupLeaderForm } from './forms';
 import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import style from './style';
 import style from './style';
+import { HistoricalProposalFromId } from './Proposal/ProposalFromId';
 
 
 const ProposalsMain = styled.main`${style}`;
 const ProposalsMain = styled.main`${style}`;
 
 
@@ -42,11 +44,27 @@ const StyledHeader = styled.header`
 `;
 `;
 
 
 function App (props: Props): React.ReactElement<Props> {
 function App (props: Props): React.ReactElement<Props> {
-  const { basePath } = props;
+  const { basePath, t } = props;
+  const tabs = [
+    {
+      isRoot: true,
+      name: 'current',
+      text: t('Current')
+    },
+    {
+      name: 'historical',
+      text: t('Historical'),
+      hasParams: true
+    }
+  ];
 
 
   return (
   return (
     <ProposalsMain className='proposal--App'>
     <ProposalsMain className='proposal--App'>
       <StyledHeader>
       <StyledHeader>
+        <Tabs
+          basePath={basePath}
+          items={tabs}
+        />
         <Breadcrumb>
         <Breadcrumb>
           <Switch>
           <Switch>
             <Route path={`${basePath}/new/:type`} render={(props: RouteComponentProps<{ type?: string }>) => (
             <Route path={`${basePath}/new/:type`} render={(props: RouteComponentProps<{ type?: string }>) => (
@@ -63,11 +81,25 @@ function App (props: Props): React.ReactElement<Props> {
               <Breadcrumb.Divider icon='right angle' />
               <Breadcrumb.Divider icon='right angle' />
               <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
               <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
             </Route>
             </Route>
+            <Route path={`${basePath}/historical`}>
+              <Breadcrumb.Section active>Historical Proposals</Breadcrumb.Section>
+            </Route>
             <Route>
             <Route>
               <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
               <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
             </Route>
             </Route>
           </Switch>
           </Switch>
         </Breadcrumb>
         </Breadcrumb>
+        <Switch>
+          <Route path={`${basePath}/historical`}>
+            <Message warning active>
+              <Message.Header>{"You're in a historical proposals view."}</Message.Header>
+              <Message.Content>
+                The data presented here comes from previous Joystream testnet chain, which
+                means all proposals are read-only and can no longer be interacted with!
+              </Message.Content>
+            </Message>
+          </Route>
+        </Switch>
       </StyledHeader>
       </StyledHeader>
       <Switch>
       <Switch>
         <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
         <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
@@ -92,7 +124,9 @@ function App (props: Props): React.ReactElement<Props> {
         <Route exact path={`${basePath}/new/terminate-working-group-leader-role`} component={TerminateWorkingGroupLeaderForm} />
         <Route exact path={`${basePath}/new/terminate-working-group-leader-role`} component={TerminateWorkingGroupLeaderForm} />
         <Route exact path={`${basePath}/active`} component={NotDone} />
         <Route exact path={`${basePath}/active`} component={NotDone} />
         <Route exact path={`${basePath}/finalized`} component={NotDone} />
         <Route exact path={`${basePath}/finalized`} component={NotDone} />
-        <Route exact path={`${basePath}/:id`} component={ProposalFromId} />
+        <Route exact path={`${basePath}/historical`} render={() => <ProposalPreviewList historical={true}/>} />
+        <Route exact path={`${basePath}/historical/:id`} component={HistoricalProposalFromId}/>
+        <Route exact path={`${basePath}/:id`} component={ProposalFromId}/>
         <Route component={ProposalPreviewList} />
         <Route component={ProposalPreviewList} />
       </Switch>
       </Switch>
     </ProposalsMain>
     </ProposalsMain>

+ 157 - 12
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -8,13 +8,15 @@ import { ParsedProposal,
   DiscussionContraints,
   DiscussionContraints,
   ProposalStatusFilter,
   ProposalStatusFilter,
   ProposalsBatch,
   ProposalsBatch,
-  ParsedProposalDetails } from '../types/proposals';
+  ParsedProposalDetails,
+  RuntimeUpgradeProposalDetails,
+  HistoricalProposalData } from '../types/proposals';
 import { ParsedMember } from '../types/members';
 import { ParsedMember } from '../types/members';
 
 
 import BaseTransport from './base';
 import BaseTransport from './base';
 
 
 import { ThreadId, PostId } from '@joystream/types/common';
 import { ThreadId, PostId } from '@joystream/types/common';
-import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails } from '@joystream/types/proposals';
+import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails, ProposalStatus } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
 import { MemberId } from '@joystream/types/members';
 import { u32, Bytes, Null } from '@polkadot/types/';
 import { u32, Bytes, Null } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
 import { BalanceOf } from '@polkadot/types/interfaces';
@@ -31,6 +33,8 @@ import CouncilTransport from './council';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { APIQueryCache } from './APIQueryCache';
 import { APIQueryCache } from './APIQueryCache';
 
 
+import HISTORICAL_PROPOSALS from './static/historical-proposals.json';
+
 type ProposalDetailsCacheEntry = {
 type ProposalDetailsCacheEntry = {
   type: ProposalType;
   type: ProposalType;
   details: ParsedProposalDetails;
   details: ParsedProposalDetails;
@@ -143,21 +147,31 @@ export default class ProposalsTransport extends BaseTransport {
     return result.map(([proposalId]) => proposalId);
     return result.map(([proposalId]) => proposalId);
   }
   }
 
 
-  async proposalsBatch (status: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
+  private checkProposalStatusFilter (status: ProposalStatus, filter: ProposalStatusFilter) {
+    if (filter === 'All') {
+      return true;
+    }
+
+    if (filter === 'Active' && status.isOfType('Active')) {
+      return true;
+    }
+
+    if (!status.isOfType('Finalized')) {
+      return false;
+    }
+
+    return status.asType('Finalized').proposalStatus.type === filter;
+  }
+
+  async proposalsBatch (statusFilter: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
     const ids = (status === 'Active' ? await this.activeProposalsIds() : await this.proposalsIds())
     const ids = (status === 'Active' ? await this.activeProposalsIds() : await this.proposalsIds())
       .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
       .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
     let rawProposalsWithIds = (await Promise.all(ids.map((id) => this.rawProposalById(id))))
     let rawProposalsWithIds = (await Promise.all(ids.map((id) => this.rawProposalById(id))))
       .map((proposal, index) => ({ id: ids[index], proposal }));
       .map((proposal, index) => ({ id: ids[index], proposal }));
 
 
-    if (status !== 'All' && status !== 'Active') {
-      rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => {
-        if (!proposal.status.isOfType('Finalized')) {
-          return false;
-        }
-
-        return proposal.status.asType('Finalized').proposalStatus.type === status;
-      });
-    }
+    rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => (
+      this.checkProposalStatusFilter(proposal.status, statusFilter)
+    ));
 
 
     const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
     const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
 
 
@@ -283,4 +297,135 @@ export default class ProposalsTransport extends BaseTransport {
       maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
       maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
     };
     };
   }
   }
+
+  private replaceHistoricalProposalLinks (text: string) {
+    return text.replace(
+      /testnet\.joystream\.org\/#\/proposals\/([0-9]+)/g,
+      'testnet.joystream.org/#/proposals/historical/$1'
+    );
+  }
+
+  private parseHistoricalProposalDetails (proposal: HistoricalProposalData['proposal']): ParsedProposalDetails {
+    const { type, details } = proposal;
+
+    if (type === 'RuntimeUpgrade') {
+      return details as RuntimeUpgradeProposalDetails;
+    }
+
+    if (type === 'Text') {
+      details[0] = this.replaceHistoricalProposalLinks(details[0] as string);
+    }
+
+    return this.api.createType('ProposalDetails', {
+      [type]: details.length > 1 ? details : details[0]
+    });
+  }
+
+  // Historical proposals methods
+  private parseHistoricalProposal ({ proposal }: HistoricalProposalData): ParsedProposal {
+    return {
+      ...proposal,
+      id: this.api.createType('ProposalId', proposal.id),
+      type: proposal.type as ProposalType,
+      status: this.api.createType('ProposalStatus', proposal.status),
+      createdAt: new Date(proposal.createdAt),
+      parameters: this.api.createType('ProposalParameters', proposal.parameters),
+      votingResults: this.api.createType('VotingResults', proposal.votingResults),
+      details: this.parseHistoricalProposalDetails(proposal),
+      description: this.replaceHistoricalProposalLinks(proposal.description)
+    };
+  }
+
+  historicalProposalById (id: ProposalId): Promise<ParsedProposal> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id === id.toNumber());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalProposal(proposalData));
+      }
+    });
+  }
+
+  private parseHistoricalProposalDiscussion (proposalData: HistoricalProposalData): ParsedDiscussion {
+    const { discussion } = proposalData;
+
+    return {
+      ...discussion,
+      threadId: this.api.createType('ThreadId', discussion.threadId),
+      posts: discussion.posts.map((post) => ({
+        ...post,
+        postId: this.api.createType('PostId', post.postId),
+        threadId: this.api.createType('ThreadId', post.threadId),
+        createdAt: new Date(post.createdAt),
+        updatedAt: new Date(post.updatedAt),
+        author: this.api.createType('Membership', post.author),
+        authorId: this.api.createType('MemberId', post.authorId),
+        text: this.replaceHistoricalProposalLinks(post.text)
+      }))
+    };
+  }
+
+  historicalProposalsBatch (statusFilter: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
+    return new Promise((resolve, reject) => {
+      const filteredProposalsData = HISTORICAL_PROPOSALS
+        .sort((a, b) => b.proposal.id - a.proposal.id)
+        .filter(({ proposal }) => (
+          this.checkProposalStatusFilter(this.api.createType('ProposalStatus', proposal.status), statusFilter)
+        ));
+
+      const totalBatches = Math.ceil(filteredProposalsData.length / batchSize);
+      const proposalsInBatchData = filteredProposalsData.slice((batchNumber - 1) * batchSize, batchNumber * batchSize);
+      const parsedProposals: ParsedProposal[] = proposalsInBatchData
+        .map((proposalData) => this.parseHistoricalProposal(proposalData));
+
+      resolve({
+        batchNumber,
+        batchSize: parsedProposals.length,
+        totalBatches,
+        proposals: parsedProposals
+      });
+    });
+  }
+
+  historicalDiscussion (id: number|ProposalId): Promise<ParsedDiscussion | null> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id.toString() === id.toString());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalProposalDiscussion(proposalData));
+      }
+    });
+  }
+
+  private parseHistoricalVotes (proposalData: HistoricalProposalData): ProposalVotes {
+    const { votes } = proposalData;
+
+    return {
+      ...votes,
+      votes: votes.votes.map((vote) => ({
+        ...vote,
+        vote: this.api.createType('VoteKind', vote.vote),
+        member: {
+          ...vote.member,
+          memberId: this.api.createType('MemberId', vote.member.memberId)
+        }
+      }))
+    };
+  }
+
+  historicalVotes (proposalId: ProposalId): Promise<ProposalVotes> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id.toString() === proposalId.toString());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalVotes(proposalData));
+      }
+    });
+  }
 }
 }

File diff suppressed because it is too large
+ 19301 - 0
pioneer/packages/joy-utils/src/transport/static/historical-proposals.json


+ 43 - 0
pioneer/packages/joy-utils/src/types/proposals.ts

@@ -118,3 +118,46 @@ export type DiscussionContraints = {
   maxPostLength: number;
   maxPostLength: number;
   maxPostEdits: number;
   maxPostEdits: number;
 }
 }
+
+export type HistoricalParsedPost = {
+  postId: number;
+  threadId: number;
+  text: string;
+  createdAt: string;
+  createdAtBlock: number;
+  updatedAt: string;
+  updatedAtBlock: number;
+  author: ParsedMember;
+  authorId: number;
+  editsCount: number;
+}
+
+export type HistoricalProposalData = {
+  proposal: {
+    id: number,
+    parameters: unknown, // JSON of ProposalParameters
+    proposerId: number,
+    title: string,
+    description: string,
+    createdAt: string,
+    status: unknown, // JSON of ProposalStatus
+    votingResults: unknown, // JSON of VotingResults
+    details: unknown[], // JSON of ParsedProposalDetails
+    type: string,
+    proposer: ParsedMember,
+    createdAtBlock: number,
+    cancellationFee: number
+  },
+  votes: {
+    councilMembersLength: number,
+    votes: {
+      vote: number;
+      member: ParsedMember & { memberId: number },
+    }[]
+  },
+  discussion: {
+    title: string,
+    threadId: number,
+    posts: HistoricalParsedPost[]
+  }
+}

+ 5 - 4
pioneer/packages/page-accounts/src/Accounts/Account.tsx

@@ -14,7 +14,7 @@ import React, { useCallback, useContext, useState, useEffect } from 'react';
 import styled from 'styled-components';
 import styled from 'styled-components';
 import { ApiPromise } from '@polkadot/api';
 import { ApiPromise } from '@polkadot/api';
 import { getLedger } from '@polkadot/react-api';
 import { getLedger } from '@polkadot/react-api';
-import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
+import { AddressInfo, AddressMini, AddressSmall, Badge, Button, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
 import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
 import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
 import { Option } from '@polkadot/types';
 import { Option } from '@polkadot/types';
 import keyring from '@polkadot/ui-keyring';
 import keyring from '@polkadot/ui-keyring';
@@ -83,7 +83,7 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
   });
   });
   const multiInfos = useMultisigApprovals(address);
   const multiInfos = useMultisigApprovals(address);
   const proxyInfo = useProxies(address);
   const proxyInfo = useProxies(address);
-  const { flags: { isDevelopment, isExternal, isHardware, isInjected, isMultisig, isProxied }, genesisHash, identity, name: accName, onSetGenesisHash, tags } = useAccountInfo(address);
+  const { flags: { isDevelopment, isExternal, isHardware, isInjected, isMultisig, isProxied }, identity, name: accName, tags } = useAccountInfo(address);
   const [{ democracyUnlockTx }, setUnlockableIds] = useState<DemocracyUnlockable>({ democracyUnlockTx: null, ids: [] });
   const [{ democracyUnlockTx }, setUnlockableIds] = useState<DemocracyUnlockable>({ democracyUnlockTx: null, ids: [] });
   const [vestingVestTx, setVestingTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
   const [vestingVestTx, setVestingTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
   const [isVisible, setIsVisible] = useState(true);
   const [isVisible, setIsVisible] = useState(true);
@@ -531,12 +531,13 @@ function Account ({ account: { address, meta }, className = '', delegation, filt
                 {t('Delegate democracy votes')}
                 {t('Delegate democracy votes')}
               </Menu.Item>
               </Menu.Item>
             ])}
             ])}
-            <ChainLock
+            {/* Joystream specific - disallow "Only this network" to avoid confusion */}
+            {/* <ChainLock
               className='accounts--network-toggle'
               className='accounts--network-toggle'
               genesisHash={genesisHash}
               genesisHash={genesisHash}
               isDisabled={api.isDevelopment}
               isDisabled={api.isDevelopment}
               onChange={onSetGenesisHash}
               onChange={onSetGenesisHash}
-            />
+            /> */}
           </Menu>
           </Menu>
         </Popup>
         </Popup>
       </td>
       </td>

+ 6 - 5
pioneer/packages/page-accounts/src/Accounts/PasswordInput.tsx

@@ -3,7 +3,8 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 
 import { Modal, Password } from '@polkadot/react-components';
 import { Modal, Password } from '@polkadot/react-components';
-import keyring from '@polkadot/ui-keyring';
+// import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
 
 
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 
 
@@ -17,12 +18,12 @@ type Props = {
 
 
 export default function PasswordInput ({ onChange, onEnter, password }: Props): React.ReactElement {
 export default function PasswordInput ({ onChange, onEnter, password }: Props): React.ReactElement {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const [isPassValid, setPassValid] = useState<boolean>(false);
-  const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: false, password2: '' });
+  const [isPassValid, setPassValid] = useState<boolean>(true);
+  const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: true, password2: '' });
 
 
   const _onPasswordChange = useCallback(
   const _onPasswordChange = useCallback(
     (password: string) => {
     (password: string) => {
-      const isPassValid = keyring.isPassValid(password);
+      const isPassValid = isPasswordValid(password);
 
 
       setPassValid(isPassValid);
       setPassValid(isPassValid);
 
 
@@ -35,7 +36,7 @@ export default function PasswordInput ({ onChange, onEnter, password }: Props):
 
 
   const onPassword2Change = useCallback(
   const onPassword2Change = useCallback(
     (password2: string) => {
     (password2: string) => {
-      const isPass2Valid = keyring.isPassValid(password2) && (password2 === password);
+      const isPass2Valid = isPasswordValid(password2) && (password2 === password);
 
 
       setPassword2({ isPass2Valid, password2 });
       setPassword2({ isPass2Valid, password2 });
 
 

+ 3 - 2
pioneer/packages/page-accounts/src/Accounts/modals/Backup.tsx

@@ -6,6 +6,7 @@ import FileSaver from 'file-saver';
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 import { AddressRow, Button, Modal, Password } from '@polkadot/react-components';
 import { AddressRow, Button, Modal, Password } from '@polkadot/react-components';
 import keyring from '@polkadot/ui-keyring';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
 
 
 import { useTranslation } from '../../translate';
 import { useTranslation } from '../../translate';
 
 
@@ -17,9 +18,9 @@ interface Props {
 function Backup ({ address, onClose }: Props): React.ReactElement<Props> {
 function Backup ({ address, onClose }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isBusy, setIsBusy] = useState(false);
   const [isBusy, setIsBusy] = useState(false);
-  const [{ isPassTouched, password }, setPassword] = useState({ isPassTouched: false, password: '' });
+  const [{ isPassTouched, password }, setPassword] = useState({ isPassTouched: true, password: '' });
   const [backupFailed, setBackupFailed] = useState(false);
   const [backupFailed, setBackupFailed] = useState(false);
-  const isPassValid = !backupFailed && keyring.isPassValid(password);
+  const isPassValid = !backupFailed && isPasswordValid(password);
 
 
   const _onChangePass = useCallback(
   const _onChangePass = useCallback(
     (password: string): void => {
     (password: string): void => {

+ 7 - 6
pioneer/packages/page-accounts/src/Accounts/modals/ChangePass.tsx

@@ -5,6 +5,7 @@
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 import { AddressRow, Button, Modal, Password } from '@polkadot/react-components';
 import { AddressRow, Button, Modal, Password } from '@polkadot/react-components';
 import keyring from '@polkadot/ui-keyring';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
 
 
 import { useTranslation } from '../../translate';
 import { useTranslation } from '../../translate';
 
 
@@ -27,24 +28,24 @@ interface OldPass {
 function ChangePass ({ address, className = '', onClose }: Props): React.ReactElement<Props> {
 function ChangePass ({ address, className = '', onClose }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isBusy, setIsBusy] = useState(false);
   const [isBusy, setIsBusy] = useState(false);
-  const [newPass1, setNewPass1] = useState<NewPass>({ isValid: false, password: '' });
-  const [newPass2, setNewPass2] = useState<NewPass>({ isValid: false, password: '' });
-  const [{ isOldValid, oldPass }, setOldPass] = useState<OldPass>({ isOldValid: false, oldPass: '' });
+  const [newPass1, setNewPass1] = useState<NewPass>({ isValid: true, password: '' });
+  const [newPass2, setNewPass2] = useState<NewPass>({ isValid: true, password: '' });
+  const [{ isOldValid, oldPass }, setOldPass] = useState<OldPass>({ isOldValid: true, oldPass: '' });
 
 
   const _onChangeNew1 = useCallback(
   const _onChangeNew1 = useCallback(
     (password: string) =>
     (password: string) =>
-      setNewPass1({ isValid: keyring.isPassValid(password), password }),
+      setNewPass1({ isValid: isPasswordValid(password), password }),
     []
     []
   );
   );
 
 
   const _onChangeNew2 = useCallback(
   const _onChangeNew2 = useCallback(
     (password: string) =>
     (password: string) =>
-      setNewPass2({ isValid: keyring.isPassValid(password) && (newPass1.password === password), password }),
+      setNewPass2({ isValid: isPasswordValid(password) && (newPass1.password === password), password }),
     [newPass1]
     [newPass1]
   );
   );
 
 
   const _onChangeOld = useCallback(
   const _onChangeOld = useCallback(
-    (oldPass: string) => setOldPass({ isOldValid: keyring.isPassValid(oldPass), oldPass }),
+    (oldPass: string) => setOldPass({ isOldValid: isPasswordValid(oldPass), oldPass }),
     []
     []
   );
   );
 
 

+ 4 - 3
pioneer/packages/page-accounts/src/Accounts/modals/Create.tsx

@@ -138,12 +138,13 @@ export function downloadAccount ({ json, pair }: CreateResult): void {
   FileSaver.saveAs(blob, `${pair.address}.json`);
   FileSaver.saveAs(blob, `${pair.address}.json`);
 }
 }
 
 
-function createAccount (suri: string, pairType: KeypairType, { genesisHash, name, tags = [] }: CreateOptions, password: string, success: string): ActionStatus {
+function createAccount (suri: string, pairType: KeypairType, { name, tags = [] }: CreateOptions, password: string, success: string): ActionStatus {
   // we will fill in all the details below
   // we will fill in all the details below
   const status = { action: 'create' } as ActionStatus;
   const status = { action: 'create' } as ActionStatus;
 
 
   try {
   try {
-    const result = keyring.addUri(suri, password, { genesisHash, name, tags }, pairType);
+    // Joystream-specific - ignore genesisHash when creating new accounts
+    const result = keyring.addUri(suri, password, { name, tags }, pairType);
     const { address } = result.pair;
     const { address } = result.pair;
 
 
     status.account = address;
     status.account = address;
@@ -170,7 +171,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
   const [isConfirmationOpen, toggleConfirmation] = useToggle();
   const [isConfirmationOpen, toggleConfirmation] = useToggle();
   const [isBusy, setIsBusy] = useState(false);
   const [isBusy, setIsBusy] = useState(false);
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
-  const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
+  const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: true, password: '' });
   const isValid = !!address && !deriveError && isNameValid && isPasswordValid && isSeedValid;
   const isValid = !!address && !deriveError && isNameValid && isPasswordValid && isSeedValid;
   const seedOpt = useMemo(() => (
   const seedOpt = useMemo(() => (
     isDevelopment
     isDevelopment

+ 7 - 6
pioneer/packages/page-accounts/src/Accounts/modals/Derive.tsx

@@ -11,6 +11,7 @@ import { AddressRow, Button, Input, InputAddress, Modal, Password, StatusContext
 import { useApi, useDebounce, useToggle } from '@polkadot/react-hooks';
 import { useApi, useDebounce, useToggle } from '@polkadot/react-hooks';
 import keyring from '@polkadot/ui-keyring';
 import keyring from '@polkadot/ui-keyring';
 import { keyExtractPath } from '@polkadot/util-crypto';
 import { keyExtractPath } from '@polkadot/util-crypto';
+import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
 
 
 import { useTranslation } from '../../translate';
 import { useTranslation } from '../../translate';
 import { downloadAccount } from './Create';
 import { downloadAccount } from './Create';
@@ -83,9 +84,9 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
   const [isConfirmationOpen, toggleConfirmation] = useToggle();
   const [isConfirmationOpen, toggleConfirmation] = useToggle();
   const [{ isLocked, lockedError }, setIsLocked] = useState<LockState>({ isLocked: source.isLocked, lockedError: null });
   const [{ isLocked, lockedError }, setIsLocked] = useState<LockState>({ isLocked: source.isLocked, lockedError: null });
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
-  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: false, password: '' });
-  const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: false, password2: '' });
-  const [{ isRootValid, rootPass }, setRootPass] = useState({ isRootValid: false, rootPass: '' });
+  const [{ isPassValid, password }, setPassword] = useState({ isPassValid: true, password: '' });
+  const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: true, password2: '' });
+  const [{ isRootValid, rootPass }, setRootPass] = useState({ isRootValid: true, rootPass: '' });
   const [suri, setSuri] = useState('');
   const [suri, setSuri] = useState('');
   const debouncedSuri = useDebounce(suri);
   const debouncedSuri = useDebounce(suri);
   const isValid = !!address && !deriveError && isNameValid && isPassValid && isPass2Valid;
   const isValid = !!address && !deriveError && isNameValid && isPassValid && isPass2Valid;
@@ -115,18 +116,18 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
   );
   );
 
 
   const _onChangePass = useCallback(
   const _onChangePass = useCallback(
-    (password: string) => setPassword({ isPassValid: keyring.isPassValid(password), password }),
+    (password: string) => setPassword({ isPassValid: isPasswordValid(password), password }),
     []
     []
   );
   );
 
 
   const _onChangePass2 = useCallback(
   const _onChangePass2 = useCallback(
-    (password2: string) => setPassword2({ isPass2Valid: keyring.isPassValid(password2) && (password2 === password), password2 }),
+    (password2: string) => setPassword2({ isPass2Valid: isPasswordValid(password2) && (password2 === password), password2 }),
     [password]
     [password]
   );
   );
 
 
   const _onChangeRootPass = useCallback(
   const _onChangeRootPass = useCallback(
     (rootPass: string): void => {
     (rootPass: string): void => {
-      setRootPass({ isRootValid: !!rootPass, rootPass });
+      setRootPass({ isRootValid: isPasswordValid(rootPass), rootPass });
       setIsLocked(({ isLocked }) => ({ isLocked, lockedError: null }));
       setIsLocked(({ isLocked }) => ({ isLocked, lockedError: null }));
     },
     },
     []
     []

+ 3 - 2
pioneer/packages/page-accounts/src/Accounts/modals/Import.tsx

@@ -10,6 +10,7 @@ import React, { useCallback, useState } from 'react';
 import { AddressRow, Button, InputAddress, InputFile, Modal, Password } from '@polkadot/react-components';
 import { AddressRow, Button, InputAddress, InputFile, Modal, Password } from '@polkadot/react-components';
 import { isObject, u8aToString } from '@polkadot/util';
 import { isObject, u8aToString } from '@polkadot/util';
 import keyring from '@polkadot/ui-keyring';
 import keyring from '@polkadot/ui-keyring';
+import { isPasswordValid } from '@polkadot/joy-utils/functions/accounts';
 
 
 import { useTranslation } from '../../translate';
 import { useTranslation } from '../../translate';
 
 
@@ -55,7 +56,7 @@ function Import ({ className = '', onClose, onStatusChange }: Props): React.Reac
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isBusy, setIsBusy] = useState(false);
   const [isBusy, setIsBusy] = useState(false);
   const [{ address, isFileValid, json }, setFile] = useState<FileState>({ address: null, isFileValid: false, json: null });
   const [{ address, isFileValid, json }, setFile] = useState<FileState>({ address: null, isFileValid: false, json: null });
-  const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: false, password: '' });
+  const [{ isPassValid, password }, setPass] = useState<PassState>({ isPassValid: true, password: '' });
 
 
   const _onChangeFile = useCallback(
   const _onChangeFile = useCallback(
     (file: Uint8Array) => setFile(parseFile(file)),
     (file: Uint8Array) => setFile(parseFile(file)),
@@ -63,7 +64,7 @@ function Import ({ className = '', onClose, onStatusChange }: Props): React.Reac
   );
   );
 
 
   const _onChangePass = useCallback(
   const _onChangePass = useCallback(
-    (password: string) => setPass({ isPassValid: keyring.isPassValid(password), password }),
+    (password: string) => setPass({ isPassValid: isPasswordValid(password), password }),
     []
     []
   );
   );
 
 

+ 1 - 1
pioneer/packages/react-api/src/Api.tsx

@@ -131,7 +131,7 @@ async function loadOnReady (api: ApiPromise, store: KeyringStore | undefined, ty
 
 
   // finally load the keyring
   // finally load the keyring
   keyring.loadAll({
   keyring.loadAll({
-    genesisHash: api.genesisHash,
+    // genesisHash: api.genesisHash, Joystream-specific - Don't care about genesis hash when loading accounts
     isDevelopment,
     isDevelopment,
     ss58Format,
     ss58Format,
     store,
     store,

+ 4 - 4
pioneer/packages/react-components/src/InputNumber.tsx

@@ -120,12 +120,12 @@ function inputToBn (input: string, si: SiDef | null, bitLength: BitLength, isZer
     }
     }
 
 
     const div = new BN(input.replace(/\.\d*$/, ''));
     const div = new BN(input.replace(/\.\d*$/, ''));
-    const modString = input.replace(/^\d+\./, '');
-    const mod = new BN(modString);
+    // const modString = input.replace(/^\d+\./, '');
+    // const mod = new BN(modString);
 
 
     result = div
     result = div
-      .mul(BN_TEN.pow(siPower))
-      .add(mod.mul(BN_TEN.pow(new BN(basePower + siUnitPower - modString.length))));
+      .mul(BN_TEN.pow(siPower));
+    // .add(mod.mul(BN_TEN.pow(new BN(basePower + siUnitPower - modString.length))));
   } else {
   } else {
     result = new BN(input.replace(/[^\d]/g, ''))
     result = new BN(input.replace(/[^\d]/g, ''))
       .mul(BN_TEN.pow(siPower));
       .mul(BN_TEN.pow(siPower));

+ 8 - 2
pioneer/scripts/dev-build-ts.js

@@ -12,6 +12,7 @@ const cpx = require('cpx');
 const fs = require('fs');
 const fs = require('fs');
 const mkdirp = require('mkdirp');
 const mkdirp = require('mkdirp');
 const path = require('path');
 const path = require('path');
+const IS_LIVE = !(process.env.IS_LIVE === false || process.env.IS_LIVE === 'false');
 
 
 const CPX = ['css', 'gif', 'hbs', 'jpg', 'js', 'png', 'svg', 'd.ts']
 const CPX = ['css', 'gif', 'hbs', 'jpg', 'js', 'png', 'svg', 'd.ts']
   .map((ext) => `src/**/*.${ext}`)
   .map((ext) => `src/**/*.${ext}`)
@@ -55,7 +56,8 @@ async function buildJs (dir) {
 
 
     if (fs.existsSync(path.join(process.cwd(), 'public'))) {
     if (fs.existsSync(path.join(process.cwd(), 'public'))) {
       buildWebpack(dir);
       buildWebpack(dir);
-    } else {
+      // Skip building anything else if we're not going for a LIVE build
+    } else if (IS_LIVE) {
       await buildBabel(dir);
       await buildBabel(dir);
     }
     }
 
 
@@ -66,6 +68,8 @@ async function buildJs (dir) {
 async function main () {
 async function main () {
   execSync('yarn polkadot-dev-clean-build');
   execSync('yarn polkadot-dev-clean-build');
 
 
+  console.log('IS_LIVE:', process.env.IS_LIVE);
+
   // By default the entry point is pioneer/, so here we move to pioneer/packages
   // By default the entry point is pioneer/, so here we move to pioneer/packages
   process.chdir('packages');
   process.chdir('packages');
 
 
@@ -74,7 +78,9 @@ async function main () {
   // This caused the build folder to end up in the root directory of the monorepo (instead of "pioneer/build")
   // This caused the build folder to end up in the root directory of the monorepo (instead of "pioneer/build")
   //
   //
   // execSync('yarn polkadot-exec-tsc --emitDeclarationOnly --outdir ../build');
   // execSync('yarn polkadot-exec-tsc --emitDeclarationOnly --outdir ../build');
-  execSync('yarn tsc --emitDeclarationOnly --outdir ./build');
+  if (IS_LIVE) {
+    execSync('yarn tsc --emitDeclarationOnly --outdir ./build');
+  }
 
 
   const dirs = fs
   const dirs = fs
     .readdirSync('.')
     .readdirSync('.')

+ 1 - 0
runtime-modules/membership/src/lib.rs

@@ -401,6 +401,7 @@ decl_module! {
                 });
                 });
 
 
                 membership.root_account = new_root_account.clone();
                 membership.root_account = new_root_account.clone();
+                <MembershipById<T>>::insert(member_id, membership);
                 Self::deposit_event(RawEvent::MemberSetRootAccount(member_id, new_root_account));
                 Self::deposit_event(RawEvent::MemberSetRootAccount(member_id, new_root_account));
             }
             }
         }
         }

+ 1 - 1
runtime-modules/membership/src/tests.rs

@@ -332,7 +332,7 @@ fn set_root_account() {
 
 
             let membership = Members::membership(member_id);
             let membership = Members::membership(member_id);
 
 
-            assert_eq!(ALICE_ACCOUNT_ID, membership.root_account);
+            assert_eq!(ALICE_NEW_ROOT_ACCOUNT, membership.root_account);
 
 
             assert!(<crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
             assert!(<crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
         });
         });

+ 5 - 2
runtime/CHANGELOG.md

@@ -1,11 +1,14 @@
-### Version 6.21.0 - Constantinople runtime upgrade B (Nicaea) - July 29 2020
+### Version 7.4.0 - Alexandria - new chain - September 21 2020
+- Update to substrate v2.0.0-rc4
+
+### Version 6.21.0 - (Constantinople) runtime upgrade B (Nicaea) - July 29 2020
 
 
 - Introduction of general Working Group runtime module
 - Introduction of general Working Group runtime module
 - Adds a new instance of the working group module - the Storage Working Group which
 - Adds a new instance of the working group module - the Storage Working Group which
   replaces the old actors module for managing the Storge Provider enrollment process
   replaces the old actors module for managing the Storge Provider enrollment process
 - New governance proposals to support new working groups
 - New governance proposals to support new working groups
 
 
-### Version 6.15.0 - Constantinople runtime upgrade A - June 2020
+### Version 6.15.0 - (Constantinople) runtime upgrade A - June 2020
 
 
 - Updated runtime to sort out type name clashes between the proposal discussion module
 - Updated runtime to sort out type name clashes between the proposal discussion module
   and forum module, in preparing to roll out proposal discussion system in pioneer.
   and forum module, in preparing to roll out proposal discussion system in pioneer.

+ 1 - 1
runtime/Cargo.toml

@@ -4,7 +4,7 @@ edition = '2018'
 name = 'joystream-node-runtime'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '7.3.0'
+version = '7.5.1'
 
 
 [dependencies]
 [dependencies]
 # Third-party dependencies
 # Third-party dependencies

+ 2 - 2
runtime/src/lib.rs

@@ -70,8 +70,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
     spec_name: create_runtime_str!("joystream-node"),
     spec_name: create_runtime_str!("joystream-node"),
     impl_name: create_runtime_str!("joystream-node"),
     impl_name: create_runtime_str!("joystream-node"),
     authoring_version: 7,
     authoring_version: 7,
-    spec_version: 3,
-    impl_version: 0,
+    spec_version: 5,
+    impl_version: 1,
     apis: crate::runtime_api::EXPORTED_RUNTIME_API_VERSIONS,
     apis: crate::runtime_api::EXPORTED_RUNTIME_API_VERSIONS,
     transaction_version: 1,
     transaction_version: 1,
 };
 };

+ 1 - 1
storage-node/packages/colossus/bin/cli.js

@@ -18,7 +18,7 @@ const debug = require('debug')('joystream:colossus')
 const PROJECT_ROOT = path.resolve(__dirname, '..')
 const PROJECT_ROOT = path.resolve(__dirname, '..')
 
 
 // Number of milliseconds to wait between synchronization runs.
 // Number of milliseconds to wait between synchronization runs.
-const SYNC_PERIOD_MS = 300000 // 5min
+const SYNC_PERIOD_MS = 120000 // 2min
 
 
 // Parse CLI
 // Parse CLI
 const FLAG_DEFINITIONS = {
 const FLAG_DEFINITIONS = {

+ 85 - 0
storage-node/packages/colossus/lib/middleware/ipfs_proxy.js

@@ -0,0 +1,85 @@
+const { createProxyMiddleware } = require('http-proxy-middleware')
+const debug = require('debug')('joystream:ipfs-proxy')
+const mime = require('mime-types')
+
+/* 
+For this proxying to work correctly, ensure IPFS HTTP Gateway is configured as a path gateway:
+This can be done manually with the following command:
+
+  $ ipfs config --json Gateway.PublicGateways '{"localhost": null }' 
+  
+The implicit default config is below which is not what we want!
+
+  $ ipfs config --json Gateway.PublicGateways '{
+    "localhost": {
+        "Paths": ["/ipfs", "/ipns"],
+        "UseSubdomains": true
+      }
+    }'
+
+https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#gateway
+*/
+
+const pathFilter = function (path, req) {
+  // we get the full path here so it needs to match the path where
+  // it is used by the openapi initializer
+  return path.match('^/asset/v0') && (req.method === 'GET' || req.method === 'HEAD')
+}
+
+const createPathRewriter = (resolve) => {
+  return async (_path, req) => {
+    // we expect the handler was used in openapi/express path with an id in the path:
+    // "/asset/v0/:id"
+    // TODO: catch and deal with hash == undefined if content id not found
+    const contentId = req.params.id
+    const hash = await resolve(contentId)
+    return `/ipfs/${hash}`
+  }
+}
+
+const createResolver = (storage) => {
+  return async (id) => await storage.resolveContentIdWithTimeout(5000, id)
+}
+
+const createProxy = (storage) => {
+  const pathRewrite = createPathRewriter(createResolver(storage))
+
+  return createProxyMiddleware(pathFilter, {
+    // Default path to local IPFS HTTP GATEWAY
+    target: 'http://localhost:8080/',
+    pathRewrite,
+    onProxyRes: function (proxRes, req, res) {
+      /*
+        Make sure the reverse proxy used infront of colosss (nginx/caddy) Does not duplicate
+        these headers to prevent some browsers getting confused especially
+        with duplicate access-control-allow-origin headers!
+        'accept-ranges': 'bytes',
+        'access-control-allow-headers': 'Content-Type, Range, User-Agent, X-Requested-With',
+        'access-control-allow-methods': 'GET',
+        'access-control-allow-origin': '*',
+        'access-control-expose-headers': 'Content-Range, X-Chunked-Output, X-Stream-Output',
+      */
+
+      if (proxRes.statusCode === 301) {
+        // capture redirect when IPFS HTTP Gateway is configured with 'UseDomains':true
+        // and treat it as an error.
+        console.error('IPFS HTTP Gateway is configured for "UseSubdomains". Killing stream')
+        res.status(500).end()
+        proxRes.destroy()
+      } else {
+        // Handle downloading as attachment /asset/v0/:id?download
+        if (req.query.download) {
+          const contentId = req.params.id
+          const contentType = proxRes.headers['content-type']
+          const ext = mime.extension(contentType) || 'bin'
+          const fileName = `${contentId}.${ext}`
+          proxRes.headers['Content-Disposition'] = `attachment; filename=${fileName}`
+        }
+      }
+    },
+  })
+}
+
+module.exports = {
+  createProxy,
+}

+ 108 - 65
storage-node/packages/colossus/lib/sync.js

@@ -19,83 +19,117 @@
 'use strict'
 'use strict'
 
 
 const debug = require('debug')('joystream:sync')
 const debug = require('debug')('joystream:sync')
+const _ = require('lodash')
+const { ContentId } = require('@joystream/types/media')
+// The number of concurrent sync sessions allowed. Must be greater than zero.
+const MAX_CONCURRENT_SYNC_ITEMS = 20
+
+async function syncContent({ api, storage, contentBeingSynced, contentCompleteSynced }) {
+  const knownEncodedContentIds = (await api.assets.getKnownContentIds()).map((id) => id.encode())
+
+  // Select ids which we have not yet fully synced
+  const needsSync = knownEncodedContentIds
+    .filter((id) => !contentCompleteSynced.has(id))
+    .filter((id) => !contentBeingSynced.has(id))
+
+  // Since we are limiting concurrent content ids being synced, to ensure
+  // better distribution of content across storage nodes during a potentially long
+  // sync process we don't want all nodes to replicate items in the same order, so
+  // we simply shuffle.
+  const candidatesForSync = _.shuffle(needsSync)
+
+  // TODO: get the data object
+  // make sure the data object was Accepted by the liaison,
+  // don't just blindly attempt to fetch them
+  while (contentBeingSynced.size < MAX_CONCURRENT_SYNC_ITEMS && candidatesForSync.length) {
+    const id = candidatesForSync.shift()
 
 
-async function syncCallback(api, storage) {
-  // The first step is to gather all data objects from chain.
-  // TODO: in future, limit to a configured tranche
-  // FIXME this isn't actually on chain yet, so we'll fake it.
-  const knownContentIds = (await api.assets.getKnownContentIds()) || []
+    try {
+      contentBeingSynced.set(id)
+      const contentId = ContentId.decode(api.api.registry, id)
+      await storage.synchronize(contentId, (err, status) => {
+        if (err) {
+          contentBeingSynced.delete(id)
+          debug(`Error Syncing ${err}`)
+        } else if (status.synced) {
+          contentBeingSynced.delete(id)
+          contentCompleteSynced.set(id)
+        }
+      })
+    } catch (err) {
+      // Most likely failed to resolve the content id
+      debug(`Failed calling synchronize ${err}`)
+      contentBeingSynced.delete(id)
+    }
+  }
+}
 
 
+async function createNewRelationships({ api, contentCompleteSynced }) {
   const roleAddress = api.identities.key.address
   const roleAddress = api.identities.key.address
   const providerId = api.storageProviderId
   const providerId = api.storageProviderId
 
 
-  // Iterate over all sync objects, and ensure they're synced.
-  const allChecks = knownContentIds.map(async (contentId) => {
-    // eslint-disable-next-line prefer-const
-    let { relationship, relationshipId } = await api.assets.getStorageRelationshipAndId(providerId, contentId)
-
-    // get the data object
-    // make sure the data object was Accepted by the liaison,
-    // don't just blindly attempt to fetch them
-
-    let fileLocal
-    try {
-      // check if we have content or not
-      const stats = await storage.stat(contentId)
-      fileLocal = stats.local
-    } catch (err) {
-      // on error stating or timeout
-      debug(err.message)
-      // we don't have content if we can't stat it
-      fileLocal = false
-    }
+  // Create new relationships for synced content if required and
+  // compose list of relationship ids to be set to ready.
+  return (
+    await Promise.all(
+      [...contentCompleteSynced.keys()].map(async (id) => {
+        const contentId = ContentId.decode(api.api.registry, id)
+        const { relationship, relationshipId } = await api.assets.getStorageRelationshipAndId(providerId, contentId)
+
+        if (relationship) {
+          // maybe prior transaction to set ready failed for some reason..
+          if (!relationship.ready) {
+            return relationshipId
+          }
+        } else {
+          // create relationship
+          debug(`Creating new storage relationship for ${id}`)
+          try {
+            return await api.assets.createStorageRelationship(roleAddress, providerId, contentId)
+          } catch (err) {
+            debug(`Error creating new storage relationship ${id}: ${err.stack}`)
+          }
+        }
+
+        return null
+      })
+    )
+  ).filter((id) => id !== null)
+}
 
 
-    if (!fileLocal) {
-      try {
-        await storage.synchronize(contentId)
-      } catch (err) {
-        // duplicate logging
-        // debug(err.message)
-        return
-      }
-      // why are we returning, if we synced the file
-      return
-    }
+async function setRelationshipsReady({ api, relationshipIds }) {
+  const roleAddress = api.identities.key.address
+  const providerId = api.storageProviderId
 
 
-    if (!relationship) {
-      // create relationship
-      debug(`Creating new storage relationship for ${contentId.encode()}`)
-      try {
-        relationshipId = await api.assets.createStorageRelationship(roleAddress, providerId, contentId)
-        await api.assets.toggleStorageRelationshipReady(roleAddress, providerId, relationshipId, true)
-      } catch (err) {
-        debug(`Error creating new storage relationship ${contentId.encode()}: ${err.stack}`)
-      }
-    } else if (!relationship.ready) {
-      debug(`Updating storage relationship to ready for ${contentId.encode()}`)
-      // update to ready. (Why would there be a relationship set to ready: false?)
+  return Promise.all(
+    relationshipIds.map(async (relationshipId) => {
       try {
       try {
         await api.assets.toggleStorageRelationshipReady(roleAddress, providerId, relationshipId, true)
         await api.assets.toggleStorageRelationshipReady(roleAddress, providerId, relationshipId, true)
       } catch (err) {
       } catch (err) {
-        debug(`Error setting relationship ready ${contentId.encode()}: ${err.stack}`)
+        debug('Error setting relationship ready')
       }
       }
-    } else {
-      // we already have content and a ready relationship set. No need to do anything
-      // debug(`content already stored locally ${contentId.encode()}`);
-    }
-  })
-
-  return Promise.all(allChecks)
+    })
+  )
 }
 }
 
 
-async function syncPeriodic(api, flags, storage) {
+async function syncPeriodic({ api, flags, storage, contentBeingSynced, contentCompleteSynced }) {
+  const retry = () => {
+    setTimeout(syncPeriodic, flags.syncPeriod, {
+      api,
+      flags,
+      storage,
+      contentBeingSynced,
+      contentCompleteSynced,
+    })
+  }
+
   try {
   try {
-    debug('Starting sync run...')
+    debug('Sync run started.')
 
 
     const chainIsSyncing = await api.chainIsSyncing()
     const chainIsSyncing = await api.chainIsSyncing()
     if (chainIsSyncing) {
     if (chainIsSyncing) {
       debug('Chain is syncing. Postponing sync run.')
       debug('Chain is syncing. Postponing sync run.')
-      return setTimeout(syncPeriodic, flags.syncPeriod, api, flags, storage)
+      return retry()
     }
     }
 
 
     const recommendedBalance = await api.providerHasMinimumBalance(300)
     const recommendedBalance = await api.providerHasMinimumBalance(300)
@@ -106,20 +140,29 @@ async function syncPeriodic(api, flags, storage) {
     const sufficientBalance = await api.providerHasMinimumBalance(100)
     const sufficientBalance = await api.providerHasMinimumBalance(100)
     if (!sufficientBalance) {
     if (!sufficientBalance) {
       debug('Provider role account does not have sufficient balance. Postponing sync run!')
       debug('Provider role account does not have sufficient balance. Postponing sync run!')
-      return setTimeout(syncPeriodic, flags.syncPeriod, api, flags, storage)
+      return retry()
     }
     }
 
 
-    await syncCallback(api, storage)
-    debug('sync run complete')
+    await syncContent({ api, storage, contentBeingSynced, contentCompleteSynced })
+    const relationshipIds = await createNewRelationships({ api, contentCompleteSynced })
+    await setRelationshipsReady({ api, relationshipIds })
+
+    debug(`Sync run completed, set ${relationshipIds.length} new relationships to ready`)
   } catch (err) {
   } catch (err) {
-    debug(`Error in syncPeriodic ${err.stack}`)
+    debug(`Error in sync run ${err.stack}`)
   }
   }
+
   // always try again
   // always try again
-  setTimeout(syncPeriodic, flags.syncPeriod, api, flags, storage)
+  retry()
 }
 }
 
 
 function startSyncing(api, flags, storage) {
 function startSyncing(api, flags, storage) {
-  syncPeriodic(api, flags, storage)
+  // ids of content currently being synced
+  const contentBeingSynced = new Map()
+  // ids of content that completed sync and may require creating a new relationship
+  const contentCompleteSynced = new Map()
+
+  syncPeriodic({ api, flags, storage, contentBeingSynced, contentCompleteSynced })
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 4 - 2
storage-node/packages/colossus/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@joystream/colossus",
   "name": "@joystream/colossus",
   "private": true,
   "private": true,
-  "version": "0.2.0",
+  "version": "0.3.0",
   "description": "Colossus - Joystream Storage Node",
   "description": "Colossus - Joystream Storage Node",
   "author": "Joystream",
   "author": "Joystream",
   "homepage": "https://github.com/Joystream/joystream",
   "homepage": "https://github.com/Joystream/joystream",
@@ -50,17 +50,19 @@
     "temp": "^0.9.0"
     "temp": "^0.9.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@joystream/storage-runtime-api": "^0.1.0",
     "@joystream/storage-node-backend": "^0.1.0",
     "@joystream/storage-node-backend": "^0.1.0",
+    "@joystream/storage-runtime-api": "^0.1.0",
     "@joystream/storage-utils": "^0.1.0",
     "@joystream/storage-utils": "^0.1.0",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
     "chalk": "^2.4.2",
     "chalk": "^2.4.2",
     "cors": "^2.8.5",
     "cors": "^2.8.5",
     "express-openapi": "^4.6.1",
     "express-openapi": "^4.6.1",
     "figlet": "^1.2.1",
     "figlet": "^1.2.1",
+    "http-proxy-middleware": "^1.0.5",
     "js-yaml": "^3.13.1",
     "js-yaml": "^3.13.1",
     "lodash": "^4.17.11",
     "lodash": "^4.17.11",
     "meow": "^7.0.1",
     "meow": "^7.0.1",
+    "mime-types": "^2.1.27",
     "multer": "^1.4.1",
     "multer": "^1.4.1",
     "si-prefix": "^0.2.0"
     "si-prefix": "^0.2.0"
   }
   }

+ 14 - 82
storage-node/packages/colossus/paths/asset/v0/{id}.js

@@ -18,12 +18,9 @@
 
 
 'use strict'
 'use strict'
 
 
-const path = require('path')
-
 const debug = require('debug')('joystream:colossus:api:asset')
 const debug = require('debug')('joystream:colossus:api:asset')
-
-const utilRanges = require('@joystream/storage-utils/ranges')
 const filter = require('@joystream/storage-node-backend/filter')
 const filter = require('@joystream/storage-node-backend/filter')
+const ipfsProxy = require('../../../lib/middleware/ipfs_proxy')
 
 
 function errorHandler(response, err, code) {
 function errorHandler(response, err, code) {
   debug(err)
   debug(err)
@@ -31,6 +28,9 @@ function errorHandler(response, err, code) {
 }
 }
 
 
 module.exports = function (storage, runtime) {
 module.exports = function (storage, runtime) {
+  // Creat the IPFS HTTP Gateway proxy middleware
+  const proxy = ipfsProxy.createProxy(storage)
+
   const doc = {
   const doc = {
     // parameters for all operations in this path
     // parameters for all operations in this path
     parameters: [
     parameters: [
@@ -45,34 +45,6 @@ module.exports = function (storage, runtime) {
       },
       },
     ],
     ],
 
 
-    // Head: report that ranges are OK
-    async head(req, res) {
-      const id = req.params.id
-
-      // Open file
-      try {
-        const size = await storage.size(id)
-        const stream = await storage.open(id, 'r')
-        const type = stream.fileInfo.mimeType
-
-        // Close the stream; we don't need to fetch the file (if we haven't
-        // already). Then return result.
-        stream.destroy()
-
-        res.status(200)
-        res.contentType(type)
-        res.header('Content-Disposition', 'inline')
-        res.header('Content-Transfer-Encoding', 'binary')
-        res.header('Accept-Ranges', 'bytes')
-        if (size > 0) {
-          res.header('Content-Length', size)
-        }
-        res.send()
-      } catch (err) {
-        errorHandler(res, err, err.code)
-      }
-    },
-
     // Put for uploads
     // Put for uploads
     async put(req, res) {
     async put(req, res) {
       const id = req.params.id // content id
       const id = req.params.id // content id
@@ -184,61 +156,21 @@ module.exports = function (storage, runtime) {
       }
       }
     },
     },
 
 
-    // Get content
     async get(req, res) {
     async get(req, res) {
-      const id = req.params.id
-      const download = req.query.download
-
-      // Parse range header
-      let ranges
-      if (!download) {
-        try {
-          const rangeHeader = req.headers.range
-          ranges = utilRanges.parse(rangeHeader)
-        } catch (err) {
-          // Do nothing; it's ok to ignore malformed ranges and respond with the
-          // full content according to https://www.rfc-editor.org/rfc/rfc7233.txt
-        }
-        if (ranges && ranges.unit !== 'bytes') {
-          // Ignore ranges that are not byte units.
-          ranges = undefined
-        }
-      }
-      debug('Requested range(s) is/are', ranges)
-
-      // Open file
-      try {
-        const size = await storage.size(id)
-        const stream = await storage.open(id, 'r')
-
-        // Add a file extension to download requests if necessary. If the file
-        // already contains an extension, don't add one.
-        let sendName = id
-        const type = stream.fileInfo.mimeType
-        if (download) {
-          let ext = path.extname(sendName)
-          if (!ext) {
-            ext = stream.fileInfo.ext
-            if (ext) {
-              sendName = `${sendName}.${ext}`
-            }
-          }
-        }
+      proxy(req, res)
+    },
 
 
-        const opts = {
-          name: sendName,
-          type,
-          size,
-          ranges,
-          download,
-        }
-        utilRanges.send(res, stream, opts)
-      } catch (err) {
-        errorHandler(res, err, err.code)
-      }
+    async head(req, res) {
+      proxy(req, res)
     },
     },
   }
   }
 
 
+  // doc.get = proxy
+  // doc.head = proxy
+  // Note: Adding the middleware this way is causing problems!
+  // We are loosing some information from the request, specifically req.query.download parameters for some reason.
+  // Does it have to do with how/when the apiDoc is being processed? binding issue?
+
   // OpenAPI specs
   // OpenAPI specs
   doc.get.apiDoc = {
   doc.get.apiDoc = {
     description: 'Download an asset.',
     description: 'Download an asset.',

+ 1 - 1
storage-node/packages/colossus/paths/discover/v0/{id}.js

@@ -12,7 +12,7 @@ module.exports = function (runtime) {
         name: 'id',
         name: 'id',
         in: 'path',
         in: 'path',
         required: true,
         required: true,
-        description: 'Actor accouuntId',
+        description: 'Storage Provider Id',
         schema: {
         schema: {
           type: 'string', // integer ?
           type: 'string', // integer ?
         },
         },

+ 1 - 29
storage-node/packages/runtime-api/index.js

@@ -61,9 +61,6 @@ class RuntimeApi {
 
 
     this.asyncLock = new AsyncLock()
     this.asyncLock = new AsyncLock()
 
 
-    // Keep track locally of account nonces.
-    this.nonces = {}
-
     // The storage provider id to use
     // The storage provider id to use
     this.storageProviderId = parseInt(options.storageProviderId) // u64 instead ?
     this.storageProviderId = parseInt(options.storageProviderId) // u64 instead ?
 
 
@@ -148,28 +145,6 @@ class RuntimeApi {
     })
     })
   }
   }
 
 
-  // Get cached nonce and use unless system nonce is greater, to avoid stale nonce if
-  // there was a long gap in time between calls to signAndSend during which an external app
-  // submitted a transaction.
-  async selectBestNonce(accountId) {
-    const cachedNonce = this.nonces[accountId]
-    // In future use this rpc method to take the pending tx pool into account when fetching the nonce
-    // const nonce = await this.api.rpc.system.accountNextIndex(accountId)
-    const { nonce } = await this.api.query.system.account(accountId)
-
-    const systemNonce = nonce
-
-    const bestNonce = cachedNonce && cachedNonce.gte(systemNonce) ? cachedNonce : systemNonce
-
-    this.nonces[accountId] = bestNonce
-
-    return bestNonce.toNumber()
-  }
-
-  incrementAndSaveNonce(accountId) {
-    this.nonces[accountId] = this.nonces[accountId].addn(1)
-  }
-
   /*
   /*
    * signAndSend() with nonce tracking, to enable concurrent sending of transacctions
    * signAndSend() with nonce tracking, to enable concurrent sending of transacctions
    * so that they can be included in the same block. Allows you to use the accountId instead
    * so that they can be included in the same block. Allows you to use the accountId instead
@@ -213,7 +188,7 @@ class RuntimeApi {
 
 
     // synchronize access to nonce
     // synchronize access to nonce
     await this.executeWithAccountLock(accountId, async () => {
     await this.executeWithAccountLock(accountId, async () => {
-      const nonce = await this.selectBestNonce(accountId)
+      const nonce = await this.api.rpc.system.accountNextIndex(accountId)
       const signed = tx.sign(fromKey, { nonce })
       const signed = tx.sign(fromKey, { nonce })
       const txhash = signed.hash
       const txhash = signed.hash
 
 
@@ -237,9 +212,6 @@ class RuntimeApi {
         } else {
         } else {
           debugTx(`Submitted: ${serialized}`)
           debugTx(`Submitted: ${serialized}`)
         }
         }
-
-        // transaction submitted successfully, increment and save nonce.
-        this.incrementAndSaveNonce(accountId)
       } catch (err) {
       } catch (err) {
         const errstr = err.toString()
         const errstr = err.toString()
         debugTx(`Rejected: ${errstr} txhash: ${txhash} nonce: ${nonce}`)
         debugTx(`Rejected: ${errstr} txhash: ${txhash} nonce: ${nonce}`)

+ 33 - 17
storage-node/packages/storage/storage.js

@@ -217,13 +217,18 @@ class Storage {
 
 
     this.ipfs = ipfsClient(this.options.ipfs.connect_options)
     this.ipfs = ipfsClient(this.options.ipfs.connect_options)
 
 
-    this.pins = {}
+    this.pinned = {}
+    this.pinning = {}
 
 
     this.ipfs.id((err, identity) => {
     this.ipfs.id((err, identity) => {
       if (err) {
       if (err) {
         debug(`Warning IPFS daemon not running: ${err.message}`)
         debug(`Warning IPFS daemon not running: ${err.message}`)
       } else {
       } else {
         debug(`IPFS node is up with identity: ${identity.id}`)
         debug(`IPFS node is up with identity: ${identity.id}`)
+        // TODO: wait for IPFS daemon to be online for this to be effective..?
+        // set the IPFS HTTP Gateway config we desire.. operator might need
+        // to restart their daemon if the config was changed.
+        this.ipfs.config.set('Gateway.PublicGateways', { 'localhost': null })
       }
       }
     })
     })
   }
   }
@@ -363,27 +368,38 @@ class Storage {
   /*
   /*
    * Synchronize the given content ID
    * Synchronize the given content ID
    */
    */
-  async synchronize(contentId) {
+  async synchronize(contentId, callback) {
     const resolved = await this.resolveContentIdWithTimeout(this._timeout, contentId)
     const resolved = await this.resolveContentIdWithTimeout(this._timeout, contentId)
 
 
-    // validate resolved id is proper ipfs_cid, not null or empty string
+    // TODO: validate resolved id is proper ipfs_cid, not null or empty string
 
 
-    if (this.pins[resolved]) {
-      return
-    }
+    if (!this.pinning[resolved] && !this.pinned[resolved]) {
+      debug(`Pinning hash: ${resolved} content-id: ${contentId}`)
+      this.pinning[resolved] = true
 
 
-    debug(`Pinning ${resolved}`)
+      // Callback passed to add() will be called on error or when the entire file
+      // is retrieved. So on success we consider the content synced.
+      this.ipfs.pin.add(resolved, { quiet: true, pin: true }, (err) => {
+        delete this.pinning[resolved]
+        if (err) {
+          debug(`Error Pinning: ${resolved}`)
+          callback && callback(err)
+        } else {
+          debug(`Pinned ${resolved}`)
+          this.pinned[resolved] = true
+          callback && callback(null, this.syncStatus(resolved))
+        }
+      })
+    } else {
+      callback && callback(null, this.syncStatus(resolved))
+    }
+  }
 
 
-    // This call blocks until file is retrieved..
-    this.ipfs.pin.add(resolved, { quiet: true, pin: true }, (err) => {
-      if (err) {
-        debug(`Error Pinning: ${resolved}`)
-        delete this.pins[resolved]
-      } else {
-        debug(`Pinned ${resolved}`)
-        // why aren't we doing this.pins[resolved] = true
-      }
-    })
+  syncStatus(ipfsHash) {
+    return {
+      syncing: this.pinning[ipfsHash] === true,
+      synced: this.pinned[ipfsHash] === true,
+    }
   }
   }
 }
 }
 
 

+ 7 - 34
storage-node/packages/util/ranges.js

@@ -21,18 +21,20 @@
 const uuid = require('uuid')
 const uuid = require('uuid')
 const streamBuf = require('stream-buffers')
 const streamBuf = require('stream-buffers')
 
 
-const debug = require('debug')('joystream:util:ranges')
-
 /*
 /*
  * Range parsing
  * Range parsing
  */
  */
 
 
+// Increase performance by "pre-computing" these regex expressions
+const PARSE_RANGE_REGEX = /^(\d+-\d+|\d+-|-\d+|\*)$/u
+const PARSE_RANGE_HEADERS_REGEX = /^(([^\s]+)=)?((?:(?:\d+-\d+|-\d+|\d+-),?)+)$/u
+
 /*
 /*
  * Parse a range string, e.g. '0-100' or '-100' or '0-'. Return the values
  * Parse a range string, e.g. '0-100' or '-100' or '0-'. Return the values
  * in an array of int or undefined (if not provided).
  * in an array of int or undefined (if not provided).
  */
  */
 function parseRange(range) {
 function parseRange(range) {
-  const matches = range.match(/^(\d+-\d+|\d+-|-\d+|\*)$/u)
+  const matches = range.match(PARSE_RANGE_REGEX)
   if (!matches) {
   if (!matches) {
     throw new Error(`Not a valid range: ${range}`)
     throw new Error(`Not a valid range: ${range}`)
   }
   }
@@ -56,8 +58,7 @@ function parseRange(range) {
  */
  */
 function parse(rangeStr) {
 function parse(rangeStr) {
   const res = {}
   const res = {}
-  debug('Parse range header value:', rangeStr)
-  const matches = rangeStr.match(/^(([^\s]+)=)?((?:(?:\d+-\d+|-\d+|\d+-),?)+)$/u)
+  const matches = rangeStr.match(PARSE_RANGE_HEADERS_REGEX)
   if (!matches) {
   if (!matches) {
     throw new Error(`Not a valid range header: ${rangeStr}`)
     throw new Error(`Not a valid range header: ${rangeStr}`)
   }
   }
@@ -74,15 +75,12 @@ function parse(rangeStr) {
 
 
   // Merge ranges into result.
   // Merge ranges into result.
   ranges.forEach((newRange) => {
   ranges.forEach((newRange) => {
-    debug('Found range:', newRange)
-
     let isMerged = false
     let isMerged = false
     for (const i in res.ranges) {
     for (const i in res.ranges) {
       const oldRange = res.ranges[i]
       const oldRange = res.ranges[i]
 
 
       // Skip if the new range is fully separate from the old range.
       // Skip if the new range is fully separate from the old range.
       if (oldRange[1] + 1 < newRange[0] || newRange[1] + 1 < oldRange[0]) {
       if (oldRange[1] + 1 < newRange[0] || newRange[1] + 1 < oldRange[0]) {
-        debug('Range does not overlap with', oldRange)
         continue
         continue
       }
       }
 
 
@@ -92,11 +90,9 @@ function parse(rangeStr) {
       const merged = [Math.min(oldRange[0], newRange[0]), Math.max(oldRange[1], newRange[1])]
       const merged = [Math.min(oldRange[0], newRange[0]), Math.max(oldRange[1], newRange[1])]
       res.ranges[i] = merged
       res.ranges[i] = merged
       isMerged = true
       isMerged = true
-      debug('Merged', newRange, 'into', oldRange, 'as', merged)
     }
     }
 
 
     if (!isMerged) {
     if (!isMerged) {
-      debug('Non-overlapping range!')
       res.ranges.push(newRange)
       res.ranges.push(newRange)
     }
     }
   })
   })
@@ -110,7 +106,6 @@ function parse(rangeStr) {
     return first[0] < second[0] ? -1 : 1
     return first[0] < second[0] ? -1 : 1
   })
   })
 
 
-  debug('Result of parse is', res)
   return res
   return res
 }
 }
 
 
@@ -159,11 +154,6 @@ class RangeSender {
     this.handlers = {}
     this.handlers = {}
     this.opened = false
     this.opened = false
 
 
-    debug('RangeSender:', this)
-    if (opts.ranges) {
-      debug('Parsed ranges:', opts.ranges.ranges)
-    }
-
     // Parameters
     // Parameters
     this.response = response
     this.response = response
     this.stream = stream
     this.stream = stream
@@ -173,7 +163,6 @@ class RangeSender {
 
 
   onError(err) {
   onError(err) {
     // Assume hiding the actual error is best, and default to 404.
     // Assume hiding the actual error is best, and default to 404.
-    debug('Error:', err)
     if (!this.response.headersSent) {
     if (!this.response.headersSent) {
       this.response.status(err.code || 404).send({
       this.response.status(err.code || 404).send({
         message: err.message || `File not found: ${this.name}`,
         message: err.message || `File not found: ${this.name}`,
@@ -185,7 +174,6 @@ class RangeSender {
   }
   }
 
 
   onEnd() {
   onEnd() {
-    debug('End of stream.')
     this.response.end()
     this.response.end()
     if (this.endCallback) {
     if (this.endCallback) {
       this.endCallback()
       this.endCallback()
@@ -195,7 +183,6 @@ class RangeSender {
   // **** No ranges
   // **** No ranges
   onOpenNoRange() {
   onOpenNoRange() {
     // File got opened, so we can set headers/status
     // File got opened, so we can set headers/status
-    debug('Open succeeded:', this.name, this.type)
     this.opened = true
     this.opened = true
 
 
     this.response.status(200)
     this.response.status(200)
@@ -228,7 +215,6 @@ class RangeSender {
     // Next range
     // Next range
     this.rangeIndex += 1
     this.rangeIndex += 1
     if (this.rangeIndex >= this.ranges.ranges.length) {
     if (this.rangeIndex >= this.ranges.ranges.length) {
-      debug('Cannot advance range index; we are done.')
       return undefined
       return undefined
     }
     }
 
 
@@ -276,7 +262,6 @@ class RangeSender {
 
 
   nextRange() {
   nextRange() {
     if (this.ranges.ranges.length === 1) {
     if (this.ranges.ranges.length === 1) {
-      debug('Cannot start new range; only one requested.')
       this.stream.off('data', this.handlers.data)
       this.stream.off('data', this.handlers.data)
       return false
       return false
     }
     }
@@ -294,20 +279,17 @@ class RangeSender {
       }
       }
       onDataRanges.write('\r\n')
       onDataRanges.write('\r\n')
       this.response.write(onDataRanges.getContents())
       this.response.write(onDataRanges.getContents())
-      debug('New range started.')
       return true
       return true
     }
     }
 
 
     // No headers means we're finishing the last range.
     // No headers means we're finishing the last range.
     this.response.write(`\r\n--${this.rangeBoundary}--\r\n`)
     this.response.write(`\r\n--${this.rangeBoundary}--\r\n`)
-    debug('End of ranges sent.')
     this.stream.off('data', this.handlers.data)
     this.stream.off('data', this.handlers.data)
     return false
     return false
   }
   }
 
 
   onOpenRanges() {
   onOpenRanges() {
     // File got opened, so we can set headers/status
     // File got opened, so we can set headers/status
-    debug('Open succeeded:', this.name, this.type)
     this.opened = true
     this.opened = true
 
 
     this.response.header('Accept-Ranges', 'bytes')
     this.response.header('Accept-Ranges', 'bytes')
@@ -347,34 +329,29 @@ class RangeSender {
     // The simplest optimization would be at ever range start to seek() to the
     // The simplest optimization would be at ever range start to seek() to the
     // start.
     // start.
     const chunkRange = [this.readOffset, this.readOffset + chunk.length - 1]
     const chunkRange = [this.readOffset, this.readOffset + chunk.length - 1]
-    debug('= Got chunk with byte range', chunkRange)
     while (true) {
     while (true) {
       let reqRange = this.ranges.ranges[this.rangeIndex]
       let reqRange = this.ranges.ranges[this.rangeIndex]
       if (!reqRange) {
       if (!reqRange) {
         break
         break
       }
       }
-      debug('Current requested range is', reqRange)
+
       if (!reqRange[1]) {
       if (!reqRange[1]) {
         reqRange = [reqRange[0], Number.MAX_SAFE_INTEGER]
         reqRange = [reqRange[0], Number.MAX_SAFE_INTEGER]
-        debug('Treating as', reqRange)
       }
       }
 
 
       // No overlap in the chunk and requested range; don't write.
       // No overlap in the chunk and requested range; don't write.
       if (chunkRange[1] < reqRange[0] || chunkRange[0] > reqRange[1]) {
       if (chunkRange[1] < reqRange[0] || chunkRange[0] > reqRange[1]) {
-        debug('Ignoring chunk; it is out of range.')
         break
         break
       }
       }
 
 
       // Since there is overlap, find the segment that's entirely within the
       // Since there is overlap, find the segment that's entirely within the
       // chunk.
       // chunk.
       const segment = [Math.max(chunkRange[0], reqRange[0]), Math.min(chunkRange[1], reqRange[1])]
       const segment = [Math.max(chunkRange[0], reqRange[0]), Math.min(chunkRange[1], reqRange[1])]
-      debug('Segment to send within chunk is', segment)
 
 
       // Normalize the segment to a chunk offset
       // Normalize the segment to a chunk offset
       const start = segment[0] - this.readOffset
       const start = segment[0] - this.readOffset
       const end = segment[1] - this.readOffset
       const end = segment[1] - this.readOffset
       const len = end - start + 1
       const len = end - start + 1
-      debug('Offsets into buffer are', [start, end], 'with length', len)
 
 
       // Write the slice that we want to write. We first create a buffer from the
       // Write the slice that we want to write. We first create a buffer from the
       // chunk. Then we slice a new buffer from the same underlying ArrayBuffer,
       // chunk. Then we slice a new buffer from the same underlying ArrayBuffer,
@@ -385,12 +362,10 @@ class RangeSender {
 
 
       // If the requested range is finished, we should start the next one.
       // If the requested range is finished, we should start the next one.
       if (reqRange[1] > chunkRange[1]) {
       if (reqRange[1] > chunkRange[1]) {
-        debug('Chunk is finished, but the requested range is missing bytes.')
         break
         break
       }
       }
 
 
       if (reqRange[1] <= chunkRange[1]) {
       if (reqRange[1] <= chunkRange[1]) {
-        debug('Range is finished.')
         if (!this.nextRange(segment)) {
         if (!this.nextRange(segment)) {
           break
           break
         }
         }
@@ -424,11 +399,9 @@ class RangeSender {
     this.handlers.end = this.onEnd.bind(this)
     this.handlers.end = this.onEnd.bind(this)
 
 
     if (this.ranges) {
     if (this.ranges) {
-      debug('Preparing to handle ranges.')
       this.handlers.open = this.onOpenRanges.bind(this)
       this.handlers.open = this.onOpenRanges.bind(this)
       this.handlers.data = this.onDataRanges.bind(this)
       this.handlers.data = this.onDataRanges.bind(this)
     } else {
     } else {
-      debug('No ranges, just send the whole file.')
       this.handlers.open = this.onOpenNoRange.bind(this)
       this.handlers.open = this.onOpenNoRange.bind(this)
       this.handlers.data = this.onDataNoRange.bind(this)
       this.handlers.data = this.onDataNoRange.bind(this)
     }
     }

+ 8 - 0
storage-node/scripts/compose/devchain-and-ipfs-node/docker-compose.yaml

@@ -4,8 +4,16 @@ services:
     image: ipfs/go-ipfs:latest
     image: ipfs/go-ipfs:latest
     ports:
     ports:
       - '127.0.0.1:5001:5001'
       - '127.0.0.1:5001:5001'
+      - '127.0.0.1:8080:8080'
     volumes:
     volumes:
       - ipfs-data:/data/ipfs
       - ipfs-data:/data/ipfs
+    entrypoint: ''
+    command: |
+      /bin/sh -c "
+        set -e
+        /usr/local/bin/start_ipfs config --json Gateway.PublicGateways '{\"localhost\": null }'
+        /sbin/tini -- /usr/local/bin/start_ipfs daemon --migrate=true
+      "
   chain:
   chain:
     image: joystream/node:latest
     image: joystream/node:latest
     ports:
     ports:

File diff suppressed because it is too large
+ 25 - 81
testnets/joy-testnet-4.json


File diff suppressed because it is too large
+ 0 - 21
testnets/nicaea-exported-state/chain_spec.json


File diff suppressed because it is too large
+ 0 - 0
testnets/nicaea-exported-state/content.json


File diff suppressed because it is too large
+ 0 - 0
testnets/nicaea-exported-state/forum.json


File diff suppressed because it is too large
+ 0 - 0
testnets/nicaea-exported-state/members.json


File diff suppressed because it is too large
+ 0 - 81
testnets/rome.json


+ 14 - 21
types/README.md

@@ -15,37 +15,30 @@ yarn add @joystream/types
 npm install --save @joystream/types
 npm install --save @joystream/types
 ```
 ```
 
 
-## Registering the types
-
-Call `registerJoystreamTypes()` before creating a Polkadot API client.
+## Example usage
 
 
 ```javascript
 ```javascript
-import { registerJoystreamTypes } from '@joystream/types';
-import { ApiPromise, WsProvider } from '@polkadot/api';
+import { types } from '@joystream/types'
+import { ApiPromise, WsProvider } from '@polkadot/api'
 
 
-async function main () {
+async function main() {
   // Initialise the provider to connect to the local node
   // Initialise the provider to connect to the local node
-  const provider = new WsProvider('ws://127.0.0.1:9944');
-
-  // Register types before creating the API
-  registerJoystreamTypes();
+  const provider = new WsProvider('ws://127.0.0.1:9944')
 
 
   // Create the API and wait until ready
   // Create the API and wait until ready
-  const api = await ApiPromise.create({ provider });
+  const api = await ApiPromise.create({ provider, types })
+
+  await api.isReady
 
 
-  // Retrieve the chain & node information information via RPC calls
+  // Retrieve the chain & node information information via rpc calls
   const [chain, nodeName, nodeVersion] = await Promise.all([
   const [chain, nodeName, nodeVersion] = await Promise.all([
     api.rpc.system.chain(),
     api.rpc.system.chain(),
     api.rpc.system.name(),
     api.rpc.system.name(),
-    api.rpc.system.version()
-  ]);
+    api.rpc.system.version(),
+  ])
 
 
-  console.log(`Chain ${chain} using ${nodeName} v${nodeVersion}`);
+  console.log(`Chain ${chain} using ${nodeName} v${nodeVersion}`)
 }
 }
 
 
-main();
-```
-
-## Examples
-
-See [joystream-api-examples](https://github.com/Joystream/joystream-api-examples) for some additional examples on usage.
+main()
+```

+ 3 - 3
yarn.lock

@@ -3361,7 +3361,7 @@
     memoizee "^0.4.14"
     memoizee "^0.4.14"
     rxjs "^6.6.0"
     rxjs "^6.6.0"
 
 
-"@polkadot/api@1.26.1", "@polkadot/api@^0.96.1", "@polkadot/api@^1.26.1":
+"@polkadot/api@1.26.1", "@polkadot/api@^1.26.1":
   version "1.26.1"
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-1.26.1.tgz#215268489c10b1a65429c6ce451c8d65bd3ad843"
   resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-1.26.1.tgz#215268489c10b1a65429c6ce451c8d65bd3ad843"
   integrity sha512-al8nmLgIU1EKo0oROEgw1mqUvrHJu4gKYBwnFONaEOxHSxBgBSSgNy1MWKNntAQYDKA4ETCj4pz7ZpMXTx2SDA==
   integrity sha512-al8nmLgIU1EKo0oROEgw1mqUvrHJu4gKYBwnFONaEOxHSxBgBSSgNy1MWKNntAQYDKA4ETCj4pz7ZpMXTx2SDA==
@@ -13595,7 +13595,7 @@ http-proxy-middleware@0.19.1:
     lodash "^4.17.11"
     lodash "^4.17.11"
     micromatch "^3.1.10"
     micromatch "^3.1.10"
 
 
-http-proxy-middleware@^1.0.3:
+http-proxy-middleware@^1.0.3, http-proxy-middleware@^1.0.5:
   version "1.0.5"
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
   resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
   integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
   integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
@@ -17753,7 +17753,7 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
   dependencies:
   dependencies:
     mime-db "1.42.0"
     mime-db "1.42.0"
 
 
-mime-types@^2.1.18, mime-types@^2.1.22, mime-types@^2.1.26, mime-types@~2.1.17:
+mime-types@^2.1.18, mime-types@^2.1.22, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17:
   version "2.1.27"
   version "2.1.27"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
   integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
   integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==

Some files were not shown because too many files changed in this diff