turtlecoin-wallet-backend
Version:
[](https://npmjs.org/package/turtlecoin-wallet-backend)
724 lines (723 loc) • 31.9 kB
JavaScript
"use strict";
// Copyright (c) 2018-2020, Zpalmtree
//
// Please see the included LICENSE file for more information.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubWallets = void 0;
const CnUtils_1 = require("./CnUtils");
const JsonSerialization_1 = require("./JsonSerialization");
const SubWallet_1 = require("./SubWallet");
const Config_1 = require("./Config");
const Logger_1 = require("./Logger");
const Types_1 = require("./Types");
const Constants_1 = require("./Constants");
const Utilities_1 = require("./Utilities");
const WalletError_1 = require("./WalletError");
const _ = require("lodash");
const turtlecoin_utils_1 = require("turtlecoin-utils");
/**
* Stores each subwallet, along with transactions and public spend keys
*/
class SubWallets {
constructor(config, isViewWallet, privateViewKey, publicSpendKeys, subWallets) {
/**
* The public spend keys this wallet contains. Used for verifying if a
* transaction is ours.
*/
this.publicSpendKeys = [];
/**
* Mapping of public spend key to subwallet
*/
this.subWallets = new Map();
/**
* Our transactions
*/
this.transactions = [];
/**
* Transactions we sent, but haven't been confirmed yet
*/
this.lockedTransactions = [];
/**
* A mapping of transaction hashes, to transaction private keys
*/
this.transactionPrivateKeys = new Map();
/**
* A mapping of key images to the subwallet public spend key that owns them
*/
this.keyImageOwners = new Map();
this.config = new Config_1.Config();
this.config = config;
this.isViewWallet = isViewWallet;
this.privateViewKey = privateViewKey;
this.publicSpendKeys = publicSpendKeys;
this.subWallets = subWallets;
}
/**
* Loads SubWallets from json
*/
static fromJSON(json) {
const subWallets = Object.create(SubWallets.prototype);
const newSubWallets = Object.assign(subWallets, {
publicSpendKeys: json.publicSpendKeys,
subWallets: new Map(json.subWallet.map((x) => [x.publicSpendKey, SubWallet_1.SubWallet.fromJSON(x)])),
transactions: json.transactions.map((x) => Types_1.Transaction.fromJSON(x)),
lockedTransactions: json.lockedTransactions.map((x) => Types_1.Transaction.fromJSON(x)),
privateViewKey: json.privateViewKey,
isViewWallet: json.isViewWallet,
transactionPrivateKeys: new Map(json.txPrivateKeys.map((x) => [x.transactionHash, x.txPrivateKey])),
keyImageOwners: new Map(),
});
newSubWallets.initKeyImageMap();
return newSubWallets;
}
/**
* @param config
* @param address
* @param scanHeight
* @param newWallet
* @param privateViewKey
* @param privateSpendKey Private spend key is optional if it's a view wallet
*/
static init(config, address, scanHeight, newWallet, privateViewKey, privateSpendKey) {
return __awaiter(this, void 0, void 0, function* () {
let timestamp = 0;
if (newWallet) {
timestamp = Utilities_1.getCurrentTimestampAdjusted();
}
const decodedAddress = yield turtlecoin_utils_1.Address.fromAddress(address, config.addressPrefix);
const publicSpendKeys = [];
publicSpendKeys.push(decodedAddress.spend.publicKey);
const subWallet = new SubWallet_1.SubWallet(config, address, scanHeight, timestamp, decodedAddress.spend.publicKey, privateSpendKey);
const subWallets = new Map();
subWallets.set(decodedAddress.spend.publicKey, subWallet);
return new SubWallets(config, privateSpendKey === undefined, privateViewKey, publicSpendKeys, subWallets);
});
}
initKeyImageMap() {
for (const [publicKey, subWallet] of this.subWallets) {
for (const keyImage of subWallet.getKeyImages()) {
this.keyImageOwners.set(keyImage, publicKey);
}
}
}
pruneSpentInputs(pruneHeight) {
for (const [, subWallet] of this.subWallets) {
subWallet.pruneSpentInputs(pruneHeight);
}
}
reset(scanHeight, scanTimestamp) {
this.transactions = [];
this.lockedTransactions = [];
this.transactionPrivateKeys = new Map();
this.keyImageOwners = new Map();
for (const [, subWallet] of this.subWallets) {
subWallet.reset(scanHeight, scanTimestamp);
}
}
rewind(scanHeight) {
this.lockedTransactions = [];
this.removeForkedTransactions(scanHeight);
}
/**
* Convert SubWallets to something we can JSON.stringify
*/
toJSON() {
return {
publicSpendKeys: this.publicSpendKeys,
subWallet: [...this.subWallets.values()].map((x) => x.toJSON()),
transactions: this.transactions.map((x) => x.toJSON()),
lockedTransactions: this.lockedTransactions.map((x) => x.toJSON()),
privateViewKey: this.privateViewKey,
isViewWallet: this.isViewWallet,
txPrivateKeys: JsonSerialization_1.txPrivateKeysToVector(this.transactionPrivateKeys),
};
}
/**
* Get the shared private view key
*/
getPrivateViewKey() {
return this.privateViewKey;
}
/**
* Get the private spend key for the given public spend key, if it exists
*/
getPrivateSpendKey(publicSpendKey) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
return [new WalletError_1.WalletError(WalletError_1.WalletErrorCode.ADDRESS_NOT_IN_WALLET), ''];
}
return [WalletError_1.SUCCESS, subWallet.getPrivateSpendKey()];
}
/**
* Gets the 'primary' subwallet
*/
getPrimarySubWallet() {
for (const [, subWallet] of this.subWallets) {
if (subWallet.isPrimaryAddress()) {
return subWallet;
}
}
throw new Error('Wallet has no primary address!');
}
/**
* Gets the primary address of the wallet
*/
getPrimaryAddress() {
return this.getPrimarySubWallet().getAddress();
}
/**
* Gets the private spend key of the primary subwallet
*/
getPrimaryPrivateSpendKey() {
return this.getPrimarySubWallet().getPrivateSpendKey();
}
/**
* Get the hashes of the locked transactions (ones we've sent but not
* confirmed)
*/
getLockedTransactionHashes() {
return this.lockedTransactions.map((x) => x.hash);
}
/**
* Add this transaction to the container. If the transaction was previously
* sent by us, remove it from the locked container
*/
addTransaction(transaction) {
Logger_1.logger.log(`Transaction details: ${JSON.stringify(transaction)}`, Logger_1.LogLevel.TRACE, [Logger_1.LogCategory.SYNC, Logger_1.LogCategory.TRANSACTIONS]);
/* Remove this transaction from the locked data structure, if we had
added it previously as an outgoing tx */
_.remove(this.lockedTransactions, (tx) => {
return tx.hash === transaction.hash;
});
if (this.transactions.some((tx) => tx.hash === transaction.hash)) {
Logger_1.logger.log(`Already seen transaction ${transaction.hash}, ignoring.`, Logger_1.LogLevel.DEBUG, [Logger_1.LogCategory.SYNC, Logger_1.LogCategory.TRANSACTIONS]);
return;
}
this.transactions.push(transaction);
}
/**
* Adds a transaction we sent to the locked transactions container
*/
addUnconfirmedTransaction(transaction) {
Logger_1.logger.log(`Unconfirmed transaction details: ${JSON.stringify(transaction)}`, Logger_1.LogLevel.TRACE, [Logger_1.LogCategory.SYNC, Logger_1.LogCategory.TRANSACTIONS]);
if (this.lockedTransactions.some((tx) => tx.hash === transaction.hash)) {
Logger_1.logger.log(`Already seen unconfirmed transaction ${transaction.hash}, ignoring.`, Logger_1.LogLevel.DEBUG, [Logger_1.LogCategory.SYNC, Logger_1.LogCategory.TRANSACTIONS]);
return;
}
this.lockedTransactions.push(transaction);
}
/**
* @param publicSpendKey The public spend key of the subwallet to add this
* input to
* @param input
*
* Store the transaction input in the corresponding subwallet
*/
storeTransactionInput(publicSpendKey, input) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
Logger_1.logger.log(`Input details: ${JSON.stringify(input)}`, Logger_1.LogLevel.TRACE, [Logger_1.LogCategory.SYNC, Logger_1.LogCategory.TRANSACTIONS]);
if (!this.isViewWallet) {
this.keyImageOwners.set(input.keyImage, publicSpendKey);
}
subWallet.storeTransactionInput(input, this.isViewWallet);
}
/**
* @param publicSpendKey The public spend key of the subwallet to mark
* the corresponding input spent in
* @param spendHeight The height the input was spent at
* @param keyImage
*
* Marks an input as spent by us, no longer part of balance or available
* for spending. Input is identified by keyImage (unique)
*/
markInputAsSpent(publicSpendKey, keyImage, spendHeight) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
subWallet.markInputAsSpent(keyImage, spendHeight);
}
markInputAsLocked(publicSpendKey, keyImage) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
subWallet.markInputAsLocked(keyImage);
}
/**
* Remove a transaction that we sent by didn't get included in a block and
* returned to us. Removes the correspoding inputs, too.
*/
removeCancelledTransaction(transactionHash) {
/* Remove the tx if it was locked */
_.remove(this.lockedTransactions, (tx) => {
return tx.hash === transactionHash;
});
/* Remove the corresponding inputs */
for (const [, subWallet] of this.subWallets) {
subWallet.removeCancelledTransaction(transactionHash);
}
}
/**
* Remove transactions which occured in a forked block. If they got added
* in another block, we'll add them back again then.
*/
removeForkedTransactions(forkHeight) {
_.remove(this.transactions, (tx) => {
return tx.blockHeight >= forkHeight;
});
let keyImagesToRemove = [];
for (const [, subWallet] of this.subWallets) {
keyImagesToRemove = keyImagesToRemove.concat(subWallet.removeForkedTransactions(forkHeight));
}
if (!this.isViewWallet) {
for (const keyImage of keyImagesToRemove) {
this.keyImageOwners.delete(keyImage);
}
}
}
/**
* Convert a timestamp to a block height. Block heights are more dependable
* than timestamps, which sometimes get treated a little funkily by the
* daemon
*/
convertSyncTimestampToHeight(timestamp, height) {
for (const [, subWallet] of this.subWallets) {
subWallet.convertSyncTimestampToHeight(timestamp, height);
}
}
haveSpendableInput(input, height) {
for (const [, subWallet] of this.subWallets) {
if (subWallet.haveSpendableInput(input, height)) {
return true;
}
}
return false;
}
/**
* Get the owner (i.e., the public spend key of the subwallet) of this
* keyImage
*
* @return Returns [true, publicSpendKey] if found, [false, ''] if not
* found
*/
getKeyImageOwner(keyImage) {
if (this.isViewWallet) {
return [false, ''];
}
const owner = this.keyImageOwners.get(keyImage);
if (owner) {
return [true, owner];
}
return [false, ''];
}
/**
* Gets all public spend keys in this container
*/
getPublicSpendKeys() {
return this.publicSpendKeys;
}
/**
* Get all [public, private] spend keys in a container
*/
getAllSpendKeys() {
const keys = [];
for (const [publicKey, subWallet] of this.subWallets) {
keys.push([publicKey, subWallet.getPrivateSpendKey()]);
}
return keys;
}
/**
* Generate the key image for an input
*/
getTxInputKeyImage(publicSpendKey, derivation, outputIndex) {
return __awaiter(this, void 0, void 0, function* () {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
if (this.isViewWallet) {
const nullKey = '0'.repeat(64);
return Promise.resolve([nullKey, nullKey]);
}
return subWallet.getTxInputKeyImage(derivation, outputIndex);
});
}
/**
* Returns the summed balance of the given subwallet addresses. If none are given,
* take from all.
*
* @return Returns [unlockedBalance, lockedBalance]
*/
getBalance(currentHeight, subWalletsToTakeFrom) {
return __awaiter(this, void 0, void 0, function* () {
let publicSpendKeys = [];
/* If no subwallets given, take from all */
if (!subWalletsToTakeFrom) {
publicSpendKeys = this.publicSpendKeys;
}
else {
for (const address of subWalletsToTakeFrom) {
const [, publicSpendKey] = yield Utilities_1.addressToKeys(address, this.config);
publicSpendKeys.push(publicSpendKey);
}
}
let unlockedBalance = 0;
let lockedBalance = 0;
/* For faster lookups in case we have a ton of transactions or
subwallets to take from */
const lookupMap = new Map(publicSpendKeys.map((x) => [x, true]));
for (const transaction of this.transactions) {
const unlocked = Utilities_1.isInputUnlocked(transaction.unlockTime, currentHeight);
for (const [publicKey, amount] of transaction.transfers) {
if (lookupMap.has(publicKey)) {
if (unlocked) {
unlockedBalance += amount;
}
else {
lockedBalance += amount;
}
}
}
}
for (const transaction of this.lockedTransactions) {
for (const [publicKey, amount] of transaction.transfers) {
if (lookupMap.has(publicKey)) {
unlockedBalance += amount;
}
}
}
let unconfirmedIncomingBalance = 0;
for (const publicSpendKey of publicSpendKeys) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
unconfirmedIncomingBalance += subWallet.getUnconfirmedChange();
}
lockedBalance += unconfirmedIncomingBalance;
unlockedBalance -= unconfirmedIncomingBalance;
return [unlockedBalance, lockedBalance];
});
}
/**
* Gets all addresses contained in this SubWallets container
*/
getAddresses() {
const addresses = [];
for (const [, subWallet] of this.subWallets) {
addresses.push(subWallet.getAddress());
}
return addresses;
}
/**
* Get input sufficient to spend the amount passed in, from the given
* subwallets, along with the keys for that inputs owner.
*
* Throws if the subwallets don't exist, or not enough money is found.
*
* @returns Returns the inputs and their owners, and the sum of their money
*/
getSpendableTransactionInputs(subWalletsToTakeFrom, currentHeight) {
return __awaiter(this, void 0, void 0, function* () {
let availableInputs = [];
/* Loop through each subwallet that we can take from */
for (const address of subWalletsToTakeFrom) {
const [, publicSpendKey] = yield Utilities_1.addressToKeys(address, this.config);
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
/* Fetch the spendable inputs */
availableInputs = availableInputs.concat(subWallet.getSpendableInputs(currentHeight));
}
/* Sort by amount, largest first */
availableInputs = _.orderBy(availableInputs, [(x) => x.input.amount], ['desc']);
/* Push into base 10 buckets. Smallest amount buckets will come first, and
* largest amounts within those buckets come first */
let buckets = new Map();
for (const input of availableInputs) {
/* Find out how many digits the amount has, i.e. 1337 has 4 digits,
420 has 3 digits */
const numberOfDigits = Math.floor(Math.log10(input.input.amount)) + 1;
/* Grab existing array or make a new one */
const tmpArr = buckets.get(numberOfDigits) || [];
/* Add input to array */
tmpArr.push(input);
/* Update bucket with new array */
buckets.set(numberOfDigits, tmpArr);
}
/* ES6 maps are sorted by insertion order, so we create a new map, sorting
* the buckets we want first in the resulting map, first. */
buckets = new Map([...buckets].sort((a, b) => {
return a[0] > b[0] ? 1 : -1;
}));
const ordered = [];
while (buckets.size > 0) {
for (const [amount, bucket] of buckets) {
/* Bucket has been exhausted, remove from list */
if (bucket.length === 0) {
buckets.delete(amount);
}
else {
/* Add the final (smallest amount in this bucket) to the
* result, and remove it */
ordered.push(bucket.pop());
}
}
}
return ordered;
});
}
getFusionTransactionInputs(subWalletsToTakeFrom, mixin, currentHeight) {
return __awaiter(this, void 0, void 0, function* () {
let availableInputs = [];
/* Loop through each subwallet that we can take from */
for (const address of subWalletsToTakeFrom) {
const [, publicSpendKey] = yield Utilities_1.addressToKeys(address, this.config);
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
/* Fetch the spendable inputs */
availableInputs = availableInputs.concat(subWallet.getSpendableInputs(currentHeight));
}
/* Shuffle the inputs */
availableInputs = _.shuffle(availableInputs);
/* Split the inputs into buckets based on what power of ten they are in
(For example, [1, 2, 5, 7], [20, 50, 80, 80], [100, 600, 700]) */
const buckets = new Map();
for (const walletAmount of availableInputs) {
/* Find out how many digits the amount has, i.e. 1337 has 4 digits,
420 has 3 digits */
const numberOfDigits = Math.ceil(Math.log10(walletAmount.input.amount + 1));
const tmp = buckets.get(numberOfDigits) || [];
tmp.push(walletAmount);
/* Insert the amount into the correct bucket */
buckets.set(numberOfDigits, tmp);
}
let fullBuckets = [];
for (const [, bucket] of buckets) {
/* Skip the buckets with not enough items */
if (bucket.length >= Constants_1.FUSION_TX_MIN_INPUT_COUNT) {
fullBuckets.push(bucket);
}
}
/* Shuffle the full buckets */
fullBuckets = _.shuffle(fullBuckets);
let bucketsToTakeFrom = [];
/* We have full buckets, take the first full bucket */
if (fullBuckets.length > 0) {
bucketsToTakeFrom = [
fullBuckets[0],
];
/* Otherwise just use all buckets */
}
else {
for (const [, bucket] of buckets) {
bucketsToTakeFrom.push(bucket);
}
}
const inputsToUse = [];
// eslint-disable-next-line max-len
/* See https://github.com/turtlecoin/turtlecoin/blob/153c08c3a046434522f7ac3ddd043037888b2bf5/src/CryptoNoteCore/Currency.cpp#L629 */
/* With 3 mixin == 314 bytes. */
const inputSize = 1 + (6 + 2) + 32 + 64 + 1 + 4 + mixin * (4 + 64);
/* Probably about 100 inputs max. This ignores other size constraints,
since it is a max, after all. */
const maxInputsToTake = Constants_1.MAX_FUSION_TX_SIZE / inputSize;
let foundMoney = 0;
/* Loop through each bucket (Remember we're only looping through one if
we've got a full bucket) */
for (const bucket of bucketsToTakeFrom) {
for (const walletAmount of bucket) {
inputsToUse.push(walletAmount);
foundMoney += walletAmount.input.amount;
if (inputsToUse.length >= maxInputsToTake) {
return [inputsToUse, foundMoney];
}
}
}
return [inputsToUse, foundMoney];
});
}
/**
* Store the private key for a given transaction
*/
storeTxPrivateKey(txPrivateKey, txHash) {
this.transactionPrivateKeys.set(txHash, txPrivateKey);
}
/**
* Store an unconfirmed incoming amount, so we can correctly display locked
* balances
*/
storeUnconfirmedIncomingInput(input, publicSpendKey) {
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
throw new Error('Subwallet not found!');
}
subWallet.storeUnconfirmedIncomingInput(input);
}
/**
* Get the transactions of the given subWallet address. If no subWallet address is given,
* gets all transactions.
*/
getTransactions(address, includeFusions) {
return __awaiter(this, void 0, void 0, function* () {
return this.filterTransactions(this.transactions, address, includeFusions);
});
}
/**
* Get the number of transactions for the given subWallet, if no subWallet is given,
* gets the total number of transactions in the wallet container. Can be used
* if you want to avoid fetching every transactions repeatedly when nothing
* has changed.
*/
getNumTransactions(address, includeFusions = true) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getTransactions(address, includeFusions)).length;
});
}
/**
* Get the unconfirmed transactions of the given subwallet address. If no subwallet address
* is given, gets all unconfirmed transactions.
*/
getUnconfirmedTransactions(address, includeFusions = true) {
return __awaiter(this, void 0, void 0, function* () {
return this.filterTransactions(this.lockedTransactions, address, includeFusions);
});
}
/**
* Get the number of unconfirmed transactions for the given subWallet, if no subWallet is given,
* gets the total number of unconfirmed transactions in the wallet container. Can be used
* if you want to avoid fetching every transactions repeatedly when nothing
* has changed.
*/
getNumUnconfirmedTransactions(address, includeFusions) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getUnconfirmedTransactions(address, includeFusions)).length;
});
}
initAfterLoad(config) {
this.config = config;
this.subWallets.forEach((subWallet) => subWallet.initAfterLoad(config));
}
addSubWallet(scanHeight) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isViewWallet) {
/* Adding a random subwallet to a view wallet makes no sense. */
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.ILLEGAL_VIEW_WALLET_OPERATION)];
}
/* Make a random address to get a new pub/priv spend key pair */
const address = yield turtlecoin_utils_1.Address.fromEntropy(undefined, undefined, this.config.addressPrefix);
if (this.publicSpendKeys.includes(address.spend.publicKey)) {
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.SUBWALLET_ALREADY_EXISTS)];
}
this.publicSpendKeys.push(address.spend.publicKey);
/**
* Create the actual address using the shared private view key and the new
* private spend key
*/
const newAddress = yield (yield turtlecoin_utils_1.Address.fromKeys(address.spend.privateKey, this.privateViewKey, this.config.addressPrefix)).address();
const subWallet = new SubWallet_1.SubWallet(this.config, newAddress, scanHeight, 0, address.spend.publicKey, address.spend.privateKey, false);
this.subWallets.set(address.spend.publicKey, subWallet);
return [newAddress, undefined];
});
}
importSubWallet(privateSpendKey, scanHeight) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isViewWallet) {
/* Adding a random subwallet to a view wallet makes no sense. */
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.ILLEGAL_VIEW_WALLET_OPERATION)];
}
const publicSpendKey = yield CnUtils_1.CryptoUtils(this.config).privateKeyToPublicKey(privateSpendKey);
if (this.publicSpendKeys.includes(publicSpendKey)) {
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.SUBWALLET_ALREADY_EXISTS)];
}
const publicViewKey = yield CnUtils_1.CryptoUtils(this.config).privateKeyToPublicKey(this.privateViewKey);
const address = yield (yield turtlecoin_utils_1.Address.fromPublicKeys(publicSpendKey, publicViewKey, undefined, this.config.addressPrefix)).address();
this.publicSpendKeys.push(publicSpendKey);
const subWallet = new SubWallet_1.SubWallet(this.config, address, scanHeight, 0, publicSpendKey, privateSpendKey, false);
this.subWallets.set(publicSpendKey, subWallet);
return [address, undefined];
});
}
importViewSubWallet(publicSpendKey, scanHeight) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isViewWallet) {
/* Adding a random subwallet to a view wallet makes no sense. */
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.ILLEGAL_NON_VIEW_WALLET_OPERATION)];
}
if (this.publicSpendKeys.includes(publicSpendKey)) {
return [undefined, new WalletError_1.WalletError(WalletError_1.WalletErrorCode.SUBWALLET_ALREADY_EXISTS)];
}
const publicViewKey = yield CnUtils_1.CryptoUtils(this.config).privateKeyToPublicKey(this.privateViewKey);
const address = yield (yield turtlecoin_utils_1.Address.fromPublicKeys(publicSpendKey, publicViewKey, undefined, this.config.addressPrefix)).address();
this.publicSpendKeys.push(publicSpendKey);
const subWallet = new SubWallet_1.SubWallet(this.config, address, scanHeight, 0, publicSpendKey, undefined, false);
this.subWallets.set(publicSpendKey, subWallet);
return [address, undefined];
});
}
deleteSubWallet(address) {
return __awaiter(this, void 0, void 0, function* () {
const [, publicSpendKey] = yield Utilities_1.addressToKeys(address, this.config);
const subWallet = this.subWallets.get(publicSpendKey);
if (!subWallet) {
return new WalletError_1.WalletError(WalletError_1.WalletErrorCode.ADDRESS_NOT_IN_WALLET);
}
if (subWallet.isPrimaryAddress()) {
return new WalletError_1.WalletError(WalletError_1.WalletErrorCode.CANNOT_DELETE_PRIMARY_ADDRESS);
}
this.subWallets.delete(publicSpendKey);
this.deleteAddressTransactions(this.transactions, publicSpendKey);
this.deleteAddressTransactions(this.lockedTransactions, publicSpendKey);
return WalletError_1.SUCCESS;
});
}
getWalletCount() {
return this.subWallets.size;
}
deleteAddressTransactions(txs, publicSpendKey) {
_.remove(txs, (tx) => {
/* See if this transaction contains the subwallet we're deleting */
if (tx.transfers.has(publicSpendKey)) {
/* If it's the only element, delete the transaction */
if (tx.transfers.size === 1) {
return true;
/* Otherwise just delete the transfer in the transaction */
}
else {
tx.transfers.delete(publicSpendKey);
}
}
return false;
});
}
filterTransactions(txs, address, includeFusions = true) {
return __awaiter(this, void 0, void 0, function* () {
const filters = [];
if (address) {
const [, publicSpendKey] = yield Utilities_1.addressToKeys(address, this.config);
filters.push((tx) => tx.transfers.has(publicSpendKey));
}
if (!includeFusions) {
filters.push((tx) => !tx.isFusionTransaction());
}
return txs.filter((tx) => filters.every((f) => f(tx)));
});
}
}
exports.SubWallets = SubWallets;