Skip to content

Commit

Permalink
Merge pull request #42 from polkadot-api/vo-gov-sdk
Browse files Browse the repository at this point in the history
feat(governance): Add docs for governance SDK
  • Loading branch information
voliva authored Jan 20, 2025
2 parents 31446dc + fd0ee30 commit 17a9c27
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 0 deletions.
176 changes: 176 additions & 0 deletions docs/pages/sdks/governance/bounties.md
Original file line number Diff line number Diff line change
@@ -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<number, Bounty>` 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<number, Bounty>
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.

178 changes: 178 additions & 0 deletions docs/pages/sdks/governance/referenda.md
Original file line number Diff line number Diff line change
@@ -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<OngoingReferendum> = 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<number, OngoingReferendum>` 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<number, OngoingReferendum>
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());
```

Binary file added docs/public/bounties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/childBounties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions vocs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
},
],
},
{
Expand Down

0 comments on commit 17a9c27

Please sign in to comment.