@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
181 lines (178 loc) • 7.69 kB
JavaScript
import { WalletInterfaceType, SignIntent } from '../__types__/enums.mjs';
import { uint8ArrayFromHexString, uint8ArrayToHexString, stringToBase64urlString } from '@turnkey/encoding';
import { getSignatureSchemeFromProvider, isEthereumProvider, isSolanaProvider } from '../utils.mjs';
const STAMP_HEADER_NAME = "X-Stamp";
class CrossPlatformWalletStamper {
/**
* Constructs a CrossPlatformWalletStamper.
*
* - Validates that at least one wallet interface is provided.
* - For each wallet interface, creates an internal `WalletStamper` bound to it.
* - Ensures the stamper instance is always initialized in a usable state.
*
* @param wallets - A partial mapping of wallet interfaces by type.
* @throws {Error} If no wallet interfaces are provided.
*/
constructor(wallets) {
this.ctx = {};
const walletEntries = Object.entries(wallets).filter(([, w]) => Boolean(w));
if (walletEntries.length === 0) {
throw new Error("Cannot create WalletStamper: no wallet interfaces provided");
}
for (const [interfaceType, wallet] of walletEntries) {
const typed = interfaceType;
this.ctx[typed] = { wallet, stamper: new WalletStamper(wallet) };
}
}
/**
* Stamps a payload using the specified wallet interface and provider.
*
* - Uses the explicitly provided interface and provider if given.
* - Falls back to the default interface and stored provider otherwise.
*
* @param payload - The string payload to sign.
* @param interfaceType - Optional wallet interface type (defaults to the active or first available).
* @param provider - Optional provider (defaults to the one set via `setProvider`).
* @returns A `TStamp` object containing the stamp header name and encoded value.
* @throws {Error} If no provider is available for the selected interface.
*/
async stamp(payload, interfaceType = this.defaultInterface(), provider) {
const ctx = this.getCtx(interfaceType);
const selectedProvider = provider ?? ctx.provider;
if (!selectedProvider) {
throw new Error(`Could not find a provider for interface '${interfaceType}'.`);
}
return ctx.stamper.stamp(payload, selectedProvider);
}
/**
* Retrieves the public key for the given provider.
*
* @param interfaceType - Optional wallet interface type (defaults to the active or first available).
* @param provider - Wallet provider for which to fetch the public key.
* @returns A promise resolving to the public key in hex format.
*/
async getPublicKey(interfaceType = this.defaultInterface(), provider) {
return this.getCtx(interfaceType).wallet.getPublicKey(provider);
}
/**
* Sets the active provider for a given wallet interface.
*
* - The active provider is used as a fallback in `stamp` if none is passed explicitly.
*
* @param interfaceType - Wallet interface type.
* @param provider - Provider instance to associate with the interface.
*/
setProvider(interfaceType, provider) {
this.getCtx(interfaceType).provider = provider;
this.activeInterfaceType = interfaceType;
}
/**
* Determines the default wallet interface to use when none is specified.
*
* - Preference order: Active provider > Ethereum > Solana > WalletConnect.
*
* @returns The default wallet interface type.
* @throws {Error} If no wallet interfaces are initialized.
*/
defaultInterface() {
if (this.activeInterfaceType) {
return this.activeInterfaceType;
}
const initializedInterfaces = Object.keys(this.ctx);
if (initializedInterfaces.includes(WalletInterfaceType.Ethereum)) {
return WalletInterfaceType.Ethereum;
}
if (initializedInterfaces.includes(WalletInterfaceType.Solana)) {
return WalletInterfaceType.Solana;
}
if (initializedInterfaces.includes(WalletInterfaceType.WalletConnect)) {
return WalletInterfaceType.WalletConnect;
}
throw new Error("No interfaces initialized");
}
/**
* Retrieves the internal context for a given wallet interface.
*
* @param interfaceType - Wallet interface type.
* @returns The context including wallet, stamper, and optional provider.
* @throws {Error} If the interface is not initialized.
*/
getCtx(interfaceType) {
const ctx = this.ctx[interfaceType];
if (!ctx) {
throw new Error(`Interface '${interfaceType}' not initialised`);
}
return ctx;
}
}
class WalletStamper {
/**
* Constructs a WalletStamper bound to a single wallet interface.
*
* @param wallet - The wallet interface used for signing.
*/
constructor(wallet) {
this.wallet = wallet;
}
/**
* Signs a payload and returns a standardized stamp header.
*
* - For Ethereum:
* - Signs using EIP-191.
* - Recovers and compresses the public key.
* - Converts the signature into DER format.
* - For Solana:
* - Signs using Ed25519.
* - Fetches the public key directly from the wallet.
*
* @param payload - The payload to sign.
* @param provider - The wallet provider used for signing.
* @returns A `TStamp` containing the header name and base64url-encoded JSON value.
* @throws {Error} If signing or public key recovery fails.
*/
async stamp(payload, provider) {
let signature;
let publicKey;
try {
signature = await this.wallet.sign(payload, provider, SignIntent.SignMessage);
}
catch (error) {
throw new Error(`Failed to sign the message: ${error}`);
}
const scheme = getSignatureSchemeFromProvider(provider);
try {
if (isEthereumProvider(provider)) {
const { recoverPublicKey, hashMessage } = await import('viem');
const { compressRawPublicKey, toDerSignature } = await import('@turnkey/crypto');
const rawPublicKey = await recoverPublicKey({
hash: hashMessage(payload),
signature: signature,
});
const publicKeyHex = rawPublicKey.startsWith("0x")
? rawPublicKey.slice(2)
: rawPublicKey;
const publicKeyBytes = uint8ArrayFromHexString(publicKeyHex);
const publicKeyBytesCompressed = compressRawPublicKey(publicKeyBytes);
publicKey = uint8ArrayToHexString(publicKeyBytesCompressed);
signature = toDerSignature(signature.replace("0x", ""));
}
else if (isSolanaProvider(provider)) {
publicKey = await this.wallet.getPublicKey(provider);
}
else {
// we should never hit this case
// if we do then it means we added support for a new chain but missed updating the stamper
throw new Error(`Unsupported provider namespace: ${provider.chainInfo.namespace}. Expected Ethereum or Solana.`);
}
}
catch (error) {
throw new Error(`Failed to recover public key: ${error}`);
}
return {
stampHeaderName: STAMP_HEADER_NAME,
stampHeaderValue: stringToBase64urlString(JSON.stringify({ publicKey, scheme, signature })),
};
}
}
export { CrossPlatformWalletStamper, WalletStamper };
//# sourceMappingURL=stamper.mjs.map