turtlecoin-wallet-backend
Version:
[](https://travis-ci.org/turtlecoin/turtlecoin-wallet-backend-js)
313 lines (312 loc) • 12.7 kB
JavaScript
"use strict";
// Copyright (c) 2018, Zpalmtree
//
// Please see the included LICENSE file for more information.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Logger_1 = require("./Logger");
const Types_1 = require("./Types");
const Utilities_1 = require("./Utilities");
const CryptoWrapper_1 = require("./CryptoWrapper");
const Config_1 = require("./Config");
const _ = require("lodash");
class SubWallet {
constructor(config, address, scanHeight, timestamp, publicSpendKey, privateSpendKey) {
/**
* A vector of the stored transaction input data, to be used for
* sending transactions later
*/
this.unspentInputs = [];
/**
* Inputs which have been used in a transaction, and are waiting to
* either be put into a block, or return to our wallet
*/
this.lockedInputs = [];
/**
* Inputs which have been spent in a transaction
*/
this.spentInputs = [];
/**
* Inputs which have come in from a transaction we sent - either from
* change or from sending to ourself - we use this to display unlocked
* balance correctly
*/
this.unconfirmedIncomingAmounts = [];
/**
* The timestamp to begin syncing the wallet at
* (usually creation time or zero)
*/
this.syncStartTimestamp = 0;
/**
* The height to begin syncing the wallet at
*/
this.syncStartHeight = 0;
this.config = new Config_1.Config();
this.address = address;
this.syncStartHeight = scanHeight;
this.syncStartTimestamp = timestamp;
this.publicSpendKey = publicSpendKey;
this.privateSpendKey = privateSpendKey;
this.primaryAddress = true;
this.config = config;
}
static fromJSON(json) {
const subWallet = Object.create(SubWallet.prototype);
return Object.assign(subWallet, {
unspentInputs: json.unspentInputs.map((x) => Types_1.TransactionInput.fromJSON(x)),
lockedInputs: json.lockedInputs.map((x) => Types_1.TransactionInput.fromJSON(x)),
spentInputs: json.spentInputs.map((x) => Types_1.TransactionInput.fromJSON(x)),
unconfirmedIncomingAmounts: json.unconfirmedIncomingAmounts.map((x) => Types_1.UnconfirmedInput.fromJSON(x)),
publicSpendKey: json.publicSpendKey,
privateSpendKey: json.privateSpendKey === '0'.repeat(64) ? undefined : json.privateSpendKey,
syncStartTimestamp: json.syncStartTimestamp,
syncStartHeight: json.syncStartHeight,
address: json.address,
primaryAddress: json.isPrimaryAddress,
});
}
toJSON() {
return {
unspentInputs: this.unspentInputs.map((x) => x.toJSON()),
lockedInputs: this.lockedInputs.map((x) => x.toJSON()),
spentInputs: this.spentInputs.map((x) => x.toJSON()),
unconfirmedIncomingAmounts: this.unconfirmedIncomingAmounts.map((x) => x.toJSON()),
publicSpendKey: this.publicSpendKey,
/* Null secret key if view wallet */
privateSpendKey: this.privateSpendKey ? this.privateSpendKey : '0'.repeat(64),
syncStartTimestamp: this.syncStartTimestamp,
syncStartHeight: this.syncStartHeight,
address: this.address,
isPrimaryAddress: this.primaryAddress,
};
}
pruneSpentInputs(pruneHeight) {
const lenBeforePrune = this.spentInputs.length;
/* Remove all spent inputs that are older than 5000 blocks old.
It is assumed the blockchain cannot fork more than this, and this
frees up a lot of disk space with large, old wallets. */
_.remove(this.spentInputs, (input) => input.spendHeight > pruneHeight);
const lenAfterPrune = this.spentInputs.length;
const difference = lenBeforePrune - lenAfterPrune;
if (difference !== 0) {
Logger_1.logger.log('Pruned ' + difference + ' spent inputs', Logger_1.LogLevel.DEBUG, Logger_1.LogCategory.SYNC);
}
}
reset(scanHeight, scanTimestamp) {
this.syncStartHeight = scanHeight;
this.syncStartTimestamp = scanTimestamp;
this.spentInputs = [];
this.lockedInputs = [];
this.unconfirmedIncomingAmounts = [];
this.unspentInputs = [];
}
/**
* Get the private spend key, or null key if view wallet
*/
getPrivateSpendKey() {
return this.privateSpendKey || '0'.repeat(64);
}
/**
* Whether this address is the primary wallet address
*/
isPrimaryAddress() {
return this.primaryAddress;
}
/**
* Get this wallets address
*/
getAddress() {
return this.address;
}
/**
* Store an unspent input
*/
storeTransactionInput(input, isViewWallet) {
if (!isViewWallet) {
/* Find the input in the unconfirmed incoming amounts - inputs we
sent ourselves, that are now returning as change. Remove from
vector if found. */
_.remove(this.unconfirmedIncomingAmounts, (storedInput) => {
return storedInput.key === input.key;
});
}
const existingInput = this.unspentInputs.find((x) => x.key === input.key);
if (existingInput !== undefined) {
Logger_1.logger.log(`Input ${input.key} was added to the wallet twice!`, Logger_1.LogLevel.ERROR, Logger_1.LogCategory.SYNC);
return;
}
this.unspentInputs.push(input);
}
/**
* Move input from unspent/locked to spend container
*/
markInputAsSpent(keyImage, spendHeight) {
/* Remove from unspent if exists */
let [removedInput] = _.remove(this.unspentInputs, (input) => {
return input.keyImage === keyImage;
});
if (!removedInput) {
/* Not in unspent, check locked */
[removedInput] = _.remove(this.lockedInputs, (input) => {
return input.keyImage === keyImage;
});
}
if (!removedInput) {
Logger_1.logger.log('Could not find key image to remove!', Logger_1.LogLevel.ERROR, Logger_1.LogCategory.SYNC);
return;
}
removedInput.spendHeight = spendHeight;
this.spentInputs.push(removedInput);
}
/**
* Move an input from the unspent container to the locked container
*/
markInputAsLocked(keyImage) {
/* Remove input from unspent */
const [removedInput] = _.remove(this.unspentInputs, (input) => {
return input.keyImage === keyImage;
});
if (!removedInput) {
Logger_1.logger.log('Could not find key image to lock!', Logger_1.LogLevel.ERROR, Logger_1.LogCategory.SYNC);
return;
}
/* Add to locked */
this.lockedInputs.push(removedInput);
}
/**
* Remove inputs belonging to a cancelled transaction and mark them as
* unspent
*/
removeCancelledTransaction(transactionHash) {
/* Find inputs used in the cancelled transaction, and remove them from
the locked inputs */
const removed = _.remove(this.lockedInputs, (input) => {
return input.parentTransactionHash === transactionHash;
}).map((input) => {
input.spendHeight = 0;
return input;
});
/* Add them to the unspent vector */
this.unspentInputs = this.unspentInputs.concat(removed);
/* Remove unconfirmed amounts we used to correctly calculate incoming
change */
_.remove(this.unconfirmedIncomingAmounts, (input) => {
return input.parentTransactionHash === transactionHash;
});
}
/**
* Remove transactions and inputs that occured after a fork height
*/
removeForkedTransactions(forkHeight) {
const lockedInputKeyImages = this.lockedInputs.map((input) => input.keyImage);
/* Both of these will get resolved by the wallet in time */
this.lockedInputs = [];
this.unconfirmedIncomingAmounts = [];
/* Remove unspent inputs which arrived after this height */
const removedUnspent = _.remove(this.unspentInputs, (input) => {
return input.blockHeight >= forkHeight;
});
/* Remove spent inputs which arrived after this height */
const removedSpent = _.remove(this.spentInputs, (input) => {
return input.blockHeight >= forkHeight;
});
/* This input arrived before the fork height, but was spent after the
fork height. So, we move them back into the unspent inputs vector. */
const nowUnspent = _.remove(this.spentInputs, (input) => {
return input.spendHeight >= forkHeight;
});
this.unspentInputs = this.unspentInputs.concat(nowUnspent.map((input) => { input.spendHeight = 0; return input; }));
/* Could do this with concat+map.. but i think this is a little more
readable */
const keyImagesToRemove = [];
for (const keyImage of lockedInputKeyImages) {
keyImagesToRemove.push(keyImage);
}
for (const input of removedUnspent) {
keyImagesToRemove.push(input.keyImage);
}
for (const input of removedSpent) {
keyImagesToRemove.push(input.keyImage);
}
return keyImagesToRemove;
}
/**
* Convert a timestamp to a height
*/
convertSyncTimestampToHeight(startTimestamp, startHeight) {
/* If we don't have a start timestamp then we don't need to convert */
if (this.syncStartTimestamp !== 0) {
this.syncStartTimestamp = startTimestamp;
this.syncStartHeight = startHeight;
}
}
/**
* Gets every stored key image
*/
getKeyImages() {
let keyImages = [];
keyImages = keyImages.concat(this.unspentInputs.map((x) => x.keyImage));
keyImages = keyImages.concat(this.lockedInputs.map((x) => x.keyImage));
keyImages = keyImages.concat(this.spentInputs.map((x) => x.keyImage));
return keyImages;
}
/**
* Generate the key image for this input
*/
getTxInputKeyImage(derivation, outputIndex) {
return __awaiter(this, void 0, void 0, function* () {
const [keyImage] = yield CryptoWrapper_1.generateKeyImagePrimitive(this.publicSpendKey, this.privateSpendKey, outputIndex, derivation, this.config);
return keyImage;
});
}
/**
* Get the unlocked/locked balance at a given height
*/
getBalance(currentHeight) {
let unlockedBalance = 0;
let lockedBalance = 0;
for (const input of this.unspentInputs) {
if (Utilities_1.isInputUnlocked(input.unlockTime, currentHeight)) {
unlockedBalance += input.amount;
}
else {
lockedBalance += input.amount;
}
}
lockedBalance += _.sumBy(this.unconfirmedIncomingAmounts, 'amount');
return [unlockedBalance, lockedBalance];
}
/**
* Gets the amount of funds returning to us as change from outgoing
* unconfirmed transactions
*/
getUnconfirmedChange() {
return _.sumBy(this.unconfirmedIncomingAmounts, 'amount');
}
/**
* Get inputs that are available to be spent, and their keys
*/
getSpendableInputs(currentHeight) {
const inputs = [];
for (const input of this.unspentInputs) {
if (Utilities_1.isInputUnlocked(input.unlockTime, currentHeight)) {
inputs.push(new Types_1.TxInputAndOwner(input, this.privateSpendKey, this.publicSpendKey));
}
}
return inputs;
}
storeUnconfirmedIncomingInput(input) {
this.unconfirmedIncomingAmounts.push(input);
}
initAfterLoad(config) {
this.config = config;
}
}
exports.SubWallet = SubWallet;