txdb: refactor x2.
This commit is contained in:
parent
3412916c89
commit
04f071f9b0
@ -3,7 +3,6 @@
|
||||
exports.Account = require('./account');
|
||||
exports.MasterKey = require('./masterkey');
|
||||
exports.Path = require('./path');
|
||||
exports.PathInfo = require('./pathinfo');
|
||||
exports.TXDB = require('./txdb');
|
||||
exports.WalletDB = require('./walletdb');
|
||||
exports.Wallet = require('./wallet');
|
||||
|
||||
@ -1,313 +0,0 @@
|
||||
/*!
|
||||
* pathinfo.js - pathinfo object for bcoin
|
||||
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
|
||||
* https://github.com/bcoin-org/bcoin
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var utils = require('../utils/utils');
|
||||
|
||||
/**
|
||||
* Path Info
|
||||
* @constructor
|
||||
* @param {WalletDB} db
|
||||
* @param {WalletID} wid
|
||||
* @param {TX} tx
|
||||
* @param {Object} table
|
||||
*/
|
||||
|
||||
function PathInfo(wallet, tx, paths) {
|
||||
if (!(this instanceof PathInfo))
|
||||
return new PathInfo(wallet, tx, paths);
|
||||
|
||||
// All relevant Accounts for
|
||||
// inputs and outputs (for database indexing).
|
||||
this.accounts = [];
|
||||
|
||||
// All output paths (for deriving during sync).
|
||||
this.paths = [];
|
||||
|
||||
// Wallet
|
||||
this.wallet = wallet;
|
||||
|
||||
// Wallet ID
|
||||
this.wid = wallet.wid;
|
||||
|
||||
// Wallet Label
|
||||
this.id = wallet.id;
|
||||
|
||||
// Map of address hashes->paths.
|
||||
this.pathMap = {};
|
||||
|
||||
// Current transaction.
|
||||
this.tx = null;
|
||||
|
||||
// Wallet-specific details cache.
|
||||
this._details = null;
|
||||
this._json = null;
|
||||
|
||||
if (tx)
|
||||
this.fromTX(tx, paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate path info from a transaction.
|
||||
* @private
|
||||
* @param {TX} tx
|
||||
* @param {Object} table
|
||||
* @returns {PathInfo}
|
||||
*/
|
||||
|
||||
PathInfo.prototype.fromTX = function fromTX(tx, paths) {
|
||||
var uniq = {};
|
||||
var i, hashes, hash, path;
|
||||
|
||||
this.tx = tx;
|
||||
|
||||
for (i = 0; i < paths.length; i++) {
|
||||
path = paths[i];
|
||||
|
||||
this.pathMap[path.hash] = path;
|
||||
|
||||
if (!uniq[path.account]) {
|
||||
uniq[path.account] = true;
|
||||
this.accounts.push(path.account);
|
||||
}
|
||||
}
|
||||
|
||||
hashes = tx.getOutputHashes('hex');
|
||||
|
||||
for (i = 0; i < hashes.length; i++) {
|
||||
hash = hashes[i];
|
||||
path = this.pathMap[hash];
|
||||
if (path)
|
||||
this.paths.push(path);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate path info from a transaction.
|
||||
* @param {WalletDB} db
|
||||
* @param {WalletID} wid
|
||||
* @param {TX} tx
|
||||
* @param {Object} table
|
||||
* @returns {PathInfo}
|
||||
*/
|
||||
|
||||
PathInfo.fromTX = function fromTX(wallet, tx, paths) {
|
||||
return new PathInfo(wallet).fromTX(tx, paths);
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether the map has paths
|
||||
* for a given address hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
PathInfo.prototype.hasPath = function hasPath(output) {
|
||||
var hash = output.getHash('hex');
|
||||
|
||||
if (!hash)
|
||||
return false;
|
||||
|
||||
return this.pathMap[hash] != null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get path for a given address hash.
|
||||
* @param {Hash} hash
|
||||
* @returns {Path}
|
||||
*/
|
||||
|
||||
PathInfo.prototype.getPath = function getPath(output) {
|
||||
var hash = output.getHash('hex');
|
||||
|
||||
if (!hash)
|
||||
return;
|
||||
|
||||
return this.pathMap[hash];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert path info to transaction details.
|
||||
* @returns {Details}
|
||||
*/
|
||||
|
||||
PathInfo.prototype.toDetails = function toDetails() {
|
||||
var details = this._details;
|
||||
|
||||
if (!details) {
|
||||
details = new Details(this);
|
||||
this._details = details;
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert path info to JSON details (caches json).
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
PathInfo.prototype.toJSON = function toJSON() {
|
||||
var json = this._json;
|
||||
|
||||
if (!json) {
|
||||
json = this.toDetails().toJSON();
|
||||
this._json = json;
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transaction Details
|
||||
* @constructor
|
||||
* @param {PathInfo} info
|
||||
*/
|
||||
|
||||
function Details(info) {
|
||||
if (!(this instanceof Details))
|
||||
return new Details(info);
|
||||
|
||||
this.db = info.wallet.db;
|
||||
this.network = this.db.network;
|
||||
this.wid = info.wid;
|
||||
this.id = info.id;
|
||||
this.hash = info.tx.hash('hex');
|
||||
this.height = info.tx.height;
|
||||
this.block = info.tx.block;
|
||||
this.index = info.tx.index;
|
||||
this.confirmations = info.tx.getConfirmations(this.db.height);
|
||||
this.fee = info.tx.getFee();
|
||||
this.ts = info.tx.ts;
|
||||
this.ps = info.tx.ps;
|
||||
this.tx = info.tx;
|
||||
this.inputs = [];
|
||||
this.outputs = [];
|
||||
|
||||
this.init(info.pathMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize transactions details
|
||||
* by pushing on mapped members.
|
||||
* @private
|
||||
* @param {Object} table
|
||||
*/
|
||||
|
||||
Details.prototype.init = function init(map) {
|
||||
this._insert(this.tx.inputs, true, this.inputs, map);
|
||||
this._insert(this.tx.outputs, false, this.outputs, map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert members in the input or output vector.
|
||||
* @private
|
||||
* @param {Input[]|Output[]} vector
|
||||
* @param {Array} target
|
||||
* @param {Object} table
|
||||
*/
|
||||
|
||||
Details.prototype._insert = function _insert(vector, input, target, map) {
|
||||
var i, io, address, hash, path, member;
|
||||
|
||||
for (i = 0; i < vector.length; i++) {
|
||||
io = vector[i];
|
||||
member = new DetailsMember();
|
||||
|
||||
if (input) {
|
||||
if (io.coin)
|
||||
member.value = io.coin.value;
|
||||
} else {
|
||||
member.value = io.value;
|
||||
}
|
||||
|
||||
address = io.getAddress();
|
||||
|
||||
if (address) {
|
||||
member.address = address;
|
||||
|
||||
hash = address.getHash('hex');
|
||||
path = map[hash];
|
||||
|
||||
if (path)
|
||||
member.path = path;
|
||||
}
|
||||
|
||||
target.push(member);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert details to a more json-friendly object.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
Details.prototype.toJSON = function toJSON() {
|
||||
var self = this;
|
||||
return {
|
||||
wid: this.wid,
|
||||
id: this.id,
|
||||
hash: utils.revHex(this.hash),
|
||||
height: this.height,
|
||||
block: this.block ? utils.revHex(this.block) : null,
|
||||
ts: this.ts,
|
||||
ps: this.ps,
|
||||
index: this.index,
|
||||
fee: utils.btc(this.fee),
|
||||
confirmations: this.confirmations,
|
||||
inputs: this.inputs.map(function(input) {
|
||||
return input.toJSON(self.network);
|
||||
}),
|
||||
outputs: this.outputs.map(function(output) {
|
||||
return output.toJSON(self.network);
|
||||
}),
|
||||
tx: this.tx.toRaw().toString('hex')
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transaction Details Member
|
||||
* @constructor
|
||||
* @property {Number} value
|
||||
* @property {Address} address
|
||||
* @property {Path} path
|
||||
*/
|
||||
|
||||
function DetailsMember() {
|
||||
if (!(this instanceof DetailsMember))
|
||||
return new DetailsMember();
|
||||
|
||||
this.value = 0;
|
||||
this.address = null;
|
||||
this.path = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the member to a more json-friendly object.
|
||||
* @param {Network} network
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
DetailsMember.prototype.toJSON = function toJSON(network) {
|
||||
return {
|
||||
value: utils.btc(this.value),
|
||||
address: this.address
|
||||
? this.address.toBase58(network)
|
||||
: null,
|
||||
path: this.path
|
||||
? this.path.toJSON()
|
||||
: null
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = PathInfo;
|
||||
@ -34,6 +34,7 @@ var DUMMY = new Buffer([0]);
|
||||
* M[account][time][hash] -> dummy (tx by time + account)
|
||||
* H[account][height][hash] -> dummy (tx by height + account)
|
||||
* C[account][hash][index] -> dummy (coin by account)
|
||||
* r[hash] -> dummy (replace by fee chain)
|
||||
*/
|
||||
|
||||
var layout = {
|
||||
@ -324,7 +325,7 @@ TXDB.prototype.commit = co(function* commit() {
|
||||
* @private
|
||||
* @param {String} event
|
||||
* @param {Object} data
|
||||
* @param {PathInfo} details
|
||||
* @param {Details} details
|
||||
*/
|
||||
|
||||
TXDB.prototype.emit = function emit(event, data, details) {
|
||||
@ -424,9 +425,9 @@ TXDB.prototype.values = function values(options) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a transactions' addresses to wallet IDs.
|
||||
* @param {TX} tx
|
||||
* @returns {Promise} - Returns {@link PathInfo}.
|
||||
* Get wallet path for output.
|
||||
* @param {Output} output
|
||||
* @returns {Promise} - Returns {@link Path}.
|
||||
*/
|
||||
|
||||
TXDB.prototype.getPath = function getPath(output) {
|
||||
@ -436,9 +437,9 @@ TXDB.prototype.getPath = function getPath(output) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a transactions' addresses to wallet IDs.
|
||||
* @param {TX} tx
|
||||
* @returns {Promise} - Returns {@link PathInfo}.
|
||||
* Test whether path exists for output.
|
||||
* @param {Output} output
|
||||
* @returns {Promise} - Returns Boolean.
|
||||
*/
|
||||
|
||||
TXDB.prototype.hasPath = function hasPath(output) {
|
||||
@ -497,12 +498,9 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) {
|
||||
input = tx.inputs[i];
|
||||
prevout = input.prevout;
|
||||
|
||||
spent = yield this.getSpent(prevout.hash, prevout.index);
|
||||
|
||||
if (spent) {
|
||||
coin = yield this.getSpentCoin(spent, prevout);
|
||||
assert(coin);
|
||||
coin = yield this.getCoin(prevout.hash, prevout.index);
|
||||
|
||||
if (coin) {
|
||||
if (this.options.verify && tx.height === -1) {
|
||||
input.coin = coin;
|
||||
if (!(yield tx.verifyInputAsync(i)))
|
||||
@ -512,9 +510,12 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
coin = yield this.getCoin(prevout.hash, prevout.index);
|
||||
spent = yield this.getSpent(prevout.hash, prevout.index);
|
||||
|
||||
if (spent) {
|
||||
coin = yield this.getSpentCoin(spent, prevout);
|
||||
assert(coin);
|
||||
|
||||
if (coin) {
|
||||
if (this.options.verify && tx.height === -1) {
|
||||
input.coin = coin;
|
||||
if (!(yield tx.verifyInputAsync(i)))
|
||||
@ -2373,8 +2374,12 @@ TXDB.prototype.abandon = co(function* abandon(hash) {
|
||||
return yield this.remove(hash);
|
||||
});
|
||||
|
||||
/*
|
||||
/**
|
||||
* Balance
|
||||
* @constructor
|
||||
* @param {WalletID} wid
|
||||
* @param {String} id
|
||||
* @param {Number} account
|
||||
*/
|
||||
|
||||
function Balance(wid, id, account) {
|
||||
@ -2388,12 +2393,24 @@ function Balance(wid, id, account) {
|
||||
this.confirmed = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a balance is equal.
|
||||
* @param {Balance} balance
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
Balance.prototype.equal = function equal(balance) {
|
||||
return this.wid === balance.wid
|
||||
&& this.confirmed === balance.confirmed
|
||||
&& this.unconfirmed === balance.unconfirmed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert balance to a more json-friendly object.
|
||||
* @param {Boolean?} minimal
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
Balance.prototype.toJSON = function toJSON(minimal) {
|
||||
return {
|
||||
wid: !minimal ? this.wid : undefined,
|
||||
@ -2404,6 +2421,11 @@ Balance.prototype.toJSON = function toJSON(minimal) {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert balance to human-readable string.
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
Balance.prototype.toString = function toString() {
|
||||
return '<Balance'
|
||||
+ ' unconfirmed=' + utils.btc(this.unconfirmed)
|
||||
@ -2411,6 +2433,11 @@ Balance.prototype.toString = function toString() {
|
||||
+ '>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Inspect balance.
|
||||
* @param {String}
|
||||
*/
|
||||
|
||||
Balance.prototype.inspect = function inspect() {
|
||||
return this.toString();
|
||||
};
|
||||
@ -2418,6 +2445,8 @@ Balance.prototype.inspect = function inspect() {
|
||||
/**
|
||||
* Chain State
|
||||
* @constructor
|
||||
* @param {WalletID} wid
|
||||
* @param {String} id
|
||||
*/
|
||||
|
||||
function TXDBState(wid, id) {
|
||||
@ -2430,6 +2459,11 @@ function TXDBState(wid, id) {
|
||||
this.committed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the state.
|
||||
* @returns {TXDBState}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.clone = function clone() {
|
||||
var state = new TXDBState(this.wid, this.id);
|
||||
state.tx = this.tx;
|
||||
@ -2439,11 +2473,21 @@ TXDBState.prototype.clone = function clone() {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Commit and serialize state.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.commit = function commit() {
|
||||
this.committed = true;
|
||||
return this.toRaw();
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert state to a balance object.
|
||||
* @returns {Balance}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.toBalance = function toBalance() {
|
||||
var balance = new Balance(this.wid, this.id, -1);
|
||||
balance.unconfirmed = this.unconfirmed;
|
||||
@ -2451,6 +2495,11 @@ TXDBState.prototype.toBalance = function toBalance() {
|
||||
return balance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serialize state.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.toRaw = function toRaw(writer) {
|
||||
var p = new BufferWriter(writer);
|
||||
|
||||
@ -2465,6 +2514,13 @@ TXDBState.prototype.toRaw = function toRaw(writer) {
|
||||
return p;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @private
|
||||
* @param {Buffer} data
|
||||
* @returns {TXDBState}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.fromRaw = function fromRaw(data) {
|
||||
var p = new BufferReader(data);
|
||||
this.tx = p.readU53();
|
||||
@ -2474,10 +2530,22 @@ TXDBState.prototype.fromRaw = function fromRaw(data) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate txdb state from serialized data.
|
||||
* @param {Buffer} data
|
||||
* @returns {TXDBState}
|
||||
*/
|
||||
|
||||
TXDBState.fromRaw = function fromRaw(wid, id, data) {
|
||||
return new TXDBState(wid, id).fromRaw(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert state to a more json-friendly object.
|
||||
* @param {Boolean?} minimal
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.toJSON = function toJSON(minimal) {
|
||||
return {
|
||||
wid: !minimal ? this.wid : undefined,
|
||||
@ -2489,28 +2557,20 @@ TXDBState.prototype.toJSON = function toJSON(minimal) {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Inspect the state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
TXDBState.prototype.inspect = function inspect() {
|
||||
return this.toJSON();
|
||||
};
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function Orphan(tx, i) {
|
||||
this.tx = tx;
|
||||
this.hash = tx.hash('hex');
|
||||
this.index = i;
|
||||
}
|
||||
|
||||
function cmp(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction Details
|
||||
* @constructor
|
||||
* @param {PathInfo} info
|
||||
* @param {TXDB} txdb
|
||||
* @param {TX} tx
|
||||
*/
|
||||
|
||||
function Details(txdb, tx) {
|
||||
@ -2538,6 +2598,11 @@ function Details(txdb, tx) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize transaction details.
|
||||
* @private
|
||||
*/
|
||||
|
||||
Details.prototype.init = function init() {
|
||||
var i, input, output, member;
|
||||
|
||||
@ -2557,6 +2622,13 @@ Details.prototype.init = function init() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add necessary info to input member.
|
||||
* @param {Number} i
|
||||
* @param {Path} path
|
||||
* @param {Coin} coin
|
||||
*/
|
||||
|
||||
Details.prototype.setInput = function setInput(i, path, coin) {
|
||||
var member = this.inputs[i];
|
||||
|
||||
@ -2571,6 +2643,12 @@ Details.prototype.setInput = function setInput(i, path, coin) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add necessary info to output member.
|
||||
* @param {Number} i
|
||||
* @param {Path} path
|
||||
*/
|
||||
|
||||
Details.prototype.setOutput = function setOutput(i, path) {
|
||||
var member = this.outputs[i];
|
||||
|
||||
@ -2644,8 +2722,12 @@ DetailsMember.prototype.toJSON = function toJSON(network) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Credit
|
||||
* Credit (wrapped coin)
|
||||
* @constructor
|
||||
* @param {Coin} coin
|
||||
* @param {Boolean?} spent
|
||||
* @property {Coin} coin
|
||||
* @property {Boolean} spent
|
||||
*/
|
||||
|
||||
function Credit(coin, spent) {
|
||||
@ -2656,6 +2738,12 @@ function Credit(coin, spent) {
|
||||
this.spent = spent || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject properties from serialized data.
|
||||
* @private
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
|
||||
Credit.prototype.fromRaw = function fromRaw(data) {
|
||||
var p = BufferReader(data);
|
||||
this.coin.fromRaw(p);
|
||||
@ -2663,10 +2751,21 @@ Credit.prototype.fromRaw = function fromRaw(data) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate credit from serialized data.
|
||||
* @param {Buffer} data
|
||||
* @returns {Credit}
|
||||
*/
|
||||
|
||||
Credit.fromRaw = function fromRaw(data) {
|
||||
return new Credit().fromRaw(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Serialize credit.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
Credit.prototype.toRaw = function toRaw(writer) {
|
||||
var p = BufferWriter(writer);
|
||||
|
||||
@ -2679,16 +2778,45 @@ Credit.prototype.toRaw = function toRaw(writer) {
|
||||
return p;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject properties from tx object.
|
||||
* @private
|
||||
* @param {TX} tx
|
||||
* @param {Number} i
|
||||
* @returns {Credit}
|
||||
*/
|
||||
|
||||
Credit.prototype.fromTX = function fromTX(tx, i) {
|
||||
this.coin.fromTX(tx, i);
|
||||
this.spent = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate credit from transaction.
|
||||
* @param {TX} tx
|
||||
* @param {Number} i
|
||||
* @returns {Credit}
|
||||
*/
|
||||
|
||||
Credit.fromTX = function fromTX(tx, i) {
|
||||
return new Credit().fromTX(tx, i);
|
||||
};
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
function Orphan(tx, i) {
|
||||
this.tx = tx;
|
||||
this.hash = tx.hash('hex');
|
||||
this.index = i;
|
||||
}
|
||||
|
||||
function cmp(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose
|
||||
*/
|
||||
|
||||
@ -1616,9 +1616,8 @@ Wallet.prototype.getOutputPaths = co(function* getOutputPaths(tx) {
|
||||
* Sync address depths based on a transaction's outputs.
|
||||
* This is used for deriving new addresses when
|
||||
* a confirmed transaction is seen.
|
||||
* @param {PathInfo} info
|
||||
* @returns {Promise} - Returns Boolean
|
||||
* (true if new addresses were allocated).
|
||||
* @param {Details} details
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
Wallet.prototype.syncOutputDepth = co(function* syncOutputDepth(details) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user