diff --git a/package.json b/package.json index 5d473cf9..a330f51f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "react-router-dom": "^6.8.1", "react-toastify": "^10.0.6", "stellar-base": "^11.0.1", - "stellar-sdk": "^11.3.0", + "stellar-sdk": "^13.1.0", "tailwind": "^4.0.0", "tailwindcss": "^3.4.3", "viem": "^2.21.43", diff --git a/signer-service/.swcrc b/signer-service/.swcrc new file mode 100644 index 00000000..7acf5e67 --- /dev/null +++ b/signer-service/.swcrc @@ -0,0 +1,12 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false + }, + "target": "es2020" + }, + "module": { + "type": "commonjs" + } +} diff --git a/signer-service/package.json b/signer-service/package.json index 73694f6c..b997669d 100644 --- a/signer-service/package.json +++ b/signer-service/package.json @@ -10,13 +10,15 @@ "yarn": "*" }, "scripts": { - "precommit": "yarn lint-staged && yarn lint", - "start": "node ./src/index.js", - "dev": "nodemon ./src/index.js", + "type-check": "tsc --noEmit", "lint-staged": "prettier --write --ignore-unknown", "lint": "eslint ./src/ --ignore-path .gitignore --ignore-pattern internals/scripts", "lint:fix": "yarn lint --fix", "lint:watch": "yarn lint --watch", + "precommit": "yarn lint-staged && yarn lint", + "start": "node ./dist/index.js", + "dev": "nodemon --watch ./src/index.ts --exec 'yarn build && yarn start'", + "build": "yarn type-check && swc src --out-dir dist --copy-files", "test": "vitest", "validate": "yarn lint && yarn test", "postpublish": "git push --tags" @@ -43,11 +45,22 @@ "mongoose": "^5.2.17", "morgan": "^1.8.1", "siwe": "^2.3.2", - "stellar-sdk": "^11.3.0", + "stellar-sdk": "^13.1.0", "viem": "^2.21.3", "winston": "^3.1.0" }, "devDependencies": { + "@pendulum-chain/types": "^1.1.1", + "@stellar/stellar-sdk": "^13.1.0", + "@swc/cli": "^0.5.2", + "@swc/core": "^1.10.4", + "@types/body-parser": "^1.19.5", + "@types/compression": "^1.7.5", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/method-override": "^3.0.0", + "@types/morgan": "^1.9.9", "eslint": "^7.29.0", "eslint-config-airbnb-base": "^14.2.0", "eslint-config-prettier": "^8.8.0", @@ -55,7 +68,8 @@ "husky": "^3.0.7", "mocha": "^6.2.2", "nodemon": "^2.0.1", - "prettier": "^2.8.7" + "prettier": "^2.8.7", + "typescript": "^5.7.2" }, "packageManager": "yarn@4.4.1" } diff --git a/signer-service/src/api/controllers/email.controller.js b/signer-service/src/api/controllers/email.controller.js deleted file mode 100644 index 8586567a..00000000 --- a/signer-service/src/api/controllers/email.controller.js +++ /dev/null @@ -1,10 +0,0 @@ -const { spreadsheet } = require('../../config/vars'); -const { storeDataInGoogleSpreadsheet } = require('./googleSpreadSheet.controller'); - -// These are the headers for the Google Spreadsheet -const EMAIL_SHEET_HEADER_VALUES = ['timestamp', 'email', 'transactionId']; - -exports.EMAIL_SHEET_HEADER_VALUES = EMAIL_SHEET_HEADER_VALUES; - -exports.storeEmail = async (req, res) => - storeDataInGoogleSpreadsheet(req, res, spreadsheet.emailSheetId, EMAIL_SHEET_HEADER_VALUES); diff --git a/signer-service/src/api/controllers/email.controller.ts b/signer-service/src/api/controllers/email.controller.ts new file mode 100644 index 00000000..c943a3dd --- /dev/null +++ b/signer-service/src/api/controllers/email.controller.ts @@ -0,0 +1,26 @@ +import { Request, Response } from 'express'; +import { config } from '../../config/vars'; +import { storeDataInGoogleSpreadsheet } from './googleSpreadSheet.controller'; + +const { spreadsheet } = config; + +const enum EmailSheetHeaders { + Timestamp = 'timestamp', + Email = 'email', + TransactionId = 'transactionId', +} + +const EMAIL_SHEET_HEADER_VALUES = [ + EmailSheetHeaders.Timestamp, + EmailSheetHeaders.Email, + EmailSheetHeaders.TransactionId, +]; + +export { EMAIL_SHEET_HEADER_VALUES }; + +export const storeEmail = async (req: Request, res: Response): Promise => { + if (!spreadsheet.emailSheetId) { + throw new Error('Email sheet ID is not configured'); + } + await storeDataInGoogleSpreadsheet(req, res, spreadsheet.emailSheetId, EMAIL_SHEET_HEADER_VALUES); +}; diff --git a/signer-service/src/api/controllers/googleSpreadSheet.controller.js b/signer-service/src/api/controllers/googleSpreadSheet.controller.js deleted file mode 100644 index acb2a623..00000000 --- a/signer-service/src/api/controllers/googleSpreadSheet.controller.js +++ /dev/null @@ -1,28 +0,0 @@ -require('dotenv').config(); - -const { spreadsheet } = require('../../config/vars'); -const { initGoogleSpreadsheet, getOrCreateSheet, appendData } = require('../services/spreadsheet.service'); - -async function storeDataInGoogleSpreadsheet(req, res, spreadsheetId, sheetHeaderValues) { - try { - // We expect the data to be an object that matches our schema - const data = req.body; - - // Try dumping transactions to spreadsheet - const sheet = await initGoogleSpreadsheet(spreadsheetId, spreadsheet.googleCredentials).then((doc) => - getOrCreateSheet(doc, sheetHeaderValues), - ); - if (sheet) { - console.log('Appending data to sheet'); - await appendData(sheet, data); - return res.status(200).json({ message: 'Data stored successfully' }); - } - - return res.status(500).json({ error: 'Failed to store data. Sheet unavailable.' }); - } catch (error) { - console.error('Error in storeData:', error); - return res.status(500).json({ error: 'Failed to store data', details: error.message }); - } -} - -module.exports = { storeDataInGoogleSpreadsheet }; diff --git a/signer-service/src/api/controllers/googleSpreadSheet.controller.ts b/signer-service/src/api/controllers/googleSpreadSheet.controller.ts new file mode 100644 index 00000000..8a96d146 --- /dev/null +++ b/signer-service/src/api/controllers/googleSpreadSheet.controller.ts @@ -0,0 +1,54 @@ +import 'dotenv/config'; +import { Request, Response } from 'express'; +import { config } from '../../config/vars'; +import { initGoogleSpreadsheet, getOrCreateSheet, appendData } from '../services/spreadsheet.service'; +import { APIError } from '../errors/api-error'; +import { GoogleCredentials } from '../services/spreadsheet.service'; + +type SheetHeaderValues = readonly string[]; + +interface SpreadsheetResponse { + message: string; +} + +interface SpreadsheetErrorResponse { + error: string; + details?: string; +} + +export async function storeDataInGoogleSpreadsheet( + req: Request, + res: Response, + spreadsheetId: string, + sheetHeaderValues: SheetHeaderValues, +): Promise> { + try { + // Ensure credentials are fully defined + const credentials: GoogleCredentials = { + email: config.spreadsheet.googleCredentials.email ?? '', + key: config.spreadsheet.googleCredentials.key ?? '', + }; + + const sheet = await initGoogleSpreadsheet(spreadsheetId, credentials).then((doc) => + getOrCreateSheet(doc, [...sheetHeaderValues]), + ); + + if (!sheet) { + throw new APIError({ + message: 'Failed to store data. Sheet unavailable.', + status: 500, + isPublic: true, + }); + } + + await appendData(sheet, req.body); + return res.status(200).json({ message: 'Data stored successfully' }); + } catch (error) { + console.error('Error in storeData:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + return res.status(500).json({ + error: 'Failed to store data', + details: errorMessage, + }); + } +} diff --git a/signer-service/src/api/controllers/moonbeam.controller.js b/signer-service/src/api/controllers/moonbeam.controller.js deleted file mode 100644 index 2ff22a5c..00000000 --- a/signer-service/src/api/controllers/moonbeam.controller.js +++ /dev/null @@ -1,85 +0,0 @@ -const { createWalletClient, createPublicClient, http, encodeFunctionData } = require('viem'); -const { moonbeam } = require('viem/chains'); -const { privateKeyToAccount } = require('viem/accounts'); -const Big = require('big.js'); - -const { - MOONBEAM_EXECUTOR_PRIVATE_KEY, - 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) => { - const { id, payload } = req.body; - - try { - const moonbeamExecutorAccount = privateKeyToAccount(MOONBEAM_EXECUTOR_PRIVATE_KEY); - - const walletClient = createWalletClient({ - account: moonbeamExecutorAccount, - chain: moonbeam, - transport: http(), - }); - - const publicClient = createPublicClient({ - chain: moonbeam, - transport: http(), - }); - - const data = encodeFunctionData({ - abi: splitReceiverABI, - functionName: 'executeXCM', - args: [id, payload], - }); - - try { - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas(); - const hash = await walletClient.sendTransaction({ - to: MOONBEAM_RECEIVER_CONTRACT_ADDRESS, - value: 0n, - data, - maxFeePerGas, - maxPriorityFeePerGas, - }); - return res.send({ hash }); - } catch (error) { - console.error('Error executing XCM:', error); - res.status(400).send({ error: 'Invalid transaction' }); - } - } catch (error) { - console.error('Error executing XCM:', error); - res.status(500).send({ error: 'Internal Server Error' }); - } -}; - -exports.sendStatusWithPk = async () => { - const slackService = new SlackNotifier(); - let moonbeamExecutorAccount; - - try { - moonbeamExecutorAccount = privateKeyToAccount(MOONBEAM_EXECUTOR_PRIVATE_KEY); - - const publicClient = createPublicClient({ - chain: moonbeam, - transport: http(), - }); - const balance = await publicClient.getBalance({ address: moonbeamExecutorAccount.address }); - - // We are checking if the balance is less than 10 GLMR - 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 }; - } - - 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/controllers/moonbeam.controller.ts b/signer-service/src/api/controllers/moonbeam.controller.ts new file mode 100644 index 00000000..4849b1f5 --- /dev/null +++ b/signer-service/src/api/controllers/moonbeam.controller.ts @@ -0,0 +1,103 @@ +import { createWalletClient, createPublicClient, http, encodeFunctionData, Hash, Address } from 'viem'; +import { moonbeam } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; +import Big from 'big.js'; +import { Request, Response } from 'express'; + +import { + MOONBEAM_EXECUTOR_PRIVATE_KEY, + MOONBEAM_RECEIVER_CONTRACT_ADDRESS, + MOONBEAM_FUNDING_AMOUNT_UNITS, +} from '../../constants/constants'; +import { SlackNotifier } from '../services/slack.service'; +// @ts-ignore +import splitReceiverABI from '../../../../mooncontracts/splitReceiverABI.json'; + +interface XcmPayload { + id: string; + payload: string; +} + +interface StatusResponse { + status: boolean; + public: Address | undefined; +} + +const createClients = (executorAccount: ReturnType) => { + const walletClient = createWalletClient({ + account: executorAccount, + chain: moonbeam, + transport: http(), + }); + + const publicClient = createPublicClient({ + chain: moonbeam, + transport: http(), + }); + + return { walletClient, publicClient }; +}; + +export const executeXcmController = async (req: Request<{}, {}, XcmPayload>, res: Response) => { + const { id, payload } = req.body; + + try { + if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { + throw new Error('Moonbeam executor private key not configured'); + } + const moonbeamExecutorAccount = privateKeyToAccount(MOONBEAM_EXECUTOR_PRIVATE_KEY as `0x${string}`); + const { walletClient, publicClient } = createClients(moonbeamExecutorAccount); + + const data = encodeFunctionData({ + abi: splitReceiverABI, + functionName: 'executeXCM', + args: [id, payload], + }); + + try { + const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas(); + const hash = await walletClient.sendTransaction({ + to: MOONBEAM_RECEIVER_CONTRACT_ADDRESS, + value: 0n, + data, + maxFeePerGas, + maxPriorityFeePerGas, + }); + return res.json({ hash }); + } catch (error) { + console.error('Error executing XCM:', error); + return res.status(400).json({ error: 'Invalid transaction' }); + } + } catch (error) { + console.error('Error executing XCM:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +export const sendStatusWithPk = async (): Promise => { + const slackService = new SlackNotifier(); + let moonbeamExecutorAccount; + + try { + if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { + throw new Error('Moonbeam executor private key not configured'); + } + moonbeamExecutorAccount = privateKeyToAccount(MOONBEAM_EXECUTOR_PRIVATE_KEY as `0x${string}`); + const { publicClient } = createClients(moonbeamExecutorAccount); + + const balance = await publicClient.getBalance({ address: moonbeamExecutorAccount.address }); + const minimumBalance = BigInt(Big(MOONBEAM_FUNDING_AMOUNT_UNITS).times(Big(10).pow(18)).toString()); + + if (balance < minimumBalance) { + await slackService.sendMessage({ + text: `Current balance of funding account is ${balance} GLMR please charge the account ${moonbeamExecutorAccount.address}.`, + }); + return { status: false, 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/controllers/pendulum.controller.js b/signer-service/src/api/controllers/pendulum.controller.js deleted file mode 100644 index 2d9fa6ba..00000000 --- a/signer-service/src/api/controllers/pendulum.controller.js +++ /dev/null @@ -1,32 +0,0 @@ -const { fundEphemeralAccount, sendStatusWithPk } = require('../services/pendulum.service'); - -exports.fundEphemeralAccountController = async (req, res) => { - const { ephemeralAddress } = req.body; - - if (!ephemeralAddress) { - return res.status(400).send({ error: 'Invalid request parameters' }); - } - - try { - const result = await fundEphemeralAccount(ephemeralAddress); - if (result) { - res.send({ status: 'success' }); - } else { - res.status(500).send({ error: 'Funding error' }); - } - } catch (error) { - console.error('Error funding ephemeral account:', error); - res.status(500).send({ error: 'Internal Server Error' }); - } -}; - -exports.sendStatusWithPk = async (req, res, next) => { - try { - const result = await sendStatusWithPk(); - - return res.json(result); - } catch (error) { - console.error('Server error:', error); - return res.status(500).json({ error: 'Server error', details: error.message }); - } -}; diff --git a/signer-service/src/api/controllers/pendulum.controller.ts b/signer-service/src/api/controllers/pendulum.controller.ts new file mode 100644 index 00000000..56cf4be9 --- /dev/null +++ b/signer-service/src/api/controllers/pendulum.controller.ts @@ -0,0 +1,53 @@ +import { Request, Response, NextFunction } from 'express'; +import { fundEphemeralAccount, sendStatusWithPk } from '../services/pendulum/pendulum.service'; + +interface FundEphemeralRequest { + ephemeralAddress: string; +} + +type ApiResponse = + | { + status: 'success'; + data: T; + } + | { + error: string; + details?: string; + }; + +export const fundEphemeralAccountController = async ( + req: Request<{}, {}, FundEphemeralRequest>, + res: Response>, +) => { + const { ephemeralAddress } = req.body; + + if (!ephemeralAddress) { + return res.status(400).send({ error: 'Invalid request parameters' }); + } + + try { + const result = await fundEphemeralAccount(ephemeralAddress); + if (result) { + return res.json({ status: 'success', data: undefined }); + } else { + return res.status(500).send({ error: 'Funding error' }); + } + } catch (error) { + console.error('Error funding ephemeral account:', error); + return res.status(500).send({ error: 'Internal Server Error' }); + } +}; + +export const sendStatusWithPkController = async (_req: Request, res: Response, _next: NextFunction) => { + try { + const result = await sendStatusWithPk(); + return res.json(result); + } catch (err) { + const error = err as Error; + console.error('Server error:', error); + return res.status(500).json({ + error: 'Server error', + details: error.message, + }); + } +}; diff --git a/signer-service/src/api/controllers/quote.controller.js b/signer-service/src/api/controllers/quote.controller.js deleted file mode 100644 index 2cd15c09..00000000 --- a/signer-service/src/api/controllers/quote.controller.js +++ /dev/null @@ -1,52 +0,0 @@ -require('dotenv').config(); - -const alchemyPayService = require('../services/alchemypay.service'); -const transakService = require('../services/transak.service'); -const moonpayService = require('../services/moonpay.service'); - -exports.SUPPORTED_PROVIDERS = ['alchemypay', 'moonpay', 'transak']; - -exports.SUPPORTED_CRYPTO_CURRENCIES = ['usdc', 'usdce', 'usdc.e', 'usdt']; - -exports.SUPPORTED_FIAT_CURRENCIES = ['eur', 'ars']; - -exports.getQuoteForProvider = async (req, res, next) => { - const { provider, fromCrypto, toFiat, amount, network } = req.query; - try { - switch (provider.toLowerCase()) { - case 'alchemypay': - try { - const alchemyPayQuote = await alchemyPayService.getQuoteFor(fromCrypto, toFiat, amount, network); - return res.json(alchemyPayQuote); - } catch (error) { - // AlchemyPay's errors are not very descriptive, so we just return a generic error message - return res.status(500).json({ error: 'Could not get quote from AlchemyPay', details: error.message }); - } - case 'moonpay': - try { - const moonpayQuote = await moonpayService.getQuoteFor(fromCrypto, toFiat, amount); - return res.json(moonpayQuote); - } catch (error) { - if (error.message === 'Token not supported') { - return res.status(404).json({ error: 'Token not supported' }); - } - return res.status(500).json({ error: 'Could not get quote from Moonpay', details: error.message }); - } - case 'transak': - try { - const transakQuote = await transakService.getQuoteFor(fromCrypto, toFiat, amount, network); - return res.json(transakQuote); - } catch (error) { - if (error.message === 'Token not supported') { - return res.status(404).json({ error: 'Token not supported' }); - } - return res.status(500).json({ error: 'Could not get quote from Transak', details: error.message }); - } - default: - return res.status(400).json({ error: 'Invalid provider' }); - } - } catch (error) { - console.error('Server error:', error); - return res.status(500).json({ error: 'Server error', details: error.message }); - } -}; diff --git a/signer-service/src/api/controllers/quote.controller.ts b/signer-service/src/api/controllers/quote.controller.ts new file mode 100644 index 00000000..f78f5d61 --- /dev/null +++ b/signer-service/src/api/controllers/quote.controller.ts @@ -0,0 +1,96 @@ +import { Request, Response, NextFunction } from 'express'; + +import * as alchemyPayService from '../services/alchemypay/alchemypay.service'; +import * as transakService from '../services/transak.service'; +import * as moonpayService from '../services/moonpay.service'; + +export const SUPPORTED_PROVIDERS = ['alchemypay', 'moonpay', 'transak'] as const; +export type Provider = (typeof SUPPORTED_PROVIDERS)[number]; + +export const SUPPORTED_CRYPTO_CURRENCIES = ['usdc', 'usdce', 'usdc.e', 'usdt'] as const; +export type CryptoCurrency = (typeof SUPPORTED_CRYPTO_CURRENCIES)[number]; + +export const SUPPORTED_FIAT_CURRENCIES = ['eur', 'ars'] as const; +export type FiatCurrency = (typeof SUPPORTED_FIAT_CURRENCIES)[number]; + +interface QuoteRequest { + provider: Provider; + fromCrypto: CryptoCurrency; + toFiat: FiatCurrency; + amount: string; + network?: string; +} + +type QuoteHandler = ( + fromCrypto: CryptoCurrency, + toFiat: FiatCurrency, + amount: string, + network?: string, +) => Promise; + +const providerHandlers: Record = { + alchemypay: async (fromCrypto, toFiat, amount, network) => { + try { + return await alchemyPayService.getQuoteFor(fromCrypto, toFiat, amount, network); + } catch (err) { + // AlchemyPay's errors are not very descriptive, so we just return a generic error message + const error = err as Error; + throw new Error(`Could not get quote from AlchemyPay: ${error.message}`); + } + }, + moonpay: async (fromCrypto, toFiat, amount) => { + try { + return await moonpayService.getQuoteFor(fromCrypto, toFiat, amount); + } catch (err) { + const error = err as Error; + throw error.message === 'Token not supported' + ? error + : new Error(`Could not get quote from Moonpay: ${error.message}`); + } + }, + transak: async (fromCrypto, toFiat, amount, network) => { + try { + return await transakService.getQuoteFor(fromCrypto, toFiat, amount, network); + } catch (err) { + const error = err as Error; + throw error.message === 'Token not supported' + ? error + : new Error(`Could not get quote from Transak: ${error.message}`); + } + }, +}; + +const getQuoteFromProvider = async ( + provider: Provider, + fromCrypto: CryptoCurrency, + toFiat: FiatCurrency, + amount: string, + network?: string, +) => { + return await providerHandlers[provider](fromCrypto, toFiat, amount, network); +}; + +export const getQuoteForProvider = async ( + req: Request<{}, {}, {}, QuoteRequest>, + res: Response, + _next: NextFunction, +) => { + const { provider, fromCrypto, toFiat, amount, network } = req.query; + const providerLower = provider.toLowerCase() as Provider; + + try { + if (!providerHandlers[providerLower]) { + return res.status(400).json({ error: 'Invalid provider' }); + } + + const quote = await getQuoteFromProvider(providerLower, fromCrypto, toFiat, amount, network); + return res.json(quote); + } catch (err) { + const error = err as Error; + if (error.message === 'Token not supported') { + return res.status(404).json({ error: 'Token not supported' }); + } + console.error('Server error:', error); + return res.status(500).json({ error: error.message }); + } +}; diff --git a/signer-service/src/api/controllers/rating.controller.js b/signer-service/src/api/controllers/rating.controller.js deleted file mode 100644 index 88301d37..00000000 --- a/signer-service/src/api/controllers/rating.controller.js +++ /dev/null @@ -1,10 +0,0 @@ -const { spreadsheet } = require('../../config/vars'); -const { storeDataInGoogleSpreadsheet } = require('./googleSpreadSheet.controller'); - -// These are the headers for the Google Spreadsheet -const RATING_SHEET_HEADER_VALUES = ['timestamp', 'rating', 'walletAddress']; - -exports.RATING_SHEET_HEADER_VALUES = RATING_SHEET_HEADER_VALUES; - -exports.storeRating = async (req, res) => - storeDataInGoogleSpreadsheet(req, res, spreadsheet.ratingSheetId, RATING_SHEET_HEADER_VALUES); diff --git a/signer-service/src/api/controllers/rating.controller.ts b/signer-service/src/api/controllers/rating.controller.ts new file mode 100644 index 00000000..70c7f631 --- /dev/null +++ b/signer-service/src/api/controllers/rating.controller.ts @@ -0,0 +1,26 @@ +import { Request, Response } from 'express'; +import { config } from '../../config/vars'; +import { storeDataInGoogleSpreadsheet } from './googleSpreadSheet.controller'; + +const { spreadsheet } = config; + +const enum RatingSheetHeaders { + Timestamp = 'timestamp', + Rating = 'rating', + WalletAddress = 'walletAddress', +} + +const RATING_SHEET_HEADER_VALUES = [ + RatingSheetHeaders.Timestamp, + RatingSheetHeaders.Rating, + RatingSheetHeaders.WalletAddress, +]; + +export { RATING_SHEET_HEADER_VALUES }; + +export const storeRating = async (req: Request, res: Response): Promise => { + if (!spreadsheet.ratingSheetId) { + throw new Error('Rating sheet ID is not configured'); + } + await storeDataInGoogleSpreadsheet(req, res, spreadsheet.ratingSheetId, RATING_SHEET_HEADER_VALUES); +}; diff --git a/signer-service/src/api/controllers/siwe.controller.js b/signer-service/src/api/controllers/siwe.controller.js deleted file mode 100644 index ae43ff9f..00000000 --- a/signer-service/src/api/controllers/siwe.controller.js +++ /dev/null @@ -1,44 +0,0 @@ -const { createAndSendNonce, verifyAndStoreSiweMessage } = require('../services/siwe.service'); -const { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } = require('../../constants/constants'); - -exports.sendSiweMessage = async (req, res) => { - const { walletAddress } = req.body; - try { - const { nonce } = await createAndSendNonce(walletAddress); - return res.json({ - nonce, - }); - } catch (e) { - console.error(e); - return res.status(500).json({ error: 'Error while generating nonce' }); - } -}; - -exports.validateSiweSignature = async (req, res) => { - const { nonce, signature, siweMessage } = req.body; - try { - const address = await verifyAndStoreSiweMessage(nonce, signature, siweMessage); - - const token = { - nonce, - signature, - }; - - res.cookie(`authToken_${address}`, token, { - httpOnly: true, - secure: true, - sameSite: 'Strict', - maxAge: DEFAULT_LOGIN_EXPIRATION_TIME_HOURS * 60 * 60 * 1000, - }); - - return res.status(200).json({ message: 'Signature is valid' }); - } catch (e) { - console.error(e); - - if (e.name === 'SiweValidationError') { - return res.status(401).json({ error: `Siwe validation error: ${e.message}` }); - } - - return res.status(500).json({ error: `Could not validate signature: ${e.message}` }); - } -}; diff --git a/signer-service/src/api/controllers/siwe.controller.ts b/signer-service/src/api/controllers/siwe.controller.ts new file mode 100644 index 00000000..35ba801b --- /dev/null +++ b/signer-service/src/api/controllers/siwe.controller.ts @@ -0,0 +1,67 @@ +import { Request, Response } from 'express'; +import { createAndSendNonce, verifyAndStoreSiweMessage } from '../services/siwe.service'; +import { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } from '../../constants/constants'; + +interface SiweRequestBody { + walletAddress?: string; + nonce?: string; + signature?: string; + siweMessage?: string; +} + +type SiweResponse = { + nonce?: string; + message?: string; + error?: string; +}; + +export const sendSiweMessage = async (req: Request, res: Response): Promise> => { + const { walletAddress } = req.body; + + if (!walletAddress) { + return res.status(400).json({ error: 'Wallet address is required' }); + } + + try { + const { nonce } = await createAndSendNonce(walletAddress); + return res.json({ nonce }); + } catch (error) { + console.error('Nonce generation error:', error); + return res.status(500).json({ error: 'Error while generating nonce' }); + } +}; + +export const validateSiweSignature = async ( + req: Request<{}, {}, SiweRequestBody>, + res: Response, +): Promise> => { + const { nonce, signature, siweMessage } = req.body; + + if (!nonce || !signature || !siweMessage) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const address = await verifyAndStoreSiweMessage(nonce, signature, siweMessage); + + const token = { nonce, signature }; + + res.cookie(`authToken_${address}`, token, { + httpOnly: true, + secure: true, + sameSite: 'strict', + maxAge: DEFAULT_LOGIN_EXPIRATION_TIME_HOURS * 60 * 60 * 1000, + }); + + return res.status(200).json({ message: 'Signature is valid' }); + } catch (error) { + console.error('Signature validation error:', error); + + if (error instanceof Error && error.name === 'SiweValidationError') { + return res.status(401).json({ error: `Siwe validation error: ${error.message}` }); + } + + const message = error instanceof Error ? error.message : 'Unknown error'; + return res.status(500).json({ error: `Could not validate signature: ${message}` }); + } +}; diff --git a/signer-service/src/api/controllers/stellar.controller.js b/signer-service/src/api/controllers/stellar.controller.js deleted file mode 100644 index cbf29b4e..00000000 --- a/signer-service/src/api/controllers/stellar.controller.js +++ /dev/null @@ -1,80 +0,0 @@ -require('dotenv').config(); - -const { Keypair } = require('stellar-sdk'); -const { FUNDING_SECRET, SEP10_MASTER_SECRET } = require('../../constants/constants'); - -const { buildCreationStellarTx, buildPaymentAndMergeTx, sendStatusWithPk } = require('../services/stellar.service'); -const { signSep10Challenge } = require('../services/sep10.service'); - -// Derive funding pk -const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET).publicKey(); - -exports.sendStatusWithPk = async (req, res, next) => { - try { - const result = await sendStatusWithPk(); - - return res.json(result); - } catch (error) { - console.error('Server error:', error); - return res.status(500).json({ error: 'Server error', details: error.message }); - } -}; - -exports.createStellarTransaction = async (req, res, next) => { - try { - let { signature, sequence } = await buildCreationStellarTx( - FUNDING_SECRET, - req.body.accountId, - req.body.maxTime, - req.body.assetCode, - req.body.baseFee, - ); - return res.json({ signature, sequence, public: FUNDING_PUBLIC_KEY }); - } catch (error) { - console.error('Error in createStellarTransaction:', error); - return res.status(500).json({ error: 'Failed to create transaction', details: error.message }); - } -}; - -exports.changeOpTransaction = async (req, res, next) => { - try { - let { signature } = await buildPaymentAndMergeTx( - FUNDING_SECRET, - req.body.accountId, - req.body.sequence, - req.body.paymentData, - req.body.maxTime, - req.body.assetCode, - req.body.baseFee, - ); - return res.json({ signature, public: FUNDING_PUBLIC_KEY }); - } catch (error) { - console.error('Error in changeOpTransaction:', error); - return res.status(500).json({ error: 'Failed to process transaction', details: error.message }); - } -}; - -exports.signSep10Challenge = async (req, res, next) => { - try { - let { masterClientSignature, masterClientPublic, clientSignature, clientPublic } = await signSep10Challenge( - req.body.challengeXDR, - req.body.outToken, - req.body.clientPublicKey, - req.derivedMemo, // Derived in middleware - ); - return res.json({ masterClientSignature, masterClientPublic, clientSignature, clientPublic }); - } catch (error) { - console.error('Error in signSep10Challenge:', error); - return res.status(500).json({ error: 'Failed to sign challenge', details: error.message }); - } -}; - -exports.getSep10MasterPK = async (req, res, next) => { - try { - const masterSep10Public = Keypair.fromSecret(SEP10_MASTER_SECRET).publicKey(); - return res.json({ masterSep10Public }); - } catch (error) { - console.error('Error in signSep10Challenge:', error); - return res.status(500).json({ error: 'Failed to sign challenge', details: error.message }); - } -}; diff --git a/signer-service/src/api/controllers/stellar.controller.ts b/signer-service/src/api/controllers/stellar.controller.ts new file mode 100644 index 00000000..fb9d785a --- /dev/null +++ b/signer-service/src/api/controllers/stellar.controller.ts @@ -0,0 +1,123 @@ +import { Request, Response, NextFunction } from 'express'; +import { Keypair } from 'stellar-sdk'; + +import { FUNDING_SECRET, SEP10_MASTER_SECRET } from '../../constants/constants'; +import { OutputTokenType } from './../../../../src/constants/tokenConfig'; +import { signSep10Challenge } from '../services/sep10/sep10.service'; +import { + buildCreationStellarTx, + buildPaymentAndMergeTx, + sendStatusWithPk, + PaymentData, +} from '../services/stellar.service'; + +const FUNDING_PUBLIC_KEY = FUNDING_SECRET ? Keypair.fromSecret(FUNDING_SECRET).publicKey() : ''; + +interface CreateTxRequest { + accountId: string; + maxTime: number; + assetCode: string; + baseFee: string; +} + +interface ChangeOpRequest extends CreateTxRequest { + sequence: string; + paymentData: PaymentData; +} + +interface Sep10Request { + challengeXDR: string; + outToken: OutputTokenType; + clientPublicKey: string; + derivedMemo: string; +} + +export const sendStatusWithPkHandler = async (_: Request, res: Response, next: NextFunction) => { + try { + const result = await sendStatusWithPk(); + return res.json(result); + } catch (error) { + console.error('Server error:', error); + return res.status(500).json({ error: 'Server error', details: (error as Error).message }); + } +}; + +export const createStellarTransactionHandler = async ( + req: Request<{}, {}, CreateTxRequest>, + res: Response, + next: NextFunction, +) => { + try { + if (!FUNDING_SECRET) { + throw new Error('FUNDING_SECRET is not configured'); + } + const { signature, sequence } = await buildCreationStellarTx( + FUNDING_SECRET, + req.body.accountId, + req.body.maxTime, + req.body.assetCode, + req.body.baseFee, + ); + return res.json({ signature, sequence, public: FUNDING_PUBLIC_KEY }); + } catch (error) { + console.error('Error in createStellarTransaction:', error); + return res.status(500).json({ error: 'Failed to create transaction', details: (error as Error).message }); + } +}; + +export const changeOpTransactionHandler = async ( + req: Request<{}, {}, ChangeOpRequest>, + res: Response, + next: NextFunction, +) => { + try { + if (!FUNDING_SECRET) { + throw new Error('FUNDING_SECRET is not configured'); + } + const { signature } = await buildPaymentAndMergeTx( + FUNDING_SECRET, + req.body.accountId, + req.body.sequence, + req.body.paymentData, + req.body.maxTime, + req.body.assetCode, + req.body.baseFee, + ); + return res.json({ signature, public: FUNDING_PUBLIC_KEY }); + } catch (error) { + console.error('Error in changeOpTransaction:', error); + return res.status(500).json({ error: 'Failed to process transaction', details: (error as Error).message }); + } +}; + +export const signSep10ChallengeHandler = async ( + req: Request<{}, {}, Sep10Request>, + res: Response, + next: NextFunction, +) => { + try { + const { masterClientSignature, masterClientPublic, clientSignature, clientPublic } = await signSep10Challenge( + req.body.challengeXDR, + req.body.outToken, + req.body.clientPublicKey, + req.body.derivedMemo, + ); + return res.json({ masterClientSignature, masterClientPublic, clientSignature, clientPublic }); + } catch (error) { + console.error('Error in signSep10Challenge:', error); + return res.status(500).json({ error: 'Failed to sign challenge', details: (error as Error).message }); + } +}; + +export const getSep10MasterPKHandler = async (_: Request, res: Response, next: NextFunction) => { + try { + if (!SEP10_MASTER_SECRET) { + throw new Error('SEP10_MASTER_SECRET is not configured'); + } + const masterSep10Public = Keypair.fromSecret(SEP10_MASTER_SECRET).publicKey(); + return res.json({ masterSep10Public }); + } catch (error) { + console.error('Error in getSep10MasterPK:', error); + return res.status(500).json({ error: 'Failed to get master public key', details: (error as Error).message }); + } +}; diff --git a/signer-service/src/api/controllers/storage.controller.js b/signer-service/src/api/controllers/storage.controller.js deleted file mode 100644 index 1b9310c6..00000000 --- a/signer-service/src/api/controllers/storage.controller.js +++ /dev/null @@ -1,48 +0,0 @@ -const { spreadsheet } = require('../../config/vars'); -const { storeDataInGoogleSpreadsheet } = require('./googleSpreadSheet.controller.js'); - -// These are the headers for the Google Spreadsheet for polygon offramp -const DUMP_SHEET_HEADER_VALUES_EVM = [ - 'timestamp', - 'offramperAddress', - 'stellarEphemeralPublicKey', - 'pendulumEphemeralPublicKey', - 'nablaApprovalTx', - 'nablaSwapTx', - 'spacewalkRedeemTx', - 'stellarOfframpTx', - 'stellarCleanupTx', - 'inputAmount', - 'inputTokenType', - 'outputAmount', - 'outputTokenType', - 'squidRouterReceiverId', - 'squidRouterReceiverHash', -]; - -const DUMP_SHEET_HEADER_VALUES_ASSETHUB = [ - 'timestamp', - 'offramperAddress', - 'stellarEphemeralPublicKey', - 'pendulumEphemeralPublicKey', - 'nablaApprovalTx', - 'nablaSwapTx', - 'spacewalkRedeemTx', - 'stellarOfframpTx', - 'stellarCleanupTx', - 'inputAmount', - 'inputTokenType', - 'outputAmount', - 'outputTokenType', -]; -exports.DUMP_SHEET_HEADER_VALUES_ASSETHUB = DUMP_SHEET_HEADER_VALUES_ASSETHUB; -exports.DUMP_SHEET_HEADER_VALUES_EVM = DUMP_SHEET_HEADER_VALUES_EVM; - -exports.storeData = async (req, res) => { - const sheetHeaderValues = req.body.offramperAddress.includes('0x') - ? DUMP_SHEET_HEADER_VALUES_EVM - : DUMP_SHEET_HEADER_VALUES_ASSETHUB; - console.log(sheetHeaderValues); - - storeDataInGoogleSpreadsheet(req, res, spreadsheet.storageSheetId, sheetHeaderValues); -}; diff --git a/signer-service/src/api/controllers/storage.controller.ts b/signer-service/src/api/controllers/storage.controller.ts new file mode 100644 index 00000000..14d472bf --- /dev/null +++ b/signer-service/src/api/controllers/storage.controller.ts @@ -0,0 +1,63 @@ +import { Request, Response } from 'express'; +import { config } from '../../config/vars'; +import { storeDataInGoogleSpreadsheet } from './googleSpreadSheet.controller'; + +const { spreadsheet } = config; + +type CommonHeaderValues = [ + 'timestamp', + 'offramperAddress', + 'stellarEphemeralPublicKey', + 'pendulumEphemeralPublicKey', + 'nablaApprovalTx', + 'nablaSwapTx', + 'spacewalkRedeemTx', + 'stellarOfframpTx', + 'stellarCleanupTx', + 'inputAmount', + 'inputTokenType', + 'outputAmount', + 'outputTokenType', +]; + +type EVMExtraHeaders = ['squidRouterReceiverId', 'squidRouterReceiverHash']; + +export const DUMP_SHEET_HEADER_VALUES_ASSETHUB: CommonHeaderValues = [ + 'timestamp', + 'offramperAddress', + 'stellarEphemeralPublicKey', + 'pendulumEphemeralPublicKey', + 'nablaApprovalTx', + 'nablaSwapTx', + 'spacewalkRedeemTx', + 'stellarOfframpTx', + 'stellarCleanupTx', + 'inputAmount', + 'inputTokenType', + 'outputAmount', + 'outputTokenType', +] as const; + +export const DUMP_SHEET_HEADER_VALUES_EVM: [...CommonHeaderValues, ...EVMExtraHeaders] = [ + ...DUMP_SHEET_HEADER_VALUES_ASSETHUB, + 'squidRouterReceiverId', + 'squidRouterReceiverHash', +] as const; + +interface StorageRequestBody { + offramperAddress: string; +} + +export const storeData = async (req: Request<{}, {}, StorageRequestBody>, res: Response): Promise => { + if (!spreadsheet.storageSheetId) { + throw new Error('Storage sheet ID is not defined'); + } + + const sheetHeaderValues = req.body.offramperAddress.includes('0x') + ? DUMP_SHEET_HEADER_VALUES_EVM + : DUMP_SHEET_HEADER_VALUES_ASSETHUB; + + console.log(sheetHeaderValues); + + await storeDataInGoogleSpreadsheet(req, res, spreadsheet.storageSheetId, sheetHeaderValues); +}; diff --git a/signer-service/src/api/controllers/subsidize.controller.js b/signer-service/src/api/controllers/subsidize.controller.js deleted file mode 100644 index 61a45362..00000000 --- a/signer-service/src/api/controllers/subsidize.controller.js +++ /dev/null @@ -1,67 +0,0 @@ -const { Keypair } = require('stellar-sdk'); -const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api'); -const Big = require('big.js'); - -const { PENDULUM_WSS, PENDULUM_FUNDING_SEED } = require('../../constants/constants'); - -const { TOKEN_CONFIG, getPaddedAssetCode } = require('../../constants/tokenConfig'); - -exports.subsidizePreSwap = async (req, res) => { - try { - const { address, amountRaw, tokenToSubsidize } = req.body; - console.log('Subsidize pre swap', address, amountRaw, tokenToSubsidize); - - const { pendulumCurrencyId, maximumSubsidyAmountRaw } = TOKEN_CONFIG[tokenToSubsidize.toLowerCase()]; - - if (Big(amountRaw).gt(Big(maximumSubsidyAmountRaw))) { - throw new Error('Amount exceeds maximum subsidy amount'); - } - - const keyring = new Keyring({ type: 'sr25519' }); - const fundingAccountKeypair = keyring.addFromUri(PENDULUM_FUNDING_SEED); - - const wsProvider = new WsProvider(PENDULUM_WSS); - const api = await ApiPromise.create({ provider: wsProvider }); - await api.isReady; - await api.tx.tokens.transfer(address, pendulumCurrencyId, amountRaw).signAndSend(fundingAccountKeypair); - - return res.status(200).json({ message: 'Subsidy transferred successfully' }); - } catch (error) { - console.error('Error in subsidizePreSwap::', error); - return res.status(500).json({ error: 'Server error', details: error.message }); - } -}; - -exports.subsidizePostSwap = async (req, res) => { - try { - const { address, amountRaw, token } = req.body; - console.log('Subsidize post swap', address, amountRaw, token); - - const { assetCode, assetIssuer, maximumSubsidyAmountRaw } = TOKEN_CONFIG[token]; - - if (Big(amountRaw).gt(Big(maximumSubsidyAmountRaw))) { - throw new Error('Amount exceeds maximum subsidy amount'); - } - - const assetIssuerHex = `0x${Keypair.fromPublicKey(assetIssuer).rawPublicKey().toString('hex')}`; - const pendulumCurrencyId = { - Stellar: { - AlphaNum4: { code: getPaddedAssetCode(assetCode), issuer: assetIssuerHex }, - }, - }; - - const keyring = new Keyring({ type: 'sr25519' }); - const fundingAccountKeypair = keyring.addFromUri(PENDULUM_FUNDING_SEED); - - const wsProvider = new WsProvider(PENDULUM_WSS); - const api = await ApiPromise.create({ provider: wsProvider }); - await api.isReady; - - await api.tx.tokens.transfer(address, pendulumCurrencyId, amountRaw).signAndSend(fundingAccountKeypair); - - return res.status(200).json({ message: 'Subsidy transferred successfully' }); - } catch (error) { - console.error('Error in subsidizePreSwap::', error); - return res.status(500).json({ error: 'Server error', details: error.message }); - } -}; diff --git a/signer-service/src/api/controllers/subsidize.controller.ts b/signer-service/src/api/controllers/subsidize.controller.ts new file mode 100644 index 00000000..54a6cacd --- /dev/null +++ b/signer-service/src/api/controllers/subsidize.controller.ts @@ -0,0 +1,117 @@ +import { Keypair } from 'stellar-sdk'; +import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; +import Big from 'big.js'; +import { Request, Response } from 'express'; + +import { PENDULUM_WSS, PENDULUM_FUNDING_SEED } from '../../constants/constants'; +import { + TOKEN_CONFIG, + StellarTokenConfig, + XCMTokenConfig, + getPaddedAssetCode, + isXCMTokenConfig, +} from '../../constants/tokenConfig'; + +interface SubsidizePreSwapRequest { + address: string; + amountRaw: string; + tokenToSubsidize: string; +} + +interface SubsidizePostSwapRequest { + address: string; + amountRaw: string; + token: string; +} + +const initializePendulum = async () => { + const wsProvider = new WsProvider(PENDULUM_WSS); + const api = await ApiPromise.create({ provider: wsProvider }); + await api.isReady; + return api; +}; + +const getFundingAccount = () => { + if (!PENDULUM_FUNDING_SEED) { + throw new Error('PENDULUM_FUNDING_SEED is not configured'); + } + + const keyring = new Keyring({ type: 'sr25519' }); + return keyring.addFromUri(PENDULUM_FUNDING_SEED); +}; + +const validateSubsidyAmount = (amount: string, maxAmount: string) => { + if (Big(amount).gt(Big(maxAmount))) { + throw new Error('Amount exceeds maximum subsidy amount'); + } +}; + +const getPendulumCurrencyConfig = (token: string): StellarTokenConfig | XCMTokenConfig => { + const normalizedToken = token.toLowerCase() as keyof typeof TOKEN_CONFIG; + const config = TOKEN_CONFIG[normalizedToken]; + + if (!config) { + throw new Error(`Unsupported token: ${token}`); + } + + return config; +}; + +export const subsidizePreSwap = async (req: Request<{}, {}, SubsidizePreSwapRequest>, res: Response) => { + try { + const { address, amountRaw, tokenToSubsidize } = req.body; + console.log('Subsidize pre swap', address, amountRaw, tokenToSubsidize); + + const config = getPendulumCurrencyConfig(tokenToSubsidize); + + validateSubsidyAmount(amountRaw, config.maximumSubsidyAmountRaw); + + const fundingAccountKeypair = getFundingAccount(); + const api = await initializePendulum(); + + await api.tx.tokens.transfer(address, config.pendulumCurrencyId, amountRaw).signAndSend(fundingAccountKeypair); + + return res.status(200).json({ message: 'Subsidy transferred successfully' }); + } catch (error) { + console.error('Error in subsidizePreSwap::', error); + return res.status(500).json({ + error: 'Server error', + details: error instanceof Error ? error.message : 'Unknown error', + }); + } +}; + +export const subsidizePostSwap = async (req: Request<{}, {}, SubsidizePostSwapRequest>, res: Response) => { + try { + const { address, amountRaw, token } = req.body; + console.log('Subsidize post swap', address, amountRaw, token); + + const config = getPendulumCurrencyConfig(token); + + validateSubsidyAmount(amountRaw, config.maximumSubsidyAmountRaw); + + if (isXCMTokenConfig(config)) { + throw new Error('Token config must be a Stellar token config'); + } + + const assetIssuerHex = `0x${Keypair.fromPublicKey(config.assetIssuer).rawPublicKey().toString('hex')}`; + const pendulumCurrencyId = { + Stellar: { + AlphaNum4: { code: getPaddedAssetCode(config.assetCode), issuer: assetIssuerHex }, + }, + }; + + const fundingAccountKeypair = getFundingAccount(); + const api = await initializePendulum(); + + await api.tx.tokens.transfer(address, pendulumCurrencyId, amountRaw).signAndSend(fundingAccountKeypair); + + return res.status(200).json({ message: 'Subsidy transferred successfully' }); + } catch (error) { + console.error('Error in subsidizePostSwap::', error); + return res.status(500).json({ + error: 'Server error', + details: error instanceof Error ? error.message : 'Unknown error', + }); + } +}; diff --git a/signer-service/src/api/errors/api-error.js b/signer-service/src/api/errors/api-error.ts similarity index 59% rename from signer-service/src/api/errors/api-error.js rename to signer-service/src/api/errors/api-error.ts index 34f69702..5ec09153 100644 --- a/signer-service/src/api/errors/api-error.js +++ b/signer-service/src/api/errors/api-error.ts @@ -1,18 +1,26 @@ -const httpStatus = require('http-status'); -const ExtendableError = require('./extendable-error'); +import httpStatus from 'http-status'; +import ExtendableError from './extendable-error'; + +interface APIErrorParams { + message: string; + errors?: unknown[]; + stack?: string; + status?: number; + isPublic?: boolean; +} /** * Class representing an API error. * @extends ExtendableError */ -class APIError extends ExtendableError { +export class APIError extends ExtendableError { /** * Creates an API error. * @param {string} message - Error message. * @param {number} status - HTTP status code of error. * @param {boolean} isPublic - Whether the message should be visible to user or not. */ - constructor({ message, errors, stack, status = httpStatus.INTERNAL_SERVER_ERROR, isPublic = false }) { + constructor({ message, errors, stack, status = httpStatus.INTERNAL_SERVER_ERROR, isPublic = false }: APIErrorParams) { super({ message, errors, @@ -22,5 +30,3 @@ class APIError extends ExtendableError { }); } } - -module.exports = APIError; diff --git a/signer-service/src/api/errors/extendable-error.js b/signer-service/src/api/errors/extendable-error.js deleted file mode 100644 index 1b80f415..00000000 --- a/signer-service/src/api/errors/extendable-error.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @extends Error - */ -class ExtendableError extends Error { - constructor({ message, errors, status, isPublic, stack }) { - super(message); - this.name = this.constructor.name; - this.message = message; - this.errors = errors; - this.status = status; - this.isPublic = isPublic; - this.isOperational = true; - this.stack = stack; - // Error.captureStackTrace(this, this.constructor.name); - } -} - -module.exports = ExtendableError; diff --git a/signer-service/src/api/errors/extendable-error.ts b/signer-service/src/api/errors/extendable-error.ts new file mode 100644 index 00000000..87771fd2 --- /dev/null +++ b/signer-service/src/api/errors/extendable-error.ts @@ -0,0 +1,32 @@ +interface ExtendableErrorParams { + message: string; + errors?: unknown[]; + status?: number; + isPublic?: boolean; + stack?: string; +} + +/** + * Base error class that can be extended with additional properties + * @extends Error + */ +class ExtendableError extends Error { + readonly errors?: unknown[]; + readonly status?: number; + readonly isPublic: boolean; + readonly isOperational: boolean; + + constructor({ message, errors, status, isPublic = false, stack }: ExtendableErrorParams) { + super(message); + this.name = this.constructor.name; + this.message = message; + this.errors = errors; + this.status = status; + this.isPublic = isPublic; + this.isOperational = true; + this.stack = stack; + // Error.captureStackTrace(this, this.constructor.name); + } +} + +export default ExtendableError; diff --git a/signer-service/src/api/helpers/anchors.js b/signer-service/src/api/helpers/anchors.js deleted file mode 100644 index ba599ef9..00000000 --- a/signer-service/src/api/helpers/anchors.js +++ /dev/null @@ -1,22 +0,0 @@ -const fetchTomlValues = async (tomlFileUrl) => { - const response = await fetch(tomlFileUrl); - if (response.status !== 200) { - throw new Error(`Failed to fetch TOML file: ${response.statusText}`); - } - - const tomlFileContent = (await response.text()).split('\n'); - const findValueInToml = (key) => { - const keyValue = tomlFileContent.find((line) => line.includes(key)); - return keyValue?.split('=')[1].trim().replaceAll('"', ''); - }; - - return { - signingKey: findValueInToml('SIGNING_KEY'), - webAuthEndpoint: findValueInToml('WEB_AUTH_ENDPOINT'), - sep24Url: findValueInToml('TRANSFER_SERVER_SEP0024'), - sep6Url: findValueInToml('TRANSFER_SERVER'), - kycServer: findValueInToml('KYC_SERVER'), - }; -}; - -module.exports = { fetchTomlValues }; diff --git a/signer-service/src/api/helpers/anchors.ts b/signer-service/src/api/helpers/anchors.ts new file mode 100644 index 00000000..9826ae4f --- /dev/null +++ b/signer-service/src/api/helpers/anchors.ts @@ -0,0 +1,38 @@ +interface TomlValues { + signingKey: string | undefined; + webAuthEndpoint: string | undefined; + sep24Url: string | undefined; + sep6Url: string | undefined; + kycServer: string | undefined; +} + +const TOML_KEYS = { + SIGNING_KEY: 'SIGNING_KEY', + WEB_AUTH_ENDPOINT: 'WEB_AUTH_ENDPOINT', + TRANSFER_SERVER_SEP0024: 'TRANSFER_SERVER_SEP0024', + TRANSFER_SERVER: 'TRANSFER_SERVER', + KYC_SERVER: 'KYC_SERVER', +} as const; + +const fetchTomlValues = async (tomlFileUrl: string): Promise => { + const response = await fetch(tomlFileUrl); + if (!response.ok) { + throw new Error(`Failed to fetch TOML file: ${response.statusText}`); + } + + const tomlFileContent = (await response.text()).split('\n'); + const findValueInToml = (key: string): string | undefined => { + const keyValue = tomlFileContent.find((line) => line.includes(key)); + return keyValue?.split('=')[1]?.trim().replace(/"/g, ''); + }; + + return { + signingKey: findValueInToml(TOML_KEYS.SIGNING_KEY), + webAuthEndpoint: findValueInToml(TOML_KEYS.WEB_AUTH_ENDPOINT), + sep24Url: findValueInToml(TOML_KEYS.TRANSFER_SERVER_SEP0024), + sep6Url: findValueInToml(TOML_KEYS.TRANSFER_SERVER), + kycServer: findValueInToml(TOML_KEYS.KYC_SERVER), + }; +}; + +export { fetchTomlValues, type TomlValues }; diff --git a/signer-service/src/api/helpers/memoDerivation.js b/signer-service/src/api/helpers/memoDerivation.js deleted file mode 100644 index 59f9292f..00000000 --- a/signer-service/src/api/helpers/memoDerivation.js +++ /dev/null @@ -1,21 +0,0 @@ -const { keccak256 } = require('viem/utils'); -const { Keyring } = require('@polkadot/api'); - -// Returns the hash value for the address. If it's a polkadot address, it will return raw data of the address. -function getHashValueForAddress(address) { - if (address.startsWith('0x')) { - return address; - } else { - const keyring = new Keyring({ type: 'sr25519' }); - return keyring.decodeAddress(address); - } -} - -//A memo derivation. -async function deriveMemoFromAddress(address) { - const hashValue = getHashValueForAddress(address); - const hash = keccak256(hashValue); - return BigInt(hash).toString().slice(0, 15); -} - -module.exports = { deriveMemoFromAddress }; diff --git a/signer-service/src/api/helpers/memoDerivation.ts b/signer-service/src/api/helpers/memoDerivation.ts new file mode 100644 index 00000000..06e3607e --- /dev/null +++ b/signer-service/src/api/helpers/memoDerivation.ts @@ -0,0 +1,31 @@ +import { keccak256 } from 'viem/utils'; +import { Keyring } from '@polkadot/api'; + +type Address = `0x${string}` | string; +type HashValue = `0x${string}` | Uint8Array; + +/** + * Returns the hash value for the address. + * For Ethereum addresses, returns the address itself. + * For Polkadot addresses, returns the raw bytes of the decoded address. + */ +function getHashValueForAddress(address: Address): HashValue { + if (address.startsWith('0x')) { + return address as `0x${string}`; + } + + const keyring = new Keyring({ type: 'sr25519' }); + return keyring.decodeAddress(address); +} + +/** + * Derives a 15-digit memo from an address by: + * 1. Getting a hash value for the address + * 2. Computing keccak256 hash + * 3. Converting to decimal string and taking first 15 digits + */ +export async function deriveMemoFromAddress(address: Address): Promise { + const hashValue = getHashValueForAddress(address); + const hash = keccak256(hashValue); + return BigInt(hash).toString().slice(0, 15); +} diff --git a/signer-service/src/api/helpers/siweMessageFormatter.js b/signer-service/src/api/helpers/siweMessageFormatter.js deleted file mode 100644 index 86ca8804..00000000 --- a/signer-service/src/api/helpers/siweMessageFormatter.js +++ /dev/null @@ -1,53 +0,0 @@ -class SignInMessage { - // fixed statement string - static LOGIN_MESSAGE = ' wants you to sign in with your account: '; - - constructor(fields) { - this.scheme = fields.scheme; - this.domain = fields.domain; - this.address = fields.address; - this.nonce = fields.nonce; - this.expirationTime = new Date(fields.expirationTime).toISOString(); - this.issuedAt = fields.issuedAt ? new Date(fields.issuedAt).toISOString() : new Date().toISOString(); - } - - toMessage() { - const header = `${this.domain}${SignInMessage.LOGIN_MESSAGE}${this.address}`; - - const body = `\nNonce: ${this.nonce}\nIssued At: ${this.issuedAt}\nExpiration Time: ${this.expirationTime}`; - - return `${header}\n\n${body}`; - } - - static fromMessage(message) { - const lines = message - .split('\n') - .map((l) => l.trim()) - .filter((l) => l.length > 0); - - const headerLine = lines.find((line) => line.includes(SignInMessage.LOGIN_MESSAGE)) || ''; - const [domain, address] = headerLine.split(SignInMessage.LOGIN_MESSAGE).map((part) => part.trim()); - - const nonceLine = lines.find((line) => line.startsWith('Nonce:')) || ''; - const nonce = nonceLine.split('Nonce:')[1]?.trim() || ''; - - const issuedAtLine = lines.find((line) => line.startsWith('Issued At:')) || ''; - const issuedAt = issuedAtLine.split('Issued At:')[1]?.trim(); // Can't really be empty. Constructor will default to current date if not defined. - const issuedAtMilis = new Date(issuedAt).getTime(); - - const expirationTimeLine = lines.find((line) => line.startsWith('Expiration Time:')) || ''; - const expirationTime = expirationTimeLine.split('Expiration Time:')[1]?.trim(); - const expirationTimeMilis = new Date(expirationTime).getTime(); - - return new SignInMessage({ - scheme: 'https', - domain, - address, - nonce, - expirationTime: expirationTimeMilis, - issuedAt: issuedAtMilis, - }); - } -} - -module.exports = { SignInMessage }; diff --git a/signer-service/src/api/helpers/siweMessageFormatter.ts b/signer-service/src/api/helpers/siweMessageFormatter.ts new file mode 100644 index 00000000..2a0ddf03 --- /dev/null +++ b/signer-service/src/api/helpers/siweMessageFormatter.ts @@ -0,0 +1,65 @@ +interface SignInMessageFields { + scheme: string; + domain: string; + address: string; + nonce: string; + expirationTime: number | string; + issuedAt?: number | string; +} + +class SignInMessage { + readonly scheme: string; + readonly domain: string; + readonly address: string; + readonly nonce: string; + readonly expirationTime: string; + readonly issuedAt: string; + + // fixed statement string + static readonly LOGIN_MESSAGE = ' wants you to sign in with your account: '; + + constructor(fields: SignInMessageFields) { + this.scheme = fields.scheme; + this.domain = fields.domain; + this.address = fields.address; + this.nonce = fields.nonce; + this.expirationTime = new Date(fields.expirationTime).toISOString(); + this.issuedAt = fields.issuedAt ? new Date(fields.issuedAt).toISOString() : new Date().toISOString(); + } + + toMessage(): string { + const header = `${this.domain}${SignInMessage.LOGIN_MESSAGE}${this.address}`; + const body = `\nNonce: ${this.nonce}\nIssued At: ${this.issuedAt}\nExpiration Time: ${this.expirationTime}`; + return `${header}\n\n${body}`; + } + + static fromMessage(message: string): SignInMessage { + const lines = message + .split('\n') + .map((line: string) => line.trim()) + .filter(Boolean); + + const headerLine = lines.find((line) => line.includes(SignInMessage.LOGIN_MESSAGE)) ?? ''; + const [domain, address] = headerLine.split(SignInMessage.LOGIN_MESSAGE).map((part) => part.trim()); + + const getValue = (prefix: string): string => { + const line = lines.find((l) => l.startsWith(prefix)) ?? ''; + return line.split(`${prefix}:`)[1]?.trim() ?? ''; + }; + + const nonce = getValue('Nonce'); + const issuedAt = getValue('Issued At'); + const expirationTime = getValue('Expiration Time'); + + return new SignInMessage({ + scheme: 'https', + domain, + address, + nonce, + expirationTime: new Date(expirationTime).getTime(), + issuedAt: new Date(issuedAt).getTime(), + }); + } +} + +export { SignInMessage, type SignInMessageFields }; diff --git a/signer-service/src/api/middlewares/auth.js b/signer-service/src/api/middlewares/auth.js deleted file mode 100644 index 614eab48..00000000 --- a/signer-service/src/api/middlewares/auth.js +++ /dev/null @@ -1,77 +0,0 @@ -const { validateSignatureAndGetMemo } = require('../services/siwe.service'); - -const getMemoFromCookiesMiddleware = async (req, res, next) => { - // If the client didn't specify, we don't want to pass a derived memo even if a cookie was sent. - - req.derivedMemo = null; // Explicit overwrite to avoid tampering, defensive. - if (!Boolean(req.body.usesMemo)) { - return next(); - } - try { - const cookies = req.cookies; - const address = req.body.address; - // Default memo (represents no memo usage at all) - let resultMemo = null; - - for (const authToken in cookies) { - if (!authToken.startsWith('authToken_')) { - continue; - } - - //check if matches the address requested by client, otherwise ignore cookie. - if (!authToken.includes(address)) { - continue; - } - - try { - const token = cookies[authToken]; - const signature = token.signature; - const nonce = token.nonce; - - if (!signature || !nonce) { - continue; - } - - const memo = await validateSignatureAndGetMemo(nonce, signature); - console.log(memo); - - // First found first used - if (memo) { - resultMemo = memo; - break; - } - } catch (e) { - continue; - } - } - - // Client declared usage of memo, but it could not be derived from provided signatures. - if (Boolean(req.body.usesMemo) && !resultMemo) { - return res.status(401).json({ - error: 'Missing or invalid authentication token', - }); - } - - req.derivedMemo = resultMemo; - - next(); - } catch (err) { - if (err.message.includes('Could not verify signature')) { - // Distinguish between failed signature check and other errors. - return res.status(401).json({ - error: 'Signature validation failed.', - details: err.message, - }); - } - console.error(`Error in getMemoFromCookiesMiddleware: ${err.message}`); - - return res.status(500).json({ - error: 'Error while verifying signature', - details: err.message, - }); - } -}; - -module.exports = { - getMemoFromCookiesMiddleware, -}; diff --git a/signer-service/src/api/middlewares/auth.ts b/signer-service/src/api/middlewares/auth.ts new file mode 100644 index 00000000..33fbb28e --- /dev/null +++ b/signer-service/src/api/middlewares/auth.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from 'express'; +import { validateSignatureAndGetMemo } from '../services/siwe.service'; + +interface AuthToken { + signature: string; + nonce: string; +} + +interface RequestWithMemo extends Request { + derivedMemo: string | null; + body: { + usesMemo?: boolean; + address: string; + }; + cookies: { + [key: string]: AuthToken; + }; +} + +async function getMemoFromCookiesMiddleware(req: RequestWithMemo, res: Response, next: NextFunction) { + req.derivedMemo = null; + + if (!req.body.usesMemo) { + return next(); + } + + try { + const { + cookies, + body: { address }, + } = req; + + const cookieKey = `authToken_${address}`; + const authToken = cookies[cookieKey]; + + if (!authToken?.signature || !authToken?.nonce) { + return res.status(401).json({ + error: 'Missing or invalid authentication token', + }); + } + + const memo = await validateSignatureAndGetMemo(authToken.nonce, authToken.signature); + + if (!memo) { + return res.status(401).json({ + error: 'Missing or invalid authentication token', + }); + } + + req.derivedMemo = memo; + next(); + } catch (error) { + const err = error as Error; + if (err.message.includes('Could not verify signature')) { + return res.status(401).json({ + error: 'Signature validation failed.', + details: err.message, + }); + } + + console.error(`Error in getMemoFromCookiesMiddleware: ${err.message}`); + return res.status(500).json({ + error: 'Error while verifying signature', + details: err.message, + }); + } +} + +export { getMemoFromCookiesMiddleware }; diff --git a/signer-service/src/api/middlewares/error.js b/signer-service/src/api/middlewares/error.js deleted file mode 100644 index 4937e023..00000000 --- a/signer-service/src/api/middlewares/error.js +++ /dev/null @@ -1,62 +0,0 @@ -const httpStatus = require('http-status'); -const expressValidation = require('express-validation'); -const APIError = require('../errors/api-error'); -const { env } = require('../../config/vars'); - -/** - * Error handler. Send stacktrace only during development - * @public - */ -const handler = (err, req, res, next) => { - const response = { - code: err.status, - message: err.message || httpStatus[err.status], - errors: err.errors, - stack: err.stack, - }; - - if (env !== 'development') { - delete response.stack; - } - - res.status(err.status); - res.json(response); -}; -exports.handler = handler; - -/** - * If error is not an instanceOf APIError, convert it. - * @public - */ -exports.converter = (err, req, res, next) => { - let convertedError = err; - - if (err instanceof expressValidation.ValidationError) { - convertedError = new APIError({ - message: 'Validation Error', - errors: err.errors, - status: err.status, - stack: err.stack, - }); - } else if (!(err instanceof APIError)) { - convertedError = new APIError({ - message: err.message, - status: err.status, - stack: err.stack, - }); - } - - return handler(convertedError, req, res); -}; - -/** - * Catch 404 and forward to error handler - * @public - */ -exports.notFound = (req, res, next) => { - const err = new APIError({ - message: 'Not found', - status: httpStatus.NOT_FOUND, - }); - return handler(err, req, res); -}; diff --git a/signer-service/src/api/middlewares/error.ts b/signer-service/src/api/middlewares/error.ts new file mode 100644 index 00000000..246f3ae1 --- /dev/null +++ b/signer-service/src/api/middlewares/error.ts @@ -0,0 +1,78 @@ +import httpStatus from 'http-status'; +import { ValidationError } from 'express-validation'; +import { Request, Response, NextFunction } from 'express'; + +import { APIError } from '../errors/api-error'; +import { config } from '../../config/vars'; + +const { env } = config; + +interface ErrorResponse { + code: number; + message: string; + errors?: unknown[]; + stack?: string; +} + +/** + * Error handler. Send stacktrace only during development + * @public + */ +const handler = (err: APIError | Error, req: Request, res: Response, next: NextFunction): void => { + const apiError = err as APIError; + const response: ErrorResponse = { + code: apiError.status || httpStatus.INTERNAL_SERVER_ERROR, + message: apiError.message || httpStatus[httpStatus.INTERNAL_SERVER_ERROR], + errors: apiError.errors, + stack: err.stack, + }; + + if (env !== 'development') { + delete response.stack; + } + + res.status(apiError.status || httpStatus.INTERNAL_SERVER_ERROR); + res.json(response); +}; + +export { handler }; + +/** + * If error is not an instanceOf APIError, convert it. + * @public + */ +export const converter = (err: Error | ValidationError, req: Request, res: Response, next: NextFunction): void => { + let convertedError: APIError; + + if (err instanceof ValidationError) { + convertedError = new APIError({ + message: 'Validation Error', + errors: err.errors, + status: err.status, + // @ts-ignore + stack: err.stack, + }); + } else if (!(err instanceof APIError)) { + convertedError = new APIError({ + message: err.message, + status: (err as any).status || httpStatus.INTERNAL_SERVER_ERROR, + stack: err.stack, + }); + } else { + convertedError = err; + } + + return handler(convertedError, req, res, next); +}; + +/** + * Catch 404 and forward to error handler + * @public + */ +export const notFound = (req: Request, res: Response, next: NextFunction): void => { + const err = new APIError({ + message: 'Not found', + status: httpStatus.NOT_FOUND, + }); + return handler(err, req, res, next); +}; diff --git a/signer-service/src/api/middlewares/validators.js b/signer-service/src/api/middlewares/validators.ts similarity index 54% rename from signer-service/src/api/middlewares/validators.js rename to signer-service/src/api/middlewares/validators.ts index 51d876a7..ffea522a 100644 --- a/signer-service/src/api/middlewares/validators.js +++ b/signer-service/src/api/middlewares/validators.ts @@ -1,18 +1,64 @@ -const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); -const { EMAIL_SHEET_HEADER_VALUES } = require('../controllers/email.controller'); -const { RATING_SHEET_HEADER_VALUES } = require('../controllers/rating.controller'); -const { - DUMP_SHEET_HEADER_VALUES_ASSETHUB, - DUMP_SHEET_HEADER_VALUES_EVM, -} = require('../controllers/storage.controller'); -const { +import { Request, Response, NextFunction } from 'express'; +import { isStellarTokenConfig, TOKEN_CONFIG, TokenConfig } from '../../constants/tokenConfig'; +import { EMAIL_SHEET_HEADER_VALUES } from '../controllers/email.controller'; +import { RATING_SHEET_HEADER_VALUES } from '../controllers/rating.controller'; +import { DUMP_SHEET_HEADER_VALUES_ASSETHUB, DUMP_SHEET_HEADER_VALUES_EVM } from '../controllers/storage.controller'; +import { SUPPORTED_PROVIDERS, SUPPORTED_CRYPTO_CURRENCIES, SUPPORTED_FIAT_CURRENCIES, -} = require('../controllers/quote.controller'); + Provider, + FiatCurrency, + CryptoCurrency, +} from '../controllers/quote.controller'; + +interface CreationBody { + accountId: string; + maxTime: number; + assetCode: string; + baseFee: string; +} + +interface QuoteQuery { + provider: Provider; + fromCrypto: CryptoCurrency; + toFiat: FiatCurrency; + amount: string; + network?: string; +} + +interface ChangeOpBody extends CreationBody { + sequence: string; + paymentData: unknown; +} + +interface SwapBody { + amountRaw: string; + address: string; + token?: keyof TokenConfig; +} + +interface Sep10Body { + challengeXDR: string; + outToken: string; + clientPublicKey: string; +} + +interface SiweCreateBody { + walletAddress: string; +} + +interface SiweValidateBody { + nonce: string; + signature: string; + siweMessage: string; +} + +type ValidatorFn = (req: Request, res: Response, next: NextFunction) => void | Response; + +const validateCreationInput: ValidatorFn = (req, res, next) => { + const { accountId, maxTime, assetCode, baseFee } = req.body as CreationBody; -const validateCreationInput = (req, res, next) => { - const { accountId, maxTime, assetCode, baseFee } = req.body; if (!accountId || !maxTime) { return res.status(400).json({ error: 'Missing accountId or maxTime parameter' }); } @@ -31,25 +77,25 @@ const validateCreationInput = (req, res, next) => { next(); }; -const validateQuoteInput = (req, res, next) => { - const { provider, fromCrypto, toFiat, amount, network } = req.query; +const validateQuoteInput: ValidatorFn = (req, res, next) => { + const { provider, fromCrypto, toFiat, amount, network } = req.query as unknown as QuoteQuery; - if (!provider || SUPPORTED_PROVIDERS.indexOf(provider.toLowerCase()) === -1) { + if (!provider || !SUPPORTED_PROVIDERS.includes(provider.toLowerCase() as Provider)) { return res .status(400) - .json({ error: 'Invalid provider. Supported providers are: ' + SUPPORTED_PROVIDERS.join(', ') }); + .json({ error: `Invalid provider. Supported providers are: ${SUPPORTED_PROVIDERS.join(', ')}` }); } - if (!fromCrypto || SUPPORTED_CRYPTO_CURRENCIES.indexOf(fromCrypto.toLowerCase()) === -1) { + if (!fromCrypto || !SUPPORTED_CRYPTO_CURRENCIES.includes(fromCrypto.toLowerCase() as CryptoCurrency)) { return res .status(400) - .json({ error: 'Invalid fromCrypto. Supported currencies are: ' + SUPPORTED_CRYPTO_CURRENCIES.join(', ') }); + .json({ error: `Invalid fromCrypto. Supported currencies are: ${SUPPORTED_CRYPTO_CURRENCIES.join(', ')}` }); } - if (!toFiat || SUPPORTED_FIAT_CURRENCIES.indexOf(toFiat.toLowerCase()) === -1) { + if (!toFiat || !SUPPORTED_FIAT_CURRENCIES.includes(toFiat.toLowerCase() as FiatCurrency)) { return res .status(400) - .json({ error: 'Invalid toFiat. Supported currencies are: ' + SUPPORTED_FIAT_CURRENCIES.join(', ') }); + .json({ error: `Invalid toFiat. Supported currencies are: ${SUPPORTED_FIAT_CURRENCIES.join(', ')}` }); } if (!amount) { @@ -67,8 +113,9 @@ const validateQuoteInput = (req, res, next) => { next(); }; -const validateChangeOpInput = (req, res, next) => { - const { accountId, sequence, paymentData, maxTime, assetCode, baseFee } = req.body; +const validateChangeOpInput: ValidatorFn = (req, res, next) => { + const { accountId, sequence, paymentData, maxTime, assetCode, baseFee } = req.body as ChangeOpBody; + if (!accountId || !sequence || !paymentData || !maxTime) { return res.status(400).json({ error: 'Missing required parameters' }); } @@ -87,9 +134,9 @@ const validateChangeOpInput = (req, res, next) => { next(); }; -const validateRequestBodyValuesForTransactionStore = () => (req, res, next) => { - const data = req.body; - const offramperAddress = data.offramperAddress; +const validateRequestBodyValuesForTransactionStore = (): ValidatorFn => (req, res, next) => { + const { offramperAddress } = req.body; + if (!offramperAddress) { return res.status(400).json({ error: 'Missing offramperAddress parameter' }); } @@ -101,26 +148,28 @@ const validateRequestBodyValuesForTransactionStore = () => (req, res, next) => { validateRequestBodyValues(requiredRequestBodyKeys)(req, res, next); }; -const validateRequestBodyValues = (requiredRequestBodyKeys) => (req, res, next) => { - const data = req.body; +const validateRequestBodyValues = + (requiredRequestBodyKeys: string[]): ValidatorFn => + (req, res, next) => { + const data = req.body; - if (!requiredRequestBodyKeys.every((key) => data[key])) { - const missingItems = requiredRequestBodyKeys.filter((key) => !data[key]); - const errorMessage = 'Request body data does not match schema. Missing items: ' + missingItems.join(', '); - console.error(errorMessage); - return res.status(400).json({ error: errorMessage }); - } + if (!requiredRequestBodyKeys.every((key) => data[key])) { + const missingItems = requiredRequestBodyKeys.filter((key) => !data[key]); + const errorMessage = `Request body data does not match schema. Missing items: ${missingItems.join(', ')}`; + console.error(errorMessage); + return res.status(400).json({ error: errorMessage }); + } - next(); -}; + next(); + }; const validateStorageInput = validateRequestBodyValuesForTransactionStore(); const validateEmailInput = validateRequestBodyValues(EMAIL_SHEET_HEADER_VALUES); const validateRatingInput = validateRequestBodyValues(RATING_SHEET_HEADER_VALUES); const validateExecuteXCM = validateRequestBodyValues(['id', 'payload']); -const validatePreSwapSubsidizationInput = (req, res, next) => { - const { amountRaw, address } = req.body; +const validatePreSwapSubsidizationInput: ValidatorFn = (req, res, next) => { + const { amountRaw, address } = req.body as SwapBody; if (amountRaw === undefined) { return res.status(400).json({ error: 'Missing "amountRaw" parameter' }); @@ -137,8 +186,8 @@ const validatePreSwapSubsidizationInput = (req, res, next) => { next(); }; -const validatePostSwapSubsidizationInput = (req, res, next) => { - const { amountRaw, address, token } = req.body; +const validatePostSwapSubsidizationInput: ValidatorFn = (req, res, next) => { + const { amountRaw, address, token } = req.body as Required; if (amountRaw === undefined) { return res.status(400).json({ error: 'Missing "amountRaw" parameter' }); @@ -157,15 +206,16 @@ const validatePostSwapSubsidizationInput = (req, res, next) => { } const tokenConfig = TOKEN_CONFIG[token]; - if (tokenConfig === undefined || tokenConfig.assetCode === undefined || tokenConfig.assetIssuer === undefined) { - return res.status(400).json({ error: 'Invalid "token" parameter' }); + if (!isStellarTokenConfig(tokenConfig)) { + return res.status(400).json({ error: 'Invalid "token" parameter - must be a Stellar token' }); } next(); }; -const validateSep10Input = (req, res, next) => { - const { challengeXDR, outToken, clientPublicKey } = req.body; +const validateSep10Input: ValidatorFn = (req, res, next) => { + const { challengeXDR, outToken, clientPublicKey } = req.body as Sep10Body; + if (!challengeXDR) { return res.status(400).json({ error: 'Missing Anchor challenge: challengeXDR' }); } @@ -180,16 +230,18 @@ const validateSep10Input = (req, res, next) => { next(); }; -const validateSiweCreate = (req, res, next) => { - const { walletAddress } = req.body; +const validateSiweCreate: ValidatorFn = (req, res, next) => { + const { walletAddress } = req.body as SiweCreateBody; + if (!walletAddress) { return res.status(400).json({ error: 'Missing param: walletAddress' }); } next(); }; -const validateSiweValidate = (req, res, next) => { - const { nonce, signature, siweMessage } = req.body; +const validateSiweValidate: ValidatorFn = (req, res, next) => { + const { nonce, signature, siweMessage } = req.body as SiweValidateBody; + if (!signature) { return res.status(400).json({ error: 'Missing param: signature' }); } @@ -205,7 +257,7 @@ const validateSiweValidate = (req, res, next) => { next(); }; -module.exports = { +export { validateChangeOpInput, validateQuoteInput, validateCreationInput, diff --git a/signer-service/src/api/services/alchemypay.service.js b/signer-service/src/api/services/alchemypay.service.js deleted file mode 100644 index cbd1b1a7..00000000 --- a/signer-service/src/api/services/alchemypay.service.js +++ /dev/null @@ -1,221 +0,0 @@ -const crypto = require('crypto'); -const { quoteProviders } = require('../../config/vars'); - -function apiSign(timestamp, method, requestUrl, body, secretKey) { - const content = timestamp + method.toUpperCase() + getPath(requestUrl) + getJsonBody(body); - return crypto.createHmac('sha256', secretKey).update(content).digest('base64'); -} - -function getPath(requestUrl) { - const uri = new URL(requestUrl); - const path = uri.pathname; - const params = Array.from(uri.searchParams.entries()); - - if (params.length === 0) { - return path; - } else { - const sortedParams = [...params].sort(([aKey], [bKey]) => aKey.localeCompare(bKey)); - const queryString = sortedParams.map(([key, value]) => `${key}=${value}`).join('&'); - return `${path}?${queryString}`; - } -} - -function getJsonBody(body) { - let map; - - try { - map = JSON.parse(body); - } catch (error) { - map = {}; - console.error("Couldn't parse JSON body", error); - } - - if (Object.keys(map).length === 0) { - return ''; - } - - map = removeEmptyKeys(map); - map = sortObject(map); - - return JSON.stringify(map); -} - -function removeEmptyKeys(map) { - const retMap = {}; - - for (const [key, value] of Object.entries(map)) { - if (value !== null && value !== '') { - retMap[key] = value; - } - } - - return retMap; -} - -function sortObject(obj) { - if (typeof obj === 'object') { - if (Array.isArray(obj)) { - return sortList(obj); - } else { - return sortMap(obj); - } - } - - return obj; -} - -function sortMap(map) { - const sortedMap = new Map(Object.entries(removeEmptyKeys(map)).sort(([aKey], [bKey]) => aKey.localeCompare(bKey))); - - for (const [key, value] of sortedMap.entries()) { - if (typeof value === 'object') { - sortedMap.set(key, sortObject(value)); - } - } - - return Object.fromEntries(sortedMap.entries()); -} - -function sortList(list) { - const objectList = []; - const intList = []; - const floatList = []; - const stringList = []; - const jsonArray = []; - - for (const item of list) { - if (typeof item === 'object') { - jsonArray.push(item); - } else if (Number.isInteger(item)) { - intList.push(item); - } else if (typeof item === 'number') { - floatList.push(item); - } else if (typeof item === 'string') { - stringList.push(item); - } else { - intList.push(item); - } - } - - intList.sort((a, b) => a - b); - floatList.sort((a, b) => a - b); - stringList.sort(); - - objectList.push(...intList, ...floatList, ...stringList, ...jsonArray); - list.length = 0; - list.push(...objectList); - - const retList = []; - - for (const item of list) { - if (typeof item === 'object') { - retList.push(sortObject(item)); - } else { - retList.push(item); - } - } - - return retList; -} - -// See https://alchemypay.readme.io/docs/price-query -function priceQuery(crypto, fiat, amount, network, side) { - const { secretKey, baseUrl, appId } = quoteProviders.alchemyPay; - const httpMethod = 'POST'; - const requestPath = '/open/api/v4/merchant/order/quote'; - const requestUrl = baseUrl + requestPath; - const timestamp = String(Date.now()); - - const bodyString = JSON.stringify({ - crypto, - network, - fiat, - amount, - side, - }); - // It's important to sort the body before signing. It's also important for the POST request to have the body sorted. - const sortedBody = getJsonBody(bodyString); - - const signature = apiSign(timestamp, httpMethod, requestUrl, sortedBody, secretKey.trim()); - - const headers = { - 'Content-Type': 'application/json', - appId, - timestamp, - sign: signature, - }; - - const request = { - method: 'POST', - headers, - body: sortedBody, - }; - - return fetch(requestUrl, request) - .then(async (response) => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const body = await response.json(); - if (!body.success) { - throw new Error( - `Could not get quote for ${crypto} to ${fiat} from AlchemyPay: ` + body.returnMsg || 'Unknown error', - ); - } - - const { cryptoPrice, rampFee, networkFee, fiatQuantity } = body.data; - - const totalFee = (Number(rampFee) || 0) + (Number(networkFee) || 0); - // According to a comment in the response sample [here](https://alchemypay.readme.io/docs/price-query#response-sample) - // the `fiatQuantity` does not yet include the fees so we need to subtract them. - const fiatAmount = Number(fiatQuantity) - totalFee; - - return { - cryptoPrice: Number(cryptoPrice), - cryptoAmount: Number(amount), - fiatAmount, - totalFee, - }; - }) - .then((data) => { - if (data.error) { - throw new Error(data.error); - } - return data; - }); -} - -// see https://alchemypay.readme.io/docs/network-code -function getAlchemyPayNetworkCode(network) { - switch (network.toUpperCase()) { - case 'POLYGON': - return 'MATIC'; - default: - return network; - } -} - -function getCryptoCurrencyCode(fromCrypto) { - if (fromCrypto.toLowerCase() === 'usdc') { - return 'USDC'; - } else if (fromCrypto.toLowerCase() === 'usdce' || fromCrypto.toLowerCase() === 'usdc.e') { - return 'USDC.e'; - } else if (fromCrypto.toLowerCase() === 'usdt') { - return 'USDT'; - } - - // The currencies need to be in uppercase - return fromCrypto.toUpperCase(); -} - -function getFiatCode(toFiat) { - // The currencies need to be in uppercase - return toFiat.toUpperCase(); -} - -exports.getQuoteFor = (fromCrypto, toFiat, amount, network) => { - const networkCode = getAlchemyPayNetworkCode(network); - const side = 'SELL'; - - return priceQuery(getCryptoCurrencyCode(fromCrypto), getFiatCode(toFiat), amount, networkCode, side); -}; diff --git a/signer-service/src/api/services/alchemypay/alchemypay.service.ts b/signer-service/src/api/services/alchemypay/alchemypay.service.ts new file mode 100644 index 00000000..8ba50750 --- /dev/null +++ b/signer-service/src/api/services/alchemypay/alchemypay.service.ts @@ -0,0 +1,171 @@ +import crypto from 'node:crypto'; +import { config } from '../../../config/vars'; +import { removeEmptyKeys, sortObject } from './helpers'; + +const { quoteProviders } = config; + +interface AlchemyPayQuote { + cryptoPrice: number; + cryptoAmount: number; + fiatAmount: number; + totalFee: number; +} + +interface AlchemyPayResponse { + success: boolean; + returnMsg?: string; + data: { + cryptoPrice: string; + rampFee: string; + networkFee: string; + fiatQuantity: string; + }; +} + +function apiSign(timestamp: string, method: string, requestUrl: string, body: string, secretKey: string): string { + const content = timestamp + method.toUpperCase() + getPath(requestUrl) + getJsonBody(body); + return crypto.createHmac('sha256', secretKey).update(content).digest('base64'); +} + +function getPath(requestUrl: string): string { + const uri = new URL(requestUrl); + const path = uri.pathname; + const params = Array.from(uri.searchParams.entries()); + + if (params.length === 0) { + return path; + } else { + const sortedParams = [...params].sort(([aKey], [bKey]) => aKey.localeCompare(bKey)); + const queryString = sortedParams.map(([key, value]) => `${key}=${value}`).join('&'); + return `${path}?${queryString}`; + } +} + +function getJsonBody(body: string): string { + let map: Record; + + try { + map = JSON.parse(body); + } catch (error) { + map = {}; + console.error("Couldn't parse JSON body", error); + } + + if (Object.keys(map).length === 0) { + return ''; + } + + map = removeEmptyKeys(map); + map = sortObject(map) as Record; + + return JSON.stringify(map); +} + +// See https://alchemypay.readme.io/docs/price-query +async function priceQuery( + crypto: string, + fiat: string, + amount: string, + network: string, + side: string, +): Promise { + const { secretKey, baseUrl, appId } = quoteProviders.alchemyPay; + if (!secretKey || !appId) throw new Error('AlchemyPay configuration missing'); + + const httpMethod = 'POST'; + const requestPath = '/open/api/v4/merchant/order/quote'; + const requestUrl = baseUrl + requestPath; + const timestamp = String(Date.now()); + + const bodyString = JSON.stringify({ + crypto, + network, + fiat, + amount, + side, + }); + // It's important to sort the body before signing. It's also important for the POST request to have the body sorted. + const sortedBody = getJsonBody(bodyString); + + const signature = apiSign(timestamp, httpMethod, requestUrl, sortedBody, secretKey.trim()); + + const headers = { + 'Content-Type': 'application/json', + appId, + timestamp, + sign: signature, + } as const; + + const request = { + method: 'POST', + headers, + body: sortedBody, + } as const; + + const response = await fetch(requestUrl, request); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const body = (await response.json()) as AlchemyPayResponse; + if (!body.success) { + throw new Error( + `Could not get quote for ${crypto} to ${fiat} from AlchemyPay: ` + body.returnMsg || 'Unknown error', + ); + } + + const { cryptoPrice, rampFee, networkFee, fiatQuantity } = body.data; + + const totalFee = (Number(rampFee) || 0) + (Number(networkFee) || 0); + // According to a comment in the response sample [here](https://alchemypay.readme.io/docs/price-query#response-sample) + // the `fiatQuantity` does not yet include the fees so we need to subtract them. + const fiatAmount = Number(fiatQuantity) - totalFee; + + return { + cryptoPrice: Number(cryptoPrice), + cryptoAmount: Number(amount), + fiatAmount, + totalFee, + }; +} + +// see https://alchemypay.readme.io/docs/network-code +function getAlchemyPayNetworkCode(network: string): string { + switch (network.toUpperCase()) { + case 'POLYGON': + return 'MATIC'; + default: + return network; + } +} + +function getCryptoCurrencyCode(fromCrypto: string): string { + if (fromCrypto.toLowerCase() === 'usdc') { + return 'USDC'; + } else if (fromCrypto.toLowerCase() === 'usdce' || fromCrypto.toLowerCase() === 'usdc.e') { + return 'USDC.e'; + } else if (fromCrypto.toLowerCase() === 'usdt') { + return 'USDT'; + } + + // The currencies need to be in uppercase + return fromCrypto.toUpperCase(); +} + +function getFiatCode(toFiat: string): string { + // The currencies need to be in uppercase + return toFiat.toUpperCase(); +} + +export const getQuoteFor = ( + fromCrypto: string, + toFiat: string, + amount: string, + network?: string, +): Promise => { + const DEFAULT_NETWORK = 'POLYGON'; + const networkCode = getAlchemyPayNetworkCode(network || DEFAULT_NETWORK); + const side = 'SELL'; + + return priceQuery(getCryptoCurrencyCode(fromCrypto), getFiatCode(toFiat), amount, networkCode, side); +}; diff --git a/signer-service/src/api/services/alchemypay/helpers.ts b/signer-service/src/api/services/alchemypay/helpers.ts new file mode 100644 index 00000000..b7a9e37e --- /dev/null +++ b/signer-service/src/api/services/alchemypay/helpers.ts @@ -0,0 +1,77 @@ +export function removeEmptyKeys(map: Record): Record { + const retMap: Record = {}; + + for (const [key, value] of Object.entries(map)) { + if (value !== null && value !== '') { + retMap[key] = value; + } + } + + return retMap; +} + +function sortMap(map: Record): Record { + const sortedMap = new Map(Object.entries(removeEmptyKeys(map)).sort(([aKey], [bKey]) => aKey.localeCompare(bKey))); + + for (const [key, value] of sortedMap.entries()) { + if (typeof value === 'object') { + sortedMap.set(key, sortObject(value)); + } + } + + return Object.fromEntries(sortedMap.entries()); +} + +function sortList(list: unknown[]): unknown[] { + const objectList: unknown[] = []; + const intList: number[] = []; + const floatList: number[] = []; + const stringList: string[] = []; + const jsonArray: object[] = []; + + for (const item of list) { + if (typeof item === 'object') { + jsonArray.push(item as object); + } else if (Number.isInteger(item)) { + intList.push(item as number); + } else if (typeof item === 'number') { + floatList.push(item); + } else if (typeof item === 'string') { + stringList.push(item); + } else { + intList.push(Number(item)); + } + } + + intList.sort((a, b) => a - b); + floatList.sort((a, b) => a - b); + stringList.sort(); + + objectList.push(...intList, ...floatList, ...stringList, ...jsonArray); + list.length = 0; + list.push(...objectList); + + const retList: unknown[] = []; + + for (const item of list) { + if (typeof item === 'object') { + retList.push(sortObject(item)); + } else { + retList.push(item); + } + } + + return retList; +} + +export function sortObject(obj: unknown): Record | unknown[] | unknown { + if (typeof obj === 'object') { + if (Array.isArray(obj)) { + return sortList(obj); + } else { + return sortMap(obj as Record); + } + } + + return obj; +} diff --git a/signer-service/src/api/services/moonpay.service.js b/signer-service/src/api/services/moonpay.service.js deleted file mode 100644 index 6e2b630a..00000000 --- a/signer-service/src/api/services/moonpay.service.js +++ /dev/null @@ -1,71 +0,0 @@ -const { quoteProviders } = require('../../config/vars'); - -// See https://dev.moonpay.com/reference/getsellquote -async function priceQuery(currencyCode, quoteCurrencyCode, baseCurrencyAmount, extraFeePercentage, payoutMethod) { - const { baseUrl, apiKey } = quoteProviders.moonpay; - const requestPath = `/v3/currencies/${currencyCode}/sell_quote`; - const requestUrl = baseUrl + requestPath; - const params = new URLSearchParams({ - apiKey, - quoteCurrencyCode, - baseCurrencyAmount, - extraFeePercentage, - }); - if (payoutMethod) { - params.append('payoutMethod', payoutMethod); - } - const paramsString = params.toString(); - const url = `${requestUrl}?${paramsString}`; - - return fetch(url).then(async (response) => { - if (!response.ok) { - const body = await response.json(); - if (body.type === 'NotFoundError') { - throw new Error('Token not supported'); - } - throw new Error(`Could not get quote for ${currencyCode} to ${quoteCurrencyCode} from Moonpay: ${body.message}`); - } - const body = await response.json(); - const { baseCurrencyAmount: receivedBaseCurrencyAmount, baseCurrencyPrice, quoteCurrencyAmount, feeAmount } = body; - - if (Number(baseCurrencyAmount) !== receivedBaseCurrencyAmount) { - throw new Error('Received baseCurrencyAmount does not match the requested baseCurrencyAmount'); - } - - return { - cryptoPrice: baseCurrencyPrice, - cryptoAmount: baseCurrencyAmount, - // The fiatAmount we receive from Moonpay already includes the fees - fiatAmount: quoteCurrencyAmount, - totalFee: feeAmount, - }; - }); -} - -function getCryptoCode(fromCrypto) { - // If fromCrypto is USDC, we need to convert it to USDC_Polygon - if ( - fromCrypto.toLowerCase() === 'usdc' || - fromCrypto.toLowerCase() === 'usdc.e' || - fromCrypto.toLowerCase() === 'usdce' - ) { - return 'usdc_polygon'; - } else if (fromCrypto.toLowerCase() === 'usdt') { - return 'usdt'; - } - - return fromCrypto.toLowerCase(); -} - -function getFiatCode(toFiat) { - return toFiat.toLowerCase(); -} - -exports.getQuoteFor = (fromCrypto, toFiat, amount) => { - // We can specify a custom fee percentage here added on top of the Moonpay fee but we don't - const extraFeePercentage = 0; - // If the fiat currency is EUR we can use SEPA bank transfer, otherwise we assume credit_debit_card - const paymentMethod = toFiat.toLowerCase() === 'eur' ? 'sepa_bank_transfer' : 'credit_debit_card'; - - return priceQuery(getCryptoCode(fromCrypto), getFiatCode(toFiat), amount, extraFeePercentage, paymentMethod); -}; diff --git a/signer-service/src/api/services/moonpay.service.ts b/signer-service/src/api/services/moonpay.service.ts new file mode 100644 index 00000000..0af64828 --- /dev/null +++ b/signer-service/src/api/services/moonpay.service.ts @@ -0,0 +1,96 @@ +import { config } from '../../config/vars'; + +const { quoteProviders } = config; + +interface MoonpayQuote { + cryptoPrice: number; + cryptoAmount: number; + fiatAmount: number; + totalFee: number; +} + +interface MoonpayResponse { + baseCurrencyAmount: number; + baseCurrencyPrice: number; + quoteCurrencyAmount: number; + feeAmount: number; + message?: string; + type?: string; +} + +// See https://dev.moonpay.com/reference/getsellquote +async function priceQuery( + currencyCode: string, + quoteCurrencyCode: string, + baseCurrencyAmount: string, + extraFeePercentage: number, + payoutMethod: string, +): Promise { + const { baseUrl, apiKey } = quoteProviders.moonpay; + if (!apiKey) throw new Error('Moonpay API key not configured'); + + const requestPath = `/v3/currencies/${currencyCode}/sell_quote`; + const requestUrl = baseUrl + requestPath; + const params = new URLSearchParams({ + apiKey, + quoteCurrencyCode, + baseCurrencyAmount, + extraFeePercentage: extraFeePercentage.toString(), + }); + if (payoutMethod) { + params.append('payoutMethod', payoutMethod); + } + const paramsString = params.toString(); + const url = `${requestUrl}?${paramsString}`; + + const response = await fetch(url); + if (!response.ok) { + const body = (await response.json()) as MoonpayResponse; + if (body.type === 'NotFoundError') { + throw new Error('Token not supported'); + } + throw new Error(`Could not get quote for ${currencyCode} to ${quoteCurrencyCode} from Moonpay: ${body.message}`); + } + const body = (await response.json()) as MoonpayResponse; + const { baseCurrencyAmount: receivedBaseCurrencyAmount, baseCurrencyPrice, quoteCurrencyAmount, feeAmount } = body; + + if (Number(baseCurrencyAmount) !== receivedBaseCurrencyAmount) { + throw new Error('Received baseCurrencyAmount does not match the requested baseCurrencyAmount'); + } + + return { + cryptoPrice: baseCurrencyPrice, + cryptoAmount: Number(baseCurrencyAmount), + // The fiatAmount we receive from Moonpay already includes the fees + fiatAmount: quoteCurrencyAmount, + totalFee: feeAmount, + }; +} + +function getCryptoCode(fromCrypto: string): string { + // If fromCrypto is USDC, we need to convert it to USDC_Polygon + if ( + fromCrypto.toLowerCase() === 'usdc' || + fromCrypto.toLowerCase() === 'usdc.e' || + fromCrypto.toLowerCase() === 'usdce' + ) { + return 'usdc_polygon'; + } else if (fromCrypto.toLowerCase() === 'usdt') { + return 'usdt'; + } + + return fromCrypto.toLowerCase(); +} + +function getFiatCode(toFiat: string): string { + return toFiat.toLowerCase(); +} + +export const getQuoteFor = (fromCrypto: string, toFiat: string, amount: string): Promise => { + // We can specify a custom fee percentage here added on top of the Moonpay fee but we don't + const extraFeePercentage = 0; + // If the fiat currency is EUR we can use SEPA bank transfer, otherwise we assume credit_debit_card + const paymentMethod = toFiat.toLowerCase() === 'eur' ? 'sepa_bank_transfer' : 'credit_debit_card'; + + return priceQuery(getCryptoCode(fromCrypto), getFiatCode(toFiat), amount, extraFeePercentage, paymentMethod); +}; diff --git a/signer-service/src/api/services/pendulum/createPolkadotApi.ts b/signer-service/src/api/services/pendulum/createPolkadotApi.ts new file mode 100644 index 00000000..28f06bdb --- /dev/null +++ b/signer-service/src/api/services/pendulum/createPolkadotApi.ts @@ -0,0 +1,50 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { PENDULUM_WSS } from '../../../constants/constants'; +import { ApiOptions } from '@polkadot/api/types'; +import { rpc } from '@pendulum-chain/types'; + +export async function createPolkadotApi(): Promise<{ + api: ApiPromise; + decimals: number; + ss58Format: number; +}> { + let api: ApiPromise; + let previousSpecVersion: number; + + const getSpecVersion = async (): Promise => { + if (!api) throw new Error('API not initialized'); + const runtimeVersion = await api.call.core.version(); + const human = runtimeVersion.toHuman() as { specVersion: number }; + return human.specVersion; + }; + + const initiateApi = async (): Promise => { + const wsProvider = new WsProvider(PENDULUM_WSS); + const options: ApiOptions = { + provider: wsProvider, + rpc, + }; + api = await ApiPromise.create(options); + await api.isReady; + + previousSpecVersion = await getSpecVersion(); + }; + + if (!api) { + await initiateApi(); + } + + if (!api.isConnected) await api.connect(); + await api.isReady; + + const currentSpecVersion = await getSpecVersion(); + if (currentSpecVersion !== previousSpecVersion) { + await initiateApi(); + } + + const chainProperties = api.registry.getChainProperties(); + const ss58Format = Number(chainProperties?.get('ss58Format')?.toString() ?? 42); + const decimals = Number(chainProperties?.get('tokenDecimals')?.toHuman()[0]) ?? 12; + + return { api, decimals, ss58Format }; +} diff --git a/signer-service/src/api/services/pendulum/helpers.ts b/signer-service/src/api/services/pendulum/helpers.ts new file mode 100644 index 00000000..5eaedc4c --- /dev/null +++ b/signer-service/src/api/services/pendulum/helpers.ts @@ -0,0 +1,24 @@ +import Big from 'big.js'; + +const ChainDecimals = 12; + +export const nativeToDecimal = (value: Big, decimals: number = ChainDecimals): Big => { + const divisor = new Big(10).pow(decimals); + return value.div(divisor); +}; + +export function multiplyByPowerOfTen(bigDecimal: Big, power: number): Big { + const newBigDecimal = new Big(bigDecimal); + if (newBigDecimal.c[0] === 0) return newBigDecimal; + + newBigDecimal.e += power; + return newBigDecimal; +} + +export function divideByPowerOfTen(bigDecimal: Big, power: number): Big { + const newBigDecimal = new Big(bigDecimal); + if (newBigDecimal.c[0] === 0) return newBigDecimal; + + newBigDecimal.e -= power; + return newBigDecimal; +} diff --git a/signer-service/src/api/services/pendulum.service.js b/signer-service/src/api/services/pendulum/pendulum.service.ts similarity index 57% rename from signer-service/src/api/services/pendulum.service.js rename to signer-service/src/api/services/pendulum/pendulum.service.ts index a7c53a58..0e1243da 100644 --- a/signer-service/src/api/services/pendulum.service.js +++ b/signer-service/src/api/services/pendulum/pendulum.service.ts @@ -1,83 +1,36 @@ -const { Keyring } = require('@polkadot/api'); -const { ApiPromise, WsProvider } = require('@polkadot/api'); -const Big = require('big.js'); -const { +import { Keyring } from '@polkadot/api'; +import Big from 'big.js'; +import { PENDULUM_FUNDING_AMOUNT_UNITS, - PENDULUM_WSS, SUBSIDY_MINIMUM_RATIO_FUND_UNITS, PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS, -} = require('../../constants/constants'); -const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); -const { SlackNotifier } = require('./slack.service'); +} from '../../../constants/constants'; +import { StellarTokenConfig, TOKEN_CONFIG, XCMTokenConfig } from '../../../constants/tokenConfig'; +import { SlackNotifier } from '../slack.service'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { AccountInfo } from '@polkadot/types/interfaces'; -require('dotenv').config(); +import dotenv from 'dotenv'; +dotenv.config(); const PENDULUM_FUNDING_SEED = process.env.PENDULUM_FUNDING_SEED; -function multiplyByPowerOfTen(bigDecimal, power) { - const newBigDecimal = new Big(bigDecimal); - if (newBigDecimal.c[0] === 0) return newBigDecimal; - - newBigDecimal.e += 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; - -async function createPolkadotApi() { - const getSpecVersion = async () => { - const runtimeVersion = await api.call.core.version(); - return runtimeVersion.toHuman().specVersion; - }; - - const initiateApi = async () => { - const wsProvider = new WsProvider(PENDULUM_WSS); - api = await ApiPromise.create({ - provider: wsProvider, - }); - await api.isReady; - - previousSpecVersion = await getSpecVersion(); - }; - - if (!api) { - await initiateApi(); - } - - if (!api.isConnected) await api.connect(); - await api.isReady; - - const currentSpecVersion = await getSpecVersion(); - if (currentSpecVersion !== previousSpecVersion) { - await initiateApi(); - } - - const chainProperties = api.registry.getChainProperties(); - const ss58Format = Number(chainProperties?.get('ss58Format')?.toString() ?? 42); - const decimals = Number(chainProperties?.get('tokenDecimals')?.toHuman()[0]) ?? 12; - - return { api, decimals, ss58Format }; -} - -function getFundingData(ss58Format, decimals) { +function getFundingData( + ss58Format: number, + decimals: number, +): { + fundingAccountKeypair: KeyringPair; + fundingAmountRaw: string; +} { const keyring = new Keyring({ type: 'sr25519', ss58Format }); - const fundingAccountKeypair = keyring.addFromUri(PENDULUM_FUNDING_SEED); + const fundingAccountKeypair = keyring.addFromUri(PENDULUM_FUNDING_SEED || ''); const fundingAmountUnits = Big(PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS); const fundingAmountRaw = multiplyByPowerOfTen(fundingAmountUnits, decimals).toFixed(); return { fundingAccountKeypair, fundingAmountRaw }; } -exports.fundEphemeralAccount = async (ephemeralAddress) => { +export const fundEphemeralAccount = async (ephemeralAddress: string): Promise => { try { const apiData = await createPolkadotApi(); const { fundingAccountKeypair, fundingAmountRaw } = getFundingData(apiData.ss58Format, apiData.decimals); @@ -91,25 +44,22 @@ exports.fundEphemeralAccount = async (ephemeralAddress) => { } }; -const ChainDecimals = 12; - -const nativeToDecimal = (value, decimals = ChainDecimals) => { - const divisor = new Big(10).pow(decimals); - - return value.div(divisor); -}; +interface StatusResponse { + status: boolean; + public: string; +} -exports.sendStatusWithPk = async () => { +export const sendStatusWithPk = async (): Promise => { 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); + const { data: balance } = (await apiData.api.query.system.account(fundingAccountKeypair.address)) as AccountInfo; let isTokensSufficient = true; // Wait for all required token balances check. await Promise.all( - Object.entries(TOKEN_CONFIG).map(async ([token, tokenConfig]) => { + Object.entries(TOKEN_CONFIG).map(async ([token, tokenConfig]: [string, StellarTokenConfig | XCMTokenConfig]) => { console.log(`Checking token ${token} balance...`); if (!tokenConfig.pendulumCurrencyId) { throw new Error(`Token ${token} does not have a currency id.`); diff --git a/signer-service/src/api/services/sep10.service.js b/signer-service/src/api/services/sep10.service.js deleted file mode 100644 index e351172d..00000000 --- a/signer-service/src/api/services/sep10.service.js +++ /dev/null @@ -1,109 +0,0 @@ -const { Keypair } = require('stellar-sdk'); -const { TransactionBuilder, Networks } = require('stellar-sdk'); -const { fetchTomlValues } = require('../helpers/anchors'); - -const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); -const { SEP10_MASTER_SECRET, CLIENT_DOMAIN_SECRET } = require('../../constants/constants'); - -const NETWORK_PASSPHRASE = Networks.PUBLIC; - -exports.signSep10Challenge = async (challengeXDR, outToken, clientPublicKey, memo) => { - const masterStellarKeypair = Keypair.fromSecret(SEP10_MASTER_SECRET); - const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_DOMAIN_SECRET); - - const { signingKey: anchorSigningKey } = await fetchTomlValues(TOKEN_CONFIG[outToken].tomlFileUrl); - const { homeDomain, clientDomainEnabled, memoEnabled } = TOKEN_CONFIG[outToken]; - - const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); - if (transactionSigned.source !== anchorSigningKey) { - throw new Error(`Invalid source account: ${transactionSigned.source}`); - } - if (transactionSigned.sequence !== '0') { - throw new Error(`Invalid sequence number: ${transactionSigned.sequence}`); - } - - // See https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#success - // memo field should be empty for the ephemeral case, or the corresponding one based on evm address - // derivation. - if (transactionSigned.memo.value !== memo) { - throw new Error('Memo does not match with specified user signature or address. Could not validate.'); - } - - const { operations } = transactionSigned; - // Verify the first manage_data operation - const firstOp = operations[0]; - if (firstOp.type !== 'manageData') { - throw new Error('The first operation should be manageData'); - } - - // clientPublicKey is either: the ephemeral, or the master account - if (firstOp.source !== clientPublicKey) { - throw new Error('First manageData operation must have the client account as the source'); - } - - if (memo !== null && memoEnabled) { - if (firstOp.source !== masterStellarKeypair.publicKey()) { - throw new Error( - 'First manageData operation must have the master signing key as the source when memo is being used.', - ); - } - } - - if (firstOp.name !== `${homeDomain} auth`) { - throw new Error(`First manageData operation should have key '${homeDomain} auth'`); - } - if (!firstOp.value || firstOp.value.length !== 64) { - throw new Error('First manageData operation should have a 64-byte random nonce as value'); - } - - let hasWebAuthDomain = false; - let hasClientDomain = false; - - for (let i = 1; i < operations.length; i++) { - const op = operations[i]; - - if (op.type !== 'manageData') { - throw new Error('All operations should be manage_data operations'); - } - - // Verify web_auth_domain operation - if (op.name === 'web_auth_domain') { - hasWebAuthDomain = true; - if (op.source !== anchorSigningKey) { - throw new Error('web_auth_domain manage_data operation must have the server account as the source'); - } - } - - if (op.name === 'client_domain') { - hasClientDomain = true; - if (op.source !== clientDomainStellarKeypair.publicKey()) { - throw new Error('client_domain manage_data operation must have the client domain account as the source'); - } - } - } - - // the web_auth_domain and client_domain operation must be present - if (!hasWebAuthDomain) { - throw new Error('Transaction must contain a web_auth_domain manageData operation'); - } - if (!hasClientDomain && clientDomainEnabled) { - throw new Error('Transaction must contain a client_domain manageData operation'); - } - - let clientDomainSignature; - if (clientDomainEnabled) { - clientDomainSignature = transactionSigned.getKeypairSignature(clientDomainStellarKeypair); - } - - let masterClientSignature; - if (memo !== null && memoEnabled) { - masterClientSignature = transactionSigned.getKeypairSignature(masterStellarKeypair); - } - - return { - clientSignature: clientDomainSignature, - clientPublic: clientDomainStellarKeypair.publicKey(), - masterClientSignature, - masterClientPublic: masterStellarKeypair.publicKey(), - }; -}; diff --git a/signer-service/src/api/services/sep10/helpers.ts b/signer-service/src/api/services/sep10/helpers.ts new file mode 100644 index 00000000..166083e3 --- /dev/null +++ b/signer-service/src/api/services/sep10/helpers.ts @@ -0,0 +1,96 @@ +import { Transaction, Operation } from 'stellar-sdk'; +import { TOKEN_CONFIG } from '../../../constants/tokenConfig'; + +interface TokenConfig { + tomlFileUrl: string; + homeDomain: string; + clientDomainEnabled: boolean; + memoEnabled: boolean; +} + +export const getOutToken = (outToken: keyof typeof TOKEN_CONFIG): TokenConfig => { + return TOKEN_CONFIG[outToken] as TokenConfig; +}; + +export const validateTransaction = (transaction: Transaction, anchorSigningKey: string, memo: string | null) => { + if (transaction.source !== anchorSigningKey) { + throw new Error(`Invalid source account: ${transaction.source}`); + } + if (transaction.sequence !== '0') { + throw new Error(`Invalid sequence number: ${transaction.sequence}`); + } + if (transaction.memo.value !== memo) { + throw new Error('Memo does not match with specified user signature or address. Could not validate.'); + } +}; + +export const validateFirstOperation = ( + operation: Operation, + clientPublicKey: string, + homeDomain: string, + memo: string | null, + memoEnabled: boolean, + masterPublicKey: string, +) => { + if (operation.type !== 'manageData') { + throw new Error('The first operation should be manageData'); + } + + if (operation.source !== clientPublicKey) { + throw new Error('First manageData operation must have the client account as the source'); + } + + if (memo !== null && memoEnabled) { + if (operation.source !== masterPublicKey) { + throw new Error( + 'First manageData operation must have the master signing key as the source when memo is being used.', + ); + } + } + + if (operation.name !== `${homeDomain} auth`) { + throw new Error(`First manageData operation should have key '${homeDomain} auth'`); + } + if (!operation.value || operation.value.length !== 64) { + throw new Error('First manageData operation should have a 64-byte random nonce as value'); + } +}; + +export const validateRemainingOperations = ( + operations: Operation[], + anchorSigningKey: string, + clientDomainPublicKey: string, + clientDomainEnabled: boolean, +) => { + let hasWebAuthDomain = false; + let hasClientDomain = false; + + for (let i = 1; i < operations.length; i++) { + const op = operations[i]; + + if (op.type !== 'manageData') { + throw new Error('All operations should be manage_data operations'); + } + + if (op.name === 'web_auth_domain') { + hasWebAuthDomain = true; + if (op.source !== anchorSigningKey) { + throw new Error('web_auth_domain manage_data operation must have the server account as the source'); + } + } + + if (op.name === 'client_domain') { + hasClientDomain = true; + if (op.source !== clientDomainPublicKey) { + throw new Error('client_domain manage_data operation must have the client domain account as the source'); + } + } + } + + if (!hasWebAuthDomain) { + throw new Error('Transaction must contain a web_auth_domain manageData operation'); + } + if (!hasClientDomain && clientDomainEnabled) { + throw new Error('Transaction must contain a client_domain manageData operation'); + } +}; diff --git a/signer-service/src/api/services/sep10/sep10.service.ts b/signer-service/src/api/services/sep10/sep10.service.ts new file mode 100644 index 00000000..ad78c78f --- /dev/null +++ b/signer-service/src/api/services/sep10/sep10.service.ts @@ -0,0 +1,73 @@ +import { Keypair, TransactionBuilder, Networks, Transaction } from 'stellar-sdk'; +import { SEP10_MASTER_SECRET, CLIENT_DOMAIN_SECRET } from '../../../constants/constants'; +import { TOKEN_CONFIG } from '../../../constants/tokenConfig'; +import { fetchTomlValues } from '../../helpers/anchors'; +import { getOutToken, validateTransaction, validateFirstOperation, validateRemainingOperations } from './helpers'; + +const NETWORK_PASSPHRASE = Networks.PUBLIC; + +interface TomlValues { + signingKey: string; +} + +interface Sep10Response { + clientSignature?: string; + clientPublic: string; + masterClientSignature?: string; + masterClientPublic: string; +} + +export const signSep10Challenge = async ( + challengeXDR: string, + outToken: keyof typeof TOKEN_CONFIG, + clientPublicKey: string, + memo: string | null, +): Promise => { + if (!SEP10_MASTER_SECRET || !CLIENT_DOMAIN_SECRET) { + throw new Error('Missing required secrets'); + } + const masterStellarKeypair = Keypair.fromSecret(SEP10_MASTER_SECRET); + const clientDomainStellarKeypair = Keypair.fromSecret(CLIENT_DOMAIN_SECRET); + + const outTokenConfig = getOutToken(outToken); + const { signingKey: anchorSigningKey } = (await fetchTomlValues(outTokenConfig.tomlFileUrl)) as TomlValues; + const { homeDomain, clientDomainEnabled, memoEnabled } = outTokenConfig; + const transactionSigned = TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); + + if (!(transactionSigned instanceof Transaction)) { + throw new Error('Expected a Transaction, got a FeeBumpTransaction'); + } + + validateTransaction(transactionSigned, anchorSigningKey, memo); + + const { operations } = transactionSigned; + validateFirstOperation( + operations[0], + clientPublicKey, + homeDomain, + memo, + memoEnabled, + masterStellarKeypair.publicKey(), + ); + + validateRemainingOperations( + operations, + anchorSigningKey, + clientDomainStellarKeypair.publicKey(), + clientDomainEnabled, + ); + + const clientDomainSignature = clientDomainEnabled + ? transactionSigned.getKeypairSignature(clientDomainStellarKeypair) + : undefined; + + const masterClientSignature = + memo !== null && memoEnabled ? transactionSigned.getKeypairSignature(masterStellarKeypair) : undefined; + + return { + clientSignature: clientDomainSignature, + clientPublic: clientDomainStellarKeypair.publicKey(), + masterClientSignature, + masterClientPublic: masterStellarKeypair.publicKey(), + }; +}; diff --git a/signer-service/src/api/services/siwe.service.js b/signer-service/src/api/services/siwe.service.ts similarity index 61% rename from signer-service/src/api/services/siwe.service.js rename to signer-service/src/api/services/siwe.service.ts index 0b28f0c5..d73c6f07 100644 --- a/signer-service/src/api/services/siwe.service.js +++ b/signer-service/src/api/services/siwe.service.ts @@ -1,24 +1,28 @@ -const siwe = require('siwe'); -const { createPublicClient, http } = require('viem'); -const { polygon } = require('viem/chains'); -const { Keyring } = require('@polkadot/api'); -const { SignInMessage } = require('../helpers/siweMessageFormatter.js'); -const { signatureVerify } = require('@polkadot/util-crypto'); -const { deriveMemoFromAddress } = require('../helpers/memoDerivation'); -const { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } = require('../../constants/constants'); +import { generateNonce } from 'siwe'; +import { createPublicClient, http } from 'viem'; +import { polygon } from 'viem/chains'; +import { signatureVerify } from '@polkadot/util-crypto'; +import { SignInMessage } from '../helpers/siweMessageFormatter'; +import { deriveMemoFromAddress } from '../helpers/memoDerivation'; +import { DEFAULT_LOGIN_EXPIRATION_TIME_HOURS } from '../../constants/constants'; class ValidationError extends Error { - constructor(message) { + constructor(message: string) { super(message); this.name = 'SiweValidationError'; } } +interface SiweData { + siweMessage: SignInMessage | undefined; + address: string; +} + // Map that will hold the siwe messages sent + nonce we defined -const siweMessagesMap = new Map(); +const siweMessagesMap = new Map(); -const createAndSendNonce = async (address) => { - const nonce = siwe.generateNonce(); +const createAndSendNonce = async (address: string): Promise<{ nonce: string }> => { + const nonce = generateNonce(); const siweMessage = undefined; // Initial message is undefined since it will be created in UI. siweMessagesMap.set(nonce, { siweMessage, address }); return { nonce }; @@ -26,7 +30,11 @@ const createAndSendNonce = async (address) => { // Used to verify the integrity and validity of the signature // For the initial verification, the siweMessage must be provided as parameter -const verifySiweMessage = async (nonce, signature, initialSiweMessage) => { +const verifySiweMessage = async ( + nonce: string, + signature: string, + initialSiweMessage?: string, +): Promise => { const existingSiweDataForNonce = siweMessagesMap.get(nonce); if (!existingSiweDataForNonce) { throw new ValidationError('Message not found, we have not sent this nonce or nonce is incorrect'); @@ -34,8 +42,11 @@ const verifySiweMessage = async (nonce, signature, initialSiweMessage) => { const siweMessage = existingSiweDataForNonce.siweMessage ? existingSiweDataForNonce.siweMessage - : SignInMessage.fromMessage(initialSiweMessage); - const address = existingSiweDataForNonce.address; + : initialSiweMessage + ? SignInMessage.fromMessage(initialSiweMessage) + : undefined; + + const { address } = existingSiweDataForNonce; if (!siweMessage) { throw new Error('Message must be provided as a parameter if it has not been initially validated.'); @@ -49,12 +60,12 @@ const verifySiweMessage = async (nonce, signature, initialSiweMessage) => { transport: http(), }); valid = await publicClient.verifyMessage({ - address: address, + address: address as `0x${string}`, message: siweMessage.toMessage(), // Validation must be done on the message as string - signature, + signature: signature as `0x${string}`, }); } else if (address.startsWith('5')) { - valid = signatureVerify(siweMessage.toMessage(), signature, address); + valid = signatureVerify(siweMessage.toMessage(), signature, address).isValid; } else { throw new ValidationError(`verifySiweMessage: Invalid address format: ${address}`); } @@ -75,10 +86,8 @@ const verifySiweMessage = async (nonce, signature, initialSiweMessage) => { }; // Since the message is created in the UI, we need to verify the fields of the message -const verifyInitialMessageFields = (siweMessage) => { - // Fields we validate on initial - const scheme = siweMessage.scheme; // must be https - const expirationTime = siweMessage.expirationTime; +const verifyInitialMessageFields = (siweMessage: SignInMessage): void => { + const { scheme, expirationTime } = siweMessage; if (scheme !== 'https') { throw new ValidationError('Scheme must be https'); @@ -89,7 +98,7 @@ const verifyInitialMessageFields = (siweMessage) => { } // Check if expiration is within a reasonable range from current time - const currentTime = new Date().getTime(); + const currentTime = Date.now(); const expirationTimestamp = new Date(expirationTime).getTime(); const expirationGracePeriod = 1000 * 60 * 10; // 10 minutes @@ -106,7 +115,7 @@ const verifyInitialMessageFields = (siweMessage) => { } }; -const verifyAndStoreSiweMessage = async (nonce, signature, siweMessage) => { +const verifyAndStoreSiweMessage = async (nonce: string, signature: string, siweMessage: string): Promise => { const validatedMessage = await verifySiweMessage(nonce, signature, siweMessage); // Perform additional checks to ensure message fields are valid @@ -114,35 +123,39 @@ const verifyAndStoreSiweMessage = async (nonce, signature, siweMessage) => { // Verification complete. Update the map and append the message. const siweData = siweMessagesMap.get(nonce); + if (!siweData) throw new ValidationError('Message data not found'); + + // Update the map with validated message siweData.siweMessage = validatedMessage; siweMessagesMap.set(nonce, siweData); - // Remove messages with same address from map except for the one we just verified. - // This will keep one valid session per address. - siweMessagesMap.forEach((data, nonce) => { - if (data.address === siweData.address && nonce !== siweData.siweMessage.nonce) { - siweMessagesMap.delete(nonce); + // Keep one valid session per address + for (const [key, data] of siweMessagesMap.entries()) { + if (data.address === siweData.address && key !== validatedMessage.nonce) { + siweMessagesMap.delete(key); } - }); + } + return siweData.address; }; -const validateSignatureAndGetMemo = async (nonce, userChallengeSignature) => { +const validateSignatureAndGetMemo = async ( + nonce: string | null, + userChallengeSignature: string | null, +): Promise => { if (!userChallengeSignature || !nonce) { return null; // Default memo value when single stellar account is used } - let message; try { - // initialSiweMessage must be undefined after an initial check, - // message must exist on the map. - message = await verifySiweMessage(nonce, userChallengeSignature, undefined); - } catch (e) { - throw new Error(`Could not verify signature: ${e.message}`); + const message = await verifySiweMessage(nonce, userChallengeSignature); + return deriveMemoFromAddress(message.address); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Could not verify signature: ${error.message}`); + } + throw error; } - - const memo = await deriveMemoFromAddress(message.address); - return memo; }; -module.exports = { verifySiweMessage, verifyAndStoreSiweMessage, createAndSendNonce, validateSignatureAndGetMemo }; +export { verifySiweMessage, verifyAndStoreSiweMessage, createAndSendNonce, validateSignatureAndGetMemo }; diff --git a/signer-service/src/api/services/slack.service.js b/signer-service/src/api/services/slack.service.js deleted file mode 100644 index 5705f4dd..00000000 --- a/signer-service/src/api/services/slack.service.js +++ /dev/null @@ -1,56 +0,0 @@ -// 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/slack.service.ts b/signer-service/src/api/services/slack.service.ts new file mode 100644 index 00000000..cfeed289 --- /dev/null +++ b/signer-service/src/api/services/slack.service.ts @@ -0,0 +1,60 @@ +// 6 hours in milliseconds +const COOLDOWN_PERIOD_MS = 6 * 60 * 60 * 1000; + +interface SlackMessage { + text: string; + [key: string]: unknown; +} + +class SlackNotifier { + private readonly webhookUrl: string; + private readonly messageHistory: Map; + + constructor() { + const token = process.env.SLACK_WEB_HOOK_TOKEN; + if (!token) { + throw new Error('SLACK_WEB_HOOK_TOKEN is not defined'); + } + this.webhookUrl = `https://hooks.slack.com/services/${token}`; + this.messageHistory = new Map(); + } + + private generateMessageSignature(message: SlackMessage): string { + return JSON.stringify(message); + } + + private isMessageAllowed(signature: string): boolean { + const now = Date.now(); + const lastSent = this.messageHistory.get(signature); + + if (!lastSent) return true; + + return now - lastSent >= COOLDOWN_PERIOD_MS; + } + + public async sendMessage(message: SlackMessage): Promise { + const signature = this.generateMessageSignature(message); + + if (!this.isMessageAllowed(signature)) { + // Message is still in cooldown period, skip sending + return; + } + + const response = await fetch(this.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }); + + if (!response.ok) { + throw new Error(`Failed to send message. Status: ${response.status}`); + } + + // Update the timestamp for this message + this.messageHistory.set(signature, Date.now()); + } +} + +export { SlackNotifier, SlackMessage }; diff --git a/signer-service/src/api/services/spreadsheet.service.js b/signer-service/src/api/services/spreadsheet.service.js deleted file mode 100644 index 3c3c0597..00000000 --- a/signer-service/src/api/services/spreadsheet.service.js +++ /dev/null @@ -1,70 +0,0 @@ -const { GoogleSpreadsheet } = require('google-spreadsheet'); -const { JWT } = require('google-auth-library'); - -const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; - -// googleCredentials: { email: string, key: string }, -exports.initGoogleSpreadsheet = async (sheetId, googleCredentials) => { - // Initialize auth - see https://theoephraim.github.io/node-google-spreadsheet/#/guides/authentication - if (!googleCredentials.email || !googleCredentials.key) { - throw new Error('Missing some google credentials'); - } - - const serviceAccountAuth = new JWT({ - // env var values here are copied from service account credentials generated by google - // see "Authentication" section in docs for more info - email: googleCredentials.email, - key: googleCredentials.key, - scopes: SCOPES, - }); - - const doc = new GoogleSpreadsheet(sheetId, serviceAccountAuth); - try { - await doc.loadInfo(); - } catch (error) { - console.error(`Error loading Google Spreadsheet ${sheetId}:`, error); - throw error; - } - - return doc; -}; - -// doc: GoogleSpreadsheet, headerValues: string[] -exports.getOrCreateSheet = async (doc, headerValues) => { - let matchingSheet = null; - - try { - for (let i = 0; i < Math.min(doc.sheetsByIndex.length, 10); i++) { - const sheet = doc.sheetsByIndex[i]; - try { - await sheet.loadHeaderRow(); - const sheetHeaders = sheet.headerValues; - - if ( - sheetHeaders.length === headerValues.length && - sheetHeaders.every((value, index) => value === headerValues[index]) - ) { - matchingSheet = sheet; - break; - } - } catch (error) { - continue; - } - } - - if (!matchingSheet) { - console.log(' Creating a new sheet.'); - matchingSheet = await doc.addSheet({ headerValues }); - } - } catch (error) { - console.error('Error iterating sheets:', error.message); - throw error; - } - - return matchingSheet; -}; - -// sheet: GoogleSpreadsheetWorksheet, data: Record -exports.appendData = async (sheet, data) => { - await sheet.addRow(data); -}; diff --git a/signer-service/src/api/services/spreadsheet.service.ts b/signer-service/src/api/services/spreadsheet.service.ts new file mode 100644 index 00000000..2d0a1f27 --- /dev/null +++ b/signer-service/src/api/services/spreadsheet.service.ts @@ -0,0 +1,85 @@ +import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from 'google-spreadsheet'; +import { JWT } from 'google-auth-library'; + +const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; + +export interface GoogleCredentials { + email: string; + key: string; +} + +interface SpreadsheetService { + initGoogleSpreadsheet: (sheetId: string, credentials: GoogleCredentials) => Promise; + getOrCreateSheet: (doc: GoogleSpreadsheet, headerValues: string[]) => Promise; + appendData: (sheet: GoogleSpreadsheetWorksheet, data: Record) => Promise; +} + +export const initGoogleSpreadsheet = async ( + sheetId: string, + credentials: GoogleCredentials, +): Promise => { + if (!credentials.email || !credentials.key) { + throw new Error('Missing required Google credentials'); + } + + const auth = new JWT({ + email: credentials.email, + key: credentials.key, + scopes: SCOPES, + }); + + const doc = new GoogleSpreadsheet(sheetId, auth); + + try { + await doc.loadInfo(); + return doc; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to load Google Spreadsheet ${sheetId}: ${message}`); + } +}; + +export const getOrCreateSheet = async ( + doc: GoogleSpreadsheet, + headerValues: string[], +): Promise => { + const MAX_SHEETS_TO_CHECK = 10; + + try { + // Try to find matching sheet + for (let i = 0; i < Math.min(doc.sheetsByIndex.length, MAX_SHEETS_TO_CHECK); i++) { + const sheet = doc.sheetsByIndex[i]; + try { + await sheet.loadHeaderRow(); + if (doHeadersMatch(sheet.headerValues, headerValues)) { + return sheet; + } + } catch { + continue; + } + } + + // Create new sheet if no match found + return await doc.addSheet({ headerValues }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to get or create sheet: ${message}`); + } +}; + +export const appendData = async (sheet: GoogleSpreadsheetWorksheet, data: Record): Promise => { + await sheet.addRow(data); +}; + +const doHeadersMatch = (existingHeaders: string[], newHeaders: string[]): boolean => { + return ( + existingHeaders.length === newHeaders.length && + existingHeaders.every((header, index) => header === newHeaders[index]) + ); +}; + +export const spreadsheetService: SpreadsheetService = { + initGoogleSpreadsheet, + getOrCreateSheet, + appendData, +}; diff --git a/signer-service/src/api/services/stellar.service.js b/signer-service/src/api/services/stellar.service.ts similarity index 63% rename from signer-service/src/api/services/stellar.service.js rename to signer-service/src/api/services/stellar.service.ts index 6a8d00d6..fdaed8f8 100644 --- a/signer-service/src/api/services/stellar.service.js +++ b/signer-service/src/api/services/stellar.service.ts @@ -1,22 +1,48 @@ -const { Horizon, Keypair, TransactionBuilder, Operation, Networks, Asset, Memo, Account } = require('stellar-sdk'); -const { +import { Horizon, Keypair, TransactionBuilder, Operation, Networks, Asset, Memo, Account } from 'stellar-sdk'; +import { HORIZON_URL, FUNDING_SECRET, STELLAR_FUNDING_AMOUNT_UNITS, STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS, -} = require('../../constants/constants'); -const { TOKEN_CONFIG, getTokenConfigByAssetCode } = require('../../constants/tokenConfig'); -const { SlackNotifier } = require('./slack.service'); +} from '../../constants/constants'; +import { StellarTokenConfig, TOKEN_CONFIG, getTokenConfigByAssetCode } from '../../constants/tokenConfig'; +import { SlackNotifier } from './slack.service'; + +export interface PaymentData { + amount: string; + memo: string; + memoType: 'text' | 'hash'; + offrampingAccount: string; +} + +interface CreationTxResult { + signature: string[]; + sequence: string; +} -// Derive funding pk -const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET).publicKey(); +interface PaymentTxResult { + signature: string[]; +} + +interface StatusResult { + status: boolean; + public: string; +} + +// Constants +const FUNDING_PUBLIC_KEY = Keypair.fromSecret(FUNDING_SECRET || '').publicKey(); const horizonServer = new Horizon.Server(HORIZON_URL); const NETWORK_PASSPHRASE = Networks.PUBLIC; -async function buildCreationStellarTx(fundingSecret, ephemeralAccountId, maxTime, assetCode, baseFee) { - const tokenConfig = getTokenConfigByAssetCode(TOKEN_CONFIG, assetCode); - if (tokenConfig === undefined) { - console.error('ERROR: Invalid asset id or configuration not found'); +async function buildCreationStellarTx( + fundingSecret: string, + ephemeralAccountId: string, + maxTime: number, + assetCode: string, + baseFee: string, +): Promise { + const tokenConfig = getTokenConfigByAssetCode(TOKEN_CONFIG, assetCode) as StellarTokenConfig; + if (!tokenConfig) { throw new Error('Invalid asset id or configuration not found'); } @@ -61,36 +87,24 @@ async function buildCreationStellarTx(fundingSecret, ephemeralAccountId, maxTime } async function buildPaymentAndMergeTx( - fundingSecret, - ephemeralAccountId, - ephemeralSequence, - paymentData, - maxTime, - assetCode, - baseFee, -) { + fundingSecret: string, + ephemeralAccountId: string, + ephemeralSequence: string, + paymentData: PaymentData, + maxTime: number, + assetCode: string, + baseFee: string, +): Promise { const ephemeralAccount = new Account(ephemeralAccountId, ephemeralSequence); const fundingAccountKeypair = Keypair.fromSecret(fundingSecret); const { amount, memo, memoType, offrampingAccount } = paymentData; - const tokenConfig = getTokenConfigByAssetCode(TOKEN_CONFIG, assetCode); - if (tokenConfig === undefined) { + const tokenConfig = getTokenConfigByAssetCode(TOKEN_CONFIG, assetCode) as StellarTokenConfig; + if (!tokenConfig) { throw new Error('Invalid asset id or configuration not found'); } - let transactionMemo; - switch (memoType) { - case 'text': - transactionMemo = Memo.text(memo); - break; - - case 'hash': - transactionMemo = Memo.hash(Buffer.from(memo, 'base64')); - break; - - default: - throw new Error(`Unexpected offramp memo type: ${memoType}`); - } + const transactionMemo = memoType === 'text' ? Memo.text(memo) : Memo.hash(Buffer.from(memo, 'base64')); const paymentTransaction = new TransactionBuilder(ephemeralAccount, { fee: baseFee, @@ -133,28 +147,29 @@ async function buildPaymentAndMergeTx( }; } -async function sendStatusWithPk() { +async function sendStatusWithPk(): Promise { const slackNotifier = new SlackNotifier(); - let stellarBalance = null; try { - // ensure the funding account exists 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}.`, + const stellarBalance = account.balances.find( + (balance: { asset_type: string; balance: string }) => balance.asset_type === 'native', + ); + + if (!stellarBalance || Number(stellarBalance.balance) < Number(STELLAR_FUNDING_AMOUNT_UNITS)) { + await slackNotifier.sendMessage({ + text: `Current balance of funding account is ${ + stellarBalance?.balance ?? 0 + } XLM please charge the account ${FUNDING_PUBLIC_KEY}.`, }); return { status: false, public: FUNDING_PUBLIC_KEY }; } return { status: true, public: FUNDING_PUBLIC_KEY }; } catch (error) { - console.error("Couldn't load Stellar account: ", error); + console.error("Couldn't load Stellar account:", error); return { status: false, public: FUNDING_PUBLIC_KEY }; } } -module.exports = { buildCreationStellarTx, buildPaymentAndMergeTx, sendStatusWithPk }; +export { buildCreationStellarTx, buildPaymentAndMergeTx, sendStatusWithPk }; diff --git a/signer-service/src/api/services/transak.service.js b/signer-service/src/api/services/transak.service.js deleted file mode 100644 index ebaf1e58..00000000 --- a/signer-service/src/api/services/transak.service.js +++ /dev/null @@ -1,84 +0,0 @@ -const { quoteProviders } = require('../../config/vars'); - -// See https://docs.transak.com/reference/get-price -async function priceQuery(cryptoCurrency, fiatCurrency, cryptoAmount, network, isBuyOrSell, paymentMethod) { - const { baseUrl, partnerApiKey } = quoteProviders.transak; - const requestPath = '/api/v1/pricing/public/quotes'; - const requestUrl = baseUrl + requestPath; - const params = new URLSearchParams({ - partnerApiKey, - cryptoCurrency, - fiatCurrency, - cryptoAmount, - network, - isBuyOrSell, - }); - if (paymentMethod) { - params.append('paymentMethod', paymentMethod); - } - const paramsString = params.toString(); - const url = `${requestUrl}?${paramsString}`; - - return fetch(url).then(async (response) => { - if (!response.ok) { - const body = await response.json(); - if (body.error.message === 'Invalid fiat currency') { - throw new Error('Token not supported'); - } - throw new Error( - `Could not get quote for ${cryptoCurrency} to ${fiatCurrency} from Transak: ${body.error.message}`, - ); - } - const body = await response.json(); - const { conversionPrice, cryptoAmount, fiatAmount, totalFee } = body.response; - - return { - cryptoPrice: conversionPrice, - cryptoAmount, - // The fiatAmount we receive from Transak already includes the fees - fiatAmount, - totalFee, - }; - }); -} - -// Helper function to get the network code for Transak. It seems like Transak just uses the commonly known network names -// as the code for the network parameter in their API so we just return the network as is. -function getTransakNetworkCode(network) { - switch (network.toUpperCase()) { - case 'POLYGON': - return 'polygon'; - default: - return network; - } -} - -function getCryptoCode(fromCrypto) { - // The currencies need to be in uppercase - if ( - fromCrypto.toLowerCase() === 'usdc' || - fromCrypto.toLowerCase() === 'usdc.e' || - fromCrypto.toLowerCase() === 'usdce' - ) { - return 'USDC'; - } else if (fromCrypto.toLowerCase() === 'usdt') { - return 'USDT'; - } - - return fromCrypto.toUpperCase(); -} - -function getFiatCode(toFiat) { - // The currencies need to be in uppercase - return toFiat.toUpperCase(); -} - -exports.getQuoteFor = (fromCrypto, toFiat, amount, network) => { - const networkCode = getTransakNetworkCode(network); - const side = 'SELL'; // We always sell our crypto for fiat - - // We assume the default payment method is used - const paymentMethod = undefined; - - return priceQuery(getCryptoCode(fromCrypto), getFiatCode(toFiat), amount, networkCode, side, paymentMethod); -}; diff --git a/signer-service/src/api/services/transak.service.ts b/signer-service/src/api/services/transak.service.ts new file mode 100644 index 00000000..05533876 --- /dev/null +++ b/signer-service/src/api/services/transak.service.ts @@ -0,0 +1,120 @@ +import { config } from '../../config/vars'; + +const { quoteProviders } = config; + +interface TransakQuoteResponse { + response: { + conversionPrice: number; + cryptoAmount: number; + fiatAmount: number; + totalFee: number; + }; + error?: { + message: string; + }; +} + +interface QuoteResult { + cryptoPrice: number; + cryptoAmount: number; + fiatAmount: number; + totalFee: number; +} + +type Network = 'POLYGON' | string; +type Side = 'BUY' | 'SELL'; + +// See https://docs.transak.com/reference/get-price +async function priceQuery( + cryptoCurrency: string, + fiatCurrency: string, + cryptoAmount: number, + network: Network, + isBuyOrSell: Side, + paymentMethod?: string, +): Promise { + const { baseUrl, partnerApiKey } = quoteProviders.transak; + const requestPath = '/api/v1/pricing/public/quotes'; + const requestUrl = `${baseUrl}${requestPath}`; + + if (!partnerApiKey) { + throw new Error('Transak partner API key is not defined'); + } + + const params = new URLSearchParams({ + partnerApiKey, + cryptoCurrency, + fiatCurrency, + cryptoAmount: cryptoAmount.toString(), + network, + isBuyOrSell, + }); + + if (paymentMethod) { + params.append('paymentMethod', paymentMethod); + } + + const url = `${requestUrl}?${params.toString()}`; + + const response = await fetch(url); + if (!response.ok) { + const body = (await response.json()) as TransakQuoteResponse; + if (body.error?.message === 'Invalid fiat currency') { + throw new Error('Token not supported'); + } + throw new Error( + `Could not get quote for ${cryptoCurrency} to ${fiatCurrency} from Transak: ${body.error?.message}`, + ); + } + + const { + response: { conversionPrice, cryptoAmount: resultCryptoAmount, fiatAmount, totalFee }, + } = (await response.json()) as TransakQuoteResponse; + + return { + cryptoPrice: conversionPrice, + cryptoAmount: resultCryptoAmount, + // The fiatAmount we receive from Transak already includes the fees + fiatAmount, + totalFee, + }; +} + +type SupportedCrypto = 'USDC' | 'USDC.E' | 'USDCE' | 'USDT' | string; + +function getTransakNetworkCode(network: string): Network { + switch (network.toUpperCase()) { + case 'POLYGON': + return 'polygon'; + default: + return network; + } +} + +function getCryptoCode(fromCrypto: SupportedCrypto): string { + const normalizedCrypto = fromCrypto.toLowerCase(); + if (['usdc', 'usdc.e', 'usdce'].includes(normalizedCrypto)) { + return 'USDC'; + } + if (normalizedCrypto === 'usdt') { + return 'USDT'; + } + return fromCrypto.toUpperCase(); +} + +function getFiatCode(toFiat: string): string { + return toFiat.toUpperCase(); +} + +export const getQuoteFor = ( + fromCrypto: SupportedCrypto, + toFiat: string, + amount: string | number, + network?: string, +): Promise => { + const DEFAULT_NETWORK = 'POLYGON'; + const networkCode = getTransakNetworkCode(network || DEFAULT_NETWORK); + const side: Side = 'SELL'; // We always sell our crypto for fiat + + return priceQuery(getCryptoCode(fromCrypto), getFiatCode(toFiat), Number(amount), networkCode, side); +}; diff --git a/signer-service/src/config/express.js b/signer-service/src/config/express.ts similarity index 59% rename from signer-service/src/config/express.js rename to signer-service/src/config/express.ts index 43ea97b1..82a00647 100644 --- a/signer-service/src/config/express.js +++ b/signer-service/src/config/express.ts @@ -1,15 +1,19 @@ -const express = require('express'); -const morgan = require('morgan'); -const bodyParser = require('body-parser'); -const compress = require('compression'); -const methodOverride = require('method-override'); -const cors = require('cors'); -const helmet = require('helmet'); -const rateLimit = require('express-rate-limit'); -const routes = require('../api/routes/v1'); -const { logs, rateLimitMaxRequests, rateLimitNumberOfProxies, rateLimitWindowMinutes } = require('./vars'); -const error = require('../api/middlewares/error'); -const cookieParser = require('cookie-parser'); +import express from 'express'; +import morgan from 'morgan'; +import bodyParser from 'body-parser'; +import compress from 'compression'; +import methodOverride from 'method-override'; +import cors from 'cors'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import cookieParser from 'cookie-parser'; + +import routes from '../api/routes/v1'; +import error from '../api/middlewares/error'; + +import { config } from './vars'; + +const { logs, rateLimitMaxRequests, rateLimitNumberOfProxies, rateLimitWindowMinutes } = config; /** * Express instance @@ -21,13 +25,14 @@ const app = express(); app.use( cors({ origin: [ - 'http://localhost:5173', - 'https://polygon-prototype-staging--pendulum-pay.netlify.app', 'https://app.vortexfinance.co', - ], - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + 'https://polygon-prototype-staging--pendulum-pay.netlify.app', + process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : null, + ].filter(Boolean) as string[], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], // Explicitly list allowed methods credentials: true, - allowedHeaders: 'Content-Type,Authorization', + allowedHeaders: ['Content-Type', 'Authorization'], // Explicitly list allowed headers + maxAge: 86400, // Cache preflight requests for 24 hours }), ); @@ -36,8 +41,8 @@ app.use( app.set('trust proxy', rateLimitNumberOfProxies); // Define rate limiter const limiter = rateLimit({ - windowMs: rateLimitWindowMinutes * 60 * 1000, - max: rateLimitMaxRequests, // Limit each IP to requests per `window` + windowMs: Number(rateLimitWindowMinutes) * 60 * 1000, + max: Number(rateLimitMaxRequests), // Limit each IP to requests per `window` standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); @@ -75,4 +80,4 @@ app.use(error.notFound); // error handler, send stacktrace only during development app.use(error.handler); -module.exports = app; +export default app; diff --git a/signer-service/src/config/logger.js b/signer-service/src/config/logger.ts similarity index 78% rename from signer-service/src/config/logger.js rename to signer-service/src/config/logger.ts index 477f2c22..c1970243 100644 --- a/signer-service/src/config/logger.js +++ b/signer-service/src/config/logger.ts @@ -1,4 +1,5 @@ -const winston = require('winston'); +import { StreamOptions } from 'morgan'; +import winston from 'winston'; const logger = winston.createLogger({ level: 'info', @@ -25,10 +26,13 @@ if (process.env.NODE_ENV !== 'production') { ); } -logger.stream = { - write: (message) => { +const stream: StreamOptions = { + write: (message: string) => { logger.info(message.trim()); }, }; -module.exports = logger; +// @ts-ignore 'morgan' +logger.stream = stream; + +export default logger; diff --git a/signer-service/src/config/vars.js b/signer-service/src/config/vars.ts similarity index 61% rename from signer-service/src/config/vars.js rename to signer-service/src/config/vars.ts index d8728b7f..6b389baa 100644 --- a/signer-service/src/config/vars.js +++ b/signer-service/src/config/vars.ts @@ -1,11 +1,48 @@ -const path = require('path'); +import path from 'path'; +import dotenv from 'dotenv'; -// import .env variables -require('dotenv').config({ +dotenv.config({ path: path.join(__dirname, '../../.env'), }); -module.exports = { +interface QuoteProvider { + baseUrl: string; + appId?: string; + secretKey?: string; + partnerApiKey?: string; + apiKey?: string; +} + +interface GoogleCredentials { + email: string | undefined; + key: string | undefined; +} + +interface SpreadsheetConfig { + googleCredentials: GoogleCredentials; + storageSheetId: string | undefined; + emailSheetId: string | undefined; + ratingSheetId: string | undefined; +} + +interface Config { + env: string; + port: string | number; + amplitudeWss: string; + pendulumWss: string; + rateLimitMaxRequests: string | number; + rateLimitWindowMinutes: string | number; + rateLimitNumberOfProxies: string | number; + logs: string; + quoteProviders: { + alchemyPay: QuoteProvider; + transak: QuoteProvider; + moonpay: QuoteProvider; + }; + spreadsheet: SpreadsheetConfig; +} + +export const config: Config = { env: process.env.NODE_ENV || 'production', port: process.env.PORT || 3000, amplitudeWss: process.env.AMPLITUDE_WSS || 'wss://rpc-amplitude.pendulumchain.tech', diff --git a/signer-service/src/constants/constants.js b/signer-service/src/constants/constants.ts similarity index 96% rename from signer-service/src/constants/constants.js rename to signer-service/src/constants/constants.ts index 2dcb7bcc..d666557d 100644 --- a/signer-service/src/constants/constants.js +++ b/signer-service/src/constants/constants.ts @@ -10,7 +10,9 @@ const STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS = '2.5'; // Amount to send to the const PENDULUM_EPHEMERAL_STARTING_BALANCE_UNITS = '0.1'; // Amount to send to the new pendulum ephemeral account created const DEFAULT_LOGIN_EXPIRATION_TIME_HOURS = 7 * 24; -require('dotenv').config(); +import dotenv from 'dotenv'; + +dotenv.config(); const PENDULUM_FUNDING_SEED = process.env.PENDULUM_FUNDING_SEED; const FUNDING_SECRET = process.env.FUNDING_SECRET; @@ -18,7 +20,7 @@ const MOONBEAM_EXECUTOR_PRIVATE_KEY = process.env.MOONBEAM_EXECUTOR_PRIVATE_KEY; const SEP10_MASTER_SECRET = FUNDING_SECRET; const CLIENT_DOMAIN_SECRET = process.env.CLIENT_DOMAIN_SECRET; -module.exports = { +export { HORIZON_URL, PENDULUM_WSS, NETWORK, diff --git a/signer-service/src/constants/tokenConfig.js b/signer-service/src/constants/tokenConfig.js deleted file mode 100644 index 4359f28f..00000000 --- a/signer-service/src/constants/tokenConfig.js +++ /dev/null @@ -1,65 +0,0 @@ -const TOKEN_CONFIG = { - eurc: { - tomlFileUrl: 'https://circle.anchor.mykobo.co/.well-known/stellar.toml', - assetCode: 'EURC', - assetIssuer: 'GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2', - vaultAccountId: '6bsD97dS8ZyomMmp1DLCnCtx25oABtf19dypQKdZe6FBQXSm', - minWithdrawalAmount: '10000000000000', - maximumSubsidyAmountRaw: '1000000000000', // 1 unit - homeDomain: 'circle.anchor.mykobo.co', - clientDomainEnabled: true, - memoEnabled: false, - pendulumCurrencyId: { - Stellar: { - AlphaNum4: { - code: '0x45555243', - issuer: '0xcf4f5a26e2090bb3adcf02c7a9d73dbfe6659cc690461475b86437fa49c71136', - }, - }, - }, - }, - 'usdc.axl': { - pendulumCurrencyId: { XCM: 12 }, - decimals: 6, - maximumSubsidyAmountRaw: '1000000', // 1 unit - }, - usdc: { - pendulumCurrencyId: { XCM: 2 }, - decimals: 6, - maximumSubsidyAmountRaw: '1000000', // 1 unit - }, - ars: { - tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', - assetCode: 'ARS', - assetIssuer: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', - vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', - minWithdrawalAmount: '11000000000000', // 11 ARS. Anchor minimum limit. - maximumSubsidyAmountRaw: '100000000000000', // Defined by us: 100 unit ~ 0.1 USD @ Oct/2024 - homeDomain: 'api.anclap.com', - clientDomainEnabled: true, - memoEnabled: true, - pendulumCurrencyId: { - Stellar: { - AlphaNum4: { - code: '0x41525300', - issuer: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', - }, - }, - }, - }, -}; - -function getTokenConfigByAssetCode(config, assetCode) { - for (const key in config) { - if (config[key].assetCode === assetCode) { - return config[key]; - } - } - return undefined; -} - -function getPaddedAssetCode(assetCode) { - return assetCode.padEnd(4, '\0'); -} - -module.exports = { TOKEN_CONFIG, getTokenConfigByAssetCode, getPaddedAssetCode }; diff --git a/signer-service/src/constants/tokenConfig.ts b/signer-service/src/constants/tokenConfig.ts new file mode 100644 index 00000000..5fceab38 --- /dev/null +++ b/signer-service/src/constants/tokenConfig.ts @@ -0,0 +1,123 @@ +export interface StellarTokenConfig { + assetCode: string; + assetIssuer: string; + clientDomainEnabled: boolean; + homeDomain: string; + maximumSubsidyAmountRaw: string; + memoEnabled: boolean; + minWithdrawalAmount: string; + pendulumCurrencyId: { + Stellar: { + AlphaNum4: { + code: string; + issuer: string; + }; + }; + }; + tomlFileUrl: string; + vaultAccountId: string; +} + +export interface XCMTokenConfig { + decimals: number; + maximumSubsidyAmountRaw: string; + pendulumCurrencyId: { XCM: number }; +} + +export function isStellarTokenConfig(config: StellarTokenConfig | XCMTokenConfig): config is StellarTokenConfig { + return ( + 'assetCode' in config && + 'assetIssuer' in config && + 'clientDomainEnabled' in config && + 'homeDomain' in config && + 'maximumSubsidyAmountRaw' in config && + 'memoEnabled' in config && + 'minWithdrawalAmount' in config && + 'pendulumCurrencyId' in config && + 'tomlFileUrl' in config && + 'vaultAccountId' in config + ); +} + +export function isXCMTokenConfig(config: StellarTokenConfig | XCMTokenConfig): config is XCMTokenConfig { + return 'decimals' in config && 'maximumSubsidyAmountRaw' in config && 'pendulumCurrencyId' in config; +} + +export type TokenConfig = { + ars: StellarTokenConfig; + eurc: StellarTokenConfig; + usdc: XCMTokenConfig; + 'usdc.axl': XCMTokenConfig; +}; + +const eurc: StellarTokenConfig = { + tomlFileUrl: 'https://circle.anchor.mykobo.co/.well-known/stellar.toml', + assetCode: 'EURC', + assetIssuer: 'GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2', + vaultAccountId: '6bsD97dS8ZyomMmp1DLCnCtx25oABtf19dypQKdZe6FBQXSm', + minWithdrawalAmount: '10000000000000', + maximumSubsidyAmountRaw: '1000000000000', // 1 unit + homeDomain: 'circle.anchor.mykobo.co', + clientDomainEnabled: true, + memoEnabled: false, + pendulumCurrencyId: { + Stellar: { + AlphaNum4: { + code: '0x45555243', + issuer: '0xcf4f5a26e2090bb3adcf02c7a9d73dbfe6659cc690461475b86437fa49c71136', + }, + }, + }, +}; + +const ars: StellarTokenConfig = { + tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml', + assetCode: 'ARS', + assetIssuer: 'GCYE7C77EB5AWAA25R5XMWNI2EDOKTTFTTPZKM2SR5DI4B4WFD52DARS', + vaultAccountId: '6bE2vjpLRkRNoVDqDtzokxE34QdSJC2fz7c87R9yCVFFDNWs', + minWithdrawalAmount: '11000000000000', // 11 ARS. Anchor minimum limit. + maximumSubsidyAmountRaw: '100000000000000', // Defined by us: 100 unit ~ 0.1 USD @ Oct/2024 + homeDomain: 'api.anclap.com', + clientDomainEnabled: true, + memoEnabled: true, + pendulumCurrencyId: { + Stellar: { + AlphaNum4: { + code: '0x41525300', + issuer: '0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1', + }, + }, + }, +}; + +export const TOKEN_CONFIG: TokenConfig = { + ars, + eurc, + usdc: { + pendulumCurrencyId: { XCM: 2 }, + decimals: 6, + maximumSubsidyAmountRaw: '1000000', // 1 unit + }, + 'usdc.axl': { + pendulumCurrencyId: { XCM: 12 }, + decimals: 6, + maximumSubsidyAmountRaw: '1000000', // 1 unit + }, +}; + +export function getTokenConfigByAssetCode( + config: TokenConfig, + assetCode: string, +): StellarTokenConfig | XCMTokenConfig | undefined { + for (const key in config) { + const token = config[key as keyof TokenConfig]; + if ('assetCode' in token && token.assetCode === assetCode) { + return token; + } + } + return; +} + +export function getPaddedAssetCode(assetCode: string): string { + return assetCode.padEnd(4, '\0'); +} diff --git a/signer-service/src/index.js b/signer-service/src/index.js deleted file mode 100755 index 770e1df6..00000000 --- a/signer-service/src/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const { port, env } = require('./config/vars'); -const logger = require('./config/logger'); -const app = require('./config/express'); -require('dotenv').config(); - -const { - FUNDING_SECRET, - PENDULUM_FUNDING_SEED, - MOONBEAM_EXECUTOR_PRIVATE_KEY, - CLIENT_DOMAIN_SECRET, -} = require('./constants/constants'); - -//stop the application if the funding secret key is not set -if (!FUNDING_SECRET) { - logger.error('FUNDING_SECRET not set in the environment variables'); - process.exit(1); -} - -// stop the application if the Pendulum funding seed is not set -if (!PENDULUM_FUNDING_SEED) { - logger.error('PENDULUM_FUNDING_SEED not set in the environment variables'); - process.exit(1); -} - -// stop the application if the Moonbeam executor private key is not set -if (!MOONBEAM_EXECUTOR_PRIVATE_KEY) { - logger.error('MOONBEAM_EXECUTOR_PRIVATE_KEY not set in the environment variables'); - process.exit(1); -} - -if (!CLIENT_DOMAIN_SECRET) { - logger.error('CLIENT_DOMAIN_SECRET not set in the environment variables'); - process.exit(1); -} - -// listen to requests -app.listen(port, () => logger.info(`server started on port ${port} (${env})`)); - -/** - * Exports express - * @public - */ -module.exports = app; diff --git a/signer-service/src/index.ts b/signer-service/src/index.ts new file mode 100755 index 00000000..d90c4868 --- /dev/null +++ b/signer-service/src/index.ts @@ -0,0 +1,44 @@ +import dotenv from 'dotenv'; + +import { config } from './config/vars'; +import logger from './config/logger'; +import app from './config/express'; +import { + FUNDING_SECRET, + PENDULUM_FUNDING_SEED, + MOONBEAM_EXECUTOR_PRIVATE_KEY, + CLIENT_DOMAIN_SECRET, +} from './constants/constants'; + +const { port, env } = config; + +dotenv.config(); + +// Consider grouping all environment checks into a single function +const validateRequiredEnvVars = () => { + const requiredVars = { + FUNDING_SECRET, + PENDULUM_FUNDING_SEED, + MOONBEAM_EXECUTOR_PRIVATE_KEY, + CLIENT_DOMAIN_SECRET, + }; + + for (const [key, value] of Object.entries(requiredVars)) { + if (!value) { + logger.error(`${key} not set in the environment variables`); + process.exit(1); + } + } +}; + +// Validate environment variables before starting the server +validateRequiredEnvVars(); + +// listen to requests +app.listen(port, () => logger.info(`server started on port ${port} (${env})`)); + +/** + * Exports express + * @public + */ +export default app; diff --git a/signer-service/tsconfig.json b/signer-service/tsconfig.json new file mode 100644 index 00000000..ef2fec6f --- /dev/null +++ b/signer-service/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/signer-service/yarn.lock b/signer-service/yarn.lock index cb59178a..dc64b6b0 100644 --- a/signer-service/yarn.lock +++ b/signer-service/yarn.lock @@ -58,6 +58,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.10.2": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 + languageName: node + linkType: hard + "@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": version: 1.6.0 resolution: "@colors/colors@npm:1.6.0" @@ -141,6 +150,175 @@ __metadata: languageName: node linkType: hard +"@napi-rs/nice-android-arm-eabi@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-android-arm-eabi@npm:1.0.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/nice-android-arm64@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-android-arm64@npm:1.0.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/nice-darwin-arm64@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-darwin-arm64@npm:1.0.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/nice-darwin-x64@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-darwin-x64@npm:1.0.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/nice-freebsd-x64@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-freebsd-x64@npm:1.0.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/nice-linux-arm-gnueabihf@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-arm-gnueabihf@npm:1.0.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/nice-linux-arm64-gnu@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-arm64-gnu@npm:1.0.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/nice-linux-arm64-musl@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-arm64-musl@npm:1.0.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/nice-linux-ppc64-gnu@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-ppc64-gnu@npm:1.0.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/nice-linux-riscv64-gnu@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-riscv64-gnu@npm:1.0.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/nice-linux-s390x-gnu@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-s390x-gnu@npm:1.0.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/nice-linux-x64-gnu@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-x64-gnu@npm:1.0.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/nice-linux-x64-musl@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-linux-x64-musl@npm:1.0.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/nice-win32-arm64-msvc@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-win32-arm64-msvc@npm:1.0.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/nice-win32-ia32-msvc@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-win32-ia32-msvc@npm:1.0.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@napi-rs/nice-win32-x64-msvc@npm:1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice-win32-x64-msvc@npm:1.0.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/nice@npm:^1.0.1": + version: 1.0.1 + resolution: "@napi-rs/nice@npm:1.0.1" + dependencies: + "@napi-rs/nice-android-arm-eabi": "npm:1.0.1" + "@napi-rs/nice-android-arm64": "npm:1.0.1" + "@napi-rs/nice-darwin-arm64": "npm:1.0.1" + "@napi-rs/nice-darwin-x64": "npm:1.0.1" + "@napi-rs/nice-freebsd-x64": "npm:1.0.1" + "@napi-rs/nice-linux-arm-gnueabihf": "npm:1.0.1" + "@napi-rs/nice-linux-arm64-gnu": "npm:1.0.1" + "@napi-rs/nice-linux-arm64-musl": "npm:1.0.1" + "@napi-rs/nice-linux-ppc64-gnu": "npm:1.0.1" + "@napi-rs/nice-linux-riscv64-gnu": "npm:1.0.1" + "@napi-rs/nice-linux-s390x-gnu": "npm:1.0.1" + "@napi-rs/nice-linux-x64-gnu": "npm:1.0.1" + "@napi-rs/nice-linux-x64-musl": "npm:1.0.1" + "@napi-rs/nice-win32-arm64-msvc": "npm:1.0.1" + "@napi-rs/nice-win32-ia32-msvc": "npm:1.0.1" + "@napi-rs/nice-win32-x64-msvc": "npm:1.0.1" + dependenciesMeta: + "@napi-rs/nice-android-arm-eabi": + optional: true + "@napi-rs/nice-android-arm64": + optional: true + "@napi-rs/nice-darwin-arm64": + optional: true + "@napi-rs/nice-darwin-x64": + optional: true + "@napi-rs/nice-freebsd-x64": + optional: true + "@napi-rs/nice-linux-arm-gnueabihf": + optional: true + "@napi-rs/nice-linux-arm64-gnu": + optional: true + "@napi-rs/nice-linux-arm64-musl": + optional: true + "@napi-rs/nice-linux-ppc64-gnu": + optional: true + "@napi-rs/nice-linux-riscv64-gnu": + optional: true + "@napi-rs/nice-linux-s390x-gnu": + optional: true + "@napi-rs/nice-linux-x64-gnu": + optional: true + "@napi-rs/nice-linux-x64-musl": + optional: true + "@napi-rs/nice-win32-arm64-msvc": + optional: true + "@napi-rs/nice-win32-ia32-msvc": + optional: true + "@napi-rs/nice-win32-x64-msvc": + optional: true + checksum: 10/ae265aa365b325830115c1cda49b05ea05e6f1163944a1485c0643c9552380cd32a2aaf12b326f353538ca6244222963eb2e9767a4713c9432eadecd027f90ea + languageName: node + linkType: hard + "@noble/curves@npm:1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" @@ -173,6 +351,33 @@ __metadata: languageName: node linkType: hard +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10/6ab2a9b8a1d67b067922c36f259e3b3dfd6b97b219c540877a4944549a4d49ea5ceba5663905ab5289682f1f3c15ff441d02f0447f620a42e1cb5e1937174d4b + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10/012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10/40033e33e96e97d77fba5a238e4bba4487b8284678906a9f616b5579ddaf868a18874c0054a75402c9fbaaa033a25ceae093af58c9c30278e35c23c9479e79b0 + languageName: node + linkType: hard + "@npmcli/agent@npm:^2.0.0": version: 2.2.2 resolution: "@npmcli/agent@npm:2.2.2" @@ -195,11 +400,78 @@ __metadata: languageName: node linkType: hard +"@open-web3/api-mobx@npm:^1.1.4": + version: 1.1.4 + resolution: "@open-web3/api-mobx@npm:1.1.4" + dependencies: + mobx: "npm:^5.15.7" + mobx-utils: "npm:^5.6.2" + peerDependencies: + "@polkadot/api": ">6.3.1" + checksum: 10/aec1630b4f47fa09ce75764e0cedb5bed1163507dbf95e393b550bc5b90185d6899fba3969f577c601b008fef7852e1e74c642ab30ef7c2241738a23638d4db4 + languageName: node + linkType: hard + +"@open-web3/orml-type-definitions@npm:1.1.4, @open-web3/orml-type-definitions@npm:^1.1.4": + version: 1.1.4 + resolution: "@open-web3/orml-type-definitions@npm:1.1.4" + dependencies: + lodash.merge: "npm:^4.6.2" + checksum: 10/a57d7b29aa9110ddb7d4d5fd0613b248ce5883fbf879fa8d510418f0339de4431f85070b21c4be25f51d7a3c1731063c2f60e547177e9d0285fb894e5c77f78e + languageName: node + linkType: hard + +"@open-web3/orml-types@npm:^1.1.4": + version: 1.1.4 + resolution: "@open-web3/orml-types@npm:1.1.4" + dependencies: + "@open-web3/orml-type-definitions": "npm:1.1.4" + peerDependencies: + "@polkadot/api": ">6.3.1" + checksum: 10/3d0aabef48e89f7c6ccee259fe403dc07d92eab19b7523104fa8a393a58c5433106a7e3add6af160505e964cd0324408d4f2996555444b8d5191708b6cbfd54c + languageName: node + linkType: hard + +"@pendulum-chain/type-definitions@npm:1.1.1": + version: 1.1.1 + resolution: "@pendulum-chain/type-definitions@npm:1.1.1" + dependencies: + "@babel/runtime": "npm:^7.10.2" + "@open-web3/orml-type-definitions": "npm:^1.1.4" + checksum: 10/bd861df826ea6e9b8d99d9a899a5c1b9d200d4078244fb1eee37856c1f9849df81b94e51d7c03c75581c118bb598f372dd0a7f27b31bcadb2ee2650ef005a568 + languageName: node + linkType: hard + +"@pendulum-chain/types@npm:^1.1.1": + version: 1.1.1 + resolution: "@pendulum-chain/types@npm:1.1.1" + dependencies: + "@babel/runtime": "npm:^7.10.2" + "@open-web3/api-mobx": "npm:^1.1.4" + "@open-web3/orml-types": "npm:^1.1.4" + "@pendulum-chain/type-definitions": "npm:1.1.1" + peerDependencies: + "@polkadot/api": ">=8" + checksum: 10/789595c0e37784b7e7b134b9cd8e75c837f6415f274d79e8b6822c702ad3ad2b7d979767a4791a6419658ad9b7035991b6cd46ced99816e64d0e3c2f9b39b6d5 + languageName: node + linkType: hard + "@pendulum-network/token-api@workspace:.": version: 0.0.0-use.local resolution: "@pendulum-network/token-api@workspace:." dependencies: + "@pendulum-chain/types": "npm:^1.1.1" "@polkadot/api": "npm:^13.2.1" + "@stellar/stellar-sdk": "npm:^13.1.0" + "@swc/cli": "npm:^0.5.2" + "@swc/core": "npm:^1.10.4" + "@types/body-parser": "npm:^1.19.5" + "@types/compression": "npm:^1.7.5" + "@types/cookie-parser": "npm:^1.4.8" + "@types/cors": "npm:^2.8.17" + "@types/express": "npm:^5.0.0" + "@types/method-override": "npm:^3.0.0" + "@types/morgan": "npm:^1.9.9" big.js: "npm:^6.2.1" body-parser: "npm:^1.17.0" compression: "npm:^1.6.2" @@ -228,7 +500,8 @@ __metadata: nodemon: "npm:^2.0.1" prettier: "npm:^2.8.7" siwe: "npm:^2.3.2" - stellar-sdk: "npm:^11.3.0" + stellar-sdk: "npm:^13.1.0" + typescript: "npm:^5.7.2" viem: "npm:^2.21.3" winston: "npm:^3.1.0" languageName: unknown @@ -753,6 +1026,13 @@ __metadata: languageName: node linkType: hard +"@sec-ant/readable-stream@npm:^0.4.1": + version: 0.4.1 + resolution: "@sec-ant/readable-stream@npm:0.4.1" + checksum: 10/aac89581652ac85debe7c5303451c2ebf8bf25ca25db680e4b9b73168f6940616d9a4bbe3348981827b1159b14e2f2e6af4b7bd5735cac898c12d5c51909c102 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.5": version: 4.1.5 resolution: "@sideway/address@npm:4.1.5" @@ -776,6 +1056,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/is@npm:^5.2.0": + version: 5.6.0 + resolution: "@sindresorhus/is@npm:5.6.0" + checksum: 10/b077c325acec98e30f7d86df158aaba2e7af2acb9bb6a00fda4b91578539fbff4ecebe9b934e24fec0e6950de3089d89d79ec02d9062476b20ce185be0e01bd6 + languageName: node + linkType: hard + "@spruceid/siwe-parser@npm:^2.1.2": version: 2.1.2 resolution: "@spruceid/siwe-parser@npm:2.1.2" @@ -821,28 +1108,44 @@ __metadata: languageName: node linkType: hard -"@stellar/js-xdr@npm:^3.1.1": +"@stellar/js-xdr@npm:^3.1.2": version: 3.1.2 resolution: "@stellar/js-xdr@npm:3.1.2" checksum: 10/96b5c52088bb2f2cc11a04ee1766ceb9431bdb0195058b9bc8d60cd1459772c2be6a9aa1bf69ba8b7589cfaf71beef96890e60d7fd7de0332c11ae1917f95440 languageName: node linkType: hard -"@stellar/stellar-base@npm:^11.0.1": - version: 11.0.1 - resolution: "@stellar/stellar-base@npm:11.0.1" +"@stellar/stellar-base@npm:^13.0.1": + version: 13.0.1 + resolution: "@stellar/stellar-base@npm:13.0.1" dependencies: - "@stellar/js-xdr": "npm:^3.1.1" + "@stellar/js-xdr": "npm:^3.1.2" base32.js: "npm:^0.1.0" bignumber.js: "npm:^9.1.2" buffer: "npm:^6.0.3" sha.js: "npm:^2.3.6" - sodium-native: "npm:^4.0.10" + sodium-native: "npm:^4.3.0" tweetnacl: "npm:^1.0.3" dependenciesMeta: sodium-native: optional: true - checksum: 10/03ad775791793548563f4dfecc62843d84f0fcf41ccc7f3bb30090dd0506061fb38ca505fbb4ebd8b97d48a06c026733fa7b1146dd04216f3363843f8d09f0f3 + checksum: 10/58cafe97521b8a87ac673a31abf8dccf7ca55cf06cc89821d30aece810919ac5949043d6b581c1cbf03da89d23724d8129c517e7ebf6721397c56bdca77ec5e5 + languageName: node + linkType: hard + +"@stellar/stellar-sdk@npm:^13.1.0": + version: 13.1.0 + resolution: "@stellar/stellar-sdk@npm:13.1.0" + dependencies: + "@stellar/stellar-base": "npm:^13.0.1" + axios: "npm:^1.7.9" + bignumber.js: "npm:^9.1.2" + eventsource: "npm:^2.0.2" + feaxios: "npm:^0.0.23" + randombytes: "npm:^2.1.0" + toml: "npm:^3.0.0" + urijs: "npm:^1.19.1" + checksum: 10/c11c958506d49d14acee23fcb2a741df74d4910002f37ee0b8669b9f66e8f373de2f2c70df41ccefcd7a9fdc72acfee1375ac23a76b8eb5d177c0eed28d18581 languageName: node linkType: hard @@ -896,6 +1199,181 @@ __metadata: languageName: node linkType: hard +"@swc/cli@npm:^0.5.2": + version: 0.5.2 + resolution: "@swc/cli@npm:0.5.2" + dependencies: + "@swc/counter": "npm:^0.1.3" + "@xhmikosr/bin-wrapper": "npm:^13.0.5" + commander: "npm:^8.3.0" + fast-glob: "npm:^3.2.5" + minimatch: "npm:^9.0.3" + piscina: "npm:^4.3.1" + semver: "npm:^7.3.8" + slash: "npm:3.0.0" + source-map: "npm:^0.7.3" + peerDependencies: + "@swc/core": ^1.2.66 + chokidar: ^3.5.1 + peerDependenciesMeta: + chokidar: + optional: true + bin: + spack: bin/spack.js + swc: bin/swc.js + swcx: bin/swcx.js + checksum: 10/f5cefcd1c7f79d12d49a1e8bc9fd23bdf604510ab7d122f836f3bd8483618239295017041b2dfb34f5398875a41a2d79cdc83f85f798c283d800cc02011371ac + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-darwin-arm64@npm:1.10.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-darwin-x64@npm:1.10.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.4" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-linux-arm64-gnu@npm:1.10.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-linux-arm64-musl@npm:1.10.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-linux-x64-gnu@npm:1.10.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-linux-x64-musl@npm:1.10.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-win32-arm64-msvc@npm:1.10.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-win32-ia32-msvc@npm:1.10.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.10.4": + version: 1.10.4 + resolution: "@swc/core-win32-x64-msvc@npm:1.10.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.10.4": + version: 1.10.4 + resolution: "@swc/core@npm:1.10.4" + dependencies: + "@swc/core-darwin-arm64": "npm:1.10.4" + "@swc/core-darwin-x64": "npm:1.10.4" + "@swc/core-linux-arm-gnueabihf": "npm:1.10.4" + "@swc/core-linux-arm64-gnu": "npm:1.10.4" + "@swc/core-linux-arm64-musl": "npm:1.10.4" + "@swc/core-linux-x64-gnu": "npm:1.10.4" + "@swc/core-linux-x64-musl": "npm:1.10.4" + "@swc/core-win32-arm64-msvc": "npm:1.10.4" + "@swc/core-win32-ia32-msvc": "npm:1.10.4" + "@swc/core-win32-x64-msvc": "npm:1.10.4" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.17" + peerDependencies: + "@swc/helpers": "*" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/3b5580b702dcec23912ec2b6472f55887006918865d960a08866e3342139af014d762223ca047bfb6f0277820835ea1f3073936a0e0e3bc86046a7335e22ca98 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.17": + version: 0.1.17 + resolution: "@swc/types@npm:0.1.17" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10/ddef1ad5bfead3acdfc41f14e79ba43a99200eb325afbad5716058dbe36358b0513400e9f22aff32432be84a98ae93df95a20b94192f69b8687144270e4eaa18 + languageName: node + linkType: hard + +"@szmarczak/http-timer@npm:^5.0.1": + version: 5.0.1 + resolution: "@szmarczak/http-timer@npm:5.0.1" + dependencies: + defer-to-connect: "npm:^2.0.1" + checksum: 10/fc9cb993e808806692e4a3337c90ece0ec00c89f4b67e3652a356b89730da98bc824273a6d67ca84d5f33cd85f317dcd5ce39d8cc0a2f060145a608a7cb8ce92 + languageName: node + linkType: hard + +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 10/889c1f1e63ac7c92c0ea22d4a2861142f1b43c3d92eb70ec42aa9e9851fab2e9952211d50f541b287781280df2f979bf5600a9c1f91fbc61b7fcf9994e9376a5 + languageName: node + linkType: hard + "@types/bn.js@npm:^5.1.6": version: 5.1.6 resolution: "@types/bn.js@npm:5.1.6" @@ -905,6 +1383,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.5": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10/1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 + languageName: node + linkType: hard + "@types/bson@npm:*, @types/bson@npm:1.x || 4.0.x": version: 4.0.5 resolution: "@types/bson@npm:4.0.5" @@ -914,6 +1402,80 @@ __metadata: languageName: node linkType: hard +"@types/compression@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/compression@npm:1.7.5" + dependencies: + "@types/express": "npm:*" + checksum: 10/9d65485bfb8e20035dedbbbebea06ea0c0554ac7bedb11ec3f2c6582cb082d121a10a03b4803b56315ae31705043eddf97eaf49c3dad6f9e39f7875b27c8feb4 + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10/7eb1bc5342a9604facd57598a6c62621e244822442976c443efb84ff745246b10d06e8b309b6e80130026a396f19bf6793b7cecd7380169f369dac3bfc46fb99 + languageName: node + linkType: hard + +"@types/cookie-parser@npm:^1.4.8": + version: 1.4.8 + resolution: "@types/cookie-parser@npm:1.4.8" + peerDependencies: + "@types/express": "*" + checksum: 10/b26ace5560adce1a746c059a75f18227ee3e3614a49c70dd29b8af204696079ced22eaa9d6cb084e3433ec433bff845cb47ffb890ae9af96a4aab2ff85e40da7 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.17": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" + dependencies: + "@types/node": "npm:*" + checksum: 10/469bd85e29a35977099a3745c78e489916011169a664e97c4c3d6538143b0a16e4cc72b05b407dc008df3892ed7bf595f9b7c0f1f4680e169565ee9d64966bde + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.0.3 + resolution: "@types/express-serve-static-core@npm:5.0.3" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10/7f5d0e09e1aec7d21ca7afe949c04b2649cee0cbe1a62208287f42748c3bf7fe65346524b1b30fcaa7fbd8c3dc4bcf88a8dc491bd91a156bae83104190a147ab + languageName: node + linkType: hard + +"@types/express@npm:*, @types/express@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/express@npm:5.0.0" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^5.0.0" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10/45b199ab669caa33e6badafeebf078e277ea95042309d325a04b1ec498f33d33fd5a4ae9c8e358342367b178fe454d7323c5dfc8002bf27070b210a2c6cc11f0 + languageName: node + linkType: hard + +"@types/http-cache-semantics@npm:^4.0.2": + version: 4.0.4 + resolution: "@types/http-cache-semantics@npm:4.0.4" + checksum: 10/a59566cff646025a5de396d6b3f44a39ab6a74f2ed8150692e0f31cc52f3661a68b04afe3166ebe0d566bd3259cb18522f46e949576d5204781cd6452b7fe0c5 + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 10/1f3d7c3b32c7524811a45690881736b3ef741bf9849ae03d32ad1ab7062608454b150a4e7f1351f83d26a418b2d65af9bdc06198f1c079d75578282884c4e8e3 + languageName: node + linkType: hard + "@types/joi@npm:^14.3.3": version: 14.3.4 resolution: "@types/joi@npm:14.3.4" @@ -928,6 +1490,22 @@ __metadata: languageName: node linkType: hard +"@types/method-override@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/method-override@npm:3.0.0" + peerDependencies: + "@types/express": "*" + checksum: 10/219c23e4fc114f26fd08f1d0c46a7a4850c6f863d94c2ca62655ca0936f7eab4fb29a5c29ffa08d6961ea5248ee61a6eebca197587510d79b53b1e0e7411bead + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10/e29a5f9c4776f5229d84e525b7cd7dd960b51c30a0fb9a028c0821790b82fca9f672dab56561e2acd9e8eed51d431bde52eafdfef30f643586c4162f1aecfc78 + languageName: node + linkType: hard + "@types/mongodb@npm:^3.5.27": version: 3.6.20 resolution: "@types/mongodb@npm:3.6.20" @@ -938,6 +1516,15 @@ __metadata: languageName: node linkType: hard +"@types/morgan@npm:^1.9.9": + version: 1.9.9 + resolution: "@types/morgan@npm:1.9.9" + dependencies: + "@types/node": "npm:*" + checksum: 10/a6969b4494de964d6b682fce93decd51988ec15a69be0d2766adacd5e942e06d9b4a5cad1708d6698b242edc5dee76ec6f909b81d68e148eb0634a0b6a4efb66 + languageName: node + linkType: hard + "@types/node@npm:*": version: 22.9.0 resolution: "@types/node@npm:22.9.0" @@ -963,6 +1550,41 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.9.17 + resolution: "@types/qs@npm:6.9.17" + checksum: 10/fc3beda0be70e820ddabaa361e8dfec5e09b482b8f6cf1515615479a027dd06cd5ba0ffbd612b654c2605523f45f484c8905a475623d6cd0c4cadcf5d0c517f5 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10/95640233b689dfbd85b8c6ee268812a732cf36d5affead89e806fe30da9a430767af8ef2cd661024fd97e19d61f3dec75af2df5e80ec3bea000019ab7028629a + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10/28320a2aa1eb704f7d96a65272a07c0bf3ae7ed5509c2c96ea5e33238980f71deeed51d3631927a77d5250e4091b3e66bce53b42d770873282c6a20bb8b0280d + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.7 + resolution: "@types/serve-static@npm:1.15.7" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:*" + checksum: 10/c5a7171d5647f9fbd096ed1a26105759f3153ccf683824d99fee4c7eb9cde2953509621c56a070dd9fb1159e799e86d300cbe4e42245ebc5b0c1767e8ca94a67 + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.5 resolution: "@types/triple-beam@npm:1.3.5" @@ -970,6 +1592,124 @@ __metadata: languageName: node linkType: hard +"@xhmikosr/archive-type@npm:^7.0.0": + version: 7.0.0 + resolution: "@xhmikosr/archive-type@npm:7.0.0" + dependencies: + file-type: "npm:^19.0.0" + checksum: 10/ae6b388ccb6ec746f85a674a47745c910df43088879870f8364ea3da37dc9196ef28c80185793cc70de17565380f146fb0bc098228656097559e40fe61150a2f + languageName: node + linkType: hard + +"@xhmikosr/bin-check@npm:^7.0.3": + version: 7.0.3 + resolution: "@xhmikosr/bin-check@npm:7.0.3" + dependencies: + execa: "npm:^5.1.1" + isexe: "npm:^2.0.0" + checksum: 10/5424fb828020ed71d52a62f48f64d87d0604e7f1a0b04d3acae3ce1fbfe75db31ab3d6741d7e89efe2f86584a1db6fb783d1e19201e0ab95fe060ccfbd60fe07 + languageName: node + linkType: hard + +"@xhmikosr/bin-wrapper@npm:^13.0.5": + version: 13.0.5 + resolution: "@xhmikosr/bin-wrapper@npm:13.0.5" + dependencies: + "@xhmikosr/bin-check": "npm:^7.0.3" + "@xhmikosr/downloader": "npm:^15.0.1" + "@xhmikosr/os-filter-obj": "npm:^3.0.0" + bin-version-check: "npm:^5.1.0" + checksum: 10/7b634f533bb266c44f50f1bf746d6238bdcf1abd91665e34ca57756f07d0bac74aeb70f41685e23cd71407f4844fb7a13999ed03a8b43199535dd6168d9c4967 + languageName: node + linkType: hard + +"@xhmikosr/decompress-tar@npm:^8.0.1": + version: 8.0.1 + resolution: "@xhmikosr/decompress-tar@npm:8.0.1" + dependencies: + file-type: "npm:^19.0.0" + is-stream: "npm:^2.0.1" + tar-stream: "npm:^3.1.7" + checksum: 10/fb2133ba49a064e56c3c4546719385a0579eec7608ad0db0cf59abb15afe47941be047646b57da98d557c8ffc8819b2f46500c9b4a8e916fa451d4d47de2d9ee + languageName: node + linkType: hard + +"@xhmikosr/decompress-tarbz2@npm:^8.0.1": + version: 8.0.1 + resolution: "@xhmikosr/decompress-tarbz2@npm:8.0.1" + dependencies: + "@xhmikosr/decompress-tar": "npm:^8.0.1" + file-type: "npm:^19.0.0" + is-stream: "npm:^2.0.1" + seek-bzip: "npm:^2.0.0" + unbzip2-stream: "npm:^1.4.3" + checksum: 10/9e4be6b256aa2ea1563af5e58cbb1c6d67d5dde7cf6976607b50e3a64e2efef92c10afa1581c6febf84781275dddf2120aeaf2d3fade6e36de10f9bd0e4131a0 + languageName: node + linkType: hard + +"@xhmikosr/decompress-targz@npm:^8.0.1": + version: 8.0.1 + resolution: "@xhmikosr/decompress-targz@npm:8.0.1" + dependencies: + "@xhmikosr/decompress-tar": "npm:^8.0.1" + file-type: "npm:^19.0.0" + is-stream: "npm:^2.0.1" + checksum: 10/0c0573b96bcb81534750ccb63e51c8a287532e524cefcc07abb11e40971df32ff14a5f9b4f85602184fa6ad97c7ea61ab1d41b38a328c52ce8bdcb2e63e742ce + languageName: node + linkType: hard + +"@xhmikosr/decompress-unzip@npm:^7.0.0": + version: 7.0.0 + resolution: "@xhmikosr/decompress-unzip@npm:7.0.0" + dependencies: + file-type: "npm:^19.0.0" + get-stream: "npm:^6.0.1" + yauzl: "npm:^3.1.2" + checksum: 10/d8e75d32efbe4dbde62654a57ed1a958260fc3c469e4ac36c10f2b6e78b478c45f0d79f83a4648e7f203da60b890b18a992d645a9a61c11e11bb79a47efc1b9e + languageName: node + linkType: hard + +"@xhmikosr/decompress@npm:^10.0.1": + version: 10.0.1 + resolution: "@xhmikosr/decompress@npm:10.0.1" + dependencies: + "@xhmikosr/decompress-tar": "npm:^8.0.1" + "@xhmikosr/decompress-tarbz2": "npm:^8.0.1" + "@xhmikosr/decompress-targz": "npm:^8.0.1" + "@xhmikosr/decompress-unzip": "npm:^7.0.0" + graceful-fs: "npm:^4.2.11" + make-dir: "npm:^4.0.0" + strip-dirs: "npm:^3.0.0" + checksum: 10/cbf8e572ff044a790d3649c0264769129f6bb8e24e4f9a2c76a5e9ee88e06cc8c150c03e0ed0f7a7df646025f0ec481bda2e8e6b13e86bbe5bb8e8280a61eb26 + languageName: node + linkType: hard + +"@xhmikosr/downloader@npm:^15.0.1": + version: 15.0.1 + resolution: "@xhmikosr/downloader@npm:15.0.1" + dependencies: + "@xhmikosr/archive-type": "npm:^7.0.0" + "@xhmikosr/decompress": "npm:^10.0.1" + content-disposition: "npm:^0.5.4" + defaults: "npm:^3.0.0" + ext-name: "npm:^5.0.0" + file-type: "npm:^19.0.0" + filenamify: "npm:^6.0.0" + get-stream: "npm:^6.0.1" + got: "npm:^13.0.0" + checksum: 10/15407be257fa9c97f076852a2444d2c2de0f77e64e5263fb553bf7e3bdacec93ee05346cce075dfd2811be34c07ec4d69b0b0c56ebb7f97804f9c53f06355436 + languageName: node + linkType: hard + +"@xhmikosr/os-filter-obj@npm:^3.0.0": + version: 3.0.0 + resolution: "@xhmikosr/os-filter-obj@npm:3.0.0" + dependencies: + arch: "npm:^3.0.0" + checksum: 10/8ec5e94e0a9f612b22997fb6cd2e82b121e03106ed0cf3404163b54c11812278e1f350d67af42d7c7093451c99d9755a6d6a284d8dae27b2b262790237e74193 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -1154,6 +1894,13 @@ __metadata: languageName: node linkType: hard +"arch@npm:^3.0.0": + version: 3.0.0 + resolution: "arch@npm:3.0.0" + checksum: 10/9af0c58900980c300737945881859df6dd2a4e4d07f697c77704a7ba85a701aa60aa7c3a3ce1eb57ef76fda726ebccf1e2a9ddd763c89fe82c961d55b4b9c374 + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -1293,7 +2040,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.8, axios@npm:^1.7.7": +"axios@npm:^1.7.7": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -1304,6 +2051,24 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.9": + 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 + +"b4a@npm:^1.6.4": + version: 1.6.7 + resolution: "b4a@npm:1.6.7" + checksum: 10/1ac056e3bce378d4d3e570e57319360a9d3125ab6916a1921b95bea33d9ee646698ebc75467561fd6fcc80ff697612124c89bb9b95e80db94c6dc23fcb977705 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -1311,6 +2076,13 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.2.0": + version: 2.5.2 + resolution: "bare-events@npm:2.5.2" + checksum: 10/479f3f062ce8a2159c4a0a47417f441579e7dee4b9a118253466de718d6506b2a0ebadb0948f0819d4a7e228ba1ddb8189829f21928586b459e1dce7ce48705e + languageName: node + linkType: hard + "base32.js@npm:^0.1.0": version: 0.1.0 resolution: "base32.js@npm:0.1.0" @@ -1348,6 +2120,27 @@ __metadata: languageName: node linkType: hard +"bin-version-check@npm:^5.1.0": + version: 5.1.0 + resolution: "bin-version-check@npm:5.1.0" + dependencies: + bin-version: "npm:^6.0.0" + semver: "npm:^7.5.3" + semver-truncate: "npm:^3.0.0" + checksum: 10/d99679cfe0964703045fe0145a98f117888942b621dfe2c2377305ee9a9d735374d8e3ecb3b476507b284af2567699f24f7ecb2feb1f27ad6086ad60b3198893 + languageName: node + linkType: hard + +"bin-version@npm:^6.0.0": + version: 6.0.0 + resolution: "bin-version@npm:6.0.0" + dependencies: + execa: "npm:^5.0.0" + find-versions: "npm:^5.0.0" + checksum: 10/78c29422ea9597eb4c8d4f0eff96df60d09aa82b53a87925bc403efbe5c55251b1a07baac538381d9096377f92d27e3c03963efa86db5bc0d6431b9563946229 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -1436,7 +2229,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:~3.0.2": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -1459,6 +2252,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 10/06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + "buffer-equal-constant-time@npm:1.0.1": version: 1.0.1 resolution: "buffer-equal-constant-time@npm:1.0.1" @@ -1466,6 +2266,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.2.1": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 + languageName: node + linkType: hard + "buffer@npm:^6.0.3": version: 6.0.3 resolution: "buffer@npm:6.0.3" @@ -1503,6 +2313,28 @@ __metadata: languageName: node linkType: hard +"cacheable-lookup@npm:^7.0.0": + version: 7.0.0 + resolution: "cacheable-lookup@npm:7.0.0" + checksum: 10/69ea78cd9f16ad38120372e71ba98b64acecd95bbcbcdad811f857dc192bad81ace021f8def012ce19178583db8d46afd1a00b3e8c88527e978e049edbc23252 + languageName: node + linkType: hard + +"cacheable-request@npm:^10.2.8": + version: 10.2.14 + resolution: "cacheable-request@npm:10.2.14" + dependencies: + "@types/http-cache-semantics": "npm:^4.0.2" + get-stream: "npm:^6.0.1" + http-cache-semantics: "npm:^4.1.1" + keyv: "npm:^4.5.3" + mimic-response: "npm:^4.0.0" + normalize-url: "npm:^8.0.0" + responselike: "npm:^3.0.0" + checksum: 10/102f454ac68eb66f99a709c5cf65e90ed89f1b9269752578d5a08590b3986c3ea47a5d9dff208fe7b65855a29da129a2f23321b88490106898e0ba70b807c912 + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -1698,6 +2530,20 @@ __metadata: languageName: node linkType: hard +"commander@npm:^6.0.0": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e + languageName: node + linkType: hard + +"commander@npm:^8.3.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 10/6b7b5d334483ce24bd73c5dac2eab901a7dbb25fd983ea24a1eeac6e7166bb1967f641546e8abf1920afbde86a45fbfe5812fbc69d0dc451bb45ca416a12a3a3 + languageName: node + linkType: hard + "compressible@npm:~2.0.18": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -1736,6 +2582,15 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:^0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10/b7f4ce176e324f19324be69b05bf6f6e411160ac94bc523b782248129eb1ef3be006f6cff431aaea5e337fe5d176ce8830b8c2a1b721626ead8933f0cbe78720 + languageName: node + linkType: hard + "content-disposition@npm:^1.0.0": version: 1.0.0 resolution: "content-disposition@npm:1.0.0" @@ -1855,6 +2710,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.3": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10/0d52657d7ae36eb130999dffff1168ec348687b48dd38e2ff59992ed916c88d328cf1d07ff4a4a10bc78de5e1c23f04b306d569e42f7a2293915c081e4dfee86 + languageName: node + linkType: hard + "data-uri-to-buffer@npm:^4.0.0": version: 4.0.1 resolution: "data-uri-to-buffer@npm:4.0.1" @@ -1962,6 +2828,15 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10/d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -1969,6 +2844,20 @@ __metadata: languageName: node linkType: hard +"defaults@npm:^3.0.0": + version: 3.0.0 + resolution: "defaults@npm:3.0.0" + checksum: 10/656215c738993a43e436bfbbe8d5aaa3d029e1a3d19fc48c98aba416270eec8d69c194d28d6aaba728366afeadccc923bf2d168613be066fa83a3e232335e65a + languageName: node + linkType: hard + +"defer-to-connect@npm:^2.0.1": + version: 2.0.1 + resolution: "defer-to-connect@npm:2.0.1" + checksum: 10/8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b + languageName: node + linkType: hard + "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" @@ -2576,6 +3465,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^5.0.0, execa@npm:^5.1.1": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^6.0.0" + human-signals: "npm:^2.1.0" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.1" + onetime: "npm:^5.1.2" + signal-exit: "npm:^3.0.3" + strip-final-newline: "npm:^2.0.0" + checksum: 10/8ada91f2d70f7dff702c861c2c64f21dfdc1525628f3c0454fd6f02fce65f7b958616cbd2b99ca7fa4d474e461a3d363824e91b3eb881705231abbf387470597 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2644,6 +3550,25 @@ __metadata: languageName: node linkType: hard +"ext-list@npm:^2.0.0": + version: 2.2.2 + resolution: "ext-list@npm:2.2.2" + dependencies: + mime-db: "npm:^1.28.0" + checksum: 10/fe69fedbef044e14d4ce9e84c6afceb696ba71500c15b8d0ce0a1e280237e17c95031b3d62d5e597652fea0065b9bf957346b3900d989dff59128222231ac859 + languageName: node + linkType: hard + +"ext-name@npm:^5.0.0": + version: 5.0.0 + resolution: "ext-name@npm:5.0.0" + dependencies: + ext-list: "npm:^2.0.0" + sort-keys-length: "npm:^1.0.0" + checksum: 10/f598269bd5de4295540ea7d6f8f6a01d82a7508f148b7700a05628ef6121648d26e6e5e942049e953b3051863df6b54bd8fe951e7877f185e34ace5d44370b33 + languageName: node + linkType: hard + "extend@npm:^3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -2658,6 +3583,26 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.5": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:^2.0.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -2679,6 +3624,24 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.6.0": + version: 1.18.0 + resolution: "fastq@npm:1.18.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/c5b501333dc8f5d188d828ea162aad03ff5a81aed185b6d4a5078aaeae0a42babc34296d7af13ebce86401cccd93c9b7b3cbf61280821c5f20af233378b42fbb + languageName: node + linkType: hard + +"feaxios@npm:^0.0.23": + version: 0.0.23 + resolution: "feaxios@npm:0.0.23" + dependencies: + is-retry-allowed: "npm:^3.0.0" + checksum: 10/e71ed43310bff03fb54338502369422c7b655bdf806fc4dc7e2bbc051d15ce7acc4f91800c3389cedfdcde6f0f6f076f6872a4d0258934aa08e0678869f2678f + languageName: node + linkType: hard + "fecha@npm:^4.2.0": version: 4.2.3 resolution: "fecha@npm:4.2.3" @@ -2696,12 +3659,40 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + languageName: node + linkType: hard + +"file-type@npm:^19.0.0": + version: 19.6.0 + resolution: "file-type@npm:19.6.0" + dependencies: + get-stream: "npm:^9.0.1" + strtok3: "npm:^9.0.1" + token-types: "npm:^6.0.0" + uint8array-extras: "npm:^1.3.0" + checksum: 10/db9221cbbfee7345688dd330dbd16482a97b570a84cd511eba14fe49ecb1fa60f80fd859b577f67b90621a048e05a0ddb073222e97cb3bd27f930d6f1b8a544c + languageName: node + linkType: hard + +"filename-reserved-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "filename-reserved-regex@npm:3.0.0" + checksum: 10/1803e19ce64d7cb88ee5a1bd3ce282470a5c263987269222426d889049fc857e302284fa71937de9582eba7a9f39539557d45e0562f2fa51cade8efc68c65dd9 + languageName: node + linkType: hard + +"filenamify@npm:^6.0.0": + version: 6.0.0 + resolution: "filenamify@npm:6.0.0" dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + filename-reserved-regex: "npm:^3.0.0" + checksum: 10/5914b64a760d49323d0454efb1f5e33338d3840df447f40556fc68730c4649797451931d60035c66068dacf326f045a912287ce8b63e15a5fba311a961f8f4b1 languageName: node linkType: hard @@ -2748,6 +3739,15 @@ __metadata: languageName: node linkType: hard +"find-versions@npm:^5.0.0": + version: 5.1.0 + resolution: "find-versions@npm:5.1.0" + dependencies: + semver-regex: "npm:^4.0.5" + checksum: 10/680bdb0081f631f7bfb6f0f8edcfa0b74ab8cabc82097a4527a37b0d042aabc56685bf459ff27991eab0baddc04eb8e3bba8a2869f5004ecf7cdd2779b6e51de + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -2813,6 +3813,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:^2.1.2": + version: 2.1.4 + resolution: "form-data-encoder@npm:2.1.4" + checksum: 10/3778e7db3c21457296e6fdbc4200642a6c01e8be9297256e845ee275f9ddaecb5f49bfb0364690ad216898c114ec59bf85f01ec823a70670b8067273415d62f6 + languageName: node + linkType: hard + "form-data@npm:^4.0.0": version: 4.0.1 resolution: "form-data@npm:4.0.1" @@ -2990,6 +3997,23 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: 10/781266d29725f35c59f1d214aedc92b0ae855800a980800e2923b3fbc4e56b3cb6e462c42e09a1cf1a00c64e056a78fa407cbe06c7c92b7e5cd49b4b85c2a497 + languageName: node + linkType: hard + +"get-stream@npm:^9.0.1": + version: 9.0.1 + resolution: "get-stream@npm:9.0.1" + dependencies: + "@sec-ant/readable-stream": "npm:^0.4.1" + is-stream: "npm:^4.0.1" + checksum: 10/ce56e6db6bcd29ca9027b0546af035c3e93dcd154ca456b54c298901eb0e5b2ce799c5d727341a100c99e14c523f267f1205f46f153f7b75b1f4da6d98a21c5e + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -3111,7 +4135,26 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"got@npm:^13.0.0": + version: 13.0.0 + resolution: "got@npm:13.0.0" + dependencies: + "@sindresorhus/is": "npm:^5.2.0" + "@szmarczak/http-timer": "npm:^5.0.1" + cacheable-lookup: "npm:^7.0.0" + cacheable-request: "npm:^10.2.8" + decompress-response: "npm:^6.0.0" + form-data-encoder: "npm:^2.1.2" + get-stream: "npm:^6.0.1" + http2-wrapper: "npm:^2.1.10" + lowercase-keys: "npm:^3.0.0" + p-cancelable: "npm:^3.0.0" + responselike: "npm:^3.0.0" + checksum: 10/35ac9fe37daca3d0a4f90305d8e64626268ef5a42584f5bcb42eea3cb9bbeb691cf9041d5ea72133a7295d1291684789a3148ff89a95f3d3ce3d0ebb6fb2f680 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -3257,6 +4300,16 @@ __metadata: languageName: node linkType: hard +"http2-wrapper@npm:^2.1.10": + version: 2.2.1 + resolution: "http2-wrapper@npm:2.2.1" + dependencies: + quick-lru: "npm:^5.1.1" + resolve-alpn: "npm:^1.2.0" + checksum: 10/e7a5ac6548318e83fc0399cd832cdff6bbf902b165d211cad47a56ee732922e0aa1107246dd884b12532a1c4649d27c4d44f2480911c65202e93c90bde8fa29d + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" @@ -3267,6 +4320,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: 10/df59be9e0af479036798a881d1f136c4a29e0b518d4abb863afbd11bf30efa3eeb1d0425fc65942dcc05ab3bf40205ea436b0ff389f2cd20b75b8643d539bf86 + languageName: node + linkType: hard + "husky@npm:^3.0.7": version: 3.1.0 resolution: "husky@npm:3.1.0" @@ -3316,7 +4376,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 @@ -3388,6 +4448,15 @@ __metadata: languageName: node linkType: hard +"inspect-with-kind@npm:^1.0.5": + version: 1.0.5 + resolution: "inspect-with-kind@npm:1.0.5" + dependencies: + kind-of: "npm:^6.0.2" + checksum: 10/2124548720116dc86f0ce1601e7a7e87ba146b934c4bd324d7ed2e93860c8a2e992c42617e71a33da88d49458e96f330cfcafdd4d0c2bf95484ff16e61abf31c + languageName: node + linkType: hard + "internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" @@ -3576,6 +4645,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^1.0.0, is-plain-obj@npm:^1.1.0": + version: 1.1.0 + resolution: "is-plain-obj@npm:1.1.0" + checksum: 10/0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931 + languageName: node + linkType: hard + "is-promise@npm:4.0.0": version: 4.0.0 resolution: "is-promise@npm:4.0.0" @@ -3593,6 +4669,13 @@ __metadata: languageName: node linkType: hard +"is-retry-allowed@npm:^3.0.0": + version: 3.0.0 + resolution: "is-retry-allowed@npm:3.0.0" + checksum: 10/12d17b44410484c6b0292ca38127b2ad026b65d1c1ead40f055effb2498c1a65b28ac5bc8f0383d0d091f84c1f791c3e09da32219fc3a745c3b1bab106db6cfb + languageName: node + linkType: hard + "is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "is-shared-array-buffer@npm:1.0.3" @@ -3609,13 +4692,20 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0": +"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 languageName: node linkType: hard +"is-stream@npm:^4.0.1": + version: 4.0.1 + resolution: "is-stream@npm:4.0.1" + checksum: 10/cbea3f1fc271b21ceb228819d0c12a0965a02b57f39423925f99530b4eb86935235f258f06310b67cd02b2d10b49e9a0998f5ececf110ab7d3760bae4055ad23 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -3859,6 +4949,13 @@ __metadata: languageName: node linkType: hard +"kind-of@npm:^6.0.2": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 10/5873d303fb36aad875b7538798867da2ae5c9e328d67194b0162a3659a627d22f742fc9c4ae95cd1704132a24b00cae5041fc00c0f6ef937dc17080dc4dbb962 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -3946,6 +5043,13 @@ __metadata: languageName: node linkType: hard +"lowercase-keys@npm:^3.0.0": + version: 3.0.0 + resolution: "lowercase-keys@npm:3.0.0" + checksum: 10/67a3f81409af969bc0c4ca0e76cd7d16adb1e25aa1c197229587eaf8671275c8c067cd421795dbca4c81be0098e4c426a086a05e30de8a9c587b7a13c0c7ccc5 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -3953,6 +5057,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10/bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-fetch-happen@npm:^13.0.0": version: 13.0.1 resolution: "make-fetch-happen@npm:13.0.1" @@ -4001,6 +5114,20 @@ __metadata: languageName: node linkType: hard +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 10/6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 + languageName: node + linkType: hard + "method-override@npm:^3.0.0": version: 3.0.0 resolution: "method-override@npm:3.0.0" @@ -4020,6 +5147,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10/6bf2a01672e7965eb9941d1f02044fad2bd12486b5553dc1116ff24c09a8723157601dc992e74c911d896175918448762df3b3fd0a6b61037dd1a9766ddfbf58 + languageName: node + linkType: hard + "mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -4027,7 +5164,7 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.53.0": +"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.28.0, mime-db@npm:^1.53.0": version: 1.53.0 resolution: "mime-db@npm:1.53.0" checksum: 10/82409c568a20254cc67a763a25e581d2213e1ef5d070a0af805239634f8a655f5d8a15138200f5f81c5b06fc6623d27f6168c612d447642d59e37eb7f20f7412 @@ -4052,6 +5189,27 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: 10/d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10/7e719047612411fe071332a7498cf0448bbe43c485c0d780046c76633a771b223ff49bd00267be122cedebb897037fdb527df72335d0d0f74724604ca70b37ad + languageName: node + linkType: hard + +"mimic-response@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-response@npm:4.0.0" + checksum: 10/33b804cc961efe206efdb1fca6a22540decdcfce6c14eb5c0c50e5ae9022267ab22ce8f5568b1f7247ba67500fe20d523d81e0e9f009b321ccd9d472e78d1850 + languageName: node + linkType: hard + "minimatch@npm:3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" @@ -4070,7 +5228,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.3, minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -4190,6 +5348,22 @@ __metadata: languageName: node linkType: hard +"mobx-utils@npm:^5.6.2": + version: 5.6.2 + resolution: "mobx-utils@npm:5.6.2" + peerDependencies: + mobx: ^4.13.1 || ^5.13.1 + checksum: 10/12b8af84b979049ac944697d0dbfc1082194f230064864e0fce82493d794e81abd46c28dbc122a05e1c6baada069b1fead4356d6237f59d024fa141da30d139b + languageName: node + linkType: hard + +"mobx@npm:^5.15.7": + version: 5.15.7 + resolution: "mobx@npm:5.15.7" + checksum: 10/623ca4812ad2f368bd1470a1d2ca4368f7dfb4dfcc6058a656e6d61230e0c449eddfc1e8adc0fda310cedbbc89809071c3a4e475a7ebe4f9bed4fe08f1a5764d + languageName: node + linkType: hard + "mocha@npm:^6.2.2": version: 6.2.3 resolution: "mocha@npm:6.2.3" @@ -4515,6 +5689,13 @@ __metadata: languageName: node linkType: hard +"normalize-url@npm:^8.0.0": + version: 8.0.1 + resolution: "normalize-url@npm:8.0.1" + checksum: 10/ae392037584fc5935b663ae4af475351930a1fc39e107956cfac44f42d5127eec2d77d9b7b12ded4696ca78103bafac5b6206a0ea8673c7bffecbe13544fcc5a + languageName: node + linkType: hard + "npm-run-path@npm:^2.0.0": version: 2.0.2 resolution: "npm-run-path@npm:2.0.2" @@ -4524,6 +5705,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: "npm:^3.0.0" + checksum: 10/5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + "object-assign@npm:^4": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -4672,6 +5862,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: "npm:^2.1.0" + checksum: 10/e9fd0695a01cf226652f0385bf16b7a24153dbbb2039f764c8ba6d2306a8506b0e4ce570de6ad99c7a6eb49520743afdb66edd95ee979c1a342554ed49a9aadd + languageName: node + linkType: hard + "opencollective-postinstall@npm:^2.0.2": version: 2.0.3 resolution: "opencollective-postinstall@npm:2.0.3" @@ -4731,6 +5930,13 @@ __metadata: languageName: node linkType: hard +"p-cancelable@npm:^3.0.0": + version: 3.0.0 + resolution: "p-cancelable@npm:3.0.0" + checksum: 10/a5eab7cf5ac5de83222a014eccdbfde65ecfb22005ee9bc242041f0b4441e07fac7629432c82f48868aa0f8413fe0df6c6067c16f76bf9217cd8dc651923c93d + languageName: node + linkType: hard + "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -4854,7 +6060,7 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^3.1.0": +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" checksum: 10/55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 @@ -4885,6 +6091,20 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^5.3.1": + version: 5.3.1 + resolution: "peek-readable@npm:5.3.1" + checksum: 10/d42940d4acbf3ebea096ecdb022484552ab4b9727bc3d01871cad81c9a1a7ff33b342db9edbc37608bb5b86e00002d692408ce9ecedf6c58862a4e44ab45e09f + languageName: node + linkType: hard + +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 10/6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -4892,13 +6112,25 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc languageName: node linkType: hard +"piscina@npm:^4.3.1": + version: 4.8.0 + resolution: "piscina@npm:4.8.0" + dependencies: + "@napi-rs/nice": "npm:^1.0.1" + dependenciesMeta: + "@napi-rs/nice": + optional: true + checksum: 10/5c28396c2fa3001bcf669d3c5d2678a2e89de622f6a8dcf0ba00ed58412d9aa8fc4aca2cd28c6901a8dbf718ce3520b92bb2cd701d8309ca04258472267a2972 + languageName: node + linkType: hard + "pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -5028,6 +6260,27 @@ __metadata: languageName: node linkType: hard +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b + languageName: node + linkType: hard + +"queue-tick@npm:^1.0.1": + version: 1.0.1 + resolution: "queue-tick@npm:1.0.1" + checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 + languageName: node + linkType: hard + +"quick-lru@npm:^5.1.1": + version: 5.1.1 + resolution: "quick-lru@npm:5.1.1" + checksum: 10/a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -5115,6 +6368,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10/5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471 + languageName: node + linkType: hard + "regexp-clone@npm:1.0.0, regexp-clone@npm:^1.0.0": version: 1.0.0 resolution: "regexp-clone@npm:1.0.0" @@ -5169,6 +6429,13 @@ __metadata: languageName: node linkType: hard +"resolve-alpn@npm:^1.2.0": + version: 1.2.1 + resolution: "resolve-alpn@npm:1.2.1" + checksum: 10/744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec + languageName: node + linkType: hard + "resolve-from@npm:^3.0.0": version: 3.0.0 resolution: "resolve-from@npm:3.0.0" @@ -5209,6 +6476,15 @@ __metadata: languageName: node linkType: hard +"responselike@npm:^3.0.0": + version: 3.0.0 + resolution: "responselike@npm:3.0.0" + dependencies: + lowercase-keys: "npm:^3.0.0" + checksum: 10/e0cc9be30df4f415d6d83cdede3c5c887cd4a73e7cc1708bcaab1d50a28d15acb68460ac5b02bcc55a42f3d493729c8856427dcf6e57e6e128ad05cba4cfb95e + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -5216,6 +6492,13 @@ __metadata: languageName: node linkType: hard +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 10/14222c9e1d3f9ae01480c50d96057228a8524706db79cdeb5a2ce5bb7070dd9f409a6f84a02cbef8cdc80d39aef86f2dd03d155188a1300c599b05437dcd2ffb + languageName: node + linkType: hard + "rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -5251,6 +6534,15 @@ __metadata: languageName: node linkType: hard +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10/cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + languageName: node + linkType: hard + "rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" @@ -5327,6 +6619,18 @@ __metadata: languageName: node linkType: hard +"seek-bzip@npm:^2.0.0": + version: 2.0.0 + resolution: "seek-bzip@npm:2.0.0" + dependencies: + commander: "npm:^6.0.0" + bin: + seek-bunzip: bin/seek-bunzip + seek-table: bin/seek-bzip-table + checksum: 10/38d49a2091ea4a01835662f606076cf032bae63480a10c84eb61dd810286f9ab24d275000a4a17e2efadcfa27bcf2b0dbeff7dabf9011487922f75bad6e57871 + languageName: node + linkType: hard + "semver-compare@npm:^1.0.0": version: 1.0.0 resolution: "semver-compare@npm:1.0.0" @@ -5334,6 +6638,22 @@ __metadata: languageName: node linkType: hard +"semver-regex@npm:^4.0.5": + version: 4.0.5 + resolution: "semver-regex@npm:4.0.5" + checksum: 10/b9e5c0573c4a997fb7e6e76321385d254797e86c8dba5e23f3cd8cf8f40b40414097a51514e5fead61dcb88ff10d3676355c01e2040f3c68f6c24bfd2073da2e + languageName: node + linkType: hard + +"semver-truncate@npm:^3.0.0": + version: 3.0.0 + resolution: "semver-truncate@npm:3.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/d8c23812218ff147f512ac4830e86860a377dba8a9733ae97d816102aca33236fa1c44c06544727153fffb93d15d0e45c49b2c40a7964aa3671769e9aed2f3f9 + languageName: node + linkType: hard + "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.7.0, semver@npm:^5.7.1": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -5352,7 +6672,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.5": +"semver@npm:^7.2.1, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -5505,7 +6825,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -5551,7 +6871,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:^3.0.0": +"slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" checksum: 10/94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c @@ -5613,7 +6933,7 @@ __metadata: languageName: node linkType: hard -"sodium-native@npm:^4.0.10": +"sodium-native@npm:^4.3.0": version: 4.3.1 resolution: "sodium-native@npm:4.3.1" dependencies: @@ -5622,6 +6942,31 @@ __metadata: languageName: node linkType: hard +"sort-keys-length@npm:^1.0.0": + version: 1.0.1 + resolution: "sort-keys-length@npm:1.0.1" + dependencies: + sort-keys: "npm:^1.0.0" + checksum: 10/f9acac5fb31580a9e3d43b419dc86a1b75e85b79036a084d95dd4d1062b621c9589906588ac31e370a0dd381be46d8dbe900efa306d087ca9c912d7a59b5a590 + languageName: node + linkType: hard + +"sort-keys@npm:^1.0.0": + version: 1.1.2 + resolution: "sort-keys@npm:1.1.2" + dependencies: + is-plain-obj: "npm:^1.0.0" + checksum: 10/0ac2ea2327d92252f07aa7b2f8c7023a1f6ce3306439a3e81638cce9905893c069521d168f530fb316d1a929bdb052b742969a378190afaef1bc64fa69e29576 + languageName: node + linkType: hard + +"source-map@npm:^0.7.3": + version: 0.7.4 + resolution: "source-map@npm:0.7.4" + checksum: 10/a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc + languageName: node + linkType: hard + "sparse-bitfield@npm:^3.0.3": version: 3.0.3 resolution: "sparse-bitfield@npm:3.0.3" @@ -5702,18 +7047,34 @@ __metadata: languageName: node linkType: hard -"stellar-sdk@npm:^11.3.0": - version: 11.3.0 - resolution: "stellar-sdk@npm:11.3.0" +"stellar-sdk@npm:^13.1.0": + version: 13.1.0 + resolution: "stellar-sdk@npm:13.1.0" dependencies: - "@stellar/stellar-base": "npm:^11.0.1" - axios: "npm:^1.6.8" + "@stellar/stellar-base": "npm:^13.0.1" + axios: "npm:^1.7.9" bignumber.js: "npm:^9.1.2" eventsource: "npm:^2.0.2" + feaxios: "npm:^0.0.23" randombytes: "npm:^2.1.0" toml: "npm:^3.0.0" urijs: "npm:^1.19.1" - checksum: 10/072c9451c1aa54c45bc670414729ec4480b3e7779b948872ed525fe009427a640915c01c3ff68ecb919fefce54a15db6a973d268b7896b635116c3cf0868e00c + checksum: 10/112868b9fea75dc17765e8e280739c8ab9ab6d4b6f8879177b6550c1b4a512ba0929412fcd7582da7ef5a5755af6be1006eed63a4d9ce1246eff72456f7ffff9 + languageName: node + linkType: hard + +"streamx@npm:^2.15.0": + version: 2.21.1 + resolution: "streamx@npm:2.21.1" + dependencies: + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + queue-tick: "npm:^1.0.1" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 10/d61ee82033f8b900226e2405aeb683de5f51a68ded1d40198d548cd9a7b2e47b7706442c9142bbc7fc59874f03063ee41ddf9e8667e63186b507b2e6b394ac28 languageName: node linkType: hard @@ -5855,6 +7216,16 @@ __metadata: languageName: node linkType: hard +"strip-dirs@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-dirs@npm:3.0.0" + dependencies: + inspect-with-kind: "npm:^1.0.5" + is-plain-obj: "npm:^1.1.0" + checksum: 10/630c16035f4e8638bcb55523a3a016668b82b526fbde818b45cfd15c2fed506e2784153932c9d4a6d9758cc2c07a69a9533c7faffad2594dd601378d613e1b67 + languageName: node + linkType: hard + "strip-eof@npm:^1.0.0": version: 1.0.0 resolution: "strip-eof@npm:1.0.0" @@ -5862,6 +7233,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 10/69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + "strip-json-comments@npm:2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -5876,6 +7254,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^9.0.1": + version: 9.1.1 + resolution: "strtok3@npm:9.1.1" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + peek-readable: "npm:^5.3.1" + checksum: 10/1800693f749911bc6b4162d935690488609803825a5dcd8ef8133988b1568ae4ddf09b70b69b51bb2bf328f5b491189970287d06a482155b13fdbbef53c5756a + languageName: node + linkType: hard + "supports-color@npm:6.0.0": version: 6.0.0 resolution: "supports-color@npm:6.0.0" @@ -5923,6 +7311,17 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^3.1.7": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10/b21a82705a72792544697c410451a4846af1f744176feb0ff11a7c3dd0896961552e3def5e1c9a6bbee4f0ae298b8252a1f4c9381e9f991553b9e4847976f05c + languageName: node + linkType: hard + "tar@npm:^6.1.11, tar@npm:^6.2.1": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -5937,6 +7336,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.3 + resolution: "text-decoder@npm:1.2.3" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10/bcdec33c0f070aeac38e46e4cafdcd567a58473ed308bdf75260bfbd8f7dc76acbc0b13226afaec4a169d0cb44cec2ab89c57b6395ccf02e941eaebbe19e124a + languageName: node + linkType: hard + "text-hex@npm:1.0.x": version: 1.0.0 resolution: "text-hex@npm:1.0.0" @@ -5951,6 +7359,13 @@ __metadata: languageName: node linkType: hard +"through@npm:^2.3.8": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -5967,6 +7382,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^6.0.0": + version: 6.0.0 + resolution: "token-types@npm:6.0.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 10/b541b605d602e8e6495745badb35f90ee8f997e43dc29bc51aee7e9a0bc3c6bc7372a305bd45f3e80d75223c2b6a5c7e65cb5159d8c4e49fa25cdbaae531fad4 + languageName: node + linkType: hard + "toml@npm:^3.0.0": version: 3.0.0 resolution: "toml@npm:3.0.0" @@ -6126,6 +7551,33 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.7.2": + version: 5.7.2 + resolution: "typescript@npm:5.7.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/4caa3904df69db9d4a8bedc31bafc1e19ffb7b24fbde2997a1633ae1398d0de5bdbf8daf602ccf3b23faddf1aeeb9b795223a2ed9c9a4fdcaf07bfde114a401a + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.7.2#optional!builtin": + version: 5.7.2 + resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=8c6c40" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/ff27fc124bceb8969be722baa38af945b2505767cf794de3e2715e58f61b43780284060287d651fcbbdfb6f917f4653b20f4751991f17e0706db389b9bb3f75d + languageName: node + linkType: hard + +"uint8array-extras@npm:^1.3.0": + version: 1.4.0 + resolution: "uint8array-extras@npm:1.4.0" + checksum: 10/4d2955d67c112e5ebaa4901272a75fc9ad14902c40f05a178b01e32387aa2702b6840472d931a1ca16e068ac59013c7d9ee2b4b2f141c4e73ba4bc7456490599 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -6138,6 +7590,16 @@ __metadata: languageName: node linkType: hard +"unbzip2-stream@npm:^1.4.3": + version: 1.4.3 + resolution: "unbzip2-stream@npm:1.4.3" + dependencies: + buffer: "npm:^5.2.1" + through: "npm:^2.3.8" + checksum: 10/4ffc0e14f4af97400ed0f37be83b112b25309af21dd08fa55c4513e7cb4367333f63712aec010925dbe491ef6e92db1248e1e306e589f9f6a8da8b3a9c4db90b + languageName: node + linkType: hard + "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" @@ -6537,3 +7999,13 @@ __metadata: checksum: 10/608ba2e62ac2c7c4572b9c6f7a2d3ef76e2deaad8c8082788ed29ae3ef33e9f68e087f07eb804ed5641de2bc4eab977405d3833b1d11ae8dbbaf5847584d96be languageName: node linkType: hard + +"yauzl@npm:^3.1.2": + version: 3.2.0 + resolution: "yauzl@npm:3.2.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + pend: "npm:~1.2.0" + checksum: 10/a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b + languageName: node + linkType: hard diff --git a/src/services/phases/signedTransactions.ts b/src/services/phases/signedTransactions.ts index 38f1e441..f925b260 100644 --- a/src/services/phases/signedTransactions.ts +++ b/src/services/phases/signedTransactions.ts @@ -1,4 +1,3 @@ -import { getAccount } from '@wagmi/core'; import { ApiPromise, Keyring } from '@polkadot/api'; import { Extrinsic } from '@pendulum-chain/api-solang'; import { Keypair } from 'stellar-sdk'; diff --git a/yarn.lock b/yarn.lock index 5ef35bbf..8d4cf8da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4635,21 +4635,28 @@ __metadata: languageName: node linkType: hard -"@stellar/stellar-base@npm:^11.0.1": - version: 11.0.1 - resolution: "@stellar/stellar-base@npm:11.0.1" +"@stellar/js-xdr@npm:^3.1.2": + version: 3.1.2 + resolution: "@stellar/js-xdr@npm:3.1.2" + checksum: 10/96b5c52088bb2f2cc11a04ee1766ceb9431bdb0195058b9bc8d60cd1459772c2be6a9aa1bf69ba8b7589cfaf71beef96890e60d7fd7de0332c11ae1917f95440 + languageName: node + linkType: hard + +"@stellar/stellar-base@npm:^13.0.1": + version: 13.0.1 + resolution: "@stellar/stellar-base@npm:13.0.1" dependencies: - "@stellar/js-xdr": "npm:^3.1.1" + "@stellar/js-xdr": "npm:^3.1.2" base32.js: "npm:^0.1.0" bignumber.js: "npm:^9.1.2" buffer: "npm:^6.0.3" sha.js: "npm:^2.3.6" - sodium-native: "npm:^4.0.10" + sodium-native: "npm:^4.3.0" tweetnacl: "npm:^1.0.3" dependenciesMeta: sodium-native: optional: true - checksum: 10/03ad775791793548563f4dfecc62843d84f0fcf41ccc7f3bb30090dd0506061fb38ca505fbb4ebd8b97d48a06c026733fa7b1146dd04216f3363843f8d09f0f3 + checksum: 10/58cafe97521b8a87ac673a31abf8dccf7ca55cf06cc89821d30aece810919ac5949043d6b581c1cbf03da89d23724d8129c517e7ebf6721397c56bdca77ec5e5 languageName: node linkType: hard @@ -4883,16 +4890,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22.7.5": - 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/node@npm:>=18.0.0": +"@types/node@npm:>=18.0.0, @types/node@npm:^22.7.5": version: 22.10.2 resolution: "@types/node@npm:22.10.2" dependencies: @@ -6186,18 +6184,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.8": - version: 1.6.8 - resolution: "axios@npm:1.6.8" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/3f9a79eaf1d159544fca9576261ff867cbbff64ed30017848e4210e49f3b01e97cf416390150e6fdf6633f336cd43dc1151f890bbd09c3c01ad60bb0891eee63 - languageName: node - linkType: hard - -"axios@npm:^1.7.8": +"axios@npm:^1.7.8, axios@npm:^1.7.9": version: 1.7.9 resolution: "axios@npm:1.7.9" dependencies: @@ -8839,6 +8826,15 @@ __metadata: languageName: node linkType: hard +"feaxios@npm:^0.0.23": + version: 0.0.23 + resolution: "feaxios@npm:0.0.23" + dependencies: + is-retry-allowed: "npm:^3.0.0" + checksum: 10/e71ed43310bff03fb54338502369422c7b655bdf806fc4dc7e2bbc051d15ce7acc4f91800c3389cedfdcde6f0f6f076f6872a4d0258934aa08e0678869f2678f + languageName: node + linkType: hard + "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.2.0 resolution: "fetch-blob@npm:3.2.0" @@ -10117,6 +10113,13 @@ __metadata: languageName: node linkType: hard +"is-retry-allowed@npm:^3.0.0": + version: 3.0.0 + resolution: "is-retry-allowed@npm:3.0.0" + checksum: 10/12d17b44410484c6b0292ca38127b2ad026b65d1c1ead40f055effb2498c1a65b28ac5bc8f0383d0d091f84c1f791c3e09da32219fc3a745c3b1bab106db6cfb + languageName: node + linkType: hard + "is-shared-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "is-shared-array-buffer@npm:1.0.2" @@ -13590,6 +13593,15 @@ __metadata: languageName: node linkType: hard +"sodium-native@npm:^4.3.0": + version: 4.3.1 + resolution: "sodium-native@npm:4.3.1" + dependencies: + node-gyp-build: "npm:^4.8.0" + checksum: 10/d08c7a68b458fa0dfd63a14dca1a114e855f4b857c3999e3e19e975434b3843335929d0539b03ca7f9796007f0560b629b33186e9b1fd6e7781199b32c48d23c + languageName: node + linkType: hard + "sonic-boom@npm:^2.2.1": version: 2.8.0 resolution: "sonic-boom@npm:2.8.0" @@ -13705,18 +13717,19 @@ __metadata: languageName: node linkType: hard -"stellar-sdk@npm:^11.3.0": - version: 11.3.0 - resolution: "stellar-sdk@npm:11.3.0" +"stellar-sdk@npm:^13.1.0": + version: 13.1.0 + resolution: "stellar-sdk@npm:13.1.0" dependencies: - "@stellar/stellar-base": "npm:^11.0.1" - axios: "npm:^1.6.8" + "@stellar/stellar-base": "npm:^13.0.1" + axios: "npm:^1.7.9" bignumber.js: "npm:^9.1.2" eventsource: "npm:^2.0.2" + feaxios: "npm:^0.0.23" randombytes: "npm:^2.1.0" toml: "npm:^3.0.0" urijs: "npm:^1.19.1" - checksum: 10/072c9451c1aa54c45bc670414729ec4480b3e7779b948872ed525fe009427a640915c01c3ff68ecb919fefce54a15db6a973d268b7896b635116c3cf0868e00c + checksum: 10/112868b9fea75dc17765e8e280739c8ab9ab6d4b6f8879177b6550c1b4a512ba0929412fcd7582da7ef5a5755af6be1006eed63a4d9ce1246eff72456f7ffff9 languageName: node linkType: hard @@ -14548,13 +14561,6 @@ __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" @@ -15231,7 +15237,7 @@ __metadata: react-router-dom: "npm:^6.8.1" react-toastify: "npm:^10.0.6" stellar-base: "npm:^11.0.1" - stellar-sdk: "npm:^11.3.0" + stellar-sdk: "npm:^13.1.0" tailwind: "npm:^4.0.0" tailwindcss: "npm:^3.4.3" ts-node: "npm:^10.9.1"