js-bigchaindb-driver/src/transaction.js

259 lines
11 KiB
JavaScript
Raw Normal View History

// Copyright BigchainDB GmbH and BigchainDB contributors
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
// Code is Apache-2.0 and docs are CC-BY-4.0
2017-11-02 20:51:24 +01:00
import { Buffer } from 'buffer'
import stableStringify from 'json-stable-stringify'
import clone from 'clone'
import base58 from 'bs58'
2018-01-04 10:15:45 +01:00
import cc from 'crypto-conditions'
2017-11-02 20:51:24 +01:00
import ccJsonify from './utils/ccJsonify'
import sha256Hash from './sha256Hash'
/**
* Construct Transactions
*/
2017-11-02 21:03:15 +01:00
export default class Transaction {
/**
* Canonically serializes a transaction into a string by sorting the keys
* @param {Object} (transaction)
2017-11-02 21:03:15 +01:00
* @return {string} a canonically serialized Transaction
*/
2018-01-15 15:42:34 +01:00
static serializeTransactionIntoCanonicalString(transaction) {
2017-11-02 21:03:15 +01:00
// BigchainDB signs fulfillments by serializing transactions into a
// "canonical" format where
2018-01-15 15:42:34 +01:00
const tx = clone(transaction)
2017-11-02 21:03:15 +01:00
// TODO: set fulfillments to null
// Sort the keys
2017-11-02 22:25:26 +01:00
return stableStringify(tx, (a, b) => (a.key > b.key ? 1 : -1))
2017-11-02 21:03:15 +01:00
}
2017-11-02 20:51:24 +01:00
2018-01-04 10:15:45 +01:00
static makeInputTemplate(publicKeys = [], fulfills = null, fulfillment = null) {
return {
2017-11-02 21:13:51 +01:00
fulfillment,
fulfills,
'owners_before': publicKeys,
}
2017-11-02 20:51:24 +01:00
}
2018-01-04 10:15:45 +01:00
static makeTransactionTemplate() {
2017-11-02 23:02:12 +01:00
const txTemplate = {
2017-11-02 21:03:15 +01:00
'id': null,
'operation': null,
'outputs': [],
'inputs': [],
'metadata': null,
'asset': null,
2018-03-21 11:30:54 +01:00
'version': '2.0',
2017-11-02 21:03:15 +01:00
}
2017-11-02 23:16:35 +01:00
return txTemplate
2017-11-02 20:51:24 +01:00
}
2018-01-15 15:42:34 +01:00
static makeTransaction(operation, asset, metadata = null, outputs = [], inputs = []) {
const tx = Transaction.makeTransactionTemplate()
2017-11-02 21:03:15 +01:00
tx.operation = operation
tx.asset = asset
tx.metadata = metadata
tx.inputs = inputs
tx.outputs = outputs
return tx
}
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
/**
* 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.
2017-11-02 21:03:15 +01:00
* 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
2017-11-02 21:03:15 +01:00
* sending it off!
*/
2018-01-15 15:42:34 +01:00
static makeCreateTransaction(asset, metadata, outputs, ...issuers) {
2017-11-02 21:03:15 +01:00
const assetDefinition = {
'data': asset || null,
}
2018-01-15 15:42:34 +01:00
const inputs = issuers.map((issuer) => Transaction.makeInputTemplate([issuer]))
2017-11-02 20:51:24 +01:00
2018-01-15 15:42:34 +01:00
return Transaction.makeTransaction('CREATE', assetDefinition, metadata, outputs, inputs)
2017-11-02 20:51:24 +01:00
}
2017-11-02 21:03:15 +01:00
/**
2017-11-02 21:32:46 +01:00
* Create an Ed25519 Cryptocondition from an Ed25519 public key
2017-11-02 21:13:51 +01:00
* to put into an Output of a Transaction
2017-11-02 21:03:15 +01:00
* @param {string} publicKey base58 encoded Ed25519 public key for the recipient of the Transaction
* @param {boolean} [json=true] If true returns a json object otherwise a crypto-condition type
* @returns {Object} Ed25519 Condition (that will need to wrapped in an Output)
2017-11-02 21:03:15 +01:00
*/
2018-01-15 15:42:34 +01:00
static makeEd25519Condition(publicKey, json = true) {
const publicKeyBuffer = Buffer.from(base58.decode(publicKey))
2017-11-02 21:03:15 +01:00
const ed25519Fulfillment = new cc.Ed25519Sha256()
ed25519Fulfillment.setPublicKey(publicKeyBuffer)
if (json) {
return ccJsonify(ed25519Fulfillment)
}
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
return ed25519Fulfillment
2017-11-02 20:51:24 +01:00
}
2017-11-02 21:03:15 +01:00
/**
* Create an Output from a Condition.
2017-11-02 21:13:51 +01:00
* 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()`)
2017-11-02 21:03:15 +01:00
* @param {string} amount Amount of the output
* @returns {Object} An Output usable in a Transaction
2017-11-02 21:03:15 +01:00
*/
2018-01-15 15:42:34 +01:00
static makeOutput(condition, amount = '1') {
2017-11-02 21:03:15 +01:00
if (typeof amount !== 'string') {
throw new TypeError('`amount` must be of type string')
}
const publicKeys = []
const getPublicKeys = details => {
if (details.type === 'ed25519-sha-256') {
if (!publicKeys.includes(details.public_key)) {
publicKeys.push(details.public_key)
}
} else if (details.type === 'threshold-sha-256') {
details.subconditions.map(getPublicKeys)
2017-11-02 20:51:24 +01:00
}
2017-11-02 21:03:15 +01:00
}
2018-01-15 15:42:34 +01:00
getPublicKeys(condition.details)
2017-11-02 21:03:15 +01:00
return {
condition,
'amount': amount,
'public_keys': publicKeys,
2017-11-02 20:51:24 +01:00
}
}
2017-11-02 21:03:15 +01:00
/**
* Create a Preimage-Sha256 Cryptocondition from a secret to put into an Output of a Transaction
* @param {string} preimage Preimage to be hashed and wrapped in a crypto-condition
* @param {boolean} [json=true] If true returns a json object otherwise a crypto-condition type
* @returns {Object} Preimage-Sha256 Condition (that will need to wrapped in an Output)
2017-11-02 21:03:15 +01:00
*/
2018-01-15 15:42:34 +01:00
static makeSha256Condition(preimage, json = true) {
2017-11-02 21:03:15 +01:00
const sha256Fulfillment = new cc.PreimageSha256()
2018-01-04 10:15:45 +01:00
sha256Fulfillment.preimage = Buffer.from(preimage)
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
if (json) {
2018-01-15 15:42:34 +01:00
return ccJsonify(sha256Fulfillment)
2017-11-02 21:03:15 +01:00
}
return sha256Fulfillment
2017-11-02 20:51:24 +01:00
}
2017-11-02 21:03:15 +01:00
/**
* Create an Sha256 Threshold Cryptocondition from threshold to put into an Output of a Transaction
* @param {number} threshold
* @param {Array} [subconditions=[]]
* @param {boolean} [json=true] If true returns a json object otherwise a crypto-condition type
* @returns {Object} Sha256 Threshold Condition (that will need to wrapped in an Output)
2017-11-02 21:03:15 +01:00
*/
2018-01-15 15:42:34 +01:00
static makeThresholdCondition(threshold, subconditions = [], json = true) {
2017-11-02 21:13:51 +01:00
const thresholdCondition = new cc.ThresholdSha256()
thresholdCondition.threshold = threshold
2017-11-02 20:51:24 +01:00
2017-11-02 21:13:51 +01:00
subconditions.forEach((subcondition) => {
// TODO: add support for Condition and URIs
thresholdCondition.addSubfulfillment(subcondition)
})
2017-11-02 20:51:24 +01:00
2017-11-02 21:13:51 +01:00
if (json) {
2018-01-15 15:42:34 +01:00
return ccJsonify(thresholdCondition)
2017-11-02 21:13:51 +01:00
}
2017-11-02 20:51:24 +01:00
2017-11-02 21:13:51 +01:00
return thresholdCondition
2017-11-02 21:03:15 +01:00
}
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
/**
* 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
2017-11-02 21:03:15 +01:00
* its Output Condition)
* @param {Object} metadata Metadata for the Transaction
* @param {Object[]} outputs Array of Output objects to add to the Transaction.
2017-11-02 21:03:15 +01:00
* 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} OutputIndices Indices of the Outputs in `unspentTransaction` that this
* Transaction fulfills.
* Note that listed public keys listed 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
2017-11-02 21:03:15 +01:00
* sending it off!
*/
// TODO:
// - Make `metadata` optional argument
2018-01-15 15:42:34 +01:00
static makeTransferTransaction(
2018-01-04 10:15:45 +01:00
unspentOutputs,
2017-11-02 21:03:15 +01:00
outputs,
2018-01-04 10:15:45 +01:00
metadata
2017-11-02 21:03:15 +01:00
) {
2018-01-04 10:15:45 +01:00
const inputs = unspentOutputs.map((unspentOutput) => {
const { tx, outputIndex } = { tx: unspentOutput.tx, outputIndex: unspentOutput.output_index }
const fulfilledOutput = tx.outputs[outputIndex]
2017-11-02 21:03:15 +01:00
const transactionLink = {
'output_index': outputIndex,
2018-01-04 10:15:45 +01:00
'transaction_id': tx.id,
2017-11-02 21:03:15 +01:00
}
2017-11-02 20:51:24 +01:00
2018-01-16 09:09:36 +01:00
return Transaction.makeInputTemplate(fulfilledOutput.public_keys, transactionLink)
2017-11-02 21:03:15 +01:00
})
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
const assetLink = {
2018-01-04 10:15:45 +01:00
'id': unspentOutputs[0].tx.operation === 'CREATE' ? unspentOutputs[0].tx.id
: unspentOutputs[0].tx.asset.id
2017-11-02 21:03:15 +01:00
}
2018-01-15 15:42:34 +01:00
return Transaction.makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs)
2017-11-02 21:03:15 +01:00
}
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
/**
* 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.
2017-11-02 21:03:15 +01:00
* @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`.
2017-11-02 21:03:15 +01:00
*/
2018-01-15 15:42:34 +01:00
static signTransaction(transaction, ...privateKeys) {
2017-11-02 21:03:15 +01:00
const signedTx = clone(transaction)
2018-06-05 15:14:15 +02:00
const serializedTransaction =
Transaction.serializeTransactionIntoCanonicalString(transaction)
2017-11-02 21:03:15 +01:00
signedTx.inputs.forEach((input, index) => {
const privateKey = privateKeys[index]
2018-01-04 10:15:45 +01:00
const privateKeyBuffer = Buffer.from(base58.decode(privateKey))
2018-03-01 12:22:36 +01:00
const transactionUniqueFulfillment = input.fulfills ? serializedTransaction
.concat(input.fulfills.transaction_id)
.concat(input.fulfills.output_index) : serializedTransaction
const transactionHash = sha256Hash(transactionUniqueFulfillment)
2017-11-02 21:03:15 +01:00
const ed25519Fulfillment = new cc.Ed25519Sha256()
2018-03-01 12:22:36 +01:00
ed25519Fulfillment.sign(Buffer.from(transactionHash, 'hex'), privateKeyBuffer)
2017-11-02 21:03:15 +01:00
const fulfillmentUri = ed25519Fulfillment.serializeUri()
2017-11-02 20:51:24 +01:00
2017-11-02 21:03:15 +01:00
input.fulfillment = fulfillmentUri
})
2017-11-02 20:51:24 +01:00
2018-06-05 16:47:17 +02:00
const serializedSignedTransaction =
Transaction.serializeTransactionIntoCanonicalString(signedTx)
2018-06-05 16:47:17 +02:00
signedTx.id = sha256Hash(serializedSignedTransaction)
2017-11-02 21:03:15 +01:00
return signedTx
}
2017-11-02 21:13:51 +01:00
}