UNPKG

freelii-passkey-kit

Version:

A helper library for creating and using smart wallet accounts on the Stellar blockchain.

229 lines (194 loc) 7.9 kB
import { xdr } from "@stellar/stellar-sdk/minimal" import { PasskeyBase } from "./base" import base64url from "base64url" import type { Tx } from "@stellar/stellar-sdk/minimal/contract" import type { Signer } from "./types" import { AssembledTransaction } from "@stellar/stellar-sdk/minimal/contract" import { Durability } from "@stellar/stellar-sdk/minimal/rpc" // Note: Hardcoded version to avoid ESM import issues with JSON const version = '0.10.46' import { LoggingService } from './logging' import type { LoggingConfig } from './logging' import type pino from 'pino' // TODO set default headers in constructor export class PasskeyServer extends PasskeyBase { private launchtubeJwt: string | undefined private mercuryJwt: string | undefined private mercuryKey: string | undefined public launchtubeUrl: string | undefined public launchtubeHeaders: Record<string, string> | undefined public mercuryProjectName: string | undefined public mercuryUrl: string | undefined constructor(options: { rpcUrl?: string, launchtubeUrl?: string, launchtubeJwt?: string, launchtubeHeaders?: Record<string, string> mercuryProjectName?: string, mercuryUrl?: string, mercuryJwt?: string, mercuryKey?: string, logging?: LoggingConfig | pino.Logger, }) { const { rpcUrl, launchtubeUrl, launchtubeJwt, launchtubeHeaders, mercuryProjectName, mercuryUrl, mercuryJwt, mercuryKey, logging, } = options super(rpcUrl) if (logging) LoggingService.init(logging) if (launchtubeUrl) this.launchtubeUrl = launchtubeUrl if (launchtubeJwt) this.launchtubeJwt = launchtubeJwt if (launchtubeHeaders) this.launchtubeHeaders = launchtubeHeaders if (mercuryProjectName) this.mercuryProjectName = mercuryProjectName if (mercuryUrl) this.mercuryUrl = mercuryUrl if (mercuryJwt) this.mercuryJwt = mercuryJwt if (mercuryKey) this.mercuryKey = mercuryKey } public async getSigners(contractId: string) { if (!this.rpc || !this.mercuryProjectName || !this.mercuryUrl || (!this.mercuryJwt && !this.mercuryKey)) { const logger = LoggingService.get() logger.error('Mercury service not configured') throw new Error('Mercury service not configured') } const signers = await fetch(`${this.mercuryUrl}/zephyr/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: this.mercuryJwt ? `Bearer ${this.mercuryJwt}` : this.mercuryKey! }, body: JSON.stringify({ project_name: this.mercuryProjectName, mode: { Function: { fname: "get_signers_by_address", arguments: JSON.stringify({ address: contractId }) } } }) }) .then(async (res) => { const response = await res.json() if (res.ok) { const logger = LoggingService.get() logger.info({ message: 'server.get_signers.success', contractId }) return response } const logger = LoggingService.get() logger.error({ message: 'server.get_signers.error', contractId, error: response }) throw response }) for (const signer of signers) { if (signer.storage === 'Temporary') { try { await this.rpc.getContractData(contractId, xdr.ScVal.scvBytes(base64url.toBuffer(signer.key)), Durability.Temporary) } catch (error) { const logger = LoggingService.get() logger.debug({ message: 'server.get_signer.evicted', contractId, signerKey: signer.key, error: error instanceof Error ? error.message : String(error) }) signer.evicted = true } } } return signers as Signer[] } public async getContractId(options: { keyId?: string, publicKey?: string, policy?: string, }, index = 0) { if (!this.mercuryProjectName || !this.mercuryUrl || (!this.mercuryJwt && !this.mercuryKey)) throw new Error('Mercury service not configured') let { keyId, publicKey, policy } = options || {} if ([keyId, publicKey, policy].filter((arg) => !!arg).length > 1) throw new Error('Exactly one of `options.keyId`, `options.publicKey`, or `options.policy` must be provided.'); let args: { key: string, kind: 'Secp256r1' | 'Ed25519' | 'Policy' } if (keyId) args = { key: keyId, kind: 'Secp256r1' } else if (publicKey) args = { key: publicKey, kind: 'Ed25519' } else if (policy) args = { key: policy, kind: 'Policy' } const res = await fetch(`${this.mercuryUrl}/zephyr/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: this.mercuryJwt ? `Bearer ${this.mercuryJwt}` : this.mercuryKey! }, body: JSON.stringify({ project_name: this.mercuryProjectName, mode: { Function: { fname: "get_addresses_by_signer", arguments: JSON.stringify(args!) } } }) }) .then(async (res) => { if (res.ok) return await res.json() as string[] throw await res.json() }) return res[index] } /* LATER - Add a method for getting a paginated or filtered list of all a wallet's events */ public async send<T>( txn: AssembledTransaction<T> | Tx | string, fee?: number, ) { const logger = LoggingService.get() if (!this.launchtubeUrl){ logger.error('Launchtube service not configured') throw new Error('Launchtube service not configured') } const data = new FormData(); if (txn instanceof AssembledTransaction) { txn = txn.built!.toXDR() } else if (typeof txn !== 'string') { txn = txn.toXDR() } data.set('xdr', txn); if (fee) data.set('fee', fee.toString()); let lt_headers = Object.assign({ 'X-Client-Name': 'passkey-kit', 'X-Client-Version': version, }, this.launchtubeHeaders) if (this.launchtubeJwt) lt_headers.authorization = `Bearer ${this.launchtubeJwt}` logger.info({ message: 'server.send', xdr: txn, fee, launchtubeUrl: this.launchtubeUrl, rpcUrl: this.rpcUrl, mercuryProjectName: this.mercuryProjectName, mercuryUrl: this.mercuryUrl, }) return fetch(this.launchtubeUrl, { method: 'POST', headers: lt_headers, body: data }).then(async (res) => { if (res.ok) { const response = await res.json() logger.info({ message: 'server.send.success', xdr: txn, status: res.status, response }) return response } else { const error = await res.json() logger.error({ message: 'server.send.error', xdr: txn, status: res.status, error }) throw error } }) } }