@shogun-sdk/accounts
Version:
Shogun with Turnkey: configs, encryption, authentication with Telegram/Turnkey OIDC, etc.
287 lines (254 loc) • 8.02 kB
text/typescript
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;
}