1
0
mirror of https://github.com/bigchaindb/js-bigchaindb-driver.git synced 2024-11-22 01:36:56 +01:00

ignore yarn.lock

This commit is contained in:
Matthias Kretschmann 2017-04-26 15:58:19 +02:00
commit e595bf812a
43 changed files with 11383 additions and 0 deletions

26
.babelrc Normal file
View File

@ -0,0 +1,26 @@
{
'presets': ['es2015-no-commonjs'],
'plugins': [
'transform-export-extensions',
'transform-object-assign',
'transform-object-rest-spread'
],
'sourceMaps': true,
'env': {
'bundle': {
'plugins': [
['transform-runtime', {
'polyfill': true,
'regenerator': false
}]
]
},
'cjs': {
'plugins': [
'add-module-exports',
'transform-es2015-modules-commonjs'
]
}
}
}

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
build/*
dist/*
node_modules/*

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "ascribe"
}

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
*.seed
*.log
*.dat
*.out
*.pid
*.gz
.idea
*.sublime-project
*.sublime-workspace
*.sublime-workspace
.env
build/*
node_modules/*

332
README.md Normal file
View File

@ -0,0 +1,332 @@
# JavaScript quickstart for BigchainDB
> :bangbang: High chance of :fire: and :rage: ahead if you expect this to be production-ready.
> :bangbang: **ONLY** (and I mean **_only_**) supports BigchainDB Server 0.9
Some naive helpers to get you on your way to making some transactions :boom:, if you'd like to use
[BigchainDB](https://github.com/bigchaindb/bigchaindb) with JavaScript.
Aimed to support usage in browsers or node; if it doesn't, well, I don't know what to say except
it's probably you :smirk:. Use at your own risk :rocket:. At least I can tell you it's ES∞+, so
you'll probably need a babel here and a bundler there (or use [one of the built versions](./dist)),
of which I expect you'll know quite well ([otherwise, go check out js-reactor :wink:](https://github.com/bigchaindb/js-reactor)).
## Getting started
Srs, just read through [index.js](./index.js) and see if you can make any sense of it.
You may also be interested in a [long-form example with actual code](#example).
The expected flow for making transactions:
1. Go get yourself some keypairs! Just make a `new Keypair()` (or a whole bunch of them, nobody's
counting :sunglasses:).
1. Go get yourself a condition! `makeEd25519Condition()` should do the trick :sparkles:.
1. Go wrap that condition as an output (don't worry about the *why*)! `makeOutput()` no sweat
:muscle:.
1. (**Optional**) You've got everyting you need, except for an asset. Maybe define one (any
JSON-serializable object will do).
1. Time to get on the rocket ship, baby. `makeCreateTransaction()` your way to lifelong glory and
fame :clap:!
1. Ok, now you've got a transaction, but we need you to *sign* (`signTransaction()`) it cause, you
know... cryptography and `¯\_(ツ)_/¯`.
1. Alright, sick dude, you've *finally* got everything you need to `POST` to a server. Phew
:sweat_drops:. Go `fetch()` your way to business, start:point_up:life4evar!
...
Alright, alright, so you've made a couple transactions. Now what? Do I hear you saying
"<sub>Transfer them??</sub>" No problem, brotha, I gotcha covered :neckbeard:.
1. Go get some more outputs (wrapping conditions), maybe based on some new made-up friends (i.e.
keypairs).
1. Go make a transfer transaction, using the transaction you want to *spend* (i.e. you can fulfill)
in `makeTransferTransaction()` :v:. *If you're not sure what any of this means (and you're as
confused as I think you are right now), you might wanna go check out [this](https://docs.bigchaindb.com/projects/server/en/latest/data-models/crypto-conditions.html)
and [this](https://docs.bigchaindb.com/projects/py-driver/en/latest/usage.html#asset-transfer)
and [this](https://tools.ietf.org/html/draft-thomas-crypto-conditions-01) first.*
1. Sign that transaction with `signTransaction()`!
1. `POST` to the server, and watch the :dollar:s drop, man.
## Needs for speeds
This implementation plays "safe" by using JS-native (or downgradable) libraries for its
crypto-related functions to keep compatabilities with the browser. If that makes you :unamused: and
you'd rather go :godmode: with some :zap: :zap:, you can try using some of these to go as fast as a
:speedboat: --:surfing_man: :
* [chloride](https://github.com/dominictarr/chloride), or its underlying [sodium](https://github.com/paixaop/node-sodium)
library
* [node-sha3](https://github.com/phusion/node-sha3) -- **MAKE SURE** to use [steakknife's fork](https://github.com/steakknife/node-sha3)
if [the FIPS 202 upgrade](https://github.com/phusion/node-sha3/pull/25) hasn't been merged
(otherwise, you'll run into all kinds of hashing problems)
## :rotating_light: WARNING WARNING WARNING :rotating_light:
> Crypto-conditions
Make sure you keep using a crypto-conditions implementation that implements the older v1 draft (e.g.
[`five-bells-condition@v3.3.1`](https://github.com/interledgerjs/five-bells-condition/releases/tag/v3.3.1)).
BigchainDB Server 0.9 does not implement the newer version of the spec and **WILL** fail if you to
use a newer implementation of crypto-conditions.
> SHA3
Make sure to use a SHA3 implementation that has been upgraded as per [FIPS 202](http://csrc.nist.gov/publications/drafts/fips-202/fips_202_draft.pdf).
Otherwise, the hashes you generate **WILL** be invalid in the eyes of the BigchainDB Server.
> Ed25519
If you do end up replacing `tweetnacl` with `chloride` (or any other Ed25519 package), you might
want to double check that it gives you a correct public/private (or verifying/signing, if they use
that lingo) keypair.
An example BigchainDB Server-generated keypair (encoded in base58):
- Public: "DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
- Private: "7Gf5YRch2hYTyeLxqNLgTY63D9K5QH2UQ7LYFeBGuKvo"
Your package should be able to take in the decoded version of the **private** key and return you the
same **public** key (once you encode that to base58).
-------
## Example
OK, OK, I gotcha, you'd rather see some *actual* code rather than a giant list of steps that don't
mean anything. :point_down: is for you.
```js
import {
Ed25519Keypair,
makeEd25519Condition,
makeOutput,
makeCreateTransaction,
makeTransferTransaction,
signTransaction,
} from 'js-bigchaindb-quickstart'; // Or however you'd like to import it
/**********************
* CREATE transaction *
**********************/
// First, create a keypair for our new friend, Ash (let's be real--who would you rather catch some
// Pokemon: Alice or the Ketchum man himself?)
const ash = new Ed25519Keypair();
console.log(ash.publicKey); // something like "DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
console.log(ash.privateKey); // something like "7Gf5YRch2hYTyeLxqNLgTY63D9K5QH2UQ7LYFeBGuKvo"
// Let's get an output and condition that lets Ash be the recipient of the new asset we're creating
const ashCondition = new makeEd25519Condition(ash.publicKey);
const ashOutput = new makeOutput(ashCondition);
console.log(ashOutput);
/* Something like
{
"amount": 1,
"condition": {
"details": {
"signature": null,
"type_id": 4,
"type": "fulfillment",
"bitmask": 32,
"public_key": "DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
},
"uri": "cc:4:20:vSfobaaMSP52nxnVkPiLMysCTR-t8JpjbWIdU6SvRYU:96"
},
"public_keys": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
*/
// Let's make an asset, to pretend this isn't boring.
const pokeAsset = {
'name': 'Pikachu',
'trait': 'Will never, ever, EVAARRR leave your back'
};
const noMetadata = null; // Let's ignore that meta-stuff for now
// Now let's go give Ash his beloved Pikachu
const createPokeTx = makeCreateTransaction(pokeAsset, noMetadata, [ashOutput], ash.publicKey);
console.log(createPokeTx);
/* Something like
{
"id": "38acf7a938a39be335afc8e7300468b981a29813d52938104ba3badfe21470c9",
"operation": "CREATE",
"outputs": [
{
"amount": 1,
"condition": {
"details": {
"signature": null,
"type_id": 4,
"type": "fulfillment",
"bitmask": 32,
"public_key": "DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
},
"uri": "cc:4:20:vSfobaaMSP52nxnVkPiLMysCTR-t8JpjbWIdU6SvRYU:96"
},
"public_keys": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
],
"inputs": [
{
"fulfillment": null,
"fulfills": null,
"owners_before": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
],
"metadata": null,
"asset": {
"data": {
"name": "Pikachu",
"trait": "Will never, ever, EVAARRR leave your back"
}
},
"version": "0.9"
}
*/
// Let's sign this thing to make it legit! (Let's call Ash the "issuer", but a registered PokeCorp
// could be the one issuing instead)
const signedCreateTx = signTransaction(createPokeTx, ash.privateKey);
console.log(signedPokeTx);
/* Something like
{
"id": "38acf7a938a39be335afc8e7300468b981a29813d52938104ba3badfe21470c9",
"operation": "CREATE",
"outputs": [
{
"amount": 1,
"condition": {
"details": {
"signature": null,
"type_id": 4,
"type": "fulfillment",
"bitmask": 32,
"public_key": "DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
},
"uri": "cc:4:20:vSfobaaMSP52nxnVkPiLMysCTR-t8JpjbWIdU6SvRYU:96"
},
"public_keys": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
],
"inputs": [
{
"fulfillment": "cf:4:vSfobaaMSP52nxnVkPiLMysCTR-t8JpjbWIdU6SvRYWj-cp1qb1vsTSt_775cGe-NQFxgyUQvcPx1nWkJRgXhMvTk2vN2QJU_nd2DgeTbIcWBF-8-N1SH2WqQLsXJLcP",
"fulfills": null,
"owners_before": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
],
"metadata": null,
"asset": {
"data": {
"name": "Pikachu",
"trait": "Will never, ever, EVAARRR leave your back"
}
},
"version": "0.9"
}
*/
// Alright, now you've got yourself a valid transaction and you can do some crazy thing like send it
// over to a BigchainDB node. I'll leave that as an exercise for you ;).
/************************
* TRANSFER transaction *
************************/
// Alright, let's get Ash some imaginary friends (remember Brock? Neither do I)
const brock = new Ed25519Keypair(); // public: "H8ZVy61CCKh5VQV9nzzzggNW8e5CyTbSiegpdLqLSmqi", private: "5xoYuPP92pznaGZF9KLsyAdR5C7yDU79of1KA9UK4qKS"
// Let's pretend that, for the sake of this example, Ash can actually part with Pikachu. Let's trade
// Pikachu to Brock (we won't be getting anything back, but if it helps, you can pretend Brock'll
// give Ash some help with his love life).
const brockCondition = new makeEd25519Condition(brock.publicKey);
const brockOutput = new makeOutput(brockCondition);
// Let's create the TRANSFER transaction cementing this trade. We'll use the "unspent" CREATE
// transaction that assigned Pikachu to Ash as an input to this TRANSFER.
// Note that we'll keep ignoring that metadata stuff.
// Also note that we could use either `createPokeTx` (unsigned) or `signedCreateTx` (signed) here
// for the input transaction. Either way, we'll be fulfilling the first (and only) output set in it.
const fulfilledOutputIndex = 0;
const transferPokeTx = makeTransferTransaction(createPokeTx, noMetadata, [brockOutput], fulfilledOutputIndex);
// OK, let's sign this TRANSFER (Ash has to, as he's the one currently in "control" of Pikachu)
const signedTransferTx = signTransaction(transferPokeTx, ash.privateKey);
console.log(signedTransferTx);
/* If everything went well, you should get something like this
{
"id": "0876962a40479e171135cd92dbae7f0216f2691561b56a579cff631371d4d128",
"operation": "TRANSFER",
"outputs": [
{
"amount": 1,
"condition": {
"details": {
"signature": null,
"type_id": 4,
"type": "fulfillment",
"bitmask": 32,
"public_key": "H8ZVy61CCKh5VQV9nzzzggNW8e5CyTbSiegpdLqLSmqi"
},
"uri": "cc:4:20:76rNv-DAIjZC0-68Gl0KEuDpcJRpCAAQXxvVbTvQAxE:96"
},
"public_keys": [
"H8ZVy61CCKh5VQV9nzzzggNW8e5CyTbSiegpdLqLSmqi"
]
}
],
"inputs": [
{
"fulfillment": "cf:4:vSfobaaMSP52nxnVkPiLMysCTR-t8JpjbWIdU6SvRYU8UJKi0Oq7QoCXIHuiWEYzxfgVEYs9HHtDIWBSkq1uvMX6l7VKwUCrK93k6JMNVBA8djOa5UGfDDF49xLVEgQI",
"fulfills": {
"output": 0,
"txid": "38acf7a938a39be335afc8e7300468b981a29813d52938104ba3badfe21470c9"
},
"owners_before": [
"DjPMHDD9JtgypDKY38mPz9f6owjAMAKhLuN1JfRAat8C"
]
}
],
"metadata": null,
"asset": {
"id": "38acf7a938a39be335afc8e7300468b981a29813d52938104ba3badfe21470c9"
},
"version": "0.9"
}
*/
// Assuming you figured out how to send a transaction to a BigchainDB node, and that the federation
// you sent it to has validated the CREATE transaction you sent, you should now be able to cement
// the TRANSFER of Pikachu to Brock by sending `signedTransferTx` to a node in the same federation.
=========================================================================================================
/*************************************************************
* *
* ~~~ CHALLENGE ~~~ *
* *
* So who's making the decentralized version of Pokemon? *
* (cause I want in) *
* *
*************************************************************/
```

13
constants/api_urls.js Normal file
View File

@ -0,0 +1,13 @@
/* eslint-disable prefer-template */
import { API_PATH } from './application_constants';
const ApiUrls = {
'transactions': API_PATH + 'transactions',
'transactions_detail': API_PATH + 'transactions/%(txId)s',
'outputs': API_PATH + 'outputs',
'statuses': API_PATH + 'statuses'
};
export default ApiUrls;

View File

@ -0,0 +1,8 @@
export const FLASK_BASE_URL = process.env.FLASK_BASE_URL;
export const BDB_SERVER_URL = process.env.BDB_SERVER_URL;
export const API_PATH = `${BDB_SERVER_URL}/api/v1/`;
export default {
API_PATH,
FLASK_BASE_URL,
};

12
dist/README.md vendored Normal file
View File

@ -0,0 +1,12 @@
# Built files, for your convenience
~Voila~ ma boies, built files so you don't have to do a thing! Just copy pasta, and get on your way.
### `/bundled`
Babelified, packaged with dependencies, and built (if you'd like), so you can drop it in anywhere
you want.
### `/node`
Babelified into a CommonJS module, so you can drop it in on any node project.

9981
dist/bundle/bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

26
dist/node/index.js vendored Normal file
View File

@ -0,0 +1,26 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Connection = exports.Transaction = exports.Ed25519Keypair = undefined;
var _Ed25519Keypair2 = require('./Ed25519Keypair');
var _Ed25519Keypair3 = _interopRequireDefault(_Ed25519Keypair2);
var _transaction = require('./transaction');
var _Transaction = _interopRequireWildcard(_transaction);
var _connection = require('./connection');
var _Connection = _interopRequireWildcard(_connection);
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 }; }
exports.Ed25519Keypair = _Ed25519Keypair3.default;
exports.Transaction = _Transaction;
exports.Connection = _Connection;

61
package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "js-bigchaindb-quickstart",
"version": "0.0.1",
"description": "Some quickstarting for BigchainDB with JavaScript (node + browser)",
"repository": {
"type": "git",
"url": "git+https://github.com/sohkai/js-bigchaindb-quickstart.git"
},
"license": "¯\\_(ツ)_/¯",
"author": "BigchainDB",
"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 ./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"
},
"devDependencies": {
"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.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",
"eslint": "^3.14.1",
"eslint-config-ascribe": "^3.0.1",
"eslint-plugin-import": "^2.2.0",
"rimraf": "^2.5.4",
"webpack": "^2.2.1"
},
"dependencies": {
"bs58": "^4.0.0",
"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": [
"bigchaindb",
"cryptoconditions"
]
}

27
src/Ed25519Keypair.js Normal file
View File

@ -0,0 +1,27 @@
import base58 from 'bs58';
import nacl from 'tweetnacl';
import sha3 from 'js-sha3';
/**
* @class Keypair Ed25519 keypair in base58 (as BigchainDB expects base58 keys)
* @type {Object}
* @param {number} secret A seed that will be used as a key derivation function
* @property {string} publicKey
* @property {string} privateKey
*/
export default function Ed25519Keypair(secret) {
let keyPair;
if (secret) {
// Quick and dirty: use key derivation function instead
const secretHash = sha3.sha3_256
.create()
.update(secret)
.array();
keyPair = nacl.sign.keyPair.fromSeed(new Uint8Array(secretHash))
} else {
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
View 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;
});
}

View 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'
};
}

View 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
}
});
}

View 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
}
});
}

View 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
View 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';

View 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
}
});
}

View 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)
}

View 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
}
})
}

View 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
}
});
}

View 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)
})
}

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,8 @@
import sha3 from 'js-sha3';
export default function sha256Hash(data) {
return sha3.sha3_256
.create()
.update(data)
.hex();
}

View 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)}`;
}

View 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
View 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';

View 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);
}

View 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,
};
}

View File

@ -0,0 +1,7 @@
export default function makeInputTemplate(publicKeys = [], fulfills = null, fulfillment = null) {
return {
fulfillment,
fulfills,
'owners_before': publicKeys,
};
}

View 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],
};
}

View 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;
}

View 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);
}

View File

@ -0,0 +1,10 @@
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
const tx = clone(transaction);
// Sort the keys
return stableStringify(tx, (a, b) => (a.key > b.key ? 1 : -1));
}

View 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;
}

63
utils/bigchaindb_utils.js Normal file
View File

@ -0,0 +1,63 @@
import request from './request';
import ApiUrls from '../constants/api_urls';
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
}
});
}

44
utils/request.js Normal file
View File

@ -0,0 +1,44 @@
import { request as baseRequest, sanitize } from 'js-utility-belt/es6';
import ApiUrls from '../constants/api_urls';
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 = {}) {
// 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;
});
}

80
webpack.config.js Normal file
View File

@ -0,0 +1,80 @@
/* eslint-disable strict, no-console, object-shorthand */
'use strict';
const path = require('path');
const webpack = require('webpack');
const PRODUCTION = process.env.NODE_ENV === 'production';
const PATHS = {
ENTRY: path.resolve(__dirname, './src/index.js'),
BUNDLE: path.resolve(__dirname, 'dist/bundle'),
NODE_MODULES: path.resolve(__dirname, 'node_modules'),
};
/** PLUGINS **/
const PLUGINS = [
new webpack.NoEmitOnErrorsPlugin(),
];
const PROD_PLUGINS = [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
},
output: {
comments: false,
},
sourceMap: true,
}),
new webpack.LoaderOptionsPlugin({
debug: false,
minimize: true,
}),
];
if (PRODUCTION) {
PLUGINS.push(...PROD_PLUGINS);
}
/** EXPORTED WEBPACK CONFIG **/
const config = {
entry: [PATHS.ENTRY],
output: {
filename: PRODUCTION ? 'bundle.min.js' : 'bundle.js',
library: 'js-bigchaindb-quickstart',
libraryTarget: 'umd',
path: PATHS.BUNDLE,
},
devtool: PRODUCTION ? '#source-map' : '#inline-source-map',
resolve: {
extensions: ['.js'],
modules: ['node_modules'], // Don't use absolute path here to allow recursive matching
},
plugins: PLUGINS,
module: {
rules: [
{
test: /\.js$/,
exclude: [PATHS.NODE_MODULES],
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}],
},
],
},
};
module.exports = config;