chore: 🔧 boltz

This commit is contained in:
Anthony Potdevin 2020-11-02 20:43:39 +01:00
parent c8156a00cd
commit 9a0e607754
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
39 changed files with 2056 additions and 65 deletions

View file

@ -7,6 +7,7 @@ import {
NormalizedCacheObject,
} from '@apollo/client';
import getConfig from 'next/config';
import possibleTypes from 'src/graphql/fragmentTypes.json';
const { publicRuntimeConfig } = getConfig();
const { apiUrl: uri } = publicRuntimeConfig;
@ -41,9 +42,7 @@ function createApolloClient(context?: ResolverContext) {
credentials: 'same-origin',
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache({
possibleTypes: { Transaction: ['InvoiceType', 'PaymentType'] },
}),
cache: new InMemoryCache(possibleTypes),
defaultOptions: {
query: {
fetchPolicy: 'cache-first',

139
package-lock.json generated
View file

@ -4009,6 +4009,11 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@boltz/bitcoin-ops": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@boltz/bitcoin-ops/-/bitcoin-ops-2.0.0.tgz",
"integrity": "sha512-AM7vFNwSD7B4XI6yeRKccWbbD/lvwoFr8U3pqhzryBQo4uMkYe5V3/kMVnml4SNuxzyqdIFu4ur3TId02sC33A=="
},
"@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@ -8473,6 +8478,15 @@
"@types/react": "*"
}
},
"@types/react-motion": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/react-motion/-/react-motion-0.0.29.tgz",
"integrity": "sha512-MD1DbdcDKruR0zz5Z0XIlrkPdjDMgYx0AHhbaoTBpDirUTt8Bd7x6OFE458nEINZxfPW0EcEOw2O8S/WwGNLsA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native": {
"version": "0.63.27",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.27.tgz",
@ -8502,6 +8516,16 @@
"@types/react-transition-group": "*"
}
},
"@types/react-slider": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/react-slider/-/react-slider-1.0.0.tgz",
"integrity": "sha512-OQpKbS1J96UwUGOqMiVedFM5ds73HQmYufL3yEoLQ51NCUof2A2NQqpeu5U1EwZQI3MhERU78uXhKJzY7Lbqbw==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-motion": "*"
}
},
"@types/react-table": {
"version": "7.0.25",
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.0.25.tgz",
@ -11763,6 +11787,26 @@
"resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.1.2.tgz",
"integrity": "sha512-UcjCWM5w0ncmeLy+L1ghgWBmFUD7F+tvYpYw/pi1PLXMhRJ10gHFbPjzX2CTMsU0ICb8CisIFBsd16DWaUd4FA=="
},
"boltz-core": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-0.3.5.tgz",
"integrity": "sha512-57dV6rv4Ktt4zFyRcFiFpFv3WiciCjx4PP37B+2TZLSL9RbGb3kJUf17A+xVPJSNpnlpF2XOek55WRP58IVuBA==",
"requires": {
"@boltz/bitcoin-ops": "^2.0.0",
"bip65": "^1.0.3",
"bip66": "^1.1.5",
"bitcoinjs-lib": "^5.2.0",
"bn.js": "^5.1.3",
"varuint-bitcoin": "^1.1.2"
},
"dependencies": {
"bn.js": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
"integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ=="
}
}
},
"boom": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz",
@ -21824,9 +21868,9 @@
}
},
"ln-service": {
"version": "50.6.0",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-50.6.0.tgz",
"integrity": "sha512-qqsvBXj3Vq7UxhTpAABgZZAwP3IldbGV31qzRAR/WkVU9sO6WgMoQ8uY1DKEWqA7im1KMTvoIHNKtGeLrmp8/g==",
"version": "50.7.0",
"resolved": "https://registry.npmjs.org/ln-service/-/ln-service-50.7.0.tgz",
"integrity": "sha512-BBvfZp+myK6SIIyj7L/BtK2To/PELhZxjHnAWD4YVSe81ubli35Q6DwXEVtqJF0/jtSqNKaGkZ1JHFnztjgCfA==",
"requires": {
"@datastructures-js/priority-queue": "4.1.2",
"async": "3.2.0",
@ -21839,7 +21883,7 @@
"express": "4.17.1",
"invoices": "1.1.4",
"is-base64": "1.1.0",
"lightning": "3.0.8",
"lightning": "3.0.9",
"macaroon": "3.0.4",
"morgan": "1.10.0",
"ws": "7.4.0"
@ -21850,6 +21894,26 @@
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
"integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ=="
},
"lightning": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/lightning/-/lightning-3.0.9.tgz",
"integrity": "sha512-3M25gUpU2rJnfVm2rYJ4FQhDEJGGHQgG13QERfmAmUT6VPjRYs/DLf1YwfGB6rIxxYT7+HtfQnsqvmxlTlgQVg==",
"requires": {
"@grpc/grpc-js": "1.2.0",
"@grpc/proto-loader": "0.5.5",
"async": "3.2.0",
"asyncjs-util": "1.2.3",
"bitcoinjs-lib": "5.2.0",
"bn.js": "5.1.3",
"body-parser": "1.19.0",
"bolt07": "1.6.0",
"bolt09": "0.1.2",
"cbor": "5.1.0",
"express": "4.17.1",
"invoices": "1.1.4",
"psbt": "1.1.6"
}
},
"ws": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
@ -25344,6 +25408,11 @@
"react-transition-group": "^4.3.0"
}
},
"react-slider": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/react-slider/-/react-slider-1.0.11.tgz",
"integrity": "sha512-NQSae3kCM+on7nXrDN33afQH95HHMVYF/NwibEKsXjIVuQIs9EON8YWGYwNVy1xOPXG3ZcHWusPyA/5AqpvOoA=="
},
"react-spinners": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.9.0.tgz",
@ -26495,6 +26564,15 @@
"tunnel-agent": "^0.6.0"
},
"dependencies": {
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"optional": true,
"requires": {
"mimic-response": "^3.1.0"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@ -26511,6 +26589,12 @@
"wide-align": "^1.1.0"
}
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"optional": true
},
"node-addon-api": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz",
@ -26534,6 +26618,17 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"optional": true
},
"simple-get": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz",
"integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==",
"optional": true,
"requires": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
}
}
},
@ -26657,34 +26752,6 @@
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"optional": true
},
"simple-get": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz",
"integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==",
"optional": true,
"requires": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
},
"dependencies": {
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"optional": true,
"requires": {
"mimic-response": "^3.1.0"
}
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"optional": true
}
}
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@ -27933,15 +28000,15 @@
}
},
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz",
"integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==",
"optional": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
"tar-stream": "^2.0.0"
},
"dependencies": {
"chownr": {

View file

@ -46,6 +46,8 @@
"bech32": "^1.1.4",
"bip32": "^2.0.6",
"bip39": "^3.0.3",
"bitcoinjs-lib": "^5.2.0",
"boltz-core": "^0.3.5",
"cookie": "^0.4.1",
"crypto-js": "^4.0.0",
"date-fns": "^2.16.1",
@ -73,6 +75,7 @@
"react-intersection-observer": "^8.30.3",
"react-qr-reader": "^2.2.1",
"react-select": "^3.1.0",
"react-slider": "^1.0.11",
"react-spinners": "^0.9.0",
"react-spring": "^8.0.27",
"react-table": "^7.6.2",
@ -121,6 +124,7 @@
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-qr-reader": "^2.1.3",
"@types/react-select": "^3.0.26",
"@types/react-slider": "^1.0.0",
"@types/react-table": "^7.0.25",
"@types/secp256k1": "^4.0.1",
"@types/styled-components": "^5.1.4",

17
pages/swap.tsx Normal file
View file

@ -0,0 +1,17 @@
import React from 'react';
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { NextPageContext } from 'next';
import { getProps } from 'src/utils/ssr';
import { SwapView } from 'src/views/swap';
const Wrapped = () => (
<GridWrapper>
<SwapView />
</GridWrapper>
);
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

79
server/api/Boltz.ts Normal file
View file

@ -0,0 +1,79 @@
import { logger } from 'server/helpers/logger';
import { appUrls } from 'server/utils/appUrls';
export const BoltzApi = {
getPairs: async () => {
try {
const response = await fetch(`${appUrls.boltz}/getpairs`);
return await response.json();
} catch (error) {
logger.error('Error getting pairs from Boltz: %o', error);
throw new Error('ErrorGettingBoltzPairs');
}
},
getFeeEstimations: async () => {
try {
const response = await fetch(`${appUrls.boltz}/getfeeestimation`);
return await response.json();
} catch (error) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
getSwapStatus: async (id: string) => {
try {
const body = { id };
const response = await fetch(`${appUrls.boltz}/swapstatus`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
return await response.json();
} catch (error) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
createReverseSwap: async (
invoiceAmount: number,
preimageHash: string,
claimPublicKey: string
) => {
try {
const body = {
type: 'reversesubmarine',
pairId: 'BTC/BTC',
orderSide: 'buy',
invoiceAmount,
preimageHash,
claimPublicKey,
};
const response = await fetch(`${appUrls.boltz}/createswap`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
return await response.json();
} catch (error) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
broadcastTransaction: async (transactionHex: string) => {
try {
const body = {
currency: 'BTC',
transactionHex,
};
const response = await fetch(`${appUrls.boltz}/broadcasttransaction`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
return await response.json();
} catch (error) {
logger.error('Error broadcasting transaction from Boltz: %o', error);
throw new Error(error);
}
},
};

28
server/helpers/crypto.ts Normal file
View file

@ -0,0 +1,28 @@
import { createHash, randomBytes } from 'crypto';
import * as bip39 from 'bip39';
import * as bip32 from 'bip32';
export const getPreimageAndHash = () => {
const preimage = randomBytes(32);
const preimageHash = createHash('sha256')
.update(preimage)
.digest()
.toString('hex');
return { preimage, hash: preimageHash };
};
export const getPrivateAndPublicKey = () => {
const secretKey = bip39.generateMnemonic();
const base58 = bip39.mnemonicToSeedSync(secretKey);
// Derive private seed
const node: bip32.BIP32Interface = bip32.fromSeed(base58);
const derived = node.derivePath(`m/0/0`);
// Get private and public key from derived private seed
const privateKey = derived.privateKey?.toString('hex');
const publicKey = derived.publicKey.toString('hex');
return { privateKey, publicKey };
};

View file

@ -9,7 +9,6 @@ import { enc } from 'crypto-js';
import * as bip39 from 'bip39';
import * as bip32 from 'bip32';
import * as secp256k1 from 'secp256k1';
import { BIP32Interface } from 'bip32';
import { appUrls } from 'server/utils/appUrls';
import { decodeLnUrl } from 'src/utils/url';
import { to } from './async';
@ -60,7 +59,7 @@ export const lnAuthUrlGenerator = async (
const base58 = bip39.mnemonicToSeedSync(secretKey);
// Derive private seed from previous private seed and path
const node: BIP32Interface = bip32.fromSeed(base58);
const node: bip32.BIP32Interface = bip32.fromSeed(base58);
const derived = node.derivePath(
`m/138/${indexes[0]}/${indexes[1]}/${indexes[2]}/${indexes[3]}`
);

View file

@ -87,6 +87,9 @@ export const authResolvers = {
const isPassword = bcrypt.compareSync(params.password, cleanPassword);
if (!isPassword) {
logger.error(
`Authentication failed from ip: ${ip} - Invalid Password!`
);
throw new Error('WrongPasswordForLogin');
}

View file

@ -0,0 +1,255 @@
import { BoltzApi } from 'server/api/Boltz';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { createChainAddress, decodePaymentRequest } from 'ln-service';
import { constructClaimTransaction, detectSwap } from 'boltz-core';
import {
CreateChainAddressType,
DecodedType,
} from 'server/types/ln-service.types';
import {
getPreimageAndHash,
getPrivateAndPublicKey,
} from 'server/helpers/crypto';
import { address, ECPair, networks, Transaction } from 'bitcoinjs-lib';
const getHexBuffer = (input: string) => {
return Buffer.from(input, 'hex');
};
type BoltzInfoType = {
max: number;
min: number;
feePercent: number;
};
type BoltzSwapStatusParams = {
ids: string[];
};
type CreateSwapParams = {
amount: number; // Value in satoshis
address?: string;
};
type ClaimTransactionParams = {
redeem: string;
transaction: string;
preimage: string;
privateKey: string;
destination: string;
fee: number;
};
type CreateBoltzReverseSwapType = {
id: string;
invoice: string;
redeemScript: string;
onchainAmount: number;
timeoutBlockHeight: number;
lockupAddress: string;
minerFeeInvoice: string;
};
export const boltzResolvers = {
Query: {
getBoltzInfo: async (
_: undefined,
__: any,
context: ContextType
): Promise<BoltzInfoType> => {
await requestLimiter(context.ip, 'getBoltzInfo');
const info = await BoltzApi.getPairs();
if (info?.error) {
logger.error(
'Error getting swap information from Boltz. Error: %o',
info.error
);
throw new Error(info.error);
}
const btcPair = info?.pairs?.['BTC/BTC'];
if (!btcPair) {
logger.error('No BTC > LN BTC information received from Boltz');
throw new Error('MissingBtcRatesFromBoltz');
}
const max = btcPair.limits?.maximal || 0;
const min = btcPair.limits?.minimal || 0;
const feePercent = btcPair.fees?.percentage || 0;
return { max, min, feePercent };
},
getBoltzSwapStatus: async (_: undefined, { ids }: BoltzSwapStatusParams) =>
ids,
},
Mutation: {
claimBoltzTransaction: async (
_: undefined,
{
redeem,
transaction,
preimage,
privateKey,
destination,
fee,
}: ClaimTransactionParams,
{ ip }: ContextType
) => {
await requestLimiter(ip, 'claimBoltzTransaction');
const redeemScript = getHexBuffer(redeem);
const lockupTransaction = Transaction.fromHex(transaction);
const info = detectSwap(redeemScript, lockupTransaction);
if (!info?.vout || !info.type) {
logger.error('Can not get vout or type from Boltz');
throw new Error('ErrorCreatingClaimTransaction');
}
const utxos = [
{
...info,
redeemScript,
txHash: lockupTransaction.getHash(),
preimage: getHexBuffer(preimage),
keys: ECPair.fromPrivateKey(getHexBuffer(privateKey)),
},
];
const destinationScript = address.toOutputScript(
destination,
networks.bitcoin
);
const finalTransaction = constructClaimTransaction(
utxos,
destinationScript,
fee
);
logger.debug('Final transaction: %o', finalTransaction);
const response = await BoltzApi.broadcastTransaction(
finalTransaction.toHex()
);
logger.debug('Response from Boltz: %o', { response });
if (!response?.transactionId) {
logger.error('Did not receive a transaction id from Boltz');
throw new Error('NoTransactionIdFromBoltz');
}
return response.transactionId;
},
createBoltzReverseSwap: async (
_: undefined,
{ amount, address }: CreateSwapParams,
context: ContextType
) => {
await requestLimiter(context.ip, 'createReverseSwap');
const { lnd } = context;
const { preimage, hash } = getPreimageAndHash();
const { privateKey, publicKey } = getPrivateAndPublicKey();
let btcAddress = address;
if (!btcAddress) {
const info = await to<CreateChainAddressType>(
createChainAddress({
lnd,
is_unused: true,
format: 'p2wpkh',
})
);
if (!info?.address) {
logger.error('Error creating onchain address for swap');
throw new Error('ErrorCreatingOnChainAddress');
}
btcAddress = info.address;
}
logger.debug('Creating swap with these params: %o', {
amount,
hash,
publicKey,
});
const info = await BoltzApi.createReverseSwap(amount, hash, publicKey);
if (info?.error) {
logger.error('Error creating reverse swap with Boltz: %o', info.error);
throw new Error(info.error);
}
const finalInfo = {
...info,
receivingAddress: btcAddress,
preimage: preimage.toString('hex'),
preimageHash: hash,
privateKey,
publicKey,
};
logger.debug('Swap info: %o', { finalInfo });
return finalInfo;
},
},
BoltzSwap: {
id: (parent: string) => parent,
boltz: async (parent: string) => {
const [info, error] = await toWithError(BoltzApi.getSwapStatus(parent));
if (error || info?.error) {
logger.error(
`Error getting status for swap with id: ${parent}. Error: %o`,
error || info.error
);
return null;
}
if (!info?.status) {
logger.debug(
`No status in Boltz response for swap with id: ${parent}. Response: %o`,
info
);
return null;
}
return info;
},
},
CreateBoltzReverseSwapType: {
decodedInvoice: async (
parent: CreateBoltzReverseSwapType,
_: undefined,
context: ContextType
) => {
const { lnd } = context;
const decoded = await to<DecodedType>(
decodePaymentRequest({
lnd,
request: parent.invoice,
})
);
return {
...decoded,
destination_node: { lnd, publicKey: decoded.destination },
};
},
},
};

View file

@ -0,0 +1,41 @@
import { gql } from '@apollo/client';
export const boltzTypes = gql`
type BoltzInfoType {
max: Int!
min: Int!
feePercent: Float!
}
type BoltzSwapTransaction {
id: String
hex: String
eta: Int
}
type BoltzSwapStatus {
status: String!
transaction: BoltzSwapTransaction
}
type BoltzSwap {
id: String
boltz: BoltzSwapStatus
}
type CreateBoltzReverseSwapType {
id: String!
invoice: String!
redeemScript: String!
onchainAmount: Int!
timeoutBlockHeight: Int!
lockupAddress: String!
minerFeeInvoice: String
decodedInvoice: decodeType
receivingAddress: String!
preimage: String
preimageHash: String
privateKey: String
publicKey: String
}
`;

View file

@ -42,6 +42,8 @@ import { lnUrlResolvers } from './lnurl/resolvers';
import { lnUrlTypes } from './lnurl/types';
import { lnMarketsResolvers } from './lnmarkets/resolvers';
import { lnMarketsTypes } from './lnmarkets/types';
import { boltzResolvers } from './boltz/resolvers';
import { boltzTypes } from './boltz/types';
const typeDefs = [
generalTypes,
@ -65,6 +67,7 @@ const typeDefs = [
tbaseTypes,
lnUrlTypes,
lnMarketsTypes,
boltzTypes,
];
const resolvers = merge(
@ -90,7 +93,8 @@ const resolvers = merge(
bosResolvers,
tbaseResolvers,
lnUrlResolvers,
lnMarketsResolvers
lnMarketsResolvers,
boltzResolvers
);
export const schema = makeExecutableSchema({ typeDefs, resolvers });

View file

@ -28,6 +28,8 @@ export const generalTypes = gql`
export const queryTypes = gql`
type Query {
getBoltzSwapStatus(ids: [String]!): [BoltzSwap]!
getBoltzInfo: BoltzInfoType!
getLnMarketsStatus: String!
getLnMarketsUrl: String!
getLnMarketsUserInfo: LnMarketsUserInfo
@ -93,6 +95,18 @@ export const queryTypes = gql`
export const mutationTypes = gql`
type Mutation {
claimBoltzTransaction(
redeem: String!
transaction: String!
preimage: String!
privateKey: String!
destination: String!
fee: Int!
): String!
createBoltzReverseSwap(
amount: Int!
address: String
): CreateBoltzReverseSwapType!
lnMarketsDeposit(amount: Int!): Boolean!
lnMarketsWithdraw(amount: Int!): Boolean!
lnMarketsLogin: AuthResponse!

View file

@ -141,6 +141,8 @@ export type GetChainTransactionsType = { transactions: ChainTransaction[] };
export type GetUtxosType = { utxos: UtxoType[] };
export type CreateChainAddressType = { address: string };
export type SendToChainAddressType = {
id: string;
confirmation_count: number;

View file

@ -7,10 +7,12 @@ export const appUrls = {
tbase,
oneml: 'https://1ml.com/node/',
blockchain: 'https://mempool.space/tx/',
blockchainAddress: 'https://mempool.space/address/',
fees: 'https://mempool.space/api/v1/fees/recommended',
ticker: 'https://blockchain.info/ticker',
github: 'https://api.github.com/repos/apotdevin/thunderhub/releases/latest',
update: 'https://github.com/apotdevin/thunderhub#updating',
lnMarkets: 'https://api.lnmarkets.com',
lnMarketsExchange: 'https://lnmarkets.com',
boltz: 'https://boltz.exchange/api',
};

View file

@ -75,7 +75,6 @@ interface BorderProps {
}
const BorderButton = styled(GeneralButton)<BorderProps>`
${({ selected }) => selected && 'cursor: default'};
${({ selected }) => selected && 'font-weight: 800'};
background-color: ${colorButtonBackground};
color: ${textColor};

View file

@ -39,6 +39,27 @@ export const copyLink = (text: string) => (
</CopyToClipboard>
);
export const getAddressLink = (transaction: string | null | undefined) => {
if (!transaction) return null;
if (disableLinks) {
return (
<>
{shorten(transaction)}
{copyLink(transaction)}
</>
);
}
const link = `${appUrls.blockchainAddress}${transaction}`;
return (
<>
<SmallLink href={link} target="_blank">
{shorten(transaction)}
</SmallLink>
{copyLink(transaction)}
</>
);
};
export const getTransactionLink = (transaction: string | null | undefined) => {
if (!transaction) return null;
if (disableLinks) {

View file

@ -21,6 +21,7 @@ const generalCSS = css`
outline: none;
max-height: 80%;
overflow-y: auto;
border: 1px solid ${themeColors.grey8};
@media (${mediaWidths.mobile}) {
/* top: 100%; */

View file

@ -0,0 +1,66 @@
import ReactSlider from 'react-slider';
import {
sliderBackgroundColor,
sliderThumbColor,
themeColors,
} from 'src/styles/Themes';
import styled from 'styled-components';
const StyledSlider = styled(ReactSlider)<{ maxWidth?: string }>`
max-width: ${({ maxWidth }) => maxWidth || '500px'};
width: 100%;
height: 38px;
display: flex;
align-items: center;
outline: none;
`;
const StyledThumb = styled.div`
height: 24px;
width: 24px;
background-color: ${sliderThumbColor};
color: #fff;
border-radius: 50%;
cursor: grab;
`;
const Thumb = (props: any) => <StyledThumb {...props} />;
const StyledTrack = styled.div<{ index: number }>`
height: 8px;
background: ${({ index }) =>
index === 1 ? sliderBackgroundColor : themeColors.blue2};
border-radius: 8px;
`;
const Track = (props: any, state: any) => (
<StyledTrack {...props} index={state.index} />
);
type SliderProps = {
value: number;
max: number;
min: number;
onChange: (value: number) => void;
maxWidth?: string;
};
export const Slider = ({
value,
max,
min,
onChange,
maxWidth,
}: SliderProps) => {
return (
<StyledSlider
maxWidth={maxWidth}
value={value}
max={max}
min={min}
renderTrack={Track}
renderThumb={Thumb}
onChange={value => value && typeof value === 'number' && onChange(value)}
/>
);
};

View file

@ -0,0 +1,63 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type ClaimBoltzTransactionMutationVariables = Types.Exact<{
redeem: Types.Scalars['String'];
transaction: Types.Scalars['String'];
preimage: Types.Scalars['String'];
privateKey: Types.Scalars['String'];
destination: Types.Scalars['String'];
fee: Types.Scalars['Int'];
}>;
export type ClaimBoltzTransactionMutation = (
{ __typename?: 'Mutation' }
& Pick<Types.Mutation, 'claimBoltzTransaction'>
);
export const ClaimBoltzTransactionDocument = gql`
mutation ClaimBoltzTransaction($redeem: String!, $transaction: String!, $preimage: String!, $privateKey: String!, $destination: String!, $fee: Int!) {
claimBoltzTransaction(
redeem: $redeem
transaction: $transaction
preimage: $preimage
privateKey: $privateKey
destination: $destination
fee: $fee
)
}
`;
export type ClaimBoltzTransactionMutationFn = Apollo.MutationFunction<ClaimBoltzTransactionMutation, ClaimBoltzTransactionMutationVariables>;
/**
* __useClaimBoltzTransactionMutation__
*
* To run a mutation, you first call `useClaimBoltzTransactionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useClaimBoltzTransactionMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [claimBoltzTransactionMutation, { data, loading, error }] = useClaimBoltzTransactionMutation({
* variables: {
* redeem: // value for 'redeem'
* transaction: // value for 'transaction'
* preimage: // value for 'preimage'
* privateKey: // value for 'privateKey'
* destination: // value for 'destination'
* fee: // value for 'fee'
* },
* });
*/
export function useClaimBoltzTransactionMutation(baseOptions?: Apollo.MutationHookOptions<ClaimBoltzTransactionMutation, ClaimBoltzTransactionMutationVariables>) {
return Apollo.useMutation<ClaimBoltzTransactionMutation, ClaimBoltzTransactionMutationVariables>(ClaimBoltzTransactionDocument, baseOptions);
}
export type ClaimBoltzTransactionMutationHookResult = ReturnType<typeof useClaimBoltzTransactionMutation>;
export type ClaimBoltzTransactionMutationResult = Apollo.MutationResult<ClaimBoltzTransactionMutation>;
export type ClaimBoltzTransactionMutationOptions = Apollo.BaseMutationOptions<ClaimBoltzTransactionMutation, ClaimBoltzTransactionMutationVariables>;

View file

@ -0,0 +1,88 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type CreateBoltzReverseSwapMutationVariables = Types.Exact<{
amount: Types.Scalars['Int'];
address?: Types.Maybe<Types.Scalars['String']>;
}>;
export type CreateBoltzReverseSwapMutation = (
{ __typename?: 'Mutation' }
& { createBoltzReverseSwap: (
{ __typename?: 'CreateBoltzReverseSwapType' }
& Pick<Types.CreateBoltzReverseSwapType, 'id' | 'invoice' | 'redeemScript' | 'onchainAmount' | 'timeoutBlockHeight' | 'lockupAddress' | 'minerFeeInvoice' | 'receivingAddress' | 'preimage' | 'preimageHash' | 'privateKey' | 'publicKey'>
& { decodedInvoice?: Types.Maybe<(
{ __typename?: 'decodeType' }
& Pick<Types.DecodeType, 'description' | 'destination' | 'expires_at' | 'id' | 'safe_tokens' | 'tokens'>
& { destination_node: (
{ __typename?: 'Node' }
& { node: (
{ __typename?: 'nodeType' }
& Pick<Types.NodeType, 'alias'>
) }
) }
)> }
) }
);
export const CreateBoltzReverseSwapDocument = gql`
mutation CreateBoltzReverseSwap($amount: Int!, $address: String) {
createBoltzReverseSwap(amount: $amount, address: $address) {
id
invoice
redeemScript
onchainAmount
timeoutBlockHeight
lockupAddress
minerFeeInvoice
receivingAddress
preimage
preimageHash
privateKey
publicKey
decodedInvoice {
description
destination
expires_at
id
safe_tokens
tokens
destination_node {
node {
alias
}
}
}
}
}
`;
export type CreateBoltzReverseSwapMutationFn = Apollo.MutationFunction<CreateBoltzReverseSwapMutation, CreateBoltzReverseSwapMutationVariables>;
/**
* __useCreateBoltzReverseSwapMutation__
*
* To run a mutation, you first call `useCreateBoltzReverseSwapMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateBoltzReverseSwapMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createBoltzReverseSwapMutation, { data, loading, error }] = useCreateBoltzReverseSwapMutation({
* variables: {
* amount: // value for 'amount'
* address: // value for 'address'
* },
* });
*/
export function useCreateBoltzReverseSwapMutation(baseOptions?: Apollo.MutationHookOptions<CreateBoltzReverseSwapMutation, CreateBoltzReverseSwapMutationVariables>) {
return Apollo.useMutation<CreateBoltzReverseSwapMutation, CreateBoltzReverseSwapMutationVariables>(CreateBoltzReverseSwapDocument, baseOptions);
}
export type CreateBoltzReverseSwapMutationHookResult = ReturnType<typeof useCreateBoltzReverseSwapMutation>;
export type CreateBoltzReverseSwapMutationResult = Apollo.MutationResult<CreateBoltzReverseSwapMutation>;
export type CreateBoltzReverseSwapMutationOptions = Apollo.BaseMutationOptions<CreateBoltzReverseSwapMutation, CreateBoltzReverseSwapMutationVariables>;

View file

@ -0,0 +1,21 @@
import { gql } from '@apollo/client';
export const CLAIM_BOLTZ_TRANSACTION = gql`
mutation ClaimBoltzTransaction(
$redeem: String!
$transaction: String!
$preimage: String!
$privateKey: String!
$destination: String!
$fee: Int!
) {
claimBoltzTransaction(
redeem: $redeem
transaction: $transaction
preimage: $preimage
privateKey: $privateKey
destination: $destination
fee: $fee
)
}
`;

View file

@ -0,0 +1,33 @@
import { gql } from '@apollo/client';
export const GET_BOLTZ_INFO = gql`
mutation CreateBoltzReverseSwap($amount: Int!, $address: String) {
createBoltzReverseSwap(amount: $amount, address: $address) {
id
invoice
redeemScript
onchainAmount
timeoutBlockHeight
lockupAddress
minerFeeInvoice
receivingAddress
preimage
preimageHash
privateKey
publicKey
decodedInvoice {
description
destination
expires_at
id
safe_tokens
tokens
destination_node {
node {
alias
}
}
}
}
}
`;

View file

@ -0,0 +1,51 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetBoltzInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type GetBoltzInfoQuery = (
{ __typename?: 'Query' }
& { getBoltzInfo: (
{ __typename?: 'BoltzInfoType' }
& Pick<Types.BoltzInfoType, 'max' | 'min' | 'feePercent'>
) }
);
export const GetBoltzInfoDocument = gql`
query GetBoltzInfo {
getBoltzInfo {
max
min
feePercent
}
}
`;
/**
* __useGetBoltzInfoQuery__
*
* To run a query within a React component, call `useGetBoltzInfoQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBoltzInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBoltzInfoQuery({
* variables: {
* },
* });
*/
export function useGetBoltzInfoQuery(baseOptions?: Apollo.QueryHookOptions<GetBoltzInfoQuery, GetBoltzInfoQueryVariables>) {
return Apollo.useQuery<GetBoltzInfoQuery, GetBoltzInfoQueryVariables>(GetBoltzInfoDocument, baseOptions);
}
export function useGetBoltzInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBoltzInfoQuery, GetBoltzInfoQueryVariables>) {
return Apollo.useLazyQuery<GetBoltzInfoQuery, GetBoltzInfoQueryVariables>(GetBoltzInfoDocument, baseOptions);
}
export type GetBoltzInfoQueryHookResult = ReturnType<typeof useGetBoltzInfoQuery>;
export type GetBoltzInfoLazyQueryHookResult = ReturnType<typeof useGetBoltzInfoLazyQuery>;
export type GetBoltzInfoQueryResult = Apollo.QueryResult<GetBoltzInfoQuery, GetBoltzInfoQueryVariables>;

View file

@ -0,0 +1,68 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetBoltzSwapStatusQueryVariables = Types.Exact<{
ids: Array<Types.Maybe<Types.Scalars['String']>>;
}>;
export type GetBoltzSwapStatusQuery = (
{ __typename?: 'Query' }
& { getBoltzSwapStatus: Array<Types.Maybe<(
{ __typename?: 'BoltzSwap' }
& Pick<Types.BoltzSwap, 'id'>
& { boltz?: Types.Maybe<(
{ __typename?: 'BoltzSwapStatus' }
& Pick<Types.BoltzSwapStatus, 'status'>
& { transaction?: Types.Maybe<(
{ __typename?: 'BoltzSwapTransaction' }
& Pick<Types.BoltzSwapTransaction, 'id' | 'hex' | 'eta'>
)> }
)> }
)>> }
);
export const GetBoltzSwapStatusDocument = gql`
query GetBoltzSwapStatus($ids: [String]!) {
getBoltzSwapStatus(ids: $ids) {
id
boltz {
status
transaction {
id
hex
eta
}
}
}
}
`;
/**
* __useGetBoltzSwapStatusQuery__
*
* To run a query within a React component, call `useGetBoltzSwapStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBoltzSwapStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBoltzSwapStatusQuery({
* variables: {
* ids: // value for 'ids'
* },
* });
*/
export function useGetBoltzSwapStatusQuery(baseOptions: Apollo.QueryHookOptions<GetBoltzSwapStatusQuery, GetBoltzSwapStatusQueryVariables>) {
return Apollo.useQuery<GetBoltzSwapStatusQuery, GetBoltzSwapStatusQueryVariables>(GetBoltzSwapStatusDocument, baseOptions);
}
export function useGetBoltzSwapStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBoltzSwapStatusQuery, GetBoltzSwapStatusQueryVariables>) {
return Apollo.useLazyQuery<GetBoltzSwapStatusQuery, GetBoltzSwapStatusQueryVariables>(GetBoltzSwapStatusDocument, baseOptions);
}
export type GetBoltzSwapStatusQueryHookResult = ReturnType<typeof useGetBoltzSwapStatusQuery>;
export type GetBoltzSwapStatusLazyQueryHookResult = ReturnType<typeof useGetBoltzSwapStatusLazyQuery>;
export type GetBoltzSwapStatusQueryResult = Apollo.QueryResult<GetBoltzSwapStatusQuery, GetBoltzSwapStatusQueryVariables>;

View file

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const GET_BOLTZ_INFO = gql`
query GetBoltzInfo {
getBoltzInfo {
max
min
feePercent
}
}
`;

View file

@ -0,0 +1,17 @@
import { gql } from '@apollo/client';
export const GET_BOLTZ_SWAP_STATUS = gql`
query GetBoltzSwapStatus($ids: [String]!) {
getBoltzSwapStatus(ids: $ids) {
id
boltz {
status
transaction {
id
hex
eta
}
}
}
}
`;

View file

@ -41,6 +41,8 @@ export type PermissionsType = {
export type Query = {
__typename?: 'Query';
getBoltzSwapStatus: Array<Maybe<BoltzSwap>>;
getBoltzInfo: BoltzInfoType;
getLnMarketsStatus: Scalars['String'];
getLnMarketsUrl: Scalars['String'];
getLnMarketsUserInfo?: Maybe<LnMarketsUserInfo>;
@ -89,6 +91,11 @@ export type Query = {
};
export type QueryGetBoltzSwapStatusArgs = {
ids: Array<Maybe<Scalars['String']>>;
};
export type QueryGetInvoiceStatusChangeArgs = {
id: Scalars['String'];
};
@ -205,6 +212,8 @@ export type QueryGetSessionTokenArgs = {
export type Mutation = {
__typename?: 'Mutation';
claimBoltzTransaction: Scalars['String'];
createBoltzReverseSwap: CreateBoltzReverseSwapType;
lnMarketsDeposit: Scalars['Boolean'];
lnMarketsWithdraw: Scalars['Boolean'];
lnMarketsLogin: AuthResponse;
@ -235,6 +244,22 @@ export type Mutation = {
};
export type MutationClaimBoltzTransactionArgs = {
redeem: Scalars['String'];
transaction: Scalars['String'];
preimage: Scalars['String'];
privateKey: Scalars['String'];
destination: Scalars['String'];
fee: Scalars['Int'];
};
export type MutationCreateBoltzReverseSwapArgs = {
amount: Scalars['Int'];
address?: Maybe<Scalars['String']>;
};
export type MutationLnMarketsDepositArgs = {
amount: Scalars['Int'];
};
@ -1081,3 +1106,46 @@ export type LnMarketsUserInfo = {
linkingpublickey?: Maybe<Scalars['String']>;
last_ip?: Maybe<Scalars['String']>;
};
export type BoltzInfoType = {
__typename?: 'BoltzInfoType';
max: Scalars['Int'];
min: Scalars['Int'];
feePercent: Scalars['Float'];
};
export type BoltzSwapTransaction = {
__typename?: 'BoltzSwapTransaction';
id?: Maybe<Scalars['String']>;
hex?: Maybe<Scalars['String']>;
eta?: Maybe<Scalars['Int']>;
};
export type BoltzSwapStatus = {
__typename?: 'BoltzSwapStatus';
status: Scalars['String'];
transaction?: Maybe<BoltzSwapTransaction>;
};
export type BoltzSwap = {
__typename?: 'BoltzSwap';
id?: Maybe<Scalars['String']>;
boltz?: Maybe<BoltzSwapStatus>;
};
export type CreateBoltzReverseSwapType = {
__typename?: 'CreateBoltzReverseSwapType';
id: Scalars['String'];
invoice: Scalars['String'];
redeemScript: Scalars['String'];
onchainAmount: Scalars['Int'];
timeoutBlockHeight: Scalars['Int'];
lockupAddress: Scalars['String'];
minerFeeInvoice?: Maybe<Scalars['String']>;
decodedInvoice?: Maybe<DecodeType>;
receivingAddress: Scalars['String'];
preimage?: Maybe<Scalars['String']>;
preimageHash?: Maybe<Scalars['String']>;
privateKey?: Maybe<Scalars['String']>;
publicKey?: Maybe<Scalars['String']>;
};

View file

@ -15,6 +15,7 @@ import {
BarChart2,
Icon,
Heart,
Shuffle,
} from 'react-feather';
import { useRouter } from 'next/router';
import { useBaseConnect } from 'src/hooks/UseBaseConnect';
@ -126,6 +127,7 @@ const STATS = '/stats';
const DONATIONS = '/leaderboard';
const CHAT = '/chat';
const SETTINGS = '/settings';
const SWAP = '/swap';
interface NavigationProps {
isBurger?: boolean;
@ -177,6 +179,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
{renderNavButton('Swap', SWAP, Shuffle, sidebar)}
{renderNavButton('Stats', STATS, BarChart2, sidebar)}
</ButtonSection>
);
@ -192,6 +195,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
{renderBurgerNav('Tools', TOOLS, Shield)}
{renderBurgerNav('Swap', SWAP, Shuffle)}
{renderBurgerNav('Stats', STATS, BarChart2)}
{connected && renderBurgerNav('Donations', DONATIONS, Heart)}
{renderBurgerNav('Chat', CHAT, MessageCircle)}

View file

@ -264,6 +264,21 @@ export const inputBorderColor = theme('mode', {
night: themeColors.grey8,
});
// ---------------------------------------
// SLIDER COLORS
// ---------------------------------------
export const sliderBackgroundColor = theme('mode', {
light: themeColors.grey3,
dark: themeColors.grey8,
night: themeColors.grey8,
});
export const sliderThumbColor = theme('mode', {
light: themeColors.grey8,
dark: 'white',
night: 'white',
});
// ---------------------------------------
// ICON COLORS
// ---------------------------------------

View file

@ -18,16 +18,22 @@ const QRCodeReader = dynamic(() => import('src/components/qrReader'), {
},
});
export const Pay = () => {
type PayProps = {
predefinedRequest?: string;
payCallback?: () => void;
};
export const Pay = ({ predefinedRequest, payCallback }: PayProps) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [request, setRequest] = useState<string>('');
const [request, setRequest] = useState<string>(predefinedRequest || '');
const [peers, setPeers] = useState<string[]>([]);
const [fee, setFee] = useState<number>(1);
const [paths, setPaths] = useState<number>(1);
const [pay, { loading }] = useBosPayMutation({
onCompleted: () => {
payCallback && payCallback();
toast.success('Payment Sent');
setRequest('');
},
@ -48,23 +54,27 @@ export const Pay = () => {
return (
<>
<SingleLine>
<InputWithDeco
title={'Request'}
value={request}
placeholder={'Invoice'}
inputCallback={value => setRequest(value)}
onEnter={() => handleEnter()}
inputMaxWidth={'440px'}
/>
<ColorButton
withMargin={'0 0 0 8px'}
onClick={() => setModalOpen(true)}
>
<Camera size={18} />
</ColorButton>
</SingleLine>
<Separation />
{!predefinedRequest && (
<>
<SingleLine>
<InputWithDeco
title={'Request'}
value={request}
placeholder={'Invoice'}
inputCallback={value => setRequest(value)}
onEnter={() => handleEnter()}
inputMaxWidth={'440px'}
/>
<ColorButton
withMargin={'0 0 0 8px'}
onClick={() => setModalOpen(true)}
>
<Camera size={18} />
</ColorButton>
</SingleLine>
<Separation />
</>
)}
<InputWithDeco
title={'Max Fee'}
value={fee}

View file

@ -35,9 +35,9 @@ export const OpenChannelCard = ({
const [fee, setFee] = useState(0);
const [publicKey, setPublicKey] = useState(initialPublicKey);
const [privateChannel, setPrivateChannel] = useState(false);
const [type, setType] = useState(dontShow || !fetchFees ? 'fee' : 'none');
const [type, setType] = useState('fee');
const [openChannel] = useOpenChannelMutation({
const [openChannel, { loading }] = useOpenChannelMutation({
onError: error => toast.error(getErrorContent(error)),
onCompleted: () => {
toast.success('Channel Opened');
@ -193,7 +193,9 @@ export const OpenChannelCard = ({
</InputWithDeco>
<Separation />
<ColorButton
loading={loading}
fullWidth={true}
disabled={!canOpen || loading}
onClick={() =>
openChannel({
variables: {
@ -205,7 +207,6 @@ export const OpenChannelCard = ({
},
})
}
disabled={!canOpen}
>
Open Channel
<ChevronRight size={18} />

View file

@ -0,0 +1,160 @@
import { InputWithDeco } from 'src/components/input/InputWithDeco';
import {
MultiButton,
SingleButton,
} from 'src/components/buttons/multiButton/MultiButton';
import {
Card,
DarkSubTitle,
Separation,
SingleLine,
SubTitle,
} from 'src/components/generic/Styled';
import { useEffect, useState } from 'react';
import { Slider } from 'src/components/slider';
import { Edit2, X } from 'react-feather';
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import styled from 'styled-components';
import { mediaWidths } from 'src/styles/Themes';
import { Input } from 'src/components/input';
import { useCreateBoltzReverseSwapMutation } from 'src/graphql/mutations/__generated__/createBoltzReverseSwap.generated';
import { toast } from 'react-toastify';
import { getErrorContent } from 'src/utils/error';
import { useMutationResultWithReset } from 'src/hooks/UseMutationWithReset';
import { saveToPc } from 'src/utils/helpers';
import { useSwapsDispatch } from './SwapContext';
type StartSwapProps = {
max: number;
min: number;
};
const StyledRow = styled.div`
display: flex;
width: 100%;
justify-content: flex-end;
@media (${mediaWidths.mobile}) {
justify-content: center;
}
`;
export const StartSwap = ({ max, min }: StartSwapProps) => {
const [amount, setAmount] = useState<number>(min);
const [isCustom, setIsCustom] = useState<boolean>(false);
const [isEdit, setIsEdit] = useState<boolean>(false);
const [address, setAddress] = useState<string>();
const [download, setDownload] = useState<boolean>(true);
const dispatch = useSwapsDispatch();
const [
getQuote,
{ data: _data, loading },
] = useCreateBoltzReverseSwapMutation({
onError: error => toast.error(getErrorContent(error)),
});
const [data, resetMutation] = useMutationResultWithReset(_data);
useEffect(() => {
if (!data?.createBoltzReverseSwap) return;
dispatch({
type: 'add',
swap: data.createBoltzReverseSwap,
});
download &&
saveToPc(
JSON.stringify(data.createBoltzReverseSwap),
`Swap-${data.createBoltzReverseSwap.id}`,
false,
true
);
resetMutation();
}, [data, dispatch, resetMutation, download]);
return (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<SingleLine>
<SubTitle>Start Swap</SubTitle>
<DarkSubTitle>Lightning BTC to BTC</DarkSubTitle>
</SingleLine>
<InputWithDeco title={'Amount'} noInput={true} amount={amount}>
<StyledRow>
{isEdit ? (
<Input
maxWidth={'440px'}
value={amount}
type={'number'}
placeholder={'Satoshis'}
onChange={value => setAmount(Number(value.target.value))}
/>
) : (
<Slider
maxWidth={'440px'}
value={amount}
max={max}
min={min}
onChange={value => setAmount(value)}
/>
)}
<ColorButton
withMargin={'0 0 0 8px'}
onClick={() => setIsEdit(p => !p)}
selected={isEdit}
>
{!isEdit ? <Edit2 size={18} /> : <X size={18} />}
</ColorButton>
</StyledRow>
</InputWithDeco>
<InputWithDeco title={'Address'} noInput={true}>
<MultiButton>
<SingleButton
selected={!isCustom}
onClick={() => {
setIsCustom(false);
setAddress('');
}}
>
Auto
</SingleButton>
<SingleButton selected={isCustom} onClick={() => setIsCustom(true)}>
Custom
</SingleButton>
</MultiButton>
</InputWithDeco>
{isCustom && (
<InputWithDeco
title={'Send to'}
placeholder={'Bitcoin address'}
value={address}
inputCallback={value => setAddress(value)}
/>
)}
<Separation />
<InputWithDeco title={'Download Backup'} noInput={true}>
<MultiButton>
<SingleButton selected={download} onClick={() => setDownload(true)}>
Yes
</SingleButton>
<SingleButton selected={!download} onClick={() => setDownload(false)}>
No
</SingleButton>
</MultiButton>
</InputWithDeco>
<ColorButton
disabled={!amount || loading}
loading={loading}
onClick={() =>
getQuote({ variables: { amount, ...(address && { address }) } })
}
arrow={true}
withMargin={'16px 0 0'}
fullWidth={true}
>
Get Quote
</ColorButton>
</Card>
);
};

View file

@ -0,0 +1,184 @@
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import {
MultiButton,
SingleButton,
} from 'src/components/buttons/multiButton/MultiButton';
import {
DarkSubTitle,
Separation,
SubTitle,
} from 'src/components/generic/Styled';
import { Input } from 'src/components/input';
import { InputWithDeco } from 'src/components/input/InputWithDeco';
import { useConfigState } from 'src/context/ConfigContext';
import { useClaimBoltzTransactionMutation } from 'src/graphql/mutations/__generated__/claimBoltzTransaction.generated';
import { useBitcoinFees } from 'src/hooks/UseBitcoinFees';
import { chartColors } from 'src/styles/Themes';
import { getErrorContent } from 'src/utils/error';
import styled from 'styled-components';
import { useSwapsDispatch, useSwapsState } from './SwapContext';
import { MEMPOOL } from './SwapStatus';
const S = {
warning: styled.div`
border: 1px solid ${chartColors.darkyellow};
background-color: rgba(255, 193, 10, 0.1);
padding: 4px 8px;
border-radius: 8px;
text-align: center;
font-size: 14px;
`,
};
export const SwapClaim = () => {
const { fetchFees } = useConfigState();
const { fast, halfHour, hour, dontShow } = useBitcoinFees();
const [fee, setFee] = useState<number>(0);
const [type, setType] = useState('fee');
const {
swaps,
claim,
claimType,
claimTransaction: transactionHex,
} = useSwapsState();
const dispatch = useSwapsDispatch();
const [
claimTransaction,
{ data, loading },
] = useClaimBoltzTransactionMutation({
onError: error => toast.error(getErrorContent(error)),
});
useEffect(() => {
if (!data?.claimBoltzTransaction || typeof claim !== 'number') return;
dispatch({
type: 'complete',
index: claim,
transactionId: data.claimBoltzTransaction,
});
toast.success('Transaction Claimed');
}, [data, dispatch, claim]);
const Missing = () => (
<>
<DarkSubTitle>
Missing information to claim transaction. Please try again.
</DarkSubTitle>
</>
);
if (typeof claim !== 'number') {
return <Missing />;
}
const claimingSwap = swaps[claim];
const { redeemScript, preimage, receivingAddress, privateKey } = claimingSwap;
if (!preimage || !transactionHex || !privateKey) {
return <Missing />;
}
const renderButton = (
onClick: () => void,
text: string,
selected: boolean
) => (
<SingleButton selected={selected} onClick={onClick}>
{text}
</SingleButton>
);
return (
<>
<SubTitle>Claim the Transaction</SubTitle>
{claimType === MEMPOOL && (
<>
<Separation />
<S.warning>
This will be an instant swap. This means that the locking
transaction from Boltz has still not been confirmed in the
blockchain.
</S.warning>
</>
)}
<Separation />
{fetchFees && !dontShow && (
<InputWithDeco title={'Fee'} noInput={true}>
<MultiButton>
{renderButton(
() => {
setType('none');
setFee(fast);
},
'Auto',
type === 'none'
)}
{renderButton(
() => {
setFee(0);
setType('fee');
},
'Fee (Sats/Byte)',
type === 'fee'
)}
</MultiButton>
</InputWithDeco>
)}
<InputWithDeco title={'Fee Amount'} amount={fee * 223} noInput={true}>
{type !== 'none' && (
<Input
maxWidth={'240px'}
placeholder={'Sats/Byte'}
type={'number'}
onChange={e => setFee(Number(e.target.value))}
/>
)}
{type === 'none' && (
<MultiButton>
{renderButton(
() => setFee(fast),
`Fastest (${fast} sats)`,
fee === fast
)}
{halfHour !== fast &&
renderButton(
() => setFee(halfHour),
`Half Hour (${halfHour} sats)`,
fee === halfHour
)}
{renderButton(
() => setFee(hour),
`Hour (${hour} sats)`,
fee === hour
)}
</MultiButton>
)}
</InputWithDeco>
<ColorButton
loading={loading}
disabled={loading || !fee || fee <= 0}
fullWidth={true}
withMargin={'16px 0 0'}
onClick={() =>
claimTransaction({
variables: {
redeem: redeemScript,
transaction: transactionHex,
preimage,
privateKey,
destination: receivingAddress,
fee,
},
})
}
>
Claim
</ColorButton>
</>
);
};

View file

@ -0,0 +1,112 @@
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { toast } from 'react-toastify';
import { CreateBoltzReverseSwap } from './types';
type State = {
swaps: CreateBoltzReverseSwap[];
open: number | null;
claim: number | null;
claimType: string | null;
claimTransaction: string | null;
};
type ActionType =
| {
type: 'add';
swap: CreateBoltzReverseSwap;
}
| { type: 'init'; swaps: CreateBoltzReverseSwap[] }
| { type: 'open'; open: number }
| {
type: 'claim';
claim: number;
claimType: string;
claimTransaction: string;
}
| { type: 'cleanup'; swaps: CreateBoltzReverseSwap[] }
| { type: 'complete'; index: number; transactionId: string }
| { type: 'close' };
type Dispatch = (action: ActionType) => void;
export const StateContext = createContext<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
const initialState: State = {
swaps: [],
open: null,
claim: null,
claimType: null,
claimTransaction: null,
};
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'init':
return { ...state, swaps: action.swaps };
case 'add':
localStorage.setItem(
'swaps',
JSON.stringify([...state.swaps, action.swap])
);
return { ...state, swaps: [...state.swaps, action.swap] };
case 'open':
return { ...state, open: action.open };
case 'claim':
return {
...state,
claim: action.claim,
claimType: action.claimType,
claimTransaction: action.claimTransaction,
};
case 'complete': {
state.swaps[action.index].claimTransaction = action.transactionId;
localStorage.setItem('swaps', JSON.stringify(state.swaps));
return { ...state, open: null, claim: null };
}
case 'cleanup':
localStorage.setItem('swaps', JSON.stringify(action.swaps));
return { ...state, swaps: action.swaps };
case 'close':
return { ...state, open: null, claim: null };
default:
return state;
}
};
const SwapsProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(stateReducer, initialState);
useEffect(() => {
try {
const swaps = JSON.parse(localStorage.getItem('swaps') || '[]');
dispatch({ type: 'init', swaps });
} catch (error) {
toast.error('Invalid swaps stored in browser');
}
}, []);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useSwapsState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error('useSwapsState must be used within a SwapsProvider');
}
return context;
};
const useSwapsDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useSwapsDispatch must be used within a SwapsProvider');
}
return context;
};
export { SwapsProvider, useSwapsState, useSwapsDispatch };

View file

@ -0,0 +1,20 @@
import { useState, useEffect } from 'react';
import { formatDistanceToNowStrict } from 'date-fns';
export const useSwapExpire = (date?: string) => {
const [, setCount] = useState(0);
useEffect(() => {
const myInterval = setInterval(() => {
setCount(p => p + 1);
}, 1000);
return () => {
clearInterval(myInterval);
};
});
if (!date) return '';
return `(Expires in ${formatDistanceToNowStrict(new Date(date), {
unit: 'second',
})})`;
};

View file

@ -0,0 +1,92 @@
import {
renderLine,
getNodeLink,
getAddressLink,
} from 'src/components/generic/helpers';
import { Card, Separation, SubTitle } from 'src/components/generic/Styled';
import { Price } from 'src/components/price/Price';
import { chartColors } from 'src/styles/Themes';
import styled from 'styled-components';
import { Pay } from '../home/account/pay/Pay';
import { useSwapsDispatch, useSwapsState } from './SwapContext';
const S = {
info: styled.div`
border: 1px solid ${chartColors.green};
background-color: rgba(10, 255, 59, 0.05);
padding: 8px 16px;
border-radius: 8px;
`,
warning: styled.div`
border: 1px solid ${chartColors.darkyellow};
background-color: rgba(255, 193, 10, 0.1);
padding: 4px 8px;
border-radius: 8px;
text-align: center;
font-size: 14px;
`,
};
export const SwapQuote = () => {
const { swaps, open } = useSwapsState();
const dispatch = useSwapsDispatch();
if (typeof open !== 'number') {
return null;
}
const openSwap = swaps[open];
if (!openSwap?.decodedInvoice) {
return (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
Error decoding invoice in swap.
</Card>
);
}
const { decodedInvoice, onchainAmount, receivingAddress, invoice } = openSwap;
const handlePaid = () => {
dispatch({ type: 'close' });
};
return (
<>
<SubTitle>{`Swap - ${openSwap.id}`}</SubTitle>
<Separation />
{renderLine(
'Sending to',
getNodeLink(
decodedInvoice.destination,
decodedInvoice.destination_node?.node?.alias
)
)}
{renderLine('Description', decodedInvoice.description)}
<Separation />
<S.info>
<SubTitle>Transaction</SubTitle>
{renderLine('You send', <Price amount={decodedInvoice.tokens} />)}
{renderLine(
'Fees you pay (Boltz + Chain fee)',
<Price amount={decodedInvoice.tokens - onchainAmount} />
)}
<Separation />
{renderLine(
'Lockup Transaction Value',
<Price amount={onchainAmount} />
)}
{renderLine('At BTC Address', getAddressLink(receivingAddress))}
</S.info>
<Separation />
<SubTitle>Pay Swap Invoice</SubTitle>
<Separation />
<Pay predefinedRequest={invoice} payCallback={handlePaid} />
<Separation />
<S.warning>
It is ok to close this modal after 5 seconds of having paid even if it
still shows as loading.
</S.warning>
</>
);
};

View file

@ -0,0 +1,276 @@
import { Fragment, useEffect, useState } from 'react';
import { Trash } from 'react-feather';
import ReactTooltip from 'react-tooltip';
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import { getTransactionLink } from 'src/components/generic/helpers';
import {
Card,
DarkSubTitle,
Separation,
SingleLine,
SubTitle,
} from 'src/components/generic/Styled';
import Modal from 'src/components/modal/ReactModal';
import { useGetBoltzSwapStatusLazyQuery } from 'src/graphql/queries/__generated__/getBoltzSwapStatus.generated';
import { chartColors, themeColors } from 'src/styles/Themes';
import styled from 'styled-components';
import { SwapClaim } from './SwapClaim';
import { useSwapsDispatch, useSwapsState } from './SwapContext';
import { useSwapExpire } from './SwapExpire';
import { SwapQuote } from './SwapQuote';
import { EnrichedSwap } from './types';
const S = {
row: styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
`,
single: styled.div`
display: flex;
align-items: center;
`,
expired: styled.div`
border: 1px solid ${chartColors.orange};
background-color: rgba(255, 193, 10, 0.1);
padding: 4px 8px;
border-radius: 8px;
`,
warning: styled.div`
border: 1px solid ${chartColors.darkyellow};
background-color: rgba(255, 193, 10, 0.1);
padding: 4px 8px;
border-radius: 8px;
`,
ready: styled.div`
border: 1px solid ${chartColors.green};
background-color: rgba(10, 255, 59, 0.05);
padding: 4px 8px;
border-radius: 8px;
`,
claiming: styled.div`
border: 1px solid ${chartColors.green};
background-color: rgba(10, 255, 59, 0.05);
color: ${chartColors.green};
padding: 4px 8px;
border-radius: 8px;
`,
finished: styled.div`
border: 1px solid ${themeColors.grey8};
background-color: rgba(10, 255, 59, 0.05);
padding: 4px 8px;
border-radius: 8px;
`,
};
const CREATED = 'swap.created';
export const MEMPOOL = 'transaction.mempool';
const CONFIRMED = 'transaction.confirmed';
const SETTLED = 'invoice.settled';
const EXPIRED = 'swap.expired';
const REFUNDED = 'transaction.refunded';
const SwapRow = ({ swap, index }: { swap: EnrichedSwap; index: number }) => {
const dispatch = useSwapsDispatch();
const ReadyComponent = () => {
const time = useSwapExpire(swap.decodedInvoice?.expires_at);
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.single>
<S.ready>Ready to Pay {time}</S.ready>
<ColorButton
onClick={() => dispatch({ type: 'open', open: index })}
arrow={true}
withMargin={'0 0 0 4px'}
>
Pay
</ColorButton>
</S.single>
</S.row>
);
};
const ErrorComponent = () => (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.expired>Unable to get status</S.expired>
</S.row>
);
if (!swap?.id) return null;
if (!swap.boltz?.status) {
return <ErrorComponent />;
}
switch (swap.boltz.status) {
case EXPIRED:
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.expired>Expired</S.expired>
</S.row>
);
case REFUNDED:
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.warning>Refunded</S.warning>
</S.row>
);
case CREATED:
return <ReadyComponent />;
case MEMPOOL:
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.single>
<S.warning>Waiting for confirmation</S.warning>
<ColorButton
onClick={() =>
dispatch({
type: 'claim',
claim: index,
claimType: MEMPOOL,
claimTransaction: swap.boltz?.transaction?.hex || '',
})
}
arrow={true}
withMargin={'0 0 0 4px'}
>
Claim Instantly
</ColorButton>
</S.single>
</S.row>
);
case CONFIRMED:
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.single>
<S.claiming>Ready to Claim</S.claiming>
<ColorButton
onClick={() =>
dispatch({
type: 'claim',
claim: index,
claimType: CONFIRMED,
claimTransaction: swap.boltz?.transaction?.hex || '',
})
}
arrow={true}
withMargin={'0 0 0 4px'}
>
Claim
</ColorButton>
</S.single>
</S.row>
);
case SETTLED:
return (
<S.row>
<DarkSubTitle>{`Id: ${swap.id}`}</DarkSubTitle>
<S.single>
{getTransactionLink(swap.claimTransaction)}
<S.finished>Completed</S.finished>
</S.single>
</S.row>
);
default:
return <ErrorComponent />;
}
};
export const SwapStatus = () => {
const { swaps, open, claim } = useSwapsState();
const dispatch = useSwapsDispatch();
const [enriched, setEnriched] = useState<EnrichedSwap[]>([]);
const [getStatus, { data, loading }] = useGetBoltzSwapStatusLazyQuery({
pollInterval: 2000,
fetchPolicy: 'network-only',
});
useEffect(() => {
if (swaps.length) {
getStatus({
variables: {
ids: swaps.map((s: { id: string }) => s.id).filter(Boolean),
},
});
}
}, [swaps, getStatus]);
useEffect(() => {
if (loading || !data?.getBoltzSwapStatus) return;
const swapsWithState: EnrichedSwap[] = swaps.map(swap => {
const status = data.getBoltzSwapStatus.find(s => s?.id === swap.id);
const enriched = { ...swap, boltz: status?.boltz };
return enriched;
});
setEnriched(swapsWithState);
}, [data, loading, swaps]);
const handleCleanup = () => {
const cleaned = enriched.filter(s => {
if (!s.boltz?.status) return true;
const status = s.boltz.status;
if (status === SETTLED || status === REFUNDED || status === EXPIRED) {
return false;
}
return true;
});
dispatch({ type: 'cleanup', swaps: cleaned });
};
if (!swaps.length || !data?.getBoltzSwapStatus || loading) {
return (
<>
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<SubTitle>Swap History</SubTitle>
<Separation />
<DarkSubTitle>You have not started any swaps.</DarkSubTitle>
</Card>
</>
);
}
return (
<>
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<SingleLine>
<SubTitle>Swap History</SubTitle>
<div data-tip data-for={`cleanup`}>
<ColorButton disabled={loading} onClick={handleCleanup}>
<Trash size={18} />
</ColorButton>
</div>
</SingleLine>
<Separation />
{enriched.map((swap, index) => (
<Fragment key={`${swap?.id}-${index}`}>
<SwapRow swap={swap} index={index} />
</Fragment>
))}
</Card>
<ReactTooltip id={`cleanup`} effect={'solid'}>
Cleanup expired, refunded and completed swaps.
</ReactTooltip>
<Modal
isOpen={typeof open === 'number' || typeof claim === 'number'}
closeCallback={() => dispatch({ type: 'close' })}
>
{typeof open === 'number' ? <SwapQuote /> : <SwapClaim />}
</Modal>
</>
);
};

56
src/views/swap/index.tsx Normal file
View file

@ -0,0 +1,56 @@
import { toast } from 'react-toastify';
import { renderLine } from 'src/components/generic/helpers';
import {
Card,
DarkSubTitle,
SingleLine,
SubTitle,
} from 'src/components/generic/Styled';
import { Link } from 'src/components/link/Link';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { Price } from 'src/components/price/Price';
import { Subtitle } from 'src/components/typography/Styled';
import { useGetBoltzInfoQuery } from 'src/graphql/queries/__generated__/getBoltzInfo.generated';
import { getErrorContent } from 'src/utils/error';
import { SwapsProvider } from './SwapContext';
import { StartSwap } from './StartSwap';
import { SwapStatus } from './SwapStatus';
export const SwapView = () => {
const { data, loading, error } = useGetBoltzInfoQuery({
onError: error => toast.error(getErrorContent(error)),
});
if (loading) {
return <LoadingCard title={'Swap'} />;
}
if (error || !data?.getBoltzInfo) {
return (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
Unable to connect to Boltz
</Card>
);
}
const { max, min, feePercent } = data.getBoltzInfo;
return (
<SwapsProvider>
<SingleLine>
<Subtitle>Reverse Swap</Subtitle>
<DarkSubTitle>
<Link href={'https://boltz.exchange/'}>powered by Boltz</Link>
</DarkSubTitle>
</SingleLine>
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<SubTitle>Information</SubTitle>
{renderLine('Boltz fee', `${feePercent}%`)}
{renderLine('Minimum amount', <Price amount={min} />)}
{renderLine('Maximum amount', <Price amount={max} />)}
</Card>
<StartSwap max={max} min={min} />
<SwapStatus />
</SwapsProvider>
);
};

40
src/views/swap/types.ts Normal file
View file

@ -0,0 +1,40 @@
import {
BoltzSwapStatus,
CreateBoltzReverseSwapType,
DecodeType,
NodeType,
} from 'src/graphql/types';
export type CreateBoltzReverseSwap = Pick<
CreateBoltzReverseSwapType,
| 'id'
| 'invoice'
| 'redeemScript'
| 'onchainAmount'
| 'timeoutBlockHeight'
| 'lockupAddress'
| 'minerFeeInvoice'
| 'receivingAddress'
| 'preimage'
| 'privateKey'
> & {
decodedInvoice?:
| (Pick<
DecodeType,
| 'description'
| 'destination'
| 'expires_at'
| 'id'
| 'safe_tokens'
| 'tokens'
> & {
destination_node: {
node: Pick<NodeType, 'alias'>;
};
})
| null;
} & { claimTransaction?: string };
export type EnrichedSwap = {
boltz?: Pick<BoltzSwapStatus, 'status' | 'transaction'> | null;
} & CreateBoltzReverseSwap;