UNPKG

@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
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