TypeScript clients for Shinami services for the Sui blockchain.
$ npm install @shinami/clients
To create a Sui RPC client:
import { createSuiClient } from "@shinami/clients";
// 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.js/keypairs/ed25519";
import { fromB64 } from "@mysten/sui.js/utils";
import {
GasStationClient,
buildGaslessTransactionBytes,
createSuiClient,
} from "@shinami/clients";
// 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 buildGaslessTransactionBytes({
sui,
build: async (txb) => {
// Program your TransactionBlock.
// DO NOT set sender or gas data.
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure(1), txb.pure(2)],
});
},
});
// Request gas sponsorship.
const { txBytes, signature: gasSignature } = await gas.sponsorTransactionBlock(
gaslessTx,
keypair.toSuiAddress(),
5_000_000
);
// Sign the sponsored tx.
const { signature } = await keypair.signTransactionBlock(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 block:
import { TransactionBlock } from "@mysten/sui.js/transactions";
import {
KeyClient,
ShinamiWalletSigner,
WalletClient,
createSuiClient,
} from "@shinami/clients";
// 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 TransactionBlock.
const txb = new TransactionBlock();
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure(1), txb.pure(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.signTransactionBlock(txBytes);
// Execute the signed tx.
const txResp = await sui.executeTransactionBlock({
transactionBlock: txBytes,
signature,
});
To use the invisible wallet to execute a gasless transaction block, which seamlessly integrates with Shinami node service and gas station:
import {
KeyClient,
ShinamiWalletSigner,
WalletClient,
buildGaslessTransactionBytes,
createSuiClient,
} from "@shinami/clients";
// 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 buildGaslessTransactionBytes({
sui,
build: async (txb) => {
// Program your TransactionBlock.
// DO NOT set sender or gas data.
txb.moveCall({
target: `${EXAMPLE_PACKAGE_ID}::math::add`,
arguments: [txb.pure(1), txb.pure(2)],
});
},
});
// Execute the gasless tx using your invisible wallet.
const txResp = await signer.executeGaslessTransactionBlock(
gaslessTx,
5_000_000
);
You can use Shinami's zkLogin wallet services as the salt provider and zkProver in your zkLogin implementation.
import { ZkProverClient, ZkWalletClient } from "@shinami/clients";
// 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 transaction blocks with ephemeralPrivateKey, and assemble the zkLogin signature
// using zkProof.
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";
// 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);
$ npm run build
$ npm run lint
$ npm run test
The integration tests make use of the Move example package, which has been deployed to Sui Testnet.
Obtain <your_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
export NODE_ACCESS_KEY=<your_super_access_key>
export GAS_ACCESS_KEY=<your_super_access_key>
export WALLET_ACCESS_KEY=<your_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 integration.env.ts.
Similar to integration test:
export NODE_ACCESS_KEY=<your_super_access_key>
export GAS_ACCESS_KEY=<your_super_access_key>
export WALLET_ACCESS_KEY=<your_super_access_key>
npm run coverage
The HTML coverage report is available at coverage/lcov-report/index.html
.
$ npm run clean