UNPKG

@turnkey/ethers

Version:

Turnkey Signer for Ethers

211 lines (207 loc) 8.72 kB
'use strict'; var ethers = require('ethers'); var http = require('@turnkey/http'); class TurnkeySigner extends ethers.AbstractSigner { constructor(config, provider) { super(provider); this._signTypedData = this.signTypedData.bind(this); this.client = config.client; this.organizationId = config.organizationId; this.signWith = config.signWith; } connect(provider) { return new TurnkeySigner({ client: this.client, organizationId: this.organizationId, signWith: this.signWith, }, provider); } // If the configured `signWith` is an Ethereum address, use it. // Otherwise, if it's a Turnkey Private Key ID, fetch the corresponding // private key's address. async getAddress() { let ethereumAddress; if (!this.signWith) { throw new http.TurnkeyActivityError({ message: `Missing signWith parameter`, }); } if (ethers.isAddress(this.signWith)) { ethereumAddress = this.signWith; } else if (!ethereumAddress) { const data = await this.client.getPrivateKey({ privateKeyId: this.signWith, organizationId: this.organizationId, }); 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 ${this.signWith} under organization ${this.organizationId}`, }); } } return ethereumAddress; } async _signTransactionImpl(unsignedTransaction) { if (http.isHttpClient(this.client)) { const { activity } = await this.client.signTransaction({ type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2", organizationId: this.organizationId, parameters: { signWith: this.signWith, type: "TRANSACTION_TYPE_ETHEREUM", unsignedTransaction, }, timestampMs: String(Date.now()), // millisecond timestamp }); http.assertActivityCompleted(activity); return http.assertNonNull(activity?.result?.signTransactionResult?.signedTransaction); } else { const { activity, signedTransaction } = await this.client.signTransaction({ signWith: this.signWith, type: "TRANSACTION_TYPE_ETHEREUM", 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 _signTransactionWithErrorWrapping(message) { let signedTx; try { signedTx = await this._signTransactionImpl(message); } catch (error) { if (error instanceof http.TurnkeyActivityError || error instanceof http.TurnkeyActivityConsensusNeededError) { throw error; } throw new http.TurnkeyActivityError({ message: `Failed to sign: ${error.message}`, cause: error, }); } return signedTx; } async signTransaction(transaction) { let { from, to, ...txn } = ethers.copyRequest(transaction); ({ to, from } = await ethers.resolveProperties({ to: transaction.to ? ethers.resolveAddress(transaction.to, this.provider) : undefined, from: transaction.from ? ethers.resolveAddress(transaction.from, this.provider) : undefined, })); // Mimic the behavior of ethers' `Wallet`: // - You don't need to pass in `tx.from` // - However if you do provide `tx.from`, verify and drop it before serialization // // https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/wallet/src.ts/index.ts#L117-L121 if (from != null) { const selfAddress = await this.getAddress(); if (ethers.getAddress(from) !== selfAddress) { throw new Error(`Transaction \`tx.from\` address mismatch. Self address: ${selfAddress}; \`tx.from\` address: ${from}`); } } delete transaction.from; const tx = ethers.Transaction.from({ ...txn, ...(to && { to }), }); const unsignedTx = tx.unsignedSerialized.substring(2); const signedTx = await this._signTransactionWithErrorWrapping(unsignedTx); return `0x${signedTx}`; } // Returns the signed prefixed-message. Per Ethers spec, this method treats: // - Bytes as a binary message // - string as a UTF8-message // i.e. "0x1234" is a SIX (6) byte string, NOT 2 bytes of data async signMessage(message) { const hashedMessage = ethers.hashMessage(message); const signedMessage = await this._signMessageWithErrorWrapping(hashedMessage); return `${signedMessage}`; } async _signMessageWithErrorWrapping(message, payloadEncoding = "PAYLOAD_ENCODING_HEXADECIMAL") { let signedMessage; try { signedMessage = await this._signMessageImpl(message, payloadEncoding); } catch (error) { if (error instanceof http.TurnkeyActivityError || error instanceof http.TurnkeyActivityConsensusNeededError) { throw error; } throw new http.TurnkeyActivityError({ message: `Failed to sign: ${error.message}`, cause: error, }); } return signedMessage; } async _signMessageImpl(message, payloadEncoding = "PAYLOAD_ENCODING_HEXADECIMAL") { let result; if (http.isHttpClient(this.client)) { const { activity } = await this.client.signRawPayload({ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", organizationId: this.organizationId, parameters: { signWith: this.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 this.client.signRawPayload({ signWith: this.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, }; } return serializeSignature(result); } async signTypedData(domain, types, value) { const populated = await ethers.TypedDataEncoder.resolveNames(domain, types, value, async (name) => { http.assertNonNull(this.provider); const address = await this.provider?.resolveName(name); http.assertNonNull(address); return address ?? ""; }); // Build the full EIP-712 payload (domain, types, and message) const payload = ethers.TypedDataEncoder.getPayload(populated.domain, types, populated.value); return this._signMessageWithErrorWrapping(JSON.stringify(payload), "PAYLOAD_ENCODING_EIP712"); } } function serializeSignature(signature) { const assembled = ethers.Signature.from({ r: `0x${signature.r}`, s: `0x${signature.s}`, v: parseInt(signature.v) + 27, }).serialized; return http.assertNonNull(assembled); } Object.defineProperty(exports, "TurnkeyActivityError", { enumerable: true, get: function () { return http.TurnkeyActivityError; } }); Object.defineProperty(exports, "TurnkeyRequestError", { enumerable: true, get: function () { return http.TurnkeyRequestError; } }); exports.TurnkeySigner = TurnkeySigner; exports.serializeSignature = serializeSignature; //# sourceMappingURL=index.js.map