-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
380 lines (297 loc) · 11.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
'use strict'
const express = require('express')
const bodyParser = require('body-parser')
const crypto = require('crypto')
const xero = require('xero-node')
const fs = require('fs')
const config = require('./xero.json')
const chp = require('chainpoint-client')
const exphbs = require('express-handlebars');
// Setup our Xero SDK (Private App Type)
if (config.privateKeyPath && !config.privateKey)
config.privateKey = fs.readFileSync(config.privateKeyPath);
const xeroClient = new xero.PrivateApplication(config);
// Create a new instance of express
const app = express()
// Set the body parser options
var options = {
type: 'application/json'
};
// Using the options above, create a bodyParser middleware that returns raw responses.
var itrBodyParser = bodyParser.raw(options)
// Setup templating & assets
var exbhbsEngine = exphbs.create({
defaultLayout: 'main',
layoutsDir: __dirname + '/views/layouts',
partialsDir: [
__dirname + '/views/partials/'
],
helpers: {
json: function(content) {
return JSON.stringify(content, null, 3)
},
ifCond: function(v1, operator, v2, options) {
switch (operator) {
case '==':
return (v1 == v2) ? options.fn(this) : options.inverse(this);
case '===':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '!=':
return (v1 != v2) ? options.fn(this) : options.inverse(this);
case '!==':
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
case '<':
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case '<=':
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case '>':
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case '>=':
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case '&&':
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case '||':
return (v1 || v2) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
debug: function(optionalValue) {
console.log("Current Context");
console.log("====================");
console.log(this);
if (optionalValue) {
console.log("Value");
console.log("====================");
console.log(optionalValue);
}
}
}
});
app.engine('handlebars', exbhbsEngine.engine);
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/views');
app.use(express.static(__dirname + '/assets'));
//
// Functions
//
function handleErr(err, req, res, endpoint) {
console.log(err)
}
function generateInvoiceHash(invoice) {
var jsonInvoice = JSON.stringify(invoice.toJSON())
return crypto.createHash('sha256').update(jsonInvoice).digest('hex');
}
async function chainpointSubmit(hashes) {
return new Promise(async (resolve, reject) => {
console.log(hashes)
// Retrieve 2 Chainpoint Nodes to submit our hashes to
let nodes = await chp.getNodes(2).catch(error => console.log("Error retrieving chainpoint nodes: "+error))
// Now send our hash to the chainpoint node
let proofHandles = await chp.submitHashes(hashes, nodes).catch(error => console.log(error))
console.log("Submitted Proof Objects: Expand objects below to inspect.")
console.log(proofHandles)
resolve(proofHandles)
});
}
async function chainpointUpdateProofs(proofHandles, sleep=12000) {
return new Promise(async (resolve, reject) => {
// Sleep for, to wait for proofs to be generated
console.log("Sleeping "+sleep+" seconds to wait for proofs to generate...")
await new Promise(resolve => setTimeout(resolve, sleep))
// Retrieve proofs from chainpoint
let proofs = await chp.getProofs(proofHandles).catch(error => console.log("Error retrieving chainpoint proofs: "+error))
console.log("Proof Objects: Expand objects below to inspect.")
console.log(proofs)
// if proof is not returned, we should reject the promise. Todo.
resolve(proofs)
})
}
async function chainpointVerifyProofs(proofs) {
return new Promise(async (resolve, reject) => {
// Verify every anchor in every Calendar proof
let verifiedProofs = await chp.verifyProofs(proofs).catch(error => console.log("Error verifying chainpoint proofs: "+error))
console.log("Verified Proof Objects: Expand objects below to inspect.")
console.log(verifiedProofs)
resolve(verifiedProofs)
})
}
async function uploadHashAttachment(proofs, proofHandles, invoice) {
return new Promise((resolve, reject) => {
console.log("Upload Attachment")
var fileData = { "invoice": invoice.toJSON(), "proofs": [], "proofHandles": [] }
// Add proofs & proofHandles to file data
proofs.map(proof => fileData['proofs'].push(proof))
proofHandles.map(proofHandle => fileData['proofHandles'].push(proofHandle))
var proofFilename = generateInvoiceHash(invoice)+"-cp-proofs.txt"
fs.writeFile("./files/"+proofFilename, JSON.stringify(fileData), function(err) {
if(err) {
return console.log(err);
reject(err)
}
console.log("The file was saved!");
const attachmentTemplate = {
FileName: proofFilename,
MimeType: 'text/plain',
};
const proofFile = "./files/"+proofFilename;
const attachmentPlaceholder = xeroClient.core.attachments.newAttachment(
attachmentTemplate
);
var result = attachmentPlaceholder.save(`Invoices/${invoice.InvoiceID}`, proofFile, false)
console.log("the file was uploaded")
resolve(result)
});
})
}
//
// Endpoints
//
// Create a route that receives our webhook & pass it our itrBodyParser
app.post('/webhook', itrBodyParser, async (req, res) => {
// We need to manually parse the body to JSON, as we've set the bodyParser to return raw responses
var jsonBody = JSON.parse(req.body.toString());
// Lets check the webhook signature
console.log("Xero Signature: "+req.headers['x-xero-signature'])
// I've created my webhookKey as a param in my Xero config file (see here: https://github.com/XeroAPI/xero-node#config-parameters)
let hmacSignature = crypto.createHmac("sha256", config.webhookKey).update(req.body.toString()).digest("base64");
console.log("Resp Signature: "+hmacSignature)
// Check the signature we've generated against the signature we received
if (req.headers['x-xero-signature'] !== hmacSignature) {
// if the signature fails, return a 401.
res.statusCode = 401
res.send()
console.log("Signature Failed: Returning response code: "+res.statusCode)
} else {
console.log("Handle the webhook event")
if (jsonBody['events'].length > 0) {
// completing them individually
// Handle the actual event & Tierion logic here
// Loop through our webhook events
jsonBody['events'].forEach(function(event) {
if (event['eventCategory'] == "INVOICE") {
// retrieve invoice from Xero API
xeroClient.core.invoices.getInvoice(event['resourceId'])
.then(async function(invoice) {
console.log("InvoiceID: ", invoice.InvoiceID)
// Hash the invoice response & send to chainpoint
let proofHandles = await chainpointSubmit(Array(generateInvoiceHash(invoice)))
let proofs = await chainpointUpdateProofs(proofHandles, 12000)
// Upload as attachment
let attachmentResult = await uploadHashAttachment(proofs, proofHandles, invoice)
// Now we need to wait at least 90 minutes for BTC proofs / anchors to be generated
// What we should do is create a task in a background queue
// But I am le tired, and lazy, and cbf spinning up a database for this PoC
// So lets take a massive shortcut and wait 7.2 million ms :-)
let updatedProofs = await chainpointUpdateProofs(proofHandles, 7200000)
// Upload the new attachment, over the existing.
let updatedAttachmentResult = await uploadHashAttachment(updatedProofs, proofHandles, invoice)
}).catch(err => {
console.log(err);
});;
}
})
}
res.statusCode = 200
res.send()
}
})
// Endpoint to view an index of Xero invoices
app.get('/invoices', function(req, res) {
// Use If-Modified-Since header to show only invoices that have been updated in past 24 hours
// As this is a PoC app, people are likely using it with an existing Xero Org, which will not have an audit history for their existing invoices
xeroClient.core.invoices.getInvoices({ modifiedAfter: new Date(Date.now() - 86400000)})
.then(function(invoices) {
res.render('invoices', {
invoices: invoices,
active: {
invoices: true,
nav: {
accounting: true
}
}
});
})
.catch(function(err) {
handleErr(err, req, res, 'invoices');
})
})
// Endpoint to view history of an invoice (stored in attachment files)
app.get('/history', function (req, res) {
var invoiceID = req.query && req.query.invoiceID ? req.query.invoiceID : null;
if (invoiceID) {
xeroClient.core.invoices.getInvoice(invoiceID)
.then(function(invoice) {
invoice.getAttachments()
.then(function(attachments) {
// We need to download the content of each attachment, convert to a json object and send to the view
var attachmentContents = []
// Filter our attachments by FileName to ensure we're only looking at the chainpoint proofs we previously saved
attachments = attachments.filter((attachment) => {
// Retrieve attachment content
if (attachment.FileName.includes('-cp-proofs.txt')) {
console.log("Retrieve Attachment")
attachmentContents.push(attachment.getContent())
}
return attachment.FileName.includes('-cp-proofs.txt')
})
Promise.all(attachmentContents).then((contents) => {
// creating an array of our proofs, which we will then verify
var verifyProofs = []
contents.map((content, i) => {
try {
attachments[i].content = JSON.parse(content)
// Now verify each proof & attach result (creating an array of promises)
verifyProofs.push(chainpointVerifyProofs(attachments[i].content.proofs))
} catch(e) {
console.log(e); // error is raised if this is a not a file of JSON content, unlikely to happen now that we're filtering out based on filename
attachments[i].content = null
}
})
// Resolve our verification proof promises and attach to the existing content
Promise.all(verifyProofs).then((verifiedProofs) => {
verifiedProofs.map((vProof,i) => {
attachments[i].content.verifiedProofs = vProof
})
// Render our view
res.render('history', {
invoice: invoice,
attachments: attachments,
InvoiceID: invoiceID,
active: {
invoices: true,
nav: {
accounting: true
}
}
});
}).catch(function(err) {
console.log("Error")
console.log(err)
})
}).catch(function(err) {
console.log("Error")
console.log(err);
});
})
.catch(function(err) {
handleErr(err, req, res, 'history');
})
})
.catch(function(err) {
handleErr(err, req, res, 'history');
})
} else {
handleErr("No History Found", req, res, 'index');
}
})
app.get('/', function(req, res) {
res.render('index')
})
// Tell our app to listen on port 3000
app.listen(3000, function (err) {
if (err) {
throw err
}
console.log('Server started on port 3000')
})