SOB: designing alby oauth connector -part 2

0 sats
0 comments

πŸ“… Last week, I successfully wrapped up the OAuth implementation for the Alby OAuth connector, ensuring secure login functionality. πŸ”’ This week, I dedicated my efforts to developing the OAuth connector's core functionalities, focusing on delivering the following features:

  1. πŸš€ getInfo

  2. πŸ’Έ SendPayment

  3. 🧾 MakeInvoice

  4. πŸ”‘ KeySend

  5. Utility functions helping oauth connector

πŸš€ getInfo

I delved into retrieving vital information through the OAuth connector. This feature empowers users by allowing them to access user-specific details or obtain valuable insights from the OAuth provider. πŸ“‹πŸ”

async getInfo(): Promise<GetInfoResponse> {
    try {
      const info = await this._request((client) =>
        client.accountInformation({})
      );
      const alias = info.lightning_address
        ? info.lightning_address
        : "🐝 getalby.com";

      return {
        data: {
          alias: alias,
        },
      };
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

In this code, the getInfo function retrieves account information from the Alby API. If a lightning address (info.lightning_address) is available, it is assigned to the alias variable. Otherwise, the fallback alias "🐝 getalby.com" is used. πŸ“œπŸ

The alias serves as a means of distinguishing Alby accounts from non-Alby accounts, such as those using different lightning nodes like LNbits, Citadel, and more. This differentiation helps showcase features that are supported only by Alby accounts. The alias is also utilized to set the Alby account name. πŸ”πŸ”„πŸ‘₯

If an error occurs during the process, it is logged to the console using console.error, and the error is re-thrown to handle it further up the call stack. 🚨

πŸ’Έ SendPayment

I concentrated on implementing the payment sending capability within the OAuth connector. Users can now conveniently initiate payments by providing recipient details, payment amounts, and any necessary parameters. πŸ’³πŸ’°

async sendPayment(args: SendPaymentArgs): Promise<SendPaymentResponse> {
    const data = await this._request((client) =>
      client.sendPayment({
        invoice: args.paymentRequest,
      })
    );

    return {
      data: {
        preimage: data.payment_preimage,
        paymentHash: data.payment_hash,
        route: { total_amt: data.amount, total_fees: data.fee },
      },
    };
  }

🏦 The sendPayment function sends a payment using the Alby OAuth connector. It takes the payment request (args.paymentRequest) and sends it through the client.sendPayment method. The function returns a response object with the payment preimage, payment hash, and route details. πŸ“²πŸ’³πŸ’Ό

The response object includes the following information:

  • preimage: The payment preimage obtained from data.payment_preimage. It represents the proof of payment. πŸ–ΌοΈ

  • paymentHash: The payment hash obtained from data.payment_hash. It uniquely identifies the payment. πŸ”’πŸ’°

  • route: An object that contains details about the payment route, including the total amount (data.amount) and the total fees (data.fee). It gives an overview of the payment's financial aspects. πŸ—ΊοΈπŸ’Έ

🧾 MakeInvoice

I focused on facilitating invoice creation through the OAuth connector. Users can generate invoices effortlessly, specifying recipient information, due dates, amounts, and any additional relevant details. πŸ’ΌπŸ’΅

 async makeInvoice(args: MakeInvoiceArgs): Promise<MakeInvoiceResponse> {
    const data = await this._request((client) =>
      client.createInvoice({
        amount: parseInt(args.amount.toString()),
        description: args.memo,
      })
    );

    return {
      data: {
        paymentRequest: data.payment_request,
        rHash: data.payment_hash,
      },
    };
  }

The makeInvoice function is responsible for creating an invoice using the Alby OAuth connector. It takes in args as the input, which includes the invoice amount (args.amount) and the invoice memo/description (args.memo). πŸ’ΌπŸ’°πŸ“

Within the function, a request is made to the Alby API using this._request and the client.createInvoice method. The amount is parsed from a string to an integer using parseInt() before being passed to the API call.

After receiving the response (data), the function constructs and returns a response object. πŸ”„πŸ’ΌπŸ’³

The response object contains the following information:

  • paymentRequest: The payment request generated from data.payment_request. It represents the invoice and contains the necessary details for making a payment. πŸ’ΌπŸ’³

  • rHash: The payment hash obtained from data.payment_hash. It serves as a unique identifier for the invoice and contributes to its security. πŸ”’πŸ—οΈ

πŸ”‘ KeySend

I worked on implementing the KeySend functionality, allowing users to send payments directly to recipients without requiring a pre-existing invoice. By leveraging cryptographic keys, users can enjoy seamless and swift transactions. βš‘πŸ”πŸ’Έ

async keysend(args: KeysendArgs): Promise<SendPaymentResponse> {
    const data = await this._request((client) =>
      client.keysend({
        destination: args.pubkey,
        amount: args.amount,
        customRecords: args.customRecords,
      })
    );

    return {
      data: {
        preimage: data.payment_preimage,
        paymentHash: data.payment_hash,
        route: { total_amt: data.amount, total_fees: data.fee },
      },
    };
  }

The keysend function facilitates a keysend payment using the Alby OAuth connector. It takes args as input, which includes the recipient's public key (args.pubkey), the payment amount (args.amount), and any custom records (args.customRecords) associated with the payment. πŸŽ―πŸ”‘πŸ’°πŸ“¦πŸ”’

Within the function, a request is made to the Alby API using this._request and the client.keysend method. The recipient's public key, payment amount, and custom records are provided as parameters for the keysend transaction.

Upon receiving the response (data), the function constructs and returns a response object. πŸ”„πŸ–ΌοΈπŸ”’πŸ’°

The response object includes the following information:

  • preimage: The payment preimage obtained from data.payment_preimage. It represents the proof of payment. πŸ–ΌοΈ

  • paymentHash: The payment hash obtained from data.payment_hash. It uniquely identifies the payment. πŸ”’πŸ’°

  • route: An object containing details about the payment route, including the total amount (data.amount) and the total fees (data.fee). It provides an overview of the payment's financial aspects. πŸ—ΊοΈπŸ’Έ

Utility function to make request to Alby API

The _request function handles API requests in the Alby OAuth connector. It first obtains the Alby client using this._getAlbyClient() and checks if the user is authorized. If the _authUser object is not created, an error is thrown. βŒπŸ™…β€β™‚οΈ

The function captures the current token in oldToken for comparison. Then, it executes the provided func with the Alby client inside a try block to process the API request. If an error occurs, it is logged and re-thrown. 🚨❌

In the finally block, the function checks if there is a new token available in this._authUser.token. If a new token exists and it differs from the old token, the _updateOAuthToken function is called to update the OAuth token. πŸ”’πŸ”„πŸ”‘

Finally, the function returns the result as the API response. βœ…

  private async _request<T>(func: (client: Client) => T) {
    const client = await this._getAlbyClient();
    if (!this._authUser) {
      throw new Error("this._authUser was not created");
    }
    const oldToken = this._authUser?.token;
    let result: T;
    try {
      result = await func(client);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      const newToken = this._authUser.token;
      if (newToken && newToken !== oldToken) {
        await this._updateOAuthToken(newToken);
      }
    }
    return result;
  }

Utility to Get Alby Client

The _getAlbyClient function is responsible for obtaining the Alby client used for API requests.

  private async _getAlbyClient(): Promise<Client> {
    if (this._clientPromise) {
      return this._clientPromise;
    }

    this._clientPromise = new Promise<Client>((resolve, reject) => {
      (async () => {
        try {
          this._authUser = await this.authorize();
          resolve(new Client(this._authUser, this._getRequestOptions()));
        } catch (error) {
          reject(error);
        }
      })();
    });
    return this._clientPromise;
  }

In this code, the _getAlbyClient function retrieves the Alby client for API requests. It first checks if the _clientPromise exists and returns it if it does, ensuring that the client is only created once and reused on subsequent calls. πŸ”„πŸ”‘

If the _clientPromise does not exist, a new promise is created. Within this promise, an asynchronous function is immediately invoked. Inside this function, the function first authorizes the user by calling this.authorize() and stores the authorized user in this._authUser. πŸ”‘βœ…

Then, a new Client instance is created, passing this._authUser and this._getRequestOptions() as parameters. This initializes the Alby client with the authorized user and the necessary request options. πŸ”’πŸ“‘

If any error occurs during the authorization or client creation process, the error is passed to the promise's reject function. ❌🚫

Finally, the promise is stored in this._clientPromise, and the promise itself is returned as the Alby client. πŸ”‘πŸ“¨

B Tech CE || GSoC2021 @CircuitVerse || GSOC'2022 Mentor @CircuitVerse || SOB'2022 @getAlby || Core Team Member @CircuitVerse || Open Source Enthusiast || Building spec to make lightning transactions smart