Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add polling mechanism #14

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
},
"plugins": ["@typescript-eslint"],
"rules": {
"require-await": ["error"]
"require-await": ["error"],
"semi": ["error", "always"],
"no-multi-spaces": ["error"]
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"serve": "node dist/server.js",
"dev": "nodemon src/server.ts",
"build": "tsc --sourceMap true",
"serve": "node dist/main.js",
"dev": "nodemon src/main.ts",
"migrate": "yarn run typeorm migration:run",
"start": "yarn migrate && yarn serve",
"test": "yarn jest",
Expand Down
22 changes: 17 additions & 5 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,33 @@ import { Server as SocketIOServer } from "socket.io";
import { Device } from "./model/device";
import { jwtVerify } from "jose";
import { RpcResponse } from "./interfaces/RpcResponse";
import { PollingService } from "./services/polling.service";

const logger = morgan("combined");

export const visibilityTimeout = 120_000;
export const waitForTransactionTimeout = 10_000;
const visibilityTimeout = 120_000;
const waitForTransactionTimeout = 10_000;

function createApp(
authOpts: AuthOptions,
clients: Clients,
webhookPublicKey: string,
origin: string[],
enablePolling: boolean,
): { app: express.Express; socketIO: SocketIOServer } {
const validateUser = checkJwt(authOpts);
const walletRoute = createWalletRoute(clients);
const { route: deviceRoute, service: deviceService } =
createDeviceRoute(clients);
const passphraseRoute = createPassphraseRoute();
const webhookRoute = createWebhook(clients, webhookPublicKey);
const userContoller = new UserController(new UserService());
const userController = new UserController(new UserService());

const pollingService = PollingService.createInstance(clients);
console.log(`Polling ${enablePolling ? "enabled" : "disabled"}`);
if (enablePolling) {
pollingService.start();
}

const app: Express = express();

Expand All @@ -50,7 +58,11 @@ function createApp(

app.get("/", (req: Request, res: Response) => res.send("OK"));

app.post("/api/login", validateUser, userContoller.login.bind(userContoller));
app.post(
"/api/login",
validateUser,
userController.login.bind(userController),
);
app.use("/api/passphrase", validateUser, passphraseRoute);
app.use("/api/devices", validateUser, deviceRoute);
app.use("/api/wallets", validateUser, walletRoute);
Expand Down Expand Up @@ -118,4 +130,4 @@ function createApp(
return { app, socketIO };
}

export { createApp };
export { createApp, visibilityTimeout, waitForTransactionTimeout };
7 changes: 4 additions & 3 deletions src/controllers/transaction.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
buildAccountTransferArgs,
buildTestTypedDataArgs,
} from "../util/transactionBuilder";
import * as TransactionDbHelper from "../services/transactionDbHelper.service";

export class TransactionController {
constructor(
Expand All @@ -27,7 +28,7 @@ export class TransactionController {

try {
const { walletId } = device!;
const transaction = await this.service.findOne(txId, walletId);
const transaction = await TransactionDbHelper.findOne(txId, walletId);
if (!transaction) {
return res.status(404).json();
}
Expand All @@ -45,7 +46,7 @@ export class TransactionController {

try {
const { walletId } = device!;
const transaction = await this.service.findOne(txId, walletId);
const transaction = await TransactionDbHelper.findOne(txId, walletId);

if (!transaction) {
return res.status(404).json();
Expand Down Expand Up @@ -170,7 +171,7 @@ export class TransactionController {
try {
const { walletId } = device!;

let transactions = await this.service.find(
let transactions = await TransactionDbHelper.find(
walletId,
orderBy as "lastUpdated" | "createdAt",
startDate,
Expand Down
1 change: 1 addition & 0 deletions src/controllers/webhook.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class WebhookController {
);

switch (type) {
// TODO: Remove - not used anymore, was used for handling incoming messages
case "NCW_DEVICE_MESSAGE": {
const { walletId, deviceId, physicalDeviceId, data } = req.body;
await handleNcwDeviceMessage(
Expand Down
2 changes: 1 addition & 1 deletion src/data-source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataSource } from "typeorm";
import opts from "./data-source-opts";

export const AppDataSource = new DataSource(opts);
export const appDataSource = new DataSource(opts);
54 changes: 27 additions & 27 deletions src/interfaces/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ export interface NetworkRecord {
}

export interface DestinationsResponse {
amount: string; // The amount to be sent to this destination.
destination: TransferPeerPathResponse; // Destination of the transaction.
amountUSD: number; // The USD value of the requested amount.
destinationAddress: string; // Address where the asset were transferred.
destinationAddressDescription: string; // Description of the address.
amlScreeningResult: AmlScreeningResult; // The result of the AML screening.
customerRefId: string; // The ID for AML providers to associate the owner of funds with transactions.
amount?: string; // The amount to be sent to this destination.
destination?: TransferPeerPathResponse; // Destination of the transaction.
amountUSD?: number; // The USD value of the requested amount.
destinationAddress?: string; // Address where the asset were transferred.
destinationAddressDescription?: string; // Description of the address.
amlScreeningResult?: AmlScreeningResult; // The result of the AML screening.
customerRefId?: string; // The ID for AML providers to associate the owner of funds with transactions.
}

export interface RewardsInfo {
Expand All @@ -157,23 +157,23 @@ export interface ITransactionDetails {
source: TransferPeerPathResponse; // Source of the transaction.
destination: TransferPeerPathResponse; // Fireblocks supports multiple destinations for UTXO-based blockchains. For other blockchains, this array will always be composed of one element.
requestedAmount: number; // The amount requested by the user.
amountInfo: AmountInfo; // Details of the transaction's amount in string format.
feeInfo: FeeInfo; // Details of the transaction's fee in string format.
amountInfo?: AmountInfo; // Details of the transaction's amount in string format.
feeInfo?: FeeInfo; // Details of the transaction's fee in string format.
amount: number; // If the transfer is a withdrawal from an exchange, the actual amount that was requested to be transferred. Otherwise, the requested amount.
netAmount: number; // The net amount of the transaction, after fee deduction.
amountUSD: number; // The USD value of the requested amount.
serviceFee: number; // The total fee deducted by the exchange from the actual requested amount (serviceFee = amount - netAmount).
treatAsGrossAmount: boolean; // For outgoing transactions, if true, the network fee is deducted from the requested amount.
serviceFee?: number; // The total fee deducted by the exchange from the actual requested amount (serviceFee = amount - netAmount).
treatAsGrossAmount?: boolean; // For outgoing transactions, if true, the network fee is deducted from the requested amount.
networkFee: number; // The fee paid to the network.
createdAt: number; // Unix timestamp.
lastUpdated: number; // Unix timestamp.
status: TransactionStatus; // The current status of the transaction.
txHash: string; // Blockchain hash of the transaction.
index: number; //[optional] For UTXO based assets this is the vOut, for Ethereum based, this is the index of the event of the contract call.
index?: number; //[optional] For UTXO based assets this is the vOut, for Ethereum based, this is the index of the event of the contract call.
subStatus: TransactionSubStatus; // More detailed status of the transaction.
sourceAddress: string; //For account based assets only, the source address of the transaction. (Note: This parameter will be empty for transactions that are not: CONFIRMING, COMPLETED, or REJECTED/FAILED after passing CONFIRMING status.)
sourceAddress?: string; //For account based assets only, the source address of the transaction. (Note: This parameter will be empty for transactions that are not: CONFIRMING, COMPLETED, or REJECTED/FAILED after passing CONFIRMING status.)
destinationAddress: string; //Address where the asset were transferred.
destinationAddressDescription: string; //Description of the address.
destinationAddressDescription?: string; //Description of the address.
destinationTag: string; //Destination tag for XRP, used as memo for EOS/XLM, or Bank Transfer Description for the fiat providers: Signet (by Signature), SEN (by Silvergate), or BLINC (by BCB Group).
signedBy: string[]; // Signers of the transaction.
createdBy: string; //Initiator of the transaction.
Expand All @@ -182,20 +182,20 @@ export interface ITransactionDetails {
note: string; //Custom note of the transaction.
exchangeTxId: string; //If the transaction originated from an exchange, this is the exchange tx ID.
feeCurrency: string; //The asset which was taken to pay the fee (ETH for ERC-20 tokens, BTC for Tether Omni).
operation: TransactionOperation; // Default operation is "TRANSFER".
amlScreeningResult: AmlScreeningResult; // The result of the AML screening.
customerRefId: string; //The ID for AML providers to associate the owner of funds with transactions.
numOfConfirmations: number; //The number of confirmations of the transaction. The number will increase until the transaction will be considered completed according to the confirmation policy.
networkRecords: NetworkRecord[]; // Transaction on the Fireblocks platform can aggregate several blockchain transactions, in such a case these records specify all the transactions that took place on the blockchain.
replacedTxHash: string; //In case of an RBF transaction, the hash of the dropped transaction.
externalTxId: string; //Unique transaction ID provided by the user.
destinations: DestinationsResponse[]; // For UTXO based assets, all outputs specified here.
blockInfo: BlockInfo; // The information of the block that this transaction was mined in, the blocks's hash and height.
rewardsInfo: RewardsInfo; // This field is relevant only for ALGO transactions.Both srcRewrds and destRewards will appear only for Vault to Vault transactions, otherwise you will receive only the Fireblocks' side of the transaction.
authorizationInfo: AuthorizationInfo; // The information about your Transaction Authorization Policy(TAP).For more information about the TAP, refer to this section in the Help Center.
operation?: TransactionOperation; // Default operation is "TRANSFER".
amlScreeningResult?: AmlScreeningResult; // The result of the AML screening.
customerRefId?: string; //The ID for AML providers to associate the owner of funds with transactions.
numOfConfirmations?: number; //The number of confirmations of the transaction. The number will increase until the transaction will be considered completed according to the confirmation policy.
networkRecords?: NetworkRecord[]; // Transaction on the Fireblocks platform can aggregate several blockchain transactions, in such a case these records specify all the transactions that took place on the blockchain.
replacedTxHash?: string; //In case of an RBF transaction, the hash of the dropped transaction.
externalTxId?: string; //Unique transaction ID provided by the user.
destinations?: DestinationsResponse[]; // For UTXO based assets, all outputs specified here.
blockInfo?: BlockInfo; // The information of the block that this transaction was mined in, the blocks's hash and height.
rewardsInfo?: RewardsInfo; // This field is relevant only for ALGO transactions.Both srcRewrds and destRewards will appear only for Vault to Vault transactions, otherwise you will receive only the Fireblocks' side of the transaction.
authorizationInfo?: AuthorizationInfo; // The information about your Transaction Authorization Policy(TAP).For more information about the TAP, refer to this section in the Help Center.
signedMessages: SignedMessage[]; // A list of signed messages returned for raw signing.
extraParameters: object; // JSON object Protocol / operation specific parameters.
systemMessages: ISystemMessageInfo[]; // objects A response from Fireblocks that communicates a message about the health of the process being performed. If this object is returned with data, you should expect potential delays or incomplete transaction statuses.
extraParameters?: object; // JSON object Protocol / operation specific parameters.
systemMessages?: ISystemMessageInfo[]; // objects A response from Fireblocks that communicates a message about the health of the process being performed. If this object is returned with data, you should expect potential delays or incomplete transaction statuses.
}

export interface ITransactionCreatedMessagePayload {
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { init } from "./server";

init();
1 change: 1 addition & 0 deletions src/middleware/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { JWTVerifyOptions, JWTVerifyGetKey, jwtVerify } from "jose";

export interface AuthOptions {
verify: JWTVerifyOptions;
key: JWTVerifyGetKey;
Expand Down
70 changes: 39 additions & 31 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createApp } from "./app";
import { FireblocksSDK } from "fireblocks-sdk";
import dotenv from "dotenv";
import { AppDataSource } from "./data-source";
import { appDataSource } from "./data-source";
import CoinMarketcap from "coinmarketcap-js";
import ms from "ms";
import { staleMessageCleanup } from "./services/message.service";
Expand All @@ -24,7 +24,10 @@ dotenv.config();

const port = process.env.PORT;

export const DEFAULT_ORIGIN = [
// Warning: Polling is not to be used in a production environment. Use webhooks instead.
const enablePolling = Boolean(process.env.ENABLE_POLLING);

const DEFAULT_ORIGIN = [
"http://localhost:5173",
"https://fireblocks.github.io",
];
Expand Down Expand Up @@ -73,8 +76,36 @@ const clients = {

const origin = getOriginFromEnv();

AppDataSource.initialize()
.then(async () => {
async function createAuthOptions() {
let authOptions: AuthOptions;

if (issuerBaseURL) {
const issuerClient = await Issuer.discover(issuerBaseURL);
authOptions = {
key: createRemoteJWKSet(new URL(issuerClient.metadata.jwks_uri!)),
verify: {
issuer: issuerClient.metadata.issuer,
audience,
},
};
} else if (jwksUri) {
authOptions = {
key: createRemoteJWKSet(new URL(jwksUri)),
verify: {
issuer,
audience,
},
};
} else {
throw new Error("Failed to resolve issuer");
}
return authOptions;
}

async function init() {
try {
await appDataSource.initialize();

console.log("Data Source has been initialized!");

const authOptions: AuthOptions = await createAuthOptions();
Expand All @@ -83,6 +114,7 @@ AppDataSource.initialize()
clients,
webhookPublicKey,
origin,
enablePolling,
);
const server = app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
Expand All @@ -102,34 +134,10 @@ AppDataSource.initialize()
methods: ["GET", "POST"],
},
});
})
.catch((err) => {
} catch (err) {
console.error("Error during Data Source initialization", err);
process.exit(1);
});

async function createAuthOptions() {
let authOptions: AuthOptions;

if (issuerBaseURL) {
const issuerClient = await Issuer.discover(issuerBaseURL);
authOptions = {
key: createRemoteJWKSet(new URL(issuerClient.metadata.jwks_uri!)),
verify: {
issuer: issuerClient.metadata.issuer,
audience,
},
};
} else if (jwksUri) {
authOptions = {
key: createRemoteJWKSet(new URL(jwksUri)),
verify: {
issuer,
audience,
},
};
} else {
throw new Error("Failed to resolve issuer");
}
return authOptions;
}

export { init, DEFAULT_ORIGIN };
Loading