UNPKG

@shogun-sdk/accounts

Version:

Shogun with Turnkey: configs, encryption, authentication with Telegram/Turnkey OIDC, etc.

287 lines (254 loc) 8.02 kB
import { Attestation, Email, EmailParam, OauthProviderParams, OidcTokenParam, PublicKeyParam, UserNameParam, Wallet, } from '../types/turnkey.js'; import { ApiKeyStamper, DEFAULT_ETHEREUM_ACCOUNTS, DEFAULT_SOLANA_ACCOUNTS, TurnkeyServerClient, } from '@turnkey/sdk-server'; import { decode, JwtPayload } from 'jsonwebtoken'; import { TURNKEY_APP_CONFIG } from '../config/index.js'; export const TURNKEY_SESSION_EXPIRE = '7200'; // 2 hours in seconds export interface ITurnkeyConfig { apiBaseUrl: string; defaultOrganizationId: string; rpId: string; iframeUrl: string; } export const getTurnkeyConfig = (baseUrl: string, organizationId: string, rpId: string): ITurnkeyConfig => ({ apiBaseUrl: baseUrl, defaultOrganizationId: organizationId, rpId, iframeUrl: 'https://auth.turnkey.com', }); export const TURNKEY_EMBEDDED_KEY = "@turnkey/embedded_key"; export const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 24 * 365 * 10; // 10 years in milliseconds export const TURNKEY_CREDENTIAL_BUNDLE = "@turnkey/credential_bundle"; export const TURNKEY_CREDENTIAL_BUNDLE_TTL_IN_MILLIS = 1000 * 60 * 60 * 24 * 365 * 10; // 48 hours in milliseconds; export const getApiKeyStamper = (apiPublicKey: string, apiPrivateKey: string) => new ApiKeyStamper({ apiPublicKey, apiPrivateKey, }); const getTurnkeyClient = (baseUrl: string, organizationId: string, stamper: ApiKeyStamper) => new TurnkeyServerClient({ apiBaseUrl: baseUrl, organizationId, stamper, }); function decodeJwt(credential: string): JwtPayload | null { const decoded = decode(credential); if (decoded && typeof decoded === 'object' && 'email' in decoded) { return decoded as JwtPayload; } return null; } export const oauth = async ({ credential, targetPublicKey, targetSubOrgId, config, stamper, }: { credential: string; targetPublicKey: string; targetSubOrgId: string; config: ITurnkeyConfig; stamper: ApiKeyStamper; }) => { const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const oauthResponse = await client.oauth({ oidcToken: credential, targetPublicKey, organizationId: targetSubOrgId, expirationSeconds: TURNKEY_SESSION_EXPIRE, }); return oauthResponse; }; export async function getSubOrgId( config: ITurnkeyConfig, param: OidcTokenParam | PublicKeyParam | EmailParam | UserNameParam, stamper: ApiKeyStamper, ) { let filterType: string; let filterValue: string; if ('userName' in param) { filterType = 'USERNAME'; filterValue = param.userName; } else if ('email' in param) { filterType = 'EMAIL'; filterValue = param.email; } else if ('publicKey' in param) { filterType = 'PUBLIC_KEY'; filterValue = param.publicKey; } else if ('oidcToken' in param) { filterType = 'OIDC_TOKEN'; filterValue = param.oidcToken; } else { throw new Error('Invalid parameter'); } const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const { organizationIds } = await client.getSubOrgIds({ organizationId: config.defaultOrganizationId, filterType, filterValue, }); return organizationIds[0]; } export const getSubOrgIdByEmail = async (config: ITurnkeyConfig, email: Email, stamper: ApiKeyStamper) => { return getSubOrgId(config, { email }, stamper); }; export const createUserSubOrg = async ({ config, username, email, passkey, oauth, delegationApiKey, onSuccess, stamper, }: { config: ITurnkeyConfig; username?: string; email?: Email; passkey?: { challenge: string; attestation: Attestation; }; oauth?: OauthProviderParams; delegationApiKey?: string; onSuccess?: (subOrg: { subOrganizationId: string; }, delegationApiKey: string) => void; stamper: ApiKeyStamper; }) => { const authenticators = passkey ? [ { authenticatorName: 'Passkey', challenge: passkey.challenge, attestation: passkey.attestation, }, ] : []; const oauthProviders = oauth ? [ { providerName: oauth.providerName, oidcToken: oauth.oidcToken, }, ] : []; let userEmail = email; // If the user is logging in with a Google Auth credential, use the email from the decoded OIDC token (credential // Otherwise, use the email from the email parameter if (oauth) { const decoded = decodeJwt(oauth.oidcToken); if (decoded?.email) { userEmail = decoded.email; } } const subOrganizationName = `Sub Org - ${email ?? ''}`; const userName = username ? username : email ? email.split('@')?.[0] || email : ''; const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const subOrg = await client.createSubOrganization({ organizationId: config.defaultOrganizationId, subOrganizationName, rootUsers: [ { userName, userEmail, oauthProviders, authenticators, apiKeys: delegationApiKey ? [{ apiKeyName: TURNKEY_APP_CONFIG.BOT_DELEGATION_NAME, publicKey: delegationApiKey, curveType: 'API_KEY_CURVE_P256', }] : [], }, ], rootQuorumThreshold: 1, wallet: { walletName: 'Default Wallet', accounts: [...DEFAULT_ETHEREUM_ACCOUNTS, ...DEFAULT_SOLANA_ACCOUNTS], }, }); const userId = subOrg.rootUserIds?.[0]; if (!userId) { throw new Error('No root user ID found'); } const { user } = await client.getUser({ organizationId: subOrg.subOrganizationId, userId, }); if(delegationApiKey && onSuccess) { await onSuccess?.(subOrg, delegationApiKey); } return { subOrg, user }; }; export const getSubOrgIdByPublicKey = async (config: ITurnkeyConfig, publicKey: string, stamper: ApiKeyStamper) => { return getSubOrgId(config, { publicKey }, stamper); }; export const getSubOrgIdByUserName = async (config: ITurnkeyConfig, userName: string, stamper: ApiKeyStamper) => { return getSubOrgId(config, { userName: userName.toLowerCase() }, stamper); }; export async function getWalletsWithAccounts(config: ITurnkeyConfig, organizationId: string, stamper: ApiKeyStamper): Promise<Wallet[]> { try { const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const { wallets } = await client.getWallets({ organizationId, }); const walletsWithAccounts = await Promise.all( wallets.map(async (wallet) => { try { const { accounts } = await client.getWalletAccounts({ organizationId, walletId: wallet.walletId, }); return { ...wallet, accounts }; } catch (error) { console.error(`Failed to fetch accounts for wallet ${wallet.walletId}:`, error); // Return wallet with empty accounts array if account fetch fails return { ...wallet, accounts: [] }; } }), ); return walletsWithAccounts; } catch (error) { console.error('Failed to fetch wallets:', error); throw new Error('Failed to fetch wallets and accounts. Please try again later.'); } } export async function getUserOrganization(config: ITurnkeyConfig, organizationId: string, stamper: ApiKeyStamper) { const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const data = await client.getOrganization({ organizationId, }); return data; } export async function initEmailRecovery( config: ITurnkeyConfig, organizationId: string, email: string, targetPublicKey: string, stamper: ApiKeyStamper, ) { const client = getTurnkeyClient(config.apiBaseUrl, config.defaultOrganizationId, stamper); const data = await client.initUserEmailRecovery({ organizationId, email: email, targetPublicKey: targetPublicKey, emailCustomization: { appName: 'Gun.fun', logoUrl: 'https://www.gun.fun/images/logo.svg', }, }); return data; }