@idosgames/wallet
The on-chain half of the iDosGames blockchain flow. @idosgames/core's
client.blockchain is deliberately report-only: it verifies deposits and
issues signed withdrawals but never signs or broadcasts a transaction. This
package is the missing piece — it connects browser & mobile wallets and
runs the exact RewardPool contract calls, threading them through the
request → submit → confirm / approve → deposit → report lifecycle so a
player can move tokens and NFTs in and out of the game.
- EVM — wagmi + viem + WalletConnect. Browser (MetaMask / any injected wallet) and mobile (via WalletConnect) are both first-class.
- Solana —
@solana/wallet-adapterfor connection; the on-chain instruction building is delegated to a smallSolanaProgramAdapteryou implement with your program's IDL (see Solana).
Everything stays server-authoritative: the bridge only submits what the backend signed/verified and mirrors the confirmed result into the core cache.
Install
npm i @idosgames/wallet @idosgames/core
# EVM peer deps:
npm i wagmi viem @tanstack/react-query
# Solana peer deps (only if you support Solana networks):
npm i @solana/web3.js @solana/wallet-adapter-react @solana/wallet-adapter-base @solana/wallet-adapter-wallets
Two entry points:
@idosgames/wallet— framework-agnostic bridge functions (viem +@solana/web3.jsonly). Use these directly if you're not on React.@idosgames/wallet/react— wagmi/React hooks + providers. Everything below uses these.
Operation category
The updated RewardPool contract tags each operation with a string category
(default "game_topup"; "community_reward" is the other known value).
Deposits read it back from the on-chain tx; withdrawals sign it into the hash,
so it must be submitted on-chain verbatim — the bridge handles that. Pass a
category to any deposit/withdraw call, or omit it for "game_topup". Constants
live in @idosgames/core as BlockchainOperationCategory.
EVM
1. Set up the provider
createEvmWalletConfig builds a wagmi config wired for both browser and mobile
wallets. Wrap your app with IDosGamesWalletProvider (WagmiProvider +
react-query) once.
import { polygon } from "viem/chains";
import {
createEvmWalletConfig,
IDosGamesWalletProvider,
} from "@idosgames/wallet/react";
const wagmiConfig = createEvmWalletConfig({
chains: [polygon], // match the EVM networks in your title's blockchain config
walletConnectProjectId: "<your walletconnect cloud id>", // enables MOBILE wallets
appName: "My Game",
});
export function Root() {
return (
<IDosGamesWalletProvider wagmiConfig={wagmiConfig}>
<App />
</IDosGamesWalletProvider>
);
}
Connect/disconnect with wagmi's own hooks (useConnect, useAccount,
useDisconnect) — the injected() connector covers MetaMask & browser
extensions, walletConnect() opens the QR/deep-link modal for phones.
2. Deposit a token
useEvmBridge(client, titleID) binds the connected wallet to the four flows.
Amounts are raw on-chain units — scale with viem's parseUnits.
import { parseUnits } from "viem";
import { useEvmBridge } from "@idosgames/wallet/react";
import type { BlockchainNetworkDefinition } from "@idosgames/core";
function DepositButton({ client, network, usdtAddress }) {
const bridge = useEvmBridge(client, "my-title-id");
async function deposit() {
// approve → depositERC20(token, amount, userID, titleID, category) → report to backend
const res = await bridge.depositToken({
network, // BlockchainNetworkDefinition from getDefinitions()
tokenAddress: usdtAddress, // ERC-20 contract
amount: parseUnits("25", 6), // 25 USDT (6 decimals) as raw units
// category defaults to "game_topup"
});
if (!res.ok) return alert(`${res.stage}: ${res.error}`);
// core cache balance is already updated; res.data is DepositTokenResponse
}
return (
<button disabled={!bridge.connected} onClick={deposit}>
Deposit 25 USDT
</button>
);
}
3. Withdraw a token
const res = await bridge.withdrawToken({
currencyID: "usdt",
networkID: "polygon",
walletAddress: bridge.account!, // destination — usually the connected wallet
amount: "25.00", // human decimal; server scales & signs raw units
});
if (!res.ok) {
// If it failed AFTER the debit, res.titleTransactionID is set — recover with
// retryWithdrawal (while Pending) or confirmWithdrawal, NEVER a fresh request.
console.error(res.stage, res.error, res.titleTransactionID);
}
The bridge runs requestTokenWithdrawal (debits in-game) → withdrawERC20
on-chain → confirmWithdrawal. A BridgeFailure tells you exactly where it
stopped via stage (request / withdraw-onchain / confirm) so you can
recover correctly — see Failure & recovery.
4. NFTs
// Deposit: safeTransferFrom(account, pool, id, amount, abi.encode(userID,titleID,category))
await bridge.depositNft({
network,
nftContractAddress,
tokenId: 42n,
amount: 1n,
});
// Withdraw: requestNFTWithdrawal → withdrawERC1155 → confirmWithdrawal
await bridge.withdrawNft({
itemID,
networkID: "polygon",
walletAddress: bridge.account!,
amount: "1",
});
Solana
The Solana RewardPool is a custom program whose instruction/account layout
isn't in this SDK — so you provide a SolanaProgramAdapter (two methods:
depositSpl and submitWithdrawal) built with your program's IDL /
@solana/web3.js and the connected wallet from @solana/wallet-adapter-react.
The SDK-side orchestration is identical to EVM.
import {
SolanaWalletBridgeProvider,
useSolanaBridge,
} from "@idosgames/wallet/react";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import type { SolanaProgramAdapter } from "@idosgames/wallet";
// Wrap (alongside IDosGamesWalletProvider if you also support EVM):
<SolanaWalletBridgeProvider
endpoint="https://api.mainnet-beta.solana.com"
wallets={[new PhantomWalletAdapter()]}
>
<App />
</SolanaWalletBridgeProvider>;
// Your program integration:
const adapter: SolanaProgramAdapter = {
async depositSpl({ mint, amountRaw, userID, titleID, category }) {
/* build + send the DepositSpl tx with your IDL; return the signature */
},
async submitWithdrawal(sig) {
/* build + send withdraw_spl with the ed25519 sig-verify ix; return the signature */
},
};
function Screen({ client }) {
const bridge = useSolanaBridge(client, "my-title-id", adapter);
// bridge.depositToken({ network, mint, amountRaw }) / bridge.withdrawToken({ currencyID, networkID, amount })
}
Failure & recovery
Every bridge call resolves to a BridgeResult<T>:
type BridgeResult<T> =
| { ok: true; onChainTxHash: string; data: T }
| {
ok: false;
stage: BridgeStage; // where it stopped
error: string;
onChainTxHash?: string; // set if the asset-moving tx already landed
titleTransactionID?: string; // set if a withdrawal already debited in-game
};
Recovery rules (the bridge never double-charges, but you drive the retry):
stage: "approve" | "deposit-onchain"— nothing was reported; safe to retry the whole deposit.stage: "report"— the on-chain tx (onChainTxHash) landed but the backend didn't credit it; retryclient.blockchain.depositToken/depositNFTwith that hash.stage: "withdraw-onchain"with atitleTransactionID— the withdrawal was already debited in-game but not submitted on-chain. Get a fresh signature withclient.blockchain.retryWithdrawal(titleTransactionID)(whilePending) and submit it withsubmitEvmTokenWithdrawal/submitEvmNftWithdrawal— never callwithdrawTokenagain (that debits twice).stage: "confirm"withonChainTxHash+titleTransactionID— the tx landed but the backend confirm didn't stick; retryclient.blockchain.confirmWithdrawal(titleTransactionID, onChainTxHash).
Framework-agnostic core
Not on React? Import the same flows from @idosgames/wallet and pass viem
clients yourself:
import { depositTokenEvm, withdrawTokenEvm } from "@idosgames/wallet";
const clients = { publicClient, walletClient, account }; // your viem clients
await depositTokenEvm({
client,
clients,
network,
tokenAddress,
amount,
titleID,
});
Notes
- The RewardPool ABI here mirrors the backend's
RewardPoolEvmV2signing (field order/names ofwithdrawERC20/withdrawERC1155/depositERC20must match, or signatures fail on-chain). - After a deposit/withdrawal the core balance cache is fresh, but
client.data.user.state.Blockchain(pending list, stats) is not — callclient.blockchain.getUserState()to refresh it. See theblockchain-systemskill for the full server-side surface, withdrawal gates, and gotchas.