Skip to content

Commit

Permalink
add txids (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
gudnuf committed Aug 21, 2024
1 parent 8858ec0 commit 3539ab4
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 21 deletions.
2 changes: 1 addition & 1 deletion global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PrismaClient } from '@prisma/client';

declare global {
// var prisma: PrismaClient;
var prisma: PrismaClient | undefined;
}

export {};
9 changes: 9 additions & 0 deletions prisma/migrations/20240812233505_token_txids/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "Token" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"redeemedAt" TIMESTAMP(3),

CONSTRAINT "Token_pkey" PRIMARY KEY ("id")
);
7 changes: 7 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,10 @@ model Notification {
user User @relation(fields: [userPubkey], references: [pubkey])
createdAt DateTime @default(now())
}

model Token {
id String @id
token String
createdAt DateTime @default(now())
redeemedAt DateTime?
}
10 changes: 8 additions & 2 deletions src/components/buttons/LightningTipButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import QRCode from 'qrcode.react';
import ClipboardButton from '@/components/buttons/utility/ClipboardButton';
import { useMemo, useState } from 'react';
import { useToast } from '@/hooks/util/useToast';
import { HttpResponseError, getInvoiceForTip, getTipStatus } from '@/utils/appApiRequests';
import { getInvoiceForTip, getTipStatus, postTokenToDb } from '@/utils/appApiRequests';
import { Validate, useForm } from 'react-hook-form';
import { useExchangeRate } from '@/hooks/util/useExchangeRate';
import { formatCents, formatSats } from '@/utils/formatting';
import SendEcashModalBody from '../modals/SendEcashModalBody';
import { PublicContact } from '@/types';
import { computeTxId } from '@/utils/cashu';

interface LightningTipButtonProps {
contact: PublicContact;
Expand Down Expand Up @@ -89,6 +90,7 @@ const LightningTipButton = ({ contact, className }: LightningTipButtonProps) =>
const statusResponse = await getTipStatus(checkingId);
if (statusResponse.token) {
setToken(statusResponse.token);
await postTokenToDb(statusResponse.token);
}
return statusResponse.paid;
} catch (error) {
Expand Down Expand Up @@ -229,7 +231,11 @@ const LightningTipButton = ({ contact, className }: LightningTipButtonProps) =>
</Modal>
<Modal show={showTokenModal} onClose={() => setShowTokenModal(false)}>
<Modal.Header>eTip for {contact.username}</Modal.Header>
<SendEcashModalBody token={token} onClose={() => setShowTokenModal(false)} />
<SendEcashModalBody
token={token}
txid={token && computeTxId(token)}
onClose={() => setShowTokenModal(false)}
/>
</Modal>
</>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/buttons/utility/ClipboardButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useToast } from '@/hooks/util/useToast';
import { Button } from 'flowbite-react';
import { ClipboardDocumentCheckIcon, ClipboardDocumentIcon } from '@heroicons/react/20/solid';
Expand All @@ -15,7 +15,7 @@ const ClipboardButton: React.FC<Props> = ({ toCopy, toShow, onClick, className =

const { addToast } = useToast();

const handleCopy = () => {
const handleCopy = useCallback(() => {
navigator.clipboard
.writeText(toCopy)
.then(() => {
Expand All @@ -34,7 +34,7 @@ const ClipboardButton: React.FC<Props> = ({ toCopy, toShow, onClick, className =
if (onClick) {
onClick();
}
};
}, [toCopy]);

return (
<Button onClick={handleCopy} className={className}>
Expand Down
20 changes: 14 additions & 6 deletions src/components/modals/ConfirmEcashReceiveModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RootState, useAppDispatch } from '@/redux/store';
import { CashuMint, CashuWallet, MintKeys, Proof, Token, getEncodedToken } from '@cashu/cashu-ts';
import { Modal, Spinner } from 'flowbite-react';
import { Button, Modal, Spinner } from 'flowbite-react';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useProofManager } from '@/hooks/cashu/useProofManager.ts';
Expand All @@ -12,6 +12,7 @@ import { useCashu } from '@/hooks/cashu/useCashu';
import { useCashuContext } from '@/hooks/contexts/cashuContext';
import { PublicContact } from '@/types';
import useContacts from '@/hooks/boardwalk/useContacts';
import ClipboardButton from '../buttons/utility/ClipboardButton';

interface ConfirmEcashReceiveModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -323,15 +324,15 @@ const ConfirmEcashReceiveModal = ({
</div>

{!lockedTo || lockedTo === '02' + user.pubkey ? (
<div className='flex flex-col md:flex-row md:justify-center justify-center items-center'>
<button
<div className='flex flex-col space-y-1 md:justify-center justify-center items-center'>
<Button
onClick={handleSwapToMain}
className='mr-3 underline text-lg mb-3 md:mb-0'
className='btn-primary mr-3 mb-3 md:mb-0 w-40'
>
Claim
</button>
</Button>
<button
className={`underline hover:cursor-pointer text-lg mb-0 ${fromActiveMint ? 'hidden' : ''} ${!supportedUnits.includes('usd') && 'hidden'}`}
className={`underline hover:cursor-pointer text-lg ${!fromActiveMint ? 'hidden' : ''} ${!supportedUnits.includes('usd') && 'hidden'}`}
onClick={handleAddMint}
>
{mintTrusted ? 'Claim to Source Mint' : 'Trust Mint and Claim'}
Expand All @@ -357,6 +358,13 @@ const ConfirmEcashReceiveModal = ({
</div>
)}
</div>
<div className='flex justify-center items-center mt-4'>
<ClipboardButton
toShow='Token'
toCopy={getEncodedToken(token)}
className='etip-button '
/>
</div>
</Modal.Body>
</Modal>
<ProcessingClaimModal isSwapping={swapping} />
Expand Down
16 changes: 13 additions & 3 deletions src/components/modals/SendEcashModalBody.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Modal, Spinner } from 'flowbite-react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import AnimatedQRCode from '../AnimatedQR';
import ClipboardButton from '../buttons/utility/ClipboardButton';
import QRCode from 'qrcode.react';
Expand All @@ -8,10 +8,11 @@ import ErrorBoundary from '../ErrorBoundary';

interface SendEcashModalBodyProps {
token?: string;
txid?: string;
onClose: () => void;
}

const SendEcashModalBody = ({ onClose, token }: SendEcashModalBodyProps) => {
const SendEcashModalBody = ({ onClose, token, txid }: SendEcashModalBodyProps) => {
const [carouselSlides, setCarouselSlides] = useState<React.ReactNode[]>([]);
const [qrError, setQrError] = useState(false);

Expand Down Expand Up @@ -54,6 +55,15 @@ const SendEcashModalBody = ({ onClose, token }: SendEcashModalBodyProps) => {
return () => {};
}, [token]);

const toCopy = useMemo(() => {
const base = `${window.location.protocol}//${window.location.host}/wallet?`;
if (txid) {
return `${base}txid=${txid}`;
} else {
return `${base}token=${token}`;
}
}, [token, txid]);

if (!token) {
return (
<Modal.Body>
Expand All @@ -75,7 +85,7 @@ const SendEcashModalBody = ({ onClose, token }: SendEcashModalBodyProps) => {
</div>
<div className='flex space-x-3'>
<ClipboardButton
toCopy={`${window.location.protocol}//${window.location.host}/wallet?token=${token}`}
toCopy={toCopy}
toShow={`Link`}
onClick={onClose}
className='btn-primary'
Expand Down
6 changes: 5 additions & 1 deletion src/components/modals/SendModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UserIcon } from '@heroicons/react/20/solid';
import ContactsModal from './ContactsModal/ContactsModal';
import { PublicContact } from '@/types';
import useNotifications from '@/hooks/boardwalk/useNotifications';
import { postTokenToDb } from '@/utils/appApiRequests';

interface SendModalProps {
isOpen: boolean;
Expand All @@ -38,6 +39,7 @@ export const SendModal = ({ isOpen, onClose }: SendModalProps) => {
const [ecashToken, setEcashToken] = useState<string | undefined>();
const [isContactsModalOpen, setIsContactsModalOpen] = useState(false);
const [lockTo, setLockTo] = useState<PublicContact | undefined>();
const [txid, setTxid] = useState<string | undefined>(); // txid of the token used for mapping to the real token in the db
const { sendTokenAsNotification } = useNotifications();

const { addToast } = useToast();
Expand Down Expand Up @@ -131,6 +133,8 @@ export const SendModal = ({ isOpen, onClose }: SendModalProps) => {
// TODO: right now we don't support generic p2pk lock, but if lockTo is not a contact,
// we should not do this.
await sendTokenAsNotification(token);
const txid = await postTokenToDb(token);
setTxid(txid);
}
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -273,7 +277,7 @@ export const SendModal = ({ isOpen, onClose }: SendModalProps) => {
);

case SendFlow.Ecash:
return <SendEcashModalBody token={ecashToken} onClose={resetModalState} />;
return <SendEcashModalBody token={ecashToken} txid={txid} onClose={resetModalState} />;

default:
return null;
Expand Down
9 changes: 8 additions & 1 deletion src/components/transactionHistory/HistoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HistoryTableRow from './HistoryTableRow';
import SendEcashModalBody from '../modals/SendEcashModalBody';
import useContacts from '@/hooks/boardwalk/useContacts';
import { PublicContact } from '@/types';
import { computeTxId } from '@/utils/cashu';

const customTheme = {
root: {
Expand All @@ -20,13 +21,19 @@ const HistoryTable: React.FC<{
const [lockedToken, setLockedToken] = useState<string>('');
const [isSendModalOpen, setIsSendModalOpen] = useState(false);
const [tokenLockedTo, setTokenLockedTo] = useState<PublicContact | null>(null);
const [txid, setTxid] = useState<string | undefined>();

const { fetchContact } = useContacts();

const openSendEcashModal = async (tx: EcashTransaction) => {
if (tx.pubkey) {
const contact = await fetchContact(tx.pubkey?.slice(2));
setTokenLockedTo(contact);
/** Begin backwards compatibility for < v0.2.2 */
if (new Date(tx.date).getTime() > 1723545105975) {
setTxid(computeTxId(tx.token));
}
/** End backwards compatibility for < v0.2.2 */
}
setLockedToken(tx.token);
setIsSendModalOpen(true);
Expand All @@ -50,7 +57,7 @@ const HistoryTable: React.FC<{
<Modal.Header>
{tokenLockedTo ? `eTip for ${tokenLockedTo.username}` : 'eTip'}
</Modal.Header>
<SendEcashModalBody token={lockedToken} onClose={closeSendEcashModal} />
<SendEcashModalBody token={lockedToken} onClose={closeSendEcashModal} txid={txid} />
</Modal>
</>
);
Expand Down
18 changes: 18 additions & 0 deletions src/lib/tokenModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import prisma from './prisma';

export const createTokenInDb = async (token: string, txid: string) => {
return await prisma.token.create({
data: {
token,
id: txid,
},
});
};

export const findTokenByTxId = async (txid: string) => {
return await prisma.token.findUnique({
where: {
id: txid,
},
});
};
26 changes: 26 additions & 0 deletions src/pages/api/token/[txid].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { GetTokenResponse } from '@/types';
import { findTokenByTxId } from '@/lib/tokenModels';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { txid } = req.query;

if (typeof txid !== 'string') {
return res.status(400).json({ message: 'Txid is required as a string' });
}

if (req.method === 'GET') {
try {
const token = await findTokenByTxId(txid);
if (!token) {
return res.status(404).json({ message: 'Token not found' });
}
return res.status(200).json(token as GetTokenResponse);
} catch (error: any) {
return res.status(500).json({ message: error.message });
}
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
32 changes: 32 additions & 0 deletions src/pages/api/token/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createTokenInDb } from '@/lib/tokenModels';
import { PostTokenRequest, PostTokenResponse } from '@/types';
import { computeTxId, getProofsFromToken } from '@/utils/cashu';
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
console.log('req', req.body);
if (req.method === 'POST') {
try {
const { token } = req.body as PostTokenRequest;

if (!token) {
return res.status(400).json({ message: 'Token is required' });
}

console.log('creating token in db', token);

const proofs = getProofsFromToken(token);

const txid = computeTxId(proofs);

await createTokenInDb(token, txid);

return res.status(200).json({ txid } as PostTokenResponse);
} catch (error: any) {
return res.status(500).json({ message: error.message });
}
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
15 changes: 12 additions & 3 deletions src/pages/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import { proofsLockedTo } from '@/utils/cashu';
import { formatUrl } from '@/utils/url';
import NotificationDrawer from '@/components/notifications/NotificationDrawer';
import { formatCents } from '@/utils/formatting';
import { findTokenByTxId } from '@/lib/tokenModels';

export default function Home({ isMobile }: { isMobile: boolean }) {
export default function Home({ isMobile, token }: { isMobile: boolean; token?: string }) {
const newUser = useRef(false);
const [tokenDecoded, setTokenDecoded] = useState<Token | null>(null);
const [ecashReceiveModalOpen, setEcashReceiveModalOpen] = useState(false);
Expand All @@ -45,7 +46,6 @@ export default function Home({ isMobile }: { isMobile: boolean }) {

useEffect(() => {
if (!router.isReady) return;
const { token } = router.query;
const localKeysets = window.localStorage.getItem('keysets');

const handleTokenQuery = async (token: string) => {
Expand Down Expand Up @@ -234,7 +234,15 @@ export const getServerSideProps: GetServerSideProps = async (
const userAgent = context.req.headers['user-agent'];
const isMobile = /mobile/i.test(userAgent as string);

const token = context.query.token as string;
let token = context.query.token as string;
const txid = context.query.txid as string;

if (txid && !token) {
const tokenEntry = await findTokenByTxId(txid);
if (tokenEntry) {
token = tokenEntry.token;
}
}

let tokenData: TokenProps | null = null;
if (token) {
Expand Down Expand Up @@ -267,6 +275,7 @@ export const getServerSideProps: GetServerSideProps = async (
return {
props: {
isMobile,
token: token || null,
pageTitle: pageTitle(tokenData) || null,
pageDescription: pageDescription(tokenData) || null,
},
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,15 @@ export const isContactNotification = (
}
return false;
};

export type PostTokenRequest = {
token: string;
};

export type PostTokenResponse = {
txid: string;
};

export type GetTokenResponse = {
token: string;
};
Loading

0 comments on commit 3539ab4

Please sign in to comment.