-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
2,284 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
Oops, something went wrong.