From ff88aec8aed15be6c19de016422075c4e471161a Mon Sep 17 00:00:00 2001 From: rzadp Date: Tue, 23 Apr 2024 17:34:12 +0200 Subject: [PATCH 1/2] create link --- src/bot-handle-comment.ts | 46 +++++++++++++++++++++----------- src/tip-opengov.ts | 55 ++++++++++++++++++++++++++++----------- src/tip.ts | 39 ++++++++++++++++++++++++--- src/util.ts | 2 +- 4 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/bot-handle-comment.ts b/src/bot-handle-comment.ts index 87360ab..06d3e79 100644 --- a/src/bot-handle-comment.ts +++ b/src/bot-handle-comment.ts @@ -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 { updatePolkassemblyPost } from "./tip-opengov"; +import { tipUser, tipUserLink } from "./tip"; +import { tipOpenGovReferendumExtrinsic, 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 } | { success: false; errorMessage: string }; @@ -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, @@ -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) { @@ -151,6 +140,33 @@ 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) + } + + 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. diff --git a/src/tip-opengov.ts b/src/tip-opengov.ts index cf9a103..9ee602b 100644 --- a/src/tip-opengov.ts +++ b/src/tip-opengov.ts @@ -8,17 +8,17 @@ import { Probot } from "probot"; import { Polkassembly } from "./polkassembly/polkassembly"; import { ContributorAccount, OpenGovTrack, State, TipRequest, TipResult } from "./types"; import { encodeProposal, formatReason, getReferendumId, tipSizeToOpenGovTrack } from "./util"; +import type{ SubmittableExtrinsic } from "@polkadot/api/types"; +import type { BN } from "@polkadot/util"; type ExtrinsicResult = { success: true; blockHash: string } | { success: false; errorMessage: string }; -export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipRequest: TipRequest }): Promise { - const { - state: { bot, botTipAccount }, - api, - tipRequest, - } = opts; - const { contributor } = tipRequest; - +export function tipOpenGovReferendumExtrinsic( + opts: { api: ApiPromise; tipRequest: TipRequest } +): | + Exclude + | {success: true, referendumExtrinsic: SubmittableExtrinsic<"promise", ISubmittableResult>, 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 }; @@ -30,6 +30,37 @@ 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 { + 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}`, @@ -37,13 +68,7 @@ export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipReque const extrinsicResult = await new Promise(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, - ) + const proposalUnsubscribe = await referendumExtrinsic .signAndSend(botTipAccount, async (refResult) => { await signAndSendCallback(bot, contributor.account, "referendum", proposalUnsubscribe, refResult) .then(resolve) diff --git a/src/tip.ts b/src/tip.ts index 2ad7e35..6933332 100644 --- a/src/tip.ts +++ b/src/tip.ts @@ -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 { +async function createApi(state: State, tipRequest: TipRequest) { const { bot } = state; const chainConfig = getChainConfig(tipRequest.contributor.account.network); const provider = new WsProvider(chainConfig.providerEndpoint); @@ -23,6 +21,16 @@ export async function tipUser(state: State, tipRequest: TipRequest): Promise { + const {provider, api} = await createApi(state, tipRequest) + try { return await tipOpenGov({ state, api, tipRequest }); } finally { @@ -30,3 +38,26 @@ export async function tipUser(state: State, tipRequest: TipRequest): Promise { + const {provider, api} = await createApi(state, tipRequest) + + try { + const preparedExtrinsic = await 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(); + } +} diff --git a/src/util.ts b/src/util.ts index 18557c0..262f965 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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 => { const track = tipSizeToOpenGovTrack(tipRequest); if ("error" in track) { return { success: false, errorMessage: track.error }; From 860784de50a062448edabee5e5930023b9925f65 Mon Sep 17 00:00:00 2001 From: rzadp Date: Tue, 23 Apr 2024 17:35:13 +0200 Subject: [PATCH 2/2] lint --- src/bot-handle-comment.ts | 25 ++++++++++---------- src/tip-opengov.ts | 50 ++++++++++++++++++--------------------- src/tip.ts | 23 ++++++++++-------- src/util.ts | 2 +- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/bot-handle-comment.ts b/src/bot-handle-comment.ts index 06d3e79..3801640 100644 --- a/src/bot-handle-comment.ts +++ b/src/bot-handle-comment.ts @@ -6,7 +6,7 @@ import { updateBalance } from "./balance"; import { matrixNotifyOnFailure, matrixNotifyOnNewTip } from "./matrix"; import { recordTip } from "./metrics"; import { tipUser, tipUserLink } from "./tip"; -import { tipOpenGovReferendumExtrinsic, updatePolkassemblyPost } from "./tip-opengov"; +import { updatePolkassemblyPost } from "./tip-opengov"; import { GithubReactionType, State, TipRequest, TipResult } from "./types"; import { formatTipSize, getTipSize, parseContributorAccount } from "./util"; @@ -146,24 +146,25 @@ export const handleTipRequest = async ( { octokitInstance }, )) ) { - let createReferendumLink: string | undefined = undefined + let createReferendumLink: string | undefined = undefined; try { - const tipLink = await tipUserLink(state, tipRequest) + const tipLink = await tipUserLink(state, tipRequest); if (!tipLink.success) { - throw new Error(tipLink.errorMessage) + throw new Error(tipLink.errorMessage); } - createReferendumLink = tipLink.extrinsicCreationLink + createReferendumLink = tipLink.extrinsicCreationLink; } catch (e) { - bot.log.error("Failed to encode and create a link to tip referendum creation.") - bot.log.error(e.message) + bot.log.error("Failed to encode and create a link to tip referendum creation."); + bot.log.error(e.message); } - - 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)` + + 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 + '.') + message: createReferendumLink ? message + ` or [PolkadotJS Apps](${createReferendumLink}).` : message + ".", }; } diff --git a/src/tip-opengov.ts b/src/tip-opengov.ts index 9ee602b..39bde4f 100644 --- a/src/tip-opengov.ts +++ b/src/tip-opengov.ts @@ -2,23 +2,27 @@ 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"; import { ContributorAccount, OpenGovTrack, State, TipRequest, TipResult } from "./types"; import { encodeProposal, formatReason, getReferendumId, tipSizeToOpenGovTrack } from "./util"; -import type{ SubmittableExtrinsic } from "@polkadot/api/types"; -import type { BN } from "@polkadot/util"; type ExtrinsicResult = { success: true; blockHash: string } | { success: false; errorMessage: string }; -export function tipOpenGovReferendumExtrinsic( - opts: { api: ApiPromise; tipRequest: TipRequest } -): | - Exclude - | {success: true, referendumExtrinsic: SubmittableExtrinsic<"promise", ISubmittableResult>, proposalByteSize: number, encodedProposal: string, track: { track: OpenGovTrack; value: BN }} { - const {api, tipRequest} = opts +export function tipOpenGovReferendumExtrinsic(opts: { api: ApiPromise; tipRequest: TipRequest }): + | Exclude + | { + 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 }; @@ -30,21 +34,14 @@ export function tipOpenGovReferendumExtrinsic( } const { encodedProposal, proposalByteSize } = encodeProposalResult; - const referendumExtrinsic = api.tx.referenda - .submit( + 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 - } + return { success: true, referendumExtrinsic, proposalByteSize, encodedProposal, track }; } export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipRequest: TipRequest }): Promise { @@ -55,11 +52,11 @@ export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipReque } = opts; const { contributor } = tipRequest; - const preparedExtrinsic = tipOpenGovReferendumExtrinsic({api, tipRequest}) + const preparedExtrinsic = tipOpenGovReferendumExtrinsic({ api, tipRequest }); if (!preparedExtrinsic.success) { - return preparedExtrinsic + return preparedExtrinsic; } - const {proposalByteSize, referendumExtrinsic, encodedProposal, track} = preparedExtrinsic + const { proposalByteSize, referendumExtrinsic, encodedProposal, track } = preparedExtrinsic; const nonce = (await api.rpc.system.accountNextIndex(botTipAccount.address)).toNumber(); bot.log( @@ -68,12 +65,11 @@ export async function tipOpenGov(opts: { state: State; api: ApiPromise; tipReque const extrinsicResult = await new Promise(async (resolve, reject) => { try { - const proposalUnsubscribe = await referendumExtrinsic - .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); } diff --git a/src/tip.ts b/src/tip.ts index 6933332..1e95cf3 100644 --- a/src/tip.ts +++ b/src/tip.ts @@ -21,7 +21,7 @@ async function createApi(state: State, tipRequest: TipRequest) { bot.log(`You are connected to chain ${chain.toString()} using ${nodeName.toString()} v${nodeVersion.toString()}`); - return {api, provider} + return { api, provider }; } /** @@ -29,7 +29,7 @@ async function createApi(state: State, tipRequest: TipRequest) { * The bot will send the referendum creation transaction itself and pay for the fees. */ export async function tipUser(state: State, tipRequest: TipRequest): Promise { - const {provider, api} = await createApi(state, tipRequest) + const { provider, api } = await createApi(state, tipRequest); try { return await tipOpenGov({ state, api, tipRequest }); @@ -43,19 +43,22 @@ export async function tipUser(state: State, tipRequest: TipRequest): Promise { - const {provider, api} = await createApi(state, tipRequest) +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 = await tipOpenGovReferendumExtrinsic({api, tipRequest}); + const preparedExtrinsic = tipOpenGovReferendumExtrinsic({ api, tipRequest }); if (!preparedExtrinsic.success) { - return preparedExtrinsic + return preparedExtrinsic; } - const transactionHex = preparedExtrinsic.referendumExtrinsic.method.toHex() + 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} + 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(); diff --git a/src/util.ts b/src/util.ts index 262f965..b0ef014 100644 --- a/src/util.ts +++ b/src/util.ts @@ -157,7 +157,7 @@ export const byteSize = (extrinsic: SubmittableExtrinsic): number => export const encodeProposal = ( api: ApiPromise, tipRequest: TipRequest, -): { encodedProposal: string; proposalByteSize: number } | Exclude => { +): { encodedProposal: string; proposalByteSize: number } | Exclude => { const track = tipSizeToOpenGovTrack(tipRequest); if ("error" in track) { return { success: false, errorMessage: track.error };