-
Notifications
You must be signed in to change notification settings - Fork 8
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
Introduce Polkassembly API integration #79
Merged
Merged
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
00a61e4
WIP - nothing is working
rzadp ac88592
Sign up and login are working
rzadp 9b3d561
Introduce track numbers (for polkassembly filters)
rzadp 72e097f
lint-fixed Polkassembly with new features
rzadp a2f18a9
yarn-lock
rzadp dfb1ca0
Cleanup
rzadp 45497de
Another test case
rzadp 88a1271
use botTipAccount from state
rzadp fd18c5d
Re-use test util
rzadp 12622ce
add polkassembly instance to the state
rzadp fb680b6
Envs
rzadp 4835663
Invoke the metadata update in opengov tip
rzadp 59074f6
Lint
rzadp 9eced2f
Merge origin/master into rzadp/polkassembly
rzadp 4704df2
Fine tune the old test
rzadp 9544867
Lint, moonbase
rzadp c579329
Cleanup
rzadp ca87697
Commit the moonbase manual test
rzadp b335c41
Cleanup
rzadp 8916113
Cleanup
rzadp a369203
Cleanup
rzadp 992c086
Remove hardcoded network
rzadp 6fa4185
Correct mishandled error
rzadp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import "@polkadot/api-augment"; | ||
import { Wallet } from "ethers"; | ||
|
||
import { Polkassembly } from "./polkassembly"; | ||
|
||
/** | ||
* This is a test suite that uses the production (non-test) Polkassembly API, | ||
* and a Moonbase Alpha testnet to perform a single test case that | ||
* creates a referendum and edits the metadata on Polkassembly. | ||
* | ||
* Moonbase is an EVM-compatible chain, so we're using an Ethereum signer. | ||
* Currently, it's the only testnet with OpenGov and Polkassembly support. | ||
* Related: https://github.com/paritytech/substrate-tip-bot/issues/46 | ||
* | ||
* The tests are mostly manual because the code doesn't support sending | ||
* Ethereum-signed blockchain transactions (only Ethereum-signed Polkassembly API calls). | ||
* Also, Moonbase Alpha doesn't have the tipper tracks. | ||
* | ||
* To run: | ||
* 1. Create a Moonbase Alpha account | ||
* 2. Fund it (upwards of 20 DEV are needed) | ||
* 3. Manually (in polkadot.js.org/apps) create a preimage and a referendum. | ||
* Use any tack, for example Root. Tipper tracks are not available. | ||
* 4. Un-skip the test, and edit the variables below. | ||
*/ | ||
describe("Polkassembly with production API and Moonbase Alpha testnet", () => { | ||
let polkassembly: Polkassembly; | ||
const moonbaseMnemonic: string | undefined = undefined; // Edit before running. | ||
const manuallyCreatedReferendumId: number | undefined = undefined; // Edit before running | ||
|
||
beforeAll(() => { | ||
if (moonbaseMnemonic === undefined || manuallyCreatedReferendumId === undefined) { | ||
throw new Error("Variables needed. Read description above."); | ||
} | ||
const wallet = Wallet.fromMnemonic(moonbaseMnemonic); | ||
polkassembly = new Polkassembly("https://api.polkassembly.io/api/v1/", { type: "ethereum", wallet }); | ||
}); | ||
|
||
test.skip("Edits a metadata of an existing referendum", async () => { | ||
await polkassembly.loginOrSignup(); | ||
await polkassembly.editPost("moonbase", { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
postId: manuallyCreatedReferendumId!, | ||
proposalType: "referendums_v2", | ||
content: `Just testing, feel free to vote nay.\nToday is ${new Date().toISOString()}`, | ||
title: "A mock referendum", | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import "@polkadot/api-augment"; | ||
import { Keyring } from "@polkadot/api"; | ||
import type { KeyringPair } from "@polkadot/keyring/types"; | ||
import { cryptoWaitReady, randomAsU8a } from "@polkadot/util-crypto"; | ||
|
||
import { Polkassembly } from "./polkassembly"; | ||
|
||
describe("Polkassembly with a test endpoint", () => { | ||
let keyringPair: KeyringPair; | ||
let polkassembly: Polkassembly; | ||
|
||
beforeAll(async () => { | ||
await cryptoWaitReady(); | ||
}); | ||
|
||
beforeEach(() => { | ||
const keyring = new Keyring({ type: "sr25519" }); | ||
// A random account for every test. | ||
keyringPair = keyring.addFromSeed(randomAsU8a(32)); | ||
polkassembly = new Polkassembly("https://test.polkassembly.io/api/v1/", { type: "polkadot", keyringPair }); | ||
}); | ||
|
||
test("Can produce a signature", async () => { | ||
await polkassembly.signMessage("something"); | ||
}); | ||
|
||
test("We are not logged in initially", () => { | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
}); | ||
|
||
test("We cannot log in without signing up first", async () => { | ||
await expect(() => polkassembly.login()).rejects.toThrowError( | ||
"Please sign up prior to logging in with a web3 address", | ||
); | ||
}); | ||
|
||
test("Can sign up", async () => { | ||
await polkassembly.signup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
}); | ||
|
||
test("Can log in and logout, having signed up", async () => { | ||
await polkassembly.signup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
|
||
polkassembly.logout(); | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
|
||
await polkassembly.login(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
}); | ||
|
||
test("Cannot sign up on different networks with the same address", async () => { | ||
await polkassembly.signup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
|
||
polkassembly.logout(); | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
|
||
await expect(() => polkassembly.signup()).rejects.toThrowError( | ||
"There is already an account associated with this address, you cannot sign-up with this address", | ||
); | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
}); | ||
|
||
test("Login-or-signup handles it all", async () => { | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
|
||
// Will sign up. | ||
await polkassembly.loginOrSignup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
|
||
// Won't throw an error when trying again. | ||
await polkassembly.loginOrSignup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
|
||
// Can log out. | ||
polkassembly.logout(); | ||
expect(polkassembly.loggedIn).toBeFalsy(); | ||
|
||
// Can log back in. | ||
await polkassembly.loginOrSignup(); | ||
expect(polkassembly.loggedIn).toBeTruthy(); | ||
}); | ||
|
||
test("Can retrieve a last referendum number on a track", async () => { | ||
const result = await polkassembly.getLastReferendumNumber("moonbase", 0); | ||
expect(typeof result).toEqual("number"); | ||
expect(result).toBeGreaterThan(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { KeyringPair } from "@polkadot/keyring/types"; | ||
import { stringToU8a } from "@polkadot/util"; | ||
import { Wallet } from "ethers"; | ||
import fetch from "node-fetch"; | ||
|
||
const headers = { "Content-Type": "application/json" }; | ||
|
||
export class Polkassembly { | ||
private token: string | undefined; | ||
|
||
constructor( | ||
private endpoint: string, | ||
private signer: { type: "polkadot"; keyringPair: KeyringPair } | { type: "ethereum"; wallet: Wallet }, // Ethereum type is used for EVM chains. | ||
) {} | ||
|
||
public get loggedIn(): boolean { | ||
return this.token !== undefined; | ||
} | ||
|
||
public get address(): string { | ||
return this.signer.type === "polkadot" ? this.signer.keyringPair.address : this.signer.wallet.address; | ||
} | ||
|
||
public async signup(): Promise<void> { | ||
if (this.loggedIn) return; | ||
const signupStartResponse = await fetch(`${this.endpoint}/auth/actions/addressSignupStart`, { | ||
headers, | ||
method: "POST", | ||
body: JSON.stringify({ address: this.address }), | ||
}); | ||
if (!signupStartResponse.ok) { | ||
throw new Error(await signupStartResponse.text()); | ||
} | ||
const signupStartBody = (await signupStartResponse.json()) as { signMessage: string }; | ||
|
||
const signupResponse = await fetch(`${this.endpoint}/auth/actions/addressSignupConfirm`, { | ||
headers, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
address: this.address, | ||
signature: await this.signMessage(signupStartBody.signMessage), | ||
wallet: this.signer.type === "polkadot" ? "polkadot-js" : "metamask", | ||
}), | ||
}); | ||
if (!signupResponse.ok) { | ||
throw new Error(await signupResponse.text()); | ||
} | ||
const signupBody = (await signupResponse.json()) as { token: string }; | ||
if (!signupBody.token) { | ||
throw new Error("Signup unsuccessful, the authentication token is missing."); | ||
} | ||
this.token = signupBody.token; | ||
} | ||
|
||
public async login(): Promise<void> { | ||
if (this.loggedIn) return; | ||
|
||
const loginStartResponse = await fetch(`${this.endpoint}/auth/actions/addressLoginStart`, { | ||
headers, | ||
method: "POST", | ||
body: JSON.stringify({ address: this.address }), | ||
}); | ||
if (!loginStartResponse.ok) { | ||
throw new Error(await loginStartResponse.text()); | ||
} | ||
const loginStartBody = (await loginStartResponse.json()) as { signMessage: string }; | ||
|
||
const loginResponse = await fetch(`${this.endpoint}/auth/actions/addressLogin`, { | ||
headers, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
address: this.address, | ||
signature: await this.signMessage(loginStartBody.signMessage), | ||
wallet: this.signer.type === "polkadot" ? "polkadot-js" : "metamask", | ||
}), | ||
}); | ||
if (!loginResponse.ok) { | ||
throw new Error(await loginResponse.text()); | ||
} | ||
const loginBody = (await loginResponse.json()) as { token: string }; | ||
if (!loginBody.token) { | ||
throw new Error("Login unsuccessful, the authentication token is missing."); | ||
} | ||
this.token = loginBody.token; | ||
} | ||
|
||
public logout(): void { | ||
this.token = undefined; | ||
} | ||
|
||
public async loginOrSignup(): Promise<void> { | ||
try { | ||
await this.login(); | ||
} catch (e) { | ||
if ((e as Error).message.includes("Please sign up")) { | ||
} | ||
await this.signup(); | ||
rzadp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
public async editPost( | ||
network: string, | ||
opts: { | ||
postId: number; | ||
title: string; | ||
content: string; | ||
proposalType: "referendums_v2"; | ||
}, | ||
): Promise<void> { | ||
if (!this.token) { | ||
throw new Error("Not logged in."); | ||
} | ||
const response = await fetch(`${this.endpoint}/auth/actions/editPost`, { | ||
headers: { ...headers, "x-network": network, authorization: `Bearer ${this.token}` }, | ||
method: "POST", | ||
body: JSON.stringify(opts), | ||
}); | ||
if (!response.ok) { | ||
throw new Error(await response.text()); | ||
} | ||
} | ||
|
||
async getLastReferendumNumber(network: string, trackNo: number): Promise<number | undefined> { | ||
const response = await fetch( | ||
`${this.endpoint}/listing/on-chain-posts?proposalType=referendums_v2&trackNo=${trackNo}&sortBy=newest`, | ||
{ headers: { ...headers, "x-network": network }, method: "POST", body: JSON.stringify({}) }, | ||
); | ||
if (!response.ok) { | ||
throw new Error(await response.text()); | ||
} | ||
const body = (await response.json()) as { posts: { post_id: number }[] }; | ||
return body.posts[0]?.post_id; | ||
} | ||
|
||
public async signMessage(message: string): Promise<string> { | ||
const messageInUint8Array = stringToU8a(message); | ||
if (this.signer.type === "ethereum") { | ||
return await this.signer.wallet.signMessage(message); | ||
} | ||
const signedMessage = this.signer.keyringPair.sign(messageInUint8Array); | ||
return "0x" + Buffer.from(signedMessage).toString("hex"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { createTestKeyring } from "@polkadot/keyring"; | ||
import { randomAsU8a } from "@polkadot/util-crypto"; | ||
|
||
export const randomAddress = (): string => createTestKeyring().addFromSeed(randomAsU8a(32)).address; | ||
|
||
export const logMock: any = console.log.bind(console); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
logMock.error = console.error.bind(console); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may be worth adding some debug logs to login and signup?