Skip to content

Commit

Permalink
feedback: update viem
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed Sep 12, 2024
1 parent 405671d commit 854c180
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 51 deletions.
6 changes: 5 additions & 1 deletion examples/with-viem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
"node": ">=18.0.0"
},
"dependencies": {
"@turnkey/http": "workspace:*",
"@turnkey/api-key-stamper": "workspace:*",
"@turnkey/http": "workspace:*",
"@turnkey/sdk-server": "workspace:*",
"@turnkey/viem": "workspace:*",
"dotenv": "^16.0.3",
"fetch": "^1.1.0",
"prompts": "^2.4.2",
"typescript": "5.1",
"viem": "^1.16.6"
},
"devDependencies": {
"@types/prompts": "^2.4.2"
}
}
155 changes: 133 additions & 22 deletions examples/with-viem/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import * as path from "path";
import * as dotenv from "dotenv";

import { createAccount } from "@turnkey/viem";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import prompts, { PromptType } from "prompts";
import {
createWalletClient,
http,
recoverMessageAddress,
type Account,
} from "viem";
import { sepolia } from "viem/chains";

import { TERMINAL_ACTIVITY_STATUSES } from "@turnkey/http";
import {
createAccount,
getSignatureFromActivity,
getSignedTransactionFromActivity,
isTurnkeyActivityConsensusNeededError,
serializeSignature,
} from "@turnkey/viem";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import { print, assertEqual } from "./util";
import { createNewWallet } from "./createNewWallet";

Expand All @@ -28,6 +36,16 @@ async function main() {
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
// The following config is useful in contexts where an activity requires consensus.
// By default, if the activity is not initially successful, it will poll a maximum
// of 3 times with an interval of 10000 milliseconds.
//
// -----
//
// activityPoller: {
// intervalMs: 10_000,
// numRetries: 5,
// },
});

const turnkeyAccount = await createAccount({
Expand All @@ -44,34 +62,127 @@ async function main() {
),
});

// This demo sends ETH back to our faucet (we keep a bunch of Sepolia ETH at this address)
const turnkeyFaucet = "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7";

// 1. Simple send tx
const transactionRequest = {
to: turnkeyFaucet as `0x${string}`,
value: 1000000000000000n,
};

const txHash = await client.sendTransaction(transactionRequest);
// 1. Sign a simple message
const { message } = await prompts([
{
type: "text" as PromptType,
name: "message",
message: "Message to sign",
initial: "Hello Turnkey",
},
]);
const address = client.account.address;

print("Source address", client.account.address);
print("Transaction sent", `https://sepolia.etherscan.io/tx/${txHash}`);
let signature;
try {
signature = await client.signMessage({
message,
});
} catch (error: any) {
signature = await handleActivityError(error).then(
async (activityId: string) => {
const rawSignature = await getSignatureFromActivity(
turnkeyClient.apiClient(),
process.env.ORGANIZATION_ID!,
activityId
);
return serializeSignature(rawSignature);
}
);
}

// 2. Sign a simple message
let address = client.account.address;
let message = "Hello Turnkey";
let signature = await client.signMessage({
message,
});
let recoveredAddress = await recoverMessageAddress({
const recoveredAddress = await recoverMessageAddress({
message,
signature,
});

print("Turnkey-powered signature:", `${signature}`);
print("Recovered address:", `${recoveredAddress}`);
assertEqual(address, recoveredAddress);

const { amount, destination } = await prompts([
{
type: "number" as PromptType,
name: "amount",
message: "Amount to send (wei). Default to 0.0000001 ETH",
initial: 100000000000,
},
{
type: "text" as PromptType,
name: "destination",
message: "Destination address (default to TKHQ warchest)",
initial: "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7",
},
]);

// 2. Simple send tx
const transactionRequest = {
to: destination as `0x${string}`,
value: amount,
};

let txHash;
try {
txHash = await client.sendTransaction(transactionRequest);
} catch (error: any) {
txHash = await handleActivityError(error).then(
async (activityId: string) => {
console.log({ activityId });
const signedTx = await getSignedTransactionFromActivity(
turnkeyClient.apiClient(),
process.env.ORGANIZATION_ID!,
activityId
);
return await client.sendRawTransaction({
serializedTransaction: signedTx,
});
}
);
}

print("Source address", client.account.address);
print("Transaction sent", `https://sepolia.etherscan.io/tx/${txHash}`);

async function handleActivityError(error: any) {
if (isTurnkeyActivityConsensusNeededError(error)) {
// Turnkey-specific error details may be wrapped by higher level errors
const activityId = error["activityId"] || error["cause"]["activityId"];
let activityStatus =
error["activityStatus"] || error["cause"]["activityId"];

while (!TERMINAL_ACTIVITY_STATUSES.includes(activityStatus)) {
console.log("\nWaiting for consensus...\n");

const { retry } = await prompts([
{
type: "text" as PromptType,
name: "retry",
message: "Consensus reached? y/n",
initial: "y",
},
]);

if (retry === "n") {
continue;
}

// Refresh activity status
activityStatus = (
await turnkeyClient.apiClient().getActivity({
activityId,
organizationId: process.env.ORGANIZATION_ID!,
})
).activity.status;
}

console.log("\nConsensus reached! Moving on...\n");

return activityId;
}

// Rethrow error
throw error;
}
}

main().catch((error) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/with-viem/src/retry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as path from "path";
import * as dotenv from "dotenv";

import { TERMINAL_ACTIVITY_STATUSES } from "@turnkey/http";
import {
TurnkeyConsensusNeededError,
createAccount,
getSignedTransactionFromActivity,
} from "@turnkey/viem";
import {
Turnkey as TurnkeyServerSDK,
TERMINAL_ACTIVITY_STATUSES,
} from "@turnkey/sdk-server";
import { createWalletClient, http, type Account } from "viem";
import { sepolia } from "viem/chains";
Expand Down
76 changes: 49 additions & 27 deletions packages/viem/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
checkActivityStatus,
TActivityId,
TurnkeyActivityError as TurnkeyHttpActivityError,
TurnkeyActivityConsensusNeededError as TurnkeyHttpActivityConsensusNeededError,
TurnkeyClient,
} from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
Expand All @@ -31,17 +32,17 @@ export type TTurnkeyConsensusNeededErrorType = TurnkeyConsensusNeededError & {
export class TurnkeyConsensusNeededError extends BaseError {
override name = "TurnkeyConsensusNeededError";

activityId: TActivityId;
activityStatus: TActivityStatus;
activityId: TActivityId | undefined;
activityStatus: TActivityStatus | undefined;

constructor({
message = "Turnkey activity requires consensus.",
activityId,
activityStatus,
}: {
message?: string;
activityId: TActivityId;
activityStatus: TActivityStatus;
message?: string | undefined;
activityId: TActivityId | undefined;
activityStatus: TActivityStatus | undefined;
}) {
super(message);
this.activityId = activityId;
Expand All @@ -64,9 +65,9 @@ export class TurnkeyActivityError extends BaseError {
activityId,
activityStatus,
}: {
message?: string;
activityId?: TActivityId;
activityStatus?: TActivityStatus;
message?: string | undefined;
activityId?: TActivityId | undefined;
activityStatus?: TActivityStatus | undefined;
}) {
super(message);
this.activityId = activityId;
Expand Down Expand Up @@ -399,11 +400,21 @@ async function signTransactionWithErrorWrapping(
signWith
);
} catch (error: any) {
if (
isTurnkeyActivityConsensusNeededError(error) ||
isTurnkeyActivityError(error)
) {
throw error;
// Wrap Turnkey error in Viem-specific error
if (error instanceof TurnkeyHttpActivityError) {
throw new TurnkeyActivityError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}

if (error instanceof TurnkeyHttpActivityConsensusNeededError) {
throw new TurnkeyConsensusNeededError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}

throw new TurnkeyActivityError({
Expand Down Expand Up @@ -471,11 +482,21 @@ async function signMessageWithErrorWrapping(
signWith
);
} catch (error: any) {
if (
isTurnkeyActivityConsensusNeededError(error) ||
isTurnkeyActivityError(error)
) {
throw error;
// Wrap Turnkey error in Viem-specific error
if (error instanceof TurnkeyHttpActivityError) {
throw new TurnkeyActivityError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}

if (error instanceof TurnkeyHttpActivityConsensusNeededError) {
throw new TurnkeyConsensusNeededError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}

throw new TurnkeyActivityError({
Expand Down Expand Up @@ -533,17 +554,18 @@ async function signMessageImpl(
};
}

const assembled = signatureToHex({
r: `0x${result!.r}`,
s: `0x${result!.s}`,
v: result!.v === "00" ? 27n : 28n,
});
return assertNonNull(serializeSignature(result));
}

// Assemble the hex
return assertNonNull(assembled);
export function serializeSignature(sig: TSignature) {
return signatureToHex({
r: `0x${sig.r}`,
s: `0x${sig.s}`,
v: sig.v === "00" ? 27n : 28n,
});
}

function isTurnkeyActivityConsensusNeededError(error: any) {
export function isTurnkeyActivityConsensusNeededError(error: any) {
return (
typeof error.walk === "function" &&
error.walk((e: any) => {
Expand All @@ -552,7 +574,7 @@ function isTurnkeyActivityConsensusNeededError(error: any) {
);
}

function isTurnkeyActivityError(error: any) {
export function isTurnkeyActivityError(error: any) {
return (
typeof error.walk === "function" &&
error.walk((e: any) => {
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 854c180

Please sign in to comment.