:twisted_rightward_arrows: Merge staging (#224)

* Update test workflow

* 📦 Build `@anonklub/*` to commonjs (#223)

* Build @anonklub/query to commonJS

* Build @anonklub/proof to ES2022

* format

* refactor: do not use `baseUrl` to build @anonklub/query

It is tricky to use a lib that defines module aliases

* refactor: do not use `baseUrl` to build @anonklub/proof

It is tricky to use a lib that defines module aliases

* Build @anonklub/cli to ES2022

* format, lint

* fix: do not build as ESM

* add comment

* Update package.json files

* fix folder name and import path

* Fix path to coverageDirectory in jest preset

* name test workflow

* trigger ?

* rename workflow

* use jest projects

Necessary to have 1 root coverage folder that coveralls needs
This commit is contained in:
sripwoud
2023-08-11 10:14:01 +02:00
committed by GitHub
parent b8e6269bba
commit d5f4cd8080
59 changed files with 793 additions and 1044 deletions

View File

@@ -12,6 +12,7 @@ parserOptions:
'infra/tsconfig.json',
'test/tsconfig.json',
'ui/tsconfig.json',
'tsconfig.json',
]
rules:
@@ -20,7 +21,7 @@ rules:
overrides:
- files: ['apis/query/test/unit/DuneRepository.test.ts']
rules:
'@typescript-eslint/no-dynamic-delete': warn
'@typescript-eslint/no-dynamic-delete': off
- files: ['*.ts']
rules:
'@typescript-eslint/dot-notation': off # because using noPropertyAccessFromIndexSignature tsc option
@@ -77,7 +78,7 @@ overrides:
'apis/prove/src/mq/*.ts',
'circuits/circom/scripts/*.ts',
'@anonklub/cli/src/index.ts',
'@anonklub/cli/src/cli/index.ts',
'@anonklub/cli/src/Cli/index.ts',
'ui/src/lib/config.ts',
]
rules:
@@ -102,3 +103,6 @@ overrides:
- files: ['shared/src/index.ts']
rules:
'sort/exports': 'off'
- files: ['@anonklub/cli/src/Prompt/index.ts']
rules:
'@typescript-eslint/naming-convention': off

View File

@@ -1,4 +1,4 @@
name: Static Analysis
name: static analysis
on:
workflow_dispatch:
pull_request:

View File

@@ -1,25 +1,29 @@
name: Test
name: test
on:
# push trigger required to get coveralls monitoring of default branch
# pull_request required to get PR comments
# pull_request trigger required to get PR comments
workflow_dispatch:
pull_request:
branches: [main, staging]
paths:
[
'{apis,circuits,@anonklub}/**/*.{js,json,ts,tsx,graphql}',
'@anonklub/**/*.ts',
'apis/**/*.{js,json,ts,graphql}',
'test/**/*.{js,ts}',
'contracts/**/*.sol',
]
push:
branches: [main, staging]
paths:
[
'{apis,circuits,@anonklub}/**/*.{js,json,ts,tsx,graphql}',
'@anonklub/**/*.ts',
'apis/**/*.{js,json,ts,graphql}',
'test/**/*.{js,ts}',
'contracts/**/*.sol',
]
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:

View File

@@ -2,28 +2,50 @@
"name": "@anonklub/cli",
"version": "1.0.0",
"description": "CLI to build zk proofs of ethereum address ownership.",
"main": "index.js",
"keywords": [],
"repository": "https://github.com/anonklub/anonklub/tree/main/%40anonklub/cli",
"homepage": "https://github.com/anonklub/anonklub",
"bugs": "https://github.com/anonklub/anonklub/issues",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"bin": {
"akcli": "dist/index.js"
},
"keywords": [
"zk",
"ethereum",
"cli",
"proof",
"zk-snark",
"address",
"ownership",
"snarkjs",
"circom",
"privacy"
],
"author": "sripwoud <me@sripwoud.xyz>",
"license": "MIT",
"dependencies": {
"@anonklub/proof": "workspace:^",
"@anonklub/proof": "1.0.0",
"@anonklub/query": "1.0.0",
"@noble/secp256k1": "^1.7.0",
"delay": "^5.0.0",
"ethers": "^5.7.2",
"inquirer": "^8",
"inquirer-fuzzy-path": "^2.3.0",
"ora": "^5",
"snarkjs": "^0.7.0",
"terminal-link": "2.1.1"
},
"devDependencies": {
"@types/inquirer": "^9.0.3",
"@types/yargs": "^17.0.24",
"snarkjs": "^0.7.0"
"@types/yargs": "^17.0.24"
},
"scripts": {
"build": "pnpm run clean && tsc --project tsconfig.compile.json",
"clean": "rm -rf dist",
"dev": "tsnd --cls --exit-child --ignore-watch node_modules --quiet --respawn --rs --transpile-only src/index.ts",
"start": "tsnd --cls --exit-child src/index.ts",
"prepack": "npm run build",
"start": "node dist/index.js",
"typecheck": "tsc"
}
}

View File

@@ -0,0 +1,122 @@
import { execSync } from 'child_process'
import terminalLink from 'terminal-link'
import { ProofRequest } from '@anonklub/proof'
import { AnonSetResponse, AnonymitySet } from '@anonklub/query'
import { Prompt } from '../Prompt/index.js'
import { AnonSetLocation, AnonSetType, ProofAction } from '../types.js'
import { CliI } from './interface'
class Cli implements CliI {
anonSet: AnonymitySet
prompt: Prompt
anonSetResponse: AnonSetResponse | undefined
constructor({ anonSet: AnonymitySet, prompt: Prompt }) {
this.anonSet = AnonymitySet
this.prompt = Prompt
}
async run() {
const proveOrVerify = await this.prompt.askProveOrVerify()
if (proveOrVerify === ProofAction.PROVE) {
await this.prove()
} else if (proveOrVerify === ProofAction.VERIFY) {
await this.verify()
}
}
async prove() {
const location = await this.prompt.askAnonSetLocation()
if (location === AnonSetLocation.ONCHAIN) {
const anonSetType = await this.prompt.askAnonSetType()
if (anonSetType === AnonSetType.ERC20_BALANCE) {
const { min, tokenAddress } = await this.prompt.askErc20AnonSetInputs()
this.anonSetResponse = await this.anonSet.fromErc20Balance({
min,
tokenAddress,
})
} else if (anonSetType === AnonSetType.ETH_BALANCE) {
const { min } = await this.prompt.askEthAnonSetInput()
this.anonSetResponse = await this.anonSet.fromEthBalance({ min })
} else if (anonSetType === AnonSetType.CRYPTO_PUNK) {
this.anonSetResponse = await this.anonSet.fromCryptoPunkOwners()
} else if (anonSetType === AnonSetType.ENS) {
const { choice, id } = await this.prompt.askEnsAnonSetInputs()
this.anonSetResponse = await this.anonSet.fromEnsProposalVoters({
choice,
id,
})
}
} else {
const path = await this.prompt.askAddressesFile()
const { default: addresses } = await import(path)
this.anonSetResponse = { data: addresses }
}
if (
this.anonSetResponse === undefined ||
this.anonSetResponse?.data?.length === 0
)
throw new Error('No addresses found')
if (this.anonSetResponse?.error !== undefined)
throw this.anonSetResponse.error
const message = await this.prompt.askMessage()
const rawSignature = await this.prompt.askRawSignature(message)
const proofRequest = new ProofRequest({
addresses: this.anonSetResponse.data,
message,
rawSignature,
url: 'http://anonklub.xyz:3000', // TODO: make this configurable
})
const { jobId } = await proofRequest.submit()
this.logProofRequestResult(jobId)
}
async verify() {
const proofPath = await this.prompt.askProofFile()
const publicSignalsPath = await this.prompt.askPublicSignalsFile()
// TODO: no need to ask for this file, include it in the pkg
const verificationKeyPath = await this.prompt.askVerificationKeyFile()
console.log({ proofPath, publicSignalsPath, verificationKeyPath })
// FIXME: use groth16 module from snarkjs directly instead, otherwise we need to install snarkjs globally for this exact command to work
const result = execSync(
`snarkjs groth16 verify ${verificationKeyPath} ${publicSignalsPath} ${proofPath}`,
)
console.log(result.toString())
}
logProofRequestResult(jobId: string) {
console.log('Proof Request submitted successfully and being now processed.')
console.log(
`Your job id is ${jobId}. Do not share this id. You'll need it to access your proof file.`,
)
console.log(
`Your proof input is already available at ${terminalLink(
'input.json',
`/proofs/${jobId}/input.json`, // TODO: prepend Proof API URL
)}`,
)
console.log('Wait 5-10 min and your results will be available at:')
console.log(
terminalLink('proof.json', `/proofs/${jobId}/proof.json`), // // TODO: prepend Proof API URL
)
console.log(
terminalLink(
'publicSignals.json',
`/proofs/${jobId}/publicSignals.json`, // TODO: prepend Proof API URL
),
)
}
}
export const cli = new Cli({
anonSet: new AnonymitySet(),
prompt: new Prompt(),
})

View File

@@ -0,0 +1,13 @@
import { AnonSetResponse, AnonymitySet } from '@anonklub/query'
import { Prompt } from '../Prompt/index.js'
export interface CliI {
anonSet: AnonymitySet
prompt: Prompt
anonSetResponse: AnonSetResponse | undefined
logProofRequestResult: (jobId: string) => void
run: () => Promise<void>
prove: () => Promise<void>
verify: () => Promise<void>
}

View File

@@ -0,0 +1,173 @@
import { ethers } from 'ethers'
import inquirer, { DistinctQuestion, QuestionCollection } from 'inquirer'
import { join } from 'path'
import {
EnsProposalVotersRequest,
Erc20BalanceAnonSetRequest,
EthBalanceAnonSetRequest,
Request,
} from '@anonklub/query'
import { AnonSetLocation, AnonSetType, ProofAction } from '../types.js'
import { PromptI } from './interface'
const excludeRegex =
/(coverage|dist|Library|node_modules|turbo|package|tsconfig|\/\.\w+)/
export class Prompt implements PromptI {
askFile = (fileName: string) =>
this.prompt<string>({
depthLimit: 6,
excludeFilter: (path: string) => !path.endsWith('.json'),
excludePath: (path: string) => excludeRegex.test(path),
itemType: 'file',
message: `What is the path to your ${fileName} .json file?`,
name: 'file',
// TODO: fix fuzzy search
rootPath: join(process.env['HOME'] as string),
// @ts-expect-error ???
type: 'fuzzypath',
})
prompt =
<T>(questions: QuestionCollection | DistinctQuestion) =>
async (): Promise<T> => {
if (questions instanceof Array)
return (await inquirer.prompt(questions as QuestionCollection)) as T
const { name } = questions as DistinctQuestion
if (name === undefined) throw new Error('Question must have a name')
const { [name]: answer } = await inquirer.prompt(questions)
return answer
}
askAddressesFile = this.askFile('addresses')
askProofFile = this.askFile('proof')
askPublicSignalsFile = this.askFile('public signals')
askVerificationKeyFile = this.askFile('verification key')
askProveOrVerify = this.prompt<ProofAction>({
choices: [
{
name: 'Prove',
value: ProofAction.PROVE,
},
{
name: 'Verify',
value: ProofAction.VERIFY,
},
],
message: 'Hey anon, what would you like to do?',
name: 'action',
type: 'list',
})
askAnonSetLocation = this.prompt<AnonSetLocation>({
choices: [
{
name: 'On chain: query the ethereum blockchain as of latest block for me',
value: AnonSetLocation.ONCHAIN,
},
{
name: 'Locally: I have a json file with the list of addresses',
value: AnonSetLocation.FILE,
},
],
message: 'Where does your anon set (list of addresses) lives?',
name: 'anonSetLocation',
type: 'list',
})
askAnonSetType = this.prompt<AnonSetType>({
choices: [
{
name: 'Holders of a min ERC20 Balance',
value: AnonSetType.ERC20_BALANCE,
},
{
name: 'Holders of a min ETH Balance',
value: AnonSetType.ETH_BALANCE,
},
{
name: 'CryptoPunk owners',
value: AnonSetType.CRYPTO_PUNK,
},
{
name: 'Addresses that participated in a ENS Governance voting round',
value: AnonSetType.ENS,
},
],
message: 'What type of anon set do you want to prove?',
name: 'anonSetType',
type: 'list',
})
askErc20AnonSetInputs = this.prompt<Erc20BalanceAnonSetRequest>([
{
default: '0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
message: 'ERC20 address you want to query',
name: 'tokenAddress',
type: 'string',
validate: (answer: string) =>
ethers.utils.isAddress(answer) || 'Invalid address',
},
{
default: '1000',
message:
'Minimum balance of ERC20 one must own to be part of the anonymity set',
name: 'min',
type: 'string',
validate: (answer: string) =>
answer.match(/^[0-9]+$/)?.length !== undefined || 'Invalid number',
},
])
askEthAnonSetInput = this.prompt<Request<EthBalanceAnonSetRequest>>({
message:
'What is the integer minimum balance (in ETH) addresses should hold?',
name: 'minBalance',
type: 'input',
validate: (answer: string) =>
answer.match(/^[0-9]+$/) !== null || 'Invalid integer',
})
askEnsAnonSetInputs = this.prompt<Request<EnsProposalVotersRequest>>([
{
default: '0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
message: 'ERC20 address you want to query',
name: 'tokenAddress',
type: 'string',
validate: (answer: string) =>
ethers.utils.isAddress(answer) || 'Invalid address',
},
{
default: '1000',
message:
'Minimum balance of ERC20 one must own to be part of the anonymity set',
name: 'min',
type: 'string',
validate: (answer: string) =>
answer.match(/^[0-9]+$/)?.length !== undefined || 'Invalid number',
},
])
askMessage = this.prompt<string>({
default: 'Hello PSE',
message: 'Message you will sign',
name: 'message',
validate: (answer: string) => answer.length !== 0 || 'Invalid message',
})
askRawSignature = async (message: string) =>
this.prompt<string>({
message: `Raw signature (0x-prefixed) of the message "${message}" signed by the private key of the address you want to prove is part of the list of addresses`,
name: 'rawSignature',
validate: (answer: string) =>
answer.match(/^0x[0-9a-fA-F]{130}$/)?.length !== undefined ||
'Invalid raw signature format',
})()
}

View File

@@ -0,0 +1,27 @@
import { DistinctQuestion, QuestionCollection } from 'inquirer'
import {
EnsProposalVotersRequest,
Erc20BalanceAnonSetRequest,
EthBalanceAnonSetRequest,
Request,
} from '@anonklub/query'
import { AnonSetLocation, AnonSetType, ProofAction } from '../types'
export interface PromptI {
askAnonSetLocation: () => Promise<AnonSetLocation>
askAnonSetType: () => Promise<AnonSetType>
askErc20AnonSetInputs: (
anonSetType: AnonSetType,
) => Promise<Request<Erc20BalanceAnonSetRequest>>
askEthAnonSetInput: () => Promise<Request<EthBalanceAnonSetRequest>>
askEnsAnonSetInputs: () => Promise<Request<EnsProposalVotersRequest>>
askFile: (fileName: string) => () => Promise<string>
askAddressesFile: () => Promise<string>
askProofFile: () => Promise<string>
askProveOrVerify: () => Promise<ProofAction>
askPublicSignalsFile: () => Promise<string>
askVerificationKeyFile: () => Promise<string>
prompt: <T>(
questions: QuestionCollection | DistinctQuestion,
) => () => Promise<T>
}

View File

@@ -1,99 +0,0 @@
import { execSync } from 'child_process'
import { ProofRequest } from '@anonklub/proof'
import { API_URLS } from '../constants'
import {
fetchEnsVotersAnonSet,
fetchErc20AnonSet,
fetchEthAnonSet,
fetchPunksAnonSet,
} from '../fetch-anonset'
import { logResult } from './log-result'
import {
AnonSetLocation,
AnonSetType,
askAddressesFile,
askAnonSetLocation,
askAnonSetType,
askEnsAnonsetInputs,
askErc20AnonsetInputs,
askEthAnonsetInputs,
askMessage,
askProofFile,
askProveOrVerify,
askPublicSignalsFile,
askRawSignature,
askVerificationKeyFile,
ProofAction,
} from './prompts'
export const cli = async () => {
const proveOrVerify = await askProveOrVerify()
switch (proveOrVerify) {
case ProofAction.PROVE: {
let addresses: string[] = []
const location = await askAnonSetLocation()
switch (location) {
case AnonSetLocation.ONCHAIN: {
const anonSetType = await askAnonSetType()
switch (anonSetType) {
case AnonSetType.ERC20_BALANCE: {
const { min, tokenAddress } = await askErc20AnonsetInputs()
addresses = await fetchErc20AnonSet({ min, tokenAddress })
break
}
case AnonSetType.ETH_BALANCE: {
const { minBalance } = await askEthAnonsetInputs()
addresses = await fetchEthAnonSet({ minBalance })
break
}
case AnonSetType.CRYPTO_PUNK: {
addresses = await fetchPunksAnonSet()
break
}
case AnonSetType.ENS: {
const { choice, id } = await askEnsAnonsetInputs()
addresses = await fetchEnsVotersAnonSet({ choice, id })
}
}
break
}
case AnonSetLocation.FILE: {
const path = await askAddressesFile()
const { default: _addresses } = await import(path)
addresses = _addresses
}
}
if (addresses.length === 0) throw new Error('No addresses list found')
const message = await askMessage()
const rawSignature = await askRawSignature(message)
const proofRequest = new ProofRequest({
addresses,
message,
rawSignature,
url: API_URLS.PROVE,
})
const { jobId } = await proofRequest.submit()
logResult(jobId)
break
}
case ProofAction.VERIFY: {
const proofPath = await askProofFile()
const publicSignalsPath = await askPublicSignalsFile()
const verificationKeyPath = await askVerificationKeyFile()
// TODO: use groth16 module from snarkjs directly instead
const result = execSync(
`node_modules/.bin/snarkjs groth16 verify ${verificationKeyPath} ${publicSignalsPath} ${proofPath}`,
)
console.log(result.toString())
}
}
}

View File

@@ -1,25 +0,0 @@
import terminalLink from 'terminal-link'
import { API_URLS } from '../constants'
export const logResult = (jobId: string) => {
console.log('Proof Request submitted successfully and being now processed.')
console.log(
`Your job id is ${jobId}. Do not share this id. You'll need it to access your proof file.`,
)
console.log(
`Your proof input is already available at ${terminalLink(
'input.json',
`${API_URLS.PROVE}/proofs/${jobId}/input.json`,
)}`,
)
console.log('Wait 5-10 min and your results will be available at:')
console.log(
terminalLink('proof.json', `${API_URLS.PROVE}/proofs/${jobId}/proof.json`),
)
console.log(
terminalLink(
'publicSignals.json',
`${API_URLS.PROVE}/proofs/${jobId}/publicSignals.json`,
),
)
}

View File

@@ -1,23 +0,0 @@
import { prompt } from './prompt'
export enum AnonSetLocation {
ONCHAIN = 'onchain',
FILE = 'file',
}
export const askAnonSetLocation = prompt<AnonSetLocation>({
choices: [
{
name: 'On chain: query the ethereum blockchain as of latest block for me',
value: AnonSetLocation.ONCHAIN,
},
{
name: 'Locally: I have a json file with the list of addresses',
value: AnonSetLocation.FILE,
},
],
message: 'Where does your anon set (list of addresses) lives?',
name: 'anonSetLocation',
type: 'list',
})

View File

@@ -1,33 +0,0 @@
import { prompt } from './prompt'
export enum AnonSetType {
ERC20_BALANCE = 'erc20_balance',
ETH_BALANCE = 'eth_balance',
CRYPTO_PUNK = 'crypto_punk',
ENS = 'ens',
}
export const askAnonSetType = prompt<AnonSetType>({
choices: [
{
name: 'Holders of a min ERC20 Balance',
value: AnonSetType.ERC20_BALANCE,
},
{
name: 'Holders of a min ETH Balance',
value: AnonSetType.ETH_BALANCE,
},
{
name: 'CryptoPunk owners',
value: AnonSetType.CRYPTO_PUNK,
},
{
name: 'Addresses that participated in a ENS Governance voting round',
value: AnonSetType.ENS,
},
],
message: 'What type of anon set do you want to prove?',
name: 'anonSetType',
type: 'list',
})

View File

@@ -1,22 +0,0 @@
import { prompt } from './prompt'
export enum Choice {
For = 'FOR',
Against = 'AGAINST',
Abstain = 'ABSTAIN',
}
export const askEnsAnonsetInputs = prompt<{ choice: Choice; id: string }>([
{
message:
'What is the ENS Proposal ID (can be found e.g on https://tally.xyz)?',
name: 'id',
type: 'input',
},
{
choices: [Choice.For, Choice.Against, Choice.Abstain],
message: 'Vote?',
name: 'choice',
type: 'list',
},
])

View File

@@ -1,25 +0,0 @@
import { ethers } from 'ethers'
import { prompt } from './prompt'
export const askErc20AnonsetInputs = prompt<{
tokenAddress: string
min: string
}>([
{
default: '0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
message: 'ERC20 address you want to query',
name: 'tokenAddress',
type: 'string',
validate: (answer: string) =>
ethers.utils.isAddress(answer) || 'Invalid address',
},
{
default: '1000',
message:
'Minimum balance of ERC20 one must own to be part of the anonymity set',
name: 'min',
type: 'string',
validate: (answer: string) =>
answer.match(/^[0-9]+$/)?.length !== undefined || 'Invalid number',
},
])

View File

@@ -1,10 +0,0 @@
import { prompt } from './prompt'
export const askEthAnonsetInputs = prompt<{ minBalance: string }>({
message:
'What is the integer minimum balance (in ETH) addresses should hold?',
name: 'minBalance',
type: 'input',
validate: (answer: string) =>
answer.match(/^[0-9]+$/) !== null || 'Invalid integer',
})

View File

@@ -1,23 +0,0 @@
import { join } from 'path'
import { prompt } from './prompt'
const excludeRegex =
/(coverage|dist|Library|node_modules|turbo|package|tsconfig|\/\.\w+)/
const askFile = (fileName: string) =>
prompt<string>({
depthLimit: 6,
excludeFilter: (path: string) => !path.endsWith('.json'),
excludePath: (path: string) => excludeRegex.test(path),
itemType: 'file',
message: `What is the path to your ${fileName} .json file?`,
name: 'file',
rootPath: join(__dirname, '..', '..', '..', '..'),
// @ts-expect-error
type: 'fuzzypath',
})
export const askAddressesFile = askFile('addresses.')
export const askProofFile = askFile('proof')
export const askPublicSignalsFile = askFile('public signals')
export const askVerificationKeyFile = askFile('verification key')

View File

@@ -1,8 +0,0 @@
import { prompt } from './prompt'
export const askMessage = prompt<string>({
default: 'Hello PSE',
message: 'Message you will sign',
name: 'message',
validate: (answer: string) => answer.length !== 0 || 'Invalid message',
})

View File

@@ -1,22 +0,0 @@
import { prompt } from './prompt'
export enum ProofAction {
PROVE = 'prove',
VERIFY = 'verify',
}
export const askProveOrVerify = prompt<ProofAction>({
choices: [
{
name: 'Prove',
value: ProofAction.PROVE,
},
{
name: 'Verify',
value: ProofAction.VERIFY,
},
],
message: 'Hey anon, what would you like to do?',
name: 'action',
type: 'list',
})

View File

@@ -1,10 +0,0 @@
import { prompt } from './prompt'
export const askRawSignature = async (message: string) =>
prompt<string>({
message: `Raw signature (0x-prefixed) of the message "${message}" signed by the private key of the address you want to prove is part of the list of addresses`,
name: 'rawSignature',
validate: (answer: string) =>
answer.match(/^0x[0-9a-fA-F]{130}$/)?.length !== undefined ||
'Invalid raw signature format',
})()

View File

@@ -1,14 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from './ask-anonset-location'
export * from './ask-anonset-type'
export * from './ask-ens-anonset-inputs'
export * from './ask-erc20-anonset-inputs'
export * from './ask-eth-anonset-inputs'
export * from './ask-file'
export * from './ask-message'
export * from './ask-prove-or-verify'
export * from './ask-raw-signature'
export * from './prompt'

View File

@@ -1,16 +0,0 @@
import inquirer, { DistinctQuestion, QuestionCollection } from 'inquirer'
import inquirerFuzzyPath from 'inquirer-fuzzy-path'
inquirer.registerPrompt('fuzzypath', inquirerFuzzyPath)
export const prompt =
<T>(questions: QuestionCollection | DistinctQuestion) =>
async (): Promise<T> => {
if (questions instanceof Array)
return (await inquirer.prompt(questions as QuestionCollection)) as T
const { name } = questions as DistinctQuestion
if (name === undefined) throw new Error('Question must have a name')
const { [name]: answer } = await inquirer.prompt(questions)
return answer
}

View File

@@ -1,4 +0,0 @@
export const API_URLS = {
PROVE: 'http://localhost:3000',
QUERY: 'https://anonset.fly.dev',
}

View File

@@ -1,50 +0,0 @@
import ora from 'ora'
import { Choice } from './cli/prompts'
import { API_URLS } from './constants'
const fetchAnonset = async (url): Promise<string[]> => {
const spinner = ora(
'Fetching Anonymity Set from 3rd party ethereum indexing services...',
)
spinner.start()
const res = await fetch(url)
const anonSet = await res.json()
spinner.stop()
return anonSet
}
export const fetchErc20AnonSet = async ({
min,
tokenAddress,
}: {
tokenAddress: string
min: string
}) =>
fetchAnonset(
`${API_URLS.QUERY}/balance/ERC20?${new URLSearchParams({
min,
tokenAddress,
}).toString()}`,
)
export const fetchEthAnonSet = async ({ minBalance }: { minBalance: string }) =>
fetchAnonset(`${API_URLS.QUERY}/balance/ETH?min=${minBalance}`)
export const fetchEnsVotersAnonSet = async ({
choice,
id,
}: {
choice: Choice
id: string
}) =>
fetchAnonset(
`${API_URLS.QUERY}/balance/ERC20?${new URLSearchParams({
choice,
id,
}).toString()}`,
)
export const fetchPunksAnonSet = async () =>
fetchAnonset(`${API_URLS.QUERY}/punks`)

View File

@@ -1,6 +1,8 @@
import { cli } from './cli'
#!/usr/bin/env node
cli().catch((err) => {
import { cli } from './Cli'
cli.run().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,16 @@
export enum ProofAction {
PROVE = 'prove',
VERIFY = 'verify',
}
export enum AnonSetLocation {
ONCHAIN = 'onchain',
FILE = 'file',
}
export enum AnonSetType {
ERC20_BALANCE = 'erc20_balance',
ETH_BALANCE = 'eth_balance',
CRYPTO_PUNK = 'crypto_punk',
ENS = 'ens',
}

View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"exclude": ["test", "test/jest.config.ts"],
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"inlineSourceMap": true,
"inlineSources": true,
"noEmit": false,
"outDir": "dist",
"preserveConstEnums": true,
"removeComments": true,
"target": "ES2022"
}
}

View File

@@ -1,14 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "node",
"target": "es2020"
"module": "CommonJS",
"moduleResolution": "Node",
"noUncheckedIndexedAccess": false,
"strictPropertyInitialization": false
},
"include": ["src"],
"references": [
{
"path": "../proof"
}
]
"include": ["src", "test"]
}

View File

@@ -1,22 +1,37 @@
{
"name": "@anonklub/proof",
"version": "1.0.0",
"description": "Shared utilities for @anonklub",
"main": "dist/index.js",
"description": "Build Anonymous Proofs (Requests) of Ethereum Address Ownership",
"author": "BlakeMScurr <blakemscurr@gmail.com>",
"contributors": ["sripwoud <me@sripwoud.xyz>"],
"repository": "https://github.com/anonklub/anonklub/tree/main/%40anonklub/proof",
"homepage": "https://github.com/anonklub/anonklub",
"bugs": "https://github.com/anonklub/anonklub/issues",
"keywords": [
"zk",
"ethereum",
"proof",
"zk-snark",
"address",
"ownership",
"snarkjs",
"circom",
"privacy"
],
"license": "MIT",
"files": ["dist"],
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"dependencies": {
"@noble/secp256k1": "^1.7.0",
"circomlibjs": "^0.1.7",
"ethers": "^5.7.2",
"undici": "^5.21.2"
},
"files": ["dist"],
"types": "dist/types/index.d.ts",
"scripts": {
"build": "tsc --project tsconfig.compile.json",
"test": "jest -c test/jest.config.ts",
"build": "pnpm run clean && tsc --project tsconfig.compile.json",
"clean": "rm -rf dist",
"prepack": "npm run build",
"typecheck": "tsc"
}
}

View File

@@ -1,4 +1,4 @@
import { ProofRequestJson } from '../ProofRequest/interface'
import { ProofRequestJson } from '../ProofRequest'
export interface CircuitInputArgs {
proofRequest: ProofRequestJson

View File

@@ -6,6 +6,8 @@ import {
ProofResult,
} from './interface'
export { ProofRequestJson }
export class ProofRequest implements ProofRequestInterface {
public readonly addresses: string[]
public readonly message: string

View File

@@ -10,4 +10,3 @@ export {
export { MerkleTree } from './MerkleTree'
export * from './poseidon'
export * from './ProofRequest'
export { ProofRequestJson } from './ProofRequest/interface'

View File

@@ -2,14 +2,6 @@ import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
coveragePathIgnorePatterns: ['!src/poseidon.ts'],
coverageThreshold: {
global: {
branches: 37,
functions: 55,
lines: 75,
statements: 75,
},
},
displayName: '@anonklub/proof',
preset: '@anonklub/test',
rootDir: '..',

View File

@@ -2,15 +2,15 @@
"extends": "./tsconfig.json",
"exclude": ["test", "test/jest.config.ts"],
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"inlineSources": true,
"inlineSourceMap": true,
"noEmit": false,
"outDir": "dist",
"moduleResolution": "Node",
"inlineSourceMap": true,
"inlineSources": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./dist/types",
"preserveConstEnums": true,
"removeComments": true,
"preserveConstEnums": true
"target": "ES2022"
}
}

View File

@@ -1,7 +1,10 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "test"],
"compilerOptions": {
"noUncheckedIndexedAccess": false
}
"module": "CommonJS",
"moduleResolution": "Node",
"noUncheckedIndexedAccess": false,
"strictPropertyInitialization": false
},
"include": ["src", "test"]
}

View File

@@ -2,17 +2,20 @@
"name": "@anonklub/query",
"version": "1.0.0",
"description": "Query Anonymity sets of Ethereum addresses",
"main": "dist/index.js",
"repository": "https://github.com/anonklub/anonklub/tree/main/%40anonklub/query",
"homepage": "https://github.com/anonklub/anonklub",
"bugs": "https://github.com/anonklub/anonklub/issues",
"files": ["dist"],
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"scripts": {
"build": "tsc --project tsconfig.compile.json",
"test": "jest -c test/jest.config.ts",
"build": "pnpm run clean && tsc --project tsconfig.compile.json",
"clean": "rm -rf dist",
"prepack": "npm run build",
"typecheck": "tsc"
},
"keywords": ["ethereum", "query", "sets", "anonymity", "addresses"],
"keywords": ["ethereum", "query", "sets", "anonymity", "address"],
"author": "sripwoud <me@sripwoud.xyz>",
"license": "ISC",
"dependencies": {
"class-validator": "^0.14.0"
}

View File

@@ -1,11 +1,11 @@
import { URLS } from 'CONSTANTS'
import { fetchJson } from 'fetch-json'
import { URLS } from '../CONSTANTS'
import { fetchJson } from '../fetch-json'
import {
EnsProposalVotersRequest,
Erc20BalanceAnonSetRequest,
EthBalanceAnonSetRequest,
} from 'requests'
import { Endpoint, Environment, Request, RequestClass } from 'types'
} from '../requests'
import { Endpoint, Environment, Request, RequestClass } from '../types'
import { AnonymitySetI } from './interface'
export class AnonymitySet implements AnonymitySetI {

View File

@@ -2,8 +2,8 @@ import {
EnsProposalVotersRequest,
Erc20BalanceAnonSetRequest,
EthBalanceAnonSetRequest,
} from 'requests'
import { AnonSetResponse } from 'types'
} from '../requests'
import { AnonSetResponse } from '../types'
export interface AnonymitySetI {
fromEthBalance: ({

View File

@@ -1,4 +1,4 @@
import { Environment } from 'types'
import { Environment } from './types'
export const URLS = {
[Environment.PRODUCTION]: 'https://anonset.fly.dev',

View File

@@ -1,4 +1,4 @@
import { AnonSetResponse } from 'types'
import { AnonSetResponse } from './types'
export const fetchJson = async (
url: string,

View File

@@ -1,4 +1,10 @@
export * from 'AnonymitySet'
export * from 'CONSTANTS'
export * from 'requests'
export { Choice, Environment } from 'types'
export * from './AnonymitySet'
export * from './CONSTANTS'
export * from './requests'
export {
AnonSetResponse,
Choice,
Endpoint,
Environment,
Request,
} from './types'

View File

@@ -1,5 +1,5 @@
import { IsDefined, IsIn, IsNumberString, Length } from 'class-validator'
import { Choice } from 'types'
import { Choice } from '../types'
export class EnsProposalVotersRequest {
@IsDefined()

View File

@@ -2,7 +2,7 @@ import {
EnsProposalVotersRequest,
Erc20BalanceAnonSetRequest,
EthBalanceAnonSetRequest,
} from 'requests'
} from './requests'
export enum Environment {
PRODUCTION = 'production',

View File

@@ -1,7 +1,6 @@
import { createEthAddresses } from '@anonklub/test'
import { faker } from '@faker-js/faker'
import { Endpoint } from 'types'
import { AnonymitySet, Choice, Environment, URLS } from '../src'
import { AnonymitySet, Choice, Endpoint, Environment, URLS } from '../src'
describe('AnonymitySet', () => {
let anonymitySet: AnonymitySet

View File

@@ -2,15 +2,15 @@
"extends": "./tsconfig.json",
"exclude": ["test", "test/jest.config.ts"],
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"inlineSources": true,
"inlineSourceMap": true,
"noEmit": false,
"outDir": "dist",
"moduleResolution": "Node",
"inlineSourceMap": true,
"inlineSources": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./dist/types",
"preserveConstEnums": true,
"removeComments": true,
"preserveConstEnums": true
"target": "ES6"
}
}

View File

@@ -1,9 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "CommonJS",
"moduleResolution": "Node",
"noUncheckedIndexedAccess": false,
"strictPropertyInitialization": false
},

View File

@@ -1,13 +1,7 @@
{
"name": "@anonklub/prove-api",
"version": "1.0.0",
"description": "ZK Proofs generation API",
"main": "index.js",
"author": "BlakeMScurr <blakemscurr@gmail.com>",
"contributors": ["sripwoud <me@sripwoud.xyz>"],
"license": "MIT",
"dependencies": {
"@anonklub/proof": "workspace:^",
"@anonklub/proof": "1.0.0",
"@bull-board/api": "^5.0.0",
"@bull-board/express": "^5.0.0",
"@noble/hashes": "^1.3.0",

View File

@@ -1,10 +1,5 @@
{
"name": "@anonklub/query-api",
"version": "1.0.0",
"description": "Express Server Template",
"main": "index.js",
"author": "sripwoud <me@sripwoud.xyz>",
"license": "MIT",
"_moduleAliases": {
"@controllers": "dist/api/controllers",
"@decorators": "dist/decorators",
@@ -61,7 +56,6 @@
"start.dev": "NODE_ENV=development tsnd --rs --exit-child --clear --quiet --files -r tsconfig-paths/register ./src",
"start.prod": "node -r module-alias/register ./dist",
"start.watch": "tsnd --respawn --rs --exit-child --clear --quiet --files -r tsconfig-paths/register ./src",
"test": "jest -c test/jest.config.ts",
"typecheck": "tsc"
}
}

View File

@@ -20,8 +20,8 @@ import { getMesh, ExecuteMeshFn, SubscribeMeshFn, MeshContext as BaseMeshContext
import { MeshStore, FsStoreStorageAdapter } from '@graphql-mesh/store';
import { path as pathModule } from '@graphql-mesh/cross-helpers';
import { ImportFn } from '@graphql-mesh/types';
import type { CryptopunksTypes } from './sources/cryptopunks/types';
import type { EnsGovernanceTypes } from './sources/ens-governance/types';
import type { CryptopunksTypes } from './sources/cryptopunks/types';
import * as importedModule$0 from "./sources/cryptopunks/introspectionSchema";
import * as importedModule$1 from "./sources/ens-governance/introspectionSchema";
export type Maybe<T> = T | null;

View File

@@ -4,14 +4,6 @@ import { compilerOptions } from '../tsconfig.json'
const jestConfig: JestConfigWithTsJest = {
coveragePathIgnorePatterns: ['.graphclient', 'lib'],
coverageThreshold: {
global: {
branches: 50,
functions: 65,
lines: 75,
statements: 75,
},
},
displayName: 'query-api',
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',

View File

@@ -1,10 +1,5 @@
{
"name": "@anonklub/circom",
"version": "1.0.0",
"description": "Prove membership in a poseidon merkle tree by proving an ECDSA signature",
"author": "BlakeMScurr <blakemscurr@gmail.com>",
"contributors": ["sripwoud <me@sripwoud.xyz>"],
"license": "MIT",
"devDependencies": {
"@anonklub/proof": "workspace:^",
"@noble/secp256k1": "^1.7.0",

View File

@@ -1,5 +1,5 @@
{
"name": "contracts",
"name": "@anonklub/contracts",
"version": "1.0.0",
"scripts": {
"build": "forge build",
@@ -9,7 +9,7 @@
"_format.fix": "forge fmt --root ..",
"_lint": "solhint '**/*.sol'",
"_lint.fix": "solhint --fix '**/*.sol'",
"test": "forge test"
"_test": "forge test"
},
"devDependencies": {
"solhint": "^3.4.1"

View File

@@ -1,5 +1,5 @@
{
"name": "anonklub-aws",
"name": "@anonklub/infra",
"main": "index.ts",
"devDependencies": {
"@types/node": "^16"

7
jest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: '@anonklub/test',
}
export default jestConfig

View File

@@ -33,7 +33,7 @@
"typescript": "^4.9.4"
},
"scripts": {
"build": "turbo run barrels && turbo run build",
"build": "turbo run build",
"build.docker.discord": "docker build -f discord-bot/Dockerfile . -t discord-bot",
"build.docker.prove": "docker build -f apis/prove/Dockerfile . -t prove-api",
"build.docker.query": "docker build -f apis/query/Dockerfile . -t query-api",
@@ -54,8 +54,8 @@
"_lint.fix": "eslint . --ext .js,.ts,.tsx --fix",
"lint.fix": "turbo run _lint.fix",
"test.watch": "jest --watch",
"pre.test": "turbo run build --filter @anonklub/proof",
"test": "pnpm run pre.test && turbo run test --filter !@anonklub/circom",
"_test": "jest",
"test": "turbo run _test",
"typecheck": "turbo run typecheck",
"validate": "turbo run _format _lint build --cache-dir=.turbo"
},

796
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
const tsJestPreset = require('ts-jest/jest-preset')
const { resolve } = require('path')
const { join } = require('path')
const projects = [
...['proof', 'query'].map((name) => `@anonklub/${name}`),
'apis/query',
].map((name) => join(__dirname, '..', name, 'test'))
/**
* @type {import('ts-jest').JestConfigWithTsJest}
@@ -9,13 +14,13 @@ module.exports = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/*.d.ts'],
coverageDirectory: resolve('../coverage'),
coverageDirectory: join(__dirname, '..', 'coverage'),
coverageThreshold: {
global: {
branches: 87,
functions: 90,
lines: 90,
statements: 90,
branches: 50,
functions: 70,
lines: 80,
statements: 80,
},
},
moduleDirectories: [
@@ -25,6 +30,7 @@ module.exports = {
'<rootDir>/src',
],
moduleFileExtensions: ['js', 'json', 'ts'],
projects,
rootDir: '..',
setupFilesAfterEnv: [require.resolve('../test/setup.ts')],
testPathIgnorePatterns: [

View File

@@ -26,5 +26,6 @@
"strict": true,
"target": "ES2022"
},
"files": ["global.d.ts"]
"files": ["global.d.ts"],
"include": ["jest.config.ts"]
}

View File

@@ -77,13 +77,16 @@
"build.graph": {
"outputs": ["src/lib/graph/.graphclient/**"]
},
"test": {
"//#_test": {
"dependsOn": ["^build"],
"inputs": [
"{apis/{prove,query},@anonklub,circuits/circom}/{src,test}/**/*.{js,jsx,ts,tsx}",
"contracts/src/**/*.sol"
"{apis/{prove,query},@anonklub,circuits/circom}/{src,test}/**/*.{js,jsx,ts,tsx}"
]
},
"_test": {
"cache": false,
"inputs": ["contracts/src/**/*.sol"]
},
"deploy": {
"dependsOn": ["test"]
}

View File

@@ -1,8 +1,5 @@
{
"name": "@anonklub/ui",
"version": "1.0.0",
"private": true,
"author": "sripwoud <me@sripwoud.xyz>",
"browser": {
"fs": false,
"tls": false,
@@ -40,7 +37,6 @@
"pinojs": "^1.0.0",
"prettier": "2.8.8"
},
"license": "MIT",
"scripts": {
"build": "next build",
"dev": "next dev",