@dotone/sdk
v2.0.4
Published
Official TypeScript SDK for DotOne Smart Chain
Downloads
90
Maintainers
Readme
@dotone/sdk
TypeScript SDK for the DotOne Staking contracts. Built on viem.
Installation
npm install @dotone/sdk
# or
pnpm add @dotone/sdkviem is a peer dependency:
npm install viemDeployed contracts (DotOne mainnet, chainId 505)
| Contract | Address |
|---|---|
| Staking | 0x136f964c9bE25b75ef608423c61F922BeE022a71 |
| StakingNFT | 0x6AC8c90A2331583498FE66A4Fe8772EB9E837b7C |
| PrincipalVault | 0x35eca0beFb7F96D57ED9AE4d50A2b888B09C2Cf6 |
| RewardVault | 0x7470F58744Fb90A96394b17F4545223fcd6cC6d6 |
| WDOT (Wrapped DOT) | 0x044b2ED77214aeA517e811f5c80980511F8ff326 |
| MockERC20 (test token) | 0xAD13394f77AEB4E157d8ec8EcaEF2D77f0894516 |
WDOT is the production staking token. You must hold real WDOT (wrap DOT) to stake it.
MockERC20 is a freely-mintable test token on DotOne mainnet — anyone can call mint(address to, uint256 amount). It has the same 3 tiers as WDOT and its reward pool is pre-seeded with 1,000,000 MOCK. Use it to test the full staking flow without acquiring real tokens.
Quick start
ABIs are bundled in the package — no external ABI files needed. Call configure() once at app startup, then createStakingClient() works anywhere in your app.
Browser (MetaMask):
import {
configure,
createStakingClient,
stakingAbi,
stakingNftAbi,
rewardVaultAbi,
} from "@dotone/sdk/staking";
import { createPublicClient, createWalletClient, http, custom } from "viem";
configure({
addresses: {
staking: "0x136f964c9bE25b75ef608423c61F922BeE022a71",
stakingNFT: "0x6AC8c90A2331583498FE66A4Fe8772EB9E837b7C",
principalVault: "0x35eca0beFb7F96D57ED9AE4d50A2b888B09C2Cf6",
rewardVault: "0x7470F58744Fb90A96394b17F4545223fcd6cC6d6",
},
abis: {
staking: stakingAbi,
stakingNFT: stakingNftAbi,
rewardVault: rewardVaultAbi,
},
});
const publicClient = createPublicClient({ transport: http("https://rpc2.dotone.network") });
const walletClient = createWalletClient({ transport: custom(window.ethereum) });
const sdk = createStakingClient({ publicClient, walletClient });Node.js / CLI (private key):
import {
configure,
createStakingClient,
stakingAbi,
stakingNftAbi,
rewardVaultAbi,
} from "@dotone/sdk/staking";
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
configure({
addresses: {
staking: "0x136f964c9bE25b75ef608423c61F922BeE022a71",
stakingNFT: "0x6AC8c90A2331583498FE66A4Fe8772EB9E837b7C",
principalVault: "0x35eca0beFb7F96D57ED9AE4d50A2b888B09C2Cf6",
rewardVault: "0x7470F58744Fb90A96394b17F4545223fcd6cC6d6",
},
abis: {
staking: stakingAbi,
stakingNFT: stakingNftAbi,
rewardVault: rewardVaultAbi,
},
});
const account = privateKeyToAccount("0x...");
const publicClient = createPublicClient({ transport: http("https://rpc2.dotone.network") });
const walletClient = createWalletClient({ account, transport: http("https://rpc2.dotone.network") });
const sdk = createStakingClient({ publicClient, walletClient });Staking tiers
Tiers are per-token and have a variable count — each accepted ERC20 has its own independent tier array configured when the token is registered via addToken. The contract auto-selects the highest qualifying active tier based on the staked amount. You do not specify a tier manually.
Query tiers at runtime — do not hardcode them:
const tiers = await sdk.getTokenTiers("0xTokenAddress");
// tiers[0].minApy, tiers[0].lockPeriod, tiers[0].minInvestment, ...WDOT and MockERC20 on DotOne mainnet share this tier config:
| Tier | APY Range | Lock | Min Investment | Perf Fee | Mgmt Fee | |---|---|---|---|---|---| | 0 — Low Risk | 8–18% | 90 days | 1,000 tokens | 15% | 1% | | 1 — Medium Risk | 20–35% | 180 days | 10,000 tokens | 20% | 1.5% | | 2 — High Risk | 35–50% | 365 days | 50,000 tokens | 30% | 2% |
Reward calculation uses midpoint APY: (minApy + maxApy) / 2.
API
Read
// Tiers — per token, variable count
const tiers = await sdk.getTokenTiers("0xTokenAddress"); // TierInfo[] for this token
const tier = await sdk.getTokenTier("0xTokenAddress", 0); // single TierInfo by id
// TierInfo field names (note: fee fields are renamed from the contract's ABI names):
// tier.minApy — minimum APY in basis points (uint16)
// tier.maxApy — maximum APY in basis points (uint16)
// tier.perfFeeBps — performance fee bps ← SDK name (contract: performanceFee)
// tier.mgmtFeeBps — management fee bps ← SDK name (contract: managementFee)
// tier.lockPeriod — lock duration in seconds (uint32)
// tier.riskScore — 0=Low Risk, 1=Medium Risk, 2=High Risk (uint8)
// tier.active — whether this tier accepts new stakes
// tier.minInvestment — minimum stake amount in wei (uint256 / bigint)
// Which tier will be assigned for a given amount?
const best = await sdk.getBestTier("0xTokenAddress", parseEther("1000"));
// → { tierId: 0, tier: TierInfo } | null
// Supported tokens
const tokens = await sdk.getSupportedTokens(); // address[] of all registered tokens
// Token support
const ok = await sdk.isTokenSupported("0xTokenAddress");
const pool = await sdk.getRewardPool("0xTokenAddress"); // reward pool balance
// User positions — enriched objects (stake + NFT metadata + pending reward + tier info)
const positions = await sdk.getUserPositions("0x..."); // active only
const allPositions = await sdk.getUserAllPositions("0x..."); // active + completed badges
// Each position contains:
// position.tokenId — NFT token ID
// position.owner — current NFT holder (may differ from original staker after transfer)
// position.stake.token — staked ERC20 address
// position.stake.amount — principal locked (wei)
// position.stake.active — true = locked, false = exited
// position.stake.tierId — tier index (0/1/2)
// position.stake.stakedAt — unix timestamp of stake
// position.stake.unlockAt — unix timestamp when lock expires
// position.stake.claimedGross — cumulative gross rewards claimed mid-lock so far (wei)
// position.stake.snapshotMinApy / snapshotMaxApy — APY bps locked at stake time
// position.stake.snapshotPerfFee / snapshotMgmtFee — fees locked at stake time
// position.nftMeta.tierId — tier at mint
// position.nftMeta.snapshotRiskScore — 0=Low, 1=Medium, 2=High
// position.nftMeta.completed — true once unstaked (permanent badge)
// position.pendingReward — claimable right now, net after performance fee (wei)
// position.tierInfo — current tier params {minApy, maxApy, lockPeriod, ...}
// position.uri — decoded tokenURI JSON {name, description, image, attributes}
// Single enriched position by tokenId
const position = await sdk.getStakePosition(tokenId);
// Raw stake data only (no NFT fetch — lighter call for lists)
const allStakes = await sdk.getUserStakes("0x..."); // StakeInfo[] — all stakes, active + past
const stake = await sdk.getStakeByToken(tokenId); // StakeInfo for one tokenId
// Pending reward (net, after performance fee deduction)
const pending = await sdk.getPendingReward(tokenId);
// NFT
const meta = await sdk.getNFTMeta(tokenId); // token, amount, tierId, APY snapshot, timestamps, completed
const uri = await sdk.getTokenURI(tokenId); // "data:application/json;base64,..." string
const owner = await sdk.getNFTOwner(tokenId); // current holder address
// NFT tier image URL
const imgUrl = await sdk.getTierImageUrl("0xTokenAddress", 0); // image URL for tier 0 of this token
// ERC20 balance
const balance = await sdk.getTokenBalance("0xTokenAddress", "0xAccount");Write — user
All write methods return a transaction hash (Hash). Await confirmation with publicClient.waitForTransactionReceipt.
// Stake — approves PrincipalVault automatically if needed, then stakes.
// Tier is auto-selected by the contract based on amount.
const { approveTx, stakeTx, tokenId } = await sdk.stakeWithApproval({
token: "0xTokenAddress",
amount: parseEther("1000"),
account: "0x...",
});
// Claim accrued rewards mid-lock (principal stays locked)
await sdk.claimReward({ tokenId, account: "0x..." });
await sdk.claimRewards({ tokenIds: [id1, id2], account: "0x..." }); // batch
// Unstake after lock expires — returns principal + all remaining rewards in one tx
await sdk.unstake({ tokenId, account: "0x..." });
await sdk.unstakeMultiple({ tokenIds: [id1, id2], account: "0x..." }); // batch
// Emergency unstake — returns principal minus mgmt fee, forfeits all rewards.
// Safe to call even when the reward pool is empty. Lock must still have expired.
await sdk.emergencyUnstake({ tokenId, account: "0x..." });Write — admin
// Add/remove accepted ERC20 tokens
// addToken registers the token and its tiers atomically — tiers cannot be added separately.
// Each tier object shape (all fields required):
await sdk.addToken({
token: "0xTokenAddress",
tiers: [
{
minApy: 800, // minimum APY in basis points (800 = 8%)
maxApy: 1800, // maximum APY in basis points (1800 = 18%)
performanceFee: 1500, // performance fee bps on gross rewards (1500 = 15%)
managementFee: 100, // management fee bps on principal (100 = 1%)
lockPeriod: 90 * 24 * 60 * 60, // lock duration in seconds
riskScore: 0, // 0=Low Risk, 1=Medium Risk, 2=High Risk
active: true,
minInvestment: parseEther("1000"),
},
// add more tiers as needed — contract supports any count
],
account: "0x...",
});
await sdk.removeToken({ token: "0xTokenAddress", account: "0x..." });
// Tier management — all tier functions require the token address (tiers are per-token)
await sdk.setTierActive({ token: "0xTokenAddress", tierId: 0, active: false, account: "0x..." });
// Note: updateTier is available directly on the contract but not wrapped by the SDK.
// Call it via viem writeContract with stakingAbi if needed.
// NFT tier images (sequential-only append; triggers EIP-4906 marketplace refresh)
await sdk.setTierImageUrl({ token: "0xTokenAddress", tierId: 0, url: "https://...", account: "0x..." });
// Fund reward pool — approves RewardVault automatically if needed, then funds.
const { approveTx, fundTx } = await sdk.fundWithApproval({
token: "0xTokenAddress",
amount: parseEther("100000"),
account: "0x...",
});
// Emergency withdraw from reward pool (capped to rewardPools[token] — cannot touch principal)
await sdk.emergencyWithdraw({ token: "0xTokenAddress", to: "0x...", amount, account: "0x..." });
// System management
await sdk.setPaused({ paused: true, account: "0x..." });
await sdk.setFeeCollector({ collector: "0x...", account: "0x..." });ABIs
All four contract ABIs are exported from @dotone/sdk/staking — no ABI JSON files required.
import {
stakingAbi, // Staking.sol
stakingNftAbi, // StakingNFT.sol
rewardVaultAbi, // RewardVault.sol
principalVaultAbi, // PrincipalVault.sol (rarely needed directly)
} from "@dotone/sdk/staking";Pass them to configure() as shown in Quick start, or use them directly with viem's readContract / writeContract if you need lower-level access.
Utility
import { decodeTokenURI } from "@dotone/sdk/staking";
const uri = await sdk.getTokenURI(tokenId);
const meta = decodeTokenURI(uri);
// → { name, description, image, attributes: [{ trait_type, value }, ...] }Vault architecture
The staking system uses two vault contracts for fund isolation:
- PrincipalVault — holds all staked principal. Only the Staking contract can move funds out. No admin drain function — user principal is safe even if the deployer is compromised. Users must approve PrincipalVault (not Staking) before staking —
stakeWithApprovalhandles this automatically. - RewardVault — holds admin-funded reward pools per token.
fundWithApprovalhandles the approval + funding in one call.
NFT position tokens
Every stake mints a transferable ERC721 position NFT. The NFT holder owns the position — rewards and principal go to whoever holds the NFT at claim/unstake time, even if it was transferred.
- On-chain base64 JSON metadata — no IPFS dependency
- NFTs are never burned — marked
completed: trueafter unstake (permanent badge) - Implements EIP-4906 so marketplaces auto-refresh on status changes
Networks
| Network | Chain ID | |---|---| | DotOne Smart Chain | 505 | | Ethereum Sepolia | 11155111 |
Building from source
pnpm install
pnpm run build # compiles src/ → dist/