mirror of
https://github.com/bigchaindb/js-bigchaindb-driver.git
synced 2024-11-22 01:36:56 +01:00
restructure driver and make cjs compatible
update examples and tutorials
This commit is contained in:
parent
63814c47bc
commit
a338a5c570
22
.babelrc
22
.babelrc
@ -1,18 +1,26 @@
|
||||
{
|
||||
'presets': [['latest', { es2015: { modules: false } }]],
|
||||
'presets': ['es2015-no-commonjs'],
|
||||
'plugins': [
|
||||
'transform-export-extensions',
|
||||
'transform-object-assign',
|
||||
'transform-object-rest-spread',
|
||||
[ 'transform-runtime', {
|
||||
'polyfill': false,
|
||||
'regenerator': false
|
||||
} ]
|
||||
'transform-object-rest-spread'
|
||||
],
|
||||
'sourceMaps': true,
|
||||
|
||||
'env': {
|
||||
'bundle': {
|
||||
'plugins': [
|
||||
['transform-runtime', {
|
||||
'polyfill': true,
|
||||
'regenerator': false
|
||||
}]
|
||||
]
|
||||
},
|
||||
'cjs': {
|
||||
'presets': ['latest']
|
||||
'plugins': [
|
||||
'add-module-exports',
|
||||
'transform-es2015-modules-commonjs'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
1860
dist/bundle/bundle.js
vendored
1860
dist/bundle/bundle.js
vendored
File diff suppressed because one or more lines are too long
28
dist/bundle/bundle.min.js
vendored
28
dist/bundle/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/bundle/bundle.min.js.map
vendored
1
dist/bundle/bundle.min.js.map
vendored
File diff suppressed because one or more lines are too long
399
dist/node/index.js
vendored
399
dist/node/index.js
vendored
@ -3,401 +3,24 @@
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.BDB_SERVER_URL = undefined;
|
||||
exports.Connection = exports.Transaction = exports.Ed25519KeyPair = undefined;
|
||||
|
||||
var _extends2 = require('babel-runtime/helpers/extends');
|
||||
var _Ed25519Keypair = require('./Ed25519Keypair');
|
||||
|
||||
var _extends3 = _interopRequireDefault(_extends2);
|
||||
var _Ed25519Keypair2 = _interopRequireDefault(_Ed25519Keypair);
|
||||
|
||||
exports.Ed25519Keypair = Ed25519Keypair;
|
||||
exports.makeEd25519Condition = makeEd25519Condition;
|
||||
exports.makeOutput = makeOutput;
|
||||
exports.makeCreateTransaction = makeCreateTransaction;
|
||||
exports.makeTransferTransaction = makeTransferTransaction;
|
||||
exports.signTransaction = signTransaction;
|
||||
exports.testMe = testMe;
|
||||
var _transaction = require('./transaction');
|
||||
|
||||
var _buffer = require('buffer');
|
||||
var _Transaction = _interopRequireWildcard(_transaction);
|
||||
|
||||
var _bs = require('bs58');
|
||||
var _connection = require('./connection');
|
||||
|
||||
var _bs2 = _interopRequireDefault(_bs);
|
||||
var _Connection = _interopRequireWildcard(_connection);
|
||||
|
||||
var _clone = require('clone');
|
||||
|
||||
var _clone2 = _interopRequireDefault(_clone);
|
||||
|
||||
var _fiveBellsCondition = require('five-bells-condition');
|
||||
|
||||
var _fiveBellsCondition2 = _interopRequireDefault(_fiveBellsCondition);
|
||||
|
||||
var _tweetnacl = require('tweetnacl');
|
||||
|
||||
var _tweetnacl2 = _interopRequireDefault(_tweetnacl);
|
||||
|
||||
var _jsSha = require('js-sha3');
|
||||
|
||||
var _jsSha2 = _interopRequireDefault(_jsSha);
|
||||
|
||||
var _jsonStableStringify = require('json-stable-stringify');
|
||||
|
||||
var _jsonStableStringify2 = _interopRequireDefault(_jsonStableStringify);
|
||||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* @class Keypair Ed25519 keypair in base58 (as BigchainDB expects base58 keys)
|
||||
* @type {Object}
|
||||
* @property {string} publicKey
|
||||
* @property {string} privateKey
|
||||
*/
|
||||
function Ed25519Keypair() {
|
||||
var keyPair = _tweetnacl2.default.sign.keyPair();
|
||||
this.publicKey = _bs2.default.encode(keyPair.publicKey);
|
||||
|
||||
// tweetnacl's generated secret key is the secret key + public key (resulting in a 64-byte buffer)
|
||||
this.privateKey = _bs2.default.encode(keyPair.secretKey.slice(0, 32));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Ed25519 Cryptocondition from an Ed25519 public key to put into an Output of a Transaction
|
||||
* @param {string} publicKey base58 encoded Ed25519 public key for the recipient of the Transaction
|
||||
* @returns {object} Ed25519 Condition (that will need to wrapped in an Output)
|
||||
*/
|
||||
function makeEd25519Condition(publicKey) {
|
||||
var publicKeyBuffer = new _buffer.Buffer(_bs2.default.decode(publicKey));
|
||||
|
||||
var ed25519Fulfillment = new _fiveBellsCondition2.default.Ed25519();
|
||||
ed25519Fulfillment.setPublicKey(publicKeyBuffer);
|
||||
var conditionUri = ed25519Fulfillment.getConditionUri();
|
||||
|
||||
return {
|
||||
'details': {
|
||||
'signature': null,
|
||||
'type_id': 4,
|
||||
'type': 'fulfillment',
|
||||
'bitmask': 32,
|
||||
'public_key': publicKey
|
||||
},
|
||||
'uri': conditionUri
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Output from a Condition.
|
||||
* Note: Assumes the given Condition was generated from a single public key (e.g. a Ed25519 Condition)
|
||||
* @param {object} condition Condition (e.g. a Ed25519 Condition from `makeEd25519Condition()`)
|
||||
* @param {number} amount Amount of the output
|
||||
* @returns {object} An Output usable in a Transaction
|
||||
*/
|
||||
function makeOutput(condition) {
|
||||
var amount = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
|
||||
|
||||
return {
|
||||
amount: amount,
|
||||
condition: condition,
|
||||
'public_keys': [condition.details.public_key]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `CREATE` transaction holding the `asset`, `metadata`, and `outputs`, to be signed by
|
||||
* the `issuers`.
|
||||
* @param {object} asset Created asset's data
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `CREATE` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the issuers' public
|
||||
* keys (so that the issuers are the recipients of the created asset).
|
||||
* @param {...string[]} issuers Public key of one or more issuers to the asset being created by this
|
||||
* Transaction.
|
||||
* Note: Each of the private keys corresponding to the given public
|
||||
* keys MUST be used later (and in the same order) when signing the
|
||||
* Transaction (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
function makeCreateTransaction(asset, metadata, outputs) {
|
||||
var assetDefinition = {
|
||||
'data': asset || null
|
||||
};
|
||||
|
||||
for (var _len = arguments.length, issuers = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
|
||||
issuers[_key - 3] = arguments[_key];
|
||||
}
|
||||
|
||||
var inputs = issuers.map(function (issuer) {
|
||||
return makeInputTemplate([issuer]);
|
||||
});
|
||||
|
||||
return makeTransaction('CREATE', assetDefinition, metadata, outputs, inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `TRANSFER` transaction holding the `asset`, `metadata`, and `outputs`, that fulfills
|
||||
* the `fulfilledOutputs` of `unspentTransaction`.
|
||||
* @param {object} unspentTransaction Previous Transaction you have control over (i.e. can fulfill
|
||||
* its Output Condition)
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `TRANSFER` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the public keys of
|
||||
* the recipients.
|
||||
* @param {...number} fulfilledOutputs Indices of the Outputs in `unspentTransaction` that this
|
||||
* Transaction fulfills.
|
||||
* Note that the public keys listed in the fulfilled Outputs
|
||||
* must be used (and in the same order) to sign the Transaction
|
||||
* (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
function makeTransferTransaction(unspentTransaction, metadata, outputs) {
|
||||
for (var _len2 = arguments.length, fulfilledOutputs = Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) {
|
||||
fulfilledOutputs[_key2 - 3] = arguments[_key2];
|
||||
}
|
||||
|
||||
var inputs = fulfilledOutputs.map(function (outputIndex) {
|
||||
var fulfilledOutput = unspentTransaction.outputs[outputIndex];
|
||||
var transactionLink = {
|
||||
'output': outputIndex,
|
||||
'txid': unspentTransaction.id
|
||||
};
|
||||
|
||||
return makeInputTemplate(fulfilledOutput.public_keys, transactionLink);
|
||||
});
|
||||
|
||||
var assetLink = {
|
||||
'id': unspentTransaction.operation === 'CREATE' ? unspentTransaction.id : unspentTransaction.asset.id
|
||||
};
|
||||
|
||||
return makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the given `transaction` with the given `privateKey`s, returning a new copy of `transaction`
|
||||
* that's been signed.
|
||||
* Note: Only generates Ed25519 Fulfillments. Thresholds and other types of Fulfillments are left as
|
||||
* an exercise for the user.
|
||||
* @param {object} transaction Transaction to sign. `transaction` is not modified.
|
||||
* @param {...string} privateKeys Private keys associated with the issuers of the `transaction`.
|
||||
* Looped through to iteratively sign any Input Fulfillments found in
|
||||
* the `transaction`.
|
||||
* @returns {object} The signed version of `transaction`.
|
||||
*/
|
||||
function signTransaction(transaction) {
|
||||
for (var _len3 = arguments.length, privateKeys = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
|
||||
privateKeys[_key3 - 1] = arguments[_key3];
|
||||
}
|
||||
|
||||
var signedTx = (0, _clone2.default)(transaction);
|
||||
signedTx.inputs.forEach(function (input, index) {
|
||||
var privateKey = privateKeys[index];
|
||||
var privateKeyBuffer = new _buffer.Buffer(_bs2.default.decode(privateKey));
|
||||
var serializedTransaction = serializeTransactionIntoCanonicalString(transaction, input);
|
||||
var ed25519Fulfillment = new _fiveBellsCondition2.default.Ed25519();
|
||||
ed25519Fulfillment.sign(new _buffer.Buffer(serializedTransaction), privateKeyBuffer);
|
||||
var fulfillmentUri = ed25519Fulfillment.serializeUri();
|
||||
|
||||
input.fulfillment = fulfillmentUri;
|
||||
});
|
||||
|
||||
return signedTx;
|
||||
}
|
||||
|
||||
/*********************
|
||||
* Transaction utils *
|
||||
*********************/
|
||||
|
||||
function makeTransactionTemplate() {
|
||||
return {
|
||||
'id': null,
|
||||
'operation': null,
|
||||
'outputs': [],
|
||||
'inputs': [],
|
||||
'metadata': null,
|
||||
'asset': null,
|
||||
'version': '0.9'
|
||||
};
|
||||
}
|
||||
|
||||
function makeInputTemplate() {
|
||||
var publicKeys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
||||
var fulfills = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
||||
var fulfillment = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
||||
|
||||
return {
|
||||
fulfillment: fulfillment,
|
||||
fulfills: fulfills,
|
||||
'owners_before': publicKeys
|
||||
};
|
||||
}
|
||||
|
||||
function makeTransaction(operation, asset) {
|
||||
var metadata = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
||||
var outputs = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
|
||||
var inputs = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [];
|
||||
|
||||
var tx = makeTransactionTemplate();
|
||||
tx.operation = operation;
|
||||
tx.asset = asset;
|
||||
tx.metadata = metadata;
|
||||
tx.inputs = inputs;
|
||||
tx.outputs = outputs;
|
||||
|
||||
// Hashing must be done after, as the hash is of the Transaction (up to now)
|
||||
tx.id = hashTransaction(tx);
|
||||
return tx;
|
||||
}
|
||||
|
||||
/****************
|
||||
* Crypto utils *
|
||||
****************/
|
||||
|
||||
function hashTransaction(transaction) {
|
||||
// Safely remove any tx id from the given transaction for hashing
|
||||
var tx = (0, _extends3.default)({}, transaction);
|
||||
delete tx.id;
|
||||
|
||||
return sha256Hash(serializeTransactionIntoCanonicalString(tx));
|
||||
}
|
||||
|
||||
function sha256Hash(data) {
|
||||
return _jsSha2.default.sha3_256.create().update(data).hex();
|
||||
}
|
||||
|
||||
function serializeTransactionIntoCanonicalString(transaction, input) {
|
||||
// BigchainDB signs fulfillments by serializing transactions into a "canonical" format where
|
||||
// each fulfillment URI is removed before sorting the remaining keys
|
||||
var tx = (0, _clone2.default)(transaction);
|
||||
var match = void 0;
|
||||
tx.inputs.forEach(function (_input) {
|
||||
|
||||
if (!(_input && input && _input['fulfills'] && input['fulfills'] && !(_input['fulfills']['txid'] === input['fulfills']['txid'] && _input['fulfills']['output'] === input['fulfills']['output']))) {
|
||||
match = tx.inputs.indexOf(_input);
|
||||
}
|
||||
_input.fulfillment = null;
|
||||
});
|
||||
if (input && match >= 0 && tx.inputs) {
|
||||
tx.inputs = [tx.inputs[match]];
|
||||
}
|
||||
// Sort the keys
|
||||
return (0, _jsonStableStringify2.default)(tx, function (a, b) {
|
||||
return a.key > b.key ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
var BDB_SERVER_URL = exports.BDB_SERVER_URL = process.env.BDB_SERVER_URL;
|
||||
function testMe() {
|
||||
return 'test';
|
||||
}
|
||||
|
||||
// import { request as baseRequest, sanitize } from 'js-utility-belt/es6';
|
||||
//
|
||||
// const FLASK_BASE_URL = process.env.FLASK_BASE_URL;
|
||||
// export const BDB_SERVER_URL = process.env.BDB_SERVER_URL;
|
||||
// const API_PATH = `${BDB_SERVER_URL}/api/v1/`;
|
||||
//
|
||||
//
|
||||
// const DEFAULT_REQUEST_CONFIG = {
|
||||
// credentials: 'include',
|
||||
// headers: {
|
||||
// 'Accept': 'application/json'
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// /**
|
||||
// * Small wrapper around js-utility-belt's request that provides url resolving, default settings, and
|
||||
// * response handling.
|
||||
// */
|
||||
// function request(url, config = {}) {
|
||||
// // Load default fetch configuration and remove any falsy query parameters
|
||||
// const requestConfig = Object.assign({}, DEFAULT_REQUEST_CONFIG, config, {
|
||||
// query: config.query && sanitize(config.query)
|
||||
// });
|
||||
// let apiUrl = url;
|
||||
//
|
||||
// if (requestConfig.jsonBody) {
|
||||
// requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
||||
// 'Content-Type': 'application/json'
|
||||
// });
|
||||
// }
|
||||
// if (!url) {
|
||||
// return Promise.reject(new Error('Request was not given a url.'));
|
||||
// } else if (!url.match(/^http/)) {
|
||||
// apiUrl = ApiUrls[url];
|
||||
// if (!apiUrl) {
|
||||
// return Promise.reject(new Error(`Request could not find a url mapping for "${url}"`));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return baseRequest(apiUrl, requestConfig)
|
||||
// .then((res) => res.json())
|
||||
// .catch((err) => {
|
||||
// console.error(err);
|
||||
// throw err;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// export function requestTransaction(txId) {
|
||||
// return request(ApiUrls['transactions_detail'], {
|
||||
// urlTemplateSpec: {
|
||||
// txId
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// export function postTransaction(transaction) {
|
||||
// return request(ApiUrls['transactions'], {
|
||||
// method: 'POST',
|
||||
// jsonBody: transaction
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// export function listTransactions({ asset_id, operation }) {
|
||||
// return request(ApiUrls['transactions'], {
|
||||
// query: {
|
||||
// asset_id,
|
||||
// operation
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// export function pollStatusAndFetchTransaction(transaction) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// const timer = setInterval(() => {
|
||||
// requestStatus(transaction.id)
|
||||
// .then((res) => {
|
||||
// console.log('Fetched transaction status:', res);
|
||||
// if (res.status === 'valid') {
|
||||
// clearInterval(timer);
|
||||
// requestTransaction(transaction.id)
|
||||
// .then((res) => {
|
||||
// console.log('Fetched transaction:', res);
|
||||
// resolve();
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }, 500)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// export function listOutputs({ public_key, unspent }) {
|
||||
// return request(ApiUrls['outputs'], {
|
||||
// query: {
|
||||
// public_key,
|
||||
// unspent
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// export function requestStatus(tx_id) {
|
||||
// return request(ApiUrls['statuses'], {
|
||||
// query: {
|
||||
// tx_id
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
exports.Ed25519KeyPair = _Ed25519Keypair2.default;
|
||||
exports.Transaction = _Transaction;
|
||||
exports.Connection = _Connection;
|
385
index.js
385
index.js
@ -1,385 +0,0 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import base58 from 'bs58';
|
||||
import clone from 'clone';
|
||||
import cc from 'five-bells-condition';
|
||||
import nacl from 'tweetnacl';
|
||||
import sha3 from 'js-sha3';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
/**
|
||||
* @class Keypair Ed25519 keypair in base58 (as BigchainDB expects base58 keys)
|
||||
* @type {Object}
|
||||
* @property {string} publicKey
|
||||
* @property {string} privateKey
|
||||
*/
|
||||
export function Ed25519Keypair() {
|
||||
const keyPair = nacl.sign.keyPair();
|
||||
this.publicKey = base58.encode(keyPair.publicKey);
|
||||
|
||||
// tweetnacl's generated secret key is the secret key + public key (resulting in a 64-byte buffer)
|
||||
this.privateKey = base58.encode(keyPair.secretKey.slice(0, 32));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Ed25519 Cryptocondition from an Ed25519 public key to put into an Output of a Transaction
|
||||
* @param {string} publicKey base58 encoded Ed25519 public key for the recipient of the Transaction
|
||||
* @returns {object} Ed25519 Condition (that will need to wrapped in an Output)
|
||||
*/
|
||||
export function makeEd25519Condition(publicKey) {
|
||||
const publicKeyBuffer = new Buffer(base58.decode(publicKey));
|
||||
|
||||
const ed25519Fulfillment = new cc.Ed25519();
|
||||
ed25519Fulfillment.setPublicKey(publicKeyBuffer);
|
||||
const conditionUri = ed25519Fulfillment.getConditionUri();
|
||||
|
||||
return {
|
||||
'details': {
|
||||
'signature': null,
|
||||
'type_id': 4,
|
||||
'type': 'fulfillment',
|
||||
'bitmask': 32,
|
||||
'public_key': publicKey,
|
||||
},
|
||||
'uri': conditionUri,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Output from a Condition.
|
||||
* Note: Assumes the given Condition was generated from a single public key (e.g. a Ed25519 Condition)
|
||||
* @param {object} condition Condition (e.g. a Ed25519 Condition from `makeEd25519Condition()`)
|
||||
* @param {number} amount Amount of the output
|
||||
* @returns {object} An Output usable in a Transaction
|
||||
*/
|
||||
export function makeOutput(condition, amount = 1) {
|
||||
return {
|
||||
amount,
|
||||
condition,
|
||||
'public_keys': [condition.details.public_key],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `CREATE` transaction holding the `asset`, `metadata`, and `outputs`, to be signed by
|
||||
* the `issuers`.
|
||||
* @param {object} asset Created asset's data
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `CREATE` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the issuers' public
|
||||
* keys (so that the issuers are the recipients of the created asset).
|
||||
* @param {...string[]} issuers Public key of one or more issuers to the asset being created by this
|
||||
* Transaction.
|
||||
* Note: Each of the private keys corresponding to the given public
|
||||
* keys MUST be used later (and in the same order) when signing the
|
||||
* Transaction (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
export function makeCreateTransaction(asset, metadata, outputs, ...issuers) {
|
||||
const assetDefinition = {
|
||||
'data': asset || null,
|
||||
};
|
||||
const inputs = issuers.map((issuer) => makeInputTemplate([issuer]));
|
||||
|
||||
return makeTransaction('CREATE', assetDefinition, metadata, outputs, inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `TRANSFER` transaction holding the `asset`, `metadata`, and `outputs`, that fulfills
|
||||
* the `fulfilledOutputs` of `unspentTransaction`.
|
||||
* @param {object} unspentTransaction Previous Transaction you have control over (i.e. can fulfill
|
||||
* its Output Condition)
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `TRANSFER` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the public keys of
|
||||
* the recipients.
|
||||
* @param {...number} fulfilledOutputs Indices of the Outputs in `unspentTransaction` that this
|
||||
* Transaction fulfills.
|
||||
* Note that the public keys listed in the fulfilled Outputs
|
||||
* must be used (and in the same order) to sign the Transaction
|
||||
* (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
export function makeTransferTransaction(unspentTransaction, metadata, outputs, ...fulfilledOutputs) {
|
||||
const inputs = fulfilledOutputs.map((outputIndex) => {
|
||||
const fulfilledOutput = unspentTransaction.outputs[outputIndex];
|
||||
const transactionLink = {
|
||||
'output': outputIndex,
|
||||
'txid': unspentTransaction.id,
|
||||
};
|
||||
|
||||
return makeInputTemplate(fulfilledOutput.public_keys, transactionLink);
|
||||
});
|
||||
|
||||
const assetLink = {
|
||||
'id': unspentTransaction.operation === 'CREATE' ? unspentTransaction.id
|
||||
: unspentTransaction.asset.id
|
||||
};
|
||||
|
||||
return makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the given `transaction` with the given `privateKey`s, returning a new copy of `transaction`
|
||||
* that's been signed.
|
||||
* Note: Only generates Ed25519 Fulfillments. Thresholds and other types of Fulfillments are left as
|
||||
* an exercise for the user.
|
||||
* @param {object} transaction Transaction to sign. `transaction` is not modified.
|
||||
* @param {...string} privateKeys Private keys associated with the issuers of the `transaction`.
|
||||
* Looped through to iteratively sign any Input Fulfillments found in
|
||||
* the `transaction`.
|
||||
* @returns {object} The signed version of `transaction`.
|
||||
*/
|
||||
export function signTransaction(transaction, ...privateKeys) {
|
||||
const signedTx = clone(transaction);
|
||||
signedTx.inputs.forEach((input, index) => {
|
||||
const privateKey = privateKeys[index];
|
||||
const privateKeyBuffer = new Buffer(base58.decode(privateKey));
|
||||
const serializedTransaction = serializeTransactionIntoCanonicalString(transaction, input);
|
||||
const ed25519Fulfillment = new cc.Ed25519();
|
||||
ed25519Fulfillment.sign(new Buffer(serializedTransaction), privateKeyBuffer);
|
||||
const fulfillmentUri = ed25519Fulfillment.serializeUri();
|
||||
|
||||
input.fulfillment = fulfillmentUri;
|
||||
});
|
||||
|
||||
return signedTx;
|
||||
}
|
||||
|
||||
/*********************
|
||||
* Transaction utils *
|
||||
*********************/
|
||||
|
||||
function makeTransactionTemplate() {
|
||||
return {
|
||||
'id': null,
|
||||
'operation': null,
|
||||
'outputs': [],
|
||||
'inputs': [],
|
||||
'metadata': null,
|
||||
'asset': null,
|
||||
'version': '0.9',
|
||||
};
|
||||
}
|
||||
|
||||
function makeInputTemplate(publicKeys = [], fulfills = null, fulfillment = null) {
|
||||
return {
|
||||
fulfillment,
|
||||
fulfills,
|
||||
'owners_before': publicKeys,
|
||||
};
|
||||
}
|
||||
|
||||
function makeTransaction(operation, asset, metadata = null, outputs = [], inputs = []) {
|
||||
const tx = makeTransactionTemplate();
|
||||
tx.operation = operation;
|
||||
tx.asset = asset;
|
||||
tx.metadata = metadata;
|
||||
tx.inputs = inputs;
|
||||
tx.outputs = outputs;
|
||||
|
||||
// Hashing must be done after, as the hash is of the Transaction (up to now)
|
||||
tx.id = hashTransaction(tx);
|
||||
return tx;
|
||||
}
|
||||
|
||||
/****************
|
||||
* Crypto utils *
|
||||
****************/
|
||||
|
||||
function hashTransaction(transaction) {
|
||||
// Safely remove any tx id from the given transaction for hashing
|
||||
const tx = { ...transaction };
|
||||
delete tx.id;
|
||||
|
||||
return sha256Hash(serializeTransactionIntoCanonicalString(tx));
|
||||
}
|
||||
|
||||
function sha256Hash(data) {
|
||||
return sha3.sha3_256
|
||||
.create()
|
||||
.update(data)
|
||||
.hex();
|
||||
}
|
||||
|
||||
function serializeTransactionIntoCanonicalString(transaction, input) {
|
||||
// BigchainDB signs fulfillments by serializing transactions into a "canonical" format where
|
||||
// each fulfillment URI is removed before sorting the remaining keys
|
||||
const tx = clone(transaction);
|
||||
let match;
|
||||
tx.inputs.forEach((_input) => {
|
||||
|
||||
if (!(_input && input && _input['fulfills'] && input['fulfills']
|
||||
&& !(_input['fulfills']['txid'] === input['fulfills']['txid']
|
||||
&& _input['fulfills']['output'] === input['fulfills']['output']))) {
|
||||
match = tx.inputs.indexOf(_input);
|
||||
}
|
||||
_input.fulfillment = null;
|
||||
});
|
||||
if (input && match >= 0 && tx.inputs) {
|
||||
tx.inputs = [tx.inputs[match]];
|
||||
}
|
||||
// Sort the keys
|
||||
return stableStringify(tx, (a, b) => (a.key > b.key ? 1 : -1));
|
||||
}
|
||||
|
||||
import { request as baseRequest, sanitize } from 'js-utility-belt/es6';
|
||||
|
||||
// import { request } from 'utils/request';
|
||||
//
|
||||
// const FLASK_BASE_URL = process.env.FLASK_BASE_URL;
|
||||
// export const BDB_SERVER_URL = process.env.BDB_SERVER_URL;
|
||||
// const API_PATH = `${BDB_SERVER_URL}/api/v1/`;
|
||||
//
|
||||
//
|
||||
const DEFAULT_REQUEST_CONFIG = {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Small wrapper around js-utility-belt's request that provides url resolving, default settings, and
|
||||
* response handling.
|
||||
*/
|
||||
function request(url, config = {}, onlyJsonResponse=true) {
|
||||
// Load default fetch configuration and remove any falsy query parameters
|
||||
const requestConfig = Object.assign({}, DEFAULT_REQUEST_CONFIG, config, {
|
||||
query: config.query && sanitize(config.query)
|
||||
});
|
||||
let apiUrl = url;
|
||||
|
||||
if (requestConfig.jsonBody) {
|
||||
requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
}
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('Request was not given a url.'));
|
||||
}
|
||||
|
||||
return baseRequest(apiUrl, requestConfig)
|
||||
.then((res) => {
|
||||
return onlyJsonResponse ? res.json() :
|
||||
{
|
||||
json: res.json(),
|
||||
url: res.url
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getApiUrls(API_PATH) {
|
||||
return {
|
||||
'blocks': API_PATH + 'blocks',
|
||||
'blocks_detail': API_PATH + 'blocks/%(blockId)s',
|
||||
'outputs': API_PATH + 'outputs',
|
||||
'statuses': API_PATH + 'statuses',
|
||||
'transactions': API_PATH + 'transactions',
|
||||
'transactions_detail': API_PATH + 'transactions/%(txId)s',
|
||||
'votes': API_PATH + 'votes'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function getTransaction(txId, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions_detail'], {
|
||||
urlTemplateSpec: {
|
||||
txId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function postTransaction(transaction, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions'], {
|
||||
method: 'POST',
|
||||
jsonBody: transaction
|
||||
})
|
||||
}
|
||||
|
||||
export function listTransactions({ asset_id, operation }, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions'], {
|
||||
query: {
|
||||
asset_id,
|
||||
operation
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function pollStatusAndFetchTransaction(tx_id, API_PATH) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setInterval(() => {
|
||||
getStatus(tx_id, API_PATH)
|
||||
.then((res) => {
|
||||
console.log('Fetched transaction status:', res);
|
||||
if (res.status === 'valid') {
|
||||
clearInterval(timer);
|
||||
getTransaction(tx_id, API_PATH)
|
||||
.then((res) => {
|
||||
console.log('Fetched transaction:', res);
|
||||
resolve(res);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
clearInterval(timer);
|
||||
reject(err);
|
||||
});
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
export function listOutputs({ public_key, unspent }, API_PATH, onlyJsonResponse=true) {
|
||||
return request(getApiUrls(API_PATH)['outputs'], {
|
||||
query: {
|
||||
public_key,
|
||||
unspent
|
||||
}
|
||||
}, onlyJsonResponse)
|
||||
}
|
||||
|
||||
export function getStatus(tx_id, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['statuses'], {
|
||||
query: {
|
||||
tx_id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getBlock(blockId, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['blocks_detail'], {
|
||||
urlTemplateSpec: {
|
||||
blockId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function listBlocks({tx_id, status}, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['blocks'], {
|
||||
query: {
|
||||
tx_id,
|
||||
status
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function listVotes(block_id, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['votes'], {
|
||||
query: {
|
||||
block_id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
15
package.json
15
package.json
@ -8,12 +8,12 @@
|
||||
},
|
||||
"license": "¯\\_(ツ)_/¯",
|
||||
"author": "BigchainDB",
|
||||
"main": "index.js",
|
||||
"main": "./src/index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint ./",
|
||||
"build": "npm run clean && npm run build:bundle && npm run build:cjs && npm run build:dist",
|
||||
"build:bundle": "webpack",
|
||||
"build:cjs": "cross-env BABEL_ENV=cjs babel index.js -d dist/node",
|
||||
"build:cjs": "cross-env BABEL_ENV=cjs babel ./src -d dist/node",
|
||||
"build:dist": "cross-env NODE_ENV=production webpack -p",
|
||||
"clean": "rimraf dist/bundle dist/node",
|
||||
"test": "echo \"Error: no test specified AWWWW YEAHHH\" && exit 1"
|
||||
@ -22,9 +22,13 @@
|
||||
"babel-cli": "^6.22.2",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.23.0",
|
||||
"babel-plugin-transform-export-extensions": "^6.22.0",
|
||||
"babel-plugin-transform-object-assign": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.22.0",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.23.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015-no-commonjs": "0.0.2",
|
||||
"babel-preset-latest": "^6.22.0",
|
||||
"babel-runtime": "^6.22.0",
|
||||
"cross-env": "^3.1.4",
|
||||
@ -39,12 +43,15 @@
|
||||
"buffer": "^5.0.2",
|
||||
"clone": "^2.1.0",
|
||||
"core-js": "^2.4.1",
|
||||
"decamelize": "^1.2.0",
|
||||
"es6-promise": "^4.0.5",
|
||||
"fetch-ponyfill": "^4.0.0",
|
||||
"five-bells-condition": "=3.3.1",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"js-sha3": "^0.5.7",
|
||||
"js-utility-belt": "^1.5.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"sprintf-js": "^1.0.3",
|
||||
"tweetnacl": "^0.14.5"
|
||||
},
|
||||
"keywords": [
|
||||
|
15
src/Ed25519Keypair.js
Normal file
15
src/Ed25519Keypair.js
Normal file
@ -0,0 +1,15 @@
|
||||
import base58 from 'bs58';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
/**
|
||||
* @class Keypair Ed25519 keypair in base58 (as BigchainDB expects base58 keys)
|
||||
* @type {Object}
|
||||
* @property {string} publicKey
|
||||
* @property {string} privateKey
|
||||
*/
|
||||
export default function Ed25519Keypair() {
|
||||
const keyPair = nacl.sign.keyPair();
|
||||
this.publicKey = base58.encode(keyPair.publicKey);
|
||||
// tweetnacl's generated secret key is the secret key + public key (resulting in a 64-byte buffer)
|
||||
this.privateKey = base58.encode(keyPair.secretKey.slice(0, 32));
|
||||
}
|
81
src/baseRequest.js
Normal file
81
src/baseRequest.js
Normal file
@ -0,0 +1,81 @@
|
||||
import { Promise } from 'es6-promise';
|
||||
import fetchPonyfill from 'fetch-ponyfill';
|
||||
import { vsprintf } from 'sprintf-js';
|
||||
|
||||
import formatText from './format_text';
|
||||
|
||||
import stringifyAsQueryParam from './stringify_as_query_param';
|
||||
|
||||
|
||||
const fetch = fetchPonyfill(Promise);
|
||||
|
||||
|
||||
/**
|
||||
* imported from https://github.com/bigchaindb/js-utility-belt/
|
||||
*
|
||||
* Global fetch wrapper that adds some basic error handling and ease of use enhancements.
|
||||
* Considers any non-2xx response as an error.
|
||||
*
|
||||
* For more information on fetch, see https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch.
|
||||
*
|
||||
* Expects fetch to already be available (either in a ES6 environment, bundled through webpack, or
|
||||
* injected through a polyfill).
|
||||
*
|
||||
* @param {string} url Url to request. Can be specified as a sprintf format string (see
|
||||
* https://github.com/alexei/sprintf.js) that will be resolved using
|
||||
* `config.urlTemplateSpec`.
|
||||
* @param {object} config Additional configuration, mostly passed to fetch as its 'init' config
|
||||
* (see https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch#Parameters).
|
||||
* @param {*} config.jsonBody Json payload to the request. Will automatically be
|
||||
* JSON.stringify()-ed and override `config.body`.
|
||||
* @param {string|object} config.query Query parameter to append to the end of the url.
|
||||
* If specified as an object, keys will be
|
||||
* decamelized into snake case first.
|
||||
* @param {*[]|object} config.urlTemplateSpec Format spec to use to expand the url (see sprintf).
|
||||
* @param {*} config.* All other options are passed through to fetch.
|
||||
*
|
||||
* @return {Promise} Promise that will resolve with the response if its status was 2xx;
|
||||
* otherwise rejects with the response
|
||||
*/
|
||||
export default function baseRequest(url, { jsonBody, query, urlTemplateSpec, ...fetchConfig } = {}) {
|
||||
let expandedUrl = url;
|
||||
|
||||
if (urlTemplateSpec != null) {
|
||||
if (Array.isArray(urlTemplateSpec) && urlTemplateSpec.length) {
|
||||
// Use vsprintf for the array call signature
|
||||
expandedUrl = vsprintf(url, urlTemplateSpec);
|
||||
} else if (urlTemplateSpec &&
|
||||
typeof urlTemplateSpec === 'object' &&
|
||||
Object.keys(urlTemplateSpec).length) {
|
||||
expandedUrl = formatText(url, urlTemplateSpec);
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Supplied urlTemplateSpec was not an array or object. Ignoring...');
|
||||
}
|
||||
}
|
||||
|
||||
if (query != null) {
|
||||
if (typeof query === 'string') {
|
||||
expandedUrl += query;
|
||||
} else if (query && typeof query === 'object') {
|
||||
expandedUrl += stringifyAsQueryParam(query);
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Supplied query was not a string or object. Ignoring...');
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonBody != null) {
|
||||
fetchConfig.body = JSON.stringify(jsonBody);
|
||||
}
|
||||
|
||||
return fetch.fetch(expandedUrl, fetchConfig)
|
||||
.then((res) => {
|
||||
// If status is not a 2xx (based on Response.ok), assume it's an error
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
|
||||
if (!(res && res.ok)) {
|
||||
throw res;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
11
src/connection/getApiUrls.js
Normal file
11
src/connection/getApiUrls.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default function getApiUrls(API_PATH) {
|
||||
return {
|
||||
'blocks': API_PATH + 'blocks',
|
||||
'blocks_detail': API_PATH + 'blocks/%(blockId)s',
|
||||
'outputs': API_PATH + 'outputs',
|
||||
'statuses': API_PATH + 'statuses',
|
||||
'transactions': API_PATH + 'transactions',
|
||||
'transactions_detail': API_PATH + 'transactions/%(txId)s',
|
||||
'votes': API_PATH + 'votes'
|
||||
};
|
||||
}
|
13
src/connection/getBlock.js
Normal file
13
src/connection/getBlock.js
Normal file
@ -0,0 +1,13 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function getBlock(blockId, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['blocks_detail'], {
|
||||
urlTemplateSpec: {
|
||||
blockId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
11
src/connection/getStatus.js
Normal file
11
src/connection/getStatus.js
Normal file
@ -0,0 +1,11 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function getStatus(tx_id, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['statuses'], {
|
||||
query: {
|
||||
tx_id
|
||||
}
|
||||
});
|
||||
}
|
11
src/connection/getTransaction.js
Normal file
11
src/connection/getTransaction.js
Normal file
@ -0,0 +1,11 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function getTransaction(txId, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions_detail'], {
|
||||
urlTemplateSpec: {
|
||||
txId
|
||||
}
|
||||
});
|
||||
}
|
9
src/connection/index.js
Normal file
9
src/connection/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
export getBlock from './getBlock';
|
||||
export getTransaction from './getTransaction';
|
||||
export getStatus from './getStatus';
|
||||
export listBlocks from './listBlocks';
|
||||
export listOutputs from './listOutputs';
|
||||
export listTransactions from './listTransactions';
|
||||
export listVotes from './listVotes';
|
||||
export pollStatusAndFetchTransaction from './pollStatusAndFetchTransaction';
|
||||
export postTransaction from './postTransaction';
|
12
src/connection/listBlocks.js
Normal file
12
src/connection/listBlocks.js
Normal file
@ -0,0 +1,12 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function listBlocks({tx_id, status}, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['blocks'], {
|
||||
query: {
|
||||
tx_id,
|
||||
status
|
||||
}
|
||||
});
|
||||
}
|
12
src/connection/listOutputs.js
Normal file
12
src/connection/listOutputs.js
Normal file
@ -0,0 +1,12 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function listOutputs({ public_key, unspent }, API_PATH, onlyJsonResponse=true) {
|
||||
return request(getApiUrls(API_PATH)['outputs'], {
|
||||
query: {
|
||||
public_key,
|
||||
unspent
|
||||
}
|
||||
}, onlyJsonResponse)
|
||||
}
|
12
src/connection/listTransactions.js
Normal file
12
src/connection/listTransactions.js
Normal file
@ -0,0 +1,12 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function listTransactions({ asset_id, operation }, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions'], {
|
||||
query: {
|
||||
asset_id,
|
||||
operation
|
||||
}
|
||||
})
|
||||
}
|
12
src/connection/listVotes.js
Normal file
12
src/connection/listVotes.js
Normal file
@ -0,0 +1,12 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function listVotes(block_id, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['votes'], {
|
||||
query: {
|
||||
block_id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
26
src/connection/pollStatusAndFetchTransaction.js
Normal file
26
src/connection/pollStatusAndFetchTransaction.js
Normal file
@ -0,0 +1,26 @@
|
||||
import getTransaction from './getTransaction';
|
||||
import getStatus from './getStatus';
|
||||
|
||||
|
||||
export default function (tx_id, API_PATH) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setInterval(() => {
|
||||
getStatus(tx_id, API_PATH)
|
||||
.then((res) => {
|
||||
console.log('Fetched transaction status:', res);
|
||||
if (res.status === 'valid') {
|
||||
clearInterval(timer);
|
||||
getTransaction(tx_id, API_PATH)
|
||||
.then((res) => {
|
||||
console.log('Fetched transaction:', res);
|
||||
resolve(res);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
clearInterval(timer);
|
||||
reject(err);
|
||||
});
|
||||
}, 500)
|
||||
})
|
||||
}
|
11
src/connection/postTransaction.js
Normal file
11
src/connection/postTransaction.js
Normal file
@ -0,0 +1,11 @@
|
||||
import getApiUrls from './getApiUrls';
|
||||
import request from '../request';
|
||||
|
||||
|
||||
export default function postTransaction(transaction, API_PATH) {
|
||||
return request(getApiUrls(API_PATH)['transactions'], {
|
||||
method: 'POST',
|
||||
jsonBody: transaction
|
||||
})
|
||||
}
|
||||
|
97
src/format_text.js
Normal file
97
src/format_text.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { sprintf } from 'sprintf-js';
|
||||
|
||||
|
||||
// Regexes taken from or inspired by sprintf-js
|
||||
const Regex = {
|
||||
TEMPLATE_LITERAL: /\${([^\)]+?)}/g,
|
||||
KEY: /^([a-z_][a-z_\d]*)/i,
|
||||
KEY_ACCESS: /^\.([a-z_][a-z_\d]*)/i,
|
||||
INDEX_ACCESS: /^\[(\d+)\]/
|
||||
};
|
||||
|
||||
/**
|
||||
* imported from https://github.com/bigchaindb/js-utility-belt/
|
||||
*
|
||||
* Formats strings similarly to C's sprintf, with the addition of '${...}' formats.
|
||||
*
|
||||
* Makes a first pass replacing '${...}' formats before passing the expanded string and other
|
||||
* arguments to sprintf-js. For more information on what sprintf can do, see
|
||||
* https://github.com/alexei/sprintf.js.
|
||||
*
|
||||
* Examples:
|
||||
* formatText('Hi there ${dimi}!', { dimi: 'Dimi' })
|
||||
* => 'Hi there Dimi!'
|
||||
*
|
||||
* formatText('${database} is %(status)s', { database: 'BigchainDB', status: 'big' })
|
||||
* => 'BigchainDB is big'
|
||||
*
|
||||
* Like sprintf-js, string interpolation for keywords and indexes is supported too:
|
||||
* formatText('Berlin is best known for its ${berlin.topKnownFor[0].name}', {
|
||||
* berlin: {
|
||||
* topKnownFor: [{
|
||||
* name: 'Currywurst'
|
||||
* }, ...
|
||||
* ]
|
||||
* }
|
||||
* })
|
||||
* => 'Berlin is best known for its Currywurst'
|
||||
*/
|
||||
export default function formatText(s, ...argv) {
|
||||
let expandedFormatStr = s;
|
||||
|
||||
// Try to replace formats of the form '${...}' if named replacement fields are used
|
||||
if (s && argv.length === 1 && typeof argv[0] === 'object') {
|
||||
const templateSpecObj = argv[0];
|
||||
|
||||
expandedFormatStr = s.replace(Regex.TEMPLATE_LITERAL, (match, replacement) => {
|
||||
let interpolationLeft = replacement;
|
||||
|
||||
/**
|
||||
* Interpolation algorithm inspired by sprintf-js.
|
||||
*
|
||||
* Goes through the replacement string getting the left-most key or index to interpolate
|
||||
* on each pass. `value` at each step holds the last interpolation result, `curMatch` is
|
||||
* the current property match, and `interpolationLeft` is the portion of the replacement
|
||||
* string still to be interpolated.
|
||||
*
|
||||
* It's useful to note that RegExp.exec() returns with an array holding:
|
||||
* [0]: Full string matched
|
||||
* [1+]: Matching groups
|
||||
*
|
||||
* And that in the regexes defined, the first matching group always corresponds to the
|
||||
* property matched.
|
||||
*/
|
||||
let value;
|
||||
let curMatch = Regex.KEY.exec(interpolationLeft);
|
||||
if (curMatch !== null) {
|
||||
value = templateSpecObj[curMatch[1]];
|
||||
|
||||
// Assigning in the conditionals here makes the code less bloated
|
||||
/* eslint-disable no-cond-assign */
|
||||
while ((interpolationLeft = interpolationLeft.substring(curMatch[0].length)) &&
|
||||
value != null) {
|
||||
if ((curMatch = Regex.KEY_ACCESS.exec(interpolationLeft))) {
|
||||
value = value[curMatch[1]];
|
||||
} else if ((curMatch = Regex.INDEX_ACCESS.exec(interpolationLeft))) {
|
||||
value = value[curMatch[1]];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-cond-assign */
|
||||
}
|
||||
|
||||
// If there's anything left to interpolate by the end then we've failed to interpolate
|
||||
// the entire replacement string.
|
||||
if (interpolationLeft.length) {
|
||||
throw new SyntaxError(
|
||||
`[formatText] failed to parse named argument key: ${replacement}`
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
return sprintf(expandedFormatStr, ...argv);
|
||||
}
|
5
src/index.js
Normal file
5
src/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
export Ed25519Keypair from './Ed25519Keypair';
|
||||
|
||||
export * as Transaction from './transaction';
|
||||
export * as Connection from './connection';
|
45
src/request.js
Normal file
45
src/request.js
Normal file
@ -0,0 +1,45 @@
|
||||
import baseRequest from './baseRequest';
|
||||
import sanitize from './sanitize';
|
||||
|
||||
|
||||
const DEFAULT_REQUEST_CONFIG = {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Small wrapper around js-utility-belt's request that provides url resolving, default settings, and
|
||||
* response handling.
|
||||
*/
|
||||
export default function request(url, config = {}, onlyJsonResponse=true) {
|
||||
// Load default fetch configuration and remove any falsy query parameters
|
||||
const requestConfig = Object.assign({}, DEFAULT_REQUEST_CONFIG, config, {
|
||||
query: config.query && sanitize(config.query)
|
||||
});
|
||||
let apiUrl = url;
|
||||
|
||||
if (requestConfig.jsonBody) {
|
||||
requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
}
|
||||
if (!url) {
|
||||
return Promise.reject(new Error('Request was not given a url.'));
|
||||
}
|
||||
|
||||
return baseRequest(apiUrl, requestConfig)
|
||||
.then((res) => {
|
||||
return onlyJsonResponse ? res.json() :
|
||||
{
|
||||
json: res.json(),
|
||||
url: res.url
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
64
src/sanitize.js
Normal file
64
src/sanitize.js
Normal file
@ -0,0 +1,64 @@
|
||||
import coreIncludes from 'core-js/library/fn/array/includes';
|
||||
import coreObjectEntries from 'core-js/library/fn/object/entries';
|
||||
|
||||
|
||||
/**
|
||||
* Abstraction for selectFromObject and omitFromObject for DRYness.
|
||||
* Set isInclusion to true if the filter should be for including the filtered items (ie. selecting
|
||||
* only them vs omitting only them).
|
||||
*/
|
||||
function filterFromObject(obj, filter, { isInclusion = true } = {}) {
|
||||
if (filter && Array.isArray(filter)) {
|
||||
return applyFilterOnObject(obj, isInclusion ? ((_, key) => coreIncludes(filter, key))
|
||||
: ((_, key) => !coreIncludes(filter, key)));
|
||||
} else if (filter && typeof filter === 'function') {
|
||||
// Flip the filter fn's return if it's for inclusion
|
||||
return applyFilterOnObject(obj, isInclusion ? filter
|
||||
: (...args) => !filter(...args));
|
||||
} else {
|
||||
throw new Error('The given filter is not an array or function. Exclude aborted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtered copy of the given object's own enumerable properties (no inherited
|
||||
* properties), keeping any keys that pass the given filter function.
|
||||
*/
|
||||
function applyFilterOnObject(obj, filterFn) {
|
||||
if (filterFn == null) {
|
||||
return Object.assign({}, obj);
|
||||
}
|
||||
|
||||
const filteredObj = {};
|
||||
coreObjectEntries(obj).forEach(([key, val]) => {
|
||||
if (filterFn(val, key)) {
|
||||
filteredObj[key] = val;
|
||||
}
|
||||
});
|
||||
|
||||
return filteredObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to lodash's _.pick(), this returns a copy of the given object's
|
||||
* own and inherited enumerable properties, selecting only the keys in
|
||||
* the given array or whose value pass the given filter function.
|
||||
* @param {object} obj Source object
|
||||
* @param {array|function} filter Array of key names to select or function to invoke per iteration
|
||||
* @return {object} The new object
|
||||
*/
|
||||
function selectFromObject(obj, filter) {
|
||||
return filterFromObject(obj, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Glorified selectFromObject. Takes an object and returns a filtered shallow copy that strips out
|
||||
* any properties that are falsy (including coercions, ie. undefined, null, '', 0, ...).
|
||||
* Does not modify the passed in object.
|
||||
*
|
||||
* @param {object} obj Javascript object
|
||||
* @return {object} Sanitized Javascript object
|
||||
*/
|
||||
export default function sanitize(obj) {
|
||||
return selectFromObject(obj, (val) => !!val);
|
||||
}
|
8
src/sha256Hash.js
Normal file
8
src/sha256Hash.js
Normal file
@ -0,0 +1,8 @@
|
||||
import sha3 from 'js-sha3';
|
||||
|
||||
export default function sha256Hash(data) {
|
||||
return sha3.sha3_256
|
||||
.create()
|
||||
.update(data)
|
||||
.hex();
|
||||
}
|
42
src/stringify_as_query_param.js
Normal file
42
src/stringify_as_query_param.js
Normal file
@ -0,0 +1,42 @@
|
||||
import coreObjectEntries from 'core-js/library/fn/object/entries';
|
||||
import decamelize from 'decamelize';
|
||||
import queryString from 'query-string';
|
||||
|
||||
|
||||
/**
|
||||
* imported from https://github.com/bigchaindb/js-utility-belt/
|
||||
*
|
||||
* Takes a key-value dictionary (ie. object) and converts it to a query-parameter string that you
|
||||
* can directly append into a URL.
|
||||
*
|
||||
* Extends queryString.stringify by allowing you to specify a `transform` function that will be
|
||||
* invoked on each of the dictionary's keys before being stringified into the query-parameter
|
||||
* string.
|
||||
*
|
||||
* By default `transform` is `decamelize`, so a dictionary of the form:
|
||||
*
|
||||
* {
|
||||
* page: 1,
|
||||
* pageSize: 10
|
||||
* }
|
||||
*
|
||||
* will be converted to a string like:
|
||||
*
|
||||
* ?page=1&page_size=10
|
||||
*
|
||||
* @param {object} obj Query params dictionary
|
||||
* @param {function} [transform=decamelize] Transform function for each of the param keys
|
||||
* @return {string} Query param string
|
||||
*/
|
||||
export default function stringifyAsQueryParam(obj, transform = decamelize) {
|
||||
if (!obj || typeof obj !== 'object' || !Object.keys(obj).length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const transformedKeysObj = coreObjectEntries(obj).reduce((paramsObj, [key, value]) => {
|
||||
paramsObj[transform(key)] = value;
|
||||
return paramsObj;
|
||||
}, {});
|
||||
|
||||
return `?${queryString.stringify(transformedKeysObj)}`;
|
||||
}
|
10
src/transaction/hashTransaction.js
Normal file
10
src/transaction/hashTransaction.js
Normal file
@ -0,0 +1,10 @@
|
||||
import serializeTransactionIntoCanonicalString from './serializeTransactionIntoCanonicalString';
|
||||
import sha256Hash from '../sha256Hash';
|
||||
|
||||
export default function hashTransaction(transaction) {
|
||||
// Safely remove any tx id from the given transaction for hashing
|
||||
const tx = { ...transaction };
|
||||
delete tx.id;
|
||||
|
||||
return sha256Hash(serializeTransactionIntoCanonicalString(tx));
|
||||
}
|
6
src/transaction/index.js
Normal file
6
src/transaction/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
export makeEd25519Condition from './makeEd25519Condition';
|
||||
export makeCreateTransaction from './makeCreateTransaction';
|
||||
export makeOutput from './makeOutput';
|
||||
export makeTransaction from './makeTransaction';
|
||||
export makeTransferTransaction from './makeTransferTransaction';
|
||||
export signTransaction from './signTransaction';
|
29
src/transaction/makeCreateTransaction.js
Normal file
29
src/transaction/makeCreateTransaction.js
Normal file
@ -0,0 +1,29 @@
|
||||
import makeInputTemplate from './makeInputTemplate';
|
||||
import makeTransaction from './makeTransaction';
|
||||
|
||||
/**
|
||||
* Generate a `CREATE` transaction holding the `asset`, `metadata`, and `outputs`, to be signed by
|
||||
* the `issuers`.
|
||||
* @param {object} asset Created asset's data
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `CREATE` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the issuers' public
|
||||
* keys (so that the issuers are the recipients of the created asset).
|
||||
* @param {...string[]} issuers Public key of one or more issuers to the asset being created by this
|
||||
* Transaction.
|
||||
* Note: Each of the private keys corresponding to the given public
|
||||
* keys MUST be used later (and in the same order) when signing the
|
||||
* Transaction (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
export default function makeCreateTransaction(asset, metadata, outputs, ...issuers) {
|
||||
const assetDefinition = {
|
||||
'data': asset || null,
|
||||
};
|
||||
const inputs = issuers.map((issuer) => makeInputTemplate([issuer]));
|
||||
|
||||
return makeTransaction('CREATE', assetDefinition, metadata, outputs, inputs);
|
||||
}
|
28
src/transaction/makeEd25519Condition.js
Normal file
28
src/transaction/makeEd25519Condition.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import base58 from 'bs58';
|
||||
import cc from 'five-bells-condition';
|
||||
|
||||
/**
|
||||
* Create an Ed25519 Cryptocondition from an Ed25519 public key to put into an Output of a Transaction
|
||||
* @param {string} publicKey base58 encoded Ed25519 public key for the recipient of the Transaction
|
||||
* @returns {object} Ed25519 Condition (that will need to wrapped in an Output)
|
||||
*/
|
||||
export default function makeEd25519Condition(publicKey) {
|
||||
const publicKeyBuffer = new Buffer(base58.decode(publicKey));
|
||||
|
||||
const ed25519Fulfillment = new cc.Ed25519();
|
||||
ed25519Fulfillment.setPublicKey(publicKeyBuffer);
|
||||
const conditionUri = ed25519Fulfillment.getConditionUri();
|
||||
|
||||
return {
|
||||
'details': {
|
||||
'signature': null,
|
||||
'type_id': 4,
|
||||
'type': 'fulfillment',
|
||||
'bitmask': 32,
|
||||
'public_key': publicKey,
|
||||
},
|
||||
'uri': conditionUri,
|
||||
};
|
||||
}
|
7
src/transaction/makeInputTemplate.js
Normal file
7
src/transaction/makeInputTemplate.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function makeInputTemplate(publicKeys = [], fulfills = null, fulfillment = null) {
|
||||
return {
|
||||
fulfillment,
|
||||
fulfills,
|
||||
'owners_before': publicKeys,
|
||||
};
|
||||
}
|
14
src/transaction/makeOutput.js
Normal file
14
src/transaction/makeOutput.js
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Create an Output from a Condition.
|
||||
* Note: Assumes the given Condition was generated from a single public key (e.g. a Ed25519 Condition)
|
||||
* @param {object} condition Condition (e.g. a Ed25519 Condition from `makeEd25519Condition()`)
|
||||
* @param {number} amount Amount of the output
|
||||
* @returns {object} An Output usable in a Transaction
|
||||
*/
|
||||
export default function makeOutput(condition, amount = 1) {
|
||||
return {
|
||||
amount,
|
||||
condition,
|
||||
'public_keys': [condition.details.public_key],
|
||||
};
|
||||
}
|
28
src/transaction/makeTransaction.js
Normal file
28
src/transaction/makeTransaction.js
Normal file
@ -0,0 +1,28 @@
|
||||
import hashTransaction from './hashTransaction';
|
||||
|
||||
|
||||
function makeTransactionTemplate() {
|
||||
return {
|
||||
'id': null,
|
||||
'operation': null,
|
||||
'outputs': [],
|
||||
'inputs': [],
|
||||
'metadata': null,
|
||||
'asset': null,
|
||||
'version': '0.9',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function makeTransaction(operation, asset, metadata = null, outputs = [], inputs = []) {
|
||||
const tx = makeTransactionTemplate();
|
||||
tx.operation = operation;
|
||||
tx.asset = asset;
|
||||
tx.metadata = metadata;
|
||||
tx.inputs = inputs;
|
||||
tx.outputs = outputs;
|
||||
|
||||
// Hashing must be done after, as the hash is of the Transaction (up to now)
|
||||
tx.id = hashTransaction(tx);
|
||||
return tx;
|
||||
}
|
40
src/transaction/makeTransferTransaction.js
Normal file
40
src/transaction/makeTransferTransaction.js
Normal file
@ -0,0 +1,40 @@
|
||||
import makeInputTemplate from './makeInputTemplate';
|
||||
import makeTransaction from './makeTransaction';
|
||||
|
||||
/**
|
||||
* Generate a `TRANSFER` transaction holding the `asset`, `metadata`, and `outputs`, that fulfills
|
||||
* the `fulfilledOutputs` of `unspentTransaction`.
|
||||
* @param {object} unspentTransaction Previous Transaction you have control over (i.e. can fulfill
|
||||
* its Output Condition)
|
||||
* @param {object} metadata Metadata for the Transaction
|
||||
* @param {object[]} outputs Array of Output objects to add to the Transaction.
|
||||
* Think of these as the recipients of the asset after the transaction.
|
||||
* For `TRANSFER` Transactions, this should usually just be a list of
|
||||
* Outputs wrapping Ed25519 Conditions generated from the public keys of
|
||||
* the recipients.
|
||||
* @param {...number} fulfilledOutputs Indices of the Outputs in `unspentTransaction` that this
|
||||
* Transaction fulfills.
|
||||
* Note that the public keys listed in the fulfilled Outputs
|
||||
* must be used (and in the same order) to sign the Transaction
|
||||
* (`signTransaction()`).
|
||||
* @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before
|
||||
* sending it off!
|
||||
*/
|
||||
export default function makeTransferTransaction(unspentTransaction, metadata, outputs, ...fulfilledOutputs) {
|
||||
const inputs = fulfilledOutputs.map((outputIndex) => {
|
||||
const fulfilledOutput = unspentTransaction.outputs[outputIndex];
|
||||
const transactionLink = {
|
||||
'output': outputIndex,
|
||||
'txid': unspentTransaction.id,
|
||||
};
|
||||
|
||||
return makeInputTemplate(fulfilledOutput.public_keys, transactionLink);
|
||||
});
|
||||
|
||||
const assetLink = {
|
||||
'id': unspentTransaction.operation === 'CREATE' ? unspentTransaction.id
|
||||
: unspentTransaction.asset.id
|
||||
};
|
||||
|
||||
return makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs);
|
||||
}
|
24
src/transaction/serializeTransactionIntoCanonicalString.js
Normal file
24
src/transaction/serializeTransactionIntoCanonicalString.js
Normal file
@ -0,0 +1,24 @@
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import clone from 'clone';
|
||||
|
||||
|
||||
export default function serializeTransactionIntoCanonicalString(transaction, input) {
|
||||
// BigchainDB signs fulfillments by serializing transactions into a "canonical" format where
|
||||
// each fulfillment URI is removed before sorting the remaining keys
|
||||
const tx = clone(transaction);
|
||||
let match;
|
||||
tx.inputs.forEach((_input) => {
|
||||
|
||||
if (!(_input && input && _input['fulfills'] && input['fulfills']
|
||||
&& !(_input['fulfills']['txid'] === input['fulfills']['txid']
|
||||
&& _input['fulfills']['output'] === input['fulfills']['output']))) {
|
||||
match = tx.inputs.indexOf(_input);
|
||||
}
|
||||
_input.fulfillment = null;
|
||||
});
|
||||
if (input && match >= 0 && tx.inputs) {
|
||||
tx.inputs = [tx.inputs[match]];
|
||||
}
|
||||
// Sort the keys
|
||||
return stableStringify(tx, (a, b) => (a.key > b.key ? 1 : -1));
|
||||
}
|
34
src/transaction/signTransaction.js
Normal file
34
src/transaction/signTransaction.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import base58 from 'bs58';
|
||||
import cc from 'five-bells-condition';
|
||||
import clone from 'clone';
|
||||
|
||||
import serializeTransactionIntoCanonicalString from './serializeTransactionIntoCanonicalString';
|
||||
|
||||
|
||||
/**
|
||||
* Sign the given `transaction` with the given `privateKey`s, returning a new copy of `transaction`
|
||||
* that's been signed.
|
||||
* Note: Only generates Ed25519 Fulfillments. Thresholds and other types of Fulfillments are left as
|
||||
* an exercise for the user.
|
||||
* @param {object} transaction Transaction to sign. `transaction` is not modified.
|
||||
* @param {...string} privateKeys Private keys associated with the issuers of the `transaction`.
|
||||
* Looped through to iteratively sign any Input Fulfillments found in
|
||||
* the `transaction`.
|
||||
* @returns {object} The signed version of `transaction`.
|
||||
*/
|
||||
export default function signTransaction(transaction, ...privateKeys) {
|
||||
const signedTx = clone(transaction);
|
||||
signedTx.inputs.forEach((input, index) => {
|
||||
const privateKey = privateKeys[index];
|
||||
const privateKeyBuffer = new Buffer(base58.decode(privateKey));
|
||||
const serializedTransaction = serializeTransactionIntoCanonicalString(transaction, input);
|
||||
const ed25519Fulfillment = new cc.Ed25519();
|
||||
ed25519Fulfillment.sign(new Buffer(serializedTransaction), privateKeyBuffer);
|
||||
const fulfillmentUri = ed25519Fulfillment.serializeUri();
|
||||
|
||||
input.fulfillment = fulfillmentUri;
|
||||
});
|
||||
|
||||
return signedTx;
|
||||
}
|
@ -9,7 +9,7 @@ const webpack = require('webpack');
|
||||
const PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
|
||||
const PATHS = {
|
||||
ENTRY: path.resolve(__dirname, 'index.js'),
|
||||
ENTRY: path.resolve(__dirname, './src/index.js'),
|
||||
BUNDLE: path.resolve(__dirname, 'dist/bundle'),
|
||||
NODE_MODULES: path.resolve(__dirname, 'node_modules'),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user