|
| 1 | +import { Buffer } from 'buffer'; |
| 2 | +import crypto from 'crypto'; |
| 3 | + |
| 4 | +import base58 from 'bs58'; |
| 5 | +import clone from 'clone'; |
| 6 | +import cc from 'five-bells-condition'; |
| 7 | +import nacl from 'tweetnacl'; |
| 8 | +import stableStringify from 'json-stable-stringify'; |
| 9 | +import uuid from 'uuid'; |
| 10 | + |
| 11 | +/** |
| 12 | + * @class Keypair Ed25519 keypair in base58 (as BigchainDB expects base58 keys) |
| 13 | + * @type {Object} |
| 14 | + * @property {string} publicKey |
| 15 | + * @property {string} privateKey |
| 16 | + */ |
| 17 | +export function Keypair() { |
| 18 | + const keyPair = nacl.sign.keyPair(); |
| 19 | + this.publicKey = base58.encode(keyPair.publicKey); |
| 20 | + this.privateKey = base58.encode(keyPair.secretKey); |
| 21 | +} |
| 22 | + |
| 23 | +/** |
| 24 | + * Create an Ed25519 Cryptocondition from an Ed25519 public key to put into a transaction |
| 25 | + * @param {string} publicKey base58 encoded Ed25519 public key for the new "owner" |
| 26 | + * @returns {object} Ed25519 Condition in a format compatible with BigchainDB |
| 27 | + * Note: Assumes that 'cid' will be adjusted afterwards. |
| 28 | + */ |
| 29 | +export function makeEd25519Condition(publicKey) { |
| 30 | + const publicKeyBuffer = new Buffer(base58.decode(publicKey)); |
| 31 | + |
| 32 | + const ed25519Fulfillment = new cc.Ed25519(); |
| 33 | + ed25519Fulfillment.setPublicKey(publicKeyBuffer); |
| 34 | + const conditionUri = ed25519Fulfillment.getConditionUri(); |
| 35 | + |
| 36 | + return { |
| 37 | + 'amount': 1, |
| 38 | + 'condition': { |
| 39 | + 'cid': 0, // Will be adjusted after adding the condition to the transaction |
| 40 | + 'owners_after': [publicKey], |
| 41 | + 'uri': conditionUri, |
| 42 | + 'details': { |
| 43 | + 'signature': null, |
| 44 | + 'type_id': 4, |
| 45 | + 'type': 'fulfillment', |
| 46 | + 'bitmask': 32, |
| 47 | + 'public_key': publicKey, |
| 48 | + }, |
| 49 | + }, |
| 50 | + }; |
| 51 | +} |
| 52 | + |
| 53 | +/** |
| 54 | + * Create an "empty" Ed25519 fulfillment from a ED25519 public key to put into a transaction. |
| 55 | + * This "mock" step is necessary in order for a transaction to be completely out so it can later |
| 56 | + * be serialized and signed. |
| 57 | + * @param {string} publicKey base58 encoded Ed25519 public key for the previous "owner" |
| 58 | + * @returns {object} Ed25519 Condition in a format compatible with BigchainDB |
| 59 | + * Note: Assumes that 'cid' will be adjusted afterwards. |
| 60 | + */ |
| 61 | +export function makeEd25519Fulfillment(publicKey) { |
| 62 | + return { |
| 63 | + 'owners_before': [publicKey], |
| 64 | + 'fid': 0, // Will be adjusted after adding the fulfillment to the transaction |
| 65 | + 'input': null, // Will be filled out after adding the fulfillment to the transaction |
| 66 | + 'fulfillment': null, // Will be generated during signing |
| 67 | + }; |
| 68 | +} |
| 69 | + |
| 70 | +/** |
| 71 | + * Generate a `CREATE` transaction holding the `assetData`, `metaData`, `conditions`, and |
| 72 | + * `fulfillments`. |
| 73 | + * @param {object} assetData Asset's `data` property |
| 74 | + * @param {object=} metaData Metadata's `data` property |
| 75 | + * @param {object[]=} conditions Array of condition objectss to add to the transaction. |
| 76 | + * Think of these as the new "owners" of the asset after the transaction. |
| 77 | + * For `CREATE` transactions, this should usually just be an Ed25519 |
| 78 | + * Condition generated from the creator's public key. |
| 79 | + * @param {object[]=} fulfillments Array of fulfillment objects to add to the transaction |
| 80 | + * Think of these as proofs that you can manipulate the asset. |
| 81 | + * For `CREATE` transactions, this should usually just be an |
| 82 | + * Ed25519 Fulfillment generated from the creator's public key. |
| 83 | + * @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before |
| 84 | + * sending it off! |
| 85 | + */ |
| 86 | +export function makeCreateTransaction(assetData, metadata, conditions, fulfillments) { |
| 87 | + const asset = { |
| 88 | + 'id': uuid.v4(), |
| 89 | + 'data': assetData || null, |
| 90 | + 'divisible': false, |
| 91 | + 'updatable': false, |
| 92 | + 'refillable': false, |
| 93 | + }; |
| 94 | + |
| 95 | + return makeTransaction('CREATE', asset, metadata, conditions, fulfillments); |
| 96 | +} |
| 97 | + |
| 98 | +/** |
| 99 | + * Generate a `TRANSFER` transaction holding the `assetData`, `metaData`, `conditions`, and |
| 100 | + * `fulfillments`. |
| 101 | + * @param {object} unspentTransaction Transaction you have control over (i.e. can fulfill its |
| 102 | + * Condition). |
| 103 | + * @param {object=} metaData Metadata's `data` property |
| 104 | + * @param {object[]=} conditions Array of condition objects to add to the transaction |
| 105 | + * Think of these as the new "owners" of the asset after the transaction. |
| 106 | + * For `TRANSFER` transactions, this should usually just be an |
| 107 | + * Ed25519 Condition generated from the new owner's public key. |
| 108 | + * @param {object[]=} fulfillments Array of fulfillment objects to add to the transaction |
| 109 | + * Think of these as proofs that you can manipulate the asset. |
| 110 | + * For `TRANSFER` transactions, this should usually just be an |
| 111 | + * Ed25519 Fulfillment generated from the creator's public key. |
| 112 | + * @returns {object} Unsigned transaction -- make sure to call signTransaction() on it before |
| 113 | + * sending it off! |
| 114 | + */ |
| 115 | +export function makeTransferTransaction(unspentTransaction, metadata, conditions, fulfillments) { |
| 116 | + // Add transactionLinks to link fulfillments with previous transaction's conditions |
| 117 | + // NOTE: Naively assumes that fulfillments are given in the same order as the conditions they're |
| 118 | + // meant to fulfill |
| 119 | + fulfillments.forEach((fulfillment, index) => { |
| 120 | + fulfillment.input = { |
| 121 | + 'cid': index, |
| 122 | + 'txid': unspentTransaction.id, |
| 123 | + }; |
| 124 | + }); |
| 125 | + |
| 126 | + const assetLink = { 'id': unspentTransaction.transaction.asset.id }; |
| 127 | + |
| 128 | + return makeTransaction('TRANSFER', assetLink, metadata, conditions, fulfillments); |
| 129 | +} |
| 130 | + |
| 131 | +/** |
| 132 | + * Sign a transaction with the given `privateKey`s. |
| 133 | + * @param {object} transaction Transaction to sign |
| 134 | + * @param {...string} privateKeys base58 Ed25519 private keys. |
| 135 | + * Looped through once to iteratively sign any Fulfillments found in |
| 136 | + * the `transaction`. |
| 137 | + * @returns {object} The original transaction, signed in-place. |
| 138 | + */ |
| 139 | +export function signTransaction(transaction, ...privateKeys) { |
| 140 | + transaction.transaction.fulfillments.forEach((fulfillment, index) => { |
| 141 | + const privateKey = privateKeys[index]; |
| 142 | + const privateKeyBuffer = new Buffer(base58.decode(privateKey)); |
| 143 | + const seriailizedTransaction = serializeTransactionWithoutFulfillments(transaction); |
| 144 | + |
| 145 | + const ed25519Fulfillment = new cc.Ed25519(); |
| 146 | + ed25519Fulfillment.sign(new Buffer(seriailizedTransaction), privateKeyBuffer); |
| 147 | + const fulfillmentUri = ed25519Fulfillment.serializeUri(); |
| 148 | + |
| 149 | + fulfillment.fulfillment = fulfillmentUri; |
| 150 | + }); |
| 151 | + |
| 152 | + return transaction; |
| 153 | +} |
| 154 | + |
| 155 | +/********************* |
| 156 | + * Transaction utils * |
| 157 | + *********************/ |
| 158 | + |
| 159 | +function makeTransactionTemplate() { |
| 160 | + return { |
| 161 | + 'id': null, |
| 162 | + 'version': 1, |
| 163 | + 'transaction': { |
| 164 | + 'operation': null, |
| 165 | + 'conditions': [], |
| 166 | + 'fulfillments': [], |
| 167 | + 'metadata': null, |
| 168 | + 'asset': null, |
| 169 | + }, |
| 170 | + }; |
| 171 | +} |
| 172 | + |
| 173 | +function makeTransaction(operation, asset, metadata, conditions = [], fulfillments = []) { |
| 174 | + const tx = makeTransactionTemplate(); |
| 175 | + tx.operation = operation; |
| 176 | + tx.transaction.asset = asset; |
| 177 | + |
| 178 | + if (metadata) { |
| 179 | + tx.transaction.metadata = { |
| 180 | + 'id': uuid.v4(), |
| 181 | + 'data': metadata, |
| 182 | + }; |
| 183 | + } |
| 184 | + |
| 185 | + tx.transaction.conditions.push(...conditions); |
| 186 | + tx.transaction.conditions.forEach((condition, index) => { |
| 187 | + condition.cid = index; |
| 188 | + }); |
| 189 | + |
| 190 | + tx.transaction.fulfillments.push(...fulfillments); |
| 191 | + tx.transaction.fulfillments.forEach((fulfillment, index) => { |
| 192 | + fulfillment.fid = index; |
| 193 | + }); |
| 194 | + |
| 195 | + tx.id = hashTransaction(tx); |
| 196 | + return tx; |
| 197 | +} |
| 198 | + |
| 199 | +/**************** |
| 200 | + * Crypto utils * |
| 201 | + ****************/ |
| 202 | + |
| 203 | +function hashTransaction(transaction) { |
| 204 | + return sha256Hash(serializeTransactionWithoutFulfillments(transaction)); |
| 205 | +} |
| 206 | + |
| 207 | +function sha256Hash(data) { |
| 208 | + return crypto |
| 209 | + .createHash('sha256') |
| 210 | + .update(data) |
| 211 | + .digest('hex'); |
| 212 | +} |
| 213 | + |
| 214 | +function serializeTransactionWithoutFulfillments(transaction) { |
| 215 | + // BigchainDB creates transactions IDs and signs fulfillments by serializing transactions |
| 216 | + // into a "canonical" format where the transaction id and each fulfillment URI are ignored and |
| 217 | + // the remaining keys are sorted |
| 218 | + const tx = clone(transaction); |
| 219 | + delete tx.id; |
| 220 | + tx.transaction.fulfillments.forEach((fulfillment) => { |
| 221 | + fulfillment.fulfillment = null; |
| 222 | + }); |
| 223 | + |
| 224 | + // Sort the keys |
| 225 | + return stableStringify(tx, (a, b) => (a.key > b.key ? 1 : -1)); |
| 226 | +} |
0 commit comments