Skip to content

Commit

Permalink
Add example UMA VASP. Fix lightspark-sdk fundNode variable reference (#…
Browse files Browse the repository at this point in the history
…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
7 people authored Oct 16, 2023
1 parent 90abeef commit fd769cb
Show file tree
Hide file tree
Showing 19 changed files with 731 additions and 67 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-toes-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lightsparkdev/lightspark-sdk": patch
---

Fix fundNode variable reference
1 change: 0 additions & 1 deletion apps/examples/uma-vasp/src/ReceivingVasp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default class ReceivingVasp {
tag: "payRequest",
});
}
// const lookup = await this.lookup(receiver);
res.send("ok");
}

Expand Down
357 changes: 350 additions & 7 deletions apps/examples/uma-vasp/src/SendingVasp.ts
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,
Expand All @@ -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);
}
}
}
Loading

0 comments on commit fd769cb

Please sign in to comment.