Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: external notifications #237

Merged
merged 15 commits into from
Oct 19, 2022
Merged

feat: external notifications #237

merged 15 commits into from
Oct 19, 2022

Conversation

r4mmer
Copy link
Member

@r4mmer r4mmer commented Sep 7, 2022

Acceptance Criteria

Description

Some refactors were made to make the plugin system more reliable.
The express app configuration was moved from index.js to app.js.
This was made so that we can startup the plugins and check the configuration before start listening for requests.
This can help with health and status checks, also it ensures the plugins can listen to all events.

Security Checklist

  • Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. More dependencies increase the possibility of one of them being hijacked and affecting us.

@r4mmer r4mmer self-assigned this Sep 7, 2022
@codecov
Copy link

codecov bot commented Sep 7, 2022

Codecov Report

Merging #237 (31b646b) into dev (6abb754) will decrease coverage by 0.31%.
The diff coverage is 82.19%.

@@            Coverage Diff             @@
##              dev     #237      +/-   ##
==========================================
- Coverage   85.19%   84.87%   -0.32%     
==========================================
  Files          24       31       +7     
  Lines         905     1071     +166     
  Branches      185      216      +31     
==========================================
+ Hits          771      909     +138     
- Misses        116      138      +22     
- Partials       18       24       +6     
Impacted Files Coverage Δ
src/helpers/validations.helper.js 100.00% <ø> (ø)
src/plugins/child.js 72.41% <72.41%> (ø)
src/services/notification.service.js 72.72% <72.72%> (ø)
src/app.js 80.95% <80.95%> (ø)
src/helpers/notification.helper.js 83.33% <83.33%> (ø)
src/plugins/hathor_sqs.js 88.23% <88.23%> (ø)
src/controllers/index.controller.js 79.56% <100.00%> (+0.44%) ⬆️
src/plugins/hathor_debug.js 100.00% <100.00%> (ø)
src/plugins/hathor_rabbitmq.js 100.00% <100.00%> (ø)
src/plugins/hathor_websocket.js 100.00% <100.00%> (ø)
... and 1 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@r4mmer
Copy link
Member Author

r4mmer commented Sep 21, 2022

FYI for reviewers

If you want to test the plugins there may be some setup needed.
I've listed below a base for configurations i've used.

Running the plugins locally

If the wallet headless is runing with docker, remember to enable the plugins with --enabled_plugins or with the envvar HEADLESS_ENABLED_PLUGINS and the value is a space separated list of plugins to run.

debug plugin

# Config options
# `plugin_debug_long`: all, off
npm start -- --plugin_debug_long all

This plugin requires no external tools.
Just start the service and check the logs.

websocket plugin

npm start -- --plugin_ws_port 8008

We can omit the port config since the default port is 8008.

We need a websocket client or a script to test this.
The wscat works extremely well for this.

wscat -c localhost:8008

sqs

# `plugin_sqs_region`
# `plugin_sqs_queue_url`
# `plugin_sqs_endpoint_url`
npm start -- --plugin_sqs_endpoint_url http://localhost:9324 --plugin_sqs_queue_url http://localhost:9324/000000000000/queue1 --plugin_sqs_region elasticmq

To avoid having to spin up an sqs queue for dev/testing we can use ElasticMQ
that has an AWS SQS compatible API, meaning it works with aws sdk and even aws cli.
To run it, we use docker-compose.

version: "2"
services:
  queue:
    image: softwaremill/elasticmq:latest
    ports:
      - 9324:9324
      - 9325:9325
    volumes:
      - ./elasticmq.conf:/opt/elasticmq.conf

Also, we use the file elasticmq.conf

include classpath("application.conf")

node-address {
  protocol = http
  host = "*"
  port = 9324
  context-path = ""
}

rest-sqs {
  enabled = true
  bind-port = 9324
  bind-hostname = "0.0.0.0"
  # Possible values: relaxed, strict
  sqs-limits = strict
}

rest-stats {
  enabled = true
  bind-port = 9325
  bind-hostname = "0.0.0.0"
}

generate-node-address = false

queues {
  queue1 {
    defaultVisibilityTimeout = 30 seconds
    delay = 5 seconds
    receiveMessageWait = 0 seconds
    fifo = false
    contentBasedDeduplication = false
    tags {
      tag1 = "tagged1"
      tag2 = "tagged2"
    }
  }
}

# Region and accountId which will be included in resource ids
aws {
  region = us-east-1
  accountId = 000000000000
}

This will start a stats dashboard running on port 9325 which we can use to check the incomming messages.
It also starts the elasticmq instance on port 9324 with a queue named queue1.
We can check that it is running and the queue was created with:

aws --endpoint-url http://localhost:9324 sqs list-queues

While we have enough for testing, I've also created a consumer to the queue, to avoid increasing the number of unread messages on queue, and to check the contents of the messages being sent.

from time import sleep
import boto3

endpoint_url = "http://localhost:9324"

# Create SQS client
sqs = boto3.client('sqs', endpoint_url=endpoint_url, region_name='elasticmq', use_ssl=False)

queue_url = 'http://localhost:9324/000000000000/queue1'

def receive_msg():
    # Receive message from SQS queue
    response = sqs.receive_message(
        QueueUrl=queue_url,
        AttributeNames=['SentTimestamp'],
        MaxNumberOfMessages=1,
        MessageAttributeNames=['All'],
        VisibilityTimeout=0,
        WaitTimeSeconds=0
    )
    message = response['Messages'][0]
    receipt_handle = message['ReceiptHandle']
    # Delete received message from queue
    sqs.delete_message(QueueUrl=queue_url,ReceiptHandle=receipt_handle)
    print('Received and deleted message: %s' % message)

while True:
    receive_msg()
    sleep(1)

rabbitMQ plugin

# `plugin_rabbitmq_url`
# `plugin_rabbitmq_queue`
npm start -- --plugin_rabbitmq_url amqp://localhost --plugin_rabbitmq_queue queue1

We can run rabbitMQ with docker with the command:

docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.10-management

And to check the messages we can use the consumer script

var amqp = require('amqplib/callback_api');

var queue = 'amqp://localhost';

amqp.connect(queue, function(error0, connection) {
    if (error0) {
        throw error0;
    }
    connection.createChannel(function(error1, channel) {
        if (error1) {
            throw error1;
        }
        var queue = 'queue1';
        channel.assertQueue(queue, { durable: false });
        console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);
        channel.consume(queue, function(msg) {
            console.log(" [x] Received %s", msg.content.toString());
        }, { noAck: true });
    });
});

@r4mmer r4mmer marked this pull request as ready for review September 21, 2022 13:43
@r4mmer r4mmer requested review from tuliomir and luislhl and removed request for luislhl September 21, 2022 13:44
config.js.template Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
src/plugins/hathor_debug.js Outdated Show resolved Hide resolved
src/plugins/hathor_rabbitmq.js Outdated Show resolved Hide resolved

const eventBus = 'message';

const WalletEventMap = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the intention is to make a map and help the user avoid code mistakes, I think it would be better to have the key names in camelCase, without the quotes: state, newTx, updateTx.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I like the idea, this would remove the original event type from the map.
For instance, if we had an event "foo-bar" which we translate to fooBar: 'wallet:foo' a dev searching for instances of the event "foo-bar" may miss this reference.

What do you think of this?
Obs: my opinion is that i would still change to camel case, i just want to show the full info on why the original event name is there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that the search by event name is the main way of finding the interactions with these events now, and that it's important to keep is searchable like this. As a short-term solution maybe just add the original event name as a comment?

const ConnectionEventMap = {
  state: 'conn:state', // original lib event: 'state'
  newTx: 'wallet:new-tx', // original lib event: 'new-tx'
  updateTx: 'wallet:update-tx', // original lib event: 'update-tx'

Do you agree that this helps avoid confusion?

--
As a more definitive solution, I would expand my suggestion to also refactor the way events are referred on the project and on the wallet lib itself:

// On wallet-lib/src/constants.js , add the following object:
  EVENT_NAMES: {
    STATE: 'state',
    NEW_TX: 'new-tx',
    UPDATE_TX: 'update-tx',
  }

// On headless/src/services/notification.service/js , simply re-use the same key names as the lib's constants object
const WalletEventMap = {
    STATE: 'wallet:state',
    NEW_TX: 'wallet:new-tx',
    UPDATE_TX: 'wallet:update-tx',
}

// The event listeners would then have the format:
import { EVENT_NAMES as LIB_EVENTS } from '@hathor/wallet-lib/constants'
hwallet.on(LIB_EVENTS.NEW_TX, tx => {
  const type = WalletEventMap.NEW_TX;
  ...
}

This seems material for another issue / PR on both projects, as I think it would make it simpler for developers to also use the lib's events beyond the external notifications.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue to track this suggestion.

src/controllers/index.controller.js Show resolved Hide resolved
src/child.js Outdated Show resolved Hide resolved
src/services/notification.service.js Outdated Show resolved Hide resolved
}

/* istanbul ignore next */
export const init = async bus => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this project needs at least one integration test, and maybe the websocket plugin could be well suited for that.

A test suite containing the startup of a wallet, the real-time notification of transactions and a successful shutdown would be great also to serve as an example code to developers.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the integration test, I think a comment inside the code pointing to the Running the plugins locally section of your comment would be great. One suggestion of a place to add it would be

// XXX: subscribe to wallet events with notificationBus
notificationBus.subscribeHathorWallet(req.body['wallet-id'], wallet);

// @see https://github.com/HathorNetwork/hathor-wallet-headless/pull/237#issuecomment-1253730867

That, or maybe adding a slightly adapted form of that text to the readme.md file under a new Real-time Notifications section.

Copy link
Contributor

@tuliomir tuliomir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code on this repository is approved by me.

The only thing I think is still good to have is to a complete sample of working code so that a developer can see a concrete example of how to put the plugin system to work. I just don't really see where to put this into this repository.

Maybe a tutorial text, a "Getting Started" repository ( containing integration tests ) on GitHub or a video on YouTube would be enough to give a bootstrap to a newcomer.

PLUGIN.md Outdated Show resolved Hide resolved
config.js.template Outdated Show resolved Hide resolved
@@ -74,6 +74,10 @@ const config = {
connectionTimeout: getIntParam('connection_timeout'),
allowPassphrase: getBooleanParam('allow_passphrase'),
confirmFirstAddress: getBooleanParam('confirm_first_address'),

// Plugin configuration
enabled_plugins: getParam('enabled_plugins', '').split(' ').filter(x => x),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this filter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the default value, since its an empty string.
After the split we will have [''] which was confusing, so I added this filter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment about this?

src/plugins/child.js Outdated Show resolved Hide resolved
src/plugins/hathor_debug.js Show resolved Hide resolved
src/plugins/hathor_sqs.js Outdated Show resolved Hide resolved
src/services/notification.service.js Outdated Show resolved Hide resolved
@lgtm-com
Copy link

lgtm-com bot commented Oct 11, 2022

This pull request introduces 1 alert when merging 44feb33 into e5de086 - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

@lgtm-com
Copy link

lgtm-com bot commented Oct 11, 2022

This pull request introduces 1 alert when merging d83609a into e5de086 - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

@lgtm-com
Copy link

lgtm-com bot commented Oct 13, 2022

This pull request introduces 1 alert when merging 967ac98 into e5de086 - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

*
* @returns {TokenBalance}
*/
function getWalletBalanceForTx(wallet, tx) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm starting to think if this method should be in the lib. It seems a bit more complete than the one we have in the wallet class getTxBalance.

Copy link
Contributor

@tuliomir tuliomir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code approved, and I left some suggestions for future refactors on the lib.

tokenBalance[token] = getEmptyBalance();
}

if ((input.token_data & TOKEN_AUTHORITY_MASK) > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this could be added to the lib later as a isAuthorityInput(input) method, analog to the existing isAuthorityOutput(output), to maintain pattern of helper methods on the lib. The isMintInput and isMeltInput are good candidates too.

https://github.com/HathorNetwork/hathor-wallet-lib/blob/master/src/wallet.js#L1972-L1984

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the concept of a mint/melt input is somewhat strange to me.
The more correct idea is that the input spends a mint/melt output.
Even the token_data I used is because the fullnode adds some metadata of the utxo being spent in the input.

if (output.decoded && output.decoded.address && wallet.isAddressMine(output.decoded.address)) {
const { token } = output;
if (!tokenBalance[token]) {
tokenBalance[token] = getEmptyBalance();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to reach this line?
If all tokens were scanned on the inputs, all of them must necessarily be present on the outputs too, right?

I'm just bringing this up because I can't see a way to make a test that covers this line and codecov will see this as a warning, but I think it may be good to keep the code this way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, a token creation transaction does not have the created token on the inputs.

@r4mmer r4mmer merged commit 6f0700b into dev Oct 19, 2022
@r4mmer r4mmer deleted the feat/external-notifications branch October 19, 2022 16:44
r4mmer added a commit that referenced this pull request Nov 1, 2022
* dev:
  feat: external notifications (#237)
  chore: atomic swap api docs (#239)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants