UNPKG

@turnkey/viem

Version:

Turnkey Helpers to work with Viem

370 lines (366 loc) 15.1 kB
'use strict'; var viem = require('viem'); var accounts = require('viem/accounts'); var secp256k1 = require('@noble/curves/secp256k1'); var http = require('@turnkey/http'); var apiKeyStamper = require('@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 viem.BaseError { constructor({ message = "Turnkey activity requires consensus.", activityId, activityStatus, }) { super(message); this.name = "TurnkeyConsensusNeededError"; this.activityId = activityId; this.activityStatus = activityStatus; } } class TurnkeyActivityError extends viem.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 http.TurnkeyActivityError({ message: `Missing signWith parameter`, }); } if (viem.isAddress(signWith)) { // override provided `ethereumAddress` ethereumAddress = signWith; } else if (!ethereumAddress) { throw new TurnkeyActivityError({ message: `Missing ethereumAddress parameter`, }); } return accounts.toAccount({ address: ethereumAddress, sign: function ({ hash }) { return signMessage(client, hash, organizationId, signWith); }, signMessage: function ({ message, }) { const hashedMessage = viem.hashMessage(message); return signMessage(client, hashedMessage, organizationId, signWith); }, signTransaction: function (transaction, options) { const serializer = options?.serializer ?? viem.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 (viem.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.ApiKeyStamper({ apiPublicKey: apiPublicKey, apiPrivateKey: apiPrivateKey, }); const client = new http.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 http.TurnkeyActivityError({ message: `Unable to find Ethereum address for key ${privateKeyId} under organization ${organizationId}`, }); } return accounts.toAccount({ address: ethereumAddress, sign: function ({ hash }) { return signMessage(client, hash, organizationId, privateKeyId); }, signMessage: function ({ message, }) { const hashedMessage = viem.hashMessage(message); return signMessage(client, hashedMessage, organizationId, privateKeyId); }, signTransaction: function (transaction, options) { const serializer = options?.serializer ?? viem.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 } = viem.parseTransaction(signedTx); // Recombine with the original transaction return viem.serializeTransaction(transaction, { r: r, s: s, v: v, }); } return signedTx; } async function signTypedData(client, data, organizationId, signWith) { return (await signMessageWithErrorWrapping(client, viem.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 http.TurnkeyActivityError) { throw new TurnkeyActivityError({ message: error.message, activityId: error.activityId, activityStatus: error.activityStatus, }); } if (error instanceof http.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 (http.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 }); http.assertActivityCompleted(activity); return http.assertNonNull(activity?.result?.signTransactionResult?.signedTransaction); } else { const { activity, signedTransaction } = await client.signTransaction({ organizationId, signWith, type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM", unsignedTransaction: unsignedTransaction, }); http.assertActivityCompleted(activity /* Type casting is ok here. The invalid types are both actually strings. TS is too strict here! */); return http.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 http.TurnkeyActivityError) { throw new TurnkeyActivityError({ message: error.message, activityId: error.activityId, activityStatus: error.activityStatus, }); } if (error instanceof http.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 (http.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 }); http.assertActivityCompleted(activity); result = http.assertNonNull(activity?.result?.signRawPayloadResult); } else { const { activity, r, s, v } = await client.signRawPayload({ organizationId, signWith, payload: message, encoding: payloadEncoding, hashFunction: "HASH_FUNCTION_NO_OP", }); http.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 http.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.secp256k1.Signature(viem.hexToBigInt(r), viem.hexToBigInt(s)).toCompactHex()}${yParity_ === 0n ? "1b" : "1c"}`; if (to === "hex") return signature; return viem.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; })); } exports.TurnkeyActivityError = TurnkeyActivityError; exports.TurnkeyConsensusNeededError = TurnkeyConsensusNeededError; exports.createAccount = createAccount; exports.createAccountWithAddress = createAccountWithAddress; exports.createApiKeyAccount = createApiKeyAccount; exports.isTurnkeyActivityConsensusNeededError = isTurnkeyActivityConsensusNeededError; exports.isTurnkeyActivityError = isTurnkeyActivityError; exports.serializeSignature = serializeSignature; exports.signAuthorization = signAuthorization; exports.signMessage = signMessage; exports.signTransaction = signTransaction; exports.signTypedData = signTypedData; //# sourceMappingURL=index.js.map