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

feat: WIP - Display active governance proposals #79

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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
Loading