TypeScript clients for Shinami services.
For Sui:
For Aptos:
$ npm install @shinami/clients
To develop against a supported blockchain, you will also need to install their respective SDK:
# For Sui
npm install @mysten/sui
# For Aptos
npm install @aptos-labs/ts-sdk
To create a Sui RPC client:
import { createSuiClient } from "@shinami/clients/sui";
// Obtain NODE_ACCESS_KEY from your Shinami web portal.
const sui = createSuiClient(NODE_ACCESS_KEY);
The returned sui
object is a SuiClient configured to use Shinami's node service.
It supports both HTTP JSON RPC requests as well as WebSocket subscriptions.
Note that NODE_ACCESS_KEY
determines which Sui network later operations are targeting.
Note that gas station should be integrated from your service backend.
This is so you don't leak your GAS_ACCESS_KEY
to your end users, and to allow you to control whose and what transactions to sponsor.
To use gas station with a local signer:
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { fromB64 } from "@mysten/sui/utils";
import {
GasStationClient,
buildGaslessTransaction,
createSuiClient,
} from "@shinami/clients/sui";
// Obtain NODE_ACCESS_KEY and GAS_ACCESS_KEY from your Shinami web portal.
// They MUST be associated with the same network.
const sui = createSuiClient(NODE_ACCESS_KEY);
const gas = new GasStationClient(GAS_ACCESS_KEY);
// You'll want to persist the key pair instead of always creating new ones.
const keypair = new Ed25519Keypair();
const gaslessTx = await buildGaslessTransaction((txb) => {
// Program your Transaction.
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure.u64(1), txb.pure.u64(2)],
});
// Sender is required for sponsorship.
// You don't need to set gas payment.
txb.setSender(keypair.toSuiAddress());
});
// Request gas sponsorship.
const { txBytes, signature: gasSignature } =
await gas.sponsorTransaction(gaslessTx);
// Sign the sponsored tx.
const { signature } = await keypair.signTransaction(fromB64(txBytes));
// Execute the sponsored & signed tx.
const txResp = await sui.executeTransactionBlock({
transactionBlock: txBytes,
signature: [signature, gasSignature],
});
To use the invisible wallet as a signer for a regular (non-sponsored) transaction:
import { Transaction } from "@mysten/sui/transactions";
import {
KeyClient,
ShinamiWalletSigner,
WalletClient,
createSuiClient,
} from "@shinami/clients/sui";
// Obtain NODE_ACCESS_KEY and WALLET_ACCESS_KEY from your Shinami web portal.
const sui = createSuiClient(NODE_ACCESS_KEY);
const key = new KeyClient(WALLET_ACCESS_KEY);
const wal = new WalletClient(WALLET_ACCESS_KEY);
// WALLET_SECRET MUST be used consistently with this wallet id.
// You are responsible for safe-keeping the (walletId, secret) pair.
// Shinami cannot recover it for you.
const signer = new ShinamiWalletSigner("my_wallet_id", wal, WALLET_SECRET, key);
// Program your Transaction.
const txb = new Transaction();
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure.u64(1), txb.pure.u64(2)],
});
txb.setSender(await signer.getAddress(true /* autoCreate */));
txb.setGasBudget(5_000_000);
// Your invisible wallet MUST have sufficient gas for this to succeed.
const txBytes = await txb.build({ client: sui });
// Sign tx with invisible wallet.
const { signature } = await signer.signTransaction(txBytes);
// Execute the signed tx.
const txResp = await sui.executeTransactionBlock({
transactionBlock: txBytes,
signature,
});
To use the invisible wallet to execute a gasless transaction, which seamlessly integrates with Shinami node service and gas station:
import {
KeyClient,
ShinamiWalletSigner,
WalletClient,
buildGaslessTransaction,
createSuiClient,
} from "@shinami/clients/sui";
// Obtain SUPER_ACCESS_KEY from your Shinami web portal.
// It MUST be authorized for all of these services:
// - Node service
// - Gas station
// - Wallet service
const sui = createSuiClient(SUPER_ACCESS_KEY);
const key = new KeyClient(SUPER_ACCESS_KEY);
const wal = new WalletClient(SUPER_ACCESS_KEY);
// WALLET_SECRET MUST be used consistently with this wallet id.
// You are responsible for safe-keeping the (walletId, secret) pair.
// Shinami cannot recover it for you.
const signer = new ShinamiWalletSigner("my_wallet_id", wal, WALLET_SECRET, key);
// Safe to do if unsure about the wallet's existence.
await signer.tryCreate();
const gaslessTx = await buildGaslessTransaction((txb) => {
// Program your Transaction.
// You don't need to set sender or gas payment when executing from invisible wallet.
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure.u64(1), txb.pure.u64(2)],
});
});
// Execute the gasless tx using your invisible wallet.
const txResp = await signer.executeGaslessTransaction(gaslessTx);
Apps using Shinami invisible wallets can participate in Bullshark Quests by allowing users to link their invisible wallets with self-custody wallets that own Bullshark NFTs, through the use of beneficiary graph.
import {
EXAMPLE_BENEFICIARY_GRAPH_ID_TESTNET,
KeyClient,
ShinamiWalletSigner,
WalletClient,
} from "@shinami/clients/sui";
// Obtain SUPER_ACCESS_KEY from your Shinami web portal.
// It MUST be authorized for all of these services:
// - Node service
// - Gas station
// - Wallet service
const key = new KeyClient(SUPER_ACCESS_KEY);
const wal = new WalletClient(SUPER_ACCESS_KEY);
// WALLET_SECRET MUST be used consistently with this wallet id.
// You are responsible for safe-keeping the (walletId, secret) pair.
// Shinami cannot recover it for you.
const signer = new ShinamiWalletSigner("my_wallet_id", wal, WALLET_SECRET, key);
// Safe to do if unsure about the wallet's existence.
await signer.tryCreate();
// Use BULLSHARK_QUEST_BENEFICIARY_GRAPH_ID_MAINNET for Mainnet.
const graphId = EXAMPLE_BENEFICIARY_GRAPH_ID_TESTNET;
const txDigest = await signer.setBeneficiary(
graphId,
// Replace with user's actual wallet address that owns the Bullshark.
"0x1234",
);
// This should return the address we just set.
const beneficiary = await signer.getBeneficiary(graphId);
You can use Shinami's zkLogin wallet services as the salt provider and zkProver in your zkLogin implementation.
import { ZkProverClient, ZkWalletClient } from "@shinami/clients/sui";
// Obtain WALLET_ACCESS_KEY from your Shinami web portal.
const zkw = new ZkWalletClient(WALLET_ACCESS_KEY);
const zkp = new ZkProverClient(WALLET_ACCESS_KEY);
// 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 { salt, address } = await zkw.getOrCreateZkLoginWallet(jwt);
// Create a zkProof.
const { zkProof } = await zkp.createZkLoginProof(
jwt,
maxEpoch,
ephemeralPublicKey,
jwtRandomness,
salt,
);
// Now you can sign transactions with ephemeralPrivateKey, and assemble the zkLogin signature
// using zkProof.
To create an Aptos client:
import { createAptosClient } from "@shinami/clients/aptos";
// Obtain NODE_ACCESS_KEY from your Shinami web portal.
const aptos = createAptosClient(NODE_ACCESS_KEY);
const state = await aptos.getLedgerInfo();
Note that gas station should be integrated from your service backend.
This is so you don't leak your GAS_ACCESS_KEY
to your end users, and to allow you to control whose and what transactions to sponsor.
To use gas station with a local signer:
import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
import { createAptosClient, GasStationClient } from "@shinami/clients/aptos";
// Obtain NODE_ACCESS_KEY from your Shinami web portal.
const aptos = createAptosClient(NODE_ACCESS_KEY);
// Obtain GAS_ACCESS_KEY from your Shinami web portal.
// It MUST be associated with the same Aptos network as above.
const gas = new GasStationClient(GAS_ACCESS_KEY);
// Because we are using sponsored transaction, this account doesn't have to
// exist on-chain beforehand. It'll be created as part of the first transaction.
// In practice, you'll likely want to persist the account key as opposed to
// always generating new ones.
const account = Account.generate({
scheme: SigningSchemeInput.Ed25519,
});
// Build a transaction as you normally would, but with fee payer placeholder.
const transaction = await aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: `${EXAMPLE_PACKAGE_ID}::math::add_entry`,
functionArguments: [1, 2],
},
withFeePayer: true,
});
// Get it sponsored.
const feePayerSig = await gas.sponsorTransaction(transaction);
// Sign the sponsored transaction as usual.
const senderSig = aptos.transaction.sign({
signer: account,
transaction,
});
// Submit the signed transaction with fee payer signature.
const pending = await aptos.transaction.submit.simple({
transaction,
senderAuthenticator: senderSig,
feePayerAuthenticator: feePayerSig,
});
// Wait for it to be committed on-chain.
const committed = await aptos.transaction.waitForTransaction({
transactionHash: pending.hash,
});
To use the invisible wallet as a signer for a regular (non-sponsored) transaction:
import {
Aptos,
AptosConfig,
Network,
AccountAuthenticatorEd25519,
} from "@aptos-labs/ts-sdk";
import {
createAptosClient,
KeyClient,
ShinamiWalletSigner,
WalletClient,
} from "@shinami/clients/aptos";
// Obtain NODE_ACCESS_KEY from your Shinami web portal.
const aptos = createAptosClient(NODE_ACCESS_KEY);
// Obtain WALLET_ACCESS_KEY from your Shinami web portal.
const key = new KeyClient(WALLET_ACCESS_KEY);
const wal = new WalletClient(WALLET_ACCESS_KEY);
// WALLET_SECRET MUST be used consistently with this wallet id.
// You are responsible for safe-keeping the (walletId, secret) pair.
// Shinami cannot recover it for you.
const signer = new ShinamiWalletSigner("my_wallet_id", wal, WALLET_SECRET, key);
// Get or create the sender account on chain. Since this is a non-sponsored transaction,
// your account MUST have APT in it in order to execute a transaction.
const senderAccount = await signer.getAddress(true, true);
// Create a transaction with no feePayer set. This is an example of a SimpleTransaction
const transaction = await aptos.transaction.build.simple({
sender: senderAccount,
data: {
function: `${EXAMPLE_PACKAGE_ID}::math::add_entry`,
functionArguments: [1, 2],
},
withFeePayer: false,
options: {
expireTimestamp: Math.floor(Date.now() / 1000) + 5 * 60,
},
});
// Sign tx with invisible wallet.
const accountAuthenticator = await signer.signTransaction(transaction);
// Submit the tx for execution
const pending = aptos.transaction.submit.simple({
transaction,
senderAuthenticator: accountAuthenticator as AccountAuthenticatorEd25519,
});
// Wait for it to be committed on-chain.
const committed = await aptos.transaction.waitForTransaction({
transactionHash: pending.hash,
});
To use the invisible wallet to execute a gasless transaction, which seamlessly integrates with Shinami's Aptos gas station:
import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
import {
createAptosClient,
KeyClient,
ShinamiWalletSigner,
WalletClient,
} from "@shinami/clients/aptos";
// Obtain SUPER_ACCESS_KEY from your Shinami web portal.
// It MUST be authorized for all of these Aptos services:
// - Node service
// - Gas station (The fund your key is tied to must have funds in it)
// - Wallet service
const aptos = createAptosClient(SUPER_ACCESS_KEY);
const key = new KeyClient(SUPER_ACCESS_KEY);
const wal = new WalletClient(SUPER_ACCESS_KEY);
// WALLET_SECRET MUST be used consistently with this wallet id.
// You are responsible for safe-keeping the (walletId, secret) pair.
// Shinami cannot recover it for you.
const signer = new ShinamiWalletSigner("my_wallet_id", wal, WALLET_SECRET, key);
// Get or create the sender account on chain. You do not need APT in this account since
// we'll be using Shinami's gas station to sponsor the transaction.
const senderAccount = await signer.getAddress(true, true);
// Create a transaction with feePayer set. This is an example of a SimpleTransaction
const transaction = await aptos.transaction.build.simple({
sender: senderAccount,
data: {
function: `${EXAMPLE_PACKAGE_ID}::math::add_entry`,
functionArguments: [1, 2],
},
withFeePayer: true,
options: {
expireTimestamp: Math.floor(Date.now() / 1000) + 5 * 60,
},
});
// Execute the gasless tx using your invisible wallet.
const pending = signer.executeGaslessTransaction(transaction);
// Wait for it to be committed on-chain.
const committed = await aptos.transaction.waitForTransaction({
transactionHash: pending.hash,
});
$ npm run build
$ npm run lint
$ npm run test
The integration tests for Sui make use of the Sui Move example package, which has been deployed to Sui Testnet.
Obtain <your_sui_super_access_key>
from Shinami web portal.
The key must be authorized for all of these services, targeting Sui Testnet:
- Node service
- Gas station - you must also have some available balance in your gas fund.
- Wallet service
The integration tests for Aptos make use of the Aptos Move example package, which has been deployed to Aptos Testnet.
Obtain <your_aptos_super_access_key>
from Shinami web portal.
The key must be authorized for all of these services, targeting Aptos Testnet:
- Gas station - you must also have some available balance in your gas fund.
Once you have the super keys for both chains,
export SUI_NODE_ACCESS_KEY=<your_sui_super_access_key>
export SUI_GAS_ACCESS_KEY=<your_sui_super_access_key>
export SUI_WALLET_ACCESS_KEY=<your_sui_super_access_key>
export APTOS_NODE_ACCESS_KEY=<your_aptos_super_access_key>
export APTOS_GAS_ACCESS_KEY=<your_aptos_super_access_key>
export APTOS_WALLET_ACCESS_KEY=<your_aptos_super_access_key>
npm run integration
The integration tests by default talk to Shinami's production endpoints. You can also make it talk to alternative endpoints by modifying sui/integration.env.ts and aptos/integration.env.ts.
Similar to integration test:
export SUI_NODE_ACCESS_KEY=<your_sui_super_access_key>
export SUI_GAS_ACCESS_KEY=<your_sui_super_access_key>
export SUI_WALLET_ACCESS_KEY=<your_sui_super_access_key>
export APTOS_NODE_ACCESS_KEY=<your_aptos_super_access_key>
export APTOS_GAS_ACCESS_KEY=<your_aptos_super_access_key>
export APTOS_WALLET_ACCESS_KEY=<your_aptos_super_access_key>
npm run coverage
The HTML coverage report is available at coverage/lcov-report/index.html
.
$ npm run clean