npm.io
1.6.0 • Published 3d ago

@solworks/poll-sdk

Licence
ISC
Version
1.6.0
Deps
4
Size
1.2 MB
Vulns
0
Weekly
0

@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 pass false) and the bet behaves exactly as before. The flag is forwarded by both initializeBetV4 and instructions.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, adminResolveBetV4600k; settleBetBatchV41M. (The limit instruction is fixed-length, so this does not affect tx size.)

Transaction size (legacy txs cap at 1232 bytes)
  1. You cannot create a many-outcome bet in a single initializeBetV4 tx. ~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.
  2. Settlement fits ≤ 3 users per batch. settleBetBatchV4 carries 5 fixed user slots; with 4+ distinct users plus the compute prefix the tx exceeds 1232 bytes. Use usersPerBatch <= 3 — a 50-member pool settles in ceil(50/3) = 17 batches. Same for adminRefundBetV4.
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 === true

The 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 → outcomeIndex via 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 votingDisabled flag lives on the BetV4 account (carved from reserved padding, so adding it did not change the account size; pre-existing bets deserialize to false).
  • Adding the flag + its VotingDisabled error is purely additive to the program — no V3 instruction, account, or error code changed.