diff --git a/src/Units.extollo.ts b/src/Units.extollo.ts index 7f4ebf6..7b449b8 100644 --- a/src/Units.extollo.ts +++ b/src/Units.extollo.ts @@ -1,10 +1,14 @@ import {Config, Controllers, HTTPServer, Files, Middlewares, Routing, Unit} from '@extollo/lib' -import {CommandLine} from "@extollo/cli"; -import {FirebaseUnit} from "./app/units/FirebaseUnit"; +import {CommandLine} from "@extollo/cli" +import {FirebaseUnit} from "./app/units/FirebaseUnit" +import {Blockchain} from "./app/units/Blockchain" +import {Transaction} from "./app/units/rtdb/Transaction" export const Units = [ Config, FirebaseUnit, + Blockchain, + Transaction, Files, CommandLine, Controllers, diff --git a/src/app/FirebaseResource.ts b/src/app/FirebaseResource.ts index 1755c95..8253c3e 100644 --- a/src/app/FirebaseResource.ts +++ b/src/app/FirebaseResource.ts @@ -25,7 +25,7 @@ export class FirebaseResource extends Iterable((res, rej) => { this.ref().orderByChild('seqID') .on('value', snapshot => { - res((this.resolveObject(snapshot.val()).reverse()?.[0]?.seqID || 0) + 1) + res((this.resolveObject(snapshot.val()).reverse()?.[0]?.seqID ?? -1) + 1) }, rej) }) } @@ -66,7 +66,22 @@ export class FirebaseResource extends Iterable { item.seqID = await this.getNextID() + // @ts-ignore + delete item.firebaseID await this.ref().push(item) + + // Look up the firebaseID + await new Promise((res, rej) => { + this.ref().orderByChild('seqID') + .limitToLast(1) + .on('value', snapshot => { + if ( snapshot.val() ) { + item.firebaseID = Object.keys(snapshot.val())[0] + } + res() + }) + }) + return item } diff --git a/src/app/units/Blockchain.ts b/src/app/units/Blockchain.ts index 0746eea..a05160b 100644 --- a/src/app/units/Blockchain.ts +++ b/src/app/units/Blockchain.ts @@ -156,6 +156,9 @@ export class Blockchain extends Unit { public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) { const lastBlock = await this.getLastBlock() + this.logging.verbose('Last block:') + this.logging.verbose(lastBlock) + const block: BlockResourceItem = { timestamp: (new Date).getTime(), uuid: uuid_v4(), @@ -178,6 +181,7 @@ export class Blockchain extends Unit { public async getGenesisBlock(): Promise { const message = openpgp.Message.fromText("0000") const privateKey = this.config.get("app.gpg.key.private") + return new Block({ timestamp: (new Date).getTime(), uuid: '0000', @@ -186,8 +190,10 @@ export class Blockchain extends Unit { lastBlockUUID: '', proof: (await openpgp.sign({ message, - privateKeys: privateKey - })).toString(), + privateKeys: await openpgp.readKey({ + armoredKey: privateKey + }), + })), firebaseID: '', seqID: -1, }) @@ -202,7 +208,7 @@ export class Blockchain extends Unit { const genesis = (await this.getGenesisBlock()).toItem() await (this.app().make(BlockResource)).push(genesis) - return this.getLastBlock() + return new Block(genesis) } /** @@ -231,7 +237,9 @@ export class Blockchain extends Unit { // Sign the hash using the server's private key return (await openpgp.sign({ message, - privateKeys: privateKey + privateKeys: await openpgp.readKey({ + armoredKey: privateKey, + }) })).toString() } diff --git a/src/app/units/rtdb/Transaction.ts b/src/app/units/rtdb/Transaction.ts index 75056bf..5e9ec9e 100644 --- a/src/app/units/rtdb/Transaction.ts +++ b/src/app/units/rtdb/Transaction.ts @@ -12,49 +12,71 @@ import { Blockchain } from "../Blockchain" */ @Singleton() export class Transaction extends Unit { + private processing: boolean = false + @Inject() protected readonly firebase!: FirebaseUnit @Inject() protected readonly blockchain!: Blockchain + @Inject() + protected readonly logging!: Logging + + claim() { + if ( !this.processing ) { + this.processing = true + return true + } + + return false + } + + release() { + this.processing = false + } + public async compareTransactions(transaction1: TransactionResourceItem, transaction2: TransactionResourceItem) { // verify signature const result1 = await openpgp.verify({ publicKeys: await openpgp.readKey({ armoredKey: transaction2.partnerPublicKey }), - message: openpgp.Message.fromText(transaction1.combinedHash), - signature: await openpgp.readSignature({ - armoredSignature: transaction1.validationSignature // parse detached signature - }) + message: await openpgp.readMessage({ + armoredMessage: transaction1.validationSignature, + }), }) + const result2 = await openpgp.verify({ publicKeys: await openpgp.readKey({ armoredKey: transaction1.partnerPublicKey }), - message: openpgp.Message.fromText(transaction2.combinedHash), - signature: await openpgp.readSignature({ - armoredSignature: transaction2.validationSignature // parse detached signature - }) + message: await openpgp.readMessage({ + armoredMessage: transaction2.validationSignature, + }), }) - return await (result1.signatures[0].verified) && await (result2.signatures[0].verified) + + return (await result1.signatures[0].verified) && (await result2.signatures[0].verified) } public async up() { - this.firebase.ref("transaction").on("value", async () => { - // array of pairs of tranaction resource items - const groupedTransactions: [TransactionResourceItem, TransactionResourceItem][] = [] + this.firebase.ref("transaction").on("child_added", async () => { + this.logging.debug('Received child_added event for transactions reference.') + if ( !this.claim() ) return + + // array of pairs of transaction resource items + let groupedTransactions: [TransactionResourceItem, TransactionResourceItem][] = [] // collection of transaction resource items let transactions = await TransactionResource.collect().collect() + // compare each item - transactions.each(transaction1 => { + await transactions.promiseMap(async transaction1 => { // for each item that is not itself - transactions.where("combinedHash", "!=", transaction1.combinedHash) + await transactions.where('combinedHash', '!=', transaction1.combinedHash) // get a second item - .each(transaction2 => { + .promiseMap(async transaction2 => { //if the item matches - if (this.compareTransactions(transaction1, transaction2)) { + if ( await this.compareTransactions(transaction1, transaction2) ) { // and remove the two matching items transactions = transactions.whereNotIn("combinedHash", [transaction1.combinedHash, transaction2.combinedHash]) // insert grouped items into groupedTransactions @@ -62,14 +84,34 @@ export class Transaction extends Unit { } }) }) + + 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 + }) + for (const group of groupedTransactions) { - await this.blockchain.submitTransactions(group) + const block = await this.blockchain.submitTransactions(group) + + this.logging.verbose('Created block:') + this.logging.verbose(block) + await this.firebase.ref("transaction").child(group[0].firebaseID).remove() await this.firebase.ref("transaction").child(group[1].firebaseID).remove() } + + this.release() }) } public async down() { + // Release all subscriptions before shutdown + this.firebase.ref("transaction").off() } }