import { Turnkey } from "@turnkey/sdk-server";
import {
AdamikCurve,
AdamikHashFunction,
AdamikSignerSpec,
} from "../adamik/types";
export class TurnkeySigner implements BaseSigner {
private turnkeyClient: Turnkey;
public chainId: string;
public signerSpec: AdamikSignerSpec;
public signerName = "TURNKEY";
private pubKey: string | undefined;
constructor(chainId: string, signerSpec: AdamikSignerSpec) {
this.chainId = chainId;
this.signerSpec = signerSpec;
// Access environment variables based on your project setup
const baseUrl =
process.env.TURNKEY_BASE_URL || import.meta.env?.VITE_TURNKEY_BASE_URL;
const apiPublicKey =
process.env.TURNKEY_API_PUBLIC_KEY ||
import.meta.env?.VITE_TURNKEY_API_PUBLIC_KEY;
const apiPrivateKey =
process.env.TURNKEY_API_PRIVATE_KEY ||
import.meta.env?.VITE_TURNKEY_API_PRIVATE_KEY;
const organizationId =
process.env.TURNKEY_ORGANIZATION_ID ||
import.meta.env?.VITE_TURNKEY_ORGANIZATION_ID;
this.turnkeyClient = new Turnkey({
apiBaseUrl: baseUrl,
apiPublicKey: apiPublicKey,
apiPrivateKey: apiPrivateKey,
defaultOrganizationId: organizationId,
});
}
private convertAdamikCurveToTurnkeyCurve(
curve: AdamikCurve
): "CURVE_SECP256K1" | "CURVE_ED25519" {
switch (curve) {
case AdamikCurve.SECP256K1:
return "CURVE_SECP256K1";
case AdamikCurve.ED25519:
return "CURVE_ED25519";
default:
throw new Error(`Unsupported curve: ${curve}`);
}
}
private convertHashFunctionToTurnkeyHashFunction(
hashFunction: AdamikHashFunction,
curve: AdamikCurve
) {
if (curve === AdamikCurve.ED25519) {
return "HASH_FUNCTION_NOT_APPLICABLE";
}
switch (hashFunction) {
case AdamikHashFunction.SHA256:
return "HASH_FUNCTION_SHA256";
case AdamikHashFunction.KECCAK256:
return "HASH_FUNCTION_KECCAK256";
default:
return "HASH_FUNCTION_NOT_APPLICABLE";
}
}
async getPubkey(): Promise<string> {
try {
const walletId =
process.env.TURNKEY_WALLET_ID ||
import.meta.env?.VITE_TURNKEY_WALLET_ID;
const { accounts } = await this.turnkeyClient
.apiClient()
.getWalletAccounts({
walletId,
paginationOptions: {
limit: "100",
},
});
const accountCompressed = accounts.find(
(account) =>
account.curve ===
this.convertAdamikCurveToTurnkeyCurve(this.signerSpec.curve) &&
getCoinTypeFromDerivationPath(account.path) ===
Number(this.signerSpec.coinType) &&
account.addressFormat === "ADDRESS_FORMAT_COMPRESSED"
);
if (accountCompressed) {
this.pubKey = accountCompressed.address;
return accountCompressed.address;
}
// Create account if not found
try {
const createAccount = await this.turnkeyClient
.apiClient()
.createWalletAccounts({
walletId,
accounts: [
{
curve: this.convertAdamikCurveToTurnkeyCurve(
this.signerSpec.curve
),
path: `m/44'/${this.signerSpec.coinType}'/0'/0/0`,
pathFormat: "PATH_FORMAT_BIP32",
addressFormat: "ADDRESS_FORMAT_COMPRESSED",
},
],
});
this.pubKey = createAccount.addresses[0];
return createAccount.addresses[0];
} catch (error) {
// Handle case where account creation fails but account exists
if (
error.message?.includes(
"wallet account corresponding to path already exists"
)
) {
// Retry fetching accounts
const { accounts } = await this.turnkeyClient
.apiClient()
.getWalletAccounts({
walletId,
});
const existingAccount = accounts.find(
(account) =>
account.curve ===
this.convertAdamikCurveToTurnkeyCurve(this.signerSpec.curve) &&
getCoinTypeFromDerivationPath(account.path) ===
Number(this.signerSpec.coinType)
);
if (existingAccount) {
this.pubKey = existingAccount.address;
return existingAccount.address;
}
}
throw error;
}
} catch (error) {
console.error("Error in Turnkey getPubkey:", error);
throw error;
}
}
async signTransaction(encodedMessage: string): Promise<string> {
if (!this.pubKey) {
this.pubKey = await this.getPubkey();
}
try {
const txSignResult = await this.turnkeyClient.apiClient().signRawPayload({
signWith: this.pubKey,
payload: encodedMessage,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: this.convertHashFunctionToTurnkeyHashFunction(
this.signerSpec.hashFunction,
this.signerSpec.curve
),
});
return extractSignature(this.signerSpec.signatureFormat, txSignResult);
} catch (error) {
console.error("Error in Turnkey signTransaction:", error);
throw error;
}
}
}
// Helper functions
function getCoinTypeFromDerivationPath(path: string): number {
const parts = path.split("/");
// Handle format like m/44'/60'/0'/0/0
for (let i = 0; i < parts.length; i++) {
if (parts[i].includes("44'") && i + 1 < parts.length) {
const nextPart = parts[i + 1];
return parseInt(nextPart.replace("'", ""));
}
}
return 0;
}
function extractSignature(signatureFormat: string, signResult: any): string {
// Implementation depends on your specific needs for different chains
// This is a placeholder that should be replaced with actual implementation
// based on signature format requirements for each blockchain
return signResult.r + signResult.s + (signResult.v ? signResult.v : "");
}