Skip to content

Commit 2baeeb1

Browse files
committed
Fix Bitwave CSV export issues
Bitwave changed its import requirements, so the export no longer matched what the importer accepts: - Timestamps use ISO 8601 UTC (YYYY-MM-DDTHH:MM:SSZ) instead of MM/DD/YY HH:MM. - The fee and fee ticker columns stay blank, since Bitwave now calculates fees itself and populated columns duplicate them after import. - The custom metadata 2 column mirrors the description column. - The account id keeps the case and spacing it was entered with. The input modal inherited the platform default of capitalizing the first character, which silently corrupted ids beginning with a lowercase letter and forced a manual fix on every import. Dropping the fee columns leaves the wallet and parent multiplier arguments unused, so they are removed along with the scene prop that supplied them.
1 parent c004de0 commit 2baeeb1

5 files changed

Lines changed: 140 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
- changed: Use a custom chart icon for the side menu Markets row, so it matches the rest of the menu.
99
- changed: Use the UI4 warning card for the Reveal Raw Keys and Reveal Master Private Key password confirmation warnings.
1010
- changed: Tron resource staking now describes its claim action as reclaiming your own TRX, instead of claiming a reward.
11+
- 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.
12+
- 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.
13+
- 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).
14+
- fixed: Exchange rate queries no longer request each chain's own asset twice, which had been inflating every rate query with duplicate pairs.
1115
- fixed: XRP minimum balance warning copy to clarify the reserve is met once the address balance reaches 1 XRP, not on top of it.
1216
- fixed: Round fiat balances to cents in the Wallets list, matching the wallet detail scene
1317
- fixed: Notification center cards no longer shrink their text to fit. Long titles and messages now truncate with an ellipsis so every card renders at the same size.

‎src/__tests__/actions/TransactionExportActions.test.ts‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { EdgeTransaction } from 'edge-core-js'
33
import fs from 'fs'
44

55
import {
6+
exportTransactionsToBitwave,
67
exportTransactionsToCSVInner,
78
exportTransactionsToQBO
89
} from '../../actions/TransactionExportActions'
@@ -13,6 +14,12 @@ const csvResult = fs.readFileSync('./src/__tests__/exportCsvResult.csv', {
1314
const qboResult = fs.readFileSync('./src/__tests__/exportQboResult.qbo', {
1415
encoding: 'utf8'
1516
})
17+
const bitwaveResult = fs.readFileSync(
18+
'./src/__tests__/exportBitwaveResult.csv',
19+
{ encoding: 'utf8' }
20+
)
21+
22+
const BITWAVE_ACCOUNT_ID = 'pgM8cDt7bySnWTzs2MyI'
1623

1724
const edgeTxs: EdgeTransaction[] = [
1825
{
@@ -188,3 +195,104 @@ test('export QBO matches reference data', function () {
188195
)
189196
expect(out).toEqual(qboResult)
190197
})
198+
199+
test('export Bitwave matches reference data', async function () {
200+
const out = await exportTransactionsToBitwave(
201+
BITWAVE_ACCOUNT_ID,
202+
[...edgeTxs],
203+
'BTC',
204+
'100'
205+
)
206+
expect(out).toEqual(bitwaveResult)
207+
})
208+
209+
/**
210+
* Splits one CSV row into its fields, unwrapping quoted values and their
211+
* doubled-quote escapes.
212+
*/
213+
function parseCsvRow(row: string): string[] {
214+
const fields: string[] = []
215+
let field = ''
216+
let quoted = false
217+
for (let i = 0; i < row.length; i++) {
218+
const char = row[i]
219+
if (quoted) {
220+
if (char !== '"') field += char
221+
else if (row[i + 1] === '"') {
222+
field += '"'
223+
i++
224+
} else quoted = false
225+
} else if (char === '"') quoted = true
226+
else if (char === ',') {
227+
fields.push(field)
228+
field = ''
229+
} else field += char
230+
}
231+
fields.push(field)
232+
return fields
233+
}
234+
235+
// Bitwave column letters, as the importer numbers them:
236+
const COLUMN_G_FEE = 6
237+
const COLUMN_H_FEE_TICKER = 7
238+
const COLUMN_I_TIME = 8
239+
const COLUMN_M_ACCOUNT_ID = 12
240+
const COLUMN_R_DESCRIPTION = 17
241+
const COLUMN_W_CUSTOM_METADATA2 = 22
242+
243+
async function exportBitwaveRows(): Promise<string[][]> {
244+
const out = await exportTransactionsToBitwave(
245+
BITWAVE_ACCOUNT_ID,
246+
[...edgeTxs],
247+
'BTC',
248+
'100'
249+
)
250+
const [header, ...rows] = out.split('\n').filter(row => row !== '')
251+
252+
// Guard the column letters this suite asserts on, so a reordered row object
253+
// fails here rather than silently invalidating every assertion below:
254+
const headerFields = parseCsvRow(header)
255+
expect(headerFields[COLUMN_G_FEE]).toEqual('fee')
256+
expect(headerFields[COLUMN_H_FEE_TICKER]).toEqual('feeTicker')
257+
expect(headerFields[COLUMN_I_TIME]).toEqual('time')
258+
expect(headerFields[COLUMN_M_ACCOUNT_ID]).toEqual('accountId')
259+
expect(headerFields[COLUMN_R_DESCRIPTION]).toEqual('description')
260+
expect(headerFields[COLUMN_W_CUSTOM_METADATA2]).toEqual(
261+
'metadata:myCustomMetadata2'
262+
)
263+
264+
expect(rows.length).toBeGreaterThan(0)
265+
return rows.map(parseCsvRow)
266+
}
267+
268+
test('export Bitwave leaves the fee columns blank', async function () {
269+
for (const fields of await exportBitwaveRows()) {
270+
expect(fields[COLUMN_G_FEE]).toEqual('')
271+
expect(fields[COLUMN_H_FEE_TICKER]).toEqual('')
272+
}
273+
})
274+
275+
test('export Bitwave duplicates the description into custom metadata 2', async function () {
276+
for (const fields of await exportBitwaveRows()) {
277+
expect(fields[COLUMN_W_CUSTOM_METADATA2]).toEqual(
278+
fields[COLUMN_R_DESCRIPTION]
279+
)
280+
}
281+
})
282+
283+
test('export Bitwave writes ISO 8601 UTC timestamps', async function () {
284+
const rows = await exportBitwaveRows()
285+
for (const fields of rows) {
286+
expect(fields[COLUMN_I_TIME]).toMatch(
287+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
288+
)
289+
}
290+
// The first transaction's date is 1524476980 (2018-04-23T09:49:40Z):
291+
expect(rows[0][COLUMN_I_TIME]).toEqual('2018-04-23T09:49:40Z')
292+
})
293+
294+
test('export Bitwave preserves the account id exactly', async function () {
295+
for (const fields of await exportBitwaveRows()) {
296+
expect(fields[COLUMN_M_ACCOUNT_ID]).toEqual(BITWAVE_ACCOUNT_ID)
297+
}
298+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"id","remoteContactId","amount","amountTicker","cost","costTicker","fee","feeTicker","time","blockchainId","memo","transactionType","accountId","contactId","categoryId","taxExempt","tradeId","description","fromAddress","toAddress","groupId","metadata:myCustomMetadata1","metadata:myCustomMetadata2"
2+
"1ff0fd8529f4c5bc","","1230000","BTC","","","","","2018-04-23T09:49:40Z","txid1","Hell yeah! Thanks for the fish <<&&>>","deposit","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Crazy Person","","receiveaddress1","","Income:Mo Money","Crazy Person"
3+
"a1734d2a42db4f78","","3210000","BTC","","","","","2018-04-24T13:36:20Z","txid2","Hell yeah! Here's a fish""","withdrawal","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Crazy Person 2","","","","Expense:Less Money","Crazy Person 2"
4+
"4152a38fd1753fb1","","3210000","BTC","","","","","2018-04-25T17:23:00Z","txid3","","deposit","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Transfer","","receiveaddress3","","Transfer:Edge","Transfer"
5+
"bd0499aac742810c","","3210000","BTC","","","","","2018-04-26T21:09:40Z","txid4","","withdrawal","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Transfer but actually income","","","","Transfer:Edge","Transfer but actually income"
6+
"28b9c7c71f2f3725","","3210000","BTC","","","","","2018-04-28T00:56:20Z","txid4","","withdrawal","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Transfer but no fee","","","","Transfer:Edge","Transfer but no fee"
7+
"a7604f215aa3ebd8","","3210000","BTC","","","","","2018-04-29T04:43:00Z","txid5","","withdrawal","pgM8cDt7bySnWTzs2MyI","","","FALSE","","Transfer but no fiat amount","","","","Transfer:Edge","Transfer but no fiat amount"
8+
"9a1aed86663a096c","","3210000","BTC","","","","","2018-04-30T08:29:40Z","txid6","","withdrawal","pgM8cDt7bySnWTzs2MyI","","","FALSE","","","","","","",""

‎src/actions/TransactionExportActions.tsx‎

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,17 @@ function makeCsvDateTime(date: number): { date: string; time: string } {
179179
}
180180
}
181181

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

190-
return `${mm}/${dd}/${yy} ${hh}:${min}`
192+
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}Z`
191193
}
192194

193195
//
@@ -447,15 +449,12 @@ export function exportTransactionsToCSVInner(
447449
}
448450

449451
export async function exportTransactionsToBitwave(
450-
wallet: EdgeCurrencyWallet,
451452
accountId: string,
452453
edgeTransactions: EdgeTransaction[],
453454
currencyCode: string,
454-
multiplier: string,
455-
parentMultiplier: string
455+
multiplier: string
456456
): Promise<string> {
457457
const items: any[] = []
458-
const parentCode = wallet.currencyInfo.currencyCode
459458

460459
for (const tx of edgeTransactions) {
461460
edgeTxToCsv(tx)
@@ -469,25 +468,15 @@ export async function exportTransactionsToBitwave(
469468
nativeAmount,
470469
networkFee,
471470
ourReceiveAddresses,
472-
parentNetworkFee,
473471
spendTargets,
474472
txid
475473
} = edgeTx
476474
const amount: string = abs(div(nativeAmount, multiplier, DECIMAL_PRECISION))
477475
const time = makeBitwaveDateTime(date)
478-
let fee: string = ''
479-
let feeTicker: string = ''
480476
const { name = '', category = '', notes = '' } = metadata ?? {}
481477

482478
let toAddress = ''
483479
if (isSend) {
484-
if (parentNetworkFee != null) {
485-
feeTicker = parentCode
486-
fee = div(parentNetworkFee, parentMultiplier, DECIMAL_PRECISION)
487-
} else {
488-
feeTicker = currencyCode
489-
fee = div(networkFee, multiplier, DECIMAL_PRECISION)
490-
}
491480
if (spendTargets != null && spendTargets.length > 0) {
492481
// We can only choose 1 `toAddress` so pick the first spendTarget
493482
toAddress = spendTargets[0].publicAddress
@@ -509,8 +498,10 @@ export async function exportTransactionsToBitwave(
509498
amountTicker: currencyCode,
510499
cost: '',
511500
costTicker: '',
512-
fee,
513-
feeTicker,
501+
// Bitwave calculates transaction fees on its own side. Exporting them
502+
// here duplicates the fees after import, so both columns stay blank:
503+
fee: '',
504+
feeTicker: '',
514505
time,
515506
blockchainId: txid,
516507
memo: notes,
@@ -525,7 +516,8 @@ export async function exportTransactionsToBitwave(
525516
toAddress,
526517
groupId: '',
527518
'metadata:myCustomMetadata1': category,
528-
'metadata:myCustomMetadata2': notes
519+
// Bitwave expects this to mirror the description column:
520+
'metadata:myCustomMetadata2': name
529521
})
530522
}
531523

‎src/components/scenes/TransactionsExportScene.tsx‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ interface StateProps {
5858
defaultIsoFiat: string
5959
exchangeMultiplier: string
6060
multiplier: string
61-
parentMultiplier: string
6261
}
6362

6463
interface DispatchProps {
@@ -303,7 +302,6 @@ class TransactionsExportSceneComponent extends React.PureComponent<
303302
defaultIsoFiat,
304303
exchangeMultiplier,
305304
multiplier,
306-
parentMultiplier,
307305
route
308306
} = this.props
309307
const { sourceWallet, tokenId } = route.params
@@ -327,10 +325,15 @@ class TransactionsExportSceneComponent extends React.PureComponent<
327325
const fileAccountId = exportTxInfo?.bitwaveAccountId ?? ''
328326

329327
if (isExportBitwave) {
330-
accountId =
328+
// Bitwave account ids are case-sensitive and may start with a lowercase
329+
// letter, so the platform default of capitalizing the first character
330+
// would corrupt them. Trim as well, since a pasted id often carries
331+
// surrounding whitespace:
332+
const rawAccountId =
331333
(await Airship.show<string | undefined>(bridge => (
332334
<TextInputModal
333335
autoFocus
336+
autoCapitalize="none"
334337
autoCorrect={false}
335338
bridge={bridge}
336339
initialValue={fileAccountId}
@@ -345,6 +348,7 @@ class TransactionsExportSceneComponent extends React.PureComponent<
345348
title={lstrings.export_transaction_bitwave_accountid_modal_title}
346349
/>
347350
))) ?? ''
351+
accountId = rawAccountId.trim()
348352
}
349353

350354
if (
@@ -451,12 +455,10 @@ class TransactionsExportSceneComponent extends React.PureComponent<
451455

452456
if (isExportBitwave) {
453457
const bitwaveFile = await exportTransactionsToBitwave(
454-
sourceWallet,
455458
accountId,
456459
txs,
457460
currencyCode,
458-
exchangeMultiplier,
459-
parentMultiplier
461+
exchangeMultiplier
460462
)
461463
files.push({
462464
contents: bitwaveFile,
@@ -544,9 +546,7 @@ export const TransactionsExportScene = connect<
544546
state,
545547
params.sourceWallet.currencyConfig,
546548
params.tokenId
547-
).multiplier,
548-
parentMultiplier: getExchangeDenom(params.sourceWallet.currencyConfig, null)
549-
.multiplier
549+
).multiplier
550550
}),
551551
dispatch => ({
552552
updateTxsFiatDispatch: async (wallet, tokenId, currencyCode, txs) => {

0 commit comments

Comments
 (0)