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
text/typescript
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
}
})
}
}