Frontend signing + backend sponsorship

How to integrate Shinami Gas Station transaction sponsorship with signing from a connected browser wallet (including Aptos Connect) or a single-app Keyless wallet

Overview

Shinami's Gas Station only supports integration with your app's backend (no CORS support) for security reasons. This limits exposure of your Gas Station access keys. If these keys are leaked, bad actors have the ability to sponsor transactions from your fund until it has been drained or you disable the key in our dashboard.

This guide is meant to help you integrate a connected browser wallet (including Aptos Connect) or single-app, embedded Aptos Keyless wallet with Shinami Gas Station transaction sponsorship. We show possible integration flow diagrams with links to associated sample code. Currently, the full sample app we built just works for a connected browser wallet, which includes Aptos Connect. However, in the "Build and sponsor on the BE, sign and submit on the FE" example, we link to a matching frontend function using a Keyless wallet that lives in the Appendix. It comes from an update to the app we're working on.

If you have not already set up a Shinami Gas Station Fund and Access Key, and sent a successful sponsorship request, see our Gas Station tutorial for guidance.



Examples

Sample app overview

We've created a sample app that uses the Aptos Labs wallet-adapter-react library and the Shinami TypeScript SDK. It integrates with Petra, Pontem, and Aptos Connect wallets out of the box. We've included it in our shinamicorp/shinami-examples repo here. It's not meant as a starter template for a production app (as an example its API endpoints have no authentication mechanism). Instead, it's meant to show you a very simple working example so you can understand the core concepts involved. It includes a README.md file to help you get it up and running quickly if you want to see a working example.



Build and sign on the FE, sponsor and submit on the BE

This example shows how to build and sign a transaction on the frontend, then send it to your backed for Shinami Gas Station sponsorship and submission to an Aptos node. Signing is done with a connected browser wallet (including Aptos Connect). For an example of signing with an embedded, single-app Keyless wallet see the next section.

Image

Overview of steps

The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on GitHub. The key functions in the sample app are connectedWalletTxFEBuildBESubmit on the frontend and sponsorAndSubmitTx on the backend.

  1. Frontend: Build a feePayer transaction (sample code).
  2. Frontend: Obtain the sender's signature over the transaction (sample code).
  3. Frontend: Send a request to your app's backend to sponsor and submit the transaction. This request will include serialized versions of the transaction and the sender's AccountAuthenticator (sample code).
  4. Backend: Send a gas_sponsorAndSubmitSignedTransaction request to Shinami's Gas Station (with the de-serialized transaction and sender AccountAuthenticator) (sample code).
  5. Shinami: Shinami will sponsor the transaction and then submit it with the sender and feePayer signatures, returning the PendingTransactionResponse to your backend if successful.
  6. Backend: Handle the response as needed.
  7. Backend: Send a response to the frontend (sample code).
  8. Frontend: Handle the response (this is the response to the request in Step 3). In our sample app, we poll an Aptos Full node until it has a record of the transaction and then print the user's message that was included in the transaction (sample code).


Build and sponsor on the BE, sign and submit on the FE

This example shows how to build and sponsor a transaction on your backend, then sign and submit it on your frontend. Signing is done with a connected browser wallet (including Aptos Connect). We also discuss how to sign with an embedded, single-app Keyless wallet.

Image

Overview of steps

The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on GitHub. The key functions in the sample app are connectedWalletTxBEBuildFESubmit on the FE and buildAndSponsorTx on the BE. For a single-app Keyless wallet version of the frontend function, see the Appendix.

  1. Frontend: Send a request to your app's backend to build and sponsor a transaction. Include any data from the FE needed to build the transaction. In our case, that's the sender's address and the message the user submitted in the form on the page (sample code).
  2. Backend: Build a feePayer transaction (sample code).
  3. Backend: Make a gas_sponsorTransaction request to Shinami's Gas Station to sponsor the transaction (sample code).
  4. Backend or Frontend: Set the transaction's feePayerAddress to the feePayer address value returned from the Gas Station sponsorship request (our TypeScript SDK sets this for you automatically and so does not explicitly return it to you). The sender can sign the transaction with either the actual feePayer address or the special 0x0 address assigned when you create a feePayer transaction. However, you must ensure the actual feePayer's address is set on the transaction before you submit it to the Aptos blockchain. If not, you'll get an INVALID_SIGNATURE error because the feePayer's signature was over a transaction with the feePayer address but the submitted transaction still has the 0x0 address.
  5. Backend: Return the serialized transaction, feePayer AccountAuthenticator, and (optionally) feePayer's address to the frontend (sample code).
  6. Frontend: Deserialize the transaction and obtain the sender's signature over the transaction (sample code).
  7. Frontend: Submit the transaction, along with the sender and feePayer signatures, to an Aptos Full node (sample code).
  8. Frontend: Handle the response (a PendingTransactionResponse if successful). In our sample app, we poll an Aptos Full node until it has a record of the transaction and then print the user's message that was included in the transaction (sample code).

Other flows in our sample app


Appendix

Signing with an embedded Keyless wallet tied to your app

Below is the single-app, embedded Keyless wallet equivalent of the connectedWalletTxBEBuildFESubmit in the "Build and sponsor on the BE, sign and submit on the FE" example above. The getLocalKeylessAccount() is a function that checks for a user's KeylessAccount instance in local browser storage and returns it if found. It comes from the Aptos Keyless Integration Guide, which you should follow first if you aren't familiar with integrating Keyless into your app.

This code is not meant as a production example. It's just meant to show the basics of signing with an embedded, single-app Keyless wallet.

const keylessTxBEBuildFESubmit = async (message: string): Promise<PendingTransactionResponse | undefined> => {
  // Step 1: check for a Keyless Account in local storage, meaning the user has 
  //         successfully logged in with Google or Apple.
  const keylessAccount = getLocalKeylessAccount();
  
  if (keylessAccount) {
    // Step 2: Request a transaction and sponsorship from the BE
    const sponsorshipResp = await axios.post('/buildAndSponsorTx', {
      message,
      sender: keylessAccount?.accountAddress.toString()
    });
    
    // Step 3: Obtain the sender signature over the transaction after deserializing it
    const simpleTx = SimpleTransaction.deserialize(new Deserializer(Hex.fromHexString(sponsorshipResp.data.simpleTx).toUint8Array()));
    const senderSig = aptosClient.sign({ signer: keylessAccount, transaction: simpleTx });

    // Step 3: Submit the transaction along with both signatures. Return the response to the caller
    const sponsorSig = AccountAuthenticator.deserialize(new Deserializer(Hex.fromHexString(sponsorshipResp.data.sponsorAuthenticator).toUint8Array()));
    return await aptosClient.transaction.submit.simple({
      transaction: simpleTx,
      senderAuthenticator: senderSig,
      feePayerAuthenticator: sponsorSig,
    });
  } else {
    console.log("No pre-existing Keyless account found :(");
  }
  return undefined;
}

Common issues

INVALID_SIGNATURE error when submitting the transaction

  • All cases
    • Make sure the transaction you're submitting has been updated to use the feePayer's address (instead of the 0x0 that gets set when a feePayer transaction is built).
  • Aptos Connect
    • One cause is due to stale data. Sign out of your Aptos Connect account and sign in again. Then, try to run the flow.

Serializing and deserializing

Here are examples of serializing and deserializing key data types you'll pass between your frontend and backend. Our sample app code does this in context, but they are presented here for quick viewing.

Use a separate Serializer and Deserializer for each piece of data you are working it. In the examples below, though, we don't use a Serializer because we take advantage of the built in ability of the types to serialize into string representation of a BCS-serialized Hex instance.

Serialize on one end (BE or FE)

// AccountAuthenticator
const serializedAccountAuthenticator = authenticator.bcsToHex().toString();
// AccountAddress
const serializedAccountAddress = accountAddress.bcsToHex().toString();
// SimpleTransaction
const serializedSimpleTransaction = simpleTx.bcsToHex().toString();

Deserialize on the other

import { 
  SimpleTransaction, Deserializer, AccountAuthenticator, Hex, AccountAddress
} from "@aptos-labs/ts-sdk"; 


AccountAuthenticator.deserialize(new Deserializer(
  Hex.fromHexString(serializedAccountAuthenticator).toUint8Array()));

AccountAddress.deserialize(new Deserializer(
  Hex.fromHexString(serializedAccountAddress).toUint8Array()));

SimpleTransaction.deserialize(new Deserializer(
  Hex.fromHexString(serializedSimpleTransaction).toUint8Array()));