diff --git a/docs/pages/sdks/governance/bounties.md b/docs/pages/sdks/governance/bounties.md new file mode 100644 index 00000000..b4c71444 --- /dev/null +++ b/docs/pages/sdks/governance/bounties.md @@ -0,0 +1,176 @@ +# Bounties SDK + +The Bounties SDK provides the following features: + +- Loads descriptions of each bounty. +- Matches referenda related to the bounty. +- Looks for scheduled changes in the bounty status. +- Abstracts different states into a TypeScript-friendly interface. + +## Getting Started + +Install the Governance SDK using your package manager: + +```sh +npm i @polkadot-api/sdk-governance +``` + +Then, initialize it by passing in the `typedApi` for your chain: + +```ts +import { createBountiesSdk } from '@polkadot-api/sdk-governance'; +import { dot } from '@polkadot-api/descriptors'; +import { getSmProvider } from 'polkadot-api/sm-provider'; +import { chainSpec } from 'polkadot-api/chains/polkadot'; +import { start } from 'polkadot-api/smoldot'; +import { createClient } from 'polkadot-api'; + +const smoldot = start(); +const chain = await smoldot.addChain({ chainSpec }); + +const client = createClient(getSmProvider(chain)); +const typedApi = client.getTypedApi(dot); + +const bountiesSdk = createBountiesSdk(typedApi); +``` + +## Get the current list of Bounties + +The Bounties pallet stores bounty descriptions on-chain as a separate storage query. The Bounties SDK automatically loads these descriptions when retrieving any bounty. + +To get the list of bounties at a given time use `getBounties`, or use `getBounty(id)` to retrieve a specific bounty by ID: + +```ts +const bounties = await bountiesSdk.getBounties(); +const bounty = await bountiesSdk.getBounty(10); +``` + +To retrieve a bounty created from a `propose_bounty` call, use `getProposedBounty` with the result from `submit`: + +```ts +const tx = typedApi.tx.Bounties.propose_bounty({ + description: Binary.fromText("Very special bounty"), + value: 100_000_000_000n, +}); +const result = await tx.signAndSubmit(signer); +const bounty = await bountiesSdk.getProposedBounty(result); +``` + +You can also subscribe to changes using the watch API. This provides two ways of working with it: `bounties$` returns a `Map` with all bounties, and there are also `bountyIds$` and `getBountyById$(id: number)` for cases where you want to show the list and detail separately. + +```ts +// Map +bountiesSdk.watch.bounties$.subscribe(console.log); + +// number[] +bountiesSdk.watch.bountyIds$.subscribe(console.log); + +// Bounty +bountiesSdk.watch.getBountyById$(5).subscribe(console.log); +``` + +The underlying subscription to bounties and descriptions is shared among all subscribers and is automatically cleaned up when all subscribers unsubscribe. + +## Bounty States + +Bounties have many states they can be in, each with its own available operations, and some have some extra parameters. + +![Bounties](/bounties.png) + +The SDK exposes these states through a union of Bounty types, discriminated by `type`. Each type includes only the methods and parameters relevant to its status. + +### Proposed + +After a bounty is proposed, it must be approved via a referendum. Use `approveBounty()` to create the approval transaction. Submit it as part of a referendum using the [Referenda SDK](/sdks/governance/referenda): + +```ts +const approveCall = proposedBounty.approveBounty(); +const callData = await approveCall.getEncodedData(); + +const tx = referendaSdk.createSpenderReferenda(callData, proposedBounty.value); +await tx.signAndSubmit(signer); +``` + +You can also filter existing referenda that are already approving the bounty: + +```ts +const referenda = await referendaSdk.getOngoingReferenda(); +const approvingReferenda = await proposedBounty.findApprovingReferenda(referenda); +``` + +Once the referendum passes, its content is removed from the chain and scheduled for enactment. The SDK can check the scheduler for these cases: + +```ts +const scheduledApprovals = await proposedBounty.getScheduledApprovals(); +// number[] which are the block number in which a change is scheduled. +console.log(scheduledApprovals); +``` + +### Approved + +No methods are available in the Approved state. The bounty is pending the next treasury spend period to become Funded. + +### Funded + +After funding, a new referendum must propose a curator. This state shares methods with [Proposed](#proposed) for filtering referenda and checking the scheduler. + +```ts +const curator = "…SS58 address…"; +const fee = 1_000_000; +const proposeCuratorCall = fundedReferendum.proposeCurator(curator, fee); +const callData = await proposeCuratorCall.getEncodedData(); + +const tx = referendaSdk.createSpenderReferenda(callData, fundedReferendum.value); +await tx.signAndSubmit(signer); +``` + +### CuratorProposed + +Has methods for `acceptCuratorRole()` and `unassignCurator()` + +### Active + +Has methods for `extendExpiry(remark: string)` and `unassignCurator()` + +### Pending Payout + +Has methods for `claim()` and `unassignCurator()`. In this case, unassign curator must also happen in a referendum. + +## Child Bounties + +Some chains support child bounties, allowing a curator to split a bounty into smaller tasks. This feature is available through a separate SDK, which requires the chain to have `ChildBounties` pallet. + +```ts +import { createChildBountiesSdk } from '@polkadot-api/sdk-governance'; + +const childBountiesSdk = createChildBountiesSdk(typedApi); +``` + +It's very similar to the bounties SDK, except that it needs a `parentId` of the parent bounty. + +```ts +const parentId = 10; + +// Fetch one child bounty +const childBounty = await childBountiesSdk.getChildBounty(parentId, 5); + +// Watch the list of child bounties +childBountiesSdk.watch(parentId).bounties$.subscribe(console.log); + +// Watch the list of child bounty IDs +childBountiesSdk.watch(parentId).bountyIds$.subscribe(console.log); + +// Get a specific watched child bounty +childBountiesSdk.watch(parentId).getBountyById$(5).subscribe(console.log); +``` + +Subscriptions to child bounties and descriptions are shared among subscribers for each `parentId` and are cleaned up when all subscribers unsubscribe. + +### States + +Child bounty states are simplified, and eliminates the need for referenda. The curator directly manages these bounties. + +![Child Bounties](/childBounties.png) + +The SDK exposes these states through a union of ChildBounty types, discriminated by `type`. Each type includes only the methods relevant to its status. + diff --git a/docs/pages/sdks/governance/referenda.md b/docs/pages/sdks/governance/referenda.md new file mode 100644 index 00000000..9aaf3d62 --- /dev/null +++ b/docs/pages/sdks/governance/referenda.md @@ -0,0 +1,178 @@ +# Referenda SDK + +The Referenda SDK provides the following features: + +- Abstraction over Preimages + - When creating a referendum, it only generates a preimage if required by the length, significantly reducing deposit costs. + - When reading a referendum, it resolves the calldata regardless of whether it's a preimage or not. +- Automatic Track Selection for Spender Referenda + - Automatically selects the appropriate spender track for a referendum with a given value. +- Approval/Support Curves for Tracks + - Provides approval/support curves as regular JavaScript functions, useful for creating charts. + - Calculates the confirmation start and end blocks based on the current referendum results. + +## Getting Started + +Install the Governance SDK using your package manager: + +```sh +npm i @polkadot-api/sdk-governance +``` + +Then, initialize it by passing in the `typedApi` for your chain: + +```ts +import { createReferendaSdk } from '@polkadot-api/sdk-governance'; +import { dot } from '@polkadot-api/descriptors'; +import { getSmProvider } from 'polkadot-api/sm-provider'; +import { chainSpec } from 'polkadot-api/chains/polkadot'; +import { start } from 'polkadot-api/smoldot'; +import { createClient } from 'polkadot-api'; + +const smoldot = start(); +const chain = await smoldot.addChain({ chainSpec }); + +const client = createClient(getSmProvider(chain)); +const typedApi = client.getTypedApi(dot); + +const referendaSdk = createReferendaSdk(typedApi); +``` + +Different chains have their own spender track configurations, which unfortunately are hard-coded and not available on-chain. By default, the Referenda SDK uses Polkadot's configuration. For Kusama, you can import the Kusama configuration and pass it into the options parameter: + +```ts +import { createReferendaSdk, kusamaSpenderOrigin } from '@polkadot-api/sdk-governance'; + +const referendaSdk = createReferendaSdk(typedApi, { + spenderOrigin: kusamaSpenderOrigin +}); +``` + +## Creating a Referendum + +There are multiple origins to choose from when creating a referendum, each with [specific purposes](https://wiki.polkadot.network/docs/learn-polkadot-opengov-origins#origins-and-tracks-info). + +For creating a referendum that requires a treasury spend, the SDK automatically selects the appropriate origin: + +```ts +const beneficiaryAddress = "………"; +const amount = 10_000_0000_000n; +const spendCall = typedApi.tx.Treasury.spend({ + amount, + beneficiary: MultiAddress.Id(beneficiaryAddress) +}); +const callData = await spendCall.getEncodedData(); + +const tx = referendaSdk.createSpenderReferenda(callData, amount); + +// Submitting the transaction will create the referendum on-chain +const result = await tx.signAndSubmit(signer); +const referendumInfo = referendaSdk.getSubmittedReferendum(result); +if (referendumInfo) { + console.log("Referendum ID:", referendumInfo.index); + console.log("Referendum Track:", referendumInfo.track); +} +``` + +For non-spender referenda, you need to provide the origin: + +```ts +const remarkCall = typedApi.tx.System.remark({ + remark: Binary.fromText("Make Polkadot even better") +}); +const callData = await remarkCall.getEncodedData(); + +const tx = referendaSdk.createReferenda( + PolkadotRuntimeOriginCaller.Origins(GovernanceOrigin.WishForChange()), + callData +); + +const result = await tx.signAndSubmit(signer); +const referendumInfo = referendaSdk.getSubmittedReferendum(result); +if (referendumInfo) { + console.log("Referendum ID:", referendumInfo.index); + console.log("Referendum Track:", referendumInfo.track); +} +``` + +When creating a referendum, if the call is short it can be inlined directly in the referendum submit call. Otherwise, it must be registered as a preimage. The SDK automatically handles this, inlining the call if possible or creating a batch transaction to register the preimage and submit the referendum with just one transaction. + +## Fetching Ongoing Referenda + +Closed referenda are mostly removed from the chain. The Referenda SDK lists ongoing referenda based from on-chain data, or can fetch one specific by index: + +```ts +const referenda: Array = await referendaSdk.getOngoingReferenda(); + +const referendum: OngoingReferendum | null = await referendaSdk.getOngoingReferendum(15); +``` + +You can also subscribe to changes using the watch API. This provides two ways of working with it: `ongoingReferenda$` returns a `Map` with all referenda, and there are also `ongoingReferendaIds$` and `getOngoingReferendumById$(id: number)` for cases where you want to show the list and detail separately. + +```ts +// Map +referendaSdk.watch.ongoingReferenda$.subscribe(console.log); + +// number[] +referendaSdk.watch.ongoingReferendaIds$.subscribe(console.log); + +// Bounty +referendaSdk.watch.getOngoingReferendumById$(5).subscribe(console.log); +``` + +`OngoingReferendum` provides helpful methods to interact with proposals. + +First of all, the proposal on a referendum can be inlined or through a preimage. `OngoingReferendum` unwraps this to get the raw call data or even the decoded call data: + +```ts +console.log(referenda[0].proposal.rawValue); // PreimagesBounded +console.log(await referenda[0].proposal.resolve()); // Binary with the call data +console.log(await referenda[0].proposal.decodedCall()); // Decoded call data +``` + +You can also check when the referendum enters or finishes the confirmation phase: + +```ts +console.log(await referenda[0].getConfirmationStart()); // number | null +console.log(await referenda[0].getConfirmationEnd()); // number | null +``` + +Lastly, there is some useful information that's not available on-chain, but through the public forums (e.g. OpenGov or subsquare). To fetch this information, like the referendum title, you can use a [Subscan API Key](https://support.subscan.io): + +```ts +const apiKey = "………"; +console.log(await referenda[0].getDetails(apiKey)); // { title: string } +``` + +## Accessing Track Details + +Referendum tracks are runtime constants with specific properties for periods, approval, and support curves. + +This SDK enhances the track by adding some helper functions to easily work with the curves. + +You can get the track from an `OngoingReferendum` through the method `referendum.getTrack()`, or you can get one by id: + +```ts +const referendumTrack = await referendum.getTrack(); +const track = await referendaSdk.getTrack(5); +``` + +Then for both `minApproval` and `minSupport` curves, it adds functions to get the values at specific points of the curve: + +```ts +// Raw curve data (LinearDescending, SteppedDecreasing or Reciprocal with parameters) +console.log(track.curve); + +// Get the threshold [0-1] value when time is at 10th block. +console.log(track.getThreshold(10)); + +// Get the first block at which the threshold 50% happens +// Returns -Infinity if the curve starts at a lower threshold (meaning it has reached the threshold since the beginning) +// Returns +Infinity if the curve ends at a higher threshold (meaning it will never reach the threshold) +console.log(track.getBlock(0.5)); + +// Get Array<{ block:number, threshold: number }> needed to draw a chart. +// It will have the minimum amount of datapoints needed to draw a line chart. +console.log(track.getData()); +``` + diff --git a/docs/public/bounties.png b/docs/public/bounties.png new file mode 100644 index 00000000..5ba46dce Binary files /dev/null and b/docs/public/bounties.png differ diff --git a/docs/public/childBounties.png b/docs/public/childBounties.png new file mode 100644 index 00000000..654815e4 Binary files /dev/null and b/docs/public/childBounties.png differ diff --git a/vocs.config.tsx b/vocs.config.tsx index 40c762c9..74bd0663 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -102,6 +102,19 @@ export default defineConfig({ text: "Ink! SDK", link: "/sdks/ink-sdk", }, + { + text: "Governance SDK", + items: [ + { + text: "Referenda", + link: "/sdks/governance/referenda", + }, + { + text: "Bounties", + link: "/sdks/governance/bounties", + }, + ], + }, ], }, {