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.
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.
In the examples below, we also link to another app that uses an embedded, single-app Keyless account for signing. It's a working example in the keyless_only
branch of our shinamicorp/shinami-examples
but we're still finalizing some of the behavior un-related to Keyless signing. It's also not meant as a starter template for a production app.
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) or a 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 connectedWalletTxFEBuildBESubmit
on the frontend and sponsorAndSubmitTx
on the backend. For a single-app Keyless wallet version of the code, see the Appendix. The flow is the same for both, and takes place after OpenID provider sign-in once you have a valid KeylessAccount
for the user.
- Frontend: Build a feePayer transaction (sample code).
- Frontend: Obtain the sender's signature over the transaction (sample code).
- 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).
- Backend: Send a
gas_sponsorAndSubmitSignedTransaction
request to Shinami's Gas Station (with the de-serialized transaction and sender AccountAuthenticator) (sample code). - Shinami: Shinami will sponsor the transaction and then submit it with the sender and feePayer signatures, returning the
PendingTransactionResponse
to your backend if successful. - Backend: Handle the response as needed.
- Backend: Send a response to the frontend (sample code).
- 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) or a 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 code, see the Appendix. The flow is the same for both, and takes place after OpenID provider sign-in once you have a valid KeylessAccount
for the user.
- 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).
- Backend: Build a feePayer transaction (sample code).
- Backend: Make a
gas_sponsorTransaction
request to Shinami's Gas Station to sponsor the transaction (sample code). - 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 special0x0
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 anINVALID_SIGNATURE
error because the feePayer's signature was over a transaction with the feePayer address but the submitted transaction still has the0x0
address. - Backend: Return the serialized transaction, feePayer AccountAuthenticator, and (optionally) feePayer's address to the frontend (sample code).
- Frontend: Deserialize the transaction and obtain the sender's signature over the transaction (sample code).
- Frontend: Submit the transaction, along with the sender and (deserialized) feePayer signatures, to an Aptos Full node (sample code).
- 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
- Build on the FE, sponsor on the BE, submit on the FE.
- Build and sponsor on the BE, sign on the FE, submit on the BE.
- Embedded Shinami Invisible Wallet: Build, sponsor, and sign on the BE.
Appendix
Embedded Keyless wallet: BE build and sponsor, FE sign and submit
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. It assumes that you've already successfully logged the user in via their Social sign-in provider and created a KeylessAccount for them that can sign on behalf of their Keyless wallet.
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 from a sample app we're working on and is not meant as a production example. It's just meant to show the basics of signing with an embedded, single-app Keyless wallet.
The function shown below is here . The '/buildAndSponsorTx'
endpoint is the same as with the connected wallet example, and is here.
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;
}
Embedded Keyless wallet: FE build and sign, BE sponsor and submit
Below is the single-app, embedded Keyless wallet equivalent of the connectedWalletTxFEBuildBESubmit
in the "Build and sign on the FE, sponsor and submit on the BE" example above. It assumes that you've already successfully logged the user in via their Social sign-in provider and created a KeylessAccount for them that can sign on behalf of their Keyless wallet.
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 from a sample app we're working on and is not meant as a production example. It's just meant to show the basics of signing with an embedded, single-app Keyless wallet.
The function shown below is here. The '/sponsorAndSubmitTx'
endpoint the same as with the connected wallet example, and is here.
const keylessTxFEBuildBESubmit = async (message: string, keylessAccount: KeylessAccount): Promise<PendingTransactionResponse> => {
console.log("keylessTxFEBuildBESubmit");
// Step 1. Build a feePayer tx with the user's input
const simpleTx = await buildSimpleMoveCallTransaction(keylessAccount.accountAddress, message);
// Step 2. Sign the transaction with the user's KeylessAccount
const senderSig = aptosClient.sign({ signer: keylessAccount, transaction: simpleTx });
// Step 3. Ask the BE to sponsor and submit the transaction
const pendingTx = await axios.post('/sponsorAndSubmitTx', {
transaction: simpleTx.bcsToHex().toString(),
senderAuth: senderSig.bcsToHex().toString()
});
return pendingTx.data.pendingTx;
}
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).
- Make sure the transaction you're submitting has been updated to use the feePayer's address (instead of the
- 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.
Single-app Keyless wallet: prover rate limit
Currently, Devnet has a limit of 10 requests per 5 min per user identity. Testnet and Mainnet each have a limit of 2 requests per 5 min per user identity.
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()));
Updated 27 minutes ago