@solworks/poll-sdk
v1.4.1
Published
SDK for the Poll.fun program.
Downloads
824
Readme
@solworks/poll-sdk
TypeScript SDK for the betmore-core-program Solana/Anchor program (Poll.fun / TG-Bet). Wraps account derivation, instruction building, and transaction sending for V2/V3 (binary) and V4 (multi-outcome, pari-mutuel) bets.
import { SDK } from '@solworks/poll-sdk';
const sdk = SDK.build({ connection, wallet, commitment: 'confirmed' });The package builds from the program's generated IDL: npm run build copies ../betmore-core-program/target/idl/* + target/types/* into src/, then runs tsc. Re-run it after any program rebuild so the bundled IDL/types match the deployed program — see Regenerating the IDL & types for the full pipeline.
V4 bets
A V4 bet is multi-outcome (up to 64 outcomes, each a ≤32-byte ASCII label) and pari-mutuel — the pot is split among backers of the winning outcome at settlement; there are no quoted odds. Every V4 program instruction is exposed two ways:
sdk.<name>V4(params)— builds, signs, and sends the transaction (returns a signature).sdk.instructions.v4.<name>(params)— returns just the instruction, for callers that batch / optimize their own transactions (e.g. program-api's tx builder).
| Instruction | rpc method | ix-builder |
|---|---|---|
| initialize bet | initializeBetV4 | instructions.v4.initializeBet |
| initialize World Cup bet | initializeWorldCupBetV4 | instructions.v4.initializeWorldCupBet |
| add outcome | addOutcomeV4 | instructions.v4.addOutcome |
| place wager | placeWagerV4 | instructions.v4.placeWager |
| initiate vote | initiateVoteV4 | instructions.v4.initiateVote |
| place vote | placeVoteV4 | instructions.v4.placeVote |
| settle batch | settleBetBatchV4 | instructions.v4.settleBetBatch |
| admin resolve | adminResolveBetV4 | instructions.v4.adminResolveBet |
| admin refund batch | adminRefundBetV4 | instructions.v4.adminRefundBet |
| close bet | closeBetV4 | instructions.v4.closeBet |
| close pool | closePoolV4 | instructions.v4.closePool |
| try cancel wager | tryCancelWagerV4 | instructions.v4.tryCancelWager |
Addresses: sdk.addresses.betV4.get(wagerId, owner), sdk.addresses.poolAuthorityV4.get(wagerId, owner). Account fetch: sdk.accounts.betV4.single(betAddress). Status enums map via SDK.convertRustEnumValueToTSEnumValue(value, MarketStatus).
Resolution modes (and the votingDisabled flag)
A V4 bet resolves in one of three ways, selected at creation:
| Mode | How it's created | Who resolves |
|---|---|---|
| Community vote (default) | isCreatorResolver: false | Any wagerer can initiateVoteV4 once minimumVoteCount active wagers exist; majority placeVoteV4 decides. |
| Creator resolver | isCreatorResolver: true | Only the bet creator initiates + casts the deciding vote. |
| Admin-resolve-only (new) | isCreatorResolver: false, votingDisabled: true | No voting at all — only the protocol admin authority via adminResolveBetV4. |
What votingDisabled does
InitializeBetV4Params.votingDisabled?: boolean (default false). When true, the on-chain program rejects every voting instruction — both initiateVoteV4 and placeVoteV4 fail with the VotingDisabled error (surfaced as the message "Voting is disabled for this bet"), for community and creator paths alike. The only way such a bet can reach a resolved state is adminResolveBetV4, which requires the signer to be the protocol's withdraw_authority.
This exists for admin/oracle-resolved markets such as World Cup pools, where the outcome is a real-world result fed by a trusted backend rather than a vote. It closes the pot-drain/collusion vector: with voting hard-disabled, neither the creator nor a colluding set of members can resolve the bet to themselves.
Backward-compatible: omit
votingDisabled(or passfalse) and the bet behaves exactly as before. The flag is forwarded by bothinitializeBetV4andinstructions.v4.initializeBet.
Create an admin-resolved bet
const { bet } = await sdk.initializeBetV4({
question: 'Who will win the World Cup?',
expectedUserCount: 50,
minimumVoteCount: 2, // irrelevant once voting is disabled; keep a valid value (>=2 for non-creator-resolver)
isCreatorResolver: false, // creator does NOT resolve
votingDisabled: true, // <-- hard-disable all voting; admin-resolve-only
initialOutcomes: ['Brazil', 'Argentina', /* ... up to 64 ASCII labels <=32 bytes */],
});Any subsequent initiateVoteV4 / placeVoteV4 on that bet throws VotingDisabled.
Resolve it (protocol admin authority only)
await sdk.adminResolveBetV4({
bet, // PublicKey of the V4 bet
outcomeIndex: 6, // winning outcome (0-based, < outcomes.length)
signers: [adminKeypair], // must be the protocol withdraw_authority
payerOverride: adminKeypair.publicKey,
});
// bet.status -> Resolved, bet.resolvedOutcomeIndex -> 6
// (if the winning outcome had zero backers, the program resolves to a refund-all state instead)adminResolveBetV4 only acts on a bet in Pending/Resolving status. Settlement/payout then runs via settleBetBatchV4 (max 5 users/batch).
Operational limits & edge cases (V4)
These are real constraints surfaced by the 48-outcome / 50-wager scale tests. They matter most for large multi-outcome bets like World Cup pools.
Compute budget
Large bets have a big account to deserialize / realloc / iterate, so the 200k default compute is not enough past ~40 outcomes. The SDK raises the compute-unit limit on the heavy V4 methods: initializeBetV4, addOutcomeV4, placeWagerV4, adminResolveBetV4 → 600k; settleBetBatchV4 → 1M. (The limit instruction is fixed-length, so this does not affect tx size.)
Transaction size (legacy txs cap at 1232 bytes)
- You cannot create a many-outcome bet in a single
initializeBetV4tx. ~40+ string labels as instruction data + accounts + the compute prefix exceed 1232 bytes (real names like "Bosnia and Herzegovina" make it worse). Seed the outcome set incrementally instead — see below. - Settlement fits ≤ 3 users per batch.
settleBetBatchV4carries 5 fixed user slots; with 4+ distinct users plus the compute prefix the tx exceeds 1232 bytes. UseusersPerBatch <= 3— a 50-member pool settles inceil(50/3) = 17batches. Same foradminRefundBetV4.
Creating a large-outcome bet (the fix for #1)
Initialize with a couple of outcomes, then addOutcomeV4 the rest before any wager (outcomes lock on the first wager):
const { bet } = await sdk.initializeBetV4({ /* ... */ initialOutcomes: ['Brazil', 'Argentina'], votingDisabled: true });
for (const label of remainingTeams) { // the other 46 teams
await sdk.addOutcomeV4({ bet, label, signers: [creator] });
}
// ...then wagers. (All outcomes must be added before the first wager.)Alternatives if the per-create tx count matters: use short on-chain labels (e.g. flag codes / indices) and map to display names off-chain — outcomeIndex is the canonical key anyway, the label is only for display + the wager label check; or add a batched add_outcomes instruction (program change) to add several labels per tx. (v0 txs + address lookup tables only compress account keys, not the label data, so they don't reliably solve this on their own.)
World Cup pools: initializeWorldCupBetV4 (the chosen fix)
For World Cup pools the program hardcodes the 48 teams (WORLD_CUP_OUTCOME_LABELS
in controllers/bet_v4.rs) and exposes a dedicated initialize_world_cup_bet_v4
instruction. The create tx carries no outcome labels — the program seeds all
48 — so a full pool is created in one transaction, with the real team names
on-chain. This path always forces votingDisabled: true and non-creator-resolver
(pools are admin/oracle resolved), so it can't be misconfigured.
const { bet } = await sdk.initializeWorldCupBetV4({
question: 'Who will win the World Cup?',
expectedUserCount: 50,
minimumVoteCount: 2, // irrelevant once voting is disabled; keep valid (>=2)
});
// bet.outcomes -> 48 teams (alphabetical), bet.votingDisabled === trueThe on-chain label order MUST stay index-aligned with the off-chain team table (
api/src/common/world-cup-teams.ts); the backend resolves a pool by mapping the winning team name →outcomeIndexvia that table. Append-only once any pool exists on-chain.
Regenerating the IDL & types
src/idl/betmore_core_program.json and src/types/betmore_core_program.ts are
committed, generated artifacts — do not hand-edit them. The .ts file is a
~17k-line camelCase type helper Anchor derives from the JSON IDL (see its header
comment); the JSON is the canonical IDL. Both are produced by the Anchor program
build, not by this package.
The full pipeline after any change to betmore-core-program:
# 1. Rebuild the program (writes target/idl/* and target/types/*)
cd betmore-core-program && anchor build # or, from repo root: npm run build
# 2. Copy the fresh IDL + types into this package, then compile
cd ../betmore-core-sdk && npm run build # runs `npm run copy` then `tsc`npm run build chains copy-idl (cp ../betmore-core-program/target/idl/* src/idl/)
and copy-types (cp ../betmore-core-program/target/types/* src/types/) before
tsc, so a plain npm run build here always refreshes both files from the latest
program build. Run npm run copy on its own to refresh without compiling.
Always regenerate after a program rebuild so the bundled IDL/types match the
deployed program. The IDL metadata.version/address in the generated files
should match the on-chain program you are targeting.
On-chain notes
- The
votingDisabledflag lives on theBetV4account (carved from reserved padding, so adding it did not change the account size; pre-existing bets deserialize tofalse). - Adding the flag + its
VotingDisablederror is purely additive to the program — no V3 instruction, account, or error code changed.
