Skip to content

Commit

Permalink
feat: WIP - Display active governance proposals
Browse files Browse the repository at this point in the history
  • Loading branch information
alainncls committed Oct 9, 2024
1 parent c5c7880 commit 4a18faf
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 3 deletions.
275 changes: 275 additions & 0 deletions packages/functions/global-api-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import {
Activation,
Proposal,
ProposalStatus,
} from '@consensys/linea-voyager/src/types';

const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json',
};

export const LXP_CONTRACT_ADDRESS =
'0xd83af4fbD77f3AB65C3B1Dc4B38D7e67AEcf599A';
export const LXP_L_CONTRACT_ADDRESS =
'0x96B3a15257c4983A6fE9073D8C91763433124B82';

const { CONTENTFUL_API_KEY, LINEASCAN_API_KEY, TALLY_API_KEY } = process.env;

/**
* This function is called on every network call.
* @param event - The event object.
* @param event.httpMethod - The HTTP method used by the caller.
* @param event.body - The HTTP request body.
* @returns The response object.
*/
export async function handler(event: {
queryStringParameters: { address: string; isLineascan: boolean };
body: string;
httpMethod: string;
}) {
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 204,
headers,
body: '',
};
}

if (event.httpMethod === 'GET') {
if (!LINEASCAN_API_KEY) {
return {
statusCode: 500,
body: JSON.stringify({
message: 'Lineascan API key not set',
}),
};
}

if (!TALLY_API_KEY) {
return {
statusCode: 500,
body: JSON.stringify({
message: 'Tally API key not set',
}),
};
}

if (!CONTENTFUL_API_KEY) {
return {
statusCode: 500,
body: JSON.stringify({
message: 'Contentful API key not set',
}),
};
}

const { address, isLineascan } = event.queryStringParameters;

if (!address || address == '') {
return {
statusCode: 400,
body: JSON.stringify({
message: 'Missing address parameter',
}),
};
}

const [
activations,
pohStatus,
openBlockScore,
lxpBalance,
lxpLBalance,
name,
proposals,
] = await Promise.all([
getActivations(CONTENTFUL_API_KEY),
fetchPohStatus(address),
getOpenBlockScore(address.toLowerCase()),
isLineascan
? fetchBalanceFromLineascan(LXP_CONTRACT_ADDRESS, address)
: Promise.resolve('0'),
isLineascan
? fetchBalanceFromLineascan(LXP_L_CONTRACT_ADDRESS, address)
: Promise.resolve('0'),
isLineascan ? fetchLineaEns(address.toLowerCase()) : undefined,
fetchActiveProposals(TALLY_API_KEY),
]);

return {
statusCode: 200,
headers,
body: JSON.stringify({
activations,
pohStatus,
openBlockScore,
lxpBalance,
lxpLBalance,
name,
proposals,
}),
};
}

return {
statusCode: 405,
headers,
body: JSON.stringify({
message: 'Method not allowed',
}),
};
}

async function getData(
url: string,
additionalHeaders?: Record<string, string>,
) {
const response = await fetch(url, {
method: 'GET',
headers: {
...additionalHeaders,
'Content-Type': 'application/json',
},
});

if (!response.ok) {
console.error(`Call to ${url} failed with status ${response.status}`);
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response.json();
}

async function postData(
url: string,
data: Record<string, string>,
additionalHeaders?: Record<string, string>,
) {
const response = await fetch(url, {
method: 'POST',
headers: {
...additionalHeaders,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!response.ok) {
console.error(`Call to ${url} failed with status ${response.status}`);
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response.json();
}

/**
* Get current active activations from Contentful.
* @returns The activations object.
*/
async function getActivations(contentfulApiKey: string) {
const GET_XP_TAG = '4WJBpV24ju4wlbr6Kvi2pt';

try {
const res = await getData(
'https://api.contentful.com/spaces/64upluvbiuck/environments/master/entries/?content_type=activationsCard',
{
Authorization: contentfulApiKey,
},
);

const allActivations = res?.items ?? [];
return allActivations.filter((activation: Activation) => {
const isCurrent =
new Date(activation?.fields?.endDate?.['en-US']) > new Date();
const hasXpTag = activation?.fields?.tags?.['en-US']?.find(
(tag) => tag?.sys?.id === GET_XP_TAG,
);
return isCurrent && hasXpTag;
});
} catch (error) {
return [];
}
}

/**
* Get the current OpenBlock XP score for an address.
* @param address - The address to get the OpenBlock XP score for.
* @returns The OpenBlock XP score for the address.
*/
async function getOpenBlockScore(address: string) {
try {
const res = await getData(
`https://kx58j6x5me.execute-api.us-east-1.amazonaws.com/linea/userPointsSearchMetaMask?user=${address}`,
{
Origin: 'snap://linea-voyager',
},
);

return res[0].xp;
} catch (error) {
return 0;
}
}

async function fetchBalanceFromLineascan(
tokenBalance: string,
address: string,
) {
try {
const res = await getData(
`https://api.lineascan.build/api?module=account&action=tokenbalance&contractaddress=${tokenBalance}&address=${address}&tag=latest&apiKey=${LINEASCAN_API_KEY}`,
);

return res.result as string;
} catch (e) {
return '0';
}
}

async function fetchPohStatus(address: string) {
try {
const pohPayload = await getData(
`https://linea-xp-poh-api.linea.build/poh/${address}`,
);
return pohPayload.poh as boolean;
} catch (e) {
return false;
}
}

async function fetchLineaEns(address: string) {
try {
const res = await postData(
`https://api.studio.thegraph.com/query/69290/ens-linea-mainnet/version/latest`,
{
query: `query getNamesForAddress {domains(first: 1, where: {and: [{or: [{owner: \"${address}\"}, {registrant: \"${address}\"}, {wrappedOwner: \"${address}\"}]}, {parent_not: \"0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2\"}, {or: [{expiryDate_gt: \"1721033912\"}, {expiryDate: null}]}, {or: [{owner_not: \"0x0000000000000000000000000000000000000000\"}, {resolver_not: null}, {and: [{registrant_not: \"0x0000000000000000000000000000000000000000\"}, {registrant_not: null}]}]}]}) {...DomainDetailsWithoutParent}} fragment DomainDetailsWithoutParent on Domain {name}`,
},
);
return res.data.domains[0].name as string;
} catch (e) {
return undefined;
}
}

async function fetchActiveProposals(tallyApiKey: string) {
try {
const res = await postData(
`https://api.tally.xyz/query`,
{
query: `query Proposals { proposals(input: { filters: { governorId: "eip155:1:0x5d2C31ce16924C2a71D317e5BbFd5ce387854039\", includeArchived: false, isDraft: false } }) { nodes { ... on Proposal { id onchainId chainId votableChains createdAt l1ChainId originalId quorum status metadata { title description eta ipfsHash previousEnd timelockId txHash discourseURL snapshotURL } end { ... on Block { id timestamp } ... on BlocklessTimestamp { timestamp } } start { ... on Block { id timestamp } ... on BlocklessTimestamp { timestamp } } } } } }`,
},
{
'Api-Key': tallyApiKey,
},
);
const allProposals = res.data.proposals.nodes as Proposal[];
return allProposals.filter(
(proposal) => proposal.status === ProposalStatus.Active,
);
} catch (e) {
return undefined;
}
}
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/Consensys/linea-voyager-snap"
},
"source": {
"shasum": "oIZBQ1pqMiZJJvAIDJuYAHRvPR3AIB2Hlt99G27DP+E=",
"shasum": "1Imd2ziyA7p1i8orH+dC66gdJAHM7w34Bn1r03QRlm4=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const callGlobalApi = async (
isLineascan: boolean,
): Promise<UserData> => {
const response = await fetch(
`https://lxp-snap-api.netlify.app/.netlify/functions/global-api?address=${address}&isLineascan=${isLineascan}`,
`https://lxp-snap-api.netlify.app/.netlify/functions/global-api-v2?address=${address}&isLineascan=${isLineascan}`,
{
method: 'GET',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const onHomePage: OnHomePageHandler = async () => {
pohStatus,
activations,
name,
proposals,
} = await getDataForUser(myAccount, chainId);

await setState({
Expand All @@ -51,6 +52,7 @@ export const onHomePage: OnHomePageHandler = async () => {
myPohStatus: pohStatus,
activations,
myLineaEns: name,
proposals,
});

return renderMainUi(myAccount);
Expand Down
13 changes: 12 additions & 1 deletion packages/snap/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { decode } from '@metamask/abi-utils';
import type { Hex } from '@metamask/utils';

import { callGlobalApi } from './api';
import type { UserData } from './types';
import type { Proposal, UserData } from './types';
import {
convertBalanceToDisplay,
LXP_CONTRACT_ADDRESS,
Expand Down Expand Up @@ -40,6 +40,16 @@ export async function getDataForUser(
? convertBalanceToDisplay(userData.lxpLBalance.toString())
: lxpLBalanceRaw;
userData.name = isLineascan ? userData.name : name;
userData.proposals = userData.proposals.map((proposal: Proposal) => {
return {
...proposal,
metadata: {
...proposal.metadata,
title: proposal.metadata.title.replace(/^#\s*/u, ''),
description: proposal.metadata.description.replace(/^#\s*/u, ''),
},
};
});

return userData;
} catch (error) {
Expand All @@ -50,6 +60,7 @@ export async function getDataForUser(
pohStatus: false,
activations: [],
name: '',
proposals: [],
};
}
}
Expand Down
26 changes: 26 additions & 0 deletions packages/snap/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type UserData = {
lxpBalance: number;
lxpLBalance: number;
name: string;
proposals: Proposal[];
};

export type SnapState = {
Expand All @@ -83,4 +84,29 @@ export type SnapState = {
myPohStatus?: boolean;
activations?: Activation[];
myLineaEns?: string;
proposals?: Proposal[];
};

export type Proposal = {
id: string;
onchainId: string;
status: ProposalStatus;
metadata: {
title: string;
description: string;
};
start: BlockTime;
end: BlockTime;
};

export enum ProposalStatus {
Active = 'active',
Canceled = 'canceled',
Executed = 'executed',
Queued = 'queued',
}

export type BlockTime = {
id: string;
timestamp: Date;
};
Loading

0 comments on commit 4a18faf

Please sign in to comment.