repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | // ═══════════════════════════════════════════════════════════════ |
| #2 | // CROSSMINT SERVICE - Wallet operations via Crossmint API |
| #3 | // Supports: MPC Wallets, Smart Wallets, and GOAT SDK Integration |
| #4 | // ═══════════════════════════════════════════════════════════════ |
| #5 | |
| #6 | import type { Env } from '../index'; |
| #7 | |
| #8 | // ───────────────────────────────────────────────── |
| #9 | // TYPES |
| #10 | // ───────────────────────────────────────────────── |
| #11 | |
| #12 | export type WalletType = 'smart' | 'mpc' | 'custodial'; |
| #13 | export type ChainType = 'solana' | 'solana-devnet'; |
| #14 | export type SignerType = 'api-key' | 'delegated' | 'admin'; |
| #15 | |
| #16 | interface CrossmintWallet { |
| #17 | address: string; |
| #18 | chain: string; |
| #19 | type: string; |
| #20 | locator: string; |
| #21 | createdAt?: string; |
| #22 | // Smart wallet specific |
| #23 | adminSignerAddress?: string; |
| #24 | walletType?: WalletType; |
| #25 | } |
| #26 | |
| #27 | export interface SmartWallet extends CrossmintWallet { |
| #28 | walletType: 'smart'; |
| #29 | adminSignerAddress: string; |
| #30 | delegatedSignerId?: string; |
| #31 | delegatedSignerStatus?: 'pending' | 'active' | 'rejected'; |
| #32 | } |
| #33 | |
| #34 | export interface MpcWallet extends CrossmintWallet { |
| #35 | walletType: 'mpc'; |
| #36 | linkedUser: string; |
| #37 | } |
| #38 | |
| #39 | interface TokenBalance { |
| #40 | token: string; |
| #41 | symbol: string; |
| #42 | amount: string; |
| #43 | decimals: number; |
| #44 | usdValue?: string; |
| #45 | } |
| #46 | |
| #47 | interface WalletBalances { |
| #48 | nativeToken: TokenBalance; |
| #49 | usdc?: TokenBalance; |
| #50 | tokens: TokenBalance[]; |
| #51 | } |
| #52 | |
| #53 | interface TransferResult { |
| #54 | id: string; |
| #55 | status: 'pending' | 'success' | 'failed'; |
| #56 | hash?: string; |
| #57 | explorerLink?: string; |
| #58 | } |
| #59 | |
| #60 | interface FundResult { |
| #61 | balances: WalletBalances; |
| #62 | transactionId?: string; |
| #63 | } |
| #64 | |
| #65 | // API versions |
| #66 | const API_VERSION = '2025-06-09'; |
| #67 | const API_VERSION_LEGACY = 'v1-alpha2'; |
| #68 | |
| #69 | // ───────────────────────────────────────────────── |
| #70 | // CROSSMINT SERVICE CLASS |
| #71 | // ───────────────────────────────────────────────── |
| #72 | |
| #73 | export class CrossmintService { |
| #74 | private apiKey: string; |
| #75 | private environment: 'staging' | 'production'; |
| #76 | private baseUrl: string; |
| #77 | |
| #78 | constructor(private env: Env) { |
| #79 | this.apiKey = env.CROSSMINT_SERVERSIDE_API_KEY || ''; |
| #80 | this.environment = this.apiKey.startsWith('sk_staging_') ? 'staging' : 'production'; |
| #81 | this.baseUrl = this.environment === 'staging' |
| #82 | ? 'https://staging.crossmint.com' |
| #83 | : 'https://www.crossmint.com'; |
| #84 | } |
| #85 | |
| #86 | // ───────────────────────────────────────────────── |
| #87 | // API REQUEST HELPER |
| #88 | // ───────────────────────────────────────────────── |
| #89 | |
| #90 | private async apiRequest<T>( |
| #91 | method: 'GET' | 'POST' | 'PUT' | 'DELETE', |
| #92 | path: string, |
| #93 | body?: unknown, |
| #94 | version: string = API_VERSION |
| #95 | ): Promise<T> { |
| #96 | if (!this.apiKey) { |
| #97 | throw new Error('Crossmint API key not configured'); |
| #98 | } |
| #99 | |
| #100 | const url = `${this.baseUrl}/api/${version}${path}`; |
| #101 | |
| #102 | const options: RequestInit = { |
| #103 | method, |
| #104 | headers: { |
| #105 | 'X-API-KEY': this.apiKey, |
| #106 | 'Content-Type': 'application/json', |
| #107 | }, |
| #108 | }; |
| #109 | |
| #110 | if (body && method !== 'GET') { |
| #111 | options.body = JSON.stringify(body); |
| #112 | } |
| #113 | |
| #114 | const res = await fetch(url, options); |
| #115 | |
| #116 | if (!res.ok) { |
| #117 | const errorText = await res.text(); |
| #118 | let errorMessage = `Crossmint API error: ${res.status}`; |
| #119 | try { |
| #120 | const errorJson = JSON.parse(errorText); |
| #121 | errorMessage = errorJson.message || errorJson.error || errorMessage; |
| #122 | } catch { |
| #123 | errorMessage = errorText || errorMessage; |
| #124 | } |
| #125 | throw new Error(errorMessage); |
| #126 | } |
| #127 | |
| #128 | return res.json(); |
| #129 | } |
| #130 | |
| #131 | // ───────────────────────────────────────────────── |
| #132 | // LOCATOR HELPERS |
| #133 | // ───────────────────────────────────────────────── |
| #134 | |
| #135 | private buildLocator(identifier: string, chain: 'solana' | 'solana-devnet'): string { |
| #136 | // Map chain to wallet type |
| #137 | const walletType = chain === 'solana-devnet' ? 'solana-mpc-wallet' : 'solana-mpc-wallet'; |
| #138 | |
| #139 | // If it's an email |
| #140 | if (identifier.includes('@')) { |
| #141 | return `email:${identifier}:${walletType}`; |
| #142 | } |
| #143 | // If it's a Solana address (base58, 32-44 chars) |
| #144 | if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(identifier)) { |
| #145 | return identifier; |
| #146 | } |
| #147 | // If it's a phone number |
| #148 | if (identifier.startsWith('+')) { |
| #149 | return `phoneNumber:${identifier}:${walletType}`; |
| #150 | } |
| #151 | // Default to userId |
| #152 | return `userId:${identifier}:${walletType}`; |
| #153 | } |
| #154 | |
| #155 | // ═══════════════════════════════════════════════════ |
| #156 | // WALLET OPERATIONS |
| #157 | // ═══════════════════════════════════════════════════ |
| #158 | |
| #159 | async createWallet(params: { |
| #160 | identifier: string; |
| #161 | chain?: 'solana' | 'solana-devnet'; |
| #162 | alias?: string; |
| #163 | }): Promise<CrossmintWallet> { |
| #164 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #165 | const locator = this.buildLocator(params.identifier, chain); |
| #166 | |
| #167 | // Use legacy API (v1-alpha2) for Solana wallet creation |
| #168 | const body: Record<string, unknown> = { |
| #169 | type: 'solana-custodial-wallet', |
| #170 | linkedUser: locator, |
| #171 | }; |
| #172 | |
| #173 | // IMPORTANT: Use legacy API version for Solana wallets |
| #174 | const result = await this.apiRequest<{ |
| #175 | address?: string; |
| #176 | publicKey?: string; |
| #177 | createdAt?: string; |
| #178 | locator?: string; |
| #179 | linkedUser?: string; |
| #180 | type?: string; |
| #181 | chain?: string; |
| #182 | }>('POST', '/wallets', body, API_VERSION_LEGACY); |
| #183 | |
| #184 | return { |
| #185 | address: result.address || result.publicKey || '', |
| #186 | chain: result.chain || chain, |
| #187 | type: result.type || 'solana-mpc-wallet', |
| #188 | locator: result.linkedUser || locator, |
| #189 | createdAt: result.createdAt, |
| #190 | }; |
| #191 | } |
| #192 | |
| #193 | async getWallet( |
| #194 | identifier: string, |
| #195 | chain: 'solana' | 'solana-devnet' |
| #196 | ): Promise<CrossmintWallet | null> { |
| #197 | const locator = this.buildLocator(identifier, chain); |
| #198 | |
| #199 | try { |
| #200 | // Use legacy API for Solana wallets |
| #201 | const result = await this.apiRequest<{ |
| #202 | address?: string; |
| #203 | publicKey?: string; |
| #204 | type?: string; |
| #205 | linkedUser?: string; |
| #206 | }>('GET', `/wallets/${encodeURIComponent(locator)}`, undefined, API_VERSION_LEGACY); |
| #207 | |
| #208 | return { |
| #209 | address: result.address || result.publicKey || '', |
| #210 | chain, |
| #211 | type: result.type || 'solana-mpc-wallet', |
| #212 | locator: result.linkedUser || locator, |
| #213 | }; |
| #214 | } catch (error) { |
| #215 | if (error instanceof Error && error.message.includes('404')) { |
| #216 | return null; |
| #217 | } |
| #218 | throw error; |
| #219 | } |
| #220 | } |
| #221 | |
| #222 | async getBalances( |
| #223 | identifier: string, |
| #224 | chain: 'solana' | 'solana-devnet', |
| #225 | tokens: string[] = ['usdc'] |
| #226 | ): Promise<WalletBalances> { |
| #227 | const locator = this.buildLocator(identifier, chain); |
| #228 | const tokenParams = tokens.join(','); |
| #229 | |
| #230 | // Use legacy API for Solana wallet balances |
| #231 | const result = await this.apiRequest<{ |
| #232 | nativeToken?: { amount?: string; usdValue?: string }; |
| #233 | usdc?: { amount?: string; usdValue?: string }; |
| #234 | tokens?: Array<{ |
| #235 | token?: string; |
| #236 | mint?: string; |
| #237 | symbol?: string; |
| #238 | amount?: string; |
| #239 | decimals?: number; |
| #240 | usdValue?: string; |
| #241 | }>; |
| #242 | }>('GET', `/wallets/${encodeURIComponent(locator)}/balances?tokens=${tokenParams}&chains=${chain}`, undefined, API_VERSION_LEGACY); |
| #243 | |
| #244 | return { |
| #245 | nativeToken: { |
| #246 | token: 'SOL', |
| #247 | symbol: 'SOL', |
| #248 | amount: result.nativeToken?.amount || '0', |
| #249 | decimals: 9, |
| #250 | usdValue: result.nativeToken?.usdValue, |
| #251 | }, |
| #252 | usdc: result.usdc ? { |
| #253 | token: 'USDC', |
| #254 | symbol: 'USDC', |
| #255 | amount: result.usdc.amount || '0', |
| #256 | decimals: 6, |
| #257 | usdValue: result.usdc.usdValue, |
| #258 | } : undefined, |
| #259 | tokens: (result.tokens || []).map(t => ({ |
| #260 | token: t.token || t.mint || '', |
| #261 | symbol: t.symbol || '', |
| #262 | amount: t.amount || '0', |
| #263 | decimals: t.decimals || 9, |
| #264 | usdValue: t.usdValue, |
| #265 | })), |
| #266 | }; |
| #267 | } |
| #268 | |
| #269 | async fundWallet( |
| #270 | identifier: string, |
| #271 | amount: number = 10, |
| #272 | chain: 'solana-devnet' = 'solana-devnet' |
| #273 | ): Promise<FundResult> { |
| #274 | if (this.environment !== 'staging') { |
| #275 | throw new Error('Staging fund is only available in staging environment'); |
| #276 | } |
| #277 | |
| #278 | const locator = this.buildLocator(identifier, chain); |
| #279 | |
| #280 | const result = await this.apiRequest<{ |
| #281 | balances?: WalletBalances; |
| #282 | transactionId?: string; |
| #283 | }>( |
| #284 | 'POST', |
| #285 | `/wallets/${encodeURIComponent(locator)}/balances`, |
| #286 | { |
| #287 | amount, |
| #288 | token: 'usdxm', |
| #289 | chain, |
| #290 | }, |
| #291 | API_VERSION_LEGACY |
| #292 | ); |
| #293 | |
| #294 | return { |
| #295 | balances: result.balances || { |
| #296 | nativeToken: { token: 'SOL', symbol: 'SOL', amount: '0', decimals: 9 }, |
| #297 | tokens: [], |
| #298 | }, |
| #299 | transactionId: result.transactionId, |
| #300 | }; |
| #301 | } |
| #302 | |
| #303 | async transfer(params: { |
| #304 | fromIdentifier: string; |
| #305 | toAddress: string; |
| #306 | token: string; |
| #307 | amount: string; |
| #308 | chain?: 'solana' | 'solana-devnet'; |
| #309 | }): Promise<TransferResult> { |
| #310 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #311 | const locator = this.buildLocator(params.fromIdentifier, chain); |
| #312 | const tokenLocator = `${chain}:${params.token}`; |
| #313 | |
| #314 | const result = await this.apiRequest<{ |
| #315 | id: string; |
| #316 | status?: string; |
| #317 | onChain?: { |
| #318 | txId?: string; |
| #319 | explorerLink?: string; |
| #320 | }; |
| #321 | hash?: string; |
| #322 | }>( |
| #323 | 'POST', |
| #324 | `/wallets/${encodeURIComponent(locator)}/tokens/${tokenLocator}/transfers`, |
| #325 | { |
| #326 | recipient: params.toAddress, |
| #327 | amount: params.amount, |
| #328 | signer: 'api-key', |
| #329 | } |
| #330 | ); |
| #331 | |
| #332 | return { |
| #333 | id: result.id, |
| #334 | status: (result.status as 'pending' | 'success' | 'failed') || 'pending', |
| #335 | hash: result.onChain?.txId || result.hash, |
| #336 | explorerLink: result.onChain?.explorerLink, |
| #337 | }; |
| #338 | } |
| #339 | |
| #340 | // ═══════════════════════════════════════════════════ |
| #341 | // UTILITY METHODS |
| #342 | // ═══════════════════════════════════════════════════ |
| #343 | |
| #344 | // ═══════════════════════════════════════════════════ |
| #345 | // ADDRESS-BASED LOOKUPS (for MPC wallets) |
| #346 | // ═══════════════════════════════════════════════════ |
| #347 | |
| #348 | async getWalletByAddress(address: string): Promise<CrossmintWallet | null> { |
| #349 | try { |
| #350 | // Use legacy API for Solana wallets - lookup by address directly |
| #351 | const result = await this.apiRequest<{ |
| #352 | address?: string; |
| #353 | publicKey?: string; |
| #354 | type?: string; |
| #355 | linkedUser?: string; |
| #356 | chain?: string; |
| #357 | }>('GET', `/wallets/${address}`, undefined, API_VERSION_LEGACY); |
| #358 | |
| #359 | return { |
| #360 | address: result.address || result.publicKey || address, |
| #361 | chain: result.chain || 'solana', |
| #362 | type: result.type || 'solana-mpc-wallet', |
| #363 | locator: result.linkedUser || '', |
| #364 | }; |
| #365 | } catch (error) { |
| #366 | if (error instanceof Error && error.message.includes('404')) { |
| #367 | return null; |
| #368 | } |
| #369 | throw error; |
| #370 | } |
| #371 | } |
| #372 | |
| #373 | async getBalancesByAddress( |
| #374 | address: string, |
| #375 | chain: 'solana' | 'solana-devnet', |
| #376 | tokens: string[] = ['usdc'] |
| #377 | ): Promise<WalletBalances> { |
| #378 | const tokenParams = tokens.join(','); |
| #379 | // Crossmint API uses 'solana' for both mainnet and devnet in balance queries |
| #380 | const chainParam = chain === 'solana-devnet' ? 'solana' : chain; |
| #381 | |
| #382 | // Use legacy API for Solana wallet balances - lookup by address directly |
| #383 | const result = await this.apiRequest<{ |
| #384 | nativeToken?: { amount?: string; usdValue?: string }; |
| #385 | usdc?: { amount?: string; usdValue?: string }; |
| #386 | tokens?: Array<{ |
| #387 | token?: string; |
| #388 | mint?: string; |
| #389 | symbol?: string; |
| #390 | amount?: string; |
| #391 | decimals?: number; |
| #392 | usdValue?: string; |
| #393 | }>; |
| #394 | }>('GET', `/wallets/${address}/balances?tokens=${tokenParams}&chains=${chainParam}`, undefined, API_VERSION_LEGACY); |
| #395 | |
| #396 | return { |
| #397 | nativeToken: { |
| #398 | token: 'SOL', |
| #399 | symbol: 'SOL', |
| #400 | amount: result.nativeToken?.amount || '0', |
| #401 | decimals: 9, |
| #402 | usdValue: result.nativeToken?.usdValue, |
| #403 | }, |
| #404 | usdc: result.usdc ? { |
| #405 | token: 'USDC', |
| #406 | symbol: 'USDC', |
| #407 | amount: result.usdc.amount || '0', |
| #408 | decimals: 6, |
| #409 | usdValue: result.usdc.usdValue, |
| #410 | } : undefined, |
| #411 | tokens: (result.tokens || []).map(t => ({ |
| #412 | token: t.token || t.mint || '', |
| #413 | symbol: t.symbol || '', |
| #414 | amount: t.amount || '0', |
| #415 | decimals: t.decimals || 9, |
| #416 | usdValue: t.usdValue, |
| #417 | })), |
| #418 | }; |
| #419 | } |
| #420 | |
| #421 | // ═══════════════════════════════════════════════════ |
| #422 | // UTILITY METHODS |
| #423 | // ═══════════════════════════════════════════════════ |
| #424 | |
| #425 | isConfigured(): boolean { |
| #426 | return !!this.apiKey; |
| #427 | } |
| #428 | |
| #429 | getInfo(): { |
| #430 | configured: boolean; |
| #431 | environment: 'staging' | 'production'; |
| #432 | baseUrl: string; |
| #433 | hasClientKey: boolean; |
| #434 | } { |
| #435 | return { |
| #436 | configured: this.isConfigured(), |
| #437 | environment: this.environment, |
| #438 | baseUrl: this.baseUrl, |
| #439 | hasClientKey: !!this.env.CROSSMINT_CLIENTSIDE_API_KEY, |
| #440 | }; |
| #441 | } |
| #442 | |
| #443 | isValidSolanaAddress(address: string): boolean { |
| #444 | return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address); |
| #445 | } |
| #446 | |
| #447 | // ═══════════════════════════════════════════════════ |
| #448 | // SMART WALLET OPERATIONS |
| #449 | // ═══════════════════════════════════════════════════ |
| #450 | |
| #451 | /** |
| #452 | * Create a Solana Smart Wallet with admin signer |
| #453 | * Smart wallets use account abstraction for enhanced security |
| #454 | */ |
| #455 | async createSmartWallet(params: { |
| #456 | adminSignerAddress?: string; |
| #457 | linkedUser?: string; |
| #458 | chain?: ChainType; |
| #459 | }): Promise<SmartWallet> { |
| #460 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #461 | |
| #462 | const body: Record<string, unknown> = { |
| #463 | type: 'solana-smart-wallet', |
| #464 | config: { |
| #465 | adminSigner: params.adminSignerAddress |
| #466 | ? { type: 'solana-keypair', address: params.adminSignerAddress } |
| #467 | : { type: 'solana-fireblocks-custodial' }, |
| #468 | }, |
| #469 | }; |
| #470 | |
| #471 | if (params.linkedUser) { |
| #472 | body.linkedUser = params.linkedUser; |
| #473 | } |
| #474 | |
| #475 | const result = await this.apiRequest<{ |
| #476 | address?: string; |
| #477 | publicKey?: string; |
| #478 | createdAt?: string; |
| #479 | type?: string; |
| #480 | config?: { |
| #481 | adminSigner?: { |
| #482 | address?: string; |
| #483 | }; |
| #484 | }; |
| #485 | }>('POST', '/wallets', body); |
| #486 | |
| #487 | return { |
| #488 | address: result.address || result.publicKey || '', |
| #489 | chain, |
| #490 | type: 'solana-smart-wallet', |
| #491 | walletType: 'smart', |
| #492 | locator: result.address || '', |
| #493 | adminSignerAddress: result.config?.adminSigner?.address || params.adminSignerAddress || '', |
| #494 | createdAt: result.createdAt, |
| #495 | }; |
| #496 | } |
| #497 | |
| #498 | /** |
| #499 | * Create a Solana MPC Wallet (Multi-Party Computation) |
| #500 | * MPC wallets are custodial wallets managed by Crossmint |
| #501 | */ |
| #502 | async createMpcWallet(params: { |
| #503 | identifier: string; |
| #504 | chain?: ChainType; |
| #505 | alias?: string; |
| #506 | }): Promise<MpcWallet> { |
| #507 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #508 | const locator = this.buildLocator(params.identifier, chain); |
| #509 | |
| #510 | const body: Record<string, unknown> = { |
| #511 | type: 'solana-mpc-wallet', |
| #512 | linkedUser: locator, |
| #513 | }; |
| #514 | |
| #515 | if (params.alias) { |
| #516 | body.config = { alias: params.alias }; |
| #517 | } |
| #518 | |
| #519 | const result = await this.apiRequest<{ |
| #520 | address?: string; |
| #521 | publicKey?: string; |
| #522 | createdAt?: string; |
| #523 | linkedUser?: string; |
| #524 | }>('POST', '/wallets', body); |
| #525 | |
| #526 | return { |
| #527 | address: result.address || result.publicKey || '', |
| #528 | chain, |
| #529 | type: 'solana-mpc-wallet', |
| #530 | walletType: 'mpc', |
| #531 | locator, |
| #532 | linkedUser: result.linkedUser || params.identifier, |
| #533 | createdAt: result.createdAt, |
| #534 | }; |
| #535 | } |
| #536 | |
| #537 | /** |
| #538 | * Get or create a smart wallet (idempotent) |
| #539 | */ |
| #540 | async getOrCreateSmartWallet(params: { |
| #541 | adminSignerAddress?: string; |
| #542 | linkedUser?: string; |
| #543 | chain?: ChainType; |
| #544 | }): Promise<SmartWallet> { |
| #545 | // For smart wallets, we always create new if no linkedUser |
| #546 | if (!params.linkedUser) { |
| #547 | return this.createSmartWallet(params); |
| #548 | } |
| #549 | |
| #550 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #551 | |
| #552 | try { |
| #553 | const existing = await this.getWallet(params.linkedUser, chain); |
| #554 | if (existing) { |
| #555 | return { |
| #556 | ...existing, |
| #557 | walletType: 'smart', |
| #558 | adminSignerAddress: params.adminSignerAddress || '', |
| #559 | }; |
| #560 | } |
| #561 | } catch { |
| #562 | // Wallet doesn't exist, create it |
| #563 | } |
| #564 | |
| #565 | return this.createSmartWallet(params); |
| #566 | } |
| #567 | |
| #568 | // ═══════════════════════════════════════════════════ |
| #569 | // DELEGATED SIGNER OPERATIONS |
| #570 | // ═══════════════════════════════════════════════════ |
| #571 | |
| #572 | /** |
| #573 | * Register a delegated signer for a smart wallet |
| #574 | */ |
| #575 | async registerDelegatedSigner(params: { |
| #576 | walletAddress: string; |
| #577 | signerAddress: string; |
| #578 | expiresAt?: string; |
| #579 | }): Promise<{ |
| #580 | id: string; |
| #581 | status: 'pending' | 'active' | 'rejected'; |
| #582 | message?: string; |
| #583 | targetSignerLocator?: string; |
| #584 | }> { |
| #585 | const body = { |
| #586 | signer: `solana-keypair:${params.signerAddress}`, |
| #587 | ...(params.expiresAt && { expiresAt: params.expiresAt }), |
| #588 | }; |
| #589 | |
| #590 | const result = await this.apiRequest<{ |
| #591 | id: string; |
| #592 | status: string; |
| #593 | message?: string; |
| #594 | targetSignerLocator?: string; |
| #595 | }>('POST', `/wallets/${params.walletAddress}/signers`, body); |
| #596 | |
| #597 | return { |
| #598 | id: result.id, |
| #599 | status: (result.status as 'pending' | 'active' | 'rejected') || 'pending', |
| #600 | message: result.message, |
| #601 | targetSignerLocator: result.targetSignerLocator, |
| #602 | }; |
| #603 | } |
| #604 | |
| #605 | /** |
| #606 | * Approve a delegated signer request |
| #607 | */ |
| #608 | async approveDelegatedSigner(params: { |
| #609 | walletAddress: string; |
| #610 | signerId: string; |
| #611 | signerLocator: string; |
| #612 | signature: unknown; |
| #613 | metadata?: Record<string, unknown>; |
| #614 | }): Promise<{ status: string }> { |
| #615 | const approval: Record<string, unknown> = { |
| #616 | signer: params.signerLocator, |
| #617 | signature: params.signature, |
| #618 | }; |
| #619 | |
| #620 | if (params.metadata) { |
| #621 | approval.metadata = params.metadata; |
| #622 | } |
| #623 | |
| #624 | const body = { |
| #625 | approvals: [approval], |
| #626 | }; |
| #627 | |
| #628 | const result = await this.apiRequest<{ status: string }>( |
| #629 | 'POST', |
| #630 | `/wallets/${params.walletAddress}/signers/${params.signerId}/approvals`, |
| #631 | body |
| #632 | ); |
| #633 | |
| #634 | return result; |
| #635 | } |
| #636 | |
| #637 | // ═══════════════════════════════════════════════════ |
| #638 | // GOAT SDK TOOL OPERATIONS |
| #639 | // ═══════════════════════════════════════════════════ |
| #640 | |
| #641 | /** |
| #642 | * Get balance for a wallet (GOAT: getBalance) |
| #643 | */ |
| #644 | async goatGetBalance(params: { |
| #645 | address: string; |
| #646 | token?: string; |
| #647 | chain?: ChainType; |
| #648 | }): Promise<{ |
| #649 | token: string; |
| #650 | symbol: string; |
| #651 | amount: string; |
| #652 | decimals: number; |
| #653 | usdValue?: string; |
| #654 | }> { |
| #655 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #656 | const token = params.token || 'sol'; |
| #657 | |
| #658 | const balances = await this.getBalancesByAddress(params.address, chain, [token]); |
| #659 | |
| #660 | if (token.toLowerCase() === 'sol') { |
| #661 | return balances.nativeToken; |
| #662 | } else if (token.toLowerCase() === 'usdc' && balances.usdc) { |
| #663 | return balances.usdc; |
| #664 | } |
| #665 | |
| #666 | const tokenBalance = balances.tokens.find( |
| #667 | t => t.token.toLowerCase() === token.toLowerCase() || t.symbol.toLowerCase() === token.toLowerCase() |
| #668 | ); |
| #669 | |
| #670 | if (tokenBalance) { |
| #671 | return tokenBalance; |
| #672 | } |
| #673 | |
| #674 | // Return zero balance if not found |
| #675 | return { |
| #676 | token, |
| #677 | symbol: token.toUpperCase(), |
| #678 | amount: '0', |
| #679 | decimals: 9, |
| #680 | }; |
| #681 | } |
| #682 | |
| #683 | /** |
| #684 | * Transfer tokens (GOAT: transfer) |
| #685 | */ |
| #686 | async goatTransfer(params: { |
| #687 | fromAddress: string; |
| #688 | toAddress: string; |
| #689 | token: string; |
| #690 | amount: string; |
| #691 | chain?: ChainType; |
| #692 | signerType?: SignerType; |
| #693 | }): Promise<TransferResult> { |
| #694 | const chain = params.chain || (this.environment === 'staging' ? 'solana-devnet' : 'solana'); |
| #695 | const tokenLocator = `${chain}:${params.token}`; |
| #696 | |
| #697 | const result = await this.apiRequest<{ |
| #698 | id: string; |
| #699 | status?: string; |
| #700 | onChain?: { |
| #701 | txId?: string; |
| #702 | explorerLink?: string; |
| #703 | }; |
| #704 | hash?: string; |
| #705 | }>( |
| #706 | 'POST', |
| #707 | `/wallets/${params.fromAddress}/tokens/${tokenLocator}/transfers`, |
| #708 | { |
| #709 | recipient: params.toAddress, |
| #710 | amount: params.amount, |
| #711 | signer: params.signerType || 'api-key', |
| #712 | } |
| #713 | ); |
| #714 | |
| #715 | return { |
| #716 | id: result.id, |
| #717 | status: (result.status as 'pending' | 'success' | 'failed') || 'pending', |
| #718 | hash: result.onChain?.txId || result.hash, |
| #719 | explorerLink: result.onChain?.explorerLink, |
| #720 | }; |
| #721 | } |
| #722 | |
| #723 | /** |
| #724 | * Get token price from CoinGecko (GOAT: getTokenPrice) |
| #725 | */ |
| #726 | async goatGetTokenPrice(params: { |
| #727 | token: string; |
| #728 | currency?: string; |
| #729 | }): Promise<{ |
| #730 | token: string; |
| #731 | price: number; |
| #732 | currency: string; |
| #733 | change24h?: number; |
| #734 | }> { |
| #735 | const currency = params.currency || 'usd'; |
| #736 | const tokenMap: Record<string, string> = { |
| #737 | 'sol': 'solana', |
| #738 | 'usdc': 'usd-coin', |
| #739 | 'bonk': 'bonk', |
| #740 | 'jup': 'jupiter', |
| #741 | 'ray': 'raydium', |
| #742 | }; |
| #743 | |
| #744 | const coinId = tokenMap[params.token.toLowerCase()] || params.token.toLowerCase(); |
| #745 | |
| #746 | try { |
| #747 | const response = await fetch( |
| #748 | `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=${currency}&include_24hr_change=true` |
| #749 | ); |
| #750 | |
| #751 | if (!response.ok) { |
| #752 | throw new Error('CoinGecko API error'); |
| #753 | } |
| #754 | |
| #755 | const data = await response.json() as Record<string, { [key: string]: number }>; |
| #756 | const tokenData = data[coinId]; |
| #757 | |
| #758 | if (!tokenData) { |
| #759 | throw new Error(`Token ${params.token} not found`); |
| #760 | } |
| #761 | |
| #762 | return { |
| #763 | token: params.token, |
| #764 | price: tokenData[currency] || 0, |
| #765 | currency, |
| #766 | change24h: tokenData[`${currency}_24h_change`], |
| #767 | }; |
| #768 | } catch (error) { |
| #769 | console.error('[GOAT] getTokenPrice error:', error); |
| #770 | throw error; |
| #771 | } |
| #772 | } |
| #773 | |
| #774 | /** |
| #775 | * Get swap quote from Jupiter (GOAT: getSwapQuote) |
| #776 | */ |
| #777 | async goatGetSwapQuote(params: { |
| #778 | inputToken: string; |
| #779 | outputToken: string; |
| #780 | amount: string; |
| #781 | slippageBps?: number; |
| #782 | }): Promise<{ |
| #783 | inputToken: string; |
| #784 | outputToken: string; |
| #785 | inputAmount: string; |
| #786 | outputAmount: string; |
| #787 | priceImpact: string; |
| #788 | route: string[]; |
| #789 | }> { |
| #790 | // Token mint addresses for common tokens (devnet/mainnet) |
| #791 | const tokenMints: Record<string, { mainnet: string; devnet: string }> = { |
| #792 | sol: { |
| #793 | mainnet: 'So11111111111111111111111111111111111111112', |
| #794 | devnet: 'So11111111111111111111111111111111111111112', |
| #795 | }, |
| #796 | usdc: { |
| #797 | mainnet: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', |
| #798 | devnet: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', |
| #799 | }, |
| #800 | }; |
| #801 | |
| #802 | const inputMint = tokenMints[params.inputToken.toLowerCase()] |
| #803 | ? tokenMints[params.inputToken.toLowerCase()][this.environment === 'staging' ? 'devnet' : 'mainnet'] |
| #804 | : params.inputToken; |
| #805 | |
| #806 | const outputMint = tokenMints[params.outputToken.toLowerCase()] |
| #807 | ? tokenMints[params.outputToken.toLowerCase()][this.environment === 'staging' ? 'devnet' : 'mainnet'] |
| #808 | : params.outputToken; |
| #809 | |
| #810 | const slippageBps = params.slippageBps || 50; // 0.5% default |
| #811 | |
| #812 | try { |
| #813 | const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${params.amount}&slippageBps=${slippageBps}`; |
| #814 | |
| #815 | const response = await fetch(quoteUrl); |
| #816 | if (!response.ok) { |
| #817 | throw new Error('Jupiter API error'); |
| #818 | } |
| #819 | |
| #820 | const data = await response.json() as { |
| #821 | inAmount: string; |
| #822 | outAmount: string; |
| #823 | priceImpactPct: string; |
| #824 | routePlan: Array<{ swapInfo: { label: string } }>; |
| #825 | }; |
| #826 | |
| #827 | return { |
| #828 | inputToken: params.inputToken, |
| #829 | outputToken: params.outputToken, |
| #830 | inputAmount: data.inAmount, |
| #831 | outputAmount: data.outAmount, |
| #832 | priceImpact: data.priceImpactPct, |
| #833 | route: data.routePlan?.map(r => r.swapInfo?.label).filter(Boolean) || [], |
| #834 | }; |
| #835 | } catch (error) { |
| #836 | console.error('[GOAT] getSwapQuote error:', error); |
| #837 | throw error; |
| #838 | } |
| #839 | } |
| #840 | |
| #841 | /** |
| #842 | * Request devnet SOL airdrop (GOAT: airdropDevnet) |
| #843 | */ |
| #844 | async goatAirdropDevnet(params: { |
| #845 | address: string; |
| #846 | amount?: number; |
| #847 | }): Promise<{ |
| #848 | success: boolean; |
| #849 | signature?: string; |
| #850 | amount: number; |
| #851 | }> { |
| #852 | if (this.environment !== 'staging') { |
| #853 | throw new Error('Airdrop only available on devnet'); |
| #854 | } |
| #855 | |
| #856 | const amount = params.amount || 1; // Default 1 SOL |
| #857 | const lamports = amount * 1_000_000_000; |
| #858 | |
| #859 | try { |
| #860 | const response = await fetch('https://api.devnet.solana.com', { |
| #861 | method: 'POST', |
| #862 | headers: { 'Content-Type': 'application/json' }, |
| #863 | body: JSON.stringify({ |
| #864 | jsonrpc: '2.0', |
| #865 | id: 1, |
| #866 | method: 'requestAirdrop', |
| #867 | params: [params.address, lamports], |
| #868 | }), |
| #869 | }); |
| #870 | |
| #871 | const data = await response.json() as { |
| #872 | result?: string; |
| #873 | error?: { message: string }; |
| #874 | }; |
| #875 | |
| #876 | if (data.error) { |
| #877 | throw new Error(data.error.message); |
| #878 | } |
| #879 | |
| #880 | return { |
| #881 | success: true, |
| #882 | signature: data.result, |
| #883 | amount, |
| #884 | }; |
| #885 | } catch (error) { |
| #886 | console.error('[GOAT] airdropDevnet error:', error); |
| #887 | throw error; |
| #888 | } |
| #889 | } |
| #890 | |
| #891 | /** |
| #892 | * Execute a GOAT tool by name |
| #893 | */ |
| #894 | async executeGoatTool( |
| #895 | toolName: string, |
| #896 | params: Record<string, unknown>, |
| #897 | walletAddress?: string |
| #898 | ): Promise<unknown> { |
| #899 | switch (toolName) { |
| #900 | case 'getBalance': |
| #901 | return this.goatGetBalance({ |
| #902 | address: (params.address as string) || walletAddress || '', |
| #903 | token: params.token as string, |
| #904 | chain: params.chain as ChainType, |
| #905 | }); |
| #906 | |
| #907 | case 'transfer': |
| #908 | if (!walletAddress) throw new Error('Wallet address required for transfer'); |
| #909 | return this.goatTransfer({ |
| #910 | fromAddress: walletAddress, |
| #911 | toAddress: params.toAddress as string, |
| #912 | token: params.token as string, |
| #913 | amount: params.amount as string, |
| #914 | chain: params.chain as ChainType, |
| #915 | signerType: params.signerType as SignerType, |
| #916 | }); |
| #917 | |
| #918 | case 'getTokenPrice': |
| #919 | return this.goatGetTokenPrice({ |
| #920 | token: params.token as string, |
| #921 | currency: params.currency as string, |
| #922 | }); |
| #923 | |
| #924 | case 'getSwapQuote': |
| #925 | return this.goatGetSwapQuote({ |
| #926 | inputToken: params.inputToken as string, |
| #927 | outputToken: params.outputToken as string, |
| #928 | amount: params.amount as string, |
| #929 | slippageBps: params.slippageBps as number, |
| #930 | }); |
| #931 | |
| #932 | case 'airdropDevnet': |
| #933 | return this.goatAirdropDevnet({ |
| #934 | address: (params.address as string) || walletAddress || '', |
| #935 | amount: params.amount as number, |
| #936 | }); |
| #937 | |
| #938 | default: |
| #939 | throw new Error(`Unknown GOAT tool: ${toolName}`); |
| #940 | } |
| #941 | } |
| #942 | |
| #943 | /** |
| #944 | * Get list of available GOAT tools |
| #945 | */ |
| #946 | getAvailableGoatTools(): Array<{ |
| #947 | name: string; |
| #948 | description: string; |
| #949 | category: string; |
| #950 | requiresWallet: boolean; |
| #951 | requiresSigning: boolean; |
| #952 | }> { |
| #953 | return [ |
| #954 | { |
| #955 | name: 'getBalance', |
| #956 | description: 'Get wallet balance for SOL or any token', |
| #957 | category: 'wallet', |
| #958 | requiresWallet: true, |
| #959 | requiresSigning: false, |
| #960 | }, |
| #961 | { |
| #962 | name: 'transfer', |
| #963 | description: 'Transfer tokens between wallets', |
| #964 | category: 'wallet', |
| #965 | requiresWallet: true, |
| #966 | requiresSigning: true, |
| #967 | }, |
| #968 | { |
| #969 | name: 'getTokenPrice', |
| #970 | description: 'Get current token price from CoinGecko', |
| #971 | category: 'data', |
| #972 | requiresWallet: false, |
| #973 | requiresSigning: false, |
| #974 | }, |
| #975 | { |
| #976 | name: 'getSwapQuote', |
| #977 | description: 'Get swap quote from Jupiter aggregator', |
| #978 | category: 'defi', |
| #979 | requiresWallet: false, |
| #980 | requiresSigning: false, |
| #981 | }, |
| #982 | { |
| #983 | name: 'airdropDevnet', |
| #984 | description: 'Request devnet SOL airdrop', |
| #985 | category: 'wallet', |
| #986 | requiresWallet: true, |
| #987 | requiresSigning: false, |
| #988 | }, |
| #989 | ]; |
| #990 | } |
| #991 | } |
| #992 |