10. Monitoraggio

I nodi della rete Symbol ricevono o inviano notifiche di cambi di stato o eventi attraverso la tecnologia di comunicazione WebSocket.

10.1 Configurazione del Listener

Creazione di un WebSocket e configurazione del listener.

nsRepo = repo.createNamespaceRepository();
wsEndpoint = NODE.replace("http", "ws") + "/ws";
listener = new sym.Listener(wsEndpoint, nsRepo, WebSocket);
listener.open();

Segue il formato per collegarsi ad un endpoint.

  • wss://{node url}:3001/ws

Il timeout per mantenere attiva la connessione è di 1 minuto, oltre il quale, se non c'è stato scambio di informazioni il listener viene scollegato.

10.2 Ricevere notifiche sulle ransazioni

Per essere notificati delle transazioni convalidate (confirmed) che interessano un Indirizzo specificato (per es. Alice), si chiamano i seguenti metodi. Similmente per quelle propagate e non ancora convalidate.

listener.open().then(() => {
  //Sottoscrive per ricevere notifiche di transazioni convalidate
  listener.confirmed(alice.address).subscribe((tx) => {
    //Stampa il dettaglio alla ricezione della notifica
    console.log(tx);
  });
  //Sottoscrive per ricevere notifiche di transazioni propagate non ancora convalidate
  listener.unconfirmedAdded(alice.address).subscribe((tx) => {
    //Stampa il dettaglio alla ricezione della notifica
    console.log(tx);
  });
});

Dopo aver impostato il listener, propaghiamo una transazione di test all'Indirizzo di Alice ottenendo quanto segue come notifica.

Output esemplificativo
> Promise {<pending>}
> TransferTransaction {type: 16724, networkType: 152, version: 1, deadline: Deadline, maxFee: UInt64, }
    deadline: Deadline {adjustedValue: 12449258375}
    maxFee: UInt64 {lower: 32000, higher: 0}
    message: RawMessage {type: -1, payload: ''}
    mosaics: []
    networkType: 152
    payloadSize: undefined
    recipientAddress: Address {address: 'TBXUTAX6O6EUVPB6X7OBNX6UUXBMPPAFX7KE5TQ', networkType: 152}
    signature: "914B625F3013635FA9C99B2F138C47CD75F6E1DF7BDDA291E449390178EB461AA389522FA126D506405163CC8BA51FA9019E0522E3FA9FED7C2F857F11FBCC09"
    signer: PublicAccount {publicKey: 'D4933FC1E4C56F9DF9314E9E0533173E1AB727BDB2A04B59F048124E93BEFBD2', address: Address}
    transactionInfo: TransactionInfo
        hash: "3B21D8842EB70A780A662CCA19B8B030E2D5C7FB4C54BDA8B3C3760F0B35FECE"
        height: UInt64 {lower: 316771, higher: 0}
        id: undefined
        index: undefined
        merkleComponentHash: "3B21D8842EB70A780A662CCA19B8B030E2D5C7FB4C54BDA8B3C3760F0B35FECE"
    type: 16724
    version: 1

Come si può verificare dai dati al campo height, le transazioni non ancora convalidate sono valorizzate con l'altezza del blocco a 0.

10.3 Ricevere notifiche sui blocchi

Per venire informati alla creazione dei nuovi blocchi.

listener.open().then(() => {
  //sottoscrizione all'evento di nuovo blocco della blockchain
  listener.newBlock().subscribe((block) => console.log(block));
});
Output esemplificativo
> Promise {<pending>}
> NewBlock
    beneficiaryAddress: Address {address: 'TAKATV2VSYBH3RX4JVCCILITWANT6JRANZI2AUQ', networkType: 152}
    blockReceiptsHash: "ABDDB66A03A270E4815C256A8125B70FC3B7EFC4B95FF5ECAD517CB1AB5F5334"
    blockTransactionsHash: "0000000000000000000000000000000000000000000000000000000000000000"
    difficulty: UInt64 {lower: 1316134912, higher: 2328}
    feeMultiplier: 0
    generationHash: "5B4F32D3F2CDD17917D530A6A967927D93F73F2B52CC590A64E3E94408D8CE96"
    hash: "E8294BDDDAE32E17242DF655805EC0FCAB3B628A331824B87A3CA7578683B09C"
    height: UInt64 {lower: 316759, higher: 0}
    networkType: 152
    previousBlockHash: "38382D616772682321D58046511DD942F36A463155C5B7FB0A2CBEE8E29B253C"
    proofGamma: "37187F1C8BD8C87CB4F000F353ACE5717D988BC220EFBCC25E2F40B1FB9B7D7A"
    proofScalar: "AD91A572E5D81EA92FE313CA00915E5A497F60315C63023A52E292E55345F705"
    proofVerificationHash: "EF58228B3EB3C422289626935DADEF11"
    signature: "A9481E5976EDA86B74433E8BCC8495788BA2B9BE0A50F9435AD90A14D1E362D934BA26069182C373783F835E55D7F3681817716295EC1EFB5F2375B6DE302801"
    signer: PublicAccount {publicKey: 'F2195B3FAFBA3DF8C31CFBD9D5BE95BB3F3A04BDB877C59EFB9D1C54ED2DC50E', address: Address}
    stateHash: "4A1C828B34DE47759C2D717845830BA14287A4EC7220B75494BDC31E9539FCB5"
    timestamp: UInt64 {lower: 3851456497, higher: 2}
    type: 33091
    version: 1

Sottoscrivendo l'evento listener.newBlock(), la frequenza delle notifiche sarà di circa una ogni 30 secondi, e la probabilità di disconnessione per inattività sarà bassa.

In alcuni rari casi, il tempo per la generazione di un nuovo blocco potrebbe superare il minuto e il listener deve eseguire una riconnessione (Altri fattori potrebbero innescare la disconnessione perciò si consiglia di aggiungere altre chiamate di sottoscrizione come quelle descritte nella prossima sezione).

10.4 Richieste di firma

Notifica dell'evento di una transazione che richiede la firma.

listener.open().then(() => {
  //Evento di transazione di gruppo bonded che necessita la firma
  listener
    .aggregateBondedAdded(alice.address)
    .subscribe(async (tx) => console.log(tx));
});
Output esemplificativo
> AggregateTransaction
    cosignatures: []
    deadline: Deadline {adjustedValue: 12450154608}
  > innerTransactions: Array(2)
        0: TransferTransaction {type: 16724, networkType: 152, version: 1, deadline: Deadline, maxFee: UInt64, }
        1: TransferTransaction {type: 16724, networkType: 152, version: 1, deadline: Deadline, maxFee: UInt64, }
    maxFee: UInt64 {lower: 94400, higher: 0}
    networkType: 152
    signature: "972968C5A2FB70C1D644BE206A190C4FCFDA98976F371DBB70D66A3AAEBCFC4B26E7833BCB86C407879C07927F6882C752C7012C265C2357CAA52C29834EFD0F"
    signer: PublicAccount {publicKey: '0E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26', address: Address}
  > transactionInfo: TransactionInfo
        hash: "44B2CD891DA0B788F1DD5D5AB24866A9A172C80C1749DCB6EB62255A2497EA08"
        height: UInt64 {lower: 0, higher: 0}
        id: undefined
        index: undefined
        merkleComponentHash: "0000000000000000000000000000000000000000000000000000000000000000"
    type: 16961
    version: 1

Tutte le transazioni aggregate che fanno riferimento all'Indirizzo specificato verranno notificate. L'evento relativo alla firma di un Indirizzo cointestato è distinto da questo e si userà un altro filtro apposito.

10.5 Consigli pratici

Connessione perpetua

Scegliere dalla lista dei nodi uno che possa andar bene e tentare la connessione.

Collegamento ad un nodo
//lista dei nodi
NODES = ["https://node.com:3001",...];
function connectNode(nodes) {
    const node = nodes[Math.floor(Math.random() * nodes.length)] ;
    console.log("tentativo con:" + node);
    return new Promise((resolve, reject) => {
        let req = new XMLHttpRequest();
        req.timeout = 2000; //timeout value:2sec(=2000ms)
        req.open('GET', node + "/node/health", true);
        req.onload = function() {
            if (req.status === 200) {
                const status = JSON.parse(req.responseText).status;
                if(status.apiNode == "up" && status.db == "up"){
                    return resolve(node);
                }else{
                    console.log("fail node status:" + status);
                    return connectNode(nodes).then(node => resolve(node));
                }
            } else {
                console.log("fail request status:" + req.status)
                return connectNode(nodes).then(node => resolve(node));
            }
        };
        req.onerror = function(e) {
            console.log("onerror:" + e)
            return connectNode(nodes).then(node => resolve(node));
        };
        req.ontimeout = function (e) {
            console.log("ontimeout")
            return connectNode(nodes).then(node => resolve(node));
        };
    req.send();
    });
}

Nel caso in cui i tempi di risposta del nodo siano alti, impostare un timeout e cambiare nodo. Controllare anche il valore della risposta ad una richiesta /node/health e ricontrollare se lo stato del nodo non è normale.

Istanziare il repository
function createRepo(nodes) {
  return connectNode(nodes).then(async function onFulfilled(node) {
    const repo = new sym.RepositoryFactoryHttp(node);
    try {
      epochAdjustment = await repo.getEpochAdjustment().toPromise();
    } catch (error) {
      console.log("fail createRepo");
      return await createRepo(nodes);
    }
    return await repo;
  });
}

Eventualmente, alcuni nodi potrebbero non aver liberato la connessione al servizio /network/properties, succede raramente, ma la chiamata getEpochAdjustment() andrebbe controllata. Se non va a buon fine, andare in ricorsione su createRepo.

listeners delle connessioni perpetue
async function listenerKeepOpening(nodes) {
  const repo = await createRepo(NODES);
  let wsEndpoint = repo.url.replace("http", "ws") + "/ws";
  const nsRepo = repo.createNamespaceRepository();
  const lner = new sym.Listener(wsEndpoint, nsRepo, WebSocket);
  try {
    await lner.open();
    lner.newBlock();
  } catch (e) {
    console.log("fail websocket");
    return await listenerKeepOpening(nodes);
  }
  lner.webSocket.onclose = async function () {
    console.log("listener onclose");
    return await listenerKeepOpening(nodes);
  };
  return lner;
}

Alla disconnessione del listener, si ricollega.

Attivare il listener.
listener = await listenerKeepOpening(NODES);

Firma automatica di transazioni non firmate

Individuare transazioni non firmate e quindi firmarle e propagarle ai nodi della rete. Sono necessari due pattern di notifica: il primo al caricamento della pagina il secondo durante visualizzazione.

//legge rxjs.operators
op = require("/node_modules/rxjs/operators");
rxjs = require("/node_modules/rxjs");

//Aggancia le notifiche per transazioni aggregate
bondedListener = listener.aggregateBondedAdded(bob.address);
bondedHttp = txRepo
  .search({ address: bob.address, group: sym.TransactionGroup.Partial })
  .pipe(
    op.delay(2000),
    op.mergeMap((page) => page.data)
  );
//Imposta i listener per le transazioni confermate relative agli Indirizzi di interessse
const statusChanged = function (address, hash) {
  const transactionObservable = listener.confirmed(address);
  const errorObservable = listener.status(address, hash);
  return rxjs.merge(transactionObservable, errorObservable).pipe(
    op.first(),
    op.map((errorOrTransaction) => {
      if (errorOrTransaction.constructor.name === "TransactionStatusError") {
        throw new Error(errorOrTransaction.code);
      } else {
        return errorOrTransaction;
      }
    })
  );
};
//Esecuzione delle firme dei cofirmatari
function exeAggregateBondedCosignature(tx) {
  txRepo
    .getTransactionsById(
      [tx.transactionInfo.hash],
      sym.TransactionGroup.Partial
    )
    .pipe(
      //Solo nel caso di transazione notificata
      op.filter((aggTx) => aggTx.length > 0)
    )
    .subscribe(async (aggTx) => {
      //Se il mio Indirizzo è in firma
      if (
        aggTx[0].innerTransactions.find((inTx) =>
          inTx.signer.equals(bob.publicAccount)
        ) != undefined
      ) {
        //Transazione con la firma di Alice
        const cosignatureTx = sym.CosignatureTransaction.create(aggTx[0]);
        const signedTx = bob.signCosignatureTransaction(cosignatureTx);
        const cosignedAggTx = await txRepo
          .announceAggregateBondedCosignature(signedTx)
          .toPromise();
        statusChanged(bob.address, signedTx.parentHash).subscribe((res) => {
          console.log(res);
        });
      }
    });
}
bondedSubscribe = function (observer) {
  observer
    .pipe(
      //Se non è già stata firmata
      op.filter((tx) => {
        return !tx.signedByAccount(
          sym.PublicAccount.createFromPublicKey(bob.publicKey, networkType)
        );
      })
    )
    .subscribe((tx) => {
      console.log(tx);
      exeAggregateBondedCosignature(tx);
    });
};
bondedSubscribe(bondedListener);
bondedSubscribe(bondedHttp);
Note

E' molto importante, per evitare di firmare automaticamente con il nostro Indirizzo transazioni fraudolente, ricordarsi di impostare una logica di controllo/filtro, per esempio verificando l'Indirizzo di chi ha inviato/creato la transazione.