@idosgames/wallet
v0.6.4
Published
Wallet-bridge companion to @idosgames/core: connect browser & mobile wallets (EVM via wagmi/viem/WalletConnect, Solana via wallet-adapter) and move tokens/NFTs in and out of the game through client.blockchain.
Maintainers
Readme
@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-walletsEntry 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.@idosgames/wallet/react/solana— the Solana React bindings, kept off/reactso an EVM-only game doesn't bundle the Solana adapters.@idosgames/wallet/react/lazy—LazyWalletLogin/LazySolanaWalletLogin/LazyWalletPanel: the sign-in buttons and the in-game deposit/withdraw panel, but each fetches the wallet machinery on demand (LazyWalletLoginon the player's first tap;LazyWalletPanelon mount). This is the only entry with no Reown AppKit in its module graph, which is what makes it safe to render on a login screen or in-game in sandboxed/preview bundlers where AppKit isn't installable. It also re-exports the supported chains as plain objects, so you never have to import thewagmi/chains/viem/chainsbarrel.
createEvmWalletConfig is memoised per WalletConnect project id: the
sign-in button and the wallet panel both call it and get the same wagmi
Config object, and a wagmi Config carries the connection in its own store. So
a wallet connected on the login screen is already connected in the in-game
panel — no reconnect, no second modal. For that to hold, keep both on the same
chain set; both default to DEFAULT_EVM_CHAINS, so the simplest correct thing
is to pass no chains to either. LazyWalletPanel takes the authenticated
client as a prop (like the login button), plus optional appName /
onClose / style / walletConnectProjectId.
LazyWalletPanel is ONE panel for both chains: it reads the title's networks
and loads the EVM panel (WalletPanel) or the Solana one (SolanaWalletPanel,
/react/solana). Both render the same form (walletPanelView.tsx: connect,
network, token, in-game balance, amount, Deposit / Withdraw) — a chain only
supplies how it connects and moves tokens. On Solana that is Reown AppKit for
the wallet and createPlatformPoolAdapter (deposit_spl / withdraw_spl
against the platform pool, network.RewardPoolAddress from the engine only);
transactions go to the title's cluster (ChainID 103 → devnet, else mainnet).
createSolanaWalletConfig is memoised per project id too, so a Solana wallet
connected at sign-in stays connected in the panel.
Inside the idosgames.com frame a game's wallet button calls
openPlatformWalletPanel() (also on /react/lazy, with isEmbeddedInPlatform
/ isEmbeddedOnForeignSite): the site shows its own wallet card — crypto in a
frame goes through the site only.
Every subpath declares a browser export condition pointing at the ESM build.
That is load-bearing, not cosmetic: some browser bundlers (CodeSandbox's
classic Sandpack bundler among them) rank the require condition above
import and would otherwise take the CommonJS build — where a dynamic
import() cannot survive, so the lazy entry would eagerly require AppKit.
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).
Embedded in an idosgames.com iframe
A game embedded in an idosgames.com iframe cannot reach the player's wallet on
its own — deposit/withdraw goes through the buttons in the site's wallet card
next to the game. The @idosgames/wallet/react/lazy components handle this by
themselves:
LazyWalletLogin/LazySolanaWalletLoginstay a normal "Sign in with wallet" button. Inside the platform's frame the tap goes tologinWithWalletViaPlatform: the SITE's already-connected wallet signs the engine's login message (the site connects one first if needed), so the wallet prompt comes from idosgames.com, and the game exchanges the signature for a session as usual. The site signs nothing but the engine's login template for its own page's title. (2026-09-23 — before that the button was replaced by a notice and wallet sign-in inside the frame was simply gone.)LazyWalletPanelrenders nothing in a frame — no notice either.- Embedded on somebody else's site through
idosgames.com/embed/{id}(two frames deep,isEmbeddedOnForeignSite()) the sign-in button renders nothing too: that page has no wallet of ours to sign with.
Every bridge and login flow that talks to the wallet directly
(depositTokenEvm, withdrawTokenEvm, loginWithWalletEvm, their Solana
equivalents, payWithWalletEvm) refuses with error: WALLET_USE_SITE_PANEL if
called inside a frame anyway, so a game that rolls its own UI can check
isEmbeddedInPlatform() up front — and call loginWithWalletViaPlatform({
client, networkID, family: "evm" | "solana" }) for sign-in:
import {
isEmbeddedInPlatform,
onWalletBalanceChanged,
} from "@idosgames/wallet";
if (isEmbeddedInPlatform()) {
// hide your own wallet UI — the player uses the site's panel instead
}
// Refresh the on-screen balance once the player finishes an operation there:
const unsubscribe = onWalletBalanceChanged(() => {
client.blockchain.getUserState();
});A game opened by its own direct link (not embedded) is unaffected — every flow works exactly as before there.
Linking a wallet (play access by token balance)
When a title's login mode requires a token balance (cfg.Blockchain.PlayAccess,
AnyWithLinkedWallet), a player who signed in by e-mail or through the site
attaches a wallet to the account by signature — not a second login. Every
sign-in button has a link mode:
<LazyWalletLogin
mode="link"
client={client}
networkID="bsc"
onLinked={(pass) => (pass?.Granted ? enterGame() : showWhyNot(pass))}
/>Framework-agnostic: linkWalletEvm, linkWalletSolana, and
linkWalletViaPlatform (inside the idosgames.com frame — the site's wallet
signs). The server checks the balance against exactly the linked wallet and
returns the pass as data.PlayAccess. The host template wires this into its
play-access screen already (renderWalletLink in src/walletLogin.tsx).
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.
