@turnkey/ethers
Version:
Turnkey Signer for Ethers
211 lines (207 loc) • 8.72 kB
JavaScript
;
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