Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taylor/wallet stamper client #409

Merged
merged 19 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/swift-ads-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@turnkey/wallet-stamper": major
"@turnkey/sdk-browser": minor
"@turnkey/sdk-react": minor
"@turnkey/crypto": minor
---

`@turnkey/wallet-stamper`

- Renamed `recoverPublicKey` to `getPublicKey` and standardized its signature, improving clarity and simplifying the process of retrieving public keys across wallet interfaces.
- Added [`EthereumWallet`](https://github.com/tkhq/sdk/blob/830c3bc68a8a14ef21d1398c1f939994b90dd08d/packages/wallet-stamper/src/ethereum.ts) interface to simplify support for Ethereum wallets

`@turnkey/sdk-browser`

- Added `TurnkeyWalletClient` with a new `getPublicKey` method, supporting easier public key access and integration with `WalletStamper`.
- Added `UserSession` interface and `authClient` to track which client was used to authenticate a session

`@turnkey/crypto`

- Added `toDerSignature` function used to convert a raw ECDSA signature into DER-encoded format for compatibility with our backend, which requires DER signatures

`@turnkey/sdk-react`

- The `useTurnkey` hook now returns the new `walletClient`, used for authenticating requests via wallet signatures
16 changes: 5 additions & 11 deletions examples/with-wallet-stamper/src/components/turnkey-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import React, { createContext, useContext, useState, useEffect } from "react";
import { createActivityPoller, type TurnkeyClient } from "@turnkey/http";

import {
SolanaWalletInterface,
TStamper,
WalletInterface,
WalletStamper,
EvmWalletInterface,
} from "@turnkey/wallet-stamper";
import { createWebauthnStamper, Email } from "@/lib/turnkey";
import { createUserSubOrg, getSubOrgByPublicKey } from "@/lib/server";
import { ChainType } from "@/lib/types";
import { useWallet } from "@solana/wallet-adapter-react";

import { useRouter } from "next/navigation";
import { ACCOUNT_CONFIG_SOLANA } from "@/lib/constants";
Expand Down Expand Up @@ -101,13 +98,7 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
chainType: ChainType = ChainType.SOLANA
) {
setAuthenticating(true);
let publicKey = null;
if (chainType === ChainType.SOLANA) {
const solanaWallet = wallet as SolanaWalletInterface;
publicKey = solanaWallet.recoverPublicKey();
} else if (chainType === ChainType.EVM) {
const evmWallet = wallet as EvmWalletInterface;
}
const publicKey = await wallet?.getPublicKey();

const res = await createUserSubOrg({
email,
Expand Down Expand Up @@ -139,7 +130,10 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
}

async function signInWithWallet(): Promise<User | null> {
const publicKey = (wallet as SolanaWalletInterface)?.recoverPublicKey();
const publicKey = await wallet?.getPublicKey();
if (!publicKey) {
return null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort of a meta question but do you prefer using nulls or undefineds?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I'll use null to distinguish between uninitialized variables and values that have been intentionally set to an empty value

}
const { organizationIds } = await getSubOrgByPublicKey(publicKey);
const organizationId = organizationIds[0];

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"fast-xml-parser": ">=4.4.1",
"semver": ">=6.3.1",
"path-to-regexp@<0.1.10": ">=0.1.10",
"secp256k1": ">=4.0.4"
"secp256k1": ">=4.0.4",
"cross-spawn": ">=7.0.5"
}
}
}
62 changes: 62 additions & 0 deletions packages/crypto/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,3 +568,65 @@ export const fromDerSignature = (derSignature: string) => {
// Concatenate and return the raw signature
return new Uint8Array([...rPadded, ...sPadded]);
};

/**
* Converts a raw ECDSA signature to DER-encoded format.
*
* This function takes a raw ECDSA signature, which is a concatenation of two 32-byte integers (r and s),
* and converts it into the DER-encoded format. DER (Distinguished Encoding Rules) is a binary encoding
* for data structures described by ASN.1.
*
* @param {string} rawSignature - The raw signature in hexadecimal string format.
* @returns {string} - The DER-encoded signature in hexadecimal string format.
*
* @throws {Error} - Throws an error if the input signature is invalid or if the encoding process fails.
*
* @example
* // Example usage:
* const rawSignature = "0x487cdb8a88f2f4044b701cbb116075c4cabe5fe4657a6358b395c0aab70694db3453a8057e442bd1aff0ecabe8a82c831f0edd7f2158b7c1feb3de9b1f20309b1c";
* const derSignature = toDerSignature(rawSignature);
* console.log(derSignature); // Outputs the DER-encoded signature as a hex string
* // "30440220487cdb8a88f2f4044b701cbb116075c4cabe5fe4657a6358b395c0aab70694db02203453a8057e442bd1aff0ecabe8a82c831f0edd7f2158b7c1feb3de9b1f20309b"
*/
export const toDerSignature = (rawSignature: string) => {
const rawSignatureBuf = uint8ArrayFromHexString(rawSignature);

// Split raw signature into r and s, each 32 bytes
const r = rawSignatureBuf.slice(0, 32);
const s = rawSignatureBuf.slice(32, 64);

// Helper function to encode an integer with DER structure
const encodeDerInteger = (integer?: Uint8Array): Uint8Array => {
// Check if integer is defined and has at least one byte
if (
integer === undefined ||
integer.length === 0 ||
integer[0] === undefined
) {
throw new Error("Invalid integer: input is undefined or empty.");
}

// Add a leading zero if the integer's most significant byte is >= 0x80
const needsPadding = integer[0] & 0x80;
const paddedInteger = needsPadding
? new Uint8Array([0x00, ...integer])
: integer;

// Prepend the integer tag (0x02) and length
return new Uint8Array([0x02, paddedInteger.length, ...paddedInteger]);
};

// DER encode r and s
const rEncoded = encodeDerInteger(r);
const sEncoded = encodeDerInteger(s);

// Combine as a DER sequence: 0x30, total length, rEncoded, sEncoded
const derSignature = new Uint8Array([
0x30,
rEncoded.length + sEncoded.length,
...rEncoded,
...sEncoded,
]);

return uint8ArrayToHexString(derSignature);
};
96 changes: 82 additions & 14 deletions packages/sdk-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,95 @@ Turnkey API documentation lives here: https://docs.turnkey.com.
$ npm install @turnkey/sdk-browser
```

### Initialize

```typescript
import { Turnkey } from "@turnkey/sdk-browser";

const turnkey = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,
// Optional: Your relying party ID - for use with Passkey authentication
rpId: process.env.TURNKEY_RP_ID,
});
```

### Turnkey Clients

#### Passkey

The Passkey client allows for authentication to Turnkey's API using Passkeys.

```typescript
const passkeyClient = turnkey.passkeyClient();

// User will be prompted to login with their passkey
await passkeyClient.login();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to describe what exactly login is doing? or perhaps this can wait for Omkar's work on documenting all these methods

cc @omkarshanbhag as some of these methods will be getting tweaked


// Make authenticated requests to Turnkey API, such as listing user's wallets
const walletsResponse = await passkeyClient.getWallets();
```

#### Iframe

The Iframe client can be initialized to interact with Turnkey's hosted iframes for sensitive operations.
The `iframeContainer` parameter is required, and should be a reference to the DOM element that will host the iframe.
The `iframeUrl` is the URL of the iframe you wish to interact with.

The example below demonstrates how to initialize the Iframe client for use with [Email Auth](https://docs.turnkey.com/embedded-wallets/sub-organization-auth)
by passing in `https://auth.turnkey.com` as the `iframeUrl`.

```typescript
const iframeClient = await turnkey.iframeClient({
// The container element that will host the iframe
iframeContainer: document.getElementById("<iframe container id>"),
iframeUrl: "https://auth.turnkey.com",
});

const injectedResponse = await iframeClient.injectCredentialBundle(
"<Credential from Email>"
);
if (injectedResponse) {
await iframeClient.getWallets();
}
```

##### IFrame URLs:

| Flow | URL |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [Email Auth](https://docs.turnkey.com/embedded-wallets/sub-organization-auth) | [auth.turnkey.com](https://auth.turnkey.com) |
| [Email Recovery](https://docs.turnkey.com/embedded-wallets/sub-organization-recovery) | [recovery.turnkey.com](https://recovery.turnkey.com) |
| [Import Wallet](https://docs.turnkey.com/features/import-wallets) | [import.turnkey.com](https://import.turnkey.com) |
| [Export Wallet](https://docs.turnkey.com/features/export-wallets) | [export.turnkey.com](https://export.turnkey.com) |

#### Wallet

The Wallet client is designed for using your Solana or EVM wallet to stamp and approve activity requests for Turnkey's API.
This stamping process leverages the wallet's signature to authenticate requests.

The example below showcases how to use an injected Ethereum wallet to stamp requests to Turnkey's API.
The user will be prompted to sign a message containing the activity request payload to be sent to Turnkey.

```typescript
import {
TurnkeyBrowserSDK,
TurnkeySDKBrowserConfig,
TurnkeySDKBrowserClient,
} from "@turnkey/sdk-browser";
createWalletClient,
custom,
recoverPublicKey,
hashMessage,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

// This config contains parameters including base URLs, iframe URLs, org ID, and rp ID (relying party ID for WebAuthn)
import turnkeyConfig from "./turnkey.json";
import { WalletStamper, EthereumWallet } from "@turnkey/wallet-stamper";

// Use the config to instantiate a Turnkey Client
const turnkeyClient = new TurnkeyBrowserSDK(turnkeyConfig);
const walletClient = turnkey.walletClient(new EthereumWallet());

// Now you can make authenticated requests!
const response = await turnkeyClient?.passkeySign.login();
// Make authenticated requests to Turnkey API, such as listing user's wallets
// User will be prompted to sign a message to authenticate the request
const walletsResponse = await walletClient.getWallets();
```

## Helpers

`@turnkey/sdk-browser` provides `TurnkeySDKBrowserClient`, which offers wrappers around commonly used Turnkey activities, such as creating new wallets and wallet accounts.

// TODO:
// - explain subtypes within sdk-client.ts
// - point to demo wallet
1 change: 1 addition & 0 deletions packages/sdk-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@turnkey/encoding": "workspace:*",
"@turnkey/http": "workspace:*",
"@turnkey/iframe-stamper": "workspace:*",
"@turnkey/wallet-stamper": "workspace:*",
"@turnkey/webauthn-stamper": "workspace:*",
"bs58check": "^3.0.1",
"buffer": "^6.0.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-browser/scripts/codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ export class TurnkeySDKClientBase {
codeBuffer.push(
`\n\t${methodName} = async (input: SdkApiTypes.${inputType}): Promise<SdkApiTypes.${responseType}> => {
const { organizationId, timestampMs, ...rest } = input;
const currentUser = await getStorageValue(StorageKeys.CurrentUser);
const currentUser = await getStorageValue(StorageKeys.UserSession);
return this.command("${endpointPath}", {
parameters: rest,
organizationId: organizationId ?? (currentUser?.organization?.organizationId ?? this.config.organizationId),
Expand All @@ -438,7 +438,7 @@ export class TurnkeySDKClientBase {
codeBuffer.push(
`\n\t${methodName} = async (input: SdkApiTypes.${inputType}): Promise<SdkApiTypes.${responseType}> => {
const { organizationId, timestampMs, ...rest } = input;
const currentUser = await getStorageValue(StorageKeys.CurrentUser);
const currentUser = await getStorageValue(StorageKeys.UserSession);
return this.activityDecision("${endpointPath}",
{
parameters: rest,
Expand Down
Loading
Loading