diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d4e907..2405557 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: node-version: 16 - name: Use npm caches - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} @@ -24,7 +24,7 @@ jobs: - name: Use node_modules caches id: cache-nm - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-nm-${{ hashFiles('package-lock.json') }} diff --git a/openapi.yaml b/openapi.yaml index f17a8c0..a9c41af 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: GroundControl push server API description: Push notifications server for BlueWallet - version: 0.0.13 + version: 0.0.14 servers: - url: http://localhost:3001 - url: https://groundcontrol-bluewallet-stg.herokuapp.com @@ -291,6 +291,10 @@ components: level: type: "string" enum: ["transactions"] + category: + type: "string" + default: "TRANSACTION_CATEGORY" + description: "Only included if type is 2, 3, or 4" sat: type: "integer" description: amount of satoshis @@ -316,6 +320,10 @@ components: level: type: "string" enum: ["transactions"] + category: + type: "string" + default: "TRANSACTION_CATEGORY" + description: "Only included if type is 2, 3, or 4" sat: type: "integer" description: amount of satoshis @@ -339,6 +347,10 @@ components: level: type: "string" enum: ["transactions"] + category: + type: "string" + default: "TRANSACTION_CATEGORY" + description: "Only included if type is 2, 3, or 4" txid: type: "string" description: txid of the transaction that got confirmed @@ -355,4 +367,4 @@ components: enum: [5] text: type: "string" - description: custom text thats displayed on push notification buble + description: custom text thats displayed on push notification bubble diff --git a/scripts/mass_send.js b/scripts/mass_send.js index f9f023c..5d4e27c 100644 --- a/scripts/mass_send.js +++ b/scripts/mass_send.js @@ -3,14 +3,15 @@ const fs = require("fs"); const text = fs.readFileSync("all_tokens_unique.csv", { encoding: "utf8" }); const lines = text.split("\n"); -console.log('# got', lines.length, 'tokens'); +console.log("# got", lines.length, "tokens"); console.log("INSERT INTO send_queue_2 VALUES "); let fisrt = true; for (const line of lines.slice(0, 1000000)) { const [token, os] = line.split("\t"); - const sql = (fisrt ? '' : ', ') + `(null, '{"type":5,"token":"${token}","os":"${os}","text":"If you are using Lightning, please read our blog post. The service is sunsetting. Balances should be moved to another service"}', null)`; + const sql = + (fisrt ? "" : ", ") + `(null, '{"type":5,"token":"${token}","os":"${os}","text":"If you are using Lightning, please read our blog post. The service is sunsetting. Balances should be moved to another service"}', null)`; console.log(sql); fisrt = false; } diff --git a/src/class/GroundControlToMajorTom.ts b/src/class/GroundControlToMajorTom.ts index a404300..8c0e258 100644 --- a/src/class/GroundControlToMajorTom.ts +++ b/src/class/GroundControlToMajorTom.ts @@ -15,10 +15,10 @@ if (!process.env.APNS_P8 || !process.env.APPLE_TEAM_ID || !process.env.APNS_P8_K } const keyFileStr = Buffer.from(process.env.GOOGLE_KEY_FILE, "hex").toString("ascii"); -require('fs').writeFileSync('/tmp/google_key_file.json', keyFileStr, {encoding: "ascii"}); +require("fs").writeFileSync("/tmp/google_key_file.json", keyFileStr, { encoding: "ascii" }); const auth = new GoogleAuth({ - keyFile: '/tmp/google_key_file.json', - scopes: 'https://www.googleapis.com/auth/cloud-platform', + keyFile: "/tmp/google_key_file.json", + scopes: "https://www.googleapis.com/auth/cloud-platform", }); /** @@ -77,7 +77,7 @@ export class GroundControlToMajorTom { ): Promise { const fcmPayload = { message: { - token: '', + token: "", data: { badge: String(pushNotification.badge), tag: pushNotification.txid, @@ -86,7 +86,7 @@ export class GroundControlToMajorTom { title: "New unconfirmed transaction", body: "You received new transfer on " + GroundControlToMajorTom.shortenAddress(pushNotification.address), }, - } + }, }; const apnsPayload = { @@ -165,7 +165,7 @@ export class GroundControlToMajorTom { static async pushOnchainAddressWasPaid(dataSource: DataSource, serverKey: string, apnsP8: string, pushNotification: components["schemas"]["PushNotificationOnchainAddressGotPaid"]): Promise { const fcmPayload = { message: { - token: '', + token: "", data: { badge: String(pushNotification.badge), tag: pushNotification.txid, @@ -174,7 +174,7 @@ export class GroundControlToMajorTom { title: "+" + pushNotification.sat + " sats", body: "Received on " + GroundControlToMajorTom.shortenAddress(pushNotification.address), }, - } + }, }; const apnsPayload = { @@ -322,7 +322,7 @@ export class GroundControlToMajorTom { try { responseText = await rawResponse.text(); } catch (error) { - console.error("error getting response from FCM", error); + console.error("error getting response from FCM", error); } delete fcmPayload["message"]["token"]; // compacting a bit, we dont need token in payload as well @@ -382,7 +382,7 @@ export class GroundControlToMajorTom { static processApnsResponse(dataSource: DataSource, response, token: string) { if (response && response.data) { try { - console.log('parsing', response.data); + console.log("parsing", response.data); const data = JSON.parse(response.data); if (data && data.reason && ["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"].includes(data.reason)) return GroundControlToMajorTom.killDeadToken(dataSource, token); } catch (_) {} diff --git a/src/openapi/api.ts b/src/openapi/api.ts index 9596e83..008e469 100644 --- a/src/openapi/api.ts +++ b/src/openapi/api.ts @@ -3,181 +3,183 @@ * Do not make direct changes to the file. */ -export type paths = { - "/lightningInvoiceGotSettled": { - post: { - responses: { - /** OK */ - 200: unknown; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LightningInvoiceSettledNotification"]; +export type paths ={ + "/lightningInvoiceGotSettled": { + post: { + responses: { + /** OK */ + 200: unknown; }; - }; - }; - }; - "/majorTomToGroundControl": { - post: { - responses: { - /** Created */ - 201: unknown; - }; - requestBody: { - content: { - "application/json": { - addresses?: string[]; - hashes?: string[]; - txids?: string[]; - token?: string; - os?: string; - } & { [key: string]: unknown }; + requestBody: { + content: { + "application/json": components["schemas"]["LightningInvoiceSettledNotification"]; + }; }; }; }; - }; - "/unsubscribe": { - post: { - responses: { - /** Created */ - 201: unknown; - }; - requestBody: { - content: { - "application/json": { - addresses?: string[]; - hashes?: string[]; - txids?: string[]; - token?: string; - os?: string; - } & { [key: string]: unknown }; + "/majorTomToGroundControl": { + post: { + responses: { + /** Created */ + 201: unknown; }; - }; - }; - }; - "/ping": { - get: { - responses: { - /** OK */ - 200: { + requestBody: { content: { - "application/json": components["schemas"]["ServerInfo"]; + "application/json": { + addresses?: string[]; + hashes?: string[]; + txids?: string[]; + token?: string; + os?: string; + } & { [key: string]: unknown } }; }; }; }; - }; - "/getTokenConfiguration": { - post: { - responses: { - /** OK */ - 200: { + "/unsubscribe": { + post: { + responses: { + /** Created */ + 201: unknown; + }; + requestBody: { content: { - "application/json": components["schemas"]["TokenConfiguration"]; + "application/json": { + addresses?: string[]; + hashes?: string[]; + txids?: string[]; + token?: string; + os?: string; + } & { [key: string]: unknown } }; }; }; - requestBody: { - content: { - "application/json": { - token?: string; - os?: string; - } & { [key: string]: unknown }; + }; + "/ping": { + get: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["ServerInfo"]; + }; + }; }; }; }; - }; - "/setTokenConfiguration": { - post: { - responses: { - /** OK */ - 200: unknown; + "/getTokenConfiguration": { + post: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["TokenConfiguration"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TokenConfiguration"] & + ({ + token: string; + os: string; + } & { [key: string]: unknown }) & { [key: string]: unknown }; + }; + }; }; - requestBody: { - content: { - "application/json": components["schemas"]["TokenConfiguration"] & - ({ + }; + "/setTokenConfiguration": { + post: { + responses: { + /** OK */ + 200: unknown; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TokenConfiguration"] & { token: string; os: string; - } & { [key: string]: unknown }) & { [key: string]: unknown }; + } & { [key: string]: unknown } + + }; }; }; }; - }; - "/enqueue": { - post: { - responses: { - /** OK */ - 200: unknown; - }; - requestBody: { - content: { - "application/json": ( - | components["schemas"]["PushNotificationLightningInvoicePaid"] - | components["schemas"]["PushNotificationOnchainAddressGotPaid"] - | components["schemas"]["PushNotificationOnchainAddressGotUnconfirmedTransaction"] - | components["schemas"]["PushNotificationTxidGotConfirmed"] - ) & { [key: string]: unknown }; + "/enqueue": { + post: { + responses: { + /** OK */ + 200: unknown; + }; + requestBody: { + content: { + "application/json": ( + + | components["schemas"]["PushNotificationLightningInvoicePaid"] + | components["schemas"]["PushNotificationOnchainAddressGotPaid"] + | components["schemas"]["PushNotificationOnchainAddressGotUnconfirmedTransaction"] + | components["schemas"]["PushNotificationTxidGotConfirmed"] + ) & { [key: string]: unknown }; }; }; }; }; - }; -}; - -export type components = { - schemas: { - ServerInfo: { - name?: string; - description?: string; - version?: string; - uptime?: number; - last_processed_block?: number; - send_queue_size?: number; - sent_24h?: number; - } & { [key: string]: unknown }; - /** @enum {string} */ - NotificationLevel: "transactions" | "news" | "price" | "tips"; - TokenConfiguration: { - level_all?: boolean; - level_transactions?: boolean; - level_news?: boolean; - level_price?: boolean; - level_tips?: boolean; - lang?: string; - app_version?: string; + } + + export type components = { + schemas: { + ServerInfo: { + name?: string; + description?: string; + version?: string; + uptime?: number; + last_processed_block?: number; + send_queue_size?: number; + sent_24h?: number; + }; + /** @enum {string} */ + NotificationLevel: "transactions" | "news" | "price" | "tips"; + TokenConfiguration: { + level_all?: boolean; + level_transactions?: boolean; + level_news?: boolean; + level_price?: boolean; + level_tips?: boolean; + lang?: string; + app_version?: string; } & { [key: string]: unknown }; - /** @description object thats posted to GroundControl to notify end-user that his specific invoice was paid by someone */ + /** @description object thats posted to GroundControl to notify end-user that his specific invoice was paid by someone */ LightningInvoiceSettledNotification: { - /** @description text that was embedded in invoice paid */ - memo?: string; - /** @description hex string preimage */ - preimage?: string; - /** @description hex string preimage hash */ - hash?: string; - /** @description exactly how much satoshis was paid to make this invoice settked (>= invoice amount) */ - amt_paid_sat?: number; + /** @description text that was embedded in invoice paid */ + memo?: string; + /** @description hex string preimage */ + preimage?: string; + /** @description hex string preimage hash */ + hash?: string; + /** @description exactly how much satoshis was paid to make this invoice settked (>= invoice amount) */ + amt_paid_sat?: number; } & { [key: string]: unknown }; - /** @description payload for push notification delivered to phone */ + + /** @description payload for push notification delivered to phone */ PushNotificationBase: { - /** - * @description type: - * * `1` - Your lightning invoice was paid - * * `2` - New transaction to one of your addresses - * * `3` - New unconfirmed transaction to one of your addresses - * * `4` - Transaction confirmed - * * `5` - Arbitrary text message - * - * @enum {integer} - */ - type: 1 | 2 | 3 | 4 | 5; - token: string; - /** @enum {string} */ - os: "android" | "ios"; - badge?: number; - level: components["schemas"]["NotificationLevel"]; + /** + * @description type: + * * `1` - Your lightning invoice was paid + * * `2` - New transaction to one of your addresses + * * `3` - New unconfirmed transaction to one of your addresses + * * `4` - Transaction confirmed + * * `5` - Arbitrary text message + * + * @enum {integer} + */ + type: 1 | 2 | 3 | 4 | 5; + token: string; + /** @enum {string} */ + os: "android" | "ios"; + badge?: number; + level: components["schemas"]["NotificationLevel"]; } & { [key: string]: unknown }; PushNotificationLightningInvoicePaid: components["schemas"]["PushNotificationBase"] & - ({ + ({ /** @enum {integer} */ type?: 1; /** @enum {string} */ @@ -188,52 +190,67 @@ export type components = { hash: string; /** @description text attached to bolt11 */ memo: string; - } & { [key: string]: unknown }) & { [key: string]: unknown }; - PushNotificationOnchainAddressGotPaid: components["schemas"]["PushNotificationBase"] & - ({ - /** @enum {integer} */ - type?: 2; - /** @enum {string} */ - level?: "transactions"; - /** @description amount of satoshis */ - sat: number; - /** @description user's onchain address that has incoming transaction */ - address: string; - /** @description txid of the transaction where this address is one of the outputs */ - txid: string; - } & { [key: string]: unknown }) & { [key: string]: unknown }; - PushNotificationOnchainAddressGotUnconfirmedTransaction: components["schemas"]["PushNotificationBase"] & - ({ + } & { [key: string]: unknown }) & { [key: string]: unknown }; + PushNotificationOnchainAddressGotPaid: components["schemas"]["PushNotificationBase"] & + ({ + /** @enum {integer} */ + type?: 2; + /** @enum {string} */ + level?: "transactions"; + /** + * @description Only included if type is 2, 3, or 4 + * @default TRANSACTION_CATEGORY + */ + category?: string; + /** @description amount of satoshis */ + sat: number; + /** @description user's onchain address that has incoming transaction */ + address: string; + /** @description txid of the transaction where this address is one of the outputs */ + txid: string; + } & { [key: string]: unknown }) & { [key: string]: unknown }; + + + PushNotificationOnchainAddressGotUnconfirmedTransaction: components["schemas"]["PushNotificationBase"] & { /** @enum {integer} */ type?: 3; /** @enum {string} */ level?: "transactions"; + /** + * @description Only included if type is 2, 3, or 4 + * @default TRANSACTION_CATEGORY + */ + category?: string; /** @description amount of satoshis */ sat: number; /** @description user's onchain address that has incoming transaction */ address: string; /** @description txid of the transaction where this address is one of the outputs */ txid: string; - } & { [key: string]: unknown }) & { [key: string]: unknown }; - PushNotificationTxidGotConfirmed: components["schemas"]["PushNotificationBase"] & - ({ - /** @enum {integer} */ + }; + PushNotificationTxidGotConfirmed: components["schemas"]["PushNotificationBase"] & ({ + /** @enum {integer} */ type?: 4; - /** @enum {string} */ - level?: "transactions"; - /** @description txid of the transaction that got confirmed */ + /** @enum {string} */ + level?: "transactions"; + /** + * @description Only included if type is 2, 3, or 4 + * @default TRANSACTION_CATEGORY + */ + category?: string; + /** @description txid of the transaction that got confirmed */ + /** @description txid of the transaction that got confirmed */ txid: string; } & { [key: string]: unknown }) & { [key: string]: unknown }; - PushNotificationMessage: components["schemas"]["PushNotificationBase"] & - ({ + PushNotificationMessage: components["schemas"]["PushNotificationBase"] & { /** @enum {integer} */ type?: 5; - /** @description custom text thats displayed on push notification buble */ + /** @description custom text thats displayed on push notification bubble */ text: string; - } & { [key: string]: unknown }) & { [key: string]: unknown }; - }; + }; + }; }; + +export interface operations {} -export type operations = {}; - -export type external = {}; +export interface external {} diff --git a/src/worker-blockprocessor.ts b/src/worker-blockprocessor.ts index 6b61aa4..d6b8cc1 100644 --- a/src/worker-blockprocessor.ts +++ b/src/worker-blockprocessor.ts @@ -52,6 +52,7 @@ async function processBlock(blockNum, sendQueueRepository: Repository level: "transactions", token: "", os: "ios", + category: "TRANSACTION_CATEGORY", }; allPotentialPushPayloadsArray.push(payload); } @@ -88,12 +89,7 @@ async function processBlock(blockNum, sendQueueRepository: Repository } // batch insert via a raw query as its faster - await sendQueueRepository - .createQueryBuilder() - .insert() - .into(SendQueue) - .values(entities2save) - .execute(); + await sendQueueRepository.createQueryBuilder().insert().into(SendQueue).values(entities2save).execute(); // now, checking if there is a subscription to one of the mined txids: const query2 = dataSource.getRepository(TokenToTxid).createQueryBuilder().where("txid IN (:...txids)", { txids }); @@ -106,6 +102,7 @@ async function processBlock(blockNum, sendQueueRepository: Repository token: t2txid.token, os: t2txid.os === "ios" ? "ios" : "android", badge: 1, + category: "TRANSACTION_CATEGORY", }; process.env.VERBOSE && console.log("enqueueing", payload); @@ -114,14 +111,8 @@ async function processBlock(blockNum, sendQueueRepository: Repository }); } - // batch insert via a raw query as its faster - await sendQueueRepository - .createQueryBuilder() - .insert() - .into(SendQueue) - .values(entities2save) - .execute(); + await sendQueueRepository.createQueryBuilder().insert().into(SendQueue).values(entities2save).execute(); } dataSource diff --git a/src/worker-processmempool.ts b/src/worker-processmempool.ts index b6db949..8c26f87 100644 --- a/src/worker-processmempool.ts +++ b/src/worker-processmempool.ts @@ -4,13 +4,16 @@ import { TokenToAddress } from "./entity/TokenToAddress"; import { SendQueue } from "./entity/SendQueue"; import dataSource from "./data-source"; import { components } from "./openapi/api"; + require("dotenv").config(); + const url = require("url"); let jayson = require("jayson/promise"); let rpc = url.parse(process.env.BITCOIN_RPC); let client = jayson.client.http(rpc); let processedTxids = {}; + if (!process.env.BITCOIN_RPC) { console.error("not all env variables set"); process.exit(); @@ -51,9 +54,10 @@ async function processMempool() { if (response.result && response.result.vout) { for (const output of response.result.vout) { if (output.scriptPubKey && (output.scriptPubKey.addresses || output.scriptPubKey.address)) { - for (const address of output.scriptPubKey?.addresses ?? (output.scriptPubKey?.address ? [output.scriptPubKey?.address] : []) ) { + for (const address of output.scriptPubKey?.addresses ?? (output.scriptPubKey?.address ? [output.scriptPubKey?.address] : [])) { addresses.push(address); processedTxids[response.result.txid] = true; + const payload: components["schemas"]["PushNotificationOnchainAddressGotUnconfirmedTransaction"] = { address, txid: response.result.txid, @@ -62,7 +66,9 @@ async function processMempool() { level: "transactions", token: "", os: "ios", + category: "TRANSACTION_CATEGORY", }; + allPotentialPushPayloadsArray.push(payload); } } @@ -125,7 +131,7 @@ dataSource try { await processMempool(); } catch (error) { - console.warn('Exception in processMempool():', error); + console.warn("Exception in processMempool():", error); } const end = +new Date(); process.env.VERBOSE && console.log("processing mempool took", (end - start) / 1000, "sec"); @@ -134,6 +140,6 @@ dataSource } }) .catch((error) => { - console.error("exception in mempool processor:", error, "comitting suicide"); + console.error("exception in mempool processor:", error, "committing suicide"); process.exit(1); }); diff --git a/src/worker-sender.ts b/src/worker-sender.ts index d6efacc..f9e1507 100644 --- a/src/worker-sender.ts +++ b/src/worker-sender.ts @@ -33,10 +33,11 @@ dataSource while (1) { // getting random record so multiple workers wont fight for the same record to send - const [record] = await sendQueueRepository .createQueryBuilder() - .orderBy('RAND()') // mysql-specific - .limit(1) - .getMany(); + const [record] = await sendQueueRepository + .createQueryBuilder() + .orderBy("RAND()") // mysql-specific + .limit(1) + .getMany(); // ^^^ 'order by rand' is suboptimal, but will have to do for now, especially if we are aiming to keep // queue table near-empty @@ -50,7 +51,7 @@ dataSource const query = `SELECT GET_LOCK(?, ?) as result`; const result = await sendQueueRepository.query(query, [`send${record.id}`, 0]); if (result[0].result !== 1) { - process.env.VERBOSE && console.log('could not acquire lock, skipping record'); + process.env.VERBOSE && console.log("could not acquire lock, skipping record"); continue; }