Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tip referendum creation link #151

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions src/bot-handle-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { IssueCommentCreatedEvent } from "@octokit/webhooks-types";
import { updateBalance } from "./balance";
import { matrixNotifyOnFailure, matrixNotifyOnNewTip } from "./matrix";
import { recordTip } from "./metrics";
import { tipUser } from "./tip";
import { tipUser, tipUserLink } from "./tip";
import { updatePolkassemblyPost } from "./tip-opengov";
import { GithubReactionType, State, TipRequest, TipResult } from "./types";
import { formatTipSize, getTipSize, parseContributorAccount } from "./util";

type OnIssueCommentResult =
| { success: true; message: string }
| { success: true; message: string; tipRequest: TipRequest; tipResult: Extract<TipResult, { success: true }> }
| { success: false; errorMessage: string };

Expand Down Expand Up @@ -71,7 +72,7 @@ export const handleIssueCommentCreated = async (state: State, event: IssueCommen
return;
}

if (result.success && state.polkassembly && result.tipResult.referendumNumber) {
if (result.success && state.polkassembly && "tipResult" in result && result.tipResult.referendumNumber) {
try {
const { url } = await updatePolkassemblyPost({
polkassembly: state.polkassembly,
Expand Down Expand Up @@ -114,18 +115,6 @@ export const handleTipRequest = async (
return { success: false, errorMessage: `@${tipRequester} Contributor and tipper cannot be the same person!` };
}

if (
!(await github.isGithubTeamMember(
{ org: allowedGitHubOrg, team: allowedGitHubTeam, username: tipRequester },
{ octokitInstance },
))
) {
return {
success: false,
errorMessage: `@${tipRequester} You are not allowed to request a tip. Only members of ${allowedGitHubOrg}/${allowedGitHubTeam} are allowed.`,
};
}

const userBio = (await octokitInstance.rest.users.getByUsername({ username: contributorLogin })).data.bio;
const contributorAccount = parseContributorAccount([pullRequestBody, userBio]);
if ("error" in contributorAccount) {
Expand All @@ -151,6 +140,34 @@ export const handleTipRequest = async (
}) a ${formatTipSize(tipRequest)} tip for pull request ${pullRequestUrl}.`,
);

if (
!(await github.isGithubTeamMember(
{ org: allowedGitHubOrg, team: allowedGitHubTeam, username: tipRequester },
{ octokitInstance },
))
) {
let createReferendumLink: string | undefined = undefined;
try {
const tipLink = await tipUserLink(state, tipRequest);
if (!tipLink.success) {
throw new Error(tipLink.errorMessage);
}
createReferendumLink = tipLink.extrinsicCreationLink;
} catch (e) {
bot.log.error("Failed to encode and create a link to tip referendum creation.");
bot.log.error(e.message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that's failed to create, then we have to return success:false and send this error to matrix and to user in PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided not to do it, because we have the fallback in the form of a link to Polkassembly docs, so this is not a critical issue I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so if this happens 100% of the time or intermittently , and this link will never be created, do we need to know about it and chime in - like investigate why it's failed to create and followup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. We still get the regular matrix notification that a tip is requested - I usually look at it anyway so I can see it that way that something is wrong.

}

let message =
`Only members of \`${allowedGitHubOrg}/${allowedGitHubTeam}\` ` +
`have permission to request the creation of the tip referendum from the bot.\n\n`;
message += `However, you can create the tip referendum yourself using [Polkassembly](https://wiki.polkadot.network/docs/learn-polkadot-opengov-treasury#submit-treasury-proposal-via-polkassembly)`;
return {
success: true,
message: createReferendumLink ? message + ` or [PolkadotJS Apps](${createReferendumLink}).` : message + ".",
};
}

const tipResult = await tipUser(state, tipRequest);

// The user doesn't need to wait until we update metrics and balances, so launching it separately.
Expand Down
61 changes: 41 additions & 20 deletions src/tip-opengov.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import "@polkadot/api-augment";
import "@polkadot/types-augment";
import { until } from "@eng-automation/js";
import { ApiPromise } from "@polkadot/api";
import type { SubmittableExtrinsic } from "@polkadot/api/types";
import { ISubmittableResult } from "@polkadot/types/types";
import type { BN } from "@polkadot/util";
import { Probot } from "probot";

import { Polkassembly } from "./polkassembly/polkassembly";
Expand All @@ -11,14 +13,16 @@ import { encodeProposal, formatReason, getReferendumId, tipSizeToOpenGovTrack }

type ExtrinsicResult = { success: true; blockHash: string } | { success: false; errorMessage: string };

export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipRequest: TipRequest }): Promise<TipResult> {
const {
state: { bot, botTipAccount },
api,
tipRequest,
} = opts;
const { contributor } = tipRequest;

export function tipOpenGovReferendumExtrinsic(opts: { api: ApiPromise; tipRequest: TipRequest }):
| Exclude<TipResult, { success: true }>
| {
success: true;
referendumExtrinsic: SubmittableExtrinsic<"promise">;
proposalByteSize: number;
encodedProposal: string;
track: { track: OpenGovTrack; value: BN };
} {
const { api, tipRequest } = opts;
const track = tipSizeToOpenGovTrack(tipRequest);
if ("error" in track) {
return { success: false, errorMessage: track.error };
Expand All @@ -30,25 +34,42 @@ export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipReque
}
const { encodedProposal, proposalByteSize } = encodeProposalResult;

const referendumExtrinsic = api.tx.referenda.submit(
// TODO: There should be a way to set those types properly.
{ Origins: track.track.trackName } as never,
{ Inline: encodedProposal },
{ after: 10 } as never,
);

return { success: true, referendumExtrinsic, proposalByteSize, encodedProposal, track };
}

export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipRequest: TipRequest }): Promise<TipResult> {
const {
state: { bot, botTipAccount },
api,
tipRequest,
} = opts;
const { contributor } = tipRequest;

const preparedExtrinsic = tipOpenGovReferendumExtrinsic({ api, tipRequest });
if (!preparedExtrinsic.success) {
return preparedExtrinsic;
}
const { proposalByteSize, referendumExtrinsic, encodedProposal, track } = preparedExtrinsic;

const nonce = (await api.rpc.system.accountNextIndex(botTipAccount.address)).toNumber();
bot.log(
`Tip proposal for ${contributor.account.address}, encoded proposal byte size: ${proposalByteSize}, nonce: ${nonce}`,
);

const extrinsicResult = await new Promise<ExtrinsicResult>(async (resolve, reject) => {
try {
const proposalUnsubscribe = await api.tx.referenda
.submit(
// TODO: There should be a way to set those types properly.
{ Origins: track.track.trackName } as never,
{ Inline: encodedProposal },
{ after: 10 } as never,
)
.signAndSend(botTipAccount, async (refResult) => {
await signAndSendCallback(bot, contributor.account, "referendum", proposalUnsubscribe, refResult)
.then(resolve)
.catch(reject);
});
const proposalUnsubscribe = await referendumExtrinsic.signAndSend(botTipAccount, async (refResult) => {
await signAndSendCallback(bot, contributor.account, "referendum", proposalUnsubscribe, refResult)
.then(resolve)
.catch(reject);
});
} catch (e) {
reject(e);
}
Expand Down
42 changes: 38 additions & 4 deletions src/tip.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { ApiPromise, WsProvider } from "@polkadot/api";

import { getChainConfig } from "./chain-config";
import { tipOpenGov } from "./tip-opengov";
import { tipOpenGov, tipOpenGovReferendumExtrinsic } from "./tip-opengov";
import { State, TipRequest, TipResult } from "./types";

/* TODO add some kind of timeout then return an error
TODO Unit tests */
export async function tipUser(state: State, tipRequest: TipRequest): Promise<TipResult> {
async function createApi(state: State, tipRequest: TipRequest) {
const { bot } = state;
const chainConfig = getChainConfig(tipRequest.contributor.account.network);
const provider = new WsProvider(chainConfig.providerEndpoint);
Expand All @@ -23,10 +21,46 @@ export async function tipUser(state: State, tipRequest: TipRequest): Promise<Tip

bot.log(`You are connected to chain ${chain.toString()} using ${nodeName.toString()} v${nodeVersion.toString()}`);

return { api, provider };
}

/**
* Tips the user using the Bot account.
* The bot will send the referendum creation transaction itself and pay for the fees.
*/
export async function tipUser(state: State, tipRequest: TipRequest): Promise<TipResult> {
const { provider, api } = await createApi(state, tipRequest);

try {
return await tipOpenGov({ state, api, tipRequest });
} finally {
await api.disconnect();
await provider.disconnect();
}
}

/**
* Prepare a referendum extrinsic, but do not actually send it to the chain.
* Create a transaction creation link for the user.
*/
export async function tipUserLink(
state: State,
tipRequest: TipRequest,
): Promise<{ success: false; errorMessage: string } | { success: true; extrinsicCreationLink: string }> {
const { provider, api } = await createApi(state, tipRequest);

try {
const preparedExtrinsic = tipOpenGovReferendumExtrinsic({ api, tipRequest });
if (!preparedExtrinsic.success) {
return preparedExtrinsic;
}
const transactionHex = preparedExtrinsic.referendumExtrinsic.method.toHex();
const chainConfig = getChainConfig(tipRequest.contributor.account.network);
const polkadotAppsUrl = `https://polkadot.js.org/apps/?rpc=${encodeURIComponent(chainConfig.providerEndpoint)}#/`;
const extrinsicCreationLink = `${polkadotAppsUrl}extrinsics/decode/${transactionHex}`;
return { success: true, extrinsicCreationLink };
} finally {
await api.disconnect();
await provider.disconnect();
}
}
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const byteSize = (extrinsic: SubmittableExtrinsic): number =>
export const encodeProposal = (
api: ApiPromise,
tipRequest: TipRequest,
): { encodedProposal: string; proposalByteSize: number } | TipResult => {
): { encodedProposal: string; proposalByteSize: number } | Exclude<TipResult, { success: true }> => {
const track = tipSizeToOpenGovTrack(tipRequest);
if ("error" in track) {
return { success: false, errorMessage: track.error };
Expand Down
Loading