0xtrails
Version:
SDK for Trails
532 lines (466 loc) • 14.5 kB
text/typescript
import { logger } from "./logger.js"
// API URLs
const SANDBOX_API_URL = "https://sandbox-integration-api.meshconnect.com"
const PRODUCTION_API_URL = "https://integration-api.meshconnect.com"
// API Credentials
const DEFAULT_SANDBOX_API_KEY =
"c2tfc2FuZF9rcmFrd3d1Zi5qY3d6ZG80bDM2ZGo0bHNtcHduazNkYm9hYmlxb2QyZWoxb2drM2dsaTE0NnphazB1cnhhcHh1MmFvcGVsZGxs"
const DEFAULT_PRODUCTION_API_KEY =
"c2tfcHJvZF81MHh0M3BmNC5kYmx6OTdjY3VrdzVuMW92cDNmZ2F6aHNjZGh3a3NscW5zdGR2OGRmdGg4NWc2c2F0bWVvNDh5enF3NGt2bnZn"
const DEFAULT_CLIENT_ID = "018880d9-425c-49fa-b0e5-08ddce68a08d"
const DEFAULT_ENVIRONMENT = "production"
export function getMeshConnectApiKey(
environment: "sandbox" | "production" = "sandbox",
): string {
return atob(
environment === "production"
? DEFAULT_PRODUCTION_API_KEY
: DEFAULT_SANDBOX_API_KEY,
)
}
export function getMeshConnectApiUrl(
environment: "sandbox" | "production" = "sandbox",
): string {
return environment === "production" ? PRODUCTION_API_URL : SANDBOX_API_URL
}
// Types
export interface MeshConnectConfig {
apiKey: string
clientId: string
environment?: "sandbox" | "production"
userId?: string
}
export interface TransferOptions {
integrationId?: string
address?: string
symbol?: string
networkId?: string
transactionId?: string
amount?: string
amountInFiat?: number
clientFee?: number
restrictMultipleAccounts?: boolean
}
export interface LinkTokenRequest {
userId: string
integrationId?: string
restrictMultipleAccounts?: boolean
transferOptions?: {
toAddresses: Array<{
networkId: string
symbol: string
address: string
amount?: string
}>
transactionId?: string
amount?: string
amountInFiat?: number
clientFee?: number
}
}
export interface LinkTokenResponse {
content: {
linkToken: string
}
success: boolean
message?: string
}
export interface MeshConnectError extends Error {
statusCode?: number
response?: any
}
/**
* Generate a random user ID with timestamp
*/
function generateUserId(): string {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
/**
* Build the request payload for link token generation
*/
function buildPayload(
config: MeshConnectConfig,
options: TransferOptions = {},
): LinkTokenRequest {
const payload: LinkTokenRequest = {
userId: config.userId || generateUserId(),
}
// Add integration ID if provided
if (options.integrationId) {
payload.integrationId = options.integrationId
}
// Add restrict multiple accounts if specified
if (options.restrictMultipleAccounts) {
payload.restrictMultipleAccounts = true
}
// Add transfer options if address is provided
if (options.address) {
payload.transferOptions = {
toAddresses: [],
}
// Build toAddresses array
if (options.symbol && options.networkId) {
payload.transferOptions.toAddresses.push({
networkId: options.networkId,
symbol: options.symbol,
address: options.address,
amount: options.amount,
})
}
// Add transaction ID if provided
if (options.transactionId) {
payload.transferOptions.transactionId = options.transactionId
}
// Add amount in fiat if provided
if (options.amountInFiat !== undefined) {
payload.transferOptions.amountInFiat = options.amountInFiat
}
if (options.amount !== undefined) {
payload.transferOptions.amount = options.amount
}
// Add client fee if provided
if (options.clientFee !== undefined) {
payload.transferOptions.clientFee = options.clientFee
}
}
return payload
}
/**
* Make API request to generate link token
*/
async function makeApiRequest(
payload: LinkTokenRequest,
config: MeshConnectConfig,
): Promise<LinkTokenResponse> {
const apiUrl = getMeshConnectApiUrl(config.environment)
const endpoint = `${apiUrl}/api/v1/linktoken`
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Client-Id": config.clientId,
"X-Client-Secret": config.apiKey,
},
body: JSON.stringify(payload),
})
const responseData = await response.json()
if (!response.ok) {
const error = new Error(
`API request failed with status ${response.status}`,
) as MeshConnectError
error.statusCode = response.status
error.response = responseData
throw error
}
return responseData as LinkTokenResponse
} catch (error) {
if (error instanceof Error && "statusCode" in error) {
throw error
}
const meshError = new Error(`Network error: ${error}`) as MeshConnectError
throw meshError
}
}
/**
* Check API health status
*/
export async function checkApiHealth(
config: MeshConnectConfig,
): Promise<boolean> {
const apiUrl = getMeshConnectApiUrl(config.environment)
const endpoint = `${apiUrl}/api/v1/health`
try {
const response = await fetch(endpoint, {
method: "GET",
headers: {
"X-Client-Id": config.clientId,
"X-Client-Secret": config.apiKey,
},
})
return response.ok
} catch (error) {
logger.console.warn("API health check failed:", error)
return false
}
}
/**
* Generate a new link token using default credentials
* Convenience function that uses the pre-configured API key and client ID
*/
export async function createDefaultLinkToken(
options: Omit<TransferOptions, "userId"> & {
environment?: "sandbox" | "production"
} = {
environment: "sandbox",
},
): Promise<LinkTokenResponse> {
const apiKey = getMeshConnectApiKey(options.environment)
const clientId = getMeshConnectClientId()
return createNewLinkToken(apiKey, clientId, options)
}
/**
* Generate a new link token with random user ID
* This is the main function equivalent to createLinkNew.sh
*/
export async function createNewLinkToken(
apiKey: string,
clientId: string,
options: Omit<TransferOptions, "userId"> & {
environment?: "sandbox" | "production"
} = {
environment: "sandbox",
},
): Promise<LinkTokenResponse> {
const config: MeshConnectConfig = {
apiKey,
clientId,
environment: options.environment,
userId: generateUserId(),
}
// Validate required parameters
if (!apiKey || apiKey.length < 10) {
throw new Error("API key (client secret) is required and must be valid")
}
if (!clientId || clientId.length < 10) {
throw new Error("Client ID is required and must be valid")
}
// Build payload
const payload = buildPayload(config, options)
// Make API request
return await makeApiRequest(payload, config)
}
/**
* Generate a link token with specific user ID and transfer options
* This provides more control over the link token generation
*/
export async function createLinkToken(
config: MeshConnectConfig,
options: TransferOptions = {},
): Promise<LinkTokenResponse> {
// Validate required parameters
if (!config.apiKey || config.apiKey.length < 10) {
throw new Error("API key (client secret) is required and must be valid")
}
if (!config.clientId || config.clientId.length < 10) {
throw new Error("Client ID is required and must be valid")
}
// Build payload
const payload = buildPayload(config, options)
// Make API request
return await makeApiRequest(payload, config)
}
/**
* Common Network IDs (based on Mesh Connect API)
*/
export const MESH_NETWORK_IDS = {
ETHEREUM: "e3c7fdd8-b1fc-4e51-85ae-bb276e075611",
POLYGON: "7436e9d0-ba42-4d2b-b4c0-8e4e606b2c12",
ARBITRUM: "a34f2431-0ddd-4de4-bc22-4a8143287aeb",
BASE: "aa883b03-120d-477c-a588-37c2afd3ca71",
AVALANCHE: "bad16371-c22a-4bf4-a311-274d046cd760",
BSC: "3b8c1557-4f06-4f56-8e4e-8c4c8c4c8c4c",
} as const
/**
* Get Mesh Connect network ID from chain ID by fetching from API
* @param chainId - The EVM chain ID (e.g., 1 for Ethereum, 8453 for Base)
* @returns The corresponding Mesh Connect network ID, or undefined if not supported
*/
export async function getMeshNetworkIdFromChainId(
chainId: number | string,
environment: "sandbox" | "production" = "sandbox",
): Promise<string | undefined> {
try {
const apiUrl = getMeshConnectApiUrl(environment)
const apiKey = getMeshConnectApiKey(environment)
const clientId = getMeshConnectClientId()
const endpoint = `${apiUrl}/api/v1/transfers/managed/networks`
const response = await fetch(endpoint, {
method: "GET",
headers: {
"X-Client-Id": clientId,
"X-Client-Secret": apiKey,
"Content-Type": "application/json",
},
})
if (!response.ok) {
throw new Error(
`Failed to fetch networks: ${response.status} ${response.statusText}`,
)
}
const data = await response.json()
const networks = data.content?.networks || []
// Find network by chain ID
const targetChainId = chainId.toString()
const network = networks.find((net: any) => net.chainId === targetChainId)
return network?.id
} catch (error) {
logger.console.error(
"[meshconnect] Failed to fetch network ID for chain ID:",
chainId,
error,
)
return undefined
}
}
/**
* Get all supported integrations from Mesh Connect API
* @param environment - The environment to use (sandbox or production)
* @returns Array of supported integrations
*/
export async function getMeshIntegrations(
environment: "sandbox" | "production" = "sandbox",
): Promise<MeshIntegration[]> {
try {
const apiUrl = getMeshConnectApiUrl(environment)
const apiKey = getMeshConnectApiKey(environment)
const clientId = getMeshConnectClientId()
const endpoint = `${apiUrl}/api/v1/transfers/managed/integrations`
const response = await fetch(endpoint, {
method: "GET",
headers: {
"X-Client-Id": clientId,
"X-Client-Secret": apiKey,
"Content-Type": "application/json",
},
})
if (!response.ok) {
throw new Error(
`Failed to fetch integrations: ${response.status} ${response.statusText}`,
)
}
const data: IntegrationsResponse = await response.json()
return data.content?.integrations || []
} catch (error) {
logger.console.error("[meshconnect] Failed to fetch integrations:", error)
return []
}
}
// Cache for integrations by environment
const integrationsCache: Record<string, Promise<MeshIntegration[]>> = {}
/**
* Get integration ID for a specific exchange type
* @param exchangeType - The exchange type (e.g., "coinbase", "binance")
* @param environment - The environment to use (sandbox or production)
* @returns The integration ID for the specified exchange, or undefined if not found
*/
export async function getMeshIntegrationId(
exchangeType: string,
environment: "sandbox" | "production" = "sandbox",
): Promise<string | undefined> {
try {
// Use cached integrations or fetch them
const cacheKey = environment
if (!integrationsCache[cacheKey]) {
integrationsCache[cacheKey] = getMeshIntegrations(environment)
}
const integrations = await integrationsCache[cacheKey]
const integration = integrations.find(
(integration) =>
integration.type.toLowerCase() === exchangeType.toLowerCase(),
)
return integration?.id
} catch (error) {
logger.console.error(
`[meshconnect] Failed to get integration ID for ${exchangeType}:`,
error,
)
return undefined
}
}
/**
* Get Coinbase integration ID
* @param environment - The environment to use (sandbox or production)
* @returns The Coinbase integration ID, or undefined if not found
*/
export async function getCoinbaseIntegrationId(
environment: "sandbox" | "production" = "sandbox",
): Promise<string | undefined> {
return getMeshIntegrationId("coinbase", environment)
}
/**
* Get Binance integration ID
* @param environment - The environment to use (sandbox or production)
* @returns The Binance integration ID, or undefined if not found
*/
export async function getBinanceIntegrationId(
environment: "sandbox" | "production" = "sandbox",
): Promise<string | undefined> {
return getMeshIntegrationId("binanceInternationalDirect", environment)
}
/**
* Get Bitfinex integration ID
* @param environment - The environment to use (sandbox or production)
* @returns The Bitfinex integration ID, or undefined if not found
*/
export async function getBitfinexIntegrationId(
environment: "sandbox" | "production" = "sandbox",
): Promise<string | undefined> {
return getMeshIntegrationId("bitfinexDirect", environment)
}
/**
* Common Integration IDs (based on Mesh Connect API)
*/
export const MESH_INTEGRATION_IDS = {
COINBASE: "9226e5c2-ebc3-4fdd-94f6-ed52cdce1420",
BINANCE: "34aeb688-decb-485f-9d80-b66466783394",
METAMASK: "45bfc688-decb-485f-9d80-b66466783395",
} as const
export interface MeshIntegration {
id: string
type: string
networks: Array<{
supportedDevices: string[]
supportedTokens: string[]
nativeSymbol: string
id: string
name: string
chainId: string
logoUrl: string
}>
supportsOutgoingTransfers: boolean
supportsIncomingTransfers: boolean
}
export interface IntegrationsResponse {
content: {
integrations: MeshIntegration[]
}
status: string
message: string
errorHash: string
errorType: string
}
/**
* Utility function to extract just the link token from the response
*/
export function extractLinkToken(response: LinkTokenResponse): string {
return response.content.linkToken
}
// Default export for convenience
export default {
createNewLinkToken,
createDefaultLinkToken,
createLinkToken,
checkApiHealth,
extractLinkToken,
getMeshIntegrations,
getMeshIntegrationId,
getCoinbaseIntegrationId,
getBinanceIntegrationId,
getBitfinexIntegrationId,
}
export function getMeshConnectClientId(): string {
return DEFAULT_CLIENT_ID
}
export function getMeshConnectEnvironment(): "sandbox" | "production" {
return DEFAULT_ENVIRONMENT
}
const networkIdMap: Record<string, string> = {
"1": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611",
"137": "7436e9d0-ba42-4d2b-b4c0-8e4e606b2c12",
"42161": "2a9c1557-4f06-4f56-8e4e-8c4c8c4c8c4c",
"56": "3b8c1557-4f06-4f56-8e4e-8c4c8c4c8c4c",
}
export function getMeshConnectNetworkId(chainId: string): string {
return networkIdMap[chainId] || "e3c7fdd8-b1fc-4e51-85ae-bb276e075611"
}