Skip to content

Commit

Permalink
chore: import mana repository
Browse files Browse the repository at this point in the history
  • Loading branch information
Sekhmet committed Feb 12, 2024
2 parents 5d7b45b + 17f313d commit 9398be2
Show file tree
Hide file tree
Showing 25 changed files with 2,284 additions and 67 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ yarn

### Compiles and hot-reloads for development

```
yarn dev
```sh
yarnh dev

# if you want to run full stack (including backend services)
yarn dev:full
```

### Compiles and minifies for production
Expand Down
12 changes: 12 additions & 0 deletions apps/mana/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = LF
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
12 changes: 12 additions & 0 deletions apps/mana/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
STARKNET_MNEMONIC=
ETH_ADDRESS=
ETH_PRIVKEY=
ETH_MNEMONIC=
ETH_RPC_URL=https://rpc.brovider.xyz/5
STARKNET_MAINNET_RPC_URL=https://starknet-mainnet.infura.io/v3/46a5dd9727bf48d4a132672d3f376146
STARKNET_GOERLI_RPC_URL=https://starknet-goerli.infura.io/v3/46a5dd9727bf48d4a132672d3f376146
STARKNET_SEPOLIA_RPC_URL=https://starknet-sepolia.infura.io/v3/46a5dd9727bf48d4a132672d3f376146
FOSSIL_ADDRESS=
HERODOTUS_API_KEY=
DATABASE_URL=postgres://postgres:password@localhost:5432/mana

10 changes: 10 additions & 0 deletions apps/mana/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.DS_Store
node_modules
dist
build

# Remove some common IDE working directories
.idea
.vscode

.env
15 changes: 15 additions & 0 deletions apps/mana/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3.8'
services:
postgres:
image: postgres:15.4
ports:
- '5432:5432'
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mana
volumes:
- pg:/var/lib/postgresql/data
volumes:
pg:
driver: local
49 changes: 49 additions & 0 deletions apps/mana/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "mana",
"version": "0.1.0",
"private": true,
"license": "MIT",
"scripts": {
"lint": "eslint . --ext .ts --fix",
"build": "tsc",
"dev": "nodemon src/index.ts",
"start": "node dist/src/index.js"
},
"eslintConfig": {
"extends": "@snapshot-labs"
},
"prettier": "@snapshot-labs/prettier-config",
"dependencies": {
"@ethereumjs/block": "3.6.3",
"@ethereumjs/common": "2.6.5",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/experimental": "5.7.0",
"@ethersproject/providers": "^5.7.0",
"@ethersproject/wallet": "^5.7.0",
"@scure/bip32": "^1.3.3",
"@scure/bip39": "^1.2.2",
"@snapshot-labs/sx": "^0.1.0-beta.60",
"abi-wan-kanabi": "^2.0.0",
"async-mutex": "^0.4.0",
"connection-string": "^4.4.0",
"cors": "^2.8.5",
"cross-fetch": "^3.1.5",
"dotenv": "^16.0.0",
"express": "^4.17.1",
"knex": "^2.5.1",
"pg": "^8.11.3",
"starknet": "5.25.0"
},
"devDependencies": {
"@snapshot-labs/eslint-config": "0.1.0-beta.13",
"@snapshot-labs/prettier-config": "0.1.0-beta.11",
"@types/bn.js": "^5.1.0",
"@types/express": "^4.17.11",
"@types/node": "^18.14.4",
"eslint": "^8.53.0",
"nodemon": "^3.0.1",
"prettier": "^3.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
94 changes: 94 additions & 0 deletions apps/mana/src/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import knex from './knex';

export const REGISTERED_TRANSACTIONS = 'registered_transactions';
export const REGISTERED_PROPOSALS = 'registered_proposals';

export async function createTables() {
const registeredTransactionsTableExists = await knex.schema.hasTable(REGISTERED_TRANSACTIONS);
const registeredProposalsTableExists = await knex.schema.hasTable(REGISTERED_PROPOSALS);

if (!registeredTransactionsTableExists) {
await knex.schema.createTable(REGISTERED_TRANSACTIONS, t => {
t.increments('id').primary();
t.timestamps(true, true);
t.boolean('processed').defaultTo(false).index();
t.boolean('failed').defaultTo(false).index();
t.string('network').index();
t.string('type').index();
t.string('hash');
t.json('data');
});
}

if (!registeredProposalsTableExists) {
await knex.schema.createTable(REGISTERED_PROPOSALS, t => {
t.string('id').primary();
t.timestamps(true, true);
t.string('chainId');
t.integer('timestamp');
t.string('strategyAddress');
t.string('herodotusId');
t.boolean('processed').defaultTo(false).index();
});
}
}

export async function registerTransaction(network: string, type: string, hash: string, data: any) {
return knex(REGISTERED_TRANSACTIONS).insert({
network,
type,
hash,
data
});
}

export async function getTransactionsToProcess() {
return knex(REGISTERED_TRANSACTIONS).select('*').where({ processed: false });
}

export async function markTransactionProcessed(id: number, { failed = false } = {}) {
return knex(REGISTERED_TRANSACTIONS)
.update({ updated_at: knex.fn.now(), processed: true, failed })
.where({ id });
}

export async function markOldTransactionsAsProcessed() {
return knex(REGISTERED_TRANSACTIONS)
.update({ updated_at: knex.fn.now(), processed: true, failed: true })
.whereRaw("created_at < now() - interval '1 day'");
}

export async function registerProposal(
id: string,
proposal: {
chainId: string;
timestamp: number;
strategyAddress: string;
herodotusId: string | null;
}
) {
return knex(REGISTERED_PROPOSALS).insert({
id,
...proposal
});
}

export async function updateProposal(id: string, proposal: { herodotusId: string }) {
return knex(REGISTERED_PROPOSALS)
.update({ updated_at: knex.fn.now(), ...proposal })
.where({ id });
}

export async function getProposalsToProcess() {
return knex(REGISTERED_PROPOSALS).select('*').where({ processed: false });
}

export async function markProposalProcessed(id: string) {
return knex(REGISTERED_PROPOSALS)
.update({ updated_at: knex.fn.now(), processed: true })
.where({ id });
}

export async function getProposal(id: string) {
return knex(REGISTERED_PROPOSALS).select('*').where({ id }).first();
}
30 changes: 30 additions & 0 deletions apps/mana/src/eth/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { Wallet } from '@ethersproject/wallet';

const DEFAULT_INDEX = 0;
const SPACES_INDICIES = {
'0x65e4329e8c0fba31883b98e2cf3e81d3cdcac780': 1, // SekhmetDAO
'0x4d95a8be4f1d24d50cc0d7b12f5576fa4bbd892b': 2 // Labs
};

export function getEthereumWallet(mnemonic: string, index: number) {
const path = `m/44'/60'/0'/0/${index}`;
return Wallet.fromMnemonic(mnemonic, path);
}

export const createWalletProxy = (mnemonic: string, chainId: number) => {
const signers = new Map<string, Wallet>();
const provider = new StaticJsonRpcProvider(`https://rpc.snapshotx.xyz/${chainId}`, chainId);

return (spaceAddress: string) => {
const normalizedSpaceAddress = spaceAddress.toLowerCase();

if (!signers.has(normalizedSpaceAddress)) {
const index = SPACES_INDICIES[normalizedSpaceAddress] || DEFAULT_INDEX;
const wallet = getEthereumWallet(mnemonic, index);
signers.set(normalizedSpaceAddress, wallet.connect(provider));
}

return signers.get(normalizedSpaceAddress) as Wallet;
};
};
37 changes: 37 additions & 0 deletions apps/mana/src/eth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import express from 'express';
import { createNetworkHandler, NETWORKS } from './rpc';
import { getEthereumWallet } from './dependencies';
import { DEFAULT_INDEX, SPACES_INDICIES } from '../stark/dependencies';

const router = express.Router();

const handlers = Object.fromEntries(
Object.keys(NETWORKS).map(chainId => [chainId, createNetworkHandler(parseInt(chainId, 10))])
);

router.post('/:chainId?', (req, res) => {
const chainId = req.params.chainId || '5';
const handler = handlers[chainId];

const { id, method, params } = req.body;
handler[method](id, params, res);
});

router.get('/relayers', (req, res) => {
const mnemonic = process.env.ETH_MNEMONIC || '';

const defaultRelayer = getEthereumWallet(mnemonic, DEFAULT_INDEX).address;
const relayers = Object.fromEntries(
Object.entries(SPACES_INDICIES).map(([spaceAddress, index]) => {
const { address } = getEthereumWallet(mnemonic, index);
return [spaceAddress, address];
})
);

res.json({
default: defaultRelayer,
...relayers
});
});

export default router;
106 changes: 106 additions & 0 deletions apps/mana/src/eth/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
clients,
evmPolygon,
evmArbitrum,
evmMainnet,
evmGoerli,
evmSepolia,
evmLineaGoerli
} from '@snapshot-labs/sx';
import { createWalletProxy } from './dependencies';
import { rpcError, rpcSuccess } from '../utils';

export const NETWORKS = {
137: evmPolygon,
42161: evmArbitrum,
1: evmMainnet,
5: evmGoerli,
11155111: evmSepolia,
59140: evmLineaGoerli
} as const;

export const createNetworkHandler = (chainId: number) => {
const networkConfig = NETWORKS[chainId];
if (!networkConfig) throw new Error('Unsupported chainId');

const getWallet = createWalletProxy(process.env.ETH_MNEMONIC || '', chainId);

const client = new clients.EvmEthereumTx({ networkConfig: networkConfig });

async function send(id, params, res) {
try {
const { signatureData, data } = params.envelope;
const { types } = signatureData;
let receipt;

const signer = getWallet(params.envelope.data.space);

console.time('Send');
console.log('Types', types);
console.log('Message', data);

if (types.Propose) {
receipt = await client.propose({
signer,
envelope: params.envelope
});
} else if (types.updateProposal) {
receipt = await client.updateProposal({
signer,
envelope: params.envelope
});
} else if (types.Vote) {
receipt = await client.vote({
signer,
envelope: params.envelope
});
}

console.log('Receipt', receipt);

return rpcSuccess(res, receipt, id);
} catch (e) {
console.log('Failed', e);
return rpcError(res, 500, e, id);
} finally {
console.timeEnd('Send');
}
}

async function execute(id, params, res) {
try {
const { space, proposalId, executionParams } = params;
const signer = getWallet(space);

const receipt = await client.execute({
signer,
space,
proposal: proposalId,
executionParams
});

return rpcSuccess(res, receipt, id);
} catch (e) {
return rpcError(res, 500, e, id);
}
}

async function executeQueuedProposal(id, params, res) {
try {
const { space, executionStrategy, executionParams } = params;
const signer = getWallet(space);

const receipt = await client.executeQueuedProposal({
signer,
executionStrategy,
executionParams
});

return rpcSuccess(res, receipt, id);
} catch (e) {
return rpcError(res, 500, e, id);
}
}

return { send, execute, executeQueuedProposal };
};
Loading

0 comments on commit 9398be2

Please sign in to comment.