edge-core-js 2.41.2 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/node/index.js CHANGED
@@ -3135,6 +3135,20 @@ function syncStorageWallet(ai, walletId) {
3135
3135
  });
3136
3136
  }
3137
3137
 
3138
+ async function makeEdgeResult(promise) {
3139
+ try {
3140
+ return {
3141
+ ok: true,
3142
+ result: await promise
3143
+ };
3144
+ } catch (error) {
3145
+ return {
3146
+ ok: false,
3147
+ error
3148
+ };
3149
+ }
3150
+ }
3151
+
3138
3152
  function getStorageWalletLastChanges(state, walletId) {
3139
3153
  return state.storageWallets[walletId].lastChanges;
3140
3154
  }
@@ -3494,30 +3508,6 @@ const upgradeTxNetworkFees = tx => {
3494
3508
  }
3495
3509
  };
3496
3510
 
3497
- function makeStorageWalletApi(ai, walletInfo) {
3498
- const {
3499
- id,
3500
- type,
3501
- keys
3502
- } = walletInfo;
3503
- return {
3504
- // Broken-out key info:
3505
- id,
3506
- type,
3507
- keys,
3508
- // Folders:
3509
- get disklet() {
3510
- return getStorageWalletDisklet(ai.props.state, id);
3511
- },
3512
- get localDisklet() {
3513
- return getStorageWalletLocalDisklet(ai.props.state, id);
3514
- },
3515
- async sync() {
3516
- await syncStorageWallet(ai, id);
3517
- }
3518
- };
3519
- }
3520
-
3521
3511
  function getCurrencyMultiplier(currencyInfo, allTokens, currencyCode) {
3522
3512
  for (const denomination of currencyInfo.denominations) {
3523
3513
  if (denomination.name === currencyCode) {
@@ -3555,484 +3545,440 @@ function waitForCurrencyWallet(ai, walletId) {
3555
3545
  return out;
3556
3546
  }
3557
3547
 
3558
- const TYPED_ARRAYS = {
3559
- '[object Float32Array]': true,
3560
- '[object Float64Array]': true,
3561
- '[object Int16Array]': true,
3562
- '[object Int32Array]': true,
3563
- '[object Int8Array]': true,
3564
- '[object Uint16Array]': true,
3565
- '[object Uint32Array]': true,
3566
- '[object Uint8Array]': true,
3567
- '[object Uint8ClampedArray]': true
3568
- };
3569
-
3570
3548
  /**
3571
- * Compares two objects that are already known to have a common `[[Class]]`.
3549
+ * A key that decrypts a login stash.
3572
3550
  */
3573
- function compareObjects(a, b, type) {
3574
- // User-created objects:
3575
- if (type === '[object Object]') {
3576
- const proto = Object.getPrototypeOf(a);
3577
- if (proto !== Object.getPrototypeOf(b)) return false;
3578
- const keys = Object.getOwnPropertyNames(a);
3579
- if (keys.length !== Object.getOwnPropertyNames(b).length) return false;
3580
3551
 
3581
- // We know that both objects have the same number of properties,
3582
- // so if every property in `a` has a matching property in `b`,
3583
- // the objects must be identical, regardless of key order.
3584
- for (const key of keys) {
3585
- if (!Object.prototype.hasOwnProperty.call(b, key) || !compare(a[key], b[key])) {
3586
- return false;
3587
- }
3588
- }
3589
- return true;
3590
- }
3552
+ /**
3553
+ * The login data decrypted into memory.
3554
+ * @deprecated Use `LoginStash` instead and decrypt it at the point of use.
3555
+ * This is an ongoing refactor to remove this type.
3556
+ */
3591
3557
 
3592
- // Arrays:
3593
- if (type === '[object Array]') {
3594
- if (a.length !== b.length) return false;
3595
- for (let i = 0; i < a.length; ++i) {
3596
- if (!compare(a[i], b[i])) return false;
3597
- }
3598
- return true;
3599
- }
3558
+ /**
3559
+ * A stash for a specific child account,
3560
+ * along with its containing tree.
3561
+ */
3600
3562
 
3601
- // Javascript dates:
3602
- if (type === '[object Date]') {
3603
- return a.getTime() === b.getTime();
3604
- }
3605
- if (type === '[object Map]') {
3606
- return compareMap(a, b);
3607
- }
3563
+ const asEdgeWalletInfo = cleaners.asObject({
3564
+ id: cleaners.asString,
3565
+ keys: asJsonObject,
3566
+ type: cleaners.asString
3567
+ });
3568
+ const wasEdgeWalletInfo = cleaners.uncleaner(asEdgeWalletInfo);
3608
3569
 
3609
- // Typed arrays:
3610
- if (TYPED_ARRAYS[type]) {
3611
- if (a.length !== b.length) return false;
3612
- for (let i = 0; i < a.length; ++i) {
3613
- if (a[i] !== b[i]) return false;
3614
- }
3615
- return true;
3616
- }
3570
+ /**
3571
+ * Returns the first keyInfo with a matching type.
3572
+ */
3573
+ function findFirstKey(keyInfos, type) {
3574
+ return keyInfos.find(info => info.type === type);
3575
+ }
3576
+ function makeAccountType(appId) {
3577
+ return appId === '' ? 'account-repo:co.airbitz.wallet' : `account-repo:${appId}`;
3578
+ }
3617
3579
 
3618
- // We don't even try comparing anything else:
3619
- return false;
3580
+ /**
3581
+ * Assembles the key metadata structure that is encrypted within a keyBox.
3582
+ * @param idKey Used to derive the wallet id. It's usually `dataKey`.
3583
+ */
3584
+ function makeKeyInfo(type, keys, idKey) {
3585
+ const hash = hmacSha256(utf8.parse(type), idKey ?? asEdgeStorageKeys(keys).dataKey);
3586
+ return {
3587
+ id: rfc4648.base64.stringify(hash),
3588
+ type,
3589
+ keys
3590
+ };
3620
3591
  }
3621
3592
 
3622
3593
  /**
3623
- * Compare Maps
3594
+ * Assembles all the resources needed to attach new keys to the account.
3595
+ * @param allowExisting True if the sync keys were derived deterministically,
3596
+ * which implies that duplicate sync keys on the server are not errors,
3597
+ * but leftovers from an earlier failed splitting attempt.
3624
3598
  */
3625
- function compareMap(map1, map2) {
3626
- if (map1.size !== map2.size) {
3627
- return false;
3599
+ function makeKeysKit(ai, sessionKey, keyInfos, allowExisting = false) {
3600
+ // For crash errors:
3601
+ ai.props.log.breadcrumb('makeKeysKit', {});
3602
+ const {
3603
+ io
3604
+ } = ai.props;
3605
+ const keyBoxes = keyInfos.map(info => ({
3606
+ created: new Date(),
3607
+ ...encrypt(io, utf8.parse(JSON.stringify(wasEdgeWalletInfo(info))), sessionKey.loginKey)
3608
+ }));
3609
+ const newSyncKeys = [];
3610
+ for (const info of keyInfos) {
3611
+ const storageKeys = cleaners.asMaybe(asEdgeStorageKeys)(info.keys);
3612
+ if (storageKeys == null) continue;
3613
+ newSyncKeys.push(rfc4648.base16.stringify(storageKeys.syncKey).toLowerCase());
3628
3614
  }
3629
- for (const [key, value] of map1) {
3630
- if (!map2.has(key) || map2.get(key) !== value) {
3631
- return false;
3615
+ return {
3616
+ loginId: sessionKey.loginId,
3617
+ server: wasCreateKeysPayload({
3618
+ allowExisting,
3619
+ keyBoxes,
3620
+ newSyncKeys
3621
+ }),
3622
+ serverPath: '/v2/login/keys',
3623
+ stash: {
3624
+ keyBoxes
3632
3625
  }
3633
- }
3634
- return true;
3626
+ };
3635
3627
  }
3628
+
3636
3629
  /**
3637
- * Returns true if two Javascript values are equal in value.
3630
+ * Flattens an array of key structures, removing duplicates.
3638
3631
  */
3639
- function compare(a, b) {
3640
- if (a === b) return true;
3641
-
3642
- // Fast path for primitives:
3643
- if (typeof a !== 'object') return false;
3644
- if (typeof b !== 'object') return false;
3645
-
3646
- // If these are objects, the internal `[[Class]]` properties must match:
3647
- const type = Object.prototype.toString.call(a);
3648
- if (type !== Object.prototype.toString.call(b)) return false;
3649
- return compareObjects(a, b, type);
3650
- }
3632
+ function mergeKeyInfos(keyInfos) {
3633
+ const out = [];
3634
+ const ids = new Map(); // Maps ID's to output array indexes
3651
3635
 
3652
- // How often to run jobs from the queue
3653
- let QUEUE_RUN_DELAY = 500;
3636
+ for (const keyInfo of keyInfos) {
3637
+ const {
3638
+ id,
3639
+ keys,
3640
+ type
3641
+ } = keyInfo;
3642
+ if (id == null || rfc4648.base64.parse(id).length !== 32) {
3643
+ throw new Error(`Key integrity violation: invalid id ${id}`);
3644
+ }
3645
+ const index = ids.get(id);
3646
+ if (index != null) {
3647
+ // We have already seen this id, so check for conflicts:
3648
+ const old = out[index];
3649
+ if (old.type !== type) {
3650
+ throw new Error(`Key integrity violation for ${id}: type ${type} does not match ${old.type}`);
3651
+ }
3652
+ for (const key of Object.keys(keys)) {
3653
+ if (old.keys[key] != null && old.keys[key] !== keys[key]) {
3654
+ throw new Error(`Key integrity violation for ${id}: ${key} keys do not match`);
3655
+ }
3656
+ }
3654
3657
 
3655
- // How many jobs to run from the queue on each cycle
3656
- let QUEUE_JOBS_PER_RUN = 3;
3657
- const updateQueue = [];
3658
- function enableTestMode() {
3659
- QUEUE_JOBS_PER_RUN = 99;
3660
- QUEUE_RUN_DELAY = 1;
3661
- }
3662
- function pushUpdate(update) {
3663
- if (updateQueue.length <= 0) {
3664
- startQueue();
3665
- }
3666
- let didUpdate = false;
3667
- for (const u of updateQueue) {
3668
- if (u.id === update.id && u.action === update.action) {
3669
- u.updateFunc = update.updateFunc;
3670
- didUpdate = true;
3671
- break;
3658
+ // Do the update:
3659
+ out[index] = {
3660
+ id,
3661
+ keys: {
3662
+ ...old.keys,
3663
+ ...keys
3664
+ },
3665
+ type
3666
+ };
3667
+ } else {
3668
+ // We haven't seen this id, so insert it:
3669
+ ids.set(id, out.length);
3670
+ out.push(keyInfo);
3672
3671
  }
3673
3672
  }
3674
- if (!didUpdate) {
3675
- updateQueue.push(update);
3676
- }
3677
- }
3678
- function startQueue() {
3679
- setTimeout(() => {
3680
- const numJobs = Math.min(QUEUE_JOBS_PER_RUN, updateQueue.length);
3681
- for (let i = 0; i < numJobs; i++) {
3682
- const u = updateQueue.shift();
3683
- if (u != null) u.updateFunc();
3684
- }
3685
- if (updateQueue.length > 0) {
3686
- startQueue();
3687
- }
3688
- }, QUEUE_RUN_DELAY);
3673
+ return out;
3689
3674
  }
3690
3675
 
3691
3676
  /**
3692
- * Reads a JSON-style object into a JavaScript `Map` object with string keys.
3693
- */
3694
- function asMap(cleaner) {
3695
- const asJsonObject = cleaners.asObject(cleaner);
3696
- return cleaners.asCodec(raw => {
3697
- const clean = asJsonObject(raw);
3698
- const out = new Map();
3699
- for (const key of Object.keys(clean)) out.set(key, clean[key]);
3700
- return out;
3701
- }, clean => {
3702
- const out = {};
3703
- clean.forEach((value, key) => {
3704
- out[key] = value;
3705
- });
3706
- return asJsonObject(out);
3707
- });
3708
- }
3709
-
3710
- /**
3711
- * Reads a JSON-style object into a JavaScript `Map` object
3712
- * with EdgeTokenId keys.
3677
+ * Decrypts the private keys contained in a login.
3713
3678
  */
3714
- function asTokenIdMap(cleaner) {
3715
- const asJsonObject = cleaners.asObject(cleaner);
3716
- return cleaners.asCodec(raw => {
3717
- const clean = asJsonObject(raw);
3718
- const out = new Map();
3719
- for (const key of Object.keys(clean)) {
3720
- out.set(key === '' ? null : key, clean[key]);
3721
- }
3722
- return out;
3723
- }, clean => {
3724
- const out = {};
3725
- clean.forEach((value, key) => {
3726
- out[key == null ? '' : key] = value;
3727
- });
3728
- return asJsonObject(out);
3729
- });
3730
- }
3731
-
3732
- const asEdgeMetadata = raw => {
3733
- const clean = asDiskMetadata(raw);
3679
+ function decryptKeyInfos(stash, loginKey, keyDates = new Map()) {
3734
3680
  const {
3735
- exchangeAmount = {}
3736
- } = clean;
3681
+ appId,
3682
+ keyBoxes = []
3683
+ } = stash;
3684
+ const legacyKeys = [];
3737
3685
 
3738
- // Delete corrupt amounts that exceed the Javascript number range:
3739
- for (const fiat of Object.keys(exchangeAmount)) {
3740
- if (String(exchangeAmount[fiat]).includes('e')) {
3741
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
3742
- delete exchangeAmount[fiat];
3743
- }
3744
- }
3745
- return clean;
3746
- };
3747
- function mergeMetadata(under, over) {
3748
- const out = {
3749
- exchangeAmount: {}
3750
- };
3686
+ // BitID wallet:
3751
3687
  const {
3752
- exchangeAmount = {}
3753
- } = out;
3754
-
3755
- // Merge the fiat amounts:
3756
- const underAmounts = under.exchangeAmount ?? {};
3757
- const overAmounts = over.exchangeAmount ?? {};
3758
- for (const fiat of Object.keys(underAmounts)) {
3759
- if (overAmounts[fiat] !== null) exchangeAmount[fiat] = underAmounts[fiat];
3688
+ mnemonicBox,
3689
+ rootKeyBox
3690
+ } = stash;
3691
+ if (mnemonicBox != null && rootKeyBox != null) {
3692
+ const rootKey = decrypt(rootKeyBox, loginKey);
3693
+ const infoKey = hmacSha256(rootKey, utf8.parse('infoKey'));
3694
+ const keys = {
3695
+ mnemonic: decryptText(mnemonicBox, infoKey),
3696
+ rootKey: rfc4648.base64.stringify(rootKey)
3697
+ };
3698
+ legacyKeys.push(makeKeyInfo('wallet:bitid', keys, rootKey));
3760
3699
  }
3761
- for (const fiat of Object.keys(overAmounts)) {
3762
- const amount = overAmounts[fiat];
3763
- if (amount != null) exchangeAmount[fiat] = amount;
3700
+
3701
+ // Account settings:
3702
+ if (stash.syncKeyBox != null) {
3703
+ const syncKey = decrypt(stash.syncKeyBox, loginKey);
3704
+ const type = makeAccountType(appId);
3705
+ const keys = wasEdgeStorageKeys({
3706
+ dataKey: loginKey,
3707
+ syncKey
3708
+ });
3709
+ legacyKeys.push(makeKeyInfo(type, keys, loginKey));
3764
3710
  }
3765
3711
 
3766
- // Merge simple fields:
3767
- if (over.bizId !== null) out.bizId = over.bizId ?? under.bizId;
3768
- if (over.category !== null) out.category = over.category ?? under.category;
3769
- if (over.name !== null) out.name = over.name ?? under.name;
3770
- if (over.notes !== null) out.notes = over.notes ?? under.notes;
3771
- return out;
3712
+ // Keys:
3713
+ const keyInfos = keyBoxes.map(box => {
3714
+ const keys = asEdgeWalletInfo(JSON.parse(decryptText(box, loginKey)));
3715
+ const created = mergeKeyDate(box.created, keyDates.get(keys.id));
3716
+ if (created != null) keyDates.set(keys.id, created);
3717
+ return keys;
3718
+ });
3719
+ return mergeKeyInfos([...legacyKeys, ...keyInfos]).map(walletInfo => fixWalletInfo(walletInfo));
3772
3720
  }
3773
- const asDiskMetadata = cleaners.asObject({
3774
- bizId: cleaners.asOptional(cleaners.asNumber),
3775
- category: cleaners.asOptional(cleaners.asString),
3776
- exchangeAmount: cleaners.asOptional(cleaners.asObject(cleaners.asNumber)),
3777
- name: cleaners.asOptional(cleaners.asString),
3778
- notes: cleaners.asOptional(cleaners.asString)
3779
- });
3780
-
3781
- /**
3782
- * The on-disk transaction format.
3783
- */
3784
3721
 
3785
3722
  /**
3786
- * The Airbitz on-disk transaction format.
3723
+ * Returns all the wallet infos accessible from this login object.
3787
3724
  */
3725
+ function decryptAllWalletInfos(stashTree, sessionKey, legacyWalletInfos, walletStates) {
3726
+ // Maps from walletId's to appId's:
3727
+ const dates = new Map();
3728
+ const appIdMap = new Map();
3729
+ const walletInfos = [...legacyWalletInfos];
3788
3730
 
3789
- /**
3790
- * The Airbitz on-disk address format.
3791
- */
3731
+ // Navigate to the starting node:
3732
+ const stash = getChildStash(stashTree, sessionKey.loginId);
3792
3733
 
3793
- /**
3794
- * An on-disk cache to quickly map Airbitz filenames to their dates.
3795
- */
3734
+ // Add the legacy wallets first:
3735
+ for (const info of legacyWalletInfos) {
3736
+ walletInfos.push(info);
3737
+ const appIds = appIdMap.get(info.id);
3738
+ if (appIds != null) appIds.push(stash.appId);else appIdMap.set(info.id, [stash.appId]);
3739
+ }
3740
+ function getAllWalletInfosLoop(stash, loginKey) {
3741
+ // Add our own walletInfos:
3742
+ const keyInfos = decryptKeyInfos(stash, loginKey, dates);
3743
+ for (const info of keyInfos) {
3744
+ walletInfos.push(info);
3745
+ const appIds = appIdMap.get(info.id);
3746
+ if (appIds != null) appIds.push(stash.appId);else appIdMap.set(info.id, [stash.appId]);
3747
+ }
3796
3748
 
3797
- // ---------------------------------------------------------------------
3798
- // building-block cleaners
3799
- // ---------------------------------------------------------------------
3800
- const asEdgeTokenId = cleaners.asEither(cleaners.asString, cleaners.asNull);
3801
- const asFeeRate = cleaners.asValue('high', 'standard', 'low');
3802
- const asEdgeTxSwap = cleaners.asObject({
3803
- orderId: cleaners.asOptional(cleaners.asString),
3804
- orderUri: cleaners.asOptional(cleaners.asString),
3805
- isEstimate: cleaners.asBoolean,
3806
- // The EdgeSwapInfo from the swap plugin:
3807
- plugin: cleaners.asObject({
3808
- pluginId: cleaners.asString,
3809
- displayName: cleaners.asString,
3810
- supportEmail: cleaners.asOptional(cleaners.asString)
3811
- }),
3812
- // Address information:
3813
- payoutAddress: cleaners.asString,
3814
- payoutCurrencyCode: cleaners.asString,
3815
- payoutTokenId: cleaners.asOptional(asEdgeTokenId),
3816
- payoutNativeAmount: cleaners.asString,
3817
- payoutWalletId: cleaners.asString,
3818
- refundAddress: cleaners.asOptional(cleaners.asString)
3819
- });
3820
- function asIntegerString(raw) {
3821
- const clean = cleaners.asString(raw);
3822
- if (!/^\d+$/.test(clean)) {
3823
- throw new Error('Expected an integer string');
3749
+ // Add our children's walletInfos:
3750
+ for (const child of stash.children ?? []) {
3751
+ if (child.parentBox == null) continue;
3752
+ getAllWalletInfosLoop(child, decrypt(child.parentBox, loginKey));
3753
+ }
3824
3754
  }
3825
- return clean;
3755
+ getAllWalletInfosLoop(stash, sessionKey.loginKey);
3756
+ return mergeKeyInfos(walletInfos).map(info => {
3757
+ return {
3758
+ appId: getLast(appIdMap.get(info.id) ?? []),
3759
+ appIds: appIdMap.get(info.id) ?? [],
3760
+ // Defaults to be overwritten:
3761
+ archived: false,
3762
+ created: dates.get(info.id),
3763
+ deleted: false,
3764
+ hidden: false,
3765
+ sortIndex: walletInfos.length,
3766
+ // Copy the `imported` field from the raw keys if it exists
3767
+ imported: info.keys.imported,
3768
+ // Actual info:
3769
+ ...walletStates[info.id],
3770
+ ...info
3771
+ };
3772
+ });
3826
3773
  }
3827
3774
 
3828
- // ---------------------------------------------------------------------
3829
- // file cleaners
3830
- // ---------------------------------------------------------------------
3775
+ /**
3776
+ * Upgrades legacy wallet info structures into the new format.
3777
+ *
3778
+ * Wallets normally have `wallet:pluginId` as their type,
3779
+ * but some legacy wallets also put format information into the wallet type.
3780
+ * This routine moves the information out of the wallet type into the keys.
3781
+ *
3782
+ * It also provides some other default values as a historical accident,
3783
+ * but the bitcoin plugin can just provide its own fallback values if
3784
+ * `format` or `coinType` are missing. Please don't make the problem worse
3785
+ * by adding more code here!
3786
+ */
3787
+ function fixWalletInfo(walletInfo) {
3788
+ const {
3789
+ id,
3790
+ keys,
3791
+ type
3792
+ } = walletInfo;
3831
3793
 
3832
- const asEdgeAssetAmount = cleaners.asObject({
3833
- pluginId: cleaners.asString,
3834
- tokenId: asEdgeTokenId,
3835
- nativeAmount: cleaners.asOptional(asIntegerString)
3836
- });
3837
- const asEdgeFiatAmount = cleaners.asObject({
3838
- // core-js style fiat code including 'iso:'
3839
- fiatCurrencyCode: cleaners.asString,
3840
- fiatAmount: cleaners.asString
3841
- });
3842
- const asEdgeSwapInfo = cleaners.asObject({
3843
- pluginId: cleaners.asString,
3844
- displayName: cleaners.asString,
3845
- isDex: cleaners.asOptional(cleaners.asBoolean),
3846
- orderUri: cleaners.asOptional(cleaners.asString),
3847
- // The orderId would be appended to this
3848
- supportEmail: cleaners.asString
3849
- });
3850
- const asEdgeTxActionSwap = cleaners.asObject({
3851
- actionType: cleaners.asValue('swap'),
3852
- swapInfo: asEdgeSwapInfo,
3853
- orderId: cleaners.asOptional(cleaners.asString),
3854
- orderUri: cleaners.asOptional(cleaners.asString),
3855
- isEstimate: cleaners.asOptional(cleaners.asBoolean),
3856
- canBePartial: cleaners.asOptional(cleaners.asBoolean),
3857
- fromAsset: asEdgeAssetAmount,
3858
- toAsset: asEdgeAssetAmount,
3859
- payoutWalletId: cleaners.asString,
3860
- payoutAddress: cleaners.asString,
3861
- refundAddress: cleaners.asOptional(cleaners.asString)
3862
- });
3863
- const asEdgeTxActionStake = cleaners.asObject({
3864
- actionType: cleaners.asValue('stake'),
3865
- pluginId: cleaners.asString,
3866
- stakeAssets: cleaners.asArray(asEdgeAssetAmount)
3867
- });
3868
- const asEdgeTxActionFiat = cleaners.asObject({
3869
- actionType: cleaners.asValue('fiat'),
3870
- orderId: cleaners.asString,
3871
- orderUri: cleaners.asOptional(cleaners.asString),
3872
- isEstimate: cleaners.asBoolean,
3873
- fiatPlugin: cleaners.asObject({
3874
- providerId: cleaners.asString,
3875
- providerDisplayName: cleaners.asString,
3876
- supportEmail: cleaners.asOptional(cleaners.asString)
3877
- }),
3878
- payinAddress: cleaners.asOptional(cleaners.asString),
3879
- payoutAddress: cleaners.asOptional(cleaners.asString),
3880
- fiatAsset: asEdgeFiatAmount,
3881
- cryptoAsset: asEdgeAssetAmount
3882
- });
3883
- const asEdgeTxActionTokenApproval = cleaners.asObject({
3884
- actionType: cleaners.asValue('tokenApproval'),
3885
- tokenApproved: asEdgeAssetAmount,
3886
- tokenContractAddress: cleaners.asString,
3887
- contractAddress: cleaners.asString
3888
- });
3889
- const asEdgeTxActionGiftCard = cleaners.asObject({
3890
- actionType: cleaners.asValue('giftCard'),
3891
- orderId: cleaners.asString,
3892
- orderUri: cleaners.asOptional(cleaners.asString),
3893
- provider: cleaners.asObject({
3894
- providerId: cleaners.asString,
3895
- displayName: cleaners.asString,
3896
- supportEmail: cleaners.asOptional(cleaners.asString)
3897
- }),
3898
- card: cleaners.asObject({
3899
- name: cleaners.asString,
3900
- imageUrl: cleaners.asOptional(cleaners.asString),
3901
- fiatAmount: cleaners.asString,
3902
- fiatCurrencyCode: cleaners.asString
3903
- }),
3904
- redemption: cleaners.asOptional(cleaners.asObject({
3905
- code: cleaners.asOptional(cleaners.asString),
3906
- url: cleaners.asOptional(cleaners.asString)
3907
- }))
3908
- });
3909
- const asEdgeTxAction = cleaners.asEither(asEdgeTxActionSwap, asEdgeTxActionStake, asEdgeTxActionFiat, asEdgeTxActionTokenApproval, asEdgeTxActionGiftCard);
3910
- const asEdgeAssetActionType = cleaners.asValue('claim', 'claimOrder', 'stake', 'stakeNetworkFee', 'stakeOrder', 'unstake', 'unstakeNetworkFee', 'unstakeOrder', 'swap', 'swapNetworkFee', 'swapOrderPost', 'swapOrderFill', 'swapOrderCancel', 'buy', 'sell', 'sellNetworkFee', 'tokenApproval', 'transfer', 'transferNetworkFee', 'giftCard');
3911
- const asEdgeAssetAction = cleaners.asObject({
3912
- assetActionType: asEdgeAssetActionType
3913
- });
3794
+ // Wallet types we need to fix:
3795
+ const defaults = {
3796
+ // BTC:
3797
+ 'wallet:bitcoin-bip44': {
3798
+ format: 'bip44',
3799
+ coinType: 0
3800
+ },
3801
+ 'wallet:bitcoin-bip49': {
3802
+ format: 'bip49',
3803
+ coinType: 0
3804
+ },
3805
+ // BCH:
3806
+ 'wallet:bitcoincash-bip32': {
3807
+ format: 'bip32'
3808
+ },
3809
+ 'wallet:bitcoincash-bip44': {
3810
+ format: 'bip44',
3811
+ coinType: 145
3812
+ },
3813
+ // BCH testnet:
3814
+ 'wallet:bitcoincash-bip44-testnet': {
3815
+ format: 'bip44',
3816
+ coinType: 1
3817
+ },
3818
+ // BTC testnet:
3819
+ 'wallet:bitcoin-bip44-testnet': {
3820
+ format: 'bip44',
3821
+ coinType: 1
3822
+ },
3823
+ 'wallet:bitcoin-bip49-testnet': {
3824
+ format: 'bip49',
3825
+ coinType: 1
3826
+ },
3827
+ // DASH:
3828
+ 'wallet:dash-bip44': {
3829
+ format: 'bip44',
3830
+ coinType: 5
3831
+ },
3832
+ // DOGE:
3833
+ 'wallet:dogecoin-bip44': {
3834
+ format: 'bip44',
3835
+ coinType: 3
3836
+ },
3837
+ // LTC:
3838
+ 'wallet:litecoin-bip44': {
3839
+ format: 'bip44',
3840
+ coinType: 2
3841
+ },
3842
+ 'wallet:litecoin-bip49': {
3843
+ format: 'bip49',
3844
+ coinType: 2
3845
+ },
3846
+ // FTC:
3847
+ 'wallet:feathercoin-bip49': {
3848
+ format: 'bip49',
3849
+ coinType: 8
3850
+ },
3851
+ 'wallet:feathercoin-bip44': {
3852
+ format: 'bip44',
3853
+ coinType: 8
3854
+ },
3855
+ // QTUM:
3856
+ 'wallet:qtum-bip44': {
3857
+ format: 'bip44',
3858
+ coinType: 2301
3859
+ },
3860
+ // UFO:
3861
+ 'wallet:ufo-bip49': {
3862
+ format: 'bip49',
3863
+ coinType: 202
3864
+ },
3865
+ 'wallet:ufo-bip84': {
3866
+ format: 'bip84',
3867
+ coinType: 202
3868
+ },
3869
+ // XZC:
3870
+ 'wallet:zcoin-bip44': {
3871
+ format: 'bip44',
3872
+ coinType: 136
3873
+ },
3874
+ // The plugin itself could handle these lines, but they are here
3875
+ // as a historical accident. Please don't add more:
3876
+ 'wallet:bitcoin-testnet': {
3877
+ format: 'bip32'
3878
+ },
3879
+ 'wallet:bitcoin': {
3880
+ format: 'bip32'
3881
+ },
3882
+ 'wallet:bitcoincash-testnet': {
3883
+ format: 'bip32'
3884
+ },
3885
+ 'wallet:litecoin': {
3886
+ format: 'bip32',
3887
+ coinType: 2
3888
+ },
3889
+ 'wallet:zcoin': {
3890
+ format: 'bip32',
3891
+ coinType: 136
3892
+ }
3893
+ };
3894
+ if (defaults[type] != null) {
3895
+ return {
3896
+ id,
3897
+ keys: {
3898
+ ...defaults[type],
3899
+ ...keys
3900
+ },
3901
+ type: type.replace(/-bip[0-9]+/, '')
3902
+ };
3903
+ }
3904
+ return walletInfo;
3905
+ }
3906
+ async function makeCurrencyWalletKeys(ai, walletType, opts) {
3907
+ const {
3908
+ importText,
3909
+ keyOptions,
3910
+ keys
3911
+ } = opts;
3914
3912
 
3915
- /**
3916
- * Old core versions used currency codes instead of tokenId's.
3917
- */
3918
- const asLegacyTokensFile = cleaners.asArray(cleaners.asString);
3913
+ // Helper function to bundle up the keys:
3914
+ function finalizeKeys(newKeys, imported) {
3915
+ if (imported != null) newKeys = {
3916
+ ...newKeys,
3917
+ imported
3918
+ };
3919
+ return fixWalletInfo(makeKeyInfo(walletType, {
3920
+ ...wasEdgeStorageKeys(createStorageKeys(ai)),
3921
+ ...newKeys
3922
+ }));
3923
+ }
3919
3924
 
3920
- /**
3921
- * Stores enabled tokenId's on disk.
3922
- */
3923
- const asTokensFile = cleaners.asObject({
3924
- // All the tokens that the engine should check.
3925
- // This includes both manually-enabled tokens and auto-enabled tokens:
3926
- enabledTokenIds: cleaners.asArray(cleaners.asString),
3927
- // These tokenId's have been detected on-chain at least once.
3928
- // The user can still remove them from the enabled tokens list.
3929
- detectedTokenIds: cleaners.asArray(cleaners.asString)
3930
- });
3931
- const asTransactionAsset = cleaners.asObject({
3932
- assetAction: cleaners.asOptional(asEdgeAssetAction),
3933
- metadata: asEdgeMetadata,
3934
- nativeAmount: cleaners.asOptional(cleaners.asString),
3935
- providerFeeSent: cleaners.asOptional(cleaners.asString)
3936
- });
3937
- const asTransactionFile = cleaners.asObject({
3938
- txid: cleaners.asString,
3939
- internal: cleaners.asBoolean,
3940
- creationDate: cleaners.asNumber,
3941
- currencies: asMap(asTransactionAsset),
3942
- tokens: cleaners.asOptional(asTokenIdMap(asTransactionAsset), () => new Map()),
3943
- deviceDescription: cleaners.asOptional(cleaners.asString),
3944
- feeRateRequested: cleaners.asOptional(cleaners.asEither(asFeeRate, asJsonObject)),
3945
- feeRateUsed: cleaners.asOptional(asJsonObject),
3946
- payees: cleaners.asOptional(cleaners.asArray(cleaners.asObject({
3947
- address: cleaners.asString,
3948
- amount: cleaners.asString,
3949
- currency: cleaners.asString,
3950
- tag: cleaners.asOptional(cleaners.asString)
3951
- }))),
3952
- savedAction: cleaners.asOptional(asEdgeTxAction),
3953
- secret: cleaners.asOptional(cleaners.asString),
3954
- swap: cleaners.asOptional(asEdgeTxSwap)
3955
- });
3956
- const asLegacyTransactionFile = cleaners.asObject({
3957
- airbitzFeeWanted: cleaners.asNumber,
3958
- meta: cleaners.asObject({
3959
- amountFeeAirBitzSatoshi: cleaners.asNumber,
3960
- balance: cleaners.asNumber,
3961
- fee: cleaners.asNumber,
3962
- // Metadata:
3963
- amountCurrency: cleaners.asNumber,
3964
- bizId: cleaners.asNumber,
3965
- category: cleaners.asString,
3966
- name: cleaners.asString,
3967
- notes: cleaners.asString,
3968
- // Obsolete/moved fields:
3969
- attributes: cleaners.asNumber,
3970
- amountSatoshi: cleaners.asNumber,
3971
- amountFeeMinersSatoshi: cleaners.asNumber,
3972
- airbitzFee: cleaners.asNumber
3973
- }),
3974
- ntxid: cleaners.asString,
3975
- state: cleaners.asObject({
3976
- creationDate: cleaners.asNumber,
3977
- internal: cleaners.asBoolean,
3978
- malleableTxId: cleaners.asString
3979
- })
3980
- });
3981
- const asLegacyAddressFile = cleaners.asObject({
3982
- seq: cleaners.asNumber,
3983
- // index
3984
- address: cleaners.asString,
3985
- state: cleaners.asObject({
3986
- recycleable: cleaners.asOptional(cleaners.asBoolean, true),
3987
- creationDate: cleaners.asOptional(cleaners.asNumber, 0)
3988
- }),
3989
- meta: cleaners.asObject({
3990
- amountSatoshi: cleaners.asOptional(cleaners.asNumber, 0) // requestAmount
3991
- // TODO: Normal EdgeMetadata
3992
- }).withRest
3993
- });
3994
- const asLegacyMapFile = cleaners.asObject(cleaners.asObject({
3995
- timestamp: cleaners.asNumber,
3996
- txidHash: cleaners.asString
3997
- }));
3925
+ // If we have raw keys, just return those:
3926
+ if (keys != null) return finalizeKeys(keys);
3998
3927
 
3999
- /**
4000
- * Public keys cached in the wallet's local storage.
4001
- */
4002
- const asPublicKeyFile = cleaners.asObject({
4003
- walletInfo: cleaners.asObject({
4004
- id: cleaners.asString,
4005
- keys: asJsonObject,
4006
- type: cleaners.asString
4007
- })
4008
- });
3928
+ // Grab the currency tools:
3929
+ const pluginId = findCurrencyPluginId(ai.props.state.plugins.currency, walletType);
3930
+ const tools = await getCurrencyTools(ai, pluginId);
4009
3931
 
4010
- /**
4011
- * The wallet's local storage file for the last seen "checkpoint". The core
4012
- * does not know the contents of the checkpoint, so it just as an arbitrary
4013
- * string.
4014
- */
4015
- const asSeenCheckpointFile = cleaners.asObject({
4016
- checkpoint: cleaners.asOptional(cleaners.asString),
4017
- subscribedAddresses: cleaners.asOptional(cleaners.asArray(cleaners.asObject({
4018
- address: cleaners.asString,
4019
- checkpoint: cleaners.asOptional(cleaners.asString)
4020
- })), () => [])
4021
- });
4022
- const asWalletFiatFile = cleaners.asObject({
4023
- fiat: cleaners.asOptional(cleaners.asString),
4024
- num: cleaners.asOptional(cleaners.asNumber)
4025
- });
4026
- const asWalletNameFile = cleaners.asObject({
4027
- walletName: cleaners.asEither(cleaners.asString, cleaners.asNull)
4028
- });
3932
+ // If we have text to import, use that:
3933
+ if (importText != null) {
3934
+ if (tools.importPrivateKey == null) {
3935
+ throw new Error('This wallet does not support importing keys');
3936
+ }
3937
+ return finalizeKeys(await tools.importPrivateKey(importText, keyOptions), true);
3938
+ }
4029
3939
 
4030
- function shuffle(arr) {
4031
- for (let i = arr.length - 1; i > 0; i--) {
4032
- const j = Math.floor(Math.random() * (i + 1));
4033
- [arr[i], arr[j]] = [arr[j], arr[i]];
3940
+ // Derive fresh keys:
3941
+ return finalizeKeys(await tools.createPrivateKey(walletType, keyOptions), false);
3942
+ }
3943
+ async function finishWalletCreation(ai, accountId, walletId, opts) {
3944
+ const {
3945
+ enabledTokenIds,
3946
+ fiatCurrencyCode,
3947
+ migratedFromWalletId,
3948
+ name
3949
+ } = opts;
3950
+ const wallet = await waitForCurrencyWallet(ai, walletId);
3951
+
3952
+ // Write ancillary files to disk:
3953
+ if (migratedFromWalletId != null) {
3954
+ await changeWalletStates(ai, accountId, {
3955
+ [walletId]: {
3956
+ migratedFromWalletId
3957
+ }
3958
+ });
4034
3959
  }
4035
- return arr;
3960
+ if (name != null) {
3961
+ await wallet.renameWallet(name);
3962
+ }
3963
+ if (fiatCurrencyCode != null) {
3964
+ await wallet.setFiatCurrencyCode(fiatCurrencyCode);
3965
+ }
3966
+ if (enabledTokenIds != null && enabledTokenIds.length > 0) {
3967
+ await wallet.changeEnabledTokenIds(enabledTokenIds);
3968
+ }
3969
+ return wallet;
3970
+ }
3971
+ function getLast(array) {
3972
+ return array[array.length - 1];
3973
+ }
3974
+
3975
+ /**
3976
+ * Returns the earliest date, or undefined if neither date exists.
3977
+ */
3978
+ function mergeKeyDate(a, b) {
3979
+ if (a == null) return b;
3980
+ if (b == null) return a;
3981
+ return new Date(Math.min(a.valueOf(), b.valueOf()));
4036
3982
  }
4037
3983
 
4038
3984
  const legacyWalletFile = makeJsonFile(asLegacyWalletFile);
@@ -4297,440 +4243,737 @@ async function reloadPluginSettings(ai, accountId) {
4297
4243
  });
4298
4244
  }
4299
4245
 
4300
- /**
4301
- * A key that decrypts a login stash.
4302
- */
4246
+ async function listSplittableWalletTypes(ai, accountId, walletId) {
4247
+ const {
4248
+ allWalletInfosFull
4249
+ } = ai.props.state.accounts[accountId];
4303
4250
 
4304
- /**
4305
- * The login data decrypted into memory.
4306
- * @deprecated Use `LoginStash` instead and decrypt it at the point of use.
4307
- * This is an ongoing refactor to remove this type.
4308
- */
4251
+ // Find the wallet we are going to split:
4252
+ const walletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === walletId);
4253
+ if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`);
4254
+ const pluginId = maybeFindCurrencyPluginId(ai.props.state.plugins.currency, walletInfo.type);
4255
+ if (pluginId == null) return [];
4309
4256
 
4310
- /**
4311
- * A stash for a specific child account,
4312
- * along with its containing tree.
4313
- */
4257
+ // Get the list of available types:
4258
+ const tools = await getCurrencyTools(ai, pluginId);
4259
+ if (tools.getSplittableTypes == null) return [];
4260
+ const types = await tools.getSplittableTypes(walletInfo);
4314
4261
 
4315
- const asEdgeWalletInfo = cleaners.asObject({
4316
- id: cleaners.asString,
4317
- keys: asJsonObject,
4318
- type: cleaners.asString
4319
- });
4320
- const wasEdgeWalletInfo = cleaners.uncleaner(asEdgeWalletInfo);
4262
+ // Filter out wallet types we have already split:
4263
+ return types.filter(type => {
4264
+ const newWalletInfo = makeSplitWalletInfo(walletInfo, type);
4265
+ const existingWalletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === newWalletInfo.id);
4266
+ // We can split the wallet if it doesn't exist, or is deleted:
4267
+ return existingWalletInfo == null || existingWalletInfo.archived || existingWalletInfo.deleted;
4268
+ });
4269
+ }
4270
+ function makeSplitWalletInfo(walletInfo, newWalletType) {
4271
+ const {
4272
+ id,
4273
+ type,
4274
+ keys
4275
+ } = walletInfo;
4276
+ const cleanKeys = cleaners.asMaybe(asEdgeStorageKeys)(keys);
4277
+ if (cleanKeys == null) {
4278
+ throw new Error(`Wallet ${id} is not a splittable type`);
4279
+ }
4280
+ const {
4281
+ dataKey,
4282
+ syncKey
4283
+ } = cleanKeys;
4284
+ const xorKey = xorData(hmacSha256(utf8.parse(type), dataKey), hmacSha256(utf8.parse(newWalletType), dataKey));
4321
4285
 
4322
- /**
4323
- * Returns the first keyInfo with a matching type.
4324
- */
4325
- function findFirstKey(keyInfos, type) {
4326
- return keyInfos.find(info => info.type === type);
4286
+ // Fix the id:
4287
+ const newWalletId = xorData(rfc4648.base64.parse(id), xorKey);
4288
+ const newSyncKey = xorData(syncKey, xorKey.subarray(0, syncKey.length));
4289
+
4290
+ // Fix the keys:
4291
+ const networkName = type.replace(/wallet:/, '').replace('-', '');
4292
+ const newNetworkName = newWalletType.replace(/wallet:/, '').replace('-', '');
4293
+ const newKeys = wasEdgeStorageKeys({
4294
+ dataKey,
4295
+ syncKey: newSyncKey
4296
+ });
4297
+ for (const key of Object.keys(keys)) {
4298
+ const newKey = key === networkName + 'Key' ? newNetworkName + 'Key' : key;
4299
+ if (newKeys[newKey] != null) continue;
4300
+ newKeys[newKey] = keys[key];
4301
+ }
4302
+ return {
4303
+ id: rfc4648.base64.stringify(newWalletId),
4304
+ keys: newKeys,
4305
+ type: newWalletType
4306
+ };
4327
4307
  }
4328
- function makeAccountType(appId) {
4329
- return appId === '' ? 'account-repo:co.airbitz.wallet' : `account-repo:${appId}`;
4308
+ async function splitWalletInfo(ai, accountId, walletInfo, splitWallets, rejectDupes) {
4309
+ const accountState = ai.props.state.accounts[accountId];
4310
+ const {
4311
+ allWalletInfosFull,
4312
+ sessionKey
4313
+ } = accountState;
4314
+
4315
+ // Validate the wallet types:
4316
+ const plugins = ai.props.state.plugins.currency;
4317
+ const splitInfos = new Map();
4318
+ for (const item of splitWallets) {
4319
+ const {
4320
+ walletType
4321
+ } = item;
4322
+ const pluginId = maybeFindCurrencyPluginId(plugins, item.walletType);
4323
+ if (pluginId == null) {
4324
+ throw new Error(`Cannot find plugin for wallet type "${walletType}"`);
4325
+ }
4326
+ if (splitInfos.has(walletType)) {
4327
+ throw new Error(`Duplicate wallet type "${walletType}"`);
4328
+ }
4329
+ splitInfos.set(walletType, makeSplitWalletInfo(walletInfo, walletType));
4330
+ }
4331
+
4332
+ // Do we need BitcoinABC/SV replay protection?
4333
+ const needsProtection = walletInfo.type === 'wallet:bitcoincash' &&
4334
+ // We can re-protect a wallet by doing a repeated split,
4335
+ // so don't check if the wallet already exists:
4336
+ splitInfos.has('wallet:bitcoinsv');
4337
+ if (needsProtection) {
4338
+ const existingWallet = ai.props.output?.currency?.wallets[walletInfo.id]?.walletApi;
4339
+ if (existingWallet == null) {
4340
+ throw new Error(`Cannot find wallet ${walletInfo.id}`);
4341
+ }
4342
+ await protectBchWallet(existingWallet);
4343
+ }
4344
+
4345
+ // Sort the wallet infos into two categories:
4346
+ const toRestore = [];
4347
+ const toCreate = [];
4348
+ for (const newWalletInfo of splitInfos.values()) {
4349
+ const existingWalletInfo = allWalletInfosFull.find(info => info.id === newWalletInfo.id);
4350
+ if (existingWalletInfo == null) {
4351
+ toCreate.push(newWalletInfo);
4352
+ } else {
4353
+ if (existingWalletInfo.archived || existingWalletInfo.deleted) {
4354
+ toRestore.push(existingWalletInfo);
4355
+ } else if (rejectDupes) {
4356
+ if (
4357
+ // It's OK to re-split if we are adding protection:
4358
+ walletInfo.type !== 'wallet:bitcoincash' || newWalletInfo.type !== 'wallet:bitcoinsv') {
4359
+ throw new Error(`This wallet has already been split (${newWalletInfo.type})`);
4360
+ }
4361
+ }
4362
+ }
4363
+ }
4364
+
4365
+ // Restore anything that has simply been deleted:
4366
+ if (toRestore.length > 0) {
4367
+ const newStates = {};
4368
+ let hasChanges = false;
4369
+ for (const existingWalletInfo of toRestore) {
4370
+ if (existingWalletInfo.archived || existingWalletInfo.deleted) {
4371
+ hasChanges = true;
4372
+ newStates[existingWalletInfo.id] = {
4373
+ archived: false,
4374
+ deleted: false,
4375
+ migratedFromWalletId: existingWalletInfo.migratedFromWalletId
4376
+ };
4377
+ }
4378
+ }
4379
+ if (hasChanges) await changeWalletStates(ai, accountId, newStates);
4380
+ }
4381
+
4382
+ // Add the keys to the login:
4383
+ if (toCreate.length > 0) {
4384
+ const kit = makeKeysKit(ai, sessionKey, toCreate, true);
4385
+ await applyKit(ai, sessionKey, kit);
4386
+ }
4387
+
4388
+ // Wait for the new wallets to load:
4389
+ const out = await Promise.all(splitWallets.map(async splitInfo => {
4390
+ const walletInfo = splitInfos.get(splitInfo.walletType);
4391
+ if (walletInfo == null) {
4392
+ throw new Error(`Missing wallet info for ${splitInfo.walletType}`);
4393
+ }
4394
+ return await makeEdgeResult(finishWalletSplitting(ai, walletInfo.id, toCreate.find(info => info.type === splitInfo.walletType) != null ? splitInfo : undefined));
4395
+ }));
4396
+ return out;
4397
+ }
4398
+ async function finishWalletSplitting(ai, walletId, item) {
4399
+ const wallet = await waitForCurrencyWallet(ai, walletId);
4400
+
4401
+ // Try to copy metadata on a best-effort basis.
4402
+ // In the future we should clone the repo instead:
4403
+ if (item?.name != null) {
4404
+ await wallet.renameWallet(item.name).catch(error => ai.props.onError(error));
4405
+ }
4406
+ if (item?.fiatCurrencyCode != null) {
4407
+ await wallet.setFiatCurrencyCode(item.fiatCurrencyCode).catch(error => ai.props.onError(error));
4408
+ }
4409
+ return wallet;
4410
+ }
4411
+ async function protectBchWallet(wallet) {
4412
+ const bchCurrency = {
4413
+ currencyCode: 'BCH',
4414
+ tokenId: null
4415
+ };
4416
+
4417
+ // Create a UTXO which can be spend only on the ABC network
4418
+ const spendInfoSplit = {
4419
+ ...bchCurrency,
4420
+ spendTargets: [{
4421
+ nativeAmount: '10000',
4422
+ otherParams: {
4423
+ script: {
4424
+ type: 'replayProtection'
4425
+ }
4426
+ },
4427
+ publicAddress: ''
4428
+ }],
4429
+ metadata: {},
4430
+ networkFeeOption: 'high'
4431
+ };
4432
+ const splitTx = await wallet.makeSpend(spendInfoSplit);
4433
+ const signedSplitTx = await wallet.signTx(splitTx);
4434
+ const broadcastedSplitTx = await wallet.broadcastTx(signedSplitTx);
4435
+ await wallet.saveTx(broadcastedSplitTx);
4436
+
4437
+ // Taint the rest of the wallet using the UTXO from before
4438
+ const {
4439
+ publicAddress
4440
+ } = await wallet.getReceiveAddress(bchCurrency);
4441
+ const spendInfoTaint = {
4442
+ ...bchCurrency,
4443
+ metadata: {
4444
+ name: 'Replay Protection Tx',
4445
+ notes: 'This transaction is to protect your BCH wallet from unintentionally spending BSV funds. Please wait for the transaction to confirm before making additional transactions using this BCH wallet.'
4446
+ },
4447
+ networkFeeOption: 'high',
4448
+ spendTargets: [{
4449
+ publicAddress,
4450
+ nativeAmount: '0'
4451
+ }]
4452
+ };
4453
+ const maxAmount = await wallet.getMaxSpendable(spendInfoTaint);
4454
+ spendInfoTaint.spendTargets[0].nativeAmount = maxAmount;
4455
+ const taintTx = await wallet.makeSpend(spendInfoTaint);
4456
+ const signedTaintTx = await wallet.signTx(taintTx);
4457
+ const broadcastedTaintTx = await wallet.broadcastTx(signedTaintTx);
4458
+ await wallet.saveTx(broadcastedTaintTx);
4330
4459
  }
4331
4460
 
4332
4461
  /**
4333
- * Assembles the key metadata structure that is encrypted within a keyBox.
4334
- * @param idKey Used to derive the wallet id. It's usually `dataKey`.
4462
+ * Combines two byte arrays via the XOR operation.
4335
4463
  */
4336
- function makeKeyInfo(type, keys, idKey) {
4337
- const hash = hmacSha256(utf8.parse(type), idKey ?? asEdgeStorageKeys(keys).dataKey);
4338
- return {
4339
- id: rfc4648.base64.stringify(hash),
4340
- type,
4341
- keys
4342
- };
4464
+ function xorData(a, b) {
4465
+ if (a.length !== b.length) {
4466
+ throw new Error(`Array lengths do not match: ${a.length}, ${b.length}`);
4467
+ }
4468
+ const out = new Uint8Array(a.length);
4469
+ for (let i = 0; i < a.length; ++i) {
4470
+ out[i] = a[i] ^ b[i];
4471
+ }
4472
+ return out;
4343
4473
  }
4344
4474
 
4345
- /**
4346
- * Assembles all the resources needed to attach new keys to the account.
4347
- * @param allowExisting True if the sync keys were derived deterministically,
4348
- * which implies that duplicate sync keys on the server are not errors,
4349
- * but leftovers from an earlier failed splitting attempt.
4350
- */
4351
- function makeKeysKit(ai, sessionKey, keyInfos, allowExisting = false) {
4352
- // For crash errors:
4353
- ai.props.log.breadcrumb('makeKeysKit', {});
4475
+ function makeStorageWalletApi(ai, walletInfo) {
4354
4476
  const {
4355
- io
4356
- } = ai.props;
4357
- const keyBoxes = keyInfos.map(info => ({
4358
- created: new Date(),
4359
- ...encrypt(io, utf8.parse(JSON.stringify(wasEdgeWalletInfo(info))), sessionKey.loginKey)
4360
- }));
4361
- const newSyncKeys = [];
4362
- for (const info of keyInfos) {
4363
- const storageKeys = cleaners.asMaybe(asEdgeStorageKeys)(info.keys);
4364
- if (storageKeys == null) continue;
4365
- newSyncKeys.push(rfc4648.base16.stringify(storageKeys.syncKey).toLowerCase());
4366
- }
4477
+ id,
4478
+ type,
4479
+ keys
4480
+ } = walletInfo;
4367
4481
  return {
4368
- loginId: sessionKey.loginId,
4369
- server: wasCreateKeysPayload({
4370
- allowExisting,
4371
- keyBoxes,
4372
- newSyncKeys
4373
- }),
4374
- serverPath: '/v2/login/keys',
4375
- stash: {
4376
- keyBoxes
4482
+ // Broken-out key info:
4483
+ id,
4484
+ type,
4485
+ keys,
4486
+ // Folders:
4487
+ get disklet() {
4488
+ return getStorageWalletDisklet(ai.props.state, id);
4489
+ },
4490
+ get localDisklet() {
4491
+ return getStorageWalletLocalDisklet(ai.props.state, id);
4492
+ },
4493
+ async sync() {
4494
+ await syncStorageWallet(ai, id);
4377
4495
  }
4378
4496
  };
4379
4497
  }
4380
4498
 
4499
+ const TYPED_ARRAYS = {
4500
+ '[object Float32Array]': true,
4501
+ '[object Float64Array]': true,
4502
+ '[object Int16Array]': true,
4503
+ '[object Int32Array]': true,
4504
+ '[object Int8Array]': true,
4505
+ '[object Uint16Array]': true,
4506
+ '[object Uint32Array]': true,
4507
+ '[object Uint8Array]': true,
4508
+ '[object Uint8ClampedArray]': true
4509
+ };
4510
+
4381
4511
  /**
4382
- * Flattens an array of key structures, removing duplicates.
4512
+ * Compares two objects that are already known to have a common `[[Class]]`.
4383
4513
  */
4384
- function mergeKeyInfos(keyInfos) {
4385
- const out = [];
4386
- const ids = new Map(); // Maps ID's to output array indexes
4514
+ function compareObjects(a, b, type) {
4515
+ // User-created objects:
4516
+ if (type === '[object Object]') {
4517
+ const proto = Object.getPrototypeOf(a);
4518
+ if (proto !== Object.getPrototypeOf(b)) return false;
4519
+ const keys = Object.getOwnPropertyNames(a);
4520
+ if (keys.length !== Object.getOwnPropertyNames(b).length) return false;
4387
4521
 
4388
- for (const keyInfo of keyInfos) {
4389
- const {
4390
- id,
4391
- keys,
4392
- type
4393
- } = keyInfo;
4394
- if (id == null || rfc4648.base64.parse(id).length !== 32) {
4395
- throw new Error(`Key integrity violation: invalid id ${id}`);
4396
- }
4397
- const index = ids.get(id);
4398
- if (index != null) {
4399
- // We have already seen this id, so check for conflicts:
4400
- const old = out[index];
4401
- if (old.type !== type) {
4402
- throw new Error(`Key integrity violation for ${id}: type ${type} does not match ${old.type}`);
4403
- }
4404
- for (const key of Object.keys(keys)) {
4405
- if (old.keys[key] != null && old.keys[key] !== keys[key]) {
4406
- throw new Error(`Key integrity violation for ${id}: ${key} keys do not match`);
4407
- }
4522
+ // We know that both objects have the same number of properties,
4523
+ // so if every property in `a` has a matching property in `b`,
4524
+ // the objects must be identical, regardless of key order.
4525
+ for (const key of keys) {
4526
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !compare(a[key], b[key])) {
4527
+ return false;
4408
4528
  }
4529
+ }
4530
+ return true;
4531
+ }
4409
4532
 
4410
- // Do the update:
4411
- out[index] = {
4412
- id,
4413
- keys: {
4414
- ...old.keys,
4415
- ...keys
4416
- },
4417
- type
4418
- };
4419
- } else {
4420
- // We haven't seen this id, so insert it:
4421
- ids.set(id, out.length);
4422
- out.push(keyInfo);
4533
+ // Arrays:
4534
+ if (type === '[object Array]') {
4535
+ if (a.length !== b.length) return false;
4536
+ for (let i = 0; i < a.length; ++i) {
4537
+ if (!compare(a[i], b[i])) return false;
4423
4538
  }
4539
+ return true;
4424
4540
  }
4425
- return out;
4541
+
4542
+ // Javascript dates:
4543
+ if (type === '[object Date]') {
4544
+ return a.getTime() === b.getTime();
4545
+ }
4546
+ if (type === '[object Map]') {
4547
+ return compareMap(a, b);
4548
+ }
4549
+
4550
+ // Typed arrays:
4551
+ if (TYPED_ARRAYS[type]) {
4552
+ if (a.length !== b.length) return false;
4553
+ for (let i = 0; i < a.length; ++i) {
4554
+ if (a[i] !== b[i]) return false;
4555
+ }
4556
+ return true;
4557
+ }
4558
+
4559
+ // We don't even try comparing anything else:
4560
+ return false;
4426
4561
  }
4427
4562
 
4428
4563
  /**
4429
- * Decrypts the private keys contained in a login.
4564
+ * Compare Maps
4430
4565
  */
4431
- function decryptKeyInfos(stash, loginKey, keyDates = new Map()) {
4432
- const {
4433
- appId,
4434
- keyBoxes = []
4435
- } = stash;
4436
- const legacyKeys = [];
4437
-
4438
- // BitID wallet:
4439
- const {
4440
- mnemonicBox,
4441
- rootKeyBox
4442
- } = stash;
4443
- if (mnemonicBox != null && rootKeyBox != null) {
4444
- const rootKey = decrypt(rootKeyBox, loginKey);
4445
- const infoKey = hmacSha256(rootKey, utf8.parse('infoKey'));
4446
- const keys = {
4447
- mnemonic: decryptText(mnemonicBox, infoKey),
4448
- rootKey: rfc4648.base64.stringify(rootKey)
4449
- };
4450
- legacyKeys.push(makeKeyInfo('wallet:bitid', keys, rootKey));
4566
+ function compareMap(map1, map2) {
4567
+ if (map1.size !== map2.size) {
4568
+ return false;
4569
+ }
4570
+ for (const [key, value] of map1) {
4571
+ if (!map2.has(key) || map2.get(key) !== value) {
4572
+ return false;
4573
+ }
4451
4574
  }
4575
+ return true;
4576
+ }
4577
+ /**
4578
+ * Returns true if two Javascript values are equal in value.
4579
+ */
4580
+ function compare(a, b) {
4581
+ if (a === b) return true;
4452
4582
 
4453
- // Account settings:
4454
- if (stash.syncKeyBox != null) {
4455
- const syncKey = decrypt(stash.syncKeyBox, loginKey);
4456
- const type = makeAccountType(appId);
4457
- const keys = wasEdgeStorageKeys({
4458
- dataKey: loginKey,
4459
- syncKey
4460
- });
4461
- legacyKeys.push(makeKeyInfo(type, keys, loginKey));
4583
+ // Fast path for primitives:
4584
+ if (typeof a !== 'object') return false;
4585
+ if (typeof b !== 'object') return false;
4586
+
4587
+ // If these are objects, the internal `[[Class]]` properties must match:
4588
+ const type = Object.prototype.toString.call(a);
4589
+ if (type !== Object.prototype.toString.call(b)) return false;
4590
+ return compareObjects(a, b, type);
4591
+ }
4592
+
4593
+ // How often to run jobs from the queue
4594
+ let QUEUE_RUN_DELAY = 500;
4595
+
4596
+ // How many jobs to run from the queue on each cycle
4597
+ let QUEUE_JOBS_PER_RUN = 3;
4598
+ const updateQueue = [];
4599
+ function enableTestMode() {
4600
+ QUEUE_JOBS_PER_RUN = 99;
4601
+ QUEUE_RUN_DELAY = 1;
4602
+ }
4603
+ function pushUpdate(update) {
4604
+ if (updateQueue.length <= 0) {
4605
+ startQueue();
4606
+ }
4607
+ let didUpdate = false;
4608
+ for (const u of updateQueue) {
4609
+ if (u.id === update.id && u.action === update.action) {
4610
+ u.updateFunc = update.updateFunc;
4611
+ didUpdate = true;
4612
+ break;
4613
+ }
4462
4614
  }
4615
+ if (!didUpdate) {
4616
+ updateQueue.push(update);
4617
+ }
4618
+ }
4619
+ function startQueue() {
4620
+ setTimeout(() => {
4621
+ const numJobs = Math.min(QUEUE_JOBS_PER_RUN, updateQueue.length);
4622
+ for (let i = 0; i < numJobs; i++) {
4623
+ const u = updateQueue.shift();
4624
+ if (u != null) u.updateFunc();
4625
+ }
4626
+ if (updateQueue.length > 0) {
4627
+ startQueue();
4628
+ }
4629
+ }, QUEUE_RUN_DELAY);
4630
+ }
4463
4631
 
4464
- // Keys:
4465
- const keyInfos = keyBoxes.map(box => {
4466
- const keys = asEdgeWalletInfo(JSON.parse(decryptText(box, loginKey)));
4467
- const created = mergeKeyDate(box.created, keyDates.get(keys.id));
4468
- if (created != null) keyDates.set(keys.id, created);
4469
- return keys;
4632
+ /**
4633
+ * Reads a JSON-style object into a JavaScript `Map` object with string keys.
4634
+ */
4635
+ function asMap(cleaner) {
4636
+ const asJsonObject = cleaners.asObject(cleaner);
4637
+ return cleaners.asCodec(raw => {
4638
+ const clean = asJsonObject(raw);
4639
+ const out = new Map();
4640
+ for (const key of Object.keys(clean)) out.set(key, clean[key]);
4641
+ return out;
4642
+ }, clean => {
4643
+ const out = {};
4644
+ clean.forEach((value, key) => {
4645
+ out[key] = value;
4646
+ });
4647
+ return asJsonObject(out);
4470
4648
  });
4471
- return mergeKeyInfos([...legacyKeys, ...keyInfos]).map(walletInfo => fixWalletInfo(walletInfo));
4472
4649
  }
4473
4650
 
4474
4651
  /**
4475
- * Returns all the wallet infos accessible from this login object.
4652
+ * Reads a JSON-style object into a JavaScript `Map` object
4653
+ * with EdgeTokenId keys.
4476
4654
  */
4477
- function decryptAllWalletInfos(stashTree, sessionKey, legacyWalletInfos, walletStates) {
4478
- // Maps from walletId's to appId's:
4479
- const dates = new Map();
4480
- const appIdMap = new Map();
4481
- const walletInfos = [...legacyWalletInfos];
4482
-
4483
- // Navigate to the starting node:
4484
- const stash = getChildStash(stashTree, sessionKey.loginId);
4485
-
4486
- // Add the legacy wallets first:
4487
- for (const info of legacyWalletInfos) {
4488
- walletInfos.push(info);
4489
- const appIds = appIdMap.get(info.id);
4490
- if (appIds != null) appIds.push(stash.appId);else appIdMap.set(info.id, [stash.appId]);
4491
- }
4492
- function getAllWalletInfosLoop(stash, loginKey) {
4493
- // Add our own walletInfos:
4494
- const keyInfos = decryptKeyInfos(stash, loginKey, dates);
4495
- for (const info of keyInfos) {
4496
- walletInfos.push(info);
4497
- const appIds = appIdMap.get(info.id);
4498
- if (appIds != null) appIds.push(stash.appId);else appIdMap.set(info.id, [stash.appId]);
4499
- }
4500
-
4501
- // Add our children's walletInfos:
4502
- for (const child of stash.children ?? []) {
4503
- if (child.parentBox == null) continue;
4504
- getAllWalletInfosLoop(child, decrypt(child.parentBox, loginKey));
4655
+ function asTokenIdMap(cleaner) {
4656
+ const asJsonObject = cleaners.asObject(cleaner);
4657
+ return cleaners.asCodec(raw => {
4658
+ const clean = asJsonObject(raw);
4659
+ const out = new Map();
4660
+ for (const key of Object.keys(clean)) {
4661
+ out.set(key === '' ? null : key, clean[key]);
4505
4662
  }
4506
- }
4507
- getAllWalletInfosLoop(stash, sessionKey.loginKey);
4508
- return mergeKeyInfos(walletInfos).map(info => {
4509
- return {
4510
- appId: getLast(appIdMap.get(info.id) ?? []),
4511
- appIds: appIdMap.get(info.id) ?? [],
4512
- // Defaults to be overwritten:
4513
- archived: false,
4514
- created: dates.get(info.id),
4515
- deleted: false,
4516
- hidden: false,
4517
- sortIndex: walletInfos.length,
4518
- // Copy the `imported` field from the raw keys if it exists
4519
- imported: info.keys.imported,
4520
- // Actual info:
4521
- ...walletStates[info.id],
4522
- ...info
4523
- };
4663
+ return out;
4664
+ }, clean => {
4665
+ const out = {};
4666
+ clean.forEach((value, key) => {
4667
+ out[key == null ? '' : key] = value;
4668
+ });
4669
+ return asJsonObject(out);
4524
4670
  });
4525
4671
  }
4526
4672
 
4527
- /**
4528
- * Upgrades legacy wallet info structures into the new format.
4529
- *
4530
- * Wallets normally have `wallet:pluginId` as their type,
4531
- * but some legacy wallets also put format information into the wallet type.
4532
- * This routine moves the information out of the wallet type into the keys.
4533
- *
4534
- * It also provides some other default values as a historical accident,
4535
- * but the bitcoin plugin can just provide its own fallback values if
4536
- * `format` or `coinType` are missing. Please don't make the problem worse
4537
- * by adding more code here!
4538
- */
4539
- function fixWalletInfo(walletInfo) {
4673
+ const asEdgeMetadata = raw => {
4674
+ const clean = asDiskMetadata(raw);
4540
4675
  const {
4541
- id,
4542
- keys,
4543
- type
4544
- } = walletInfo;
4676
+ exchangeAmount = {}
4677
+ } = clean;
4545
4678
 
4546
- // Wallet types we need to fix:
4547
- const defaults = {
4548
- // BTC:
4549
- 'wallet:bitcoin-bip44': {
4550
- format: 'bip44',
4551
- coinType: 0
4552
- },
4553
- 'wallet:bitcoin-bip49': {
4554
- format: 'bip49',
4555
- coinType: 0
4556
- },
4557
- // BCH:
4558
- 'wallet:bitcoincash-bip32': {
4559
- format: 'bip32'
4560
- },
4561
- 'wallet:bitcoincash-bip44': {
4562
- format: 'bip44',
4563
- coinType: 145
4564
- },
4565
- // BCH testnet:
4566
- 'wallet:bitcoincash-bip44-testnet': {
4567
- format: 'bip44',
4568
- coinType: 1
4569
- },
4570
- // BTC testnet:
4571
- 'wallet:bitcoin-bip44-testnet': {
4572
- format: 'bip44',
4573
- coinType: 1
4574
- },
4575
- 'wallet:bitcoin-bip49-testnet': {
4576
- format: 'bip49',
4577
- coinType: 1
4578
- },
4579
- // DASH:
4580
- 'wallet:dash-bip44': {
4581
- format: 'bip44',
4582
- coinType: 5
4583
- },
4584
- // DOGE:
4585
- 'wallet:dogecoin-bip44': {
4586
- format: 'bip44',
4587
- coinType: 3
4588
- },
4589
- // LTC:
4590
- 'wallet:litecoin-bip44': {
4591
- format: 'bip44',
4592
- coinType: 2
4593
- },
4594
- 'wallet:litecoin-bip49': {
4595
- format: 'bip49',
4596
- coinType: 2
4597
- },
4598
- // FTC:
4599
- 'wallet:feathercoin-bip49': {
4600
- format: 'bip49',
4601
- coinType: 8
4602
- },
4603
- 'wallet:feathercoin-bip44': {
4604
- format: 'bip44',
4605
- coinType: 8
4606
- },
4607
- // QTUM:
4608
- 'wallet:qtum-bip44': {
4609
- format: 'bip44',
4610
- coinType: 2301
4611
- },
4612
- // UFO:
4613
- 'wallet:ufo-bip49': {
4614
- format: 'bip49',
4615
- coinType: 202
4616
- },
4617
- 'wallet:ufo-bip84': {
4618
- format: 'bip84',
4619
- coinType: 202
4620
- },
4621
- // XZC:
4622
- 'wallet:zcoin-bip44': {
4623
- format: 'bip44',
4624
- coinType: 136
4625
- },
4626
- // The plugin itself could handle these lines, but they are here
4627
- // as a historical accident. Please don't add more:
4628
- 'wallet:bitcoin-testnet': {
4629
- format: 'bip32'
4630
- },
4631
- 'wallet:bitcoin': {
4632
- format: 'bip32'
4633
- },
4634
- 'wallet:bitcoincash-testnet': {
4635
- format: 'bip32'
4636
- },
4637
- 'wallet:litecoin': {
4638
- format: 'bip32',
4639
- coinType: 2
4640
- },
4641
- 'wallet:zcoin': {
4642
- format: 'bip32',
4643
- coinType: 136
4679
+ // Delete corrupt amounts that exceed the Javascript number range:
4680
+ for (const fiat of Object.keys(exchangeAmount)) {
4681
+ if (String(exchangeAmount[fiat]).includes('e')) {
4682
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
4683
+ delete exchangeAmount[fiat];
4644
4684
  }
4645
- };
4646
- if (defaults[type] != null) {
4647
- return {
4648
- id,
4649
- keys: {
4650
- ...defaults[type],
4651
- ...keys
4652
- },
4653
- type: type.replace(/-bip[0-9]+/, '')
4654
- };
4655
4685
  }
4656
- return walletInfo;
4657
- }
4658
- async function makeCurrencyWalletKeys(ai, walletType, opts) {
4686
+ return clean;
4687
+ };
4688
+ function mergeMetadata(under, over) {
4689
+ const out = {
4690
+ exchangeAmount: {}
4691
+ };
4659
4692
  const {
4660
- importText,
4661
- keyOptions,
4662
- keys
4663
- } = opts;
4693
+ exchangeAmount = {}
4694
+ } = out;
4664
4695
 
4665
- // Helper function to bundle up the keys:
4666
- function finalizeKeys(newKeys, imported) {
4667
- if (imported != null) newKeys = {
4668
- ...newKeys,
4669
- imported
4670
- };
4671
- return fixWalletInfo(makeKeyInfo(walletType, {
4672
- ...wasEdgeStorageKeys(createStorageKeys(ai)),
4673
- ...newKeys
4674
- }));
4696
+ // Merge the fiat amounts:
4697
+ const underAmounts = under.exchangeAmount ?? {};
4698
+ const overAmounts = over.exchangeAmount ?? {};
4699
+ for (const fiat of Object.keys(underAmounts)) {
4700
+ if (overAmounts[fiat] !== null) exchangeAmount[fiat] = underAmounts[fiat];
4701
+ }
4702
+ for (const fiat of Object.keys(overAmounts)) {
4703
+ const amount = overAmounts[fiat];
4704
+ if (amount != null) exchangeAmount[fiat] = amount;
4675
4705
  }
4676
4706
 
4677
- // If we have raw keys, just return those:
4678
- if (keys != null) return finalizeKeys(keys);
4707
+ // Merge simple fields:
4708
+ if (over.bizId !== null) out.bizId = over.bizId ?? under.bizId;
4709
+ if (over.category !== null) out.category = over.category ?? under.category;
4710
+ if (over.name !== null) out.name = over.name ?? under.name;
4711
+ if (over.notes !== null) out.notes = over.notes ?? under.notes;
4712
+ return out;
4713
+ }
4714
+ const asDiskMetadata = cleaners.asObject({
4715
+ bizId: cleaners.asOptional(cleaners.asNumber),
4716
+ category: cleaners.asOptional(cleaners.asString),
4717
+ exchangeAmount: cleaners.asOptional(cleaners.asObject(cleaners.asNumber)),
4718
+ name: cleaners.asOptional(cleaners.asString),
4719
+ notes: cleaners.asOptional(cleaners.asString)
4720
+ });
4721
+
4722
+ /**
4723
+ * The on-disk transaction format.
4724
+ */
4679
4725
 
4680
- // Grab the currency tools:
4681
- const pluginId = findCurrencyPluginId(ai.props.state.plugins.currency, walletType);
4682
- const tools = await getCurrencyTools(ai, pluginId);
4726
+ /**
4727
+ * The Airbitz on-disk transaction format.
4728
+ */
4683
4729
 
4684
- // If we have text to import, use that:
4685
- if (importText != null) {
4686
- if (tools.importPrivateKey == null) {
4687
- throw new Error('This wallet does not support importing keys');
4688
- }
4689
- return finalizeKeys(await tools.importPrivateKey(importText, keyOptions), true);
4690
- }
4730
+ /**
4731
+ * The Airbitz on-disk address format.
4732
+ */
4691
4733
 
4692
- // Derive fresh keys:
4693
- return finalizeKeys(await tools.createPrivateKey(walletType, keyOptions), false);
4694
- }
4695
- async function finishWalletCreation(ai, accountId, walletId, opts) {
4696
- const {
4697
- enabledTokenIds,
4698
- fiatCurrencyCode,
4699
- migratedFromWalletId,
4700
- name
4701
- } = opts;
4702
- const wallet = await waitForCurrencyWallet(ai, walletId);
4734
+ /**
4735
+ * An on-disk cache to quickly map Airbitz filenames to their dates.
4736
+ */
4703
4737
 
4704
- // Write ancillary files to disk:
4705
- if (migratedFromWalletId != null) {
4706
- await changeWalletStates(ai, accountId, {
4707
- [walletId]: {
4708
- migratedFromWalletId
4709
- }
4710
- });
4711
- }
4712
- if (name != null) {
4713
- await wallet.renameWallet(name);
4714
- }
4715
- if (fiatCurrencyCode != null) {
4716
- await wallet.setFiatCurrencyCode(fiatCurrencyCode);
4717
- }
4718
- if (enabledTokenIds != null && enabledTokenIds.length > 0) {
4719
- await wallet.changeEnabledTokenIds(enabledTokenIds);
4738
+ // ---------------------------------------------------------------------
4739
+ // building-block cleaners
4740
+ // ---------------------------------------------------------------------
4741
+ const asEdgeTokenId = cleaners.asEither(cleaners.asString, cleaners.asNull);
4742
+ const asFeeRate = cleaners.asValue('high', 'standard', 'low');
4743
+ const asEdgeTxSwap = cleaners.asObject({
4744
+ orderId: cleaners.asOptional(cleaners.asString),
4745
+ orderUri: cleaners.asOptional(cleaners.asString),
4746
+ isEstimate: cleaners.asBoolean,
4747
+ // The EdgeSwapInfo from the swap plugin:
4748
+ plugin: cleaners.asObject({
4749
+ pluginId: cleaners.asString,
4750
+ displayName: cleaners.asString,
4751
+ supportEmail: cleaners.asOptional(cleaners.asString)
4752
+ }),
4753
+ // Address information:
4754
+ payoutAddress: cleaners.asString,
4755
+ payoutCurrencyCode: cleaners.asString,
4756
+ payoutTokenId: cleaners.asOptional(asEdgeTokenId),
4757
+ payoutNativeAmount: cleaners.asString,
4758
+ payoutWalletId: cleaners.asString,
4759
+ refundAddress: cleaners.asOptional(cleaners.asString)
4760
+ });
4761
+ function asIntegerString(raw) {
4762
+ const clean = cleaners.asString(raw);
4763
+ if (!/^\d+$/.test(clean)) {
4764
+ throw new Error('Expected an integer string');
4720
4765
  }
4721
- return wallet;
4722
- }
4723
- function getLast(array) {
4724
- return array[array.length - 1];
4766
+ return clean;
4725
4767
  }
4726
4768
 
4769
+ // ---------------------------------------------------------------------
4770
+ // file cleaners
4771
+ // ---------------------------------------------------------------------
4772
+
4773
+ const asEdgeAssetAmount = cleaners.asObject({
4774
+ pluginId: cleaners.asString,
4775
+ tokenId: asEdgeTokenId,
4776
+ nativeAmount: cleaners.asOptional(asIntegerString)
4777
+ });
4778
+ const asEdgeFiatAmount = cleaners.asObject({
4779
+ // core-js style fiat code including 'iso:'
4780
+ fiatCurrencyCode: cleaners.asString,
4781
+ fiatAmount: cleaners.asString
4782
+ });
4783
+ const asEdgeSwapInfo = cleaners.asObject({
4784
+ pluginId: cleaners.asString,
4785
+ displayName: cleaners.asString,
4786
+ isDex: cleaners.asOptional(cleaners.asBoolean),
4787
+ orderUri: cleaners.asOptional(cleaners.asString),
4788
+ // The orderId would be appended to this
4789
+ supportEmail: cleaners.asString
4790
+ });
4791
+ const asEdgeTxActionSwap = cleaners.asObject({
4792
+ actionType: cleaners.asValue('swap'),
4793
+ swapInfo: asEdgeSwapInfo,
4794
+ orderId: cleaners.asOptional(cleaners.asString),
4795
+ orderUri: cleaners.asOptional(cleaners.asString),
4796
+ isEstimate: cleaners.asOptional(cleaners.asBoolean),
4797
+ canBePartial: cleaners.asOptional(cleaners.asBoolean),
4798
+ fromAsset: asEdgeAssetAmount,
4799
+ toAsset: asEdgeAssetAmount,
4800
+ payoutWalletId: cleaners.asString,
4801
+ payoutAddress: cleaners.asString,
4802
+ refundAddress: cleaners.asOptional(cleaners.asString)
4803
+ });
4804
+ const asEdgeTxActionStake = cleaners.asObject({
4805
+ actionType: cleaners.asValue('stake'),
4806
+ pluginId: cleaners.asString,
4807
+ stakeAssets: cleaners.asArray(asEdgeAssetAmount)
4808
+ });
4809
+ const asEdgeTxActionFiat = cleaners.asObject({
4810
+ actionType: cleaners.asValue('fiat'),
4811
+ orderId: cleaners.asString,
4812
+ orderUri: cleaners.asOptional(cleaners.asString),
4813
+ isEstimate: cleaners.asBoolean,
4814
+ fiatPlugin: cleaners.asObject({
4815
+ providerId: cleaners.asString,
4816
+ providerDisplayName: cleaners.asString,
4817
+ supportEmail: cleaners.asOptional(cleaners.asString)
4818
+ }),
4819
+ payinAddress: cleaners.asOptional(cleaners.asString),
4820
+ payoutAddress: cleaners.asOptional(cleaners.asString),
4821
+ fiatAsset: asEdgeFiatAmount,
4822
+ cryptoAsset: asEdgeAssetAmount
4823
+ });
4824
+ const asEdgeTxActionTokenApproval = cleaners.asObject({
4825
+ actionType: cleaners.asValue('tokenApproval'),
4826
+ tokenApproved: asEdgeAssetAmount,
4827
+ tokenContractAddress: cleaners.asString,
4828
+ contractAddress: cleaners.asString
4829
+ });
4830
+ const asEdgeTxActionGiftCard = cleaners.asObject({
4831
+ actionType: cleaners.asValue('giftCard'),
4832
+ orderId: cleaners.asString,
4833
+ orderUri: cleaners.asOptional(cleaners.asString),
4834
+ provider: cleaners.asObject({
4835
+ providerId: cleaners.asString,
4836
+ displayName: cleaners.asString,
4837
+ supportEmail: cleaners.asOptional(cleaners.asString)
4838
+ }),
4839
+ card: cleaners.asObject({
4840
+ name: cleaners.asString,
4841
+ imageUrl: cleaners.asOptional(cleaners.asString),
4842
+ fiatAmount: cleaners.asString,
4843
+ fiatCurrencyCode: cleaners.asString
4844
+ }),
4845
+ redemption: cleaners.asOptional(cleaners.asObject({
4846
+ code: cleaners.asOptional(cleaners.asString),
4847
+ url: cleaners.asOptional(cleaners.asString)
4848
+ }))
4849
+ });
4850
+ const asEdgeTxAction = cleaners.asEither(asEdgeTxActionSwap, asEdgeTxActionStake, asEdgeTxActionFiat, asEdgeTxActionTokenApproval, asEdgeTxActionGiftCard);
4851
+ const asEdgeAssetActionType = cleaners.asValue('claim', 'claimOrder', 'stake', 'stakeNetworkFee', 'stakeOrder', 'unstake', 'unstakeNetworkFee', 'unstakeOrder', 'swap', 'swapNetworkFee', 'swapOrderPost', 'swapOrderFill', 'swapOrderCancel', 'buy', 'sell', 'sellNetworkFee', 'tokenApproval', 'transfer', 'transferNetworkFee', 'giftCard');
4852
+ const asEdgeAssetAction = cleaners.asObject({
4853
+ assetActionType: asEdgeAssetActionType
4854
+ });
4855
+
4856
+ /**
4857
+ * Old core versions used currency codes instead of tokenId's.
4858
+ */
4859
+ const asLegacyTokensFile = cleaners.asArray(cleaners.asString);
4860
+
4861
+ /**
4862
+ * Stores enabled tokenId's on disk.
4863
+ */
4864
+ const asTokensFile = cleaners.asObject({
4865
+ // All the tokens that the engine should check.
4866
+ // This includes both manually-enabled tokens and auto-enabled tokens:
4867
+ enabledTokenIds: cleaners.asArray(cleaners.asString),
4868
+ // These tokenId's have been detected on-chain at least once.
4869
+ // The user can still remove them from the enabled tokens list.
4870
+ detectedTokenIds: cleaners.asArray(cleaners.asString)
4871
+ });
4872
+ const asTransactionAsset = cleaners.asObject({
4873
+ assetAction: cleaners.asOptional(asEdgeAssetAction),
4874
+ metadata: asEdgeMetadata,
4875
+ nativeAmount: cleaners.asOptional(cleaners.asString),
4876
+ providerFeeSent: cleaners.asOptional(cleaners.asString)
4877
+ });
4878
+ const asTransactionFile = cleaners.asObject({
4879
+ txid: cleaners.asString,
4880
+ internal: cleaners.asBoolean,
4881
+ creationDate: cleaners.asNumber,
4882
+ currencies: asMap(asTransactionAsset),
4883
+ tokens: cleaners.asOptional(asTokenIdMap(asTransactionAsset), () => new Map()),
4884
+ deviceDescription: cleaners.asOptional(cleaners.asString),
4885
+ feeRateRequested: cleaners.asOptional(cleaners.asEither(asFeeRate, asJsonObject)),
4886
+ feeRateUsed: cleaners.asOptional(asJsonObject),
4887
+ payees: cleaners.asOptional(cleaners.asArray(cleaners.asObject({
4888
+ address: cleaners.asString,
4889
+ amount: cleaners.asString,
4890
+ currency: cleaners.asString,
4891
+ tag: cleaners.asOptional(cleaners.asString)
4892
+ }))),
4893
+ savedAction: cleaners.asOptional(asEdgeTxAction),
4894
+ secret: cleaners.asOptional(cleaners.asString),
4895
+ swap: cleaners.asOptional(asEdgeTxSwap)
4896
+ });
4897
+ const asLegacyTransactionFile = cleaners.asObject({
4898
+ airbitzFeeWanted: cleaners.asNumber,
4899
+ meta: cleaners.asObject({
4900
+ amountFeeAirBitzSatoshi: cleaners.asNumber,
4901
+ balance: cleaners.asNumber,
4902
+ fee: cleaners.asNumber,
4903
+ // Metadata:
4904
+ amountCurrency: cleaners.asNumber,
4905
+ bizId: cleaners.asNumber,
4906
+ category: cleaners.asString,
4907
+ name: cleaners.asString,
4908
+ notes: cleaners.asString,
4909
+ // Obsolete/moved fields:
4910
+ attributes: cleaners.asNumber,
4911
+ amountSatoshi: cleaners.asNumber,
4912
+ amountFeeMinersSatoshi: cleaners.asNumber,
4913
+ airbitzFee: cleaners.asNumber
4914
+ }),
4915
+ ntxid: cleaners.asString,
4916
+ state: cleaners.asObject({
4917
+ creationDate: cleaners.asNumber,
4918
+ internal: cleaners.asBoolean,
4919
+ malleableTxId: cleaners.asString
4920
+ })
4921
+ });
4922
+ const asLegacyAddressFile = cleaners.asObject({
4923
+ seq: cleaners.asNumber,
4924
+ // index
4925
+ address: cleaners.asString,
4926
+ state: cleaners.asObject({
4927
+ recycleable: cleaners.asOptional(cleaners.asBoolean, true),
4928
+ creationDate: cleaners.asOptional(cleaners.asNumber, 0)
4929
+ }),
4930
+ meta: cleaners.asObject({
4931
+ amountSatoshi: cleaners.asOptional(cleaners.asNumber, 0) // requestAmount
4932
+ // TODO: Normal EdgeMetadata
4933
+ }).withRest
4934
+ });
4935
+ const asLegacyMapFile = cleaners.asObject(cleaners.asObject({
4936
+ timestamp: cleaners.asNumber,
4937
+ txidHash: cleaners.asString
4938
+ }));
4939
+
4727
4940
  /**
4728
- * Returns the earliest date, or undefined if neither date exists.
4941
+ * Public keys cached in the wallet's local storage.
4729
4942
  */
4730
- function mergeKeyDate(a, b) {
4731
- if (a == null) return b;
4732
- if (b == null) return a;
4733
- return new Date(Math.min(a.valueOf(), b.valueOf()));
4943
+ const asPublicKeyFile = cleaners.asObject({
4944
+ walletInfo: cleaners.asObject({
4945
+ id: cleaners.asString,
4946
+ keys: asJsonObject,
4947
+ type: cleaners.asString
4948
+ })
4949
+ });
4950
+
4951
+ /**
4952
+ * The wallet's local storage file for the last seen "checkpoint". The core
4953
+ * does not know the contents of the checkpoint, so it just as an arbitrary
4954
+ * string.
4955
+ */
4956
+ const asSeenCheckpointFile = cleaners.asObject({
4957
+ checkpoint: cleaners.asOptional(cleaners.asString),
4958
+ subscribedAddresses: cleaners.asOptional(cleaners.asArray(cleaners.asObject({
4959
+ address: cleaners.asString,
4960
+ checkpoint: cleaners.asOptional(cleaners.asString)
4961
+ })), () => [])
4962
+ });
4963
+ const asWalletFiatFile = cleaners.asObject({
4964
+ fiat: cleaners.asOptional(cleaners.asString),
4965
+ num: cleaners.asOptional(cleaners.asNumber)
4966
+ });
4967
+ const asWalletNameFile = cleaners.asObject({
4968
+ walletName: cleaners.asEither(cleaners.asString, cleaners.asNull)
4969
+ });
4970
+
4971
+ function shuffle(arr) {
4972
+ for (let i = arr.length - 1; i > 0; i--) {
4973
+ const j = Math.floor(Math.random() * (i + 1));
4974
+ [arr[i], arr[j]] = [arr[j], arr[i]];
4975
+ }
4976
+ return arr;
4734
4977
  }
4735
4978
 
4736
4979
  const passwordAuthSnrp = userIdSnrp;
@@ -6817,6 +7060,9 @@ async function reloadWalletFiles(input, changes) {
6817
7060
 
6818
7061
  // Used for detectedTokenIds & enabledTokenIds:
6819
7062
  const initialTokenIds = [];
7063
+ const initialSyncStatus = {
7064
+ totalRatio: 0
7065
+ };
6820
7066
  const currencyWalletInner = reduxKeto.buildReducer({
6821
7067
  accountId(state, action, next) {
6822
7068
  if (state != null) return state;
@@ -6982,15 +7228,15 @@ const currencyWalletInner = reduxKeto.buildReducer({
6982
7228
  }
6983
7229
  return state;
6984
7230
  },
6985
- syncRatio(state = 0, action) {
7231
+ syncStatus(state = initialSyncStatus, action) {
6986
7232
  switch (action.type) {
6987
- case 'CURRENCY_ENGINE_CHANGED_SYNC_RATIO':
7233
+ case 'CURRENCY_ENGINE_CHANGED_SYNC_STATUS':
6988
7234
  {
6989
- return action.payload.ratio;
7235
+ return action.payload.status;
6990
7236
  }
6991
7237
  case 'CURRENCY_ENGINE_CLEARED':
6992
7238
  {
6993
- return 0;
7239
+ return initialSyncStatus;
6994
7240
  }
6995
7241
  }
6996
7242
  return state;
@@ -7263,12 +7509,14 @@ function makeCurrencyWalletCallbacks(input) {
7263
7509
  onAddressesChecked(ratio) {
7264
7510
  pushUpdate({
7265
7511
  id: walletId,
7266
- action: 'onAddressesChecked',
7512
+ action: 'onSyncStatusChanged',
7267
7513
  updateFunc: () => {
7268
7514
  input.props.dispatch({
7269
- type: 'CURRENCY_ENGINE_CHANGED_SYNC_RATIO',
7515
+ type: 'CURRENCY_ENGINE_CHANGED_SYNC_STATUS',
7270
7516
  payload: {
7271
- ratio,
7517
+ status: {
7518
+ totalRatio: ratio
7519
+ },
7272
7520
  walletId
7273
7521
  }
7274
7522
  });
@@ -7449,6 +7697,21 @@ function makeCurrencyWalletCallbacks(input) {
7449
7697
  }
7450
7698
  });
7451
7699
  },
7700
+ onSyncStatusChanged(syncStatus) {
7701
+ pushUpdate({
7702
+ id: walletId,
7703
+ action: 'onSyncStatusChanged',
7704
+ updateFunc: () => {
7705
+ input.props.dispatch({
7706
+ type: 'CURRENCY_ENGINE_CHANGED_SYNC_STATUS',
7707
+ payload: {
7708
+ status: syncStatus,
7709
+ walletId
7710
+ }
7711
+ });
7712
+ }
7713
+ });
7714
+ },
7452
7715
  onTransactions(txEvents) {
7453
7716
  const {
7454
7717
  accountId,
@@ -7948,7 +8211,10 @@ function makeCurrencyWalletApi(input, engine, tools, publicWalletInfo) {
7948
8211
  return skipBlockHeight ? 0 : input.props.walletState.height;
7949
8212
  },
7950
8213
  get syncRatio() {
7951
- return input.props.walletState.syncRatio;
8214
+ return input.props.walletState.syncStatus.totalRatio;
8215
+ },
8216
+ get syncStatus() {
8217
+ return input.props.walletState.syncStatus;
7952
8218
  },
7953
8219
  get unactivatedTokenIds() {
7954
8220
  return input.props.walletState.unactivatedTokenIds;
@@ -8395,6 +8661,9 @@ function makeCurrencyWalletApi(input, engine, tools, publicWalletInfo) {
8395
8661
  await engine.resyncBlockchain();
8396
8662
  yaob.emit(out, 'transactionsRemoved', undefined);
8397
8663
  },
8664
+ async split(splitWallets) {
8665
+ return await splitWalletInfo(ai, accountId, walletInfo, splitWallets, false);
8666
+ },
8398
8667
  // URI handling:
8399
8668
  async encodeUri(options) {
8400
8669
  return await tools.encodeUri(options, makeMetaTokens(input.props.state.accounts[accountId].customTokens[pluginId]));
@@ -9008,195 +9277,6 @@ async function deleteLogin(ai, login) {
9008
9277
  await loginFetch(ai, 'POST', '/v2/login/delete', request);
9009
9278
  }
9010
9279
 
9011
- async function listSplittableWalletTypes(ai, accountId, walletId) {
9012
- const {
9013
- allWalletInfosFull
9014
- } = ai.props.state.accounts[accountId];
9015
-
9016
- // Find the wallet we are going to split:
9017
- const walletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === walletId);
9018
- if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`);
9019
- const pluginId = maybeFindCurrencyPluginId(ai.props.state.plugins.currency, walletInfo.type);
9020
- if (pluginId == null) return [];
9021
-
9022
- // Get the list of available types:
9023
- const tools = await getCurrencyTools(ai, pluginId);
9024
- if (tools.getSplittableTypes == null) return [];
9025
- const types = await tools.getSplittableTypes(walletInfo);
9026
-
9027
- // Filter out wallet types we have already split:
9028
- return types.filter(type => {
9029
- const newWalletInfo = makeSplitWalletInfo(walletInfo, type);
9030
- const existingWalletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === newWalletInfo.id);
9031
- // We can split the wallet if it doesn't exist, or is deleted:
9032
- return existingWalletInfo == null || existingWalletInfo.archived || existingWalletInfo.deleted;
9033
- });
9034
- }
9035
- function makeSplitWalletInfo(walletInfo, newWalletType) {
9036
- const {
9037
- id,
9038
- type,
9039
- keys
9040
- } = walletInfo;
9041
- const cleanKeys = cleaners.asMaybe(asEdgeStorageKeys)(keys);
9042
- if (cleanKeys == null) {
9043
- throw new Error(`Wallet ${id} is not a splittable type`);
9044
- }
9045
- const {
9046
- dataKey,
9047
- syncKey
9048
- } = cleanKeys;
9049
- const xorKey = xorData(hmacSha256(utf8.parse(type), dataKey), hmacSha256(utf8.parse(newWalletType), dataKey));
9050
-
9051
- // Fix the id:
9052
- const newWalletId = xorData(rfc4648.base64.parse(id), xorKey);
9053
- const newSyncKey = xorData(syncKey, xorKey.subarray(0, syncKey.length));
9054
-
9055
- // Fix the keys:
9056
- const networkName = type.replace(/wallet:/, '').replace('-', '');
9057
- const newNetworkName = newWalletType.replace(/wallet:/, '').replace('-', '');
9058
- const newKeys = wasEdgeStorageKeys({
9059
- dataKey,
9060
- syncKey: newSyncKey
9061
- });
9062
- for (const key of Object.keys(keys)) {
9063
- const newKey = key === networkName + 'Key' ? newNetworkName + 'Key' : key;
9064
- if (newKeys[newKey] != null) continue;
9065
- newKeys[newKey] = keys[key];
9066
- }
9067
- return {
9068
- id: rfc4648.base64.stringify(newWalletId),
9069
- keys: newKeys,
9070
- type: newWalletType
9071
- };
9072
- }
9073
- async function splitWalletInfo(ai, accountId, walletId, newWalletType) {
9074
- const accountState = ai.props.state.accounts[accountId];
9075
- const {
9076
- allWalletInfosFull,
9077
- sessionKey
9078
- } = accountState;
9079
-
9080
- // Find the wallet we are going to split:
9081
- const walletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === walletId);
9082
- if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`);
9083
-
9084
- // Handle BCH / BTC+segwit special case:
9085
- if (newWalletType === 'wallet:bitcoincash' && walletInfo.type === 'wallet:bitcoin' && walletInfo.keys.format === 'bip49') {
9086
- throw new Error('Cannot split segwit-format Bitcoin wallets to Bitcoin Cash');
9087
- }
9088
-
9089
- // Handle BitcoinABC/SV replay protection:
9090
- const needsProtection = newWalletType === 'wallet:bitcoinsv' && walletInfo.type === 'wallet:bitcoincash';
9091
- if (needsProtection) {
9092
- const oldWallet = ai.props.output.currency.wallets[walletId].walletApi;
9093
- if (oldWallet == null) throw new Error('Missing Wallet');
9094
- await protectBchWallet(oldWallet);
9095
- }
9096
-
9097
- // See if the wallet has already been split:
9098
- const newWalletInfo = makeSplitWalletInfo(walletInfo, newWalletType);
9099
- const existingWalletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === newWalletInfo.id);
9100
- if (existingWalletInfo != null) {
9101
- if (existingWalletInfo.archived || existingWalletInfo.deleted) {
9102
- // Simply undelete the existing wallet:
9103
- const walletInfos = {};
9104
- walletInfos[newWalletInfo.id] = {
9105
- archived: false,
9106
- deleted: false,
9107
- migratedFromWalletId: existingWalletInfo.migratedFromWalletId
9108
- };
9109
- await changeWalletStates(ai, accountId, walletInfos);
9110
- return newWalletInfo.id;
9111
- }
9112
- if (needsProtection) return newWalletInfo.id;
9113
- throw new Error('This wallet has already been split');
9114
- }
9115
-
9116
- // Add the keys to the login:
9117
- const kit = makeKeysKit(ai, sessionKey, [newWalletInfo], true);
9118
- await applyKit(ai, sessionKey, kit);
9119
-
9120
- // Try to copy metadata on a best-effort basis.
9121
- // In the future we should clone the repo instead:
9122
- try {
9123
- const wallet = await waitForCurrencyWallet(ai, newWalletInfo.id);
9124
- const oldWallet = ai.props.output.currency.wallets[walletId].walletApi;
9125
- if (oldWallet != null) {
9126
- if (oldWallet.name != null) await wallet.renameWallet(oldWallet.name);
9127
- if (oldWallet.fiatCurrencyCode != null) {
9128
- await wallet.setFiatCurrencyCode(oldWallet.fiatCurrencyCode);
9129
- }
9130
- }
9131
- } catch (error) {
9132
- ai.props.onError(error);
9133
- }
9134
- return newWalletInfo.id;
9135
- }
9136
- async function protectBchWallet(wallet) {
9137
- const bchCurrency = {
9138
- currencyCode: 'BCH',
9139
- tokenId: null
9140
- };
9141
-
9142
- // Create a UTXO which can be spend only on the ABC network
9143
- const spendInfoSplit = {
9144
- ...bchCurrency,
9145
- spendTargets: [{
9146
- nativeAmount: '10000',
9147
- otherParams: {
9148
- script: {
9149
- type: 'replayProtection'
9150
- }
9151
- },
9152
- publicAddress: ''
9153
- }],
9154
- metadata: {},
9155
- networkFeeOption: 'high'
9156
- };
9157
- const splitTx = await wallet.makeSpend(spendInfoSplit);
9158
- const signedSplitTx = await wallet.signTx(splitTx);
9159
- const broadcastedSplitTx = await wallet.broadcastTx(signedSplitTx);
9160
- await wallet.saveTx(broadcastedSplitTx);
9161
-
9162
- // Taint the rest of the wallet using the UTXO from before
9163
- const {
9164
- publicAddress
9165
- } = await wallet.getReceiveAddress(bchCurrency);
9166
- const spendInfoTaint = {
9167
- ...bchCurrency,
9168
- metadata: {
9169
- name: 'Replay Protection Tx',
9170
- notes: 'This transaction is to protect your BCH wallet from unintentionally spending BSV funds. Please wait for the transaction to confirm before making additional transactions using this BCH wallet.'
9171
- },
9172
- networkFeeOption: 'high',
9173
- spendTargets: [{
9174
- publicAddress,
9175
- nativeAmount: '0'
9176
- }]
9177
- };
9178
- const maxAmount = await wallet.getMaxSpendable(spendInfoTaint);
9179
- spendInfoTaint.spendTargets[0].nativeAmount = maxAmount;
9180
- const taintTx = await wallet.makeSpend(spendInfoTaint);
9181
- const signedTaintTx = await wallet.signTx(taintTx);
9182
- const broadcastedTaintTx = await wallet.broadcastTx(signedTaintTx);
9183
- await wallet.saveTx(broadcastedTaintTx);
9184
- }
9185
-
9186
- /**
9187
- * Combines two byte arrays via the XOR operation.
9188
- */
9189
- function xorData(a, b) {
9190
- if (a.length !== b.length) {
9191
- throw new Error(`Array lengths do not match: ${a.length}, ${b.length}`);
9192
- }
9193
- const out = new Uint8Array(a.length);
9194
- for (let i = 0; i < a.length; ++i) {
9195
- out[i] = a[i] ^ b[i];
9196
- }
9197
- return out;
9198
- }
9199
-
9200
9280
  /**
9201
9281
  * Approves or rejects vouchers on the server.
9202
9282
  */
@@ -9543,7 +9623,9 @@ const makeMemoryWalletInner = async (ai, config, walletType, opts = {}) => {
9543
9623
  const log = makeLog(ai.props.logBackend, `${walletId}-${walletType}`);
9544
9624
  let balanceMap = new Map();
9545
9625
  let detectedTokenIds = [];
9546
- let syncRatio = 0;
9626
+ let syncStatus = {
9627
+ totalRatio: 0
9628
+ };
9547
9629
  let needsUpdate = false;
9548
9630
  const updateWallet = () => {
9549
9631
  if (needsUpdate) {
@@ -9556,48 +9638,54 @@ const makeMemoryWalletInner = async (ai, config, walletType, opts = {}) => {
9556
9638
  updateWallet();
9557
9639
  }, 0);
9558
9640
  const plugin = ai.props.state.plugins.currency[config.currencyInfo.pluginId];
9559
- const engine = await plugin.makeCurrencyEngine(walletInfo, {
9560
- callbacks: {
9561
- onAddressChanged: () => {},
9562
- onAddressesChecked: progressRatio => {
9563
- if (out.syncRatio === 1) return;
9564
- if (progressRatio === 1) {
9565
- syncRatio = progressRatio;
9566
- needsUpdate = true;
9567
- }
9568
- },
9569
- onNewTokens: tokenIds => {
9570
- const sortedTokenIds = tokenIds.sort((a, b) => a.localeCompare(b));
9571
- if (detectedTokenIds.length !== sortedTokenIds.length) {
9641
+ const callbacks = {
9642
+ onAddressChanged: () => {},
9643
+ onAddressesChecked(totalRatio) {
9644
+ callbacks.onSyncStatusChanged({
9645
+ totalRatio
9646
+ });
9647
+ },
9648
+ onNewTokens: tokenIds => {
9649
+ const sortedTokenIds = tokenIds.sort((a, b) => a.localeCompare(b));
9650
+ if (detectedTokenIds.length !== sortedTokenIds.length) {
9651
+ detectedTokenIds = sortedTokenIds;
9652
+ needsUpdate = true;
9653
+ return;
9654
+ }
9655
+ for (let i = 0; i < sortedTokenIds.length; i++) {
9656
+ if (detectedTokenIds[i] !== sortedTokenIds[i]) {
9572
9657
  detectedTokenIds = sortedTokenIds;
9573
9658
  needsUpdate = true;
9574
9659
  return;
9575
9660
  }
9576
- for (let i = 0; i < sortedTokenIds.length; i++) {
9577
- if (detectedTokenIds[i] !== sortedTokenIds[i]) {
9578
- detectedTokenIds = sortedTokenIds;
9579
- needsUpdate = true;
9580
- return;
9581
- }
9582
- }
9583
- },
9584
- onSeenTxCheckpoint: () => {},
9585
- onStakingStatusChanged: () => {},
9586
- onSubscribeAddresses: () => {},
9587
- onTokenBalanceChanged: (tokenId, balance) => {
9588
- if (balanceMap.get(tokenId) === balance) return;
9589
- balanceMap = new Map(balanceMap);
9590
- balanceMap.set(tokenId, balance);
9661
+ }
9662
+ },
9663
+ onSeenTxCheckpoint: () => {},
9664
+ onStakingStatusChanged: () => {},
9665
+ onSubscribeAddresses: () => {},
9666
+ onSyncStatusChanged(status) {
9667
+ if (syncStatus.totalRatio === 1) return;
9668
+ if (status.totalRatio === 1) {
9669
+ syncStatus = status;
9591
9670
  needsUpdate = true;
9592
- },
9593
- onTransactions: () => {},
9594
- onTransactionsChanged: () => {},
9595
- onTxidsChanged: () => {},
9596
- onUnactivatedTokenIdsChanged: () => {},
9597
- onWcNewContractCall: () => {},
9598
- onBlockHeightChanged: () => {},
9599
- onBalanceChanged: () => {}
9671
+ }
9672
+ },
9673
+ onTokenBalanceChanged: (tokenId, balance) => {
9674
+ if (balanceMap.get(tokenId) === balance) return;
9675
+ balanceMap = new Map(balanceMap);
9676
+ balanceMap.set(tokenId, balance);
9677
+ needsUpdate = true;
9600
9678
  },
9679
+ onTransactions: () => {},
9680
+ onTransactionsChanged: () => {},
9681
+ onTxidsChanged: () => {},
9682
+ onUnactivatedTokenIdsChanged: () => {},
9683
+ onWcNewContractCall: () => {},
9684
+ onBlockHeightChanged: () => {},
9685
+ onBalanceChanged: () => {}
9686
+ };
9687
+ const engine = await plugin.makeCurrencyEngine(walletInfo, {
9688
+ callbacks,
9601
9689
  customTokens: {
9602
9690
  ...config.customTokens
9603
9691
  },
@@ -9652,7 +9740,10 @@ const makeMemoryWalletInner = async (ai, config, walletType, opts = {}) => {
9652
9740
  return detectedTokenIds;
9653
9741
  },
9654
9742
  get syncRatio() {
9655
- return syncRatio;
9743
+ return syncStatus.totalRatio;
9744
+ },
9745
+ get syncStatus() {
9746
+ return syncStatus;
9656
9747
  },
9657
9748
  async changeEnabledTokenIds(tokenIds) {
9658
9749
  if (engine.changeEnabledTokenIds != null) {
@@ -10349,7 +10440,26 @@ function makeAccountApi(ai, accountId) {
10349
10440
  getWalletInfo: AccountSync.prototype.getWalletInfo,
10350
10441
  listWalletIds: AccountSync.prototype.listWalletIds,
10351
10442
  async splitWalletInfo(walletId, newWalletType) {
10352
- return await splitWalletInfo(ai, accountId, walletId, newWalletType);
10443
+ const {
10444
+ allWalletInfosFull
10445
+ } = accountState();
10446
+ const walletInfo = allWalletInfosFull.find(walletInfo => walletInfo.id === walletId);
10447
+ if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`);
10448
+ const existingWallet = ai.props.output?.currency?.wallets[walletInfo.id]?.walletApi;
10449
+
10450
+ // The following check has not been needed since about 2021,
10451
+ // when the currency plugins became responsible for listing
10452
+ // their own splittable types, but keep it for safety:
10453
+ if (walletInfo.type === 'wallet:bitcoin' && walletInfo.keys.format === 'bip49' && newWalletType === 'wallet:bitcoincash') {
10454
+ throw new Error('Cannot split segwit-format Bitcoin wallets to Bitcoin Cash');
10455
+ }
10456
+ const [result] = await splitWalletInfo(ai, accountId, walletInfo, [{
10457
+ walletType: newWalletType,
10458
+ name: existingWallet?.name ?? undefined,
10459
+ fiatCurrencyCode: existingWallet?.fiatCurrencyCode
10460
+ }], true);
10461
+ if (result.ok) return result.result.id;
10462
+ throw result.error;
10353
10463
  },
10354
10464
  async listSplittableWalletTypes(walletId) {
10355
10465
  return await listSplittableWalletTypes(ai, accountId, walletId);
@@ -10590,19 +10700,6 @@ function getRawPrivateKey(ai, accountId, walletId) {
10590
10700
  }
10591
10701
  return info;
10592
10702
  }
10593
- async function makeEdgeResult(promise) {
10594
- try {
10595
- return {
10596
- ok: true,
10597
- result: await promise
10598
- };
10599
- } catch (error) {
10600
- return {
10601
- ok: false,
10602
- error
10603
- };
10604
- }
10605
- }
10606
10703
 
10607
10704
  const initialCustomTokens = {};
10608
10705
  const blankSessionKey = {