diff --git a/src/transaction/makeCreateTransaction.js b/src/transaction/makeCreateTransaction.js index 8b259dea..552080af 100644 --- a/src/transaction/makeCreateTransaction.js +++ b/src/transaction/makeCreateTransaction.js @@ -1,3 +1,4 @@ +import makeFulfillment from './makeFulfillment' import makeInputTemplate from './makeInputTemplate' import makeTransaction from './makeTransaction' @@ -25,7 +26,8 @@ export default function makeCreateTransaction(asset, metadata, outputs, ...issue const assetDefinition = { 'data': asset || null, } - const inputs = issuers.map((issuer) => makeInputTemplate([issuer])) + // Create transaction has 1 and just 1 input + const inputs = makeInputTemplate(issuers, null, makeFulfillment(issuers)) - return makeTransaction('CREATE', assetDefinition, metadata, outputs, inputs) + return makeTransaction('CREATE', assetDefinition, metadata, outputs, [inputs]) } diff --git a/src/transaction/makeFulfillment.js b/src/transaction/makeFulfillment.js new file mode 100644 index 00000000..7faea107 --- /dev/null +++ b/src/transaction/makeFulfillment.js @@ -0,0 +1,21 @@ +export default function makeFulfillment(issuers = []) { + if (issuers.length === 1) { + const fulfillment = { + type: 'ed25519-sha-256', + public_keys: issuers + } + return fulfillment + } else { + const subcdts = [] + issuers.map((issuer) => ( + subcdts.push({ type: 'ed25519-sha-256', + public_key: issuer + }) + )) + const fulfillment = { + type: 'threshold-sha-256', + subconditions: subcdts + } + return fulfillment + } +} diff --git a/src/transaction/makeTransaction.js b/src/transaction/makeTransaction.js index 00c8ce26..5af659c7 100644 --- a/src/transaction/makeTransaction.js +++ b/src/transaction/makeTransaction.js @@ -1,3 +1,4 @@ +import clone from 'clone' import hashTransaction from './hashTransaction' @@ -16,13 +17,19 @@ function makeTransactionTemplate() { export default function makeTransaction(operation, asset, metadata = null, outputs = [], inputs = []) { const tx = makeTransactionTemplate() + + const realInputs = clone(inputs) tx.operation = operation tx.asset = asset tx.metadata = metadata - tx.inputs = inputs + tx.inputs = realInputs tx.outputs = outputs // Hashing must be done after, as the hash is of the Transaction (up to now) + tx.inputs.forEach((input) => { + input.fulfillment = null + }) tx.id = hashTransaction(tx) + tx.inputs = inputs return tx } diff --git a/src/transaction/makeTransferTransaction.js b/src/transaction/makeTransferTransaction.js index 3d62bd23..18532d0f 100644 --- a/src/transaction/makeTransferTransaction.js +++ b/src/transaction/makeTransferTransaction.js @@ -1,3 +1,4 @@ +import makeFulfillment from './makeFulfillment' import makeInputTemplate from './makeInputTemplate' import makeTransaction from './makeTransaction' @@ -36,14 +37,14 @@ export default function makeTransferTransaction( 'output_index': outputIndex, 'transaction_id': unspentTransaction.id, } - - return makeInputTemplate(fulfilledOutput.public_keys, transactionLink) + return makeInputTemplate(fulfilledOutput.public_keys, transactionLink, + makeFulfillment(fulfilledOutput.public_keys)) }) - const assetLink = { 'id': unspentTransaction.operation === 'CREATE' ? unspentTransaction.id : unspentTransaction.asset.id } + const makeT = makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs) - return makeTransaction('TRANSFER', assetLink, metadata, outputs, inputs) + return makeT } diff --git a/src/transaction/signTransaction.js b/src/transaction/signTransaction.js index 32422a7f..8b1a05c1 100644 --- a/src/transaction/signTransaction.js +++ b/src/transaction/signTransaction.js @@ -20,15 +20,43 @@ import serializeTransactionIntoCanonicalString from './serializeTransactionIntoC */ 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) - const ed25519Fulfillment = new cc.Ed25519Sha256() - ed25519Fulfillment.sign(new Buffer(serializedTransaction), privateKeyBuffer) - const fulfillmentUri = ed25519Fulfillment.serializeUri() - - input.fulfillment = fulfillmentUri + transaction.inputs.forEach((input) => { + input.fulfillment = null // OJOOO + }) + const serializedTransaction = serializeTransactionIntoCanonicalString(transaction) + signedTx.inputs.forEach((input) => { + if (input.fulfillment.type === 'ed25519-sha-256') { + const privateKey = privateKeys[0] + privateKeys.splice(0, 1) + const privateKeyBuffer = new Buffer(base58.decode(privateKey)) + const ed25519Fulfillment = new cc.Ed25519Sha256() + ed25519Fulfillment.sign(new Buffer(serializedTransaction), privateKeyBuffer) + const fulfillmentUri = ed25519Fulfillment.serializeUri() + input.fulfillment = fulfillmentUri + } else if (input.fulfillment.type === 'threshold-sha-256') { + // Not valid for more than one input with m-of-n signatures where m < n. + const thresholdFulfillment = new cc.ThresholdSha256() + // m represents the number of signatures needed for this input + let m = 0 + input.fulfillment.subconditions.forEach((subcdt) => { + const ed25519subFulfillment = new cc.Ed25519Sha256() + if (privateKeys.length > 0) { + m++ + const privateKey = privateKeys[0] + privateKeys.splice(0, 1) + const privateKeyBuffer = new Buffer(base58.decode(privateKey)) + ed25519subFulfillment.sign(new Buffer(serializedTransaction), privateKeyBuffer) + } else { + // All conditions are needed as the "circuit definition" is needed. + const publicKeyBuffer = new Buffer(base58.decode(subcdt.public_key)) + ed25519subFulfillment.setPublicKey(publicKeyBuffer) + } + thresholdFulfillment.addSubfulfillmentUri(ed25519subFulfillment) + }) + thresholdFulfillment.setThreshold(m) + const fulfillmentUri = thresholdFulfillment.serializeUri() + input.fulfillment = fulfillmentUri + } }) return signedTx diff --git a/test/integration/test_integration.js b/test/integration/test_integration.js index b5e53fee..992efd70 100644 --- a/test/integration/test_integration.js +++ b/test/integration/test_integration.js @@ -42,6 +42,24 @@ test('Valid CREATE transaction', t => { }) +test('Valid CREATE transaction with Threshold input', t => { + const conn = new Connection(API_PATH) + + const tx = Transaction.makeCreateTransaction( + asset(), + metaData, + [aliceOutput], + alice.publicKey, + bob.publicKey + ) + const txSigned = Transaction.signTransaction(tx, alice.privateKey, bob.privateKey) + + return conn.postTransaction(txSigned) + .then(({ id }) => conn.pollStatusAndFetchTransaction(id)) + .then(resTx => t.truthy(resTx)) +}) + + test('Valid TRANSFER transaction with single Ed25519 input', t => { const conn = new Connection(API_PATH) const createTx = Transaction.makeCreateTransaction( @@ -109,6 +127,39 @@ test('Valid TRANSFER transaction with multiple Ed25519 inputs', t => { }) }) +test('Valid TRANSFER transaction with Threshold input', t => { + const conn = new Connection(API_PATH) + const createTx = Transaction.makeCreateTransaction( + asset(), + metaData, + [aliceOutput], + alice.publicKey, + bob.publicKey + ) + const createTxSigned = Transaction.signTransaction( + createTx, + alice.privateKey, + bob.privateKey + ) + + return conn.postTransaction(createTxSigned) + .then(({ id }) => conn.pollStatusAndFetchTransaction(id)) + .then(() => { + const transferTx = Transaction.makeTransferTransaction( + createTxSigned, + metaData, + [aliceOutput], + 0 + ) + const transferTxSigned = Transaction.signTransaction( + transferTx, + alice.privateKey + ) + return conn.postTransaction(transferTxSigned) + .then(({ id }) => conn.pollStatusAndFetchTransaction(id)) + .then(resTx => t.truthy(resTx)) + }) +}) test('Search for spent and unspent outputs of a given public key', t => { const conn = new Connection(API_PATH) @@ -214,7 +265,6 @@ test('Search for spent outputs for a given public key', t => { ) const createTxSigned = Transaction.signTransaction( createTx, - carol.privateKey, carol.privateKey ) diff --git a/test/transaction/test_cryptoconditions.js b/test/transaction/test_cryptoconditions.js index c6bab619..0c741afc 100644 --- a/test/transaction/test_cryptoconditions.js +++ b/test/transaction/test_cryptoconditions.js @@ -61,8 +61,9 @@ test('Fulfillment correctly formed', t => { [Transaction.makeOutput(Transaction.makeEd25519Condition(alice.publicKey))], [0] ) - const msg = Transaction.serializeTransactionIntoCanonicalString(txTransfer) const txSigned = Transaction.signTransaction(txTransfer, alice.privateKey) + + const msg = Transaction.serializeTransactionIntoCanonicalString(txTransfer) t.truthy(cc.validateFulfillment(txSigned.inputs[0].fulfillment, txCreate.outputs[0].condition.uri, new Buffer(msg))) diff --git a/test/transaction/test_transaction.js b/test/transaction/test_transaction.js index d5a28ed3..79f02ef0 100644 --- a/test/transaction/test_transaction.js +++ b/test/transaction/test_transaction.js @@ -4,6 +4,7 @@ import sinon from 'sinon' import { Transaction } from '../../src' import * as makeTransaction from '../../src/transaction/makeTransaction' // eslint-disable-line import makeInputTemplate from '../../src/transaction/makeInputTemplate' +import makeFulfillment from '../../src/transaction/makeFulfillment' import { alice, @@ -85,7 +86,8 @@ test('Create TRANSFER transaction based on CREATE transaction', t => { [aliceOutput], [makeInputTemplate( [alice.publicKey], - { output_index: 0, transaction_id: createTx.id } + { output_index: 0, transaction_id: createTx.id }, + makeFulfillment([alice.publicKey]) )] ] @@ -113,7 +115,8 @@ test('Create TRANSFER transaction based on TRANSFER transaction', t => { [aliceOutput], [makeInputTemplate( [alice.publicKey], - { output_index: 0, transaction_id: transferTx.id } + { output_index: 0, transaction_id: transferTx.id }, + makeFulfillment([alice.publicKey]) )] ]