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 currentLinkbased 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
linkFactoryto create the firstLinkfrom 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.handleInvoicemethod 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
bindto 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 functionswhich 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 thesettlesmethod 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 thesettlemethod onLinkwhich will settle theLink. - It should then create a new
Linkusing 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
createPreimagemethod onInvoicepreviously. - Constructs the memo for the invoice. Recall that we implemented the
createMemomethod onInvoicepreviously. - Creates the invoice using the
IInvoiceDataMapper.addmethod. - 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"