-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add example UMA VASP. Fix lightspark-sdk fundNode variable reference (#…
…284) * Add SendingVaspRequestCache and implement the first sending vasp request (#6840) GitOrigin-RevId: 2feef192ac499eb6f873219d1bee4c81c60aa19b * Implement payReq request for uma JS VASP (#6846) GitOrigin-RevId: 4e28858eb367509f5f6aede93d4d5ef175aad4cb * CI update lock file for PR * Finish the other sending VASP calls for JS demo VASP (#6857) GitOrigin-RevId: b11d94ab5ae2257d334c6193a193bdf89f30c18a * feat: add landing page, icons, and fix icons in Button GitOrigin-RevId: 46b04b13bfdf7bc02a013c97cba56d5908cc3322 * use html select for language selector GitOrigin-RevId: 43d6ecdb9be4446ac206ed67a0da2ad33253c600 * [umame-docs] fix navlink and collapsible styles, fonts, animations, in side and mobile navs GitOrigin-RevId: d6328a0b278102f1a26d0dda722f65b2c8d4e56e * [lightspark-sdk] Fix fund_node var ref (#6891) GitOrigin-RevId: eb773fca3cce6174a8d31f59224a7475b677bfb9 * Update from public js-sdk main branch (#6838) Update public `js` sources with the latest code from the [public repository](https://github.com/lightsparkdev/js-sdk) main branch. This typically happens when new versions of the SDK are released and version updates need to be synced. The PR should be merged as soon as possible to avoid updates to webdev overwriting the changes in the js-sdk develop branch. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Lightspark Eng <[email protected]> Co-authored-by: Corey Martin <[email protected]> GitOrigin-RevId: 626c59ad85156be2e05fda63c01bbac3420c7348 * CI update lock file for PR * Create fresh-toes-count.md --------- Co-authored-by: Jeremy Klein <[email protected]> Co-authored-by: Lightspark Eng <[email protected]> Co-authored-by: Brian Siao Tick Chong <[email protected]> Co-authored-by: Corey Martin <[email protected]> Co-authored-by: lightspark-ci-js-sdk[bot] <134011073+lightspark-ci-js-sdk[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
90abeef
commit fd769cb
Showing
19 changed files
with
731 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@lightsparkdev/lightspark-sdk": patch | ||
--- | ||
|
||
Fix fundNode variable reference |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,23 @@ | ||
import { LightsparkClient } from "@lightsparkdev/lightspark-sdk"; | ||
import { convertCurrencyAmount, hexToBytes } from "@lightsparkdev/core"; | ||
import { | ||
CurrencyUnit, | ||
InvoiceData, | ||
LightsparkClient, | ||
OutgoingPayment, | ||
TransactionStatus, | ||
} from "@lightsparkdev/lightspark-sdk"; | ||
import * as uma from "@uma-sdk/core"; | ||
import { Express, Request, Response } from "express"; | ||
import settings from "../../settings.json" assert { type: "json" }; | ||
import SendingVaspRequestCache, { | ||
SendingVaspPayReqData, | ||
} from "./SendingVaspRequestCache.js"; | ||
import UmaConfig from "./UmaConfig.js"; | ||
|
||
export default class SendingVasp { | ||
private readonly requestCache: SendingVaspRequestCache = | ||
new SendingVaspRequestCache(); | ||
|
||
constructor( | ||
private readonly config: UmaConfig, | ||
private readonly lightsparkClient: LightsparkClient, | ||
|
@@ -25,19 +39,348 @@ export default class SendingVasp { | |
|
||
private async handleClientUmaLookup(req: Request, res: Response) { | ||
const receiver = req.params.receiver; | ||
// const lookup = await this.lookup(receiver); | ||
res.send("ok"); | ||
if (!receiver) { | ||
res.status(400).send("Missing receiver"); | ||
return; | ||
} | ||
|
||
const [receiverId, receivingVaspDomain] = receiver.split("@"); | ||
if (!receiverId || !receivingVaspDomain) { | ||
res.status(400).send("Invalid receiver"); | ||
return; | ||
} | ||
|
||
const lnrulpRequestUrl = await uma.getSignedLnurlpRequestUrl({ | ||
isSubjectToTravelRule: true, | ||
receiverAddress: receiver, | ||
signingPrivateKey: this.config.umaSigningPrivKey(), | ||
senderVaspDomain: req.hostname, // TODO: Might need to include the port here. | ||
}); | ||
|
||
let response: globalThis.Response; | ||
try { | ||
response = await fetch(lnrulpRequestUrl); | ||
} catch (e) { | ||
res.status(424).send("Error fetching Lnurlp request."); | ||
return; | ||
} | ||
|
||
// TODO: Handle versioning via the 412 response. | ||
|
||
if (!response.ok) { | ||
res.status(424).send(`Error fetching Lnurlp request. ${response.status}`); | ||
return; | ||
} | ||
|
||
let lnurlpResponse: uma.LnurlpResponse; | ||
try { | ||
lnurlpResponse = uma.parseLnurlpResponse(await response.text()); | ||
} catch (e) { | ||
console.error("Error parsing lnurlp response.", e); | ||
res.status(424).send("Error parsing Lnurlp response."); | ||
return; | ||
} | ||
|
||
let pubKeys = await this.fetchPubKeysOrFail(receivingVaspDomain, res); | ||
if (!pubKeys) return; | ||
|
||
try { | ||
const isSignatureValid = await uma.verifyUmaLnurlpResponseSignature( | ||
lnurlpResponse, | ||
hexToBytes(pubKeys.signingPubKey), | ||
); | ||
if (!isSignatureValid) { | ||
res.status(424).send("Invalid UMA response signature."); | ||
return; | ||
} | ||
} catch (e) { | ||
console.error("Error verifying UMA response signature.", e); | ||
res.status(424).send("Error verifying UMA response signature."); | ||
return; | ||
} | ||
|
||
const callbackUuid = this.requestCache.saveLnurlpResponseData( | ||
lnurlpResponse, | ||
receiverId, | ||
receivingVaspDomain, | ||
); | ||
|
||
res.send({ | ||
currencies: lnurlpResponse.currencies, | ||
minSendableSats: lnurlpResponse.minSendable, | ||
maxSendableSats: lnurlpResponse.maxSendable, | ||
callbackUuid: callbackUuid, | ||
// You might not actually send this to a client in practice. | ||
receiverKycStatus: lnurlpResponse.compliance.kycStatus, | ||
}); | ||
} | ||
|
||
private async handleClientUmaPayreq(req: Request, res: Response) { | ||
const callbackUuid = req.params.callbackUuid; | ||
// const payreq = await this.payreq(callbackUuid); | ||
res.send("ok"); | ||
if (!callbackUuid) { | ||
res.status(400).send("Missing callbackUuid"); | ||
return; | ||
} | ||
|
||
const initialRequestData = | ||
this.requestCache.getLnurlpResponseData(callbackUuid); | ||
if (!initialRequestData) { | ||
res.status(400).send("callbackUuid not found"); | ||
return; | ||
} | ||
|
||
const amountStr = req.query.amount; | ||
if (!amountStr || typeof amountStr !== "string") { | ||
res.status(400).send("Missing amount"); | ||
return; | ||
} | ||
const amount = parseInt(amountStr); | ||
if (isNaN(amount)) { | ||
res.status(400).send("Invalid amount"); | ||
return; | ||
} | ||
|
||
const currencyCode = req.query.currencyCode; | ||
if (!currencyCode || typeof currencyCode !== "string") { | ||
res.status(400).send("Missing currencyCode"); | ||
return; | ||
} | ||
const currencyValid = initialRequestData.lnurlpResponse.currencies.some( | ||
(c) => c.code === currencyCode, | ||
); | ||
if (!currencyValid) { | ||
res.status(400).send("Currency code not supported"); | ||
return; | ||
} | ||
|
||
let pubKeys = await this.fetchPubKeysOrFail( | ||
initialRequestData.receivingVaspDomain, | ||
res, | ||
); | ||
if (!pubKeys) return; | ||
|
||
const payerProfile = this.getPayerProfile( | ||
initialRequestData.lnurlpResponse.requiredPayerData, | ||
); | ||
const trInfo = | ||
'["message": "Here is some fake travel rule info. It is up to you to actually implement this if needed."]'; | ||
// In practice this should be loaded from your node: | ||
const payerUtxos: string[] = []; | ||
const utxoCallback = this.getUtxoCallback(req, "1234abcd"); | ||
|
||
let payReq: uma.PayRequest; | ||
try { | ||
payReq = await uma.getPayRequest({ | ||
receiverEncryptionPubKey: hexToBytes(pubKeys.encryptionPubKey), | ||
sendingVaspPrivateKey: this.config.umaSigningPrivKey(), | ||
currencyCode, | ||
amount, | ||
payerIdentifier: payerProfile.identifier, | ||
payerKycStatus: uma.KycStatus.Verified, | ||
utxoCallback, | ||
trInfo, | ||
payerUtxos, | ||
payerName: payerProfile.name, | ||
payerEmail: payerProfile.email, | ||
}); | ||
} catch (e) { | ||
console.error("Error generating payreq.", e); | ||
res.status(500).send("Error generating payreq."); | ||
return; | ||
} | ||
|
||
let response: globalThis.Response; | ||
try { | ||
response = await fetch(initialRequestData.lnurlpResponse.callback, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(payReq), | ||
}); | ||
} catch (e) { | ||
res.status(500).send("Error sending payreq."); | ||
return; | ||
} | ||
|
||
if (!response.ok) { | ||
res.status(424).send(`Payreq failed. ${response.status}`); | ||
return; | ||
} | ||
|
||
let payResponse: uma.PayReqResponse; | ||
try { | ||
payResponse = await uma.parsePayReqResponse(await response.text()); | ||
} catch (e) { | ||
console.error("Error parsing payreq response.", e); | ||
res.status(424).send("Error parsing payreq response."); | ||
return; | ||
} | ||
|
||
// This is where you'd pre-screen the UTXOs from payResponse.compliance.utxos. | ||
|
||
let invoice: InvoiceData; | ||
try { | ||
invoice = await this.lightsparkClient.decodeInvoice( | ||
payResponse.encodedInvoice, | ||
); | ||
} catch (e) { | ||
console.error("Error decoding invoice.", e); | ||
res.status(500).send("Error decoding invoice."); | ||
return; | ||
} | ||
|
||
const newCallbackUuid = this.requestCache.savePayReqData( | ||
payResponse.encodedInvoice, | ||
utxoCallback, | ||
invoice, | ||
); | ||
|
||
res.send({ | ||
callbackUuid: newCallbackUuid, | ||
encodedInvoice: payResponse.encodedInvoice, | ||
amount: invoice.amount, | ||
conversionRate: payResponse.paymentInfo.multiplier, | ||
exchangeFeesMillisatoshi: | ||
payResponse.paymentInfo.exchangeFeesMillisatoshi, | ||
currencyCode: payResponse.paymentInfo.currencyCode, | ||
}); | ||
} | ||
|
||
private async fetchPubKeysOrFail(receivingVaspDomain: string, res: Response) { | ||
try { | ||
return await uma.fetchPublicKeyForVasp({ | ||
cache: this.pubKeyCache, | ||
vaspDomain: receivingVaspDomain, | ||
}); | ||
} catch (e) { | ||
console.error("Error fetching public key.", e); | ||
res.status(424).send("Error fetching public key."); | ||
} | ||
} | ||
|
||
private async handleClientSendPayment(req: Request, res: Response) { | ||
const callbackUuid = req.params.callbackUuid; | ||
// const payment = await this.sendPayment(callbackUuid); | ||
res.send("ok"); | ||
if (!callbackUuid) { | ||
res.status(400).send("Missing callbackUuid"); | ||
return; | ||
} | ||
|
||
const payReqData = this.requestCache.getPayReqData(callbackUuid); | ||
if (!payReqData) { | ||
res.status(400).send("callbackUuid not found"); | ||
return; | ||
} | ||
|
||
if (new Date(payReqData.invoiceData.expiresAt) < new Date()) { | ||
res.status(400).send("Invoice expired"); | ||
return; | ||
} | ||
|
||
if (payReqData.invoiceData.amount.originalValue <= 0) { | ||
res | ||
.status(400) | ||
.send("Invoice amount invalid. Uma requires positive amounts."); | ||
return; | ||
} | ||
|
||
let payment: OutgoingPayment; | ||
try { | ||
const paymentResult = await this.lightsparkClient.payUmaInvoice( | ||
this.config.nodeID, | ||
payReqData.encodedInvoice, | ||
/* maxeesMsats */ 1_000_000, | ||
); | ||
if (!paymentResult) { | ||
throw new Error("Payment request failed."); | ||
} | ||
payment = await this.waitForPaymentCompletion(paymentResult); | ||
} catch (e) { | ||
console.error("Error paying invoice.", e); | ||
res.status(500).send("Error paying invoice."); | ||
return; | ||
} | ||
|
||
await this.sendPostTransactionCallback(payment, payReqData); | ||
|
||
res.send({ | ||
paymentId: payment.id, | ||
didSucceed: payment.status === TransactionStatus.SUCCESS, | ||
}); | ||
} | ||
|
||
/** | ||
* NOTE: In a real application, you'd want to use the authentication context to pull out this information. It's not | ||
* actually always Alice sending the money ;-). | ||
*/ | ||
private getPayerProfile(requiredPayerData: uma.PayerDataOptions) { | ||
const port = process.env.PORT || settings.umaVasp.port; | ||
return { | ||
name: requiredPayerData.nameRequired ? "Alice FakeName" : undefined, | ||
email: requiredPayerData.emailRequired ? "[email protected]" : undefined, | ||
// Note: This is making an assumption that this is running on localhost. We should make it configurable. | ||
identifier: `$alice@localhost:${port}`, | ||
}; | ||
} | ||
|
||
private getUtxoCallback(req: Request, txId: string): string { | ||
const protocol = req.protocol; | ||
const host = req.hostname; | ||
const path = `/api/uma/utxoCallback?txId=${txId}`; | ||
return `${protocol}://${host}${path}`; | ||
} | ||
|
||
private async waitForPaymentCompletion( | ||
paymentResult: OutgoingPayment, | ||
retryNum = 0, | ||
): Promise<OutgoingPayment> { | ||
if (paymentResult.status === TransactionStatus.SUCCESS) { | ||
return paymentResult; | ||
} | ||
|
||
const payment = await this.lightsparkClient.executeRawQuery( | ||
OutgoingPayment.getOutgoingPaymentQuery(paymentResult.id), | ||
); | ||
if (!payment) { | ||
throw new Error("Payment not found."); | ||
} | ||
|
||
if (payment.status !== TransactionStatus.PENDING) { | ||
return payment; | ||
} | ||
|
||
const maxRetries = 40; | ||
if (retryNum >= maxRetries) { | ||
throw new Error("Payment timed out."); | ||
} | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 250)); | ||
return this.waitForPaymentCompletion(payment); | ||
} | ||
|
||
private async sendPostTransactionCallback( | ||
payment: OutgoingPayment, | ||
payReqData: SendingVaspPayReqData, | ||
) { | ||
const utxos = | ||
payment.umaPostTransactionData?.map((d) => { | ||
d.utxo, convertCurrencyAmount(d.amount, CurrencyUnit.MILLISATOSHI); | ||
}) ?? []; | ||
try { | ||
const postTxResponse = await fetch(payReqData.utxoCallback, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ utxos }), | ||
}); | ||
if (!postTxResponse.ok) { | ||
console.error( | ||
`Error sending post transaction callback. ${postTxResponse.status}`, | ||
); | ||
} | ||
} catch (e) { | ||
console.error("Error sending post transaction callback.", e); | ||
} | ||
} | ||
} |
Oops, something went wrong.