# Gas Station API Source: https://docs.shinami.com/api-docs/aptos/gas-station/api Increase user engagement and retention by sponsoring transactions ## Overview You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Shinami's Gas Station API lets you easily sponsor transactions for your users. APT is pulled from a fund you create to power these transactions, managed in Shinami's dashboard. Shinami facilitates sponsored transactions based on logic determined by you, the app developer. Examples include sponsoring each user's initial transaction(s) for better onboarding and sponsoring transactions of a particular type that you want to encourage. ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header `"X-Api-Key: ACCESS_KEY"`) or in the request url (`https://api.us1.shinami.com/aptos/gas/v1/ACCESS_KEY`). We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for how to set up a Gas-Station access key. **Call Shinami's Gas Station from your backend server** Shinami Gas Station does not support CORS requests, so you will get a CORS error if you make requests from your frontend. Use your backend server to integrate with Shinami's Gas Station. This limits exposure of your sponsorship 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. For a tutorial on how to combine frontend signing with backend sponsorship, see our [Frontend signing + backend sponsorship tutorial](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing). **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also consider batching multiple Move calls into a [Move script](https://aptos.dev/build/smart-contracts/scripts). You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Aptos Gas Station API](/developer-guides/core-integration-topics/error-reference#aptos-gas-station-api). ### Send your first request For a quick sample request that doesn't require building a transaction, ask for the balance of the fund your access key is tied to with [gas\_getFund](/api-docs/aptos/gas-station/api#gas-getfund). ### Tutorials with E2E sample code Check out our [TypeScript tutorial](/developer-guides/aptos/tutorials/gas-station-backend-only) for more code samples and details on the end-to-end flow of creating, sponsoring, and executing a transaction. Also, since our Gas Station does not support CORS (browser) requests for security reasons, our [Frontend signing + backend sponsorship tutorial](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing) . ## Methods ### gas\_sponsorTransaction Sponsor a transaction by having your Shinami Gas Station fund act as the feePayer. **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Aptos Gas Station Help Center page](/help-center/aptos/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). 4. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. **Request Parameters** | Name | Type | Description | | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rawTransaction | ***TypeScript SDK*** SimpleTransaction \| MultiAgentTransaction

***cURL***
String \| byte array | ***TypeScript SDK***
An AnyRawTransaction (the result of a successful call to `aptos.transaction.build.simple()` or `aptos.transaction.build.multiAgent()`). For a code example of building and sponsoring a transaction, see [the Appendix](/api-docs/aptos/gas-station/api#how-to-build-sponsor-sign-and-submit-a-transaction).

***cURL***
BCS-serialized [`RawTransaction`](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110) component of an AnyRawTransaction. Can be passed either as an unsigned byte array or a hex string. For an example of creating this in Python, see the `Aptos Python SDK: rawTransaction` tab of our Example Request Templates.

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | | ***cURL-only***
secondarySignerAddresses | String\[] | Optional array of additional signer addresses. Must be in the exact order. Required for multi-agent transactions. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const accountAuthenticator = await gasStationClient.sponsorTransaction(transaction); ``` ```bash cURL - template theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorTransaction", "params":[ "{{rawTransaction}}", ["{{secondarySignerOne}}"] ], "id":1 }' ``` ```bash cURL - example theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorTransaction", "params":[ "0x3de05e9561457529d7b46a93bfa78d9927a403e51a61dc65c5d9ea3e6ec319e80000000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d0300000000006400000000000000e31b56660000000002", ["0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d"] ], "id":1 }' ``` ```bash Aptos Python SDK: rawTransaction theme={null} # Here is an example of preparing a `RawTransaction` to be sent # as the `rawTransaction` parameter # Imports used in the below code snippet from aptos_sdk.async_client import RestClient from aptos_sdk.bcs import Serializer ... ## Set up your transaction payload (`txPayload`) of type `TransactionPayload` ## Also the `senderAccount`, which is of type `Account` ... rest_client = RestClient(APTOS_TESTNET_NODE_URL) raw_transaction = await rest_client.create_bcs_transaction(senderAccount, txPayload)) # Option 1: send as an unsigned byte array shinami_rawTransaction = list(raw_transaction.to_bytes()) # Option 2: send as a hex string transaction_serializer = Serializer() raw_transaction.serialize(transaction_serializer) shinami_rawTransaction = transaction_serializer.output().hex() ``` **Example Response** ```bash Shinami TypeScript SDK theme={null} { public_key: { key: { data: [Object] } }, signature: { data: { data: [Object] } } } ``` ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "feePayer":{ "address":"0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "signature":[0,32,87,185,212,148,211,30,213,111,144,35,98,238,246,233,212,250,141,34,227,144,171,146,27,203,22,120,200,236,145,94,203,207,64,3,238,3,81,139,240,173,30,31,146,34,145,151,191,54,93,34,206,249,77,214,168,228,65,165,220,65,174,220,137,196,173,168,37,116,136,85,216,217,121,46,1,205,38,147,183,201,252,246,40,62,127,220,20,246,41,244,5,209,109,224,114,189,3] } }, "id":1 } ``` **TypeScript SDK Response Fields** **Notes:** transaction.feePayerAddress is updated in-place upon a successful return. | Type | Description | | :------------------- | :--------------------------------------------------------------------------------------------------------- | | AccountAuthenticator | AccountAuthenticator of the sponsor, to be included in the request to submit the transaction to the chain. | **cURL Response Fields** | Name | Type | Description | | :----------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | feePayer.address | String | Fee payer account address. If desired you can use this to set the transaction's `feePayerAddress` before obtaining sender and any secondary signer signatures. | | feePayer.signature | Unsigned byte array | BCS-serialized AccountAuthenticator of the fee payer, as an array of unsigned bytes. | ### gas\_sponsorAndSubmitSignedTransaction This method first sponsors a transaction with your Shinami Gas Station fund and then submits the transaction to an Aptos fullnode for execution. Therefore, you need to obtain all necessary signatures - with wildcard fee payer address `0x0` - *before* calling this method. **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Aptos Gas Station Help Center page](/help-center/aptos/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). 4. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. **Request Parameters** | Name | Type | Description | | :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rawTransaction | ***SDK***
SimpleTransaction \| MultiAgentTransaction

***cURL***
Hex string \| Unsigned byte array. For an example of creating this in Python, see the `Aptos Python SDK` tab of our Example Request Templates. | ***TypeScript SDK***
(The result of a successful call to `aptos.transaction.build.simple()` or `aptos.transaction.build.multiAgent()`). For a code example of building and sponsoring a transaction, see [the Appendix](/api-docs/aptos/gas-station/api#how-to-build-sponsor-sign-and-submit-a-transaction) .

***cURL***
BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110) component of an AnyRawTransaction. Can be passed either as an unsigned byte array or a hex string.

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | | senderSignature | ***SDK***
AccountAuthenticator

***cURL***
BCS-serialized [AccountAuthenticator](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506) . Can be passed either as an unsigned byte array or a hex string. For an example of creating this in Python, see the `Aptos Python SDK` tab of our Example Request Templates. | The signature of the sender over the transaction with wildcard fee payer address (`0x0`). | | secondarySigners | ***SDK***
AccountAuthenticator\[]

***cURL***
\["address" : \, "signature": \(Can be passed either as an unsigned byte array or a hex string.)] | Optional array of additional signers and their signatures. Must be in the exact order. Required for multi-agent transactions | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const pendingTx = await gasStationClient.sponsorAndSubmitSignedTransaction( {{transaction}}, {{senderSignature}}, {{[secondarySignatureOne, secondarySignatureTwo...]}} ); ``` ```bash cURL template theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorAndSubmitSignedTransaction", "params":[ "{{rawTransaction}}", "{{senderSignature}}", [{"address": "{{secondarySenderAddress}}","signature": "{{secondaryAddressSignature}}"}] ], "id":1 }' ``` ```bash cURL -- multiAgentMoveScript exampl theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorAndSubmitSignedTransaction", "params":[ "0x8a59591db8c95f7065cc1476c913e0015bea279cc8ba852d97f9269f181b3ecc0000000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d0300000000006400000000000000af1551660000000002", "0x0200202775e36c43afe1cd6b49514aaee0377fe60d2a154c74774248b7248d1554555800400c3c374f5a16084ca25c7f5f6105fe32e3b6acddbe7e4d369791c12fd7efd7838209e114c2d212bd31d7b3f38b5fa737145051659c18205ec3a32195aa02d406", [{ "address": "0x5f4a07084ae99c7d208ad4d6dc0315f271b8e2b915b38ee241e3b26e18885fa2", "signature": "0x0200204faef99c8a1b3b1511348081f3b73fe59550121f6081bfaefa4f8a55e61533bd0040efb3618c7e31ea3073f61e0f9798353979cb5ed13a6f8d52f5bf3791e54d3c760d223ec0721557254f821f1d5fecab96b2ad085a82e27ae1995b939876f3260e" }] ], "id":1 }' ``` ```bash Aptos Python SDK theme={null} # Here is an example of preparing a `RawTransaction` to be sent as the `rawTransaction` parameter # and an AccountAuthenticator to be sent as the `senderSignature` parameter. # Imports used in the below code snippet from aptos_sdk.async_client import RestClient from aptos_sdk.bcs import Serializer from aptos_sdk.transactions import FeePayerRawTransaction ... ## Set up your transaction payload (`txPayload`) of type `TransactionPayload` ## Also the `senderAccount`, which is of type `Account` ... rest_client = RestClient(APTOS_TESTNET_NODE_URL) ## RawTransaction raw_transaction = await rest_client.create_bcs_transaction(senderAccount, txPayload)) # Option 1: send as an unsigned byte array shinami_rawTransaction = list(raw_transaction.to_bytes()) # Option 2: send as a hex string transaction_serializer = Serializer() raw_transaction.serialize(transaction_serializer) shinami_rawTransaction = transaction_serializer.output().hex() ## sender's AccountAuthenticator # Build a fee payer transaction and have the sender sign it fee_payer_transaction = FeePayerRawTransaction(raw_transaction, [], None) sender_authenticator = senderAccount.sign_transaction(fee_payer_transaction) auth_serializer = Serializer() sender_authenticator.serialize(auth_serializer) # Option 1: send as an unsigned byte array shinami_senderSignature = list(auth_serializer.to_bytes()) # Option 2: send as a hex string shinami_senderSignature = auth_serializer.output().hex() ``` **Example Response** ```bash Shinami TypeScript SDK expandable theme={null} { hash: '0xc0f973af799eaa463a9a00f22b917e25c512e4e46ae765e9683030d1bad93436', sender: '0xadf5e46c8096d4587032dbe35cb261134ad062d28a247da2c8a6b28097944415', sequence_number: '7', max_gas_amount: '200000', gas_unit_price: '100', expiration_timestamp_secs: '1717631961', payload: { function: '0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message', type_arguments: [], arguments: [ 'hello' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x8df7fae1ff10bb4135d700c1d787f4b8f1896001fcf78e19e059530c138f2f67', signature: '0xbfd09c753b98f3b04798f8ee902cc08d9770be5951d23df5cc5323ef671894b7328d66b91a02b3561293fa3d6e200ff6f2f35969781f19bd2b595be7ab074304', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6', fee_payer_signer: { public_key: '0x57b9d494d31ed56f902362eef6e9d4fa8d22e390ab921bcb1678c8ec915ecbcf', signature: '0x097a5c04894e874485f8d10b6092bd9cef3c4eede1d524d9b676cb06267d3dce4388742c38f03e8d65e2ead408ed32fdb735c9b50ad96cf0260fc2c5c1a2430e', type: 'ed25519_signature' }, type: 'fee_payer_signature' } } ``` ```bash cURL -- simple transaction expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1716589253", "gas_unit_price" : "100", "hash" : "0x7740a3baee841c049d79a5aee0021dc8a98f65d72854b94dbb79a8dfa4381b20", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "Test message" ], "function" : "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "sender" : "0xd0c8e3d35d0ff8912d74aab5ef42442b8425de5e4504639f7612bcf3c3876442", "sequence_number" : "0", "signature" : { "fee_payer_address" : "0x11045a6f53a6e9064fd92b45038649c334fd1afd60848a090facab14e07cf70a", "fee_payer_signer" : { "public_key" : "0x7dfd1009d1dff08cd60e900c1eccda628808016e5885bc82a66f4d97ba959594", "signature" : "0x96c29f29f4925b6083794c16d289b6168135a63f1c95ed0e5a273bd923aeaf0a6efc3c5ff2963fff83b72b8f3786ecb1f5b1439072086d1f58e3e88bd2b18e05", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0x6cf81b2b0adcd10c6d10edc3a85fa093dfa4eae890076528ea8edc4eed3b9a35", "signature" : "0x9a331c9189b4ef008418e887ef0ee25382b2a1f74fa0c725b3e9432c2ea7fa37899f8e8490f202c521a52dda3509822c11de3c5b834d6111adf6d0bc397f9a02", "type" : "single_key_signature" }, "type" : "fee_payer_signature" } } } } ``` ```bash cURL -- multiAgent Move Script transaction theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1716589999", "gas_unit_price" : "100", "hash" : "0x47e3b8632257f91611be18f999a776745f6542298a15c2ca6016c9cfc88a751b", "max_gas_amount" : "200000", "payload" : { "arguments" : [], "code" : { "abi" : { "generic_type_params" : [], "is_entry" : true, "is_view" : false, "name" : "main", "params" : [ "&signer", "&signer" ], "return" : [], "visibility" : "public" }, "bytecode" : "0xa11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b03380102" }, "type" : "script_payload", "type_arguments" : [] }, "sender" : "0x8a59591db8c95f7065cc1476c913e0015bea279cc8ba852d97f9269f181b3ecc", "sequence_number" : "0", "signature" : { "fee_payer_address" : "0x221351e64436aa7b6732c791a3e66c6cb8386b2a2a27c6ab17a67f66aa449335", "fee_payer_signer" : { "public_key" : "0xdb3031396202fb5a21c5c3f100986a5ad9e9484c12bd5bae8e7c916dff4bf33a", "signature" : "0xceb7b95aaa7d2557ff98824ea4fd98e124db2d6b87dcb75d1b9f5b33c3362a5a4c3fe8975c434edbfd2985acc9e9e4aadd3430b7a47dc668c522db2dd7116207", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [ "0x5f4a07084ae99c7d208ad4d6dc0315f271b8e2b915b38ee241e3b26e18885fa2" ], "secondary_signers" : [ { "public_key" : "0x4faef99c8a1b3b1511348081f3b73fe59550121f6081bfaefa4f8a55e61533bd", "signature" : "0xefb3618c7e31ea3073f61e0f9798353979cb5ed13a6f8d52f5bf3791e54d3c760d223ec0721557254f821f1d5fecab96b2ad085a82e27ae1995b939876f3260e", "type" : "single_key_signature" } ], "sender" : { "public_key" : "0x2775e36c43afe1cd6b49514aaee0377fe60d2a154c74774248b7248d15545558", "signature" : "0x0c3c374f5a16084ca25c7f5f6105fe32e3b6acddbe7e4d369791c12fd7efd7838209e114c2d212bd31d7b3f38b5fa737145051659c18205ec3a32195aa02d406", "type" : "single_key_signature" }, "type" : "fee_payer_signature" } } } } ``` **Response Fields** | Type | Description | | :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pendingTransactionResponse | The submitted transaction waiting in the Fullnode's [mempool](https://aptos.dev/network/blockchain/blockchain-deep-dive#mempool). Same as [PendingTransactionResponse](https://github.com/aptos-labs/aptos-ts-sdk/blob/de936697bdc552eb9c88e676d85686449dba3467/src/types/index.ts#L333-L343) . | ### gas\_getFund Get the balance for the Gas Station fund tied to the request's API access key. When you create a Gas Station access key, you [link it to exactly one Gas Station fund](/developer-guides/core-integration-topics/authentication-and-api-keys#3a-aptos-key-configuration). So, when you make this request, we return the balance for the fund that's tied to the access key you use for the request. To check which fund a Gas Station access key is tied too, see the [Aptos Gas Station FAQ page of our Help Center](/help-center/aptos/gas-station-faq#how-do-i-check-which-fund-an-access-key-is-tied-to%3F) **Request Parameters** None. **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const fundInfo = await gasStationClient.getFund(); ``` ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_getFund", "params":[], "id":1 }' ``` **Example Response** ```bash Shinami TypeScript SDK theme={null} { name: 'test_fund', balance: 1030412796, inFlight: 0, network: 'APTOS_TESTNET', depositAddress: null // see comment in "Response Fields" section below } ``` ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "balance" : 0, "depositAddress" : null, // see comment in "Response Fields" section below "inFlight" : 0, "name" : "Mainnet fund two", "network" : "APTOS_MAINNET" } } ``` **Response Fields** | Name | Type | Description | | :------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | string | The name of the fund tied to this API access key. | | network | string | The network the fund is associated with. | | balance | integer | Available balance of the the fund in Octas. | | inFlight | integer | The portion of the fund balance that is currently locked for use with active sponsorships. Shown in Octas. | | depositAddress | string \| null | A string representing an Aptos address if a deposit address has been created for the fund. null if no deposit address has been created. If you do not have a deposit address, see how to generate one in the [Aptos Gas Station FAQ page in our Help Center](/help-center/aptos/gas-station-faq#how-do-i-generate-and-find-the-deposit-address-of-a-fund%3F) . | ### gas\_encodeAndSponsorTransaction Sponsors a JSON transaction request by providing fee payer info, and encodes it into message bytes for signing. Intended to be used only by clients without BCS encoding capability. If you're using the Aptos TypeScript SDK you should use sponsorTransaction instead. The full transaction flow for using this API is as follows: 1. Construct the transaction JSON. 2. Call this API to get the encoded message bytes for signing, as well as fee payer address and signature. 3. Sender (and any secondary signers) sign the message bytes. 4. Submit the transaction JSON together with all signatures to an Aptos fullnode for execution (an example of this call and response is shown in the `cURL - submit sponsored tx example` tabs). **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Aptos Gas Station Help Center page](/help-center/aptos/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). **Request Parameters** | Name | Type | Description | | :--------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | submission | object | [EncodeSubmissionRequest](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/ecosystem/typescript/sdk/src/generated/models/EncodeSubmissionRequest.ts#L12-L23) , same as the request body on Aptos fullnode REST API `/transactions/encode_submission` (see documentation [here](https://aptos.dev/rest-api)).

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | **Example Request Template** Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL - encodeAndSponsor template expandable theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_encodeAndSponsorTransaction", "params":[{ "sender": "{{senderAddress}}", "sequence_number": "{{sequenceNumber}}", "max_gas_amount": "{{maxGasAmount}}", "gas_unit_price": "{{gasUnitPrice}}", "expiration_timestamp_secs": "{{timestamp}}", "payload": { "type": "{{payloadType}}", "function": "{{functionId}}", "type_arguments": [ "{{typeArgs}}" ], "arguments": [ {{arguments}} ] }, "secondary_signers": [ {{secondarySignerAddresses}} ] }], "id":1 }' ``` ```bash cURL - encodeAndSponsor example expandable theme={null} curl https://api.us1.shinami.com/aptos/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_encodeAndSponsorTransaction", "params":[{ "sender": "0xadf5e46c8096d4587032dbe35cb261134ad062d28a247da2c8a6b28097944415", "sequence_number": "7", "max_gas_amount": "200000", "gas_unit_price": "100", "expiration_timestamp_secs": "1717606333", "payload": { "type": "entry_function_payload", "function": "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", "type_arguments": [], "arguments": [ "hello" ] }, "secondary_signers": [] }], "id":1 }' ``` ```bash cURL - submit sponsored tx example expandable theme={null} ## This is a an example of the request to an Aptos fullnode that you ## make after getting the response from Shinami's gas_encodeAndSponsorTransaction ## and then generating the sender sig on the transactionSigningMessage returned. curl --request POST \ --url https://api.testnet.aptoslabs.com/v1/transactions \ --header 'Content-Type: application/json' \ -d '{ "sender": "0xadf5e46c8096d4587032dbe35cb261134ad062d28a247da2c8a6b28097944415", "sequence_number": "9", "max_gas_amount": "200000", "gas_unit_price": "100", "expiration_timestamp_secs": "1718671754", "payload": { "type": "entry_function_payload", "function": "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", "type_arguments": [], "arguments": [ "hello" ] }, "signature": { "type": "fee_payer_signature", "sender": { "type":"ed25519_signature", "public_key":"0x8df7fae1ff10bb4135d700c1d787f4b8f1896001fcf78e19e059530c138f2f67", "signature":"0x4b9bc91c2c6886c25a007e660bc7c457f851fe68a90896f5e3905f5ac2c256af918cf2afc3237d70ad24360ee9e86bdf053a5d7d85ea59d60bd387c3d67dd102" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "fee_payer_address": "0x11045a6f53a6e9064fd92b45038649c334fd1afd60848a090facab14e07cf70a", "fee_payer_signer": { "type":"ed25519_signature", "public_key":"0x7dfd1009d1dff08cd60e900c1eccda628808016e5885bc82a66f4d97ba959594", "signature":"0x743b061bf0cc3629df36962a7374682f026fad09d0a51a0138dcbc9b6f4cf748f853807b2a139a90466b9653685b136542a7cd0f8725038a2c060f0edfd96507" } } }' ``` **Example Response** ```bash cURL - encodeAndSponsor example expandable theme={null} { "jsonrpc":"2.0", "result":{ "transactionSigningMessage":[94,250,60,79,2,248,58,15,75,45,105,252,149,198,7,204,2,130,92,196,231,190,83,110,240,153,45,240,80,217,230,124,1,173,245,228,108,128,150,212,88,112,50,219,227,92,178,97,19,74,208,98,210,138,36,125,162,200,166,178,128,151,148,68,21,7,0,0,0,0,0,0,0,2,193,60,54,65,186,63,195,110,106,98,245,110,90,75,138,31,101,29,197,217,220,40,11,211,73,213,228,208,38,109,8,23,7,109,101,115,115,97,103,101,11,115,101,116,95,109,101,115,115,97,103,101,0,1,6,5,104,101,108,108,111,64,13,3,0,0,0,0,0,100,0,0,0,0,0,0,0,189,151,96,102,0,0,0,0,2,0,44,220,236,102,164,139,89,108,146,48,36,16,47,237,165,238,117,145,151,254,132,76,91,165,96,19,211,185,169,146,135,182], "feePayerAddress":"0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "feePayerSignature":{ "type":"ed25519_signature", "public_key":"0x57b9d494d31ed56f902362eef6e9d4fa8d22e390ab921bcb1678c8ec915ecbcf", "signature":"0x894063384997bfa3d29e42e01d9306297784a14b67baf102d501c6f3cfacd784c8dc7ae3f5abd8e3620b09011b09f9db044d25900217e9923c15f7d7876da000" } }, "id":1 } ``` ```bash cURL - submit sponsored tx example expandable theme={null} { "expiration_timestamp_secs" : "1718671754", "gas_unit_price" : "100", "hash" : "0xa37344e691317b9d034f0995ad693090c35fe46d69fc4c8b1821ff24373cea81", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "hello" ], "function" : "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "sender" : "0xadf5e46c8096d4587032dbe35cb261134ad062d28a247da2c8a6b28097944415", "sequence_number" : "9", "signature" : { "fee_payer_address" : "0x11045a6f53a6e9064fd92b45038649c334fd1afd60848a090facab14e07cf70a", "fee_payer_signer" : { "public_key" : "0x7dfd1009d1dff08cd60e900c1eccda628808016e5885bc82a66f4d97ba959594", "signature" : "0x743b061bf0cc3629df36962a7374682f026fad09d0a51a0138dcbc9b6f4cf748f853807b2a139a90466b9653685b136542a7cd0f8725038a2c060f0edfd96507", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0x8df7fae1ff10bb4135d700c1d787f4b8f1896001fcf78e19e059530c138f2f67", "signature" : "0x4b9bc91c2c6886c25a007e660bc7c457f851fe68a90896f5e3905f5ac2c256af918cf2afc3237d70ad24360ee9e86bdf053a5d7d85ea59d60bd387c3d67dd102", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } ``` **Response Fields** | Name | Type | Description | | :------------------------ | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | transactionSigningMessage | Unsigned byte array | Encoded message bytes for the transaction. To be signed by the sender and all secondary signers. | | feePayerAddress | Hex string | Fee payer account address. To be used to fill in `signature.fee_payer_address` property in the JSON transaction submission API. | | feePayerSignature | object | [AccountSignature](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/ecosystem/typescript/sdk/src/generated/models/AccountSignature.ts#L19) of the fee payer. To be used to fill in `signature.fee_payer_signer` property in the JSON transaction submission API. | ## Appendix ### How to build, sponsor, sign, and submit a transaction This section shows an example of how to build, sponsor, sign, and submit a simple transaction using the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) and the [Aptos TS SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk). For a high-level image of the sponsorship flow [see here](/product-overviews/aptos/gas-station). For multiple end-to-end examples, see our [Gas Station TypeScript Tutorial](/developer-guides/aptos/tutorials/gas-station-backend-only). **Key requirements** 1. Build your transaction with the fee payer address set to `0x0` (this is what happens when you set `withFeePayer: true` as shown below). The sender and any secondary signers can sign the transaction with this address, or with the actual feePayer's address you obtain from Shinami upon a successful sponsorship. 2. 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it as in the example below. Replace all instances of `{{name}}` with the actual value for that name. ```bash TypeScript expandable theme={null} // 1. Import the required dependencies import { GasStationClient } from "@shinami/clients/aptos"; import { Account, Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; // 2. Create an Aptos client and generate an account to act as the sender const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); // 3. Generate a sender. In production, you won't be using a newly generated account // each time. The sender will often be a wallet the user connected or an embedded // wallet you control for the user. This just makes for a shorter example. const sender = Account.generate({}); // 4. Create a Shinami Gas Station client for sponsoring our transactions. const SHINAMI_TESTNET_GAS_KEY = "{{APTOS_TESTNET_GAS_STATION_ACCESS_KEY}}"; const gasStationClient = new GasStationClient(SHINAMI_TESTNET_GAS_KEY); // 5. Build a transaction // This transaction makes a function call to a module we've deployed on Testnet const FIVE_MINUTES_FROM_NOW_IN_SECONDS = Math.floor(Date.now() / 1000) + (5 * 60); const transaction = await aptos.transaction.build.simple({ sender: sender.accountAddress, withFeePayer: true, data: { function: "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", functionArguments: ["test message"] }, options: { expireTimestamp: FIVE_MINUTES_FROM_NOW_IN_SECONDS } }); // 6. Obtain the sender's signature const senderAuthenticator = aptos.transaction.sign({ signer: sender, transaction }); /// /// STEP 7 OPTION 1: SPONSOR AND SUBMIT IN ONE REQUEST /// // 7a. Ask Shinami to sponsor and submit the transaction const pendingTx = await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator ); /// END STEP 7 OPTION 1 /// /// STEP 7 OPTION 2: SPONSOR AND SUBMIT IN TWO REQUESTS /// To run this version, un-comment the code for 7b and 7b.2 below, and comment out the code for 7a above // 7b.1 Send the transaction to Shinami for sponsorship // const feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // console.log("transaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress); // Note that our SDK updates the transaction's feePayer address on a successful sponsorship. // The sender and any secondary signers can sign the transaction with the actual feePayer's // address or with the special `0x0` feePayer address given when the feePayer transaction is // built (which we do above). However, the transaction must be submitted with the actual // feePayer's address. If you don't use our TS SDK, you must explictly set it with the // `feePayer.address` value we return when you ask for a sponsorship. // 7b.2. Submit the transaction to the Aptos blockchain // const pendingTx = await aptos.transaction.submit.simple({ // transaction, // senderAuthenticator, // feePayerAuthenticator, // }); /// END STEP 7 OPTION 2 // 8. Poll the Aptos node until the transaction is finalized const executedTransaction = await aptos.waitForTransaction({ transactionHash: pendingTx.hash }); console.log("Transaction hash:", executedTransaction.hash); console.log("Transaction status:", executedTransaction.vm_status); console.log("Explorer link:", `https://explorer.aptoslabs.com/txn/${executedTransaction.hash}/userTxnOverview?network=testnet`); ``` # Invisible Wallet API Source: https://docs.shinami.com/api-docs/aptos/wallet-services/invisible-wallet-api App-controlled, embedded wallets for a smooth UX ## Overview Shinami’s Invisible Wallets abstract away Web3 elements like seed phrases, third-party wallet connections, gas fees, and signing popups. They are embedded, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature. You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Core use cases include app-managed NFTs or closed-loop tokens. For a breakdown of the wallets we offer and wallet use-cases, see our [high-level guide](/product-overviews/aptos/wallets). ### Shinami Gas Station Integration All methods below that write to the Aptos blockchain have their gas fees sponsored by you via a Gas Station fund you create. This is because Invisible Wallets are designed to easily onboard Web2-native users (who may not want to download a wallet app, manage a seed phrase, and complete KYC checks to buy APT for gas). See the [Aptos Gas Station FAQ page in our Help Center](/help-center/aptos/gas-station-faq) for how guidance on how to set up a fund. ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header ('X-Api-Key: ACCESS\_KEY') or in the request url (`https://api.us1.shinami.com/aptos/wallet/v1/ACCESS_KEY`). We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for how to set up a Wallet-Services-only access key and an access key with Wallet Services and Gas Station rights (needed to execute transactions and initialize Invisible Wallets on chain). **Call this API from your backend** Shinami Wallet Services do not support CORS requests, so if you make requests to these APIs from your frontend you'll get a CORS error. This is for security reasons: exposed keys and wallet information could lead to malicious actors signing transactions on behalf of your users. **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#aptos-invisible-wallet-api). ### WalletId and Secret Pairing **When you create an Invisible Wallet, you must create, store, link, and never change the following two values**: * `walletId`: Your internal id for a wallet. When you provide us a `walletId` in a method call, it tells us which Invisible Wallet to use. It could be your internal `userId` value, or a new arbitrary and unique value you link to the `userId`. * `secret`: Your internal secret for a wallet. The `sessionToken` you generate with it is combined with Shinami data to obtain a signature from the associated wallet. Ideally it would be different for each wallet so that if one `secret` is compromised the rest are not. **When you create an Invisible Wallet, you forever link its walletId it to the secret you used:** **So, if you try to use the walletId with a different secret, you'll get an error:** ### Tutorial with E2E sample code Check out our [TypeScript tutorial](/developer-guides/aptos/tutorials/invisible-wallets) for more code samples and details on the end-to-end flow of creating and using Invisible Wallets to execute sponsored transactions. ## Methods ### key\_createSession For security purposes, you must generate a session token before you create a wallet, or sign/execute transactions. Session tokens are valid and can be reused for 10 minutes. You may also use an instance of `ShinamiWalletSigner` to manage session token generation and refreshes for a given wallet. This is shown in the methods below that have a `sessionToken` parameter in an additional sample code tab. **Request Parameters** | Name | Type | Description | | :----- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secret | string | Used to encrypt and decrypt a wallet's private key. Therefore, it must always be used with the same `walletId` and cannot be changed in the future (see [walletId and secret pairing](/api-docs/aptos/wallet-services/invisible-wallet-api#walletid-and-secret-pairing)) | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/key/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"key_createSession", "params":[ "{{secret}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { KeyClient } from "@shinami/clients/aptos"; const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{secret}}); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w", "id": 1 } ``` ```TypeScript Shinami TypeScript SDK theme={null} "eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w" ``` **Response Data** | Type | Description | | :----- | :--------------------------------------------------------------------------------------------- | | string | `sessionToken` corresponding to the provided `secret`. Valid and can be reused for 10 minutes. | ### wal\_createWallet Creates a new Shinami Invisible Wallet you control, but does not yet initialize the address on an Aptos network (e.g. Mainnet). Generally, you can wait to initialize a wallet if you wish (see more in [the description of `wal_initializeWalletOnChain`](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-initializewalletonchain)). **Wallet creation limit** On the free tier you have a limit of wallet creations per month as shown on the "Aptos Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#aptos-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Request Parameters** | Name | Type | Description | | :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/aptos/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_createWallet", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}"; const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.createWallet( {{walletID}}, sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Return the Aptos wallet address associated with the // walletId, or create an Invisible Wallet tied to that // walletId if none is found and return its address. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN_IF_CREATING = false; const accountAddress = await signer.getAddress( CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN_IF_CREATING ); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Aptos address of created wallet. | ### wal\_initializeWalletOnChain Initializes a Shinami Invisible Wallet on the Aptos network the Gas Station rights of your access key is associated with (e.g. Mainnet). This comes after a call to [`wal_createWallet`](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-createwallet) but is not always needed. That's because: 1. Initialization happens automatically if you send a transaction with an uninitialized wallet as the sender (e.g. calling [`wal_executeGaslessTransaction`](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-executegaslesstransaction)) . 2. Also, if something is transferred to an address you control th e keys for (e.g. an NFT) before you initialize it, you'll still be able to control that NFT after you initialize it. **Important notes** 1. **Gas Station fund required:** You need a Gas Station fund with APT in order to sponsorship transactions for an Invisible Wallet, including the transaction that explicitly initializes it on chain. For information on how to set this up, see the [Aptos Gas Station FAQ page in our Help Center](/help-center/aptos/gas-station-faq). 2. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). **Request Parameters** | Name | Type | Description | | :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/aptos/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_initializeWalletOnChain", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.initializeWalletOnChain( {{walletID}}, sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Return the Aptos wallet address associated with the // walletId, or create an Invisible Wallet tied to that // walletId if none is found and return its address. // Initialize it on the network the access key's Gas // Station rights are associated with. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN_IF_CREATING = true; const accountAddress = await signer.getAddress( CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN_IF_CREATING ); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Aptos address of created wallet. | ### wal\_createWalletOnChain Creates and initializes a Shinami Invisible Wallet onto the Aptos chain. Generally, you can wait to initialize a wallet if you wish (see more in [the description of `wal_initializeWalletOnChain`](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-initializewalletonchain)). We provide a variety of wallet creation and initialization methods to give you flexibility. **Important notes** 1. **Wallet creation limit:** On the free tier you have a limit of wallet creations per month as shown on the "Aptos Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#aptos-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. 2. **Gas Station fund required:** You need a Gas Station fund with APT in order to sponsorship transactions for an Invisible Wallet, including the transaction that explicitly initializes it on chain. For information on how to set this up, see the [Aptos Gas Station FAQ page in our Help Center](/help-center/aptos/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). **Request Parameters** | Name | Type | Description | | :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/aptos/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_createWalletOnChain", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.createWalletOnChain( "{{walletID}}", sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Return the Aptos wallet address associated with the // walletId, or create an Invisible Wallet tied to that // walletId if none is found and return its address. // Initialize it on the network the access key's Gas // Station rights are associated with. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN_IF_CREATING = true; const accountAddress = await signer.getAddress( CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN_IF_CREATING ); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Aptos address of created wallet. | ### wal\_getWallet Gets the address of an existing Invisible Wallet, regardless of whether it is initialized or uninitialized on chain. **Request Parameters** | Name | Type | Description | | :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with:1 ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_getWallet", "params": [ "{{walletId}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const accountAddress = await walletClient.getWallet( {{walletID}} ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}"; const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const accountAddress = await signer.getAddress(); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Aptos address of created wallet. | ### wal\_signTransaction Use an initialized Shinami Invisible Wallet to sign a transaction. When the wallet is the sender and you are sponsoring as the feePayer, use `wal_executeGaslessTransaction`, which does all of: sign, sponsor, and submit the transaction to the Aptos network. **Request Parameters** | Name | Type | Description | | :---------------------------------------- | :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/aptos/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | | ***SDK-only***
transaction | AnyRawTransaction | A [`SimpleTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/simpleTransaction.ts#L15) or a [`MultiAgentTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/multiAgentTransaction.ts#L14) *that does not have a fee payer*. You can find an example of building a transaction [in our tutorial](/developer-guides/aptos/tutorials/invisible-wallets#5-generate-a-feepayer-transaction) | | ***cURL-only***
rawTransaction | Hex string \| unsigned byte array | BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110). Can be passed either as an unsigned byte array or a Hex string. For a Python example of how to serialize this to send to Shinami, see the `Aptos Python SDK: rawTransaction` tab of the Example Request Templates. | | ***cURL-only***
secondaryAddresses | string\[] | (Optional) Required for multi-agent transactions. Array of addresses of the secondary signers for the transaction. Must be in the exact order. | | ***cURL-only***
feePayerAddress | string | (Optional) Address of a feepayer for the rawTransaction. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_signTransaction", "params": [ "{{walletId}}", "{{sessionToken}}", "{{rawTransaction}}", "{{(optional)secondardAddresses}}", "{{(optional)feePayerAddress}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); // sign a non-feePayer transaction const accountAuthenticator = await walletClient.signTransaction( {{walletID}}, sessionToken, {{transaction}} ); ``` ```TypeScript Shinami TypeScript SDK: ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // sign a non-feePayer transaction const accountAuthenticator = await signer.signTransaction({{transaction}}); ``` ```Python Aptos Python SDK: rawTransaction theme={null} # Here is an example of preparing a `RawTransaction` to be sent # as the `rawTransaction` parameter from aptos_sdk.bcs import Serializer ... # Step 1: # Build your RawTransaction with your Invisible Wallet address as the # sender address. Below, we represent your RawTransaction # with the variable `raw_transaction` # Step 2: # Prepare your RawTransaction to send to Shinami's API. # Two options are shown below # Option 1: send as an unsigned byte array shinami_rawTransaction = list(raw_transaction.to_bytes()) # Option 2: send as a hex string transaction_serializer = Serializer() raw_transaction.serialize(transaction_serializer) shinami_rawTransaction = transaction_serializer.output().hex() ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "signature":[0,32,136,128,248,41,24,171,70,49,247,26,7,233,63,120,178,130,84,171,58,188,184,203,137,23,210,92,89,62,182,10,55,2,64,239,224,55,68,196,84,93,88,60,245,168,112,48,199,204,225,105,109,15,6,64,145,58,175,225,89,230,38,71,179,223,193,212,114,240,142,212,165,123,209,71,50,202,29,114,133,212,40,240,123,38,189,76,4,208,237,156,135,159,154,143,122,206,9] , "id":1 } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAuthenticator // https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAuthenticator // https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | | `{ "signature": Unsigned byte array }` | [AccountAuthenticator](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506) | The Invisible Wallet's signature for the transaction. | **Notes for signing a non-sponsored transaction:** * The Invisible wallet must be initialized on chain. * If you attempt to sign *immediately* after initializing a wallet you may get an error that the address was not found on chain, as it take a couple of seconds for the initialization to be processed by the Aptos blockchain. ### wal\_executeGaslessTransaction Sponsors, signs, and executes a gasless transaction from a Shinami Invisible Wallet. This is a convenient end-to-end method for submitting sponsored transactions to the Aptos chain as opposed to doing all the steps individually. **Important notes** 1. **Gas Station fund required:** You need a Gas Station fund with APT in order to sponsorship transactions for an Invisible Wallet. For information on how to set this up, see the [Aptos Gas Station FAQ page in our Help Center](/help-center/aptos/gas-station-faq). 2. **Shinami sponsorship fees:** See our [Aptos Billing FAQ](/help-center/billing/aptos-faq#gas-station). 3. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. **Request Parameters** | Name | Type | Description | | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/aptos/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | | ***SDK-only***
transaction | AnyRawTransaction | A [`SimpleTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/simpleTransaction.ts#L15) or a [`MultiAgentTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/multiAgentTransaction.ts#L14). You can find an example of building a transaction [in our tutorial](/developer-guides/aptos/tutorials/invisible-wallets#5-generate-a-feepayer-transaction) | | ***cURL-only***
rawTransaction | Hex string \| unsigned byte array | BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110). Can be passed either as an unsigned byte array or a Hex string. For a Python example of how to serialize this to send to Shinami, see the `Aptos Python SDK: rawTransaction` tab of the Example Request Templates. | | secondarySigners | *SDK* AccountAuthenticator\[]*cURL* \["address" : hex string, "signature": BCS-serialized AccountAuthenticator(Can be passed either as an unsigned byte array or a hex string.)] | (optional) Array of additional signers and their signatures. Must be in the exact order. Required for multi-agent transactions. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL-template theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_executeGaslessTransaction", "params": [ "{{walletId}}", "{{sessionToken}}", "{{rawTransaction}}", [{ "address": "{{secondarySignerOneAddress}}", "signature": "{{secondarySignerOneSignature}}" }] ], "id": 1 }' ``` ```bash cURL- example theme={null} curl https://api.us1.shinami.com/aptos/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"wal_executeGaslessTransaction", "params":[ "{{walletID}}", "eyJraWQiOiJrZXkyMDI0MDUwMiIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiIzY1dmODVTQnB1X0VseG5fS19yNTVnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJMdTRxX1Y3aG9fWDdyYXZWIn0.whquEkYQM1GBK3d26VoZNuxCuI1gQMMAA5tGWvPzoGM.mUtlN_sbR9i5rhmX.Ow_jH6bifeciMSxxK49fOLZ5DY3SJ-SnvcYEjmfnJQEIdnbKOOgK7KKjln8QUNgQpIrPtOm5yVNmo8p8Pg60uA7M7a4qBwDHh3_5ffdjAbaW6IYdbfcmDy5ydWrC84nHKLpgSMWHwEltkfE.KnjXE3IAYFX67hycEoH9gQ", "0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de0200000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d0300000000006400000000000000b51f8c660000000002", [{ "address": "0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d", "signature": "0x0020924795a3b1508fa51bed2b8539075eb410665114422aa80aac88732e3b92adbf4064a77a74aad50c1229dd65543d9d1cdd96542484328188ecaba585de6e22aff5de3b5bb65f76a630e5ab1ff2e32e62b59d31e65206b8e224f397db5bb36bc500" }] ], "id":1 }' ``` ```TypeScript Shinami TS SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const pendingTx = await walletClient.executeGaslessTransaction( {{walletID}}, sessionToken, {{transaction}}, [{{secondarySignatureOne}}, ...] ); ``` ```TypeScript Shinami TS SDK: ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const pendingTx = await signer.executeGaslessTransaction( {{transaction}}, [{{secondarySignatureOne}}, ...] ); ``` ```Python Aptos Python SDK: rawTransaction theme={null} # Here is an example of preparing a `RawTransaction` to be sent # as the `rawTransaction` parameter from aptos_sdk.bcs import Serializer ... # Step 1: # Build your RawTransaction with your Invisible Wallet address as the # sender address. Below, we represent your RawTransaction # with the variable `raw_transaction` # Step 2: # Prepare your RawTransaction to send to Shinami's API. # Two options are shown below # Option 1: send as an unsigned byte array shinami_rawTransaction = list(raw_transaction.to_bytes()) # Option 2: send as a hex string transaction_serializer = Serializer() raw_transaction.serialize(transaction_serializer) shinami_rawTransaction = transaction_serializer.output().hex() ``` **Example Response** ```bash cURL - template expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1720213435", "gas_unit_price" : "150", "hash" : "0xfee720e44614f8164da7d58333ef3947abbe997e3ac10451ee9a56f9788d8e34", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "hello" ], "function" : "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "sender" : "0x35d86428a7aee9863f9aa52d6aa4b583e2bcb7ee9d3e7d1cf1b6a7e13da503af", "sequence_number" : "4", "signature" : { "fee_payer_address" : "0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "fee_payer_signer" : { "public_key" : "0x57b9d494d31ed56f902362eef6e9d4fa8d22e390ab921bcb1678c8ec915ecbcf", "signature" : "0x1bb1186530bba89d8fa0d2706aba8d9908b11f7249f947e8833ecddd5dbb73f75fb8e325188631778d4ea4c8d10fafa03309f01b5373f1c5cac150b48bbddc09", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0x8880f82918ab4631f71a07e93f78b28254ab3abcb8cb8917d25c593eb60a3702", "signature" : "0xb67b702ab701bf88800a764639a495b11baf3d1df3553030e33f4ef92342d2962d867be5be2731cf6d3bbfe3281f99a641c9f900185c90787448d1284b2e7e0e", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } } } ``` ```bash cURL - multiAgent example expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1720459189", "gas_unit_price" : "100", "hash" : "0xd30736a7a854060ff449ea67451863e6134137303f7ac3439a16d46df1f9413e", "max_gas_amount" : "200000", "payload" : { "arguments" : [], "code" : { "abi" : { "generic_type_params" : [], "is_entry" : true, "is_view" : false, "name" : "main", "params" : [ "&signer", "&signer" ], "return" : [], "visibility" : "public" }, "bytecode" : "0xa11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b03380102" }, "type" : "script_payload", "type_arguments" : [] }, "sender" : "0x6c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de", "sequence_number" : "2", "signature" : { "fee_payer_address" : "0x11045a6f53a6e9064fd92b45038649c334fd1afd60848a090facab14e07cf70a", "fee_payer_signer" : { "public_key" : "0x7dfd1009d1dff08cd60e900c1eccda628808016e5885bc82a66f4d97ba959594", "signature" : "0x6ef8afa108df6ae0c50486dc0356e9e16abb9115d99ba6fac64a5fbcdd3e85c1775e3031511679458b4a574f55dd357919278acafc63dc124b9a70a663e8c706", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [ "0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d" ], "secondary_signers" : [ { "public_key" : "0x924795a3b1508fa51bed2b8539075eb410665114422aa80aac88732e3b92adbf", "signature" : "0x64a77a74aad50c1229dd65543d9d1cdd96542484328188ecaba585de6e22aff5de3b5bb65f76a630e5ab1ff2e32e62b59d31e65206b8e224f397db5bb36bc500", "type" : "ed25519_signature" } ], "sender" : { "public_key" : "0xdc27624c0bf8ee11c8d6bf54e182b12a8f876eb56e66df740cb4de7adefbfa57", "signature" : "0xf3bd6086a1381d36ce46dae5bf6aff2d9652a7d7ba846dacbb67b858ab20a6bf230c8f5b9c6b59d27b41d00ef0f8ec0b29e7e0335d4a4dbac4e2a6469375f50f", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } } } ``` ```TypeScript Shinami TS SDK expandable theme={null} { hash: '0x7a337a8952378b6a9174605b56c258f15b18f3a337ba766c02a624759f401c9f', sender: '0x35d86428a7aee9863f9aa52d6aa4b583e2bcb7ee9d3e7d1cf1b6a7e13da503af', sequence_number: '2', max_gas_amount: '200000', gas_unit_price: '150', expiration_timestamp_secs: '1720131046', payload: { function: '0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message', type_arguments: [], arguments: [ 'hello' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x8880f82918ab4631f71a07e93f78b28254ab3abcb8cb8917d25c593eb60a3702', signature: '0x3a446c1586ccec7bb98ad2010c7a13a238b163e096343ae7e9b858b2bd6de6cd5e7beaa6dccd67154bc5014aae4296ce13eda82668c643de99cfd547560f7801', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e', fee_payer_signer: { public_key: '0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7', signature: '0x8bb1f219fa65e036953c0ecf2d6aad017a4efd76c6ea493ac42c36ca93e8bbbe256d97ddd2cd2dc7f6e8b8bd5437041eec0cdfa18e52b0c6591d03f3c0061a02', type: 'ed25519_signature' }, type: 'fee_payer_signature' } } ``` ```TypeScript Shinami TS SDK (ShinamiWalletSigner) expandable theme={null} { hash: '0xd84a42c447049501d1ef334012c39b4602eb977df1c466652f59a1387c395b8f', sender: '0x35d86428a7aee9863f9aa52d6aa4b583e2bcb7ee9d3e7d1cf1b6a7e13da503af', sequence_number: '1', max_gas_amount: '200000', gas_unit_price: '150', expiration_timestamp_secs: '1720130820', payload: { function: '0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message', type_arguments: [], arguments: [ 'hello' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x8880f82918ab4631f71a07e93f78b28254ab3abcb8cb8917d25c593eb60a3702', signature: '0xccc9544d93c9563195d1596fbeeec33ae041439752114504d10592821cf3f24bd0bc006671c88e83e0449b42b41b5c06ab6f16ea5e70e8d945a0887584334207', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e', fee_payer_signer: { public_key: '0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7', signature: '0x607a3b86faf10153fd2afcb1ccec258f8e35dfb985fe2725efc77859a3f0b1e9c4e79de71c8357d8f9ee6f8508db7e0042dec270355a34c3d6d2964edd5bfb0e', type: 'ed25519_signature' }, type: 'fee_payer_signature' } } ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :--------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | | `{ "pendingTransaction": object with the same shape as a `PendingTransactionResponse` }` | [PendingTransactionResponse](https://github.com/aptos-labs/aptos-ts-sdk/blob/de936697bdc552eb9c88e676d85686449dba3467/src/types/index.ts#L333-L343) | The submitted transaction waiting in mempool. | # Gas Station API Source: https://docs.shinami.com/api-docs/movement/gas-station/api Increase user engagement and retention by sponsoring transactions ## Overview You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). Movement is a [fork of Aptos](https://github.com/movementlabsxyz/aptos-core), so developing for both of these chains is very similar. If you're getting started with Movement, they have some [developer docs](https://docs.movementnetwork.xyz/general) to help. ### Use Cases Shinami's Gas Station API lets you easily sponsor transactions for your users. MOVE is pulled from a fund you create to power these transactions, managed in Shinami's dashboard. Shinami facilitates sponsored transactions based on logic determined by you, the app developer. Examples include sponsoring each user's initial transaction(s) for better onboarding and sponsoring transactions of a particular type that you want to encourage. ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header `"X-Api-Key: ACCESS_KEY"`) or in the request url (`https://api.us1.shinami.com/movement/gas/v1/ACCESS_KEY`). We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for how to set up a Gas-Station access key. **Call Shinami's Gas Station from your backend server** Shinami Gas Station does not support CORS requests, so you will get a CORS error if you make requests from your frontend. Use your backend server to integrate with Shinami's Gas Station. This limits exposure of your sponsorship 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. For a tutorial on how to combine frontend signing with backend sponsorship, see our [Frontend signing + backend sponsorship tutorial](/developer-guides/movement/tutorials/gas-station-with-frontend-signing). **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also consider batching multiple Move calls into a [Move script](https://aptos.dev/build/smart-contracts/scripts). You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Movement Gas Station API](/developer-guides/core-integration-topics/error-reference#movement-gas-station-api). ### Send your first request For a quick sample request that doesn't require building a transaction, ask for the balance of the fund your access key is tied to with [gas\_getFund](/api-docs/movement/gas-station/api#gas-getfund). ### Tutorials with E2E sample code Check out our [TypeScript tutorial](/developer-guides/movement/tutorials/gas-station-backend-only) for more code samples and details on the end-to-end flow of creating, sponsoring, and executing a transaction. Also, since our Gas Station does not support CORS (browser) requests for security reasons, our [Frontend signing + backend sponsorship tutorial](/developer-guides/movement/tutorials/gas-station-with-frontend-signing) . ## Methods ### gas\_sponsorTransaction Sponsor a transaction by having your Shinami Gas Station fund act as the feePayer. **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/movement/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/movement/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Movement Gas Station Help Center page](/help-center/movement/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). 4. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. **Request Parameters** | Name | Type | Description | | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rawTransaction | ***TypeScript SDK*** SimpleTransaction \| MultiAgentTransaction

***cURL***
String \| byte array | ***TypeScript SDK***
An AnyRawTransaction (the result of a successful call to `aptos.transaction.build.simple()` or `aptos.transaction.build.multiAgent()`). For a code example of building and sponsoring a transaction, see [the Appendix](/api-docs/movement/gas-station/api#appendix).

***cURL***
BCS-serialized [`RawTransaction`](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110) component of an AnyRawTransaction. Can be passed either as an unsigned byte array or a hex string.

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | | ***cURL-only***
secondarySignerAddresses | String\[] | Optional array of additional signer addresses. Must be in the exact order. Required for multi-agent transactions. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const accountAuthenticator = await gasStationClient.sponsorTransaction(transaction); ``` ```bash cURL - template theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorTransaction", "params":[ "{{rawTransaction}}", ["{{secondarySignerOne}}"] ], "id":1 }' ``` ```bash cURL - example theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorTransaction", "params":[ "0x3de05e9561457529d7b46a93bfa78d9927a403e51a61dc65c5d9ea3e6ec319e80000000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d0300000000006400000000000000e31b56660000000002", ["0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d"] ], "id":1 }' ``` **Example Response** ```bash Shinami TypeScript SDK theme={null} { public_key: { key: { data: [Object] } }, signature: { data: { data: [Object] } } } ``` ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "feePayer":{ "address":"0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e", "signature":[0,32,137,149,98,199,215,190,68,86,33,155,108,8,235,180,176,251,229,173,69,37,243,215,74,81,79,247,57,14,157,222,179,167,64,11,221,70,103,95,161,7,160,34,170,143,169,187,20,28,194,66,217,163,204,96,189,220,180,217,26,70,92,244,46,255,149,50,33,61,244,60,172,91,48,234,186,24,39,86,113,159,161,73,155,69,51,180,245,42,234,141,119,159,54,227,154,154,9] } }, "id":1 } ``` **TypeScript SDK Response Fields** **Note:** transaction.feePayerAddress is updated in-place upon a successful return. | Type | Description | | :------------------- | :--------------------------------------------------------------------------------------------------------- | | AccountAuthenticator | AccountAuthenticator of the sponsor, to be included in the request to submit the transaction to the chain. | **cURL Response Fields** | Name | Type | Description | | :----------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | feePayer.address | String | Fee payer account address. If desired you can use this to set the transaction's `feePayerAddress` before obtaining sender and any secondary signer signatures. | | feePayer.signature | Unsigned byte array | BCS-serialized AccountAuthenticator of the fee payer, as an array of unsigned bytes. | ### gas\_sponsorAndSubmitSignedTransaction This method first sponsors a transaction with your Shinami Gas Station fund and then submits the transaction to a Movement fullnode for execution. Therefore, you need to obtain all necessary signatures - with wildcard fee payer address `0x0` - *before* calling this method. **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/movement/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/movement/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Movement Gas Station Help Center page](/help-center/movement/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). 4. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. **Request Parameters** | Name | Type | Description | | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rawTransaction | ***SDK***
SimpleTransaction \| MultiAgentTransaction

***cURL***
Hex string \| Unsigned byte array. | ***TypeScript SDK***
(The result of a successful call to `aptos.transaction.build.simple()` or `aptos.transaction.build.multiAgent()`). For a code example of building and sponsoring a transaction, see [the Appendix](/api-docs/movement/gas-station/api#how-to-build%2C-sponsor%2C-sign%2C-and-submit-a-transaction) .

***cURL***
BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110) component of an AnyRawTransaction. Can be passed either as an unsigned byte array or a hex string.

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | | senderSignature | ***SDK***
AccountAuthenticator

***cURL***
BCS-serialized [AccountAuthenticator](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506) . Can be passed either as an unsigned byte array or a hex string. | The signature of the sender over the transaction with wildcard fee payer address (`0x0`). | | secondarySigners | ***SDK***
AccountAuthenticator\[]

***cURL***
\["address" : \, "signature": \(Can be passed either as an unsigned byte array or a hex string.)] | Optional array of additional signers and their signatures. Must be in the exact order. Required for multi-agent transactions | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const pendingTx = await gasStationClient.sponsorAndSubmitSignedTransaction( {{transaction}}, {{senderSignature}}, {{[secondarySignatureOne, secondarySignatureTwo...]}} ); ``` ```bash cURL template theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorAndSubmitSignedTransaction", "params":[ "{{rawTransaction}}", "{{senderSignature}}", [{"address": "{{secondarySenderAddress}}","signature": "{{secondaryAddressSignature}}"}] ], "id":1 }' ``` ```bash cURL -- multiAgentMoveScript example theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorAndSubmitSignedTransaction", "params":[ "0x405a6cf78cc40a0cb463f7d349df8ef4aa5ed882a97dad8da452f7331c974b6e0400000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d030000000000640000000000000025d8e26800000000fa01e06043f8671f25fe4eb248ee1c532d88c08d3091d72e66724b33520538840f90012cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "0x020020079f710d4487dcb34808a83f4aff678c43b6589073b94875b57fe15f3249742a0040b16d000c79e77c8913fd9b1e7ac8cedc4017b31db0d9653980ee47a0a35651cb0a7006cc689617b5236143869b31b2276409f3f4660214a5fea5bbeda654e60b", [{ "address": "0xe06043f8671f25fe4eb248ee1c532d88c08d3091d72e66724b33520538840f90", "signature": "0x02002089977d7515cb24bc9d8879bb88a6f4aa354bab7cbecd2fcb5f80f2d6f70c69390040963c0c1f0bcf529b2edf9a7d2de3560b40a67158e4c7c6bd56b1bfd61a4dfff06273df0fcfa547054754d6a7ac2cd2a941b3380ccc735f0999afcc7ca1ce2d0d" }] ], "id":1 }' ``` **Example Response** ```bash Shinami TypeScript SDK expandable theme={null} { hash: '0x043b3d7314671041082f891786b5981c27468e7cb4313ea7aa332bf28c54f087', sender: '0x1be0888d5213fb02b058b42b07726112370a041c2ef15727c9feaa5c92376a6c', sequence_number: '0', max_gas_amount: '200000', gas_unit_price: '100', expiration_timestamp_secs: '1759506438', payload: { function: '0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message', type_arguments: [], arguments: [ 'test message' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x0f1d83ac143a381a9a4ca5eaddd3ae02b9e99ad7b7a92f762170dad2c6ce3150', signature: '0x7d7aa38ddc1a9b1a4455cb52ef703afb8371aeb14eb581fe6a3f98bdef3169810d3e4a2e1ce5c6f1ad0fe52d364f8778081b5cc956c575e37164439dd49cda00', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e', fee_payer_signer: { public_key: '0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7', signature: '0xcdaca8ceb0026fa3a7da2bc96b6ecfcdb581f4de7992f29a805176fc82017edd0afe7ca9d930b94e4ecbee15196a1cb9cf64a6e5ab813e1c0844ee0b0de0b303', type: 'ed25519_signature' }, type: 'fee_payer_signature' }, replay_protection_nonce: null } ``` ```bash cURL -- simple transaction expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1759506750", "gas_unit_price" : "100", "hash" : "0x1eeb452faa6f294e32032a7b0fef698df072f3efec175e81bd28b70e92c664a4", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "test message" ], "function" : "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "replay_protection_nonce" : null, "sender" : "0x23ee130215c3772be60e7dbcd39b1d7f7d4e380c3770afb664fbc4e5717bec18", "sequence_number" : "0", "signature" : { "fee_payer_address" : "0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "fee_payer_signer" : { "public_key" : "0x57b9d494d31ed56f902362eef6e9d4fa8d22e390ab921bcb1678c8ec915ecbcf", "signature" : "0xfbcb6f5893825d9ca7ac1122c6c340854991aa4ac2316771ed35265d24351ecaa9655c88b5e675363e8418cef223de879160847cec899395e3d5030bbabc870f", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0x53018c45078771d1906af7956c82a0a2f56607aed9b3ddd0d922ba4e3406d2b8", "signature" : "0x5db6bb5f0903d2a4b0e243d0bbdaba61142817f5dbe6b7df24c5de7bfca8df55d74bf92ff756bb4720ce85ae1a4aea7682eec868956e9dbd4c4a22f6f98bec0a", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } } } ``` ```bash cURL -- multiAgent Move Script transaction theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1759696933", "gas_unit_price" : "100", "hash" : "0x5e5d5d943a132388beb63a64c8500feda4ca56c0b545bc473fdcd0c10fdab9f5", "max_gas_amount" : "200000", "payload" : { "arguments" : [], "code" : { "abi" : { "generic_type_params" : [], "is_entry" : true, "is_view" : false, "name" : "main", "params" : [ "&signer", "&signer" ], "return" : [], "visibility" : "public" }, "bytecode" : "0xa11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b03380102" }, "type" : "script_payload", "type_arguments" : [] }, "replay_protection_nonce" : null, "sender" : "0x405a6cf78cc40a0cb463f7d349df8ef4aa5ed882a97dad8da452f7331c974b6e", "sequence_number" : "4", "signature" : { "fee_payer_address" : "0x2cdcec66a48b596c923024102feda5ee759197fe844c5ba56013d3b9a99287b6", "fee_payer_signer" : { "public_key" : "0x57b9d494d31ed56f902362eef6e9d4fa8d22e390ab921bcb1678c8ec915ecbcf", "signature" : "0xa3db14b16a239d2a9e9104062bda5ee5136319b68589c40a13c2e4e879af9c1a84a076605a5bae17afcc270880b707b17a0f3cf5770522ffbad1d734d36e7c05", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [ "0xe06043f8671f25fe4eb248ee1c532d88c08d3091d72e66724b33520538840f90" ], "secondary_signers" : [ { "public_key" : { "type" : "ed25519", "value" : "0x89977d7515cb24bc9d8879bb88a6f4aa354bab7cbecd2fcb5f80f2d6f70c6939" }, "signature" : { "type" : "ed25519", "value" : "0x963c0c1f0bcf529b2edf9a7d2de3560b40a67158e4c7c6bd56b1bfd61a4dfff06273df0fcfa547054754d6a7ac2cd2a941b3380ccc735f0999afcc7ca1ce2d0d" }, "type" : "single_key_signature" } ], "sender" : { "public_key" : { "type" : "ed25519", "value" : "0x079f710d4487dcb34808a83f4aff678c43b6589073b94875b57fe15f3249742a" }, "signature" : { "type" : "ed25519", "value" : "0xb16d000c79e77c8913fd9b1e7ac8cedc4017b31db0d9653980ee47a0a35651cb0a7006cc689617b5236143869b31b2276409f3f4660214a5fea5bbeda654e60b" }, "type" : "single_key_signature" }, "type" : "fee_payer_signature" } } } } ``` **Response Fields** | Type | Description | | :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pendingTransactionResponse | The submitted transaction waiting in the Fullnode's [mempool](https://aptos.dev/network/blockchain/blockchain-deep-dive#mempool). Same as [PendingTransactionResponse](https://github.com/aptos-labs/aptos-ts-sdk/blob/de936697bdc552eb9c88e676d85686449dba3467/src/types/index.ts#L333-L343) . | ### gas\_getFund Get the balance for the Gas Station fund tied to the request's API access key. When you create a Gas Station access key, you [link it to exactly one Gas Station fund](/developer-guides/core-integration-topics/authentication-and-api-keys#3b-movement-key-configuration). So, when you make this request, we return the balance for the fund that's tied to the access key you use for the request. To check which fund a Gas Station access key is tied too, see the [Movement Gas Station FAQ page of our Help Center](/help-center/movement/gas-station-faq#how-do-i-check-which-fund-an-access-key-is-tied-to%3F) **Request Parameters** None. **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("{{gasStationAccessKey}}"); const fundInfo = await gasStationClient.getFund(); ``` ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_getFund", "params":[], "id":1 }' ``` **Example Response** ```bash Shinami TypeScript SDK theme={null} { network: 'MOVEMENT_TESTNET', name: 'First fund', balance: 2099485200, inFlight: 20000000, depositAddress: null // see comment in "Response Fields" section below } ``` ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "balance" : 999832400, "depositAddress" : null, // see comment in "Respone Fields" section below "inFlight" : 0, "name" : "test_fund1", "network" : "MOVEMENT_TESTNET" } } ``` **Response Fields** | Name | Type | Description | | :------------- | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | string | The name of the fund tied to this API access key. | | network | string | The network the fund is associated with. | | balance | integer | Available balance of the the fund (100000000 = 1 MOVE). | | inFlight | integer | The portion of the fund balance that is currently locked for use with active sponsorships (100000000 = 1 MOVE). | | depositAddress | string \| null | A string representing a Movement address if a deposit address has been created for the fund. null if no deposit address has been created. If you do not have a deposit address, see how to generate one in the [Movement Gas Station FAQ page in our Help Center](/help-center/movement/gas-station-faq#how-do-i-generate-and-find-the-deposit-address-of-a-fund%3F) . | ### gas\_encodeAndSponsorTransaction Sponsors a JSON transaction request by providing fee payer info, and encodes it into message bytes for signing. Intended to be used only by clients without BCS encoding capability. If you're using the Aptos TypeScript SDK you should use sponsorTransaction instead. The full transaction flow for using this API is as follows: 1. Construct the transaction JSON. 2. Call this API to get the encoded message bytes for signing, as well as fee payer address and signature. 3. Sender (and any secondary signers) sign the message bytes. 4. Submit the transaction JSON together with all signatures to a Movement fullnode for execution (an example of this call and response is shown in the `cURL - submit sponsored tx example` tabs). **Important notes** 1. **Gas Station doesn't support CORS, so you must send requests from your backend.** For an overview of all the ways to integrate frontend signing with backend sponsorship, see our [Frontend Signing + Backend Sponsorship guide](/developer-guides/movement/tutorials/gas-station-with-frontend-signing), including [tips on serializing and deserializing](/developer-guides/movement/tutorials/gas-station-with-frontend-signing#serializing-and-deserializing) the necessary data types as you pass them between FE and BE. 2. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station API access key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Movement Gas Station Help Center page](/help-center/movement/gas-station-faq). 3. **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). **Request Parameters** | Name | Type | Description | | :--------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | submission | object | [EncodeSubmissionRequest](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/ecosystem/typescript/sdk/src/generated/models/EncodeSubmissionRequest.ts#L12-L23) , same as the request body on Movement fullnode REST API `/transactions/encode_submission` (see documentation [here](https://aptos.dev/rest-api)).

**Important:** the transaction's expiration timestamp must be set to a time within the next hour, in seconds. Otherwise, our service will not sponsor the transaction. | **Example Request Template** Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL - encodeAndSponsor template expandable theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_encodeAndSponsorTransaction", "params":[{ "sender": "{{senderAddress}}", "sequence_number": "{{sequenceNumber}}", "max_gas_amount": "{{maxGasAmount}}", "gas_unit_price": "{{gasUnitPrice}}", "expiration_timestamp_secs": "{{timestamp}}", "payload": { "type": "{{payloadType}}", "function": "{{functionId}}", "type_arguments": [ "{{typeArgs}}" ], "arguments": [ {{arguments}} ] }, "secondary_signers": [ {{secondarySignerAddresses}} ] }], "id":1 }' ``` ```bash cURL - encodeAndSponsor example expandable theme={null} curl https://api.us1.shinami.com/movement/gas/v1/ \ -X POST \ -H 'X-API-Key: {{gasStationAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "gas_encodeAndSponsorTransaction", "params": [{ "sender": "0xf0ff587359e229fbcac9e4d98d5c5f20ac85c058778d413d3f18b6bbab66167c", "sequence_number": "0", "max_gas_amount": "200000", "gas_unit_price": "100", "expiration_timestamp_secs": "1759699576", "payload": { "type": "entry_function_payload", "function": "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", "type_arguments": [], "arguments": ["hello"] }, "secondary_signers": [] }], "id": 1 }' ``` ```bash cURL - submit sponsored tx example expandable theme={null} ## This is a an example of the request to a Movement fullnode that you ## make after getting the response from Shinami's gas_encodeAndSponsorTransaction ## and then generating the sender signature on the transactionSigningMessage returned. curl https://testnet.movementnetwork.xyz/v1/transactions \ -X POST \ --header 'Content-Type: application/json' \ -d '{ "sender": "0x5ebaaf21714dafb9d629adfffc58b2b764e6ed17be350408efdb26e7fc3a509a", "sequence_number": "0", "max_gas_amount": "200000", "gas_unit_price": "100", "expiration_timestamp_secs": "1759766829", "payload": { "type": "entry_function_payload", "function": "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", "type_arguments": [], "arguments": [ "hello" ] }, "signature": { "type": "fee_payer_signature", "sender": { "type": "ed25519_signature", "public_key": "0xb5c16b156f634c633315de99f744ecbd3bb620bbc88706bcd25aa588fe141b65", "signature": "0x6b28115e1b092ea0aeb24bbba7341ffc7240a58c754ce72d3dae89b6eb85adc183fb58dee54f9855d624f75454beadca683d55a023c5a6064b2cc5088a49b500" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "fee_payer_address": "0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e", "fee_payer_signer": { "type": "ed25519_signature", "public_key": "0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7", "signature": "0x91ca02d0d9a80eadedf9c7bcfc85b904b1e7f6bc1b0291e9ad420dfea571c504f0c2f8fd83046f744977d7341af6f9ac365a60e2f1bcdf52285a767451d12f06" } } }' ``` **Example Response** ```bash cURL - encodeAndSponsor example expandable theme={null} { "jsonrpc":"2.0", "result":{ "transactionSigningMessage":[94,250,60,79,2,248,58,15,75,45,105,252,149,198,7,204,2,130,92,196,231,190,83,110,240,153,45,240,80,217,230,124,1,240,255,88,115,89,226,41,251,202,201,228,217,141,92,95,32,172,133,192,88,119,141,65,61,63,24,182,187,171,102,22,124,0,0,0,0,0,0,0,0,2,229,107,39,41,114,52,70,205,8,54,167,209,39,56,9,73,16,48,204,242,236,153,53,213,152,191,223,11,255,238,68,134,7,109,101,115,115,97,103,101,11,115,101,116,95,109,101,115,115,97,103,101,0,1,6,5,104,101,108,108,111,64,13,3,0,0,0,0,0,100,0,0,0,0,0,0,0,120,226,226,104,0,0,0,0,250,0,113,214,199,31,104,241,143,250,183,59,116,176,104,12,121,0,36,50,75,136,44,49,16,210,237,129,105,94,49,2,96,126], "feePayerAddress":"0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e", "feePayerSignature":{ "type":"ed25519_signature", "public_key":"0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7", "signature":"0xee1ad9fa7d736d802e3457c89aa1b61154d35015422edb5123cfe91945230648dee3ecbd917d0903d4b048d66ad27691c1d87ad0460e18c7c416b22e2b23eb0f" } }, "id":1 } ``` ```bash cURL - submit sponsored tx example expandable theme={null} { "expiration_timestamp_secs" : "1759766829", "gas_unit_price" : "100", "hash" : "0x749ec66e1feee0e71bb3b5c268016f7a525ac12ed03f3f7b240609ab5417ebed", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "hello" ], "function" : "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "replay_protection_nonce" : null, "sender" : "0x5ebaaf21714dafb9d629adfffc58b2b764e6ed17be350408efdb26e7fc3a509a", "sequence_number" : "0", "signature" : { "fee_payer_address" : "0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e", "fee_payer_signer" : { "public_key" : "0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7", "signature" : "0x91ca02d0d9a80eadedf9c7bcfc85b904b1e7f6bc1b0291e9ad420dfea571c504f0c2f8fd83046f744977d7341af6f9ac365a60e2f1bcdf52285a767451d12f06", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0xb5c16b156f634c633315de99f744ecbd3bb620bbc88706bcd25aa588fe141b65", "signature" : "0x6b28115e1b092ea0aeb24bbba7341ffc7240a58c754ce72d3dae89b6eb85adc183fb58dee54f9855d624f75454beadca683d55a023c5a6064b2cc5088a49b500", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } ``` **Response Fields** | Name | Type | Description | | :------------------------ | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | transactionSigningMessage | Unsigned byte array | Encoded message bytes for the transaction. To be signed by the sender and all secondary signers. | | feePayerAddress | Hex string | Fee payer account address. To be used to fill in `signature.fee_payer_address` property in the JSON transaction submission API. | | feePayerSignature | object | [AccountSignature](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/ecosystem/typescript/sdk/src/generated/models/AccountSignature.ts#L19) of the fee payer. To be used to fill in `signature.fee_payer_signer` property in the JSON transaction submission API. | ## Appendix ### How to build, sponsor, sign, and submit a transaction This section shows an example of how to build, sponsor, sign, and submit a simple transaction using the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) and the [Aptos TS SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk). For a high-level image of the sponsorship flow [see here](/product-overviews/movement/gas-station). For multiple end-to-end examples, see our [Gas Station TypeScript Tutorial](/developer-guides/movement/tutorials/gas-station-backend-only). **Key requirements** 1. Build your transaction with the fee payer address set to `0x0` (this is what happens when you set `withFeePayer: true` as shown below). The sender and any secondary signers can sign the transaction with this address, or with the actual feePayer's address you obtain from Shinami upon a successful sponsorship. 2. 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it as in the example below. Replace all instances of `{{name}}` with the actual value for that name. ```bash TypeScript expandable theme={null} // 1. Import the required dependencies import { GasStationClient } from "@shinami/clients/aptos"; import { Account, Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; // 2. Create a Movement client const config = new AptosConfig({ network: Network.CUSTOM, fullnode: 'https://testnet.movementnetwork.xyz/v1', faucet: 'https://faucet.testnet.movementnetwork.xyz/', }); const movementClient = new Aptos(config); // 3. Generate a sender. In production, you won't be using a newly generated account // each time. The sender will often be a wallet the user connected or an embedded // wallet you control for the user. This just makes for a shorter example. const sender = Account.generate({}); // 4. Create a Shinami Gas Station client for sponsoring our transactions. const SHINAMI_TESTNET_GAS_KEY = "{{MOVEMENT_TESTNET_GAS_STATION_ACCESS_KEY}}"; const gasStationClient = new GasStationClient(SHINAMI_TESTNET_GAS_KEY); // 5. Build a transaction // This transaction makes a function call to a module we've deployed on Testnet // You may be able to use the SDK's default expiration (20 seconds at time of writing) // but you must set the expiration time less than 1 hour from now. const FIVE_MINUTES_FROM_NOW_IN_SECONDS = Math.floor(Date.now() / 1000) + (5 * 60); const transaction = await movementClient.transaction.build.simple({ sender: sender.accountAddress, withFeePayer: true, data: { function: "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", functionArguments: ["test message"] }, options: { expireTimestamp: FIVE_MINUTES_FROM_NOW_IN_SECONDS, maxGasAmount: 20000 } }); // 6. Obtain the sender's signature const senderAuthenticator = movementClient.transaction.sign({ signer: sender, transaction }); /// /// STEP 7 OPTION 1: SPONSOR AND SUBMIT IN ONE REQUEST /// // 7a. Ask Shinami to sponsor and submit the transaction const pendingTx = await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator ); /// END STEP 7 OPTION 1 /// /// STEP 7 OPTION 2: SPONSOR AND SUBMIT IN TWO REQUESTS /// To run this version, un-comment the code for 7b and 7b.2 below, and comment out the code for 7a above // 7b. Send the transaction to Shinami for sponsorship // const feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // Note that our SDK updates the transaction's feePayer address on a successful sponsorship. // The sender and any secondary signers can sign the transaction with the actual feePayer's // address or with the special `0x0` feePayer address given when the feePayer transaction is // built (which we do above). However, the transaction must be submitted with the actual // feePayer's address. If you don't use our TS SDK, you must explictly set it with the // `feePayer.address` value we return when you ask for a sponsorship. // console.log("transaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress?.bcsToHex().toString()); // 7b.2. Submit the transaction to the Movement blockchain // const pendingTx = await movementClient.transaction.submit.simple({ // transaction, // senderAuthenticator, // feePayerAuthenticator, // }); /// END STEP 7 OPTION 2 // 8. Poll the Movemnent node until the transaction is finalized const executedTransaction = await movementClient.waitForTransaction({ transactionHash: pendingTx.hash }); console.log("Transaction hash:", executedTransaction.hash); console.log("Transaction status:", executedTransaction.vm_status); console.log("Explorer link:", `https://explorer.movementnetwork.xyz/txn/${executedTransaction.hash}?network=bardock+testnet`); ``` # Invisible Wallet API Source: https://docs.shinami.com/api-docs/movement/wallet-services/invisible-wallet-api App-controlled, embedded wallets for a smooth UX ## Overview Shinami’s Invisible Wallets abstract away Web3 elements like seed phrases, third-party wallet connections, gas fees, and signing popups. They are embedded, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature. You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Core use cases include app-managed NFTs or closed-loop tokens. For a breakdown of the wallets we offer and wallet use-cases, see our [high-level guide](/product-overviews/movement/wallets). ### Shinami Gas Station Integration All methods below that write to the Movement blockchain have their gas fees sponsored by you via a Gas Station fund you create. This is because Invisible Wallets are designed to easily onboard Web2-native users (who may not want to download a wallet app, manage a seed phrase, and complete KYC checks to buy MOVE for gas). See the [Movement Gas Station FAQ page in our Help Center](/help-center/movement/gas-station-faq) for how guidance on how to set up a fund. ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header ('X-Api-Key: ACCESS\_KEY') or in the request url (`https://api.us1.shinami.com/movement/wallet/v1/ACCESS_KEY`). We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for how to set up a Wallet-Services-only access key and an access key with Wallet Services and Gas Station rights (needed to execute transactions and initialize Invisible Wallets on chain). **Call this API from your backend** Shinami Wallet Services do not support CORS requests, so if you make requests to these APIs from your frontend you'll get a CORS error. This is for security reasons: exposed keys and wallet information could lead to malicious actors signing transactions on behalf of your users. **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#movement-invisible-wallet-api). ### WalletId and Secret Pairing **When you create an Invisible Wallet, you must create, store, link, and never change the following two values**: * `walletId`: Your internal id for a wallet. When you provide us a `walletId` in a method call, it tells us which Invisible Wallet to use. It could be your internal `userId` value, or a new arbitrary and unique value you link to the `userId`. * `secret`: Your internal secret for a wallet. The `sessionToken` you generate with it is combined with Shinami data to obtain a signature from the associated wallet. Ideally it would be different for each wallet so that if one `secret` is compromised the rest are not. **When you create an Invisible Wallet, you forever link its walletId it to the secret you used:** **So, if you try to use the walletId with a different secret, you'll get an error:** ### Tutorial with E2E sample code Check out our [TypeScript tutorial](/developer-guides/movement/tutorials/invisible-wallets) for more code samples and details on the end-to-end flow of creating and using Invisible Wallets to execute sponsored transactions. ## Methods ### key\_createSession For security purposes, you must generate a session token before you create a wallet, or sign/execute transactions. Session tokens are valid and can be reused for 10 minutes. You may also use an instance of `ShinamiWalletSigner` to manage session token generation and refreshes for a given wallet. This is shown in the methods below that have a `sessionToken` parameter in an additional sample code tab. **Request Parameters** | Name | Type | Description | | :----- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secret | string | Used to encrypt and decrypt a wallet's private key. Therefore, it must always be used with the same `walletId` and cannot be changed in the future (see [walletId and secret pairing](/api-docs/movement/wallet-services/invisible-wallet-api#walletid-and-secret-pairing)) | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/key/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"key_createSession", "params":[ "{{secret}}" ], "id":1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient } from "@shinami/clients/aptos"; const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{secret}}); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w", "id": 1 } ``` ```TypeScript Shinami TypeScript SDK theme={null} "eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w" ``` **Response Data** | Type | Description | | :----- | :--------------------------------------------------------------------------------------------- | | string | `sessionToken` corresponding to the provided `secret`. Valid and can be reused for 10 minutes. | ### wal\_createWallet Creates a new Shinami Invisible Wallet you control, but does not yet initialize the address on a Movement network (e.g. Mainnet). Generally, you can wait to initialize a wallet if you wish (see more in [the description of `wal_initializeWalletOnChain`](/api-docs/movement/wallet-services/invisible-wallet-api#wal-initializewalletonchain)). **Wallet creation limit** On the free tier you have a limit of wallet creations per month as shown on the "Movement Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#movement-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Request Parameters** | Name | Type | Description | | :----------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/movement/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_createWallet", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}"; const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.createWallet( {{walletID}}, sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Return the Movement wallet address associated with the // walletId, or create an Invisible Wallet tied to that // walletId if none is found and return its address. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN_IF_CREATING = false; const accountAddress = await signer.getAddress( CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN_IF_CREATING ); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Movement address of created wallet. | ### wal\_initializeWalletOnChain Initializes a Shinami Invisible Wallet on the Movement network the Gas Station rights of your access key is associated with (e.g. Mainnet). This comes after a call to [`wal_createWallet`](/api-docs/movement/wallet-services/invisible-wallet-api#wal-createwallet) but is not always needed. That's because: 1. Initialization happens automatically if you send a transaction with an uninitialized wallet as the sender (e.g. calling [`wal_executeGaslessTransaction`](/api-docs/movement/wallet-services/invisible-wallet-api#wal-executegaslesstransaction)) . 2. If something is transferred to an address you control the keys for (e.g. an NFT) before you initialize it, you'll still be able to control that NFT after you initialize it. **Access Key Requirements:** In addition to Wallet Services rights, making this request requires an access key with Gas Station rights for the Movement network you wish to initialize the wallet on. This is because initialization involves submitting a basic transaction with the Invisible Wallet as the sender and the gas fee sponsored by your Gas Station fund. See how to make one [here](/developer-guides/core-integration-topics/authentication-and-api-keys#3b-movement-key-configuration). See also the next section: **Gas Station fund required:** You need a Gas Station fund with MOVE in order to sponsorship transactions for an Invisible Wallet, including the transaction that explicitly initializes it on chain. For information on how to set this up, see the [Movement Gas Station FAQ page in our Help Center](/help-center/movement/gas-station-faq). **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). **Request Parameters** | Name | Type | Description | | :----------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/movement/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_initializeWalletOnChain", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.initializeWalletOnChain( {{walletID}}, sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const accountAddress = await signer.tryInitializeOnChain(); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Movement address of created wallet. | ### wal\_createWalletOnChain Creates and initializes a Shinami Invisible Wallet onto the Movement chain. Generally, you can wait to initialize a wallet if you wish (see more in [the description of `wal_initializeWalletOnChain`](/api-docs/movement/wallet-services/invisible-wallet-api#wal-initializewalletonchain)). We provide a variety of wallet creation and initialization methods to give you flexibility. **Wallet creation limit** On the free tier you have a limit of wallet creations per month as shown on the "Movement Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#movement-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Access Key Requirements:** In addition to Wallet Services rights, making this request requires an access key with Gas Station rights for the Movement network you wish to initialize the wallet on. This is because initialization involves submitting a basic transaction with the Invisible Wallet as the sender and the gas fee sponsored by your Gas Station fund. See how to make one [here](/developer-guides/core-integration-topics/authentication-and-api-keys#3b-movement-key-configuration). See also the next section: **Gas Station fund required:** You need a Gas Station fund with MOVE in order to sponsorship transactions for an Invisible Wallet, including the transaction that explicitly initializes it on chain. For information on how to set this up, see the [Movement Gas Station FAQ page in our Help Center](/help-center/movement/gas-station-faq). **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). **Request Parameters** | Name | Type | Description | | :----------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/movement/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_createWalletOnChain", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const accountAddress = await walletClient.createWalletOnChain( "{{walletID}}", sessionToken ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Return the Movement wallet address associated with the // walletId, or create an Invisible Wallet tied to that // walletId if none is found and return its address. // Initialize it on the network the access key's Gas // Station rights are associated with. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN_IF_CREATING = true; const accountAddress = await signer.getAddress( CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN_IF_CREATING ); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress: https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Movement address of created wallet. | ### wal\_getWallet Gets the address of an existing Invisible Wallet, regardless of whether it is initialized or uninitialized on chain. **Request Parameters** | Name | Type | Description | | :------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_getWallet", "params": [ "{{walletId}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const accountAddress = await walletClient.getWallet( {{walletID}} ); ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const accountAddress = await signer.getAddress(); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "accountAddress" : "0x45329e69bf823b556ab930a244ab33e0a71f4065d52b4a763e51af2f4b4d214a" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAddress // https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L4 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | | `{ "accountAddress": string }` | [AccountAddress](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/core/accountAddress.ts#L42) | The Movement address of created wallet. | ### wal\_signTransaction Use an initialized Shinami Invisible Wallet to sign a transaction. When the wallet is the sender and you are sponsoring as the feePayer, use `wal_executeGaslessTransaction`, which does all of: sign, sponsor, and submit the transaction to the Movement network. **Request Parameters** | Name | Type | Description | | :---------------------------------------- | :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/movement/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | | ***SDK-only***
transaction | AnyRawTransaction | A [`SimpleTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/simpleTransaction.ts#L15) or a [`MultiAgentTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/multiAgentTransaction.ts#L14) *that does not have a fee payer*. You can find an example of building a transaction [in our tutorial](/developer-guides/movement/tutorials/invisible-wallets#5-generate-a-feepayer-transaction) | | ***cURL-only***
rawTransaction | Hex string \| unsigned byte array | BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110). Can be passed either as an unsigned byte array or a Hex string. | | ***cURL-only***
secondaryAddresses | string\[] | (Optional) Required for multi-agent transactions. Array of addresses of the secondary signers for the transaction. Must be in the exact order. | | ***cURL-only***
feePayerAddress | string | (Optional) Address of a feepayer for the rawTransaction. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_signTransaction", "params": [ "{{walletId}}", "{{sessionToken}}", "{{rawTransaction}}", "{{(optional)secondardAddresses}}", "{{(optional)feePayerAddress}}" ], "id": 1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); // sign a non-feePayer transaction const accountAuthenticator = await walletClient.signTransaction( {{walletID}}, sessionToken, {{transaction}} ); ``` ```TypeScript Shinami TypeScript SDK: ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // sign a non-feePayer transaction const accountAuthenticator = await signer.signTransaction({{transaction}}); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "signature":[0,32,136,128,248,41,24,171,70,49,247,26,7,233,63,120,178,130,84,171,58,188,184,203,137,23,210,92,89,62,182,10,55,2,64,239,224,55,68,196,84,93,88,60,245,168,112,48,199,204,225,105,109,15,6,64,145,58,175,225,89,230,38,71,179,223,193,212,114,240,142,212,165,123,209,71,50,202,29,114,133,212,40,240,123,38,189,76,4,208,237,156,135,159,154,143,122,206,9] }, "id":1 } ``` ```TypeScript Shinami TypeScript SDK theme={null} AccountAuthenticator // https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506 ``` ```TypeScript Shinami TypeScript SDK using ShinamiWalletSigner theme={null} AccountAuthenticator // https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506 ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | | `{ "signature": Unsigned byte array }` | [AccountAuthenticator](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/authenticator.rs#L488-L506) | The Invisible Wallet's signature for the transaction. | **Notes for signing a non-sponsored transaction:** * The Invisible wallet must be initialized on chain. * If you attempt to sign *immediately* after initializing a wallet you may get an error that the address was not found on chain, as it take a couple of seconds for the initialization to be processed by the Movement blockchain. ### wal\_executeGaslessTransaction Sponsors, signs, and executes a gasless transaction from a Shinami Invisible Wallet. This is a convenient end-to-end method for submitting sponsored transactions to the Movement chain as opposed to doing all the steps individually. **Access Key Requirements:** In addition to Wallet Services rights, making this request requires an access key with Gas Station rights for the Movement network you wish to sponsor and execute the transaction on. See how to make one [here](/developer-guides/core-integration-topics/authentication-and-api-keys#3b-movement-key-configuration). See also the next section: **Gas Station fund required:** You need a Gas Station fund with MOVE in order to sponsorship transactions for an Invisible Wallet. For information on how to set this up, see the [Movement Gas Station FAQ page in our Help Center](/help-center/movement/gas-station-faq). **Shinami sponsorship fees:** See our [Movement Billing FAQ](/help-center/billing/movement-faq#gas-station). **Request Parameters** | Name | Type | Description | | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. Note: you cannot change this value in the future, so do not use a value that you or your users might change, such as a editable username. | | sessionToken | string | The token generated by [`key_createSession`](/api-docs/movement/wallet-services/invisible-wallet-api#key_createsession) with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | | ***SDK-only***
transaction | AnyRawTransaction | A [`SimpleTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/simpleTransaction.ts#L15) or a [`MultiAgentTransaction`](https://github.com/aptos-labs/aptos-ts-sdk/blob/28803638e61a6eac877924cb6b0903df77e5842f/src/transactions/instances/multiAgentTransaction.ts#L14). You can find an example of building a transaction [in our tutorial](/developer-guides/movement/tutorials/invisible-wallets#5:-generate-a-feepayer-transaction) | | ***cURL-only***
rawTransaction | Hex string \| unsigned byte array | BCS-serialized [RawTransaction](https://github.com/aptos-labs/aptos-core/blob/4045f57eb4b1fe2d055263d0c541f0ca76d1df42/types/src/transaction/mod.rs#L84-L110). Can be passed either as an unsigned byte array or a Hex string. | | secondarySigners | (*SDK*) AccountAuthenticator \| (*cURL*) \["address" : hex string, "signature": BCS-serialized AccountAuthenticator(Can be passed either as an unsigned byte array or a hex string.)] | (optional) Array of additional signers and their signatures. Must be in the exact order. Required for multi-agent transactions. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL-template theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "wal_executeGaslessTransaction", "params": [ "{{walletId}}", "{{sessionToken}}", "{{rawTransaction}}", [{ "address": "{{secondarySignerOneAddress}}", "signature": "{{secondarySignerOneSignature}}" }] ], "id": 1 }' ``` ```bash cURL- example theme={null} curl https://api.us1.shinami.com/movement/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"wal_executeGaslessTransaction", "params":[ "{{walletID}}", "eyJraWQiOiJrZXkyMDI0MDUwMiIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiIzY1dmODVTQnB1X0VseG5fS19yNTVnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJMdTRxX1Y3aG9fWDdyYXZWIn0.whquEkYQM1GBK3d26VoZNuxCuI1gQMMAA5tGWvPzoGM.mUtlN_sbR9i5rhmX.Ow_jH6bifeciMSxxK49fOLZ5DY3SJ-SnvcYEjmfnJQEIdnbKOOgK7KKjln8QUNgQpIrPtOm5yVNmo8p8Pg60uA7M7a4qBwDHh3_5ffdjAbaW6IYdbfcmDy5ydWrC84nHKLpgSMWHwEltkfE.KnjXE3IAYFX67hycEoH9gQ", "0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de0200000000000000008302a11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b033801020000400d0300000000006400000000000000b51f8c660000000002", [{ "address": "0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d", "signature": "0x0020924795a3b1508fa51bed2b8539075eb410665114422aa80aac88732e3b92adbf4064a77a74aad50c1229dd65543d9d1cdd96542484328188ecaba585de6e22aff5de3b5bb65f76a630e5ab1ff2e32e62b59d31e65206b8e224f397db5bb36bc500" }] ], "id":1 }' ``` ```TypeScript Shinami TS SDK theme={null} import { KeyClient, WalletClient } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); let sessionToken = await keyClient.createSession({{walletSecret}}); const pendingTx = await walletClient.executeGaslessTransaction( {{walletID}}, sessionToken, {{transaction}}, [{{secondarySignatureOne}}, ...] ); ``` ```TypeScript Shinami TS SDK: ShinamiWalletSigner theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/aptos"; const walletClient = new WalletClient({{walletServicesAccessKey}}); const keyClient = new KeyClient({{walletServicesAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const pendingTx = await signer.executeGaslessTransaction( {{transaction}}, [{{secondarySignatureOne}}, ...] ); ``` **Example Response** ```bash cURL - template expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1764803867", "gas_unit_price" : "100", "hash" : "0xaba0adbe5de0dcb9729b341d3a453e7c4048ca4be81a428f2f5ca2adf4af4119", "max_gas_amount" : "200000", "payload" : { "arguments" : [ "test_message" ], "function" : "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", "type" : "entry_function_payload", "type_arguments" : [] }, "replay_protection_nonce" : null, "sender" : "0x5d0d97f000cf532e6aba3472bbef2b5500dfdfc851e0c80eefbd8c32a85d92fd", "sequence_number" : "0", "signature" : { "fee_payer_address" : "0x53a2bd6b226ec498c1852cb265bd6654ac312c1997dd2f83541e50b3f5b641d", "fee_payer_signer" : { "public_key" : "0xd8a9d426d19daa42207d2045768bca80b6e2d3e04b5afd45ad999ce192df256a", "signature" : "0x15b70683a172976c5dde1af536de1da008e1e12c36d2a37839ae50bb1fb011f204e3ce7db8eb33942f213c4bb14d040abc94569e4d0c2e193ba6e2dc1347f802", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [], "secondary_signers" : [], "sender" : { "public_key" : "0x2029551ff1db2875b77ddbf3ee23fc57ef0bca7e442cc58adc4d57383fe55734", "signature" : "0x5ca1fecbbceb7a95916073d4eff29193a7f15dc5bda6e9340c203fdab817bb4cac6e9f94c44b53d7d06d579bd64a6802f00d44afb1162807431519bb4f774f03", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } } } ``` ```bash cURL - multiAgent example expandable theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "pendingTransaction" : { "expiration_timestamp_secs" : "1720459189", "gas_unit_price" : "100", "hash" : "0xd30736a7a854060ff449ea67451863e6134137303f7ac3439a16d46df1f9413e", "max_gas_amount" : "200000", "payload" : { "arguments" : [], "code" : { "abi" : { "generic_type_params" : [], "is_entry" : true, "is_view" : false, "name" : "main", "params" : [ "&signer", "&signer" ], "return" : [], "visibility" : "public" }, "bytecode" : "0xa11ceb0b060000000701000602060a03101104210405252a074f42089101200000000100020103040100010004080001050304010002060607000107080501000002020202060c060c020b000108010b0001080101080102060c03010b000109000001060c010502050b000109000a6170746f735f636f696e04636f696e067369676e657204436f696e094170746f73436f696e0877697468647261770a616464726573735f6f66076465706f7369740000000000000000000000000000000000000000000000000000000000000001000001110a0006640000000000000038000c020a0106c80000000000000038000c030b0111010b0238010b0011010b03380102" }, "type" : "script_payload", "type_arguments" : [] }, "sender" : "0x6c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de", "sequence_number" : "2", "signature" : { "fee_payer_address" : "0x11045a6f53a6e9064fd92b45038649c334fd1afd60848a090facab14e07cf70a", "fee_payer_signer" : { "public_key" : "0x7dfd1009d1dff08cd60e900c1eccda628808016e5885bc82a66f4d97ba959594", "signature" : "0x6ef8afa108df6ae0c50486dc0356e9e16abb9115d99ba6fac64a5fbcdd3e85c1775e3031511679458b4a574f55dd357919278acafc63dc124b9a70a663e8c706", "type" : "ed25519_signature" }, "secondary_signer_addresses" : [ "0x7e3a6349ad8a43a6b7c3307b4b05d9008548a11673f440af5e516a9bc5114b3d" ], "secondary_signers" : [ { "public_key" : "0x924795a3b1508fa51bed2b8539075eb410665114422aa80aac88732e3b92adbf", "signature" : "0x64a77a74aad50c1229dd65543d9d1cdd96542484328188ecaba585de6e22aff5de3b5bb65f76a630e5ab1ff2e32e62b59d31e65206b8e224f397db5bb36bc500", "type" : "ed25519_signature" } ], "sender" : { "public_key" : "0xdc27624c0bf8ee11c8d6bf54e182b12a8f876eb56e66df740cb4de7adefbfa57", "signature" : "0xf3bd6086a1381d36ce46dae5bf6aff2d9652a7d7ba846dacbb67b858ab20a6bf230c8f5b9c6b59d27b41d00ef0f8ec0b29e7e0335d4a4dbac4e2a6469375f50f", "type" : "ed25519_signature" }, "type" : "fee_payer_signature" } } } } ``` ```TypeScript Shinami TS SDK expandable theme={null} { hash: '0x7a337a8952378b6a9174605b56c258f15b18f3a337ba766c02a624759f401c9f', sender: '0x35d86428a7aee9863f9aa52d6aa4b583e2bcb7ee9d3e7d1cf1b6a7e13da503af', sequence_number: '2', max_gas_amount: '200000', gas_unit_price: '150', expiration_timestamp_secs: '1720131046', payload: { function: '0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message', type_arguments: [], arguments: [ 'hello' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x8880f82918ab4631f71a07e93f78b28254ab3abcb8cb8917d25c593eb60a3702', signature: '0x3a446c1586ccec7bb98ad2010c7a13a238b163e096343ae7e9b858b2bd6de6cd5e7beaa6dccd67154bc5014aae4296ce13eda82668c643de99cfd547560f7801', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e', fee_payer_signer: { public_key: '0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7', signature: '0x8bb1f219fa65e036953c0ecf2d6aad017a4efd76c6ea493ac42c36ca93e8bbbe256d97ddd2cd2dc7f6e8b8bd5437041eec0cdfa18e52b0c6591d03f3c0061a02', type: 'ed25519_signature' }, type: 'fee_payer_signature' } } ``` ```TypeScript Shinami TS SDK (ShinamiWalletSigner) expandable theme={null} { hash: '0xd84a42c447049501d1ef334012c39b4602eb977df1c466652f59a1387c395b8f', sender: '0x35d86428a7aee9863f9aa52d6aa4b583e2bcb7ee9d3e7d1cf1b6a7e13da503af', sequence_number: '1', max_gas_amount: '200000', gas_unit_price: '150', expiration_timestamp_secs: '1720130820', payload: { function: '0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message', type_arguments: [], arguments: [ 'hello' ], type: 'entry_function_payload' }, signature: { sender: { public_key: '0x8880f82918ab4631f71a07e93f78b28254ab3abcb8cb8917d25c593eb60a3702', signature: '0xccc9544d93c9563195d1596fbeeec33ae041439752114504d10592821cf3f24bd0bc006671c88e83e0449b42b41b5c06ab6f16ea5e70e8d945a0887584334207', type: 'ed25519_signature' }, secondary_signer_addresses: [], secondary_signers: [], fee_payer_address: '0x71d6c71f68f18ffab73b74b0680c790024324b882c3110d2ed81695e3102607e', fee_payer_signer: { public_key: '0x899562c7d7be4456219b6c08ebb4b0fbe5ad4525f3d74a514ff7390e9ddeb3a7', signature: '0x607a3b86faf10153fd2afcb1ccec258f8e35dfb985fe2725efc77859a3f0b1e9c4e79de71c8357d8f9ee6f8508db7e0042dec270355a34c3d6d2964edd5bfb0e', type: 'ed25519_signature' }, type: 'fee_payer_signature' } } ``` **Response Data** | Type (cURL) | Type (SDK) | Description | | :--------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | | `{ "pendingTransaction": object with the same shape as a `PendingTransactionResponse` }` | [PendingTransactionResponse](https://github.com/aptos-labs/aptos-ts-sdk/blob/de936697bdc552eb9c88e676d85686449dba3467/src/types/index.ts#L333-L343) | The submitted transaction waiting in mempool. | # API Docs Source: https://docs.shinami.com/api-docs/overview API docs for each of our services along with a few key integration resources. # Key resources for all integrations How to create and manage your API access keys and authenticate your requests. Error codes and resolution steps for all Shinami APIs. An SDK for all Shinami services. # API docs for ### Aptos App-controlled, embedded wallets. Sponsor transactions to drive engagement and retention. ### Movement Create wallets for your users. Sponsor transactions to drive engagement and retention. ### Sui App-controlled, embedded wallets. Sponsor transactions to drive engagement and retention. User-controlled OAuth wallets. We have a starter app template to get you up and going quickly. # Gas Station API Source: https://docs.shinami.com/api-docs/sui/gas-station/api Increase user engagement and retention by sponsoring transactions ## Overview You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Shinami's Gas Station API lets you easily sponsor transactions for your users. SUI is pulled from a fund you create to power these transactions, managed in Shinami's dashboard (see a how to create a fund on the [Sui Gas Station FAQ page of our Help Center](/help-center/sui/gas-station-faq)). Shinami facilitates sponsored transactions based on logic determined by you, the app developer. Examples include sponsoring each user's initial transaction(s) for better onboarding and sponsoring transactions of a particular type that you want to encourage. Apps looking to onboard and retain Web2 users may choose to sponsor all transactions and use Gas Station alongside Shinami's app-controlled [Invisible Wallets](/api-docs/sui/wallet-services/invisible-wallet-api) or user-controlled [zkLogin wallets](/api-docs/sui/wallet-services/zklogin-wallet-api) to make Web3 fully unseen. For a game, this allows players to focus on gameplay, while behind the scenes the game creates and modifies game assets in a Sui wallet designated for the player. Players get the benefits of Web3 without friction. ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header ('X-Api-Key: ACCESS\_KEY') or in the request url, e.g. `https://api.us1.shinami.com/sui/gas/v1/ACCESS_KEY`. We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). For more information, including how to set up API access keys, see our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide. **Call Shinami's Gas Station from your backend server** Shinami Gas Station does not support CORS requests, so you will get a CORS error if you make requests from your frontend. Use your backend server to integrate with Shinami's Gas Station. This limits exposure of your sponsorship 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. For an example flow of combining frontend signing and/or transaction inputs with backend sponsorship, see our [Frontend signing + backend sponsorship](/developer-guides/sui/tutorials/gas-station-with-frontend-signing) tutorial. **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also consider batching multiple Move calls into a single programmable transaction block for sponsorship. They'll execute in sequence, having the same effect as if they were submitted separately. Finally, you can [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Gas Station API](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api). ### Send your first request For a quick sample request that doesn't require building a transaction, ask for the balance of the fund your access key is tied to with [gas\_getFund](/api-docs/sui/gas-station/api#gas-getfund). ### Tutorials with E2E sample code Check out our [TypeScript tutorial](/developer-guides/sui/tutorials/gas-station-backend-only) for more code samples and details on the end-to-end flow of creating, sponsoring, and executing a transaction. Also, since our Gas Station does not support CORS (browser) requests for security reasons, our [Frontend signing + backend sponsorship tutorial](/developer-guides/sui/tutorials/gas-station-with-frontend-signing) . ## Methods ### gas\_sponsorTransactionBlock Sponsor a gas-less transaction. This request takes two parts of a `TransactionData` object: the sender address and the programmable transaction block to be executed (with gas information is omitted, this is considered a `TransactionKind`object). It asks the Gas Station to attach a gas object to it and produce a complete `TransactionData` object signed by the sponsor (your Gas Station fund). The returned `TransactionData` then needs to be signed by the sender and sent to Shinami's Node Service (or any full node) with both signatures to be executed with [`sui_executeTransactionBlock`](/api-docs/sui/node-service/json-rpc#sui-executetransactionblock). **Important notes** 1. **You cannot use the gas object in a sponsored transaction for other purposes.** For example, you cannot write `const [coin] = txb.splitCoins(txb.gas,[txb.pure(100)]);` because it's accessing `txb.gas`. If you try to sponsor a TransactionKind that uses the gas object you will [get a JSON-RPC `-32602` error](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api) . 2. **Gas Station doesn't support CORS, so you must send requests from your backend.** For more info and some sample code, see our [FE signing + BE sponsorship guide](/developer-guides/sui/tutorials/gas-station-with-frontend-signing). 3. **We charge a small fee (in SUI) per sponsorship** request to cover our costs. For details, visit the "Sui Gas Station" tab on the [Billing page](https://app.shinami.com/billing/#sui_gas) of your Shinami dashboard. 4. **You need to create a Gas Station fund in your Shinami dashboard before you create an access key.** When you create a gas station key, you assign it to a network (e.g. Testnet) and a fund on that network. For how to do that and find the deposit address, see our [Sui Gas Station Help Center page](/help-center/sui/gas-station-faq). 5. **Auto vs manual budgeting:** when you omit the `gasBudget` parameter, we set your gas budget automatically for you. We recommend this for most use cases. We put your `transactionBytes` through a [sui\_dryRunTransactionBlock](/api-docs/sui/node-service/json-rpc/write-api#sui-dryruntransactionblock) request as a free service before we attempt to sponsor it. This call will generate error messages for certain invalid transactions, and we'll return these errors back to you, which should be the same as if you had made a `sui_dryRunTransactionBlock` request yourself. For more on auto vs manual budgeting, see the [Appendix of our Gas Station tutorial](/developer-guides/sui/tutorials/gas-station-backend-only#tips-for-setting-your-sponsorship-budget). **Request Parameters: Shinami SDK** | Name | Type | Description | | :----------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | tx | GaslessTransaction | Shinami `GaslessTransaction` interface with the following members (For a code example of building and sponsoring a transaction, [see the Appendix](/api-docs/sui/gas-station/api#how-to-build-sponsor-sign-and-execute-a-transaction)): | | tx.txKind | string | Base64 encoded, BCS serialized `TransactionKind` | | tx.sender | string | Sui address of the `TransactionKind` sender | | tx.gasBudget | string \| number | (Optional) The gas budget you wish to use for the transaction, in MIST. The transaction will fail if the gas cost exceeds this value.

- If provided, we use the value as the budget of the sponsorship.

- If omitted, we estimate the transaction cost for you. We then add a buffer (5% for non-shared objects, 25% for shared objects) and use that total value as the budget of the sponsorship. | | tx.gasPrice | string \| number | (Optional) Gas price override. Must be equal to or greater than the current reference gas price. If omitted the current reference price is used.

Under normal network conditions, the expectation is that this does not need to be set. For times of high network congestion, setting a gasPrice higher than the reference gas price gives your transaction higher priority. For more info, see [Sui's documentation on gas pricing](https://docs.sui.io/concepts/tokenomics/gas-pricing#computation). | **Request Parameters: cURL** | Name | Type | Description | | :--------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | transactionBytes | string | Base64 encoded, BCS serialized `TransactionKind` | | sender | string | Sui address of the `TransactionKind` sender | | gasBudget | string | (Optional) The gas budget you wish to use for the transaction, in MIST. The transaction will fail if the gas cost exceeds this value.

- If provided, we use the value as the budget of the sponsorship.

- If omitted, we estimate the transaction cost for you. We then add a buffer (5% for non-shared objects, 25% for shared objects) and use that total value as the budget of the sponsorship. | | gasPrice | string | Optional gas price (must be equal to or greater than the current reference gas price). If omitted the current reference price is used.

Under normal network conditions, the expectation is that this does not need to be set. For times of high network congestion, setting a gasPrice higher than the reference gas price gives your transaction higher priority. For more info, see [Sui's documentation on gas pricing](https://docs.sui.io/concepts/tokenomics/gas-pricing#computation). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/gas/v1 \ -X POST \ -H 'X-API-Key: {{gasAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_sponsorTransactionBlock", "params":[ "{{gaslessTransactionBytes}}", "{{senderAddress}}", "{{gasBudget}}", "{{gasPrice}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/sui"; const gasClient = new GasStationClient("{{gasAccessKey}}"); const sponsorshipData = await gasClient.sponsorTransaction( {{tx}} // if tx.gasBudget is omitted we use our auto-budgeting feature ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result" :{ "txBytes":"AAAAAQBCe5tpWnBOY2pjc+wz7B0Heo1Zj5cSbCJY3KWF60tybgljb2luX2ZsaXAKY2xhaW1fZmVlcwAAL44vPwjxyww1jOTrS7w8XpXnnhmIeecTKvuXScM6uIIBBqTsJrGuzKqeva+7IsFRCsYB7a6LpS34G1SEzn6DwSCoMHQAAAAAACCt2aRuz44LdsjRwsOpaPls3E4N7zVyDIT8AYWkinZQKh1jLUb/cEkQM/78TmOY3OqklD3PYlErTVc3i1q3A7xe6AMAAAAAAABAS0wAAAAAAAA=", "txDigest":"HvtKY9RwuE7NC4gLauLFPY3h5qepEy8R7aZHnc4gJu6G", "signature":"AAXr9gCHncXfpJiIuQLd1IyPCz/gIGebCeIFMkqollVsGfxVsoU1unAqOiQyvt4Xa18j/WC8dAN1pvf5Wsfnrw65rDp1CB4nXagWW48gUU79IRg3kJf+6erjnVYdenXh+A==", "expireAtTime":1695267721 }, "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} { "txBytes":"AAAAAQBCe5tpWnBOY2pjc+wz7B0Heo1Zj5cSbCJY3KWF60tybgljb2luX2ZsaXAKY2xhaW1fZmVlcwAAL44vPwjxyww1jOTrS7w8XpXnnhmIeecTKvuXScM6uIIBBqTsJrGuzKqeva+7IsFRCsYB7a6LpS34G1SEzn6DwSCoMHQAAAAAACCt2aRuz44LdsjRwsOpaPls3E4N7zVyDIT8AYWkinZQKh1jLUb/cEkQM/78TmOY3OqklD3PYlErTVc3i1q3A7xe6AMAAAAAAABAS0wAAAAAAAA=", "txDigest":"HvtKY9RwuE7NC4gLauLFPY3h5qepEy8R7aZHnc4gJu6G", "signature":"AAXr9gCHncXfpJiIuQLd1IyPCz/gIGebCeIFMkqollVsGfxVsoU1unAqOiQyvt4Xa18j/WC8dAN1pvf5Wsfnrw65rDp1CB4nXagWW48gUU79IRg3kJf+6erjnVYdenXh+A==", "expireAtTime":1695267721 } ``` **Response Fields** | Name | Type | Description | | :----------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | | txBytes | string | Base64 encoded, BCS serialized `TransactionData`, which now includes the gas sponsorship data. | | txDigest | string | Base 58 encoded transaction digest. Store this and use it to call `gas_getSponsoredTransactionBlockStatus` defined below if needed. | | signature | string | Base64 encoded transaction signature from the sponsor (your Gas Station fund wallet). | | expireAtTime | int | Expiration time of the assigned gas object, in Unix epoch seconds. All sponsorships created with this method are set to expire one hour after creation. | ### gas\_getSponsoredTransactionBlockStatus Check the status of a transaction you've sponsored to see if the gas object has been spent or not. Note that for many use cases, you will not need to check the status of your sponsorships. This is because when you get a successful response from the [`gas_sponsorTransactionBlock`](/api-docs/sui/gas-station/api#gas-sponsortransactionblock) request, you will immediately obtain a sender signature and submit it to our Node Service for execution (and therefore you'll know the status). **Request Parameters** | Name | Type | Description | | :---------------- | :----- | :------------------------------------------------------------------------- | | transactionDigest | String | Base58 encoded transaction digest (returned by gasSponsorTransactionBlock) | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/gas/v1 \ -X POST \ -H 'X-API-Key: {{gasAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_getSponsoredTransactionBlockStatus", "params":[ "{{transactionDigest}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/sui"; const gasClient = new GasStationClient("{{gasAccessKey}}"); // pass the digest returned by gas_sponsorTransactionBlock const status = await gas.getSponsoredTransactionStatus({{transactionDigest}}); ``` **Example Response** ```bash JSON theme={null} { "jsonrpc":"2.0", "result":"IN_FLIGHT", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "IN_FLIGHT" ``` **Possible Result Values** | Name | Type | Description | | :----------- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | "IN\_FLIGHT" | String | A gas object has been assigned and its `expireAtTime` has not been reached. A transaction execution attempt has not been made. | | "INVALID" | String | We did not find a gas object attached to the transaction that was sponsored by the fund this access key is tied to OR The gas object's `expireAtTime` was reached and it is still unspent. | | "COMPLETE" | String | A transaction execution attempt was made and the attached gas object was spent. This does not guarantee that the transaction was successfully executed. | ### gas\_getFund Get the balance for the Gas Station fund tied to the request's API access key. When you create a Gas Station access key, you [link it to exactly one Gas Station fund](/developer-guides/core-integration-topics/authentication-and-api-keys#sui-gas-station). So, when you make this request, we return the balance for the fund that's tied to the access key you use for the request. To check which fund a Gas Station access key is tied too, see the [Sui Gas Station FAQ page of our Help Center](/help-center/sui/gas-station-faq) **Request Parameters** none **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/gas/v1 \ -X POST \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"gas_getFund", "params":[], "id":1 }' ``` ```TypeScript Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/sui"; const gasClient = new GasStationClient("YOUR_API_KEY"); const fundInfo = await gasClient.getFund(); ``` **Example Response** ```bash JSON theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "balance" : 4915573880, "depositAddress" : "0x73e9b9188e84fc642290cf84c5b774f78bb49b60327f8fba23ebc4c5cfaf028b", "inFlight" : 0, "name" : "First test fund", "network" : "SUI_TESTNET" } } ``` ```TypeScript Shinami TypeScript SDK theme={null} { network: 'SUI_TESTNET', name: 'First test fund', balance: 4915573880, inFlight: 0, depositAddress: '0x73e9b9188e84fc642290cf84c5b774f78bb49b60327f8fba23ebc4c5cfaf028b' } ``` **Response Fields** | Name | Type | Description | | :------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | network | String | The network the fund is tied to. | | name | String | The name given to the fund when it was created. | | balance | Int | The balance of the fund, in MIST. (1,000,000,000 MIST = 1 SUI) | | inFlight | Int | The amount we are currently withholding from your fund for active sponsorships, in MIST. This is what will be used to pay Sui transaction fees and Shinami fees for the sponsorship. | | depositAddress | String \| null | `String` representing a Sui address if a deposit address has been created for the fund. `null` if no deposit address has been created. If you do not have a deposit address, follow the steps in the [Sui Gas Station FAQ page of our Help Center](/help-center/sui/gas-station-faq) and a deposit address will be automatically generated. | ## Appendix ### How to build, sponsor, sign, and execute a transaction This section shows an example of how to build, sponsor, sign, and submit a transaction using the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) and the [Mysten TypeScript SDK](https://www.npmjs.com/package/@mysten/sui). For a high-level image of the sponsorship flow [see here](/product-overviews/sui/gas-station#diagram). For multiple end-to-end examples, see our [Gas Station TypeScript tutorial](/developer-guides/sui/tutorials/gas-station-backend-only). **Key requirements** 1. When you prepare a transaction for sponsorship, make you prepare a `TransactionKind`, which is a transaction without the gas information (since this will be assigned by our Gas Station). When you make the `build()` call on your `Transaction`, make sure that `onlyTransactionKind: true`. In the code below, our SDK's `buildGaslessTransaction` function does this [behind the scenes](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/clients/src/sui/gas.ts#L189). Replace all instances of `{{name}}` with the actual value for that name. ```TypeScript Shinami TypeScript SDK expandable theme={null} // 1. Import everything you need import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; import { SuiGrpcClient } from '@mysten/sui/grpc'; import { fromBase64 } from "@mysten/sui/utils"; import { GasStationClient, buildGaslessTransaction } from "@shinami/clients/sui"; // 2. Copy your Testnet Gas Station key value const GAS_STATION_ACCESS_KEY = "KEY_VALUE"; // 3. Set up your Gas Station and Node Service clients. Here we're using the Mysten // free Node Service, which is useful for testing but not production. const gasStationClient = new GasStationClient(GAS_STATION_ACCESS_KEY); const nodeClient = new SuiGrpcClient({ baseUrl: 'https://fullnode.testnet.sui.io:443', network: 'testnet', }); // 4. Generate a sender. In production, you won't be using a newly generated sender // each time. The sender will often be a wallet the user connected or an embedded // wallet you control for the user. This just makes for a shorter example. const sender = new Ed25519Keypair(); // 5. Build a GaslessTransaction const gaslessTx = await buildGaslessTransaction( (txb) => { txb.moveCall({ target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access", arguments: [txb.object('0x6')], }); }, { sui: nodeClient } ); // 6. Set the sender address and ask for a sponsorship gaslessTx.sender = sender.toSuiAddress(); const { txBytes, signature: sponsorSignature } = await gasStationClient.sponsorTransaction( gaslessTx // by not setting gaslessTx.gasBudget we take // advantage of Shinami auto-budgeting ); // 7. Obtain the sender signature const { signature: senderSignature } = await sender.signTransaction(fromBase64(txBytes)); // 8. Submit the transaction, along with the sender and sponsor signatures const executeResponse = await nodeClient.executeTransaction({ transaction: fromBase64(txBytes), signatures: [sponsorSignature, senderSignature] }); console.log("Transaction digest:", executeResponse.Transaction?.digest); console.log("Transaction status:", executeResponse.Transaction?.status); console.log("Explorer link:", `https://suiscan.xyz/testnet/tx/${executeResponse.Transaction?.digest}`); ``` # Invisible Wallet API v2 Source: https://docs.shinami.com/api-docs/sui/wallet-services/invisible-wallet-api App-controlled embedded wallets for a smooth UX On July 30, 2026, the [old version](/api-docs/sui/wallet-services/invisible-wallet-api-v1) of this API was retired. This is the version to use. ## Overview Shinami’s Invisible Wallets abstract away Web3 elements like seed phrases, third-party wallet connections, gas fees, and signing popups. They are embedded, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature. You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Core use cases include app-managed NFTs or closed-loop tokens. For a breakdown of the wallets we offer and wallet use-cases, see our [high-level guide](/product-overviews/sui/wallets#wallet-services-overview). ### Shinami Gas Station Integration All methods below that write to the Sui blockchain have their gas fees sponsored by you via a Gas Station you create (see the [Sui Gas Station FAQ page of our Help Center](/help-center/sui/gas-station-faq) for how guidance on how to set up a fund and add free Testnet Sui to it). This is because Invisible Wallets are designed to easily onboard Web2-native users (who may not want to download a wallet app, manage a seed phrase, and complete KYC checks to buy SUI for gas). ### Authentication, Rate Limits and Error Handling **Authentication** You authenticate via an access key passed in a header ('X-Api-Key: ACCESS\_KEY') or in the request url, e.g. `https://api.us1.shinami.com/sui/wallet/v2/ACCESS_KEY`. We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). For more information, including how to set up an access key with Wallet Services rights, see our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide. **Call this API from your backend** Shinami Wallet Services do not support CORS requests, so if you call these APIs from your frontend you'll get a CORS error. This is for security reasons: exposed keys and wallet information could lead to malicious actors signing transactions on behalf of your users. **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#sui-invisible-wallet-api). ### WalletId and Secret Pairing *When you create an Invisible Wallet, you must create, store, link, and never change the following two values*: * `walletId`: Your internal id for a wallet. When you provide us a `walletId` in a method call, it tells us which Invisible Wallet to use. It could be your internal `userId` value, or a new arbitrary and unique value you link to the `userId`. * `secret`: Your internal secret for a wallet. The `sessionToken` you generate with it is combined with Shinami data to obtain a signature from the associated wallet. Ideally it would be different for each wallet so that if one `secret` is compromised the rest are not. **When you create an Invisible Wallet, you forever link its walletId it to the secret you used:** **So, if you try to use the walletId with a different secret, you'll get an error:** ### Tutorial with E2E sample code Check out our [TypeScript tutorial](/developer-guides/sui/tutorials/invisible-wallets) for more code samples and details on the end-to-end flow of creating and using Invisible Wallets to execute sponsored transactions. ## Methods ### shinami\_key\_createSession For security purposes, you must generate a session token before you create a wallet, or sign or execute transactions. Session tokens are valid and can be reused for 10 minutes. You may also use an instance of `ShinamiWalletSigner` to manage session token generation and refreshes for a given wallet. This is shown in the methods below that have a `sessionToken` parameter in an additional sample code tab. **Request Parameters** | Name | Type | Description | | :----- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secret | string | Used to encrypt and decrypt a wallet's private key. Therefore, it must always be used with the same `walletId` and cannot be changed in the future (see [walletId and secret pairing](/api-docs/sui/wallet-services/invisible-wallet-api#walletid-and-secret-pairing)) | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/key/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_key_createSession", "params":[ "{{secret}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { KeyClient } from "@shinami/clients/sui"; const key = new KeyClient({{walletAccessKey}}); const sessionToken = await key.createSession({{secret}}); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w", "id": 1 } ``` ```bash Shinami TypeScript SDK theme={null} "eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w" ``` **Response Fields** | Name | Type | Description | | :----- | :----- | :--------------------------------------------------------------------------------------------- | | result | string | `sessionToken` corresponding to the provided `secret`. Valid and can be reused for 10 minutes. | ### shinami\_wal\_createWallet Programmatically generates a unique wallet for a user that is Sui network agnostic (has the same address on Devnet, Testnet, and Mainnet). On the free tier you have a limit of wallet creations per month as shown on the "Sui Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#sui-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Each `walletId` only works with the `secret` you create it with (via the `sessionToken` you pass to this method). Your application MUST remember the (`walletId`, `secret`) pair associated with each Invisible Wallet you create. If you forget or change either of these values, the wallet's private key will be lost and we cannot recover it for you.** **Request Parameters** | Name | Type | Description | | :----------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. **Note:** you cannot change this value in the future, so do not use a value that you or your users might change, such as an editable `username`. | | sessionToken | string | The token generated by `shinami_key_createSession` with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v2 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_createWallet", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const walletAddress = await walletClient.createWallet( {{walletId}}, {{sessionToken}} ); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Returns the Sui address of the invisible wallet, // creating it if it hasn't been created yet. const CREATE_WALLET_IF_NOT_FOUND = true; const walletAddress = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` **Response Data** | Type | Description | | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | String | The Sui address of the Invisible Wallet created for this walletId. Network-agnostic (the address will be the same on Devnet, Testnet, and Mainnet). | ### shinami\_wal\_getWallet Retrieve a user's wallet address based your unique walletId value for it. **Request Parameters** | Name | Type | Description | | :------- | :----- | :------------------------------------------------------------ | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v2 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_wal_getWallet", "params":[ "{{walletId}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const walletAddress = await walletClient.getWallet( {{walletId}} ); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const walletAddress = await signer.getAddress(); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` **Response Fields** | Type | Description | | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | String | The Sui address of the Invisible Wallet created for this walletId. Network-agnostic (the address will be the same on Devnet, Testnet, and Mainnet). | ### shinami\_wal\_signTransactionBlock Signs a fully constructed transaction so that it can be executed. This is a low level API - it requires integration with [Gas Station API](/api-docs/sui/gas-station/api) for transaction sponsorship (if needed) and an RPC provider for transaction execution. This method gives you more control over how you submit transactions to Sui compared to `shinami_wal_executeGaslessTransactionBlock`, which sponsors, signs, and executes an Invisible Wallet transaction in one method call. **Request Parameters** | Name | Type | Description | | :----------- | :------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | txBytes | SDK: string \| Uint8ArraycURL: string | BCS serialized TransactionData, which includes gas data. It lacks only the sender's signature (which this method generates) before it can be submitted to the chain. If `string`, assumed to be Base64 encoded. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v2 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_signTransactionBlock", "params": [ "{{walletId}}", "{{sessionToken}}", "{{txBytes}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const { signature, txDigest } = await walletClient.signTransaction( {{walletId}}, {{sessionToken}}, {{txBytes}} ) ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const { signature, txDigest } = await signer.signTransaction( {{txBytes}} ); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "signature":"AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==", "txDigest":"BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj" }, "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} { signature: 'AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==', txDigest: 'BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj' } ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} { signature: 'AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==', txDigest: 'BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj' } ``` **Response Fields** | Name | Type | Description | | :-------- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | signature | string | Base64 encoded transaction signature, signed by the wallet key. To be used alongside the `txBytes` sent to this method and the gas sponsor's signature (if applicable) when executing the transaction. | | txDigest | string | Base 58 encoded transaction digest. | ### shinami\_wal\_signPersonalMessage Signs a personal message using an Invisible Wallet. The signature can be verified with the `PersonalMessage` intent scope. The request template below titled `End-to-end example with ShinamiWalletSigner - Shinami TS SDK` shows an end-to-end example of signing and a message and verifying a signature. **Request Parameters** | Name | Type | Description | | :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | message | string | Message bytes to be signed. See an example in the request template below titled `End-to-end example with ShinamiWalletSigner - Shinami TS SDK` | | wrapBcs | boolean | Optional. Set it to `true` when calling the API directly to match the verification behavior of the Sui TypeScript SDK. When using the Shinami TypeScript SDK it's set to `true` by default. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v2 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_signPersonalMessage", "params": [ "{{walletId}}", "{{sessionToken}}", "{{message}}", {{wrapBCs}} ], "id": 1 }' ``` ```bash Shinami TypeScript SDK - template theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const signature = await walletClient.signPersonalMessage( {{walletId}}, {{sessionToken}}, {{message}}, {{wrapBCS}} // optional, defaults to true if not provided ); ``` ```bash End-to-end example with ShinamiWalletSigner - Shinami TS SDK expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; import { verifyPersonalMessage } from '@mysten/sui.js/verify'; const walletClient = new WalletClient({{walletAccessKey}}); const key = new KeyClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // encode the as a base64 string const message = "I have the private keys."; const messageAsBase64String = btoa(message); // use Shinami Wallet Service to sign the message const signature = await signer.signPersonalMessage( messageAsBase64String ); // When we check the signature, we encode the message as a byte array // and not a base64 string like when we signed it const messageBytes = new TextEncoder().encode(message); // Failure throws a `Signature is not valid for the provided message` Error const publicKey = await verifyPersonalMessage(messageBytes, signature); // Get the wallet address we signed with so we can check against it const walletAddress = await signer.getAddress(); console.log(walletAddress == publicKey.toSuiAddress()); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"AFKIGo7e/eaqCbrrDKIVh4wjpHVudqP8Pbdzo+spztZGmUfiDPY9EPnTx7RnadSQHCSpxgP+QwaAvsJc4JMfswR51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "AFKIGo7e/eaqCbrrDKIVh4wjpHVudqP8Pbdzo+spztZGmUfiDPY9EPnTx7RnadSQHCSpxgP+QwaAvsJc4JMfswR51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==" ``` ```bash End-to-end example with ShinamiWalletSigner - Shinami TS SDK theme={null} true ``` **Response Data** | Type | Description | | :----- | :--------------------------------------------------------------------------------------------------------------------- | | string | Base64 encoded signature, produced by the private key of the Invisible Wallet associated with the provided `walletId`. | ### shinami\_wal\_executeGaslessTransactionBlock Sponsors, signs, and executes a gasless transaction from a wallet. This is a convenient end-to-end method for submitting sponsored transactions to the chain when you also use Shinami Gas Station. It sponsors the transaction using the Gas Station fund tied to the access key used to make the request. To see how to set up an Access Key with rights to all services, see our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#multiple-services). **Important notes** 1. **To call this method, you need an access key that is authorized for Wallet Services and Gas Station** 2. **You cannot use the gas object in a sponsored transaction for other purposes:** For example, you cannot write `const [coin] = txb.splitCoins(txb.gas,[txb.pure(100)]);` because it's accessing `txb.gas`. If you try to sponsor a TransactionKind that uses the gas object you will [get a JSON-RPC `-32602` error](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api) back from the Gas Station sponsorship attempt. 3. **Shinami sponsorship fees:** We charge a small fee (in SUI) per sponsorship request to cover our costs. For details, visit the [Billing tab](https://app.shinami.com/billing) in your Shinami dashboard. **Request Parameters** | Name | Type | Description | | :--------------------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | *cURL only:* txBytes | Base64String | Base64 encoded `TransactionKind` (as opposed to `TransactionData`) bytes. So, it does not include gas information. | | *cURL only:* gasBudget | string \| number \| null | (Optional) The gas budget you wish to use for the transaction, in MIST. The transaction will fail if the gas cost exceeds this value.

- If provided, we use the value as the budget of the sponsorship.

- If omitted, we estimate the transaction cost for you. We then add a buffer (5% for non-shared objects, 25% for shared objects) and use that total value as the budget of the sponsorship. Example: `"10000000"` or `10000000`. | | *cURL only:* gasPrice. | string \| number \| null | (Optional) Gas price override. Must be equal to or greater than the current reference gas price. If omitted the current reference price is used.

Under normal network conditions, the expectation is that this does not need to be set. For times of high network congestion, setting a gasPrice higher than the reference gas price gives your transaction higher priority. For more info, see [Sui's documentation on gas pricing](https://docs.sui.io/concepts/tokenomics/gas-pricing#computation). Example: `"1500"` or `1500`. | | *cURL only:* readMask. | \[string] \| null | (Optional) A list of the fields you want to be returned from the [`ExecutedTransaction` response](https://docs.sui.io/doc/protocol-messages-full.html#sui-rpc-v2-ExecutedTransaction) (example: `["balance_changes","transaction.digest"]`). | | *SDK-only:* tx | GaslessTransaction | `TransactionKind` and additional optional data `sender`, `gasBudget`, and `gasPrice`. The result of a call to `buildGaslessTransaction`. | After a transaction executes, read APIs (like getBalance or getObject) might not immediately show the effects. You must also wait before executing a subsequent transaction that depends on objects created or modified by the first one. WaitForLocalExecution is not supported in gRPC. Use [waitForTransaction](https://sdk.mystenlabs.com/sui/transactions/signing-and-execution#waiting-for-indexing) to ensure read after write consistency. **Auto-budgeting notes** * As a part of auto-budgeting, we put your `transactionBytes` through a [`simulateTransaction`](https://sdk.mystenlabs.com/sui/clients/core#simulatetransaction) request as a free service before we attempt to sponsor it. This call will generate error messages for certain invalid transactions, such as if the `transactionBytes` are transferring an object that's not owned by the `sender` address you provide. We'll return these errors back to you, which should be the same as if you had made a `simulateTransaction` request yourself. We do not do this step if you manually budget, so any issues that would be caught by `simulateTransaction` will instead produce an error when you try to execute the transaction. * In the time between sponsorship and execution, shared objects can change in a way that increases their transaction cost. Therefore, we encourage you to execute sponsored transactions quickly, if possible, to ensure that the sponsorship amount is sufficient. This is why we add a larger buffer on auto-budgeted sponsorships when a shared object is involved. While we believe this buffer will work in most cases, we encourage you to monitor the success rate of your auto-budgeted transactions to gauge whether your specific use-case requires manually setting an even larger `gasBudget`. **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL expandable theme={null} curl https://api.us1.shinami.com/sui/wallet/v2 \ -X POST \ -H 'X-API-Key: {{allServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_executeGaslessTransactionBlock", "params": [ "{{walletId}}", "{{sessionToken}}", "{{txBytes}}", "{{gasBudget}}", "{{gasPrice}}" "{{readMask}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK expandable theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{allServicesAccessKey}}); const sponsorSignAndExecuteResponse = await walletClient.executeGaslessTransaction( {{walletId}}, {{sessionToken}}, {{tx}}, // if tx.gasBudget is undefined we use our auto-budgeting feature {{readMask}} ); console.log(JSON.stringify(sponsorSignAndExecuteResponse)); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const sponsorSignAndExecuteResponse = await signer.executeGaslessTransaction( gaslessTx, // by not setting gaslessTx.gasBudget we take advantage of Shinami auto-budgeting ["balance_changes", "transaction.digest"] ); console.log(JSON.stringify(sponsorSignAndExecuteResponse)); ``` **Example Response** ```bash cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "transaction" : { "effects" : { "bcs" : { "name" : "TransactionEffects", "value" : "AQB7BAAAAAAAAEBCDwAAAAAA0KEpAAAAAADI7A4AAAAAAJgmAAAAAAAAILCzPZEFwHcGB/Bx3NgPR5utEz9VLnFDJoZrRaYm5RpdAQAAAAAAAiASrEO5f5yGyxk1qS2Lpy1FRPmxAnLinxhp2r190H+2ACC6iiiQHEbeUgyyMuHayINqx0J7QbCyT6nEefcCTk+wXuBZNRgAAAAAAnaTbRZItDPZbR8HdrYTUvCdIAu7YIa6p1RDUnNpi4bCAd9ZNRgAAAAAIDyRkCT30uLnKo627J+DgBX9oNZcR0xEbNfoVEg56U4fAJh5SY5IrcTSHiYM127Qe963wJ3iHcOn6ttGEnwerxETASC6+0E8Z9zRCWB8oMhZ5sKxZP/37GRMUlvJqHXR3QTdyQCYeUmOSK3E0h4mDNdu0Hvet8Cd4h3Dp+rbRhJ8Hq8REwC9FwM24E42LJ+GgtwMshbxjb5+91u3TU8M3m4faYmrIQABILx4DoZM++kPQyBpSSqXWm546EZt6qwAfcs3JIbu8mKPAJCQ/9/twkWJlZ2CfSrkgmJAbyYdbSg5z0guXz+9YcWdAQAA" }, "changedObjects" : [ { "idOperation" : "NONE", "inputDigest" : "55SCX1r8Xb8A4cPp5XSs4Fg2QxY8JWqiCofRUmrEcwPc", "inputOwner" : { "address" : "0x9879498e48adc4d21e260cd76ed07bdeb7c09de21dc3a7eadb46127c1eaf1113", "kind" : "ADDRESS" }, "inputState" : "INPUT_OBJECT_STATE_EXISTS", "inputVersion" : "406149599", "objectId" : "0x76936d1648b433d96d1f0776b61352f09d200bbb6086baa754435273698b86c2", "objectType" : "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI>", "outputDigest" : "Dau3V56JuP2V5iHdoRQgbMpa9YsrnkxrqQr6RJ8tPAQG", "outputOwner" : { "address" : "0x9879498e48adc4d21e260cd76ed07bdeb7c09de21dc3a7eadb46127c1eaf1113", "kind" : "ADDRESS" }, "outputState" : "OUTPUT_OBJECT_STATE_OBJECT_WRITE", "outputVersion" : "406149600" }, { "idOperation" : "CREATED", "inputState" : "INPUT_OBJECT_STATE_DOES_NOT_EXIST", "objectId" : "0xbd170336e04e362c9f8682dc0cb216f18dbe7ef75bb74d4f0cde6e1f6989ab21", "objectType" : "0x86841b9e38726ee77e4720861ddb3a4e4518afdcf84a1972b952568ee59ffe70::sword::Sword", "outputDigest" : "DghpuYpp6m3kxgp5pBRjVQnhJiHNrQ9JhbfVjSGmU7hG", "outputOwner" : { "address" : "0x9090ffdfedc24589959d827d2ae48262406f261d6d2839cf482e5f3fbd61c59d", "kind" : "ADDRESS" }, "outputState" : "OUTPUT_OBJECT_STATE_OBJECT_WRITE", "outputVersion" : "406149600" } ], "dependencies" : [ "2Fth2qN8uwEW2bxKnzm8sNvgCH4UndASg3vYnRVqaw6X", "DZB29fPCXFGSbF9qMDs6iyCXedhhKZ1f8VsskRfY6L8d" ], "digest" : "78gwmLnUam2fwsgc2bs9w11wxRTg35BJskHWDCJQcnp9", "epoch" : "1147", "gasObject" : { "idOperation" : "NONE", "inputDigest" : "55SCX1r8Xb8A4cPp5XSs4Fg2QxY8JWqiCofRUmrEcwPc", "inputOwner" : { "address" : "0x9879498e48adc4d21e260cd76ed07bdeb7c09de21dc3a7eadb46127c1eaf1113", "kind" : "ADDRESS" }, "inputState" : "INPUT_OBJECT_STATE_EXISTS", "inputVersion" : "406149599", "objectId" : "0x76936d1648b433d96d1f0776b61352f09d200bbb6086baa754435273698b86c2", "outputDigest" : "Dau3V56JuP2V5iHdoRQgbMpa9YsrnkxrqQr6RJ8tPAQG", "outputOwner" : { "address" : "0x9879498e48adc4d21e260cd76ed07bdeb7c09de21dc3a7eadb46127c1eaf1113", "kind" : "ADDRESS" }, "outputState" : "OUTPUT_OBJECT_STATE_OBJECT_WRITE", "outputVersion" : "406149600" }, "gasUsed" : { "computationCost" : "1000000", "nonRefundableStorageFee" : "9880", "storageCost" : "2728400", "storageRebate" : "978120" }, "lamportVersion" : "406149600", "status" : { "success" : true }, "transactionDigest" : "CtmH1VQ5DVRiruSZFJV1M4dGxoDpasVkyb18win6Lr4c", "version" : 2 } } } } ``` ```bash Shinami TypeScript SDK theme={null} { "transaction":{ "signatures":[], "balanceChanges":[ { "address":"0x8e1e504fbf0c54d43e948951f50f3710fff83bb91ee2c911509118ed1331f5ef", "coinType":"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", "amount":"-2750280" } ], "transaction":{ "digest":"wU8azYQkUNfFikAJXrL7cTp3jGU5eSQxEPT8H3C2bzd" } } } ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} { "transaction":{ "signatures":[], "balanceChanges":[ { "address":"0x8e1e504fbf0c54d43e948951f50f3710fff83bb91ee2c911509118ed1331f5ef", "coinType":"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", "amount":"-2750280" } ], "transaction":{ "digest":"wU8azYQkUNfFikAJXrL7cTp3jGU5eSQxEPT8H3C2bzd" } } } ``` **Response Fields** | Type | Description | | :----------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | | [`ExecutedTransaction`](https://docs.sui.io/doc/protocol-messages-full.html#sui-rpc-v2-ExecutedTransaction) in JSON form | Contains information about the executed transaction with optional fields depending on your `readMask` request value. | # (retired) Invisible Wallet API v1 Source: https://docs.shinami.com/api-docs/sui/wallet-services/invisible-wallet-api-v1 No longer supported - use the new API **This API was retired at end of UTC day July 30, 2026. Use the [new API version](/api-docs/sui/wallet-services/invisible-wallet-api).** Migrating to [API v2](/api-docs/sui/wallet-services/invisible-wallet-api) involves the following changes: 1. Change the wallet service URL of your requests from `https://api.us1.shinami.com/sui/wallet/v1` to `https://api.us1.shinami.com/sui/wallet/v2`. **Important:** key service URL is not changing. It stays as v1: `https://api.us1.shinami.com/sui/key/v1`. 2. Do not send the following requests, which are no longer supported: `shinami_walx_setBeneficiary`, `shinami_walx_unsetBeneficiary`, `shinami_walx_getBeneficiary`. 3. If you send `shinami_wal_executeGaslessTransactionBlock`, adjust to [the updated request and response bodies](/api-docs/sui/wallet-services/invisible-wallet-api#shinami_wal_executegaslesstransactionblock), which is now aligned with Mysten's gRPC version of executeTransactionBlock. The latest version of our [Shinami clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) includes native support for Invisible Wallet API v2. It also has breaking changes to support [v2 of the Sui TypeScript SDK](https://github.com/MystenLabs/ts-sdks/blob/main/packages/sui/CHANGELOG.md) (which has the latest support for gRPC). ## Methods ### shinami\_key\_createSession For security purposes, you must generate a session token before you create a wallet, or sign or execute transactions. Session tokens are valid and can be reused for 10 minutes. You may also use an instance of `ShinamiWalletSigner` to manage session token generation and refreshes for a given wallet. This is shown in the methods below that have a `sessionToken` parameter in an additional sample code tab. **Request Parameters** | Name | Type | Description | | :----- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secret | string | Used to encrypt and decrypt a wallet's private key. Therefore, it must always be used with the same `walletId` and cannot be changed in the future (see [walletId and secret pairing](/api-docs/sui/wallet-services/invisible-wallet-api#walletid-and-secret-pairing)) | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/key/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_key_createSession", "params":[ "{{secret}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { KeyClient } from "@shinami/clients/sui"; const key = new KeyClient({{walletAccessKey}}); const sessionToken = await key.createSession({{secret}}); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w", "id": 1 } ``` ```bash Shinami TypeScript SDK theme={null} "eyJraWQiOiJrZXkyMDIzMDgxMSIsImVuYyI6IkEyNTZHQ00iLCJ0YWciOiI4SVpQWXlHeDlmOTd6U2NIdmN6N3lnIiwiYWxnIjoiQTI1NkdDTUtXIiwiaXYiOiJQWVJXZFJrbnNMMlNnVzhfIn0.ygDCI-NcvUcH7wYc0Bp0-59qeIfGOqLyXZGsLF4pW0M.aOAW0AwBvAWpaS-S.QmesdNIdNIYbT59RET-lNuzNMUvS-xb2exhXrAIlspnIkV3nuBx7PKC_GgJ7C1EqJx3tDtQaLLDGdrO8_s-75oK88ls5mzDRR-w2A0VdCcTH0_JwsQgijIbCKFWS0g.dULMzxZ4gGbm2unqOnzv8w" ``` **Response Fields** | Name | Type | Description | | :----- | :----- | :--------------------------------------------------------------------------------------------- | | result | string | `sessionToken` corresponding to the provided `secret`. Valid and can be reused for 10 minutes. | ### shinami\_wal\_createWallet Programmatically generates a unique wallet for a user that is Sui network agnostic (has the same address on Devnet, Testnet, and Mainnet). On the free tier you have a limit of wallet creations per month as shown on the "Sui Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#sui-invisible-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Each `walletId` only works with the `secret` you create it with (via the `sessionToken` you pass to this method). Your application MUST remember the (`walletId`, `secret`) pair associated with each Invisible Wallet you create. If you forget or change either of these values, the wallet's private key will be lost and we cannot recover it for you.** **Request Parameters** | Name | Type | Description | | :----------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | A unique ID you maintain for the wallet. Can be based on your internal user IDs. **Note:** you cannot change this value in the future, so do not use a value that you or your users might change, such as an editable `username`. | | sessionToken | string | The token generated by `shinami_key_createSession` with the unalterable secret you will permanently associate with this walletId (and, ideally, only this walletId). | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_createWallet", "params": [ "{{walletId}}", "{{sessionToken}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const walletAddress = await walletClient.createWallet( {{walletId}}, {{sessionToken}} ); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // Returns the Sui address of the invisible wallet, // creating it if it hasn't been created yet. const CREATE_WALLET_IF_NOT_FOUND = true; const walletAddress = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` **Response Data** | Type | Description | | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | String | The Sui address of the Invisible Wallet created for this walletId. Network-agnostic (the address will be the same on Devnet, Testnet, and Mainnet). | ### shinami\_wal\_getWallet Retrieve a user's wallet address based your unique walletId value for it. **Request Parameters** | Name | Type | Description | | :------- | :----- | :------------------------------------------------------------ | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_wal_getWallet", "params":[ "{{walletId}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const walletAddress = await walletClient.getWallet( {{walletId}} ); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const walletAddress = await signer.getAddress(); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} "0xecaeb4a763dd49f2cd13aeaf2e7ab01f704bbc8c2bd9c2e991b726d0632c3b4f" ``` **Response Fields** | Type | Description | | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | String | The Sui address of the Invisible Wallet created for this walletId. Network-agnostic (the address will be the same on Devnet, Testnet, and Mainnet). | ### shinami\_wal\_signTransactionBlock Signs a fully constructed transaction so that it can be executed. This is a low level API - it requires integration with [Gas Station API](/api-docs/sui/gas-station/api) for transaction sponsorship (if needed) and an RPC provider for transaction execution. This method gives you more control over how you submit transactions to Sui compared to `shinami_wal_executeGaslessTransactionBlock`, which sponsors, signs, and executes an Invisible Wallet transaction in one method call. **Request Parameters** | Name | Type | Description | | :----------- | :------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | txBytes | SDK: string \| Uint8ArraycURL: string | BCS serialized TransactionData, which includes gas data. It lacks only the sender's signature (which this method generates) before it can be submitted to the chain. If `string`, assumed to be Base64 encoded. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_signTransactionBlock", "params": [ "{{walletId}}", "{{sessionToken}}", "{{txBytes}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const { signature, txDigest } = await walletClient.signTransaction( {{walletId}}, {{sessionToken}}, {{txBytes}} ) ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const { signature, txDigest } = await signer.signTransaction( {{txBytes}} ); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":{ "signature":"AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==", "txDigest":"BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj" }, "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} { signature: 'AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==', txDigest: 'BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj' } ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} { signature: 'AKzbe4FlhuT9saKFDUEdmCELBVa/NhsERc2XPahGC+8Ar6YMoK+DH+xs8xg/RSYF7HeZ4UmwnSPJFZpYjgWWZQB51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==', txDigest: 'BSFD6oDgftrtcVCZF8EAkUcmWWyd8ZRsMCGSh6EbtqCj' } ``` **Response Fields** | Name | Type | Description | | :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | signature | string | Base64 encoded transaction signature, signed by the wallet key. To be used alongside the `txBytes` sent to this method and the gas sponsor's signature (if applicable) when you execute the transaction block | | txDigest | string | Base 58 encoded transaction digest. | ### shinami\_wal\_signPersonalMessage Signs a personal message using an Invisible Wallet. The signature can be verified with the `PersonalMessage` intent scope. The request template below titled `End-to-end example with ShinamiWalletSigner - Shinami TS SDK` shows an end-to-end example of signing and a message and verifying a signature. **Request Parameters** | Name | Type | Description | | :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | message | string | Message bytes to be signed. See an example in the request template below titled `End-to-end example with ShinamiWalletSigner - Shinami TS SDK` | | wrapBcs | boolean | Optional. Set it to `true` when calling the API directly to match the verification behavior of the Sui TypeScript SDK. When using the Shinami TypeScript SDK it's set to `true` by default. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL theme={null} curl https://api.us1.shinami.com/sui/wallet/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_signPersonalMessage", "params": [ "{{walletId}}", "{{sessionToken}}", "{{message}}", {{wrapBCs}} ], "id": 1 }' ``` ```bash Shinami TypeScript SDK - template theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{walletAccessKey}}); const signature = await walletClient.signPersonalMessage( {{walletId}}, {{sessionToken}}, {{message}}, {{wrapBCS}} // optional, defaults to true if not provided ); ``` ```bash End-to-end example with ShinamiWalletSigner - Shinami TS SDK expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; import { verifyPersonalMessage } from '@mysten/sui.js/verify'; const walletClient = new WalletClient({{walletAccessKey}}); const key = new KeyClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); // encode the as a base64 string const message = "I have the private keys."; const messageAsBase64String = btoa(message); // use Shinami Wallet Service to sign the message const signature = await signer.signPersonalMessage( messageAsBase64String ); // When we check the signature, we encode the message as a byte array // and not a base64 string like when we signed it const messageBytes = new TextEncoder().encode(message); // Failure throws a `Signature is not valid for the provided message` Error const publicKey = await verifyPersonalMessage(messageBytes, signature); // Get the wallet address we signed with so we can check against it const walletAddress = await signer.getAddress(); console.log(walletAddress == publicKey.toSuiAddress()); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc":"2.0", "result":"AFKIGo7e/eaqCbrrDKIVh4wjpHVudqP8Pbdzo+spztZGmUfiDPY9EPnTx7RnadSQHCSpxgP+QwaAvsJc4JMfswR51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==", "id":1 } ``` ```bash Shinami TypeScript SDK theme={null} "AFKIGo7e/eaqCbrrDKIVh4wjpHVudqP8Pbdzo+spztZGmUfiDPY9EPnTx7RnadSQHCSpxgP+QwaAvsJc4JMfswR51Goyfzm4soRhJY9gDmt/wDZYCm81bkCP87eBm1T+Xw==" ``` ```bash End-to-end example with ShinamiWalletSigner - Shinami TS SDK theme={null} true ``` **Response Data** | Type | Description | | :----- | :--------------------------------------------------------------------------------------------------------------------- | | string | Base64 encoded signature, produced by the private key of the Invisible Wallet associated with the provided `walletId`. | ### shinami\_wal\_executeGaslessTransactionBlock Sponsors, signs, and executes a gasless transaction from a wallet. This is a convenient end-to-end method for submitting sponsored transactions to the chain when you also use Shinami Gas Station. It sponsors the transaction using the Gas Station fund tied to the access key used to make the request. To see how to set up an Access Key with rights to all services, see our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#multiple-services). Note that this call produces a Node service `sui_executeTransactionBlock` request which counts against your daily request total (and so your billing). **Important notes** 1. **To call this method, you need an access key that is authorized for all of these Shinami products:** Wallet Services, Gas Station, Node Service. 2. **You cannot use the gas object in a sponsored transaction for other purposes:** For example, you cannot write `const [coin] = txb.splitCoins(txb.gas,[txb.pure(100)]);` because it's accessing `txb.gas`. If you try to sponsor a TransactionKind that uses the gas object you will [get a JSON-RPC `-32602` error](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api) back from the Gas Station sponsorship attempt. 3. **Shinami sponsorship fees:** We charge a small fee (in SUI) per sponsorship request to cover our costs. For details, visit the [Billing tab](https://app.shinami.com/billing) in your Shinami dashboard. **Request Parameters** | Name | Type | Description | | :----------------------- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | walletId | string | Your unique, internal id for the associated Invisible Wallet. | | sessionToken | string | The token generated by `shinami_key_createSession` with the same secret you used when creating this wallet. | | options | object | [``](https://github.com/MystenLabs/sui/blob/3699dd3646aec6a6370ecfd2cf957d7b40fbc16d/sdk/typescript/src/client/types/generated.ts#L1320) - Optional. Sui options for specifying the transaction content to be returned | | requestType (deprecated) | string | [``](https://github.com/MystenLabs/sui/blob/3699dd3646aec6a6370ecfd2cf957d7b40fbc16d/sdk/typescript/src/client/types/generated.ts#L266) - Optional. The execution request type (`WaitForEffectsCert` or `WaitForLocalExecution`). Note that calling this method - via our SDK or otherwise - does not have the same result as using the [Mysten SDK v 1.6 and above](https://github.com/MystenLabs/sui/blob/main/sdk/typescript/CHANGELOG.md#160) for `executeTransactionBlock` in that it does not also call `waitForTransaction` to poll the Fullnode to ensure that the transaction has been indexed after execution. If you require read-after-write consistency you will need to explicitly call `waitForTransaction` after calling this method. | | *SDK-only:* tx | GaslessTransaction | `TransactionKind` and additional optional data `sender`, `gasBudget`, and `gasPrice`. The result of a call to `buildGaslessTransaction`. | | *cURL only:* txBytes | string | Base64 encoded `TransactionKind` (as opposed to `TransactionData`) bytes. So, it does not include gas information. | | *cURL only:* gasBudget | string | (Optional) The gas budget you wish to use for the transaction, in MIST. The transaction will fail if the gas cost exceeds this value.

- If provided, we use the value as the budget of the sponsorship.

- If omitted, we estimate the transaction cost for you. We then add a buffer (5% for non-shared objects, 25% for shared objects) and use that total value as the budget of the sponsorship. | **Auto-budgeting notes** * As a part of auto-budgeting, we put your `transactionBytes` through a [sui\_dryRunTransactionBlock](/api-docs/sui/node-service/json-rpc/write-api#sui-dryruntransactionblock) request as a free service before we attempt to sponsor it. This call will generate error messages for certain invalid transactions, such as if the `transactionBytes` are transferring an object that's not owned by the `sender` address you provide. We'll return these errors back to you, which should be the same as if you had made a `sui_dryRunTransactionBlock` request yourself. We do not do this step if you manually budget, so any issues that would be caught by `sui_dryRunTransactionBlock` will instead produce an error when you try to execute the transaction. * In the time between sponsorship and execution, shared objects can change in a way that increases their transaction cost. Therefore, we encourage you to execute sponsored transactions quickly, if possible, to ensure that the sponsorship amount is sufficient. This is why we add a larger buffer on auto-budgeted sponsorships when a shared object is involved. While we believe this buffer will work in most cases, we encourage you to monitor the success rate of your auto-budgeted transactions to gauge whether your specific use-case requires manually setting an even larger `gasBudget`. **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL expandable theme={null} curl https://api.us1.shinami.com/sui/wallet/v1 \ -X POST \ -H 'X-API-Key: {{allServicesAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "shinami_wal_executeGaslessTransactionBlock", "params": [ "{{walletId}}", "{{sessionToken}}", "{{txBytes}}", "{{gasBudget}}", { "showInput": false, "showRawInput": false, "showEffects": false, "showEvents": false, "showObjectChanges": false, "showBalanceChanges": false }, "{{requestType}}" ], "id": 1 }' ``` ```bash Shinami TypeScript SDK expandable theme={null} import { WalletClient } from "@shinami/clients/sui"; const walletClient = new WalletClient({{allServicesAccessKey}}); const txResponse = await walletClient.executeGaslessTransaction( {{walletId}}, {{sessionToken}}, {{tx}}, // if tx.gasBudget is undefined we use our auto-budgeting featur { showInput: false, showRawInput: false, showEffects: false, showEvents: false, showObjectChanges: false, showBalanceChanges: false }, {{requestType}} // must set to `None` or `WaitForLocalExecution` if showEffects, showObjectChanges, or showBalanceChanges are set to true ); ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner expandable theme={null} import { WalletClient, KeyClient, ShinamiWalletSigner } from "@shinami/clients/sui"; const keyClient = new KeyClient({{walletAccessKey}}); const walletClient = new WalletClient({{walletAccessKey}}); const signer = new ShinamiWalletSigner( {{walletId}}, walletClient, {{walletSecret}}, keyClient ); const txResponse = await signer.executeGaslessTransaction( {{tx}}, // if tx.gasBudget is undefined we use our auto-budgeting feature { showInput: false, showRawInput: false, showEffects: false, showEvents: false, showObjectChanges: false, showBalanceChanges: false }, {{requestType}} // must set to `None` or `WaitForLocalExecution` if showEffects, showObjectChanges, or showBalanceChanges are set to true ); ``` **Example Response** ```bash cURL theme={null} { "jsonrpc": "2.0", "result": { "digest": "Em4C8d6rRSUQ72kUWd627UfXTqDAVWQjJq9tmUFfnrmm", "confirmedLocalExecution": true }, "id": 1 } ``` ```bash Shinami TypeScript SDK theme={null} { digest: 'B6j8ePkw84R1rpUqxmjaZ3dTuu6GhPdY9MqoSso6kAn7', confirmedLocalExecution: true } ``` ```bash Shinami TypeScript SDK using ShinamiWalletSigner theme={null} { digest: 'B6j8ePkw84R1rpUqxmjaZ3dTuu6GhPdY9MqoSso6kAn7', confirmedLocalExecution: true } ``` **Response Fields** | Type | Description | | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SuiTransactionBlockResponse | [``](https://github.com/MystenLabs/sui/blob/3699dd3646aec6a6370ecfd2cf957d7b40fbc16d/sdk/typescript/src/client/types/generated.ts#L1299) containing information about the executed transaction. | # zkLogin wallet API Source: https://docs.shinami.com/api-docs/sui/wallet-services/zklogin-wallet-api User-controlled wallets to make Web3 unseen ## Overview zkLogin is a Sui primitive that allows users to transact on Sui using the OAuth login flows they're familiar with, eliminating the friction of handling cryptographic keys or remembering mnemonics. zkLogin can be thought of as a two-factor authentication scheme, since sending a transaction requires both a credential from a recent OAuth login and a salt managed by someone other than the OAuth provider (in this case, Shinami). To learn more, see [the Sui Foundation's zkLogin doc](https://docs.sui.io/concepts/cryptography/zklogin). You'll find API endpoints and key usage notes below. If you ever need help you can [reach out to us](/help-center/overview#contacting-support). ### Use Cases Core use cases include app-managed NFTs or closed-loop tokens. For a breakdown of the wallets we offer and wallet use-cases, see our [high-level guide](/product-overviews/sui/wallets#wallet-services-overview). ### Authentication, Rate Limits, and Error Handling **Authentication** You authenticate via an access key passed in a header ('X-Api-Key: ACCESS\_KEY') or in the request url, e.g. `https://api.us1.shinami.com/sui/zkprover/v1/ACCESS_KEY`. We recommend using a request header and not putting access keys in your request URLs for reduced visibility (in logs, etc). These steps are done automatically by our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). For more information, including how to set up an access key with Wallet Services rights, see our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide. **Call this API from your backend** Shinami Wallet Services do not support CORS requests, so if you call these APIs from your frontend you'll get a CORS error. This is for security reasons: exposed wallet information could lead to malicious actors signing transactions on behalf of your users. **Error Handling** See our Error Reference for guidance on the errors you may receive from our services, including a section on errors specific to the [zkLogin wallet API](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api). **Rate Limits** When you surpass the QPS limit for a key, we return a JSON-RPC error code `-32010`. We recommend implementing retries with a backoff to handle any rate limits errors that arise. You can also [adjust the rate limits of your keys](/developer-guides/core-integration-topics/authentication-and-api-keys#update-a-key%E2%80%99s-qps%2Fcups-values) to better balance your QPS allotment across your keys. We also have a limit of two zk proofs per address per minute. When you hit this limit, we return a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api). ### zkLogin Addresses A zkLogin wallet address is derived from `iss, aud, sub, salt` in the OpenID connect response, where: * `iss`: The OpenID provider. * `aud` : The unique identifier assigned to your application by the OpenID provider. * `sub`: The OpenID provider's locally unique and never reassigned identifier for the user. * `salt`: A consistent, ideally unique, value used to unlink the OpenID identifier with the on-chain address. As a result, a user will have a new zkLogin wallet address if any of the above values change. For example, if one of your users logs in via Google and then Twitch - or with one Google email and then another - and you use both credentials with zkLogin, each credential will be linked to a different address. To learn more, see [the Sui Foundation's zkLogin doc.](https://docs.sui.io/concepts/cryptography/zklogin#will-my-zklogin-address-ever-change) ## Tutorial + SDKs The endpoints below show sample requests using our [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) (in addition to cURL). We also have a [Next.js zkLogin SDK](https://www.npmjs.com/package/@shinami/nextjs-zklogin), which aims to provide full-stack support for building user authentication and Sui transaction execution into your Next.js application, using zkLogin primitives. We built a [Next.js starter template](https://github.com/shinamicorp/shinami-typescript-sdk/tree/aaeed1e9774dcf027e082bb699165710e9e6a4cb/examples/nextjs-zklogin) to quickly get you off the ground using this SDK. Even if you are starting with an existing Next.js application, it's recommended to check out that example as a reference end-to-end implementation. Our [Next.js tutorial](/developer-guides/sui/tutorials/zklogin-nextjs-app-template) has guidance on running our zkLogin app starter template. ## zkLogin wallet service methods ### shinami\_zkw\_getOrCreateZkLoginWallet Retrieves a zkLogin wallet along with its associated salt, creating a new wallet if necessary. This method provides the option to create and use multiple zkLogin wallets per (user, OAuth provider) pair by using the optional `subWallet` parameter. Note that your users will already have a different zkLogin wallet for each OAuth provider they use, whose `jwt` you pass to this method (see [zkLogin Addresses](/api-docs/sui/wallet-services/zklogin-wallet-api#zklogin-addresses) for more details). On the free tier you have a limit of wallet creations per month as shown on the "Sui Wallet Services" tab of the [billing page](https://app.shinami.com/billing/) in your dashboard (where you can also see how to upgrade if needed). If you hit this limit, you will get a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api) and should not retry. All other wallet operations will still work for the month, like signing with wallets you've already created. **Request Parameters** | Name | Type | Description | | :----------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jwt | `String` | A valid JWT signed by one of the supported OpenID providers. The JWT nonce must be prepared according to the [zkLogin requirements](https://docs.sui.io/guides/developer/cryptography/zklogin-integration#get-jwt) . | | keyClaimName | `String` | Optional. The claim name in the JWT that identifies a particular user. Defaults to "sub". Currently, Mysten's zkLogin implementation only supports the "sub" field of the OpenID spec. This parameter allows for forward compatibility as the implementation expands to support other values. | | subWallet | `Integer` | Optional. The sub-wallet id, which enables the same OpenID user to have more than one wallet address tied to your app. Defaults to `0` | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL expandable theme={null} curl https://api.us1.shinami.com/sui/zkwallet/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_zkw_getOrCreateZkLoginWallet", "params":[ "{{signedJWTToken}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK theme={null} import { ZkWalletClient } from "@shinami/clients/sui"; // Obtain wallet access key from your Shinami web portal. const zkWalletClient = new ZkWalletClient({{walletAccessKey}}); // Prepare a nonce according to the zkLogin requirements. // Obtain a valid jwt with that nonce from a supported OpenID provider. // Get zkLogin wallet salt. const walletInfo = await zkWalletClient.getOrCreateZkLoginWallet({{jwt}}); ``` **Example Response** ```bash cURL expandable theme={null} { "jsonrpc": "2.0", "result": { "userId": { "iss": "https://id.twitch.tv/oauth2", "aud": "2bc0920c90ufn0adomxlca3pldbdpo", "keyClaimName": "sub", "keyClaimValue": "984333370" }, "subWallet": 0, "salt": "kL+fRWN8888cKl5D72qzCQ==", "address": "0x6a7b07da9d2c210bbbe044dbe29f0c5ae20444584a4d8ee9a804839f8256ea3c" }, "id": 1 } ``` ```bash Shinami TypeScript SDK theme={null} { userId: { iss: 'https://id.twitch.tv/oauth2', aud: '2bc0920c90ufn0adomxlca3pldbdpo', keyClaimName: 'sub', keyClaimValue: '984333370' }, subWallet: 0, salt: 192403790493605555550623021243959522057n, address: '0x6a7b07da9d2c210bbbe044dbe29f0c5ae20444584a4d8ee9a804839f8256ea3c' } ``` **Response Data** | Name | Type | Description | | :-------- | :------------------------------------------ | :---------------------------------------------------- | | userId | `Object` | The OAuth user information used to create the wallet. | | subWallet | `Integer` | The subWallet number. | | salt | cURL: `Base64 encoded String` SDK: `BigInt` | The salt associated with this zkLogin wallet. | | address | `Hex encoded String` | The Sui zkLogin address of this wallet. | ## zkProver service methods ### shinami\_zkp\_createZkLoginProof Generate a zkLogin proof prior to signing and executing a transaction. The example below uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients). **Notes:** * Devnet has a different zkey than Testnet and Mainnet. Shinami's zk prover currently only works with Testnet and Mainnet. * We have a limit of two zk proofs per address per minute. When you hit this limit, we return a [JSON-RPC code `-32012`](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api). **Request Parameters** | Name | Type | Description | | :------------------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jwt | `string` | A valid JWT signed by one of the supported OpenID providers. The JWT nonce must be prepared according to the [zkLogin requirements](https://docs.sui.io/guides/developer/cryptography/zklogin-integration#get-jwt). | | maxEpoch | cURL: `string`SDK: `number` | The max epoch used to prepare the JWT nonce. This governs the expiration of the zkLogin proof. Once it expires, you will have to ask the user to sign in again. One epoch is roughly one day. See the expiration of recent epochs in a Sui explorer like [Suivision](https://suivision.xyz/) or [Suiscan](https://suiscan.xyz/) (note that Mainnet and Testnet are not on the same epoch). | | extendedEphemeralPublicKey | cURL: `BigInt representation or Base64 encoded`SDK: `PublicKey` | The extended ephemeral public key used to prepare the JWT nonce ([see example of how to generate it here](https://docs.sui.io/guides/developer/cryptography/zklogin-integration#get-the-zero-knowledge-proof)). The corresponding private key must be used to sign any transactions to be executed with this proof. | | jwtRandomness | cURL: `string: BigInt representation or Base64 encoded` SDK: `bigint` | The random bytes used to prepare the JWT nonce ([see example here](https://docs.sui.io/guides/developer/cryptography/zklogin-integration#get-jwt)). | | salt | cURL: `string: BigInt representation or Base64 encoded` SDK: `bigint` | The zkLogin wallet salt. Together with the provided JWT, this determines the zkLogin wallet's on-chain address. This method works with salts managed by Shinami, buy you, by another third party, or by the user. Note, though, that [if the salt changes, the user's address changes](/api-docs/sui/wallet-services/zklogin-wallet-api#zklogin-addresses). | | keyClaimName | `string` | Optional. The claim name in the JWT that identifies a particular user. Defaults to "sub". Currently, Mysten's zkLogin implementation only supports the `sub` field of the OpenID spec. This parameter allows for forward compatibility as the implementation expands to support other values. | **Example Request Template** The TypeScript example uses the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients), which you can install with: ```bash Shell theme={null} npm install @shinami/clients ``` Replace all instances of `{{name}}` with the actual value for that name. ```bash cURL expandable theme={null} curl https://api.us1.shinami.com/sui/zkprover/v1 \ -X POST \ -H 'X-API-Key: {{walletAccessKey}}' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"shinami_zkp_createZkLoginProof", "params":[ "{{jwt}}", "{{maxEpoch}}", "{{extendedEphemeralPublicKey}}", "{{jwtRandomness}}", "{{salt}}" ], "id":1 }' ``` ```bash Shinami TypeScript SDK expandable theme={null} import { ZkProverClient } from "@shinami/clients/sui"; // Obtain wallet access key from your Shinami web portal. const zkProverClient = new ZkProverClient({{walletAccessKey}}); // Create a zkProof. const zkProof = await zkProverClient.createZkLoginProof( {{jwt}}, {{maxEpoch}}, {{ephemeralPublicKey}}, {{jwtRandomness}}, {{salt}} ); // Now you can sign transaction blocks with ephemeralPrivateKey, and assemble the zkLogin signature // using zkProof. ``` ```bash cURL partial examples expandable theme={null} curl https://api.us1.shinami.com/sui/zkprover/v1 \ -X POST \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ // extendedEphemeralPublicKey, jwtRandomness, and salt as base64 string -d '{ "jsonrpc":"2.0", "method":"shinami_zkp_createZkLoginProof", "params":[ "{jwt}", "{{maxEpoch}}", "AF1VXNxX7pN1rJW7K+P9Ml/f0n2gk1e/MOKcIt1rws08", "T6LZabVx1kjRq/kUiRdgdg==", "Jpu+BrIH4yijTBhkgJg1xw==" ], "id":1 }' // extendedEphemeralPublicKey, jwtRandomness, and salt as BigInt string -d '{ "jsonrpc":"2.0", "method":"shinami_zkp_createZkLoginProof", "params":[ "{jwt}", "{{maxEpoch}}", "107198930224357545647865893180858238822499898890599700364072551532840382986657", "105854573416086459770441403231754477686", "51319324041191237311498376502115317191" ], "id":1 }' ``` **Example Response** ```bash cURL expandable theme={null} { "jsonrpc": "2.0", "result": { "zkProof": { "proofPoints": { "a": ["15523188928150068463877187176143113666714843703470604151076285692983325640642", "5937622317780822197753258881898720685630444182643789312895382585679701582730", "1"], "b": [ ["16826302723856343423452057269835204629749355045593813315170205467027472681113", "13797818130832460450334887874511731864855590610477696974834238052347712422314"], ["13373917712679082794928414217966585760669198351828608413595476883128268622714", "5700527829855762859349841977943997112007134227264278415875624634861579977183"], ["1", "0"] ], "c": ["7520248514700601656024563691610669758825183429353083260336955084416144661718", "20552359067831153630006195193902483617859079319246848227965146473634030062290", "1"] }, "issBase64Details": { "value": "wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw", "indexMod4": 2 }, "headerBase64": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ" } }, "id": 1 } ``` ```bash Shinami TypeScript SDK expandable theme={null} { proofPoints: { a: [ '10210145730043037811940974600447596138332245137712632111543948579663620583434', '7161160787659733995589070724990899004457677439433421417304252171408748983773', '1' ], b: [ [Array], [Array], [Array] ], c: [ '14336916288409630215040261300089886420211729238372809076105326107110995622366', '16791539167604801843541798961147781044566621780138018818605704529730677460898', '1' ] }, issBase64Details: { value: 'wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw', indexMod4: 2 }, headerBase64: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ' } ``` **Response Data** | Type | Description | | :------------ | :----------------------------------- | | `JSON object` | The zkLogin proof for the given JWT. | ## Appendix ### Common issues #### JSON RPC -32002 on transaction execution (Invalid user signature) **Example error** ```bash theme={null} JsonRpcError: Invalid user signature: Required Signature from 0x6e20e4e7e3822ccd19614dd8db4fafdab3e0a888d54d272ef678a4c24937c5b4 is absent ["0x1c1a56df0bd80abc6dd097b98cd4ff341c3ef2bdb0f2ea7d0ebd87eff3268b6c", "0x3452ddf0448cc52e62d263c1c49553cb744dbb76bca56e9f12c98b2ffb9b82a5"] at SuiHTTPTransport.request (...node_modules/@mysten/sui/src/client/http-transport.ts:123:10) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at SuiClient.executeTransactionBlock (...src/server/main.ts:135:30) { code: -32002, type: 'TransactionExecutionClientError' } ``` **Possible cause: passing incorrect or missing values when generating address seed and zk signature** Ensure that you're providing all the correct values when generating your emphemeral Keypair signature, address seed, and zkLogin signature (all the steps [in this section](https://docs.sui.io/guides/developer/cryptography/zklogin-integration#assemble-the-zklogin-signature-and-submit-the-transaction)). The above error happened for me because I wasn't properly storing the `sub` value, and I was retrieving it as `undefined` from session storage when generating my address seed. Once I printed out my key variable values (sub, aud, salt, maxEpoch, ephemeral Keypair public key or address) at the time of fetching the JWT and compared them to the what I fetched from session storage right before generating my address seed and zkLogin signature, I was able to catch and fix the issue. Then my transaction when through without an error. #### Other issues See the [zkLogin section of our Error Guide](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api) for examples. # Shinami updates Source: https://docs.shinami.com/changelog Sunsetted our Sui Invisible Wallet API v1. You must now use [API v2](/api-docs/sui/wallet-services/invisible-wallet-api), which support Sui gRPC. Launched `v0.12.0` of our clients SDK and `v0.5.0` of our nextsjs-zklogin SDK to support Mysten's Sui Typescript SDK `v2` and our Invisible Wallet API `v2`. **Breaking changes for Sui users:** see Mysten's SDK version 2.0 [migration guide](https://sdk.mystenlabs.com/sui/migrations/sui-2.0). Launched [v2 of our Sui Invisible Wallet API](/api-docs/sui/wallet-services/invisible-wallet-api) to support Sui gRPC transaction execution! v1 and v2 of the API no longer support `shinami_walx_setBeneficiary`, `shinami_walx_unsetBeneficiary`, and `shinami_walx_getBeneficiary` (methods that supported long-endeded Mysten promotions). Sunsetted our Sui Node Service so that we can focus our efforts on products that are more needed in the ecosystem. As a result, we've updated our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) to version `0.11.0` to remove support for Sui Node Service. Sunsetted our Aptos Validator due to limited usage. Sunsetted our Aptos Node Service so that we can focus our efforts on products that are more needed in the ecosystem. We've updated our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) to remove support for Aptos Node Service (v0.10.0). With this renewed focus, we look forward to continuing to provide the best infra for Move devs in 2026 - Happy New Year! Launched our [Invisible Wallet Service](/api-docs/movement/wallet-services/invisible-wallet-api) on Movement! We've also updated our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) to support the launch (v0.9.8). Launched our [Gas Station](/api-docs/movement/gas-station/api) on Movement Mainnet! Launched our [Gas Station](/api-docs/movement/gas-station/api) on Movement Testnet! Mainnet to follow in a couple weeks. Now you can sponsor transactions to provide a great user experience! We've also updated our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) to support the launch (v0.9.7). We've been hard at work on our Movement products as well as behind the scenes improvements for all customers so you continue to get world-class infrastructure with a great dashboard! We're coming to Movement to continue our mission of providing the best developer experience for Move devs! We're in the design phase and will be launching products this fall. Beyond this, we've been working on behind-the-scenes improvements so you continue to get world-class infrastructure with a great dashboard! Launched a major overhaul to the [API keys page](https://app.shinami.com/access-keys) in our dashboard. Added sample code, an improved key creation and editing experience, and more! Launched an [Aptos validator](https://explorer.aptoslabs.com/validator/0xa65b244c7c9d86c652e502f811106d99827694adf8bea16d44b1522b81afe1d6?network=mainnet) on Mainnet to continue our efforts to support the Move ecosystem! Launched an Aptos [GraphQL explorer right in the dashboard](https://app.shinami.com/aptos/node) to help devs quickly write and test queries! Released Shinami Clients SDK [`version 0.9.6`](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/clients-v0.9.6) with improved support for using Shinami services in different geographic regions. Initially, this supports our new shared Sui Mainnet Node Service in Tokyo. Shinami is now multi-region! We launched a shared Sui Mainnet node in Tokyo, with more chain+service+region launches to follow ([FAQ](/help-center/workspace-faq#regions-faq)). New base URL for our services hosted in`US East`, `https://api.us1.shinami.com/`, replaces now-deprecated URL `https://api.shinami.com/`. We'll still support for deprecated URL for a while, but please move to the new URL when you make Integration changes (if you use our SDK you'll automatically migrate to the new base URL). Released Shinami Clients SDK [`version 0.9.5`](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/clients-v0.9.5) with support for our new Sui node URL launched last week. Changed our Sui Node Service URL to `https://api.shinami.com/sui/node/v1`. For now, `https://api.shinami.com/node/v1` is still supported. Please migrate the next time you make code changes. If you update to the version `0.9.5` or higher of our [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) you will migrate automatically (for all calls using the SDK). We've been using the last few weeks to work on lots of behind the scenes improvements so you continue to get world-class infrastructure with a great dashboard into 2025 and beyond! [Workspace contact email](https://app.shinami.com/workspace) is now [editable](/help-center/workspace-faq#what-is-the-workspace-contact-email-and-how-do-i-change-it)! Launched [Sign-in with Google](/help-center/workspace-faq#login-options-and-account-linking)! 1. Updated our Gas Station and Wallet Services access key creation and editing so that now you have an allotment you can distribute across your keys as you prefer. 2. Introduced JSON-RPC error [`-32012 Resource Limit Exceeded`](/developer-guides/core-integration-topics/error-reference#json-rpc-errors) to communicate situations where you should not immediately retry. For example, hitting the daily free request limit of our Sui Node Service Free plan. Launched [Aptos Indexer GraphQL Service](/api-docs/aptos/node-service/graphql/overview) Released [`version 0.9.4`](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/clients-v0.9.4) of the Shinami Clients SDK with a new client for the Shinami Aptos Full node REST API and forthcoming Indexer GraphQL API. Launched [Aptos Fullnode REST API Service](/api-docs/aptos/node-service/rest-api/overview). Released [`version 0.9.3`](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/clients-v0.9.3) which added `depositAddress` to the Aptos Gas Station `getFund` response. Added `depositAddress` to the Aptos Gas Station [`gasGetFund`](/api-docs/aptos/gas-station/api#gas-getfund) cURL response (SDK support coming shortly). Added support for Sign in with Apple to our Sui [Next.js zkLogin SDK](https://www.npmjs.com/package/@shinami/nextjs-zklogin). 1. Launched our [Aptos Invisible Wallet Service](/api-docs/aptos/wallet-services/invisible-wallet-api) 2. [Sui Invisible Wallet Service](/api-docs/sui/wallet-services/invisible-wallet-api) update: we've added support for new Key Service and Wallet Service URLs which add `/sui` to the path (`https://api.shinami.com/sui/key/v1`, and `https://api.shinami.com/sui/wallet/v1`). We will support the previous path without `/sui` for the time being, but plan to remove it at some point in the future when we bump our API version. *Breaking changes for Sui!* Released [clients-v0.9.0](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/clients-v0.9.0) and [nextjs-zklogin-v0.4.0](https://github.com/shinamicorp/shinami-typescript-sdk/releases/tag/nextjs-zklogin-v0.4.0) with support for Mysten Sui SDK 1.0. See our linked release notes for more information. Added an optional `gasPrice` parameter to Sui Gas Station's [gas\_sponsorTransactionBlock](/api-docs/sui/gas-station/api#gas-sponsortransactionblock). 1. Launched three new Aptos Gas Station methods: [`gas_sponsorAndSubmitSignedTransaction`](/api-docs/aptos/gas-station/api#gas-sponsorandsubmitsignedtransaction), [`gas_getFund`](/api-docs/aptos/gas-station/api#gas-getfund), and [`gas_encodeAndSponsorTransaction`](/api-docs/aptos/gas-station/api#gas-encodeandsponsortransaction). 2. Launched version 0.8.1 of our [clients SDK](https://www.npmjs.com/package/@shinami/clients) with support for the first two methods (the third is not intended for use with the SDK). 1. Launched our Aptos Gas Station Service with a [`gas_sponsorTransaction`](/api-docs/aptos/gas-station/api#gas-sponsortransaction) method to sponsor a transaction. 2. Updated [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients). Deprecated `"@shinami/clients"` import path in favor of `"@shinami/clients/sui"` and `"@shinami/clients/aptos"`. This will be a breaking change in a future release. Added a [gas\_getFund](/api-docs/sui/gas-station/api#gas-getfund) method to get the balance for a Gas Station fund. 1. Launched an auto-budgeting feature for Sui Gas Station sponsorships! `gasBudget` is now optional, and if omitted we'll estimate the gas fee of the transaction for you and add a buffer for safety. This applies to Sui Gas Station's [gas\_sponsorTransactionBlock](/api-docs/sui/gas-station/api#gas-sponsortransactionblock) and Sui Invisible Wallet's [shinami\_wal\_executeGaslessTransactionBlock](/api-docs/sui/wallet-services/invisible-wallet-api#shinami_wal_executegaslesstransactionblock) 2. Added Sui Gas Station [error code -2](/developer-guides/core-integration-topics/error-reference#gas-station-api) to distinguish between a failed sponsorship request being due to an issue on our end or yours. Shinami now supports Sui zkLogin wallets! We introduced two new methods to our [Sui zkLogin wallet API](/api-docs/sui/wallet-services/zklogin-wallet-api): * [shinami\_zkw\_getOrCreateZkLoginWallet](/api-docs/sui/wallet-services/zklogin-wallet-api#shinami_zkw_getorcreatezkloginwallet): Retrieves a zkLogin wallet along with its associated salt, creating a new wallet if necessary. This method provides the option to create and use multiple zkLogin wallets per (user, OAuth provider) pair by using the optional subWwallet parameter. * [shinami\_zkp\_createZkLoginProof](/api-docs/sui/wallet-services/zklogin-wallet-api#shinami_zkp_createzkloginproof): Generate a zkLogin proof prior to signing and executing a transaction. Added beneficiary graph methods to our [Sui Invisible Wallet API](/api-docs/sui/wallet-services/invisible-wallet-api) to support Mysten's Bullshark Quests. * shinami\_walx\_setBeneficiary * shinami\_walx\_unsetBeneficiary * shinami\_walx\_getBeneficiary [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new system read method `sui_getChainIdentifier`. [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new system read method `sui_getProtocolConfig`. [Sui JSON-RPC API and WebSocket API](/api-docs/sui/node-service/json-rpc/overview): Added new system read method `suix_getValidatorsApyNew` and new subscription method `suix_subscribeTransaction` [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new name service read methods: `suix_resolveNameServiceAddress` and `suix_resolveNameServiceNames` *Breaking changes for Sui JSON-RPC Devnet v0.29.0!* [Read more here](https://github.com/MystenLabs/sui/releases/tag/devnet-0.29.0) [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new object read method `sui_multiGetObjects`. [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new system read methods `sui_getReferenceGasPrice`, `sui_getCheckpointContentsByDigest`, and `sui_getCheckpointSummaryByDigest`. [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new delegation write methods `sui_requestWithdrawDelegation`, `sui_requestSwitchDelegation`, and `sui_requestAddDelegation`, as well as new system read methods `sui_getValidators`,`sui_getDelegatedStakes`,`sui_getCheckpointContents`,`sui_getCheckpointContentsBySequenceNumber`,`sui_getCheckpointSummary`, and `sui_getLatestCheckpointSequenceNumber`. [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new coin read methods: `sui_getAllBalance` and `sui_getAllCoins`, as well as new transaction read methods: `sui_devInspectTransaction` and `sui_devInspectMoveCall`. [Sui JSON-RPC API](/api-docs/sui/node-service/json-rpc/overview): Added new coin read methods: `sui_getBalance`, `sui_getCoins`, and `sui_getTotalSupply`, as well as new object read methods `sui_getDynamicFields` and `sui_getDynamicFieldObject`. # Common questions Source: https://docs.shinami.com/developer-guides/aptos/move-guides/common-questions Answers to common developer questions ## Overview This doc covers some key Aptos Move programming questions you might face when using a full node to read and write to the Aptos blockchain. Make sure to also: * See our [Aptos Move Developer Resources doc](/developer-guides/aptos/move-guides/key-resources) for a list of helpful developer resources. ## How to manage concurrent transactions ### Client design You can design your client to be able to perform a high number of concurrent transactions in two main ways. Both ideas are covered in this Aptos Labs doc on [Transaction Management](https://aptos.dev/en/build/guides/transaction-management): 1. Scale the number of transactions you can submit at once from the same wallet address. 1. You can push upwards of 100 transactions in a block from the same address (an account can have up to 100 uncommitted but submitted transactions). This requires having a sequence number generator to ensure that concurrently submitted transactions have differing and valid sequence numbers. 2. Scale the number of addresses that can perform a given transaction. 1. The above doc shows how to go beyond the "100 submitted but uncommitted transactions per account" limit by using worker accounts that share access to the `SignerCap` of a shared resource account. ### Smart contract design There are some cases where it's hard to have parallelization of transactions that make the same Move call. An example is an NFT minting function that assigns a number to each NFT (e.g. #17/100). The Aptos team designed Aggregators for this use case. Aggregators are wrappers around integers. They allow for differed reads and writes, allowing transactions that affect the same variable to execute in parallel. In a test, the Aptos team found that Aggregators let them mint a 1,000,000 NFT collection in 90 seconds - an improvement of \~10x! For more info on their design, abilities, and limitations, see: * [Blog post describing the problems aggregators solve](https://medium.com/aptoslabs/aggregators-how-sequential-workloads-are-executed-in-parallel-on-the-aptos-blockchain-e7992c70cefb) * [Mainnet module](https://explorer.aptoslabs.com/account/0x0000000000000000000000000000000000000000000000000000000000000001/modules/code/aggregator_v2?network=testnet) - look for `aggregator_v2` * [Documentation](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/doc/aggregator_v2.md) # Fungible tokens Source: https://docs.shinami.com/developer-guides/aptos/move-guides/fungible-tokens A summary of fungible tokens on Aptos with links to key resources ## Overview This doc provides an overview of the different types of fungible tokens on the Aptos blockchain. It's meant for both non-technical and technical audiences - giving an overview and then linking to more code examples and resources for developers who want to learn more. To learn about non-fungible tokens (NFTs) on Aptos, see [our guide](/developer-guides/aptos/move-guides/nfts). **Terms:** Often, the native currency of a chain is [referred to](https://www.coinbase.com/learn/crypto-basics/what-is-the-difference-between-a-coin-and-a-token) as a coin (e.g. APT for the Aptos blockchain), and currencies that live on that chain are referred to as tokens (e.g. the CELL token we reference below). But you'll also hear both referred to as a coin, token, or fungible asset at times. To pick a general term that applies to all cases, we're calling them fungible tokens here. ## Types of tokens on APTOS ### APT The native currency of the Aptos blockchain. Used to [pay the gas fees](https://aptos.dev/en/network/blockchain/gas-txn-fee) for transactions and often used to pay for other things like NFTs - including this interesting-looking creature listed for 3.68 APT: In addition to buying assets and paying for gas, you can [stake](https://aptos.dev/en/network/blockchain/staking) your APT with a validator, helping to secure the network in exchange for staking rewards. Aptos is a delegated proof-of-stake blockchain, meaning value in the form of staked APT secures the network (unlike Bitcoin, which is proof of work, meaning value in the form of computing resources secures the network). Like the tokens of other blockchains, the price of APT [can vary a lot over time](https://coinmarketcap.com/currencies/aptos/). This variance means that gas and NFT prices can fluctuate day-to-day in fiat terms. Not all tokens on Aptos have this variance, as you'll see in the next section. ### Stablecoins Stablecoins are tokens that are pegged to an underlying asset, often a fiat currency like the US dollar. They are an effort to put value on chain without the price fluctuations that tokens like APT experience. This provides safer ways to perform certain financial transactions, combining the speed and low fees of the Aptos blockchain with the relative stability of certain fiat currencies. For example, international remittance payments and a coffee shop taking payment for coffee both benefit from knowing that the value transmitted one day won't be worth, for example, 5% less the next day. Stablecoins can be bridged from another chain (like [USDC](https://explorer.aptoslabs.com/coin/0x5e156f1207d0ebfa19a9eeff00d62a282278fb8719f4fab3a586a0a2c0fffbea::coin::T?network=mainnet) bridged from the Ethereum blockchain) or native to Aptos (like [native USDC](https://explorer.aptoslabs.com/fungible_asset/0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b?network=mainnet)). Native stablecoins are generally safer because they don't have the additional trust assumptions that a bridge has (you have to trust the code and the team behind the bridge, in addition to the team and the code behind the stable coin). Native stablecoins on Aptos [include](https://defillama.com/stablecoins/Aptos) Circle's USDC and Tether's USDT. ### Other fungible tokens This guide covers the current [Fungible Asset Standard ](https://aptos.dev/en/build/smart-contracts/fungible-asset)and not the legacy [Aptos Coin Standard](https://aptos.dev/en/build/smart-contracts/aptos-coin). The new standard provides a consistent way to represent currencies, real-world-assets (e.g. stock shares), and other fungible assets. This standardization means that all apps - explorers, wallets, marketplaces, DeFi apps, etc - can read use these assets in the same way. It's fairly common for apps to have a token as a way to provide token holders with governance power to help shape the future of the app, as well as financial/reward incentives (for example Cellana's CELL token, [here on CoinGecko](https://www.coingecko.com/en/coins/cellana-finance), which allows users [some control](https://docs.cellana.finance/voting-system/basics) over the protocol). In addition to tokens created by apps, there is also the realm of memecoins - e.g. [Uptos](https://uptos.xyz/). This is not a plug for any token or financial advice, just examples of the types of fungible tokens you'll see. Apps like [Move Pump](https://movepump.com/create-token) have made it easy for non-developers to create tokens as well. You can see a list of the top coins on Aptos by market cap [here](https://www.coingecko.com/en/categories/aptos-ecosystem). ## Creating and using fungible tokens As described in the [Fungible Asset Standard guide ](https://aptos.dev/en/build/smart-contracts/fungible-asset), this involves three steps: 1. Create a non-deletable Object to own the new fungible assets metadata. The creator of the asset-type owns this object, and it holds information like the name, symbol, and number of decimal places of the asset. It can't be deleted because if accounts own the asset then losing this general information about the asset type would be catastrophic. 2. Generate `Ref`s to enable any desired permissions (e.g. `MintRef` for the capability to mint the asset, `TransferRef` for the capability to freeze and unfreeze the transfer rights of a given account - important for meeting certain regulations - and `BurnRef` for the capability to burn the asset). 3. Mint the assets and transfer them to accounts. Steps 1 and 2 are shown below. This is a partial example taken from a larger[ Aptos Labs example](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/move-examples/fungible_asset/fa_coin/sources/FACoin.move). ```bash Aptos Move expandable theme={null} module FACoin::fa_coin { use aptos_framework::fungible_asset::{Self, MintRef, TransferRef, BurnRef, Metadata, FungibleAsset}; use aptos_framework::object::{Self, Object}; use aptos_framework::primary_fungible_store; use aptos_framework::function_info; use aptos_framework::dispatchable_fungible_asset; use std::error; use std::signer; use std::string::{Self, utf8}; use std::option; const ASSET_SYMBOL: vector = b"FA"; #[resource_group_member(group = aptos_framework::object::ObjectGroup)] /// Hold refs to control the minting, transfer and burning of fungible assets. struct ManagedFungibleAsset has key { mint_ref: MintRef, transfer_ref: TransferRef, burn_ref: BurnRef, } /// Initialize metadata object and store the refs. // :!:>initialize fun init_module(admin: &signer) { let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL); primary_fungible_store::create_primary_store_enabled_fungible_asset( constructor_ref, option::none(), utf8(b"FA Coin"), /* name */ utf8(ASSET_SYMBOL), /* symbol */ 8, /* decimals */ utf8(b"http://example.com/favicon.ico"), /* icon */ utf8(b"http://example.com"), /* project */ ); // Create mint/burn/transfer refs to allow creator to manage the fungible asset. let mint_ref = fungible_asset::generate_mint_ref(constructor_ref); let burn_ref = fungible_asset::generate_burn_ref(constructor_ref); let transfer_ref = fungible_asset::generate_transfer_ref(constructor_ref); let metadata_object_signer = object::generate_signer(constructor_ref); move_to( &metadata_object_signer, ManagedFungibleAsset { mint_ref, transfer_ref, burn_ref } ); // <:!:initialize ... // more module code ``` ## Creating and using Stablecoins Aptos Labs has good sample code for creating a [stablecoin](https://learn.aptoslabs.com/en/code-examples/stablecoin) so check that out if you want to make one. If you want some Testnet USDC for testing a stablecoin in your transactions and Move code, see Circle's [Testnet faucet](https://faucet.circle.com/). ## Reading token data to use in your app **Example account** For the following examples, we're going to fetch data for this [Testnet account](https://explorer.aptoslabs.com/account/0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de/coins?network=testnet) that has three fungible token types * `0x1::aptos_coin::AptosCoin` * `0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake` * `0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832` ([Testnet USDC](https://explorer.aptoslabs.com/fungible_asset/0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832?network=testnet)) and one non-fungible asset (an NFT sword for a pretend game). ### REST API **Coin balances for an account** You can make a [\`GET / accounts/\{address}/balance/\{asset\_type}](https://fullnode.testnet.aptoslabs.com/v1/spec#/operations/get_account_balance) request for the balance of a specific asset type. A request for `0x1::aptos_coin::AptosCoin` is shown - using both cURL and the [Aptos TypeScript SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk) but you can make one for the other two fungible asset types of the account (shown above) as well. **Request** ```bash cURL /balance theme={null} curl --request GET \ --url https://fullnode.testnet.aptoslabs.com/v1/accounts/0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de/balance/0x1::aptos_coin::AptosCoin \ --header 'Accept: application/json, application/x-bcs' | json_pp ``` ```bash Aptos TS SDK getBalance theme={null} import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); const resp = await aptosClient.getBalance({ accountAddress: "0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de", asset: "0x1::aptos_coin::AptosCoin" }); console.log(resp); ``` **Response** ```bash cURL /balance theme={null} 400000400 # 4.00000400 APT ``` ```bash Aptos TS SDK getBalance theme={null} 400000400 // 4.00000400 APT ``` ### GraphQL Indexer API **Coin balances for an account along with coin and metadata** You can use the `getAccountCoinsData` from the [Aptos TypeScript SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk) or construct a `current_fungible_asset_balances` query directly as shown below. **Request** ```bash Aptos TS SDK getAccountCoinsData theme={null} import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); const resp = await aptosClient.getFungibleAssetMetadata({ minimumLedgerVersion: undefined, options: { where: { asset_type: { _eq: "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake" } } } }); console.log(resp); ``` ```bash cURL theme={null} curl -X POST https://api.testnet.aptoslabs.com/v1/graphql \ -H "Content-Type: application/json" \ -d '{ "query":"query MyQuery {current_fungible_asset_balances(where: {owner_address: {_eq: \"0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de\"}, token_standard: {}}) { amount_v1 amount_v2 asset_type_v1 asset_type_v2 last_transaction_timestamp last_transaction_timestamp_v1 last_transaction_timestamp_v2 last_transaction_version last_transaction_version_v1 last_transaction_version_v2 metadata { token_standard symbol supply_aggregator_table_key_v1 supply_aggregator_table_handle_v1 project_uri name last_transaction_version last_transaction_timestamp icon_uri decimals creator_address asset_type}}}" }' | json_pp ``` **Response** ```bash Aptos TS SDK getAccountCoinsData expandable theme={null} [ { amount: 400000400, asset_type: '0x1::aptos_coin::AptosCoin', is_frozen: false, is_primary: true, last_transaction_timestamp: '2024-07-08T20:59:37', last_transaction_version: 4401107226, owner_address: '0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de', storage_id: '0x0f34dbbf79be72a2eacc2da1b605327ec89d50531d8a2a5046d7e2cc362a7897', token_standard: 'v1', metadata: { token_standard: 'v1', symbol: 'APT', supply_aggregator_table_key_v1: '0x619dc29a0aac8fa146714058e8dd6d2d0f3bdf5f6331907bf91f3acd81e6935', supply_aggregator_table_handle_v1: '0x1b854694ae746cdbd8d44186ca4929b2b337df21d1c74633be19b2710552fdca', project_uri: null, name: 'Aptos Coin', last_transaction_version: 0, last_transaction_timestamp: '1970-01-01T00:00:00', icon_uri: null, decimals: 8, creator_address: '0x0000000000000000000000000000000000000000000000000000000000000001', asset_type: '0x1::aptos_coin::AptosCoin' } }, { amount: 10000000, asset_type: '0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832', is_frozen: false, is_primary: true, last_transaction_timestamp: '2025-03-05T18:14:01', last_transaction_version: 6644150380, owner_address: '0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de', storage_id: '0xb665e50deac9b3fe8a1a62e3a6a82f3dd6ac059d561bd64e181025aa42114082', token_standard: 'v2', metadata: { token_standard: 'v2', symbol: 'USDC', supply_aggregator_table_key_v1: null, supply_aggregator_table_handle_v1: null, project_uri: 'https://circle.com/usdc', name: 'USDC', last_transaction_version: 6643331561, last_transaction_timestamp: '2025-03-04T18:15:06', icon_uri: 'https://circle.com/usdc-icon', decimals: 6, creator_address: '0x72d1e6aa6a648a3afc5d45d8d66b353f1e1837a728c0813beec77f28a697fa7a', asset_type: '0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832' } }, { amount: 21674, asset_type: '0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake', is_frozen: false, is_primary: true, last_transaction_timestamp: '2025-03-04T21:31:22', last_transaction_version: 6643448594, owner_address: '0x006c55632dca03fe10c78f5e983cfd560ec55e587d930aa12ea50d75ea3262de', storage_id: '0x368a1f18aaad1324a8e5293a6f961ca75438e268f2e14c51aa3abc7dc8e40b3e', token_standard: 'v1', metadata: { token_standard: 'v1', symbol: 'Cake', supply_aggregator_table_key_v1: null, supply_aggregator_table_handle_v1: null, project_uri: null, name: 'PancakeSwap Token', last_transaction_version: 2689373781, last_transaction_timestamp: '2024-07-02T06:57:30', icon_uri: null, decimals: 8, creator_address: '0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf', asset_type: '0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake' } } ] ``` ```json cURL expandable theme={null} { "data" : { "current_fungible_asset_balances" : [ { "amount_v1" : 400000400, "amount_v2" : null, "asset_type_v1" : "0x1::aptos_coin::AptosCoin", "asset_type_v2" : null, "last_transaction_timestamp" : "2024-07-08T20:59:37", "last_transaction_timestamp_v1" : "2024-07-08T20:59:37", "last_transaction_timestamp_v2" : null, "last_transaction_version" : 4401107226, "last_transaction_version_v1" : 4401107226, "last_transaction_version_v2" : null, "metadata" : { "asset_type" : "0x1::aptos_coin::AptosCoin", "creator_address" : "0x0000000000000000000000000000000000000000000000000000000000000001", "decimals" : 8, "icon_uri" : null, "last_transaction_timestamp" : "1970-01-01T00:00:00", "last_transaction_version" : 0, "name" : "Aptos Coin", "project_uri" : null, "supply_aggregator_table_handle_v1" : "0x1b854694ae746cdbd8d44186ca4929b2b337df21d1c74633be19b2710552fdca", "supply_aggregator_table_key_v1" : "0x619dc29a0aac8fa146714058e8dd6d2d0f3bdf5f6331907bf91f3acd81e6935", "symbol" : "APT", "token_standard" : "v1" } }, { "amount_v1" : null, "amount_v2" : 10000000, "asset_type_v1" : null, "asset_type_v2" : "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832", "last_transaction_timestamp" : "2025-03-05T18:14:01", "last_transaction_timestamp_v1" : null, "last_transaction_timestamp_v2" : "2025-03-05T18:14:01", "last_transaction_version" : 6644150380, "last_transaction_version_v1" : null, "last_transaction_version_v2" : 6644150380, "metadata" : { "asset_type" : "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832", "creator_address" : "0x72d1e6aa6a648a3afc5d45d8d66b353f1e1837a728c0813beec77f28a697fa7a", "decimals" : 6, "icon_uri" : "https://circle.com/usdc-icon", "last_transaction_timestamp" : "2025-03-04T18:15:06", "last_transaction_version" : 6643331561, "name" : "USDC", "project_uri" : "https://circle.com/usdc", "supply_aggregator_table_handle_v1" : null, "supply_aggregator_table_key_v1" : null, "symbol" : "USDC", "token_standard" : "v2" } }, { "amount_v1" : 21674, "amount_v2" : null, "asset_type_v1" : "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake", "asset_type_v2" : null, "last_transaction_timestamp" : "2025-03-04T21:31:22", "last_transaction_timestamp_v1" : "2025-03-04T21:31:22", "last_transaction_timestamp_v2" : null, "last_transaction_version" : 6643448594, "last_transaction_version_v1" : 6643448594, "last_transaction_version_v2" : null, "metadata" : { "asset_type" : "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake", "creator_address" : "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf", "decimals" : 8, "icon_uri" : null, "last_transaction_timestamp" : "2024-07-02T06:57:30", "last_transaction_version" : 2689373781, "name" : "PancakeSwap Token", "project_uri" : null, "supply_aggregator_table_handle_v1" : null, "supply_aggregator_table_key_v1" : null, "symbol" : "Cake", "token_standard" : "v1" } } ] } } ``` **Metadata for a coin** You can use the `getFungibleAssetMetadata` from the [Aptos TypeScript SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk) or construct a `fungible_asset_metadata` query directly as shown below. **Request** ```bash Aptos TypeScript SDK theme={null} import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); const resp = await aptosClient.getFungibleAssetMetadata({ minimumLedgerVersion: undefined, options: { where: { asset_type: { _eq: "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake" } } } }); console.log(resp); ``` ```bash cURL theme={null} curl -X POST https://api.testnet.aptoslabs.com/v1/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query MyQuery { fungible_asset_metadata(where: {asset_type: {_eq: \"0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake\"}}) { icon_uri maximum_v2 project_uri supply_aggregator_table_handle_v1 supply_aggregator_table_key_v1 supply_v2 symbol token_standard name last_transaction_version last_transaction_timestamp decimals creator_address asset_type}}" }' | json_pp ``` **Response** ```bash Aptos TypeScript SDK expandable theme={null} [ { icon_uri: null, project_uri: null, supply_aggregator_table_handle_v1: null, supply_aggregator_table_key_v1: null, creator_address: '0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf', asset_type: '0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake', decimals: 8, last_transaction_timestamp: '2024-07-02T06:57:30', last_transaction_version: 2689373781, name: 'PancakeSwap Token', symbol: 'Cake', token_standard: 'v1', supply_v2: null, maximum_v2: null } ] ``` ```json cURL expandable theme={null} { "data" : { "fungible_asset_metadata" : [ { "asset_type" : "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf::pancake::Cake", "creator_address" : "0xe0e5ad285cbcdb873b2ee15bb6bcac73d9d763bcb58395e894255eeecf3992cf", "decimals" : 8, "icon_uri" : null, "last_transaction_timestamp" : "2024-07-02T06:57:30", "last_transaction_version" : 2689373781, "maximum_v2" : null, "name" : "PancakeSwap Token", "project_uri" : null, "supply_aggregator_table_handle_v1" : null, "supply_aggregator_table_key_v1" : null, "supply_v2" : null, "symbol" : "Cake", "token_standard" : "v1" } ] } } ``` ## Resources Here, we list many of the resources we listed above in a compact form: ### General * [Fungible tokens on Aptos by marketcap](https://coinmarketcap.com/view/aptos-ecosystem/) (including APT and stablecoins) ### APT * [Transaction fee calculation](https://aptos.dev/en/network/blockchain/gas-txn-fee) (gas and storage fees). * [Delegated staking](https://aptos.dev/en/network/blockchain/delegated-staking) ### Other fungible tokens * [Aptos Fungible Asset Standard](https://aptos.dev/en/build/smart-contracts/fungible-asset) * [`0x1::fungible_asset` Move function reference](https://aptos.dev/move-reference/mainnet/aptos-framework/fungible_asset) ### Stablecoins * How to build a stablecoin on Aptos [sample code](https://learn.aptoslabs.com/en/code-examples/stablecoin). * Stablecoins on Aptos [by market cap](https://defillama.com/stablecoins/Aptos). # Key Resources Source: https://docs.shinami.com/developer-guides/aptos/move-guides/key-resources Docs and dev tools for Aptos Move development ## New to Aptos Move development? Look here. ### Sample code to get you started * For tutorials on how to make your first transaction/NFT/Move Module/etc, using the Aptos SDK see [here](https://aptos.dev/tutorials/) . * For guidance and examples designed for those new to web3 or those coming from another chain, see [Aptos Learn](https://learn.aptoslabs.com/) . ### Aptos Move language guides * For a full guide to the language, see [Learn the Move Language](https://aptos.dev/move/move-on-aptos). * Tips for [writing secure Move code](https://aptos.dev/en/build/smart-contracts/move-security-guidelines#object-ownership-check). ### SDKs To make working with Aptos and Shinami easier, try these SDKs: * [Aptos SDKs](https://aptos.dev/build/sdks) for TypeScript, Python, Go, C#/.NET, Rust, and more. ### Aptos CLI * See the [Aptos CLI guide](https://aptos.dev/tools/aptos-cli/) for how to set up and use the CLI. ### Help with development questions For help and tips from fellow Aptos developers, see: * [Aptos developer discussions](https://github.com/aptos-labs/aptos-developer-discussions/discussions) on GitHub. ## Creating production code? Look here. ### Aptos source code and other apps * See the [aptos-core](https://github.com/aptos-labs/aptos-core) repo on GitHub. * Find [Ecosystem Projects](https://aptosnetwork.com/ecosystem/directory) on the Aptos Foundation site to see a big list of projects on Aptos with links to their GitHub accounts. ### Testing your code ### Unit tests * For guidance on writing and running unit tests for your Move modules, see the [Unit Tests section](https://aptos.dev/move/book/unit-testing) of The Move Book. ### Move Prover * For guidance on how to install and use the Move Prover, see [here](https://aptos.dev/build/smart-contracts/prover/prover-guide#running-the-move-prover) . ## Stay up to date on the Aptos Move ecosystem * [Aptos Labs on X](https://twitter.com/AptosLabs) for ecosystem announcements. * [Aptos Network on Discord](https://discord.com/invite/aptosnetwork) for ecosystem news and joining the Aptos community. * [Aptos Announcements Telegram channel](https://t.me/aptos_official) for the latest official news and announcements about Aptos. * [Aptos Labs blog](https://aptoslabs.com/blog) for longer-form technical content about Aptos. # NFTs (non-fungible tokens) Source: https://docs.shinami.com/developer-guides/aptos/move-guides/nfts A summary of NFTs on Aptos with links to key resources ## Overview NFTs are unique (non-fungible) tokens that represent art, in-game items, tickets, or other items that someone can own, buy, sell, or trade. Each NFT has a unique identifier, or object id, that distinguishes it from other objects on the Aptos blockchain. On Aptos, an NFT can be owned by an account or another object. An example of an object owning an NFT might be a NFT representing an in-game character owning a NFT representing a sword. NFTs on Aptos can be soul-bound, meaning that once transferred to their owner they cannot be transferred again. To learn about fungible tokens on Aptos - like the APT coin itself - see [our guide](/developer-guides/aptos/move-guides/fungible-tokens). Read below to see an overview of NFTs on Aptos and links to relevant tutorials, standards, etc. We cover the current standard for NFTs on Aptos, the [Digital Asset (DA) Standard](https://aptos.dev/en/build/smart-contracts/digital-asset), which has been live since 2023. We do not cover the previous standard, the Aptos Token Standard, which you can read about [here](https://aptos.dev/en/build/smart-contracts/aptos-token) if needed. ## Collections, mints, and updates ### A no(-Move)-code option Aptos provides a solution that handles core NFT collection and token requirements at `0x4::AptosTokenObjects` on [Mainnet](https://explorer.aptoslabs.com/account/0x0000000000000000000000000000000000000000000000000000000000000004/modules/code/aptos_token?network=mainnet) and [Testnet](https://explorer.aptoslabs.com/account/0x0000000000000000000000000000000000000000000000000000000000000004/modules/code/aptos_token?network=testnet) so that you can create collection and mint tokens using pre-published Move code. There are even helper functions in the TypeScript SDK that build the transactions you need (which we use below)! If you're creating the next viral PFP collection, this may be all that you need. If you're making something more complicated - like the next great adventure game - you may want to write your own Move code that uses some of the `0x4::AptosTokenObjects` functions but adds more logic. We show some examples of this below, but if you want to see more complex examples, make sure to check out the resources at the bottom of the page. ### Collections Every NFT token belong to a collection. So, first you must create a collection with key details like a name, description, maximum supply, and more. Collections and tokens (NFTs) are separate objects with their own distinct addresses. You can create a `Collection` with a fixed supply or an unlimited one. Below is code for creating a collection with a fixed supply and a 1% royalty. For a game, it wouldn’t make much sense to limit the core NFTs players need to play the game - like an initial hero character or a basic sword - as that would limit the number of players you can have. However, you might place limits on certain rare items like an all-powerful sword in order to maintain their specialness - whether by setting a maximum number, a low likelihood of minting, or other limitations. **TypeScript example** You can create one using the [Aptos SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk): ```bash TypeScript theme={null} const createCollectionTransaction = await nodeClient.createCollectionTransaction({ creator: collectionCreator, description: "Items for the best game in the world", name: "Epic Figtherz Battlez Collection", uri: "https://link-to-my-game-website", maxSupply: 1000, mutableDescription: false, mutableRoyalty: false, mutableURI: true, mutableTokenDescription: true, mutableTokenName: true, mutableTokenProperties: true, mutableTokenURI: true, tokensBurnableByCreator: false, tokensFreezableByCreator: false, royaltyNumerator: 1, royaltyDenominator: 100 }); await nodeClient.signAndSubmitTransaction({ signer: collectionCreator, transaction: createCollectionTransaction }); ``` Here's my [create collection transaction on chain](https://explorer.aptoslabs.com/txn/0xc704348025fc9a3e5db377c1f7a70adcce2608f20d33b0410b6cd996420f8c1a?network=testnet) and [here are the collection's resources](https://explorer.aptoslabs.com/object/0xd4c0c251a38a7af47dc3d1c0868cd0b494efcf72792f945ffe083b5ba04a55c1/resources?network=testnet). One of the things you can see in the explorer, as shown below, is the fact that I get 1% in royalties when one of my NFTs is sold: **Move example** You can also write your own Move code that calls functions from the `0x4::AptosTokenObjects` package, as shown in the [DA Standard doc](https://aptos.dev/en/build/smart-contracts/digital-asset). Here, we have a supply cap of 1,000 but no royalty: ```bash Move theme={null} use aptos_token_objects::collection; use std::option::{Self, Option}; public entry fun create_collection(creator: &signer) { let max_supply = 1000; let royalty = option::none(); // Maximum supply cannot be changed after collection creation collection::create_fixed_collection( creator, "Items for the best game in the world", max_supply, "Epic Figtherz Battlez Collection", royalty, "https://link-to-my-game-website", ); } ``` Then, you'd publish that package and construct a transaction to call the function. ### Minting A mint is a Move function call to the NFT smart contract. In the contract, you define the rules for which addresses can mint and what they can mint. The `0x4::AptosTokenObjects` package mints into the creator's wallet address, and from there the creator can transfer them to another wallet or list them on a marketplace if desired. **TypeScript example** ```bash TypeScript theme={null} const mintTransaction = await nodeClient.mintDigitalAssetTransaction({ creator: collectionCreator, collection: "Epic Figtherz Battlez Collection", description: "A basic sword to help in battle.", name: "basic sword", uri: "https://increasing-indigo-platypus.myfilebase.com/ipfs/QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV", propertyKeys: ["power", "durability"], propertyTypes: ["U16", "U8"], propertyValues: [80, 100] }); const mintTxResp = await nodeClient.signAndSubmitTransaction({ signer: collectionCreator, transaction: mintTransaction }); ``` Here's my [mint transaction](https://explorer.aptoslabs.com/txn/0x966b6b3869685e817fc86cd4d29718f12391f498c2817912634af04b7db1326e?network=testnet) which added a "basic sword" to the tokens I own: And if you [look at the token](https://explorer.aptoslabs.com/token/0xad0709ae89c0bc8e9fbf6fcfbaad9a3fd105384e0870f57e05f3ab0b4371b20a/0/overview?network=testnet), you can see the two properties I added (`power` and `durability`). You can also see that 1 out of a maximum supply of 1,000 has been minted. **Move example** Again, here is the same operation in Move: ```bash Move theme={null} use aptos_token_objects::token; use std::option::{Self, Option}; public entry fun mint_token(creator: &signer) { let royalty = option::none(); token::create_named_token( creator, "Epic Figtherz Battlez Collection", "A basic sword to help in battle.", "basic sword", royalty, "https://increasing-indigo-platypus.myfilebase.com/ipfs/QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV", ); } ``` ## Modify an NFT A static PFP is nice. But when NFTs can change and add attributes over time, that makes things a lot more interesting. For instance, a sword can lose durability after a fight. ## Modify an attribute **Typescript example** Here, we're lowering a sword's durability after being in a battle. ```bash TypeScript theme={null} // 1. Check the sword's current durability const SWORD_ADDRESS = "0xad0709ae89c0bc8e9fbf6fcfbaad9a3fd105384e0870f57e05f3ab0b4371b20a"; const swordData = await nodeClient.getDigitalAssetData({ digitalAssetAddress: SWORD_ADDRESS }); const FIGHT_DURABILITY_LOSS = 5; let currentDurability = swordData.token_properties[DURABILITY_NAME]; let newDurability = currentDurability - FIGHT_DURABILITY_LOSS; console.log("Lowering durability from ", currentDurability, "to ", FIGHT_DURABILITY_LOSS); // 2. Create and submit a transaction to modify it const modifyTx = await nodeClient.updateDigitalAssetPropertyTransaction({ creator: collectionCreator, propertyKey: DURABILITY_NAME, propertyType: DURABILITY_TYPE, propertyValue: newDurability, digitalAssetAddress: SWORD_ADDRESS }); const modifyTxResp = await nodeClient.signAndSubmitTransaction({ signer: collectionCreator, transaction: modifyTx }); console.log(modifyTxResp); ``` Here is my [transaction](https://explorer.aptoslabs.com/txn/0x6c5f2ba85c4c39c7d80efd3a47528348d4a8a376b9a3f598f1f7f451b5968cee?network=testnet), and here is the updated sword: ## Add an attribute Now, let's say I defeat a boss and earn an enchantment on the sword! This enchantment applies a small dose of poison to enemies I attack, dealing them some extra damage. **Typescript example** ```bash TypeScript theme={null} const ENCHANTMENT = "enchantment"; const SMALL_POISON = 'small poison'; const newAbilityTx = await nodeClient.addDigitalAssetTypedPropertyTransaction({ creator: collectionCreator, propertyKey: ENCHANTMENT, propertyType: "STRING", propertyValue: SMALL_POISON, digitalAssetAddress: SWORD_ADDRESS }); const newAbilityTxResp = await nodeClient.signAndSubmitTransaction({ signer: collectionCreator, transaction: newAbilityTx }); console.log(newAbilityTxResp); ``` Here is my [transaction](https://explorer.aptoslabs.com/txn/0x0458ca608523dea133cfebf611b41bf10a33f973f14f675ebde4e4c9ff6312cc?network=testnet), and here is my sword now (notice I lost some more durability in my boss fight): You can remove abilities as well. ## Where the data lives ### Off-chain data storage While NFTs exist on chain, sometimes part of their data does not. The typical off-chain data would be any associated large media files (image, video, audio) - e.g. the full size and thumbnail image of a hero character. Examples of off-chain storage include: * A decentralized storage network such as * An [IPFS](https://ipfs.tech/) pinning service, such as [Filebase](https://filebase.com/) or [Pinata](https://pinata.cloud/). * Centralized storage like an AWS S3 bucket + CloudFront CDN. ### Caching The ultimate truth about an NFT’s ownership and attributes lives in the latest state of the Aptos blockchain. However, frequently read data can often be cached (especially in cases where your app controls minting and updating NFTs, and so knows when NFT data changes). ## Ownership, transfer, and sale of NFTs ### Ownership Games use Shinami Invisible (NFT) Wallets to hold its users NFTs. In both cases, the user doesn't need to remember a seed phrase to manage their wallet - they just need to know how to log into your app. Your app determines when and how to tell the user their in-game object is an NFT on the Aptos blockchain. For a richer comparison overview of how our Invisible Wallets work, see our [high-level guide](/product-overviews/aptos/wallets). ### Sale and transfer If using Shinami Invisible Wallets, your app can choose when to present the user with an option to sell or trade the NFT to another player. Your app can also provide a means for users to take control of their keys by, for example, creating a cross-app, [Aptos Connect wallet](https://aptosconnect.app/), and transferring the NFT to it. If you create a royalty, then you can make money if a player sells an item on a NFT marketplace. ## Resources ### Official standards and resources * The [Aptos Digital Assest (DA) Standard](https://aptos.dev/en/build/smart-contracts/digital-asset) is the current standard for creating NFTs on Aptos. It's made possible by the [Aptos Object Model](https://aptos.dev/en/build/smart-contracts/objects). * This standard replaced the legacy [Aptos Token Standard](https://aptos.dev/en/build/smart-contracts/aptos-token). ### Developer Tutorials and sample code * Aptos Labs guide to [minting your first NFT](https://aptos.dev/en/build/guides/your-first-nft). * Aptos Learn site tutorial to [create an NFT marketplace](https://learn.aptoslabs.com/en/code-examples/nft-marketplace). * Aptos Labs [token object examples sample code](https://github.com/aptos-labs/aptos-core/tree/main/aptos-move/move-examples/token_objects) has a rich set of examples for building nested NFTs with complex behavior (as well as soul-bound NFTs in the `ambassador` folder). ### Aptos NFT marketplaces * You can find a list of marketplaces [here](https://aptosfoundation.org/ecosystem/projects/marketplaces). # Gas Station: backend-only Source: https://docs.shinami.com/developer-guides/aptos/tutorials/gas-station-backend-only Full sample code for building, sponsoring, signing, and submitting a sponsored transaction. ## Overview In this tutorial you'll learn how to sponsor transactions with Shinami's Gas Station. For more on the benefits of sponsoring transactions and to see an image of an end-to-end sponsorship flow, see our [Gas Station high-level guide](/product-overviews/aptos/gas-station) The full code for this TypeScript tutorial is available on [GitHub](https://github.com/shinamicorp/shinami-examples/tree/main/aptos/typescript/backend_examples). **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. ### Requests must be from your BE Our Gas Station does not support CORS, so if you attempt to make requests to it from the FE you'll get a CORS error. We do this because exposing a Gas Station API key on the FE is a security risk - an attacker could drain the APT in your Gas Station fund associated with the key by using it to sponsor transactions. For an overview of ways to integrate frontend signing with backend sponsorship, see our [Frontend signing + backend sponsorship tutorial](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing). If you're using our Invisible Wallets, you can simply build a transaction on your backend and then sign, sponsor, and submit it with one [`wal_executeGaslessTransaction request`](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-executegaslesstransaction) . ### 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 Gas Station](/developer-guides/core-integration-topics/error-reference#aptos-service-specific-error-codes) as well as the "General Error Codes" section at the top that apply to all Shinami services. ## Usage and Billing FAQ Make sure to check out the Aptos Gas Station [product usage FAQ](/help-center/aptos/gas-station-faq) and [billing FAQ](/help-center/billing/aptos-faq#gas-station) in our Help Center for answers to common questions. ## Required setup **Note:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet You'll need a Testnet Gas Station fund in order to sponsor transactions. We have guidance on creating one in the [Aptos Gas Station page of our Help Center](/help-center/aptos/gas-station-faq). When you make a Testnet fund we deposit some APT in it so you can start testing immediately. ### 2. Clone the github repo and install dependencies Clone the [shinami-examples github repo](https://github.com/shinamicorp/shinami-examples/tree/main), cd into the `shinami-examples/aptos/typescript/backend_examples` directory, and run `npm install`to install the dependencies for running the code. If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) . Below, we'll be using the `gas_station.ts` file in the `shinami-examples/aptos/typescript/backend_examples/src` directory. ### 3. Create an API access key and copy it into the file. **Create an access key with Testnet Gas Station rights** You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key that has `Gas Station` rights for Testnet. See our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#create-an-access-key) for info on how to set up a key with rights to multiple services. Make sure to link it to the fund you created in step 1. **Add it to the gas\_station.ts file** Once you have your key, use it as the value for the `TESTNET_APTOS_GAS_STATION_API_KEY` constant. You'll see that we immediately use it to instantiate a Gas Station client (for sponsoring transactions). ```bash TypeScript theme={null} // Create a Shinami Gas Station client for sponsoring our transactions const TESTNET_APTOS_GAS_STATION_API_KEY = "TESTNET_APTOS_GAS_STATION_API_KEY"; const gasStationClient = new GasStationClient(TESTNET_APTOS_GAS_STATION_API_KEY); ``` ### 4: Open your Shinami dashboard Technically not required, but we recommend visiting the ["Completed transactions" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) of your Shinami dashboard. After running the code examples, take a look at this page. It may take a moment, but you'll see the digests of transactions you sponsor that are committed to the blockchain appear as in the image below. This tab, and the "In flight transactions" tab (for sponsored transactions that haven't yet been committed to the Aptos blockchain) can be helpful when testing. ## Code examples ### Overview Below, we'll review each of our sample code functions and how to run them. At a high-level, you'll uncomment just one sample code function - e.g. `sponsorTransactionSimple()` in the code block below. Then, save the change, run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile, and run `node build/gas_station.ts` to run. This will work as downloaded for the simple transaction examples, but the multi-agent ones require compiling a Move script (explained in the Appendix). ```bash TypeScript expandable theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await sponsorTransactionSimple(); //sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); //sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); //checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); // Wait for the transaction to move past the pending state if (committedTransaction) { console.log("Polling for tx hash: ", committedTransaction.hash); const executedTransaction = await aptosClient.waitForTransaction({ transactionHash: committedTransaction.hash }); console.log("Transaction status:", executedTransaction.vm_status); } else { console.log("There was an issue with building and submitting the transaction."); } ``` Each of the sample code functions returns a `Promise`. If our function was successful, we wait for the transaction to be committed to the blockchain and then print out the hash and status. Example: ``` Transaction hash: 0x8952262bec100c8426f089fa3230eb582bbfd8e42e3cfef98dc8bea449927494 Transaction status: Executed successfully ``` You can look up the digest in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) or [Aptos Scan](https://aptoscan.com/?network=testnet) (make sure you've set the explorer to Testnet first). This is an image showing that [my transaction](https://explorer.aptoslabs.com/txn/0x8952262bec100c8426f089fa3230eb582bbfd8e42e3cfef98dc8bea449927494?network=testnet) had both a sender and a fee payer signature (your sender and feePayer addresses will be different): ### Sponsor a simple transaction **Understand the code** The `sponsorTransactionSimple` function performs all the steps needed to build, sponsor, sign, and submit a simple transaction: Create an Account to use as the sender. Build the transaction (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-simpletransaction-for-sponsorship)). Sponsor the transaction with a call to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the `console.log()` statement below. Sign the transaction as the sender. Submit the transaction with the sender and feePayer signatures and returns the result. ```bash TypeScript expandable theme={null} async function sponsorTransactionSimple(): Promise { // 1. Set up our sender. const sender = await generateSingleKeyAccountEd25519(); // 2. Build a simple transaction. let transaction = await buildSimpleMoveCallTransaction(sender.accountAddress); // 3. Sponsor the transaction with Shinami Gas Station. let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // Note that the SDK updates the transaction's feePayer address on a successful sponsorship console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress); // 4. Generate the sender's signature. const senderAuthenticator = aptosClient.transaction.sign({ signer: sender, transaction: transaction }); // 5. Submit the transaction with the sender and fee payer signatures return await aptosClient.transaction.submit.simple({ transaction, senderAuthenticator, feePayerAuthenticator: feePayerAuthenticator, }); } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await sponsorTransactionSimple(); // sponsorTransactionMultiAgent(); // sponsorAndSubmitSignedTransactionSimple(); // sponsorAndSubmitSignedTransactionMultiAgent(); // checkFundBalanceAndDepositIfNeeded(); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) (make sure you've set the explorer to Testnet) ### Sponsor a multi-agent transaction **Understand the code** The `sponsorTransactionMultiAgent` function performs all the steps needed to build, sponsor, sign, and submit a multi-agent Move script transaction. Before you run it, you'll need to generate two, funded Accounts that will swap Octa (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Builds the transaction (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script)). Sponsors the transaction with a request to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the `console.log()` statement below. Generates the sender and secondary signer signatures. Submits the transaction with the sender, secondary signer, and feePayer signatures and returns the result. ```bash TypeScript expandable theme={null} async function sponsorTransactionMultiAgent(fundedSenderAccount: SingleKeyAccount, fundedSecondarySigner: SingleKeyAccount): Promise { // 1. Build a multiAgent transaction let transaction = await buildMultiAgentScriptTransaction( fundedSenderAccount.accountAddress, fundedSecondarySigner.accountAddress); // 2. Sponsor the transaction with Shinami Gas Station let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // Note that the SDK updates the transaction's feePayer address on a successful sponsorship console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress); // 3. Generate the sender and secondary signer signatures const senderAuthenticator = aptosClient.transaction.sign({ signer: fundedSenderAccount, transaction }); const secondarySignerAuthenticator = aptosClient.transaction.sign({ signer: fundedSecondarySigner, transaction }); // 4. Submit the transaction with the sender, seconardy signer, and feePayer signatures return await aptosClient.transaction.submit.multiAgent({ transaction, senderAuthenticator, additionalSignersAuthenticators: [secondarySignerAuthenticator], feePayerAuthenticator: feePayerAuthenticator }); } ``` **Update, save, compile, run** In order to run this example, you'll need to compile the Move script used as shown in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script). Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); //sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); //checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) (make sure you've set the explorer to Testnet) ## Sponsor and submit a signed, simple transaction **Understand the code** The `sponsorAndSubmitSignedTransactionSimple` function performs all the steps needed to build and sign a simple transaction, and then send it to Gas Station for sponsorship and submission to the Aptos blockchain: Create an Account to use as the sender. Build the transaction (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-simpletransaction-for-sponsorship)). Generate the sender's signature. Make a request to Gas Station to sponsor and submit the signed transaction, returning the result. ```bash TypeScript expandable theme={null} async function sponsorAndSubmitSignedTransactionSimple(): Promise { // 1. Set up our sender. const sender = await generateSingleKeyAccountEd25519(); // 2. Build a simple transaction. const transaction = await buildSimpleMoveCallTransaction(sender.accountAddress); // 3. Generate the sender's signature. const senderAuthenticator = aptosClient.transaction.sign({ signer: sender, transaction }); // 4. Ask Shinami to sponsor and submit the transaction return await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator ); } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); //sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); //checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) (make sure you've set the explorer to Testnet) ## Sponsor and submit a multi-agent transaction **Understand the code** The `sponsorAndSubmitSignedTransactionMultiAgent` function performs all the steps needed to build and sign a multi-agent Move script transaction, and then send it to Gas Station for sponsorship and submission to the Aptos blockchain. Before you run it, you'll need to generate two, funded Accounts that will swap Octa if you haven't already done so (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Builds the transaction (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script)). Generates the sender and secondary signer signatures. Makes a request to Gas Station to sponsor and submit the signed transaction, returning the result. ```bash TypeScript expandable theme={null} async function sponsorAndSubmitSignedTransactionMultiAgent( fundedSenderAccount:SingleKeyAccount, fundedSecondarySigner: SingleKeyAccount): Promise { // 1. Build a multiAgent transaction let transaction = await buildMultiAgentScriptTransaction( fundedSenderAccount.accountAddress, fundedSecondarySigner.accountAddress); // 2. Generate the sender and secondary signer signatures const senderAuthenticator = aptosClient.transaction.sign({ signer: fundedSenderAccount, transaction }); const secondarySignerAuthenticator = aptosClient.transaction.sign({ signer: fundedSecondarySigner, transaction }); // 3. Ask Shinami to sponsor and submit the transaction return await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator, [secondarySignerAuthenticator] ); } ``` **Update, save, compile, run** In order to run this example, you'll need to compile the Move script used as shown in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script). Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await // sponsorTransactionSimple(); // sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // sponsorAndSubmitSignedTransactionSimple(); sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) (make sure you've set the explorer to Testnet) ## Check your fund balance and make a deposit **Understand the code** The `checkFundBalanceAndDepositIfNeeded` function does... well, exactly what it says it does 😄. Before you run it, you'll need to generate a funded Account if you haven't already done so (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Makes a call to [`gas_getFund`](/api-docs/aptos/gas-station/api#gas-getfund) to obtain your fund's balance. Checks if the amount that's available to use is less than the minimum amount you want. 1. If true, it: 1. creates the transaction to transfer APT to your fund address 2. obtains the sender's signature (from the funded Account you made) 3. submits the transaction and returns the result. 2. Else, it returns `undefined`. ```bash TypeScript expandable theme={null} async function checkFundBalanceAndDepositIfNeeded(fundedSenderAccount: SingleKeyAccount): Promise { const MIN_FUND_BALANCE_OCTA = 1_000_000_000; // 10 APT const { balance, inFlight, depositAddress } = await gasStationClient.getFund(); // You'll want to deposit more than 1000 Octa, and what you want may be dynamic based on the // current (balance - inFlight) amount, etc. This is just a simple example. const STANDARD_DEPOSIT_AMOUNT = 1000; // Deposit address can be null - see our Help Center for how to generate an address: // https://docs.shinami.com/help-center/aptos/gas-station-faq#how-do-i-generate-and-find-the-deposit-address-of-a-fund if (depositAddress && ((balance - inFlight) < MIN_FUND_BALANCE_OCTA)) { // Create a SimpleTransaction that transfers APT from the sender to your Gas Station fund const transferTx = await aptosClient.transferCoinTransaction({ sender: fundedSenderAccount.accountAddress, recipient: depositAddress, amount: STANDARD_DEPOSIT_AMOUNT }); // Obtain the sender's signature const senderAuth = fundedSenderAccount.signTransactionWithAuthenticator(transferTx); // Submit the transaction return await aptosClient.transaction.submit.simple({ transaction: transferTx, senderAuthenticator: senderAuth }); } console.log("No deposit because no deposit address or a balance above the minimum you've set."); return undefined; } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); //sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); //sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/aptos/typescript/backend_examples` directory to compile. Then, run `node build/gas_station.js`. If successful, you can [view the deposit history of your fund](/help-center/aptos/gas-station-faq#faq) on the ["Funds" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_funds) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like [Aptos Explorer](https://explorer.aptoslabs.com/?network=testnet) (make sure you've set the explorer to Testnet) ## Appendix ### Build a SimpleTransaction for sponsorship The below function creates a transaction that calls a function on a Move module we've [deployed to Testnet](https://explorer.aptoslabs.com/account/0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817/modules/code/message?network=testnet) (it's from an [Aptos tutorial](https://aptos.dev/tutorials/first-move-module/)). 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it. ```bash TypeScript theme={null} async function buildSimpleMoveCallTransaction(sender: AccountAddress, expirationSeconds?: number): Promise { let transaction = await aptosClient.transaction.build.simple({ sender: sender, withFeePayer: true, data: { function: "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message", functionArguments: [new MoveString("Test message")] }, options: { expireTimestamp: expirationSeconds, maxGasAmount: 20000 } }); console.log("\nResponse from aptos.transaction.build.simple()"); console.log(transaction); return transaction; } ``` ### Build a MultiAgentTransaction with a compiled Move script **Step 1: Building a MultiAgentTransaction for sponsorship.** For a multi-agent transaction, you'll need to provide the secondary signers when building the transaction (in addition to the sender). 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it. This function reads a complied Move script inside your local directory and builds a MultiAgentTransaction with it. You'll need to compile the script to use this function (see "Step 2" below). ```bash TypeScript expandable theme={null} async function buildMultiAgentScriptTransaction(sender: AccountAddress, secondarySigner: AccountAddress, expirationSeconds?: number): Promise { let buffer = readFileSync("./move/build/test/bytecode_scripts/unfair_swap_coins.mv"); let bytecode = Uint8Array.from(buffer); let transaction = await aptosClient.transaction.build.multiAgent({ sender: sender, secondarySignerAddresses: [secondarySigner], withFeePayer: true, data: { bytecode: bytecode, functionArguments: [] }, options: { expireTimestamp: expirationSeconds, maxGasAmount: 20000 } }); console.log("\nResponse from aptos.transaction.build.multiAgent()"); console.log(transaction); return transaction; } ``` **Step 2: Aptos Move** **Move script** The script we'll use is located at `shinami-examples/aptos/typescript/backend_examples/move/sources/unfair_swap_coins.move` ```bash TypeScript theme={null} script { use aptos_framework::aptos_coin; use aptos_framework::coin; use aptos_framework::signer; fun unfair_swap_coins( sender: &signer, secondary: &signer ) { let coin_first = coin::withdraw(sender, 100); let coin_second = coin::withdraw(secondary, 200); coin::deposit(signer::address_of(secondary), coin_first); coin::deposit(signer::address_of(sender), coin_second); } } ``` **Build the Move script** For this, you'll need to install the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/). Once you've done so, run `cd move` to get to the root of the `shinami-examples/aptos/typescript/backend_examples/move` directory. Then, run `aptos move compile`. You should see a `build/test/bytecode_scripts/unfair_swap_coins.mv`. That's the compiled, bytecode version of the below Move script. This Move script has the sender give the secondary signer 100 Octa in exchange for 200 Octa - not fair! For more guidance on how to run a multi-agent transaction see [here](https://github.com/aptos-labs/developer-docs/blob/fde289de3d02368d3fe878a3e5cd9c725a22a376/apps/nextra/pages/en/build/sdks/ts-sdk/building-transactions/multi-agent-transactions.mdx#L196), and for guidance on running Move scripts see [here](https://aptos.dev/build/smart-contracts/scriptsrunning-scripts/). ### Generating funded accounts for testing For the purposes of reusing the same account for repeated tests with this tutorial - which is required when you use one of the functions that takes a funded account as an argument - you can find and reuse a SingleKeyAccount's private key as shown in the below code (near the [top of the file](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/backend_examples/src/gas_station.ts#L22)). This is just provided for easy testing. Your app should determine the best way to manage any keys it controls, and you should always protect your private keys and not share them. There are three steps, as outlined in the code: Uncomment out step 1 (you may want to also comment out other code below that would run, but it's not necessary). Then, Save changes the file, transpile with tsc, and run with node build/gas\_station.js. Look at your console for the two addresses printed out. Go to [https://aptos.dev/en/network/faucet](https://aptos.dev/en/network/faucet) and copying in each address to give it Testnet APT. You may need to refresh the page after funding the first address. Note that the faucet currently has a limit of 5 requests per day. Comment out step 1. Look at your console for the two private keys printed out. Uncomment step 3 and set one as the value for `PKEY_ONE` and one as the value for `PKEY_TWO`. Save changes to the file. Now, the next time you transpile and run the code, you'll be able to use these two funded accounts ```bash TypeScript expandable theme={null} // **** // Code for generating two reusable, funded accounts for testing purposes // Step 1: uncomment the next six lines. Save the file, transpile with tsc, and run with node build/gas_station.js const accountOne = await generateSingleKeyAccountEd25519(); console.log("Address 1: ", accountOne.accountAddress.toString()); console.log("Private key 1: ", PrivateKey.formatPrivateKey(Buffer.from(accountOne.privateKey.toUint8Array()).toString('hex'), PrivateKeyVariants.Ed25519)); const accountTwo = await generateSingleKeyAccountEd25519(); console.log("Address 2: ", accountTwo.accountAddress.toString()); console.log("Private key 2: ", PrivateKey.formatPrivateKey(Buffer.from(accountTwo.privateKey.toUint8Array()).toString('hex'), PrivateKeyVariants.Ed25519)); // End step 1 // Step 2: visit the Aptos Testnet faucet page at https://aptos.dev/en/network/faucet // and request APT for each of the two addresses that were printed to the console from // step 1. You may need to refresh the page after your first request. // CAUTION: the faucet currently has a limit of 5 requests per day. // Step 3: // a. Comment out the four lines from run 1. // b. Uncomment the eight code lines below. // c. Set the values of `PKEY_ONE` and `PKEY_TWO` to the private key values printed to the console in Step 1. // d. Save the file, transpile with tsc, and run with node build/gas_station.js // const PKEY_ONE = "ed25519-priv-0x..."; // const fundedSenderAccount = new SingleKeyAccount({ // privateKey: new Ed25519PrivateKey(PKEY_ONE) // }); // const PKEY_TWO = "ed25519-priv-0x..."; // const fundedSecondarySignerAccount = new SingleKeyAccount({ // privateKey: new Ed25519PrivateKey(PKEY_TWO) // }); // End step 3 // **** ``` ### Sponsoring a transaction for a non-funded account As our examples above show, you can sponsor a transaction for an Account that has not yet been funded. The transaction fee for an account's first transaction will cost more than the fee for the same transaction at the same time for a funded account. Example fee statements are below, but your results may vary. Example `0x1::transaction_fee::FeeStatement` for a `message::setmessage` Testnet Move call [transaction](https://explorer.aptoslabs.com/txn/0xeed9b72b7eb1db25ee1962d52a513ed770e729f260b347bd04682652b3066dd0/events?network=testnet) from an unfunded account: ``` { execution_gas_units:"4" io_gas_units:"1" storage_fee_octas:"92840" storage_fee_refund_octas:"0" total_charge_gas_units:"933" } ``` Example `0x1::transaction_fee::FeeStatement` for a `message::setmessage` Testnet Move call [transaction](https://explorer.aptoslabs.com/txn/0xd658444c999176248fd8781539970670865bbddcad3aa5a5c6379e477aaebba8/events?network=testnet) from a funded account: ``` { execution_gas_units:"3" io_gas_units:"1" storage_fee_octas:"43680" storage_fee_refund_octas:"0" total_charge_gas_units:"441" } ``` ### Tips for setting your sponsorship budget See the Aptos doc on [Gas and Storage Fees](https://aptos.dev/concepts/gas-txn-fee/) for a more detailed overview of how transaction costs are determined, as well as guidance on how to estimate the costs for a transaction. See the [Aptos Gas Station tab on the Billing page](https://app.shinami.com/billing/#aptos_gas) of your dashboard to see how Shinami charges for sponsorships. Note: only workspace admins can view and change billing information. # Gas Station: how to integrate frontend transaction signing Source: https://docs.shinami.com/developer-guides/aptos/tutorials/gas-station-with-frontend-signing How to integrate 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 API 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 will help you integrate a connected browser wallet (including [Aptos Connect](https://aptosconnect.app/)) or single-app, embedded [Aptos Keyless wallet](https://aptos.dev/en/build/guides/aptos-keyless/introduction) 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](/developer-guides/aptos/tutorials/gas-station-backend-only) for guidance. Finally, after you review the images and code below, if you get stuck on something and have a question feel free to [reach out to us](/help-center/overview#contacting-support). ## Examples ### Sample app overview We've created a sample app that uses the [`Aptos Labs wallet-adapter-react library`](https://github.com/aptos-labs/aptos-wallet-adapter) and the [Shinami TypeScript SDK](https://github.com/shinamicorp/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](https://github.com/shinamicorp/shinami-examples/tree/main/aptos/typescript/wallet_adapter_react). 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`](https://github.com/shinamicorp/shinami-examples/tree/keyless_only/aptos/typescript/keyless) 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 Full node. Signing is done with a connected browser wallet (including [Aptos Connect](https://aptosconnect.app/)) 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](https://github.com/shinamicorp/shinami-examples/tree/main/aptos/typescript/wallet_adapter_react). The key functions in the sample app are [`connectedWalletTxFEBuildBESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L194) on the frontend and [`sponsorAndSubmitTx`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/server/main.ts#L146) on the backend. For a single-app Keyless wallet version of the code, see the [Appendix](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#embedded-keyless-wallet-fe-build-and-sign-be-sponsor-and-submit). The flow is the same for both, and takes place after OpenID provider sign-in once you have a valid `KeylessAccount` for the user. 1. *Frontend:* Build a feePayer transaction. 2. *Frontend:* Obtain the sender's signature over the transaction. 3. *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. 4. *Backend:* Send a [`gas_sponsorAndSubmitSignedTransaction`](/api-docs/aptos/gas-station/api#gas_sponsorandsubmitsignedtransaction) request to Shinami's Gas Station (with the de-serialized transaction and sender AccountAuthenticator). 5. *Shinami*: Shinami will sponsor the transaction and then submit it with the sender and feePayer signatures, returning the `PendingTransactionResponse` to your backend if successful. 6. *Backend:* Handle the response as needed. 7. *Backend:* Send a response to the frontend. 8. *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. ### 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](https://aptosconnect.app/)) 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](https://github.com/shinamicorp/shinami-examples/tree/main/aptos/typescript/wallet_adapter_react). The key functions in the sample app are [`connectedWalletTxBEBuildFESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L102) on the FE and [`buildAndSponsorTx`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/server/main.ts#L100) on the BE. For a single-app Keyless wallet version of the code, see the [Appendix](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#embedded-keyless-wallet-be-build-and-sponsor-fe-sign-and-submit). The flow is the same for both, and takes place after OpenID provider sign-in once you have a valid `KeylessAccount` for the user. 1. *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. 2. *Backend:* Build a feePayer transaction. 3. *Backend:* Make a [`gas_sponsorTransaction`](/api-docs/aptos/gas-station/api#gas_sponsortransaction) request to Shinami's Gas Station to sponsor the transaction. 4. *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 special `0x0` 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 an `INVALID_SIGNATURE` error because the feePayer's signature was over a transaction with the feePayer address but the submitted transaction still has the `0x0` address. 5. *Backend:* Return the serialized transaction, feePayer AccountAuthenticator, and (optionally) feePayer's address to the frontend. 6. *Frontend:* Deserialize the transaction and obtain the sender's signature over the transaction. 7. *Frontend:* Submit the transaction, along with the sender and (deserialized) feePayer signatures, to an Aptos Full node. 8. *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. ### Other flows in our sample app * [Build and sign on the FE, sponsor on the BE, submit on the FE.](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L159) * [Build and sponsor on the BE, sign on the FE, submit on the BE.](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L128) * [Embedded Shinami Invisible Wallet: Build, sponsor, and sign on the BE.](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L236) ## Appendix ### Embedded Keyless wallet: BE build and sponsor, FE sign and submit Below is the single-app, embedded Keyless wallet equivalent of the [`connectedWalletTxBEBuildFESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L102) in the ["Build and sponsor on the BE, sign and submit on the FE"](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#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`](https://github.com/aptos-labs/aptos-ts-sdk/blob/f54cac824a41e41dea09c7a6916858a8604dc901/src/account/KeylessAccount.ts#L37) instance in local browser storage and returns it if found. It comes from the [Aptos Keyless Integration Guide](https://aptos.dev/en/build/guides/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](https://github.com/shinamicorp/shinami-examples/blob/keyless_only/aptos/typescript/keyless/src/client/pages/TransactionPage.tsx#L136) . The `'/buildAndSponsorTx'` endpoint is the same as with the connected wallet example, and is [here](https://github.com/shinamicorp/shinami-examples/blob/keyless_only/aptos/typescript/keyless/src/server/main.ts#L55). ```bash TypeScript theme={null} const keylessTxBEBuildFESubmit = async (message: string, keylessAccount: KeylessAccount): Promise => { // Step 1: Ask the BE to build and sponsor a transaction with // the user's input and address const sponsorshipResp = await axios.post('/buildAndSponsorTx', { message, sender: keylessAccount?.accountAddress.toString() }); // Step 2: 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 // and 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, }); } ``` ### Embedded Keyless wallet: FE build and sign, BE sponsor and submit Below is the single-app, embedded Keyless wallet equivalent of the [`connectedWalletTxFEBuildBESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/aptos/typescript/wallet_adapter_react/src/client/App.tsx#L194)in the ["Build and sign on the FE, sponsor and submit on the BE"](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing#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`](https://github.com/aptos-labs/aptos-ts-sdk/blob/f54cac824a41e41dea09c7a6916858a8604dc901/src/account/KeylessAccount.ts#L37) instance in local browser storage and returns it if found. It comes from the [Aptos Keyless Integration Guide](https://aptos.dev/en/build/guides/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](https://github.com/shinamicorp/shinami-examples/blob/keyless_only/aptos/typescript/keyless/src/client/pages/TransactionPage.tsx#L114). The `'/sponsorAndSubmitTx'` endpoint the same as with the connected wallet example, and is [here](https://github.com/shinamicorp/shinami-examples/blob/keyless_only/aptos/typescript/keyless/src/server/main.ts#L77). ```bash TypeScript theme={null} const keylessTxFEBuildBESubmit = async (message: string, keylessAccount: KeylessAccount): Promise => { // 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). * **Aptos Connect** * One cause is due to stale data. Sign out of your [Aptos Connect](https://aptosconnect.app/) 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)** ```bash TypeScript theme={null} // AccountAuthenticator const serializedAccountAuthenticator = authenticator.bcsToHex().toString(); // AccountAddress const serializedAccountAddress = accountAddress.bcsToHex().toString(); // SimpleTransaction const serializedSimpleTransaction = simpleTx.bcsToHex().toString(); ``` **Deserialize on the other** ```bash TypeScript theme={null} 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())); ``` # Invisible Wallets: backend-only Source: https://docs.shinami.com/developer-guides/aptos/tutorials/invisible-wallets How to integrate Shinami Invisible Wallets ## Overview In this tutorial, we show you how to use the [Shinami Invisible Wallet API](/api-docs/aptos/wallet-services/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](/product-overviews/aptos/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](/developer-guides/core-integration-topics/error-reference#aptos-invisible-wallet-api) as well as the "General Error Codes" section at the top that apply to all Shinami services. ### Usage and Billing FAQ Make sure to check out the Aptos Wallet Services [product usage FAQ](/help-center/aptos/wallet-services-faq) and [billing FAQ](/help-center/billing/aptos-faq#wallet-services) in our Help Center for answers to common questions. ## Tutorial **Notes:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 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](/help-center/aptos/gas-station-faq). 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](https://github.com/shinamicorp/shinami-examples/tree/main), 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](https://www.typescriptlang.org/download/) ) . If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). 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 Wallet Services and Testnet Gas Station rights You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key with both `Wallet Services` rights and with Testnet `Gas Station` rights (Wallet Service rights work for all networks). See [in our Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#create-an-access-key) for info on how to set up a key with rights to all services. Once you've created the key, enter it as the value for `ALL_SERVICES_TESTNET_ACCESS_KEY`. Replace all instances of `{{name}}` with the actual value for that name. ```bash TypeScript theme={null} // 2. Copy your access key value. Must have rights to Wallet Services and Testnet Gas Station. 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](/api-docs/aptos/wallet-services/invisible-wallet-api#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. ```bash Shinami TypeScript SDK expandable theme={null} // 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 and Aptos clients 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); // Create an Aptos client for building, submitting, and fetching transactions const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); // 5. Create a ShinamiWalletSinger to more easily manage the Invisible Wallet const signer = new ShinamiWalletSigner( WALLET_ID, walletClient, WALLET_SECRET, keyClient ); // 6. Create an Invisible Wallet. The call to `executeGaslessTransaction` below will // inititalize the wallet on-chain, so we do not need to initialize it on-chain // at creation time. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN = false; const walletAddress = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN); console.log("Invisible wallet address: ", walletAddress.toString()); ``` **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 initializes 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. ```bash Shinami TypeScript SDK theme={null} // 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](https://explorer.aptoslabs.com/account/0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817/modules/code/message?network=testnet) (it's from an [Aptos tutorial](https://aptos.dev/tutorials/first-move-module/)). 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. ```bash TypeScript theme={null} async function simpleMoveCallTransaction(sender: AccountAddress, withFeePayer = true): Promise { 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** ```bash Shinami TypeScript SDK theme={null} // 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](https://aptoscan.com/?network=testnet) (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](/developer-guides/aptos/tutorials/invisible-wallets#sign-sponsor-and-submit-a-transaction-in-multiple-steps). By default, the code also runs the a function that signs a transaction and then verifies the signature, as [explained in the Appendix](/developer-guides/aptos/tutorials/invisible-wallets#sign-a-transaction-and-verify-the-signature). ## 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. ```bash TypeScript theme={null} async function signSponsorAndSubmitTransactionInTwoSteps(onChainWalletSigner: ShinamiWalletSigner, transaction: SimpleTransaction): Promise { // 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: ```bash TypeScript theme={null} // 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](https://aptoscan.com/?network=testnet) (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. ```bash TypeScript theme={null} async function signAndVerifyTransaction(onChainWalletSigner: ShinamiWalletSigner, transaction: SimpleTransaction): Promise { // 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: ```bash TypeScript theme={null} // 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. # Authentication and API Keys Source: https://docs.shinami.com/developer-guides/core-integration-topics/authentication-and-api-keys How to create and manage your API access keys and authenticate your requests. ## Overview ### How to authenticate You can authenticate with all Shinami services via an access key passed in a header (`'X-Api-Key: ACCESS_KEY'`). Some service API docs will list additional methods, but this method works across the board. We have a lot of helpful content below, but if you don't find an answer to your question you can [reach out to us](/help-center/overview#contacting-support). ### Security callout: when to send from your backend **Gas Station and Wallet Services - always send from your backend** You will need to send requests to our Gas Station and Wallet Services APIs from your backend. These services do not support CORS, so if you attempt to make requests to them from the FE you'll get a CORS error. We do this because exposing these keys on the FE is a security risk. With a Gas Station key access key, an attacker could drain your Gas Station fund associated with the key by using it to sponsor transactions. With a wallet services access key, a bad actor could sign transactions from your user's wallets. ### Rotating API access keys You can disable an API access key and create a new one as needed (see [Disable, enable, or delete a key](/developer-guides/core-integration-topics/authentication-and-api-keys#disable-enable-or-delete-a-key)). ### API Access Key Limit You can make a total of 10 API access keys. You may use this allotment across whatever combination of Shinami services and (Chain, Network) pairs that works best for you. ## Create an Access Key ### 1. Visit the Access Keys page and choose a chain All access key creations start with clicking "+ Create Key" on the [Access Keys page](https://app.shinami.com/access-keys) of your dashboard and then selecting a chain. Once the chain is selected, the modal expands to show the key configuration options for the given chain as shown in the Aptos, Movement, and Sui key configuration sections below. ### 2a. Aptos key configuration Below, we show the creation of a Testnet access key with all services selected. Choose only the services you need for your key. You can't add or remove a service to a key after creating it (but you can make a new key). 1. The chain the key works for (Aptos) 2. Select the network the key works for (Aptos Testnet or Aptos Mainnet). 3. If sponsoring transactions, select the "Gas Station" checkbox to give the key Gas Station rights. 1. Assign a max QPS (requests per second) to the key. If you need more QPS, reach out to us at \`[support@shinami.com](mailto:support@shinami.com) and let us know the email you sign in to your workspace with, how much QPS you need and for how long (a one-time event vs sustained high QPS needs). 2. Then, link the key to a Gas Station fund on the network. If you don't already have one, you can create it through the modal. This link cannot later be changed to a different fund (but you can make a new key). All requests for sponsorship using the key will draw APT from the linked fund. For guidance on creating a Gas Station fund see the [Aptos Gas Station page of our Help Center](/help-center/aptos/gas-station-faq). 4. If using our Invisible Wallets, select the "Wallet Services" checkbox to give the key Wallet Services rights. 1. Assign a max QPS (requests per second) to the key (noting that you'll need to periodically [create a session token](/api-docs/aptos/wallet-services/invisible-wallet-api#key-createsession) for interacting with each a wallet). Your QPS allotment per network is based on your [Wallet Services plan](https://app.shinami.com/billing#aptos_wallet). 2. Note that Invisible Wallet keys will need rights to all services to use certain methods, like [submitting a sponsored transaction on behalf of a wallet](/api-docs/aptos/wallet-services/invisible-wallet-api#wal-executegaslesstransaction). 5. Give the key a name. We recommend something useful, describing the app or service the key is for. 6. Click "+ Create key". The key will then show up in the Access Keys table. See below for how to find sample code and make configuration changes to your key. ### 2b. Movement key configuration Below, we show the creation of a Testnet access key. You can't add or remove a service to a key after creating it (but you can make a new key). 1. The chain the key works for (Movement) 2. Select the network the key works for (Movement Testnet or Movement Mainnet). 3. If sponsoring transactions, select the "Gas Station" checkbox to give the key Gas Station rights. 1. Assign a max QPS (requests per second) to the key. If you need more QPS, reach out to us at `support@shinami.com` and let us know the email you sign in to your workspace with, how much QPS you need and for how long (a one-time event vs sustained high QPS needs). 2. Then, link the key to a Gas Station fund on the network. If you don't already have one, you can create it through the modal. This link cannot later be changed to a different fund (but you can make a new key). All requests for sponsorship using the key will draw MOVE from the linked fund. For guidance on creating a Gas Station fund see the [Movement Gas Station page of our Help Center](/help-center/movement/gas-station-faq#how-do-i-create-a-fund%3F). 4. If using our Invisible Wallets, select the “Wallet Services” checkbox to give the key Wallet Services rights. 1. Assign a max QPS (requests per second) to the key (noting that you’ll need to periodically [create a session token](/api-docs/movement/wallet-services/invisible-wallet-api#key-createsession) for interacting with each a wallet). Your QPS allotment per network is based on your [Wallet Services plan](https://app.shinami.com/billing/#movement_wallet). 2. Note that Invisible Wallet keys will need rights to all services to use certain methods, like [submitting a sponsored transaction on behalf of a wallet](/api-docs/movement/wallet-services/invisible-wallet-api#wal-executegaslesstransaction). 5. Give the key a name. We recommend something useful, describing the app or service the key is for, e.g. "Best Swap App BE sponsorship". 6. Click "+ Create key". The key will then show up in the Access Keys table. See below for how to find sample code and make configuration changes to your key. ### 2c. Sui key configuration Below, we show the creation of a Testnet access key with all services selected. Choose only the services you need for your key. You can't add or remove a service to a key after creating it (but you can make a new key). 1. The chain the key works for (Sui) 2. Select the network the key works for (Sui Testnet or Sui Mainnet). 3. If sponsoring transactions, select the "Gas Station" checkbox to give the key Gas Station rights. 1. Assign a max QPS (requests per second) to the key. If you need more QPS, reach out to us at `support@shinami.com` and let us know the email you sign in to your workspace with, how much QPS you need and for how long (a one-time event vs sustained high QPS needs). 2. Then, link the key to a Gas Station fund on the network. If you don't already have one, you can create it through the modal. This link cannot later be changed to a different fund (but you can make a new key). All requests for sponsorship using the key will draw SUI from the linked fund. For guidance on creating a Gas Station fund see the [Sui Gas Station page of our Help Center](/help-center/sui/gas-station-faq#/how-do-i-create-a-fund). 4. If using our Invisible Wallets or zkLogin wallet API, select the "Wallet Services" checkbox to give the key Wallet Services rights. 1. Assign a max QPS (requests per second) to the key (noting that for Invisible Wallets you'll need to periodically [create a session token](/api-docs/sui/wallet-services/invisible-wallet-api#shinami-key-createsession) for interacting with each wallet). Your QPS allotment per network is based on your [Wallet Services plan](https://app.shinami.com/billing#sui_wallet). 2. Note that Invisible Wallet keys will need rights to all services to use certain methods, like [submitting a sponsored transaction on behalf of a wallet](/api-docs/sui/wallet-services/invisible-wallet-api#shinami-wal-executegaslesstransactionblock). 5. Give the key a name. We recommend something useful, describing the app or service the key is for as well as whether you intend it for the frontend or backend, e.g. "Best Swap App FE testing". 6. Click "+ Create key". The key will then show up in the Access Keys table. See below for how to find sample code and make configuration changes to your key. ## Key management ### Edit name and QPS; find service URLs and sample code 1. In the [Access Keys table](https://app.shinami.com/access-keys), expand and collapse the expanded tray for a key. 2. Change the name of a key. 3. Copy your key value. We put "(FOR BACKEND USE)" next to any key with Gas Station and/or Wallet Services rights because those services do not support CORS and are intended for BE-only use for security reasons. 4. Switch between services for a given key. 5. If needed, set an IP allowlist (BE keys) or a domain allowlist (FE keys) - see the next section below. 6. Update the QPS allotments for the service for the key (and click "Save"). 7. Visit the API docs for the service. 8. Find the relevant URL(s) for the service. 9. Find sample code you can copy and paste to make a sample request to get you up and running quickly (your API key value is already added to the code). The SDK examples aren't meant as a production code template, since you'll likely import your API key value, etc. ### Set sender domain and IP address allowlists for a key When a key's allowlists are empty, we accept all valid requests that use that key. If you've entered values into an allowlist, we check against it when getting a request that uses that access key. If the domain and/or IP address the request originates from is not in the corresponding allowlist, we'll return a `HTTP 401 Unauthorized`. If you have entries in both lists, we'll check that a request satisfies both lists. You can add up to 10 entries in both domain and IP allowlists. **General advice** * Test with empty allowlists first to ensure your requests are working properly. * Allowlists are generally not needed for backend keys because HTTPs means others can't see your key. * IP whitelists are rare and should only be considered when requests are *guaranteed* to come from a fixed set of IP addresses. 1. In the [Access Keys table](https://app.shinami.com/access-keys), click the "+" to open the key's details tray. The icon then becomes a "-" you can click to collapse the tray. 2. Select the "Access Control" tab. 3. Enter the value you want and click "Add +". 4. Click "Save". **Note:** It may take up to five minutes for the changes to take effect. ### Disable-enable or delete a key 1. In the [Access Keys table](https://app.shinami.com/access-keys), select the key(s) you want to update. 2. Choose the action you want to take for the key(s). We encourage you to opt for disable over delete, since deletion is unrecoverable. You may, for example, wish to keep a disabled key in your Access keys table for a while after disabling it so that you can find the key's value if needed for an investigation. If you do choose to delete a key, you'll need to then confirm the deletion through a dialog box: # Error Reference Source: https://docs.shinami.com/developer-guides/core-integration-topics/error-reference Error codes and resolution steps for all Shinami APIs. ## Overview Shinami APIs generally use the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) standard. HTTP and JSON-RPC error codes you may receive when interacting with our services are listed below, along with service-specific errors. We have a lot of helpful content below, but if you don't find an answer to your question you can [reach out to us](/help-center/overview#contacting-support). ## General Error Codes ### HTTP Errors All of our services return standard [HTTP codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) including (but not limited to): | Code | HTTP Meaning | Notes | Services | | :--- | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | 400 | Bad Request | Check your request to ensure it's properly formatted. | All | | 401 | Unauthorized | When you create an API Access Key, you give it rights to one or more services. Confirm that you've correctly copied the value of an [Access Key](https://app.shinami.com/access-keys) with rights for the service(s) you're trying to use. Confirm that the key has not been disabled.

If you have an [IP and/or domain whitelist/allowlist for the key](/developer-guides/core-integration-topics/authentication-and-api-keys#set-sender-domain-and-ip-address-allowlists-for-a-key), ensure that the IP and/or domain of the requests you're sending matches an entry in the list(s). It can be good to test first with empty allowlists - so all requests with the API key are accepted - and then add values later if desired, once you know everything is working. | All | | 500 | Internal Server Error | We're unable to process your request right now. You may occasionally see a few of these in the course of normal operations, and a retry will often succeed.

However, you should not see these consistently or in a large amount. Get in touch with us if you do. | All | ### JSON-RPC Errors For JSON-RPC errors, Shinami returns `HTTP 200` with the JSON-RPC error in the JSON response. Common errors across our services include: | Error Code | Error Type | Notes | | :--------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32000 to -32099 | Server error | Read the error message for more information. | | -32010 | Too many requests | You may have exceeded your QPS rate limit. We recommend implementing retries with a backoff to deal with any rate limits you encounter. | | -32012 | Resource Limit Exceeded | You have reached your quota for the given operation for the current time period. Do not retry the operation in the same time period as you will get the same response.

For example, you surpassed our Sui zkLogin API's cap of two proofs per address per minute. If you're unclear on the limit you reached or want to discuss the possibility of raising it, reach out to `support@shinami.com`. | | -32600 | Invalid Request | The JSON sent is not a valid request object. Double check your request formatting to ensure it's valid JSON and has all required request elements. | | -32601 | Method not found | The method does not exist / is not available. Double-check the method name you provided. | | -32602 | Invalid params | Invalid method parameter(s). Double check the count, type, and values of your request parameters. Read the error message for more information. | | -32603 | Internal error | Internal JSON-RPC error. Read the error message for more information. | | -32700 | Parse error | An error occurred on the server while parsing the JSON input payload. Double-check your request. Perhaps there's an extra or missing `{`,`}`,`"`, `[`,`]`, etc. | ### Other Errors You may see the following errors when using our [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients). | Error Code | Message | Notes | | :--------- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 7979 | Bad response format | This is a code our SDK returns in some cases in place of an HTTP 401. If you get this, double check that you're using a valid, enabled access key value. If you have any domain or IP allowlists on the key, make sure you are matching them with your request. It can be good to test first with empty allowlists - so all requests with the API key are accepted - and then add values later if desired, once you know everything is working. | ## Aptos: service-specific error codes ### Aptos Gas Station API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Gas Station API. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32602 | "expirationTimestampSecs out of range" | Make sure that your transaction expiration timestamp is not in the past and is less than one hour from now. | | -32602 | "Wrong chainId" | Our Aptos Gas Station works on Testnet (`chainId: 2`) and Mainnet (`chainId: 1`). Make sure you're setting the appropriate chainId. | | -32602 | "gasUnitPrice out of range" | Make sure you're using a price per gas unit that is within the minimum and maximum allowed values. For more info, [see here](https://aptos.dev/concepts/base-gas/#unit-and-pricing-constants). | | -32602 | "maxGasAmount out of range" | Make sure you're using a max gas amount that is within the minimum and maximum allowed values. For more info, [see here](https://aptos.dev/concepts/gas-txn-fee#gas-parameters-set-by-governance). | | -32602 | "Stale sequenceNumber" | Make sure you're using a sequence number for the transaction that is the equal to or greater than the account's current on-chain sequence number. | | -32602 | "Bad rawTransaction bytes" | Ensure you're sending a successfully constructed transaction in the format our [API expects](/api-docs/aptos/gas-station/api#methods). | **Other errors** | Error Code | Error Message | Notes | | :--------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -1 | "No active fund" | You need an active Gas Station fund tied to the access key you're using in order to successfully sponsor a transaction. | | -2 | "Insufficient fund balance" | The Gas Station fund tied to the API access key you used does not have sufficient APT to sponsor this transaction. This could be due in part to tying up unnecessarily large amount of APT in active sponsorships. For that, we recommend setting gas budgets that are enough to execute your transactions with just a small buffer added. For guidance on how to deposit APT into your fund, see the [Aptos Gas station page of our Help Center](/help-center/aptos/gas-station-faq). | | -3 | "Transaction submission failed" | Read the error message for additional information. | ### Aptos Invisible Wallet API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Invisible Wallet API. Invisible Wallet requests that require an access key with Gas Station rights can also return any of the [Aptos Gas Station errors](/developer-guides/core-integration-topics/error-reference#aptos-gas-station-api), as we'll propagate those back to you if there is a Gas Station error as a part of processing your request. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :--------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32003 | "Make sure the key you are using has access to the required services" | The access key you are using does not have access to all the Shinami services the request interacts with. Make sure that the key you use Gas Station rights in addition to Invisible Wallet rights if needed. | | -32602 | "Wallet ID not found" | Confirm you're using a `walletId` value that you've used to successfully create a wallet (as this suggests you haven't created an Invisible Wallet with this id yet.). | | -32012 | Resource Limit Exceeded | You have reached the maximum number of wallets you can create this month for Aptos. Do not retry on this error. Other wallet operations will still work, like signing with wallets you've already created. If you need to create more wallets this month reach out to `support@shinami.com` to discuss an Enterprise wallet plan that fits your needs. | | -32013 | "Wallet ID already exists, use 'getWallet' to retrieve the address" | You have already created (and possibly initialized) a wallet with this walletId. | | -32013 | "Wallet ID already exists, use 'initializeWalletOnChain' to initialize for a specific network" | As the error message states, you can use `initializeWalletOnChain` with the walletID, or simply use the walletID in a transaction with `executeGaslessTransaction` and it will be initialized in the process. | | -32602 | "Failed to decrypt wallet key" | Successful decryption requires pairing a `walletId` with a `sessionToken` generated by the `secret` used when creating the wallet. Confirm that you're using the `secret` that was used to generate a `sessionId` when creating this wallet. | | -32602 | "Bad session token" | The sessionToken has expired or is not parseable. Ensure you're correctly passing a sessionToken value that has not expired. | ### Example JSON-RPC Errors Below are two example requests and their associated JSON-RPC error responses, one using cURL and one using our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). *Replace all instances of `{{name}}` with the actual value for that name* ```bash cURL theme={null} // Unfortunately, this is not a method we support, :( curl https://api.us1.shinami.com/aptos/gas/v1 \ -X POST \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"getAMilkshakeAndFries", "params":[], "id":1 }' // Response: { "error" : { "code" : -32601, "message" : "Method not found" }, "id" : 1, "jsonrpc" : "2.0" } ``` ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("YOUR_API_KEY"); const transaction = // build a tranasaction where // transaction.rawTransaction.expiration_timestamp_secs // is set in the past, or more than an hour in the future await gasStationClient.sponsorTransaction(transaction); // Response: JSONRPCError: Invalid params at new JSONRPCError ... { code: -32602, data: { details: 'expirationTimestampSecs out of range' } ``` *** ## Movement: service-specific error codes ### Movement Gas Station API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Gas Station API. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32602 | "expirationTimestampSecs out of range" | Make sure that your transaction expiration timestamp is not in the past and is less than one hour from now. | | -32602 | "Wrong chainId" | Our Movement Gas Station works on Testnet and Mainnet. Make sure you're setting the appropriate chainId. | | -32602 | "gasUnitPrice out of range" | Make sure you're using a price per gas unit that is within the minimum and maximum allowed values. For more info, [see here](https://aptos.dev/concepts/base-gas/#unit-and-pricing-constants). | | -32602 | "maxGasAmount out of range" | Make sure you're using a max gas amount that is within the minimum and maximum allowed values. For more info, [see here](https://aptos.dev/concepts/gas-txn-fee#gas-parameters-set-by-governance). | | -32602 | "Stale sequenceNumber" | Make sure you're using a sequence number for the transaction that is the equal to or greater than the account's current on-chain sequence number. | | -32602 | "Bad rawTransaction bytes" | Ensure you're sending a successfully constructed transaction in the format our [API expects](/api-docs/movement/gas-station/api#methods). | **Other errors** | Error Code | Error Message | Notes | | :--------- | :------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -1 | "No active fund" | You need an active Gas Station fund tied to the access key you're using in order to successfully sponsor a transaction. | | -2 | "Insufficient fund balance" | The Gas Station fund tied to the API access key you used does not have sufficient MOVE to sponsor this transaction. This could be due in part to tying up unnecessarily large amount of MOVE in active sponsorships. For that, we recommend setting gas budgets that are enough to execute your transactions with just a small buffer added. For guidance on how to deposit MOVE into your fund, see the [Movement Gas station page of our Help Center](/help-center/movement/gas-station-faq). | | -3 | "Transaction submission failed" | Read the error message for additional information. | ### Movement Invisible Wallet API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Invisible Wallet API. Invisible Wallet requests that require an access key with Gas Station rights can also return any of the [Movement Gas Station errors](/developer-guides/core-integration-topics/error-reference#movement-gas-station-api), as we'll propagate those back to you if there is a Gas Station error as a part of processing your request. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :--------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32003 | "Make sure the key you are using has access to the required services" | The access key you are using does not have access to all the Shinami services the request interacts with. Make sure that the key you use Gas Station rights in addition to Invisible Wallet rights if needed. | | -32602 | "Wallet ID not found" | Confirm you're using a `walletId` value that you've used to successfully create a wallet (as this suggests you haven't created an Invisible Wallet with this id yet.). | | -32012 | Resource Limit Exceeded | You have reached the maximum number of wallets you can create this month for Movement. Do not retry on this error. Other wallet operations will still work, like signing with wallets you've already created. If you need to create more wallets this month reach out to `support@shinami.com` to discuss an Enterprise wallet plan that fits your needs. | | -32013 | "Wallet ID already exists, use 'getWallet' to retrieve the address" | You have already created (and possibly initialized) a wallet with this walletId. | | -32013 | "Wallet ID already exists, use 'initializeWalletOnChain' to initialize for a specific network" | As the error message states, you can use `initializeWalletOnChain` with the walletID, or simply use the walletID in a transaction with `executeGaslessTransaction` and it will be initialized in the process. | | -32602 | "Failed to decrypt wallet key" | Successful decryption requires pairing a `walletId` with a `sessionToken` generated by the `secret` used when creating the wallet. Confirm that you're using the `secret` that was used to generate a `sessionId` when creating this wallet. | | -32602 | "Bad session token" | The sessionToken has expired or is not parseable. Ensure you're correctly passing a sessionToken value that has not expired. | ### Example JSON-RPC Errors Below are two example requests and their associated JSON-RPC error responses, one using cURL and one using our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). *Replace all instances of `{{name}}` with the actual value for that name* ```bash cURL theme={null} // Unfortunately, this is not a method we support, :( curl https://api.us1.shinami.com/movement/gas/v1 \ -X POST \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"getAMilkshakeAndFries", "params":[], "id":1 }' // Response: { "error" : { "code" : -32601, "message" : "Method not found" }, "id" : 1, "jsonrpc" : "2.0" } ``` ```bash Shinami TypeScript SDK theme={null} import { GasStationClient } from "@shinami/clients/aptos"; const gasStationClient = new GasStationClient("YOUR_API_KEY"); const transaction = // build a tranasaction where // transaction.rawTransaction.expiration_timestamp_secs // is set in the past, or more than an hour in the future await gasStationClient.sponsorTransaction(transaction); // Response: JSONRPCError: Invalid params at new JSONRPCError ... { code: -32602, data: { details: 'expirationTimestampSecs out of range' } ``` *** ## Sui: service-specific error codes ### Sui Gas Station API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Gas Station API. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32602 | "gasBudget: value out of range" | You requested a gas budget either lower than the minimum amount required to pay for a transaction, or far larger than needed. Please set a GAS\_BUDGET that will cover the expect cost plus a small margin. [See here for tips.](/developer-guides/sui/tutorials/gas-station-backend-only#tips-for-setting-your-sponsorship-budget) | | -32602 | "GasCoin argument not allowed for sponsorship." | In a sponsored transaction, you cannot use the gas object provided by Shinami for other purposes. For example, you cannot write `const [coin] = txb.splitCoins(txb.gas,[txb.pure(100)]);` because it's accessing `txb.gas`. | **Other errors** | Error Code | Error Message | Notes | | :--------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -1 | "Gas object unavailable" | We had an issue processing the request. Try again at a later time, or try with a smaller gas budget. | | -2 | "Insufficient fund balance" | Add extra SUI to your fund balance. This could be due in part to tying up unnecessarily large amounts of SUI in existing sponsorships. For that, we recommend using our [auto-budgeting feature](/developer-guides/sui/tutorials/gas-station-backend-only#tips-for-setting-your-sponsorship-budget). For guidance on how to deposit SUI into your fund, see the [Sui Gas Station page of our Help Center](/help-center/sui/gas-station-faq) . | ### Sui Invisible Wallet API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our Invisible Wallet API. Invisible Wallet requests that require an access key with Gas Station rights can also return any of the [Sui Gas Station errors](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api), as we'll propagate those back to you if there is a Gas Station error as a part of processing your request. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message | Notes | | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32003 | "Make sure the key you are using has access to the required services" | The access key you are using does not have access to all the Shinami services the request interacts with. Make sure that the key you use has Gas Station rights in addition to Invisible Wallet rights if needed. | | -32003 | "Access key not associated with a network" | Wallet access keys are not associated with a network and work with all networks (Testnet, Mainnet). Gas Station keys are, however, so when using Wallet Service requests that require Gas Station access, make sure that the access key you're using has rights to those services on the network you're using. For guidance on how to set up an access key with rights to all services see our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys). | | -32012 | Resource Limit Exceeded | You have reached the maximum number of wallets you can create this month for Sui (across both Invisible and zkLogin Wallets). Do not retry on this error. Other wallet operations will still work, like signing with wallets you've already created. If you need to create more wallets this month reach out to `support@shinami.com` to discuss an Enterprise wallet plan that fits your needs. | | -32602 | "Wallet ID not found" | Confirm you're using a `walletId` value that you've used in a successful call to `shinami_wal_createWallet` (as this suggests you haven't created an Invisible Wallet with this id yet.). | | -32602 | "Failed to decrypt wallet key" | Successful decryption requires pairing a `walletId` with a `sessionToken` generated by the `secret` used when creating the wallet. Confirm that you're using the `secret` that was used to generate a `sessionId` when creating this wallet. | | -32602 | "Wallet ID already exists, use 'getWallet' to retrieve the address" | You've already created a wallet using this `walletID`. Use `shinami_wal_getWallet` to look up the wallet's Sui address if desired. | | -32602 | "Bad session token" | The sessionToken has expired or is not parseable. Ensure you're correctly passing a sessionToken value that has not expired. | | -32602 | *Beneficiary graph error messages include:* "Failed to build tx. Please double check beneficiaryGraphId", "Non-existing beneficiaryGraphId", "beneficiaryGraphId not an object", "Invalid data for beneficiaryGraphId" | Confirm that you are using the correct object id of a beneficiary graph that currently exists to the network you're making a request to (try looking it up in the Sui Explorer for that network). | ### Sui zkLogin wallet API In addition to the [general error codes](/developer-guides/core-integration-topics/error-reference#general-error-codes) listed above, the following codes are returned by our zkLogin wallet API. Note that you may see additional errors beyond what's listed below. **JSON-RPC errors** | Error Code | Error Message Example | Notes | | :--------- | :--------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32012 | "Resource Limit Exceeded" | Returned on `shinami_zkp_createZkLoginProof` requests.

You have exceed the zk proof generation cap of 2 per address per minute. Do not immediately retry. Wait up to a minute and try again.

Returned on `shinami_zkw_getOrCreateZkLoginWallet` requests.

You have reached the maximum number of wallets you can create this month for Sui (across both zkLogin and Invisible Wallets). Do not retry on this error. Other wallet operations will still work, like fetching the salt for wallets you've already created. If you need to create more wallets this month reach out to `support@shinami.com` to discuss an Enterprise wallet plan that fits your needs. | | -32602 | "The issuer `{{issuer name}}` is not supported" | Use an OAuth provider from the list of [supported providers](https://docs.sui.io/concepts/cryptography/zklogin#openid-providers). | | -32602 | "Bad jwt. The Token has expired on 2023-11-08T23:52:00Z." | Ensure you're using a non-expired JWT. | | -32602 | "Nonce TjpyZAr252rQ8dzOwoA1vioMTzY does not match computed nonce LAo4DvFaHZ88kREHZmRqgz8EBVw" | Ensure that the `jwtRandomness`, `ephemeralKeyPair`, and `maxEpoch` values passed to `shinami_zkp_createZkLoginProof` are the ones used to prepare the nonce from the OAuth flow that generated the `jwt` that's also being passed to the function. | | -32602 | `{"name":"InputValidationError","message":"JWT randomness 8641894711085230802105740842295470710772303705 must be 16 bytes"}` | Ensure that you're passing each parameter's value in the expected representation. For example, our SDK's `createZkLoginProof` method expects the `jwtRandomness` parameter as a `bigint`. | ### Example JSON-RPC errors Below are two example JSON-RPC error responses - one using cURL and one using our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). *Replace all instances of `{{name}}` with the actual value for that name* ```bash cURL theme={null} // Unfortunately, this is not a method we support, :( curl https://api.us1.shinami.com/sui/gas/v1 \ -X POST \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"getAMilkshakeAndFries", "params":[], "id":1 }' // Response: { "error" : { "code" : -32601, "message" : "Method not found" }, "id" : 1, "jsonrpc" : "2.0" } ``` ```bash Shinami TypeScript SDK theme={null} // Here, we are making a request to Shinami Gas Station for transaction // sponsorship with a value of 1 MIST. This is too low to have a chance // of success, so we get a -32602 Invalid params error import { GasStationClient } from "@shinami/clients/sui"; const gasClient = new GasStationClient("YOUR_API_KEY"); await gasClient.sponsorTransaction( {{transactionToSponsor}}, {{senderAddress}}, 1 // 1 MIST ); // Response: JSONRPCError: Invalid params at new JSONRPCError... { code: -32602, data: { details: 'gasBudget: 1 out of range' } } ``` # Adding on-chain elements to your game Source: https://docs.shinami.com/developer-guides/learning-about-web-3/adding-on-chain-elements-to-your-game This high-level guide is geared towards both developers and non-developers considering whether or not to add Web3 elements to a game. ## Overview Below, we cover the benefits of adding Web3 elements to a game and help you think about how much to add (spoiler alert: you don't need that much to make a big difference). We then cover the key questions you need to ask to get started planning. We also provide a high-level overview of the possible interactions between your app, other apps, and the blockchain so that you'll know what to look into further in order to proceed. Finally, we share next steps if you're interested in learning more. ## Adding Web3 elements: why and what ### Player ownership = engagement, retention Web3 = infusing your game with assets users can own, upgrade, lend, trade, use for rewards and experiences, and more. It's a powerful way to increase engagement and can enhance replay-ability and community excitement about your game. Games have always rewarded players for time spent and accomplishments achieved. For example, leveling up a character, earning gold or gems that can be used to buy items, and unlocking new levels. What changes with Web3 is that players now have true ownership over these items and rewards. This ownership is also publicly visible - not restricted to a private database that only your app has access to. This means that other Web3 apps can see the data in a standardized way that they know how to read. This can enable new avenues for engagement and value accrual. For example, players joining a Web3 social network can display the game sword they proudly earned on their profile by proving that they own it. Or, they could sell the item on a marketplace, earning some profit for their long hours spent on the game (and earning your game a cut of the sale). As we'll discuss below, you may or may not give players full control over their assets immediately, as full control means more effort on their part and less control on yours. You may slowly give them more control over time, or only give this control to players who want it. The goal is not to force a bunch of effort and new ideas onto all players, or to create new unpredictability for you, the game maker. It's to use true ownership and public visibility of assets, and the new opportunities this creates, only when it benefits your users and your game. Two common examples of owned assets are covered next. ### In-game items Gamers are used to acquiring items and upgrades for their efforts. Now, you can give players verifiable ownership on a blockchain of some of these items. Why is this good for you or the player Players feel more invested in your game, because this unlocks the opportunity to buy, sell, lend, or trade these verifiably scarce items in a transparent way where they won't be scammed. No more transferring money to a PayPal account and hoping that the person on the other end actually gives you their World of Warcraft password. Allowing players to have control over using, displaying, and trading these items gives them an enhanced sense of connection to your game and a pride in their in-game accomplishments. It can also help build a vibrant community around your game, as players interact with each other and display their assets for yet-to-be-acquired users to see. Letting players sell their items adds a new way for both the players and your game to make money on what the players are doing anyway: having fun playing your game. Your game gets access to a global market where you can get a cut of these sales. For an example of how player ownership of in-game items can drive engagement and retention, see our [case study with BlueJay Games](https://www.shinami.com/case-studies/bluejay). ### Tokens A game-specific blockchain token can be used to give players a stake in the success of the game's community. Tokens can be given for playing the game, but also for taking other actions that drive interest to the game like posting on social media. These tokens can be exchanged for all sorts of rewards like unlocking new maps, game merchandise, etc. Players can also be given the option to sell some of their tokens to others, earning money for their time and effort building your ecosystem and creating additional value around your game. The benefits for you include driving player engagement and community growth, rewarding players fairly, and easily tying rewards to key actions you want to promote. For an example of how a token can be used to drive engagement and retention, see our [case study with Playground](https://www.shinami.com/case-studies/playground). ## How to add Web3 elements to your Web2 game ### Start small. Let's not forget what players care most about: a great UX and a game that's fun to play. You don't need to write every change in player or game state to the chain. A good place to start is with one key item like an in-game object represented as on-chain NFT. This could be a character or a key item that character uses. Just owning one important thing can make a big difference in how connected a user feels to a game. As a result, you don't have to read from the blockchain or write to it constantly. You can cache some chain-based game state and will almost certainly store some in your internal, private databases. You only need to write major changes to the blockchain, like when a user acquires that key item for the first time or when it's upgraded. ### Introduce it slowly. Player ownership is something you can slowly layer in over time - after all, these things are a reward for spending time in your game. When players launch your game, the reason they come back the next day is because of great gameplay and/or community. No NFT can make up for a boring game. Instead, think about ownership as one of multiple reasons why a player sticks around for week two or month two. Maybe they don't earn ownership of a game object, or game tokens, until level 20. Or, after 10 hours of gameplay. From there, maybe they don't earn an upgrade or more tokens until level 30, or another 5 hours. And, maybe that second point is where you introduce to them that they have a personal Web3 wallet behind the scenes. You can then give players the option to learn more or take direct control of their wallet, but don't force it. Later, you can introduce actions they can take with their wallet, like selling their object to another player or trading some of their tokens for a reward. Eventually, they can gain full control over the items in their wallet by either taking control of the wallet or transferring the items to a wallet they control. The point is to not make Web3 knowledge a requirement of playing your game. Instead, these elements are an optional but rewarding enhancement to a player's experience with your game and game community. ## Questions to think through The following questions are designed to help you think about how and what Web3 elements to add to your game. The presumption of this section is that you do not want a game where users need to be Web3 experts to play. ### How will users ultimately benefit? You should have a plan for at least one way you think users will benefit from storing a game asset in a blockchain wallet as opposed to your private database. The plan may change as you get more feedback from players and see what's working well about your game and what can be improved. Further, the benefit may not come immediately. Still, you should have one in mind. Examples include: * Users will eventually be able to take full control of their NFT(s) or tokens and sell or trade them as they wish on third party Web3 marketplaces and exchanges. * Users will be able to sell or trade their NFT(s) or tokens in an in-game marketplace you create. * Users will gain special rewards for having the NFT(s) or tokens - perhaps the ability to unlock new levels or level-up their heroes faster. **Decision to make** * What is our initial plan for how users will benefit from this effort, both initially and in the future ### What initial asset(s) will be on chain? This is directly tied to your plan for the value users will get. As we covered above, key options include: * NFT(s) representing in-game characters or items. * Game-specific reward tokens. * Designing a good plan for how your game's tokens are created, earned, used, transferred, burned, etc - called your "tokenomics" - can be a significant amount of work. It's important to think through all the relevant user scenarios, to balance rewards and distribution in a way that's fair and meaningful to players, and to be flexible enough to accommodate both a user base that can change size over time and the fact that your rewards and plans may change over time. You also may want to include an element of governance with your token - where token holders can vote on the direction of your game (but be careful if non-players can buy these tokens). **Decisions to make** * Determine which game assets, if any, will be stored on the blockchain. It's okay to start small with one item - like a player's in-game character - and add more over time. * Determine whether you will have an in-game currency that lives on the blockchain. You could keep game tokens inside the game and not allow transfer or sale of the tokens to happen outside of the game (at least initially). You could also award players experience points in your database as a first step, and later convert those to tokens once you've finalized your token plan. ### What on-blockchain code will your app need? You will need the code - called a smart contract - that defines your NFT or game token specifications and how to create, transfer, upgrade (for NFTs), and (optionally) destroy these assets. We talked about blockchain as a public database above, but really it's a public computer with code that runs on top of a database. Some of the code that runs on it is smart contracts (the other code is the blockchain protocol itself, which processes transactions). You often won't need to write a lot of code, but you'll need someone with experience working in the programming language of the chain (or able to learn it). **Needs** * A developer on your team or a dev shop you hire you to write your smart contract(s). * (Especially if you make an in-game currency) A smart contract audit (a specialist reviews your code to look for errors in your desired functionality, security vulnerabilities, etc). ### How often will a blockchain write occur (and who pays)? * If you made the wallet, you should pay the small gas fee for the transactions as the users will not have any tokens needed to pay. * If the user connected the wallet, it's up to you. However, sponsoring the gas fee can drive more interactions with your game. **Needs** * A developer on your team or a dev shop you hire to write code that builds and executes blockchain transactions that mint game assets (NFTs or token). This will involve both the service that manages your player wallets (e.g. Shinami wallet service) and a Node Service to write to the blockchain. Shinami has a [TypeScript SDK](https://www.npmjs.com/package/@shinami/clients) that makes this easy. * A plan to sponsor the gas fees of these transactions. This is done in the same code as the first bullet point. With Shinami you can sponsor the gas for a transaction for an embedded wallet with 0 additional requests needed! ### How often will a blockchain read occur (and will you cache/duplicate this info in your internal db)? If you have full control of player wallets, the most you would need to read is likely each time a player logs into your game and then whenever a change occurs (e.g. your game upgrades the player's character and the NFT representing it). Further, you may wish to duplicate some or all of the information about each player's character or game token total in your own internal database. This can provide faster read times, fewer requests to your blockchain Node provider, and redundancy. If some or all of your players have control over their wallet (and thus their NFT(s) or game tokens), you'll need to check the blockchain regularly. This is because players can transfer assets to another wallet (including selling them to another player). In this case, you may wish you read from the wallet when each new level starts, every X minutes, etc. You still may wish to cache or duplicate some of this data in an internal database, but you'll need a mechanism to check and update this information regularly to catch any changes in what a player owns. These changes could also mean a player's inventory decreases or increases - for example if player A buys an NFT representing an item from player B because they want to use it in the game, or if player A has two accounts and transfers game tokens from one of their accounts to the another. **Needs** * A developer on your team or a dev shop you hire to write code that reads what exists in a user’s wallet. * (optional) A caching strategy for frequently read data. * (optional) A data duplication strategy for game assets that also exist on-chain. ### What wallet type(s) will you support? This ties into how users authenticate with your app as well as whether you want to accommodate users connecting a pre-existing Web3 wallet to your app. It is also a question of user control vs hassle: embedded wallets, a common choice for games targeting Web2 natives, introduce little to no friction for the player with the tradeoff that players don't have full control over their assets. It may be that the wallet players use can change over the course of their time with your game. For example, you may start all players with embedded wallets and give them the option to later migrate to a connected wallet that stores their game assets (maybe as a part of future work to add more Web3 elements to your game). Here is a brief summary of wallet types: | # | Wallet type | User authentication | Required user interaction | Wallet only works with your game | User can initiate transfers, sales, etc of their game assets on their own, even when not logged into your game | Target user type | | :- | :------------------------------------------------------------- | :------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------- | :------------------------------------------------------------------------------------------------------------- | :--------------------------- | | 1 | Invisible NFT wallet (embedded) | Whatever your game already does. | None. Provides for a very smooth UX. | Yes | No | Web2 native | | 2 | Single-app zk SSO wallet (Sui zkLogin, Aptos Keyless) | User logs into their supported SSO provider. | Logging into their account with an SSO provider (e.g. Google), which may or may not be how they already log into your game. Beyond that it provides for a very smooth UX. | Yes | No | Web2 native | | 3 | Connected browser or app wallet (including SSO-login wallets). | User logs into your game and their wallet so they can connect the wallet. | 1. Logging into their wallet app and connecting it to your app. 2. Remembering their secret phrase, wallet password, and/or their SSO login password (e.g. for Google). 3. Approving certain transactions (e.g. minting an NFT or listing it for sale). | No | Yes | Web3 native, or Web3-curious | A true Web3 experience means players have full control over their assets, so you may want to eventually migrate your users to #3 (if they opt in). Or, you might decide to introduce Web3 elements while retaining the great UX that comes with embedded wallets - like an in-game marketplace that lets users freely buy, sell, and trade game assets with other players. **Decision to make** * Determine what kind of wallet(s) you want to support your initial use case. #1 or #2 in the table above are good starting points. You can read more about them in our high-level Wallet Services guide for [Aptos](/product-overviews/aptos/wallets#why-use-shinami-invisible-wallets), [Movement](/product-overviews/movement/wallets#why-use-shinami-invisible-wallets), and [Sui](/product-overviews/sui/wallets#why-use-an-embedded-wallet). ### When and how will you introduce the Web3 elements of your game to users? For a summary of these considerations, see the ["How to add Web3 elements to your Web2 game?"](/developer-guides/learning-about-web-3/adding-on-chain-elements-to-your-game#introduce-it-slowly) section above. Here, we give an image showing what was discussed above, along with a comparison for the connected wallet case. **Example timeline for introducing Web3 elements** As you can see, for the common embedded-wallet case, you don't even have to tell the user they have a wallet that stores something until they've reached a certain milestone - like playing for 15 hours or beating level 8. In the case of a connected wallet, you'll likely want to tell players right away when they can expect to earn something and what they'll earn (because these are Web3 natives who are expecting to own an asset or token for investing time in your game). **Decision to make** * When and how will you introduce the Web3 elements of your game to users ### What blockchain will you build on? Shinami works with Move blockchains because of the powerful combination of low latency, high-throughput, cheap transaction fees, and a secure smart contract programming language. We have guides that cover the specific benefits of connecting your game to the [Aptos](/product-overviews/aptos/gaming) or [Sui](/product-overviews/sui/gaming) blockchain. **Decision to make** * Choose a blockchain to integrate with. ## High-level overview of interactions Below is a high-level overview of the key interactions that could take place. It's not meant as a detailed, technical integration guide, but as a way for you to get an initial sense of the overall picture so you can learn what you may need to look into further. Summary of some of the apps and services above: 1. **User-connected wallet.** Most Web2 games adding Web3 are likely to not have this, at least at first. It's for the case when Web3-knowledgeable users have full control over their assets, with the extra user-effort that comes with that. Examples include [Petra](https://petra.app/) for the Aptos blockchain and [Slush Wallet](https://suiwallet.com/) for the Sui blockchain. 2. **Asset resource server.** Not all the data associated with an in-game asset lives on chain. Some, like logic involving game mechanics or all the audio dialogue for a character, will live in your private databases. Others, like an image for the character, needs to be at a publicly-accessible endpoint. This is so that other apps can read from it, including: user-connected wallets, blockchain explorers, and NFT marketplaces. For examples, see our guide summarizing how NFTs work on [Aptos](/developer-guides/aptos/move-guides/nfts#where-the-data-lives) or [Sui](/developer-guides/sui/move-guides/nfts#where-the-data-lives). 3. **Web3 infrastructure provider.** You'll need the following pieces of infrastructure 1. | Service | What it does | High-level guide to learn more about the services and what Shinami offers | | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | | Node | Reads from and writes to a blockchain. This is how you mint NFTs or in-game tokens and transfer them to players wallets. | N/A | | Gas Station | Facilitates easy sponsorship of user wallet transactions. Important because for an embedded wallet a user will have no crypto in it and may not even know it exists at first. | [Aptos](/product-overviews/aptos/gas-station) - [Movement](/product-overviews/movement/gas-station) - [Sui](/product-overviews/sui/gas-station) | | Wallet Service | Helps you create and manage embedded user wallets. | [Aptos](/product-overviews/aptos/wallets) - [Movement](/product-overviews/movement/wallets) - [Sui](/product-overviews/sui/wallets) | 4. **Other Web3 Apps.** These are common apps that may interact with your on-chain data. Examples:
1. | App type | What it does | App examples | | :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | | Blockchain Explorer | Lets players explore what they own on a blockchain. | [SuiVision (showing a Shinami game demo NFT)](https://testnet.suivision.xyz/object/0x2fa4e8cac7dc7cc35a443ab30f426cb2dabe8b85c0dbda73a6b102e05caee58a) | | NFT Marketplace | Allows players to to buy or sell NFTs. Only relevant if you enable this for players via embedded wallets or let them connect their own wallet. This functionality could be replaced by an in-game marketplace you run. | [Tradeport](https://www.tradeport.xyz/?tab=trending) | | Crypto Exchange | Lets people buy or sell a token, or trade one kind of token for another. Only relevant if you have an in-game token and you allow it to be sold on exchanges. | [Bluefin](https://bluefin.io/), [Panora](https://panora.exchange/) | ## Next steps Here are a few good options: * Read about how [Playground](https://www.shinami.com/case-studies/playground) and [BlueJay Games](https://www.shinami.com/case-studies/bluejay) use Shinami + Web3 elements to drive user engagement. * Read about the benefits and details of connecting your game to the [Aptos](/product-overviews/aptos/gaming) or [Sui](/product-overviews/sui/gaming) blockchain. * Reach out to us at `support@shinami.com` if you have any questions! # Key Resources Source: https://docs.shinami.com/developer-guides/movement/move-guides/key-resources Docs and dev tools for Move development on Movement ## New to Move development on Movement? Look here. ### Sample code to get you started * For tutorials on how to make your first transaction/NFT/Move Module/etc, using the Aptos SDK (which Movement uses since Movement uses Aptos Move) see [here](https://docs.movementnetwork.xyz/devs) . ### Aptos Move language guides * For a full guide to the language, see [Learn the Move Language](https://aptos.dev/move/move-on-aptos). * Tips for [writing secure Move code](https://aptos.dev/en/build/smart-contracts/move-security-guidelines#object-ownership-check). ### SDKs To make working with Movement and Shinami easier, try these SDKs: * [Aptos SDKs](https://docs.movementnetwork.xyz/devs/interactonchain/tsSdk) for TypeScript, Python, and Rust, with guidance on how to configure to point to Movement Node endpoints. ### Movement CLI * See the [Movement CLI guide](https://docs.movementnetwork.xyz/devs/movementcli) for how to set up and use the CLI. ### Help with development questions For help and tips from fellow Movement developers, see: * [Movement developer ](https://forums.movementnetwork.xyz/)forums. ## Creating production code? Look here. ### Movement source code and other apps * See the forked [aptos-core](https://github.com/movementlabsxyz/aptos-core) repo on GitHub. * Find [Ecosystem Projects](https://www.movementnetwork.xyz/ecosystem) on the Movement Network Foundation site to see a big list of projects on Movement. ### Testing your code ### Unit tests * For guidance on writing and running unit tests for your Move modules, see the [Unit Tests section](https://aptos.dev/move/book/unit-testing) of The Move Book. ### Move Prover * For guidance on how to install and use the Move Prover, see [here](https://aptos.dev/build/smart-contracts/prover/prover-guide#running-the-move-prover) . ## Stay up to date on the Aptos Move ecosystem * [Movement Labs on X](https://x.com/movementlabsxyz) for ecosystem announcements. * [Move Builders Telegram channel](https://t.me/+430YefZ9s65iODQx) for converstations between fellow builders and members of the Movement team. * [Movement Telegram Channel](https://t.me/movementlabsxyz) for the latest official news and announcements. * [Movement Labs blog](https://blog.movementlabs.xyz/) for longer-form technical content and discussions about the Movement ecosystem. # Gas Station: backend-only Source: https://docs.shinami.com/developer-guides/movement/tutorials/gas-station-backend-only Full sample code for building, sponsoring, signing, and submitting a sponsored transaction. ## Overview In this tutorial you'll learn how to sponsor transactions with Shinami's Gas Station. For more on the benefits of sponsoring transactions and to see an image of an end-to-end sponsorship flow, see our [Gas Station high-level guide](/product-overviews/movement/gas-station) The full code for this TypeScript tutorial is available on [GitHub](https://github.com/shinamicorp/shinami-examples). Movement is a [fork of Aptos](https://github.com/movementlabsxyz/aptos-core), so developing for both of these chains is very similar. If you're getting started with Movement, they have some [developer docs](https://docs.movementnetwork.xyz/general) to help. **Orderless transactions:** We currently don't support sponsoring [orderless transactions](https://aptos.dev/en/build/sdks/ts-sdk/building-transactions/orderless-transactions) but plan to add this support in the future. If you need this support, please reach out to us at `support@shinami.com`. ### Requests must be from your BE Our Gas Station does not support CORS, so if you attempt to make requests to it from the FE you'll get a CORS error. We do this because exposing a Gas Station API key on the FE is a security risk - an attacker could drain the MOVE in your Gas Station fund associated with the key by using it to sponsor transactions. For an overview of ways to integrate frontend signing with backend sponsorship, see our [Frontend signing + backend sponsorship tutorial](/developer-guides/movement/tutorials/gas-station-with-frontend-signing). ### 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 Movement Gas Station](/developer-guides/core-integration-topics/error-reference#movement-gas-station-api) as well as the "General Error Codes" section at the top that apply to all Shinami services. ## Usage and Billing FAQ Make sure to check out the Movement Gas Station [product usage FAQ](/help-center/movement/gas-station-faq) and [billing FAQ](/help-center/billing/movement-faq#gas-station) in our Help Center for answers to common questions. ## Required setup **Notes:** * This tutorial requires a Shinami account. You can sign up for one [here](https://app.shinami.com/signup). * If you have questions not covered by the tutorial you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet You'll need a Testnet Gas Station fund in order to sponsor transactions. We have guidance on creating one in the [Movement Gas Station page of our Help Center](/help-center/movement/gas-station-faq#how-do-i-create-a-fund%3F). When you make a Testnet fund we deposit some MOVE in it so you can start testing immediately. ### 2. Clone the github repo and install dependencies Clone the [shinami-examples github repo](https://github.com/shinamicorp/shinami-examples/tree/main), cd into the `shinami-examples/movement/typescript/backend_examples` directory, and run `npm install`to install the dependencies for running the code. If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) . Below, we'll be using the `gas_station.ts` file in the `shinami-examples/movement/typescript/backend_examples/src` directory. ### 3. Create an API access key and copy it into the file. **Create an access key with Testnet Node Service and Gas Station rights** You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key that has `Gas Station` rights for Testnet transaction sponsorship. See our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#create-an-access-key) for info on how to create a key. Make sure to link it to the fund you created in step 1. **Add it to the gas\_station.ts file** Once you have your key, use it as the value for the `TESTNET_MOVEMENT_GAS_STATION_API_KEY` constant. You'll see that we immediately use it to instantiate a Gas Station client (for sponsoring transactions). ```bash TypeScript theme={null} const TESTNET_MOVEMENT_GAS_STATION_API_KEY = "TESTNET_MOVEMENT_GAS_STATION_API_KEY"; const gasStationClient = new GasStationClient(TESTNET_MOVEMENT_GAS_STATION_API_KEY); ``` ### 4: Open your Shinami dashboard Technically not required, but we recommend visiting the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami dashboard. After running the code examples, take a look at this page. It may take a moment, but you'll see the digests of transactions you sponsor that are committed to the blockchain appear as in the image below. This tab, and the "In flight transactions" tab (for sponsored transactions that haven't yet been committed to the Movement blockchain) can be helpful when testing. ## Code examples ### Overview Below, we'll review each of our sample code functions and how to run them. At a high-level, you'll uncomment just one sample code function - e.g. `sponsorTransactionSimple()` in the code block below. Then, save the change, Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile, and run `node build/gas_station.js` to run. This will work as downloaded for the simple transaction examples, but the multi-agent ones require compiling a Move script (explained in the Appendix). ```bash TypeScript expandable theme={null} const committedTransaction = await sponsorTransactionSimple(); // sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // sponsorAndSubmitSignedTransactionSimple(); // sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); // Wait for the transaction to move past the pending state if (committedTransaction) { console.log("Polling for tx hash: ", committedTransaction.hash); const executedTransaction = await movementClient.waitForTransaction({ transactionHash: committedTransaction.hash }); console.log("Transaction status:", executedTransaction.vm_status); } else { console.log("There was an issue with building and submitting the transaction."); } ``` Each of the sample code functions returns a `Promise`. If our function was successful, we wait for the transaction to be committed to the blockchain and then print out the hash and status. Example: ``` Polling for tx hash: 0x0ef95fed63f9ca8d8ce4b1b1676511cfb5a61cd97b3e43b47cdc3224788f4346 Transaction status: Executed successfully ``` You can look up the digest in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). This is an image showing that [my transaction](https://explorer.movementnetwork.xyz/txn/0x0ef95fed63f9ca8d8ce4b1b1676511cfb5a61cd97b3e43b47cdc3224788f4346?network=bardock+testnet) had both a sender and a fee payer signature (your sender and feePayer addresses will be different): ### Sponsor a simple transaction **Understand the code** The `sponsorTransactionSimple` function performs all the steps needed to build, sponsor, sign, and submit a simple transaction: Creates an Account to use as the sender. Builds the transaction (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-simpletransaction-for-sponsorship)). Sponsors the transaction with a call to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the `console.log()` statement below. Signs the transaction as the sender. Submits the transaction with the sender and feePayer signatures and returns the result. ```bash TypeScript expandable theme={null} async function sponsorTransactionSimple(): Promise { // 1. Set up our sender. const sender = await generateSingleKeyAccountEd25519(); // 2. Build a simple transaction. let transaction = await buildSimpleMoveCallTransaction(sender.accountAddress); // 3. Sponsor the transaction with Shinami Gas Station. let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // Note that the SDK updates the transaction's feePayer address on a successful sponsorship console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress); // 4. Generate the sender's signature. const senderAuthenticator = movementClient.transaction.sign({ signer: sender, transaction: transaction }); // 5. Submit the transaction with the sender and fee payer signatures return await movementClient.transaction.submit.simple({ transaction, senderAuthenticator, feePayerAuthenticator: feePayerAuthenticator, }); } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await sponsorTransactionSimple(); // sponsorTransactionMultiAgent(); // sponsorAndSubmitSignedTransactionSimple(); // sponsorAndSubmitSignedTransactionMultiAgent(); // checkFundBalanceAndDepositIfNeeded(); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). ### Sponsor a multi-agent transaction **Understand the code** The `sponsorTransactionMultiAgent` function performs all the steps needed to build, sponsor, sign, and submit a multi-agent Move script transaction. Before you run it, you'll need to generate two funded Accounts that will swap Octa (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Builds the transaction (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script)). Sponsors the transaction with a request to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the `console.log()` statement below. Generates the sender and secondary signer signatures. Submits the transaction with the sender, secondary signer, and feePayer signatures and returns the result. ```bash TypeScript expandable theme={null} async function sponsorTransactionMultiAgent( fundedSenderAccount: SingleKeyAccount, fundedSecondarySigner: SingleKeyAccount): Promise { // 1. Build a multiAgent transaction let transaction = await buildMultiAgentScriptTransaction( fundedSenderAccount.accountAddress, fundedSecondarySigner.accountAddress); // 2. Sponsor the transaction with Shinami Gas Station let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction); // Note that the SDK updates the transaction's feePayer address on a successful sponsorship console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress); // 3. Generate the sender and secondary signer signatures const senderAuthenticator = movementClient.transaction.sign({ signer: fundedSenderAccount, transaction }); const secondarySignerAuthenticator = movementClient.transaction.sign({ signer: fundedSecondarySigner, transaction }); // 4. Submit the transaction with the sender, seconardy signer, and feePayer signatures return await movementClient.transaction.submit.multiAgent({ transaction, senderAuthenticator, additionalSignersAuthenticators: [secondarySignerAuthenticator], feePayerAuthenticator: feePayerAuthenticator }); } ``` **Update, save, compile, run** In order to run this example, you'll need to compile the Move script used as shown in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script). Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); //sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); //checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). ## Sponsor and submit a signed, simple transaction **Understand the code** The `sponsorAndSubmitSignedTransactionSimple` function performs all the steps needed to build and sign a simple transaction, and then send it to Gas Station for sponsorship and submission to the Movement blockchain: Creates an Account to use as the sender. Builds the transaction (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-simpletransaction-for-sponsorship)). Generates the sender's signature. Makes a request to Gas Station to sponsor and submit the signed transaction, returning the result. ```bash TypeScript expandable theme={null} async function sponsorAndSubmitSignedTransactionSimple(): Promise { // 1. Set up our sender. const sender = await generateSingleKeyAccountEd25519(); // 2. Build a simple transaction. const transaction = await buildSimpleMoveCallTransaction(sender.accountAddress); // 3. Generate the sender's signature. const senderAuthenticator = movementClient.transaction.sign({ signer: sender, transaction }); // 4. Ask Shinami to sponsor and submit the transaction return await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator ); } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); //sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); //checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). ## Sponsor and submit a multi-agent transaction **Understand the code** The `sponsorAndSubmitSignedTransactionMultiAgent` function performs all the steps needed to build and sign a multi-agent Move script transaction, and then send it to Gas Station for sponsorship and submission to the Movement blockchain. Before you run it, you'll need to generate two, funded Accounts that will swap Octa if you haven't already done so (see how it's done in the [Appendix](/developer-guides/aptos/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Builds the transaction (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script)). Generates the sender and secondary signer signatures. Makes a request to Gas Station to sponsor and submit the signed transaction, returning the result. ```bash TypeScript expandable theme={null} async function sponsorAndSubmitSignedTransactionMultiAgent( fundedSenderAccount:SingleKeyAccount, fundedSecondarySigner: SingleKeyAccount): Promise { // 1. Build a multiAgent transaction let transaction = await buildMultiAgentScriptTransaction( fundedSenderAccount.accountAddress, fundedSecondarySigner.accountAddress); // 2. Generate the sender and secondary signer signatures const senderAuthenticator = movementClient.transaction.sign({ signer: fundedSenderAccount, transaction }); const secondarySignerAuthenticator = movementClient.transaction.sign({ signer: fundedSecondarySigner, transaction }); // 3. Ask Shinami to sponsor and submit the transaction return await gasStationClient.sponsorAndSubmitSignedTransaction( transaction, senderAuthenticator, [secondarySignerAuthenticator] ); } ``` **Update, save, compile, run** In order to run this example, you'll need to compile the Move script used as shown in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#build-a-multiagenttransaction-with-a-compiled-move-script). Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await // sponsorTransactionSimple(); // sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // sponsorAndSubmitSignedTransactionSimple(); sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); // checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). ## Check your fund balance and make a deposit **Understand the code** The `checkFundBalanceAndDepositIfNeeded` function does... well, exactly what it says it does 😄. Before you run it, you'll need to generate a funded Account if you haven't already done so (see how it's done in the [Appendix](/developer-guides/movement/tutorials/gas-station-backend-only#generating-funded-accounts-for-testing)). The function: Makes a call to [`gas_getFund`](/api-docs/movement/gas-station/api#gas-getfund) to obtain your fund's balance. Checks if the amount that's available to use is less than the minimum amount you want. 1. If true, it: 1. creates the transaction to transfer MOVE to your fund address 2. obtains the sender's signature (from the funded Account you made) 3. submits the transaction and returns the result. 2. Else, it returns `undefined`. ```bash TypeScript expandable theme={null} async function checkFundBalanceAndDepositIfNeeded(fundedSenderAccount: SingleKeyAccount): Promise { const MIN_FUND_BALANCE_OCTA = 1_000_000_000; // 10 MOVE const { balance, inFlight, depositAddress } = await gasStationClient.getFund(); console.log("Fund balance not in use (in Move):", (balance - inFlight) / 100_000_000); // You may want to deposit more than 100_000_000 Octa (1 MOVE), and what you want may be dynamic // based on the current (balance - inFlight) amount, etc. This is just a simple example. const STANDARD_DEPOSIT_AMOUNT = 100_000_000; // Deposit address can be null - see our Help Center for how to generate an address: // https://docs.shinami.com/help-center/movement/gas-station-faq#how-do-i-generate-and-find-the-deposit-address-of-a-fund if (depositAddress && ((balance - inFlight) < MIN_FUND_BALANCE_OCTA)) { // Create a SimpleTransaction that transfers MOVE from the sender to your Gas Station fund const transferTx = await movementClient.transferCoinTransaction({ sender: fundedSenderAccount.accountAddress, recipient: depositAddress, amount: STANDARD_DEPOSIT_AMOUNT }); // Obtain the sender's signature const senderAuth = fundedSenderAccount.signTransactionWithAuthenticator(transferTx); // Submit the transaction return await movementClient.transaction.submit.simple({ transaction: transferTx, senderAuthenticator: senderAuth }); } console.log("No deposit because no deposit address or a balance above the minimum you've set."); return undefined; } ``` **Update, save, compile, run** Make sure the function is the only sample code function uncommented: ```bash TypeScript theme={null} // // -- Choose which sample code function to use to generate a PendingTransactionResponse // // const committedTransaction = await //sponsorTransactionSimple(); //sponsorTransactionMultiAgent(fundedSenderAccount, fundedSecondarySignerAccount); //sponsorAndSubmitSignedTransactionSimple(); //sponsorAndSubmitSignedTransactionMultiAgent(fundedSenderAccount,fundedSecondarySignerAccount); checkFundBalanceAndDepositIfNeeded(fundedSenderAccount); ``` Save the file if you made a change. Run `tsc` in the `shinami-examples/movement/typescript/backend_examples` directory to transpile. Then, run `node build/gas_station.js`. If successful, you can view the sponsorship on the ["Completed transactions" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) of your Shinami Dashboard. You can also look up the transaction digest printed to the console in the [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (make sure the explorer is set to Testnet). ## Appendix ### Build a SimpleTransaction for sponsorship The below function creates a transaction that calls a function on a Move module we've [deployed to Testnet](https://explorer.movementnetwork.xyz/account/0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486/modules/packages/hello_blockchain?network=bardock+testnet) (it's from a [Movement tutorial](https://docs.movementnetwork.xyz/devs/firstMoveContract)). The `set_message` function allows the caller to store a message at their address inside a `MessageHolder` struct. 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it. ```bash TypeScript theme={null} async function buildSimpleMoveCallTransaction(sender: AccountAddress, expirationSeconds?: number): Promise { let transaction = await movementClient.transaction.build.simple({ sender: sender, withFeePayer: true, data: { function: "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", functionArguments: ["hello"] }, options: { expireTimestamp: expirationSeconds, maxGasAmount: 20000 } }); console.log("\nResponse from movementClient.transaction.build.simple()"); console.log(transaction); return transaction; } ``` ### Build a MultiAgentTransaction with a compiled Move script **Step 1: Building a MultiAgentTransaction for sponsorship.** For a multi-agent transaction, you'll need to provide the secondary signers when building the transaction (in addition to the sender). 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 embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it. This function reads a complied Move script inside your local directory and builds a MultiAgentTransaction with it. You'll need to compile the script to use this function (see "Step 2" below). ```bash TypeScript expandable theme={null} async function buildMultiAgentScriptTransaction( sender: AccountAddress, secondarySigner: AccountAddress, expirationSeconds?: number): Promise { let buffer = readFileSync("./move/build/test/bytecode_scripts/unfair_swap_coins.mv"); let bytecode = Uint8Array.from(buffer); let transaction = await movementClient.transaction.build.multiAgent({ sender: sender, secondarySignerAddresses: [secondarySigner], withFeePayer: true, data: { bytecode: bytecode, functionArguments: [] }, options: { expireTimestamp: expirationSeconds, maxGasAmount: 20000 } }); console.log("\nResponse from movementClient.transaction.build.multiAgent()"); console.log(transaction); return transaction; } ``` **Step 2: Aptos Move** **Move script** The script we'll use is located at `shinami-examples/movement/typescript/backend_examples/move/sources/unfair_swap_coins.move` ```bash TypeScript theme={null} script { use aptos_framework::aptos_coin; use aptos_framework::coin; use aptos_framework::signer; fun unfair_swap_coins( sender: &signer, secondary: &signer ) { let coin_first = coin::withdraw(sender, 100); let coin_second = coin::withdraw(secondary, 200); coin::deposit(signer::address_of(secondary), coin_first); coin::deposit(signer::address_of(sender), coin_second); } } ``` **Build the Move script** For this, you'll need to install the [Movement CLI](https://docs.movementnetwork.xyz/devs/movementcli). Once you've done so, run `cd move` to get to the root of the `shinami-examples/movement/typescript/backend_examples/move` directory. Then, run `movement move compile`. You should see a `build/test/bytecode_scripts/unfair_swap_coins.mv`. That's the compiled, bytecode version of the below Move script. This Move script has the sender give the secondary signer 100 Octa in exchange for 200 Octa - not fair! For more guidance on how to run a multi-agent transaction see [here](https://github.com/aptos-labs/developer-docs/blob/fde289de3d02368d3fe878a3e5cd9c725a22a376/apps/nextra/pages/en/build/sdks/ts-sdk/building-transactions/multi-agent-transactions.mdx#L196), and for guidance on running Move scripts see [here](https://aptos.dev/build/smart-contracts/scriptsrunning-scripts/). ### Generating funded accounts for testing For the purposes of reusing the same account for repeated tests with this tutorial - which is required when you use one of the functions that takes a funded account as an argument - you can find and reuse a SingleKeyAccount's private key as shown in the below code (which is near the top of the file). This is just provided for easy testing. Your app should determine the best way to manage any keys it controls, and you should always protect your private keys and not share them. There are three steps, as outlined in the code: Uncomment out step 1 (you may want to also comment out other code below that would run, but it's not necessary). Then, Save changes the file, transpile with `tsc`, and run with `node build/gas_station.js`. Look at your console for the two addresses printed out. Go to [https://faucet.movementnetwork.xyz/](https://faucet.movementnetwork.xyz/) and copying in each address to give it Testnet MOVE. You may need to refresh the page after funding the first address. Note that the faucet currently has a limit of a few requests per day. Comment out step 1. Look at your console for the two private keys printed out. Uncomment step 3 and set one as the value for `PKEY_ONE` and one as the value for `PKEY_TWO`. Save changes to the file. Now, the next time you transpile and run the code, you'll be able to use these two funded accounts ```bash TypeScript expandable theme={null} // **** // Code for generating two reusable, funded accounts for testing purposes // Step 1: uncomment the next six lines. Save the file, transpile with tsc, and run with node build/gas_station.js const accountOne = await generateSingleKeyAccountEd25519(); console.log("Address 1: ", accountOne.accountAddress.toString()); console.log("Private key 1: ", PrivateKey.formatPrivateKey(Buffer.from(accountOne.privateKey.toUint8Array()).toString('hex'), PrivateKeyVariants.Ed25519)); const accountTwo = await generateSingleKeyAccountEd25519(); console.log("Address 2: ", accountTwo.accountAddress.toString()); console.log("Private key 2: ", PrivateKey.formatPrivateKey(Buffer.from(accountTwo.privateKey.toUint8Array()).toString('hex'), PrivateKeyVariants.Ed25519)); // End step 1 // Step 2: visit the Movement Testnet faucet page at https://faucet.movementnetwork.xyz/ and // request MOVE for each of the two addresses that were printed to the console from step 1. // Step 3: // a. Comment out the four lines from run 1. // b. Uncomment the eight code lines below. // c. Set the values of `PKEY_ONE` and `PKEY_TWO` to the private key values printed to the console in Step 1. // d. Save the file, transpile with tsc, and run with node build/gas_station.js // const PKEY_ONE = "ed25519-priv-0x..."; // const fundedSenderAccount = new SingleKeyAccount({ // privateKey: new Ed25519PrivateKey(PKEY_ONE) // }); // const PKEY_TWO = "ed25519-priv-0x..."; // const fundedSecondarySignerAccount = new SingleKeyAccount({ // privateKey: new Ed25519PrivateKey(PKEY_TWO) // }); // End step 3 // **** ``` ### Sponsoring a transaction for a non-funded account As our examples above show, you can sponsor a transaction for an Account that has not yet been funded. The transaction fee for an account's first transaction will cost more than the fee for the same transaction at the same time for a funded account. Example fee statements are below, but your results may vary. Example `0x1::transaction_fee::FeeStatement` for a `message::setmessage` Testnet Move call sponsored transaction from an unfunded account: ``` { execution_gas_units:"4" io_gas_units:"1" storage_fee_octas:"92840" storage_fee_refund_octas:"0" total_charge_gas_units:"933" } ``` Example `0x1::transaction_fee::FeeStatement` for a `message::setmessage` Testnet Move call sponsored transaction from a funded account: ``` { execution_gas_units:"3" io_gas_units:"1" storage_fee_octas:"43680" storage_fee_refund_octas:"0" total_charge_gas_units:"441" } ``` ### Tips for setting your sponsorship budget See the Aptos doc on [Gas and Storage Fees](https://aptos.dev/concepts/gas-txn-fee/) for a more detailed overview of how transaction costs are determined, as well as guidance on how to estimate the costs for a transaction. See the [Movmement Gas Station tab on the Billing page](https://app.shinami.com/billing/#movement_gas) of your dashboard to see how Shinami charges for sponsorships. Note: only workspace admins can view and change billing information. # Gas Station: how to integrate frontend transaction signing Source: https://docs.shinami.com/developer-guides/movement/tutorials/gas-station-with-frontend-signing How to integrate Gas Station transaction sponsorship with signing from a connected browser 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 API 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 will help you integrate a connected browser 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](/developer-guides/movement/tutorials/gas-station-backend-only). It's useful to run the intial example to understand sponsorship before you connect the frontend, and the code repo you clone includes the code for this tutorial as well. Finally, after you review the images and code below, if you get stuck on something and have a question feel free to [reach out to us](/help-center/overview#contacting-support). ## Examples ### Sample app overview We've created a sample app that uses the [`Aptos Labs wallet-adapter-react library`](https://github.com/aptos-labs/aptos-wallet-adapter) and the [Shinami TypeScript SDK](https://github.com/shinamicorp/shinami-typescript-sdk). It integrates well with [Nightly Chrome extension wallet](https://chromewebstore.google.com/detail/nightly/fiikommddbeccaoicoejoniammnalkfa) out of the box, and [Razor Chrome extension wallet](https://chromewebstore.google.com/detail/razor-wallet/fdcnegogpncmfejlfnffnofpngdiejii) if you follow the README step to enable it. This app is included it in our `shinamicorp/shinami-examples` repo [here](https://github.com/shinamicorp/shinami-examples/tree/main/movement/typescript/wallet_adapter_react). 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. ### Consider obtaining the user signature pre-sponsorship You should think about whether building the transaction on the frontend or backend is a better fit for your app. The same goes for where to submit the transaction for execution. All combinations work, as long as you ask for sponsorship from your backend. In general, it's beneficial to have the user sign the transaction *before* you ask for a sponsorship. This is because if you ask for a sponsorship and then the user fails to sign, you'll still pay [our small, minimum fee](https://app.shinami.com/billing/#movement_gas) for creating a sponsorship that goes unused. This means that the user will sign against the `0x0` placeholder feePayer address generated when you build a feePayer transaction, and then the transaction you submit will need to have the actual feepayer's address set (done automatically by our TypeScript SDK). ### 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 a Movement node. Signing is done with a connected browser wallet. **Image** Movement-Connected-Wallet-BE-sponsor-and-submit-tx **Overview of steps** The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on [GitHub](https://github.com/shinamicorp/shinami-examples/tree/main/movement/typescript/wallet_adapter_react). The key functions in the sample app are [`connectedWalletTxFEBuildBESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/client/App.tsx#L197) on the frontend and [`sponsorAndSubmitTx`](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/server/main.ts#L154) on the backend. 1. *Frontend:* Build a feePayer transaction. 2. *Frontend:* Obtain the sender's signature over the transaction. 3. *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. 4. *Backend:* Send a [`gas_sponsorAndSubmitSignedTransaction`](/api-docs/movement/gas-station/api#gas_sponsorandsubmitsignedtransaction) request to Shinami's Gas Station (with the de-serialized transaction and sender AccountAuthenticator). 5. *Shinami*: Shinami will sponsor the transaction and then submit it with the sender and feePayer signatures, returning the `PendingTransactionResponse` to your backend if successful. 6. *Backend:* Handle the response as needed. 7. *Backend:* Send a response to the frontend. 8. *Frontend:* Handle the response (this is the response to the request in Step 3). In our sample app, we poll a Movement node until it has a record of the transaction and then print the user's message that was included in the transaction. ### 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. **Image** Movement-Connected-Wallet-BE-build-and-sponsor-FE-submit-tx **Overview of steps** The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on [GitHub](https://github.com/shinamicorp/shinami-examples/tree/main/movement/typescript/wallet_adapter_react). The key functions in the sample app are [`connectedWalletTxBEBuildFESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/client/App.tsx#L105) on the FE and [`buildAndSponsorTx`](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/server/main.ts#L108) on the BE. 1. *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. 2. *Backend:* Build a feePayer transaction. 3. *Backend:* Make a [`gas_sponsorTransaction`](/api-docs/movement/gas-station/api#gas_sponsortransaction) request to Shinami's Gas Station to sponsor the transaction. 4. *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 special `0x0` 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 Movement blockchain. If not, you'll get an `INVALID_SIGNATURE` error because the feePayer's signature was over a transaction with the feePayer address but the submitted transaction still has the `0x0` address. 5. *Backend:* Return the serialized transaction, feePayer AccountAuthenticator, and (optionally) feePayer's address to the frontend. 6. *Frontend:* Deserialize the transaction and obtain the sender's signature over the transaction. 7. *Frontend:* Submit the transaction, along with the sender and (deserialized) feePayer signatures, to a Movement node. 8. *Frontend:* Handle the response (a `PendingTransactionResponse` if successful). In our sample app, we poll a Movement node until it has a record of the transaction and then print the user's message that was included in the transaction. ### Other flows in our sample app * [Build and sign on the FE, sponsor on the BE, submit on the FE.](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/client/App.tsx#L162) * [Build and sponsor on the BE, sign on the FE, submit on the BE.](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/client/App.tsx#L131) * [Embedded Shinami Invisible Wallet: Build, sponsor, and sign on the BE.](https://github.com/shinamicorp/shinami-examples/blob/main/movement/typescript/wallet_adapter_react/src/client/App.tsx#L238) ## Appendix ### Common issues #### INVALID\_SIGNATURE error when submitting the transaction Make sure the transaction you're submitting has been updated to use the feePayer's address (you must overwrite the `0x0` value that is intially set when a feePayer transaction is built). ### 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)** ```bash TypeScript theme={null} // AccountAuthenticator const serializedAccountAuthenticator = authenticator.bcsToHex().toString(); // AccountAddress const serializedAccountAddress = accountAddress.bcsToHex().toString(); // SimpleTransaction const serializedSimpleTransaction = simpleTx.bcsToHex().toString(); ``` **Deserialize on the other** ```bash TypeScript theme={null} 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())); ``` # Invisible Wallets: backend-only Source: https://docs.shinami.com/developer-guides/movement/tutorials/invisible-wallets How to integrate Shinami Invisible Wallets ## Overview In this tutorial, we show you how to use the [Shinami Invisible Wallet API](/api-docs/movement/wallet-services/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](/product-overviews/movement/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 MOVE 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 MOVE. 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 Movement Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#movement-invisible-wallet-api) as well as the "General Error Codes" section at the top that apply to all Shinami services. ### Usage and Billing FAQ Make sure to check out the Movment Wallet Services [product usage FAQ](/help-center/movement/wallet-services-faq) and [billing FAQ](/help-center/billing/movement-faq#wallet-services) in our Help Center for answers to common questions. ## Tutorial **Notes:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet You'll need a Testnet Gas Station fund with MOVE in it in order to sponsor transactions. We have guidance on creating one in the [Movement Gas Station page of our Help Center](/help-center/movement/gas-station-faq). When you make a Testnet fund we deposit some MOVE in it so you can start testing immediately. ### 2. Clone the github repo Clone the [shinami-examples github repo](https://github.com/shinamicorp/shinami-examples/tree/main), cd into the `shinami-examples/movement/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](https://www.typescriptlang.org/download/) ) . If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Below, we'll be using the `invisible_wallet.ts` file in the `shinami-examples/movement/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 them both. See [in our Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#create-an-access-key) for info on how to set up a key with rights to all services. Once you've created the key, enter it as the value for `ALL_SERVICES_TESTNET_ACCESS_KEY`. Replace all instances of `{{name}}` with the actual value for that name. ```TypeScript TypeScript theme={null} // 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](/api-docs/movement/wallet-services/invisible-wallet-api#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 Movement and 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. ```TypeScript Shinami TypeScript SDK expandable theme={null} // 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}}"; // 4a. Instantiate your Movement client const config = new AptosConfig({ network: Network.CUSTOM, fullnode: 'https://testnet.movementnetwork.xyz/v1', faucet: 'https://faucet.testnet.movementnetwork.xyz/', }); const movementClient = new Aptos(config); // 4b. Instantiate your Shinami clients 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 // will initailize an un-initialized wallet, so we do not need to pre-initialize // a wallet we create. const CREATE_WALLET_IF_NOT_FOUND = true; const INITIALIZE_ON_CHAIN = false; const walletAddress = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND, INITIALIZE_ON_CHAIN); console.log("Invisible wallet address: ", walletAddress.toString()); ``` **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 MOVE (your Gas Station fund sponsors a simple transaction which initializes 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 MOVE 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. ```TypeScript Shinami TypeScript SDK theme={null} // 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](https://explorer.movementnetwork.xyz/account/0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486/modules/packages/hello_blockchain?network=bardock+testnet) (it's from a [Movement Labs tutorial](https://docs.movementnetwork.xyz/devs/firstMoveContract)). 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. ```TypeScript TypeScript theme={null} async function simpleMoveCallTransaction(sender: AccountAddress, withFeePayer = true): Promise { return await movementClient.transaction.build.simple({ sender: sender, withFeePayer: withFeePayer, data: { function: "0xe56b2729723446cd0836a7d1273809491030ccf2ec9935d598bfdf0bffee4486::message::set_message", functionArguments: ["test_message"] }, options: { maxGasAmount: 20000 } }); } ``` ### 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 Movement blockchain. **Explore the code** ```TypeScript Shinami TypeScript SDK theme={null} // 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 movementClient.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/movement/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 [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (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](/developer-guides/movement/tutorials/invisible-wallets#sign,-sponsor,-and-submit-a-transaction-in-multiple-steps). By default, the code also runs the a function that signs a transaction and then verifies the signature, as [explained in the Appendix](/developer-guides/movement/tutorials/invisible-wallets#sign-a-transaction-and-verify-the-signature). ## 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. ```TypeScript theme={null} async function signSponsorAndSubmitTransactionInTwoSteps(walletSigner: ShinamiWalletSigner, transaction: SimpleTransaction): Promise { // 1. Generate the sender signature const senderSignature = await walletSigner.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 `movementClient.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: ```TypeScript theme={null} // 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/movement/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 [Movement Explorer](https://explorer.movementnetwork.xyz/?network=bardock+testnet) (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. ```TypeScript theme={null} async function signAndVerifyTransaction(walletSigner: ShinamiWalletSigner, transaction: SimpleTransaction): Promise { // 1. Generate the sender signature const accountAuthenticator = await walletSigner.signTransaction(transaction); // 2. Verify the signature. const signingMessage = movementClient.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: ```TypeScript theme={null} // 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/movement/typescript/backend_examples` directory, run `tsc` to compile the file. Finally, run `node build/invisible_wallet.ts` to run the code. # Developer Guides and Tutorials Source: https://docs.shinami.com/developer-guides/overview Tutorials to help you integrate our products and guides to help with Move development and core integration topics. ## Overview **Core Integration Guides** Error codes and resolution steps for all Shinami APIs. How to create and manage your API access keys and authenticate your requests. **Integration Tutorials** Gas Station and Wallet Services. Gas Station and Wallet Services. Gas Station and Wallet Services. **Guides to help with Move development** *** # Aptos ### Shinami Integration Tutorials Full, runnable sample code that creates a wallet and then builds, sponsors, and submits a transaction on its behalf. Full, runnable sample code that builds, sponsors, and submits a transaction. Gas Station requests must be from your backend (no CORS support). App example with frontend connected wallet signing. ### Move Developer Guides Some useful resources for Aptos Move smart contract development Answers to some key questions Aptos developers have. A summary of NFTs on Aptos with links to key resources. A summary of fungible tokens on Aptos with links to key resources. *** # Movement ### Shinami Integration Tutorials Gas Station requests must be from your backend (no CORS support). App example with frontend connected wallet signing. Full, runnable sample code that builds, sponsors, and submits a transaction. Full, runnable sample code that creates a wallet and then builds, sponsors, and submits a transaction on its behalf. ### Move Developer Guides Some useful resources for Movement smart contract development *** # Sui ### Shinami Integration Tutorials Full, runnable sample code that builds, sponsors, and submits a transaction. Gas Station requests must be from your backend (no CORS support). App example with frontend connected wallet signing. Full, runnable sample code that creates a wallet and then builds, sponsors, and submits a transaction on its behalf. How to build your first zkLogin app. Uses our Next.js zkLogin SDK and our app starter template. ### Move Developer Guides Some useful resources for Sui Move smart contract development An overview of the additional Node Service calls made by building a transaction pre-execution. A summary of NFTs on Sui with links to key resources. A summary of fungible tokens on Sui with links to key resources. # Fungible tokens Source: https://docs.shinami.com/developer-guides/sui/move-guides/fungible-tokens A summary of fungible tokens on Sui with links to key resources ## Overview This doc provides an overview of the different types of fungible tokens on the Sui blockchain. It's meant for both non-technical and technical audiences - giving an overview and then linking to more code examples and resources for developers who want to learn more. To learn about non-fungible tokens (NFTs) on Sui, see [our guide](/developer-guides/sui/move-guides/nfts). **Terms:** Often, the native currency of a chain is [referred to](https://www.coinbase.com/learn/crypto-basics/what-is-the-difference-between-a-coin-and-a-token) as a coin (e.g. SUI for the Sui blockchain), and currencies that live on that chain are referred to as tokens (e.g. the BLUE token we reference below). However, Sui's API and standards refer to some tokens under this definition as coins. Because of this and because of the fact that both types are fungible tokens - unlike NFTs, if you and I have 10 of them there's no difference between our 10 - we refer to all cases as fungible tokens. ## Types of tokens on Sui ### SUI The native currency of the Sui blockchain. Used to [pay the gas fees](https://docs.sui.io/concepts/tokenomics/gas-in-sui) for transactions and often used to pay for other things like NFTs - including this interesting-looking creature listed for 44 SUI: In addition to buying assets and paying for gas, you can [stake](https://docs.sui.io/concepts/tokenomics/staking-unstaking) your SUI with a validator, helping to secure the Sui network in exchange for staking rewards. Sui is a delegated proof-of-stake blockchain, meaning value in the form of staked SUI secures the network (unlike Bitcoin, which is proof of work, meaning value in the form of computing resources secures the network). Like the tokens of other blockchains, the price of SUI [can vary a lot over time](https://coinmarketcap.com/currencies/sui/). This variance means that gas and NFT prices can fluctuate day-to-day in fiat terms. Not all tokens on Sui have this variance, as you'll see in the next section. ### Stablecoins Stablecoins are tokens that are pegged to an underlying asset, often a fiat currency like the US dollar. They are an effort to put value on chain without the price fluctuations that tokens like SUI experience. This provides safer ways to perform certain financial transactions, combining the speed and low fees of the Sui blockchain with the relative stability of certain fiat currencies. For example, international remittance payments and a coffee shop taking payment for coffee both benefit from knowing that the value transmitted one day won't be worth, for example, 5% less the next day. Stablecoins can be [bridged](https://docs.sui.io/concepts/tokenomics/sui-bridging) from another chain (like [USDC](https://suivision.xyz/coin/0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN) bridged from the Ethereum blockchain) or native to Sui (like [native USDC](https://suivision.xyz/coin/0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC)). Native stablecoins are generally safer because they don't have the additional trust assumptions that a bridge has (you have to trust the code and the team behind the bridge, in addition to the team and the code behind the stable coin). ### Other tradable tokens Using the [Coin Standard](https://docs.sui.io/standards/coin), developers can make new fungible tokens on the Sui blockchain that can be traded on exchanges and used by wallets and apps in a fashion similar to SUI. It's fairly common for apps to have a token as a way to provide token holders with governance power to help shape the future of the app, as well as provide financial/reward incentives (for example Bluefin's [BLUE token](https://learn.bluefin.io/bluefin/other/blue-token), here on [Coinmarketcap](https://coinmarketcap.com/currencies/bluefin/)). In addition to tokens created by apps like Bluefin, there is also the realm of [memecoins](https://www.coingecko.com/en/categories/sui-meme). This is not a plug for certain tokens or financial advice - just examples of the types of fungible tokens you'll see. Apps like [Sui Coins](https://www.suicoins.com) have made it easy for non-developers to create tokens as well. You can see a list of all the coins on Sui [here](https://suivision.xyz/coins). Tokens that follow the Coin Standard can have the added restriction of an address deny list that lists the addresses that can't use the token in transactions. This would be to meet regulatory requirements, such as for a stablecoin. ### Closed-loop tokens Using the [Closed-Loop Token Standard](https://docs.sui.io/standards/closed-loop-token), you can make fungible tokens on the Sui blockchain that cannot be used as freely as SUI or Coin Standard tokens. For example, you can limit the applications and wallet addresses that can use the token, as well as set custom rules for transfers, spends, and conversions. One example is a currency for a game that is only to be used within said game (and not bought and sold on external exchanges, where non-players can manipulate your in-game economy and throw it off balance). Another example is an app that rewards customers with loyalty tokens but only wants the tokens to be used by certain services and accounts. Finally, an app that requires KYC checks for legal purposes could restrict the use of tokens to accounts that have completed those checks. You can read more examples in [this blog post](https://blog.sui.io/closed-loop-tokens-explained/). ## Creating and using fungible tokens ### Open-Loop Coin Standard As [described by](https://docs.sui.io/standards/coin) the Sui Foundation, "The Coin standard is the technical standard used for smart contracts on Sui for creating coins on the Sui blockchain. The standardization of coin creation on Sui means that wallets, exchanges, and other smart contracts can manage coins created on Sui the same as they manage SUI, without any additional processing logic." You can also create regulated coins (like stablecoins) that have a deny list of addresses that can't use them, and even a [global on/off switch](https://docs.sui.io/standards/coin#global-pause-switch) that can prevent everyone from using them until you turn the switch back to "on"! Here is a very simple example (modified from a Sui Foundation example). We're minting the best memecoin ever! ```bash Sui Move expandable theme={null} module coin::coin; use sui::coin::{Self, TreasuryCap}; use sui::url; public struct COIN has drop {} fun init(witness: COIN, ctx: &mut TxContext) { let (treasury, metadata) = coin::create_currency( witness, 6, b"BMEME", b"Best Meme", b"This is the best meme coin ever!", option::some(url::new_unsafe_from_bytes(b"https://www.pexels.com/photo/close-up-photo-of-dog-wearing-sunglasses-1629781/")), ctx, ); transfer::public_freeze_object(metadata); transfer::public_transfer(treasury, ctx.sender()) } public fun mint( treasury_cap: &mut TreasuryCap, amount: u64, recipient: address, ctx: &mut TxContext, ) { let coin = coin::mint(treasury_cap, amount, ctx); transfer::public_transfer(coin, recipient) } ``` After publishing this to testnet, we were able to make a mint call via the CLI to mint coins to an address. ```bash Sui CLI theme={null} sui client call --package 0x50d3049f4a36fe6ca263e29dbc75b441378487fb2c955ae4b15b7bb50efcea36 --module coin --function mint --args 0xe9ba0ad7186394948c259da1a6a29e5b406c7e988c8cf65cabc480a2eabad523 1000 0xeb583665fb5a35f488234bd8715be86bab0f8ba5311d95ee5a8f67b6088ca2b0 ``` Here is the [transaction](https://testnet.suivision.xyz/txblock/H6BtcmgvHzNtwvgN1Xxc3FQfXJ4Q8Snz5VTKB8iJb5dx?tab=Changes) showing the recipient getting 10.0 `BMEME` (or, Best Meme) coin. ### Closed-Loop Token Standard The [Closed-Loop Token Standard](https://docs.sui.io/standards/closed-loop-token) lets you limit the applications that can use the token (say, just your app, or your app and a partner app). Closed-loop tokens don't allow transfers, conversions, and spends by default. You can set up custom rules that allow some of these actions under certain conditions based on your needs. This is useful if, say, you have an in-game economy or a loyalty rewards token that you don't want to be openly sold on exchanges in an unrestricted fashion. As an example, this Sui Foundation [sample project](https://docs.sui.io/guides/developer/coin/loyalty) creates a token with a rule that it can only be used at this module's gift shop. In the `buy_a_gift` method, [approval](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/sui/token.md#function-add_approval) is given for the spend. Key parts of the sample project are shown below: ```js Sui Move expandable theme={null} /// The OTW for the Token / Coin. public struct LOYALTY has drop {} /// This is the Rule requirement for the `GiftShop`. public struct GiftShop has drop {} public struct Gift has key, store { id: UID, } // Create a new LOYALTY currency, create a `TokenPolicy` for it. fun init(otw: LOYALTY, ctx: &mut TxContext) { let (treasury_cap, coin_metadata) = coin::create_currency( otw, 0, // no decimals b"LOY", // symbol b"Loyalty Token", // name b"Token for Loyalty", // description option::none(), // url ctx, ); let (mut policy, policy_cap) = token::new_policy(&treasury_cap, ctx); // Enforce that the token can only be used spent at this shop token::add_rule_for_action( &mut policy, &policy_cap, token::spend_action(), ctx, ); token::share_policy(policy); transfer::public_freeze_object(coin_metadata); transfer::public_transfer(policy_cap, tx_context::sender(ctx)); transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); } ... // more module code /// Buy a gift for 10 tokens. The `Gift` is received, and the `Token` is /// spent (stored in the `ActionRequest`'s `burned_balance` field). public fun buy_a_gift(token: Token, ctx: &mut TxContext): (Gift, ActionRequest) { assert!(token::value(&token) == GIFT_PRICE, EIncorrectAmount); let gift = Gift { id: object::new(ctx) }; let mut req = token::spend(token, ctx); // only required because we've set this rule token::add_approval(GiftShop {}, &mut req, ctx); (gift, req) } ``` ## Using Stablecoins To use stablecoins in your app, see [this guide](https://docs.sui.io/guides/developer/stablecoins). Here, we show a simple example for native USDC taken from this [Mysten sample code](https://github.com/MystenLabs/sui/blob/main/examples/move/usdc_usage/sources/example.move) (with comments and a slight modification we've added). ```bash Sui Move expandable theme={null} module usdc_usage::example; use sui::coin::Coin; use sui::sui::SUI; use usdc::usdc::USDC; public struct Sword has key, store { id: UID, strength: u64 } public fun buy_sword_with_usdc( coin: Coin, tx_context: &mut TxContext ): Sword { // We're creating a sword that's as strong as the amount of money the user // spends. Since USDC has six decimal places, a $1 USDC sword would // have a strength of 1,000,000! // See: https://docs.sui.io/references/framework/sui/coin#sui_coin_value let sword = create_sword(coin.value(), tx_context); // After making the sword, we transfer the sender's USDC to oursleves // See: https://docs.sui.io/references/framework/sui/transfer#sui_transfer_public_transfer transfer::public_transfer(coin, @0xYOUR_TREASURY_ADDRESS); // At some point we need to transfer the sword to the sender or they'll // be very unhappy. Here, we assume that another function calls this // one and handles the transfer step. sword } fun create_sword(strength: u64, tx_context: &mut TxContext): Sword { let id = object::new(tx_context); Sword { id, strength } } ... // more module code below ``` ## Reading token data to show in your app ### Fetch an address's coins The main call is [`suix_getAllCoins`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-getallcoins) for coins of all types owned by a user. You can also use [`suix_getCoins`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-getcoins) request if you only care about one coin type. Each coin is represent as an individual object in Sui. For example, an address with SUI has a set of 0-many SUI coin objects that add up to its total balance. Here is a partial response showing one coin object - a SUI coin worth `1999995900 MIST`, or `1.99 SUI`, and another two worth `100 MIST` (so, `balance` here means the balance of a coin object and not [the address's overall SUI balance](https://testnet.suivision.xyz/account/0xeb583665fb5a35f488234bd8715be86bab0f8ba5311d95ee5a8f67b6088ca2b0), which is higher). **Partial cURL response** ```json TypeScript theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "data" : [ { "balance" : "1999995900", "coinObjectId" : "0x5e0807e912974b727b6d3bc05a139ed0ff49370432283ea5b0705bc697366f08", "coinType" : "0x2::sui::SUI", "digest" : "6ZHZiiqfDmoWrHiMBsfQwWtrsWS5eNzwwBEyjH4nQxWy", "previousTransaction" : "BeRn1PNWq7QhucWnv2ohk6watpbkxSR63p5EAwgScSdo", "version" : "101882055" }, { "balance" : "100", "coinObjectId" : "0x050a6a7496e71d3354d0d2d9e32ac60d66297755ad5c871ec198a429b3d56bed", "coinType" : "0x2::sui::SUI", "digest" : "FYXD5ijJRg43VoP8ZWcqYhszDfPGiNVV9sQ81bx5WLtz", "previousTransaction" : "G45zwUKQLPMZ9nZy4WwQn9rJiL8S5prKbN1J1vnugrNh", "version" : "101882046" }, ... ``` ### Get the icon and other data for a coin You can use [`suix_getCoinMetadata`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-getcoinmetadata). Here is the result for coin`"0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"` ([ native USDC](https://suivision.xyz/coin/0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC)) on Mainnet: **Partial cURL response** ```json cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "decimals" : 6, "description" : "USDC is a US dollar-backed stablecoin issued by Circle. USDC is designed to provide a faster, safer, and more efficient way to send, spend, and exchange money around the world.", "iconUrl" : "https://circle.com/usdc-icon", "id" : "0x69b7a7c3c200439c1b5f3b19d7d495d5966d5f08de66c69276152f8db3992ec6", "name" : "USDC", "symbol" : "USDC" } } ``` As described [here](https://docs.sui.io/standards/coin#coinmetadata), in addition to the name, symbol, and description, we're also given an [icon URL](https://circle.com/usdc-icon) to fetch the icon for display in a DeFi app or wallet, say - which in this case is . We're also told that the token uses six decimals (whereas [SUI](https://suivision.xyz/coin/0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI) uses 9, since 1 billion MIST = 1 SUI). ### A helpful community project A tool that's worth looking into if you have to regularly fetch data about coins is [CoinMeta](https://github.com/juzybits/polymedia-coinmeta), which exists to solve two issues: * Some coins do not return image data in the above request, or they do but the image is not in a web-optimized format for displaying a small logo. * If you want info about a lot of coins, then you have to make a JSON-RPC [`suix_getCoinMetadata`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-getcoinmetadata) request for each one (where this project has an endpoint that can return info for multiple coins in one response). ### Get the total supply for a coin You can use [`sui_getTotalSupply` ](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-gettotalsupply)for this. Here is the result for `"0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"` ([ native USDC](https://suivision.xyz/coin/0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC)) on Mainnet: **Partial cURL response** ```json cURL theme={null} { "id" : 1, "jsonrpc" : "2.0", "result" : { "value" : "319585979218700" } } ``` Recall from the section above that the token uses 6 decimals. So, the total amount of native USDC on the Sui blockchain at the time of writing this is `319,585,979.218700`. ## Resources Here, we list many of the resources we listed above in a compact form: ### General * [All open-loop fungible tokens on Sui ](https://suivision.xyz/coins)(including SUI and stablecoins). ### SUI * [SUI Tokenomics](https://docs.sui.io/concepts/tokenomics) (including docs on staking and how gas prices are calculated). * [Staking SUI](https://docs.sui.io/concepts/tokenomics/staking-unstaking). * [How gas fees are calculated for transactions](https://docs.sui.io/concepts/tokenomics/gas-in-sui). ### Open-loop tokens * [Sui Coin Standard](https://docs.sui.io/standards/coin): “the technical standard used for smart contracts on Sui for creating coins on the Sui blockchain.” * [Developer guide](https://docs.sui.io/guides/developer/coin/regulated) for creating coins with an address-deny list (a requirement for coins like stablecoins) ### Closed-loop tokens Closed-Loop Tokens let you limit the applications that can use a certain token. Useful for applications like loyalty/reward points, complying with certain regulations, or creating a closed in-game economy. * [Standard](https://docs.sui.io/standards/closed-loop-token) * [Blog post](https://blog.sui.io/closed-loop-tokens-explained/) summarizing the benefits and giving example use-cases. * Developer guides for creating an [in-game currency](https://docs.sui.io/guides/developer/coin/in-game-token) and a [loyalty token](https://docs.sui.io/guides/developer/coin/loyalty) ### Stablecoins * [Developer guide](https://docs.sui.io/guides/developer/stablecoins) for using Stablecoins * [List of stablecoins on Sui, both bridged and native](https://defillama.com/stablecoins/Sui) # Key Resources Source: https://docs.shinami.com/developer-guides/sui/move-guides/key-resources Docs and dev tools for Sui Move development ## Dev tools ### Sui CLI The [Sui Client CLI guide](https://docs.sui.io/references/cli/client) walks you through setting up the CLI, which you'll use at least for publishing test Move packages and sending transactions to them. Depending on how you do the installation, it can take a while, so run it while you eat lunch. * See also the [`sui client ptb`](https://docs.sui.io/references/cli/ptb) command that allows you to build and execute programmable transaction blocks directly from your CLI, including through bash scripts. ### IDEs For a list of IDEs with Move plugins - including VS Code, IntelliJ, and Emacs - [see here](https://docs.sui.io/guides/developer/getting-started#move-ides-and-plugins). ### SDKs * For the list of official and community-built Sui SDKs, [see here](https://docs.sui.io/references/sui-sdks). You won't need one for the basic review of core concepts, which involves reading and writing Sui Move code. However, as you get into building a frontend or backend for an app they'll come in handy. * When using Shinami services to read and write to the blockchain, sponsor transactions, and perform embedded (Invisible) wallet operations, you'll want to use our [Shinami Clients TypeScript SDK](https://www.npmjs.com/package/@shinami/clients). When building a zkLogin app see also our [Next.js zkLogin SDK](https://www.npmjs.com/package/@shinami/nextjs-zklogin). ### Sui Explorer To view your transactions and explore deployed packages, use [suivision.xyz](https://suivision.xyz/) or [suiscan.xyz](https://suiscan.xyz/mainnet/home). Make sure you're searching the correct network (Devnet, Testnet, Mainnet). ## New to the language or need a refresher ### Core concepts with code examples * [Let's Move Sui](https://letsmovesui.com/) covers the basics - including Coins and NFTs - with short reading and coding exercises. * *Watch out:* the coding exercise solutions occasionally have a small issue (e.g. a changed or omitted variable name). It's not a huge issue - if you're following along you'll catch these and they're not a blocker. Also, by the time you read this maybe that's been fixed. * [The Move Book](https://move-book.com/) is a comprehensive guide to Move and the Sui blockchain. It starts with core concepts - types of functions, data types, etc. It moves on to design patterns and key topics like time, error handling, etc. * This is a work in progress that gets semi-regular updates. Currently some topics are yet to be filled with content. Still, it's a good resource. ### Coming from Move * For a summary of the key differences between Move and Sui Move, see [here](https://docs.sui.io/concepts/sui-move-concepts#differences). ### Sui dev portal * The [Sui Developer Portal](https://sui.io/developers) has links to everything devs might be interested in, such as docs and tutorials, sample code, and how to join the next hackathon. Some of these links show up elsewhere in this doc, but it's a good resource to bookmark. ## Example app tutorials * The Sui Foundation has [app examples](https://docs.sui.io/guides/developer/app-examples) with sample Move and frontend code, and tutorials on how to create and run the apps. * **Watch out:** I found occasional issues with these - a Move module function that was missing in the docs but that was in the associated Github repo, a function call that gave me a deprecation warning in VSCode that I had to update. Nothing huge - just be aware that you may encounter a small issue or two. The fix will likely be clear. * For deploying and running a zkLogin wallet app that interacts with a simple module we've already deployed, see our [Next.js App Template turorial](/developer-guides/sui/tutorials/zklogin-nextjs-app-template) . ## Key topics ### Coins and tokens * Sui foundation docs like the [currency standard](https://docs.sui.io/standards/coin), using the Closed-Loop Token standard for creating an [in-game currency](https://docs.sui.io/guides/developer/coin/in-game-token) or [loyalty tokens](https://docs.sui.io/guides/developer/coin/loyalty). * For learning the basics, see the Let's Move Sui [coding exercises for tokens and coins](https://letsmovesui.com/courses/2/modules/5/lesson/1). ### NFTs * Sui foundation [doc on NFTs](https://docs.sui.io/guides/developer/nft) with links to key related docs like creating an [NFT rental mechanism](https://docs.sui.io/guides/developer/nft/nft-rental) that could be used in gaming, ticketing, virtual land, subscriptions, and more (using Kiosks and the Kiosk App Standard). * For learning the basics, see the Let's Move Sui [coding exercises for NFTs](https://letsmovesui.com/courses/2/modules/4/lesson/1). ### Transaction execution * Learn about the steps a transaction on the Sui network goes though, including when its results are processed and available for reading on Fullnodes in the Sui Foundation's [Life of a Transaction](https://docs.sui.io/concepts/transactions) doc (first sub-section in left nav). For tips on getting read-after-write consistency, with code examples, see [our guidance](/developer-guides/sui/move-guides/common-questions). * Avoiding shared object contention in high throughput apps (e.g. with gas coins and AdminCaps): see [our guidance](/developer-guides/sui/move-guides/common-questions#how-should-i-handle-concurrency-to-avoid-equivocation-and-object-version-sequencenumber-errors), especially in cases where it makes sense for you to sponsor the transaction fee. See also [`Mysten's SerialTransactionExecutor`](https://sdk.mystenlabs.com/typescript/executors#serialtransactionexecutor) and (experimental) `ParallelTransactionExecutor` for cases where you are performing non-sponsored transactions. ## Beyond the basics ### Sui Move source code * For core Sui Move types like `address`, `coin`, `tx_context`, and `object`, see [Sui framework sources](https://github.com/MystenLabs/sui/tree/main/crates/sui-framework/packages/sui-framework/sources). ### Best practices * See the Sui Foundation's [Sui Developer Cheat Sheet](https://docs.sui.io/guides/developer/dev-cheat-sheet). ### Unit testing For guidance on writing and running unit tests for your Move modules, see * The [Unit Tests](https://move-book.com/move-basics/testing.html) section of The Move Book. * The [sui::test\_scenario module](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/sui-framework/sources/test/test_scenario.move) that provides a scenario that emulates a series of Sui transactions, each with a potentially different user executing them. ### Real-world code examples There are a lot of great real-world examples of Move code. Here is just a small selection: * [SPAM](https://github.com/juzybits/polymedia-spam) - "Spam to Earn" a.k.a. "Proof of Spam" on Sui. * Bidder: a decentralized platform for creating and bidding on auctions ([code](https://github.com/juzybits/polymedia-bidder) , [app](https://bidder.polymedia.app/)). ## Have a question For help and tips from fellow Sui developers, see: * [Suinami Riders Telegram group](https://t.me/+pxh89f8xU5RmYjNh) * [Sui Discord Server's](https://discord.gg/sui) `dev_discussion`, `move_lang`, and other channels. * [Sui Developer Forums](https://forums.sui.io/) * [Sui Eng office hours](https://docs.google.com/forms/d/e/1FAIpQLSdWPf0H-t-QWV0Ik_5o86mKFlVNFsu_r5fyDxIL9BW_oSxpwA/viewform) * "Office Hours are reserved for advanced technical consultation only. Please leverage documentation or the developer forum first before using this. Questions that can be answered in other sources may be de-prioritized." ## Stay up to date * Sui [Discord Server](https://discord.gg/sui): get free Testnet and Devnet Sui (`testnet_faucet` and `devnet_faucet` channels) and keep up with key announcements (`dev_announcements`, `testnet_updates`, `mainnet_updates`, `annoucements`, and other channels). * [Sui on X](https://twitter.com/SuiNetwork) for ecosystem announcements. * [Sui Developer Roadmap](https://forums.sui.io/t/sui-developer-roadmap-2024/45229) lists key changes to Sui and tooling that has been completed or is being worked on. # NFTs (non-fungible-tokens) Source: https://docs.shinami.com/developer-guides/sui/move-guides/nfts A summary of NFTs on Sui with links to key resources ## Overview NFTs are unique (non-fungible) tokens that represent art, in-game items, tickets, or other items that someone can own, buy, sell, or trade. Each NFT has a unique identifier, or object id, that distinguishes it from other objects on the Sui blockchain. On Sui, an NFTs can be owned by an account or another object. An example of an object owning an NFT might be a NFT representing an in-game character owning a NFT representing a sword. To learn about fungible tokens on Sui - like the SUI coin itself - see [our guide](/developer-guides/sui/move-guides/fungible-tokens). Read below to see an overview of NFTs and links to relevant tutorials, standards, and sample code. ## NFT minting and dynamic updates ### Smart contracts NFTs are created and managed using smart contracts (Move packages published to Devnet, Testnet, or Mainnet). A simple example is described in [this Sui Foundation doc](https://docs.sui.io/guides/developer/nft), with a partial code snippet taken from [the associated Move code](https://github.com/MystenLabs/sui/blob/main/examples/move/nft/sources/testnet_nft.move) shown below. ```bash Move expandable theme={null} // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 module examples::testnet_nft; use std::string; use sui::{event, url::{Self, Url}}; /// The struct that represents the NFT. public struct TestnetNFT has key, store { id: UID, name: string::String, description: string::String, image_url: Url, } /// Function to mint a new NFT and transfer it to the sender. /// There are no restrictions here on who can mint, or how many /// mints can take place. #[allow(lint(self_transfer))] public fun mint_to_sender( name: vector, description: vector, url: vector, ctx: &mut TxContext, ) { let sender = ctx.sender(); /// 1. mint a new NFT with the sender-provided values let nft = TestnetNFT { id: object::new(ctx), name: string::utf8(name), description: string::utf8(description), url: url::new_unsafe_from_bytes(url), }; /// 2. transfer it to the sender transfer::public_transfer(nft, sender); } ``` ### Minting A mint is a Move function call to the NFT smart contract. In the contract, you define the rules for which addresses can mint and what they can mint. Examples include: * Any address can mint, or any address willing to pay a fee * An wallet with an AdminCap your app controls mints and transfers NFTs to user wallets * Only addresses with permission can mint. For example, an app with an admin wallet account that issues mint and upgrade tickets that limit who can mint and which NFTs can be upgraded. Beyond who can mint, you can place other limits - e.g. only 2,000 can be minted. In a game, it wouldn’t make much sense to limit the core NFTs players use in the game - like an initial hero character or a basic sword - as that would limit the number of players you can have. However, you might place limits on certain rare items like an all-powerful sword in order to maintain their specialness - whether by setting a maximum number, a low likelihood of minting, or other limitations. ### Dynamic NFT updates A static NFT - like a hip animal PFP - is nice. But it's also nice to have NFTs that can change over time. **Example one:** a hero NFT could level up its skills. Here's a partial example: ```bash Move expandable theme={null} /// Levels up a hero with a level-up ticket. public fun level_up_hero( hero: &mut Hero, ticket: LevelUpTicket, damage: u8, speed: u8, defense: u8, ) { let LevelUpTicket { id, hero_id, attribute_points } = ticket; assert!(object::borrow_id(hero) == &hero_id, EHeroIdMismatch); assert!(damage + speed + defense == attribute_points, EAttributePointsMismatch); object::delete(id); hero.level = hero.level + 1; hero.damage = hero.damage + damage; hero.speed = hero.speed + speed; hero.defense = hero.defense + defense; } ``` **Example two:** An NFT could represent a ticket, which can be exchanged for entry or an item. Once it's been redeemed, you'll want to mark it as such and potentially update the image url. [Here is an example](https://blog.sui.io/redeemable-nfts/) of doing that when NFT holders used their NFTs to get a cup of coffee. ## Where the data lives ### Off-chain data storage While NFTs exist on chain, sometimes part of their data does not. The typical off-chain data would be any associated large media files (image, video, audio) - e.g. the full size and thumbnail image of a hero character. The [Sui Object Display](https://docs.sui.io/standards/display) standard controls how NFTs appear in other apps: marketplaces, social media apps, explorers, etc. it includes standard fields such as `image_url` that apps like explorers and marketplaces use to know how to fetch the off-chain data when displaying an NFT. Examples of off-chain storage include: * A decentralized storage network such as * [Walrus](https://www.walrus.xyz/), Mysten Labs' decentralized storage solution for Sui. * An [IPFS](https://ipfs.tech/) pinning service, such as [Filebase](https://filebase.com/) or [Pinata](https://pinata.cloud/). * Centralized storage like an AWS S3 bucket + CloudFront CDN. ### Caching The ultimate truth about an NFT’s ownership and attributes lives in the latest state of the Sui blockchain. However, frequently read data can often be cached (especially in cases where your app controls minting and updating NFTs, and so knows when NFT data changes). ## Ownership, transfer, and sale of NFTs ### Ownership Games use Shinami Invisible (NFT) Wallets or zkLogin wallets to hold its users NFTs. In both cases, the user doesn't need to remember a seed phrase to manage their wallet - they just need to know how to log into your app or log into the social sign in provider you enable - Google/Facebook/Twitch/etc. With either wallet type, your app determines when and how to tell the user their in-game object is an NFT on the Sui blockchain. For a richer comparison of the different types of user wallets, see our [Wallet Services guide](/product-overviews/sui/wallets). ### Sale and transfer If using Invisible Wallets or zkLogin wallets, your app can choose when to present the user with an option to sell or trade the NFT to another player. Your app can also provide a means for users to take control of their keys by, for example, creating a cross-app, zkLogin browser wallet with [Sui Wallet](https://suiwallet.com/) and transferring the NFT to it. When a user fully controls their keys, you can use [Kiosks](https://docs.sui.io/standards/kiosk) to enforce transfer rules and collect a royalty if a user sells an in-game item. ### Rental NFT renting allows an NFT owner to give another wallet temporary access to the privileges that NFT provides. For example, someone can rent a powerful game item for a few days to use in a tournament. For more information and sample code, see [this Sui Foundation doc](https://docs.sui.io/guides/developer/nft/nft-rental). ## Resources ### Official standards and resources * [Sui Object Display standard](https://docs.sui.io/standards/display) * [Sui Kiosk overview and sample code](https://docs.sui.io/standards/kiosk) ### Community tools and resources * Suiet wallet outlines [the fields they look at when displaying an NFT](https://std.suiet.app/nft/) * [Origin Byte:](https://github.com/Origin-Byte/nft-protocol/tree/main) a collection of tools and protocols to help developers with NFTs and NFT collections. ### Developer Tutorials * Sui Foundation [simple example](https://docs.sui.io/guides/developer/nft) to publish a Move package where anyone can mint an NFT. * [Sui Foundation NFT rental example](https://docs.sui.io/guides/developer/nft/nft-rental) ### Top Sui NFT marketplaces * [suiscan last 30 day marketplace activity](https://suiscan.xyz/mainnet/nfts/marketplaces) ## Common questions ### I'm seeing a lot of broken links when I fetch NFT data When you use [`suix_getOwnedObjects`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#suix-getownedobjects), [`suiGetObject`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#sui-getobject), and [`sui_multiGetObjects`](/api-docs/sui/node-service/json-rpc/read-api-objects-coins#sui-multigetobjects) with `showDisplay: true` you may see a lot of `image_url` values that are broken (404s). Because of abandoned projects and scams that airdrop NFTs to a lot of addresses, this is fairly common. Search for the project to see if it seems legit. You can also look up the package on Suiscan, which has scam warnings. For example, this NFT (we also set `showType: true`): ```TypeScript theme={null} { objectId: '0x8bdf47e4e77d5eeba5b9a044c6c2f8511ce64ca75682c0fb5389de9a26a422b8', version: '446027767', digest: 'FiGZywnDRCsysWVyNkp37Be9yhHPpxw6bUb43NE1h3zX', type: '0xbdcda1902faf59c4c1896544d819b6c0299263712a9d6a4a809fbad4bab93901::nft_claim::SendClaimNFT', display: { data: { description: 'This NFT is your claim to SEND Tokens. Do not burn or transfer it to someonen else.', image_url: 'https://nk.up.railway.app/nft.png', name: 'SEND Claim NFT' }, error: null } } ``` Searching the package - `0xbdcda1902faf59c4c1896544d819b6c0299263712a9d6a4a809fbad4bab93901` - [on Suiscan](https://suiscan.xyz/mainnet/collection/0xbdcda1902faf59c4c1896544d819b6c0299263712a9d6a4a809fbad4bab93901::nft_claim::SendClaimNFT/items) shows: # Transaction.build()'s hidden requests Source: https://docs.shinami.com/developer-guides/sui/move-guides/transaction-block-build-requests An example of the extra Node requests you'll create when building a transaction. ## Summary Calling Transaction.build() on a [programmable Transaction Block](https://docs.sui.io/guides/developer/sui-101/building-ptb) is generally required before executing a transaction. Unless you [build your transaction offline](https://docs.sui.io/guides/developer/sui-101/building-ptb#building-offline), this method takes a Fullnode client and uses it to make additional requests to a Sui Fullnode in order to gather the information it needs to build the programmable transaction block. You can also pass a Transaction into the Shinami or Mysten SDK's `devInspectTransactionBlock` method, in which case it calls `Transaction.build()` on the Transaction before sending the `sui_devInspectTransactionBlock` request to the Sui network ([covered below](/developer-guides/sui/move-guides/transaction-block-build-requests#devinspecttransactionblock)). Finally, our SDK helper method `buildGaslessTransaction`, used to prepare a Transaction for sponsorship with our Gas Station, also calls `Transaction.build()` ([covered below](/developer-guides/sui/move-guides/transaction-block-build-requests#buildgaslesstransaction-shinami-typescript-sdk)). We made this guide so you can better: 1. understand the `Transaction.build()` process 2. estimate your usage and billing with your Node provider. **Notes:** The examples below show 4-5 Node Service requests generated from calling `Transaction.build()`. However, the count of requests can increase above 5 for larger and different transaction blocks. You can run a test with your transactions and observe your own results. ## Transaction.build() In order to build the transaction, the [`Transaction` build method](https://github.com/MystenLabs/ts-sdks/blob/main/packages/sui/src/transactions/Transaction.ts#L765) uses the Fullnode client you pass it to make a variety of JSON-RPC requests to get the information it needs to build the transaction. This can include, but is not necessarily limited to, fetching the current reference gas price, retrieving the structured representation of any Move calls, fetching information about any objects involved in the transaction, and checking the available gas coins of the address paying for the gas (sender or otherwise). *The exact type and count of requests your Transaction.build() calls produce will vary based on the contents of your transaction blocks.* When you build the transaction block, you pass in a Sui Fullnode client: ```bash Shinami TypeScript SDK theme={null} const nodeClient = new SuiClient({ url: getFullnodeUrl("testnet") }); let txb = new Transaction(); // populate the Transaction const txBytes = await txb.build({ client: nodeClient, onlyTransactionKind: false}); ``` One of my recent calls to`txb.build()` produced the following requests to the client I provided it: ``` "sui_getNormalizedMoveFunction": 1, "sui_getProtocolConfig": 1, "sui_multiGetObjects": 1, "suix_getCoins": 1, "suix_getReferenceGasPrice": 1 ``` *The exact type and count of requests your Transaction.build() calls produce will vary based on the contents of your transaction blocks.* ## devInspectTransactionBlock `devInspectTransactionBlock` can be sent a Transaction. If you do this, [it will call Transaction.build() with the input block](https://github.com/MystenLabs/ts-sdks/blob/main/packages/sui/src/jsonRpc/client.ts#L777) , which will lead to additional Node Service requests made from the build process. ```bash Shinami TypeScript SDK theme={null} let txb = new Transaction(); // populate the Transaction await nodeClient.devInspectTransactionBlock({ transactionBlock: txb, sender: SENDER_ADDRESS }); ``` One of my recent calls to `sui_devInspectTransactionBlock` with an Transaction produced four requests to Shinami's Node service: ``` "sui_getNormalizedMoveFunction": 1, "sui_getProtocolConfig": 1, "sui_multiGetObjects": 1, "sui_devInspectTransactionBlock": 1 ``` *The exact type and count of requests your Transaction.build() calls produce will vary based on the contents of your transaction blocks.* ## buildGaslessTransaction (Shinami TypeScript SDK) Our SDK has a helper function called `buildGaslessTransaction` to prepare a Transaction for Gas Station sponsorship. Here is an example of calling it with a function to populate a transaction: ```bash Shinami TypeScript SDK theme={null} import { buildGaslessTransaction } from "@shinami/clients/sui"; ... await buildGaslessTransaction( (txb) => { txb.moveCall({ target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access", arguments: [txb.object('0x6')], }); }, { sui: nodeClient } ); ``` This function [calls `Transaction.build()`](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/clients/src/sui/gas.ts#L188). As a result, it will produce the same set of requests that would get produced if you called `Transaction.build()` directly on a similarly populated Transaction. # Gas Station: backend-only Source: https://docs.shinami.com/developer-guides/sui/tutorials/gas-station-backend-only Provides multiple code examples for using the service with step-by-step explanations. Also provides guidance on common questions and issues developers have when using the service. ## Overview In this tutorial you'll learn how to sponsor transactions with Shinami's Gas Station and submit them for execution on the Sui blockchain. We also cover key tips for using our Gas Station. For our examples, we'll be constructing our [programmable transactions](https://docs.sui.io/guides/developer/sui-101/building-ptb) using the [Sui TypeScript SDK](https://github.com/MystenLabs/ts-sdks/tree/main/packages/sui) provided by Mysten. We'll also use the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) to make JSON-RPC calls to Shinami Gas Station for transaction sponsorship. This is not meant as a complete template for production code, but as a way to show you how to perform all the requests our Gas Station API supports. ### What is a sponsored transaction? When calling [`sui_executeTransactionBlock`](/api-docs/sui/node-service/json-rpc#sui-executetransactionblock) a Base64 encoded string is passed in which encodes the transactions to be executed, the sender address, and the gas information for the transaction block. This unit is called a `TransactionData` object. 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. Gas Station attaches a `GasData` object from your Gas Station fund that will pay for the transaction. Then, it produces the BCS-serialized, Base64-encoded string that represents the full `TransactionData` object, and sends it back to you along with the sponsor signature authorizing this transaction. From there, the last piece needed to call `sui_executeTransactionBlock` is the sender's signature over the `TransactionData`. We'll go through all of this below. ### Requests must be from your BE Our Gas Station does not support CORS, so if you attempt to make requests to it from your frontend you'll get a CORS error. We do this because exposing a Gas Station API key on the frontend is a security risk - an attacker could drain the SUI in your Gas Station fund associated with the key by using it to sponsor transactions. For an example flow of combining frontend signing and/or transaction inputs with backend sponsorship, see our [Frontend signing + backend sponsorship](/developer-guides/sui/tutorials/gas-station-with-frontend-signing) tutorial. ### 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 Sui Gas Station](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api) as well as the "General Error Codes" section at the top that apply to all Shinami services. ### Usage and Billing FAQ Make sure to check out the Sui Gas Station [product usage FAQ](/help-center/sui/gas-station-faq) and [billing FAQ](/help-center/billing/sui-faq#gas-station) in our Help Center for answers to common questions. ## Required setup **Notes:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet See the [Sui Gas Station page of our Help Center](/help-center/sui/gas-station-faq) for guidance on how to set up a fund on Testnet if you don't already have one. When you create a Testnet fund, we add some SUI to it so you can test immediately. ### 2. Clone the github repo Clone the [shinami-examples github repo](https://github.com/shinamicorp/shinami-examples/tree/main), cd into the `shinami-examples/sui/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](https://www.typescriptlang.org/download/) ) . If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Below, we'll be using the `gas_station.ts` file in the `shinami-examples/sui/typescript/backend_examples/src` directory. ### 3: Create an API access key and copy it into the file. **Create a key** You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key that has `Gas Station` rights for Testnet (see our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#/create-an-access-key) to see how to make a key). **Add it to the gas\_station.ts file** Once you have your key, use it as the value for the `GAS_STATION_ACCESS_KEY` constant. You'll see that we immediately use it to instantiate a Gas Station client (for sponsoring transactions). We also create and a Node client for executing them (we're using the Mysten free Node Service in this example, but you'll need to find another provider for your production app). ```bash Shinami Typescript SDK theme={null} // 2. Copy your Testnet Gas Station key value const GAS_STATION_TESTNET_ACCESS_KEY = "{{gasStationTestnetAccessKey}}"; // 3. Set up your Gas Station and Node Service clients const gasStationClient = new GasStationClient(GAS_STATION_TESTNET_ACCESS_KEY); const nodeClient = new SuiGrpcClient({ baseUrl: 'https://fullnode.testnet.sui.io:443', network: 'testnet', }); ``` ### 4. Generate a new Keypair and log its secret key for subsequent runs We need a Sui address to act as the sender, so we'll generate a new KeyPair. On the first run, the secret key will be printed to the console. We'll reuse the same address for all runs, so you can see all the history in one account. **Notes:** Your app should have its own way to manage accounts and keys. This is just meant as a convenience when doing your initial tests with Gas Station using our tutorial code. Some examples require the sender to have a SUI coin, so maintaining a consistent sender is required. Here's what you'll do: 1. Run the code by running the `tsc` command in a terminal at the root of the `shinami-examples/sui/typescript/backend_examples` directory to transpile the `.ts` files in the `/src` directory to JavaScript. 2. Run `node build/gas_station.js` to run the resulting file. 3. A secret key will be printed to the console, e.g. `secretKey: suiprivkey...` value. 4. Before running the code again, replace the call to `generateSecretKey()` with the value of the secret key. **Initial code:** ```bash Shinami Typescript SDK theme={null} // 4. Create a KeyPair to act as the sender async function generateSecretKey() : Promise { const keyPair = new Ed25519Keypair(); console.log("secretKey:", keyPair.getSecretKey()) return keyPair.getSecretKey(); } const ENCODED_SECRET_KEY = await generateSecretKey(); ``` **After running the code, change it to:** ```bash Shinami Typescript SDK theme={null} // 4. Create a KeyPair to act as the sender async function generateSecretKey() : Promise { const keyPair = new Ed25519Keypair(); console.log("secretKey:", keyPair.getSecretKey()) return keyPair.getSecretKey(); } const ENCODED_SECRET_KEY = "suiprivkey1qqdn8409tumvyq" //await generateSecretKey(); // the value above is too short for a real key ``` ### 5. Open your Shinami dashboard Technically not required, but we recommend visiting the ["Completed transactions" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_completed_tx) of your Shinami dashboard. This tab, and the "In flight transactions" tab (for sponsored transactions that haven't yet been executed) can be helpful when testing. After running the examples below, take a look at these pages. It may take a few moments or clicking "Refresh data", but you'll see the digest of the transaction you sponsored appear. The code you run will execute the transaction, but note that a transaction can remain in the "In flight transactions" table for a minute or two (until our Gas Station updates the status of the associated gas objects). ## Code examples ### Overview Below, we'll review some of our sample code functions and how to run them. At a high-level, you'll uncomment just one function that builds a Shinami type `GaslessTransaction` for sponsorship - e.g. `clockMoveCallGaslessTransaction()` in the code block below. Then, save any changes you made to the file, run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript, and finally run `node build/gas_station.js` to run the resulting file. The functions that take arguments require setting values for the associated constant (generally the object id of a SUI coin or other object owned by the sender). At a high level, we do the same three things each time: 1. Generate a transaction without gas info (Step 5). 2. Sponsor, sign, and submit the transaction to the Sui blockchain (Step 6). 3. Poll a Full node until the transaction is check-pointed and received by the node, then print the status (Step 7). ```bash Shinami Typescript SDK expandable theme={null} // 5. Generate the GaslessTransaction for sponsorship const gaslessTx = await clockMoveCallGaslessTransaction(); // clockMoveCallGaslessTransactionAlternateVersion(); // clockMoveCallGaslessTransactionOfflineBuildManualBudget(); // checkFundBalanceAndDepositIfNeeded(SUI_COIN_TO_DEPOSIT_ID, SENDER_ADDRESS); // splitCoinOwnedByGaslessTransaction(COIN_TO_SPLIT_FROM_ID, SENDER_ADDRESS); // mergeCoinsGaslessTransaction(COIN_TO_SPLIT_FROM_ID, COIN_TO_MERGE_ID, SENDER_ADDRESS); // transferObjectToRecipientGaslessTransaction(OBJ_ID_TO_TRANSFER, SENDER_ADDRESS, RECIPIENT_ADDRESS); if (gaslessTx) { gaslessTx.sender = SENDER_ADDRESS; // 6. Sponsor, sign, and execute the transaction const txDigest = await sponsorAndExecuteTransactionForKeyPairSender( gaslessTx, keyPairFromSecretKey ); console.log("digest", txDigest); // 7. Wait until the node has processed the transaction and print the status const txInfo = await nodeClient.waitForTransaction({ digest: txDigest }); // You can look up the digest in a Sui explorer - make sure to switch to Testnet console.log("\ntxDigest: ", txDigest); console.log("status:", txInfo.Transaction?.status); } ``` ### Sponsor, sign, and execute flow The `sponsorAndExecuteTransactionForKeyPairSender` does the following: 1. Asks Shinami Gas Station to sponsor the transaction we generated above using the Gas Station fund tied to the access key you're using. Gas Station returns the full transaction - now with gas payment information - as well as the sponsor signature. We use Shinami's auto-budgeting feature because we omit a `gasBudget`. To learn more about auto-budgeting (and manual budgeting) see the Appendix. 2. Generates the sender signature. 3. Submits the full transaction data, along with the sender and sponsor signatures to the Sui blockchain. ```bash Shinami Typescript SDK expandable theme={null} async function sponsorAndExecuteTransactionForKeyPairSender( gaslessTx: GaslessTransaction, keypair: Ed25519Keypair): Promise { // 1. Send the GaslessTransaction to Shinami Gas Station for sponsorship. let sponsoredResponse = await gasStationClient.sponsorTransaction( gaslessTx // when gaslessTx.gasBudget is undefined we take advantage of Shinami auto-budgeting ); console.log("\nsponsorTransactionBlock response (includes sender 'signature' and 'txBytes' with gas info now included):"); console.log(sponsoredResponse); // 2. Sign the full transaction payload with the sender's key. let senderSig = await Transaction.from(sponsoredResponse?.txBytes).sign( { signer: keypair } ); // 3. Submit the full transaction payload, along with the gas owner // and sender signatures, for execution on the Sui network const signatures = [senderSig?.signature, sponsoredResponse?.signature]; let response = await nodeClient.executeTransaction({ transaction: fromBase64(sponsoredResponse?.txBytes), signatures: [senderSig?.signature, sponsoredResponse?.signature] }); if (!response.Transaction?.status?.success) { const error = response.FailedTransaction?.effects; throw new Error(`Transaction failed: ${error || 'Unknown error'}`); } return response.Transaction.digest; } ``` ### Sponsor and execute a Move call **Understand the code** The `clockMoveCallGaslessTransaction` function builds a TransactionBlock that calls a function on a Move module we've [deployed to Testnet](https://testnet.suivision.xyz/package/0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16). This is a very simple Move module based on a [sample Move project](https://docs.sui.io/guides/developer/sui-101/access-time) from Mysten. We're calling its one function, `access`, which takes a read-only reference to 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. We print the GaslessTransaction to the console so you can see it's shape. ```bash Typescript theme={null} async function clockMoveCallGaslessTransaction() : Promise { const gaslessTx = await buildGaslessTransaction( (txb) => { txb.moveCall({ target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access", arguments: [txb.object('0x6')], }); }, { sui: nodeClient } ); console.log("\nbuildGaslessTransaction response (your GaslessTransaction for sponsorship):"); console.log(gaslessTx); return gaslessTx } ``` **Run the code** Make sure you've saved your changes to the file from above (adding your API key and setting a fixed private key value). Run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript. Then, run `node build/gas_station.js`. You'll see a lot of output in the console - we added a lot of printing so you can see the shape of API call responses and function results. Of course, once you understand them you can comment out the `console.log()` statements. **View the transaction in an explorer** The last thing printed to the console is the digest of the transaction you sponsored. It will be the same value as the `txDigest` returned by our `gasStationClient.sponsorTransactionBlock` request, since at that point the digest is created over the full TransactionData that will be submitted to the chain (which includes transaction operations, gas object, and sender address). Your digest will of course be different than my example since the sponsorship gas object and the sender are different: ```bash Terminal theme={null} txDigest: 8J8Nsphaq1RfJPqQAj6AkA8Mrte9SpYiXGup15Vp3Uf9 status: { success: true, error: null } ``` You can look up the digest in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure you've set the explorer to Testnet first). This is an image from Suivision showing that my transaction emitted one `TimeEvent` as mentioned above: ## Appendix ### You can't access the gas coin in a sponsored transaction In a sponsored transaction, you cannot use the gas object provided by Shinami for other purposes. For example, you cannot write `const [coin] = txb.splitCoins(txb.gas,[txb.pure(100)]);` because it's accessing `txb.gas`. If you try to sponsor a TransactionKind that uses the gas object you will [get a JSON-RPC -32602 error](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api). ### Check a fund's balance and deposit more SUI in the fund You can use our [`gas_getFund`](/api-docs/sui/gas-station/api#gas-getfund) endpoint to check the balance of the Gas Station fund tied to the request's API access key. We generate a deposit address for the fund when you look it up in the Shinami dashboard - go ahead and do that now ([see how](/help-center/sui/gas-station-faq#faq)). If you don't, the `depositAddress` will be `null` - which is why we guard against it in the code below. **Understand the `checkFundBalanceAndDepositIfNeeded`** **Note:** In order to run the `checkFundBalanceAndDepositIfNeeded` function, you'll need to have a SUI coin in the address controlled by the KeyPair we're using as a sender. It takes that coin's object id as its lone argument. A simple options is to [use the faucet](https://docs.sui.io/guides/developer/getting-started/get-coins) to send 1 SUI to that address. The function does the following: 1. Sets a minimum fund balance (in MIST), above which it will make a deposit. 2. Calls `getFund` to obtain the fund information. 3. Checks that it has a deposit address and if the MIST available for new sponsorships is less than the minimum fund balance. We subtract `inFlight` from `balance`, because `inFlight` is what's already reserved for your existing, active sponsorships. 4. If the conditions are met, it creates a `GaslessTransaction` that transfers the coin passed into the function to your fund. Otherwise, it returns `underfined` so we know not to poll the Full node for a transaction. Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami Typescript SDK expandable theme={null} async function checkFundBalanceAndDepositIfNeeded(suiCoinObjectIdToDeposit: string, senderAddress: string): Promise { const MIN_FUND_BALANCE_MIST = 50_000_000_000; // 50 SUI const { balance, inFlight, depositAddress } = await gasStationClient.getFund(); // Deposit address can be null - see our Help Center for how to generate an address: // https://docs.shinami.com/help-center/sui/gas-station-faq#how-do-i-generate-and-find-the-deposit-address-of-a-fund%3F if (depositAddress && ((balance - inFlight) < MIN_FUND_BALANCE_MIST)) { // We're not actually checking it's a SUI coin we're transferring, which you should do. // We're also going to sponsor this with the gas fund we're depositing to, which only // works if there's a little SUI left. return await transferObjectToRecipientGaslessTransaction( suiCoinObjectIdToDeposit, senderAddress, depositAddress ); } console.log("No deposit because no deposit address or a balance above the minimum you've set."); return undefined; } ``` **Set the sender-owned SUI coin ID and Un-comment the function** 1. Set the value of `SUI_COIN_TO_DEPOSIT_ID` to the object ID of a SUI coin owned by the sender. This function is one of the reasons we started the tutorial by having you hard code a private key: so that you had a fixed sender you could send a SUI coin to. 2. Make sure the `checkFundBalanceAndDepositIfNeeded` function is the only sample code function uncommented. ```bash Shinami TypeScript SDK expandable theme={null} // Values for some of the commented out function calls. // Objects must be owned by the sender controlled by the KeyPair. const SUI_COIN_TO_DEPOSIT_ID = "0x0d0c06d477"; // too short for an actual ID const COIN_TO_SPLIT_FROM_ID = "{{SUIcoinObjectID}}"; const COIN_TO_MERGE_ID = "{{SUIcoinObjectID}}"; const OBJ_ID_TO_TRANSFER = "{{objId}}"; const RECIPIENT_ADDRESS = "{{SuiAddress}}"; // 5. Generate the GaslessTransaction for sponsorship const gaslessTx = await // clockMoveCallGaslessTransaction(); // clockMoveCallGaslessTransactionAlternateVersion(); // clockMoveCallGaslessTransactionOfflineBuildManualBudget(); checkFundBalanceAndDepositIfNeeded(SUI_COIN_TO_DEPOSIT_ID, SENDER_ADDRESS); // splitCoinOwnedByGaslessTransaction(COIN_TO_SPLIT_FROM_ID, SENDER_ADDRESS); // mergeCoinsGaslessTransaction(COIN_TO_SPLIT_FROM_ID, COIN_TO_MERGE_ID, SENDER_ADDRESS); // transferObjectToRecipientGaslessTransaction(OBJ_ID_TO_TRANSFER, SENDER_ADDRESS, RECIPIENT_ADDRESS); ``` **Run the code** Make sure you've saved your changes to the file from above. Run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript. Then, run `node build/gas_station.js`. You can look up the digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure you've set the explorer to Testnet first). You can also [view the deposit history of your fund](/help-center/aptos/gas-station-faq#faq) on the ["Funds" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_funds) of your Shinami Dashboard. ### Tips for setting your sponsorship budget **Auto-budgeting** When you omit the `gasBudget` parameter in a sponsorship request using our [Gas Station API](/api-docs/sui/gas-station/api#gas-sponsortransactionblock) or our [Invisible Wallet API](/api-docs/sui/wallet-services/invisible-wallet-api#shinami-wal-executegaslesstransactionblock), we estimate the transaction cost for you. We then add a buffer of 5% for non-shared objects and 25% for shared objects. The larger buffer for shared objects is because in the time between sponsorship and execution, shared objects can change in a way that increases their transaction cost due to transactions from other apps and individuals. Therefore, we encourage you to execute sponsored transactions quickly if possible. While we believe our buffers work well in most cases, we encourage you to monitor the success rate of your auto-budgeted transactions to gauge whether your specific use-case requires manually setting an even larger gasBudget. As a part of auto-budgeting, we put your `transactionBytes` through a [sui\_dryRunTransactionBlock](/api-docs/sui/node-service/json-rpc/write-api#sui-dryruntransactionblock) request as a free service before we attempt to sponsor it. This call will generate error messages for certain invalid transactions, such as if the `transactionBytes` are transferring an object that's not owned by the `sender` address you provide. We'll return these errors back to you, which should be the same as if you had made a `sui_dryRunTransactionBlock` request yourself. We do not do this step if you manually budget, so any issues that would be caught by `sui_dryRunTransactionBlock` will instead produce an error when you try to execute the transaction. **Manual budgeting** When you provide a value for the `gasBudget` parameter, remember that: 1. The budget set for a transaction along with a fee will be "held" from your gas credits fund while the transaction is in-flight. Funds withheld cannot be used for new sponsorships. Our sponsorship fee is also a percentage of your gas budget (see full explanation in the ["Sui Gas Station" tab of the Billing page](https://app.shinami.com/billing/#sui_gas) in your Shinami dashboard. So, you don't want to set your gas budget unnecessarily high. 2. However, if your budget is set too low for the chain to process, the transaction will fail, but gas credits will still be deducted for the computation cost of running the failed transaction on the Sui blockchain. Note that the reference gas price and storage price can change over time, so a similar transaction's cost one day might be different on another day. 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 a Sui explorer like [Suivision](https://suivision.xyz/) or [Suiscan](https://suiscan.xyz/). You can also use [`sui_devInspectTransactionBlock`](/api-docs/sui/node-service/json-rpc/write-api#sui-devinspecttransactionblock) to estimate the gas cost needed by the transaction. However, `sui_devInspectTransactionBlock` does not catch all transaction execution errors and should not be used for testing the correctness of a transaction. [`sui_dryRunTransactionBlock`](/api-docs/sui/node-service/json-rpc/write-api#sui-dryruntransactionblock) is better for testing transaction correctness but requires an attached gas object, making it a little harder to test with. There's an overview of how gas fees are calculated [here](https://docs.sui.io/concepts/tokenomics/gas-in-sui) for more detail. ### Other Transaction examples There are more types of [transactions](https://docs.sui.io/guides/developer/sui-101/building-ptb) than a Move function call like we did above. Below are some additional examples you can sponsor and execute. To run any one of them, you need to: 1. Uncomment only that function in Step 5. 2. Set the required values for the constants that represent its arguments This is the example for `splitCoinOwnedByGaslessTransaction`. Note that ```bash TypeScript expandable theme={null} const SENDER_ADDRESS = keyPairFromSecretKey.toSuiAddress(); console.log("sender address:", SENDER_ADDRESS); // Values for some of the commented out function calls. // Objects must be owned by the sender controlled by the KeyPair. const SUI_COIN_TO_DEPOSIT_ID = "{{SUIcoinObjectID}}"; const COIN_TO_SPLIT_FROM_ID = "0x0d0c06d477"; // too short for an actual ID const COIN_TO_MERGE_ID = "{{SUIcoinObjectID}}"; const OBJ_ID_TO_TRANSFER = "{{objId}}"; const RECIPIENT_ADDRESS = "{{SuiAddress}}"; // 5. Generate the GaslessTransaction for sponsorship const gaslessTx = await // clockMoveCallGaslessTransaction(); // clockMoveCallGaslessTransactionAlternateVersion(); // clockMoveCallGaslessTransactionOfflineBuildManualBudget(); // checkFundBalanceAndDepositIfNeeded(SUI_COIN_TO_DEPOSIT_ID, SENDER_ADDRESS); splitCoinOwnedByGaslessTransaction(COIN_TO_SPLIT_FROM_ID, SENDER_ADDRESS); // mergeCoinsGaslessTransaction(COIN_TO_SPLIT_FROM_ID, COIN_TO_MERGE_ID, SENDER_ADDRESS); // transferObjectToRecipientGaslessTransaction(OBJ_ID_TO_TRANSFER, SENDER_ADDRESS, RECIPIENT_ADDRESS); ``` Once you've set things for the transaction you want: Save your changes. Run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript. Then, run `node build/gas_station.js`. You can look up the digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure you've set the explorer to Testnet first). ### Another way to build a TransactionKind Above, we called the `clockMoveCallGaslessTransaction` function, which uses the `buildGaslessTransaction` helper function in our TypeScript SDK to build the transaction bytes and turn the bytes into a Base64 string in one request. If you have a need, you can also break those two steps up. The `clockMoveCallGaslessTransactionAlternateVersion()` does this by: 1. Creating a new `Transaction` and populating it with the Move call. 2. Building the transaction (with `onlyTransactionKind: true` because this will be a sponsored transaction). 3. Converting the result to a Base64 string 4. Returning a `GaslessTransaction`. ```bash TypeScript expandable theme={null} async function clockMoveCallGaslessTransactionAlternateVersion() : Promise { let txb = new Transaction(); 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), '') ); console.log("\nTransactionKind base64 string for sponsorship:"); console.log(gaslessPayloadBase64); return { txKind: gaslessPayloadBase64, sender: SENDER_ADDRESS, gasBudget: undefined, gasPrice: undefined }; } ``` **Note:** Building transaction blocks will issue requests to your configured node RPC. For more information on how Sui's programmable transaction blocks are built, see our [TransactionBlock.build() guide](/developer-guides/sui/move-guides/transaction-block-build-requests). **Run the code** **Un-comment the function** Make sure the `clockMoveCallGaslessTransactionAlternateVersion` is the only function un-commented in Step 5: ```bash Shinami TypeScript SDK theme={null} // 5. Generate the GaslessTransaction for sponsorship const gaslessTx = await // clockMoveCallGaslessTransaction(); clockMoveCallGaslessTransactionAlternateVersion(); // clockMoveCallGaslessTransactionOfflineBuildManualBudget(); // checkFundBalanceAndDepositIfNeeded(SUI_COIN_TO_DEPOSIT_ID, SENDER_ADDRESS); // splitCoinOwnedByGaslessTransaction(COIN_TO_SPLIT_FROM_ID, SENDER_ADDRESS); // mergeCoinsGaslessTransaction(COIN_TO_SPLIT_FROM_ID, COIN_TO_MERGE_ID, SENDER_ADDRESS); // transferObjectToRecipientGaslessTransaction(OBJ_ID_TO_TRANSFER, SENDER_ADDRESS, RECIPIENT_ADDRESS); ``` **Run the code** Make sure you've saved your changes to the file from above. Run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript. Then, run `node build/gas_station.js`. You can look up the digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure you've set the explorer to Testnet first). ### Check the status of a sponsorship **Why you may not need to check the status of a sponsorship** For many use cases, you will not need to check the status of your sponsorships. This is because when you get a successful response from the `gas_sponsorTransactionBlock` request, you will immediately get a sender signature and submit it to your Node Service for execution (and therefore you'll know the status). Even in the case when a long time elapses between sponsoring the transaction and submitting it to the Sui network, there may not be a need to check the status. If the transaction is executed more than an hour after sponsorship, there will be an error with the associated gas object (because we enforce a 1 hour TTL). If you and the user still want to proceed with the transaction, you can just go through the process again to re-build it and re-sponsor it. Finally, we return the transaction digest of the sponsored transaction in the `txDigest` response field. This can be used with a [`sui_getTransactionBlock` request](/api-docs/sui/node-service/json-rpc/read-api-events-transactions#sui-gettransactionblock) to a Sui Node service to check on the status of the transaction. So, you have the option to check on the status of the transaction directly, rather than indirectly through checking the attached sponsorship. **How to check the status of a sponsorship** All of that said, there may be certain cases where your application needs to check the status of a sponsorship. To use our method for this: 1. Uncomment out the `await checkSponsorshipStatusExample();` line beneath the `checkSponsorshipStatusExample` function, as shown below. 2. Save the changes, transpile the code to JavaScript by running the `tsc` command, and then run the resulting file with `node build/gas_station.js` ```bash TypeScript theme={null} async function checkSponsorshipStatusExample() : Promise { const gaslessTx = await clockMoveCallGaslessTransaction(); gaslessTx.sender = SENDER_ADDRESS; const sponsorship = await gasStationClient.sponsorTransaction(gaslessTx); const sponsorshipStatus = await gasStationClient.getSponsoredTransactionStatus( sponsorship.txDigest ); console.log("sponsored txDigest:", sponsorship.txDigest); console.log("Sponsorship Status:", sponsorshipStatus); } await checkSponsorshipStatusExample(); ``` **Note:** Note that the status will likely be `IN_FLIGHT` even if you've just executed the transaction. This is because it can take a little time after execution for our system to register that the gas object for the transaction has been used. You can read about the possible sponsorship statuses on our [Sui Gas Station API doc](/api-docs/sui/gas-station/api#gas-getsponsoredtransactionblockstatus). ### Getting the lowest latency possible **Options to consider** Generally, customers don't have latency concerns with our Gas Station. At the time of writing this, our Mainnet p95 sponsorship latency across all customers in the last month was 102.7ms, with an average of 45.5ms. This is the latency from when a request hits our server to when we return a response. This latency includes sponsorships that use our auto-budgeting (the vast majority) and those that don't. If your use case requires the lowest latency possible, you can try one or both of the options below. The size of the savings varies a lot depending on your location and your transactions, so you should test with and without these behaviors and only use one if it's meaningful for you. Also, note that the latencies you see on Testnet may not be the same on Mainnet (but should give you an idea of the magnitude of the savings you can achieve). 1. Build your transaction [offline](https://docs.sui.io/guides/developer/sui-101/building-ptb#building-offline). This can save a few requests that would otherwise get made to a Fullnode client [via the `Transaction.build()` request](/developer-guides/sui/move-guides/transaction-block-build-requests). Note, though, that those requests are usually *extremely* fast to fulfill once they reach your Node provider - often sub 10ms - so this is only a latency concern if your servers are geographically far from our those of your Node Service provider. 2. Manually budget your transaction by providing a `gasBudget` value. This skips the [`sui_dryRunTransactionBlock`](/api-docs/sui/node-service/json-rpc/write-api#sui-dryruntransactionblock) request we do as a part of auto-budgeting to determine the right gas budget for your transaction. This `sui_dryRunTransactionBlock` request also catches some transaction errors pre-sponsorship, so you'll lose this check. We generally recommend auto-budgeting because it's fast and sets an ideal budget for you, but if you need the lowest latency possible, manual budgeting speeds things up. How much depends on the complexity of your transactions. Again, our recent p95 for sponsorships- *including* those where we auto-budget and run a `sui_dryRunTransactionBlock` - was just over 100ms on Mainnet. Still, we see some customers whose `sui_dryRunTransactionBlock` p95 latency by itself is > 100ms. So, if you're sponsoring very complex transactions, you could save a meaningful amount of time with manual budgeting. 1. If you choose to do manual budgeting, you still have the option to do a `sui_dryRunTransactionBlock` before requesting sponsorship so that you maintain low latency from the point of asking for a sponsorship onward and you still get the benefits of error checks on your transaction and better gas estimation. **Understand the code** We added a method to our sample code that shows how to do both of these things called `clockMoveCallGaslessTransactionOfflineBuildManualBudget()`. The method: 1. Provides all the information needed about the input object offline (see [this doc](https://docs.sui.io/guides/developer/sui-101/building-ptb#building-offline) for more info). 2. Builds the transaction without a Full node client and sets the gas budget manually. ```bash TypeScript expandable theme={null} async function clockMoveCallGaslessTransactionOfflineBuildManualBudget(): Promise { const sharedClockInput = Inputs.SharedObjectRef({ objectId: "0x6", mutable: false, initialSharedVersion: 1 }); return await buildGaslessTransaction( (txb) => { txb.moveCall({ target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access", arguments: [txb.object(sharedClockInput)], }); }, // 'buildGaslessTransaction' includes a Transaction.build() call. We don't pass in a node client since we're building offline. { // See our manual bugeting tips: https://docs.shinami.com/developer-guides/sui/tutorials/gas-station-backend-only#tips-for-setting-your-sponsorship-budget gasBudget: 15_000_000 } ); } ``` **Run the code** **Un-comment the function** Make sure the `clockMoveCallGaslessTransactionAlternateVersion` is the only function un-commented in Step 5: ```bash Shinami TypeScript SDK theme={null} // 5. Generate the GaslessTransaction for sponsorship const gaslessTx = await // clockMoveCallGaslessTransaction(); // clockMoveCallGaslessTransactionAlternateVersion(); clockMoveCallGaslessTransactionOfflineBuildManualBudget(); // checkFundBalanceAndDepositIfNeeded(SUI_COIN_TO_DEPOSIT_ID, SENDER_ADDRESS); // splitCoinOwnedByGaslessTransaction(COIN_TO_SPLIT_FROM_ID, SENDER_ADDRESS); // mergeCoinsGaslessTransaction(COIN_TO_SPLIT_FROM_ID, COIN_TO_MERGE_ID, SENDER_ADDRESS); // transferObjectToRecipientGaslessTransaction(OBJ_ID_TO_TRANSFER, SENDER_ADDRESS, RECIPIENT_ADDRESS); ``` **Run the code** Make sure you've saved your changes to the file from above. Run `tsc` in the `shinami-examples/sui/typescript/backend_examples` directory to transpile the code to JavaScript. Then, run `node build/gas_station.js`. You can look up the digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure you've set the explorer to Testnet first). ### Integrating Gas Station with Shinami zkLogin wallet API Shinami's [zkLogin Wallet API](/api-docs/sui/wallet-services/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](https://docs.sui.io/concepts/cryptography/zklogin#the-complete-zklogin-flow). 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](/developer-guides/sui/tutorials/zklogin-nextjs-app-template). ### Integrating Gas Station with Shinami Invisible Wallets Shinami [Invisible Wallets](/api-docs/sui/wallet-services/invisible-wallet-api) 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](/developer-guides/sui/tutorials/invisible-wallets) that includes using them with Gas Station. # Gas Station: how to integrate frontend transaction signing Source: https://docs.shinami.com/developer-guides/sui/tutorials/gas-station-with-frontend-signing How to integrate Shinami Gas Station transaction sponsorship with signing from a connected browser wallet. ## Overview This guide will help you integrate connected browser wallet signing with Shinami Gas Station transaction sponsorship. We show an integration flow diagram with links to associated sample code. 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 API 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](/developer-guides/core-integration-topics/authentication-and-api-keys#disable-enable-or-delete-a-key) in your Shinami dashboard. 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](/developer-guides/sui/tutorials/gas-station-backend-only) for guidance. Finally, after you review the images and code below, if you get stuck on something and have a question feel free to [reach out to us](/help-center/overview#contacting-support). ## Examples ### Sample app overview We've created a sample app with a React frontend that uses [Sui dApp kit](https://sdk.mystenlabs.com/dapp-kit) and a backend using the Shinami TypeScript SDK. It integrates with Sui Wallet and a few other connected browser wallets out of the box. We've included it in our [`shinamicorp/shinami-examples` repo](https://github.com/shinamicorp/shinami-examples/tree/main/sui/typescript/dapp_kit_example) . 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. Otherwise, you can just follow the diagram and associated links to the code below. ### Build and sponsor on the BE, sign and submit on the FE **Image** **Summary of steps** The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on [GitHub](https://github.com/shinamicorp/shinami-examples/tree/main/sui/typescript/dapp_kit_example). The key functions in the sample app are [`connectedWalletTxBEBuildFESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/client/App.tsx#L130) on the frontend and [`/buildSponsoredtx`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/server/main.ts#L109) on the backend. 1. *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 two integers the user submitted in the form on the page. 2. *Backend:* Build a gasless Move call transaction with the numbers the user provided. Because we are using the Shinami TypeScript SDK, this produces a [`GaslessTransaction`](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/clients/src/sui/gas.ts#L18), which is just an interface that contains all the required and optional values our `gas_sponsorTransactionBlock` endpoint accepts. 3. *Backend:* Set the sender's address (provided by the frontend) and get the transaction sponsored via a [`gas_sponsorTransactionBlock`](/api-docs/sui/gas-station/api#gas_sponsortransactionblock) request to Shinami's Gas Station. 4. *Backend:* Handle the response and, if successful, return the sponsored transaction and the sponsor's signature to to the frontend for the sender to sign. 5. *Frontend:* Obtain the sender's signature over the transaction. 6. *Frontend:* Submit the transaction and both signatures to the chain with an `executeTransaction` request. 7. *Frontend:* Handle the response as needed. In our sample app, we poll a Sui Full node until it has a record of the transaction and then print the user's math result that was included in the transaction. ### Build and sponsor on the BE, sign on the FE, submit on the BE **Image** **Summary of steps** The links in the steps below take you to the relevant code locations in our sample React + TypeScript app on [GitHub](https://github.com/shinamicorp/shinami-examples/tree/main/sui/typescript/dapp_kit_example). The key functions in the sample app are [`connectedWalletTxBEBuildBESubmit`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/client/App.tsx#L106) on the frontend and [`/buildSponsoredtx`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/server/main.ts#L109) and [`/executeSponsoredTx`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/server/main.ts#L131) on the backend. 1. *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 two integers the user submitted in the form on the page. 2. *Backend:* Build a gasless Move call transaction with the numbers the user provided. Because we are using the Shinami TypeScript SDK, this produces a [`GaslessTransaction`](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/clients/src/sui/gas.ts#L18), which is just an interface that contains all the required and optional values our `gas_sponsorTransactionBlock` endpoint accepts. 3. *Backend:* Set the sender's address (provided by the frontend) and get the transaction sponsored via a [`gas_sponsorTransactionBlock`](/api-docs/sui/gas-station/api#gas_sponsortransactionblock) request to Shinami's Gas Station. 4. *Backend:* Handle the response and, if successful, return the sponsored transaction to the frontend for the sender to sign. You technically do not need to send the sponsor signature to the FE in step 4 because we are executing the transaction on the backend, but it's easy to pass it back and forth. 5. *Frontend:* Obtain the sender's signature over the transaction. 6. *Frontend:* Make a request to the backend to submit the transaction to the chain. Include the transaction and sponsor signature, along with the newly-acquired sender signature. 7. *Backend:* Submit the transaction and both signatures to the chain with an `executeTransaction` request. 8. *Backend:* Handle the response as needed. 9. *Backend:* Response to the frontend. In our sample app, we send back the `SuiTransactionBlockResponse` from the Full node. 10. *Frontend:* Handle the response as needed. In our sample app, we poll a Sui Full node until it has a record of the transaction and then print the user's math result that was included in the transaction. ### Other flows in our sample app * [Build on the FE, sponsor on the BE, and and submit on the FE.](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/dapp_kit_example/src/client/App.tsx#L155) ## Appendix ### Signing with an embedded zkLogin wallet tied to your app This follows a very similar flow to the example above. The key difference is that the frontend signs with the user's ephemeral KeyPair and the backend assembles the zk signature using the emphemeral signature and the zkProof. For an image of this flow, see the [README of our Next.js zkLogin SDK](https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/packages/nextjs-zklogin#high-level-architecture). # Invisible Wallets: backend-only Source: https://docs.shinami.com/developer-guides/sui/tutorials/invisible-wallets How to utilize the Shinami Invisible Wallet ## Overview In this tutorial, we show you how to use the [Shinami Invisible Wallet API](/api-docs/sui/wallet-services/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. We also offer user-controlled [zkLogin wallet](/api-docs/sui/wallet-services/zklogin-wallet-api) services. Both can be "invisible" from the perspective of the end-user, who benefits from Web3 ownership without the burden of wallet management. The examples below show how to create a new wallet and then sign and execute transactions using that wallet. We'll be constructing [programmable transactions](https://docs.sui.io/guides/developer/sui-101/building-ptb) using the [Sui TypeScript SDK](https://github.com/MystenLabs/sui/tree/main/sdk/typescript/) provided by Mysten. We'll also use the [Shinami Clients SDK](https://www.npmjs.com/package/@shinami/clients) to make JSON-RPC calls to create and use Invisible Wallets. The examples also show how to leverage [Shinami's Gas Station](/product-overviews/sui/gas-station) to seamlessly sponsor, sign and execute a transaction in one request. When you create an Invisible Wallet for a user it won't have any SUI 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 SUI. 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 Sui Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#sui-invisible-wallet-api) as well as the "General Error Codes" section at the top that apply to all Shinami services. ### Usage and Billing FAQ Make sure to check out the Sui Wallet Services [product usage FAQ](/help-center/sui/wallet-services-faq) and [billing FAQ](/help-center/billing/sui-faq#wallet-services) in our Help Center for answers to common questions. ## Tutorial **Notes:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet See the [Sui Gas Station page of our Help Center](/help-center/sui/gas-station-faq)for guidance on how to set up a fund on Testnet if you don't already have one. When you create a Testnet fund, we add some SUI to it so you can test immediately. ### 2. Clone the github repo Clone the [shinami-examples github repo](https://github.com/shinamicorp/shinami-examples/tree/main), cd into the `shinami-examples/sui/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](https://www.typescriptlang.org/download/) ) . If you need to install npm and Node.js, [see here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Below, we'll be using the `invisible_wallet.ts` file in the `shinami-examples/sui/typescript/backend_examples/src` directory. ### 3. Create an API access key with Testnet rights to Gas Station and Wallet Services You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key with rights to `Gas Station` and `Wallet Services` on Testnet. See our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#/create-an-access-key) for info on how to set up a key with rights to all services. Once you've created the key, enter it as the value for `GAS_AND_WALLET_KEY`: Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK theme={null} // 2. Copy your access key value. Must have Testnet rights to all Shinami services. 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](/api-docs/sui/wallet-services/invisible-wallet-api#walletid-and-secret-pairing). You'll need to define a (`walletId`, `secret`) pair first. Choose them and enter them into the file as the values for `WALLET_ONE_ID` and `WALLET_ONE_SECRET` (Step 3 in the code block below). In steps 4-6 below, we set up our Shinami clients, create an instance of a `ShinamiWalletSigner` to make wallet management easier, and then create the Invisible Wallet. Replace all instances of `{{name}}` with the actual value for that name. ```bash Shinami TypeScript SDK expandable theme={null} // 3. Set up a wallet id and an associated secret const WALLET_ONE_ID = "{{walletOneId}}"; const WALLET_ONE_SECRET = "{{walletOneSecret}}"; // 4. Instantiate your Shinami clients const keyClient = new KeyClient(ALL_SERVICES_TESTNET_ACCESS_KEY); const walletClient = new WalletClient(ALL_SERVICES_TESTNET_ACCESS_KEY); const nodeClient = new SuiGrpcClient({ baseUrl: 'https://fullnode.testnet.sui.io:443', network: 'mainnet', }); // 5. Create a signer for the Invisible Wallet const signer = new ShinamiWalletSigner( WALLET_ONE_ID, walletClient, WALLET_ONE_SECRET, keyClient ); // 6. Create the wallet. This request returns the Sui address of an // Invisible Wallet, creating it if it hasn't been created yet const CREATE_WALLET_IF_NOT_FOUND = true; const WALLET_ONE_SUI_ADDRESS = await signer.getAddress(CREATE_WALLET_IF_NOT_FOUND); console.log("Invisible wallet Sui address:", WALLET_ONE_SUI_ADDRESS); ``` You'll use the `WALLET_ONE_SUI_ADDRESS` value in the code below and also to look up your wallet in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure the explorer is set to Testnet first). ### 5. Generate a GaslessTransaction Next, we make a call to `buildGaslessTransaction`, which takes a feePayer transaction and returns Shinami type `GaslessTransaction`. ```bash Shinami TypeScript SDK theme={null} // 7. Generate a GaslessTransaction for sponsorship const gaslessTx = await buildGaslessTransaction( await mintSwordTransaction(), { sui: nodeClient } ); ``` The call to `mintSwordTransaction` builds a Transaction that calls a function on a Move module we've [deployed to Testnet](https://testnet.suivision.xyz/package/0x86841b9e38726ee77e4720861ddb3a4e4518afdcf84a1972b952568ee59ffe70?tab=Code). This is a simple example of a function that mints an NFT. The Move code for the package lives our sample code repo at [`shinami-examples/sui/move/simple_nft/sources/simple_nft.move`](https://github.com/shinamicorp/shinami-examples/blob/main/sui/move/simple_nft/sources/simple_nft.move). It takes the following arguments: 1. The name of the item as a String (here a "Basic sword"). 2. The IPFS CID of the item's [image](https://increasing-indigo-platypus.myfilebase.com/ipfs/QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV). 3. The sword's attack power. 4. The sword's durability (here 80%). It's meant to simulate minting an NFT for a user of a game who just earned an item. However, in a real game you might have other ways to set these values - not as simple function arguments. To learn more about NFTs on SUI and find links to helpful documentation, see our [guide to NFTs on SUI](/developer-guides/sui/move-guides/nfts). ```bash Shinami TypeScript SDK theme={null} async function mintSwordTransaction(): Promise { const tx = new Transaction(); tx.moveCall({ target: "0x86841b9e38726ee77e4720861ddb3a4e4518afdcf84a1972b952568ee59ffe70::sword::mint", arguments: [ tx.pure.string("Basic sword"), // name tx.pure.string("QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV"), // IPFS CID tx.pure.u16(5_000), // attack power tx.pure.u8(80) // remaining durability ] }); return tx; } ``` Building transactions creates requests to your configured node RPC. For more information on how Sui's programmable transaction blocks are built, see our [Transaction.build() guide](/developer-guides/sui/move-guides/transaction-block-build-requests). ### 6: Sponsor, sign, and execute a transaction **Understand the code** In Step 8, you have three methods to choose from. We'll be running the first, `sponsorSignExecuteInOneRequest`. The other two functions are reviewed in the Appendix. After executing the transaction we wait for the Full node we're using to have processed the checkpoint with the transaction in it and then we print out the status and digest (which you can look up in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home)). ```bash Shinami TypeScript SDK theme={null} // 8. Choose a sample code method to run const txDigest = await sponsorSignExecuteInOneRequest(signer, gaslessTx); // sponsorSignExecuteInThreeRequests(signer, gaslessTx); if (txDigest != undefined) { const txInfo = await nodeClient.waitForTransaction({ digest: txDigest }); // You can look up the digest in a Sui explorer - make sure to switch to Testnet console.log("\ntxDigest: ", txDigest); console.log("status:", txInfo.Transaction?.status); } ``` This method is why we needed an API access key to rights with all services, as it uses Gas Station to sponsor the transaction and produce the sponsor's signature and Wallet Service to sign the sponsored transaction as the sender. The method then uses the configured node RPC to execute the transaction. The `sponsorSignExecuteInOneRequest` function is very short because we take advantage of the Invisible Wallet API's `executeGaslessTransaction` method, which sponsors a transaction, has the Invisible Wallet sign it, and then submits it to the Sui network. ```bash Shinami TypeScript SDK theme={null} async function sponsorSignExecuteInOneRequest(signer: ShinamiWalletSigner, gaslessTx: GaslessTransaction): Promise { const sponsorSignAndExecuteResponse = await signer.executeGaslessTransaction( gaslessTx, // by not setting gaslessTx.gasBudget we take advantage of Shinami auto-budgeting ["balance_changes", "transaction.digest"] ); return sponsorSignAndExecuteResponse.transaction?.transaction?.digest; } ``` We make things even simpler by taking advantage of our auto-budgeting feature by not setting a value for `gaslessTx.gasBudget`. This way we don't have to do the work to calculate the ideal budget - Shinami does it for us! If you want to learn more about using auto-budgeting or setting a `gasBudget` manually, see [our Gas Station Tutorial's section on the topic](/developer-guides/sui/tutorials/gas-station-backend-only#tips-for-setting-your-sponsorship-budget). **Run the code** Make sure you've saved the changes to the file made above: adding your API access key and wallet ID and secret. In the `shinami-examples/sui/typescript/backend_examples` directory, run `tsc` to transpile the file into JavaScript. Then, run the resulting code with `node build/invisible_wallet.js`. You can look up the transaction digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure the explorer is set to Testnet first). Make sure you're viewing Testnet when you search as that's where we're executing our transactions. There are more examples to try out in the sample code, as explained in the Appendix below. ## Appendix ### Sponsor, sign, and execute a transaction in three requests The below code does the exact same thing we did to start the tutorial, but using three requests instead of one. That's more work, but it gives you greater flexibility if you need it. **Explore the `signSponsorExecuteInThreeRequests` function** This function: 1. Makes a sponsorship request to Shinami Gas Station 2. Signs the Transaction returned by Gas Station (which now has gas information). 3. Submits the signed, sponsored Transaction to a Node service to execute it on chain. ```bash TypeScript expandable theme={null} async function sponsorSignExecuteInThreeRequests(signer: ShinamiWalletSigner, gaslessTx: GaslessTransaction): Promise { // 1. Sponsor the GaslessTransaction with a call to Gas Station const gasStationClient = new GasStationClient(ALL_SERVICES_TESTNET_ACCESS_KEY); gaslessTx.sender = await signer.getAddress(); const sponsoredResponse = await gasStationClient.sponsorTransaction( gaslessTx // by not setting gaslessTx.gasBudget we take advantage of Shinami auto-budgeting ); // 2. Sign the transaction (the Invisible Wallet is the sender) const senderSignature = await signer.signTransaction( sponsoredResponse.txBytes ); // 3. Use the transaction bytes and sponsor signature produced by // `sponsorTransactionBlock` along with the sender's signature const executeSponsoredTxResponse = await nodeClient.executeTransaction({ transaction: fromBase64(sponsoredResponse.txBytes), signatures: [senderSignature.signature, sponsoredResponse.signature], include: { effects: true } }); return executeSponsoredTxResponse.Transaction?.digest; } ``` **Uncomment the function call** Adjust step 8 so that only the `sponsorSignExecuteInThreeRequests` function is un-commented, as shown below: ```bash Shinami TypeScript SDK theme={null} // 8. Choose a sample code method to run const txDigest = await // sponsorSignExecuteInOneRequest(signer, gaslessTx); sponsorSignExecuteInThreeRequests(signer, gaslessTx); ``` **Run the code** Make sure you've saved the changes made above. In the `shinami-examples/sui/typescript/backend_examples` directory, run `tsc` to transpile the file into JavaScript. Then, run the resulting code with `node build/invisible_wallet.js`. You can look up the transaction digest printed to the console in a Sui explorer like [Suivision](https://testnet.suivision.xyz/) or [Suiscan](https://suiscan.xyz/testnet/home) (make sure the explorer is set to Testnet first). Make sure you're viewing Testnet when you search as that's where we're executing our transactions. ### Sign and verify a personal message To prove ownership of a wallet, you can use Wallet Service's [method to sign a personal message](/api-docs/sui/wallet-services/invisible-wallet-api#shinami_wal_signpersonalmessage). The below also uses a method from the Mysten TypeScript SDK to verify whether the signature was from the expected address. **Explore the `signAndVerifyPersonalMessage` function** This function takes a ShinamiWalletSigner (which can sign on behalf of an Invisible Wallet). This function: 1. Creates a message for the Invisible Wallet to sign. 2. Uses the `signPersonalMessage` function to sign it. 3. Encodes the result in the format required by Mysten's `verifyPersonalMessage` function. 4. Calls the `verifyPersonalMessage` function, getting back the PublicKey associated with the signature. This function throws an error if the signature is not valid for the message. Success does not yet confirm the right person signed it. 5. Checks whether the address associated with the signature is the same as the address of our Invisible Wallet. Of course, this is a trivial check because we know we signed it, but in practice someone else (who knows what the message was) is doing the checking. ```bash Shinami TypeScript SDK expandable theme={null} async function signAndVerifyPersonalMessage(signer: ShinamiWalletSigner) : Promise { // 1. Encode the message as a Base64 string const message = "I control the private key, haha!"; const messageAsBase64String = btoa(message); // 2. Sign the message with the Invisible Wallet private key const signature = await signer.signPersonalMessage( messageAsBase64String ); // 3. When we check the signature, we encode the message as a byte array // and not a Base64 string like when we signed it const messageBytes = new TextEncoder().encode(message); // 4. Determine whether the signature is valid for the messsage. // Failure throws an Error with message: `Signature is not valid for the provided message`. // Returns the public key associated with the signature. const publicKey = await verifyPersonalMessageSignature(messageBytes, signature); // 5. Check whether the signer's address matches the Invisible Wallet's address if (publicKey.toSuiAddress() !== await signer.getAddress()) { console.log("\nSignature valid for message, but was signed by a different key pair, :("); return false; } else { console.log("\nSignature was valid and signed by the Invisible Wallet!"); return true; } } ``` **Note:** For compatibility with the Mysten SDK, the `wrapBcs` parameter needs to be set to true. When using the Shinami Clients SDK, it's set to true by default. **Uncomment the function call** Uncomment the function call in step 9: ```bash Shinami TypeScript SDK theme={null} // 9. (optional) Uncomment the next line to sign a personal message with // the Invisible Wallet and then verify that the wallet signed it. await signAndVerifyPersonalMessage(signer); ``` **Run the code** Make sure you've saved the changes made above. In the `shinami-examples/sui/typescript/backend_examples` directory, run `tsc` to transpile the file into JavaScript. Then, run the resulting code with `node build/invisible_wallet.js`. There's no transaction digest associated with this code. You'll just see a printout of whether or not the check was successful. ### Another sample Move call We have another Move function in the code that's not called by default, but which you can call by changing the code [at the top of the file](https://github.com/shinamicorp/shinami-examples/blob/main/sui/typescript/backend_examples/src/invisible_wallet.ts#L40) to: ```bash Shinami TypeScript SDK theme={null} // 7. Generate a GaslessTransaction for sponsorship const gaslessTx = await buildGaslessTransaction( await clockMoveCallTransaction(), { sui: nodeClient } ); ``` The call to `clockMoveCallTransaction` builds a Transaction that calls a function on a Move module we've [deployed to Testnet](https://testnet.suivision.xyz/package/0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16). This is a very simple Move module based on a [sample Move project](https://docs.sui.io/guides/developer/sui-101/access-time) from Mysten. We're calling its one function, `access`, which takes a read-only reference to 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. ```bash Shinami TypeScript SDK theme={null} async function clockMoveCallTransaction(): Promise { const tx = new Transaction(); tx.moveCall({ target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access", arguments: [tx.object('0x6')] }); return tx; } ``` **Notes:** Building transactions creates requests to your configured node RPC. For more information on how Sui's programmable transaction blocks are built, see our [Transaction.build() guide](/developer-guides/sui/move-guides/transaction-block-build-requests). # zkLogin App Template (Next.js) Source: https://docs.shinami.com/developer-guides/sui/tutorials/zklogin-nextjs-app-template How to build your first zkLogin app ## Overview This tutorial will help you build your first Next.js zkLogin app. It uses a [starter template](https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/examples/nextjs-zklogin) we've created that makes it very easy! The app uses the [Shinami Next.js zkLogin SDK](https://www.npmjs.com/package/@shinami/nextjs-zklogin) to make requests to our [zkLogin wallet API](/api-docs/sui/wallet-services/zklogin-wallet-api). It also uses [Shinami Gas Station](/api-docs/sui/gas-station/api) to sponsor transactions for the zkLogin wallets created. Sponsoring your users' transactions removes friction like completing KYC checks to buy SUI for gas. This is especially useful for zkLogin wallets since when you create one the user will not have any SUI in it (and won't even know their wallet address unless you tell them). ### Usage and Billing FAQ Make sure to check out the Sui Wallet Services [product usage FAQ](/help-center/sui/wallet-services-faq) and [billing FAQ](/help-center/billing/sui-faq#wallet-services) in our Help Center for answers to common questions ## Tutorial Steps **Notes:** * This tutorial requires a Shinami account. If you do not have one, you can sign up [here](https://app.shinami.com/signup). * We take you through the process step-by-step below, but if you get stuck you can [reach out to us](/help-center/overview#contacting-support). ### 1. Create a Shinami Gas Station fund on Testnet See the [Sui Gas Station page of our Help Center](/help-center/sui/gas-station-faq) for guidance on how to create a fund. ### 2. Create a Shinami API access key You'll need an API key with rights to both Wallet Services and Gas Station on Testnet. The Gas Station rights should be connected to the Gas Station fund you created in step 1. See our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#/create-an-access-key) for guidance on how to make a key. For security purposes, any access keys with Gas Station or Wallet Services rights should be [used only on your app backend](/developer-guides/core-integration-topics/authentication-and-api-keys#security-callout-when-to-send-from-your-backend) to prevent exposing them. ### 3. Create an OAuth client app for Twitch, Google, Apple, and/or Facebook Below, we integrate with Twitch, Facebook, and Google, but you're only required to integrate with one. See [Mysten's doc](https://docs.sui.io/guides/developer/cryptography/zklogin-integration/developer-account) for information on how to set up a developer account and an OAuth login client app with any of the OpenID Connect providers currently supported. We didn't show a full Apple example because it requires a paid developer account which you may not have, but we add a few key notes about Apple integration below. #### Twitch The redirect URL to set up in the [Twitch developer console](https://dev.twitch.tv/console) for your app is `http://localhost:3000/auth/twitch`. You'll also need to copy the Client ID value that Twitch assigns your app to use as an environmental variable in the next step. #### Facebook For your app in the [Facebook developer console](https://developers.facebook.com/apps/), `http://localhost:3000` is automatically allowed as a redirect URI and doesn't need to be explicitly set. You'll also need to copy the App ID value Facebook assigns your app to use as an environmental variable in the next step. #### Google For your [Google app](https://console.cloud.google.com) , you'll need to set the redirect URI to: `http://localhost:3000/auth/google`. You'll also need to copy the App ID value that Google assigns your app to use as an environmental variable in the next step. #### Apple For Apple, you must authorize a slightly different redirect URI (note the `api/` prefix): `https:///api/auth/apple`. Note that Apple doesn't support either localhost or http. Thus for local testing, you'll need to access the dev server on a mock domain, either by modifying `/etc/hosts` (e.g. adding `127.0.0.1 my-local-site.com`) or through a tunneling service such as [ngrok](https://ngrok.com/). Either way, you must authorize the resulting URL on your Sign in with Apple application. ### 4. Download and configure the sample project **Download** This project is located on [github](https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/examples/nextjs-zklogin). You can use [npm](https://docs.npmjs.com/cli/v10/commands/npm-init), [Yarn](https://classic.yarnpkg.com/en/docs/cli/create/), or [pnpm](https://pnpm.io/) to bootstrap the example: ```bash Shell theme={null} npx create-next-app --example https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/examples/nextjs-zklogin my-zklogin-app yarn create next-app --example https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/examples/nextjs-zklogin my-zklogin-app ``` **Configure** Next, you need to set the values for key environmental variables for the project. These should be in a `.env.local` file which should not committed to git since it contains sensitive information like API access key values. In the root directory of the project, run `cp .env .env.local` on the command line to make a `.env.local` file that's a copy of the `.env` file. Below, we show how to fill in values for the new `.env.local` file. You'll need to make four changes to the file: 1. For each OpenID Connect provider you created a client app with, uncomment the associated `NEXT_PUBLIC_*_CLIENT_ID` variable and set the value to the associated client/app ID. In my examples above, the partial ID values are `2bc0920` for Twitch, `141521` for Facebook, and `766995` for Google. Use your own client/app id values, which will be longer. 2. Uncomment the `IRON_SESSION_SECRET` variable and generate a session secret, e.g. with `openssl rand -hex 32` on the command line. Example output is `bdd51e3b307c7cdae48c3dd2ce83f5e7a0b6db10fdeea0b4d5990f857c156dd7` (but don't share your secrets!) 3. Uncomment the `SHINAMI_SUPER_ACCESS_KEY` variable and set it to the Testnet access key you created with rights to `Wallet Services` and `Gas Station`. Let's imagine it's `sui_testnet_abc123`. This is too short for a real key. Also, do not expose your keys! This key will only be used on the backend, as should be done with any Shinami API access key with Gas Station or Wallet Service rights. Open the file and fill in those values: ```bash Shell expandable theme={null} # OAuth application configs. Configure the ones you want to support. They'll serve as both the auth # mechanism for this app and transaction signing mechanism (zkLogin). Note that if you have multiple # providers configured, the same person will be considered a different user for each provider they # log in with (with different zkLogin wallets), even if they may share the same email. # # Refer to this link for instructions on setting up these OAuth applications: # https://docs.sui.io/guides/developer/cryptography/zklogin-integration/developer-account # NEXT_PUBLIC_GOOGLE_CLIENT_ID='766995' NEXT_PUBLIC_FACEBOOK_CLIENT_ID='141521' NEXT_PUBLIC_TWITCH_CLIENT_ID='2bc0920' # NEXT_PUBLIC_APPLE_CLIENT_ID='' # Example command to generate random secrets: # openssl rand -hex 32 # # IRON_SESSION_SECRET='Randomly generated secret to encrypt your app session cookie' # Obtain your Shinami access keys from https://app.shinami.com. # The example code uses Shinami gas station and wallet services. # SHINAMI_SUPER_ACCESS_KEY='sui_testnet_abc123' # Optional: override the Sui gRPC endpoint for backend use. # Defaults to the Mysten public fullnode for NEXT_PUBLIC_SUI_NETWORK. # SUI_RPC_URL='https://fullnode.testnet.sui.io:443' # Optional: override the Sui gRPC endpoint for frontend use. # Defaults to the Mysten public fullnode for NEXT_PUBLIC_SUI_NETWORK. # NEXT_PUBLIC_SUI_RPC_URL='https://fullnode.testnet.sui.io:443' # Optional: override the Sui GraphQL endpoint for backend use. # Used only for queries with no gRPC equivalent # Defaults to the Mysten public GraphQL endpoint for NEXT_PUBLIC_SUI_NETWORK. # SUI_GRAPHQL_URL='https://graphql.testnet.sui.io/graphql' # 'devnet', 'testnet', or 'mainnet' NEXT_PUBLIC_SUI_NETWORK='testnet' # This example package is deployed on testnet. # Source code: https://github.com/shinamicorp/shinami-typescript-sdk/tree/main/examples/sui-move EXAMPLE_MOVE_PACKAGE_ID='0xd8f042479dcb0028d868051bd53f0d3a41c600db7b14241674db1c2e60124975' ``` We've included the address of a [Move package we've deployed to Testnet](https://testnet.suivision.xyz/package/0xd8f042479dcb0028d868051bd53f0d3a41c600db7b14241674db1c2e60124975?tab=Code) so that your sample project can interact with it, via the environmental variable `EXAMPLE_MOVE_PACKAGE_ID`. This package has a public entry function that takes two numbers and adds them together (I know, super exciting!). Beyond this, our starter template [sets the maxEpoch value](https://github.com/shinamicorp/shinami-typescript-sdk/blob/9a59e0e241198721a2e3d9ca30896d4ea7d60a1e/examples/nextjs-zklogin/pages/auth/login.tsx#L21), which determines the expiration time of the ephemeral KeyPair, to current epoch + 1. This means that when the Sui epoch number moves above that value, a new ephemeral KeyPair and thus JWT will be needed, so the user will need to complete the OpenID authentication flow again. If you instead use `relativeToCurrentEpoch(sui, 0)` for example, the ephemeral KeyPair will expire when the current Sui epoch does. For more on KeyPair expiration, see [Sui Foundation's zkLogin doc](https://docs.sui.io/concepts/cryptography/zklogin#get-jwt-token). ### 5. Run and interact with the app #### Run the app and login Now, you can run the project! On the command line, run `npm run dev` . Then, visit [http://localhost:3000](http://localhost:3000) in your browser and you'll be greeted with prompt to sign in. Click "Sign in", and you'll see an option for each of the OpenID Connect providers you have a `*_CLIENT_ID` environmental variable for. Since we didn't set a value for Apple but did for the other three, we see the following: Choose one and log in. For me, I chose Twitch: After clicking "Authorize", wait a few seconds for the OpenID Connect flow to complete. #### Make a Move function call Now, you should be successfully logged in and see the following: Click "Sui calculator". You'll see: When you enter values and click "Calculate on Sui": 1. The frontend grabs the numbers the user entered (which will be the arguments to the Move function call) and [sends a request](https://github.com/shinamicorp/shinami-typescript-sdk/blob/951cdfeb9494f950506df2dff961583f542096c7/examples/nextjs-zklogin/pages/protected.tsx#L30) to the `/api/add/tx` endpoint via a [via a mutation](https://github.com/shinamicorp/shinami-typescript-sdk/blob/951cdfeb9494f950506df2dff961583f542096c7/examples/nextjs-zklogin/lib/hooks/api.ts#L27). 2. The backend [generates the gasless transaction](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/examples/nextjs-zklogin/pages/api/add/%5B...api%5D.ts#L23) and then makes a request to Shinami Gas Station to [sponsor the transaction](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/nextjs-zklogin/src/server/pages/tx.ts#L73) using the Testnet fund linked to the `SHINAMI_SUPER_ACCESS_KEY` you provided. 3. The frontend [signs](https://github.com/shinamicorp/shinami-typescript-sdk/blob/951cdfeb9494f950506df2dff961583f542096c7/packages/nextjs-zklogin/src/client/hooks/tx.ts#L45) the sponsored transaction with the zkLogin wallet's ephemeral KeyPair. And then makes a request to the `/api/add/tx` endpoint. 4. The Backend [assembles the zkLogin signature](https://github.com/shinamicorp/shinami-typescript-sdk/blob/main/packages/nextjs-zklogin/src/server/pages/tx.ts#L95) and then uses free Mysten node service service to submit the transaction to the Sui Testnet with `sui_executeTransactionBlock`. For a helpful image of the full zkLogin flow, see the [Sui Foundation's zkLogin doc](https://docs.sui.io/concepts/cryptography/zklogin#the-complete-zklogin-flow) Assuming you have added SUI to your Gas Station, enter 1 + 2, click "Calculate on Sui" and wait a few seconds. During the processing, you'll see output like the following in the terminal window running the project: `Preparing add tx for zkLogin wallet 0x6a7b07da9d2c210bbbe044dbe222225ae20dd4584a4d8ee9a804839f8256ea3c`. Then, shortly thereafter, you'll see the result and the associated transaction digest (your digest will be different): Clicking on the transaction digest link will take you to the transaction in the SuiVision explorer. If you choose the "User Signatures" tab, you'll see that two signatures were submitted - a zkLogin signature from the sender (your zkLogin wallet) and an ED25519 gas sponsor signature from Shinami's Gas Station: If you visit the ["Completed transactions" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_completed_tx) of your Shinami dashboard you can see the transaction you just executed. Note, though, that sponsored transactions can remain in the "In flight transactions" tab after a successful execution for a minute or so (until our gas object cleanup job runs). So, check that tab if needed. #### A note on zkLogin addresses If you click "Sign out" in your localhost tab and then sign in via the same `(OpenID provider,username)` pair, you'll have the same zkLogin wallet address. Signing in with a different combination will create a different address (see more info on what makes up a zkLogin address in our [zkLogin wallet API doc](/api-docs/sui/wallet-services/zklogin-wallet-api#zklogin-addresses)). As you can see, if I go back to [http://localhost:3000/](http://localhost:3000/) and sign out and then log in with Facebook, I have no recent transactions because this is a different address: ## Conclusion You've just successfully set up and run a zkLogin app that creates zkLogin wallets and executes sponsored transactions for them! For more information on zkLogin, see: * Our [zkLogin wallet API](/api-docs/sui/wallet-services/zklogin-wallet-api) * The [Sui Foundation's documentation about zkLogin](https://sui.io/zklogin) # Gas Station Source: https://docs.shinami.com/help-center/aptos/gas-station-faq Answers to frequently asked questions about Aptos Gas Station. For billing questions see our [Aptos Billing FAQ](/help-center/billing/aptos-faq). ## FAQ ### How do I create a fund? You create Gas Station funds on the ["Funds" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_funds) in your Shinami dashboard. A fund is tied to one network (Testnet, Mainnet) and you can have up to 10 funds per network. Note that currently there is no option to delete or rename a fund. When you create a fund on Testnet we deposit some APT in it so you can start testing right away. Sorry, we don't do that on Mainnet 🥲. ### How do I generate and find the deposit address of a fund? Once your fund has been created, you can make deposits in order to start sponsoring transactions. On the ["Funds" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_funds) in your Shinami dashboard, click `View` in the table (or `+ Add Gas Credits` above it). Below, we clicked `View`because it take you directly to the fund summary): In the modal that pops up, you will be able to see fund details and deposit history. When you do this the first time, a deposit address is generated. So, you need to do this at least once. This is the address you will make deposits to. Your address will be different than the example shown, and will be different for each of your Gas Station funds. Click the copy button (highlighted) to copy the address. **Note:** Gas Station fund deposits can only be used to sponsor transactions and cannot be withdrawn. That said, you don't need to deposit an amount that will last you for months. You can deposit a smaller amount and [monitor your fund balance](/help-center/sui/gas-station-faq#how-do-i-monitor-my-fund-balance-and-recent-spend%3F). When the balance gets low, you can add enough to last another week or two (or whatever works best for you). ### How do I view a fund's deposit history? On the ["Funds" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_funds) in your Shinami dashboard, click "View" in the "Details" column next to the fund: Then, click the "Deposit history" tab of the modal that comes up. Below, we show a brand new fund with no deposits: ### How do I use a fund for sponsorship? When you set up an API access key with Gas Station rights for a network, you link it to a fund on that network. Then, whenever you make an API request to sponsor a transaction, we use the fund linked to the API key you use. See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for an example of setting up a Gas Station API access key. ### How do I check which fund an access key is tied to? Visit the [Access Keys page](https://app.shinami.com/access-keys) of your Shinami dashboard and follow the steps below: 1. Click on the `+` next to the key in the "Access keys" table to expand the key editor tray. Once you click the `+` it becomes a `-` you can use to close the tray. 2. If the key has Gas Station rights, select the Gas Station tab. 3. View the fund it's tied to in the "Other details" section at the bottom. ### How do I get free Testnet funds for testing? Here are [some options](https://aptos.dev/en/build/apis/faucet-api#integrating-with-the-faucet). ### How do I monitor my fund balance and recent spend? #### Fund balance There are two main ways to do this: 1. Manually in your Shinami Dashboard on the ["Funds" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_funds) by looking at the "Funds" table. The "Gas Credits Available" column is the total amount in a fund, and the "Gas Credits in Flight" column is the portion of that being held for active sponsorships. So, the amount you can use for new sponsorships is "Gas Credits Available" - "Gas Credits in Flight" 2. Through the API with a [`gas_getFund` request](/api-docs/aptos/gas-station/api#gas-getfund). #### Recent spend in your Shinami Dashboard you can view your Gas Station spend over time on the ["Sponsorship Insights" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_sponsorship_insights). Make sure to choose the network and time range you prefer - for example "Aptos Mainnet" + "Last 30 days". In the summary metrics at the top, you can see your "Total sponsorship cost", as well as the "Average sponsorship cost" over the time range. If you scroll down, the "Total sponsorship spend" graph, whose title is at the very bottom of the image, shows your spend over time by fund. ### Where can I see individual transactions I've sponsored? The [Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_completed_tx) has the following two tabs: * **In flight transactions:** Any active but as-yet unused sponsorships will show up here. For example, if you sponsor a transaction but the user does not sign it, or you do not execute it for a few minutes or more. Note that sponsorships can sometimes take a minute or two to show up in this list and then to move out of it into the "completed transactions" list. * **Completed transactions:** See your sponsorships that have been used or that went unused (expired). You can search by sender, transaction digest, or fund name. The data is limited to the last 14 days and 10,000 transactions. For historical sponsorships (from the previous month and older) see how to [download a CSV of your monthly Gas Station activity on Mainnet on the Billing page](/help-center/billing/aptos-faq#how-do-i-view-my-monthly-gas-station-billing-summary-statements). ### How do I view my API requests, errors, and latency? You can view these on the ["API Insights" tab of the Aptos Gas Station page](https://app.shinami.com/aptos/gas/#aptos_gas_insights) in your Shinami dashboard. Here are some tips: 1. Filter by network, access key, method, and/or time range. Changing these filters re-fetches all the insights on the page. 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a HTTP 200 and a JSON-RPC error, broken down by error code. If you filter the page by an individual method, e.g. `gas_sponsorTransactionBlock`, you'll only see the JSON-RPC errors you received on that method. This pairs well with our Error Guide's section on [Aptos Gas Station](/developer-guides/core-integration-topics/error-reference#aptos-gas-station-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `gas_sponsorTransactionBlock` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. # Wallet Services Source: https://docs.shinami.com/help-center/aptos/wallet-services-faq Answers to frequently asked questions about Aptos Wallet Service. For billing questions see our [Aptos Billing FAQ](/help-center/billing/aptos-faq). ## FAQ ### How do I view my API requests, errors, and latency? Visit the ["API Insights" tab of the Aptos Wallet Services page](https://app.shinami.com/aptos/wallet/#aptos_wallet_api_insights) in your Shinami dashboard. Here are some tips: 1. Filter by key, method, and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. In this case, my test account has a lot of non-rate-limit errors so I should investigate what they are. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a JSON-RPC error, grouped by code. If you filter the page by an individual method, e.g. `wal_executeGaslessTransaction`, you'll see the JSON-RPC errors you received on only that method. This graph pairs well with our Error Guide's section on the [Aptos Wallet Service](/developer-guides/core-integration-topics/error-reference#aptos-invisible-wallet-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `wal_executeGaslessTransaction` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. ### How do I view my wallet activity (creations, active wallets)? Visit the ["Wallet Insights" tab of the Aptos Wallet Services page](https://app.shinami.com/aptos/wallet/#aptos_wallet_insights) in your Shinami dashboard 1. Filter by key and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your activity. 3. If you hover over a bar you'll see details about what it represents. 4. The Active wallets graph is below. #### What each graph shows 1. **Wallets created**: successful wallet creations. 2. **Active wallets**: A wallet is considered active if you used it in at least one request during the time period (including the request to create the wallet). In order to provide you with real-time metrics at scale, unique active wallets is an approximate count. Expect an error margin of about 2%, but larger for small sample sizes (e.g. \< 100 wallets). ### What happens if I hit the monthly wallet creation cap? If you hit the monthly creation cap, you won't be allowed to create new wallets this month. However, all your requests with wallets you've already created - signing, etc - will still work. We explain more about the free tier limit and how to upgrade in our [Aptos Biling FAQ "Wallet Services" section (bottom of the page)](/help-center/billing/aptos-faq#wallet-services). You can monitor your wallet creations over the month as [shown above](/help-center/aptos/wallet-services-faq#how-do-i-view-my-wallet-activity-creations-active-wallets). ### How do I migrate my existing users to Invisible Wallets? You can easily create an Invisible Wallet for them behind the scenes. There's no need for them to go through any authentication again. When you create an Invisible Wallet, you associate a `walletID` with each of your users and use that in Invisible Wallet API requests. This can be their existing userID, or another ID you tie to their userID. Just make sure a `walletID` - and its associated `walletSecret` - never change. The steps are: 1. Generate unique (`walletID`, `walletSecret`) pairs for each user. 2. Make an API request to create a wallet for each user. 3. If you are migrating from a pre-existing embedded wallet, you can create and execute a transfer transaction to move the contents to their new, Invisible Wallet. # Aptos Products Source: https://docs.shinami.com/help-center/billing/aptos-faq Answers to frequently asked questions about the costs, viewing usage, etc. ## Gas Station ### What fee does Shinami charge for sponsorships? For a limited time, our Aptos Gas Station will sponsor your transactions without charging any Shinami fees. Your fund will only pay the transaction fee charged by the Aptos blockchain for each sponsored transaction. After this promotional period, we will charge your Gas Station fund a small Shinami fee for each sponsorship created. For future-proofing to minimize fees, we encourage you to set a gas budget (`gas_unit_price × max_gas_amount`) that's not unnecessarily high. ### How do I view my fund balance and recent usage? See our Aptos Gas Station Product FAQ for [helpful guidance](/help-center/aptos/gas-station-faq#how-do-i-monitor-my-fund-balance-and-recent-spend). ### How do I view my monthly Gas Station billing summary statements? We provide a monthly report of your sponsorship activity for your Mainnet funds. Visit the ["Aptos Gas Station tab" of the Billing page](https://app.shinami.com/billing/#aptos_gas) in your Shinami dashboard to see the "Statements" table. You'll find an entry for each month you've been sponsoring transactions on Mainnet. We add an entry for a new month 3-4 days after it ends. You can see a summary of your total sponsorships, as well as the total Aptos transaction fees and Shinami fees for those transactions. Click the `+` next to `All` in the `Fund(s)` column to see a breakdown by fund. I've clicked it below so now it's a `-` that I can click to close the expanded row. You can also download a full report for the month, which is a CSV file containing each transaction sponsorship you made. You can do this by clicking the "Download" button in the "FULL REPORT" column. ## Wallet Services ### What is the cost? When you create a Shinami account, you are automatically put on the free Wallet Services tier. This tier has a monthly wallet creation cap for Aptos wallets as shown in the ["Aptos Wallet Services" tab of the Billing page](https://app.shinami.com/billing/#aptos_wallet) in your Shinami dashboard. If you need more than this, or more than the default QPS, reach out to `support@shinami.com` and we can create a custom plan for you based on your committed usage and QPS needs. Even on the free tier, some Wallet Service requests can incur a cost due to their use of Gas Station to sponsor the transaction. In the API docs it will be clear when Gas Station services are used, and you will be charged as explained in the [Gas Station section above](/help-center/billing/aptos-faq#what-fee-does-shinami-charge-for-sponsorships). # Movement Products Source: https://docs.shinami.com/help-center/billing/movement-faq Answers to frequently asked questions about the costs, viewing usage, etc. ## Gas Station ### What fee does Shinami charge for sponsorships? We charge a small fee per sponsorship to help cover our infrastrucre costs. For full details, visit the [“Movement Gas Station” tab of the Billing page](https://app.shinami.com/billing/#movement_gas) in your Shinami dashboard. ### How do I view my fund balance and recent usage? See our Movement Gas Station Product FAQ for [helpful guidance](/help-center/movement/gas-station-faq#how-do-i-monitor-my-fund-balance-and-recent-spend%3F). ### How do I view my monthly Gas Station billing summary statements? We provide a monthly report of your sponsorship activity for your Mainnet funds. Visit the ["Movement Gas Station tab" of the Billing page](https://app.shinami.com/billing/#movement_gas) in your Shinami dashboard to see the "Statements" table. You'll find an entry for each month you've been sponsoring transactions on Mainnet. We add an entry for a new month 3-4 days after it ends. You can see a summary of your total sponsorships, as well as the total Movement transaction fees and Shinami fees for those transactions. Click the `+` next to `All` in the `Fund(s)` column to see a breakdown by fund. I've clicked it below so now it's a `-` that I can click to close the expanded row. You can also download a full report for the month, which is a CSV file containing each transaction sponsorship you made. You can do this by clicking the "Download" button in the "FULL REPORT" column. ## Wallet Services ### What is the cost? When you create a Shinami account, you are automatically put on the free Wallet Services tier. This tier has a monthly wallet creation cap for Movement wallets as shown in the ["Movement Wallet Services" tab of the Billing page](https://app.shinami.com/billing/#movement_wallet) in your Shinami dashboard. If you need more than this, or more than the default QPS, reach out to `support@shinami.com` and we can create a custom plan for you based on your committed usage and QPS needs. Even on the free tier, some Wallet Service requests can incur a cost due to their use of *other* Shinami services. Namely, executing a sponsored transaction for an Invisible Wallet uses our Gas Station service, and you will be charged for your use of Gas Station [as explained in its billing section](https://app.shinami.com/billing/#movement_gas). In the API docs it will be clear when Gas Station service is used because we call out the need to use an API access key with rights to Gas Station when applicable. # Sui Products Source: https://docs.shinami.com/help-center/billing/sui-faq Answers to frequently asked questions about the costs, viewing usage, etc. ## Gas Station ### What fee does Shinami charge for sponsorships? We charge a small fee per sponsorship. For full details, visit the ["Sui Gas Station" tab of the Billing page](https://app.shinami.com/billing/#sui_gas) in your Shinami dashboard. Our costs for running our Gas Station include on-chain costs to split, merge, and recycle the gas objects used in sponsorship. They also include the cloud costs for running our highly available and scalable infrastructure. ### How do I monitor my fund balance and recent usage? See our Sui Gas Station Product FAQ for [helpful guidance](/help-center/sui/gas-station-faq#how-do-i-monitor-my-fund-balance-and-recent-spend). ### How do I view my monthly Gas Station billing summary statements? Visit the ["Sui Gas Station" tab of the Billing page](https://app.shinami.com/billing/#sui_gas) in your Shinami dashboard to view the "Mainnet Statements" table. You'll find an entry for each month you've been sponsoring transactions on Mainnet. We add an entry for a new month 3-4 days after it ends. You can see a summary of your total sponsorships, as well as the total Sui transaction fees and Shinami fees for those transactions. Click the `+` next to `All` in the `Fund(s)` column to see a breakdown by fund. I've clicked it below so now it's a `-` that I can click to close the expanded row. You can also download a full report for the month, which is a CSV file containing each transaction sponsorship you made. You can do this by clicking the "Download" button in the "FULL REPORT" column. ## Wallet Services ### What is the cost? When you create a Shinami account, you are automatically put on the free Wallet Services tier. This tier has a monthly wallet creation cap for Sui wallets as shown in the ["Sui Wallet Services" tab of the Billing page](https://app.shinami.com/billing/#sui_wallet) of your Shinami dashboard. The limit is applies to the sum of wallets you've created across all wallet types, so we check if `zkLogin_wallets_created_this_month + Invisible_Wallets_created_this_month < free_tier_limit`. If you need more than this, or more than the default QPS, reach out to `support@shinami.com` and we can create a custom plan for you based on your committed usage and QPS needs. Even on the free tier, some Wallet Service requests can incur a cost due to their use of *other* Shinami services. For example, executing a sponsored transaction for an Invisible Wallet uses our Gas Station, and you will be charged for your use as explained in the Gas Station billing section. In the API docs it will be clear when Gas Station is used because we call out the need to use an API access key with rights to those services when applicable. # Gas Station Source: https://docs.shinami.com/help-center/movement/gas-station-faq Answers to frequently asked questions about Movement Gas Station. For billing questions see our [Movement Billing FAQ](/help-center/billing/movement-faq). ## FAQ ### How do I create a fund? You create Gas Station funds on the ["Funds" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_funds) in your Shinami dashboard. A fund is tied to one network (Testnet, Mainnet) and you can have up to 10 funds per network. Note that currently there is no option to delete or rename a fund. When you create a fund on Testnet we deposit some MOVE in it so you can start testing right away. Sorry, we don't do that on Mainnet 🥲. ### How do I generate and find the deposit address of a fund? Once your fund has been created, you can make deposits in order to start sponsoring transactions. On the ["Funds" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_funds) in your Shinami dashboard, click `View` in the table (or `+ Add Gas Credits` above it). Below, we clicked `View`because it take you directly to the fund summary): In the modal that pops up, you will be able to see fund details and deposit history. When you do this the first time, a deposit address is generated. So, you need to do this at least once. This is the address you will make deposits to. Your address will be different than the example shown, and will be different for each of your Gas Station funds. Click the copy button (highlighted) to copy the address. **Note:** Gas Station fund deposits can only be used to sponsor transactions and cannot be withdrawn. That said, you don't need to deposit an amount that will last you for months. You can deposit a smaller amount and [monitor your balance](/help-center/movement/gas-station-faq#fund-balance). When the balance gets low, you can add enough to last another week or two (or whatever works best for you). ### How do I view a fund's deposit history? On the ["Funds" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_funds) in your Shinami dashboard, click "View" in the "Details" column next to the fund: Then, click the "Deposit history" tab of the modal that comes up. Below, we show a brand new fund with no deposits: ### How do I use a fund for sponsorship? When you set up an API access key with Gas Station rights for a network, you link it to a fund on that network. Then, whenever you make an API request to sponsor a transaction, we use the fund linked to the API key you use. See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for an example of setting up a Gas Station API access key. ### How do I check which fund an access key is tied to? Visit the [Access Keys page](https://app.shinami.com/access-keys) of your Shinami dashboard and follow the steps below: 1. Click on the `+` next to the key in the "Access keys" table to expand the key editor tray. Once you click the `+` it becomes a `-` you can use to close the tray. 2. If the key has Gas Station rights, select the Gas Station tab. 3. View the fund it's tied to in the "Other details" section at the bottom. ### How do I get free Testnet funds for testing? Visit [Movement's Testnet faucet](https://faucet.movementnetwork.xyz/). ### How do I monitor my fund balance and recent spend? #### Fund balance There are two main ways to do this: 1. Manually in your Shinami Dashboard on the ["Funds" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_funds) by looking at the "Funds" table. The "Gas Credits Available" column is the total amount in a fund, and the "Gas Credits in Flight" column is the portion of that being held for active sponsorships. So, the amount you can use for new sponsorships is "Gas Credits Available" - "Gas Credits in Flight" 2. Through the API with a [`gas_getFund request`](/api-docs/movement/gas-station/api#gas-getfund). #### Recent spend In your Shinami Dashboard you can view your Gas Station spend over time on the ["Sponsorship Insights" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_sponsorship_insights). Make sure to choose the network and time range you prefer - for example "Movement Testnet" + "Last 30 days". In the summary metrics at the top, you can see your "Total sponsorship cost", as well as the "Average sponsorship cost" over the time range. If you scroll down, the "Total sponsorship spend" graph, whose title is at the very bottom of the image, shows your spend over time by fund. ### Where can I see individual transactions I've sponsored? The [Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_completed_tx) has the following two tabs: * **In flight transactions:** Any active but as-yet unused sponsorships will show up here. For example, if you sponsor a transaction but the user does not sign it, or you do not execute it for a few minutes or more. Note that sponsorships can sometimes take a minute or two to show up in this list and then to move out of it into the "completed transactions" list. * **Completed transactions:** See your sponsorships that have been used or that went unused (expired). You can search by sender, transaction digest, or fund name. The data is limited to the last 14 days and 10,000 transactions. For historical sponsorships (from the previous month and older) see how to [download a CSV of your monthly Gas Station activity on Mainnet on the Billing page](/help-center/billing/movement-faq#how-do-i-view-my-monthly-gas-station-billing-summary-statements). ### How do I view my API requests, errors, and latency? You can view these on the ["API Insights" tab of the Movement Gas Station page](https://app.shinami.com/movement/gas/#movement_gas_insights) in your Shinami dashboard. Here are some tips: 1. Filter by network, access key, method, and/or time range. Changing these filters re-fetches all the insights on the page. 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show entries in the graph. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a HTTP 200 and a JSON-RPC error, broken down by error code. If you filter the page by an individual method, e.g. `gas_sponsorTransactionBlock`, you'll only see the JSON-RPC errors you received on that method. This pairs well with our Error Guide's section on [Movement Gas Station](/developer-guides/core-integration-topics/error-reference#movement-gas-station-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `gas_sponsorTransactionBlock` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. # Wallet Services Source: https://docs.shinami.com/help-center/movement/wallet-services-faq Answers to frequently asked questions about Movement Wallet Service. For billing questions see our [Movement Billing FAQ](/help-center/billing/movement-faq). ## FAQ ### How do I view my API requests, errors, and latency? Visit the ["API Insights" tab of the Movement Wallet Services page](https://app.shinami.com/movement/wallet/#movement_wallet_api_insights) in your Shinami dashboard. Here are some tips: 1. Filter by key, method, and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. In this case, my test account has a lot of non-rate-limit errors so I should investigate what they are. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a JSON-RPC error, grouped by code. If you filter the page by an individual method, e.g. `wal_executeGaslessTransaction`, you'll see the JSON-RPC errors you received on only that method. This graph pairs well with our Error Guide's section on the [Movement Wallet Service](/developer-guides/core-integration-topics/error-reference#movement-invisible-wallet-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `wal_executeGaslessTransaction` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. ### How do I view my wallet activity (creations, active wallets)? Visit the ["Wallet Insights" tab of the Movement Wallet Services page](https://app.shinami.com/movement/wallet/#movement_wallet_insights) in your Shinami dashboard 1. Filter by key and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your activity. 3. If you hover over a bar you'll see details about what it represents. 4. The Active wallets graph is below. #### What each graph shows 1. **Wallets created**: successful wallet creations. 2. **Active wallets**: A wallet is considered active if you used it in at least one request during the time period (including the request to create the wallet). In order to provide you with real-time metrics at scale, unique active wallets is an approximate count. Expect an error margin of about 2%, but larger for small sample sizes (e.g. \< 100 wallets). ### What happens if I hit the monthly wallet creation cap? If you hit the monthly creation cap, you won't be allowed to create new wallets this month. However, all your requests with wallets you've already created - signing, etc - will still work. We explain more about the free tier limit and how to upgrade in our [Movement Biling FAQ "Wallet Services" section (bottom of the page)](/help-center/billing/movement-faq#wallet-services). You can monitor your wallet creations over the month as [shown above](/help-center/movement/wallet-services-faq#how-do-i-view-my-wallet-activity-creations-active-wallets). ### How do I migrate my existing users to Invisible Wallets? You can easily create an Invisible Wallet for them behind the scenes. There's no need for them to go through any authentication again. When you create an Invisible Wallet, you associate a `walletID` with each of your users and use that in Invisible Wallet API requests. This can be their existing userID, or another ID you tie to their userID. Just make sure a `walletID` - and its associated `walletSecret` - never change. The steps are: 1. Generate unique (`walletID`, `walletSecret`) pairs for each user. 2. Make an API request to create a wallet for each user. 3. If you are migrating from a pre-existing embedded wallet, you can create and execute a transfer transaction to move the contents to their new, Invisible Wallet. # Help Center Overview Source: https://docs.shinami.com/help-center/overview ## Categories Guidance on log in, team management, contact email for key service updates, managing mutiple workspaces, and more. How billing works for each (blockchain, product) pair. Answers to common questions, including using your Shinami dashboard. Answers to common questions, including using your Shinami dashboard. Answers to common questions, including using your Shinami dashboard. How to contact Shinami support. ### Account and workspace Guidance on log in, team management, contact email for key service updates, managing mutiple workspaces, and more. Important tips for new Shinami users. ### Billing Gas Station and Wallet Service. Gas Station and Wallet Service. Gas Station and Wallet Service. ### Product FAQ #### Aptos Answers to common questions, including using your Shinami dashboard. Answers to common questions, including using your Shinami dashboard. #### Movement Answers to common questions, including using your Shinami dashboard. Answers to common questions, including using your Shinami dashboard. #### Sui Answers to common questions, including using your Shinami dashboard. Answers to common questions, including using your Shinami dashboard. ### Contacting support If you can't find the answer to your question in our docs, feel free to email us at `support@shinami.com`. Make sure to include: * The email address you log into Shinami with if it's not the one you're sending from (so we can look up your account details). * A detailed description of your question or issue. Screenshots, error codes and messages, and code snippets are always helpful if relevant to your question. Our active support hours are between 09:00-17:00 Pacific Time Monday-Friday. So, depending on the day and your time zone our response time will vary. # Setting up your workspace Source: https://docs.shinami.com/help-center/setting-up-your-workspace A few quick tips to make sure you get off to a great start. ## Welcome to Shinami! This guide is for you if you just made a Shinami account. It shows you key steps to take to set up your account. It doesn't cover anything about your integration. ## Here's how to ### invite a team member, change your contact email and workspace name Maybe you like working alone. That's cool. Skip to the next section. If you prefer working on a team (like me) you'll want to visit the [Workspace page of your dashboard](https://app.shinami.com/workspace) (shown below). **Note:** Only admins can invite others. When you create an account, you're an admin. 1. If you were invited to multiple workspaces, you'll switch between them here. Lucky you. So popular. 2. The workspace button takes you to... you guessed it. 3. Set the workspace to your company, app, or pet's name - something the person you invite will recognize. 4. Invite someone. By default, they're a regular user and you're an admin with the power of a god!!!\* 5. If you mistyped the invited email address, or suddenly decide that you actually *do* want to work alone, you can remove the invite. If the person you invited says they didn't get it, you can resend (make sure to ask them to check their spam folder just in case). 6. This is the email address we send important things like billing emails and occasional emails about key product changes that affect your workspace. By default it's the email address that created the workspace. That's a lot of pressure for one person! If this is better as another email, like `engineering@superapp.io`or `accountspayable@superapp.io`, go ahead and update it now. Currently, we don't send a verification email to the new address, so make sure to double-check the value you enter! 7. Jedi. Sith. Choose a side. 8. Visit the Access Keys and make your first API key! Learn how in our [Authentication and API Keys guide](/developer-guides/core-integration-topics/authentication-and-api-keys#create-an-access-key). 9. Find our services for each chain by expanding the `>`s. 10. Obviously you'll never want to log out of our product, so you can ignore this one. \*Okay, *technically* just the power to change the workspace name, invite and remove teammates, and update billing info. For more on workspaces and roles, see the [Workspaces FAQ page of our Help Center](/help-center/workspace-faq). ### understand billing To read about what you get for free and what you have to pay for, see your dashboard's [Billing page](https://app.shinami.com/billing). It has a tab for each blockchain and then a subtab for each product we offer for that blockchain. ### get help with questions & issues. You need it. I'm kidding. But only kind of: we all need help sometimes. Here are some key resources you'll want to keep in mind: * [Help Center](/help-center/overview) for commons questions about using your dashboard and using each product. * You can always [reach out to us](/help-center/overview#contacting-support). # Gas Station Source: https://docs.shinami.com/help-center/sui/gas-station-faq Answers to frequently asked questions about Sui Gas Station. For billing questions see our [Sui Billing FAQ](/help-center/billing/sui-faq). ## FAQ ### How do I create a fund? You create funds on the ["Funds" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_funds) in your Shinami dashboard. A fund is tied to one network (Testnet, Mainnet) and you can have up to 10 funds per network. Note that currently there is no option to delete or rename a fund. When you create a fund on Testnet we deposit some SUI in it so you can start testing right away. Sorry, we don't do that on Mainnet 🥲. ### How do I generate and find the deposit address of a fund? Once your fund has been created, you can make deposits in order to start sponsoring transactions. Visit the ["Funds" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_funds) in your Shinami dashboard. To find your fund's address, click `View` in the table (or `+ Add Gas Credits` above it). Below, we clicked `View`because it take you directly to the fund summary): In the modal that pops up, you will be able to see fund details and deposit history. When you do this the first time, a deposit address is generated. So, you need to do this at least once. This is the address you will make deposits to. Your address will be different than the example shown, and will be different for each of your Gas Station funds. Click the copy button (highlighted) to copy the address. **Note:** Gas Station fund deposits can only be used to sponsor transactions and cannot be withdrawn. That said, you don't need to deposit an amount that will last you for months. You can deposit a smaller amount and [monitor your fund balance](/help-center/sui/gas-station-faq#how-do-i-monitor-my-fund-balance-and-recent-spend%3F). When the balance gets low, you can add enough to last another week or two (or whatever works best for you). ### How do I view a fund's deposit history? On the ["Funds" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_funds) in your Shinami dashboard, click "View" in the "Details" column next to the fund: Then, click the "Deposit history" tab of the modal that comes up. ### How do I use a fund for sponsorship? When you set up an API access key with Gas Station rights for a network, you link it to a fund on that network. Then, whenever you make an API request to sponsor a transaction, we use the fund linked to the API key you use. See our [Authentication and API Keys](/developer-guides/core-integration-topics/authentication-and-api-keys) guide for an example of setting up a Gas Station API access key. ### How do I check which fund an access key is tied to? Visit the [Access Keys page](https://app.shinami.com/access-keys) of your Shinami dashboard and follow the steps below: 1. Click on the `+` next to the key in the "Access keys" table to expand the key editor tray. Once you click the `+` it becomes a `-` you can use to close the tray. 2. If the key has Gas Station rights, select the Gas Station tab. 3. View the fund it's tied to in the "Other details" section at the bottom. ### How do I get free Testnet funds for testing? Here are [some options](https://docs.sui.io/guides/developer/getting-started/get-coins). ### How do I monitor my fund balance and recent spend? #### Fund balance: There are two main ways to do this: 1. Manually on the ["Funds" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_funds) in your Shinami dashboard, by looking at the "Funds" table. The "Gas Credits Available" column is the total amount in a fund, and the "Gas Credits in Flight" column is the portion of that being held for active sponsorships. So, the amount you can use for new sponsorships is "Gas Credits Available" - "Gas Credits in Flight" * 2. Through the API with a [`gas_getFund request`](/api-docs/sui/gas-station/api#gas-getfund). #### Recent spend View the ["Sponsorship Insights" tab of the Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_sponsorship_insights) in your Shinami dashboard. Make sure to choose the network and time range you prefer - for example "Sui Mainnet" + "Last 30 days". In the summary metrics at the top, you can see your "Total sponsorship cost", as well as the "Average" and "Max sponsorship cost" over the time range. The sponsorship cost is the total cost you pay, so it includes both the Sui network gas transaction fee and the Shinami fee. If you scroll down, the "Total sponsorship spend" graph, whose title is at the very bottom of the image, shows your spend over time by fund. ### Where can I see individual transactions I've sponsored? The [Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_completed_tx) has the following two tabs: * **In flight transactions:** Any active but as-yet unused sponsorships will show up here. For example, if you sponsor a transaction but the user does not sign it, or you do not execute it for a few minutes or more. Note that sponsorships can sometimes take a minute or two to show up in this list and then to move out of it into the "completed transactions" list. * **Completed transactions:** See your sponsorships that have been used or that went unused (expired). You can search by sender, transaction digest, or fund name. The data is limited to the last 14 days and 10,000 transactions. For historical sponsorships (from the previous month and older) see how to [download a CSV of your monthly Gas Station activity on Mainnet on the Billing page](/help-center/billing/sui-faq#how-do-i-view-my-monthly-gas-station-billing-summary-statements). ### How do I view my API requests, errors, and latency? Visit the ["API Insights" tab of Sui Gas Station page](https://app.shinami.com/sui/gas/#sui_gas_insights) in your Shinami dashboard. Here are some tips: 1. Filter by network, access key, method, and/or time range. Changing these filters re-fetches all the insights on the page. 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a HTTP 200 and a JSON-RPC error, broken down by error code. If you filter the page by an individual method, e.g. `gas_sponsorTransactionBlock`, you'll only see the JSON-RPC errors you received on that method. This pairs well with our Error Guide's section on [Sui Gas Station](/developer-guides/core-integration-topics/error-reference#sui-gas-station-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `gas_sponsorTransactionBlock` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. # Wallet Services Source: https://docs.shinami.com/help-center/sui/wallet-services-faq Answers to frequently asked questions about Sui Wallet Services. For billing questions see our [Sui Billing FAQ](/help-center/billing/sui-faq). ## FAQ ### How do I view my API requests, errors, and latency? Visit the ["API Insights" tab of the Sui Wallet Services page](https://app.shinami.com/sui/wallet/#sui_wallet_api_insights) in your Shinami dashboard. Here are some tips: 1. Filter by key, method, and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your error rates. Of course, a low error rate over a long period could hide one day that had a high error-rate, so it's worth occasionally looking at the graphs below. 3. If you hover over a section of a chart bar, you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. There are more graphs below! Scroll down to view error and latency insights. #### What each graph shows 1. **Request count by method**: This shows the count of all requests, including all errors, broken down by method name (assuming you've selected "All methods" in the filter at the top of the page). 2. **Error count by JSON RPC error code**: This shows the count of requests that got a HTTP 200 and a JSON-RPC error, broken down by error code. If you filter the page by an individual method, e.g. `shinami_wal_executeGaslessTransactionBlock`, you'll see the JSON-RPC errors you received on only that method. This pairs well with our Error Guide's sections on the Sui [Invisible Wallet API](/developer-guides/core-integration-topics/error-reference#sui-invisible-wallet-api) and [zkLogin wallet API](/developer-guides/core-integration-topics/error-reference#sui-zklogin-wallet-api). 3. **Error ratio by method**: This shows the JSON-RPC error ratio for each method. For example, if you sent 100 `shinami_wal_executeGaslessTransactionBlock` requests in a time bucket (e.g. a day) and you got a JSON-RPC error on two of them, your error ratio would be `2%` for that method in that time bucket. If you see a high error ratio it's useful to scroll up to the "Request count by method" graph to see how many of that method you sent in a time bucket: a 100% error ratio could happen if you send one request and it gets an error. 4. **Requests latency by method - P50**: This shows the 50th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 5. **Requests latency by method - P95**: This shows the 95th percentile latency for each method across all of its requests, including errors. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 6. **Successful requests latency**: This shows the 50th and 95th percentile latencies for all of your successful requests (no HTTP or JSON-RPC error). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. 7. **Requests with non-rate-limit errors latency**: This shows the 50th and 95th percentile latencies for all of your requests that got a JSON-RPC error (excluding rate-limit-errors since they have very low latency). It does not group by method. This is the latency within our system, so the latency you observe will be a little higher because of network travel time. ### How do I view my wallet activity (creations, active wallets)? Visit the ["Wallet Insights" tab of the Sui Wallet Services page](https://app.shinami.com/sui/wallet/#sui_wallet_insights) in your Shinami dashboard. 1. Filter by key and/or time range. Changing these filters re-fetches all the insights on the page. Note that wallet service keys are network agnostic as wallet-only operations are not tied to Testnet or Mainnet (e.g. creating a wallet). So, if you have a specific key you use for Testnet and Mainnet, you can filter by that key instead of the default of "All keys". 2. Summary metrics for a quick look at your activity. 3. If you hover over a bar you'll see details about what it represents. 4. You can click items in the legend to hide or show them. 5. The Active wallets graph is below. #### What each graph shows 1. **Wallets created**: successful wallet creations. 2. **Active wallets**: A wallet is considered active if you used it in at least one request during the time period (including the request to create the wallet). In order to provide you with real-time metrics at scale, unique active wallets is an approximate count. Expect an error margin of about 2%, but larger for small sample sizes (e.g. \< 100 wallets). ### What happens if I hit the monthly wallet creation cap? If you hit the monthly creation cap, you won't be allowed to create new wallets this month (either zkLogin or Invisible). However, all your requests with wallets you've already created - signing, etc - will still work. We explain more about the free tier limit and how to upgrade in our [Sui Billing FAQ "Wallet Services" section (bottom of page)](/help-center/billing/sui-faq#wallet-services). You can monitor your wallet creations over the month as [shown above](/help-center/sui/wallet-services-faq#how-do-i-view-my-wallet-activity-creations-active-wallets). ### How do I migrate my existing users to Invisible Wallets? You can easily create an Invisible Wallet for them behind the scenes. There's no need for them to go through any authentication again. When you create an Invisible Wallet, you associate a `walletID` with each of your users and use that in Invisible Wallet API requests. This can be their existing userID, or another ID you tie to their userID. Just make sure a `walletID` - and its associated `walletSecret` - never change. The steps are: 1. Generate unique (`walletID`, `walletSecret`) pairs for each user. 2. Make an API request to create a wallet for each user. 3. If you are migrating from a pre-existing embedded wallet, you can create and execute a transfer transaction to move the contents to their new, Invisible Wallet. # Accounts and Workspaces Source: https://docs.shinami.com/help-center/workspace-faq ## Overview Find answers below to frequently asked questions about: ## Account FAQ ### Login options and account linking You can use an email+password or Sign in with Google. Here are a couple of good to knows: 1. If you sign up first with an email+password and then sign in with Google 1. with the same email, we will log you into the same Shinami account+workspace. 2. with a different email, will we create a new Shinami account+workspace. 2. If you sign up first with sign in with Google, and then sign in with email+password 1. if you want to log into the same Shinami account+workspace with an email+password, you need to go through the sign up flow for that email+password (you can't just click "forgot password"). Once you verify your email and create a password, you'll be able to use both login flows to sign into the account+workspace. ## Workspace FAQ ### What is a workspace and where are the settings When you create a Shinami account you have a workspace where you are the admin (this includes when you join Shinami through a workspace invitation). A workspace is a set of Gas Station funds and/or wallet data, API keys, and billing information that can be shared by multiple users. You can find your workspace settings by clicking "Workspace" near bottom of the left nav to go to the [workspace page](https://app.shinami.com/workspace) : Read below to find out how to manage your workspace! ### How do I change my workspace name **Admin-only operation** On the [Workspace page](https://app.shinami.com/workspace) of the Shinami dashboard, admins can change the name of the workspace by clicking the edit button next to the name: then typing in the new name: and, finally, clicking enter/return on your keyboard to save the change: ### How do I invite someone to join my workspace **Admin-only operation** On the [Workspace page](https://app.shinami.com/workspace) of the Shinami dashboard, admins can invite others to join the team. First, click the "+ Invite Member" button. Then, in the modal that pops up, enter the email address you wish you invite and click "+ Invite". Now, you'll see the email address in your Workspace team table with the pending status of "Invited". If you accidentally invite the wrong email address, you can cancel the invitation - see the next section: ### How do I cancel or resend a pending invitation **Admin-only operation** A workspace admin can cancel or resend a pending invitation on the [Workspace page](https://app.shinami.com/workspace) of the Shinami Dashboard. Click the three dots in the `ACTIONS` column for the user, and then click the action you wish you take. ### How do I change a team member's role or remove them **Admin-only operation** A workspace admin can change a user's role or remove them from the workspace on the [Workspace page](https://app.shinami.com/workspace) of the Shinami Dashboard. For an existing account with the "user" role, click the three dots in the `ACTIONS` column for the user, and select "Make admin" or "Remove member". For an existing account with the "admin" role, click the three dots in the `ACTIONS` column for the user, and select "Remove admin privileges" or "Remove member". ### What is the workspace contact email and how do I change it The workspace contact email is shown on under `Settings` on the [Workspace page](https://app.shinami.com/workspace) of your Shinami dashboard. This contact receives key emails from Shinami, such as billing emails and key product updates. By default it's the email address that created the workspace. **Admin-only operation** If this is better as another email, like `[engineering@superapp.io]`or `[accountspayable@superapp.io]`, you can click the edit icon to change it. Currently, we don't send a verification email to the new address, so make sure to double-check the value you enter! ### How many people can join a workspace Up to five members can belong to a workspace. ### How many workspaces can I be a part of Once you've created or joined your first workspace, you cannot create a new personal workspace with the same email address. However, there is currently no limit to the number of workspaces the same email address can be invited to and join. So, if three different Shinami workspaces invited your email address to join, you could join them all. ### What can a workspace admin do that a regular user cannot The admin role has the following permissions that regular users do not have: **Team management** * Invite a new member to the workspace. * Cancel or resend a pending workspace invitation. * Promote a team member to the admin role. * Demote a team member from the admin role to the user role. * Remove a team member from the workspace. **Workspace settings** * Change the workspace name. **Billing** * View and edit billing information. All other actions in the console can be performed by both admin and user roles. # Shinami Docs Overview Source: https://docs.shinami.com/home ## Integrate Explore our endpoints. Sample code tutorials and other helpful guides. Error codes and resolution tips for each service. ## Explore and Learn Non-technical Shinami product summaries (with high-level integration overviews). Answers to usage and billing questions. Recent updates to our products. # Why and how to build your game on Aptos? Source: https://docs.shinami.com/product-overviews/aptos/gaming Increase engagement, retention, and revenue ## Overview This guide covers the benefits of using the Aptos blockchain and using Shinami for your infrastructure needs. It then provides a high-level overview of the key integration flows. Finally, it summarizes the developer resources to help you get started building. ### Why add how to add Web3 elements to your game? For a rich discussion of this, along with guidance on creating a plan to do so, see our [Adding Web3 elements to your game: why and how?](/developer-guides/learning-about-web-3/adding-on-chain-elements-to-your-game) guide. ### Why use Aptos + Shinami? The Aptos blockchain was designed from the ground up to support the needs of large scale, highly-performant games. And with [Aptos Gamestack](https://gamestack.aptoslabs.com/), an all-in-one platform for Web3 game development - with payment support, SDKs, and more - you know Aptos is a blockchain that's serious about gaming. Shinami is the Aptos ecosystem's leading developer platform, offering all that games need to deliver on the promise of Web3 gaming. **Safety** When you think "blockchain," you might also think "security hacks that steal people's money". Aptos was built to guard against this. The code that runs on the Aptos blockchain is built using Aptos Move, a programming language designed for writing safe code that's easy to verify the correctness of. **Dynamic and composable on-chain assets** NFTs on Aptos can be dynamic, meaning you can upgrade them for a player as they spend more time in your game or meet achievements. They can be composable, meaning that an NFT owned by a player can itself own an NFT - for example, a hero the player owns can be equipped with a sword. Dynamic, composable NFTs allow for countless possibilities to evolve and enhance your players' NFTs - and thus game experience - as they accomplish in-game tasks. **Low latency and predictably low fees.** Aptos is highly scalable and allows for parallel execution of transactions, as compared to the one-at-a-time processing of older blockchains like Ethereum. This allows for transactions to have the sub-second speeds needed to give your players the smooth, uninterrupted experience they demand. The Aptos architecture also allows for predictable, very cheap transaction fees (as opposed to the huge spikes in transaction fees you see when a big NFT project launch on Ethereum). **Web3 apps with a Web2 UX. Powered by Shinami.** Shinami has the tools you need to provide a great user experience. We offer user-controlled [embedded wallets](/product-overviews/aptos/wallets) that let you abstract away friction like signing popups, requiring players to download a wallet application, and other UX hurdles that would turn Web2 gamers away. Games use Shinami's [Gas Station](/product-overviews/aptos/gas-station) to sponsor the gas fee for their users' transactions and offer a seamless, free-to-play experience. This means that when a player earns or updates an NFT representing a game asset, they don't have to go through a long KYC process to buy APT for the small gas fee. ## High-level technical overview ### How to create a Web3 wallet for a user There are two main options here, both shown the in the image below: 1. Use a Shinami Invisible Wallet. This is a backend wallet that your app manages in the following way: whenever a user creates an account in your game - using whatever account creation and login flow you already have or want to have - your app creates two additional pieces of data you store in your database for the user. These two things are a Shinami wallet id and a Shinami wallet secret. Then, you app will use the id and secret as a sort of username and password in its API requests to Shinami to create and perform operations with the user's wallet. 2. Use an Aptos Keyless wallet. This is a frontend (including mobile app) wallet that works specifically with social sign in with Google, Apple, etc (full list of options is what's show as supported on Mainnet [here](https://aptos.dev/en/build/guides/aptos-keyless/oidc-support) . Each time the user logs in, you'll ask a pepper service for a pepper (like a special password the wallet needs to work), and a prover service for a zkProof (a fancy mathematical proof that you use to write to the blockchain with the wallet). These services are currently run by Aptos Labs and easily execute behind the scenes if you use their SDK. For a more complete discussion of Shinami's wallet services, see our [high-level guide](/product-overviews/aptos/wallets). For more on Aptos Keyless, see [here](https://aptos.dev/en/build/guides/aptos-keyless/introduction). ### How to read and update a player's NFT on chain **Diagram** **Summary of steps** 1. When a player logs in, you lookup the objects that player owns and present them to the player (you can also store a copy of these in your database). At this point in our example, your player has a character with no weapon. 2. Once the user defeats a boss in the game, they earn a sword to enhance their character's attack power. Your backend makes a request to mint a sword NFT and assign it to the player's character. Now, the player owns a character who owns a sword. All the better to keep playing and take on the next boss! ## Developer resources ### Aptos Move game and transaction code Prior to integrating with Shinami, you should create and deploy [Aptos Move smart contracts](https://aptos.dev/en/build/smart-contracts) to manage your on-chain assets (characters, items, etc.) and game logic. In addition to performing reads, you'll [write transactions](https://aptos.dev/en/build/guides/first-transaction) that interact with your on-chain assets and smart contracts (using the Aptos Labs [Aptos SDK](https://github.com/aptos-labs/aptos-ts-sdk) for this is recommended). ### Shinami services to power your game Shinami has the services games need to deliver on the promise of Web3 gaming. We also have [SDKs](https://www.npmjs.com/package/@shinami/clients) so you can get started quickly. Key services are: * [**Gas Station**](/api-docs/aptos/gas-station/api): sponsor the gas fees for transactions involving your users' wallets for a seamless, free-to-play experience. * **Wallet Services** - abstract away signing popups, seed phrases, and other web3 friction for a smooth UX * [**Invisible Wallet**](/api-docs/aptos/wallet-services/invisible-wallet-api): app-controlled, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature. Uses your app's existing authentication. ### Other technologies and tools you might use * [Aptos Keyless](https://aptos.dev/en/build/guides/aptos-keyless): an OpenID-based login flow where the end user just signs into, for example, their Google account. Endpoints for the related zkProofs and peppers are managed by Aptos labs, but all of our services are fully compatible with these wallets. We have a guide to show you [how to integrate Keyless wallets with our services](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing). * OpenDive [AptosKit Swift SDK](https://github.com/OpenDive/AptosKit) # Gas Station (transaction sponsorship) Source: https://docs.shinami.com/product-overviews/aptos/gas-station How to sponsor user gas fees to increase user conversion and retention ## What's in this doc This guide provides a high-level overview of Shinami's Gas Station and the benefits it provides to your application. It shows you an image of the entire sponsorship flow, from setting up a fund in your Shinami web dashboard all the way through executing a sponsored transaction. Finally, it summarizes our developer resources for getting started with Gas Station. ## Gas Station overview ### What are gas fees? A gas fee is the fee charged for writing data to the Aptos blockchain. Writing data is called executing a transaction. Gas fees pay the teams that run the hardware that processes transactions and stores any new data written to the chain. This hardware is called a validator node, as it validates whether or not a transaction is allowed to happen. While gas fees for Aptos are very small - averaging well under 0.001 APT per transaction this year as shown in the "Network Fees (APT)" graph [here](https://aptoscan.com/analytics) - they must be paid. Further, they must be paid in APT. ### Why sponsor transactions? *In short:* To create a low-friction user experience and increase user conversion, retention, and engagement. *In full:* Typically, the wallet that's executing the transaction (the sender) pays the gas fees for a transaction. This means the wallet must have APT in it. For web2 native users, this adds too much friction: completing KYC checks to purchase APT from an exchange, downloading and setting up a wallet app, transferring APT to their wallet, and then connecting their wallet to your app. You won't convert many Web2 users this way. Instead, you can use Shinami's Gas Station to sponsor their transactions and use embedded or Keyless wallets they don't need to manage. This allows for a great UX, improving sign-up conversion and user retention. Even apps that target Web3-natives, who are used to managing a self-custody wallet, can have a lot of friction from gas fees. For example, when DeFi users who trade non-APT tokens have no APT but need to buy some just to pay a tiny gas fee. This annoyance could lead to fewer actions you want to promote, like deposits and swaps. Sponsoring these actions can increase both your MAU and your revenue per user. ### What is a gas station? A gas station is a service that sponsors your user's transactions. It does two main things: 1. Sponsor the gas fees of an individual transaction. It must create this sponsorship on-demand and quickly. 2. Efficiently and performantly manage the use of your APT sponsorship fund at scale, through high QPS spikes. A gas station manages the above for you, so that you can focus on building your app. With Shinami's Gas Station, you periodically deposit APT in a fund in your Shinami account, and then our Gas Station uses it to handle the management and optimization of transaction sponsorships at scale. You just request a sponsorship for each transaction, and we take care of the rest! ## End-to-end flow ### sponsor -> sign -> submit **Summary of steps** App creates a Gas Station fund in the Shinami dashboard (only happens once per fund). App sends APT to the fund to pay for upcoming sponsorships. (you'll top off the fund every so often as needed). An end user interaction with the app initiates a transaction. For example the user requests to mint an NFT or the app mints the user a free NFT for accomplishing a task. App constructs a mint NFT transaction without fee payer information. The transaction expiration must be set to 1 hour or less. App sends the raw transaction to Shinami's Gas Station for sponsorship, along with any secondary signer addresses via a [`gas_sponsorTransaction request`](/api-docs/aptos/gas-station/api#gas-sponsortransaction). Shinami Gas Station sponsors the transaction from the app's fund and returns the fee payer account address and signature. App gets the sender signature (and any secondary signer signatures required). App submits the transaction with all signatures to the Aptos blockchain. ### sign -> sponsor & submit **Summary of steps** This assumes you've already created and funded a Gas Station fund (steps 1 & 2 in the [first image](/product-overviews/aptos/gas-station#end-to-end-flow)). An end user interaction with the app initiates a transaction. For example the user requests to mint an NFT or the app mints the user a free NFT for accomplishing a task. App constructs a mint NFT transaction without fee payer information. The transaction expiration must be set to 1 hour or less. App gets the sender signature (and any secondary signer signatures required). App sends the raw transaction to Shinami's Gas Station, along with any secondary signer addresses, and asks it to sponsor AND submit the transaction via a [`gas_sponsorAndSubmitSignedTransaction` request](/api-docs/aptos/gas-station/api#gas-sponsorandsubmitsignedtransaction). Shinami Gas Station sponsors the transaction from the app's fund and sends the sponsored transaction, along with the sender and sponsor signatures, to a Full node for submission to the Aptos blockchain ## Creating and using a Gas Station fund See the [Aptos Gas Station FAQ page of our Help Center](/help-center/aptos/gas-station-faq) for information on how to set up a Gas Station fund and how to sponsor transactions using the fund. ## Developer resources See our: * [Gas Station API](/api-docs/aptos/gas-station/api) for available endpoints and guidance. * [Gas Station TypeScript Tutorial](/developer-guides/aptos/tutorials/gas-station-backend-only) for full code examples of building, sponsoring, and submitting a transaction from your backend only. * [Frontend Signing with Backend Sponsorship Tutorial](/developer-guides/aptos/tutorials/gas-station-with-frontend-signing) for diagrams and code examples showing how to integrate Gas Station transaction sponsorship with signing from a connected browser wallet (including Aptos Connect) or a single-app Keyless wallet. # User Wallets Source: https://docs.shinami.com/product-overviews/aptos/wallets Provide a great user experience by creating embedded wallets for your users. ## What's in this doc This guide provides a high-level summary of Shinami's Wallet Services and the benefits it provides to your application. We also show you an image of a transaction flow so you can better understand what an integration entails. ## Wallet Services overview ### What is a wallet? A wallet represents an address on a blockchain where a users stores things they own (APT and other tokens, NFTs, etc). Each address has a private key needed to authorize any action taken by that address, and a wallet is the software that safely stores that private key and uses it to authorize (sign) actions the user wants to take with that address. Wallets exist on a spectrum from a hardware wallet that the user has more secure control over but is a lot of a effort to manage and use, to an embedded wallet that the user has less control over but removes all of the friction. | Wallet Type | User Friction | Wallet works across different apps | User has to manage their private key and complete signing popups for each transaction? | Core use cases | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------- | :--------------------------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | | Connected hardware wallet (e.g. a [Ledger wallet](https://www.ledger.com/)) | High | Yes | Yes | All user asset types. Very safe storage with infrequent app connection and transaction signing. | | Connected browser extension or mobile app wallet (e.g. the [Petra Aptos Wallet](https://chromewebstore.google.com/detail/petra-aptos-wallet/ejjladinnckdgjemekebdpeokbikhfci)) | Medium | Yes | Yes | All user asset types. Active use across apps. | | Shinami embedded Invisible Wallet | None after app login | No, just one app | No | App managed NFTs or closed-loop tokens. | ### Why use Shinami Invisible Wallets? To have a frictionless UX. A connected wallet means users must approve each transaction. This means they don't need to place a lot of trust in the apps they interact with. These wallets are also portable across apps, so that a user can have one address to interact with all the apps they use if desired. However, these wallets require Web3 familiarity that Web2 users don't have. They require users to download a wallet app and manage their private keys through seed-phrases. They also present friction that interrupts a user's engagement with your app, like signing popups. By comparison, Shinami Invisible Wallets abstract away private key management and disruptive signing popups. They offer a convenient, on-chain way to store the assets a user obtains through interacting with your app (for example, an NFT representing an in-game character). Whereas Aptos Keyless wallets rely on a user's Google login, Invisible Wallets work with whatever existing authentication your app has in place (e.g. SSO, email+password, magic link, one-time-passcode to email or SMS, no auth, etc). Additionally, Invisible Wallets pair easily with [Shinami Gas Station](/product-overviews/aptos/gas-station), which helps you pay the small gas fees for your users' transactions. This means your users don't have the friction of completing KYC to buy APT and then sending it to their wallet, which can lead Web2-native users to abandon your app. Finally, these wallets are tied to your app, meaning a user cannot connect them to another app. This means that another app can't alter or transfer an NFT you give a user. Invisible Wallets wallets, therefore, offer an on-ramp for Web2-native users onto Web3 applications. They offer ownership that lacks the full user control of connected, self-custody wallets. In exchange, they allow you to provide the great, frictionless user experience that Web2 users are used to. As users get more comfortable with Web3, you can graduate their assets to full self-custody wallets they control if desired. ### What is a wallet service? A wallet service provides the infrastructure you need to * generate a new wallet for each of your users * sign the transactions that represent the actions taken by the user (e.g. leveling up their game NFT) * (typical, but not required) sponsor and execute those transactions. By using Shinami's Wallet Services, you can focus on building out your app and creating a great user experience. You just interact with our easy-to-use API and we take care of the rest! ## End-to-end flow ### Diagram ### Summary of steps Before the transaction flow, your app previously created a unique, unchangeable (walletID, secret) pair tied to the user's id in your system and used it to create an Invisible Wallet for the user. This is shown in the box titled "Previously." *Transaction flow steps:* The user defeats a boss and earns the right to upgrade their hero's stats. They decide how to upgrade them and click "Upgrade" on your app's frontend. Your app's backend constructs a fee payer transaction that will upgrade the user's hero as they chose. Your app's backend sends one request to Shinami, which conveniently does all of the following: Signs the transaction using the Invisible Wallet for the user. Sponsors the transaction's gas fee using your Gas Station fund. Submits the transaction to the Aptos blockchain to update the player's hero NFT. ## Developer resources * For a look at the API, see our [Invisible Wallet API doc](/api-docs/aptos/wallet-services/invisible-wallet-api). * For an end-to-end tutorial, see our [Invisible Wallet TypeScript Tutorial](/developer-guides/aptos/tutorials/invisible-wallets). # Gas Station (transaction sponsorship) Source: https://docs.shinami.com/product-overviews/movement/gas-station How to sponsor user gas fees to increase user conversion and retention ## What's in this doc This guide provides a high-level overview of Shinami's Gas Station and the benefits it provides to your application. It shows you an image of the entire sponsorship flow, from setting up a fund in your Shinami web dashboard all the way through executing a sponsored transaction. Finally, it summarizes our developer resources for getting started with Gas Station. ## Gas Station overview ### What are gas fees? A gas fee is the fee charged for writing data to the Movement blockchain. Writing data is called executing a transaction. Gas fees pay the teams that run the hardware that processes transactions and stores any new data written to the chain. This hardware is called a validator node, as it validates whether or not a transaction is allowed to happen. While gas fees for Movement are very small, they must be paid. Further, they must be paid in the MOVE token. ### Why sponsor transactions? *In short:* To create a low-friction user experience and increase user conversion, retention, and engagement. *In full:* Typically, the wallet that's executing the transaction (the sender) pays the gas fees for a transaction. This means the wallet must have MOVE in it. For web2 native users, this adds too much friction: completing KYC checks to purchase MOVE on an exchange, downloading and setting up a wallet app, transferring that MOVE to their wallet, and then connecting their wallet to your app. You won't convert many web2 users this way. Instead, you can use Shinami's Gas Station to sponsor their transactions and use embedded wallets they don't need to manage. This allows for a great UX, improving sign-up conversion and user retention. Even apps that target web3-natives, who are used to managing a self-custody wallet, can have a lot of friction from gas fees. For example, when DeFi users who trade non-MOVE tokens have no MOVE need to buy some just to pay a tiny gas fee. This annoyance could lead to fewer actions you want to promote, like deposits and swaps. Sponsoring these actions can increase both your MAU and your revenue per user. ### What is a gas station? A gas station is a service that sponsors your user's transactions. It does two main things: 1. Sponsor the gas fees of an individual transaction. It must create this sponsorship on-demand and quickly. 2. Efficiently and performantly manage the use of your MOVE sponsorship fund at scale, through high QPS spikes. A gas station manages the above for you, so that you can focus on building your app. With Shinami's Gas Station, you periodically deposit MOVE in a fund in your Shinami account, and then our Gas Station uses it to handle the management and optimization of transaction sponsorships at scale. You just request a sponsorship for each transaction, and we take care of the rest! ## End-to-end flow ### sponsor -> sign -> submit **Summary of steps** App creates a Gas Station fund in the Shinami dashboard (only happens once per fund). App sends MOVE to the fund to pay for upcoming sponsorships (you'll top off the fund every so often as needed). An end user interaction with the app initiates a transaction. For example the user requests to mint an NFT or the app mints the user a free NFT for accomplishing a task. App constructs a mint NFT transaction without fee payer information. The transaction expiration must be set to 1 hour or less. App sends the raw transaction to Shinami's Gas Station for sponsorship, along with any secondary signer addresses via a [`gas_sponsorTransaction` request](/api-docs/movement/gas-station/api#gas-sponsortransaction). Shinami Gas Station sponsors the transaction from the app's fund and returns the fee payer account address and signature. App gets the sender signature (and any secondary signer signatures required). App submits the transaction with all signatures to the Movement blockchain. ### sign -> sponsor & submit **Summary of steps** This assumes you've already created and funded a Gas Station fund (steps 1 & 2 in the [first image](/product-overviews/movement/gas-station#end-to-end-flow)). An end user interaction with the app initiates a transaction. For example the user requests to mint an NFT or the app mints the user a free NFT for accomplishing a task. App constructs a mint NFT transaction without fee payer information. The transaction expiration must be set to 1 hour or less. App gets the sender signature (and any secondary signer signatures required). App sends the raw transaction to Shinami's Gas Station, along with any secondary signer addresses, and asks it to sponsor AND submit the transaction using a [`gas_sponsorAndSubmitSignedTransaction` request](/api-docs/movement/gas-station/api#gas-sponsorandsubmitsignedtransaction). Shinami Gas Station sponsors the transaction from the app's fund and sends the sponsored transaction, along with the sender and sponsor signatures, to a node for submission to the Movement blockchain ## Creating and using a Gas Station fund See the [Movement Gas Station FAQ page](/help-center/movement/gas-station-faq) of our Help Center for information on how to set up a Gas Station fund and how to sponsor transactions using the fund. ## Developer resources See our: * [Gas Station API doc](/api-docs/movement/gas-station/api) for available endpoints and guidance. * [Gas Station TypeScript Tutorial](/developer-guides/movement/tutorials/gas-station-backend-only) for full code examples of building, sponsoring, and submitting a transaction from your backend only. * [Frontend Signing with Backend Sponsorship Tutorial](/developer-guides/movement/tutorials/gas-station-with-frontend-signing) for diagrams and code examples showing how to integrate Gas Station transaction sponsorship with signing from a connected browser wallet. # User Wallets Source: https://docs.shinami.com/product-overviews/movement/wallets Provide a great user experience by creating embedded wallets for your users. ## What's in this doc This guide provides a high-level summary of Shinami's Wallet Services and the benefits it provides to your application. We also show you an image of a transaction flow so you can better understand what an integration entails. ## Wallet Services overview ### What is a wallet? A wallet represents an address on a blockchain where a users stores things they own (MOVE and other tokens, NFTs, etc). Each address has a private key needed to authorize any action taken by that address, and a wallet is the software that safely stores that private key and uses it to authorize (sign) actions the user wants to take with that address. Wallets exist on a spectrum from a hardware wallet that the user has more secure control over but is a lot of a effort to manage and use, to an embedded wallet that the user has less control over but removes all of the friction. | Wallet Type | User Friction | Wallet works across different apps | User has to manage their private key and complete signing popups for each transaction? | Core use cases | | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------- | :--------------------------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | | Connected hardware wallet (e.g. a [Ledger wallet](https://www.ledger.com/)) | High | Yes | Yes | All user asset types. Very safe storage with infrequent app connection and transaction signing. | | Connected browser extension or mobile app wallet (e.g. the [OKX Wallet](https://chromewebstore.google.com/detail/okx-wallet/mcohilncbfahbmgdjkbpemcciiolgcge)) | Medium | Yes | Yes | All user asset types. Active use across apps. | | Shinami embedded Invisible Wallet | None after app login | No, just one app | No | App managed NFTs or closed-loop tokens. | ### Why use Shinami Invisible Wallets? To have a frictionless UX. A connected wallet means users must approve each transaction. This means they don't need to place a lot of trust in the apps they interact with. These wallets are also portable across apps, so that a user can have one address to interact with all the apps they use if desired. However, these wallets require Web3 familiarity that Web2 users don't have. They require users to download a wallet app and manage their private keys through seed-phrases. They also present friction that interrupts a user's engagement with your app, like signing popups. By comparison, Shinami Invisible Wallets abstract away private key management and disruptive signing popups. They offer a convenient, on-chain way to store the assets a user obtains through interacting with your app (for example, an NFT representing an in-game character). Invisible Wallets add no additional action for users because they work with whatever existing authentication your app has in place (e.g. SSO, email+password, magic link, one-time-passcode to email or SMS, no auth, etc). Additionally, Invisible Wallets pair easily with [Shinami Gas Station](/product-overviews/movement/gas-station), which enables you to easily pay the small gas fees for your users' transactions. This means your users don't have the friction of completing KYC to buy MOVE and then sending it to their wallet, which can lead Web2-native users to abandon your app. Finally, these wallets are tied to your app, meaning a user cannot connect them to another app. This means that another app can't alter or transfer an NFT you give a user. Invisible Wallets wallets, therefore, offer an on-ramp for Web2-native users onto Web3 applications. They offer ownership that lacks the full user control of connected, self-custody wallets. In exchange, they allow you to provide the great, frictionless user experience that Web2 users are used to. As users get more comfortable with Web3, you can graduate their assets to full self-custody wallets they control if desired. ### What is a wallet service? A wallet service provides the infrastructure you need to * generate a new wallet for each of your users * sign the transactions that represent the actions taken by the user (e.g. leveling up their game NFT) * (typical, but not required) sponsor and execute those transactions. By using Shinami's Wallet Services, you can focus on building out your app and creating a great user experience. You just interact with our easy-to-use API and we take care of the rest! ## End-to-end flow ### Diagram ### Summary of steps Before the transaction flow, your app previously created a unique, unchangeable (walletID, secret) pair tied to the user's id in your system and used it to create an Invisible Wallet for the user. This is shown in the box titled "Previously." *Transaction flow steps:* The user defeats a boss and earns the right to upgrade their hero's stats. They decide how to upgrade them and click "Upgrade" on your app's frontend. Your app's backend constructs a fee payer transaction that will upgrade the user's hero as they chose. Your app's backend sends one request to Shinami, which conveniently does all of the following: Signs the transaction using the Invisible Wallet for the user. Sponsors the transaction's gas fee using your Gas Station fund. Submits the transaction to the Movement blockchain to update the player's hero NFT. ## Developer resources * For a look at the API, see our [Invisible Wallet API doc](/api-docs/movement/wallet-services/invisible-wallet-api). * For an end-to-end tutorial, see our [Invisible Wallet TypeScript Tutorial](/developer-guides/movement/tutorials/invisible-wallets). # Product Overviews Source: https://docs.shinami.com/product-overviews/overview Learn about a product: what it is, when it's useful, and how it works. ## Overviews for ### Aptos **product overviews** The benefits of sponsoring transactions and a high-level integration overview. A comparison of wallets types and when our embedded wallet is the right fit. **related guides** The benefits of building your game using Aptos + Shinami along with a high-level overview of key integration points. ### Movement **product overviews** The benefits of sponsoring transactions and a high-level integration overview. A comparison of wallets types and when our wallets are the best fit. ### Sui **product overviews** The benefits of sponsoring transactions and a high-level integration overview. A comparison of wallets types and when each type is the right fit. **related guides** The benefits of building your game using Sui + Shinami along with a high-level overview of key integration points. A comparison on the two main 3rd party options for transaction sponsorship. # Why and how to build your game on Sui? Source: https://docs.shinami.com/product-overviews/sui/gaming Increase engagement, retention, and revenue ## Overview This guide covers the benefits of using the Sui blockchain and using Shinami for your infrastructure needs. It then provides a high-level overview of the key integration flows. Finally, it summarizes the developer resources to help you get started building. ## Why add how to add Web3 elements to your game For a rich discussion of this, along with guidance on creating a plan to do so, see our [Adding Web3 elements to your game: why and how?](/developer-guides/learning-about-web-3/adding-on-chain-elements-to-your-game) guide. ## Why use Sui + Shinami? The Sui blockchain was designed from the ground up to support the needs of large scale, highly-performant games. And with the release of the [SuiPlay0X1 handheld gaming device](https://www.suiplay0x1.com/), you know Sui is a blockchain that's serious about gaming. Shinami is the Sui ecosystem's leading developer platform, offering all that games need to deliver on the promise of Web3 gaming. **Safety** When you think "blockchain," you might also think "security hacks that steal people's money". Sui was designed from the beginning to guard against this. The code that runs on the Sui blockchain is built using Sui Move, a programming language created for easily writing code with strong safely guarantees. **Object-centric with dynamic, composable objects** Sui is a blockchain built from the ground up to treat objects as first-class entities (as opposed to an account-centric model). Objects are not entries in a smart contract. They are stored directly on chain, making them both more secure and more accessible by games and other apps. NFTs are objects on Sui, and in the rest of this guide we'll use object, NFT, and player asset to refer to the same idea: an in-game asset represented by an NFT you give to a player (which is an object stored on the Sui blockchain). NFTs on Sui can be dynamic. Game developers can alter NFT metadata without having to burn and reissue them, which means you can make changes to your game and reflect those changes in existing player assets. They can be composable, meaning that an NFT owned by a player can itself own an NFT - for example, a hero the player owns can be equipped with a sword. Dynamic, composable NFTs allow for countless possibilities to evolve and enhance your players' NFTs - and thus game experience - as they accomplish in-game tasks. **Low latency and predictably low fees.** Sui is highly scalable and allows for parallel execution of independent transactions. For example, updating an NFT owned by a player doesn't require waiting on global transaction sequencing. This allows for transactions to have the sub-second speeds needed to give your players the smooth, uninterrupted experience they demand. The architecture is horizontally scalable, allowing for predictable, very cheap transaction fees (as opposed to the huge spikes in transaction fees you see when a big NFT project launch on Ethereum). **Web3 apps with a Web2 UX. Powered by Shinami.** Shinami is the leading developer platform for apps on Sui. We provide both app-controlled and user-controlled [wallet infrastructure](/product-overviews/sui/wallets), both of which let you abstract away friction like signing popups, requiring players to download a wallet application, and other UX hurdles that would turn Web2 gamers away. Finally, games use Shinami's [Gas Station](/product-overviews/sui/gas-station) to sponsor the gas fee for their users' transactions and offer a seamless, free-to-play experience. This means that when a player earns or updates an NFT representing a game asset, they don't have to go through a long KYC process to buy SUI for the small gas fee. ## High-level technical overview ### How to create a Web3 wallet for a user There are two main options here, both shown the in the image below: 1. Use a Shinami Invisible Wallet. This is a backend wallet that your app manages in the following way: whenever a user creates an account in your game - using whatever account creation and login flow you already have or want to have - your app creates two additional pieces of data you store in your database for the user. These two things are a Shinami wallet id and a Shinami wallet secret. Then, you app will use the id and secret as a sort of username and password in its API requests to Shinami to create and perform operations with the user's wallet. 2. Use a Sui zkLogin wallet. This is a frontend (including mobile app) wallet that works specifically with social sign in with Google, Apple, Twitch, Facebook, etc (full list of options is what's show as supported on Mainnet [here](https://docs.sui.io/concepts/cryptography/zklogin#openid-providers)). When the user first logs in and you create an account for them, you then ask Shinami to create the wallet, which means to create a salt (like a special password the wallet needs to work). Then, each time the user logs in you ask Shinami for the salt and to generate a zkLogin proof (a fancy mathematical proof that you use to write to the blockchain with the wallet). For a more complete discussion of Shinami's wallet services, see our [high-level guide](/product-overviews/sui/wallets). ### How to read and update a player's NFT on chain **Diagram** This diagram shows an example using our Invisible (embedded) wallets. For zkLogin wallets the read is the same, but the wallet creation is different (shown in the image above) and the write to the chain is a bit different as shown in our [Sui wallets product overview](/product-overviews/sui/wallets#user-controlled-zklogin-wallets). **Summary of steps** 1. When a player logs in, you lookup the objects that player owns and present them to the player (you can also store a copy of these in your database). At this point in our example, your player has a character with no weapon. 2. Once the user defeats a boss in the game, they earn a sword to enhance their character's attack power. Your backend makes a request to mint a sword NFT and assign it to the player's character. Now, the player owns a character who owns a sword. All the better to keep playing and take on the next boss! ## Developer resources ### Sui Move game and transaction code Prior to integrating with Shinami, you should create and deploy [Sui Move smart contracts](https://docs.sui.io/concepts/sui-move-concepts) to manage your on-chain assets (characters, items, etc.) and game logic. You'll use [Programmable Transaction Blocks](https://docs.sui.io/guides/developer/sui-101/building-ptb?ref=blog.sui.io) to build the transactions that interact with your on-chain assets and smart contracts (using [Sui's SDK](https://github.com/MystenLabs/sui/blob/321ceb2d93f47bbe0c171a652861a649fb66b858/sdk/typescript/README.md) for this is recommended). ### Shinami services to power your game Shinami has all the services games need to deliver on the promise of Web3 gaming. We also have [SDKs](https://www.npmjs.com/package/@shinami/clients) so you can get started quickly. Key services are: * [**Gas Station**](/api-docs/sui/gas-station/api): sponsor the gas fees for transactions involving your users' wallets for a seamless, free-to-play experience. * **Wallet Services** - abstract away signing popups, seed phrases, and other web3 friction for a smooth UX * [**Invisible Wallet**](/api-docs/sui/wallet-services/invisible-wallet-api): app-controlled, backend wallets under the shared custody of your app and Shinami. Both parties must cooperate in order to obtain a valid signature. Uses your app's existing authentication. * [**zkLogin Wallet**](/api-docs/sui/wallet-services/zklogin-wallet-api): user-controlled wallets tied to a user's OpenID authentication. [zkLogin](https://docs.sui.io/concepts/cryptography/zklogin) is a Sui primitive that provides the ability to send transactions from a Sui address using an OpenID credential, without publicly linking the two. # Gas Station (transaction sponsorship) Source: https://docs.shinami.com/product-overviews/sui/gas-station Sponsor gas fees to increase user conversion and retention ## What's in this doc This guide provides a high-level overview of Shinami's Gas Station and the benefits it provides to your application. It shows you an image of the entire sponsorship flow, from setting up a fund in your Shinami web dashboard all the way through executing a sponsored transaction. Finally, it summarizes our developer resources for getting started with Gas Station. ## Gas Station overview ### What are gas fees? A gas fee is the fee charged for writing data to the Sui blockchain. Writing data is called executing a transaction. Gas fees pay the teams that run the hardware that processes transactions and stores any new data written to the chain. This hardware is called a validator node, as it validates whether or not a transaction is allowed to happen. While gas fees for Sui are very small - averaging around or below [0.003 SUI per transaction in the last 12 months](https://suiscan.xyz/mainnet/analytics/network/avg.%20gas%20fee) - they must be paid. Further, they must be paid in SUI. ### Why sponsor transactions? *In short:* To create a low-friction user experience and increase user conversion, retention, and engagement. *In full:* Typically, the wallet that's executing the transaction (the sender) pays the gas fees for a transaction. This means the wallet must have SUI in it. For web2 native users, this adds too much friction: completing KYC checks to purchase SUI on an exchange, downloading and setting up a wallet app, transferring SUI to their wallet, and then connecting their wallet to your app. You won't convert many Web2 users this way. Instead, you can use Shinami's [Sui Wallet Services](/product-overviews/sui/wallets) to create wallets for users and Gas Station to sponsor their transactions. This allows for a great UX, improving sign-up conversion and user retention. Even apps that target Web3-natives, who are used to managing a self-custody wallet, can have a lot of friction from gas fees. For example, when DeFi users who trade non-SUI tokens have no SUI and don't want to buy it just to pay a tiny gas fee. This annoyance could lead to fewer actions you want to promote, like deposits and swaps. Sponsoring these actions can increase both your MAU and your revenue per user. ### What is a gas station? A gas station is a service that sponsors your user's transactions. It does two main things: 1. Sponsor the gas fees of an individual transaction. This includes determining how much SUI is needed. If you don't know how much is needed, you'll have to lock up a large amount of SUI when promising to sponsor each transaction. If you're not efficient here, you may need to keep a very large (and expensive) reserve of SUI in order to sponsor transactions. 2. Manage all the SUI that's used to pay for transactions, including creating the unique coin(s) to be used for each sponsorship, and cleaning up the partial bits of SUI leftover from completed (or failed) sponsored transactions. The more user activity you have, and the more transactions per second, the faster you have to generate and clean up the SUI coins needed for sponsorships. Doing this at scale is challenging. A gas station manages the above for you, so that you can focus on building your app. With Shinami's Gas Station, you periodically deposit SUI in a fund in your Shinami account, and then our Gas Station uses it to handle the management and optimization of gas coins (SUI) used to facilitate sponsored transactions. You just request a sponsorship for each transaction, and we take care of the rest! ### Why use Shinami? Shinami is the leading developer infrastructure provider for the Sui ecosystem. We've been successfully supporting Sui builders since the early days of Sui Mainnet (see our [case studies](https://www.shinami.com/case-studies) for examples of the success our customers have seen). For an overview of how we compare to Enoki (the main Shinami Gas Station competitor), see our [Shinami vs Enoki](/product-overviews/sui/shinami-vs-enoki) guide. ## End-to-end flow Here is an image of the full end-to-end flow of a sponsoring a user's transaction with Gas Station. Steps 1 and 2 don't need to happen for each sponsorship after that. ### Diagram ### Summary of steps You create a Gas Station fund in the Shinami dashboard (only happens once per fund). Your app sends SUI to the fund to pay for upcoming sponsorships (only happens periodically, when your fund is running low on SUI). You can also do this manually by sending SUI from a wallet to your fund's deposit address. An end user interaction with the app initiates a transaction. For example the user asks to mint an NFT or the app mints the user a free NFT for accomplishing a task. Alternatively, if using our embedded Invisible Wallets your application might initiate the transaction - e.g. to perform an NFT airdrop or an NFT update to track a user's points. Your app constructs the mint NFT transaction without a gas object under the hood (check out [our TypeScript tutorial](/developer-guides/sui/tutorials/gas-station-backend-only) for examples). App sends it to the Shinami Gas Station for sponsorship via a [`gas_sponsorTransactionBlock`](/api-docs/sui/gas-station/api#gas-sponsortransactionblock) request. Gas Station sponsors the transaction from the app's fund and produces the full transaction data and sponsor signature needed for execution. A signature is obtained from the user's wallet in one of the following ways 1. Programmatically on the backend with no user action via an app-controlled [Invisible Wallet](/api-docs/sui/wallet-services/invisible-wallet-api) or a user-controlled [zkLogin wallet](/api-docs/sui/wallet-services/zklogin-wallet-api). 2. User is presented a pop-up to confirm and sign (connected, self-custody wallet - check out our [Frontend Signing + Backend Sponsorship tutorial](/developer-guides/sui/tutorials/gas-station-with-frontend-signing) to see an example). App submits the transaction to a Sui RPC provider. ## Creating and using a Gas Station fund See the [Sui Gas Station FAQ page of our Help Center](/help-center/sui/gas-station-faq) for information on how to set up a Gas Station fund, how to deposit free Testnet SUI into it, and how to sponsor transaction using that fund. ## Developer resources **Gas Station only** * Our [Gas Station API](/api-docs/sui/gas-station/api) covers the methods you'll need for sponsoring transactions and shows you how to create a gas-less transaction for sponsorship. * Our [Sponsored Transaction TypeScript Tutorial](/developer-guides/sui/tutorials/gas-station-backend-only) provides end-to-end sample code for sponsoring a variety of different transactions from your backend only. * Our [Frontend Signing with Backend Sponsorship Tutorial](/developer-guides/sui/tutorials/gas-station-with-frontend-signing) for diagrams and code examples showing how to integrate Gas Station transaction sponsorship with signing from a connected browser wallet. **Gas Station + Invisible Wallets and zkLogin wallets** * Our [Invisible Wallet TypeScript Tutorial](/developer-guides/sui/tutorials/invisible-wallets) provides end-to-end sample code for setting up an app-controlled wallet on behalf of a user and sponsoring a transaction for it with Shinami Gas Station. * Our [zkLogin wallet Next.js Tutorial](/developer-guides/sui/tutorials/zklogin-nextjs-app-template) helps you quickly set up your first zkLogin app that integrates with Shinami Gas Station for transaction sponsorship. # Shinami Gas Station vs Enoki Source: https://docs.shinami.com/product-overviews/sui/shinami-vs-enoki How to decide which to use for building on Sui ## Overview We know it's not always easy to decide which infrastructure provider to integrate with. So, we've created a helpful comparison showing some of the key criteria in deciding between Shinami and other options like [Enoki](https://enoki.mystenlabs.com/#pricing). You can also view our [case studies](https://www.shinami.com/case-studies) to hear what our customers like about working with us. ## Comparison | You want to... | Shinami | Enoki | Why it's important | | :------------------------------------------------ | :------ | :---- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Have free zkLogin wallet support | ✅ | ❌ | Since you're *already* paying for sponsoring transactions, Shinami doesn't require any additional wallet fees on top of that for the first 20,000 wallet creations each month. | | Only pay in fiat | ❌ | ✅ | Shinami's Gas Station charges in SUI because operational costs are primarily in SUI. If you don't want to touch SUI, Enoki provides a fiat-only alternative for sponsoring transactions. | | See a rich set of business and developer metrics. | ✅ | ❌ | Shinami provides filterable usage metrics across all products. For Gas Station, you can view spend over time to help you budget and adjust sponsorship policies. For wallet services, viewing monthly and daily active wallets gives you insight into your user behavior and trends. | | Work across Move chains. | ✅ | ❌ | If you later want to launch your app (or a new one) on Aptos or Movement you can do it from the same dashboard. And you can do it with the leading infrastructure provider for the Move ecosystem. | | Have multiple teammates share the same account. | ✅ | ✅ | Often, multiple people from the team need to access the dashboard and manage different tasks like API keys and billing. | | Have additional wallet options | ✅ | ❌ | Shinami also offers an Invisible Wallet that works with any user authentication flow and provides more control over user wallet management | ## Learn more Here are some next steps to consider: * Read one of our [case studies](https://www.shinami.com/case-studies) to see how our customers use and benefit from our Gas Station. * Read our [High-Level Guide](/product-overviews/sui/gas-station) that shows how our Gas Station works (and links to our API doc and developer tutorial). * Reach out to `support@shinami.com` to ask us a question. # User Wallets Source: https://docs.shinami.com/product-overviews/sui/wallets Provide a great user experience by choosing the right wallet for your use case. ## What's in this doc This guide provides a high-level summary of the benefits of integrating Shinami's Wallet Services into your application. To help you choose the right wallet for your needs, we summarize the similarities and key differences between our app-controlled Invisible Wallets and user-controlled zkLogin wallets. We also show you an image of a transaction flow for each kind of wallet so you can better understand what an integration entails. Finally, we share the developer resources we have for getting up and running with both kinds of wallets. ## Wallet Services overview ### What is a wallet? A wallet represents an address on a blockchain where a user stores things they own (SUI and other tokens, NFTs, etc). Each address has a private key - and for zkLogin wallets a mathematical proof - needed to authorize any action taken by that address. A wallet is the software that securely generates and stores private keys and zero-knowledge proofs and uses them to authorize (sign) actions the user wants to take with that address. Wallets exist on a spectrum from a hardware wallet that the user has more secure control over but is a lot of a effort to manage and use, to an embedded wallet that the user has less control over but removes all of the friction. | Wallet Type | User Friction | Wallet works across different apps | User has to manage their private key and complete signing popups for each transaction? | Core use cases | | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | :--------------------------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | | Connected hardware wallet (e.g. a [Ledger wallet](https://www.ledger.com/)) | High | Yes | Yes | All user asset types. Very safe storage with infrequent app connection and transaction signing. | | Connected browser extension or mobile app wallet (e.g. the [Sui Wallet](https://chromewebstore.google.com/detail/sui-wallet/opcgpfmipidbgpenhmajoajpbobppdil)) | Medium | Yes | Yes | All user asset types. Active use across apps. | | Shinami embedded wallet: Invisible or zkLogin | None after app or social login | No, just your app | No | App managed NFTs or closed-loop tokens. | ### Why use an embedded wallet? To have a frictionless UX. A connected wallet means users must approve each transaction. This means they don't need to place a lot of trust in the apps they interact with. These wallets are also portable across apps, so that a user can have one address to interact with all the apps they use if desired. However, these wallets require Web3 familiarity that Web2 users don't have. They require users to download a wallet app and manage their private keys through seed-phrases. They also present friction that interrupts a user's engagement with your app, like signing popups. By comparison, Shinami Invisible Wallets and zkLogin wallets remove the user burden of private key management and disruptive signing popups. They offer a convenient, on-chain way to store the assets a user obtains through interacting with your app (for example, an NFT representing an in-game character). Additionally, they both pair easily with [Shinami Gas Station](/product-overviews/sui/gas-station), which helps you pay the small fees for your users' transactions. This means your users don't have the friction of completing KYC to buy SUI and then sending it to their wallet, which can lead Web2-native users to abandon your app. Finally, these wallets are tied to your app, meaning a user cannot connect them to another app. This means that another app can't alter or transfer an NFT you give a user. Invisible Wallets and zkLogin wallets, therefore, offer an on-ramp for Web2-native users onto Web3 applications. They offer ownership that lacks the full user control of connected, self-custody wallets. In exchange, they allow you to provide the great user experience that Web2 users expect. As users get more comfortable with Web3, you can graduate their assets to full self-custody wallets they control if desired. ### What is a wallet service? A wallet service provides the infrastructure you need to * generate a new wallet for each of your users * sign the transactions that represent the actions taken by the user's wallet (e.g. leveling up their game NFT) * (typical, but not required) sponsor and execute those transactions By using Shinami's Wallet Services, you can focus on building out your app and creating a great user experience. You just interact with our easy-to-use API and we take care of the rest! | An ephemeral KeyPair generated on your frontend, a valid JWT returned by the user's OAuth login flow that incorporates a nonce generated with the KeyPair, a salt managed by a salt provider [such as Shinami](/api-docs/sui/wallet-services/zklogin-wallet-api#zklogin-wallet-service-methods), and finally a zkProof attesting the above from a prover [such as Shinami](/api-docs/sui/wallet-services/zklogin-wallet-api#zkprover-service-methods). See [the full flow here](https://docs.sui.io/concepts/cryptography/zklogin#the-complete-zklogin-flow). | ### Why use Shinami? Shinami is the leading developer infrastructure provider for the Sui ecosystem. We've been successfully supporting Sui builders since the early days of Sui Mainnet (see our [case studies](https://www.shinami.com/case-studies) for examples of the success our customers have seen). For an overview of how we compare to Enoki (the main Shinami zkLogin wallet competitor), see our [Shinami vs Enoki](/product-overviews/sui/shinami-vs-enoki) guide. ### Differences between Shinami Invisible Wallets and zkLogin wallets Below, we have summarized the key differences between the two types of wallets. | Comparison point | Shinami Invisible Wallet | zkLogin Wallet | | :------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | *End-user ownership* | Users have less ownership. That said, in both cases your can offer the user an option to transfer something in their wallet to a self-custody wallet as they gain more Web3 knowledge. Or, you can allow them to list it in an in-app marketplace, etc. | Users have more ownership since signing transactions requires a valid JWT obtained from their OAuth login flow (i.e. they have to sign in to Google, etc, first). That said, the user is almost always unaware of the salt and so cannot perform a transaction from the wallet on their own. | | *User authentication* | Works with your app's existing login flow. This could be username+password, SSO, magic link or one-time passcodes, no-auth sign-in, etc. | Users sign in with a social sign in provider like Google, Facebook, Twitch, or Apple. Requires your app to integrate with one of the [supported OpenID providers](https://docs.sui.io/concepts/cryptography/zklogin#openid-providers) | | *User wallet authentication session length* | Determined by your app's existing logic. | Determined by the maxEpoch value you set when initiating the user's OAuth login flow. When the current Sui epoch value surpasses this epoch, you'll need to generate a new ephemeral KeyPair and initiate a new OAuth login flow for the user. | | *Transaction latency* | Fast | Fast, but typically requires an extra backend to frontend roundtrip for obtaining a signature. The signature is obtained quickly, without input from the user. | | *Integration points required* | Backend | Frontend for signing at a minimum, with backend recommended for calling endpoints to generate a zkProof, and to generate, sponsor, and execute each transaction. | | *What's required for a signature?* | A (`walletId`, `secret`) pair your app manages, along with a salt managed by Shinami. | | ## Transaction Flow Diagrams ### App-controlled Invisible Wallets **Image** **Summary of steps** Before the transaction flow, your app previously created a unique, unchangeable (walletID, secret) pair tied to the user's id in your system and used it to create an Invisible Wallet for the user. This is shown in the box titled "Previously." *Transaction flow steps:* The user defeats a boss and earns the right to upgrade their hero's stats. They decide how to upgrade them and click "Upgrade." Your app backend constructs the transaction that will upgrade the user's hero as they chose. It does not contain gas payment information yet. Your app backend sends [one request to Shinami to update the player's NFT onchain](/api-docs/sui/wallet-services/invisible-wallet-api#shinami-wal-executegaslesstransactionblock), which conveniently does all of the following: Sponsors the transaction's gas fee using your Gas Station fund. Signs the transaction using the Invisible Wallet for the user. Submits the transaction to the Sui blockchain using an RPC provider. ### User-controlled zkLogin Wallets **Image** Before the transaction flow, your app previously [asked for a zkProof](/api-docs/sui/wallet-services/zklogin-wallet-api#zkprover-service-methods) when the user completed their OAuth login flow. This is shown in the box titled "Previously." **Summary of steps** The user defeats a boss and earns the right to upgrade their hero's stats. They decide how to upgrade them and click "Upgrade." Your app backend constructs a transaction that will upgrade the user's hero as they chose. It does not contain gas payment information yet. You app backend sends a request to Shinami's Gas Station to [sponsor the transaction and generate a sponsor signature](/api-docs/sui/gas-station/api#gas-sponsortransactionblock). Your app backend sends a request to the frontend to obtain a signature for the transaction using the user's ephemeral KeyPair. Your app backend assembles the zkLogin signature for the transaction using the ephemeral KeyPair signature from the frontend and the zkLogin proof you previously obtained for the KeyPair when the user last went through an OAuth login flow. You submit the transaction to a Sui RPC provider. This updates the player's hero NFT onchain. ## Developer resources ### App-controlled Invisible Wallets * For a look at the API, see our [Invisible Wallet API](/api-docs/sui/wallet-services/invisible-wallet-api). * For an end-to-end tutorial, see our [Invisible Wallet TypeScript Tutorial](/developer-guides/sui/tutorials/invisible-wallets). ### User-controlled zkLogin wallets * For a look at the API, see our [zkLogin wallet API](/api-docs/sui/wallet-services/zklogin-wallet-api). * For a template to get started creating your first zkLogin app, see our [zkLogin wallet Next.js Tutorial](/developer-guides/sui/tutorials/zklogin-nextjs-app-template).