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/CHANGELOG.md +14 -0
- package/android/src/main/assets/edge-core-js/edge-core.js +1 -1
- package/ios/EdgeCoreModule.swift +16 -1
- package/lib/core/account/account-api.js +39 -11
- package/lib/core/account/memory-wallet.js +48 -40
- package/lib/core/actions.js +1 -0
- package/lib/core/currency/wallet/currency-wallet-api.js +20 -1
- package/lib/core/currency/wallet/currency-wallet-callbacks.js +17 -3
- package/lib/core/currency/wallet/currency-wallet-reducer.js +9 -4
- package/lib/core/login/splitting.js +114 -55
- package/lib/flow/types.js +47 -7
- package/lib/flow/webby.js +35 -0
- package/lib/node/index.js +1177 -1080
- package/lib/types/types.js +40 -0
- package/lib/util/edgeResult.js +11 -0
- package/package.json +1 -1
- package/src/types/types.ts +47 -7
- package/src/types/webby.js +33 -0
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
|
-
*
|
|
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
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
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
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
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
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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
|
-
|
|
3619
|
-
|
|
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
|
-
*
|
|
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
|
|
3626
|
-
|
|
3627
|
-
|
|
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
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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
|
-
*
|
|
3630
|
+
* Flattens an array of key structures, removing duplicates.
|
|
3638
3631
|
*/
|
|
3639
|
-
function
|
|
3640
|
-
|
|
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
|
-
|
|
3653
|
-
|
|
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
|
-
//
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
3736
|
-
|
|
3681
|
+
appId,
|
|
3682
|
+
keyBoxes = []
|
|
3683
|
+
} = stash;
|
|
3684
|
+
const legacyKeys = [];
|
|
3737
3685
|
|
|
3738
|
-
//
|
|
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
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
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
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
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
|
-
//
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3791
|
-
*/
|
|
3731
|
+
// Navigate to the starting node:
|
|
3732
|
+
const stash = getChildStash(stashTree, sessionKey.loginId);
|
|
3792
3733
|
|
|
3793
|
-
|
|
3794
|
-
|
|
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
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
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
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
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
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4302
|
-
|
|
4246
|
+
async function listSplittableWalletTypes(ai, accountId, walletId) {
|
|
4247
|
+
const {
|
|
4248
|
+
allWalletInfosFull
|
|
4249
|
+
} = ai.props.state.accounts[accountId];
|
|
4303
4250
|
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
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
|
-
|
|
4312
|
-
|
|
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
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
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
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
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
|
|
4329
|
-
|
|
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
|
-
*
|
|
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
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
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
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
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
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
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
|
-
*
|
|
4512
|
+
* Compares two objects that are already known to have a common `[[Class]]`.
|
|
4383
4513
|
*/
|
|
4384
|
-
function
|
|
4385
|
-
|
|
4386
|
-
|
|
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
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
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
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
4564
|
+
* Compare Maps
|
|
4430
4565
|
*/
|
|
4431
|
-
function
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
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
|
-
//
|
|
4454
|
-
if (
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
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
|
-
*
|
|
4652
|
+
* Reads a JSON-style object into a JavaScript `Map` object
|
|
4653
|
+
* with EdgeTokenId keys.
|
|
4476
4654
|
*/
|
|
4477
|
-
function
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
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
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4542
|
-
|
|
4543
|
-
type
|
|
4544
|
-
} = walletInfo;
|
|
4676
|
+
exchangeAmount = {}
|
|
4677
|
+
} = clean;
|
|
4545
4678
|
|
|
4546
|
-
//
|
|
4547
|
-
const
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
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
|
|
4657
|
-
}
|
|
4658
|
-
|
|
4686
|
+
return clean;
|
|
4687
|
+
};
|
|
4688
|
+
function mergeMetadata(under, over) {
|
|
4689
|
+
const out = {
|
|
4690
|
+
exchangeAmount: {}
|
|
4691
|
+
};
|
|
4659
4692
|
const {
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
keys
|
|
4663
|
-
} = opts;
|
|
4693
|
+
exchangeAmount = {}
|
|
4694
|
+
} = out;
|
|
4664
4695
|
|
|
4665
|
-
//
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
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
|
-
//
|
|
4678
|
-
if (
|
|
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
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4726
|
+
/**
|
|
4727
|
+
* The Airbitz on-disk transaction format.
|
|
4728
|
+
*/
|
|
4683
4729
|
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
-
|
|
4693
|
-
|
|
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
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
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
|
|
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
|
-
*
|
|
4941
|
+
* Public keys cached in the wallet's local storage.
|
|
4729
4942
|
*/
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
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
|
-
|
|
7231
|
+
syncStatus(state = initialSyncStatus, action) {
|
|
6986
7232
|
switch (action.type) {
|
|
6987
|
-
case '
|
|
7233
|
+
case 'CURRENCY_ENGINE_CHANGED_SYNC_STATUS':
|
|
6988
7234
|
{
|
|
6989
|
-
return action.payload.
|
|
7235
|
+
return action.payload.status;
|
|
6990
7236
|
}
|
|
6991
7237
|
case 'CURRENCY_ENGINE_CLEARED':
|
|
6992
7238
|
{
|
|
6993
|
-
return
|
|
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: '
|
|
7512
|
+
action: 'onSyncStatusChanged',
|
|
7267
7513
|
updateFunc: () => {
|
|
7268
7514
|
input.props.dispatch({
|
|
7269
|
-
type: '
|
|
7515
|
+
type: 'CURRENCY_ENGINE_CHANGED_SYNC_STATUS',
|
|
7270
7516
|
payload: {
|
|
7271
|
-
|
|
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.
|
|
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
|
|
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
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
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
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
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
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
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
|
|
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
|
-
|
|
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 = {
|