Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions classes/transaction.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const generateTxSignature = require('../functions/generate-tx-signature')
const createP2PKHLockScript = require('../functions/create-p2pkh-lock-script')
const encodeAddress = require('../functions/encode-address')
const encodeHex = require('../functions/encode-hex')
const decodeHex = require('../functions/decode-hex')
const extractP2PKHLockScriptPubkeyhash = require('../functions/extract-p2pkh-lock-script-pubkeyhash')
Expand Down Expand Up @@ -164,6 +165,8 @@ class Transaction {
}

sign (privateKey) {
if (Array.isArray(privateKey)) return this.#signMany(privateKey)

if (Object.isFrozen(this)) throw new Error('transaction finalized')

if (typeof privateKey === 'string') { privateKey = PrivateKey.fromString(privateKey) }
Expand Down Expand Up @@ -197,6 +200,48 @@ class Transaction {
return this
}

#signMany (privateKeys) {
if (Object.isFrozen(this)) throw new Error('transaction finalized')

const keys = {}
for (let privateKey of privateKeys) {
if (typeof privateKey === 'string') { privateKey = PrivateKey.fromString(privateKey) }
if (!(privateKey instanceof PrivateKey)) throw new Error(`not a private key: ${privateKey}`)

keys[privateKey.toAddress()] = privateKey
}

for (let vin = 0; vin < this.inputs.length; vin++) {
const input = this.inputs[vin]
const output = input.output

if (input.script.length) continue
if (!output) continue

const outputScript = output.script
const outputSatoshis = output.satoshis

if (!isP2PKHLockScript(output.script)) continue

const inputAddress = encodeAddress(extractP2PKHLockScriptPubkeyhash(output.script))
if (keys[inputAddress] === undefined) continue

const privateKey = keys[inputAddress]

const txsignature = generateTxSignature(this, vin, outputScript, outputSatoshis,
privateKey.number, privateKey.toPublicKey().point)

const writer = new BufferWriter()
writePushData(writer, txsignature)
writePushData(writer, privateKey.toPublicKey().toBuffer())
const script = writer.toBuffer()

input.script = Script.fromBuffer(script)
}

return this
}

verify () {
const parents = this.inputs.map(input => input.output)
const minFeePerKb = require('../index').feePerKb
Expand Down
34 changes: 34 additions & 0 deletions test/classes/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,13 +470,16 @@ describe('Transaction', () => {
const privateKey = PrivateKey.fromRandom()
const tx1 = new Transaction().to(privateKey.toAddress(), 1000)
const tx2 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000).sign(privateKey)
const tx3 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000).sign([privateKey])
expect(tx2.inputs[0].script.length > 0).to.equal(true)
expect(tx3.inputs[0].script.length > 0).to.equal(true)
nimble.functions.verifyScript(tx2.inputs[0].script, tx1.outputs[0].script, tx2, 0, tx1.outputs[0].satoshis)
})

it('supports string private key', () => {
const privateKey = PrivateKey.fromRandom()
new Transaction().sign(privateKey.toString()) // eslint-disable-line
new Transaction().sign([privateKey.toString()]) // eslint-disable-line
})

it('does not sign different addresses', () => {
Expand All @@ -485,24 +488,31 @@ describe('Transaction', () => {
const tx0 = new Transaction().to(privateKey1.toAddress(), 1000)
const tx1 = new Transaction().to(privateKey2.toAddress(), 1000)
const tx2 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey2.toAddress(), 2000).sign(privateKey2)
const tx3 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey2.toAddress(), 2000).sign([privateKey2])
expect(tx2.inputs[0].script.length === 0).to.equal(true)
expect(tx2.inputs[1].script.length > 0).to.equal(true)
expect(tx3.inputs[0].script.length === 0).to.equal(true)
expect(tx3.inputs[1].script.length > 0).to.equal(true)
})

it('does not sign non-p2pkh', () => {
const privateKey = PrivateKey.fromRandom()
const script = Array.from([...nimble.functions.createP2PKHLockScript(privateKey.toAddress().pubkeyhash), 1])
const utxo = { txid: new Transaction().hash, vout: 0, script, satoshis: 1000 }
const tx = new Transaction().from(utxo).sign(privateKey)
const tx2 = new Transaction().from(utxo).sign([privateKey])
expect(tx.inputs[0].script.length).to.equal(0)
expect(tx2.inputs[0].script.length).to.equal(0)
})

it('does not sign without previous outputs', () => {
const privateKey = PrivateKey.fromRandom()
const tx1 = new Transaction().to(privateKey.toAddress(), 1000)
const input = { txid: tx1.hash, vout: 0, script: [], sequence: 0 }
const tx2 = new Transaction().input(input).to(privateKey.toAddress(), 2000).sign(privateKey)
const tx3 = new Transaction().input(input).to(privateKey.toAddress(), 2000).sign([privateKey])
expect(tx2.inputs[0].script.length).to.equal(0)
expect(tx3.inputs[0].script.length).to.equal(0)
})

it('does not sign if already have sign data', () => {
Expand All @@ -511,19 +521,42 @@ describe('Transaction', () => {
const tx2 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000)
tx2.inputs[0].script = [0x01]
tx2.sign(privateKey)
const tx3 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000)
tx3.inputs[0].script = [0x01]
tx3.sign([privateKey])
expect(tx2.inputs[0].script).to.deep.equal([0x01])
expect(tx3.inputs[0].script).to.deep.equal([0x01])
})

it('returns self for chaining', () => {
const tx = new Transaction()
expect(tx.sign(PrivateKey.fromRandom())).to.equal(tx)
expect(tx.sign([PrivateKey.fromRandom()])).to.equal(tx)
})

it('throws if private key not provided', () => {
expect(() => new Transaction().sign()).to.throw('not a private key: ')
expect(() => new Transaction().sign({})).to.throw('not a private key: [object Object]')
expect(() => new Transaction().sign(123)).to.throw('not a private key: 123')
expect(() => new Transaction().sign('abc')).to.throw('bad checksum')

expect(() => new Transaction().sign([new nimble.Script()])).to.throw('not a private key')
expect(() => new Transaction().sign([123])).to.throw('not a private key: 123')
expect(() => new Transaction().sign(['abc'])).to.throw('bad checksum')
})

it('sign many', () => {
const privateKey1 = PrivateKey.fromRandom()
const privateKey2 = PrivateKey.fromRandom()
const privateKey3 = PrivateKey.fromRandom()
const tx0 = new Transaction().to(privateKey1.toAddress(), 1000)
const tx1 = new Transaction().to(privateKey2.toAddress(), 1000)
const tx2 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey3.toAddress(), 2000).sign([privateKey1, privateKey2])
const tx3 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey3.toAddress(), 2000).sign([privateKey2, privateKey1]) // the order of the private keys should not matter
expect(tx2.inputs[0].script.length > 0).to.equal(true)
expect(tx2.inputs[1].script.length > 0).to.equal(true)
expect(tx3.inputs[0].script.length > 0).to.equal(true)
expect(tx3.inputs[1].script.length > 0).to.equal(true)
})
})

Expand Down Expand Up @@ -567,6 +600,7 @@ describe('Transaction', () => {
expect(() => tx.output({ script: [], satoshis: 0 })).to.throw(err)
expect(() => tx.change(address)).to.throw(err)
expect(() => tx.sign(privateKey)).to.throw(err)
expect(() => tx.sign([privateKey])).to.throw(err)

tx.n = 1
expect('n' in tx).to.equal(false)
Expand Down