Skip to content

Commit

Permalink
Implement full audit of on-chain tx
Browse files Browse the repository at this point in the history
  • Loading branch information
mesudip committed Dec 2, 2024
1 parent 6bb2096 commit afd8bf8
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 85 deletions.
3 changes: 2 additions & 1 deletion integration_test/lib/constants/environments.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { config } from 'dotenv';
config();
const environments = {
txTimeOut: 3 * 40 * 1000,
txTimeOut: 4 * 40 * 1000,
baseUrl: process.env.HOST_URL || 'http://localhost:3000',
kuber: {
apiUrl:
process.env.KUBER_API_URL || 'https://preview.kuber.cardanoapi.io',
apiKey: process.env.KUBER_API_KEY || '',
},
blockfrostApiKey: process.env.BLOCKFROST_API_KEY,
networkId: process.env.NETWORK_ID || '0',
ci: process.env.CI,
};
Expand Down
15 changes: 14 additions & 1 deletion integration_test/lib/fixtures/importWallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { CardanoTestWalletJson } from '@cardanoapi/cardano-test-wallet/types';
import { Page } from '@playwright/test';
import { Browser, BrowserContext, Page } from '@playwright/test';
import { StaticWallet } from '@types';
import loadEternlExtension from './loadExtension';
import LoginPage from '@pages/loginPage';

export async function injectWalletExtension(
page: Page,
Expand All @@ -22,4 +24,15 @@ export async function injectWalletExtension(
}, wallet);
}


export async function pageWithInjectedWallet(context:BrowserContext|Browser, wallet:StaticWallet): Promise<Page> {
const page = await context.newPage();
await loadEternlExtension(page);

await injectWalletExtension(page, wallet);

const loginPage = new LoginPage(page);
await loginPage.login();
return page;
}
export const importWallet = injectWalletExtension
130 changes: 130 additions & 0 deletions integration_test/lib/helpers/blockfrost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import environments from "@constants/environments";

//@ts-ignore
const blockfrostApiUrl = 'https://cardano-'+(environments.networkId == 0? 'preview':'mainnet')+'.blockfrost.io/api/v0/txs/';
const blockfrostApiKey = environments.blockfrostApiKey;

async function fetchVoteMetadata (txHash:string): Promise<any> {
const url = `${blockfrostApiUrl}${txHash}/metadata`
console.log("Fetching:",url)
const response = await fetch(url, {
method: 'GET',
headers: {
'project_id': blockfrostApiKey,
},
});
const txData: any[] = await response.json();
const label27=txData.filter(x=>x.label==='27')[0].json_metadata
return label27;
};

export async function fetchPollSummaryMetadata(txHash: string):Promise<string[]>{
return await fetchVoteMetadata(txHash)
}
interface OnChainVoteData {
[delegate: string]: string[];
}

export type Vote = VotePayload & {
voterName: string
cosesign1: string
cosekey: string
}

export interface VotePayload{
pollName: string,
constutionHash: string,
constutionLink: string,
voterStake: string,
vote: 'yes'| 'no' | 'abstain'
challenge: string
}
export async function fetchVoteTxMetadata(txHash: string):Promise<Vote[]>{
const voteData: OnChainVoteData[] = await fetchVoteMetadata(txHash)
return voteData.map(v=>{
const voterName=Object.keys(v)
const voteData =v[voterName[0]]
const voteStr=voteData.join('')
const result =extractVoteData(voteStr)
result.voterName=voterName[0]
return result

})
}

function extractVoteData(data:string): Vote {
// Regular Expressions for matching different parts
const pollNameRegex = /Poll: ([^,]+)/;
const constitutionHashRegex = /Hashed Constitution Text: ([^,]+)/;
const constitutionLinkRegex = /Link to Constitution Text: ([^,]+)/;
const voteRegex = /Vote: (\w+)/;
const walletRegex = /Wallet: ([^,]+)/;
const cosign1Regex = /Signature: ([^,]+)/;
const cosekeyRegex = /Public Key: ([^,]+)/;
const challengeRegex = /Challenge: ([^,]+)/;
// Match each one
const pollNameMatch = data.match(pollNameRegex);
const constitutionHashMatch = data.match(constitutionHashRegex);
const constitutionLinkMatch = data.match(constitutionLinkRegex);
const voteMatch = data.match(voteRegex);
const walletMatch = data.match(walletRegex);
const cosign1Match = data.match(cosign1Regex);
const cosekeyMatch = data.match(cosekeyRegex);
const challengeMatch = data.match(challengeRegex)

const validateVote = (vote:string): 'yes'|'no'|'abstain' => {
const validVotes = ['yes', 'no', 'abstain'];
return validVotes.includes(vote) ? vote as any : 'abstain';
};

return {
pollName: pollNameMatch ? pollNameMatch[1] : '',
voterName: walletMatch ? walletMatch[1] : '',
constutionHash: constitutionHashMatch ? constitutionHashMatch[1] : '',
constutionLink: constitutionLinkMatch ? constitutionLinkMatch[1] : '',
voterStake: walletMatch ? walletMatch[1] : '',
challenge: challengeMatch? challengeMatch[1]: '',
vote: voteMatch ? validateVote(voteMatch[1]) : 'abstain',
cosesign1: cosign1Match ? cosign1Match[1] : '',
cosekey: cosekeyMatch ? cosekeyMatch[1] : ''
};
}

///Wallet: stake_test1uqp28kgg6cafhzuf74eyh8trpa4fh5w50776wk5xh5npv8gx0zh92,
//Poll: Rustic Plastic Chips, Hashed Constitution Text: 16885f03dd0368dbcaaf80ea0ebc5c2e81b46dd082ec628662d69968af096f8d,
// Link to Constitution Text: https://proud-publication.info/, Vote: no,
// Timestamp: 12/2/2024, 11:14:32 PM, Challenge: fa2c6340ddfbb809735bd51217f0cbcbd3bd2b487654549f054e74ac5e8c9d87
export function decodeOnChainPayload(data: string): VotePayload {
// Regular Expressions for matching different parts of the data
const pollNameRegex = /Poll: ([^,]+)/;
const constitutionHashRegex = /Hashed Constitution Text: ([^,]+)/;
const constitutionLinkRegex = /Link to Constitution Text: ([^,]+)/;
const voteRegex = /Vote: (\w+)/;
const walletRegex = /Wallet: ([^,]+)/;
const challengeRegex = /Challenge: ([^,]+)/;

// Match each part of the data
const pollNameMatch = data.match(pollNameRegex);
const constitutionHashMatch = data.match(constitutionHashRegex);
const constitutionLinkMatch = data.match(constitutionLinkRegex);
const voteMatch = data.match(voteRegex);
const walletMatch = data.match(walletRegex);
const challengeMatch = data.match(challengeRegex);

// Validate the vote (same as the one in the extractVoteData function)
const validateVote = (vote: string): 'yes' | 'no' | 'abstain' => {
const validVotes = ['yes', 'no', 'abstain'];
return validVotes.includes(vote) ? vote as any : 'abstain';
};

// Return the parsed data in a structured format
return {
pollName: pollNameMatch ? pollNameMatch[1] : '',
constutionHash: constitutionHashMatch ? constitutionHashMatch[1] : '',
constutionLink: constitutionLinkMatch ? constitutionLinkMatch[1] : '',
voterStake: walletMatch ? walletMatch[1] : '', // In this case, it seems to be the wallet address again
challenge: challengeMatch ? challengeMatch[1] : '',
vote: voteMatch ? validateVote(voteMatch[1]) : 'abstain',
};
}

68 changes: 68 additions & 0 deletions integration_test/lib/helpers/poll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { alternateWallets, delegateWallets, organizerWallets } from "@constants/staticWallets";
import { pageWithInjectedWallet } from "@fixtures/importWallet";
import HomePage from "@pages/homePage";
import { StaticWallet } from "@types";
import { nAtaTime, sleep } from "./txUtil";
import PollPage from "@pages/pollPage";
import Logger from "./logger";
import { Browser, expect, Page } from "@playwright/test";
export type VotedPoll = {
pollId: number,
votes: Record<string,'yes'| 'no'|'abstain'>
}
export async function createFullyVotedPoll (browser: Browser,voteCount?:number) : Promise<VotedPoll&{organizerPollPage:Page}> {
const getPage=async (w:StaticWallet)=>await pageWithInjectedWallet(browser,w)


const organizerPages=await Promise.all([organizerWallets[0]].map(getPage));

const organizerHomePage = new HomePage(organizerPages[0]);
const deleted = await organizerHomePage.deleteOpenPollCards();
if(deleted){
await organizerHomePage.goto()
}
const pollId = await organizerHomePage.createPoll()
await organizerHomePage.beginVoteBtn.click();

const voteButtons = [
'vote-yes-button',
'vote-no-button',
'vote-abstain-button',
];
const votes=['yes','no','abstain']
const castedVotes={}


await nAtaTime(voteCount? delegateWallets.slice(0,voteCount):delegateWallets,async (wallet,index)=>{
const context = await browser.newContext()

let page = await pageWithInjectedWallet(context,wallet)

let pollPage=new PollPage(page)
await pollPage.goto(pollId)

const isActive = await pollPage.voteYesBtn.waitFor({state: "visible",timeout: 30000}).then(()=>true).catch(()=>false)

if(!isActive){
Logger.info("User is not active voter: "+wallet.stakeAddress)
await page.close()
wallet=alternateWallets[index]
page = await pageWithInjectedWallet(context,alternateWallets[index])
pollPage=new PollPage(page)
await pollPage.goto(pollId)
}

const randomVote = Math.floor(Math.random() * 3);
await page.getByTestId(voteButtons[randomVote]).click();
castedVotes[wallet.stakeAddress] = votes[randomVote];
await expect(page.getByText('Vote recorded!'),"Expected Vote to be recorded for user: "+wallet.stakeAddress).toBeVisible({timeout:20000})
await page.close()
await context.close()
},6);

const organizerPollPage=new PollPage(organizerPages[0])
await organizerPollPage.endVoting()
await sleep(2000)
return {pollId,votes:castedVotes,organizerPollPage:organizerPages[0]}

}
20 changes: 19 additions & 1 deletion integration_test/lib/helpers/txUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,22 @@ export async function waitForTxSubmit(

export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

export async function nAtaTime<T,U>(array: T[], f: (item: T,index?:number) => Promise<U>,n:number =10): Promise<U[]> {
const chunkSize = n;
let results: U[] = [];

// Process the array in chunks of `n`
for (let i = 0; i < array.length; i += chunkSize) {
const chunk = array.slice(i, i + chunkSize);

// Wait for all async operations on this chunk to finish and collect the results
const chunkResults = await Promise.all(chunk.map((item,index) => f(item,index)));

// Push the results of the current chunk to the results array
results.push(...chunkResults);
}

return results;
}
6 changes: 3 additions & 3 deletions integration_test/lib/pages/loginPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export default class LoginPage {

await this.connectWalletBtn.first().click();
await this.eternlWalletBtn.click({ force: true });
await expect(this.page.getByTestId('connected-user-name')).toBeVisible({timeout: 30000})
// this is taking absurdly long time when testing with 10 parallel users
await expect(this.page.getByTestId('connected-user-name')).toBeVisible({timeout: 40000})
}

async logout(): Promise<void> {
Expand All @@ -25,7 +26,6 @@ export default class LoginPage {
}

async isLoggedIn(): Promise<void> {
await this.connectWalletBtn.first().click();
await expect(this.disconnectWalletBtn).toBeVisible();
await expect(this.page.getByTestId('connected-user-name')).toBeVisible({timeout: 40000})
}
}
8 changes: 8 additions & 0 deletions integration_test/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export default defineConfig({
testMatch: '**/*independent.spec.ts',
use: { ...devices['Galaxy Tab S4'] },
},

{
name: 'on-chain',
testMatch: 'y-on-chain-tests.spec.ts',
use: { ...devices['Desktop Chrome'] },
dependencies: environments.ci ? ['auth setup'] : [],
},


/* Test against mobile viewports. */
// {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
newDelegate2Page,
newDelegatePage,
} from '@helpers/page';
import { pollTransaction, sleep, waitForTxSubmit } from '@helpers/txUtil';


test.beforeEach(async () => {
await setAllureEpic('1. Convention Organizers');
Expand Down Expand Up @@ -306,54 +306,6 @@ test.describe('Create Poll', () => {
});
});

test.describe('Onchain Poll', () => {
test.use({
pollType: 'VotedPoll',
}); //
/**
* Description: The transaction that contains the aggregated results on-chain must also contain all of the transaction IDs of the vote transactions.
User story: As an Observer I want to have access to all the vote transaction IDs in one transaction, so that I only need to be given the reference to one transaction ID to adit the vote on-chain.
Acceptance Criteria: Given that I am an observer, when I look up the transaction ID of the results summary transaction on-chain, then I will see all the transaction IDs of the votes for this poll.
*/ 2;

test('1-1H . Given CO, can submit poll results onchain', async ({
page,
pollId,
}) => {
test.slow()
const pollPage = new PollPage(page);
pollPage.goto(pollId);
const waiter = waitForTxSubmit(page)
await pollPage.uploadVoteOnchainBtn.waitFor({ state: 'visible' });
await sleep(1000)

// Click the button and start transaction submission
await pollPage.uploadVoteOnchainBtn.click();
console.log("Upload votes button clicked!!")
const votesTxId = await waiter
const summaryTxWaiter = waitForTxSubmit(page)
await pollTransaction(votesTxId)


const summaryTxId = await summaryTxWaiter
await pollTransaction(summaryTxId);

// click on viewVote Onchain Button and check the url in new tab

const newPagePopup = page.waitForEvent('popup')
await pollPage.viewTxOnchainBtn.waitFor({ state: 'visible' });
await pollPage.viewTxOnchainBtn.click()
const newPage = await newPagePopup

await newPage.waitForLoadState('domcontentloaded');
const expectedUrl = `/transaction/${summaryTxId}`;
expect(newPage.url()).toContain(expectedUrl);

});
});

test.describe('User Control', () => {
test.use({ pollType: 'CreatePoll' });

Expand Down
Loading

0 comments on commit afd8bf8

Please sign in to comment.