mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-21 14:04:03 +01:00
chore: 🔧 boltz
This commit is contained in:
parent
c8156a00cd
commit
9a0e607754
39 changed files with 2056 additions and 65 deletions
|
@ -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
139
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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
17
pages/swap.tsx
Normal 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
79
server/api/Boltz.ts
Normal 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
28
server/helpers/crypto.ts
Normal 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 };
|
||||
};
|
|
@ -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]}`
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
255
server/schema/boltz/resolvers.ts
Normal file
255
server/schema/boltz/resolvers.ts
Normal 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 },
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
41
server/schema/boltz/types.ts
Normal file
41
server/schema/boltz/types.ts
Normal 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
|
||||
}
|
||||
`;
|
|
@ -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 });
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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%; */
|
||||
|
|
66
src/components/slider/index.tsx
Normal file
66
src/components/slider/index.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
};
|
63
src/graphql/mutations/__generated__/claimBoltzTransaction.generated.tsx
generated
Normal file
63
src/graphql/mutations/__generated__/claimBoltzTransaction.generated.tsx
generated
Normal 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>;
|
88
src/graphql/mutations/__generated__/createBoltzReverseSwap.generated.tsx
generated
Normal file
88
src/graphql/mutations/__generated__/createBoltzReverseSwap.generated.tsx
generated
Normal 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>;
|
21
src/graphql/mutations/claimBoltzTransaction.ts
Normal file
21
src/graphql/mutations/claimBoltzTransaction.ts
Normal 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
|
||||
)
|
||||
}
|
||||
`;
|
33
src/graphql/mutations/createBoltzReverseSwap.ts
Normal file
33
src/graphql/mutations/createBoltzReverseSwap.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
51
src/graphql/queries/__generated__/getBoltzInfo.generated.tsx
generated
Normal file
51
src/graphql/queries/__generated__/getBoltzInfo.generated.tsx
generated
Normal 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>;
|
68
src/graphql/queries/__generated__/getBoltzSwapStatus.generated.tsx
generated
Normal file
68
src/graphql/queries/__generated__/getBoltzSwapStatus.generated.tsx
generated
Normal 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>;
|
11
src/graphql/queries/getBoltzInfo.ts
Normal file
11
src/graphql/queries/getBoltzInfo.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_BOLTZ_INFO = gql`
|
||||
query GetBoltzInfo {
|
||||
getBoltzInfo {
|
||||
max
|
||||
min
|
||||
feePercent
|
||||
}
|
||||
}
|
||||
`;
|
17
src/graphql/queries/getBoltzSwapStatus.ts
Normal file
17
src/graphql/queries/getBoltzSwapStatus.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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']>;
|
||||
};
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
// ---------------------------------------
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
160
src/views/swap/StartSwap.tsx
Normal file
160
src/views/swap/StartSwap.tsx
Normal 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>
|
||||
);
|
||||
};
|
184
src/views/swap/SwapClaim.tsx
Normal file
184
src/views/swap/SwapClaim.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
112
src/views/swap/SwapContext.tsx
Normal file
112
src/views/swap/SwapContext.tsx
Normal 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 };
|
20
src/views/swap/SwapExpire.tsx
Normal file
20
src/views/swap/SwapExpire.tsx
Normal 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',
|
||||
})})`;
|
||||
};
|
92
src/views/swap/SwapQuote.tsx
Normal file
92
src/views/swap/SwapQuote.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
276
src/views/swap/SwapStatus.tsx
Normal file
276
src/views/swap/SwapStatus.tsx
Normal 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
56
src/views/swap/index.tsx
Normal 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
40
src/views/swap/types.ts
Normal 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;
|
Loading…
Add table
Reference in a new issue