@bajetech/digitalbits-wallet-sdk
Version:
A library to make it easier to write wallets that interact with the DigitalBits blockchain
573 lines • 26.4 kB
JavaScript
import { __assign, __awaiter, __generator, __spreadArray } from "tslib";
import debounce from "lodash/debounce";
import DigitalBitsSdk, { Account as DigitalBitsAccount, Asset, Keypair, Operation, Server, StrKey, TransactionBuilder, } from "xdb-digitalbits-sdk";
import { NATIVE_ASSET_IDENTIFIER } from "../constants/digitalbits";
import { getDigitalBitsSdkAsset } from "./index";
import { makeDisplayableBalances } from "./makeDisplayableBalances";
import { makeDisplayableOffers } from "./makeDisplayableOffers";
import { makeDisplayablePayments } from "./makeDisplayablePayments";
import { makeDisplayableTrades } from "./makeDisplayableTrades";
function isAccount(obj) {
return obj && obj.publicKey !== undefined;
}
var DataProvider = /** @class */ (function () {
function DataProvider(params) {
var accountKey = isAccount(params.accountOrKey)
? params.accountOrKey.publicKey
: params.accountOrKey;
if (!accountKey) {
throw new Error("No account key provided.");
}
if (!params.serverUrl) {
throw new Error("No server url provided.");
}
if (!params.networkPassphrase) {
throw new Error("No network passphrase provided.");
}
// make sure the account key is a real account
try {
Keypair.fromPublicKey(accountKey);
}
catch (e) {
throw new Error("The provided key was not valid: ".concat(accountKey));
}
var metadata = params.metadata || {};
this.callbacks = {};
this.errorHandlers = {};
this.effectStreamEnder = undefined;
this.networkPassphrase = params.networkPassphrase;
this.serverUrl = params.serverUrl;
this.server = new Server(this.serverUrl, metadata);
this.accountKey = accountKey;
this._watcherTimeouts = {};
}
/**
* Return true if the key is valid. (It doesn't comment on whether the
* key is a funded account.)
*/
DataProvider.prototype.isValidKey = function () {
return StrKey.isValidEd25519PublicKey(this.accountKey);
};
/**
* Return the current key.
*/
DataProvider.prototype.getAccountKey = function () {
return this.accountKey;
};
/**
* Return the server object, in case the consumer wants to call an
* unsupported function.
*/
DataProvider.prototype.getServer = function () {
return this.server;
};
/**
* Check if the current account is funded or not.
*/
DataProvider.prototype.isAccountFunded = function () {
return __awaiter(this, void 0, void 0, function () {
var e_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.fetchAccountDetails()];
case 1:
_a.sent();
return [2 /*return*/, true];
case 2:
e_1 = _a.sent();
return [2 /*return*/, !e_1.isUnfunded];
case 3: return [2 /*return*/];
}
});
});
};
/**
* Fetch outstanding offers.
*/
DataProvider.prototype.fetchOpenOffers = function (params) {
if (params === void 0) { params = {}; }
return __awaiter(this, void 0, void 0, function () {
var offers;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.server
.offers()
.forAccount(this.accountKey)
.limit(params.limit || 10)
.order(params.order || "desc")
.cursor(params.cursor || "")
.call()];
case 1:
offers = _a.sent();
return [2 /*return*/, this._processOpenOffers(offers)];
}
});
});
};
/**
* Fetch recent trades.
*/
DataProvider.prototype.fetchTrades = function (params) {
if (params === void 0) { params = {}; }
return __awaiter(this, void 0, void 0, function () {
var trades;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.server
.trades()
.forAccount(this.accountKey)
.limit(params.limit || 10)
.order(params.order || "desc")
.cursor(params.cursor || "")
.call()];
case 1:
trades = _a.sent();
return [2 /*return*/, this._processTrades(trades)];
}
});
});
};
/**
* Fetch payments (also includes path payments account creation).
*/
DataProvider.prototype.fetchPayments = function (params) {
if (params === void 0) { params = {}; }
return __awaiter(this, void 0, void 0, function () {
var payments;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.server
.payments()
.forAccount(this.accountKey)
.limit(params.limit || 10)
.order(params.order || "desc")
.cursor(params.cursor || "")
.join("transactions")
.call()];
case 1:
payments = _a.sent();
return [2 /*return*/, this._processPayments(payments)];
}
});
});
};
/**
* Fetch account details (balances, signers, etc.).
*/
DataProvider.prototype.fetchAccountDetails = function () {
return __awaiter(this, void 0, void 0, function () {
var accountSummary, balances, sponsor, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.server
.accounts()
.accountId(this.accountKey)
.call()];
case 1:
accountSummary = _a.sent();
balances = makeDisplayableBalances(accountSummary);
sponsor = accountSummary.sponsor
? { sponsor: accountSummary.sponsor }
: {};
return [2 /*return*/, __assign(__assign({}, sponsor), { id: accountSummary.id, subentryCount: accountSummary.subentry_count, sponsoredCount: accountSummary.num_sponsored, sponsoringCount: accountSummary.num_sponsoring, inflationDestination: accountSummary.inflation_destination, thresholds: accountSummary.thresholds, signers: accountSummary.signers, flags: accountSummary.flags, sequenceNumber: accountSummary.sequence, balances: balances })];
case 2:
err_1 = _a.sent();
err_1.isUnfunded = err_1.response && err_1.response.status === 404;
throw err_1;
case 3: return [2 /*return*/];
}
});
});
};
/**
* Fetch account details, then re-fetch whenever the details update.
* If the account doesn't exist yet, it will re-check it every 2 seconds.
* Returns a function you can execute to stop the watcher.
*/
DataProvider.prototype.watchAccountDetails = function (params) {
var _this = this;
var onMessage = params.onMessage, onError = params.onError;
this.fetchAccountDetails()
// if the account is funded, watch for effects.
.then(function (res) {
onMessage(res);
_this.callbacks.accountDetails = debounce(function () {
_this.fetchAccountDetails().then(onMessage).catch(onError);
}, 2000);
_this.errorHandlers.accountDetails = onError;
_this._startEffectWatcher().catch(function (err) {
onError(err);
});
})
// otherwise, if it's a 404, try again in a bit.
.catch(function (err) {
if (err.isUnfunded) {
_this._watcherTimeouts.watchAccountDetails = setTimeout(function () {
_this.watchAccountDetails(params);
}, 2000);
}
onError(err);
});
return {
refresh: function () {
_this.stopWatchAccountDetails();
_this.watchAccountDetails(params);
},
stop: function () {
_this.stopWatchAccountDetails();
},
};
};
/**
* Fetch payments, then re-fetch whenever the details update.
* Returns a function you can execute to stop the watcher.
*/
DataProvider.prototype.watchPayments = function (params) {
var _this = this;
var onMessage = params.onMessage, onError = params.onError;
var getNextPayments;
this.fetchPayments()
// if the account is funded, watch for effects.
.then(function (res) {
// for the first page load, "prev" is the people we want to get next!
getNextPayments = res.prev;
// onMessage each payment separately
res.records.forEach(onMessage);
_this.callbacks.payments = debounce(function () {
getNextPayments()
.then(function (nextRes) {
// afterwards, "next" will be the next person!
getNextPayments = nextRes.next;
// get new things
if (nextRes.records.length) {
nextRes.records.forEach(onMessage);
}
})
.catch(onError);
}, 2000);
_this.errorHandlers.payments = onError;
_this._startEffectWatcher().catch(function (err) {
onError(err);
});
})
// otherwise, if it's a 404, try again in a bit.
.catch(function (err) {
if (err.isUnfunded) {
_this._watcherTimeouts.watchPayments = setTimeout(function () {
_this.watchPayments(params);
}, 2000);
}
onError(err);
});
return {
refresh: function () {
_this.stopWatchPayments();
_this.watchPayments(params);
},
stop: function () {
_this.stopWatchPayments();
},
};
};
/**
* Given a destination key, return a transaction that removes all trustlines
* and offers on the tracked account and merges the account into a given one.
*
* @throws Throws if the account has balances.
* @throws Throws if the destination account is invalid.
*/
DataProvider.prototype.getStripAndMergeAccountTransaction = function (destinationKey) {
return __awaiter(this, void 0, void 0, function () {
var destinationProvider, account, e_2, hasNonZeroBalance, offers, additionalOffers, next, res, e_3, accountObject, fee, feeStats, e_4, transaction, _a, _b;
var _c;
var _this = this;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
// make sure the destination is a funded account
if (!StrKey.isValidEd25519PublicKey(destinationKey)) {
throw new Error("The destination is not a valid DigitalBits address.");
}
try {
destinationProvider = new DataProvider({
serverUrl: this.serverUrl,
accountOrKey: destinationKey,
networkPassphrase: this.networkPassphrase,
});
destinationProvider.fetchAccountDetails();
}
catch (e) {
if (e.isUnfunded) {
throw new Error("The destination account is not funded yet.");
}
throw new Error("Couldn't fetch the destination account, error: ".concat(e.toString()));
}
_d.label = 1;
case 1:
_d.trys.push([1, 3, , 4]);
return [4 /*yield*/, this.fetchAccountDetails()];
case 2:
account = _d.sent();
return [3 /*break*/, 4];
case 3:
e_2 = _d.sent();
throw new Error("Couldn't fetch account details, error: ".concat(e_2.toString()));
case 4:
hasNonZeroBalance = Object.keys(account.balances).reduce(function (memo, identifier) {
var balance = account.balances[identifier];
if (identifier !== NATIVE_ASSET_IDENTIFIER && (balance === null || balance === void 0 ? void 0 : balance.total.gt(0))) {
return true;
}
return memo;
}, false);
if (hasNonZeroBalance) {
throw new Error("This account can't be closed until all non-XDB balances are 0.");
}
offers = [];
_d.label = 5;
case 5:
_d.trys.push([5, 9, , 10]);
additionalOffers = void 0;
next = function () {
return _this.server
.offers()
.forAccount(_this.accountKey)
.limit(25)
.order("desc")
.call();
};
_d.label = 6;
case 6:
if (!(additionalOffers === undefined || additionalOffers.length)) return [3 /*break*/, 8];
return [4 /*yield*/, next()];
case 7:
res = _d.sent();
additionalOffers = res.records;
next = res.next;
offers = __spreadArray(__spreadArray([], offers, true), additionalOffers, true);
return [3 /*break*/, 6];
case 8: return [3 /*break*/, 10];
case 9:
e_3 = _d.sent();
throw new Error("Couldn't fetch open offers, error: ".concat(e_3.stack));
case 10:
accountObject = new DigitalBitsAccount(this.accountKey, account.sequenceNumber);
fee = DigitalBitsSdk.BASE_FEE;
_d.label = 11;
case 11:
_d.trys.push([11, 13, , 14]);
return [4 /*yield*/, this.server.feeStats()];
case 12:
feeStats = _d.sent();
fee = feeStats.max_fee.p70;
return [3 /*break*/, 14];
case 13:
e_4 = _d.sent();
return [3 /*break*/, 14];
case 14:
_a = TransactionBuilder.bind;
_b = [void 0, accountObject];
_c = {
fee: fee,
networkPassphrase: this.networkPassphrase
};
return [4 /*yield*/, this.server.fetchTimebounds(10 * 60 * 1000)];
case 15:
transaction = new (_a.apply(TransactionBuilder, _b.concat([(_c.timebounds = _d.sent(),
_c)])))();
// strip offers
offers.forEach(function (offer) {
var seller = offer.seller, selling = offer.selling, buying = offer.buying, id = offer.id;
var operation;
// check if we're the seller
if (seller === _this.accountKey) {
operation = Operation.manageSellOffer({
selling: selling.asset_code && selling.asset_issuer
? new Asset(selling.asset_code, selling.asset_issuer)
: Asset.native(),
buying: buying.asset_code && buying.asset_issuer
? new Asset(buying.asset_code, buying.asset_issuer)
: Asset.native(),
amount: "0",
price: "0",
offerId: id,
});
}
else {
operation = Operation.manageBuyOffer({
selling: selling.asset_code && selling.asset_issuer
? new Asset(selling.asset_code, selling.asset_issuer)
: Asset.native(),
buying: buying.asset_code && buying.asset_issuer
? new Asset(buying.asset_code, buying.asset_issuer)
: Asset.native(),
buyAmount: "0",
price: "0",
offerId: id,
});
}
transaction.addOperation(operation);
});
// strip trustlines
Object.keys(account.balances).forEach(function (identifier) {
if (identifier === NATIVE_ASSET_IDENTIFIER) {
return;
}
var balance = account.balances[identifier];
transaction.addOperation(Operation.changeTrust({
asset: getDigitalBitsSdkAsset(balance.token),
limit: "0",
}));
});
transaction.addOperation(Operation.accountMerge({
destination: destinationKey,
}));
return [2 /*return*/, transaction.build()];
}
});
});
};
/**
* Stop acount details watcher.
*/
DataProvider.prototype.stopWatchAccountDetails = function () {
if (this._watcherTimeouts.watchAccountDetails) {
clearTimeout(this._watcherTimeouts.watchAccountDetails);
}
if (this.effectStreamEnder) {
this.effectStreamEnder();
this.effectStreamEnder = undefined;
}
delete this.callbacks.accountDetails;
delete this.errorHandlers.accountDetails;
};
/**
* Stop payments watcher.
*/
DataProvider.prototype.stopWatchPayments = function () {
if (this._watcherTimeouts.watchPayments) {
clearTimeout(this._watcherTimeouts.watchPayments);
}
if (this.effectStreamEnder) {
this.effectStreamEnder();
this.effectStreamEnder = undefined;
}
delete this.callbacks.payments;
delete this.errorHandlers.payments;
};
DataProvider.prototype._processOpenOffers = function (offers) {
return __awaiter(this, void 0, void 0, function () {
var tradeRequests, tradeResponses;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
tradeRequests = offers.records.map(function (_a) {
var id = _a.id;
return _this.server.trades().forOffer("".concat(id)).call();
});
return [4 /*yield*/, Promise.all(tradeRequests)];
case 1:
tradeResponses = _a.sent();
return [2 /*return*/, {
next: function () { return offers.next().then(function (res) { return _this._processOpenOffers(res); }); },
prev: function () { return offers.prev().then(function (res) { return _this._processOpenOffers(res); }); },
records: makeDisplayableOffers({ publicKey: this.accountKey }, {
offers: offers.records,
tradeResponses: tradeResponses.map(function (res) {
return res.records;
}),
}),
}];
}
});
});
};
DataProvider.prototype._processTrades = function (trades) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
return [2 /*return*/, {
next: function () { return trades.next().then(function (res) { return _this._processTrades(res); }); },
prev: function () { return trades.prev().then(function (res) { return _this._processTrades(res); }); },
records: makeDisplayableTrades({ publicKey: this.accountKey }, trades.records),
}];
});
});
};
DataProvider.prototype._processPayments = function (payments) {
return __awaiter(this, void 0, void 0, function () {
var _a;
var _this = this;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_a = {
next: function () { return payments.next().then(function (res) { return _this._processPayments(res); }); },
prev: function () { return payments.prev().then(function (res) { return _this._processPayments(res); }); }
};
return [4 /*yield*/, makeDisplayablePayments({ publicKey: this.accountKey }, payments.records)];
case 1: return [2 /*return*/, (_a.records = _b.sent(),
_a)];
}
});
});
};
// Account details and payments use the same stream watcher
DataProvider.prototype._startEffectWatcher = function () {
var _a;
return __awaiter(this, void 0, void 0, function () {
var recentEffect, cursor;
var _this = this;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (this.effectStreamEnder) {
return [2 /*return*/, Promise.resolve({})];
}
return [4 /*yield*/, this.server
.effects()
.forAccount(this.accountKey)
.limit(1)
.order("desc")
.call()];
case 1:
recentEffect = _b.sent();
cursor = (_a = recentEffect.records[0]) === null || _a === void 0 ? void 0 : _a.paging_token;
this.effectStreamEnder = this.server
.effects()
.forAccount(this.accountKey)
.cursor(cursor || "")
.stream({
onmessage: function () {
// run all callbacks
var callbacks = Object.values(_this.callbacks).filter(function (callback) { return !!callback; });
if (callbacks.length) {
callbacks.forEach(function (callback) {
callback();
});
}
},
onerror: function (e) {
// run error handlers
var errorHandlers = Object.values(_this.errorHandlers).filter(function (errorHandler) { return !!errorHandler; });
if (errorHandlers.length) {
errorHandlers.forEach(function (errorHandler) {
errorHandler(e);
});
}
},
});
return [2 /*return*/, Promise.resolve({})];
}
});
});
};
return DataProvider;
}());
export { DataProvider };
//# sourceMappingURL=DataProvider.js.map