@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
216 lines (212 loc) • 9.38 kB
JavaScript
;
var app = require('@wallet-standard/app');
var encoding = require('@turnkey/encoding');
var enums = require('../../../__types__/enums.js');
var utils = require('../../../utils.js');
/**
* Abstract base class for Solana wallet implementations using Wallet Standard.
*
* Provides shared logic for:
* - Provider discovery via `@wallet-standard/app` (`getWallets()`).
* - Connecting via `standard:connect` and disconnecting via `standard:disconnect`.
* - Public key retrieval from the wallet's account address (base58 → hex).
*/
class BaseSolanaWallet {
constructor() {
this.interfaceType = enums.WalletInterfaceType.Solana;
/**
* Retrieves the ed25519 public key for the active account as hex (no 0x prefix).
*
* - Ensures the wallet is connected (calls `standard:connect` if needed).
* - Decodes the Wallet Standard account address (base58) to raw bytes.
*
* @param provider - The wallet provider to use.
* @returns Hex-encoded ed25519 public key (no 0x prefix).
* @throws {Error} If no account is available.
*/
this.getPublicKey = async (provider) => {
const wallet = asSolana(provider);
await connectAccount(wallet);
const account = wallet.accounts[0];
if (!account) {
throw new Error("No account in wallet");
}
const rawBytes = encoding.bs58.decode(account.address);
return encoding.uint8ArrayToHexString(rawBytes);
};
/**
* Discovers Solana-capable Wallet Standard providers.
*
* - Uses `getWallets().get()` and filters wallets with at least one `chains` entry
* starting with `"solana:"`.
* - For each wallet, collects branding info and any currently connected addresses.
*
* @returns A list of discovered Solana `WalletProvider`s (may be empty).
*/
this.getProviders = async () => {
const discovered = [];
const walletsApi = app.getWallets();
const providers = walletsApi
.get()
.filter((w) => w.chains.some((c) => c.startsWith("solana:")));
await Promise.all(providers.map(async (wallet) => {
// wrap account access in a timeout to prevent hanging providers from blocking discovery
const connectedAddresses = await utils.withTimeoutFallback((async () => {
try {
return wallet.accounts?.map((a) => a.address) ?? [];
}
catch {
return [];
}
})(), []);
discovered.push({
interfaceType: enums.WalletInterfaceType.Solana,
chainInfo: {
namespace: enums.Chain.Solana,
},
// Some wallet providers include leading whitespace in their icon URLs.
// Next.js 15+ and other frameworks are strict about URL formatting,
// so we trim
info: {
name: wallet.name,
...(wallet.icon && { icon: wallet.icon.trim() }),
},
provider: wallet,
connectedAddresses,
});
}));
return discovered;
};
/**
* Connects the wallet account, prompting the user if necessary.
*
* - Calls `standard:connect` only if no accounts are present. This will prompt the user to connect their wallet.
*
* @param provider - The wallet provider to connect.
* @returns A promise that resolves with the connected wallet's address.
* @throws {Error} If the wallet does not implement `standard:connect`.
*/
this.connectWalletAccount = async (provider) => {
const wallet = asSolana(provider);
return await connectAccount(wallet);
};
/**
* Disconnects the wallet account using Wallet Standard.
*
* - Calls `standard:disconnect` if implemented.
* - Throws if the wallet does not implement `standard:disconnect`.
*
* @param provider - The wallet provider to disconnect.
* @returns A promise that resolves once the wallet disconnects.
* @throws {Error} If `standard:disconnect` is not supported by the wallet.
*/
this.disconnectWalletAccount = async (provider) => {
const wallet = asSolana(provider);
const disconnectFeature = wallet.features["standard:disconnect"];
if (disconnectFeature) {
await disconnectFeature.disconnect();
}
else {
throw new Error("Wallet does not support standard:disconnect");
}
};
}
}
/**
* Signs a message or transaction with the connected Solana wallet.
*
* - Ensures the wallet is connected (may prompt via `standard:connect` if its not).
* - `SignMessage` → `solana:signMessage` (returns hex signature).
* - `SignTransaction` → `solana:signTransaction` (returns hex signature).
*
* @param payload - UTF-8 string (for message) or hex string (for transaction bytes).
* @param provider - The wallet provider to use.
* @param intent - The signing intent.
* @returns Hex-encoded signature (no 0x prefix).
* @throws {Error} If the provider lacks required features or intent is unsupported.
*/
class SolanaWallet extends BaseSolanaWallet {
constructor() {
super(...arguments);
this.sign = async (payload, provider, intent) => {
const wallet = asSolana(provider);
await connectAccount(wallet);
const account = wallet.accounts[0];
if (!account)
throw new Error("No account available");
switch (intent) {
case enums.SignIntent.SignMessage: {
const signFeature = wallet.features["solana:signMessage"];
if (!signFeature)
throw new Error("Provider does not support solana:signMessage");
const data = new TextEncoder().encode(payload);
const results = await signFeature.signMessage({
account,
message: data,
});
if (!results?.length || !results[0]?.signature) {
throw new Error("No signature returned from signMessage");
}
return encoding.uint8ArrayToHexString(results[0].signature);
}
case enums.SignIntent.SignTransaction: {
const signFeature = wallet.features["solana:signTransaction"];
if (!signFeature)
throw new Error("Provider does not support solana:signTransaction");
const data = encoding.uint8ArrayFromHexString(payload);
const results = await signFeature.signTransaction({
account,
transaction: data,
});
if (!results?.length || !results[0]?.signedTransaction) {
throw new Error("No signature returned from signTransaction");
}
return encoding.uint8ArrayToHexString(results[0].signedTransaction);
}
default:
throw new Error(`Unsupported sign intent: ${intent}`);
}
};
}
}
/**
* Casts a WalletRpcProvider to a Wallet Standard Solana wallet.
*
* - Validates presence of the Wallet Standard `features` map and `solana:signMessage`.
* - Use this before calling Solana-specific features (signMessage, signTransaction, etc.).
*
* @param provider - The wallet provider to cast.
* @returns The Wallet Standard wallet object.
* @throws {Error} If the provider is not a Wallet Standard Solana wallet.
*/
const asSolana = (provider) => {
if (provider.provider &&
"features" in provider.provider &&
"solana:signMessage" in provider.provider.features) {
return provider.provider;
}
throw new Error("Expected a Wallet-Standard provider (Solana wallet)");
};
/**
* Ensures the given Wallet Standard wallet has at least one connected account.
*
* - If accounts already exist, resolves immediately.
* - If not, attempts `standard:connect`, which may prompt the user.
*
* @param wallet - The Wallet Standard wallet to connect.
* @returns A promise that resolves with the connected wallet's address.
* @throws {Error} If the wallet does not implement `standard:connect`.
*/
const connectAccount = async (wallet) => {
if (wallet.accounts.length)
return wallet.accounts[0].address;
const stdConnect = wallet.features["standard:connect"];
if (stdConnect) {
await stdConnect.connect();
return wallet.accounts[0].address;
}
throw new Error("Wallet is not connected and does not implement standard:connect");
};
exports.BaseSolanaWallet = BaseSolanaWallet;
exports.SolanaWallet = SolanaWallet;
//# sourceMappingURL=solana.js.map