From 845e292f1599a71eadb13a3fbc705eac376016a5 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Fri, 24 Nov 2023 13:23:44 -0600 Subject: [PATCH 1/8] Update federated-passkeys example to use wallets and wallet accounts --- .../with-federated-passkeys/src/app/types.ts | 19 ++ .../with-federated-passkeys/src/app/util.ts | 17 ++ .../src/pages/api/createSubOrg.ts | 32 ++-- .../src/pages/api/getPrivateKeys.ts | 72 -------- .../src/pages/api/getWallet.ts | 59 ++++++ .../src/pages/index.module.css | 10 +- .../src/pages/index.tsx | 170 ++++++++---------- 7 files changed, 194 insertions(+), 185 deletions(-) create mode 100644 examples/with-federated-passkeys/src/app/types.ts delete mode 100644 examples/with-federated-passkeys/src/pages/api/getPrivateKeys.ts create mode 100644 examples/with-federated-passkeys/src/pages/api/getWallet.ts diff --git a/examples/with-federated-passkeys/src/app/types.ts b/examples/with-federated-passkeys/src/app/types.ts new file mode 100644 index 000000000..05f3638db --- /dev/null +++ b/examples/with-federated-passkeys/src/app/types.ts @@ -0,0 +1,19 @@ +export type CreateSubOrgResponse = { + subOrgId: string; + wallet: TFormattedWallet; +}; + +export type GetWalletRequest = { + organizationId: string; +}; + +export type TFormattedWallet = { + id: string; + name: string; + accounts: TFormattedWalletAccount[]; +}; + +export type TFormattedWalletAccount = { + address: string; + path: string; +}; diff --git a/examples/with-federated-passkeys/src/app/util.ts b/examples/with-federated-passkeys/src/app/util.ts index d26b4fd42..f21c06e36 100644 --- a/examples/with-federated-passkeys/src/app/util.ts +++ b/examples/with-federated-passkeys/src/app/util.ts @@ -1,3 +1,5 @@ +import { TFormattedWallet } from "./types"; + export function refineNonNull( input: T | null | undefined, errorMessage?: string @@ -8,3 +10,18 @@ export function refineNonNull( return input; } + +/** + * This function returns the next available BIP 32 path for the wallet + * For example: a wallet with the last address at "m/44'/60'/0'/0/13" will yield "m/44'/60'/0'/0/14" + * @param wallet + */ +export function getNextPath(wallet: TFormattedWallet): string { + const lastAccount = wallet.accounts[wallet.accounts.length - 1]; + const lastAccountNum = parseInt(lastAccount.path.split("/")[5]); + return lastAccount.path + .split("/") + .slice(0, 5) + .concat((lastAccountNum + 1).toString()) + .join("/"); +} diff --git a/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts b/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts index a4098cdab..0ca8ed941 100644 --- a/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts +++ b/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts @@ -5,6 +5,7 @@ import { createActivityPoller, } from "@turnkey/http"; import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { CreateSubOrgResponse, TFormattedWallet } from "@/app/types"; type TAttestation = TurnkeyApiTypes["v1Attestation"]; @@ -14,12 +15,6 @@ type CreateSubOrgRequest = { attestation: TAttestation; }; -type CreateSubOrgResponse = { - subOrgId: string; - privateKeyId: string; - privateKeyAddress: string; -}; - type ErrorMessage = { message: string; }; @@ -81,20 +76,25 @@ export default async function createUser( }); const subOrgId = refineNonNull( - completedActivity.result.createSubOrganizationResultV3?.subOrganizationId + completedActivity.result.createSubOrganizationResultV4?.subOrganizationId ); - const privateKeys = refineNonNull( - completedActivity.result.createSubOrganizationResultV3?.privateKeys - ); - const privateKeyId = refineNonNull(privateKeys?.[0]?.privateKeyId); - const privateKeyAddress = refineNonNull( - privateKeys?.[0]?.addresses?.[0]?.address + const wallet = refineNonNull( + completedActivity.result.createSubOrganizationResultV4?.wallet ); + const walletAddress = wallet.addresses?.[0]; res.status(200).json({ - subOrgId, - privateKeyId, - privateKeyAddress, + subOrgId: subOrgId, + wallet: { + id: wallet.walletId, + name: walletName, + accounts: [ + { + address: walletAddress, + path: "m/44'/60'/0'/0/0", + }, + ], + }, }); } catch (e) { console.error(e); diff --git a/examples/with-federated-passkeys/src/pages/api/getPrivateKeys.ts b/examples/with-federated-passkeys/src/pages/api/getPrivateKeys.ts deleted file mode 100644 index 74c37f954..000000000 --- a/examples/with-federated-passkeys/src/pages/api/getPrivateKeys.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { TurnkeyApiTypes, TurnkeyClient } from "@turnkey/http"; -import { ApiKeyStamper } from "@turnkey/api-key-stamper"; - -type TPrivateKey = TurnkeyApiTypes["v1PrivateKey"]; - -type TFormattedPrivateKey = { - privateKeyId: string; - privateKeyName: string; - privateKeyAddress: string; -}; - -type GetPrivateKeysRequest = { - organizationId: string; -}; - -type GetPrivateKeysResponse = { - privateKeys: TFormattedPrivateKey[]; -}; - -type ErrorMessage = { - message: string; -}; - -// This getter _can_ be performed by the parent org -export default async function getPrivateKeys( - req: NextApiRequest, - res: NextApiResponse -) { - const getPrivateKeysRequest = req.body as GetPrivateKeysRequest; - - const turnkeyClient = new TurnkeyClient( - { baseUrl: process.env.NEXT_PUBLIC_BASE_URL! }, - new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }) - ); - - const organizationId = getPrivateKeysRequest.organizationId; - - try { - const privateKeysResponse = await turnkeyClient.getPrivateKeys({ - organizationId, - }); - - // By default, the API will return private keys in descending order of create date. - // We reverse it here, so that visually the most recently created private keys will be - // added to the bottom of the list instead of the top. - const privateKeys = privateKeysResponse.privateKeys - .reverse() - .map((pk: TPrivateKey) => { - return { - privateKeyId: pk.privateKeyId!, - privateKeyName: pk.privateKeyName!, - privateKeyAddress: pk.addresses[0].address!, - }; - }); - - res.status(200).json({ - privateKeys, - }); - } catch (e) { - console.error(e); - - res.status(500).json({ - message: "Something went wrong.", - }); - } - - res.json; -} diff --git a/examples/with-federated-passkeys/src/pages/api/getWallet.ts b/examples/with-federated-passkeys/src/pages/api/getWallet.ts new file mode 100644 index 000000000..5ea7e20bf --- /dev/null +++ b/examples/with-federated-passkeys/src/pages/api/getWallet.ts @@ -0,0 +1,59 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { TurnkeyApiTypes, TurnkeyClient } from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { GetWalletRequest, TFormattedWallet } from "@/app/types"; + +type TWalletAccount = TurnkeyApiTypes["v1WalletAccount"]; + +type ErrorMessage = { + message: string; +}; + +// This can be performed by the parent org since parent orgs have read-only access to all their sub-orgs +export default async function getWallet( + req: NextApiRequest, + res: NextApiResponse +) { + const getWalletRequest = req.body as GetWalletRequest; + + const turnkeyClient = new TurnkeyClient( + { baseUrl: process.env.NEXT_PUBLIC_BASE_URL! }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + + const organizationId = getWalletRequest.organizationId; + + try { + const walletsResponse = await turnkeyClient.getWallets({ + organizationId, + }); + const accountsResponse = await turnkeyClient.getWalletAccounts({ + organizationId: organizationId, + walletId: walletsResponse.wallets[0].walletId, + }); + + const accounts = accountsResponse.accounts.map((acc: TWalletAccount) => { + return { + address: acc.address, + path: acc.path, + }; + }); + + res.status(200).json({ + id: walletsResponse.wallets[0].walletId, + name: walletsResponse.wallets[0].walletName, + accounts: accounts, + }); + } catch (e) { + console.error(e); + + res.status(500).json({ + message: "Something went wrong.", + }); + } + + res.json; +} diff --git a/examples/with-federated-passkeys/src/pages/index.module.css b/examples/with-federated-passkeys/src/pages/index.module.css index 50896cd13..cd99757c2 100644 --- a/examples/with-federated-passkeys/src/pages/index.module.css +++ b/examples/with-federated-passkeys/src/pages/index.module.css @@ -42,6 +42,7 @@ .prompt { font-family: "Inter"; + text-align: center; } .base { @@ -88,10 +89,13 @@ } .td { - border-spacing: 1px; + /* From https://qwtel.com/posts/software/the-monospaced-system-ui-css-font-stack/ */ + font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + padding: 10px; } .th { - padding-right: 5vw; - border-spacing: 1px; + border-bottom: 1px solid #4d4d4d; } diff --git a/examples/with-federated-passkeys/src/pages/index.tsx b/examples/with-federated-passkeys/src/pages/index.tsx index 781fff40b..760504113 100644 --- a/examples/with-federated-passkeys/src/pages/index.tsx +++ b/examples/with-federated-passkeys/src/pages/index.tsx @@ -5,19 +5,21 @@ import { WebauthnStamper } from "@turnkey/webauthn-stamper"; import { useForm } from "react-hook-form"; import axios from "axios"; import * as React from "react"; +import { CreateSubOrgResponse, TFormattedWallet } from "@/app/types"; +import { getNextPath } from "@/app/util"; type subOrgFormData = { subOrgName: string; }; -type privateKeyFormData = { - privateKeyName: string; +type walletAccountFormData = { + path: string; }; -type privateKeyResult = { - privateKeyId: string; - privateKeyName: string; - privateKeyAddress: string; +type walletResult = { + walletId: string; + walletName: string; + accounts: string; }; // All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms @@ -54,18 +56,17 @@ function sleep(ms: number): Promise { export default function Home() { const [subOrgId, setSubOrgId] = React.useState(null); - const [privateKeys, setPrivateKeys] = React.useState([]); + const [wallet, setWallet] = React.useState(null); const { register: subOrgFormRegister, handleSubmit: subOrgFormSubmit } = useForm(); const { - register: privateKeyFormRegister, - handleSubmit: privateKeyFormSubmit, - } = useForm(); + register: createWalletAccountFormRegister, + handleSubmit: createWalletAccountFormSubmit, + } = useForm(); - const getPrivateKeys = async (organizationId: string) => { - const res = await axios.post("/api/getPrivateKeys", { organizationId }); - - setPrivateKeys(res.data.privateKeys); + const getWallet = async (organizationId: string) => { + const res = await axios.post("/api/getWallet", { organizationId }); + setWallet(res.data); }; const { register: _loginFormRegister, handleSubmit: loginFormSubmit } = @@ -74,26 +75,30 @@ export default function Home() { const turnkeyClient = new TurnkeyClient( { baseUrl: process.env.NEXT_PUBLIC_BASE_URL! }, new WebauthnStamper({ - rpId: "localhost", + rpId: "372b-68-203-12-187.ngrok-free.app", }) ); - const createPrivateKey = async (data: privateKeyFormData) => { - if (!subOrgId) { + const createWalletAccount = async (data: walletAccountFormData) => { + if (subOrgId === null) { throw new Error("sub-org id not found"); } + if (wallet === null) { + throw new Error("wallet not found"); + } - const signedRequest = await turnkeyClient.stampCreatePrivateKeys({ - type: "ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2", + const signedRequest = await turnkeyClient.stampCreateWalletAccounts({ + type: "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS", organizationId: subOrgId, timestampMs: String(Date.now()), parameters: { - privateKeys: [ + walletId: wallet.id, + accounts: [ { - privateKeyName: data.privateKeyName, + path: data.path, + pathFormat: "PATH_FORMAT_BIP32", curve: "CURVE_SECP256K1", - addressFormats: ["ADDRESS_FORMAT_ETHEREUM"], - privateKeyTags: [], + addressFormat: "ADDRESS_FORMAT_ETHEREUM", }, ], }, @@ -101,8 +106,8 @@ export default function Home() { await axios.post("/api/proxyRequest", signedRequest); await sleep(1000); // alternative would be to poll the activity itself repeatedly - await getPrivateKeys(subOrgId); - alert(`Hooray! Key "${data.privateKeyName}" created.`); + await getWallet(subOrgId); + alert(`Hooray! New address at path "${data.path}" created.`); }; const createSubOrg = async (data: subOrgFormData) => { @@ -113,13 +118,8 @@ export default function Home() { // https://www.w3.org/TR/webauthn-2/#sctn-sample-registration const attestation = await getWebAuthnAttestation({ publicKey: { - authenticatorSelection: { - residentKey: "preferred", - requireResidentKey: false, - userVerification: "preferred", - }, rp: { - id: "localhost", + id: "372b-68-203-12-187.ngrok-free.app", name: "Turnkey Federated Passkey Demo", }, challenge, @@ -147,30 +147,25 @@ export default function Home() { challenge: base64UrlEncode(challenge), }); - setSubOrgId(res.data.subOrgId); - setPrivateKeys([ - ...privateKeys, - { - privateKeyId: res.data.privateKeyId, - privateKeyName: res.data.privateKeyName, - privateKeyAddress: res.data.privateKeyAddress, - }, - ]); + const subOrgResponse = res.data as CreateSubOrgResponse; + + setSubOrgId(subOrgResponse.subOrgId); + setWallet(subOrgResponse.wallet); }; - const privateKeysTable = ( + const walletTable = (
- + - {privateKeys.map((val, key) => { + {wallet?.accounts.map((account, key) => { return ( - - + + ); })} @@ -179,12 +174,6 @@ export default function Home() { ); - const privateKeyElements = privateKeys.map((pk) => ( -
  • - {pk.privateKeyAddress} -
  • - )); - const login = async () => { // We use the parent org ID, which we know at all times, const res = await turnkeyClient.getWhoami({ @@ -194,8 +183,7 @@ export default function Home() { // 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); - - await getPrivateKeys(res.organizationId); + await getWallet(res.organizationId); }; return ( @@ -212,9 +200,7 @@ export default function Home() { {!subOrgId && (
    -

    - First, create your sub-organization: -

    +

    Create your sub-organization:

    )} - {subOrgId && privateKeys.length === 1 && ( -
    -

    - 🚀🥳🎉 Hooray! Here's your first private key address: -

    - {privateKeyElements} -
    - )} - {subOrgId && privateKeys.length > 1 && ( -
    -

    - 🚀🥳🎉 Hooray! Here are your private keys: -

    - {privateKeysTable} -
    - )} - {subOrgId && ( -
    -

    - 👀 Want more? Create another using your passkey{" "} -

    - -
    + )} ); From 60efd16759353e7c2420ef308e5645596c1a06ce Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 27 Nov 2023 17:56:34 -0600 Subject: [PATCH 2/8] Add instruction for mobile testing --- examples/with-federated-passkeys/.env.local.example | 3 ++- examples/with-federated-passkeys/README.md | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/with-federated-passkeys/.env.local.example b/examples/with-federated-passkeys/.env.local.example index 9a6d95474..56519191a 100644 --- a/examples/with-federated-passkeys/.env.local.example +++ b/examples/with-federated-passkeys/.env.local.example @@ -1,4 +1,5 @@ API_PUBLIC_KEY="" API_PRIVATE_KEY="" NEXT_PUBLIC_ORGANIZATION_ID="" -NEXT_PUBLIC_BASE_URL="https://api.turnkey.com" \ No newline at end of file +NEXT_PUBLIC_BASE_URL="https://api.turnkey.com" +NEXT_PUBLIC_RPID="localhost" diff --git a/examples/with-federated-passkeys/README.md b/examples/with-federated-passkeys/README.md index d90d81a1c..6926d2a16 100644 --- a/examples/with-federated-passkeys/README.md +++ b/examples/with-federated-passkeys/README.md @@ -44,3 +44,11 @@ $ pnpm run dev ``` This command will run a NextJS app on port 3000. If you navigate to http://localhost:3000 in your browser, you can follow the prompts to create a sub-organization and private key for the newly created sub-organization. + +### Testing passkey prompts on real mobile devices + +The easiest way to test this demo on mobile is through ngrok: +* Install by following the instruction here: https://dashboard.ngrok.com/get-started/setup +* Open a new tunnel to port 3000: `ngrok http 3000` +* Update `NEXT_PUBLIC_RPID` to the ngrok domain (e.g. `372b-68-203-12-187.ngrok-free.app`) +* Now visit the ngrok URL on your mobile device From c0ad01c5dea6a35e2ac07077b8e50fe3ccde7475 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 27 Nov 2023 17:56:52 -0600 Subject: [PATCH 3/8] Cleanup CSS to be mobile friendly --- .../src/app/globals.css | 107 ------------------ .../src/pages/index.module.css | 26 ++--- .../src/pages/index.tsx | 6 +- 3 files changed, 13 insertions(+), 126 deletions(-) delete mode 100644 examples/with-federated-passkeys/src/app/globals.css diff --git a/examples/with-federated-passkeys/src/app/globals.css b/examples/with-federated-passkeys/src/app/globals.css deleted file mode 100644 index f4bd77c0c..000000000 --- a/examples/with-federated-passkeys/src/app/globals.css +++ /dev/null @@ -1,107 +0,0 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/examples/with-federated-passkeys/src/pages/index.module.css b/examples/with-federated-passkeys/src/pages/index.module.css index cd99757c2..7dabef222 100644 --- a/examples/with-federated-passkeys/src/pages/index.module.css +++ b/examples/with-federated-passkeys/src/pages/index.module.css @@ -17,12 +17,15 @@ } .main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - gap: 60px; + max-width: 800px; + margin: auto; + font-family: "Inter"; +} + +.logo { + display: block; + margin: 24px auto; + width: 100px; } .input { @@ -76,16 +79,8 @@ gap: 10px; } -.baseTable { - font-family: "Inter"; - width: 75%; - display: flex; - justify-content: center; - align-items: center; -} - .table { - width: 100%; + word-break:break-all; } .td { @@ -98,4 +93,5 @@ .th { border-bottom: 1px solid #4d4d4d; + padding: 10px; } diff --git a/examples/with-federated-passkeys/src/pages/index.tsx b/examples/with-federated-passkeys/src/pages/index.tsx index 760504113..4e906c116 100644 --- a/examples/with-federated-passkeys/src/pages/index.tsx +++ b/examples/with-federated-passkeys/src/pages/index.tsx @@ -75,7 +75,7 @@ export default function Home() { const turnkeyClient = new TurnkeyClient( { baseUrl: process.env.NEXT_PUBLIC_BASE_URL! }, new WebauthnStamper({ - rpId: "372b-68-203-12-187.ngrok-free.app", + rpId: process.env.NEXT_PUBLIC_RPID!, }) ); @@ -154,7 +154,6 @@ export default function Home() { }; const walletTable = ( -
    Name AddressPath
    {val.privateKeyName}{val.privateKeyAddress}{account.address}{account.path}
    @@ -171,7 +170,6 @@ export default function Home() { })}
    -
    ); const login = async () => { @@ -188,7 +186,7 @@ export default function Home() { return (
    - + Turnkey Logo Date: Mon, 27 Nov 2023 17:59:33 -0600 Subject: [PATCH 4/8] Handle errors and alert when they happen --- .../src/pages/index.tsx | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/examples/with-federated-passkeys/src/pages/index.tsx b/examples/with-federated-passkeys/src/pages/index.tsx index 4e906c116..c5b439f8e 100644 --- a/examples/with-federated-passkeys/src/pages/index.tsx +++ b/examples/with-federated-passkeys/src/pages/index.tsx @@ -86,28 +86,34 @@ export default function Home() { if (wallet === null) { throw new Error("wallet not found"); } + + try { + const signedRequest = await turnkeyClient.stampCreateWalletAccounts({ + type: "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS", + organizationId: subOrgId, + timestampMs: String(Date.now()), + parameters: { + walletId: wallet.id, + accounts: [ + { + path: data.path, + pathFormat: "PATH_FORMAT_BIP32", + curve: "CURVE_SECP256K1", + addressFormat: "ADDRESS_FORMAT_ETHEREUM", + }, + ], + }, + }); - const signedRequest = await turnkeyClient.stampCreateWalletAccounts({ - type: "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS", - organizationId: subOrgId, - timestampMs: String(Date.now()), - parameters: { - walletId: wallet.id, - accounts: [ - { - path: data.path, - pathFormat: "PATH_FORMAT_BIP32", - curve: "CURVE_SECP256K1", - addressFormat: "ADDRESS_FORMAT_ETHEREUM", - }, - ], - }, - }); - - await axios.post("/api/proxyRequest", signedRequest); - await sleep(1000); // alternative would be to poll the activity itself repeatedly - await getWallet(subOrgId); - alert(`Hooray! New address at path "${data.path}" created.`); + await axios.post("/api/proxyRequest", signedRequest); + await sleep(1000); // alternative would be to poll the activity itself repeatedly + await getWallet(subOrgId); + alert(`Hooray! New address at path "${data.path}" created.`); + } catch (e: any) { + const message = `caught error: ${e.toString()}`; + console.error(message); + alert(message) + } }; const createSubOrg = async (data: subOrgFormData) => { From 7151ba23aec0be89c81e69bccf09433f240be41e Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 27 Nov 2023 18:05:03 -0600 Subject: [PATCH 5/8] Fix RPID to reference configs --- examples/with-federated-passkeys/src/pages/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/with-federated-passkeys/src/pages/index.tsx b/examples/with-federated-passkeys/src/pages/index.tsx index c5b439f8e..2ed6870d0 100644 --- a/examples/with-federated-passkeys/src/pages/index.tsx +++ b/examples/with-federated-passkeys/src/pages/index.tsx @@ -124,8 +124,13 @@ export default function Home() { // https://www.w3.org/TR/webauthn-2/#sctn-sample-registration const attestation = await getWebAuthnAttestation({ publicKey: { + authenticatorSelection: { + residentKey: "preferred", + requireResidentKey: false, + userVerification: "preferred", + }, rp: { - id: "372b-68-203-12-187.ngrok-free.app", + id: process.env.NEXT_PUBLIC_RPID!, name: "Turnkey Federated Passkey Demo", }, challenge, From 32779100a7cd146c17a8481c33106e647bd728e4 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 27 Nov 2023 18:05:43 -0600 Subject: [PATCH 6/8] prettier-all:write --- examples/with-federated-passkeys/README.md | 9 ++-- .../src/pages/index.module.css | 2 +- .../src/pages/index.tsx | 43 +++++++++++-------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/with-federated-passkeys/README.md b/examples/with-federated-passkeys/README.md index 6926d2a16..5e3aba7f7 100644 --- a/examples/with-federated-passkeys/README.md +++ b/examples/with-federated-passkeys/README.md @@ -48,7 +48,8 @@ This command will run a NextJS app on port 3000. If you navigate to http://local ### Testing passkey prompts on real mobile devices The easiest way to test this demo on mobile is through ngrok: -* Install by following the instruction here: https://dashboard.ngrok.com/get-started/setup -* Open a new tunnel to port 3000: `ngrok http 3000` -* Update `NEXT_PUBLIC_RPID` to the ngrok domain (e.g. `372b-68-203-12-187.ngrok-free.app`) -* Now visit the ngrok URL on your mobile device + +- Install by following the instruction here: https://dashboard.ngrok.com/get-started/setup +- Open a new tunnel to port 3000: `ngrok http 3000` +- Update `NEXT_PUBLIC_RPID` to the ngrok domain (e.g. `372b-68-203-12-187.ngrok-free.app`) +- Now visit the ngrok URL on your mobile device diff --git a/examples/with-federated-passkeys/src/pages/index.module.css b/examples/with-federated-passkeys/src/pages/index.module.css index 7dabef222..876b2f269 100644 --- a/examples/with-federated-passkeys/src/pages/index.module.css +++ b/examples/with-federated-passkeys/src/pages/index.module.css @@ -80,7 +80,7 @@ } .table { - word-break:break-all; + word-break: break-all; } .td { diff --git a/examples/with-federated-passkeys/src/pages/index.tsx b/examples/with-federated-passkeys/src/pages/index.tsx index 2ed6870d0..3eea49dd3 100644 --- a/examples/with-federated-passkeys/src/pages/index.tsx +++ b/examples/with-federated-passkeys/src/pages/index.tsx @@ -86,7 +86,7 @@ export default function Home() { if (wallet === null) { throw new Error("wallet not found"); } - + try { const signedRequest = await turnkeyClient.stampCreateWalletAccounts({ type: "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS", @@ -112,7 +112,7 @@ export default function Home() { } catch (e: any) { const message = `caught error: ${e.toString()}`; console.error(message); - alert(message) + alert(message); } }; @@ -165,22 +165,22 @@ export default function Home() { }; const walletTable = ( - - - - - - - {wallet?.accounts.map((account, key) => { - return ( - - - - - ); - })} - -
    AddressPath
    {account.address}{account.path}
    + + + + + + + {wallet?.accounts.map((account, key) => { + return ( + + + + + ); + })} + +
    AddressPath
    {account.address}{account.path}
    ); const login = async () => { @@ -197,7 +197,12 @@ export default function Home() { return (
    - + Turnkey Logo Date: Tue, 28 Nov 2023 08:54:35 -0600 Subject: [PATCH 7/8] Explain HD wallet default path --- .../src/pages/api/createSubOrg.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts b/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts index 0ca8ed941..45abd73cf 100644 --- a/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts +++ b/examples/with-federated-passkeys/src/pages/api/createSubOrg.ts @@ -19,6 +19,14 @@ type ErrorMessage = { message: string; }; +// Default path for the first Ethereum address in a new HD wallet. +// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form: +// m / purpose' / coin_type' / account' / change / address_index +// - Purpose is a constant set to 44' following the BIP43 recommendation. +// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md +// - Account, Change, and Address Index are set to 0 +const ETHEREUM_WALLET_DEFAULT_PATH = "m/44'/60'/0'/0/0"; + export default async function createUser( req: NextApiRequest, res: NextApiResponse @@ -67,7 +75,7 @@ export default async function createUser( { curve: "CURVE_SECP256K1", pathFormat: "PATH_FORMAT_BIP32", - path: "m/44'/60'/0'/0/0", + path: ETHEREUM_WALLET_DEFAULT_PATH, addressFormat: "ADDRESS_FORMAT_ETHEREUM", }, ], @@ -91,7 +99,7 @@ export default async function createUser( accounts: [ { address: walletAddress, - path: "m/44'/60'/0'/0/0", + path: ETHEREUM_WALLET_DEFAULT_PATH, }, ], }, From 9e648f1e556a1009abdd4aa5b8dcfe73bc20fdd3 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 28 Nov 2023 08:54:52 -0600 Subject: [PATCH 8/8] Mention NEXT_PUBLIC_RPID in README --- examples/with-federated-passkeys/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/with-federated-passkeys/README.md b/examples/with-federated-passkeys/README.md index 5e3aba7f7..dbc6fef87 100644 --- a/examples/with-federated-passkeys/README.md +++ b/examples/with-federated-passkeys/README.md @@ -36,6 +36,7 @@ Now open `.env.local` and add the missing environment variables: - `API_PRIVATE_KEY` - `NEXT_PUBLIC_ORGANIZATION_ID` - `NEXT_PUBLIC_BASE_URL` (the `NEXT_PUBLIC` prefix makes the env variable accessible to the frontend app) +- `NEXT_PUBLIC_RPID` should be `localhost` unless you're accessing this demo through your own domain ### 3/ Running the app