Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
- changed: Use a custom chart icon for the side menu Markets row, so it matches the rest of the menu.
- changed: Use the UI4 warning card for the Reveal Raw Keys and Reveal Master Private Key password confirmation warnings.
- changed: Tron resource staking now describes its claim action as reclaiming your own TRX, instead of claiming a reward.
- fixed: Bitwave CSV exports now use ISO 8601 UTC timestamps, leave the fee columns blank so Bitwave does not double-count fees, and copy the description into the second custom metadata column.
- fixed: Bitwave account ids are no longer capitalized by the keyboard or padded with whitespace when entered, so exports import without hand-editing the account id.
- fixed: NYM max swaps from EVM wallets now report the correct limit error instead of an unsupported-route error (edge-exchange-plugins 2.52.1).
- fixed: Exchange rate queries no longer request each chain's own asset twice, which had been inflating every rate query with duplicate pairs.
- fixed: XRP minimum balance warning copy to clarify the reserve is met once the address balance reaches 1 XRP, not on top of it.
Expand Down
3 changes: 0 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export default [

'src/actions/SoundActions.ts',
'src/actions/TokenTermsActions.tsx',
'src/actions/TransactionExportActions.tsx',

'src/actions/WalletListActions.tsx',

Expand Down Expand Up @@ -313,8 +312,6 @@ export default [
'src/components/scenes/SwapSettingsScene.tsx',
'src/components/scenes/SwapSuccessScene.tsx',

'src/components/scenes/TransactionsExportScene.tsx',

'src/components/scenes/WalletRestoreScene.tsx',
'src/components/scenes/WcConnectionsScene.tsx',
'src/components/scenes/WcConnectScene.tsx',
Expand Down
94 changes: 94 additions & 0 deletions src/__tests__/actions/TransactionExportActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { EdgeTransaction } from 'edge-core-js'
import fs from 'fs'

import {
exportTransactionsToBitwave,
exportTransactionsToCSVInner,
exportTransactionsToQBO
} from '../../actions/TransactionExportActions'
Expand Down Expand Up @@ -188,3 +189,96 @@ test('export QBO matches reference data', function () {
)
expect(out).toEqual(qboResult)
})

/**
* Splits one CSV row into its fields, unwrapping quoted values and their
* doubled-quote escapes.
*/
function parseCsvRow(row: string): string[] {
const fields: string[] = []
let field = ''
let quoted = false
for (let i = 0; i < row.length; i++) {
const char = row[i]
if (quoted) {
if (char !== '"') field += char
else if (row[i + 1] === '"') {
field += '"'
i++
} else quoted = false
} else if (char === '"') quoted = true
else if (char === ',') {
fields.push(field)
field = ''
} else field += char
}
fields.push(field)
return fields
}

// Bitwave column letters, as the importer numbers them:
const COLUMN_G_FEE = 6
const COLUMN_H_FEE_TICKER = 7
const COLUMN_I_TIME = 8
const COLUMN_M_ACCOUNT_ID = 12
const COLUMN_R_DESCRIPTION = 17
const COLUMN_W_CUSTOM_METADATA2 = 22

const BITWAVE_ACCOUNT_ID = 'pgM8cDt7bySnWTzs2MyI'

async function exportBitwaveRows(): Promise<string[][]> {
const out = await exportTransactionsToBitwave(
BITWAVE_ACCOUNT_ID,
[...edgeTxs],
'BTC',
'100'
)
const [header, ...rows] = out.split('\n').filter(row => row !== '')

// Guard the column letters this suite asserts on, so a reordered row object
// fails here rather than silently invalidating every assertion below:
const headerFields = parseCsvRow(header)
expect(headerFields[COLUMN_G_FEE]).toEqual('fee')
expect(headerFields[COLUMN_H_FEE_TICKER]).toEqual('feeTicker')
expect(headerFields[COLUMN_I_TIME]).toEqual('time')
expect(headerFields[COLUMN_M_ACCOUNT_ID]).toEqual('accountId')
expect(headerFields[COLUMN_R_DESCRIPTION]).toEqual('description')
expect(headerFields[COLUMN_W_CUSTOM_METADATA2]).toEqual(
'metadata:myCustomMetadata2'
)

expect(rows.length).toBeGreaterThan(0)
return rows.map(parseCsvRow)
}

test('export Bitwave leaves the fee columns blank', async function () {
for (const fields of await exportBitwaveRows()) {
expect(fields[COLUMN_G_FEE]).toEqual('')
expect(fields[COLUMN_H_FEE_TICKER]).toEqual('')
}
})

test('export Bitwave duplicates the description into custom metadata 2', async function () {
for (const fields of await exportBitwaveRows()) {
expect(fields[COLUMN_W_CUSTOM_METADATA2]).toEqual(
fields[COLUMN_R_DESCRIPTION]
)
}
})

test('export Bitwave writes ISO 8601 UTC timestamps', async function () {
const rows = await exportBitwaveRows()
for (const fields of rows) {
expect(fields[COLUMN_I_TIME]).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
)
}
// The first transaction's date is 1524476980 (2018-04-23T09:49:40Z):
expect(rows[0][COLUMN_I_TIME]).toEqual('2018-04-23T09:49:40Z')
})

test('export Bitwave preserves the account id exactly', async function () {
for (const fields of await exportBitwaveRows()) {
expect(fields[COLUMN_M_ACCOUNT_ID]).toEqual(BITWAVE_ACCOUNT_ID)
}
})
50 changes: 22 additions & 28 deletions src/actions/TransactionExportActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export function updateTxsFiat(
}
}
})
.catch(e => {
console.warn(e.message)
.catch((e: unknown) => {
console.warn(e instanceof Error ? e.message : String(e))
})
)
if (promises.length >= UPDATE_TXS_MAX_PROMISES) {
Expand Down Expand Up @@ -179,15 +179,17 @@ function makeCsvDateTime(date: number): { date: string; time: string } {
}
}

/** ISO 8601 UTC, the timestamp format Bitwave requires for imports. */
function makeBitwaveDateTime(date: number): string {
const d = new Date(date * 1000)
const yy = d.getUTCFullYear().toString().slice(-2)
const yyyy = d.getUTCFullYear().toString()
const mm = padZero((d.getUTCMonth() + 1).toString())
const dd = padZero(d.getUTCDate().toString())
const hh = padZero(d.getUTCHours().toString())
const min = padZero(d.getUTCMinutes().toString())
const ss = padZero(d.getUTCSeconds().toString())

return `${mm}/${dd}/${yy} ${hh}:${min}`
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}Z`
}

//
Expand Down Expand Up @@ -269,6 +271,7 @@ export function exportTransactionsToQBO(
): string {
const STMTTRN: any[] = []
const now = makeOfxDate((testDateNow ?? Date.now()) / 1000)
const hasDenom = denom != null

for (const tx of edgeTransactions) {
const newTxs = getTransferTx(tx, fiatCurrencyCode)
Expand All @@ -280,8 +283,8 @@ export function exportTransactionsToQBO(
}
}

function edgeTxToQbo(edgeTx: EdgeTransaction) {
const TRNAMT: string = denom
function edgeTxToQbo(edgeTx: EdgeTransaction): void {
const TRNAMT: string = hasDenom
? div(edgeTx.nativeAmount, denom, DECIMAL_PRECISION)
: edgeTx.nativeAmount
const TRNTYPE = lt(edgeTx.nativeAmount, '0') ? 'DEBIT' : 'CREDIT'
Expand Down Expand Up @@ -394,6 +397,7 @@ export function exportTransactionsToCSVInner(
denomName: string = ''
): string {
const items: any[] = []
const hasDenom = denom != null

for (const tx of edgeTransactions) {
const newTxs = getTransferTx(tx, fiatCurrencyCode)
Expand All @@ -405,11 +409,11 @@ export function exportTransactionsToCSVInner(
}
}

function edgeTxToCsv(edgeTx: EdgeTransaction) {
const amount: string = denom
function edgeTxToCsv(edgeTx: EdgeTransaction): void {
const amount: string = hasDenom
? div(edgeTx.nativeAmount, denom, DECIMAL_PRECISION)
: edgeTx.nativeAmount
const networkFeeField: string = denom
const networkFeeField: string = hasDenom
? div(edgeTx.networkFee, denom, DECIMAL_PRECISION)
: edgeTx.networkFee
const { date, time } = makeCsvDateTime(edgeTx.date)
Expand Down Expand Up @@ -445,48 +449,35 @@ export function exportTransactionsToCSVInner(
}

export async function exportTransactionsToBitwave(
wallet: EdgeCurrencyWallet,
accountId: string,
edgeTransactions: EdgeTransaction[],
currencyCode: string,
multiplier: string,
parentMultiplier: string
multiplier: string
): Promise<string> {
const items: any[] = []
const parentCode = wallet.currencyInfo.currencyCode

for (const tx of edgeTransactions) {
edgeTxToCsv(tx)
}

function edgeTxToCsv(edgeTx: EdgeTransaction) {
function edgeTxToCsv(edgeTx: EdgeTransaction): void {
const {
date,
isSend,
metadata,
nativeAmount,
networkFee,
ourReceiveAddresses,
parentNetworkFee,
spendTargets,
txid
} = edgeTx
const amount: string = abs(div(nativeAmount, multiplier, DECIMAL_PRECISION))
const time = makeBitwaveDateTime(date)
let fee: string = ''
let feeTicker: string = ''
const { name = '', category = '', notes = '' } = metadata ?? {}

let toAddress = ''
if (isSend) {
if (parentNetworkFee != null) {
feeTicker = parentCode
fee = div(parentNetworkFee, parentMultiplier, DECIMAL_PRECISION)
} else {
feeTicker = currencyCode
fee = div(networkFee, multiplier, DECIMAL_PRECISION)
}
if (spendTargets && spendTargets.length > 0) {
if (spendTargets != null && spendTargets.length > 0) {
// We can only choose 1 `toAddress` so pick the first spendTarget
toAddress = spendTargets[0].publicAddress
}
Expand All @@ -507,8 +498,10 @@ export async function exportTransactionsToBitwave(
amountTicker: currencyCode,
cost: '',
costTicker: '',
fee,
feeTicker,
// Bitwave calculates transaction fees on its own side. Exporting them
// here duplicates the fees after import, so both columns stay blank:
fee: '',
feeTicker: '',
time,
blockchainId: txid,
memo: notes,
Expand All @@ -523,7 +516,8 @@ export async function exportTransactionsToBitwave(
toAddress,
groupId: '',
'metadata:myCustomMetadata1': category,
'metadata:myCustomMetadata2': notes
// Bitwave expects this to mirror the description column:
'metadata:myCustomMetadata2': name
})
}

Expand Down
Loading
Loading