Creating the AppController
Class
Now that we have all the components built, we'll turn our attention to the primary logic controller for our application! This logic resides in the AppController
class located in server/src/domain
. This class is responsible for constructing and maintaining the chain of ownership based on paid invoices.
The constructor of this class takes a few things we've previously worked on such as:
IInvoiceDataMapper
- we'll use this to create and fetch invoices from our Lightning Network nodeIMessageSigner
- we'll use this validate signatures that we receive from remote nodesLinkFactory
- we'll use this to create links in our ownership chain
If you take a look at this class, you'll also notice that we have the chain
property that maintains the list of Link
in our application. This is where our application state will be retained in memory.
public chain: Link[];
There is also a conveniently added chaintip
property that returns the last record in the chain.
public get chainTip(): Link {
return this.chain[this.chain.length - 1];
}
One other note about our AppController
is that it uses the observer
pattern to notify a subscriber about changes to the chain. In this case the subscriber will be all of the open websockets. The observer will receive an array of changed Link
whenever the chain changes. This can be found in the listener
property on the AppController
class.
public listener: (info: Link[]) => void;
Dev Note: Why not use EventEmitter
? Well we certainly could. Since this example only has a single event it's easy to bake in a handler/callback function for Link
change events.
Lastly, this class will implement three functions that we'll discuss in more detail. These methods create a clean interface for our application logic to sit between external users (REST API and Websockets) and our Lightning Network node. These methods are:
start
- this method is used to start the application and synchronize the game state with the invoices of a Lightning Network nodehandleInvoice
- this method is used to check invoices that are received by the Lightning Network nodecreateInvoice
- constructs an invoice for the currentLink
based on information provided by some user.
Starting the Application
We should now have a general understanding of the AppController
class. A great place to begin is how we start the application. We do this with the start
method. This method is used to bootstrap our application under two start up scenarios:
- The first time the application is started
- Subsequent restarts when we have some links in the chain
In either case, we need to get the game state synchronized. The synchronization requires two steps:
- Create the first link using the
seed
- Synchronize the application by looking at all of our Lightning Network node's invoices using
IInvoiceDataMapper
Back when we discussed the IInvoiceDataMapper
we had a sync
method. If you recall, this method accepted an InvoiceHandler
that defined a simple function that has one argument, an Invoice
.
export type InvoiceHandler = (invoice: Invoice) => Promise<void>;
If you take a look at the AppController
. You'll see that handleInvoice
matches this signature! This is not a coincidence. We'll use the handleInvoice
method to process all invoices that our Lightning Network node knows about.
Now that we understand that, let's do an exercise and implement our start
method.
Exercise: Implement start
To implement the start
method requires us to perform two tasks:
- Use the
linkFactory
to create the firstLink
from the seed and add it to thechain
- Once the first link is created, initiate the synchronization of invoices using the
IInvoiceDataMapper
(as mentioned, provide theAppController.handleInvoice
method as the handler).
public async start(seed: string, startSats: number) {
// Exercise
}
Dev Tip: One of the trickier aspects of JavaScript is scoping of this
. Since the handleInvoice
method will be used as a callback but it belongs to the AppController
class, special care must be made to ensure that it does not lose scope when it is called by the sync
method. You will have an issue if you provide it directly as an argument to the sync
method: await this.invoiceDataMapper.sync(this.handleInvoice);
. Doing this treats the handleInvoice
method as an unbound function, which means any use of this
inside of that function will be scoped to the caller instead of the AppController
class instance.
You can retain scope of the AppController
class instance in two ways:
- use
bind
to bind the function to the desired scope. Eg: bind it to the current instance of the classawait this.invoiceDataMapper.sync(this.handleInvoice.bind(this))
. - use
arrow functions
which retain the scoping of the caller. Eg:await this.invoiceDataMapper.sync(invoice => this.handleInvoice(invoice))
.
When you are finished you can verify you successfully implemented the method with the following command:
npm run test:server -- --grep "AppController.*start"
Exercise: Implement handleInvoice
Next on the docket, we need to process invoices we receive from our Lightning Network node. The handleInvoice
is called every time an invoice is found, created, or fulfilled by our Lightning Network node. This method does a few things to correctly process an invoice:
- Checks if the invoice settles the current
Link
. Hint look at thesettles
method on theInvoice
. If the invoice doesn't settle the currentLink
, no further action is required. - If the invoice does settle the current
Link
, it should call thesettle
method onLink
which will settle theLink
. - It should then create a new
Link
using theLinkFactory.createFromSettled
. - It should add the new unsettled link to the application's chain
- Finally, it will send the settled link and the new link to the listener.
This method is partially implemented for you. Complete the method by settling the current link and constructing the next link from the settled link.
public async handleInvoice(invoice: Invoice) {
if (invoice.settles(this.chainTip)) {
// settle the current chain tip
// create a new unsettled Link
// add the new link to the chain
// send settled and new to the listener
if (this.listener) {
this.listener([settled, nextLink]);
}
}
}
When you are finished you can verify you successfully implemented the method with the following command:
npm run test:server -- --grep "AppController.*handleInvoice"
Exercise: createInvoice
The last bit of code AppController
is responsible for is creating invoices. This method is responsible for interacting with the Lightning Network node's message signature verification through the IMessageSigner
interface. It will also interact with the Lightning Network node to create the invoice via the IInvoiceDataMapper
.
Recall that when someone wants to take ownership of the current link they'll need to send a digital signature of the current linkId
.
Our method does a few things:
- Verifies the signature is for the current
linkId
. If invalid, it returns a failure. - Constructs the preimage for the invoice. Recall that we implemented the
createPreimage
method onInvoice
previously. - Constructs the memo for the invoice. Recall that we implemented the
createMemo
method onInvoice
previously. - Creates the invoice using the
IInvoiceDataMapper.add
method. - Return a success or failure result to the caller.
This method is partially implemented for you.
public async createInvoice(
remoteSignature: string,
sats: number,
): Promise<CreateInvoiceResult> {
// verify the invoice provided by the user
const verification = await this.signer.verify(this.chainTip.linkId, remoteSignature);
// return failure if signature fails
if (!verification.valid) {
return { success: false, error: "Invalid signature" };
}
// Exercise: create the preimage
// Exercise: create the memo
// try to create the invoice
try {
const paymentRequest = await this.invoiceDataMapper.add(sats, memo, preimage);
return {
success: true,
paymentRequest,
};
} catch (ex) {
return {
success: false,
error: ex.message,
};
}
}
When you are finished you can verify you successfully implemented the method with the following command:
npm run test:server -- --grep "AppController.*createInvoice"