Invisible Wallet TypeScript Tutorial
How to utilize the Shinami Invisible Wallet
In this tutorial, we will be going over how to use the Shinami Invisible Wallet to interact with the Sui blockchain. For more details on this offering, please see the Invisible Wallet API guide. The full code for this TypeScript tutorial is available on Github.
The examples here will go over how to create a new wallet, sign and execute transactions from that wallet, and how to leverage its native integration with Shinami's Gas Station to seamlessly sign and execute a sponsored transaction in one call. We'll be constructing our programmable transactions using the Sui TypeScript SDK provided by Mysten and also using the typed-rpc library to make JSON-RPC calls to Shinami endpoints. Code sections that require substitution of custom values are stubbed with <>
.
In order to use Shinami's Invisible Wallet, we will need to create an <API_ACCESS_KEY>
to interact with Shinami's services. You can do this from Shinami's Access Keys dashboard. When creating this key, be sure to enable it for all services (Node Service
, Gas Station
, Invisible Wallet
):

Now for our TypeScript code, let's start by importing the packages we'll need from Mysten and the typed-rpc library.
import {
Connection,
JsonRpcProvider,
TransactionBlock,
SuiTransactionBlockResponse,
ExecuteTransactionRequestType
} from "@mysten/sui.js";
import { rpcClient } from "typed-rpc";
Next, let's set some basic parameters we'll need for our example calls:
<API_ACCESS_KEY>
is the key that was created above in Shinami's Access Key dashboard<WALLET_ID>
and<SECRET>
is a 1-to-1 pairing and can be thought of as a username/password equivalent for the wallet being created.<COIN_ID>
is the id of the Sui object that we will be using in our transactions.
Note:
<SECRET>
is currently immutable and tied to the wallet that is created. If, for example, you are using the user's password as the secret, and the user changes the password, there's currently no way to update the secret on the Shinami side.
// The Key Service is Shinami's secure and stateless way to get access to the Invisible Wallet
const KEY_SERVICE_RPC_URL = "https://api.shinami.com/key/v1/";
// The Wallet Service is the endpoint to issue calls on behalf of the wallet.
const WALLET_SERVICE_RPC_URL = "https://api.shinami.com/wallet/v1/";
// Shinami Sui Node endpoint + Mysten provided faucet endpoint:
const connection = new Connection({
fullnode: 'https://api.shinami.com/node/v1/<API_ACCESS_KEY>',
});
const suiProvider = new JsonRpcProvider(connection);
const walletId = "<WALLET_ID>";
const secret = "<SECRET>";
const GAS_BUDGET = 5000000;
// You can send testnet Sui to your wallet address via the Sui Discord testnet faucet
const sourceCoinId = "<COIN_ID>";
As mentioned above, we are using the typed-rpc library here to setup the typed request and response structure to/from the Shinami Invisible Wallet endpoints:
Note:
Although we support passing in access keys as part of the URI, we encourage users to utilize the
X-API-Key
header instead as it keeps this key slightly less exposed (e.g. in logs).
// Key Service interaction setup
interface KeyServiceRpc {
shinami_key_createSession(secret: string): string;
}
const keyService = rpcClient<KeyServiceRpc>(KEY_SERVICE_RPC_URL, {
getHeaders() {
return {
"X-API-Key": "<API_ACCESS_KEY>"
};
},
});
// Wallet Service interaction setup
interface WalletServiceRpc {
shinami_wal_createWallet(walletId: string, sessionToken: string): string;
shinami_wal_getWallet(walletId: string): string;
shinami_wal_signTransactionBlock(walletId: string, sessionToken: string, txBytes: string):
SignTransactionResult;
shinami_wal_executeGaslessTransactionBlock(
walletId: string,
sessionToken: string,
txBytes: string,
gasBudget: number,
options?: {},
requestType?: ExecuteTransactionRequestType
): SuiTransactionBlockResponse;
}
interface SignTransactionResult {
signature: string;
txDigest: string;
}
const walletService = rpcClient<WalletServiceRpc>(WALLET_SERVICE_RPC_URL, {
getHeaders() {
return {
"X-API-Key": "<API_ACCESS_KEY>"
};
},
});
We'll be submitting a programmable transaction on behalf of the wallet, splitting off two coins of value 10,000
and 20,000
MIST respectively from a main SUI coin, and transferring them back to the wallet address. Other types of transactions can be setup similarly; please see the sponsored transaction tutorial for more details.
We'll create two versions: one with gas context and one without (for sponsorship).
// Create a programmable transaction block to split off two new coins of value 10,000 and 20,000 MIST.
// The transaction block without gas context
const progTxnSplitGasless = (sender:string, sourceCoinId:string) => {
const txb = new TransactionBlock();
// Split two new coins out of sourceCoinId, one with 10000 balance, and the other with 20000
const [coin1, coin2] = txb.splitCoins(
txb.object(sourceCoinId),
[txb.pure(10000), txb.pure(20000)]
);
// Each new object created in a transaction must have an owner
txb.transferObjects(
[coin1, coin2],
txb.pure(sender)
);
return txb;
}
// The transaction block with gas context
const progTxnSplit = (sender:string, sourceCoinId:string) => {
const txb = new TransactionBlock();
// Split two new coins out of sourceCoinId, one with 10000 balance, and the other with 20000
const [coin1, coin2] = txb.splitCoins(
txb.object(sourceCoinId),
[txb.pure(10000), txb.pure(20000)]
);
// Each new object created in a transaction must have an owner
txb.transferObjects(
[coin1, coin2],
txb.pure(sender)
);
// Set gas context and sender
txb.setSender(sender);
txb.setGasBudget(GAS_BUDGET);
txb.setGasOwner(sender);
return txb;
}
Now that our setup is complete, let's begin the end-to-end flow. We'll start by creating a session token, using that to create a new wallet address, and verifying we can get our wallet address.
const invisibleWalletE2E = async() => {
// Create an ephemeral session token to access Invisible Wallet functionality
const sessionToken = await keyService.shinami_key_createSession(secret);
// Create a new wallet (can only be done once with the same walletId). Make
// sure to transfer Sui coins to your wallet before trying to run the
// following transactions
const createdWalletAddress = await walletService.shinami_wal_createWallet(walletId, sessionToken);
// Retrieve the wallet address via the walletId. Should be the same as createdWalletAddress
const walletAddress = await walletService.shinami_wal_getWallet(walletId);
...
Note:
You can request SUI to your wallet address via the #testnet-faucet Discord channel.
Let's first look at the scenario without sponsorship, where we have more control over signing and execution of a transaction with our newly created wallet. In this case, we will create the full transaction payload with gas context, sign the transaction, and send it to the Sui blockchain for processing.
...
// Get the transaction block of the full transaction.
const txbFull = progTxnSplit(walletAddress, sourceCoinId);
// Generate the bcs serialized transaction payload
const payloadBytesFull = await txbFull.build({ provider: suiProvider });
// Convert the payload byte array to a base64 encoded string
const payloadBytesFullBase64 = btoa(
payloadBytesFull.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
// Sign the payload via the Shinami Wallet Service.
const sig = await walletService.shinami_wal_signTransactionBlock(walletId, sessionToken, payloadBytesFullBase64);
// Execute the signed transaction on the Sui blockchain
const executeResponseFull = await suiProvider.executeTransactionBlock(
{
transactionBlock: payloadBytesFullBase64,
signature: sig.signature,
options: {showEffects: true},
requestType: "WaitForLocalExecution"
}
);
console.log("Execution Status:", executeResponseFull.effects?.status.status);
...
Note:
The use of
GasCoin
as an argument can be a security concern with a Gas Station so we have prohibited its use in any requests for sponsorship.
An advantage of using Shinami's Invisible Wallet is that it is fully integrated with Shinami's Gas Station, so we can also sponsor and execute a transaction in one call from a wallet address. Let's see how that's done:
...
// Get the transaction block of the gasless transaction
const txbGasless = progTxnSplitGasless(walletAddress, sourceCoinId);
// Generate the bcs serialized transaction payload
const payloadBytesGasless = await txbGasless.build({ provider: suiProvider, onlyTransactionKind: true });
// Convert the payload byte array to a base64 encoded string
const payloadBytesGaslessBase64 = btoa(
payloadBytesGasless.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
// Sponsor and execute the transaction with one call
const executeResponseGasless = await walletService.shinami_wal_executeGaslessTransactionBlock(
walletId,
sessionToken,
payloadBytesGaslessBase64,
GAS_BUDGET,
{
showInput: false,
showRawInput: false,
showEffects: true,
showEvents: false,
showObjectChanges: false,
showBalanceChanges: false
},
"WaitForLocalExecution"
);
console.log("Execution Status:", executeResponseGasless.effects?.status.status);
Note:
If you want sponsorship for your transaction block, you must include
onlyTransactionKind
set totrue
when building the bcs serialized transaction payload. For the non-sponsorship case, you will need to provide your own gas data and build it withonlyTransactionKind
set tofalse
or omitted (the default).
As you can see, with Shinami's Invisible Wallet you have flexibility on how to integrate for your needs. On a fundamental level, you can use it as a managed wallet for users with a low level API to securely sign transactions. Alternatively, if you plan on also using Shinami's Gas Station Service, you can leverage our high level API to sponsor, sign, and execute a transaction in one convenient call.
We hope this tutorial has been helpful. Let us know if you have any feedback by shooting us an email at [email protected].
Updated about 1 month ago