Source: index.js

"use strict";
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 helpers_1 = require("./helpers");
const url_1 = require("url");
require('isomorphic-fetch');
const StrKey = require('stellar-base').StrKey;
exports.initialClientState = {
    addressesForChannelAccount: {},
    addressesForCounterpartyAccount: {},
    updateNumberForSequenceNumber: {},
    from: 0,
};
/**
 * The Starlight API Client object is the root object for all API interactions.
 * To interact with Starlight, a Client object must always be instantiated
 * first.
 * @class
 */
class Client {
    /**
     * @typedef Status
     * @type {object}
     * @property {boolean} IsConfigured
     * @property {boolean} IsLoggedIn
     */
    /**
     * @typedef ClientResponse
     * @template T
     * @type {object}
     * @property {string} body - The body of the response.
     * @property {boolean} ok - Whether the request was successful.
     * @property {number | undefined} status - Status code from the HTTP response (if any).
     * @property {error | undefined} error - a JavaScript error, if there was an error making the request.
     */
    /**
     * Create a Client.
     * @param {string} baseURL - The URL of the Starlight agent.
     * @param {object} clientState - The client state.
     */
    constructor(baseURL, clientState = exports.initialClientState) {
        this.baseURL = baseURL;
        this.clientState = clientState;
    }
    /**
     * Resets the client's state to an initial state.
     */
    clearState() {
        return __awaiter(this, void 0, void 0, function* () {
            this.clientState = exports.initialClientState;
        });
    }
    /**
     * Restore the client's state from a snapshot.
     * @param {object} clientState - The client state.
     */
    setState(clientState) {
        return __awaiter(this, void 0, void 0, function* () {
            this.clientState = clientState;
        });
    }
    /**
     * Configure the instance with a username, password, and horizon URL.
     *
     * @async
     * @param {object} params - The configuration parameters.
     * @param {string} params.HorizonURL - The Horizon URL (by default, https://horizon-testnet.stellar.org).
     * @param {string} params.Username - This will be the first part of your Stellar address (as in "alice*stellar.org").
     * @param {string} params.Password - This will also be used to encrypt the instance's private key in storage.
     *
     * @returns {Promise<ClientResponse<Status>>}
     */
    configInit(params) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/config-init', params);
        });
    }
    /**
     * Edit the instance's configuration.
     * @param {object} params - The configuration parameters.
     * @param {string} [params.HorizonURL] - A new Horizon URL.
     * @param {string} [params.Password] - A new password.
     * @param {string} [params.OldPassword] - The old password, which must be provided if a new password is provided.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    configEdit(params) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/config-edit', params);
        });
    }
    /**
     * Attempt to open a channel with a specific counterparty.
     * @param {string} counterpartyAddress - The Stellar address of your counterparty (e.g., "alice*stellar.org").
     * @param {number} initialDeposit - The amount (in stroops) you will initially deposit into the channel.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    createChannel(counterpartyAddress, initialDeposit) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-create-channel', {
                GuestAddr: counterpartyAddress,
                HostAmount: initialDeposit,
            });
        });
    }
    /**
     * Cooperatively close a channel.
     * @param {string} channelID - The channel ID.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    close(channelID) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-command', {
                ChannelID: channelID,
                Command: {
                    Name: 'CloseChannel',
                },
            });
        });
    }
    /**
     * Cancel a proposed channel that your counterparty has not yet accepted.
     * @param {string} channelID - The channel ID.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    cancel(channelID) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-command', {
                ChannelID: channelID,
                Command: {
                    Name: 'CleanUp',
                },
            });
        });
    }
    /**
     * Attempt to force close a channel.
     * @param {string} channelID - The channel ID.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    forceClose(channelID) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-command', {
                ChannelID: channelID,
                Command: {
                    Name: 'ForceClose',
                },
            });
        });
    }
    /**
     * Make a payment over a channel.
     * @param {string} channelID - The channel ID.
     * @param {number} amount - The amount (in stroops) to be paid.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    channelPay(channelID, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-command', {
                ChannelID: channelID,
                Command: {
                    Name: 'ChannelPay',
                    Amount: amount,
                },
            });
        });
    }
    /**
     * Make a payment on the public network.
     * @param {string} channelID - The channel ID.
     * @param {number} amount - The amount (in stroops) to be paid.
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    walletPay(recipient, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-wallet-pay', {
                Dest: recipient,
                Amount: amount,
            });
        });
    }
    /**
     * Add more money to a channel you created.
     * @param {string} channelID - The channel ID.
     * @param {number} amount - The amount (in stroops) to be deposited.
     * @returns {Promise<ClientResponse<string>>}
     */
    deposit(channelID, amount) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/do-command', {
                ChannelID: channelID,
                Command: {
                    Name: 'TopUp',
                    Amount: amount,
                },
            });
        });
    }
    /**
     * Authenticate with a Starlight instance.
     * This also decrypts the instance's private key,
     * allowing it to sign transactions and accept channels.
     * @param {string} username
     * @param {number} password
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    login(username, password) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/login', {
                username,
                password,
            });
        });
    }
    /**
     * Log out of a Starlight instance.
     * @param {string} username
     * @param {number} password
     *
     * @returns {Promise<ClientResponse<string>>}
     */
    logout() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/logout');
        });
    }
    /**
     * Find the account ID (e.g., "G...") corresponding to a Stellar address (e.g., "alice*stellar.org").
     * @param {string} address
     *
     * @returns {Promise<ClientResponse<Status>>} accountID
     */
    findAccount(address) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/find-account', {
                stellar_addr: address,
            });
        });
    }
    /**
     * Get the current status of the instance (whether the instance is configured, and if so, whether the user is logged in).
     *
     * @returns {Promise<Status | undefined>}
     */
    getStatus() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.request('/api/status');
        });
    }
    /**
     * Subscribe to updates from the Starlight instance.
     * The first time this is called, the handler will be called with all updates in the instance's history.
     *
     * @param {function} updateHandler - A handler function that updates will be passed to.
     */
    subscribe(updateHandler) {
        this.updateHandler = updateHandler;
        this.loop();
    }
    /**
     * Stop subscribing to updates from the Starlight instance.
     */
    unsubscribe() {
        this.updateHandler = undefined;
    }
    /**
     * This method is only public so it can be used in tests.
     * @ignore
     */
    handleFetchResponse(rawResponse) {
        const handler = this.updateHandler;
        const response = this.responseHandler
            ? this.responseHandler(rawResponse)
            : rawResponse;
        if (handler && response.ok && response.body.length >= 1) {
            response.body.forEach((event) => {
                if (event.Type === 'channel') {
                    const channel = event.Channel;
                    const CounterpartyAccount = channel.Role === 'Host' ? channel.GuestAcct : channel.HostAcct;
                    this.clientState = Object.assign({}, this.clientState, { addressesForCounterpartyAccount: Object.assign({}, this.clientState.addressesForCounterpartyAccount, { [CounterpartyAccount]: channel.CounterpartyAddress }), addressesForChannelAccount: Object.assign({}, this.clientState.addressesForChannelAccount, { [channel.EscrowAcct]: channel.CounterpartyAddress }) });
                }
                this.clientState = Object.assign({}, this.clientState, { from: event.UpdateNum + 1 });
                const update = this.eventToUpdate(event);
                if (update) {
                    handler(update);
                }
            });
        }
        return response.ok;
    }
    fetch() {
        return __awaiter(this, void 0, void 0, function* () {
            const From = this.clientState.from;
            const response = yield this.request('/api/updates', { From });
            return this.handleFetchResponse(response);
        });
    }
    loop() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.updateHandler) {
                return;
            }
            const ok = yield this.fetch();
            if (!ok) {
                yield this.backoff(10000);
            }
            this.loop();
        });
    }
    backoff(ms) {
        return __awaiter(this, void 0, void 0, function* () {
            yield new Promise(resolve => setTimeout(resolve, ms));
        });
    }
    eventToUpdate(event) {
        const clientState = this.clientState;
        switch (event.Type) {
            case 'account': {
                const op = helpers_1.getWalletOp(event, this.clientState.addressesForCounterpartyAccount);
                if (event.InputTx) {
                    // check if this is from a channel account
                    const counterpartyAddress = this.clientState
                        .addressesForChannelAccount[StrKey.encodeEd25519PublicKey(event.InputTx.Env.Tx.SourceAccount.Ed25519)];
                    if (counterpartyAddress !== undefined) {
                        // it's from a channel
                        // activity is handled elsewhere
                        return {
                            Type: 'accountUpdate',
                            Account: event.Account,
                            UpdateLedgerTime: event.UpdateLedgerTime,
                            UpdateNum: event.UpdateNum,
                            ClientState: clientState,
                        };
                    }
                }
                return {
                    Type: 'walletActivityUpdate',
                    Account: event.Account,
                    UpdateLedgerTime: event.UpdateLedgerTime,
                    UpdateNum: event.UpdateNum,
                    WalletOp: op,
                    ClientState: clientState,
                };
            }
            case 'channel':
                // TODO: remove channel account from this mapping when channel is closed
                const ops = helpers_1.getChannelOps(event);
                if (ops.length === 0) {
                    return {
                        Type: 'channelUpdate',
                        Account: event.Account,
                        Channel: event.Channel,
                        UpdateLedgerTime: event.UpdateLedgerTime,
                        UpdateNum: event.UpdateNum,
                        ClientState: clientState,
                    };
                }
                else {
                    return {
                        Type: 'channelActivityUpdate',
                        Account: event.Account,
                        Channel: event.Channel,
                        UpdateLedgerTime: event.UpdateLedgerTime,
                        UpdateNum: event.UpdateNum,
                        ChannelOp: ops[0],
                        ClientState: clientState,
                    };
                }
            case 'config':
                return {
                    Type: 'configUpdate',
                    Config: event.Config,
                    Account: event.Account,
                    UpdateLedgerTime: event.UpdateLedgerTime,
                    UpdateNum: event.UpdateNum,
                    ClientState: clientState,
                };
            case 'init':
                return {
                    Type: 'initUpdate',
                    Config: event.Config,
                    Account: event.Account,
                    UpdateLedgerTime: event.UpdateLedgerTime,
                    UpdateNum: event.UpdateNum,
                    ClientState: clientState,
                };
            case 'tx_failed':
            case 'tx_success':
                return {
                    Type: event.Type === 'tx_failed' ? 'txFailureUpdate' : 'txSuccessUpdate',
                    Tx: event.InputTx,
                    Account: event.Account,
                    UpdateLedgerTime: event.UpdateLedgerTime,
                    UpdateNum: event.UpdateNum,
                    ClientState: clientState,
                };
        }
    }
    request(path = '', data = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            let urlString;
            if (this.baseURL) {
                const url = new url_1.URL(path, this.baseURL);
                urlString = url.href;
            }
            else {
                urlString = path;
            }
            const rawResponse = yield this.post(urlString, data);
            const response = this.responseHandler
                ? this.responseHandler(rawResponse)
                : rawResponse;
            return response;
        });
    }
    post(url = ``, data = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            let response;
            try {
                // Default options marked with *
                response = yield fetch(url, {
                    method: 'POST',
                    cache: 'no-cache',
                    credentials: 'same-origin',
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8',
                        Cookie: this.cookie,
                    },
                    body: JSON.stringify(data),
                });
                const cookie = response.headers.get('set-cookie');
                if (cookie) {
                    this.cookie = cookie;
                }
                if ((response.headers.get('content-type') || '').includes('json')) {
                    return {
                        body: yield response.json(),
                        ok: response.ok,
                        status: response.status,
                    };
                }
                else {
                    return { body: '', ok: response.ok, status: response.status };
                }
            }
            catch (error) {
                return { body: '', ok: false, error };
            }
        });
    }
}
exports.Client = Client;