diff --git a/clients/js/scripts/proxy.ts b/clients/js/scripts/proxy.ts index 4982d71f..4137d055 100644 --- a/clients/js/scripts/proxy.ts +++ b/clients/js/scripts/proxy.ts @@ -26,6 +26,7 @@ const LISTEN_PORT = 3000; const DIE_ON_UNENCRYPTED = true; const UPSTREAM_URL = 'http://127.0.0.1:8545'; const SHOW_ENCRYPTED_RESULTS = true; +const LOG_ALL = true; console.log('DIE_ON_UNENCRYPTED', DIE_ON_UNENCRYPTED); console.log('UPSTREAM_URL', UPSTREAM_URL); @@ -65,7 +66,7 @@ async function onRequest(req: IncomingMessage, response: ServerResponse) { let showResult = false; for (const body of bodies) { - const log = loggedMethods.includes(body.method); + const log = LOG_ALL ?? loggedMethods.includes(body.method); if (log) { if (body.method === 'oasis_callDataPublicKey') { @@ -134,6 +135,10 @@ async function onRequest(req: IncomingMessage, response: ServerResponse) { ); } } + else { + showResult = LOG_ALL; + console.log(body); + } } } @@ -148,7 +153,7 @@ async function onRequest(req: IncomingMessage, response: ServerResponse) { console.log(' - RESULT', pj); } - response.writeHead(200, 'OK'); + response.writeHead(200, 'OK', { 'Content-Type': 'application/json' }); response.write(JSON.stringify(pj)); response.end(); } diff --git a/examples/ethersv5-ts-esm/package.json b/examples/ethersv5-ts-esm/package.json index c95d4b65..24499a07 100644 --- a/examples/ethersv5-ts-esm/package.json +++ b/examples/ethersv5-ts-esm/package.json @@ -1,23 +1,23 @@ { - "name": "example-ethersv5-ts-esm", - "private": true, - "main": "lib/index.js", - "type": "module", - "scripts": { - "lint": "prettier --cache --plugin-search-dir=. --check *.cjs test/**.ts scripts/**.ts contracts/**.sol && solhint contracts/**.sol", - "format": "prettier --cache --plugin-search-dir=. --write *.cjs test/**.ts scripts/**.ts contracts/**.sol && solhint --fix contracts/**.sol", - "build": "tsc -b", - "test": "tsc -b && pnpm node build/examples/ethersv5-ts-esm/src/index.js" - }, - "dependencies": { - "@oasisprotocol/sapphire-paratime": "workspace:^", - "ethers": "5.5.0" - }, - "devDependencies": { - "@types/node": "^17.0.10", - "@tsconfig/strictest": "2.0.2", - "prettier": "^2.5.1", - "ts-node": "10.9.2", - "typescript": "4.7.4" - } + "name": "example-ethersv5-ts-esm", + "private": true, + "main": "lib/index.js", + "type": "module", + "scripts": { + "lint": "prettier --cache --plugin-search-dir=. --check *.cjs test/**.ts scripts/**.ts contracts/**.sol && solhint contracts/**.sol", + "format": "prettier --cache --plugin-search-dir=. --write *.cjs test/**.ts scripts/**.ts contracts/**.sol && solhint --fix contracts/**.sol", + "build": "tsc -b", + "test": "tsc -b && pnpm node build/examples/ethersv5-ts-esm/src/index.js" + }, + "dependencies": { + "@oasisprotocol/sapphire-paratime": "workspace:^", + "ethers": "5.5.0" + }, + "devDependencies": { + "@types/node": "^17.0.10", + "@tsconfig/strictest": "2.0.2", + "prettier": "^2.5.1", + "ts-node": "10.9.2", + "typescript": "4.7.4" } +} diff --git a/examples/ethersv6-ts-esm/package.json b/examples/ethersv6-ts-esm/package.json index 08a6a6be..77feda0a 100644 --- a/examples/ethersv6-ts-esm/package.json +++ b/examples/ethersv6-ts-esm/package.json @@ -1,20 +1,20 @@ { - "name": "example-ethersv6-ts-esm", - "private": true, - "main": "lib/index.js", - "type": "module", - "scripts": { - "build": "tsc -b", - "test": "tsc -b && pnpm node build/examples/ethersv6-ts-esm/src/index.js" - }, - "dependencies": { - "@oasisprotocol/sapphire-paratime": "workspace:^", - "ethers": "6.9.0" - }, - "devDependencies": { - "@types/node": "^17.0.10", - "@tsconfig/strictest": "2.0.2", - "ts-node": "10.9.2", - "typescript": "5.3.3" - } + "name": "example-ethersv6-ts-esm", + "private": true, + "main": "lib/index.js", + "type": "module", + "scripts": { + "build": "tsc -b", + "test": "tsc -b && pnpm node build/examples/ethersv6-ts-esm/src/index.js" + }, + "dependencies": { + "@oasisprotocol/sapphire-paratime": "workspace:^", + "ethers": "6.9.0" + }, + "devDependencies": { + "@types/node": "^17.0.10", + "@tsconfig/strictest": "2.0.2", + "ts-node": "10.9.2", + "typescript": "5.3.3" } +} diff --git a/examples/viem/.gitignore b/examples/viem/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/examples/viem/.gitignore @@ -0,0 +1 @@ +build diff --git a/examples/viem/package.json b/examples/viem/package.json new file mode 100644 index 00000000..9705ddef --- /dev/null +++ b/examples/viem/package.json @@ -0,0 +1,21 @@ +{ + "name": "example-viem", + "private": true, + "type": "module", + "main": "lib/index.js", + "scripts": { + "build": "tsc -b", + "test": "tsc -b && node --loader ts-node/esm src/index.ts" + }, + "dependencies": { + "@oasisprotocol/sapphire-viem": "workspace:^", + "viem": "^2.7.1" + }, + "devDependencies": { + "@tsconfig/strictest": "2.0.2", + "@types/node": "^17.0.10", + "abitype": "^1.0.0", + "ts-node": "10.9.2", + "typescript": "5.3.3" + } +} diff --git a/examples/viem/src/index.ts b/examples/viem/src/index.ts new file mode 100644 index 00000000..1139be34 --- /dev/null +++ b/examples/viem/src/index.ts @@ -0,0 +1,41 @@ +import { Hex, createWalletClient, getContract, http, publicActions } from 'viem'; + +import OmnibusJSON from "../../../contracts/artifacts/contracts/tests/Omnibus.sol/Omnibus.json" assert { type: "json" }; +import { narrow } from 'abitype' +import { privateKeyToAccount } from "viem/accounts"; +import { sapphireLocalnet, wrapWalletClient } from '@oasisprotocol/sapphire-viem'; + + +async function main () { + const account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'); + + const transport = http('http://127.0.0.1:3000'); + + const walletClient = wrapWalletClient(createWalletClient({ + account, + chain: sapphireLocalnet, + transport + })); + + const hash = await walletClient.deployContract({ + abi: narrow(OmnibusJSON.abi), + bytecode: OmnibusJSON.bytecode as Hex, + }); + + const pc = walletClient.extend(publicActions); + const receipt = await pc.waitForTransactionReceipt({hash}); + console.log('Receipt', receipt); + + const contractAddress = receipt.contractAddress!; + + console.log('getContract') + const c = getContract({ + address: contractAddress, + abi: narrow(OmnibusJSON.abi), + client: walletClient + }) + const x = await c.read['testSignedQueries']!(); + console.log(x); +} + +await main (); diff --git a/examples/viem/tsconfig.json b/examples/viem/tsconfig.json new file mode 100644 index 00000000..9da514da --- /dev/null +++ b/examples/viem/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/strictest/tsconfig.json", + "compilerOptions": { + "outDir": "./build", + "module": "NodeNext", + "target": "ES2022", + "moduleResolution": "NodeNext", + "resolveJsonModule": true + }, + "include": [ + "./src" + ] +} diff --git a/integrations/viem/.gitignore b/integrations/viem/.gitignore new file mode 100644 index 00000000..257d29b2 --- /dev/null +++ b/integrations/viem/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.tgz +*.tsbuildinfo \ No newline at end of file diff --git a/integrations/viem/package.json b/integrations/viem/package.json new file mode 100644 index 00000000..f8a5dd8e --- /dev/null +++ b/integrations/viem/package.json @@ -0,0 +1,83 @@ +{ + "type": "module", + "name": "@oasisprotocol/sapphire-viem", + "license": "Apache-2.0", + "version": "0.1.0", + "description": "Viem support for the Oasis Sapphire ParaTime.", + "homepage": "https://github.com/oasisprotocol/sapphire-paratime/tree/main/integrations/viem", + "repository": { + "type": "git", + "url": "https://github.com/oasisprotocol/sapphire-paratime.git" + }, + "keywords": [ + "sapphire", + "paratime", + "oasis", + "web3", + "viem", + "wagmi" + ], + "files": [ + "dist", + "!dist/*.tsbuildinfo", + "src" + ], + "sideEffects": false, + "main": "./dist/_cjs/index.js", + "module": "./dist/_esm/index.js", + "types": "./dist/_types/index.d.ts", + "typings": "./dist/_types/index.d.ts", + "exports": { + "node": { + "import": "./dist/_esm/index.js", + "require": "./dist/_cjs/index.cjs", + "types": "./dist/_types/index.d.ts" + }, + "default": "./dist/_esm/index.js" + }, + "scripts": { + "lint": "prettier --cache --check . && eslint --ignore-path .gitignore .", + "format": "prettier --cache --write . && eslint --ignore-path .gitignore --fix .", + "build": "npm run clean && npm run build:cjs && npm run build:esm && npm run build:types", + "build:cjs": "tsc --project ./tsconfig.build.json --module commonjs --outDir ./dist/_cjs --removeComments --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/_cjs/package.json", + "build:esm": "tsc --project ./tsconfig.build.json --module es2015 --outDir ./dist/_esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/_esm/package.json", + "build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/_types --emitDeclarationOnly --declaration --declarationMap", + "clean": "rm -rf dist", + "test": "jest", + "coverage": "jest --coverage", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@noble/hashes": "1.3.2", + "@oasisprotocol/deoxysii": "0.0.5", + "tweetnacl": "1.0.3", + "viem": "2.x", + "cborg": "1.10.2", + "type-fest": "2.19.0", + "@oasisprotocol/sapphire-paratime": "workspace:^" + }, + "peerDependencies": { + "viem": "2.x", + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "^18.7.18", + "@types/node-fetch": "^2.6.2", + "@typescript-eslint/eslint-plugin": "^5.38.0", + "@typescript-eslint/parser": "^5.38.0", + "eslint": "^8.23.1", + "eslint-config-prettier": "^8.5.0", + "jest": "^29.7.0", + "node-fetch": "^2.6.7", + "prettier": "^2.7.1", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/integrations/viem/src/chain.ts b/integrations/viem/src/chain.ts new file mode 100644 index 00000000..95e43178 --- /dev/null +++ b/integrations/viem/src/chain.ts @@ -0,0 +1,97 @@ +import type { Chain } from "viem"; + +export const sapphireLocalnet = { + id: 23293, + name: "Oasis Sapphire Localnet", + //network: "sapphire-localnet", + nativeCurrency: { + decimals: 18, + name: "Test Rose", + symbol: "TEST", + }, + rpcUrls: { + public: { + http: ["http://localhost:8545"], + webSocket: ["wss://localhost:8545/ws"], + }, + default: { + http: ["https://localhost:8545"], + webSocket: ["wss://localhost:8545/ws"], + }, + }, + blockExplorers: { + default: { + name: "Oasis Foundation", + url: "https://explorer.sapphire.oasis.io/", + }, + }, +} as const satisfies Chain; + +export const sapphire = { + id: 23294, + name: "Oasis Sapphire", + //network: "sapphire", + nativeCurrency: { + decimals: 18, + name: "Rose", + symbol: "ROSE", + }, + rpcUrls: { + public: { + http: ["https://sapphire.oasis.io"], + webSocket: ["wss://sapphire.oasis.io/ws"], + }, + default: { + http: ["https://sapphire.oasis.io"], + webSocket: ["wss://sapphire.oasis.io/ws"], + }, + }, + blockExplorers: { + default: { + name: "Oasis Foundation", + url: "https://explorer.sapphire.oasis.io/", + }, + }, +} as const satisfies Chain; + +export const sapphireTestnet = { + id: 23295, + name: "Oasis Sapphire Testnet", + //network: "sapphireTest", + nativeCurrency: { + decimals: 18, + name: "Rose", + symbol: "ROSE", + }, + rpcUrls: { + public: { + http: ["https://testnet.sapphire.oasis.dev"], + webSocket: ["wss://testnet.sapphire.oasis.dev/ws"], + }, + default: { + http: ["https://testnet.sapphire.oasis.dev"], + webSocket: ["wss://testnet.sapphire.oasis.dev/ws"], + }, + }, + blockExplorers: { + default: { + name: "Oasis Foundation", + url: "https://testnet.explorer.sapphire.oasis.dev/", + }, + }, +} as const satisfies Chain; + +export const getSapphireChain = (chainId?: string): Chain => { + if( chainId === undefined ) { + return sapphire; + } + switch (chainId) { + case "23294": + return sapphire; + case "23295": + return sapphireTestnet; + case "23293": + return sapphireLocalnet; + } + throw new Error(`Unknown Sapphire chain id: ${chainId}`); +}; diff --git a/integrations/viem/src/cipher.ts b/integrations/viem/src/cipher.ts new file mode 100644 index 00000000..5c0acece --- /dev/null +++ b/integrations/viem/src/cipher.ts @@ -0,0 +1,338 @@ +import { hmac } from "@noble/hashes/hmac"; +import { sha512_256 } from "@noble/hashes/sha512"; +import * as deoxysii from "@oasisprotocol/deoxysii"; +import * as cbor from "cborg"; +import * as nacl from "tweetnacl"; +import { type BoxKeyPair } from 'tweetnacl'; +import type { Promisable } from "type-fest"; +import { isBytes, isHex, toBytes, toHex } from "viem"; + +export const OASIS_CALL_DATA_PUBLIC_KEY = "oasis_callDataPublicKey"; + +const mainnetParams = { + chainId: 0x5afe, + defaultGateway: "https://sapphire.oasis.io/", + runtimeId: + "0x000000000000000000000000000000000000000000000000f80306c9858e7279", +}; +const testnetParams = { + chainId: 0x5aff, + defaultGateway: "https://testnet.sapphire.oasis.dev/", + runtimeId: + "0x000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c", +}; +const localnetParams = { + chainId: 0x5afd, + defaultGateway: "http://localhost:8545/", + runtimeId: + "0x8000000000000000000000000000000000000000000000000000000000000000", +}; +export const NETWORKS = { + mainnet: mainnetParams, + testnet: testnetParams, + localnet: localnetParams, + [mainnetParams.chainId]: mainnetParams, + [testnetParams.chainId]: testnetParams, + [localnetParams.chainId]: localnetParams, +}; + +export class CallError extends Error { + public constructor( + message: string, + public readonly response: unknown, + ) { + super(message); + } +} + +export enum Kind { + Plain = 0, + X25519DeoxysII = 1, + Mock = Number.MAX_SAFE_INTEGER, +} + +export type Envelope = { + format?: Kind; + body: + | Uint8Array + | { + pk: Uint8Array; + nonce: Uint8Array; + data: Uint8Array; + }; +}; + +type AeadEnvelope = { nonce: Uint8Array; data: Uint8Array }; +export type CallResult = { + ok?: string | Uint8Array | AeadEnvelope; + fail?: CallFailure; + unknown?: AeadEnvelope; +}; +export type CallFailure = { module: string; code: number; message?: string }; + +export abstract class Cipher { + public abstract kind: Promisable; + public abstract publicKey: Promisable; + + public abstract encrypt(plaintext: Uint8Array): Promise<{ + ciphertext: Uint8Array; + nonce: Uint8Array; + }>; + public abstract decrypt( + nonce: Uint8Array, + ciphertext: Uint8Array, + ): Promise; + + /** Encrypts the plaintext and encodes it for sending. */ + public async encryptEncode(plaintext?: BytesLike): Promise<`0x${string}`> { + const envelope = await this.encryptEnvelope(plaintext); + return envelope ? toHex(cbor.encode(envelope)) : "0x"; + } + + /** Encrypts the plaintext and formats it into an envelope. */ + public async encryptEnvelope( + plaintext?: BytesLike, + ): Promise { + if (plaintext === undefined) return; + if (!isBytesLike(plaintext)) { + throw new Error("Attempted to sign tx having non-byteslike data."); + } + if (plaintext.length === 0) return; // Txs without data are just balance transfers, and all data in those is public. + const { data, nonce } = await this.encryptCallData(arrayify(plaintext)); + const [format, pk] = await Promise.all([this.kind, this.publicKey]); + const body = pk.length && nonce.length ? { pk, nonce, data } : data; + if (format === Kind.Plain) return { body }; + return { format, body }; + } + + protected async encryptCallData( + plaintext: Uint8Array, + ): Promise { + const body = cbor.encode({ body: plaintext }); + const { ciphertext: data, nonce } = await this.encrypt(body); + return { data, nonce }; + } + + /** + * Decrypts the data contained within call + * + * This is useful for creating tools, and also decoding + * previously-sent transactions that have used the same + * encryption key. + */ + + public async decryptCallData( + nonce: Uint8Array, + ciphertext: Uint8Array, + ): Promise { + return cbor.decode(await this.decrypt(nonce, ciphertext)).body; + } + + /** + * @hidden Encrypts a CallResult in the same way as would be returned by the runtime. + * This method is not part of the SemVer interface and may be subject to change. + */ + public async encryptCallResult( + result: CallResult, + reportUnknown = false, + ): Promise { + if (result.fail) return cbor.encode(result); + const encodedResult = cbor.encode(result); + const { ciphertext, nonce } = await this.encrypt(encodedResult); + const prop = reportUnknown ? "unknown" : "ok"; + return cbor.encode({ [prop]: { nonce, data: ciphertext } }); + } + + /** Decrypts the data contained within a hex-encoded serialized envelope. */ + public async decryptEncoded(callResult: BytesLike): Promise { + return toHex( + await this.decryptCallResult(cbor.decode(arrayify(callResult))), + ); + } + + /** Decrypts the data contained within a result envelope. */ + public async decryptCallResult(res: CallResult): Promise { + function formatFailure(fail: CallFailure): string { + if (fail.message) return fail.message; + return `Call failed in module '${fail.module}' with code '${fail.code}'`; + } + if (res.fail) throw new CallError(formatFailure(res.fail), res.fail); + if (res.ok && (typeof res.ok === "string" || res.ok instanceof Uint8Array)) + return arrayify(res.ok); + const { nonce, data } = (res.ok as AeadEnvelope) ?? res.unknown; + const inner = cbor.decode(await this.decrypt(nonce, data)); + if (inner.ok) return arrayify(inner.ok); + if (inner.fail) throw new CallError(formatFailure(inner.fail), inner.fail); + throw new CallError( + `Unexpected inner call result: ${JSON.stringify(inner)}`, + inner, + ); + } +} + +/** + * A {@link Cipher} that derives a shared secret using X25519 and then uses DeoxysII for encrypting using that secret. + * + * This is the default cipher. + */ +export class X25519DeoxysII extends Cipher { + public override readonly kind = Kind.X25519DeoxysII; + public override readonly publicKey: Uint8Array; + + private cipher: deoxysii.AEAD; + private key: Uint8Array; // Stored for curious users. + + /** Creates a new cipher using an ephemeral keypair stored in memory. */ + static ephemeral(peerPublicKey: BytesLike): X25519DeoxysII { + const keypair = nacl.box.keyPair(); + return new X25519DeoxysII(keypair, arrayify(peerPublicKey)); + } + + static fromSecretKey( + secretKey: BytesLike, + peerPublicKey: BytesLike, + ): X25519DeoxysII { + const keypair = nacl.box.keyPair.fromSecretKey(arrayify(secretKey)); + return new X25519DeoxysII(keypair, arrayify(peerPublicKey)); + } + + public constructor(keypair: BoxKeyPair, peerPublicKey: Uint8Array) { + super(); + this.publicKey = keypair.publicKey; + // Derive a shared secret using X25519 (followed by hashing to remove ECDH bias). + this.key = hmac + .create(sha512_256, "MRAE_Box_Deoxys-II-256-128") + .update(nacl.scalarMult(keypair.secretKey, peerPublicKey)) + .digest(); + this.cipher = new deoxysii.AEAD(new Uint8Array(this.key)); // deoxysii owns the input + } + + public async encrypt(plaintext: Uint8Array): Promise<{ + ciphertext: Uint8Array; + nonce: Uint8Array; + }> { + const nonce = nacl.randomBytes(deoxysii.NonceSize); + const ciphertext = this.cipher.encrypt(nonce, plaintext); + return { nonce, ciphertext }; + } + + public async decrypt( + nonce: Uint8Array, + ciphertext: Uint8Array, + ): Promise { + return this.cipher.decrypt(nonce, ciphertext); + } +} + +/** A cipher that pretends to be an encrypting cipher. Used for tests. */ +export class Mock extends Cipher { + public override readonly kind = Kind.Mock; + public override readonly publicKey = new Uint8Array([1, 2, 3]); + + public static readonly NONCE = new Uint8Array([10, 20, 30, 40]); + + public async encrypt(plaintext: Uint8Array): Promise<{ + ciphertext: Uint8Array; + nonce: Uint8Array; + }> { + return { nonce: Mock.NONCE, ciphertext: plaintext }; + } + + public async decrypt( + nonce: Uint8Array, + ciphertext: Uint8Array, + ): Promise { + if (toHex(nonce) !== toHex(Mock.NONCE)) throw new Error("incorrect nonce"); + return ciphertext; + } +} + +/** + * A Cipher that constructs itself only when needed. + * Useful for deferring async construction (e.g., fetching public keys) until in an async context. + * + * @param generator A function that yields the cipher implementation. This function must be multiply callable and without observable side effects (c.f. Rust's `impl Fn()`). + */ +export function lazy(generator: () => Promisable): Cipher { + // Note: in cases when `generate` is run concurrently, the first fulfillment will be used. + return new Proxy( + {}, + { + get(target: { inner?: Promise }, prop) { + // Props (Promiseable) + if (prop === "kind" || prop === "publicKey") { + if (!target.inner) target.inner = Promise.resolve(generator()); + return target.inner.then((c) => Reflect.get(c, prop)); + } + // Funcs (async) + return async (...args: unknown[]) => { + if (!target.inner) target.inner = Promise.resolve(generator()); + return target.inner.then((c) => Reflect.get(c, prop).apply(c, args)); + }; + }, + }, + ) as Cipher; +} + +export async function fetchRuntimePublicKeyByChainId( + chainId: number, + opts?: { fetch?: typeof fetch }, +): Promise { + const { defaultGateway: gatewayUrl } = NETWORKS[chainId]; + if (!gatewayUrl) + throw new Error( + `Unable to fetch runtime public key for network with unknown ID: ${chainId}.`, + ); + const fetchImpl = globalThis?.fetch ?? opts?.fetch; + if( ! fetchImpl ) { + throw new Error('No fetch implementation found!'); + } + const res = await fetchRuntimePublicKeyBrowser(gatewayUrl, fetchImpl); + return arrayify(res.result.key); +} + +type CallDataPublicKeyResponse = { + result: { key: string; checksum: string; signature: string }; +}; + +async function fetchRuntimePublicKeyBrowser( + gwUrl: string, + fetchImpl: typeof fetch, +): Promise { + const res = await fetchImpl(gwUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: makeCallDataPublicKeyBody(), + }); + if (!res.ok) { + throw new CallError("Failed to fetch runtime public key.", res); + } + return await res.json(); +} + +function makeCallDataPublicKeyBody(): string { + return JSON.stringify({ + jsonrpc: "2.0", + id: Math.floor(Math.random() * 1e9), + method: OASIS_CALL_DATA_PUBLIC_KEY, + params: [], + }); +} + +export type BytesLike = string | Uint8Array; + +export function isBytesLike(byteslike: any): byteslike is BytesLike { + return isHex(byteslike) || isBytes(byteslike); +} + +export function arrayify(byteslike: BytesLike): Uint8Array { + if (isBytes(byteslike)) { + return byteslike; + } else if (isHex(byteslike)) { + return toBytes(byteslike.startsWith("0x") ? byteslike : `0x${byteslike}`); + } else { + throw new Error("attempted to decode non-byteslike data"); + } +} diff --git a/integrations/viem/src/index.ts b/integrations/viem/src/index.ts new file mode 100644 index 00000000..16a98236 --- /dev/null +++ b/integrations/viem/src/index.ts @@ -0,0 +1,2 @@ +export { sapphire, sapphireLocalnet, sapphireTestnet, getSapphireChain } from './chain.js'; +export { wrapWalletClient, wrapPublicClient } from "./wrapper.js" \ No newline at end of file diff --git a/integrations/viem/src/utils.ts b/integrations/viem/src/utils.ts new file mode 100644 index 00000000..2d194214 --- /dev/null +++ b/integrations/viem/src/utils.ts @@ -0,0 +1,116 @@ +import { toBytes, type EIP1193Provider } from "viem"; +import { Cipher, fetchRuntimePublicKeyByChainId, type Envelope, type BytesLike, arrayify, isBytesLike, Kind as CipherKind } from "./cipher.js"; +import * as cbor from "cborg"; + +export type Hooks = { + [K in keyof T]?: T[K]; +}; + + +export const SAPPHIRE_PROP = "sapphire"; + + +export type SapphireAnnex = { + [SAPPHIRE_PROP]: { + cipher: Cipher; + }; +}; + + +export async function fetchRuntimePublicKey( + request: EIP1193Provider["request"], + chainId?: number, +): Promise { + try { + const resp: any = await request({ + method: "oasis_callDataPublicKey" as any, + args: [], + }); + if (resp && "key" in resp) { + return toBytes(resp.key); + } + } catch (e: any) { + console.error( + "failed to fetch runtime public key using upstream transport:", + e, + ); + } + if (!chainId) { + throw new Error("unable to fetch runtime public key. chain not provided"); + } + return fetchRuntimePublicKeyByChainId(chainId); +} + + +export function makeProxy( + upstream: U, + cipher: Cipher, + hooks: Hooks, +): U & SapphireAnnex { + return new Proxy(upstream, { + get(upstream, prop) { + if (prop === SAPPHIRE_PROP) return { cipher }; + if (prop in hooks) return Reflect.get(hooks, prop); + const value = Reflect.get(upstream, prop); + return typeof value === "function" ? value.bind(upstream) : value; + }, + }) as U & SapphireAnnex; +} + + +// ----------------------------------------------------------------------------- +// Determine if the CBOR encoded calldata is a signed query or an evelope + +export class EnvelopeError extends Error {} + +interface SignedQuery { + data: Envelope; + leash: any; + signature: Uint8Array; +} + +type SignedQueryOrEnvelope = Envelope | SignedQuery; + +function isSignedQuery(x: SignedQueryOrEnvelope): x is SignedQuery { + return 'data' in x && 'leash' in x && 'signature' in x; +} + +export function isCalldataEnveloped(calldata: BytesLike, allowSignedQuery: boolean) { + try { + const outer_envelope = cbor.decode(arrayify(calldata)) as SignedQueryOrEnvelope; + let envelope: Envelope; + if (isSignedQuery(outer_envelope)) { + if (!allowSignedQuery) { + throw new EnvelopeError('Got unexpected signed query!'); + } + envelope = outer_envelope.data; + } else { + envelope = outer_envelope; + } + if (!envelopeFormatOk(envelope)) { + throw new EnvelopeError( + 'Bogus Sapphire enveloped data found in transaction!', + ); + } + return true; + } catch (e: any) { + if (e instanceof EnvelopeError) throw e; + } + return false; +} + +export function envelopeFormatOk(envelope: Envelope): boolean { + const { format, body, ...extra } = envelope; + + if (Object.keys(extra).length > 0) return false; + + if (!body) return false; + + if (format !== null && format !== CipherKind.Plain) { + if (isBytesLike(body)) return false; + + if (!isBytesLike(body.data)) return false; + } + + return true; +} \ No newline at end of file diff --git a/integrations/viem/src/wrapper.ts b/integrations/viem/src/wrapper.ts new file mode 100644 index 00000000..11ced6c7 --- /dev/null +++ b/integrations/viem/src/wrapper.ts @@ -0,0 +1,133 @@ +import { + type EIP1193Provider, + type PublicClient, + type WalletClient, + encodeFunctionData, +} from "viem"; + +import { + Cipher, + lazy as lazyCipher, + X25519DeoxysII, +} from "./cipher.js"; + +import { fetchRuntimePublicKey, makeProxy, type Hooks } from './utils.js' +import { call, deployContract, estimateGas, prepareTransactionRequest } from "viem/actions"; + + +function getAction( + client: any, + action: (_: any, params: params) => returnType, + // Some minifiers drop `Function.prototype.name`, meaning that `action.name` + // will not work. For that case, the consumer needs to pass the name explicitly. + name: string, +) { + return (params: params): returnType => + ( + client as any & { + [key: string]: (params: params) => returnType; + } + )[action.name || name](params) ?? action(client, params); +} + + +export function wrapPublicClient( + upstream: U, + overrides?: Partial<{ + cipher: Cipher; + transport: { request: EIP1193Provider['request'] }; + }>, +): U { + const transport = overrides?.transport ?? upstream.transport; + if (!transport) + throw new Error( + 'unknown transport. please configure one on the wallet client or pass it as an override', + ); + const cipher = + overrides?.cipher ?? + lazyCipher(async () => { + const rtPubKey = await fetchRuntimePublicKey(transport.request, await upstream.getChainId()); + return X25519DeoxysII.ephemeral(rtPubKey); + }); + return makeProxy(upstream, cipher, { + async estimateGas(req) { + console.log('Hooked estimateGas'); + return estimateGas(this as any, { + ...req, + data: await cipher.encryptEncode(req.data), + }); + }, + async call(req) { + console.log('Hooked call'); + return call(this as any, { + ...req, + data: await cipher.encryptEncode(req.data), + }); + }, + } as Hooks); +} + +export function wrapWalletClient( + upstream: U, + overrides?: Partial<{ + cipher: Cipher; + transport: { request: EIP1193Provider["request"] }; + }>, +): U { + const transport = overrides?.transport ?? upstream.transport; + if (!transport) + throw new Error( + "unknown transport. please configure one on the wallet client or pass it as an override", + ); + const cipher = + overrides?.cipher ?? + lazyCipher(async () => { + const rtPubKey = await fetchRuntimePublicKey(transport.request, await upstream.getChainId()); + return X25519DeoxysII.ephemeral(rtPubKey); + }); + return makeProxy(upstream, cipher, { + async deployContract(args) { + console.log('Hooked deploy'); + return deployContract(this as any, args); + }, + async writeContract(req) { + console.log('Hooked writeContract', req); + const data = encodeFunctionData({ + abi: req.abi, + args: req.args, + functionName: req.functionName, + } as any); + const encryptedData = await cipher.encryptEncode(data); + const hash = getAction( + this, + upstream.sendTransaction.bind(this), + "sendTransaction", + )({ + data: encryptedData, + to: req.address, + ...req, + }); + return hash; + }, + async prepareTransactionRequest(req) { + console.log('Hooked prepareTransactionRequest', req); + return prepareTransactionRequest(this as any, req); + }, + /* + async sendRawTransaction(req) { + console.log('hooked sendRawTransaction', req); + return upstream.sendRawTransaction(req); + }, + async sendTransaction(req) { + console.log('Hooked sendTransaction', req); + req.data = await cipher.encryptEncode(req.data); + return upstream.sendTransaction(req); + }, + async signTransaction(req) { + console.log('Hooked signTransaction', req); + req.data = await cipher.encryptEncode(req.data); + return upstream.signTransaction(req); + }, + */ + } as Hooks); +} diff --git a/integrations/viem/tsconfig.base.json b/integrations/viem/tsconfig.base.json new file mode 100644 index 00000000..9f4248cf --- /dev/null +++ b/integrations/viem/tsconfig.base.json @@ -0,0 +1,45 @@ +{ + // This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config. + "include": [], + "compilerOptions": { + // Incremental builds + // NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes. + "incremental": false, + + // Type checking + "strict": true, + "useDefineForClassFields": true, // Not enabled by default in `strict` mode unless we bump `target` to ES2022. + "noFallthroughCasesInSwitch": true, // Not enabled by default in `strict` mode. + "noImplicitReturns": true, // Not enabled by default in `strict` mode. + "useUnknownInCatchVariables": true, // TODO: This would normally be enabled in `strict` mode but would require some adjustments to the codebase. + "noImplicitOverride": true, // Not enabled by default in `strict` mode. + "noUnusedLocals": true, // Not enabled by default in `strict` mode. + "noUnusedParameters": true, // Not enabled by default in `strict` mode. + // TODO: The following options are also not enabled by default in `strict` mode and would be nice to have but would require some adjustments to the codebase. + // "exactOptionalPropertyTypes": true, + // "noUncheckedIndexedAccess": true, + + // JavaScript support + "allowJs": false, + "checkJs": false, + + // Interop constraints + "esModuleInterop": false, + "allowSyntheticDefaultImports": false, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "importHelpers": true, // This is only used for build validation. Since we do not have `tslib` installed, this will fail if we accidentally make use of anything that'd require injection of helpers. + + // Language and environment + "moduleResolution": "NodeNext", + "module": "NodeNext", + "target": "ES2021", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping. + "lib": [ + "ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances. + "DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped. + ], + + // Skip type checking for node modules + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/integrations/viem/tsconfig.build.json b/integrations/viem/tsconfig.build.json new file mode 100644 index 00000000..9e33c5aa --- /dev/null +++ b/integrations/viem/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + // This file is used to compile the for cjs and esm (see package.json build scripts). It should exclude all test files. + "extends": "./tsconfig.base.json", + "include": ["src"], + "exclude": [ + "src/**/*.test.ts", + "src/**/*.test-d.ts", + "src/**/*.bench.ts" + ], + "compilerOptions": { + "moduleResolution": "node", + "sourceMap": true, + "rootDir": "./src", + "outDir": "./dist" + } +} \ No newline at end of file diff --git a/integrations/viem/tsconfig.json b/integrations/viem/tsconfig.json new file mode 100644 index 00000000..27ca9794 --- /dev/null +++ b/integrations/viem/tsconfig.json @@ -0,0 +1,9 @@ +{ + // This configuration is used for local development and type checking. + "extends": "./tsconfig.base.json", + "include": ["src"], + "exclude": [], + "compilerOptions": { + "baseUrl": ".", + } + } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73a6236e..8a7430f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -438,6 +438,31 @@ importers: specifier: ^4.8.4 version: 4.9.5 + examples/viem: + dependencies: + '@oasisprotocol/sapphire-viem': + specifier: workspace:^ + version: link:../../integrations/viem + viem: + specifier: ^2.7.1 + version: 2.7.1(typescript@5.3.3) + devDependencies: + '@tsconfig/strictest': + specifier: 2.0.2 + version: 2.0.2 + '@types/node': + specifier: ^17.0.10 + version: 17.0.45 + abitype: + specifier: ^1.0.0 + version: 1.0.0(typescript@5.3.3) + ts-node: + specifier: 10.9.2 + version: 10.9.2(@types/node@17.0.45)(typescript@5.3.3) + typescript: + specifier: 5.3.3 + version: 5.3.3 + integrations/hardhat: dependencies: '@oasisprotocol/sapphire-paratime': @@ -460,6 +485,70 @@ importers: specifier: ^4.9.4 version: 4.9.5 + integrations/viem: + dependencies: + '@noble/hashes': + specifier: 1.3.2 + version: 1.3.2 + '@oasisprotocol/deoxysii': + specifier: 0.0.5 + version: 0.0.5 + '@oasisprotocol/sapphire-paratime': + specifier: workspace:^ + version: link:../../clients/js + cborg: + specifier: 1.10.2 + version: 1.10.2 + tweetnacl: + specifier: 1.0.3 + version: 1.0.3 + type-fest: + specifier: 2.19.0 + version: 2.19.0 + viem: + specifier: 2.x + version: 2.7.1(typescript@5.3.3) + devDependencies: + '@types/jest': + specifier: ^29.5.11 + version: 29.5.11 + '@types/node': + specifier: ^18.7.18 + version: 18.16.18 + '@types/node-fetch': + specifier: ^2.6.2 + version: 2.6.4 + '@typescript-eslint/eslint-plugin': + specifier: ^5.38.0 + version: 5.60.1(@typescript-eslint/parser@5.60.1)(eslint@8.43.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^5.38.0 + version: 5.60.1(eslint@8.43.0)(typescript@5.3.3) + eslint: + specifier: ^8.23.1 + version: 8.43.0 + eslint-config-prettier: + specifier: ^8.5.0 + version: 8.8.0(eslint@8.43.0) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.16.18)(ts-node@10.9.2) + node-fetch: + specifier: ^2.6.7 + version: 2.6.12 + prettier: + specifier: ^2.7.1 + version: 2.8.8 + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.23.5)(jest@29.7.0)(typescript@5.3.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.16.18)(typescript@5.3.3) + typescript: + specifier: ^5.3.3 + version: 5.3.3 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -4015,6 +4104,10 @@ packages: /@scure/base@1.1.1: resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + /@scure/base@1.1.5: + resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} + dev: false + /@scure/bip32@1.1.5: resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} dependencies: @@ -4027,7 +4120,15 @@ packages: dependencies: '@noble/curves': 1.1.0 '@noble/hashes': 1.3.2 - '@scure/base': 1.1.1 + '@scure/base': 1.1.5 + dev: false + + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.5 dev: false /@scure/bip39@1.1.1: @@ -4040,7 +4141,7 @@ packages: resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} dependencies: '@noble/hashes': 1.3.2 - '@scure/base': 1.1.1 + '@scure/base': 1.1.5 dev: false /@sentry/core@5.30.0: @@ -4628,10 +4729,6 @@ packages: pretty-format: 29.7.0 dev: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true - /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -4863,6 +4960,34 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@5.60.1(@typescript-eslint/parser@5.60.1)(eslint@8.43.0)(typescript@5.3.3): + resolution: {integrity: sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 5.60.1 + '@typescript-eslint/type-utils': 5.60.1(eslint@8.43.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.60.1(eslint@8.43.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.43.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.3 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/experimental-utils@5.62.0(eslint@8.43.0)(typescript@4.9.5): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4896,6 +5021,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.3.3): + resolution: {integrity: sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.60.1 + '@typescript-eslint/types': 5.60.1 + '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.3.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.43.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@5.60.1: resolution: {integrity: sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4932,6 +5077,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@5.60.1(eslint@8.43.0)(typescript@5.3.3): + resolution: {integrity: sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.3.3) + '@typescript-eslint/utils': 5.60.1(eslint@8.43.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.43.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.60.1: resolution: {integrity: sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4963,6 +5128,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@5.60.1(typescript@5.3.3): + resolution: {integrity: sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.60.1 + '@typescript-eslint/visitor-keys': 5.60.1 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4991,7 +5177,7 @@ packages: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.60.1 '@typescript-eslint/types': 5.60.1 @@ -5004,6 +5190,26 @@ packages: - typescript dev: true + /@typescript-eslint/utils@5.60.1(eslint@8.43.0)(typescript@5.3.3): + resolution: {integrity: sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.60.1 + '@typescript-eslint/types': 5.60.1 + '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.3.3) + eslint: 8.43.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@5.62.0(eslint@8.43.0)(typescript@4.9.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5212,6 +5418,19 @@ packages: typescript: 4.9.5 dev: false + /abitype@1.0.0(typescript@5.3.3): + resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + dependencies: + typescript: 5.3.3 + /abortcontroller-polyfill@1.7.5: resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} dev: true @@ -8126,7 +8345,7 @@ packages: peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.60.1(eslint@8.43.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.43.0)(typescript@4.9.5) eslint: 8.43.0 transitivePeerDependencies: - supports-color @@ -8204,7 +8423,7 @@ packages: glob-parent: 6.0.2 globals: 13.20.0 graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.0 import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 @@ -10371,6 +10590,14 @@ packages: ws: 8.15.1 dev: false + /isows@1.0.3(ws@8.13.0): + resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.13.0 + dev: false + /isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} dev: true @@ -16068,11 +16295,45 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.5.3 + semver: 7.5.4 typescript: 4.9.5 yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.1(@babel/core@7.23.5)(jest@29.7.0)(typescript@5.3.3): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.23.5 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@18.16.18)(ts-node@10.9.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.3.3 + yargs-parser: 21.1.1 + dev: true + /ts-node@10.8.0(@types/node@17.0.45)(typescript@4.7.4): resolution: {integrity: sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==} hasBin: true @@ -16227,6 +16488,37 @@ packages: yn: 3.1.1 dev: true + /ts-node@10.9.2(@types/node@18.16.18)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.16.18 + acorn: 8.11.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node@8.10.2(typescript@4.9.5): resolution: {integrity: sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==} engines: {node: '>=6.0.0'} @@ -16270,6 +16562,16 @@ packages: typescript: 4.9.5 dev: true + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -16462,7 +16764,6 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true - dev: true /typical@4.0.0: resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} @@ -16715,6 +17016,29 @@ packages: extsprintf: 1.3.0 dev: true + /viem@2.7.1(typescript@5.3.3): + resolution: {integrity: sha512-izAX2KedTFnI2l0ZshtnlK2ZuDvSlKeuaanWyNwC4ffDgrCGtwX1bvVXO3Krh53lZgqvjd8UGpjGaBl3WqJ4yQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 1.0.0(typescript@5.3.3) + isows: 1.0.3(ws@8.13.0) + typescript: 5.3.3 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /vscode-oniguruma@1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} dev: true @@ -18171,6 +18495,19 @@ packages: utf-8-validate: optional: true + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.15.1: resolution: {integrity: sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==} engines: {node: '>=10.0.0'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aaf9d0c3..0acecf8f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,9 +5,11 @@ packages: - examples/hardhat - examples/ethersv5-ts-esm - examples/ethersv6-ts-esm + - examples/viem - examples/web3js-ts-esm - examples/hardhat-boilerplate - examples/hardhat-boilerplate/frontend - examples/onchain-signer - examples/truffle - integrations/hardhat + - integrations/viem