Sponsored Transaction TypeScript Tutorial
How to sponsor transactions with Shinami on Sui
Overview
In this tutorial, we'll go over how to sponsor transactions with Shinami's Gas Station and submit them for execution on the Sui blockchain. The full code for this TypeScript tutorial is available on GitHub.
For our examples, we'll be constructing our programmable transactions using the Sui TypeScript SDK provided by Mysten. We'll also use the Shinami Clients SDK to make JSON-RPC calls to Shinami Gas Station for transaction sponsorship and Node Service for transaction execution.
Sponsored Transactions
When calling sui_executeTransactionBlock
a base64 encoded string is passed in which encodes the transaction to be executed, the sender address, and the gas information for the transaction block. This unit is called a TransactionData
object, and along with one or more signatures, it's what's needed to write to the chain.
When a transaction is being sponsored, the sponsor sends Shinami Gas Station two parts of a TransactionData
object: the sender address and the transaction block to be executed (with gas information omitted, this is considered a TransactionKind
object). The gas station is responsible for attaching a GasData
object that represents the gas object that will be powering the transaction. Finally, the BCS-serialized, Base64-encoded string that represents the full TransactionData
object is produced and sent back along with the gas station's signature authorizing this transaction.
From there, the last piece needed to call sui_executeTransactionBlock
is the sender's signature over the TransactionData
.
Tutorial: creating, sponsoring, and executing a transaction
1. Create a Shinami Gas Station fund on testnet
See our Gas Station Guide for how to set up a fund on Testnet and add free Sui to it if you don't have one.
2: Set up your access key
You use API access keys to interact with Shinami's services. You create them in your Shinami Dashboard. For this tutorial, we'll create one access key that has both Node Service
and Gas Station
rights for Testnet (you could also make a two keys, one with only Node Service
rights and one with only Gas Station
rights).
When you create a key with Node Service
rights, you assign a max QPS and max number of WebSocket connections the key is allowed. You can change a key's values later by clicking on the +
next to the key in Access Keys table to open the key editor. When you create an access key with Gas Station
rights, you need to link it to a Gas Station fund. All requests for sponsorship using the key draw Sui from that fund. So, you'll link this key to the Testnet fund you created in step one. For example:

3. Sponsor a transaction with a newly-generated KeyPair as sender
The below code creates a transaction that calls a function on a Move module we've deployed to testnet. This is a very simple Move module based on a sample Move project from Mysten. We're calling its one function, access
, which takes a read-only reference of the sui::clock::Clock
instance, located at address 0x6
as its single parameter. The function emits a single event that contains the current timestamp obtained from the Clock
instance.
Bonus: if you want to listen to the event emitted by the Move call over a WebSocket connection, see the cURL and TypeScript examples for suix_subscribeEvent
in our WebSocket API doc.
Replace all instances of {{name}}
with the actual value for that name
import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
import { TransactionBlock } from "@mysten/sui.js/transactions";
import { GasStationClient, createSuiClient, buildGaslessTransactionBytes } from "@shinami/clients";
// 1. Copy your testnet Gas Station and Node Service key values
const GAS_AND_NODE_TESTNET_ACCESS_KEY = "{{gasAndNodeServiceTestnetAccessKey}}";
// 2. Set up your Gas Station and Node Service clients
const nodeClient = createSuiClient(GAS_AND_NODE_TESTNET_ACCESS_KEY);
const gasStationClient = new GasStationClient(GAS_AND_NODE_TESTNET_ACCESS_KEY);
// 3. Generate a new KeyPair to represent the sender
let keyPair = new Ed25519Keypair();
const SENDER_ADDRESS = keyPair.toSuiAddress();
// 4. Generate the TransactionKind for sponsorship as a Base64 encoded string
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
sui: nodeClient,
build: async (txb) => {
txb.moveCall({
target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
arguments: [txb.object('0x6')]
});
}
});
// 5. Set your gas budget, in MIST
const GAS_BUDGET = 5_000_000;
// 6. Send the TransactionKind to Shinami Gas Station for sponsorship
let sponsoredResponse = await gasStationClient.sponsorTransactionBlock(
gaslessPayloadBase64,
SENDER_ADDRESS,
GAS_BUDGET
);
// 7. The transaction should be sponsored now, so its status will be "IN_FLIGHT"
let sponsoredStatus = await gasStationClient.getSponsoredTransactionBlockStatus(
sponsoredResponse.txDigest
);
console.log("Transaction Digest:", sponsoredResponse.txDigest);
// For me this printed "Transaction Digest: GE6rWNfjVk7GiNSRHExaYnQB6TNKRpWBbQrAAK1Cqax5"
// which we'll see in the image below.
console.log("Sponsorship Status:", sponsoredStatus);
// Printed "Sponsorship Status: IN_FLIGHT"
At this point, if you look in your Shinami dashboard at the In flight transactions, you'll see the digest you just printed out (of course, your digest will be different). If you run this code twice, you'll get two digests as a different gas object is being used each time.

In flight transactions will remain here until they are executed or the sponsorship expires before an execution attempt was made (all sponsorships have a 1 hour TTL). After a transaction is executed, the digest can remain in-flight for a few minutes until our Gas Station updates the status of the associated gas objects.
Setting your sponsorship GAS_BUDGET
The GAS_BUDGET
is up to you to set. When doing so, remember that:
- The budget set for a transaction will be "held" from your gas credits fund while the transaction is in flight.
- If your budget is set too low, the transaction will fail, but gas credits will still be deducted for the computation cost of running the failed transaction on the Sui blockchain.
This ends up being an exercise in fine tuning so your budgeted gas is enough to push a transaction through, but not overly provisioned that you are inefficient in your gas credit usage. One place to review the cost of your successful transactions is in the Sui Explorer. You can also use sui_devInspectTransactionBlock
to estimate the gas cost needed by the transaction. We recommend adding a small buffer to the estimated value to be safe.
Note: sui_devInspectTransactionBlock
does not catch all transaction execution errors and should not be used for testing the correctness of a transaction (sui_dryRunTransactionBlock
is better for testing transaction correctness, but requires an attached gas object so is not ideal for pre-sponsorship gas budget testing.).
We're also working on building auto-budgeting into Gas Station to make this process even easier for you.
4. Sign and execute the transaction
Now, we'll go ahead and execute the transaction we sponsored. The below code continues where we left off:
// 8. Sign the full transaction payload with the sender's key.
let senderSig = await TransactionBlock.from(sponsoredResponse.txBytes).sign(
{
signer: keyPair,
}
);
// 9. Send the full transaction payload, along with the gas owner
// and sender's signatures for execution on the Sui network
let executeResponse = await nodeClient.executeTransactionBlock({
transactionBlock: sponsoredResponse.txBytes,
signature: [senderSig.signature, sponsoredResponse.signature],
options: { showEffects: true },
requestType: "WaitForLocalExecution",
});
console.log("Transaction Digest:", executeResponse.digest);
// Printed "Transaction Digest: GE6rWNfjVk7GiNSRHExaYnQB6TNKRpWBbQrAAK1Cqax5"
console.log("Execution Status:", executeResponse.effects?.status.status);
// Printed "Execution Status: success"
The digest printed out will be the same one as what we sponsored, as expected, since the digest is created over the same transaction operations, gas object, and sender address. If we look in the Sui Explorer on Testnet, we can see the digest (of course, your digest will be different):

Additional examples
Other TransactionBlock types
Of course, there are more types of programmable transactions than a Move function call like we did above. Below are some additional examples of gas-less transactions you can sponsor and execute. Note: they all use the same gaslessPayloadBase64
variable name and can replace step 4 of Section 2 above (so, if you have them all uncommented at the same time you'll get a compiler error).
Replace all instances of {{name}}
with the actual value for that name
// Comment out step 4 above
// 4. Generate the TransactionKind for sponsorship as a Base64 encoded string
// const gaslessPayloadBase64 = await buildGaslessTransactionBytes({
// sui: nodeClient,
// build: async (txb) => {
// txb.moveCall({
// target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
// arguments: [txb.object('0x6')]
// });
// }
// });
// and replace it with one of:
// -- Other TransactionBlock examples -- //
// Create two new small coins by taking MIST from a larger one.
// If you ask for testnet Sui for an account you have the
// private key or passphrase to, you can split two coins from that.
//
//
const COIN_TO_SPLIT = "{{coinObjectId}}";
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
sui: nodeClient,
build: async (txb) => {
const [coin1, coin2] = txb.splitCoins(txb.object(COIN_TO_SPLIT), [
txb.pure(10000),
txb.pure(20000),
]);
// each new object created in a transaction must be sent to an owner
txb.transferObjects([coin1, coin2], txb.pure(SENDER_ADDRESS));
}
});
// Transfer one or more object(s) owned by the sender address to the recipient
// An easy example is a small coin you created with the above transaction.
//
//
const RECIPIENT_ADDRESS = "{{recipientSuiAddress}}";
const OBJECT_TO_SEND = "{{objectId}}";
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
sui: nodeClient,
build: async (txb) => {
txb.transferObjects(
[txb.object(OBJECT_TO_SEND)],
txb.pure(RECIPIENT_ADDRESS)
);
}
});
// Merge one or more smaller coins into another, destroying the small coin(s)
// and increasing the value of the large one.
//
//
const TARGET_COIN = "{{targetCoin}}";
const COIN_SOURCE1 = "{{coinSource1}}";
const COIN_SOURCE2 = "{{coinSource2}}";
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
sui: nodeClient,
build: async (txb) => {
txb.mergeCoins(txb.object(TARGET_COIN), [
txb.object(COIN_SOURCE1),
txb.object(COIN_SOURCE2),
]);
}
});
Another way to generate a transaction to sponsor
Above, we used the convenience method buildGaslessTransactionBytes
that builds a gas-less transaction, converts it to BCS serialized bytes, and then converts that into the base64 encoded string expected by gasStationClient.sponsorTransactionBlock
. Here, we show those three steps broken out into individual pieces:
// Comment out step 4 above
// 4. Generate the TransactionKind for sponsorship as a Base64 encoded string
// const gaslessPayloadBase64 = await buildGaslessTransactionBytes({
// sui: nodeClient,
// build: async (txb) => {
// txb.moveCall({
// target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
// arguments: [txb.object('0x6')]
// });
// }
// });
// and relace it with
// 4. Generate the TransactionKind for sponsorship as a Base64 encoded string
const txb = new TransactionBlock();
txb.moveCall({
target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
arguments: [txb.object('0x6')]
});
// generate the bcs serialized transaction data without any gas object data
const gaslessPayloadBytes = await txb.build({ client: nodeClient, onlyTransactionKind: true});
// convert the byte array to a base64 encoded string
const gaslessPayloadBase64 = btoa(
gaslessPayloadBytes
.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
Generate a KeyPair from a private key or passphrase
If you have the private key or recovery phrase from an existing wallet, you can use those to create the sender's KeyPair. Below is code demonstrating both options.
Replace all instances of {{name}}
with the actual value for that name
// -- Generate a KeyPair from a private key or passphrase -- //
// Create the sender's address key pair from the sender's private key
const SENDER_PRIVATE_KEY = "{{privateKey}}";
const buf = Buffer.from(SENDER_PRIVATE_KEY, "base64");
const keyPairFromSecretKey = Ed25519Keypair.fromSecretKey(buf.slice(1));
console.log(keyPairFromSecretKey.toSuiAddress());
// Create the sender's address key pair from the sender's recovery phrase
const SENDER_PASSPHRASE = "{{passphrase}}";
const keyPairFromPassphrase = Ed25519Keypair.deriveKeypair(SENDER_PASSPHRASE);
console.log(keyPairFromPassphrase.toSuiAddress());
Integrating Gas Station with Shinami zkLogin wallet API
Shinami's zkLogin Wallet API powers user-controlled wallets that are fully compatible with transactions sponsored by Shinami Gas Station. After generating a zkProof, you can create a gas-less transaction, sponsor it with your Gas Station fund with the zkLogin wallet address as the sender, and sign the transaction with with zkWallet's ephemeral private key. For more information on zkLogin and the order of operations for producing and submitting a transaction to Sui, see Mysten's zkLogin doc. For an end-to-end tutorial showing you how to create your first zkLogin Next.js app that integrates with Shinami's Gas Station API and zkLogin Wallet API, see our zkLogin wallet Next.js Tutorial.
Integrating Gas Station with Shinami Invisible Wallets
Shinami Invisible Wallets are app-controlled wallets you can use to abstract away web3 elements like seed phrases, third-party wallet connections, gas fees, and signing popups. They have native integration with Gas Station, including a method that allow you to sponsor, sign, and execute a transaction in one method call. We've created a tutorial for Invisible Wallets that includes using them with Gas Station.
Updated 7 days ago