diff --git a/examples/deployer/src/index.ts b/examples/deployer/src/index.ts index 882433a54..73f05b1d9 100644 --- a/examples/deployer/src/index.ts +++ b/examples/deployer/src/index.ts @@ -6,6 +6,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { createNewEthereumPrivateKey } from "./createNewEthereumPrivateKey"; import compile from "./compile"; @@ -16,11 +18,19 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID!, }); diff --git a/examples/rebalancer/src/provider.ts b/examples/rebalancer/src/provider.ts index e671f43c0..3c10e57e1 100644 --- a/examples/rebalancer/src/provider.ts +++ b/examples/rebalancer/src/provider.ts @@ -2,6 +2,8 @@ import * as path from "path"; import * as dotenv from "dotenv"; import { ethers } from "ethers"; import { TurnkeySigner } from "@turnkey/ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { Environment } from "./utils"; const DEFAULT_INFURA_COMMUNITY_KEY = "84842078b09946638c03157f83405213"; @@ -34,11 +36,19 @@ export function getTurnkeySigner( provider: ethers.providers.Provider, privateKeyId: string ): TurnkeySigner { + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId, }); diff --git a/examples/sweeper/package.json b/examples/sweeper/package.json index 4c64db38f..7e856a318 100644 --- a/examples/sweeper/package.json +++ b/examples/sweeper/package.json @@ -10,6 +10,7 @@ "dependencies": { "@turnkey/ethers": "workspace:*", "@turnkey/http": "workspace:*", + "@turnkey/api-key-stamper": "workspace:*", "@uniswap/sdk-core": "^3.1.1", "dotenv": "^16.0.3", "ethers": "^5.7.2", diff --git a/examples/sweeper/src/provider.ts b/examples/sweeper/src/provider.ts index 16429c9c0..7a37c38db 100644 --- a/examples/sweeper/src/provider.ts +++ b/examples/sweeper/src/provider.ts @@ -2,6 +2,8 @@ import * as path from "path"; import * as dotenv from "dotenv"; import { ethers } from "ethers"; import { TurnkeySigner } from "@turnkey/ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { Environment } from "./utils"; const DEFAULT_INFURA_COMMUNITY_KEY = "84842078b09946638c03157f83405213"; @@ -28,19 +30,27 @@ export function getProvider( return provider; } -// Initialize a Turnkey Signer -const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, - organizationId: process.env.ORGANIZATION_ID!, - privateKeyId: process.env.PRIVATE_KEY_ID!, -}); - // getTurnkeySigner returns a TurnkeySigner connected to the passed-in Provider // (https://docs.ethers.org/v5/api/providers/) export function getTurnkeySigner( provider: ethers.providers.Provider ): TurnkeySigner { + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + + // Initialize a Turnkey Signer + const turnkeySigner = new TurnkeySigner({ + client: turnkeyClient, + organizationId: process.env.ORGANIZATION_ID!, + privateKeyId: process.env.PRIVATE_KEY_ID!, + }); + return turnkeySigner.connect(provider); } diff --git a/examples/trading-runner/src/provider.ts b/examples/trading-runner/src/provider.ts index 7157b4272..3c10e57e1 100644 --- a/examples/trading-runner/src/provider.ts +++ b/examples/trading-runner/src/provider.ts @@ -2,10 +2,12 @@ import * as path from "path"; import * as dotenv from "dotenv"; import { ethers } from "ethers"; import { TurnkeySigner } from "@turnkey/ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { Environment } from "./utils"; const DEFAULT_INFURA_COMMUNITY_KEY = "84842078b09946638c03157f83405213"; -const DEFAULT_ENV = Environment.GOERLI; +const DEFAULT_ENV = Environment.SEPOLIA; // Load environment variables from `.env.local` dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); @@ -16,9 +18,9 @@ let provider = new ethers.providers.InfuraProvider( ); export function getProvider( - env = Environment.GOERLI + env = Environment.SEPOLIA ): ethers.providers.Provider { - if (env !== Environment.GOERLI) { + if (env !== Environment.SEPOLIA) { provider = new ethers.providers.InfuraProvider( env, process.env.INFURA_KEY || DEFAULT_INFURA_COMMUNITY_KEY @@ -34,11 +36,19 @@ export function getTurnkeySigner( provider: ethers.providers.Provider, privateKeyId: string ): TurnkeySigner { + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId, }); diff --git a/examples/with-ethers-and-passkeys/.env.local.example b/examples/with-ethers-and-passkeys/.env.local.example new file mode 100644 index 000000000..1479747c0 --- /dev/null +++ b/examples/with-ethers-and-passkeys/.env.local.example @@ -0,0 +1,4 @@ +API_PUBLIC_KEY="" +API_PRIVATE_KEY="" +NEXT_PUBLIC_ORGANIZATION_ID="" +NEXT_PUBLIC_TURNKEY_API_BASE_URL=https://api.turnkey.com diff --git a/examples/with-ethers-and-passkeys/.eslintrc.json b/examples/with-ethers-and-passkeys/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/examples/with-ethers-and-passkeys/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/with-ethers-and-passkeys/.gitignore b/examples/with-ethers-and-passkeys/.gitignore new file mode 100644 index 000000000..8f322f0d8 --- /dev/null +++ b/examples/with-ethers-and-passkeys/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/with-ethers-and-passkeys/README.md b/examples/with-ethers-and-passkeys/README.md new file mode 100644 index 000000000..c72be197d --- /dev/null +++ b/examples/with-ethers-and-passkeys/README.md @@ -0,0 +1,56 @@ +# Example: `with-ethers-and-passkeys` + +This example shows how to create sub-organizations, create private keys, and sign with the [`@turnkey/ethers`](../../packages/ethers/) signer, using passkeys. + +![UI screenshot](./img/ui-screenshot.png) + +The flow showcases 3 ways to make requests to Turnkey: + +- the initial request to create a new [sub-organization](https://docs.turnkey.com/getting-started/sub-organizations) is authenticated in the NextJS backend with an API signature (using `API_PUBLIC_KEY`/`API_PRIVATE_KEY` from your `.env.local` file) +- the request to create a new ETH address is signed on the frontend with your passkey, but it's passed to the NextJS backend as a signed request (the body, stamp, and url are POSTed). This lets the backend submit this request on your behalf, and poll until the new "create private keys" activity completes. Once the activity completes it returns the new address to the frontend +- the request to sign a message is done 100% client-side via a Turnkey Ethers signer (see [@turnkey/ethers](../../packages/ethers/)): it's signed with your passkey, and submitted from the browser to the Turnkey API directly. + +If you want to see a ethers demo with API keys instead of passkeys, head to the example [`with-ethers`](../with-ethers/). + +## Getting started + +### 1/ Cloning the example + +Make sure you have `Node.js` installed locally; we recommend using Node v16+. + +```bash +$ git clone https://github.com/tkhq/sdk +$ cd sdk/ +$ corepack enable # Install `pnpm` +$ pnpm install -r # Install dependencies +$ pnpm run build-all # Compile source code +$ cd examples/with-ethers-and-passkeys/ +``` + +### 2/ Setting up Turnkey + +The first step is to set up your Turnkey organization and account. By following the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) guide, you should have: + +- A public/private API key pair for Turnkey +- An organization ID + +Once you've gathered these values, add them to a new `.env.local` file. Notice that your API private key should be securely managed and **_never_** be committed to git. + +```bash +$ cp .env.local.example .env.local +``` + +Now open `.env.local` and add the missing environment variables: + +- `API_PUBLIC_KEY` +- `API_PRIVATE_KEY` +- `NEXT_PUBLIC_TURNKEY_API_BASE_URL` +- `NEXT_PUBLIC_ORGANIZATION_ID` + +### 3/ Running the app + +```bash +$ pnpm run dev +``` + +This command will start a NextJS app on localhost. If you navigate to http://localhost:3000 in your browser, you can follow the prompts to create a sub organization, create a private key for the newly created sub-organization, and sign a message using your passkey with a ethers custom account! diff --git a/examples/with-ethers-and-passkeys/img/ui-screenshot.png b/examples/with-ethers-and-passkeys/img/ui-screenshot.png new file mode 100644 index 000000000..4c54d5ee5 Binary files /dev/null and b/examples/with-ethers-and-passkeys/img/ui-screenshot.png differ diff --git a/examples/with-ethers-and-passkeys/next.config.js b/examples/with-ethers-and-passkeys/next.config.js new file mode 100644 index 000000000..658404ac6 --- /dev/null +++ b/examples/with-ethers-and-passkeys/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +module.exports = nextConfig; diff --git a/examples/with-ethers-and-passkeys/package.json b/examples/with-ethers-and-passkeys/package.json new file mode 100644 index 000000000..ea2d40945 --- /dev/null +++ b/examples/with-ethers-and-passkeys/package.json @@ -0,0 +1,37 @@ +{ + "name": "@turnkey/example-with-ethers-and-passkeys", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "typecheck": "tsc --noEmit" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@turnkey/http": "workspace:*", + "@turnkey/api-key-stamper": "workspace:*", + "@turnkey/webauthn-stamper": "workspace:*", + "@turnkey/ethers": "workspace:*", + "@types/node": "20.3.1", + "@types/react": "18.2.14", + "@types/react-dom": "18.2.6", + "axios": "^1.4.0", + "encoding": "^0.1.13", + "eslint": "8.43.0", + "eslint-config-next": "13.4.7", + "esm": "^3.2.25", + "install": "^0.13.0", + "next": "13.4.7", + "npm": "^9.7.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "^7.45.1", + "typescript": "5.1.3", + "ethers": "^5.7.2" + } +} diff --git a/examples/with-ethers-and-passkeys/public/favicon.svg b/examples/with-ethers-and-passkeys/public/favicon.svg new file mode 100644 index 000000000..c2d1cb966 --- /dev/null +++ b/examples/with-ethers-and-passkeys/public/favicon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/examples/with-ethers-and-passkeys/public/fonts/inter/Inter-Regular.woff2 b/examples/with-ethers-and-passkeys/public/fonts/inter/Inter-Regular.woff2 new file mode 100644 index 000000000..6c2b6893d Binary files /dev/null and b/examples/with-ethers-and-passkeys/public/fonts/inter/Inter-Regular.woff2 differ diff --git a/examples/with-ethers-and-passkeys/public/logo.svg b/examples/with-ethers-and-passkeys/public/logo.svg new file mode 100644 index 000000000..f983fd29b --- /dev/null +++ b/examples/with-ethers-and-passkeys/public/logo.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/examples/with-ethers-and-passkeys/src/app/globals.css b/examples/with-ethers-and-passkeys/src/app/globals.css new file mode 100644 index 000000000..d94d4168d --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/app/globals.css @@ -0,0 +1,5 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} diff --git a/examples/with-ethers-and-passkeys/src/pages/_document.tsx b/examples/with-ethers-and-passkeys/src/pages/_document.tsx new file mode 100644 index 000000000..a773ac630 --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/pages/_document.tsx @@ -0,0 +1,19 @@ +import Document, { Html, Head, Main, NextScript } from "next/document"; + +class EthersPasskeysDemo extends Document { + render() { + return ( + + + + + +
+ + + + ); + } +} + +export default EthersPasskeysDemo; diff --git a/examples/with-ethers-and-passkeys/src/pages/api/createKey.ts b/examples/with-ethers-and-passkeys/src/pages/api/createKey.ts new file mode 100644 index 000000000..2ca17fbdf --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/pages/api/createKey.ts @@ -0,0 +1,95 @@ +import axios from "axios"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { + TSignedRequest, + TurnkeyClient, + createActivityPoller, +} from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { refineNonNull } from "./utils"; + +type TResponse = { + message: string; + address?: string; + privateKeyId?: string; +}; + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + +export default async function createKey( + req: NextApiRequest, + res: NextApiResponse +) { + let signedRequest = req.body as TSignedRequest; + + try { + const activityResponse = await axios.post( + signedRequest.url, + signedRequest.body, + { + headers: { + [signedRequest.stamp.stampHeaderName]: + signedRequest.stamp.stampHeaderValue, + }, + } + ); + + if (activityResponse.status !== 200) { + res.status(500).json({ + message: `expected 200, got ${activityResponse.status}`, + }); + } + + const stamper = new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }); + const client = new TurnkeyClient( + { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! }, + stamper + ); + + const activityPoller = createActivityPoller({ + client: client, + requestFn: client.getActivity, + }); + + const activityId = refineNonNull(activityResponse.data.activity?.id); + const subOrgId = refineNonNull( + activityResponse.data.activity?.organizationId + ); + + const completedActivity = await activityPoller({ + activityId, + organizationId: subOrgId, + }); + + const privateKeys = + completedActivity.result.createPrivateKeysResultV2?.privateKeys; + + // XXX: sorry for the ugly code! We expect a single key / address returned. + // If we have more than one key / address returned, or none, this would break. + const address = privateKeys + ?.map((pk) => pk.addresses?.map((addr) => addr.address).join("")) + .join(""); + const privateKeyId = privateKeys?.map((pk) => pk.privateKeyId).join(""); + + res.status(200).json({ + message: "successfully created key", + address: address, + privateKeyId: privateKeyId, + }); + } catch (e) { + console.error(e); + + res.status(500).json({ + message: `Something went wrong, caught error: ${e}`, + }); + } +} diff --git a/examples/with-viem-and-passkeys/src/pages/api/subOrg.ts b/examples/with-ethers-and-passkeys/src/pages/api/createSubOrg.ts similarity index 93% rename from examples/with-viem-and-passkeys/src/pages/api/subOrg.ts rename to examples/with-ethers-and-passkeys/src/pages/api/createSubOrg.ts index 6cdd81ae2..87e8207ea 100644 --- a/examples/with-viem-and-passkeys/src/pages/api/subOrg.ts +++ b/examples/with-ethers-and-passkeys/src/pages/api/createSubOrg.ts @@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { TurnkeyApiTypes, TurnkeyClient } from "@turnkey/http"; import { createActivityPoller } from "@turnkey/http"; import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { refineNonNull } from "./utils"; type TAttestation = TurnkeyApiTypes["v1Attestation"]; @@ -99,14 +100,3 @@ export default async function createUser( }); } } - -function refineNonNull( - input: T | null | undefined, - errorMessage?: string -): T { - if (input == null) { - throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`); - } - - return input; -} diff --git a/examples/with-ethers-and-passkeys/src/pages/api/utils.ts b/examples/with-ethers-and-passkeys/src/pages/api/utils.ts new file mode 100644 index 000000000..d26b4fd42 --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/pages/api/utils.ts @@ -0,0 +1,10 @@ +export function refineNonNull( + input: T | null | undefined, + errorMessage?: string +): T { + if (input == null) { + throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`); + } + + return input; +} diff --git a/examples/with-ethers-and-passkeys/src/pages/index.module.css b/examples/with-ethers-and-passkeys/src/pages/index.module.css new file mode 100644 index 000000000..3c4cced5d --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/pages/index.module.css @@ -0,0 +1,90 @@ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../../public/fonts/inter/Inter-Regular.woff2?v=3.19") + format("woff2"); +} + +.main { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + text-align: center; + padding: 2rem; + gap: 30px; + font-family: "Inter"; + max-width: 50ch; + margin: auto; +} + +.input { + width: 100%; + margin: 0; + padding: 10px 16px; + border-radius: 8px; + border-width: 1px; + border-style: solid; + border-color: rgba(216, 219, 227, 1); + font-family: "Inter"; +} + +.label { + font-family: "Inter"; +} + +.prompt { + font-family: "Inter"; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 10px 16px; + border-radius: 8px; + border-width: 1px; + border-style: solid; + cursor: pointer; + color: white; + background-color: rgba(43, 47, 51, 1); + border-color: rgba(63, 70, 75, 1); + font-family: "Inter"; +} + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.form input { + max-width: 44ch; +} + +.info { + display: block; + font-size: 0.9em; + padding: 10px 16px; + margin: 10px; + border-radius: 8px; + background-color: rgb(240, 249, 250); + word-break: break-all; + box-shadow: inset 0 0 4px #e2e8e8; +} + +.code { + font-size: 1em; + font-weight: bold; +} + +.explainer { + line-height: 1.4em; + font-size: 0.9em; + color: rgb(134, 153, 156); +} diff --git a/examples/with-ethers-and-passkeys/src/pages/index.tsx b/examples/with-ethers-and-passkeys/src/pages/index.tsx new file mode 100644 index 000000000..1720048a0 --- /dev/null +++ b/examples/with-ethers-and-passkeys/src/pages/index.tsx @@ -0,0 +1,342 @@ +import Image from "next/image"; +import styles from "./index.module.css"; +import axios from "axios"; +import { ethers } from "ethers"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { getWebAuthnAttestation, TurnkeyClient } from "@turnkey/http"; +import { WebauthnStamper } from "@turnkey/webauthn-stamper"; +import { TurnkeySigner } from "@turnkey/ethers"; + +type subOrgFormData = { + subOrgName: string; +}; + +type privateKeyFormData = { + privateKeyName: string; +}; + +type signingFormData = { + messageToSign: string; +}; + +const generateRandomBuffer = (): ArrayBuffer => { + const arr = new Uint8Array(32); + crypto.getRandomValues(arr); + return arr.buffer; +}; + +const base64UrlEncode = (challenge: ArrayBuffer): string => { + return Buffer.from(challenge) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +}; + +type TPrivateKeyState = { + id: string; + address: string; +} | null; + +type TSignedMessage = { + message: string; + signature: string; +} | null; + +const humanReadableDateTime = (): string => { + return new Date().toLocaleString().replaceAll("/", "-").replaceAll(":", "."); +}; + +export default function Home() { + const [subOrgId, setSubOrgId] = useState(null); + const [privateKey, setPrivateKey] = useState(null); + const [signedMessage, setSignedMessage] = useState(null); + + const { handleSubmit: subOrgFormSubmit } = useForm(); + const { register: signingFormRegister, handleSubmit: signingFormSubmit } = + useForm(); + const { handleSubmit: privateKeyFormSubmit } = useForm(); + const { register: _loginFormRegister, handleSubmit: loginFormSubmit } = + useForm(); + + const stamper = new WebauthnStamper({ + rpId: "localhost", + }); + + const passkeyHttpClient = new TurnkeyClient( + { + baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL!, + }, + stamper + ); + + const createPrivateKey = async () => { + if (!subOrgId) { + throw new Error("sub-org id not found"); + } + + const signedRequest = await passkeyHttpClient.stampCreatePrivateKeys({ + type: "ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2", + organizationId: subOrgId, + timestampMs: String(Date.now()), + parameters: { + privateKeys: [ + { + privateKeyName: `ETH Key ${Math.floor(Math.random() * 1000)}`, + curve: "CURVE_SECP256K1", + addressFormats: ["ADDRESS_FORMAT_ETHEREUM"], + privateKeyTags: [], + }, + ], + }, + }); + + const response = await axios.post("/api/createKey", signedRequest); + + setPrivateKey({ + id: response.data["privateKeyId"], + address: response.data["address"], + }); + }; + + const signMessage = async (data: signingFormData) => { + if (!subOrgId || !privateKey) { + throw new Error("sub-org id or private key not found"); + } + + const ethersSigner = new TurnkeySigner({ + client: passkeyHttpClient, + organizationId: subOrgId, + privateKeyId: privateKey.id, + }); + + const signedMessage = await ethersSigner.signMessage(data.messageToSign); + + setSignedMessage({ + message: data.messageToSign, + signature: signedMessage, + }); + }; + + const createSubOrg = async () => { + const challenge = generateRandomBuffer(); + const subOrgName = `Turnkey Ethers+Passkey Demo - ${humanReadableDateTime()}`; + const authenticatorUserId = generateRandomBuffer(); + + const attestation = await getWebAuthnAttestation({ + publicKey: { + rp: { + id: "localhost", + name: "Turnkey Ethers Passkey Demo", + }, + challenge, + pubKeyCredParams: [ + { + type: "public-key", + // All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms + // Turnkey only supports ES256 at the moment. + alg: -7, + }, + ], + user: { + id: authenticatorUserId, + name: subOrgName, + displayName: subOrgName, + }, + }, + }); + + const res = await axios.post("/api/createSubOrg", { + subOrgName: subOrgName, + attestation, + challenge: base64UrlEncode(challenge), + }); + + setSubOrgId(res.data.subOrgId); + }; + + const login = async () => { + // We use the parent org ID, which we know at all times, + const res = await passkeyHttpClient.getWhoami({ + organizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!, + }); + // to get the sub-org ID, which we don't know at this point because we don't + // have a DB. Note that we are able to perform this lookup by using the + // credential ID from the users WebAuthn stamp. + setSubOrgId(res.organizationId); + }; + + return ( +
+ + Turnkey Logo + +
+ {subOrgId && ( +
+ Your sub-org ID:
+ {subOrgId} +
+ )} + {privateKey && ( +
+ ETH address:
+ {privateKey.address} +
+ )} + {signedMessage && ( +
+ Message:
+ {signedMessage.message} +
+
+ Signature:
+ {signedMessage.signature} +
+
+ + Verify with Etherscan + +
+ )} +
+ {!subOrgId && ( +
+

First, create a new sub-organization

+

+ We'll prompt your browser to create a new passkey. The details + (credential ID, authenticator data, client data, attestation) will + be used to create a new{" "} + + Turnkey Sub-Organization + + . +
+
+ This request to Turnkey will be created and signed by the backend + API key pair. +

+
+ +
+
+
+

Already created a sub-organization? Log back in

+

+ Based on the parent organization ID and a stamp from your passkey + used to created the sub-organization, we can look up your + sug-organization using the + + Whoami endpoint. + +

+
+ +
+
+ )} + {subOrgId && !privateKey && ( +
+

Next, create a new Ethereum address using your passkey

+

+ We will sign the key creation request ( + + /public/v1/submit/create_private_keys + + ) with your passkey, and forward it to Turnkey through the NextJS + backend. +
+
+ This ensures we can safely poll for activity completion and handle + errors. +

+
+ +
+
+ )} + {subOrgId && privateKey && ( +
+

Now let's sign something!

+

+ We'll use an{" "} + + Ethers signer + {" "} + to do this, using{" "} + + @turnkey/ethers + + . You can kill your NextJS server if you want, everything happens on + the client-side! +

+
+ + +
+
+ )} +
+ ); +} diff --git a/examples/with-ethers-and-passkeys/tsconfig.json b/examples/with-ethers-and-passkeys/tsconfig.json new file mode 100644 index 000000000..0c7555fa7 --- /dev/null +++ b/examples/with-ethers-and-passkeys/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/with-ethers/src/index.ts b/examples/with-ethers/src/index.ts index a73a4aaec..c2e04f440 100644 --- a/examples/with-ethers/src/index.ts +++ b/examples/with-ethers/src/index.ts @@ -6,6 +6,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { createNewEthereumPrivateKey } from "./createNewEthereumPrivateKey"; import WETH_TOKEN_ABI from "./weth-contract-abi.json"; @@ -18,11 +20,19 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID!, }); diff --git a/examples/with-ethers/src/legacySepolia.ts b/examples/with-ethers/src/legacySepolia.ts index df89a58d8..0d03cee01 100644 --- a/examples/with-ethers/src/legacySepolia.ts +++ b/examples/with-ethers/src/legacySepolia.ts @@ -6,6 +6,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { createNewEthereumPrivateKey } from "./createNewEthereumPrivateKey"; import WETH_TOKEN_ABI from "./weth-contract-abi.json"; @@ -18,11 +20,19 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID!, }); diff --git a/examples/with-gnosis/src/index.ts b/examples/with-gnosis/src/index.ts index 8d7636b4e..9f1fe2338 100644 --- a/examples/with-gnosis/src/index.ts +++ b/examples/with-gnosis/src/index.ts @@ -6,6 +6,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import EthersAdapter from "@safe-global/safe-ethers-lib"; import Safe, { SafeFactory, @@ -28,27 +30,31 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner1 = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID_1!, }); const turnkeySigner2 = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID_2!, }); const turnkeySigner3 = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID_3!, }); diff --git a/examples/with-nonce-manager/src/managedOptimistic.ts b/examples/with-nonce-manager/src/managedOptimistic.ts index 0b922f91d..073ed3189 100644 --- a/examples/with-nonce-manager/src/managedOptimistic.ts +++ b/examples/with-nonce-manager/src/managedOptimistic.ts @@ -7,6 +7,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { createNewEthereumPrivateKey } from "./createNewEthereumPrivateKey"; import { print, sleep, getUpdatedTransaction } from "./util"; import { @@ -139,11 +141,19 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID!, }); diff --git a/examples/with-nonce-manager/src/simpleSequential.ts b/examples/with-nonce-manager/src/simpleSequential.ts index d3a54b3b7..4c08497ce 100644 --- a/examples/with-nonce-manager/src/simpleSequential.ts +++ b/examples/with-nonce-manager/src/simpleSequential.ts @@ -6,6 +6,8 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); import { TurnkeySigner } from "@turnkey/ethers"; import { ethers } from "ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { createNewEthereumPrivateKey } from "./createNewEthereumPrivateKey"; import { print } from "./util"; @@ -16,11 +18,19 @@ async function main() { return; } + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + // Initialize a Turnkey Signer const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, + client: turnkeyClient, organizationId: process.env.ORGANIZATION_ID!, privateKeyId: process.env.PRIVATE_KEY_ID!, }); diff --git a/examples/with-uniswap/package.json b/examples/with-uniswap/package.json index 0b986b23e..665da33eb 100644 --- a/examples/with-uniswap/package.json +++ b/examples/with-uniswap/package.json @@ -10,6 +10,7 @@ "dependencies": { "@turnkey/ethers": "workspace:*", "@turnkey/http": "workspace:*", + "@turnkey/api-key-stamper": "workspace:*", "@uniswap/sdk-core": "^3.1.1", "@uniswap/v3-core": "^1.0.1", "@uniswap/v3-sdk": "^3.9.0", diff --git a/examples/with-uniswap/src/provider.ts b/examples/with-uniswap/src/provider.ts index 08cb8cfac..6ffe78630 100644 --- a/examples/with-uniswap/src/provider.ts +++ b/examples/with-uniswap/src/provider.ts @@ -1,18 +1,20 @@ -import * as dotenv from "dotenv"; import * as path from "path"; - -// Load environment variables from `.env.local` -dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); - +import * as dotenv from "dotenv"; import { ethers } from "ethers"; import { TurnkeySigner } from "@turnkey/ethers"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; import { Environment } from "./constants"; +const DEFAULT_INFURA_COMMUNITY_KEY = "84842078b09946638c03157f83405213"; const DEFAULT_ENV = Environment.GOERLI; +// Load environment variables from `.env.local` +dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); + let provider = new ethers.providers.InfuraProvider( DEFAULT_ENV, - process.env.INFURA_KEY! + process.env.INFURA_KEY || DEFAULT_INFURA_COMMUNITY_KEY ); export function getProvider( @@ -21,26 +23,34 @@ export function getProvider( if (env !== Environment.GOERLI) { provider = new ethers.providers.InfuraProvider( env, - process.env.INFURA_KEY! + process.env.INFURA_KEY || DEFAULT_INFURA_COMMUNITY_KEY ); } return provider; } -// Initialize a Turnkey Signer -const turnkeySigner = new TurnkeySigner({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - baseUrl: process.env.BASE_URL!, - organizationId: process.env.ORGANIZATION_ID!, - privateKeyId: process.env.PRIVATE_KEY_ID!, -}); - // getTurnkeySigner returns a TurnkeySigner connected to the passed-in Provider // (https://docs.ethers.org/v5/api/providers/) export function getTurnkeySigner( provider: ethers.providers.Provider ): TurnkeySigner { + const turnkeyClient = new TurnkeyClient( + { + baseUrl: process.env.BASE_URL!, + }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + + // Initialize a Turnkey Signer + const turnkeySigner = new TurnkeySigner({ + client: turnkeyClient, + organizationId: process.env.ORGANIZATION_ID!, + privateKeyId: process.env.PRIVATE_KEY_ID!, + }); + return turnkeySigner.connect(provider); } diff --git a/examples/with-viem-and-passkeys/README.md b/examples/with-viem-and-passkeys/README.md index 30edbbf47..fc3c142d2 100644 --- a/examples/with-viem-and-passkeys/README.md +++ b/examples/with-viem-and-passkeys/README.md @@ -45,7 +45,7 @@ Now open `.env.local` and add the missing environment variables: - `API_PUBLIC_KEY` - `API_PRIVATE_KEY` - `NEXT_PUBLIC_TURNKEY_API_BASE_URL` -- `ORGANIZATION_ID` +- `NEXT_PUBLIC_ORGANIZATION_ID` ### 3/ Running the app diff --git a/examples/with-viem-and-passkeys/src/pages/api/createKey.ts b/examples/with-viem-and-passkeys/src/pages/api/createKey.ts index ad1562063..2ca17fbdf 100644 --- a/examples/with-viem-and-passkeys/src/pages/api/createKey.ts +++ b/examples/with-viem-and-passkeys/src/pages/api/createKey.ts @@ -1,8 +1,12 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { TSignedRequest, TurnkeyClient } from "@turnkey/http"; import axios from "axios"; -import { TActivityResponse } from "@turnkey/http/dist/shared"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { + TSignedRequest, + TurnkeyClient, + createActivityPoller, +} from "@turnkey/http"; import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { refineNonNull } from "./utils"; type TResponse = { message: string; @@ -42,49 +46,44 @@ export default async function createKey( }); } - let response = activityResponse.data as TActivityResponse; - let attempts = 0; + const stamper = new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }); + const client = new TurnkeyClient( + { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! }, + stamper + ); - while (attempts < 3) { - if (response.activity.status != "ACTIVITY_STATUS_COMPLETED") { - const stamper = new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }); - const client = new TurnkeyClient( - { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! }, - stamper - ); - response = await client.getActivity({ - organizationId: response.activity.organizationId, - activityId: response.activity.id, - }); + const activityPoller = createActivityPoller({ + client: client, + requestFn: client.getActivity, + }); - await sleep(500); + const activityId = refineNonNull(activityResponse.data.activity?.id); + const subOrgId = refineNonNull( + activityResponse.data.activity?.organizationId + ); - attempts++; - } else { - const privateKeys = - response.activity.result.createPrivateKeysResultV2?.privateKeys; + const completedActivity = await activityPoller({ + activityId, + organizationId: subOrgId, + }); - // XXX: sorry for the ugly code! We expect a single key / address returned. - // If we have more than one key / address returned, or none, this would break. - const address = privateKeys - ?.map((pk) => pk.addresses?.map((addr) => addr.address).join("")) - .join(""); - const privateKeyId = privateKeys?.map((pk) => pk.privateKeyId).join(""); + const privateKeys = + completedActivity.result.createPrivateKeysResultV2?.privateKeys; - res.status(200).json({ - message: "successfully created key", - address: address, - privateKeyId: privateKeyId, - }); - return; - } - } + // XXX: sorry for the ugly code! We expect a single key / address returned. + // If we have more than one key / address returned, or none, this would break. + const address = privateKeys + ?.map((pk) => pk.addresses?.map((addr) => addr.address).join("")) + .join(""); + const privateKeyId = privateKeys?.map((pk) => pk.privateKeyId).join(""); - res.status(500).json({ - message: "failed to create key", + res.status(200).json({ + message: "successfully created key", + address: address, + privateKeyId: privateKeyId, }); } catch (e) { console.error(e); diff --git a/examples/with-viem-and-passkeys/src/pages/api/createSubOrg.ts b/examples/with-viem-and-passkeys/src/pages/api/createSubOrg.ts new file mode 100644 index 000000000..87e8207ea --- /dev/null +++ b/examples/with-viem-and-passkeys/src/pages/api/createSubOrg.ts @@ -0,0 +1,102 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { TurnkeyApiTypes, TurnkeyClient } from "@turnkey/http"; +import { createActivityPoller } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { refineNonNull } from "./utils"; + +type TAttestation = TurnkeyApiTypes["v1Attestation"]; + +type CreateSubOrgWithPrivateKeyRequest = { + subOrgName: string; + challenge: string; + privateKeyName: string; + attestation: TAttestation; +}; + +type CreateSubOrgResponse = { + subOrgId: string; + privateKeyId: string; + privateKeyAddress: string; +}; + +type ErrorMessage = { + message: string; +}; + +export default async function createUser( + req: NextApiRequest, + res: NextApiResponse +) { + const createSubOrgRequest = req.body as CreateSubOrgWithPrivateKeyRequest; + + try { + const turnkeyClient = new TurnkeyClient( + { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + + const activityPoller = createActivityPoller({ + client: turnkeyClient, + requestFn: turnkeyClient.createSubOrganization, + }); + + const privateKeyName = `Default ETH Key`; + + const completedActivity = await activityPoller({ + type: "ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V3", + timestampMs: String(Date.now()), + organizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!, + parameters: { + subOrganizationName: createSubOrgRequest.subOrgName, + rootQuorumThreshold: 1, + rootUsers: [ + { + userName: "New user", + apiKeys: [], + authenticators: [ + { + authenticatorName: "Passkey", + challenge: createSubOrgRequest.challenge, + attestation: createSubOrgRequest.attestation, + }, + ], + }, + ], + privateKeys: [ + { + privateKeyName: privateKeyName, + curve: "CURVE_SECP256K1", + addressFormats: ["ADDRESS_FORMAT_ETHEREUM"], + privateKeyTags: [], + }, + ], + }, + }); + + const subOrgId = refineNonNull( + completedActivity.result.createSubOrganizationResultV3?.subOrganizationId + ); + const privateKeys = refineNonNull( + completedActivity.result.createSubOrganizationResultV3?.privateKeys + ); + const privateKeyId = refineNonNull(privateKeys?.[0]?.privateKeyId); + const privateKeyAddress = refineNonNull( + privateKeys?.[0]?.addresses?.[0]?.address + ); + + res.status(200).json({ + subOrgId, + privateKeyId, + privateKeyAddress, + }); + } catch (e) { + console.error(e); + + res.status(500).json({ + message: "Something went wrong.", + }); + } +} diff --git a/examples/with-viem-and-passkeys/src/pages/api/utils.ts b/examples/with-viem-and-passkeys/src/pages/api/utils.ts new file mode 100644 index 000000000..d26b4fd42 --- /dev/null +++ b/examples/with-viem-and-passkeys/src/pages/api/utils.ts @@ -0,0 +1,10 @@ +export function refineNonNull( + input: T | null | undefined, + errorMessage?: string +): T { + if (input == null) { + throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`); + } + + return input; +} diff --git a/examples/with-viem-and-passkeys/src/pages/index.tsx b/examples/with-viem-and-passkeys/src/pages/index.tsx index dd5c6748c..6f306b1c8 100644 --- a/examples/with-viem-and-passkeys/src/pages/index.tsx +++ b/examples/with-viem-and-passkeys/src/pages/index.tsx @@ -157,7 +157,7 @@ export default function Home() { }, }); - const res = await axios.post("/api/subOrg", { + const res = await axios.post("/api/createSubOrg", { subOrgName: subOrgName, attestation, challenge: base64UrlEncode(challenge), diff --git a/packages/ethers/package.json b/packages/ethers/package.json index e94bcb80f..2d10a6cfa 100644 --- a/packages/ethers/package.json +++ b/packages/ethers/package.json @@ -43,7 +43,8 @@ }, "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", - "@turnkey/http": "workspace:*" + "@turnkey/http": "workspace:*", + "@turnkey/api-key-stamper": "workspace:*" }, "devDependencies": { "@ethersproject/experimental": "^5.7.0", diff --git a/packages/ethers/src/__tests__/index-test.ts b/packages/ethers/src/__tests__/index-test.ts index bc4f9d529..fec6a5b9a 100644 --- a/packages/ethers/src/__tests__/index-test.ts +++ b/packages/ethers/src/__tests__/index-test.ts @@ -5,6 +5,8 @@ import hre from "hardhat"; import { test, expect, beforeEach, describe } from "@jest/globals"; import { TurnkeySigner, TurnkeyActivityError } from "../"; import Test721 from "./contracts/artifacts/src/__tests__/contracts/source/Test721.sol/Test721.json"; +import { TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; // @ts-expect-error const testCase: typeof test = (...argList) => { @@ -18,6 +20,7 @@ const testCase: typeof test = (...argList) => { describe("TurnkeySigner", () => { let connectedSigner: TurnkeySigner; + let signerWithProvider: TurnkeySigner; let chainId: number; let expectedEthAddress: string; let bannedToAddress: string; @@ -63,14 +66,32 @@ describe("TurnkeySigner", () => { // @ts-ignore const provider = hre.ethers.provider; + // create new client + const turnkeyClient = new TurnkeyClient( + { + baseUrl, + }, + new ApiKeyStamper({ + apiPublicKey, + apiPrivateKey, + }) + ); + connectedSigner = new TurnkeySigner({ - apiPublicKey, - apiPrivateKey, - baseUrl, + client: turnkeyClient, organizationId, privateKeyId, }).connect(provider); + signerWithProvider = new TurnkeySigner( + { + client: turnkeyClient, + organizationId, + privateKeyId, + }, + provider + ); + chainId = (await connectedSigner.provider!.getNetwork()).chainId; eip1193 = new Eip1193Bridge(connectedSigner, provider); @@ -78,11 +99,16 @@ describe("TurnkeySigner", () => { setBalance(expectedEthAddress, ethers.utils.parseEther("999999")); }); - testCase("basics", async () => { + testCase("basics for connected signer", async () => { expect(ethers.Signer.isSigner(connectedSigner)).toBe(true); expect(await connectedSigner.getAddress()).toBe(expectedEthAddress); }); + testCase("basics for connected signer via constructor", async () => { + expect(ethers.Signer.isSigner(signerWithProvider)).toBe(true); + expect(await signerWithProvider.getAddress()).toBe(expectedEthAddress); + }); + testCase("it signs transactions", async () => { const tx = await connectedSigner.signTransaction({ to: "0x2Ad9eA1E677949a536A270CEC812D6e868C88108", diff --git a/packages/ethers/src/index.ts b/packages/ethers/src/index.ts index b14c1c7ab..17962ca1c 100644 --- a/packages/ethers/src/index.ts +++ b/packages/ethers/src/index.ts @@ -1,10 +1,6 @@ import { ethers } from "ethers"; -import { - TurnkeyApi, - TurnkeyActivityError, - TurnkeyRequestError, - init as httpInit, -} from "@turnkey/http"; +import { TurnkeyActivityError, TurnkeyRequestError } from "@turnkey/http"; +import type { TurnkeyClient } from "@turnkey/http"; import type { TypedDataSigner } from "@ethersproject/abstract-signer"; import type { UnsignedTransaction, @@ -15,17 +11,9 @@ import type { type TConfig = { /** - * Turnkey API public key + * Turnkey client */ - apiPublicKey: string; - /** - * Turnkey API private key - */ - apiPrivateKey: string; - /** - * Turnkey API base URL - */ - baseUrl: string; + client: TurnkeyClient; /** * Turnkey organization ID */ @@ -37,7 +25,7 @@ type TConfig = { }; export class TurnkeySigner extends ethers.Signer implements TypedDataSigner { - private readonly config: TConfig; + private readonly client: TurnkeyClient; public readonly organizationId: string; public readonly privateKeyId: string; @@ -46,36 +34,27 @@ export class TurnkeySigner extends ethers.Signer implements TypedDataSigner { super(); ethers.utils.defineReadOnly(this, "provider", provider); - this.config = config; - - const { - apiPublicKey, - apiPrivateKey, - baseUrl, - organizationId, - privateKeyId, - } = config; - - this.organizationId = organizationId; - this.privateKeyId = privateKeyId; - - httpInit({ - apiPublicKey, - apiPrivateKey, - baseUrl, - }); + this.client = config.client; + + this.organizationId = config.organizationId; + this.privateKeyId = config.privateKeyId; } connect(provider: ethers.providers.Provider): TurnkeySigner { - return new TurnkeySigner(this.config, provider); + return new TurnkeySigner( + { + client: this.client, + organizationId: this.organizationId, + privateKeyId: this.privateKeyId, + }, + provider + ); } async getAddress(): Promise { - const data = await TurnkeyApi.getPrivateKey({ - body: { - privateKeyId: this.config.privateKeyId, - organizationId: this.config.organizationId, - }, + const data = await this.client.getPrivateKey({ + privateKeyId: this.privateKeyId, + organizationId: this.organizationId, }); const maybeAddress = data.privateKey.addresses.find( @@ -84,7 +63,7 @@ export class TurnkeySigner extends ethers.Signer implements TypedDataSigner { if (typeof maybeAddress !== "string" || !maybeAddress) { throw new TurnkeyActivityError({ - message: `Unable to find Ethereum address for key ${this.config.privateKeyId} under organization ${this.config.organizationId}`, + message: `Unable to find Ethereum address for key ${this.privateKeyId} under organization ${this.organizationId}`, }); } @@ -92,17 +71,15 @@ export class TurnkeySigner extends ethers.Signer implements TypedDataSigner { } private async _signTransactionImpl(message: string): Promise { - const { activity } = await TurnkeyApi.signTransaction({ - body: { - type: "ACTIVITY_TYPE_SIGN_TRANSACTION", - organizationId: this.config.organizationId, - parameters: { - privateKeyId: this.config.privateKeyId, - type: "TRANSACTION_TYPE_ETHEREUM", - unsignedTransaction: message, - }, - timestampMs: String(Date.now()), // millisecond timestamp + const { activity } = await this.client.signTransaction({ + type: "ACTIVITY_TYPE_SIGN_TRANSACTION", + organizationId: this.organizationId, + parameters: { + privateKeyId: this.privateKeyId, + type: "TRANSACTION_TYPE_ETHEREUM", + unsignedTransaction: message, }, + timestampMs: String(Date.now()), // millisecond timestamp }); const { id, status, type } = activity; @@ -203,18 +180,16 @@ export class TurnkeySigner extends ethers.Signer implements TypedDataSigner { } async _signMessageImpl(message: string): Promise { - const { activity } = await TurnkeyApi.signRawPayload({ - body: { - type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD", - organizationId: this.config.organizationId, - parameters: { - privateKeyId: this.config.privateKeyId, - payload: message, - encoding: "PAYLOAD_ENCODING_HEXADECIMAL", - hashFunction: "HASH_FUNCTION_NO_OP", - }, - timestampMs: String(Date.now()), // millisecond timestamp + const { activity } = await this.client.signRawPayload({ + type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD", + organizationId: this.organizationId, + parameters: { + privateKeyId: this.privateKeyId, + payload: message, + encoding: "PAYLOAD_ENCODING_HEXADECIMAL", + hashFunction: "HASH_FUNCTION_NO_OP", }, + timestampMs: String(Date.now()), // millisecond timestamp }); const { id, status, type } = activity; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9705998ec..933d38a69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: examples/sweeper: dependencies: + '@turnkey/api-key-stamper': + specifier: workspace:* + version: link:../../packages/api-key-stamper '@turnkey/ethers': specifier: workspace:* version: link:../../packages/ethers @@ -185,6 +188,69 @@ importers: specifier: ^5.7.2 version: 5.7.2 + examples/with-ethers-and-passkeys: + dependencies: + '@turnkey/api-key-stamper': + specifier: workspace:* + version: link:../../packages/api-key-stamper + '@turnkey/ethers': + specifier: workspace:* + version: link:../../packages/ethers + '@turnkey/http': + specifier: workspace:* + version: link:../../packages/http + '@turnkey/webauthn-stamper': + specifier: workspace:* + version: link:../../packages/webauthn-stamper + '@types/node': + specifier: 20.3.1 + version: 20.3.1 + '@types/react': + specifier: 18.2.14 + version: 18.2.14 + '@types/react-dom': + specifier: 18.2.6 + version: 18.2.6 + axios: + specifier: ^1.4.0 + version: 1.4.0 + encoding: + specifier: ^0.1.13 + version: 0.1.13 + eslint: + specifier: 8.43.0 + version: 8.43.0 + eslint-config-next: + specifier: 13.4.7 + version: 13.4.7(eslint@8.43.0)(typescript@5.1.3) + esm: + specifier: ^3.2.25 + version: 3.2.25 + ethers: + specifier: ^5.7.2 + version: 5.7.2 + install: + specifier: ^0.13.0 + version: 0.13.0 + next: + specifier: 13.4.7 + version: 13.4.7(@babel/core@7.20.12)(react-dom@18.2.0)(react@18.2.0) + npm: + specifier: ^9.7.2 + version: 9.7.2 + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.45.1 + version: 7.45.1(react@18.2.0) + typescript: + specifier: 5.1.3 + version: 5.1.3 + examples/with-federated-passkeys: dependencies: '@turnkey/api-key-stamper': @@ -325,6 +391,9 @@ importers: examples/with-uniswap: dependencies: + '@turnkey/api-key-stamper': + specifier: workspace:* + version: link:../../packages/api-key-stamper '@turnkey/ethers': specifier: workspace:* version: link:../../packages/ethers @@ -487,6 +556,9 @@ importers: '@ethersproject/abstract-signer': specifier: ^5.7.0 version: 5.7.0 + '@turnkey/api-key-stamper': + specifier: workspace:* + version: link:../api-key-stamper '@turnkey/http': specifier: workspace:* version: link:../http