OSDN Git Service

copy chain sdk code to dashboard
authorYongfeng LI <wliyongfeng@gmail.com>
Wed, 27 Dec 2017 06:48:44 +0000 (14:48 +0800)
committerYongfeng LI <wliyongfeng@gmail.com>
Wed, 27 Dec 2017 06:48:44 +0000 (14:48 +0800)
27 files changed:
.gitignore
package-lock.json
package.json
src/features/app/components/Container.jsx
src/features/app/components/Main/Main.jsx
src/features/core/actions.js
src/features/shared/components/RawJsonButton.jsx
src/features/shared/routes.js
src/sdk/api/accessTokens.js [new file with mode: 0644]
src/sdk/api/accounts.js [new file with mode: 0644]
src/sdk/api/assets.js [new file with mode: 0644]
src/sdk/api/authorizationGrants.js [new file with mode: 0644]
src/sdk/api/balances.js [new file with mode: 0644]
src/sdk/api/config.js [new file with mode: 0644]
src/sdk/api/hsmSigner.js [new file with mode: 0644]
src/sdk/api/mockHsmKeys.js [new file with mode: 0644]
src/sdk/api/transactionFeeds.js [new file with mode: 0644]
src/sdk/api/transactions.js [new file with mode: 0644]
src/sdk/api/unspentOutputs.js [new file with mode: 0644]
src/sdk/client.js [new file with mode: 0644]
src/sdk/connection.js [new file with mode: 0644]
src/sdk/errors.js [new file with mode: 0644]
src/sdk/index.js [new file with mode: 0644]
src/sdk/page.js [new file with mode: 0644]
src/sdk/shared.js [new file with mode: 0644]
src/sdk/util.js [new file with mode: 0644]
src/utility/environment.js

index 2fc8115..9b5363a 100644 (file)
@@ -4,3 +4,4 @@ npm-debug.log
 public
 errorShots
 .idea
+.DS_Store
index e474428..ea70a5f 100644 (file)
             "babel-generator": "6.26.0",
             "babel-helpers": "6.24.1",
             "babel-messages": "6.23.0",
+            "babel-register": "6.26.0",
             "babel-runtime": "6.26.0",
             "babel-template": "6.26.0",
             "babel-traverse": "6.26.0",
             "private": "0.1.8",
             "slash": "1.0.0",
             "source-map": "0.5.7"
+          },
+          "dependencies": {
+            "babel-register": {
+              "version": "6.26.0",
+              "resolved": "http://registry.npm.taobao.org/babel-register/download/babel-register-6.26.0.tgz",
+              "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+              "requires": {
+                "babel-core": "6.26.0",
+                "babel-runtime": "6.26.0",
+                "core-js": "2.5.3",
+                "home-or-tmp": "2.0.0",
+                "lodash": "4.17.4",
+                "mkdirp": "0.5.1",
+                "source-map-support": "0.4.18"
+              }
+            }
           }
         },
         "babel-register": {
         "check-error": "1.0.2"
       }
     },
-    "chain-sdk": {
-      "version": "1.2.1",
-      "resolved": "http://registry.npm.taobao.org/chain-sdk/download/chain-sdk-1.2.1.tgz",
-      "integrity": "sha1-YbAHdLIlfyc/70LHO1xye/HyJJw=",
-      "requires": {
-        "btoa": "1.1.2",
-        "fetch-ponyfill": "3.0.2",
-        "uuid": "3.0.1"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "3.0.1",
-          "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.0.1.tgz",
-          "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE="
-        }
-      }
-    },
     "chalk": {
       "version": "1.1.3",
       "resolved": "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz",
index 583c361..377d186 100644 (file)
     "Safari >= 8"
   ],
   "dependencies": {
+    "btoa": "^1.1.2",
+    "fetch-ponyfill": "~3.0.2",
+    "uuid": "~2.0.2",
     "babel-polyfill": "~6.16.0",
     "bootstrap-sass": "~3.3.7",
-    "chain-sdk": "~1.2.1",
     "classnames": "~2.2.5",
-    "fetch-ponyfill": "~3.0.2",
     "lodash": "~4.17.4",
     "moment": "~2.14.1",
     "moment-timezone": "~0.5.5",
@@ -44,8 +45,7 @@
     "redux-form": "~5.3.2",
     "redux-thunk": "~2.1.0",
     "reselect": "^3.0.0",
-    "sha.js": "^2.4.8",
-    "uuid": "~2.0.2"
+    "sha.js": "^2.4.8"
   },
   "devDependencies": {
     "autoprefixer": "~6.7.7",
index 150b03d..30ebff3 100644 (file)
@@ -88,8 +88,8 @@ class Container extends React.Component {
 export default connect(
   (state) => ({
     authOk: !state.core.requireClientToken || state.core.validToken,
-    configKnown: state.core.configKnown,
-    configured: state.core.configured,
+    configKnown: true,
+    configured: true,
     onTestnet: state.core.onTestnet,
   }),
   (dispatch) => ({
index 422b5a3..e3efa10 100644 (file)
@@ -50,7 +50,7 @@ class Main extends React.Component {
 export default connect(
   (state) => ({
     canLogOut: state.core.requireClientToken,
-    connected: state.core.connected,
+    connected: true,
     showDropwdown: state.app.dropdownState == 'open',
   }),
   (dispatch) => ({
index 944a1f2..886cee1 100644 (file)
@@ -7,7 +7,9 @@ const clearSession = ({ type: 'USER_LOG_OUT' })
 const fetchCoreInfo = (options = {}) => {
   return (dispatch) => {
     return chainClient().config.info()
-      .then((info) => dispatch(updateInfo(info)))
+      .then((info) => {
+        dispatch(updateInfo(info))
+      })
       .catch((err) => {
         if (options.throw || !err.status) {
           throw err
@@ -29,6 +31,7 @@ let actions = {
   clearSession,
   logIn: (token) => (dispatch) => {
     dispatch(setClientToken(token))
+    debugger
     return dispatch(fetchCoreInfo({throw: true}))
       .then(() => dispatch({type: 'USER_LOG_IN'})
     )
index f61c045..07379ab 100644 (file)
@@ -1,5 +1,5 @@
 import React from 'react'
-import { Connection } from 'chain-sdk'
+import { Connection } from 'sdk'
 
 class RawJsonButton extends React.Component {
   showRawJson(item){
index 02c991c..0959f8e 100644 (file)
@@ -13,12 +13,7 @@ const makeRoutes = (store, type, List, New, Show, Update, options = {}) => {
       return
     }
 
-    const pageNumber = parseInt(state.location.query.page || 1)
-    if (pageNumber == 1) {
-      store.dispatch(actions[type].fetchPage(query, pageNumber, { refresh: true }))
-    } else {
-      store.dispatch(actions[type].fetchPage(query, pageNumber))
-    }
+    store.dispatch(actions[type].fetchAll())
   }
 
   const childRoutes = []
@@ -54,7 +49,10 @@ const makeRoutes = (store, type, List, New, Show, Update, options = {}) => {
     name: options.name || humanize(type + 's'),
     indexRoute: {
       component: List,
-      onEnter: (nextState, replace) => { loadPage(nextState, replace) },
+      onEnter: (nextState, replace) => {
+        debugger
+        loadPage(nextState, replace)
+      },
       onChange: (_, nextState, replace) => { loadPage(nextState, replace) }
     },
     childRoutes: childRoutes
diff --git a/src/sdk/api/accessTokens.js b/src/sdk/api/accessTokens.js
new file mode 100644 (file)
index 0000000..710a1b4
--- /dev/null
@@ -0,0 +1,85 @@
+const shared = require('../shared')
+
+/**
+ * Access tokens are `name:secret-token` pairs that can be granted one or more
+ * policies for accessing Chain Core features. See {@link module:AuthorizationGrantsApi the
+ * access control API} for more info.
+ *
+ * More info: {@link https://chain.com/docs/core/learn-more/authentication-and-authorization}
+ * @typedef {Object} AccessToken
+ * @global
+ *
+ * @property {String} id
+ * User specified, unique identifier.
+ *
+ * @property {String} token
+ * Only returned in the response from {@link AccessTokensApi~create}.
+ *
+ * @property {String} createdAt
+ * Timestamp of token creation, RFC3339 formatted.
+ *
+ * @property {String} type
+ * DEPRECATED. Do not use in 1.2 or later. Either 'client' or 'network'.
+ */
+
+/**
+ * API for interacting with {@link AccessToken access tokens}.
+ *
+ * More info: {@link https://chain.com/docs/core/learn-more/authentication-and-authorization}
+ * @module AccessTokensApi
+ */
+const accessTokens = (client) => {
+  return {
+    /**
+     * Create a new access token.
+     *
+     * @param {Object} params - Parameters for access token creation.
+     * @param {String} params.id - User specified, unique identifier.
+     * @param {String} params.type - DEPRECATED. Do not use in 1.2 or later. Either 'client' or 'network'.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<AccessToken>} Newly created access token.
+     */
+    create: (params, cb) =>
+      shared.create(client, '/create-access-token', params, {skipArray: true, cb}),
+
+    /**
+     * Get one page of access tokens sorted by descending creation time,
+     * optionally filtered by type.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.type - Type of access tokens to return.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<AccessToken>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'accessTokens', '/list-access-tokens', params, {cb}),
+
+    /**
+     * Request all access tokens matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.type - Type of access tokens to return.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<AccessToken>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                    rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'list-access-tokens', params, processor, cb),
+
+    /**
+     * Delete the specified access token.
+     *
+     * @param {String} id - Access token ID.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Object>} Success message or error.
+     */
+    delete: (id, cb) => shared.tryCallback(
+      client.request('/delete-access-token', {id: id}),
+      cb
+    ),
+  }
+}
+
+module.exports = accessTokens
diff --git a/src/sdk/api/accounts.js b/src/sdk/api/accounts.js
new file mode 100644 (file)
index 0000000..e1048e1
--- /dev/null
@@ -0,0 +1,180 @@
+const shared = require('../shared')
+
+/**
+ * An account is an object in Chain Core that tracks ownership of assets on a
+ * blockchain by creating and tracking control programs.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/accounts}
+ * @typedef {Object} Account
+ * @global
+ *
+ * @property {String} id
+ * Unique account identifier.
+ *
+ * @property {String} alias
+ * User specified, unique identifier.
+ *
+ * @property {Key[]} keys
+ * The list of keys used to create control programs under the account.
+ * Signatures from these keys are required for spending funds held in the account.
+ *
+ * @property {Number} quorum
+ * The number of keys required to sign transactions for the account.
+ *
+ * @property {Object} tags
+ * User-specified tag structure for the account.
+ */
+
+/**
+ * A receiver is an object that wraps an account control program with additional
+ * payment information, such as expiration dates.
+ *
+ * <br/></br>
+ * More info: {@link https://chain.com/docs/core/build-applications/control-programs}
+ * @typedef {Object} Receiver
+ * @global
+ *
+ * @property {String} controlProgram
+ * The underlying control program that will be used in transactions paying to this receiver.
+ *
+ * @property {String} expiresAt
+ * Timestamp indicating when the receiver will cease to be valid, RFC3339 formatted.
+ */
+
+/**
+ * API for interacting with {@link Account accounts}.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/accounts}
+ * @module AccountsApi
+ */
+const accountsAPI = (client) => {
+  /**
+   * @typedef {Object} createRequest
+   *
+   * @property {String} [alias]
+   * User specified, unique identifier.
+   *
+   * @property {String[]} rootXpubs
+   * The list of keys used to create control programs under the account.
+   *
+   * @property {Number} quorum
+   * The number of keys required to sign transactions for the account.
+   *
+   * @property {Object} [tags]
+   * User-specified tag structure for the account.
+   */
+
+  /**
+   * @typedef {Object} updateTagsRequest
+   *
+   * @property {String} [id]
+   * The account ID. Either the ID or alias must be specified, but not both.
+   *
+   * @property {String} [alias]
+   * The account alias. Either the ID or alias must be specified, but not both.
+   *
+   * @property {Object} tags
+   * A new set of tags, which will replace the existing tags.
+   */
+
+  /**
+   * @typedef {Object} createReceiverRequest
+   *
+   * @property {String} [accountAlias]
+   * The unique alias of the account. accountAlias or accountId must be
+   * provided.
+   *
+   * @property {String} [accountId]
+   * The unique ID of the account. accountAlias or accountId must be
+   * provided.
+   *
+   * @property {String} [expiresAt]
+   * An RFC3339 timestamp indicating when the receiver will cease to be valid.
+   * Defaults to 30 days in the future.
+   */
+
+  return {
+    /**
+     * Create a new account.
+     *
+     * @param {module:AccountsApi~createRequest} params - Parameters for account creation.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Account>} Newly created account.
+     */
+    create: (params, cb) => shared.create(client, '/create-account', params, {cb}),
+
+    /**
+     * Create multiple new accounts.
+     *
+     * @param {module:AccountsApi~createRequest[]} params - Parameters for creation of multiple accounts.
+     * @param {batchCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse<Account>>} Newly created accounts.
+     */
+    createBatch: (params, cb) => shared.createBatch(client, '/create-account', params, {cb}),
+
+    /**
+     * Update account tags.
+     *
+     * @param {module:AccountsApi~updateTagsRequest} params - Parameters for updating account tags.
+     * @param {objectCallback} [cb] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Object>} Success message.
+     */
+    updateTags: (params, cb) => shared.singletonBatchRequest(client, '/update-account-tags', params, cb),
+
+    /**
+     * Update tags for multiple assets.
+     *
+     * @param {module:AccountsApi~updateTagsRequest[]} params - Parameters for updating account tags.
+     * @param {batchCallback} [cb] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse<Object>>} A batch of success responses and/or errors.
+     */
+    updateTagsBatch: (params, cb) => shared.batchRequest(client, '/update-account-tags', params, cb),
+
+    /**
+     * Get one page of accounts matching the specified query.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<Account>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'accounts', '/list-accounts', params, {cb}),
+
+    /**
+     * Request all accounts matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<Account>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'accounts', params, processor, cb),
+
+    /**
+     * Create a new receiver under the specified account.
+     *
+     * @param {module:AccountsApi~createReceiverRequest} params - Parameters for receiver creation.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Receiver>} Newly created receiver.
+     */
+    createReceiver: (params, cb) => shared.create(client, '/create-account-receiver', params, {cb}),
+
+    /**
+     * Create multiple receivers under the specified accounts.
+     *
+     * @param {module:AccountsApi~createReceiverRequest[]} params - Parameters for creation of multiple receivers.
+     * @param {batchCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse<Receiver>>} Newly created receivers.
+     */
+    createReceiverBatch: (params, cb) => shared.createBatch(client, '/create-account-receiver', params, {cb}),
+  }
+}
+
+module.exports = accountsAPI
diff --git a/src/sdk/api/assets.js b/src/sdk/api/assets.js
new file mode 100644 (file)
index 0000000..4829568
--- /dev/null
@@ -0,0 +1,145 @@
+const shared = require('../shared')
+
+/**
+ * An asset is a type of value that can be issued on a blockchain. All units of
+ * a given asset are fungible. Units of an asset can be transacted directly
+ * between parties without the involvement of the issuer.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/assets}
+ * @typedef {Object} Asset
+ * @global
+ *
+ * @property {String} id
+ * Globally unique identifier of the asset.
+ * Asset version 1 specifies the asset id as the hash of:
+ * - the asset version
+ * - the asset's issuance program
+ * - the core's VM version
+ * - the hash of the network's initial block
+ *
+ * @property {String} alias
+ * User specified, unique identifier.
+ *
+ * @property {String} issuanceProgram
+ *
+ * @property {Key[]} keys
+ * The list of keys used to issue units of the asset.
+ *
+ * @property {Number} quorum
+ * The number of signatures required to issue new units of the asset.
+ *
+ * @property {Object} defintion
+ * User-specified, arbitrary/unstructured data visible across
+ * blockchain networks. Version 1 assets specify the definition in their
+ * issuance programs, rendering the definition immutable.
+ *
+ * @property {Object} tags
+ * User-specified tag structure for the asset.
+ */
+
+/**
+ * API for interacting with {@link Asset assets}.
+ * 
+ * More info: {@link https://chain.com/docs/core/build-applications/assets}
+ * @module AssetsApi
+ */
+const assetsAPI = (client) => {
+  /**
+   * @typedef {Object} createRequest
+   *
+   * @property {String} [alias]
+   * User specified, unique identifier.
+   *
+   * @property {String[]} rootXpubs
+   * The list of keys used to create the issuance program for the asset.
+   *
+   * @property {Number} quorum
+   * The number of keys required to issue units of the asset.
+   *
+   * @property {Object} [tags]
+   * User-specified, arbitrary/unstructured data local to the asset's originating core.
+   *
+   * @property {Object} [defintion]
+   * User-specified, arbitrary/unstructured data visible across blockchain networks.
+   */
+
+  /**
+   * @typedef {Object} updateTagsRequest
+   *
+   * @property {String} [id]
+   * The asset ID. Either the ID or alias must be specified, but not both.
+   *
+   * @property {String} [alias]
+   * The asset alias. Either the ID or alias must be specified, but not both.
+   *
+   * @property {Object} [tags]
+   * A new set of tags, which will replace the existing tags.
+   */
+
+  return {
+    /**
+     * Create a new asset.
+     *
+     * @param {module:AssetsApi~createRequest} params - Parameters for asset creation.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Asset>} Newly created asset.
+     */
+    create: (params, cb) => shared.create(client, '/create-asset', params, {cb}),
+
+    /**
+     * Create multiple new assets.
+     *
+     * @param {module:AssetsApi~createRequest[]} params - Parameters for creation of multiple assets.
+     * @param {batchCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse<Asset>>} Newly created assets.
+     */
+    createBatch: (params, cb) => shared.createBatch(client, '/create-asset', params, {cb}),
+
+    /**
+     * Update asset tags.
+     *
+     * @param {module:AssetsApi~updateTagsRequest} params - Parameters for updating asset tags.
+     * @param {objectCallback} [cb] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Object>} Success message.
+     */
+    updateTags: (params, cb) => shared.singletonBatchRequest(client, '/update-asset-tags', params, cb),
+
+    /**
+     * Update tags for multiple assets.
+     *
+     * @param {module:AssetsApi~updateTagsRequest[]} params - Parameters for updating asset tags.
+     * @param {batchCallback} [cb] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse<Object>>} A batch of success responses and/or errors.
+     */
+    updateTagsBatch: (params, cb) => shared.batchRequest(client, '/update-asset-tags', params, cb),
+
+    /**
+     * Get one page of assets matching the specified query.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<Asset>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'assets', '/list-assets', params, {cb}),
+
+    /**
+     * Request all assets matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {QueryProcessor<Asset>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'assets', params, processor, cb),
+  }
+}
+
+module.exports = assetsAPI
diff --git a/src/sdk/api/authorizationGrants.js b/src/sdk/api/authorizationGrants.js
new file mode 100644 (file)
index 0000000..a32f0b3
--- /dev/null
@@ -0,0 +1,126 @@
+const shared = require('../shared')
+const util = require('../util')
+
+/**
+ * Authorization grants provide a mapping from guard objects (access tokens or X509
+ * certificates) to a list of predefined Chain Core access policies.
+ *
+ * * **client-readwrite**: full access to the Client API
+ * * **client-readonly**: access to read-only Client endpoints
+ * * **monitoring**: access to monitoring-specific endpoints
+ * * **crosscore**: access to the cross-core API, including fetching blocks and
+ *   submitting transactions to the generator, but not including block signing
+ * * **crosscore-signblock**: access to the cross-core API's block singing
+ *   functionality
+ *
+ * More info: {@link https://chain.com/docs/core/learn-more/authentication-and-authorization}
+ * @typedef {Object} AuthorizationGrant
+ * @global
+ *
+ * @property {String} guardType
+ * Type of credential, either 'access_token' or 'x509'.
+ *
+ * @property {Object} guardData
+ * Data used by the guard to identity incoming credentials.
+ *
+ * If guardType is 'access_token', you should provide an instance of
+ * {@link module:AuthorizationGrantsApi~AccessTokenGuardData}, which identifies access tokens by ID.
+ *
+ * If guardType is 'x509', you should provide an instance of {@link module:AuthorizationGrantsApi~X509GuardData},
+ * which identifies x509 certificates based on kev-value pairs in specified
+ * certificate fields.
+ *
+ * @property {String} policy
+ * Authorization single policy to attach to specific grant.
+ *
+ * @property {Boolean} protected
+ * Whether the grant can be deleted. Only used for internal purposes.
+ *
+ * @property {String} createdAt
+ * Time of grant creation, RFC3339 formatted.
+ */
+
+/**
+ * API for interacting with {@link AuthorizationGrant access grants}.
+ *
+ * More info: {@link https://chain.com/docs/core/learn-more/authentication-and-authorization}
+ * @module AuthorizationGrantsApi
+ */
+const authorizationGrants = (client) => ({
+  /**
+   * @typedef {Object} AccessTokenGuardData
+   *
+   * @property {String} id
+   * Unique identifier of an access token
+   */
+
+  /**
+   * @typedef {Object} X509GuardData
+   * x509 certificates are identified by their Subject attribute. You can
+   * configure the guard by specifying values for the Subject's sub-attributes,
+   * such as CN or OU. If a certificate's Subject contains all of the
+   * sub-attribute values specified in the guard, the guard will produce a
+   * positive match.
+   *
+   * @property {Object} subject - Object identifying key-value pairs in the subject field.
+   * @property {(String|Array)} subject.C - Country attribute
+   * @property {(String|Array)} subject.O - Organization attribute
+   * @property {(String|Array)} subject.OU - Organizational Unit attribute
+   * @property {(String|Array)} subject.L - Locality attribute
+   * @property {(String|Array)} subject.ST - State/Province attribute
+   * @property {(String|Array)} subject.STREET - Street Address attribute
+   * @property {(String|Array)} subject.POSTALCODE - Postal Code attribute
+   * @property {String} subject.SERIALNUMBER - Serial Number attribute
+   * @property {String} subject.CN - Common Name attribute
+   */
+
+  /**
+   * Create a new access grant.
+   *
+   * @param {Object} params - Parameters for access grant creation.
+   * @param {String} params.guardType - Type of credential to guard with, either 'access_token' or 'x509'.
+   * @param {Object} params.guardData - Object containing data needed to identify the incoming credential.
+   * @param {String} params.policy - Authorization polciy to attach to specific grant. See {@link AuthorizationGrant} for a list of available policiies.
+   * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {Promise<Object>} Success message or error.
+   */
+  create: (params, cb) => {
+    params = Object.assign({}, params)
+    if (params.guardType == 'x509') {
+      params.guardData = util.sanitizeX509GuardData(params.guardData)
+    }
+
+    return shared.create(
+      client,
+      '/create-authorization-grant',
+      params,
+      {skipArray: true, cb}
+    )
+  },
+
+  /**
+   * Delete the specfiied access grant.
+   *
+   * @param {Object} params - Parameters for access grant deletion.
+   * @param {String} params.guardType - Type of credential to delete, either 'access_token' or 'x509'.
+   * @param {Object} params.guardData - Object containing data needed to identify the credential to be removed.
+   * @param {String} params.policy - Authorization policy to remove. See {@link AuthorizationGrant} for a list of available policiies.
+   * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {Promise<Object>} Success message or error.
+   */
+  delete: (params, cb) => shared.tryCallback(
+    client.request('/delete-authorization-grant', params),
+    cb
+  ),
+
+  /**
+   * Get all access grants.
+   *
+   * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {Promise<Array<AuthorizationGrant>>} Requested page of results.
+   */
+  list: (cb) =>
+    shared.query(client, 'accessTokens', '/list-authorization-grants', {}, {cb}),
+})
+
+module.exports = authorizationGrants
diff --git a/src/sdk/api/balances.js b/src/sdk/api/balances.js
new file mode 100644 (file)
index 0000000..7a2790e
--- /dev/null
@@ -0,0 +1,61 @@
+const shared = require('../shared')
+
+/**
+ * Any balance on the blockchain is simply a summation of unspent outputs.
+ * Unlike other queries in Chain Core, balance queries do not return Chain Core
+ * objects, only simple sums over the amount fields in a specified list of
+ * unspent output objects
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/queries}
+ * @typedef {Object} Balance
+ * @global
+ *
+ * @property {Number} amount
+ * Sum of the unspent outputs.
+ *
+ * @property {Object} sumBy
+ * List of parameters on which to sum unspent outputs.
+ */
+
+/**
+* API for interacting with {@link Balance balances}.
+ * 
+ * More info: {@link https://chain.com/docs/core/build-applications/queries}
+ * @module BalancesApi
+ */
+const balancesAPI = (client) => {
+  return {
+    /**
+     * Get one page of balances matching the specified query.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Array<String>} params.sumBy - List of unspent output attributes to sum by.
+     * @param {Integer} params.timestamp - A millisecond Unix timestamp. By using this parameter, you can perform queries that reflect the state of the blockchain at different points in time.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<Balance>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'balances', '/list-balances', params, {cb}),
+
+    /**
+     * Request all balances matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Array<String>} params.sumBy - List of unspent output attributes to sum by.
+     * @param {Integer} params.timestamp - A millisecond Unix timestamp. By using this parameter, you can perform queries that reflect the state of the blockchain at different points in time.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<Balance>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'balances', params, processor, cb),
+  }
+}
+
+module.exports = balancesAPI
diff --git a/src/sdk/api/config.js b/src/sdk/api/config.js
new file mode 100644 (file)
index 0000000..b0cf9c3
--- /dev/null
@@ -0,0 +1,147 @@
+const shared = require('../shared')
+
+/**
+ * Basic information about the configuration of Chain Core, as well as any
+ * errors encountered when updating the local state of the blockchain
+ *
+ * More info: {@link https://chain.com/docs/core/get-started/configure}
+ * @typedef {Object} CoreInfo
+ *
+ * @property {Object} snapshot
+ * @property {Number} snapshot.attempt
+ * @property {Number} snapshot.height
+ * @property {Number} snapshot.size
+ * @property {Number} snapshot.downloaded
+ * @property {Boolean} snapshot.inProgress
+ *
+ * @property {Boolean} isConfigured
+ * Whether the core has been configured.
+ *
+ * @property {String} configuredAt
+ * RFC3339 timestamp reflecting when the core was configured.
+ *
+ * @property {Boolean} isSigner
+ * Whether the core is configured as a block signer.
+ *
+ * @property {Boolean} isGenerator
+ * Whether the core is configured as the blockchain generator.
+ *
+ * @property {String} generatorUrl
+ * URL of the generator.
+ *
+ * @property {String} generatorAccessToken
+ * The access token used to connect to the generator.
+ *
+ * @property {String} blockchainId
+ * Hash of the initial block.
+ *
+ * @property {Number} blockHeight
+ * Height of the blockchain in the local core.
+ *
+ * @property {Number} generatorBlockHeight
+ * Height of the blockchain in the generator
+ *
+ * @property {String} generatorBlockHeightFetchedAt
+ * RFC3339 timestamp reflecting the last time generator_block_height was updated.
+ *
+ * @property {Boolean} isProduction
+ * Whether the core is running in production mode.
+ *
+ * @property {Number} crosscoreRpcVersion
+ * The cross-core API version supported by this core.
+ *
+ * @property {Number} networkRpcVersion
+ * DEPRECATED. Do not use in 1.2 or greater. Superseded by {@link crosscoreRpcVersion}.
+ *
+ * @property {String} coreId
+ * A random identifier for the core, generated during configuration.
+ *
+ * @property {String} version
+ * The release version of the cored binary.
+ *
+ * @property {String} buildCommit
+ * Git SHA of build source.
+ *
+ * @property {String} buildDate
+ * Unixtime (as string) of binary build.
+ *
+ * @property {Object} buildConfig
+ * Features enabled or disabled in this build of Chain Core.
+ *
+ * @property {Boolean} buildConfig.isLocalhostAuth
+ * Whether any request from the loopback device (localhost) should be
+ * automatically authenticated and authorized, without additional
+ * credentials.
+ *
+ * @property {Boolean} buildConfig.isMockHsm
+ * Whether the MockHSM API is enabled.
+ *
+ * @property {Boolean} buildConfig.isReset
+ * Whether the core reset API call is enabled.
+ *
+ * @property {Boolean} buildConfig.isPlainHttp
+ * Whether non-TLS HTTP requests (http://...) are allowed.
+ *
+ * @property {Object} health
+ * Blockchain error information.
+ */
+
+/**
+ * Chain Core can be configured as a new blockchain network, or as a node in an
+ * existing blockchain network.
+ *
+ * More info: {@link https://chain.com/docs/core/get-started/configure}
+ * @module ConfigApi
+ */
+const configAPI = (client) => {
+  return {
+    /**
+     * Reset specified Chain Core.
+     *
+     * @param {Boolean} everything - If `true`, all objects including access tokens and
+     *                               MockHSM keys will be deleted. If `false`, then access tokens
+     *                               and MockHSM keys will be preserved.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} Promise resolved on success.
+     */
+    reset: (everything = false, cb) => shared.tryCallback(
+      client.request('/reset', {everything: everything}),
+      cb
+    ),
+
+    /**
+     * Configure specified Chain Core.
+     *
+     * @param {Object} opts - options for configuring Chain Core.
+     * @param {Boolean} opts.isGenerator - Whether the local core will be a block generator
+     *                                      for the blockchain; i.e., you are starting a new blockchain on
+     *                                      the local core. `false` if you are connecting to a
+     *                                      pre-existing blockchain.
+     * @param {String} opts.generatorUrl - A URL for the block generator. Required if
+     *                                      `isGenerator` is false.
+     * @param {String} opts.generatorAccessToken - An access token provided by administrators
+     *                                               of the block generator. Required if `isGenerator` is false.
+     * @param {String} opts.blockchainId - The unique ID of the generator's blockchain.
+     *                                      Required if `isGenerator` is false.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} Promise resolved on success.
+     */
+    configure: (opts = {}, cb) => shared.tryCallback(
+      client.request('/configure', opts),
+      cb
+    ),
+
+    /**
+     * Get info on specified Chain Core.
+     *
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<CoreInfo>} Requested info of specified Chain Core.
+     */
+    info: (cb) => shared.tryCallback(
+      client.request('/info'),
+      cb
+    ),
+  }
+}
+
+module.exports = configAPI
diff --git a/src/sdk/api/hsmSigner.js b/src/sdk/api/hsmSigner.js
new file mode 100644 (file)
index 0000000..1e590f7
--- /dev/null
@@ -0,0 +1,136 @@
+const shared = require('../shared')
+
+/**
+ * @class
+ * In order to issue or transfer asset units on a blockchain, a transaction is
+ * created in Chain Core and sent to the HSM for signing. The HSM signs the
+ * transaction without ever revealing the private key. Once signed, the
+ * transaction can be submitted to the blockchain successfully.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/keys}
+ */
+class HsmSigner {
+
+  /**
+   * Create a new HSM signer object.
+   *
+   * @returns {HsmSigner}
+   */
+  constructor() {
+    this.signers = {}
+  }
+
+  /**
+   * addKey - Add a new key/signer pair to the HSM signer.
+   *
+   * @param {Object|String} key - An object with an xpub key, or an xpub as a string.
+   * @param {Connection} connection - Authenticated connection to a specific HSM instance.
+   * @returns {void}
+   */
+  addKey(key, connection) {
+    const id = `${connection.baseUrl}-${connection.token || 'noauth'}`
+    let signer = this.signers[id]
+    if (!signer) {
+      signer = this.signers[id] = {
+        connection: connection,
+        xpubs: []
+      }
+    }
+
+    signer.xpubs.push(typeof key == 'string' ? key : key.xpub)
+  }
+
+  /**
+   * sign - Sign a single transaction.
+   *
+   * @param {Object} template - A single transaction template.
+   * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {Object} Transaction template with all possible signatures added.
+   */
+  sign(template, cb) {
+    let promise = Promise.resolve(template)
+
+    // Return early if no signers
+    if (Object.keys(this.signers).length == 0) {
+      return shared.tryCallback(promise, cb)
+    }
+
+    for (let signerId in this.signers) {
+      const signer = this.signers[signerId]
+
+      promise = promise.then(nextTemplate =>
+        signer.connection.request('/sign-transaction', {
+          transactions: [nextTemplate],
+          xpubs: signer.xpubs
+        })
+      ).then(resp => resp[0])
+    }
+
+    return shared.tryCallback(promise, cb)
+  }
+
+  /**
+   * signBatch - Sign a batch of transactions.
+   *
+   * @param {Array<Object>} templates Array of transaction templates.
+   * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {BatchResponse} Tranasaction templates with all possible signatures
+   *                         added, as well as errors.
+   */
+  signBatch(templates, cb) {
+    templates = templates.filter((template) => template != null)
+    let promise = Promise.resolve(templates)
+
+    // Return early if no signers
+    if (Object.keys(this.signers).length == 0) {
+      return shared.tryCallback(promise.then(() => new shared.BatchResponse(templates)), cb)
+    }
+
+    let originalIndex = [...Array(templates.length).keys()]
+    const errors = []
+
+    for (let signerId in this.signers) {
+      const nextTemplates = []
+      const nextOriginalIndex = []
+      const signer = this.signers[signerId]
+
+      promise = promise.then(txTemplates =>
+        signer.connection.request('/sign-transaction', {
+          transactions: txTemplates,
+          xpubs: signer.xpubs
+        }).then(resp => {
+          const batchResponse = new shared.BatchResponse(resp)
+
+          batchResponse.successes.forEach((template, index) => {
+            nextTemplates.push(template)
+            nextOriginalIndex.push(originalIndex[index])
+          })
+
+          batchResponse.errors.forEach((error, index) => {
+            errors[originalIndex[index]] = error
+          })
+
+          originalIndex = nextOriginalIndex
+          return nextTemplates
+        })
+      )
+    }
+
+    return shared.tryCallback(promise.then(txTemplates => {
+      const resp = []
+      txTemplates.forEach((item, index) => {
+        resp[originalIndex[index]] = item
+      })
+
+      errors.forEach((error, index) => {
+        if (error != null) {
+          resp[index] = error
+        }
+      })
+
+      return new shared.BatchResponse(resp)
+    }), cb)
+  }
+}
+
+module.exports = HsmSigner
diff --git a/src/sdk/api/mockHsmKeys.js b/src/sdk/api/mockHsmKeys.js
new file mode 100644 (file)
index 0000000..ead5ca4
--- /dev/null
@@ -0,0 +1,83 @@
+const uuid = require('uuid')
+const shared = require('../shared')
+
+ /**
+  * Cryptographic private keys are the primary authorization mechanism on a
+  * blockchain. For development environments, Chain Core provides a convenient
+  * MockHSM.
+  * 
+  * More info: {@link https://chain.com/docs/core/build-applications/keys}
+  *
+  * @typedef {Object} MockHsmKey
+  * @global
+  *
+  * @property {String} alias
+  * User specified, unique identifier of the key.
+  *
+  * @property {String} xpub
+  * Hex-encoded string representation of the key.
+  */
+
+/**
+ * API for interacting with {@link MockHsmKey MockHSM keys}.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/keys}
+ * @module MockHsmKeysApi
+ */
+
+const mockHsmKeysAPI = (client) => {
+  return {
+    /**
+     * Create a new MockHsm key.
+     *
+     * @param {Object} [params={}] - Parameters for MockHSM key creation.
+     * @param {String} params.alias - User specified, unique identifier.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<MockHsmKey>} Newly created MockHSM key.
+     */
+    create: (params, cb) => {
+      let body = Object.assign({ clientToken: uuid.v4() }, params)
+      return shared.tryCallback(
+        client.request('/mockhsm/create-key', body).then(data => data),
+        cb
+      )
+    },
+
+    /**
+     * Get one page of MockHsm keys, optionally filtered to specified aliases.
+     *
+     * <b>NOTE</b>: The <code>filter</code> parameter of {@link Query} is unavailable for the MockHSM.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {Array.<string>} params.aliases - List of requested aliases, max 200.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<MockHsmKey>>} Requested page of results.
+     */
+    query: (params, cb) => {
+      if (Array.isArray(params.aliases) && params.aliases.length > 0) {
+        params.pageSize = params.aliases.length
+      }
+
+      return shared.query(client, 'mockHsm.keys', '/mockhsm/list-keys', params, {cb})
+    },
+
+    /**
+     * Request all MockHsm keys matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * <b>NOTE</b>: The <code>filter</code> parameter of {@link Query} is unavailable for the MockHSM.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {Array.<string>} params.aliases - List of requested aliases, max 200.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<MockHsmKey>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'mockHsm.keys', params, processor, cb),
+  }
+}
+
+module.exports = mockHsmKeysAPI
diff --git a/src/sdk/api/transactionFeeds.js b/src/sdk/api/transactionFeeds.js
new file mode 100644 (file)
index 0000000..b385282
--- /dev/null
@@ -0,0 +1,265 @@
+const shared = require('../shared')
+
+const uuid = require('uuid')
+
+/**
+ * Hardcoding value of (2 ** 63) - 1 since JavaScript rounds this value up,
+ * which causes issues when attempting to query TransactionFeed.
+ * @ignore
+ */
+const MAX_BLOCK_HEIGHT = '9223372036854775807'
+
+/**
+ * @class
+ * A single transaction feed that can be consumed. See {@link TransactionFeeds}
+ * for actions to create TransactionFeed objects.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/real-time-transaction-processing}
+ *
+ * @property {String} id
+ * Unique transaction feed identifier.
+ *
+ * @property {String} alias
+ * User specified, unique identifier.
+ *
+ * @property {String} filter
+ * @property {String} after
+ */
+class TransactionFeed {
+  /**
+   * Called once for every item received via the transaction feed.
+   *
+   * @callback FeedProcessor
+   * @param {Object} item - Item to process.
+   * @param {function(Boolean)} next - Continue to the next item when it becomes
+   *                                   available. Passing true to this callback
+   *                                   will update the feed to acknowledge that
+   *                                   the current item was consumed.
+   * @param {function(Boolean)} done - Terminate the processing loop. Passing
+   *                                   true to this callback will update the
+   *                                   feed to acknowledge that the current item
+   *                                   was consumed.
+   * @param {function(Error)} fail - Terminate the processing loop due to an
+   *                                 application-level error. This callback
+   *                                 accepts an optional error argument. The
+   *                                 feed will not be updated, and the current
+   *                                 item will not be acknowledged.
+   */
+
+  /**
+   * Create a new transaction feed consumer.
+   *
+   * @param {Object} feed - API response from {@link module:TransactionFeedsApi}
+   *                        `create` or `get` call.
+   * @param {Client} client - Configured Chain client object
+   * @returns {TransactionFeed}
+   */
+  constructor(feed, client) {
+    this.id = feed['id']
+    this.alias = feed['alias']
+    this.after = feed['after']
+    this.filter = feed['filter']
+
+    let nextAfter
+
+    const ack = () => client.request('/update-transaction-feed', {
+      id: this.id,
+      after: nextAfter,
+      previousAfter: this.after
+    }).then(() => { this.after = nextAfter })
+
+    const query = params => client.transactions.query(params)
+
+    /**
+     * Process items returned from a transaction feed in real-time.
+     *
+     * @param {FeedProcessor} consumer - Called once with each item to do any
+     *                                   desired processing. The callback can
+     *                                   optionally choose to terminate the loop.
+     * @param {Number} [timeout=86400] - Number of seconds to wait before
+     *                                   closing connection.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     */
+    this.consume = (consumer, ...args) => {
+      let timeout = 24*60*60
+      let cb
+      switch (args.length) {
+        case 0:
+          // promise with default timeout
+          break
+        case 1:
+          if (args[0] instanceof Function) {
+            cb = args[0]
+          } else {
+            timeout = args[0]
+          }
+          break
+        case 2:
+          timeout = args[0]
+          cb = args[1]
+          break
+        default:
+          throw new Error('Invalid arguments')
+      }
+
+      const promise = new Promise((resolve, reject) => {
+        let queryArgs = {
+          filter: this.filter,
+          after: this.after,
+          timeout: (timeout * 1000),
+          ascendingWithLongPoll: true,
+        }
+
+        const nextPage = () => {
+          query(queryArgs).then(page => {
+            let index = 0
+            let prevItem
+
+            const done = shouldAck => {
+              let p
+              if (shouldAck) {
+                p = ack(prevItem)
+              } else {
+                p = Promise.resolve()
+              }
+              p.then(resolve).catch(reject)
+            }
+
+            const next = shouldAck => {
+              let p
+              if (shouldAck && prevItem) {
+                p = ack(prevItem)
+              } else {
+                p = Promise.resolve()
+              }
+
+              p.then(() => {
+                if (index >= page.items.length) {
+                  queryArgs = page.next
+                  nextPage()
+                  return
+                }
+
+                prevItem = page.items[index]
+                nextAfter = `${prevItem.blockHeight}:${prevItem.position}-${MAX_BLOCK_HEIGHT}`
+                index++
+
+                // Pass the next item to the consumer, as well as three loop
+                // operations:
+                //
+                // - next(shouldAck): maybe ack, then continue/long-poll to next item.
+                // - done(shouldAck): maybe ack, then terminate the loop by fulfilling the outer promise.
+                // - fail(err): terminate the loop by rejecting the outer promise.
+                //              Use this if you want to bubble an async error up to
+                //              the outer promise catch function.
+                //
+                // The consumer can also terminate the loop by returning a promise
+                // that will reject.
+
+                let res = consumer(prevItem, next, done, reject)
+                if (res && typeof res.catch === 'function') {
+                  res.catch(reject)
+                }
+              }).catch(reject) // fail consume loop on ack failure, or on thrown exceptions from "then" function
+            }
+
+            next()
+          }).catch(reject) // fail consume loop on query failure
+        }
+
+        nextPage()
+      })
+
+      return shared.tryCallback(promise, cb)
+    }
+  }
+}
+
+/**
+ * You can use transaction feeds to process transactions as they arrive on the
+ * blockchain. This is helpful for real-time applications such as notifications
+ * or live-updating interfaces.
+ * 
+ * More info: {@link https://chain.com/docs/core/build-applications/real-time-transaction-processing}
+ * @module TransactionFeedsApi
+ */
+const transactionFeedsAPI = (client) => {
+  return {
+    /**
+     * Create a new transaction feed.
+     *
+     * @param {Object} params - Parameters for creating Transaction Feeds.
+     * @param {String} params.alias - A unique alias for the transaction feed.
+     * @param {String} params.filter - A valid filter string for the `/list-transactions`
+     *                               endpoint. The transaction feed will be composed of future
+     *                               transactions that match the filter.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<TransactionFeed>} Newly created transaction feed
+     */
+    create: (params, cb) => {
+      let body = Object.assign({ clientToken: uuid.v4() }, params)
+      return shared.tryCallback(
+        client.request('/create-transaction-feed', body).then(data => new TransactionFeed(data, client)),
+        cb
+      )
+    },
+
+    /**
+     * Get single transaction feed given an id/alias.
+     *
+     * @param {Object} params - Parameters to get single Transaction Feed.
+     * @param {String} params.id - The unique ID of a transaction feed. Either `id` or
+     *                           `alias` is required.
+     * @param {String} params.alias - The unique alias of a transaction feed. Either `id` or
+     *                              `alias` is required.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<TransactionFeed>} Requested transaction feed object
+     */
+    get: (params, cb) => shared.tryCallback(
+      client.request('/get-transaction-feed', params).then(data => new TransactionFeed(data, client)),
+      cb
+    ),
+
+    /**
+     * Delete a transaction feed given an id/alias.
+     *
+     * @param {Object} params - Parameters to delete single Transaction Feed.
+     * @param {String} params.id - The unique ID of a transaction feed. Either `id` or
+     *                           `alias` is required.
+     * @param {String} params.alias - The unique alias of a transaction feed. Either `id` or
+     *                              `alias` is required.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @return {Promise} Promise resolved on success
+     */
+    delete: (params, cb) => shared.tryCallback(
+      client.request('/delete-transaction-feed', params).then(data => data),
+      cb
+    ),
+
+
+    /**
+     * Get one page of transaction feeds.
+     *
+     * @param {Object} params={} - Pagination information.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<TransactionFeed>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'transactionFeeds', '/list-transaction-feeds', params, {cb}),
+
+    /**
+     * Request all transaction feeds matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Pagination information.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<TransactionFeed>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'transactionFeeds', params, processor, cb),
+  }
+}
+
+module.exports = transactionFeedsAPI
diff --git a/src/sdk/api/transactions.js b/src/sdk/api/transactions.js
new file mode 100644 (file)
index 0000000..5cba86f
--- /dev/null
@@ -0,0 +1,446 @@
+const shared = require('../shared')
+const errors = require('../errors')
+
+// TODO: replace with default handler in requestSingle/requestBatch variants
+function checkForError(resp) {
+  if ('code' in resp) {
+    throw errors.create(
+      errors.types.BAD_REQUEST,
+      errors.formatErrMsg(resp, ''),
+      {body: resp}
+    )
+  }
+  return resp
+}
+
+/**
+ * A blockchain consists of an immutable set of cryptographically linked
+ * transactions. Each transaction contains one or more actions.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/transaction-basics}
+ * @typedef {Object} Transaction
+ * @global
+ *
+ * @property {String} id
+ * Unique transaction identifier.
+ *
+ * @property {String} timestamp
+ * Time of transaction, RFC3339 formatted.
+ *
+ * @property {String} blockId
+ * Unique identifier, or block hash, of the block containing a transaction.
+ *
+ * @property {Number} blockHeight
+ * Height of the block containing a transaction.
+ *
+ * @property {Number} position
+ * Position of a transaction within the block.
+ *
+ * @property {Object} referenceData
+ * User specified, unstructured data embedded within a transaction.
+ *
+ * @property {Boolean} isLocal
+ * A flag indicating one or more inputs or outputs are local.
+ *
+ * @property {TransactionInput[]} inputs
+ * List of specified inputs for a transaction.
+ *
+ * @property {TransactionOutput[]} outputs
+ * List of specified outputs for a transaction.
+ */
+
+/**
+ * @typedef {Object} TransactionInput
+ * @global
+ *
+ * @property {String} type
+ * The type of the input. Possible values are "issue", "spend".
+ *
+ * @property {String} assetId
+ * The id of the asset being issued or spent.
+ *
+ * @property {String} assetAlias
+ * The alias of the asset being issued or spent (possibly null).
+ *
+ * @property {Hash} assetDefinition
+ * The definition of the asset being issued or spent (possibly null).
+ *
+ * @property {Hash} assetTags
+ * The tags of the asset being issued or spent (possibly null).
+ *
+ * @property {Boolean} assetIsLocal
+ * A flag indicating whether the asset being issued or spent is local.
+ *
+ * @property {Integer} amount
+ * The number of units of the asset being issued or spent.
+ *
+ * @property {String} spentOutputId
+ * The id of the output consumed by this input. ID is nil if this is an issuance input.
+ *
+ * @property {String} accountId
+ * The id of the account transferring the asset (possibly null if the
+ * input is an issuance or an unspent output is specified).
+ *
+ * @property {String} accountAlias
+ * The alias of the account transferring the asset (possibly null if the
+ * input is an issuance or an unspent output is specified).
+ *
+ * @property {String} accountTags
+ * The tags associated with the account (possibly null).
+ *
+ * @property {String} issuanceProgram
+ * A program specifying a predicate for issuing an asset (possibly null
+ * if input is not an issuance).
+ *
+ * @property {Object} referenceData
+ * User specified, unstructured data embedded within an input (possibly null).
+ *
+ * @property {Boolean} isLocal
+ * A flag indicating if the input is local.
+ */
+
+/**
+ * Each new transaction in the blockchain consumes some unspent outputs and
+ * creates others. An output is considered unspent when it has not yet been used
+ * as an input to a new transaction. All asset units on a blockchain exist in
+ * the unspent output set.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/unspent-outputs}
+ * @typedef {Object} TransactionOutput
+ * @global
+ *
+ * @property {String} id
+ * The id of the output.
+ *
+ * @property {String} type
+ * The type of the output. Possible values are "control" and "retire".
+ *
+ * @property {String} purpose
+ * The purpose of the output. Possible values are "receive" and "change".
+ *
+ * @property {Number} position
+ * The output's position in a transaction's list of outputs.
+ *
+ * @property {String} assetId
+ * The id of the asset being issued or spent.
+ *
+ * @property {String} assetAlias
+ * The alias of the asset being issued or spent (possibly null).
+ *
+ * @property {Hash} assetDefinition
+ * The definition of the asset being issued or spent (possibly null).
+ *
+ * @property {Hash} assetTags
+ * The tags of the asset being issued or spent (possibly null).
+ *
+ * @property {Boolean} assetIsLocal
+ * A flag indicating whether the asset being issued or spent is local.
+ *
+ * @property {Integer} amount
+ * The number of units of the asset being issued or spent.
+ *
+ * @property {String} accountId
+ * The id of the account transferring the asset (possibly null).
+ *
+ * @property {String} accountAlias
+ * The alias of the account transferring the asset (possibly null).
+ *
+ * @property {String} accountTags
+ * The tags associated with the account (possibly null).
+ *
+ * @property {String} controlProgram
+ * The control program which must be satisfied to transfer this output.
+ *
+ * @property {Object} referenceData
+ * User specified, unstructured data embedded within an input (possibly null).
+ *
+ * @property {Boolean} isLocal
+ * A flag indicating if the input is local.
+ */
+
+/**
+ * @class
+ * A convenience class for building transaction template objects.
+ */
+class TransactionBuilder {
+  /**
+   * constructor - return a new object used for constructing a transaction.
+   */
+  constructor() {
+    this.actions = []
+
+
+    /**
+     * If true, build the transaction as a partial transaction.
+     * @type {Boolean}
+     */
+    this.allowAdditionalActions = false
+
+    /**
+     * Base transaction provided by a third party.
+     * @type {Object}
+     */
+    this.baseTransaction = null
+  }
+
+  /**
+   * Add an action that issues assets.
+   *
+   * @param {Object} params - Action parameters.
+   * @param {String} params.assetId - Asset ID specifying the asset to be issued.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.assetAlias - Asset alias specifying the asset to be issued.
+   *                                      You must specify either an ID or an alias.
+   * @param {String} params.amount - Amount of the asset to be issued.
+   */
+  issue(params) {
+    this.actions.push(Object.assign({}, params, {type: 'issue'}))
+  }
+
+  /**
+   * Add an action that controls assets with an account specified by identifier.
+   *
+   * @param {Object} params - Action parameters.
+   * @option params [String] :assetId Asset ID specifying the asset to be controlled.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.assetAlias - Asset alias specifying the asset to be controlled.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.accountId - Account ID specifying the account controlling the asset.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.accountAlias - Account alias specifying the account controlling the asset.
+   *                                   You must specify either an ID or an alias.
+   * @param {Number} params.amount - Amount of the asset to be controlled.
+   */
+  controlWithAccount(params) {
+    this.actions.push(Object.assign({}, params, {type: 'control_account'}))
+  }
+
+  /**
+   * Add an action that controls assets with a receiver.
+   *
+   * @param {Object} params - Action parameters.
+   * @param {Object} params.receiver - The receiver object in which assets will be controlled.
+   * @param {String} params.assetId - Asset ID specifying the asset to be controlled.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.assetAlias - Asset alias specifying the asset to be controlled.
+   *                                   You must specify either an ID or an alias.
+   * @param {Number} params.amount - Amount of the asset to be controlled.
+   */
+  controlWithReceiver(params) {
+    this.actions.push(Object.assign({}, params, {type: 'control_receiver'}))
+  }
+
+  /**
+   * Add an action that spends assets from an account specified by identifier.
+   *
+   * @param {Object} params - Action parameters.
+   * @param {String} params.assetId - Asset ID specifying the asset to be spent.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.assetAlias - Asset alias specifying the asset to be spent.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.accountId - Account ID specifying the account spending the asset.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.accountAlias - Account alias specifying the account spending the asset.
+   *                                   You must specify either an ID or an alias.
+   * @param {Number} params.amount - Amount of the asset to be spent.
+   */
+  spendFromAccount(params) {
+    this.actions.push(Object.assign({}, params, {type: 'spend_account'}))
+  }
+
+  /**
+   * Add an action that spends an unspent output.
+   *
+   * @param {Object} params - Action parameters.
+   * @param {String} params.outputId - ID of the transaction output to be spent.
+   */
+  spendUnspentOutput(params) {
+    this.actions.push(Object.assign({}, params, {type: 'spend_account_unspent_output'}))
+  }
+
+  /**
+   * Add an action that retires units of an asset.
+   *
+   * @param {Object} params - Action parameters.
+   * @param {String} params.assetId - Asset ID specifying the asset to be retired.
+   *                                   You must specify either an ID or an alias.
+   * @param {String} params.assetAlias - Asset alias specifying the asset to be retired.
+   *                                   You must specify either an ID or an alias.
+   * @param {Number} params.amount - Amount of the asset to be retired.
+   */
+  retire(params) {
+    this.actions.push(Object.assign({}, params, {type: 'retire'}))
+  }
+
+  /**
+   * transactionReferenceData - Sets the transaction-level reference data. May
+   *                            only be used once per transaction.
+   *
+   * @param {Object} referenceData - User specified, unstructured data to
+   *                                  be embedded in a transaction.
+   */
+  transactionReferenceData(referenceData) {
+    this.actions.push({
+      type: 'set_transaction_reference_data',
+      referenceData
+    })
+  }
+}
+
+/**
+ * API for interacting with {@link Transaction transactions}.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/transaction-basics}
+ * @module TransactionsApi
+ */
+const transactionsAPI = (client) => {
+  /**
+   * Processing callback for building a transaction. The instance of
+   * {@link TransactionBuilder} modified in the function is used to build a transaction
+   * in Chain Core.
+   *
+   * @callback builderCallback
+   * @param {TransactionBuilder} builder
+   */
+
+  // TODO: implement finalize
+  const finalize = (template, cb) => shared.tryCallback(
+    Promise.resolve(template),
+    cb
+  )
+
+  // TODO: implement finalizeBatch
+  const finalizeBatch = (templates, cb) => shared.tryCallback(
+    Promise.resolve(new shared.BatchResponse(templates)),
+    cb
+  )
+
+  return {
+    /**
+     * Get one page of transactions matching the specified query.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Number} params.startTime -  A Unix timestamp in milliseconds. When specified, only transactions with a block time greater than the start time will be returned.
+     * @param {Number} params.endTime - A Unix timestamp in milliseconds. When specified, only transactions with a block time less than the start time will be returned.
+     * @param {Number} params.timeout - A time in milliseconds after which a server timeout should occur. Defaults to 1000 (1 second).
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<Transaction>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'transactions', '/list-transactions', params, {cb}),
+
+    /**
+     * Request all transactions matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Number} params.startTime -  A Unix timestamp in milliseconds. When specified, only transactions with a block time greater than the start time will be returned.
+     * @param {Number} params.endTime - A Unix timestamp in milliseconds. When specified, only transactions with a block time less than the start time will be returned.
+     * @param {Number} params.timeout - A time in milliseconds after which a server timeout should occur. Defaults to 1000 (1 second).
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<Transaction>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'transactions', params, processor, cb),
+
+    /**
+     * Build an unsigned transaction from a set of actions.
+     *
+     * @param {module:TransactionsApi~builderCallback} builderBlock - Function that adds desired actions
+     *                                         to a given builder object.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Object>} Unsigned transaction template, or error.
+     */
+    build: (builderBlock, cb) => {
+      const builder = new TransactionBuilder()
+
+      try {
+        builderBlock(builder)
+      } catch (err) {
+        return Promise.reject(err)
+      }
+
+      return shared.tryCallback(
+        client.request('/build-transaction', [builder]).then(resp => checkForError(resp[0])),
+        cb
+      )
+    },
+
+    /**
+     * Build multiple unsigned transactions from multiple sets of actions.
+     *
+     * @param {Array<module:TransactionsApi~builderCallback>} builderBlocks - Functions that add desired actions
+     *                                                 to a given builder object, one
+     *                                                 per transaction.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<BatchResponse>} Batch of unsigned transaction templates, or errors.
+     */
+    buildBatch: (builderBlocks, cb) => {
+      const builders = []
+      for (let i in builderBlocks) {
+        const b = new TransactionBuilder()
+        try {
+          builderBlocks[i](b)
+        } catch (err) {
+          return Promise.reject(err)
+        }
+        builders.push(b)
+      }
+
+      return shared.createBatch(client, '/build-transaction', builders, {cb})
+    },
+
+    /**
+     * sign - Sign a single transaction.
+     *
+     * @param {Object} template - A single transaction template.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Object} Transaction template with all possible signatures added.
+     */
+    sign: (template, cb) => finalize(template)
+      .then(finalized => client.signer.sign(finalized, cb)),
+
+    /**
+     * signBatch - Sign a batch of transactions.
+     *
+     * @param {Array<Object>} templates Array of transaction templates.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {BatchResponse} Tranasaction templates with all possible signatures
+     *                         added, as well as errors.
+     */
+    signBatch: (templates, cb) => finalizeBatch(templates)
+      // TODO: merge batch errors from finalizeBatch
+      .then(finalized => client.signer.signBatch(finalized.successes, cb)),
+
+    /**
+     * Submit a signed transaction to the blockchain.
+     *
+     * @param {Object} signed - A fully signed transaction template.
+     * @returns {Promise<Object>} Transaction ID of the successful transaction, or error.
+     */
+    submit: (signed, cb) => shared.tryCallback(
+      client.request('/submit-transaction', {transactions: [signed]}).then(resp => checkForError(resp[0])),
+      cb
+    ),
+
+    /**
+     * Submit multiple signed transactions to the blockchain.
+     *
+     * @param {Array<Object>} signed - An array of fully signed transaction templates.
+     * @returns {Promise<BatchResponse>} Batch response of transaction IDs, or errors.
+     */
+    submitBatch: (signed, cb) => shared.tryCallback(
+      client.request('/submit-transaction', {transactions: signed})
+            .then(resp => new shared.BatchResponse(resp)),
+      cb
+    ),
+  }
+}
+
+module.exports = transactionsAPI
diff --git a/src/sdk/api/unspentOutputs.js b/src/sdk/api/unspentOutputs.js
new file mode 100644 (file)
index 0000000..3bf2464
--- /dev/null
@@ -0,0 +1,104 @@
+const shared = require('../shared')
+
+/**
+ * Each new transaction in the blockchain consumes some unspent outputs and
+ * creates others. An output is considered unspent when it has not yet been used
+ * as an input to a new transaction. All asset units on a blockchain exist in
+ * the unspent output set.
+ *
+ * More info: {@link https://chain.com/docs/core/build-applications/unspent-outputs}
+ * @typedef {Object} UnspentOutput
+ * @global
+ *
+ * @property {String} id
+ * Unique transaction identifier.
+ *
+ * @property {String} type
+ * The type of the output. Possible values are "control" and "retire".
+ *
+ * @property {String} purpose
+ * The purpose of the output. Possible values are "receive" and "change".
+ *
+ * @property {String} transactionId
+ * The transaction containing the output.
+ *
+ * @property {Number} position
+ * The output's position in a transaction's list of outputs.
+ *
+ * @property {String} assetId
+ * The id of the asset being issued or spent.
+ *
+ * @property {String} assetAlias
+ * The alias of the asset being issued or spent (possibly null).
+ *
+ * @property {Object} assetDefinition
+ * The definition of the asset being issued or spent (possibly null).
+ *
+ * @property {Object} assetTags
+ * The tags of the asset being issued or spent (possibly null).
+ *
+ * @property {Boolean} assetIsLocal
+ * A flag indicating whether the asset being issued or spent is local.
+ *
+ * @property {Number} amount
+ * The number of units of the asset being issued or spent.
+ *
+ * @property {String} accountId
+ * The id of the account transferring the asset (possibly null).
+ *
+ * @property {String} accountAlias
+ * The alias of the account transferring the asset (possibly null).
+ *
+ * @property {Object} accountTags
+ * The tags associated with the account (possibly null).
+ *
+ * @property {String} controlProgram
+ * The control program which must be satisfied to transfer this output.
+ *
+ * @property {Object} referenceData
+ * User specified, unstructured data embedded within an input (possibly null).
+ *
+ * @property {Boolean} isLocal
+ * A flag indicating if the input is local.
+ */
+
+/**
+ * API for interacting with {@link UnspentOutput unspent outputs}.
+ * 
+ * More info: {@link https://chain.com/docs/core/build-applications/unspent-outputs}
+ * @module UnspentOutputsApi
+ */
+const unspentOutputsAPI = (client) => {
+  return {
+    /**
+     * Get one page of unspent outputs matching the specified query.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Integer} params.timestamp - A millisecond Unix timestamp. By using this parameter, you can perform queries that reflect the state of the blockchain at different points in time.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {pageCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise<Page<UnspentOutput>>} Requested page of results.
+     */
+    query: (params, cb) => shared.query(client, 'unspentOutputs', '/list-unspent-outputs', params, {cb}),
+
+    /**
+     * Request all unspent outputs matching the specified query, calling the
+     * supplied processor callback with each item individually.
+     *
+     * @param {Object} params={} - Filter and pagination information.
+     * @param {String} params.filter - Filter string, see {@link https://chain.com/docs/core/build-applications/queries}.
+     * @param {Array<String|Number>} params.filterParams - Parameter values for filter string (if needed).
+     * @param {Integer} params.timestamp - A millisecond Unix timestamp. By using this parameter, you can perform queries that reflect the state of the blockchain at different points in time.
+     * @param {Number} params.pageSize - Number of items to return in result set.
+     * @param {QueryProcessor<UnspentOutput>} processor - Processing callback.
+     * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+     * @returns {Promise} A promise resolved upon processing of all items, or
+     *                   rejected on error.
+     */
+    queryAll: (params, processor, cb) => shared.queryAll(client, 'unspentOutputs', params, processor, cb),
+  }
+}
+
+module.exports = unspentOutputsAPI
diff --git a/src/sdk/client.js b/src/sdk/client.js
new file mode 100644 (file)
index 0000000..205678b
--- /dev/null
@@ -0,0 +1,124 @@
+const Connection = require('./connection')
+const authorizationGrantsAPI = require('./api/authorizationGrants')
+const accessTokensAPI = require('./api/accessTokens')
+const accountsAPI = require('./api/accounts')
+const assetsAPI = require('./api/assets')
+const balancesAPI = require('./api/balances')
+const configAPI = require('./api/config')
+const hsmSigner = require('./api/hsmSigner')
+const mockHsmKeysAPI = require('./api/mockHsmKeys')
+const transactionsAPI = require('./api/transactions')
+const transactionFeedsAPI = require('./api/transactionFeeds')
+const unspentOutputsAPI = require('./api/unspentOutputs')
+
+/**
+ * The Chain API Client object is the root object for all API interactions.
+ * To interact with Chain Core, a Client object must always be instantiated
+ * first.
+ * @class
+ */
+class Client {
+  /**
+   * constructor - create a new Chain client object capable of interacting with
+   * the specified Chain Core.
+   *
+   * Passing a configuration object is the preferred way of calling this constructor.
+   * However, to support code written for 1.1 and older, the constructor supports passing
+   * in a string URL and an optional string token as the first and second parameter, respectively.
+   *
+   * @param {Object} opts - Plain JS object containing configuration options.
+   * @param {String} opts.url - Chain Core URL.
+   * @param {String} opts.accessToken - Chain Core access token.
+   * @returns {Client}
+   */
+  constructor(opts = {}) {
+    // If the first argument is a string,
+    // support the deprecated constructor params.
+    if (typeof opts === 'string') {
+      opts = {
+        url: arguments[0],
+        accessToken: arguments[1] || ''
+      }
+    }
+    opts.url = opts.url || 'http://localhost:9888'
+    this.connection = new Connection(opts.url, opts.accessToken, opts.agent)
+    this.signer = new hsmSigner()
+
+    /**
+     * API actions for access tokens
+     * @type {module:AccessTokensApi}
+     */
+    this.accessTokens = accessTokensAPI(this)
+
+    /**
+     * API actions for access control grants
+     * @type {module:AuthorizationGrantsApi}
+     */
+    this.authorizationGrants = authorizationGrantsAPI(this)
+
+    /**
+     * API actions for accounts
+     * @type {module:AccountsApi}
+     */
+    this.accounts = accountsAPI(this)
+
+    /**
+     * API actions for assets.
+     * @type {module:AssetsApi}
+     */
+    this.assets = assetsAPI(this)
+
+    /**
+     * API actions for balances.
+     * @type {module:BalancesApi}
+     */
+    this.balances = balancesAPI(this)
+
+    /**
+     * API actions for config.
+     * @type {module:ConfigApi}
+     */
+    this.config = configAPI(this)
+
+    /**
+     * @property {module:MockHsmKeysApi} keys API actions for MockHSM keys.
+     * @property {Connection} signerConnection MockHSM signer connection.
+     */
+    this.mockHsm = {
+      keys: mockHsmKeysAPI(this),
+      signerConnection: new Connection(`${opts.url}/mockhsm`, opts.accessToken, opts.agent)
+    }
+
+    /**
+     * API actions for transactions.
+     * @type {module:TransactionsApi}
+     */
+    this.transactions = transactionsAPI(this)
+
+    /**
+     * API actions for transaction feeds.
+     * @type {module:TransactionFeedsApi}
+     */
+    this.transactionFeeds = transactionFeedsAPI(this)
+
+    /**
+     * API actions for unspent outputs.
+     * @type {module:UnspentOutputsApi}
+     */
+    this.unspentOutputs = unspentOutputsAPI(this)
+  }
+
+
+  /**
+   * Submit a request to the stored Chain Core connection.
+   *
+   * @param {String} path
+   * @param {object} [body={}]
+   * @returns {Promise}
+   */
+  request(path, body = {}) {
+    return this.connection.request(path, body)
+  }
+}
+
+module.exports = Client
diff --git a/src/sdk/connection.js b/src/sdk/connection.js
new file mode 100644 (file)
index 0000000..78a50f8
--- /dev/null
@@ -0,0 +1,183 @@
+// FIXME: Microsoft Edge has issues returning errors for responses
+// with a 401 status. We should add browser detection to only
+// use the ponyfill for unsupported browsers.
+const { fetch } = require('fetch-ponyfill')()
+const errors = require('./errors')
+const btoa = require('btoa')
+
+const blacklistAttributes = [
+  'after',
+  'asset_tags',
+  'asset_definition',
+  'account_tags',
+  'next',
+  'reference_data',
+  'tags',
+]
+
+const snakeize = (object) => {
+  for(let key in object) {
+    let value = object[key]
+    let newKey = key
+
+    // Skip all-caps keys
+    if (/^[A-Z]+$/.test(key)) {
+      continue
+    }
+
+    if (/[A-Z]/.test(key)) {
+      newKey = key.replace(/([A-Z])/g, v => `_${v.toLowerCase()}`)
+      delete object[key]
+    }
+
+    if (typeof value == 'object' && blacklistAttributes.indexOf(newKey) == -1) {
+      value = snakeize(value)
+    }
+
+    object[newKey] = value
+  }
+
+  return object
+}
+
+const camelize = (object) => {
+  for (let key in object) {
+    let value = object[key]
+    let newKey = key
+
+    if (/_/.test(key)) {
+      newKey = key.replace(/([_][a-z])/g, v => v[1].toUpperCase())
+      delete object[key]
+    }
+
+    if (typeof value == 'object' && blacklistAttributes.indexOf(key) == -1) {
+      value = camelize(value)
+    }
+
+    object[newKey] = value
+  }
+
+  return object
+}
+
+/**
+ * @class
+ * Connection information for an instance of Chain Core.
+ */
+class Connection {
+  /**
+   * constructor - create a new Chain client object capable of interacting with
+   * the specified Chain Core.
+   *
+   * @param {String} baseUrl Chain Core URL.
+   * @param {String} token   Chain Core client token for API access.
+   * @param {String} agent   https.Agent used to provide TLS config.
+   * @returns {Client}
+   */
+  constructor(baseUrl, token = '', agent) {
+    this.baseUrl = baseUrl
+    this.token = token || ''
+    this.agent = agent
+  }
+
+  /**
+   * Submit a request to the specified Chain Core.
+   *
+   * @param  {String} path
+   * @param  {object} [body={}]
+   * @returns {Promise}
+   */
+  request(path, body = {}) {
+    if (!body) {
+      body = {}
+    }
+
+    // Convert camelcased request body field names to use snakecase for API
+    // processing.
+    const snakeBody = snakeize(body) // Ssssssssssss
+
+    let req = {
+      method: 'POST',
+      headers: {
+        'Accept': 'application/json',
+        'Content-Type': 'application/json',
+
+        // TODO(jeffomatic): The Fetch API has inconsistent behavior between
+        // browser implementations and polyfills.
+        //
+        // - For Edge: we can't use the browser's fetch API because it doesn't
+        // always returns a WWW-Authenticate challenge to 401s.
+        // - For Safari/Chrome: using fetch-ponyfill (the polyfill) causes
+        // console warnings if the user agent string is provided.
+        //
+        // For now, let's not send the UA string.
+        //'User-Agent': 'chain-sdk-js/0.0'
+      },
+      body: JSON.stringify(snakeBody)
+    }
+
+    if (this.token) {
+      req.headers['Authorization'] = `Basic ${btoa(this.token)}`
+    }
+
+    if (this.agent) {
+      req.agent = this.agent
+    }
+
+    return fetch(this.baseUrl + path, req).catch((err) => {
+      throw errors.create(
+        errors.types.FETCH,
+        'Fetch error: ' + err.toString(),
+        {sourceError: err}
+      )
+    }).then((resp) => {
+      if (resp.status == 204) {
+        return { status: 204 }
+      }
+
+      return resp.json().catch(() => {
+        throw errors.create(
+          errors.types.JSON,
+          'Could not parse JSON response',
+          {response: resp, status: resp.status}
+        )
+      }).then((body) => {
+        if (resp.status / 100 == 2) {
+          return body
+        }
+
+        // Everything else is a status error.
+        let errType = null
+        if (resp.status == 401) {
+          errType = errors.types.UNAUTHORIZED
+        } else if (resp.status == 404) {
+          errType = errors.types.NOT_FOUND
+        } else if (resp.status / 100 == 4) {
+          errType = errors.types.BAD_REQUEST
+        } else {
+          errType = errors.types.SERVER
+        }
+
+        throw errors.create(
+          errType,
+          errors.formatErrMsg(body, null),
+          {
+            response: resp,
+            status: resp.status,
+            body: body,
+            requestId: null
+          }
+        )
+      }).then((body) => {
+        // After processing the response, convert snakecased field names to
+        // camelcase to match language conventions.
+        return camelize(body)
+      })
+    })
+  }
+}
+
+Connection.snakeize = snakeize
+Connection.camelize = camelize
+
+module.exports = Connection
diff --git a/src/sdk/errors.js b/src/sdk/errors.js
new file mode 100644 (file)
index 0000000..d62c55f
--- /dev/null
@@ -0,0 +1,67 @@
+const lib = {
+  create: function(type, message, props = {}) {
+    let err
+    if (props.body) {
+      err = lib.newBatchError(props.body, props.requestId)
+    } else {
+      err = new Error(message)
+    }
+
+    err = Object.assign(err, props, {
+      chainClientError: true,
+      type: type,
+    })
+    return err
+  },
+
+  isChainError: function(err) {
+    return err && !!err.chainClientError
+  },
+
+  isBatchError: function (v) {
+    return v && v.code && !v.stack
+  },
+
+  newBatchError: function (body, requestId = false) {
+    let err = new Error(lib.formatErrMsg(body, requestId))
+    err.code = body.code
+    err.chainMessage = body.message
+    err.detail = body.detail
+    err.requestId = requestId
+    err.resp = body.resp
+    return err
+  },
+
+  // TODO: remove me in favor of ErrorBanner.jsx rendering
+  formatErrMsg: function(body, requestId) {
+    let tokens = []
+
+    if (typeof body.code === 'string' && body.code.length > 0) {
+      tokens.push('Code: ' + body.code)
+    }
+
+    tokens.push('Message: ' + body.message)
+
+    if (typeof body.detail === 'string' && body.detail.length > 0) {
+      tokens.push('Detail: ' + body.detail)
+    }
+
+    if (requestId) {
+      tokens.push('Request-ID: ' + requestId)
+    }
+
+    return tokens.join(' ')
+  },
+
+  types: {
+    FETCH: 'FETCH',
+    CONNECTIVITY: 'CONNECTIVITY',
+    JSON: 'JSON',
+    UNAUTHORIZED: 'UNAUTHORIZED',
+    NOT_FOUND: 'NOT_FOUND',
+    BAD_REQUEST: 'BAD_REQUEST',
+    SERVER_ERROR: 'SERVER_ERROR',
+  }
+}
+
+module.exports = lib
diff --git a/src/sdk/index.js b/src/sdk/index.js
new file mode 100644 (file)
index 0000000..e294c68
--- /dev/null
@@ -0,0 +1,7 @@
+const Client = require('./client')
+const Connection = require('./connection')
+
+module.exports = {
+  Client,
+  Connection,
+}
diff --git a/src/sdk/page.js b/src/sdk/page.js
new file mode 100644 (file)
index 0000000..0577723
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * @callback pageCallback
+ * @param {error} error
+ * @param {Page} page - Requested page of results.
+ */
+
+/**
+ * @class
+ * One page of results returned from an API request. With any given page object,
+ * the next page of results in the query set can be requested.
+ */
+class Page {
+
+  /**
+   * Create a page object
+   *
+   * @param  {Object} data API response for a single page of data.
+   * @param  {Client} client Chain Client.
+   * @param  {String} memberPath key-path pointing to module implementing the
+   *                  desired `query` method.
+   */
+  constructor(data, client, memberPath) {
+    /**
+     * Array of Chain Core objects. Available types are documented in the
+     * {@link global global namespace}.
+     *
+     * @type {Array}
+     */
+    this.items = []
+
+    /**
+     * Object representing the query for the immediate next page of results. Can
+     * be passed without modification to the `query` method that generated the
+     * Page object containing it. 
+     * @type {Object}
+     */
+    this.next = {}
+
+
+    /**
+     * Indicator that there are more results to load if true.
+     * @type {Boolean}
+     */
+    this.lastPage = false
+
+    Object.assign(this, data)
+
+    this.client = client
+    this.memberPath = memberPath
+  }
+
+  /**
+   * Fetch the next page of data for the query specified in this object.
+   *
+   * @param {objectCallback} [callback] - Optional callback. Use instead of Promise return value as desired.
+   * @returns {Promise<Page>} A promise resolving to a Page object containing
+   *                         the requested results.
+   */
+  nextPage(cb) {
+    let queryOwner = this.client
+    this.memberPath.split('.').forEach((member) => {
+      queryOwner = queryOwner[member]
+    })
+
+    return queryOwner.query(this.next, cb)
+  }
+}
+
+module.exports = Page
diff --git a/src/sdk/shared.js b/src/sdk/shared.js
new file mode 100644 (file)
index 0000000..3963faf
--- /dev/null
@@ -0,0 +1,216 @@
+const uuid = require('uuid')
+const errors = require('./errors')
+const Page = require('./page')
+
+/**
+ * @callback objectCallback
+ * @param {error} error
+ * @param {Object} object - Object response from API.
+ */
+
+/**
+ * @callback batchCallback
+ * @param {error} error
+ * @param {BatchResponse} batchResponse - Newly created objects (and errors).
+ */
+
+/**
+ * Called once for each item in the result set.
+ *
+ * @callback QueryProcessor
+ * @param {Object} item - Item to process.
+ * @param {function} next - Call to proceed to the next item for processing.
+ * @param {function(err)} done - Call to terminate iteration through the result
+ *                               set. Accepts an optional error argument which
+ *                               will be passed to the promise rejection or
+ *                               callback depending on async calling style.
+ */
+
+ /**
+  * @typedef {Object} Key
+  * @global
+  *
+  * @property {String} rootXpub
+  * @property {String} accountXpub
+  * @property {String[]} accountDerivationPath
+  */
+
+/**
+ * @class
+ */
+class BatchResponse {
+  /**
+   * constructor
+   *
+   * @param  {Array<Object>} resp - List of items which are objects or errors.
+   */
+  constructor(resp) {
+    /**
+     * Items from the input array which were successfully processed. This value
+     * is a sparsely populated array, maintaining the indexes of the items as
+     * they were originally submitted.
+     * @type {Array<Object>}
+     */
+    this.successes = []
+
+    /**
+     * Items from the input array which resulted in an error. This value
+     * is a sparsely populated array, maintaining the indexes of the items as
+     * they were originally submitted.
+     * @type {Array<Object>}
+     */
+    this.errors = []
+
+    resp.forEach((item, index) => {
+      if (item.code) {
+        this.errors[index] = item
+      } else {
+        this.successes[index] = item
+      }
+    })
+
+    /**
+     * Original input array
+     * @type {Array<Object>}
+     */
+    this.response = resp
+  }
+}
+
+const tryCallback = (promise, cb) => {
+  if (typeof cb !== 'function') return promise
+
+  return promise.then(value => {
+    setTimeout(() => cb(null, value), 0)
+  }, error => {
+    setTimeout(() => cb(error, null), 0)
+  })
+}
+
+const batchRequest = (client, path, params, cb) => {
+  return tryCallback(
+    client.request(path, params).then(resp => new BatchResponse(resp)),
+    cb
+  )
+}
+
+module.exports = {
+  batchRequest,
+
+  singletonBatchRequest: (client, path, params = {}, cb) => {
+    return tryCallback(
+      batchRequest(client, path, [params]).then(batch => {
+        if (batch.errors[0]) {
+          throw errors.newBatchError(batch.errors[0])
+        }
+        return batch.successes[0]
+      }),
+      cb
+    )
+  },
+
+  create: (client, path, params = {}, opts = {}) => {
+    const object = Object.assign({ clientToken: uuid.v4() }, params)
+    let body = object
+    if (!opts.skipArray) {
+      body = [body]
+    }
+
+    return tryCallback(
+      client.request(path, body).then(data => {
+        if (errors.isBatchError(data[0])) throw errors.newBatchError(data[0])
+
+        if (Array.isArray(data)) return data[0]
+        return data
+      }),
+      opts.cb
+    )
+  },
+
+  createBatch: (client, path, params = [], opts = {}) => {
+    params = params.map((item) =>
+      Object.assign({ clientToken: uuid.v4() }, item))
+
+    return tryCallback(
+      client.request(path, params).then(resp => new BatchResponse(resp)),
+      opts.cb
+    )
+  },
+
+  query: (client, memberPath, path, params = {}, opts = {}) => {
+    return tryCallback(
+      client.request(path, params).then(data => new Page(data, client, memberPath)),
+      opts.cb
+    )
+  },
+
+  /*
+   * NOTE: Requires query to be implemented on client for the specified member.
+   */
+  queryAll: (client, memberPath, params, processor = () => {}, cb) => {
+    let nextParams = params
+
+    let queryOwner = client
+    memberPath.split('.').forEach((member) => {
+      queryOwner = queryOwner[member]
+    })
+
+    const promise = new Promise((resolve, reject) => {
+      const done = (err) => {
+        if (cb) {
+          cb(err)
+          return
+        } else if (err) {
+          reject(err)
+        }
+
+        resolve()
+      }
+
+      const nextPage = () => {
+        queryOwner.query(nextParams).then(page => {
+          let index = 0
+          let item
+
+          const next = () => {
+            if (index >= page.items.length) {
+              if (page.lastPage) {
+                done()
+              } else {
+                nextParams = page.next
+                nextPage()
+              }
+              return
+            }
+
+            item = page.items[index]
+            index++
+
+            // Pass the next item to the processor, as well as two loop
+            // operations:
+            //
+            // - next(): Continue to next item
+            // - done(err): Then terminate the loop by fulfilling the outer promise
+            //
+            // The process can also terminate the loop by returning a promise
+            // that will reject.
+
+            let res = processor(item, next, done)
+            if (res && typeof res.catch === 'function') {
+              res.catch(reject)
+            }
+          }
+
+          next()
+        }).catch(reject) // fail processor loop on query failure
+      }
+
+      nextPage()
+    })
+
+    return tryCallback(promise, cb)
+  },
+
+  tryCallback,
+  BatchResponse,
+}
diff --git a/src/sdk/util.js b/src/sdk/util.js
new file mode 100644 (file)
index 0000000..412625c
--- /dev/null
@@ -0,0 +1,42 @@
+const x509SubjectAttributes = {
+  C: {array: true},
+  O: {array: true},
+  OU: {array: true},
+  L: {array: true},
+  ST: {array: true},
+  STREET: {array: true},
+  POSTALCODE: {array: true},
+  SERIALNUMBER: {array: false},
+  CN: {array: false},
+}
+
+const sanitizeX509GuardData = guardData => {
+  const keys = Object.keys(guardData)
+  if (keys.length !== 1 || keys[0].toLowerCase() !== 'subject') {
+    throw new Error('X509 guard data must contain exactly one key, "subject"')
+  }
+
+  const newSubject = {}
+  const oldSubject = guardData[keys[0]]
+  for (let k in oldSubject) {
+    const attrib = x509SubjectAttributes[k.toUpperCase()]
+    if (!attrib) {
+      throw new Error(`X509 guard data contains invalid subject attribute: ${k}`)
+    }
+
+    let v = oldSubject[k]
+    if (!attrib.array && Array.isArray(v)) {
+      throw new Error(`X509 guard data contains invalid array for attribute ${k}: ${v.toString()}`)
+    } else if (attrib.array && !Array.isArray(v)) {
+      newSubject[k] = [v]
+    } else {
+      newSubject[k] = v
+    }
+  }
+
+  return {subject: newSubject}
+}
+
+module.exports = {
+  sanitizeX509GuardData,
+}
index 6108c17..d9a8e34 100644 (file)
@@ -1,6 +1,6 @@
 /* global process */
 
-import chainSdk from 'chain-sdk'
+import chainSdk from 'sdk'
 import { store } from 'app'
 
 import { useRouterHistory } from 'react-router'
@@ -11,7 +11,7 @@ if (process.env.NODE_ENV === 'production') {
   apiHost = window.location.origin
   basename = '/dashboard'
 } else {
-  apiHost = process.env.API_URL || 'http://localhost:3000/api'
+  apiHost = process.env.API_URL || 'http://localhost:9888'
   basename = ''
 }