Skip to content

Commit

Permalink
new example: with-ethers-and-passkeys
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed Sep 7, 2023
1 parent c878237 commit 11331f9
Show file tree
Hide file tree
Showing 19 changed files with 949 additions and 1 deletion.
4 changes: 4 additions & 0 deletions examples/with-ethers-and-passkeys/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
API_PUBLIC_KEY="<Turnkey API Public Key (that starts with 02 or 03)>"
API_PRIVATE_KEY="<Turnkey API Private Key>"
NEXT_PUBLIC_ORGANIZATION_ID="<Turnkey organization ID>"
NEXT_PUBLIC_TURNKEY_API_BASE_URL=https://api.turnkey.com
3 changes: 3 additions & 0 deletions examples/with-ethers-and-passkeys/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
35 changes: 35 additions & 0 deletions examples/with-ethers-and-passkeys/.gitignore
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions examples/with-ethers-and-passkeys/README.md
Original file line number Diff line number Diff line change
@@ -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!
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/with-ethers-and-passkeys/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = nextConfig;
37 changes: 37 additions & 0 deletions examples/with-ethers-and-passkeys/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
25 changes: 25 additions & 0 deletions examples/with-ethers-and-passkeys/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
29 changes: 29 additions & 0 deletions examples/with-ethers-and-passkeys/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions examples/with-ethers-and-passkeys/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
19 changes: 19 additions & 0 deletions examples/with-ethers-and-passkeys/src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Document, { Html, Head, Main, NextScript } from "next/document";

class EthersPasskeysDemo extends Document {
render() {
return (
<Html>
<Head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

export default EthersPasskeysDemo;
96 changes: 96 additions & 0 deletions examples/with-ethers-and-passkeys/src/pages/api/createKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { TSignedRequest, TurnkeyClient } from "@turnkey/http";
import axios from "axios";
import { TActivityResponse } from "@turnkey/http/dist/shared";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";

type TResponse = {
message: string;
address?: string;
privateKeyId?: string;
};

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}

export default async function createKey(
req: NextApiRequest,
res: NextApiResponse<TResponse>
) {
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}`,
});
}

let response = activityResponse.data as TActivityResponse;
let attempts = 0;

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,
});

await sleep(500);

attempts++;
} else {
const privateKeys =
response.activity.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,
});
return;
}
}

res.status(500).json({
message: "failed to create key",
});
} catch (e) {
console.error(e);

res.status(500).json({
message: `Something went wrong, caught error: ${e}`,
});
}
}
Loading

0 comments on commit 11331f9

Please sign in to comment.