2021-04-10 09:23:54 +00:00
|
|
|
import { FirebaseUnit } from "../FirebaseUnit"
|
|
|
|
import { TransactionResource, TransactionResourceItem } from "../../rtdb/TransactionResource"
|
|
|
|
import { Singleton, Inject } from "@extollo/di"
|
|
|
|
import { Unit, Logging } from "@extollo/lib"
|
2021-04-10 07:15:54 +00:00
|
|
|
import * as openpgp from "openpgp"
|
2021-04-10 09:23:54 +00:00
|
|
|
import { Blockchain } from "../Blockchain"
|
2021-04-10 07:30:39 +00:00
|
|
|
|
2021-04-10 07:15:54 +00:00
|
|
|
/**
|
|
|
|
* Transaction Unit
|
|
|
|
* ---------------------------------------
|
2021-04-10 12:26:42 +00:00
|
|
|
* This unit listens for transactions created on the realtime database.
|
|
|
|
* When new ones come through, it matches them up, validates them, and pushes
|
|
|
|
* them onto this server's blockchain.
|
2021-04-10 07:15:54 +00:00
|
|
|
*/
|
|
|
|
@Singleton()
|
|
|
|
export class Transaction extends Unit {
|
2021-04-10 12:26:42 +00:00
|
|
|
/** True if currently processing transactions. */
|
2021-04-10 12:12:03 +00:00
|
|
|
private processing: boolean = false
|
|
|
|
|
2021-04-10 07:15:54 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly firebase!: FirebaseUnit
|
|
|
|
|
2021-04-10 07:30:39 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly blockchain!: Blockchain
|
|
|
|
|
2021-04-10 12:12:03 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly logging!: Logging
|
|
|
|
|
2021-04-10 12:26:42 +00:00
|
|
|
/** Claim the right to process transactions. Returns true if the right was granted. */
|
2021-04-10 12:12:03 +00:00
|
|
|
claim() {
|
|
|
|
if ( !this.processing ) {
|
|
|
|
this.processing = true
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-04-10 12:26:42 +00:00
|
|
|
/** Release the right to claim transactions. */
|
2021-04-10 12:12:03 +00:00
|
|
|
release() {
|
|
|
|
this.processing = false
|
|
|
|
}
|
|
|
|
|
2021-04-10 12:26:42 +00:00
|
|
|
/**
|
|
|
|
* Given two transactions, determine whether the came from a valid interaction.
|
|
|
|
* That is, do the two transactions vouch for each-other cryptographically.
|
|
|
|
* @param transaction1
|
|
|
|
* @param transaction2
|
|
|
|
*/
|
2021-04-10 09:23:54 +00:00
|
|
|
public async compareTransactions(transaction1: TransactionResourceItem, transaction2: TransactionResourceItem) {
|
2021-04-10 07:15:54 +00:00
|
|
|
// verify signature
|
|
|
|
const result1 = await openpgp.verify({
|
|
|
|
publicKeys: await openpgp.readKey({
|
|
|
|
armoredKey: transaction2.partnerPublicKey
|
|
|
|
}),
|
2021-04-10 12:12:03 +00:00
|
|
|
message: await openpgp.readMessage({
|
|
|
|
armoredMessage: transaction1.validationSignature,
|
|
|
|
}),
|
2021-04-10 07:15:54 +00:00
|
|
|
})
|
2021-04-10 12:12:03 +00:00
|
|
|
|
2021-04-10 07:15:54 +00:00
|
|
|
const result2 = await openpgp.verify({
|
|
|
|
publicKeys: await openpgp.readKey({
|
|
|
|
armoredKey: transaction1.partnerPublicKey
|
|
|
|
}),
|
2021-04-10 12:12:03 +00:00
|
|
|
message: await openpgp.readMessage({
|
|
|
|
armoredMessage: transaction2.validationSignature,
|
|
|
|
}),
|
2021-04-10 07:15:54 +00:00
|
|
|
})
|
2021-04-10 12:12:03 +00:00
|
|
|
|
|
|
|
return (await result1.signatures[0].verified) && (await result2.signatures[0].verified)
|
2021-04-10 07:15:54 +00:00
|
|
|
}
|
2021-04-10 09:23:54 +00:00
|
|
|
|
2021-04-10 12:26:42 +00:00
|
|
|
/**
|
|
|
|
* Subscribe to the transactions reference and wait for new transactions to be added.
|
|
|
|
*/
|
2021-04-10 07:15:54 +00:00
|
|
|
public async up() {
|
2021-04-10 12:12:03 +00:00
|
|
|
this.firebase.ref("transaction").on("child_added", async () => {
|
|
|
|
this.logging.debug('Received child_added event for transactions reference.')
|
|
|
|
if ( !this.claim() ) return
|
2021-04-10 14:00:45 +00:00
|
|
|
await this.firebase.trylock('block')
|
2021-04-10 12:12:03 +00:00
|
|
|
|
|
|
|
// array of pairs of transaction resource items
|
|
|
|
let groupedTransactions: [TransactionResourceItem, TransactionResourceItem][] = []
|
2021-04-10 07:15:54 +00:00
|
|
|
// collection of transaction resource items
|
|
|
|
let transactions = await TransactionResource.collect().collect()
|
2021-04-10 12:12:03 +00:00
|
|
|
|
2021-04-10 07:15:54 +00:00
|
|
|
// compare each item
|
2021-04-10 12:12:03 +00:00
|
|
|
await transactions.promiseMap(async transaction1 => {
|
2021-04-10 07:15:54 +00:00
|
|
|
// for each item that is not itself
|
2021-04-10 12:12:03 +00:00
|
|
|
await transactions.where('combinedHash', '!=', transaction1.combinedHash)
|
2021-04-10 09:23:54 +00:00
|
|
|
// get a second item
|
2021-04-10 12:12:03 +00:00
|
|
|
.promiseMap(async transaction2 => {
|
2021-04-10 09:23:54 +00:00
|
|
|
//if the item matches
|
2021-04-10 12:12:03 +00:00
|
|
|
if ( await this.compareTransactions(transaction1, transaction2) ) {
|
2021-04-10 09:23:54 +00:00
|
|
|
// and remove the two matching items
|
|
|
|
transactions = transactions.whereNotIn("combinedHash", [transaction1.combinedHash, transaction2.combinedHash])
|
|
|
|
// insert grouped items into groupedTransactions
|
|
|
|
groupedTransactions.push([transaction1, transaction2])
|
|
|
|
}
|
|
|
|
})
|
2021-04-10 07:15:54 +00:00
|
|
|
})
|
2021-04-10 12:12:03 +00:00
|
|
|
|
|
|
|
const seenCombinedHashes: string[] = []
|
|
|
|
groupedTransactions = groupedTransactions.filter(group => {
|
|
|
|
const key = group.map(x => x.combinedHash).sort().join('-')
|
|
|
|
if ( !seenCombinedHashes.includes(key) ) {
|
|
|
|
seenCombinedHashes.push(key)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
|
2021-04-10 07:30:39 +00:00
|
|
|
for (const group of groupedTransactions) {
|
2021-04-10 12:12:03 +00:00
|
|
|
const block = await this.blockchain.submitTransactions(group)
|
|
|
|
|
|
|
|
this.logging.verbose('Created block:')
|
|
|
|
this.logging.verbose(block)
|
|
|
|
|
2021-04-10 09:23:54 +00:00
|
|
|
await this.firebase.ref("transaction").child(group[0].firebaseID).remove()
|
|
|
|
await this.firebase.ref("transaction").child(group[1].firebaseID).remove()
|
2021-04-10 07:30:39 +00:00
|
|
|
}
|
2021-04-10 12:12:03 +00:00
|
|
|
|
|
|
|
this.release()
|
2021-04-10 14:00:45 +00:00
|
|
|
await this.firebase.unlock('block')
|
2021-04-10 07:15:54 +00:00
|
|
|
})
|
|
|
|
}
|
2021-04-10 09:23:54 +00:00
|
|
|
|
2021-04-10 12:26:42 +00:00
|
|
|
/**
|
|
|
|
* Release listeners and resources before shutdown.
|
|
|
|
*/
|
2021-04-10 07:15:54 +00:00
|
|
|
public async down() {
|
2021-04-10 12:12:03 +00:00
|
|
|
// Release all subscriptions before shutdown
|
|
|
|
this.firebase.ref("transaction").off()
|
2021-04-10 07:15:54 +00:00
|
|
|
}
|
|
|
|
}
|