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: index EVM spaces #1123

Draft
wants to merge 8 commits into
base: sekhmet/checkpoint-multi-chain
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
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"prettier": "@snapshot-labs/prettier-config",
"dependencies": {
"@apollo/server": "^4.2.2",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/address": "^5.6.1",
"@ethersproject/bignumber": "^5.6.1",
"@ethersproject/contracts": "^5.7.0",
Expand Down
163 changes: 163 additions & 0 deletions apps/api/src/common/ipfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { dropIpfs, getJSON } from './utils';
import {
ProposalMetadataItem,
StrategiesParsedMetadataDataItem,
StrategiesParsedMetadataItem,
VoteMetadataItem,
VotingPowerValidationStrategiesParsedMetadataItem
} from '../../.checkpoint/models';

type CommonConfig = {
indexerName: string;
};

export async function handleProposalMetadata(
metadataUri: string,
config: CommonConfig
) {
const exists = await ProposalMetadataItem.loadEntity(
dropIpfs(metadataUri),
config.indexerName
);
if (exists) return;

const proposalMetadataItem = new ProposalMetadataItem(
dropIpfs(metadataUri),
config.indexerName
);
proposalMetadataItem.choices = ['For', 'Against', 'Abstain'];
proposalMetadataItem.labels = [];

const metadata: any = await getJSON(metadataUri);
if (metadata.title) proposalMetadataItem.title = metadata.title;
if (metadata.body) proposalMetadataItem.body = metadata.body;
if (metadata.discussion)
proposalMetadataItem.discussion = metadata.discussion;
if (metadata.execution)
proposalMetadataItem.execution = JSON.stringify(metadata.execution);
if (
Array.isArray(metadata.labels) &&
metadata.labels.every((label: string) => typeof label === 'string')
) {
proposalMetadataItem.labels = metadata.labels;
}
if (
Array.isArray(metadata.choices) &&
metadata.choices.length === 3 &&
metadata.choices.every((choice: string) => typeof choice === 'string')
) {
proposalMetadataItem.choices = metadata.choices;
}

await proposalMetadataItem.save();
}

export async function handleStrategiesParsedMetadata(
metadataUri: string,
config: CommonConfig
) {
const exists = await StrategiesParsedMetadataDataItem.loadEntity(
dropIpfs(metadataUri),
config.indexerName
);
if (exists) return;

const strategiesParsedMetadataItem = new StrategiesParsedMetadataDataItem(
dropIpfs(metadataUri),
config.indexerName
);

const metadata: any = await getJSON(metadataUri);
if (metadata.name) strategiesParsedMetadataItem.name = metadata.name;
if (metadata.description)
strategiesParsedMetadataItem.description = metadata.description;

if (metadata.properties) {
if (metadata.properties.decimals) {
strategiesParsedMetadataItem.decimals = metadata.properties.decimals;
}
if (metadata.properties.symbol) {
strategiesParsedMetadataItem.symbol = metadata.properties.symbol;
}
if (metadata.properties.token)
strategiesParsedMetadataItem.token = metadata.properties.token;
if (metadata.properties.payload) {
strategiesParsedMetadataItem.payload = metadata.properties.payload;
}
}

await strategiesParsedMetadataItem.save();
}

export async function handleVoteMetadata(
metadataUri: string,
config: CommonConfig
) {
const exists = await VoteMetadataItem.loadEntity(
dropIpfs(metadataUri),
config.indexerName
);
if (exists) return;

const voteMetadataItem = new VoteMetadataItem(
dropIpfs(metadataUri),
config.indexerName
);

const metadata: any = await getJSON(metadataUri);
voteMetadataItem.reason = metadata.reason ?? '';

await voteMetadataItem.save();
}

export async function handleStrategiesMetadata(
spaceId: string,
metadataUris: string[],
startingIndex: number,
config: CommonConfig,
type:
| typeof StrategiesParsedMetadataItem
| typeof VotingPowerValidationStrategiesParsedMetadataItem = StrategiesParsedMetadataItem
) {
for (let i = 0; i < metadataUris.length; i++) {
const metadataUri = metadataUris[i];
if (!metadataUri) continue;

const index = startingIndex + i;
const uniqueId = `${spaceId}/${index}/${dropIpfs(metadataUri)}`;

const exists = await type.loadEntity(uniqueId, config.indexerName);
if (exists) continue;

const strategiesParsedMetadataItem = new type(uniqueId, config.indexerName);
strategiesParsedMetadataItem.space = spaceId;
strategiesParsedMetadataItem.index = index;

if (metadataUri.startsWith('ipfs://')) {
strategiesParsedMetadataItem.data = dropIpfs(metadataUri);

await handleStrategiesParsedMetadata(metadataUri, config);
}

await strategiesParsedMetadataItem.save();
}
}

export async function handleVotingPowerValidationMetadata(
spaceId: string,
metadataUri: string,
config: CommonConfig
) {
if (!metadataUri) return;

const metadata: any = await getJSON(metadataUri);
if (!metadata.strategies_metadata) return;

await handleStrategiesMetadata(
spaceId,
metadata.strategies_metadata,
0,
config,
VotingPowerValidationStrategiesParsedMetadataItem
);
}
45 changes: 45 additions & 0 deletions apps/api/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { faker } from '@faker-js/faker';
import fetch from 'cross-fetch';
import { hash } from 'starknet';

export function getUrl(uri: string, gateway = 'pineapple.fyi') {
const ipfsGateway = `https://${gateway}`;
if (!uri) return null;
if (
!uri.startsWith('ipfs://') &&
!uri.startsWith('ipns://') &&
!uri.startsWith('https://') &&
!uri.startsWith('http://')
)
return `${ipfsGateway}/ipfs/${uri}`;
const uriScheme = uri.split('://')[0];
if (uriScheme === 'ipfs')
return uri.replace('ipfs://', `${ipfsGateway}/ipfs/`);
if (uriScheme === 'ipns')
return uri.replace('ipns://', `${ipfsGateway}/ipns/`);
return uri;
}

export function getCurrentTimestamp() {
return Math.floor(Date.now() / 1000);
}

export function dropIpfs(metadataUri: string) {
return metadataUri.replace('ipfs://', '');
}

export function getSpaceName(address: string) {
const seed = parseInt(
hash.getSelectorFromName(address).toString().slice(0, 12)
);
faker.seed(seed);
const noun = faker.word.noun(6);
return `${noun.charAt(0).toUpperCase()}${noun.slice(1)} DAO`;
}

export async function getJSON(uri: string) {
const url = getUrl(uri);
if (!url) throw new Error('Invalid URI');

return fetch(url).then(res => res.json());
}
3 changes: 3 additions & 0 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** Infura API key used by default for network nodes. */
export const DEFAULT_INFURA_API_KEY =
process.env.INFURA_API_KEY || '46a5dd9727bf48d4a132672d3f376146';
69 changes: 69 additions & 0 deletions apps/api/src/evm/abis/AxiomExecutionStrategy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
[
"constructor(address _owner, uint256 _quorum, address _axiomV2QueryAddress, address _snapshotAddress, uint256 _snapshotSlot, address _spaceAddress, bytes32 _querySchema)",
"error ArrayLengthMismatch()",
"error AuthenticatorNotWhitelisted()",
"error AxiomV2QueryAddressIsZero()",
"error CallbackTypeIsNotOffChain()",
"error CallerMustBeAxiomV2Query()",
"error EmptyArray()",
"error ExceedsStrategyLimit()",
"error ExecutionFailed()",
"error FailedToPassProposalValidation()",
"error InvalidCaller()",
"error InvalidDuration(uint32 minVotingDuration, uint32 maxVotingDuration)",
"error InvalidPayload()",
"error InvalidProposal()",
"error InvalidProposalStatus(uint8 status)",
"error InvalidSpace()",
"error InvalidStrategyIndex(uint256 index)",
"error NoActiveVotingStrategies()",
"error OffchainVotesAlreadyProven()",
"error OffchainVotesNotProven()",
"error ProposalEndBlockNumberDoesNotMatch()",
"error ProposalFinalized()",
"error ProposalStartBlockNumberDoesNotMatch()",
"error QuerySchemaDoesNotMatch()",
"error SnapshotAddressDoesNotMatch()",
"error SnapshotSlotDoesNotMatch()",
"error SourceChainIdDoesNotMatch()",
"error SpaceAddressDoesNotMatch()",
"error UserAlreadyVoted()",
"error UserHasNoVotingPower()",
"error VotingDelayHasPassed()",
"error VotingPeriodHasEnded()",
"error VotingPeriodHasNotStarted()",
"error WriteOffchainVotesCalledBeforeProposalMaxEndBlockNumber()",
"error ZeroAddress()",
"event AxiomExecutionStrategySetUp(address indexed owner, uint256 quorum, address indexed snapshotAddress, uint256 snapshotSlot, address spaceAddress, bytes32 querySchema)",
"event AxiomV2Call(uint64 indexed sourceChainId, address caller, bytes32 indexed querySchema, uint256 indexed queryId)",
"event AxiomV2OffchainCall(uint64 indexed sourceChainId, address caller, bytes32 indexed querySchema, uint256 indexed queryId)",
"event Initialized(uint8 version)",
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
"event QuerySchemaUpdated(bytes32 newQuerySchema)",
"event QuorumUpdated(uint256 newQuorum)",
"event SpaceDisabled(address space)",
"event SpaceEnabled(address space)",
"event TransactionExecuted(tuple(address to, uint256 value, bytes data, uint8 operation, uint256 salt) transaction)",
"event WriteOffchainVotes(uint256 proposalId, uint256 queryId, uint256 offchainForVotes, uint256 offchainAgainstVotes, uint256 offchainAbstainVotes)",
"function axiomV2Callback(uint64 sourceChainId, address caller, bytes32 querySchema, uint256 queryId, bytes32[] axiomResults, bytes extraData)",
"function axiomV2OffchainCallback(uint64 sourceChainId, address caller, bytes32 querySchema, uint256 queryId, bytes32[] axiomResults, bytes extraData)",
"function axiomV2QueryAddress() view returns (address)",
"function disableSpace(address space)",
"function enableSpace(address space)",
"function execute(uint256 proposalId, tuple(address author, uint32 startBlockNumber, address executionStrategy, uint32 minEndBlockNumber, uint32 maxEndBlockNumber, uint8 finalizationStatus, bytes32 executionPayloadHash, uint256 activeVotingStrategies) proposal, uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain, bytes payload)",
"function getProposalStatus(tuple(address author, uint32 startBlockNumber, address executionStrategy, uint32 minEndBlockNumber, uint32 maxEndBlockNumber, uint8 finalizationStatus, bytes32 executionPayloadHash, uint256 activeVotingStrategies) proposal, uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain) view returns (uint8)",
"function getStrategyType() pure returns (string)",
"function isSpaceEnabled(address space) view returns (uint256)",
"function offchainVoteResults(uint256 proposalId) view returns (bool areOffchainVotesProven, uint256 offchainVotesFor, uint256 offchainVotesAgainst, uint256 offchainVotesAbstain)",
"function owner() view returns (address)",
"function querySchema() view returns (bytes32)",
"function quorum() view returns (uint256)",
"function renounceOwnership()",
"function setQuerySchema(bytes32 _querySchema)",
"function setQuorum(uint256 _quorum)",
"function setUp(bytes initializeParams)",
"function snapshotAddress() view returns (address)",
"function snapshotSlot() view returns (uint256)",
"function space() view returns (address)",
"function transferOwnership(address newOwner)"
]
35 changes: 35 additions & 0 deletions apps/api/src/evm/abis/L1AvatarExecutionStrategy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[
"constructor()",
"function disableSpace(uint256 space)",
"function enableSpace(uint256 space)",
"function execute(uint256 space, uint256 proposalId, tuple(uint32 startTimestamp, uint32 minEndTimestamp, uint32 maxEndTimestamp, uint8 finalizationStatus, uint256 executionPayloadHash, uint256 executionStrategy, uint256 authorAddressType, uint256 author, uint256 activeVotingStrategies) proposal, tuple(uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain) votes, uint256 executionHash, tuple(address to, uint256 value, bytes data, uint8 operation)[] transactions)",
"function executionRelayer() view returns (uint256)",
"function getProposalStatus(tuple(uint32 startTimestamp, uint32 minEndTimestamp, uint32 maxEndTimestamp, uint8 finalizationStatus, uint256 executionPayloadHash, uint256 executionStrategy, uint256 authorAddressType, uint256 author, uint256 activeVotingStrategies) proposal, uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain) view returns (uint8)",
"function getStrategyType() pure returns (string)",
"function isSpaceEnabled(uint256 space) view returns (uint256)",
"function owner() view returns (address)",
"function quorum() view returns (uint256)",
"function renounceOwnership()",
"function setExecutionRelayer(uint256 _executionRelayer)",
"function setQuorum(uint256 _quorum)",
"function setStarknetCore(address _starknetCore)",
"function setTarget(address _target)",
"function setUp(address _owner, address _target, address _starknetCore, uint256 _executionRelayer, uint256[] _starknetSpaces, uint256 _quorum)",
"function starknetCore() view returns (address)",
"function target() view returns (address)",
"function transferOwnership(address newOwner)",
"event ExecutionRelayerSet(uint256 indexed newExecutionRelayer)",
"event Initialized(uint8 version)",
"event L1AvatarExecutionStrategySetUp(address indexed _owner, address _target, address _starknetCore, uint256 _executionRelayer, uint256[] _starknetSpaces, uint256 _quorum)",
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
"event ProposalExecuted(uint256 indexed space, uint256 proposalId)",
"event QuorumUpdated(uint256 newQuorum)",
"event SpaceDisabled(uint256 space)",
"event SpaceEnabled(uint256 space)",
"event StarknetCoreSet(address indexed newStarknetCore)",
"event TargetSet(address indexed newTarget)",
"error ExecutionFailed()",
"error InvalidPayload()",
"error InvalidProposalStatus(uint8 status)",
"error InvalidSpace()"
]
7 changes: 7 additions & 0 deletions apps/api/src/evm/abis/L1AvatarExecutionStrategyFactory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"constructor(address _implementation)",
"function createContract(address _owner, address _target, address _starknetCore, uint256 _executionRelayer, uint256[] _starknetSpaces, uint256 _quorum, bytes32 salt)",
"function deployedContracts(uint256) view returns (address)",
"function implementation() view returns (address)",
"event ContractDeployed(address indexed contractAddress)"
]
8 changes: 8 additions & 0 deletions apps/api/src/evm/abis/ProxyFactory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
"error FailedInitialization()",
"error InvalidImplementation()",
"error SaltAlreadyUsed()",
"event ProxyDeployed(address implementation, address proxy)",
"function deployProxy(address implementation, bytes initializer, uint256 saltNonce)",
"function predictProxyAddress(address implementation, bytes32 salt) view returns (address)"
]
27 changes: 27 additions & 0 deletions apps/api/src/evm/abis/SimpleQuorumAvatarExecutionStrategy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[
"constructor(address _owner, address _target, address[] _spaces, uint256 _quorum)",
"error ExecutionFailed()",
"error InvalidProposalStatus(uint8 status)",
"error InvalidSpace()",
"event AvatarExecutionStrategySetUp(address _owner, address _target, address[] _spaces, uint256 _quorum)",
"event Initialized(uint8 version)",
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
"event QuorumUpdated(uint256 newQuorum)",
"event SpaceDisabled(address space)",
"event SpaceEnabled(address space)",
"event TargetSet(address indexed newTarget)",
"function disableSpace(address space)",
"function enableSpace(address space)",
"function execute(uint256 proposalId, tuple(address author, uint32 startBlockNumber, address executionStrategy, uint32 minEndBlockNumber, uint32 maxEndBlockNumber, uint8 finalizationStatus, bytes32 executionPayloadHash, uint256 activeVotingStrategies) proposal, uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain, bytes payload)",
"function getProposalStatus(tuple(address author, uint32 startBlockNumber, address executionStrategy, uint32 minEndBlockNumber, uint32 maxEndBlockNumber, uint8 finalizationStatus, bytes32 executionPayloadHash, uint256 activeVotingStrategies) proposal, uint256 votesFor, uint256 votesAgainst, uint256 votesAbstain) view returns (uint8)",
"function getStrategyType() pure returns (string)",
"function isSpaceEnabled(address space) view returns (uint256)",
"function owner() view returns (address)",
"function quorum() view returns (uint256)",
"function renounceOwnership()",
"function setQuorum(uint256 _quorum)",
"function setTarget(address _target)",
"function setUp(bytes initParams)",
"function target() view returns (address)",
"function transferOwnership(address newOwner)"
]
Loading