@turnkey/viem
Version:
Turnkey Helpers to work with Viem
357 lines (354 loc) • 14.8 kB
JavaScript
import { BaseError, isAddress, serializeTransaction, hashMessage, parseTransaction, serializeTypedData, hexToBigInt, hexToBytes } from 'viem';
import { toAccount } from 'viem/accounts';
import { secp256k1 } from '@noble/curves/secp256k1';
import { TurnkeyActivityError as TurnkeyActivityError$1, TurnkeyClient, TurnkeyActivityConsensusNeededError, isHttpClient, assertActivityCompleted, assertNonNull } from '@turnkey/http';
import { ApiKeyStamper } from '@turnkey/api-key-stamper';
/**
* Detects the transaction type from a serialized transaction payload.
* @param serializedTx - The hex-encoded transaction (without 0x prefix)
* @returns The transaction type to use with Turnkey API
*/
function detectTransactionType(serializedTx) {
// Check if the transaction starts with 0x76 (Tempo transaction)
if (serializedTx.startsWith("76")) {
return "TRANSACTION_TYPE_TEMPO";
}
// Default to Ethereum for all other transactions
return "TRANSACTION_TYPE_ETHEREUM";
}
class TurnkeyConsensusNeededError extends BaseError {
constructor({ message = "Turnkey activity requires consensus.", activityId, activityStatus, }) {
super(message);
this.name = "TurnkeyConsensusNeededError";
this.activityId = activityId;
this.activityStatus = activityStatus;
}
}
class TurnkeyActivityError extends BaseError {
constructor({ message = "Received unexpected Turnkey activity status.", activityId, activityStatus, }) {
super(message);
this.name = "TurnkeyActivityError";
this.activityId = activityId;
this.activityStatus = activityStatus;
}
}
function createAccountWithAddress(input) {
const { client, organizationId, signWith } = input;
let { ethereumAddress } = input;
if (!signWith) {
throw new TurnkeyActivityError$1({
message: `Missing signWith parameter`,
});
}
if (isAddress(signWith)) {
// override provided `ethereumAddress`
ethereumAddress = signWith;
}
else if (!ethereumAddress) {
throw new TurnkeyActivityError({
message: `Missing ethereumAddress parameter`,
});
}
return toAccount({
address: ethereumAddress,
sign: function ({ hash }) {
return signMessage(client, hash, organizationId, signWith);
},
signMessage: function ({ message, }) {
const hashedMessage = hashMessage(message);
return signMessage(client, hashedMessage, organizationId, signWith);
},
signTransaction: function (transaction, options) {
const serializer = options?.serializer ??
serializeTransaction;
return signTransaction(client, transaction, serializer, organizationId, signWith);
},
signTypedData: function (typedData) {
return signTypedData(client, typedData, organizationId, signWith);
},
signAuthorization: function (parameters) {
return signAuthorization(client, parameters, organizationId, signWith);
},
});
}
async function createAccount(input) {
const { client, organizationId, signWith } = input;
let { ethereumAddress } = input;
if (!signWith) {
throw new TurnkeyActivityError({
message: `Missing signWith parameter`,
});
}
if (isAddress(signWith)) {
// override provided `ethereumAddress`
ethereumAddress = signWith;
}
else if (!ethereumAddress) {
// we have a private key ID, but not an ethereumAddress
const data = await client.getPrivateKey({
privateKeyId: signWith,
organizationId: organizationId,
});
ethereumAddress = data.privateKey.addresses.find((item) => item.format === "ADDRESS_FORMAT_ETHEREUM")?.address;
if (typeof ethereumAddress !== "string" || !ethereumAddress) {
throw new TurnkeyActivityError({
message: `Unable to find Ethereum address for key ${signWith} under organization ${organizationId}`,
});
}
}
return createAccountWithAddress({
client,
organizationId,
signWith,
ethereumAddress,
});
}
/**
* Creates a new Custom Account backed by a Turnkey API key.
* @deprecated use {@link createAccount} instead.
*/
async function createApiKeyAccount(config) {
const { apiPublicKey, apiPrivateKey, baseUrl, organizationId, privateKeyId } = config;
const stamper = new ApiKeyStamper({
apiPublicKey: apiPublicKey,
apiPrivateKey: apiPrivateKey,
});
const client = new TurnkeyClient({
baseUrl: baseUrl,
}, stamper);
const data = await client.getPrivateKey({
privateKeyId: privateKeyId,
organizationId: organizationId,
});
const ethereumAddress = data.privateKey.addresses.find((item) => item.format === "ADDRESS_FORMAT_ETHEREUM")?.address;
if (typeof ethereumAddress !== "string" || !ethereumAddress) {
throw new TurnkeyActivityError$1({
message: `Unable to find Ethereum address for key ${privateKeyId} under organization ${organizationId}`,
});
}
return toAccount({
address: ethereumAddress,
sign: function ({ hash }) {
return signMessage(client, hash, organizationId, privateKeyId);
},
signMessage: function ({ message, }) {
const hashedMessage = hashMessage(message);
return signMessage(client, hashedMessage, organizationId, privateKeyId);
},
signTransaction: function (transaction, options) {
const serializer = options?.serializer ??
serializeTransaction;
return signTransaction(client, transaction, serializer, organizationId, privateKeyId);
},
signTypedData: function (typedData) {
return signTypedData(client, typedData, organizationId, privateKeyId);
},
signAuthorization: function (parameters) {
return signAuthorization(client, parameters, organizationId, privateKeyId);
},
});
}
async function signAuthorization(client, parameters, organizationId, signWith) {
const { chainId, nonce, to = "object" } = parameters;
const address = parameters.contractAddress ?? parameters.address;
if (!address) {
throw new TurnkeyActivityError({
message: "Unable to sign authorization: address is undefined.",
});
}
const signature = await signMessageWithErrorWrapping(client, JSON.stringify({
address,
chainId,
nonce,
}), organizationId, signWith, "PAYLOAD_ENCODING_EIP7702_AUTHORIZATION", to);
if (to === "object")
return {
address,
chainId,
nonce,
r: `${signature.r}`,
s: `${signature.s}`,
v: BigInt(signature.v),
yParity: Number(signature.v),
};
return signature;
}
async function signMessage(client, message, organizationId, signWith) {
const signedMessage = await signMessageWithErrorWrapping(client, message, organizationId, signWith);
return `${signedMessage}`;
}
async function signTransaction(client, transaction, serializer, organizationId, signWith) {
// Note: for Type 3 transactions, we are specifically handling parsing for payloads containing only the transaction payload body, without any wrappers around blobs, commitments, or proofs.
// See more: https://github.com/wevm/viem/blob/3ef19eac4963014fb20124d1e46d1715bed5509f/src/accounts/utils/signTransaction.ts#L54-L55
const signableTransaction = transaction.type === "eip4844"
? { ...transaction, sidecars: false }
: transaction;
const serializedTx = await serializer(signableTransaction);
const nonHexPrefixedSerializedTx = serializedTx.replace(/^0x/, "");
// Automatically detect transaction type from the serialized payload
const transactionType = detectTransactionType(nonHexPrefixedSerializedTx);
const signedTx = await signTransactionWithErrorWrapping(client, nonHexPrefixedSerializedTx, organizationId, signWith, transactionType);
if (transaction.type === "eip4844") {
// Grab components of the signature
const { r, s, v } = parseTransaction(signedTx);
// Recombine with the original transaction
return serializeTransaction(transaction, {
r: r,
s: s,
v: v,
});
}
return signedTx;
}
async function signTypedData(client, data, organizationId, signWith) {
return (await signMessageWithErrorWrapping(client, serializeTypedData(data), organizationId, signWith, "PAYLOAD_ENCODING_EIP712", "hex"));
}
async function signTransactionWithErrorWrapping(client, unsignedTransaction, organizationId, signWith, transactionType) {
let signedTx;
try {
signedTx = await signTransactionImpl(client, unsignedTransaction, organizationId, signWith, transactionType);
}
catch (error) {
// Wrap Turnkey error in Viem-specific error
if (error instanceof TurnkeyActivityError$1) {
throw new TurnkeyActivityError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}
if (error instanceof TurnkeyActivityConsensusNeededError) {
throw new TurnkeyConsensusNeededError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}
throw new TurnkeyActivityError({
message: `Failed to sign: ${error.message}`,
});
}
return `0x${signedTx}`;
}
async function signTransactionImpl(client, unsignedTransaction, organizationId, signWith, transactionType) {
if (isHttpClient(client)) {
const { activity } = await client.signTransaction({
type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
organizationId: organizationId,
parameters: {
signWith,
type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM",
unsignedTransaction: unsignedTransaction,
},
timestampMs: String(Date.now()), // millisecond timestamp
});
assertActivityCompleted(activity);
return assertNonNull(activity?.result?.signTransactionResult?.signedTransaction);
}
else {
const { activity, signedTransaction } = await client.signTransaction({
organizationId,
signWith,
type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM",
unsignedTransaction: unsignedTransaction,
});
assertActivityCompleted(activity /* Type casting is ok here. The invalid types are both actually strings. TS is too strict here! */);
return assertNonNull(signedTransaction);
}
}
async function signMessageWithErrorWrapping(client, message, organizationId, signWith, payloadEncoding = "PAYLOAD_ENCODING_HEXADECIMAL", to = "hex") {
let signedMessage;
try {
signedMessage = await signMessageImpl(client, message, organizationId, signWith, payloadEncoding, to);
}
catch (error) {
// Wrap Turnkey error in Viem-specific error
if (error instanceof TurnkeyActivityError$1) {
throw new TurnkeyActivityError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}
if (error instanceof TurnkeyActivityConsensusNeededError) {
throw new TurnkeyConsensusNeededError({
message: error.message,
activityId: error.activityId,
activityStatus: error.activityStatus,
});
}
throw new TurnkeyActivityError({
message: `Failed to sign: ${error.message}`,
});
}
return signedMessage;
}
async function signMessageImpl(client, message, organizationId, signWith, payloadEncoding, to) {
let result;
if (isHttpClient(client)) {
const { activity } = await client.signRawPayload({
type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
organizationId: organizationId,
parameters: {
signWith,
payload: message,
encoding: payloadEncoding,
hashFunction: "HASH_FUNCTION_NO_OP",
},
timestampMs: String(Date.now()), // millisecond timestamp
});
assertActivityCompleted(activity);
result = assertNonNull(activity?.result?.signRawPayloadResult);
}
else {
const { activity, r, s, v } = await client.signRawPayload({
organizationId,
signWith,
payload: message,
encoding: payloadEncoding,
hashFunction: "HASH_FUNCTION_NO_OP",
});
assertActivityCompleted(activity /* Type casting is ok here. The invalid types are both actually strings. TS is too strict here! */);
result = {
r,
s,
v,
};
}
if (to === "object") {
return {
r: `0x${result.r}`,
s: `0x${result.s}`,
v: BigInt(result.v),
};
}
return assertNonNull(serializeSignature(result, to));
}
// Modified from Viem implementation:
// https://github.com/wevm/viem/blob/c8378d22f692f48edde100693159874702f36330/src/utils/signature/serializeSignature.ts#L38-L39
function serializeSignature(sig, to = "hex") {
const { r: rString, s: sString, v: vString } = sig;
const r = `0x${rString}`;
const s = `0x${sString}`;
const v = BigInt(vString);
// Turnkey's `v` returned can be used as a proxy for yParity
const yParity_ = v;
const signature = `0x${new secp256k1.Signature(hexToBigInt(r), hexToBigInt(s)).toCompactHex()}${yParity_ === 0n ? "1b" : "1c"}`;
if (to === "hex")
return signature;
return hexToBytes(signature);
}
function isTurnkeyActivityConsensusNeededError(error) {
return (typeof error.walk === "function" &&
error.walk((e) => {
return e instanceof TurnkeyConsensusNeededError;
}));
}
function isTurnkeyActivityError(error) {
return (typeof error.walk === "function" &&
error.walk((e) => {
return e instanceof TurnkeyActivityError;
}));
}
export { TurnkeyActivityError, TurnkeyConsensusNeededError, createAccount, createAccountWithAddress, createApiKeyAccount, isTurnkeyActivityConsensusNeededError, isTurnkeyActivityError, serializeSignature, signAuthorization, signMessage, signTransaction, signTypedData };
//# sourceMappingURL=index.mjs.map