Invisible Wallets (TypeScript)
How to integrate Shinami Invisible Wallets
Overview
In this tutorial, we show you how to use the Shinami Invisible Wallet API. Shinami’s Invisible Wallets are app-controlled, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature.
The examples also show how to leverage Shinami's Gas Station to seamlessly sign, sponsor, and execute a transaction in one request. When you create an Invisible Wallet for a user it won't have any APT in it. With Gas Station you can sponsor your user's transaction fees so they don't have to download a wallet app and complete KYC to buy APT. Removing this friction - along with the burden of remembering recovery phrases and reading signing pop-ups - is a great way to smoothly onboard Web2-native users.
Understanding errors
Check the error code and message of any errors you get. We outline common errors in our Error Reference - make sure to check out the section specific to Aptos Invisible Wallet API as well as the "General Error Codes" section at the top that apply to all Shinami services.
Tutorial
Notes:
- This tutorial requires a Shinami account. If you do not have one, you can sign up here.
- We take you through the process step-by-step below, but if you get stuck you can reach out to us.
1: Create a Shinami Gas Station fund on Testnet
You'll need a Testnet Gas Station fund with APT in it in order to sponsor transactions. We have guidance on creating one in the Aptos Gas Station page of our Help Center. When you make a Testnet fund we deposit some APT in it so you can start testing immediately.
2. Clone the github repo
Clone the shinami-examples github repo, cd into the shinami-examples/aptos/typescript/backend_examples
directory, and run npm install
to install the dependencies for running the code. Run tsc
in your terminal, and if the command isn't found run npm install typescript --save-dev
(see other options here ) . If you need to install npm and Node.js, see here. Below, we'll be using the invisible_wallet.ts
file in the shinami-examples/aptos/typescript/backend_examples/src
directory.
3: Create an API access key with Testnet rights to all Shinami services
You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key with rights for all Testnet services - Wallet Services
, Node Service
, and Gas Station
- because some of the calls require rights to all services. See our Authentication and API Keys guide for info on how to set up a key with rights to all services.
Once you've created the key, enter is as the value for ALL_SERVICES_TESTNET_ACCESS_KEY
.
Replace all instances of {{name}}
with the actual value for that name
// 2. Copy your access key value
const ALL_SERVICES_TESTNET_ACCESS_KEY = "{{allServicesTestnetAccessKey}}";
It will be used in the creation of all the Shinami clients you'll need for these examples (setup shown in the next code block below).
4: Create an Invisible Wallet
For each Invisible Wallet you create, you need to create and store two new pieces of information: a unique walletId
and its associated, ideally unique secret
. This 1-to-1 pairing can be thought of as a username/password equivalent you use with Shinami's API for each wallet. A walletId
only works with the secret
used when the wallet was created, so your application MUST remember each (walletId
, secret
) pair. If you forget or change either value, the wallet's private key will be unrecoverable. For more information and images, see WalletId and Secret Pairing.
You'll need to define a (walletId
, secret
) pair first. Choose and enter them into the file as the values for WALLET_ONE_ID
and WALLET_ONE_SECRET
in Step 3 below.
In steps 4-6 below, we first set up our Shinami clients. We then create a ShinamiWalletSigner
instance to make our operations easier as it simplifies things by abstracting away session token management. Finally, we create the wallet and initialize it on Testnet (since that's where the Gas Station and Node Service rights of our access key are tied to).
Replace all instances of {{name}}
with the actual value for that name
// 3. Set up a walletId and its associated secret. Just for the tutorial. Your
// app should figure out the best way to manage its wallet IDs and secrets.
const WALLET_ID = "{{walletID}}";
const WALLET_SECRET = "{{walletSecret}}";
// 4. Instantiate your Shinami clients
const aptosClient = createAptosClient(ALL_SERVICES_TESTNET_ACCESS_KEY);
const keyClient = new KeyClient(ALL_SERVICES_TESTNET_ACCESS_KEY);
const walletClient = new WalletClient(ALL_SERVICES_TESTNET_ACCESS_KEY);
// Only required for `signSponsorAndSubmitTransactionInTwoSteps` example:
const gasClient = new GasStationClient(ALL_SERVICES_TESTNET_ACCESS_KEY);
// 5. Create a ShinamiWalletSinger to more easily manage the Invisible Wallet
const signer = new ShinamiWalletSigner(
WALLET_ID,
walletClient,
WALLET_SECRET,
keyClient
);
// 6. Create the Invisible Wallet. The call to `executeGaslessTransaction` below
// would initailize an un-initialized wallet. However, we are explicitly
// initializing it now in case you only run the `signAndVerifyTransaction`
// function, because a wallet must be initialized in order to sign a transaction.
// This requires the Gas Station fund associated with this access key to have
// sufficient APT to pay for the initialization transaction, as well as the
// transaction we execute below.
const CREATE_WALLET_IF_NOT_FOUND = true;
const INITIALIZE_ON_CHAIN = true;
const walletAddress = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN);
A note on wallet initialization
When you create a wallet, you have the choice of whether or not to initialize it on Testnet or Mainnet at the moment of creation. Initialization costs a very small amount of APT (your Gas Station fund sponsors a simple transaction which initializing the wallet). When you execute a transaction on behalf of an un-initialized wallet, wallet initialization happens as a part of that transaction. So, if the first action of a new user's wallet will always be to execute a transaction, when a user creates an account on your app you may wish to create an un-initialized wallet. If they engage with your app enough to reach their first transaction, the executeGaslessTransaction
call will initialize their wallet (for a small APT fee).
In the code above, we explicitly initialize the wallet for safety in case you only run the function that signs a transaction (since only initialized wallets can sign).
5: Generate a feePayer Transaction
Next, we build a SimpleTransaction with a feePayer where the Invisible Wallet is the sender.
// 7. Generate a feePayer transaction where an Invisible Wallet is the sender
const simpleTx = await simpleMoveCallTransaction(walletAddress);
The below simpleMoveCallTransaction
function creates a transaction that calls a function on a Move module we've deployed to Testnet (it's from an Aptos tutorial). The set_message
function allows the caller to store a message at their address inside a MessageHolder
. If there was already a value the user was storing, the function emits an event that says what the messaged changed from and to, and who made the change.
The transaction's expiration timestamp must be set to a time within the next hour (as is required by our Gas Station). When you control the generation of the sender's signature, as with an Invisible Wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. This is what we do below by not setting a timestamp.
async function simpleMoveCallTransaction(sender: AccountAddress, withFeePayer = true): Promise<SimpleTransaction> {
return await aptosClient.transaction.build.simple({
sender: sender,
withFeePayer: withFeePayer,
data: {
function: "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message",
functionArguments: ["hello"]
}
});
}
6: Review and run the code to sign, sponsor, and execute a transaction
The executeGaslessTransaction
method uses Wallet Service to sign the sponsored transaction as the sender and then Gas Station to sponsor the transaction and produce the sponsor's signature, before submitting the transaction to the Aptos blockchain.
Explore the code
// 8. Sign, sponsor, and submit the transaction
const pendingTx = await signer.executeGaslessTransaction(simpleTx);
// await signSponsorAndSubmitTransactionInTwoSteps(signer, simpleTx)
// 9. Wait for the transaction to execute and print its status
const executedTransaction = await aptosClient.waitForTransaction({
transactionHash: pendingTx.hash
});
console.log("\nTransaction hash:", executedTransaction.hash);
console.log("Transaction status:", executedTransaction.vm_status);
Run the code
Make sure you've saved the changes to the file made above: adding your API access key and a wallet ID and secret. In the shinami-examples/aptos/typescript/backend_examples
directory, run tsc
to compile the file. Then, run node build/invisible_wallet.ts
to run the code. You'll see the transaction digest printed to the console. You can look it up in an explorer like Aptos Scan (make sure you've selected Testnet). To run an example where you break up transaction signing, sponsorship, and submission into multiple steps, see the example in the Appendix.
By default, the code also runs the a function that signs a transaction and then verifies the signature, as explained in the Appendix.
Appendix
Sign, sponsor, and submit a transaction in multiple steps
In most cases where you are sponsoring transactions for Invisible Wallets, you will use the executeGaslessTransaction
method to sign, sponsor, and execute transaction in one request. However, there may be cases where you want to break this into 2-3 steps. The below function gives an example.
Explore the code
The signSponsorAndSubmitTransactionInTwoSteps
does the following:
- Generates the sender's signature for the Invisible Wallet
- Asks Shinami's Gas Station to sponsor and submit the signed transaction. As the comment explains, you could break this step in two by asking for just a sponsorship and then submitting the transaction along with the sender and feePayer signatures.
async function signSponsorAndSubmitTransactionInTwoSteps(onChainWalletSigner: ShinamiWalletSigner,
transaction: SimpleTransaction): Promise<PendingTransactionResponse> {
// 1. Generate the sender signature (from an Invisible Wallet that's been initialized on chain)
const senderSignature = await onChainWalletSigner.signTransaction(transaction);
// 2. Ask Shinami to sponsor and submit the transaction.
// You could also break this into two steps with a call to
// `gasClient.sponsorTransaction()` and then `aptosClient.transaction.submit.simple()`
return await gasClient.sponsorAndSubmitSignedTransaction(transaction, senderSignature);
}
Run the code
Make sure the function call in Step 8 is only one uncommented, so it looks like this:
// 8. Sign, sponsor, and submit the transaction
const pendingTx = // await signer.executeGaslessTransaction(simpleTx);
await signSponsorAndSubmitTransactionInTwoSteps(signer, simpleTx)
Make sure you've saved your changes. Then, in the shinami-examples/aptos/typescript/backend_examples
directory, run tsc
to compile the file. Finally, run node build/invisible_wallet.ts
to run the code. You'll see the transaction digest printed to the console. You can look it up in an explorer like Aptos Scan (make sure you've selected Testnet).
Sign a transaction and verify the signature
The below example shows how to sign a transaction and verify the signature.
Explore the code
The signAndVerifyTransaction
does the following:
- Creates a feePayer SimpleTransaction, where the Invisible Wallet is the sender
- Generates the Invisible Wallet's signature on the Transaction.
- Verifies that the signature was valid.
async function signAndVerifyTransaction(onChainWalletSigner: ShinamiWalletSigner,
transaction: SimpleTransaction): Promise<void> {
// 1. Generate the sender signature (from an Invisible Wallet that's been initialized on chain)
const accountAuthenticator = await onChainWalletSigner.signTransaction(transaction);
// 2. Verify the signature.
const signingMessage = aptosClient.getSigningMessage({ transaction });
const accountAuthenticatorEd25519 = accountAuthenticator as AccountAuthenticatorEd25519;
const verifyResult = accountAuthenticatorEd25519.public_key.verifySignature(
{
message: signingMessage,
signature: accountAuthenticatorEd25519.signature,
},
);
console.log("\nInvisible Wallet signature was valid:", verifyResult);
}
Run the code
Make sure the function call in Step 10 is uncommented, so it looks like this:
// 10. (optional) Uncomment the next line to sign a transaction and verify the signature:
await signAndVerifyTransaction(signer, simpleTx);
Make sure you've saved any changes. Then, in the shinami-examples/aptos/typescript/backend_examples
directory, run tsc
to compile the file. Finally, run node build/invisible_wallet.ts
to run the code.
Updated about 1 month ago