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(artifacts): download artifacts from pse CDN #97

Merged
merged 9 commits into from
Jun 21, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/great-eggs-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zk-kit/artifacts": minor
---

download artifacts from https://snark-artifacts.pse.dev
17 changes: 4 additions & 13 deletions packages/artifacts/src/download/download.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { createWriteStream, existsSync } from 'node:fs'
import { mkdir } from 'node:fs/promises'
import { dirname } from 'node:path'
import type { Urls } from './urls.ts'

async function fetchRetry(urls: string[]): Promise<ReturnType<typeof fetch>> {
const [url] = urls
if (!url) throw new Error('No urls to try')
return fetch(url).catch(() => fetchRetry(urls.slice(1)))
}

export async function download(urls: Urls | string[] | string, outputPath: string) {
const { body, ok, statusText, url } = Array.isArray(urls)
? await fetchRetry(urls as string[])
: await fetch(urls as string)
export async function download(url: string, outputPath: string) {
const { body, ok, statusText } = await fetch(url)
if (!ok)
throw new Error(`Failed to fetch ${url}: ${statusText}`)
if (!body) throw new Error('Failed to get response body')
Expand Down Expand Up @@ -42,7 +33,7 @@ export async function download(urls: Urls | string[] | string, outputPath: strin
}
}

export async function maybeDownload(urls: Urls | string[] | string, outputPath: string) {
if (!existsSync(outputPath)) await download(urls, outputPath)
export async function maybeDownload(url: string, outputPath: string) {
if (!existsSync(outputPath)) await download(url, outputPath)
return outputPath
}
27 changes: 19 additions & 8 deletions packages/artifacts/src/download/index.browser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import type { SnarkArtifacts } from './types'
import { getSnarkArtifactUrls } from './urls'
import { type Project, projects } from '../projects'
import type { SnarkArtifacts, Version } from './types'
import { getBaseUrl } from './urls'

// TODO: retry for browser?
// beisdes, is caching already handled by circom/snarkjs?
sripwoud marked this conversation as resolved.
Show resolved Hide resolved
export default async function maybeGetSnarkArtifacts(
...pars: Parameters<typeof getSnarkArtifactUrls>
project: Project,
options: {
parameters?: (bigint | number | string)[]
version?: Version
cdnUrl?: string
} = {},
): Promise<SnarkArtifacts> {
const { wasms, zkeys } = await getSnarkArtifactUrls(...pars)
if (!projects.includes(project))
throw new Error(`Project '${project}' is not supported`)

options.version ??= 'latest'
const url = await getBaseUrl(project, options.version)
const parameters = options.parameters
? `-${options.parameters.join('-')}`
: ''

return {
wasm: wasms[0],
zkey: zkeys[0],
wasm: `${url}${parameters}.wasm`,
zkey: `${url}${parameters}.zkey`,
}
}
15 changes: 7 additions & 8 deletions packages/artifacts/src/download/index.node.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { tmpdir } from 'node:os'
import { maybeDownload } from './download.ts'
import _maybeGetSnarkArtifacts from './index.browser.ts'
import type { SnarkArtifacts } from './types'
import { getSnarkArtifactUrls } from './urls'

// https://unpkg.com/@zk-kit/poseidon-artifacts@latest/poseidon.wasm -> @zk/poseidon-artifacts@latest/poseidon.wasm
const extractEndPath = (url: string) => url.substring(url.indexOf('@zk'))
const extractEndPath = (url: string) => url.split('pse.dev/')[1]

/**
* Downloads SNARK artifacts (`wasm` and `zkey`) files if not already present in OS tmp folder.
Expand All @@ -18,17 +17,17 @@ const extractEndPath = (url: string) => url.substring(url.indexOf('@zk'))
* @returns {@link SnarkArtifacts}
*/
export default async function maybeGetSnarkArtifacts(
...pars: Parameters<typeof getSnarkArtifactUrls>
...pars: Parameters<typeof _maybeGetSnarkArtifacts>
): Promise<SnarkArtifacts> {
const { wasms, zkeys } = await getSnarkArtifactUrls(
const urls = await _maybeGetSnarkArtifacts(
...pars,
)

const outputPath = `${tmpdir()}/${extractEndPath(wasms[0])}`
const outputPath = `${tmpdir()}/snark-artifacts/${extractEndPath(urls.wasm)}`

const [wasm, zkey] = await Promise.all([
maybeDownload(wasms, outputPath),
maybeDownload(zkeys, outputPath.replace(/.wasm$/, '.zkey')),
maybeDownload(urls.wasm, outputPath),
maybeDownload(urls.zkey, outputPath.replace(/.wasm$/, '.zkey')),
])

return {
Expand Down
4 changes: 0 additions & 4 deletions packages/artifacts/src/download/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
*/
export type SnarkArtifacts = Record<'wasm' | 'zkey', string>

// Recursively build an array type of length L with elements of type T.
export type ArrayOf<T, L extends number, A extends unknown[] = []> = A['length'] extends L ? A
: ArrayOf<T, L, [T, ...A]>

type Digit = `${number}`
type PreRelease = 'alpha' | 'beta'

Expand Down
38 changes: 5 additions & 33 deletions packages/artifacts/src/download/urls.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Project, projects } from '../projects'
import type { ArrayOf, Version } from './types'
import type { Project } from '../projects'
import type { Version } from './types'

export type Urls = ArrayOf<string, 3>
const BASE_URL = 'https://snark-artifacts.pse.dev'

export async function getAvailableVersions(project: Project) {
const res = await fetch(`https://registry.npmjs.org/@zk-kit/${project}-artifacts`)
Expand All @@ -15,35 +15,7 @@ async function isVersionAvailableOrThrow(project: Project, version: Version) {
throw new Error(`Version '${version}' is not available for project '${project}'`)
}

export async function getBaseUrls(project: Project, version: Version): Promise<Urls> {
export async function getBaseUrl(project: Project, version: Version): Promise<string> {
await isVersionAvailableOrThrow(project, version)
return [
`https://unpkg.com/@zk-kit/${project}-artifacts@${version}/${project}`,
`https://raw.githubusercontent.com/privacy-scaling-explorations/snark-artifacts/@zk-kit/${project}-artifacts@${version}/packages/${project}/${project}`,
`https://cdn.jsdelivr.net/npm/@zk-kit/${project}-artifacts@${version}/${project}`,
]
}

export async function getSnarkArtifactUrls(
project: Project,
options: {
parameters?: (bigint | number | string)[]
version?: Version
cdnUrl?: string
} = {},
) {
if (!projects.includes(project))
throw new Error(`Project '${project}' is not supported`)

options.version ??= 'latest'
const urls = await getBaseUrls(project, options.version)

const parameters = options.parameters
? `-${options.parameters.join('-')}`
: ''

return {
wasms: urls.map((url) => `${url}${parameters}.wasm`) as unknown as Urls,
zkeys: urls.map(url => `${url}${parameters}.zkey`) as unknown as Urls,
}
return `${BASE_URL}/${project}/${version}/${project}`
}
Loading