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

Note: this tutorial requires a Shinami account. If you do not have one, you can sign up here.

1: Create a Shinami Gas Station fund on Testnet and deposit APT

A Gas Station fund is required, as some Invisible Wallet requests sponsor the associated transaction fees on behalf of the wallet. See our product FAQ for guidance on how to set up a fund on Testnet and add free APT to it if you don't already have one.

2. Clone the github repo

Clone the shinami-examples github repo, cd into the shinami-examples/aptos/typescript/backend_examples directory, and run npm installto 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 and Gas Station - because some of the calls require rights to both 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}}";

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 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 Aptos and Shinami clients
const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
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:

  1. Generates the sender's signature for the Invisible Wallet
  2. 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:

  1. Creates a feePayer SimpleTransaction, where the Invisible Wallet is the sender
  2. Generates the Invisible Wallet's signature on the Transaction.
  3. 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.