diff --git a/components/TradesTable/index.tsx b/components/TradesTable/index.tsx index c92de882..d244c28d 100644 --- a/components/TradesTable/index.tsx +++ b/components/TradesTable/index.tsx @@ -55,6 +55,7 @@ export class TradesTable extends React.Component [ @@ -66,6 +67,7 @@ export class TradesTable extends React.Component{trade.boughtCurrency}, {(trade.amountSold / trade.rate).toFixed(8)}, {`${trade.transactionFee} ${trade.transactionFeeCurrency}`}, + {`${trade.tradeFee} ${trade.tradeFeeCurrency}`}, , ])} /> diff --git a/pages/import.tsx b/pages/import.tsx index aa6c4944..27e4ada2 100644 --- a/pages/import.tsx +++ b/pages/import.tsx @@ -125,6 +125,7 @@ const Import = () => { const [showNewIncome, setShowNewIncome] = useState(false); const [processing, setProcessing] = useState(false); const [fileBrowseOpen, setFileBrowseOpen] = useState(false); + const [secondFileData, setSecondFileData] = useState(''); const [processedData, setProcessedData] = useState([]); const [duplicateData, setDuplicateData] = useState([]); const [alertData, setAlertData] = useState(undefined); @@ -179,6 +180,8 @@ const Import = () => { setDuplicateData, setAlertData, setProcessedData, + secondFileData, + setSecondFileData, )} browse={fileBrowseOpen} /> @@ -305,6 +308,8 @@ const readFile = ( setDuplicateData: (duplicateData: DuplicateDataTypes) => void, setAlertData: (alertData: IAlertData) => void, setProcessedData: (processedData: ProcessedDataTypes) => void, + secondFileData: string, + setSecondFileData: (secondFileData: string) => void, ) => async ( fileData: string, input: React.RefObject @@ -318,8 +323,16 @@ const readFile = ( const processedData = await processData({ ...importDetails, data: fileData, + data2: secondFileData, }); - if (processedData && processedData.length) { + if (typeof processedData === 'string') { + setSecondFileData(fileData); + alert(`To import ${processedData} completly, \ +please provide both the crypto transactions and the fiat transactions files.\n +Please now select the other one.`); + setFileBrowseOpen(true); + } else if (processedData && processedData.length) { + setSecondFileData(''); const duplicateData = duplicateCheck( importDetails, savedData, processedData, ); diff --git a/src/parsers/index.ts b/src/parsers/index.ts index 6fdeec31..d36f9868 100644 --- a/src/parsers/index.ts +++ b/src/parsers/index.ts @@ -36,7 +36,7 @@ export async function getCSVData(fileData: string): Promise { }); } -export async function processData(importDetails: IImport): Promise { +export async function processData(importDetails: IImport): Promise { switch (importDetails.type) { case ImportType.TRADES: return await processTradesImport(importDetails); @@ -48,7 +48,3 @@ export async function processData(importDetails: IImport): Promise { +export default async function processTradesImport(importDetails: IImport): Promise { if (importDetails.location in parserMapping) { const parser = parserMapping[importDetails.location]; return await parser(importDetails); @@ -24,7 +26,10 @@ export default async function processTradesImport(importDetails: IImport): Promi const headers = importDetails.data.substr(0, importDetails.data.indexOf('\n')); const headersHash = crypto.createHash('sha256').update(headers).digest('hex'); for (const key in ExchangesTradeHeaders) { - if (ExchangesTradeHeaders[key] === headersHash) { + if (ExchangesTradeHeaders[key].split(';').includes(headersHash)) { + if (key === EXCHANGES.Liquid && importDetails.data2 === '') { + return 'Liquid trades'; + } return processTradesImport({ ...importDetails, location: key, diff --git a/src/parsers/trades/liquid/index.ts b/src/parsers/trades/liquid/index.ts new file mode 100644 index 00000000..4a17c6e6 --- /dev/null +++ b/src/parsers/trades/liquid/index.ts @@ -0,0 +1,224 @@ +import { getCSVData } from '../../'; +import { EXCHANGES, IImport, IPartialTrade, ITrade } from '../../../types'; +import { createDateAsUTC, createID } from '../../utils'; + +enum LiquidOrderDirection { + PAY = 'PAY', + RECEIVE = 'RECEIVE', +} + +interface ILiquid { + exchange: string; + currency_type: string; + direction: string; + transaction_id: string; + transaction_type: string; + gross_amount: string; + currency: string; + execution_id: string; + generated_for_type: string; + generated_for_id: string; + transaction_hash: string; + from_address: string; + to_address: string; + state: string; + created_at_jst: string; + updated_at_jst: string; + created_at_utc: string; + updated_at: string; + notes: string; + used_id: string; + account_id: string; +} + +interface ILiquidGroup { + [key: string]: ILiquid[]; +} + +function groupByExecutionID(group: ILiquidGroup, line: ILiquid) { + group[line.execution_id] = group[line.execution_id] ?? []; + group[line.execution_id].push(line); + return group; +} + +function groupByCreatedAtUTC(group: ILiquidGroup, line: ILiquid) { + group[line.created_at_utc] = group[line.created_at_utc] ?? []; + group[line.created_at_utc].push(line); + return group; +} + +export default async function processData(importDetails: IImport): Promise { + let data2: ILiquid[] = []; + if (importDetails.data2 !== undefined ) { + data2 = await getCSVData(importDetails.data2) as ILiquid[]; + } + const data: ILiquid[] = (await getCSVData(importDetails.data) as ILiquid[]).concat(data2); + const internalFormat: ITrade[] = []; + const sorted = data.sort(function(a, b){ + const dateA = createDateAsUTC(new Date(a.created_at_utc)).getTime(); + const dateB = createDateAsUTC(new Date(b.created_at_utc)).getTime(); + return dateA - dateB; + }); + const grouped = sorted.reduce(groupByExecutionID, {}); + for (const execution in grouped) { + const trades = grouped[execution]; + const tradeToAdd: IPartialTrade = { + date : createDateAsUTC(new Date(trades[0].created_at_utc)).getTime(), + exchange : EXCHANGES.Liquid, + exchangeID : execution, + }; + if (execution === '') { + const dateGrouped = trades.reduce(groupByCreatedAtUTC, {}); + for (const date in dateGrouped) { + const groupedTrades = dateGrouped[date]; + const firstLine = groupedTrades[0]; + switch (firstLine.transaction_type) { + /*case 'funding': { + // TODO: create a deposit transaction + break; + } + case 'withdrawal': { + // TODO: create a withdrawal transaction + break; + }*/ + case 'quick_exchange': { + if (groupedTrades.length == 2) { + internalFormat.push(addTrade(tradeToAdd, groupedTrades[0], groupedTrades[1])); + } else { + console.error(`Error parsing ${tradeToAdd.exchange} quick exchange. + It extends over ${groupedTrades.length} lines`); + } + break; + } + default: { + console.log(`Ignored ${tradeToAdd.exchange} trade of type ${firstLine.transaction_type}`); + } + } + } + continue; + } + switch (trades[0].transaction_type) { + case 'trade': { + switch (trades.length) { + case 1: { + internalFormat.push(addSingleLineTrade(tradeToAdd, trades[0])); + break; + } + case 2: { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[1])); + break; + } + case 3: { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[1], trades[2])); + break; + } + case 4: { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[1], trades[2], trades[3])); + break; + } + case 6: { + let secondTrade = tradeToAdd; + if (trades[0].direction !== trades[2].direction) { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[2], trades[4], trades[5])); + internalFormat.push(addTrade(secondTrade, trades[1], trades[3])); + } else { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[3], trades[4], trades[5])); + internalFormat.push(addTrade(secondTrade, trades[1], trades[2])); + } + break; + } + case 8: { + let secondTrade = tradeToAdd; + if (trades[0].direction !== trades[2].direction) { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[2], trades[4], trades[5])); + internalFormat.push(addTrade(secondTrade, trades[1], trades[3], trades[6], trades[7])); + } else { + internalFormat.push(addTrade(tradeToAdd, trades[0], trades[3], trades[4], trades[5])); + internalFormat.push(addTrade(secondTrade, trades[1], trades[2], trades[6], trades[7])); + } + break; + } + default: { + console.error(`Error parsing ${tradeToAdd.exchange} trade. + It extends over ${trades.length} lines`); + console.info(trades); + } + } + break; + } + case 'rebate_trade_fee': + case 'trade_fee': { + console.error(`Error parsing ${tradeToAdd.exchange} trade. + First line should not be of type ${trades[0].transaction_type}`); + break; + } + default: { + console.log(`Ignored ${tradeToAdd.exchange} trade of type ${trades[0].transaction_type}`); + } + } + } + return internalFormat; +} + +function addSingleLineTrade( + tradeToAdd: IPartialTrade, + trade: ILiquid, +) : ITrade { + if (trade.direction.toUpperCase() === LiquidOrderDirection.PAY) { + tradeToAdd.boughtCurrency = trade.currency; + tradeToAdd.soldCurrency = trade.currency; + tradeToAdd.amountSold = 0; + tradeToAdd.rate = 1; + tradeToAdd.tradeFee = parseFloat(trade.gross_amount); + tradeToAdd.tradeFeeCurrency = trade.currency; + } else if (trade.direction.toUpperCase() === LiquidOrderDirection.RECEIVE) { + // TODO: Replace by an income + tradeToAdd.soldCurrency = 'USDT'; + tradeToAdd.boughtCurrency = trade.currency; + tradeToAdd.amountSold = 0; + tradeToAdd.rate = 0 / parseFloat(trade.gross_amount); + } + else { + console.info(trade); + throw new Error(`Error parsing ${tradeToAdd.exchange} trade.direction=${trade.direction}`); + } + tradeToAdd.ID = createID(tradeToAdd); + return tradeToAdd as ITrade; +} + +function addTrade( + tradeToAdd: IPartialTrade, + firstHalf: ILiquid, + secondHalf: ILiquid, + feeTrade?: ILiquid, + rebateFeeTrade?: ILiquid, +): ITrade { + let firstHalfDirection = firstHalf.direction.toUpperCase(); + let secondHalfDirection = secondHalf.direction.toUpperCase(); + if (firstHalfDirection === LiquidOrderDirection.PAY && secondHalfDirection === LiquidOrderDirection.RECEIVE) { + tradeToAdd.boughtCurrency = secondHalf.currency; + tradeToAdd.soldCurrency = firstHalf.currency; + tradeToAdd.amountSold = Math.abs(parseFloat(firstHalf.gross_amount)); + tradeToAdd.rate = Math.abs(parseFloat(firstHalf.gross_amount) / parseFloat(secondHalf.gross_amount)); + } else if (firstHalfDirection === LiquidOrderDirection.RECEIVE && secondHalfDirection === LiquidOrderDirection.PAY) { + tradeToAdd.soldCurrency = secondHalf.currency; + tradeToAdd.boughtCurrency = firstHalf.currency; + tradeToAdd.amountSold = Math.abs(parseFloat(secondHalf.gross_amount)); + tradeToAdd.rate = Math.abs(parseFloat(secondHalf.gross_amount) / parseFloat(firstHalf.gross_amount)); + } else { + console.info(firstHalf); + console.info(secondHalf); + throw new Error(`Error parsing ${tradeToAdd.exchange} firstHalf.direction=${firstHalf.direction} + and secondHalf.direction=${secondHalf.direction}`); + } + if (feeTrade !== undefined) { + let amount = parseFloat(feeTrade.gross_amount); + if (rebateFeeTrade !== undefined) { + amount -= parseFloat(rebateFeeTrade.gross_amount); + } + tradeToAdd.tradeFee = amount; + tradeToAdd.tradeFeeCurrency = feeTrade.currency; + } + tradeToAdd.ID = createID(tradeToAdd); + return tradeToAdd as ITrade; +} diff --git a/src/types/import.ts b/src/types/import.ts index cc10e6e6..97f383c6 100644 --- a/src/types/import.ts +++ b/src/types/import.ts @@ -18,6 +18,7 @@ export interface IImport { currency?: string; location: Location; data: string; + data2?: string; } export interface IDuplicate { diff --git a/src/types/locations.ts b/src/types/locations.ts index c0053d4b..941f9b13 100644 --- a/src/types/locations.ts +++ b/src/types/locations.ts @@ -3,6 +3,7 @@ export type Location = EXCHANGES | string; export enum EXCHANGES { Bittrex = 'BITTREX', Gemini= 'GEMINI', + Liquid = 'LIQUID', Poloniex = 'POLONIEX', Kraken = 'KRAKEN', Binance = 'BINANCE', @@ -16,6 +17,8 @@ export enum IncomeImportTypes { export enum ExchangesTradeHeaders { BITTREX = '07230399aaa8d1f15e88e38bd43a01c5ef1af6c1f9131668d346e196ff090d80', GEMINI = '996edee25db7f3d1dd16c83c164c6cff8c6d0f5d6b3aafe6d1700f2a830f6c9e', + LIQUID = '12c125c9080e41e087ff0955826bba94568d637214b166a296447e53ae469abe;\ +271db9c7cabdb85db30b433b24a8e446640f1a9c0195b7120bf5275527823c72', POLONIEX = 'd7484d726e014edaa059c0137ac91183a7eaa9ee5d52713aa48bb4104b01afb0', KRAKEN = '85bf27e799cc0a30fe5b201cd6a4724e4a52feb433f41a1e8b046924e3bf8dc5', BINANCE = '4d0d5df894fe488872e513f6148dfa14ff29272e759b7fb3c86d264687a7cf99', diff --git a/src/types/trade.ts b/src/types/trade.ts index 07731197..1de8b25b 100644 --- a/src/types/trade.ts +++ b/src/types/trade.ts @@ -13,6 +13,8 @@ export interface ITrade { ID: string; transactionFee: number; transactionFeeCurrency: string; + tradeFee?: number; + tradeFeeCurrency?: string; } export interface ITradeWithFiatRate extends ITrade {