diff --git a/.prettierignore b/.prettierignore index 5de88e0f..5d7a1389 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,6 +9,7 @@ docs/ .lighthouseci/ **/coins/* *.yml +*.sol package-lock.json package.json diff --git a/_redirects b/_redirects index b8c597fe..4f2e1411 100644 --- a/_redirects +++ b/_redirects @@ -1,3 +1,3 @@ /api/production/* https://prototype-signer-service-polygon.pendulumchain.tech/:splat 200 /api/staging/* https://prototype-signer-service-polygon-staging.pendulumchain.tech/:splat 200 -/* /index.html 200 +/* /index.html 200 \ No newline at end of file diff --git a/package.json b/package.json index 801c7888..11916299 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "@reown/appkit-adapter-wagmi": "^1.3.1", "@sentry/react": "^8.36.0", "@sentry/vite-plugin": "^2.22.6", + "@slack/web-api": "^7.7.0", "@talismn/connect-components": "^1.1.9", + "@talismn/connect-wallets": "^1.2.5", "@tanstack/react-query": "^5.61.0", "@walletconnect/modal": "^2.6.2", "@walletconnect/universal-provider": "^2.12.2", diff --git a/signer-service/README.md b/signer-service/README.md index 1c0a4ac5..b0895784 100644 --- a/signer-service/README.md +++ b/signer-service/README.md @@ -25,6 +25,7 @@ The following environment variables are available to configure the service. - `FUNDING_SECRET`: Secret key to sign the funding transactions on Stellar. - `PENDULUM_FUNDING_SEED`: Seed phrase to sign the funding transactions on Pendulum. - `MOONBEAM_EXECUTOR_PRIVATE_KEY`: Private key to sign the transactions on Moonbeam. +- `SLACK_WEB_HOOK_TOKEN` - Slack web hook token for error reporting. ### Optional diff --git a/signer-service/src/api/controllers/moonbeam.controller.js b/signer-service/src/api/controllers/moonbeam.controller.js index 19bc2c72..2ff22a5c 100644 --- a/signer-service/src/api/controllers/moonbeam.controller.js +++ b/signer-service/src/api/controllers/moonbeam.controller.js @@ -8,6 +8,7 @@ const { MOONBEAM_RECEIVER_CONTRACT_ADDRESS, MOONBEAM_FUNDING_AMOUNT_UNITS, } = require('../../constants/constants'); +const { SlackNotifier } = require('../services/slack.service'); const splitReceiverABI = require('../../../../mooncontracts/splitReceiverABI.json'); exports.executeXcmController = async (req, res) => { @@ -54,6 +55,7 @@ exports.executeXcmController = async (req, res) => { }; exports.sendStatusWithPk = async () => { + const slackService = new SlackNotifier(); let moonbeamExecutorAccount; try { @@ -66,12 +68,16 @@ exports.sendStatusWithPk = async () => { const balance = await publicClient.getBalance({ address: moonbeamExecutorAccount.address }); // We are checking if the balance is less than 10 GLMR - const minimum_balance = Big(MOONBEAM_FUNDING_AMOUNT_UNITS).times(Big(10).pow(18)); - if (balance < minimum_balance) { + const minimumBalance = Big(MOONBEAM_FUNDING_AMOUNT_UNITS).times(Big(10).pow(18)); + + if (balance < minimumBalance) { + slackService.sendMessage({ + text: `Current balance of funding account is ${balance} GLMR please charge the account ${moonbeamExecutorAccount.address}.`, + }); return { status: false, public: moonbeamExecutorAccount.address }; - } else { - return { status: true, public: moonbeamExecutorAccount.address }; } + + return { status: true, public: moonbeamExecutorAccount.address }; } catch (error) { console.error('Error fetching Moonbeam executor balance:', error); return { status: false, public: moonbeamExecutorAccount?.address }; diff --git a/signer-service/src/api/routes/v1/index.js b/signer-service/src/api/routes/v1/index.js index 00556acc..063b4afc 100644 --- a/signer-service/src/api/routes/v1/index.js +++ b/signer-service/src/api/routes/v1/index.js @@ -15,7 +15,7 @@ const { sendStatusWithPk: sendStellarStatusWithPk } = require('../../services/st const { sendStatusWithPk: sendPendulumStatusWithPk } = require('../../services/pendulum.service'); const { sendStatusWithPk: sendMoonbeamStatusWithPk } = require('../../controllers/moonbeam.controller'); -async function sendStatusWithPk(req, res, next) { +async function sendStatusWithPk(_, res) { const stellar = await sendStellarStatusWithPk(); const pendulum = await sendPendulumStatusWithPk(); const moonbeam = await sendMoonbeamStatusWithPk(); diff --git a/signer-service/src/api/services/pendulum.service.js b/signer-service/src/api/services/pendulum.service.js index fdf09809..a7c53a58 100644 --- a/signer-service/src/api/services/pendulum.service.js +++ b/signer-service/src/api/services/pendulum.service.js @@ -8,6 +8,7 @@ const { PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS, } = require('../../constants/constants'); const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); +const { SlackNotifier } = require('./slack.service'); require('dotenv').config(); @@ -21,6 +22,14 @@ function multiplyByPowerOfTen(bigDecimal, power) { return newBigDecimal; } +function divideByPowerOfTen(bigDecimal, power) { + const newBigDecimal = new Big(bigDecimal); + if (newBigDecimal.c[0] === 0) return newBigDecimal; + + newBigDecimal.e -= power; + return newBigDecimal; +} + let api; let previousSpecVersion; @@ -82,7 +91,16 @@ exports.fundEphemeralAccount = async (ephemeralAddress) => { } }; +const ChainDecimals = 12; + +const nativeToDecimal = (value, decimals = ChainDecimals) => { + const divisor = new Big(10).pow(decimals); + + return value.div(divisor); +}; + exports.sendStatusWithPk = async () => { + const slackNotifier = new SlackNotifier(); const apiData = await createPolkadotApi(); const { fundingAccountKeypair } = getFundingData(apiData.ss58Format, apiData.decimals); const { data: balance } = await apiData.api.query.system.account(fundingAccountKeypair.address); @@ -109,14 +127,28 @@ exports.sendStatusWithPk = async () => { if (remainingMaxSubsidiesAvailable.lt(SUBSIDY_MINIMUM_RATIO_FUND_UNITS)) { isTokensSufficient = false; console.log(`Token ${token} balance is insufficient.`); + + slackNotifier.sendMessage({ + text: `Current balance of funding account is ${nativeToDecimal( + tokenBalance, + ).toString()} ${token} please charge the account ${fundingAccountKeypair.address}.`, + }); } }), ); const minimumBalanceFundingAccount = multiplyByPowerOfTen(Big(PENDULUM_FUNDING_AMOUNT_UNITS), apiData.decimals); const nativeBalance = Big(balance?.free?.toString() ?? '0'); + if (nativeBalance.gte(minimumBalanceFundingAccount) && isTokensSufficient) { return { status: true, public: fundingAccountKeypair.address }; } + if (nativeBalance.lt(minimumBalanceFundingAccount)) { + slackNotifier.sendMessage({ + text: `Current balance of funding account is ${nativeToDecimal( + nativeBalance, + ).toString()} PEN please charge the account ${fundingAccountKeypair.address}.`, + }); + } return { status: false, public: fundingAccountKeypair.address }; }; diff --git a/signer-service/src/api/services/slack.service.js b/signer-service/src/api/services/slack.service.js new file mode 100644 index 00000000..5705f4dd --- /dev/null +++ b/signer-service/src/api/services/slack.service.js @@ -0,0 +1,56 @@ +// Store last message timestamps with their message signatures +const messageHistory = new Map(); +// 6 hours in milliseconds +const cooldownPeriod = 6 * 60 * 60 * 1000; + +class SlackNotifier { + constructor() { + if (process.env.SLACK_WEB_HOOK_TOKEN) { + this.webhookUrl = `https://hooks.slack.com/services/${process.env.SLACK_WEB_HOOK_TOKEN}`; + } else { + throw new Error('SLACK_WEB_HOOK_TOKEN is not defined'); + } + } + + generateMessageSignature(message) { + // Create a unique signature for the message + return JSON.stringify(message); + } + + isMessageAllowed(signature) { + const now = Date.now(); + const lastSent = messageHistory.get(signature); + + if (!lastSent) return true; + + return now - lastSent >= cooldownPeriod; + } + + async sendMessage(message) { + const signature = this.generateMessageSignature(message); + + if (!this.isMessageAllowed(signature)) { + // Message is still in cooldown period, skip sending + return; + } + + const payload = JSON.stringify(message); + + const response = await fetch(this.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: payload, + }); + + if (!response.ok) { + throw new Error(`Failed to send message. Status: ${response.status}`); + } + + // Update the timestamp for this message + messageHistory.set(signature, Date.now()); + } +} + +exports.SlackNotifier = SlackNotifier; diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.js index 592f42fd..6a8d00d6 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.js @@ -6,6 +6,8 @@ const { STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS, } = require('../../constants/constants'); const { TOKEN_CONFIG, getTokenConfigByAssetCode } = require('../../constants/tokenConfig'); +const { SlackNotifier } = require('./slack.service'); + // Derive funding pk const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET).publicKey(); const horizonServer = new Horizon.Server(HORIZON_URL); @@ -132,14 +134,19 @@ async function buildPaymentAndMergeTx( } async function sendStatusWithPk() { + const slackNotifier = new SlackNotifier(); + let stellarBalance = null; + try { // ensure the funding account exists - const horizonServer = new Horizon.Server(HORIZON_URL); - let account = await horizonServer.loadAccount(FUNDING_PUBLIC_KEY); - let stellarBalance = account.balances.find((balance) => balance.asset_type === 'native'); + const account = await horizonServer.loadAccount(FUNDING_PUBLIC_KEY); + stellarBalance = account.balances.find((balance) => balance.asset_type === 'native'); // ensure we have at the very least 10 XLM in the account if (Number(stellarBalance.balance) < STELLAR_FUNDING_AMOUNT_UNITS) { + slackNotifier.sendMessage({ + text: `Current balance of funding account is ${stellarBalance.balance} XLM please charge the account ${FUNDING_PUBLIC_KEY}.`, + }); return { status: false, public: FUNDING_PUBLIC_KEY }; } diff --git a/signer-service/src/index.js b/signer-service/src/index.js index 07036af7..770e1df6 100755 --- a/signer-service/src/index.js +++ b/signer-service/src/index.js @@ -1,4 +1,3 @@ -const { Keypair } = require('stellar-sdk'); const { port, env } = require('./config/vars'); const logger = require('./config/logger'); const app = require('./config/express'); diff --git a/yarn.lock b/yarn.lock index e4afabb4..ee80dd8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4422,6 +4422,42 @@ __metadata: languageName: node linkType: hard +"@slack/logger@npm:^4.0.0": + version: 4.0.0 + resolution: "@slack/logger@npm:4.0.0" + dependencies: + "@types/node": "npm:>=18.0.0" + checksum: 10/dc79e9d2032c4bf9ce01d96cc72882f003dd376d036f172d4169662cfc2c9b384a80d5546b06021578dd473e7059f064303f0ba851eeb153387f2081a1e3062e + languageName: node + linkType: hard + +"@slack/types@npm:^2.9.0": + version: 2.14.0 + resolution: "@slack/types@npm:2.14.0" + checksum: 10/fa24a113b88e087f899078504c2ba50ab9795f7c2dd1a2d95b28217a3af20e554494f9cc3b8c8ce173120990d98e19400c95369f9067cecfcc46c08b59d2a46f + languageName: node + linkType: hard + +"@slack/web-api@npm:^7.7.0": + version: 7.8.0 + resolution: "@slack/web-api@npm:7.8.0" + dependencies: + "@slack/logger": "npm:^4.0.0" + "@slack/types": "npm:^2.9.0" + "@types/node": "npm:>=18.0.0" + "@types/retry": "npm:0.12.0" + axios: "npm:^1.7.8" + eventemitter3: "npm:^5.0.1" + form-data: "npm:^4.0.0" + is-electron: "npm:2.2.2" + is-stream: "npm:^2" + p-queue: "npm:^6" + p-retry: "npm:^4" + retry: "npm:^0.13.1" + checksum: 10/f2a698f853d0aaab11a23e9ce659e2cf3e43792eeb6861ef05258bbd2bb5cfe3bad8fc7bff4ee6fe471e67ec9fb6b22ef16be4478ab68f139df730337ebe15bb + languageName: node + linkType: hard + "@socket.io/component-emitter@npm:~3.1.0": version: 3.1.2 resolution: "@socket.io/component-emitter@npm:3.1.2" @@ -4709,7 +4745,7 @@ __metadata: languageName: node linkType: hard -"@talismn/connect-wallets@npm:1.2.8": +"@talismn/connect-wallets@npm:1.2.8, @talismn/connect-wallets@npm:^1.2.5": version: 1.2.8 resolution: "@talismn/connect-wallets@npm:1.2.8" peerDependencies: @@ -4875,6 +4911,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=18.0.0": + version: 22.10.2 + resolution: "@types/node@npm:22.10.2" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/451adfefed4add58b069407173e616220fd4aaa3307cdde1bb701aa053b65b54ced8483db2f870dcedec7a58cb3b06101fbc19d85852716672ec1fd3660947fa + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.5 resolution: "@types/prop-types@npm:15.7.5" @@ -4892,6 +4937,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:0.12.0": + version: 0.12.0 + resolution: "@types/retry@npm:0.12.0" + checksum: 10/bbd0b88f4b3eba7b7acfc55ed09c65ef6f2e1bcb4ec9b4dca82c66566934351534317d294a770a7cc6c0468d5573c5350abab6e37c65f8ef254443e1b028e44d + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12": version: 7.3.13 resolution: "@types/semver@npm:7.3.13" @@ -6178,6 +6230,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.8": + version: 1.7.9 + resolution: "axios@npm:1.7.9" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/b7a5f660ea53ba9c2a745bf5ad77ad8bf4f1338e13ccc3f9f09f810267d6c638c03dac88b55dae8dc98b79c57d2d6835be651d58d2af97c174f43d289a9fd007 + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs2@npm:^0.3.3": version: 0.3.3 resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3" @@ -8620,6 +8683,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^4.0.4": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 10/8030029382404942c01d0037079f1b1bc8fed524b5849c237b80549b01e2fc49709e1d0c557fa65ca4498fc9e24cff1475ef7b855121fcc15f9d61f93e282346 + languageName: node + linkType: hard + "events@npm:3.3.0, events@npm:^3.0.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -9944,6 +10014,13 @@ __metadata: languageName: node linkType: hard +"is-electron@npm:2.2.2": + version: 2.2.2 + resolution: "is-electron@npm:2.2.2" + checksum: 10/de5aa8bd8d72c96675b8d0f93fab4cc21f62be5440f65bc05c61338ca27bd851a64200f31f1bf9facbaa01b3dbfed7997b2186741d84b93b63e0aff1db6a9494 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -10106,7 +10183,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0": +"is-stream@npm:^2, is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 @@ -11906,6 +11983,13 @@ __metadata: languageName: node linkType: hard +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 10/93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 + languageName: node + linkType: hard + "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -11951,6 +12035,35 @@ __metadata: languageName: node linkType: hard +"p-queue@npm:^6": + version: 6.6.2 + resolution: "p-queue@npm:6.6.2" + dependencies: + eventemitter3: "npm:^4.0.4" + p-timeout: "npm:^3.2.0" + checksum: 10/60fe227ffce59fbc5b1b081305b61a2f283ff145005853702b7d4d3f99a0176bd21bb126c99a962e51fe1e01cb8aa10f0488b7bbe73b5dc2e84b5cc650b8ffd2 + languageName: node + linkType: hard + +"p-retry@npm:^4": + version: 4.6.2 + resolution: "p-retry@npm:4.6.2" + dependencies: + "@types/retry": "npm:0.12.0" + retry: "npm:^0.13.1" + checksum: 10/45c270bfddaffb4a895cea16cb760dcc72bdecb6cb45fef1971fa6ea2e91ddeafddefe01e444ac73e33b1b3d5d29fb0dd18a7effb294262437221ddc03ce0f2e + languageName: node + linkType: hard + +"p-timeout@npm:^3.2.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: "npm:^1.0.0" + checksum: 10/3dd0eaa048780a6f23e5855df3dd45c7beacff1f820476c1d0d1bcd6648e3298752ba2c877aa1c92f6453c7dd23faaf13d9f5149fc14c0598a142e2c5e8d649c + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -13031,6 +13144,13 @@ __metadata: languageName: node linkType: hard +"retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d + languageName: node + linkType: hard + "reusify@npm:^1.0.4": version: 1.0.4 resolution: "reusify@npm:1.0.4" @@ -14490,6 +14610,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe + languageName: node + linkType: hard + "unenv@npm:^1.9.0": version: 1.9.0 resolution: "unenv@npm:1.9.0" @@ -15138,7 +15265,9 @@ __metadata: "@reown/appkit-adapter-wagmi": "npm:^1.3.1" "@sentry/react": "npm:^8.36.0" "@sentry/vite-plugin": "npm:^2.22.6" + "@slack/web-api": "npm:^7.7.0" "@talismn/connect-components": "npm:^1.1.9" + "@talismn/connect-wallets": "npm:^1.2.5" "@tanstack/react-query": "npm:^5.61.0" "@types/big.js": "npm:^6" "@types/bn.js": "npm:^5"