├── coverage ├── ruby.png ├── snow.png ├── amber.png ├── glass.png ├── updown.png └── emerald.png ├── docker ├── .DS_Store ├── geth │ ├── startGeth.sh │ ├── setup.sh │ └── Dockerfile ├── regtest │ ├── docker │ │ └── regtest │ │ │ └── data │ │ │ └── core │ │ │ └── cookies │ │ │ ├── .bitcoin-cookie │ │ │ └── .litecoin-cookie │ ├── data │ │ ├── lnd │ │ │ ├── macaroons │ │ │ │ ├── macaroons.db │ │ │ │ ├── admin.macaroon │ │ │ │ ├── invoice.macaroon │ │ │ │ └── readonly.macaroon │ │ │ ├── lnd.conf │ │ │ └── certificates │ │ │ │ ├── tls.key │ │ │ │ └── tls.cert │ │ └── core │ │ │ └── config.conf │ ├── scripts │ │ ├── entrypoint.sh │ │ ├── configure.sh │ │ ├── setup.sh │ │ └── utils.sh │ └── startRegtest.sh ├── stacks │ ├── .env │ ├── start.sh │ └── config │ │ └── Config.toml ├── lnd │ ├── patches │ │ ├── ltcd.patch │ │ └── btcutil.patch │ └── Dockerfile ├── berkeley-db │ └── Dockerfile ├── eclair │ └── Dockerfile ├── c-lightning │ └── Dockerfile ├── zcash │ └── Dockerfile ├── dogecoin-core │ └── Dockerfile ├── litecoin-core │ └── Dockerfile └── bitcoin-core │ └── Dockerfile ├── jest.config.js ├── .gitattributes ├── contracts ├── claim-for-trait.clar ├── nft-trait.clar ├── restricted-token-trait.clar ├── triggerswap_v1.clar ├── sip-010-trait.clar ├── triggerswap_v2.clar ├── triggerswap_v5.clar └── usda-token.clar ├── lib ├── notifications │ ├── Markup.ts │ ├── DiskUsageChecker.ts │ └── OtpManager.ts ├── backup │ └── Errors.ts ├── grpc │ └── Errors.ts ├── lightning │ ├── Errors.ts │ └── ConnectionHelper.ts ├── rates │ ├── data │ │ ├── Exchange.ts │ │ └── exchanges │ │ │ ├── CoinbasePro.ts │ │ │ ├── Poloniex.ts │ │ │ ├── Okcoin.ts │ │ │ ├── Bitfinex.ts │ │ │ ├── Binance.ts │ │ │ ├── Kraken.ts │ │ │ └── Coingecko.ts │ ├── Errors.ts │ └── RateCalculator.ts ├── api │ └── Errors.ts ├── cli │ ├── commands │ │ ├── GetInfo.ts │ │ ├── NewKeys.ts │ │ ├── NewPreimage.ts │ │ ├── GetBalance.ts │ │ ├── GetAddress.ts │ │ ├── DeriveKeys.ts │ │ ├── UpdateTimeoutBlockDelta.ts │ │ ├── SendCoins.ts │ │ ├── Claim.ts │ │ └── Refund.ts │ ├── BuilderComponents.ts │ ├── rsk │ │ ├── commands │ │ │ ├── Mine.ts │ │ │ ├── Send.ts │ │ │ └── Refund.ts │ │ └── EthereumUtils.ts │ ├── stacks │ │ ├── commands │ │ │ ├── Mine.ts │ │ │ └── Send.ts │ │ └── StacksUtils.ts │ ├── ethereum │ │ ├── commands │ │ │ ├── Mine.ts │ │ │ ├── Send.ts │ │ │ ├── Refund.ts │ │ │ └── Lock.ts │ │ └── EthereumUtils.ts │ └── Command.ts ├── consts │ ├── Errors.ts │ └── Consts.ts ├── DenominationConverter.ts ├── db │ ├── models │ │ ├── DatabaseVersion.ts │ │ ├── ChainTip.ts │ │ ├── Pair.ts │ │ ├── KeyProvider.ts │ │ ├── PendingEthereumTransaction.ts │ │ ├── ProviderSwap.ts │ │ ├── StacksTransaction.ts │ │ ├── DirectSwap.ts │ │ ├── ChannelCreation.ts │ │ └── Client.ts │ ├── PairRepository.ts │ ├── PendingEthereumTransactionRepository.ts │ ├── DatabaseVersionRepository.ts │ ├── ChainTipRepository.ts │ ├── KeyRepository.ts │ ├── DirectSwapRepository.ts │ ├── ProviderSwapRepository.ts │ ├── StacksTransactionRepository.ts │ └── ChannelCreationRepository.ts ├── service │ ├── PaymentRequestUtils.ts │ └── InvoiceExpiryHelper.ts ├── wallet │ ├── rsk │ │ ├── EthereumUtils.ts │ │ ├── Errors.ts │ │ └── EthereumTransactionTracker.ts │ ├── ethereum │ │ ├── EthereumUtils.ts │ │ ├── Errors.ts │ │ └── EthereumTransactionTracker.ts │ ├── providers │ │ ├── WalletProviderInterface.ts │ │ ├── RskWalletProvider.ts │ │ └── EtherWalletProvider.ts │ └── stacks │ │ ├── Errors.ts │ │ └── EthereumTransactionTracker.ts ├── BaseClient.ts ├── chain │ ├── Errors.ts │ └── MempoolSpace.ts ├── VersionCheck.ts ├── CliTools.ts └── Logger.ts ├── tslint-alt.json ├── dockercmd.sh ├── test ├── integration │ ├── rates │ │ └── data │ │ │ ├── Consts.ts │ │ │ ├── DataAggregator.spec.ts │ │ │ └── Exchanges.spec.ts │ ├── Nodes.ts │ └── wallet │ │ ├── providers │ │ └── EtherWalletProvider.spec.ts │ │ └── EthereumTools.ts ├── unit │ ├── data │ │ └── Utils.ts │ ├── swap │ │ └── InvoiceUtils.ts │ ├── DenominationConverter.spec.ts │ ├── service │ │ └── InvoiceExpiryHelper.spec.ts │ ├── wallet │ │ └── ethereum │ │ │ └── EthereumUtils.spec.ts │ ├── rates │ │ └── RateCalculator.spec.ts │ ├── VersionCheck.spec.ts │ ├── notifications │ │ └── OtpManager.spec.ts │ └── db │ │ └── Migration.spec.ts └── Utils.ts ├── mkdocs.yml ├── bin ├── stats ├── checkFailed ├── report ├── boltz-rsk ├── boltz-ethereum ├── boltz-stacks ├── boltz-cli └── boltzd ├── docker-compose ├── docker-compose.yml ├── readme.md ├── frontend │ └── .env └── lnstx │ └── boltz.conf ├── .github └── workflows │ ├── clarinet.yml │ ├── ci.yml │ └── docker-image.yml ├── Dockerfile ├── tools ├── otp.py ├── requirements.txt ├── ci backup.yml ├── sse.py ├── README.md └── miner_fee.py ├── tests ├── Wrapped-USD_test.ts └── restricted-token-trait_test.ts ├── docs ├── index.md ├── swap-files.md ├── channel-creation.md └── 0-confirmation.md ├── parseGitCommit.js ├── .eslintrc.js ├── README.md ├── Clarinet.toml ├── .gitignore ├── .git-commit-template.txt └── tslint.json /coverage/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/ruby.png -------------------------------------------------------------------------------- /coverage/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/snow.png -------------------------------------------------------------------------------- /docker/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/docker/.DS_Store -------------------------------------------------------------------------------- /coverage/amber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/amber.png -------------------------------------------------------------------------------- /coverage/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/glass.png -------------------------------------------------------------------------------- /coverage/updown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/updown.png -------------------------------------------------------------------------------- /coverage/emerald.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/coverage/emerald.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /docker/geth/startGeth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo docker run \ 4 | -d \ 5 | --name geth \ 6 | boltz/geth:1.10.4 7 | -------------------------------------------------------------------------------- /docker/regtest/docker/regtest/data/core/cookies/.bitcoin-cookie: -------------------------------------------------------------------------------- 1 | __cookie__:258546fbc559c18e454bae9d83a692988bf673d9371c146481a4ffea490a2a99 -------------------------------------------------------------------------------- /docker/regtest/docker/regtest/data/core/cookies/.litecoin-cookie: -------------------------------------------------------------------------------- 1 | __cookie__:80b496110d63784e061b6d758b569b3d96b8689049ae532e1cd15cd2fa4099aa -------------------------------------------------------------------------------- /docker/regtest/data/lnd/macaroons/macaroons.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/docker/regtest/data/lnd/macaroons/macaroons.db -------------------------------------------------------------------------------- /docker/regtest/data/lnd/macaroons/admin.macaroon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/docker/regtest/data/lnd/macaroons/admin.macaroon -------------------------------------------------------------------------------- /docker/regtest/data/lnd/lnd.conf: -------------------------------------------------------------------------------- 1 | [Application Options] 2 | debuglevel=debug 3 | 4 | noseedbackup=1 5 | 6 | [protocol] 7 | protocol.wumbo-channels = true 8 | -------------------------------------------------------------------------------- /docker/regtest/data/lnd/macaroons/invoice.macaroon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/docker/regtest/data/lnd/macaroons/invoice.macaroon -------------------------------------------------------------------------------- /docker/regtest/data/lnd/macaroons/readonly.macaroon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LNSwap/lnstxbridge/HEAD/docker/regtest/data/lnd/macaroons/readonly.macaroon -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | bin/* linguist-language=Javascript 2 | proto/* linguist-vendored 3 | lib/proto/* linguist-generated 4 | package-lock.json linguist-generated 5 | -------------------------------------------------------------------------------- /contracts/claim-for-trait.clar: -------------------------------------------------------------------------------- 1 | (define-trait claim-for-trait 2 | ( 3 | ;; claim/mint an nft for a principal 4 | (claim-for (principal) (response uint uint)) 5 | ) 6 | ) -------------------------------------------------------------------------------- /docker/regtest/data/core/config.conf: -------------------------------------------------------------------------------- 1 | regtest=1 2 | 3 | rest=1 4 | server=1 5 | 6 | daemon=1 7 | txindex=1 8 | 9 | fallbackfee=0.00001 10 | 11 | rpcallowip=0.0.0.0/0 12 | 13 | [regtest] 14 | rpcbind=0.0.0.0 15 | -------------------------------------------------------------------------------- /lib/notifications/Markup.ts: -------------------------------------------------------------------------------- 1 | // TODO: add all other used emojis to this object 2 | export const Emojis = { 3 | Checkmark: ':white_check_mark:', 4 | RotatingLight: ':rotating_light:', 5 | }; 6 | 7 | export const codeBlock = '```'; 8 | -------------------------------------------------------------------------------- /docker/geth/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting GETH" 4 | nohup geth --datadir /gethData --dev > /dev/null 2>&1 & num="0" 5 | 6 | echo "Waiting for startup" 7 | sleep 15 8 | 9 | echo "Killing GETH" 10 | killall geth 11 | 12 | echo "Waiting for shutdown" 13 | sleep 15 14 | -------------------------------------------------------------------------------- /docker/regtest/data/lnd/certificates/tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIJUg4g7YTbDKMqFoVMUzVTa0DNVsLYjvIq2U3kS9KHf1oAoGCCqGSM49 3 | AwEHoUQDQgAE/6uZ2iFvJAysD2Q2gdFOijBxq9ZlQKGInVz2j8qHXmmp54PBEbrU 4 | O5KMFgEnkbCzMh9YfSoCRTsFV2S50wVghg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tslint-alt.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tslint.json", 3 | "rules": { 4 | "no-boolean-literal-compare": false, 5 | "no-floating-promises": false, 6 | "no-unnecessary-type-assertion": false, 7 | "await-promise": false, 8 | "no-circular-imports": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dockercmd.sh: -------------------------------------------------------------------------------- 1 | # helper script to pass commands to docker containers 2 | container="regtest" 3 | bar="$1" 4 | echo ${bar} 5 | 6 | cmd='bash -c "bitcoin-cli send {\"'${bar}'\":1}"' 7 | final_cmd="docker exec -it $container $cmd" 8 | 9 | echo "running command: \"$final_cmd\"" 10 | eval $final_cmd -------------------------------------------------------------------------------- /test/integration/rates/data/Consts.ts: -------------------------------------------------------------------------------- 1 | export const baseAsset = 'LTC'; 2 | export const quoteAsset = 'BTC'; 3 | 4 | export const checkPrice = (price: number): void => { 5 | expect(typeof price).toEqual('number'); 6 | 7 | expect(price).toBeLessThan(1); 8 | expect(price).toBeGreaterThan(0); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/backup/Errors.ts: -------------------------------------------------------------------------------- 1 | import { Error } from '../consts/Types'; 2 | import { concatErrorCode } from '../Utils'; 3 | import { ErrorCodePrefix } from '../consts/Enums'; 4 | 5 | export default { 6 | BACKUP_DISABLED: (): Error => ({ 7 | message: 'backups are disabled because of incomplete configuration', 8 | code: concatErrorCode(ErrorCodePrefix.Backup, 0), 9 | }), 10 | }; 11 | -------------------------------------------------------------------------------- /lib/grpc/Errors.ts: -------------------------------------------------------------------------------- 1 | import { Error } from '../consts/Types'; 2 | import { ErrorCodePrefix } from '../consts/Enums'; 3 | import { concatErrorCode } from '../Utils'; 4 | 5 | export default { 6 | COULD_NOT_BIND: (host: string, port: number): Error => ({ 7 | message: `gRPC couldn't bind on: ${host}:${port}`, 8 | code: concatErrorCode(ErrorCodePrefix.Grpc, 0), 9 | }), 10 | }; 11 | -------------------------------------------------------------------------------- /lib/lightning/Errors.ts: -------------------------------------------------------------------------------- 1 | import { Error } from '../consts/Types'; 2 | import { ErrorCodePrefix } from '../consts/Enums'; 3 | import { concatErrorCode } from '../Utils'; 4 | 5 | export default { 6 | COULD_NOT_FIND_FILES: (chainType: string): Error => ({ 7 | message: `could not find required files for ${chainType} LND`, 8 | code: concatErrorCode(ErrorCodePrefix.Lnd, 0), 9 | }), 10 | }; 11 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Boltz documentation 2 | theme: readthedocs 3 | 4 | nav: 5 | - Home: index.md 6 | - REST API: api.md 7 | - Swap Lifecycle: lifecycle.md 8 | - Swap Files: swap-files.md 9 | - Scripting: scripting.md 10 | - Channel Creation: channel-creation.md 11 | - 0-confirmation: 0-confirmation.md 12 | - Regtest environment: regtest.md 13 | - Deployment: deployment.md 14 | -------------------------------------------------------------------------------- /docker/regtest/scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source utils.sh 4 | 5 | startNodes 6 | bitcoin-cli loadwallet $DEFAULT_WALLET_NAME > /dev/null 7 | 8 | startLnds 9 | 10 | mkdir -p /cookies 11 | cp /root/.bitcoin/regtest/.cookie /cookies/.bitcoin-cookie 12 | cp /root/.litecoin/regtest/.cookie /cookies/.litecoin-cookie 13 | 14 | chmod 777 /cookies/.* 15 | 16 | while true; do 17 | sleep 1 18 | done 19 | -------------------------------------------------------------------------------- /bin/stats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { generateStats } = require('../dist/lib/CliTools'); 3 | 4 | const { argv } = require('yargs').options({ 5 | dbpath: { 6 | describe: 'Path to the database file', 7 | type: 'string', 8 | }, 9 | }); 10 | 11 | // Delete non-config keys from argv 12 | delete argv._; 13 | delete argv.$0; 14 | delete argv.help; 15 | delete argv.version; 16 | 17 | generateStats(argv); 18 | -------------------------------------------------------------------------------- /lib/rates/data/Exchange.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Exchange { 4 | getPrice(baseAsset: string, quoteAsset: string): Promise; 5 | } 6 | 7 | const makeRequest = async (url: string): Promise => { 8 | const response = await axios.get(url, { 9 | timeout: 5000, 10 | }); 11 | 12 | return response.data; 13 | }; 14 | 15 | export default Exchange; 16 | export { makeRequest }; 17 | -------------------------------------------------------------------------------- /lib/api/Errors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | COULD_NOT_PARSE_HEX: (argName: string): string => `could not parse hex string: ${argName}`, 3 | INVALID_PARAMETER: (argName: string): string => `invalid parameter: ${argName}`, 4 | UNDEFINED_PARAMETER: (argName: string): string => `undefined parameter: ${argName}`, 5 | UNSUPPORTED_PARAMETER: (symbol: string, argName: string): string => `${symbol} does not support ${argName}`, 6 | }; 7 | -------------------------------------------------------------------------------- /bin/checkFailed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { checkFailedSwaps } = require('../dist/lib/CliTools'); 3 | 4 | const { argv } = require('yargs').options({ 5 | dbpath: { 6 | describe: 'Path to the database file', 7 | type: 'string', 8 | }, 9 | }); 10 | 11 | // Delete non-config keys from argv 12 | delete argv._; 13 | delete argv.$0; 14 | delete argv.help; 15 | delete argv.version; 16 | 17 | checkFailedSwaps(argv); 18 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | lnstxbridge_api: 4 | image: pseudozach/lnstxbridge:latest 5 | ports: 6 | - "9002:9002" 7 | volumes: 8 | - ./lnstx/:/root/.lnstx/ 9 | lnstxbridge_frontend: 10 | image: pseudozach/lnstxbridge-frontend:latest 11 | ports: 12 | - "3000:3000" 13 | volumes: 14 | - ./frontend/.env:/usr/src/app/.env 15 | depends_on: 16 | - lnstxbridge_api 17 | -------------------------------------------------------------------------------- /lib/cli/commands/GetInfo.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { callback, loadBoltzClient } from '../Command'; 3 | import { GetInfoRequest } from '../../proto/boltzrpc_pb'; 4 | 5 | export const command = 'getinfo'; 6 | 7 | export const describe = 'gets information about the Boltz instance and the nodes it is connected to'; 8 | 9 | export const handler = (argv: Arguments): void => { 10 | loadBoltzClient(argv).getInfo(new GetInfoRequest(), callback); 11 | }; 12 | -------------------------------------------------------------------------------- /test/unit/data/Utils.ts: -------------------------------------------------------------------------------- 1 | import { OrderSide } from '../../../lib/consts/Enums'; 2 | 3 | export const createSwap = (isSwap: boolean, isBuy: boolean, params: Record): T => { 4 | return { 5 | ...params, 6 | fee: 1000, 7 | pair: 'LTC/BTC', 8 | minerFee: 10000, 9 | routingFee: isSwap ? 1 : 0, 10 | orderSide: isBuy ? OrderSide.BUY : OrderSide.SELL, 11 | createdAt: new Date('2020-11-10 21:45:57.690 +00:00') 12 | } as any as T; 13 | }; 14 | -------------------------------------------------------------------------------- /docker/stacks/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | GIT_TAG=master 3 | PG_HOST=postgres 4 | PG_PORT=5432 5 | PG_USER=postgres 6 | PG_PASSWORD=postgres 7 | PG_DATABASE=postgres 8 | STACKS_CHAIN_ID=0x80000000 9 | V2_POX_MIN_AMOUNT_USTX=90000000260 10 | STACKS_CORE_EVENT_PORT=3700 11 | STACKS_CORE_EVENT_HOST=0.0.0.0 12 | STACKS_BLOCKCHAIN_API_PORT=3999 13 | STACKS_BLOCKCHAIN_API_HOST=0.0.0.0 14 | STACKS_BLOCKCHAIN_API_DB=pg 15 | STACKS_CORE_RPC_HOST=stacks-blockchain 16 | STACKS_CORE_RPC_PORT=20443 17 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/CoinbasePro.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class CoinbasePro implements Exchange { 4 | private static readonly API = 'https://api.pro.coinbase.com'; 5 | 6 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 7 | const response = await makeRequest(`${CoinbasePro.API}/products/${baseAsset}-${quoteAsset}/ticker`); 8 | return Number(response.price); 9 | } 10 | } 11 | 12 | export default CoinbasePro; 13 | -------------------------------------------------------------------------------- /bin/report: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { generateReport } = require('../dist/lib/CliTools'); 3 | 4 | const { argv } = require('yargs').options({ 5 | dbpath: { 6 | describe: 'Path to the database file', 7 | type: 'string', 8 | }, 9 | reportpath: { 10 | describe: 'Path to the CSV report', 11 | type: 'string', 12 | }, 13 | }); 14 | 15 | // Delete non-config keys from argv 16 | delete argv._; 17 | delete argv.$0; 18 | delete argv.help; 19 | delete argv.version; 20 | 21 | generateReport(argv); 22 | -------------------------------------------------------------------------------- /lib/cli/commands/NewKeys.ts: -------------------------------------------------------------------------------- 1 | import { ECPair } from 'bitcoinjs-lib'; 2 | import { getHexString, stringify } from '../../Utils'; 3 | 4 | export const command = 'newkeys'; 5 | 6 | export const describe = 'generates a new keypair'; 7 | 8 | export const builder = {}; 9 | 10 | export const handler = (): void => { 11 | const keys = ECPair.makeRandom({}); 12 | 13 | console.log(stringify({ 14 | publicKey: getHexString(keys.publicKey), 15 | privateKey: getHexString(keys.privateKey as Buffer), 16 | })); 17 | }; 18 | -------------------------------------------------------------------------------- /contracts/nft-trait.clar: -------------------------------------------------------------------------------- 1 | (define-trait nft-trait 2 | ( 3 | ;; Last token ID, limited to uint range 4 | (get-last-token-id () (response uint uint)) 5 | 6 | ;; URI for metadata associated with the token 7 | (get-token-uri (uint) (response (optional (string-ascii 256)) uint)) 8 | 9 | ;; Owner of a given token identifier 10 | (get-owner (uint) (response (optional principal) uint)) 11 | 12 | ;; Transfer from the sender to a new principal 13 | (transfer (uint principal principal) (response bool uint)) 14 | ) 15 | ) -------------------------------------------------------------------------------- /.github/workflows/clarinet.yml: -------------------------------------------------------------------------------- 1 | name: Clarity CI 2 | on: [push] 3 | jobs: 4 | tests: 5 | name: "Test contracts with Clarinet" 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: "Execute unit tests" 10 | uses: docker://hirosystems/clarinet:latest 11 | with: 12 | args: test --coverage --manifest-path=./Clarinet.toml 13 | - name: "Export code coverage" 14 | uses: codecov/codecov-action@v1 15 | with: 16 | files: ./coverage.lcov 17 | verbose: true -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | RUN apt-get update 4 | RUN apt-get -y install curl gnupg git rsync build-essential 5 | RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - 6 | RUN apt-get -y install nodejs 7 | 8 | WORKDIR /usr/src/app 9 | 10 | # Install app dependencies 11 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 12 | # where available (npm@5+) 13 | # COPY package-docker.json ./package.json 14 | COPY . ./ 15 | RUN npm install 16 | RUN npm run compile 17 | 18 | EXPOSE 9002 19 | CMD [ "npm", "run", "start" ] -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Poloniex.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Poloniex implements Exchange { 4 | private static readonly API = 'https://poloniex.com/public'; 5 | 6 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 7 | const response = await makeRequest(`${Poloniex.API}?command=returnTicker`); 8 | const pairData = response[`${quoteAsset.toUpperCase()}_${baseAsset.toUpperCase()}`]; 9 | 10 | return Number(pairData.last); 11 | } 12 | } 13 | 14 | export default Poloniex; 15 | -------------------------------------------------------------------------------- /tools/otp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Generates OTP tokens""" 3 | from argparse import ArgumentParser 4 | import pyotp 5 | 6 | if __name__ == "__main__": 7 | PARSER = ArgumentParser(description="Generate OTP tokens") 8 | 9 | # CLI arguments 10 | PARSER.add_argument( 11 | "secret", 12 | help="Secret from which the token will be generated", 13 | type=str, 14 | nargs=1, 15 | ) 16 | 17 | ARGS = PARSER.parse_args() 18 | 19 | TOTP = pyotp.TOTP(ARGS.secret[0]) 20 | print("Token: {}".format(TOTP.now())) 21 | -------------------------------------------------------------------------------- /lib/consts/Errors.ts: -------------------------------------------------------------------------------- 1 | import { concatErrorCode } from '../Utils'; 2 | import { ErrorCodePrefix } from './Enums'; 3 | import { Error } from './Types'; 4 | 5 | export default { 6 | IS_DISCONNECTED: (clientName: string): Error => ({ 7 | message: `${clientName} is disconnected`, 8 | code: concatErrorCode(ErrorCodePrefix.General, 1), 9 | }), 10 | COULD_NOT_PARSE_CONFIG: (filename: string, error: string): Error => ({ 11 | message: `could not parse ${filename} config: ${error}`, 12 | code: concatErrorCode(ErrorCodePrefix.General, 2), 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /lib/cli/commands/NewPreimage.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { crypto } from 'bitcoinjs-lib'; 3 | import { getHexString, stringify } from '../../Utils'; 4 | 5 | export const command = 'newpreimage'; 6 | 7 | export const describe = 'generates a new preimage and its hash'; 8 | 9 | export const builder = {}; 10 | 11 | export const handler = (): void => { 12 | const preimage = randomBytes(32); 13 | 14 | console.log(stringify({ 15 | preimage: getHexString(preimage), 16 | preimageHash: getHexString(crypto.sha256(preimage)), 17 | })); 18 | }; 19 | -------------------------------------------------------------------------------- /bin/boltz-rsk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('yargs') 4 | .options({ 5 | 'provider': { 6 | describe: 'HTTP endpoint of the web3 provider', 7 | default: 'http://127.0.0.1:4444', 8 | type: 'string', 9 | alias: 'p', 10 | }, 11 | 'signer': { 12 | describe: 'address for the JsonRpcSigner', 13 | default: '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', 14 | type: 'string', 15 | alias: 's', 16 | }, 17 | }) 18 | .commandDir('../dist/lib/cli/rsk/commands/') 19 | .demandCommand(1, '') 20 | .strict() 21 | .argv; 22 | -------------------------------------------------------------------------------- /docker/regtest/scripts/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir /cookies 4 | 5 | # Bitcoin Core 6 | echo "zmqpubrawtx=tcp://0.0.0.0:29000" >> /root/.bitcoin/bitcoin.conf 7 | echo "zmqpubrawblock=tcp://0.0.0.0:29001" >> /root/.bitcoin/bitcoin.conf 8 | echo "zmqpubhashblock=tcp://0.0.0.0:29002" >> /root/.bitcoin/bitcoin.conf 9 | 10 | # Litecoin Core 11 | echo "zmqpubrawtx=tcp://0.0.0.0:30000" >> /root/.litecoin/litecoin.conf 12 | echo "zmqpubrawblock=tcp://0.0.0.0:30001" >> /root/.litecoin/litecoin.conf 13 | echo "zmqpubhashblock=tcp://0.0.0.0:30002" >> /root/.litecoin/litecoin.conf 14 | -------------------------------------------------------------------------------- /bin/boltz-ethereum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('yargs') 4 | .options({ 5 | 'provider': { 6 | describe: 'HTTP endpoint of the web3 provider', 7 | default: 'http://127.0.0.1:8545', 8 | type: 'string', 9 | alias: 'p', 10 | }, 11 | 'signer': { 12 | describe: 'address for the JsonRpcSigner', 13 | default: '0x455fCf9Af2938F8f1603182bFBC867af1731d450', 14 | type: 'string', 15 | alias: 's', 16 | }, 17 | }) 18 | .commandDir('../dist/lib/cli/ethereum/commands/') 19 | .demandCommand(1, '') 20 | .strict() 21 | .argv; 22 | -------------------------------------------------------------------------------- /bin/boltz-stacks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('yargs') 4 | .options({ 5 | 'provider': { 6 | describe: 'HTTP endpoint of the web3 provider', 7 | default: 'http://127.0.0.1:4444', 8 | type: 'string', 9 | alias: 'p', 10 | }, 11 | 'signer': { 12 | describe: 'address for the JsonRpcSigner', 13 | default: '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', 14 | type: 'string', 15 | alias: 's', 16 | }, 17 | }) 18 | .commandDir('../dist/lib/cli/stacks/commands/') 19 | // .demandCommand(1, '') 20 | // .strict() 21 | .argv; 22 | -------------------------------------------------------------------------------- /lib/cli/commands/GetBalance.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import BuilderComponents from '../BuilderComponents'; 3 | import { callback, loadBoltzClient } from '../Command'; 4 | import { GetBalanceRequest } from '../../proto/boltzrpc_pb'; 5 | 6 | export const command = 'getbalance'; 7 | 8 | export const describe = 'gets the balance of all wallets'; 9 | 10 | export const builder = { 11 | symbol: BuilderComponents.symbol, 12 | }; 13 | 14 | export const handler = (argv: Arguments): void => { 15 | loadBoltzClient(argv).getBalance(new GetBalanceRequest(), callback); 16 | }; 17 | -------------------------------------------------------------------------------- /contracts/restricted-token-trait.clar: -------------------------------------------------------------------------------- 1 | (define-trait restricted-token-trait 2 | ( 3 | ;; Called to detect if a transfer restriction will take place. Returns the 4 | ;; error code that explains why the transfer failed. 5 | (detect-transfer-restriction (uint principal principal) (response uint uint)) 6 | 7 | ;; Returns human readable string for a specific transfer restriction error code 8 | ;; which is returned from (detect-transfer-restriction). 9 | ;; This is a convenience function for end user wallets. 10 | (message-for-restriction (uint) (response (string-ascii 1024) uint)) 11 | ) 12 | ) -------------------------------------------------------------------------------- /lib/rates/Errors.ts: -------------------------------------------------------------------------------- 1 | import { Error } from '../consts/Types'; 2 | import { concatErrorCode } from '../Utils'; 3 | import { ErrorCodePrefix } from '../consts/Enums'; 4 | 5 | export default { 6 | COULD_NOT_FIND_RATE: (pair: string): Error => ({ 7 | message: `could not find rate of pair: ${pair}`, 8 | code: concatErrorCode(ErrorCodePrefix.Rates, 0), 9 | }), 10 | CONFIGURATION_INCOMPLETE: (symbol: string, missingValue: string): Error => ({ 11 | message: `could not init currency ${symbol} because of missing config value: ${missingValue}`, 12 | code: concatErrorCode(ErrorCodePrefix.Rates, 1), 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Okcoin.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Okcoin implements Exchange { 4 | private static readonly API = 'https://www.okcoin.com/api/spot/v3'; 5 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 6 | const baseusdrate = await makeRequest(`${Okcoin.API}/instruments/${baseAsset.toUpperCase()}-USD/ticker`); 7 | const quoteusdrate = await makeRequest(`${Okcoin.API}/instruments/${quoteAsset.toUpperCase()}-USD/ticker`); 8 | return Number(baseusdrate.best_ask/quoteusdrate.best_bid); 9 | } 10 | } 11 | 12 | export default Okcoin; 13 | -------------------------------------------------------------------------------- /lib/DenominationConverter.ts: -------------------------------------------------------------------------------- 1 | const decimals = 100000000; 2 | 3 | /** 4 | * Convert whole coins to satoshis 5 | */ 6 | export const coinsToSatoshis = (coins: number): number => { 7 | return coins * decimals; 8 | }; 9 | 10 | /** 11 | * Convert satoshis to whole coins and remove trailing zeros 12 | */ 13 | export const satoshisToCoins = (satoshis: number): number => { 14 | return roundToDecimals(satoshis / decimals, 8); 15 | }; 16 | 17 | /** 18 | * Round a number to a specific amount of decimals 19 | */ 20 | const roundToDecimals = (number: number, decimals: number): number => { 21 | return Number(number.toFixed(decimals)); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/db/models/DatabaseVersion.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type DatabaseVersionType = { 4 | version: number; 5 | }; 6 | 7 | class DatabaseVersion extends Model implements DatabaseVersionType { 8 | public version!: number; 9 | 10 | public static load = (sequelize: Sequelize): void => { 11 | DatabaseVersion.init({ 12 | version: { type: new DataTypes.INTEGER, primaryKey: true, allowNull: false }, 13 | }, { 14 | sequelize, 15 | timestamps: false, 16 | tableName: 'version', 17 | }); 18 | } 19 | } 20 | 21 | export default DatabaseVersion; 22 | export { DatabaseVersionType }; 23 | -------------------------------------------------------------------------------- /lib/consts/Consts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | import { OutputType } from 'boltz-core'; 3 | 4 | export const ReverseSwapOutputType = OutputType.Bech32; 5 | 6 | // Decimals from WEI to 10 ** -8 7 | export const etherDecimals = BigNumber.from(10).pow(BigNumber.from(10)); 8 | 9 | // Decimals from GWEI to WEI 10 | // 1 wei = 0.000000001 gwei 11 | export const gweiDecimals = BigNumber.from(10).pow(BigNumber.from(9)); 12 | 13 | // This amount will be multiplied with the current gas price to determine 14 | // how much Ether should be sent to the claim address as prepay miner fee 15 | export const ethereumPrepayMinerFeeGasLimit = BigNumber.from(100000); 16 | 17 | -------------------------------------------------------------------------------- /bin/boltz-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('yargs') 4 | .options({ 5 | rpc: { 6 | hidden: true, 7 | }, 8 | 'rpc.host': { 9 | describe: 'gRPC service host', 10 | alias: 'h', 11 | default: 'localhost', 12 | type: 'string', 13 | }, 14 | 'rpc.port': { 15 | describe: 'gRPC service port', 16 | alias: 'p', 17 | default: 9000, 18 | type: 'number', 19 | }, 20 | tlscertpath: { 21 | describe: 'Path to the TLS certificate', 22 | type: 'string', 23 | alias: 't', 24 | }, 25 | }) 26 | .commandDir('../dist/lib/cli/commands/') 27 | .demandCommand(1, '') 28 | .strict() 29 | .argv; 30 | -------------------------------------------------------------------------------- /lib/cli/commands/GetAddress.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import BuilderComponents from '../BuilderComponents'; 3 | import { callback, loadBoltzClient } from '../Command'; 4 | import { GetAddressRequest } from '../../proto/boltzrpc_pb'; 5 | 6 | export const command = 'getaddress '; 7 | 8 | export const describe = 'gets an address of a specified wallet'; 9 | 10 | export const builder = { 11 | symbol: BuilderComponents.symbol, 12 | }; 13 | 14 | export const handler = (argv: Arguments): void => { 15 | const request = new GetAddressRequest(); 16 | 17 | request.setSymbol(argv.symbol); 18 | 19 | loadBoltzClient(argv).getAddress(request, callback); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Bitfinex.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Bitfinex implements Exchange { 4 | private static readonly API = 'https://api.bitfinex.com/v2/'; 5 | 6 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 7 | const response = await makeRequest(`${Bitfinex.API}/ticker/t${this.replaceUSDT(baseAsset)}${this.replaceUSDT(quoteAsset)}`); 8 | return Number(response[6]); 9 | } 10 | 11 | /** 12 | * Bitfinex calls USDT "UST" internally 13 | */ 14 | private replaceUSDT = (asset: string) => { 15 | return asset === 'USDT' ? 'UST' : asset; 16 | } 17 | } 18 | 19 | export default Bitfinex; 20 | -------------------------------------------------------------------------------- /lib/service/PaymentRequestUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the BIP21 prefix for a currency 3 | */ 4 | const getBip21Prefix = (symbol: string) => { 5 | return symbol === 'BTC' ? 'bitcoin' : 'litecoin'; 6 | }; 7 | 8 | /** 9 | * Encode a BIP21 payment request 10 | */ 11 | export const encodeBip21 = (symbol: string, address: string, satoshis: number, label?: string): string | undefined => { 12 | if (symbol !== 'BTC' && symbol !== 'LTC') { 13 | return undefined; 14 | } 15 | 16 | let request = `${getBip21Prefix(symbol)}:${address}?amount=${satoshis / 100000000}`; 17 | 18 | if (label) { 19 | request += `&label=${label.replace(/ /g, '%20')}`; 20 | } 21 | 22 | return request; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/db/PairRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import Pair, { PairType } from './models/Pair'; 3 | 4 | class PairRepository { 5 | public getPairs = (): Promise => { 6 | return Pair.findAll({}); 7 | } 8 | 9 | public addPair = (pair: PairType): Promise => { 10 | return Pair.create(pair); 11 | } 12 | 13 | public removePair = (id: string): Promise => { 14 | return Pair.destroy({ 15 | where: { 16 | id: { 17 | [Op.eq]: id, 18 | }, 19 | }, 20 | }); 21 | } 22 | 23 | public dropTable = async (): Promise => { 24 | return Pair.drop(); 25 | } 26 | } 27 | 28 | export default PairRepository; 29 | -------------------------------------------------------------------------------- /docker/lnd/patches/ltcd.patch: -------------------------------------------------------------------------------- 1 | --- a/chaincfg/params.go 2 | +++ b/chaincfg/params.go 3 | @@ -380,11 +380,11 @@ var RegressionNetParams = Params{ 4 | 5 | // Human-readable part for Bech32 encoded segwit addresses, as defined in 6 | // BIP 173. 7 | - Bech32HRPSegwit: "bcrt", // always bcrt for reg test net 8 | + Bech32HRPSegwit: "rltc", // always rltc for reg test net 9 | 10 | // Address encoding magics 11 | PubKeyHashAddrID: 0x6f, // starts with m or n 12 | - ScriptHashAddrID: 0xc4, // starts with 2 13 | + ScriptHashAddrID: 0x3a, // starts with Q 14 | PrivateKeyID: 0xef, // starts with 9 (uncompressed) or c (compressed) 15 | 16 | // BIP32 hierarchical deterministic extended key magics 17 | -------------------------------------------------------------------------------- /contracts/triggerswap_v1.clar: -------------------------------------------------------------------------------- 1 | ;; triggers claimstx from lnswap and claim-for from any nft for trustless LN purchases 2 | 3 | ;; claim/mint an nft for a principal 4 | (define-trait claim-for-trait 5 | ( 6 | (claim-for (principal) (response uint uint)) 7 | ) 8 | ) 9 | 10 | (define-public (triggerStx (preimage (buff 32)) (amount (buff 16)) (claimAddress (buff 42)) (refundAddress (buff 42)) (timelock (buff 16)) (nftPrincipal ) (userPrincipal principal)) 11 | (begin 12 | (try! (contract-call? .stxswap_v8 claimStx preimage amount claimAddress refundAddress timelock)) 13 | (try! (contract-call? nftPrincipal claim-for userPrincipal)) 14 | (ok true) 15 | ) 16 | ) -------------------------------------------------------------------------------- /lib/db/models/ChainTip.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type ChainTipType = { 4 | symbol: string; 5 | height: number; 6 | }; 7 | 8 | class ChainTip extends Model implements ChainTipType { 9 | public symbol!: string; 10 | public height!: number; 11 | 12 | public static load = (sequelize: Sequelize): void => { 13 | ChainTip.init({ 14 | symbol: { type: new DataTypes.STRING(255), primaryKey: true }, 15 | height: { type: new DataTypes.INTEGER(), allowNull: false }, 16 | }, { 17 | sequelize, 18 | timestamps: false, 19 | tableName: 'chainTips', 20 | }); 21 | } 22 | } 23 | 24 | export default ChainTip; 25 | export { ChainTipType }; 26 | -------------------------------------------------------------------------------- /docker/regtest/startRegtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run \ 4 | -d \ 5 | --name regtest \ 6 | --volume "${PWD}"/docker/regtest/data/core/cookies:/cookies \ 7 | -p 10735:10735 \ 8 | -p 9736:9735 \ 9 | -p 18443:18443 \ 10 | -p 19443:19443 \ 11 | -p 18332:18332 \ 12 | -p 18232:18232 \ 13 | -p 29000:29000 \ 14 | -p 29001:29001 \ 15 | -p 29002:29002 \ 16 | -p 30000:30000 \ 17 | -p 30001:30001 \ 18 | -p 30002:30002 \ 19 | -p 40000:40000 \ 20 | -p 40001:40001 \ 21 | -p 40002:40002 \ 22 | -p 50000:50000 \ 23 | -p 50001:50001 \ 24 | -p 50002:50002 \ 25 | -p 10009:10009 \ 26 | -p 10011:10011 \ 27 | -p 11009:11009 \ 28 | -p 11010:11010 \ 29 | -p 8081:8081 \ 30 | boltz/regtest:3.1.2 31 | -------------------------------------------------------------------------------- /lib/db/PendingEthereumTransactionRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import PendingEthereumTransaction from './models/PendingEthereumTransaction'; 3 | 4 | class PendingEthereumTransactionRepository { 5 | public findByNonce = (nonce: number): Promise => { 6 | return PendingEthereumTransaction.findAll({ 7 | where: { 8 | nonce: { 9 | [Op.lte]: nonce, 10 | }, 11 | } 12 | }); 13 | } 14 | 15 | public addTransaction = (hash: string, nonce: number): Promise => { 16 | return PendingEthereumTransaction.create({ 17 | hash, 18 | nonce, 19 | }); 20 | } 21 | } 22 | 23 | export default PendingEthereumTransactionRepository; 24 | -------------------------------------------------------------------------------- /lib/db/DatabaseVersionRepository.ts: -------------------------------------------------------------------------------- 1 | import DatabaseVersion from './models/DatabaseVersion'; 2 | 3 | class DatabaseVersionRepository { 4 | public getVersion = (): Promise => { 5 | return DatabaseVersion.findOne(); 6 | } 7 | 8 | public createVersion = (version: number): Promise => { 9 | return DatabaseVersion.create({ 10 | version, 11 | }); 12 | } 13 | 14 | public updateVersion = async (newVersion: number): Promise => { 15 | await DatabaseVersion.update({ 16 | version: newVersion, 17 | }, { 18 | where: {}, 19 | }); 20 | } 21 | 22 | public dropTable = (): Promise => { 23 | return DatabaseVersion.drop(); 24 | } 25 | } 26 | 27 | export default DatabaseVersionRepository; 28 | -------------------------------------------------------------------------------- /lib/db/models/Pair.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type PairType = { 4 | id: string; 5 | base: string; 6 | quote: string; 7 | }; 8 | 9 | class Pair extends Model implements PairType { 10 | public id!: string; 11 | public base!: string; 12 | public quote!: string; 13 | 14 | public static load = (sequelize: Sequelize): void => { 15 | Pair.init({ 16 | id: { type: new DataTypes.STRING(255), primaryKey: true }, 17 | base: { type: new DataTypes.STRING(255), allowNull: false }, 18 | quote: { type: new DataTypes.STRING(255), allowNull: false }, 19 | }, { 20 | sequelize, 21 | tableName: 'pairs', 22 | timestamps: false, 23 | }); 24 | } 25 | } 26 | 27 | export default Pair; 28 | export { PairType }; 29 | -------------------------------------------------------------------------------- /test/unit/swap/InvoiceUtils.ts: -------------------------------------------------------------------------------- 1 | import bolt11 from '@boltz/bolt11'; 2 | import { randomBytes } from 'crypto'; 3 | import { ECPair } from 'bitcoinjs-lib'; 4 | import { getHexString, getUnixTime } from '../../../lib/Utils'; 5 | 6 | const invoiceSigningKeys = ECPair.makeRandom(); 7 | 8 | export const createInvoice = (preimageHash?: string, timestamp?: number): string => { 9 | const invoiceEncode = bolt11.encode({ 10 | satoshis: 100, 11 | timestamp: timestamp || getUnixTime(), 12 | payeeNodeKey: getHexString(invoiceSigningKeys.publicKey), 13 | tags: [ 14 | { 15 | data: preimageHash || getHexString(randomBytes(32)), 16 | tagName: 'payment_hash', 17 | }, 18 | ], 19 | }); 20 | 21 | return bolt11.sign(invoiceEncode, invoiceSigningKeys.privateKey!).paymentRequest!; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/cli/commands/DeriveKeys.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import BuilderComponents from '../BuilderComponents'; 3 | import { callback, loadBoltzClient } from '../Command'; 4 | import { DeriveKeysRequest } from '../../proto/boltzrpc_pb'; 5 | 6 | export const command = 'derivekeys '; 7 | 8 | export const describe = 'derives a keypair from the index of an HD wallet'; 9 | 10 | export const builder = { 11 | symbol: BuilderComponents.symbol, 12 | index: { 13 | describe: 'index of the keypair', 14 | type: 'number', 15 | }, 16 | }; 17 | 18 | export const handler = (argv: Arguments): void => { 19 | const request = new DeriveKeysRequest(); 20 | 21 | request.setSymbol(argv.symbol); 22 | request.setIndex(argv.index); 23 | 24 | loadBoltzClient(argv).deriveKeys(request, callback); 25 | }; 26 | -------------------------------------------------------------------------------- /test/unit/DenominationConverter.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomRange } from '../Utils'; 2 | import { coinsToSatoshis, satoshisToCoins } from '../../lib/DenominationConverter'; 3 | 4 | describe('DenominationConverter', () => { 5 | test('should convert satoshis to whole coins', () => { 6 | const randomSat = randomRange(7000); 7 | const coins = Number((randomSat / 100000000).toFixed(8)); 8 | 9 | expect(satoshisToCoins(randomSat)).toEqual(coins); 10 | expect(satoshisToCoins(1000)).toEqual(0.00001); 11 | }); 12 | 13 | test('should convert whole coins to satoshis', () => { 14 | const randomCoins = randomRange(1000); 15 | const sats = Number((randomCoins * 100000000).toFixed(8)); 16 | 17 | expect(coinsToSatoshis(randomCoins)).toEqual(sats); 18 | expect(coinsToSatoshis(100)).toEqual(10000000000); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Binance.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Binance implements Exchange { 4 | private static readonly API = 'https://api.binance.com/api/v3'; 5 | 6 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 7 | // const response = await makeRequest(`${Binance.API}/ticker/price?symbol=${baseAsset.toUpperCase()}${quoteAsset.toUpperCase()}`); 8 | // if(!response.price) { 9 | const baseusdtrate = await makeRequest(`${Binance.API}/ticker/price?symbol=${baseAsset.toUpperCase()}USDT`); 10 | const quoteusdtrate = await makeRequest(`${Binance.API}/ticker/price?symbol=${quoteAsset.toUpperCase()}USDT`); 11 | return Number(baseusdtrate.price/quoteusdtrate.price); 12 | // } else { 13 | // return Number(response.price); 14 | // } 15 | } 16 | } 17 | 18 | export default Binance; 19 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Kraken.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Kraken implements Exchange { 4 | private static readonly API = 'https://api.kraken.com/0/public'; 5 | 6 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 7 | const pair = `${this.parseAsset(baseAsset)}${this.parseAsset(quoteAsset)}`; 8 | const response = await makeRequest(`${Kraken.API}/Ticker?pair=${pair}`); 9 | const lastTrade = (Object.values(response['result'])[0] as Record)['c']; 10 | 11 | return Number(lastTrade[0]); 12 | } 13 | 14 | private parseAsset = (asset: string) => { 15 | const assetUpperCase = asset.toUpperCase(); 16 | 17 | switch (assetUpperCase) { 18 | case 'BTC': return 'XBT'; 19 | default: return assetUpperCase; 20 | } 21 | } 22 | } 23 | 24 | export default Kraken; 25 | -------------------------------------------------------------------------------- /lib/cli/BuilderComponents.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | network: { 3 | describe: 'network on which the should be broadcast', 4 | type: 'string', 5 | }, 6 | privateKey: { 7 | describe: 'private key of the key pair', 8 | type: 'string', 9 | }, 10 | redeemScript: { 11 | describe: 'redeem script of the swap', 12 | type: 'string', 13 | }, 14 | rawTransaction: { 15 | describe: 'raw lockup transaction', 16 | type: 'string', 17 | }, 18 | destinationAddress: { 19 | describe: 'address to which the coins should be claimed', 20 | type: 'string', 21 | }, 22 | symbol: { 23 | describe: 'ticker symbol of the currency', 24 | type: 'string', 25 | }, 26 | token: { 27 | describe: 'whether a token should be claimed', 28 | type: 'boolean', 29 | }, 30 | timeoutBlockHeight: { 31 | describe: 'refund timeout', 32 | type: 'string', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /tests/Wrapped-USD_test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; 4 | 5 | Clarinet.test({ 6 | name: "Ensure that <...>", 7 | async fn(chain: Chain, accounts: Map) { 8 | let block = chain.mineBlock([ 9 | /* 10 | * Add transactions with: 11 | * Tx.contractCall(...) 12 | */ 13 | ]); 14 | assertEquals(block.receipts.length, 0); 15 | assertEquals(block.height, 2); 16 | 17 | block = chain.mineBlock([ 18 | /* 19 | * Add transactions with: 20 | * Tx.contractCall(...) 21 | */ 22 | ]); 23 | assertEquals(block.receipts.length, 0); 24 | assertEquals(block.height, 3); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /lib/db/ChainTipRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import ChainTip from './models/ChainTip'; 3 | 4 | class ChainTipRepository { 5 | public getChainTips = (): Promise => { 6 | return ChainTip.findAll(); 7 | } 8 | 9 | public findOrCreateTip = async (symbol: string, height: number): Promise => { 10 | const [chainTip] = await ChainTip.findOrCreate({ 11 | where: { 12 | symbol: { 13 | [Op.eq]: symbol, 14 | } 15 | }, 16 | defaults: { 17 | symbol, 18 | height, 19 | }, 20 | }); 21 | 22 | // console.log("findOrCreateTip: ", symbol, JSON.stringify(chainTip)); 23 | return chainTip; 24 | } 25 | 26 | public updateTip = (chainTip: ChainTip, height: number): Promise => { 27 | return chainTip.update({ 28 | height, 29 | }); 30 | } 31 | } 32 | 33 | export default ChainTipRepository; 34 | -------------------------------------------------------------------------------- /tests/restricted-token-trait_test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; 4 | 5 | Clarinet.test({ 6 | name: "Ensure that <...>", 7 | async fn(chain: Chain, accounts: Map) { 8 | let block = chain.mineBlock([ 9 | /* 10 | * Add transactions with: 11 | * Tx.contractCall(...) 12 | */ 13 | ]); 14 | assertEquals(block.receipts.length, 0); 15 | assertEquals(block.height, 2); 16 | 17 | block = chain.mineBlock([ 18 | /* 19 | * Add transactions with: 20 | * Tx.contractCall(...) 21 | */ 22 | ]); 23 | assertEquals(block.receipts.length, 0); 24 | assertEquals(block.height, 3); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /lib/wallet/rsk/EthereumUtils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, providers } from 'ethers'; 2 | import GasNow from './GasNow'; 3 | import { gweiDecimals } from '../../consts/Consts'; 4 | import { getBiggerBigNumber, getHexBuffer } from '../../Utils'; 5 | 6 | /** 7 | * Removes the 0x prefix of the Ethereum bytes 8 | */ 9 | export const parseBuffer = (input: string): Buffer => { 10 | return getHexBuffer(input.slice(2)); 11 | }; 12 | 13 | /** 14 | * Formats the gas provided price or queries an estimation from the web3 provider 15 | * 16 | * @param provider web3 provider 17 | * @param gasPrice denominated in GWEI 18 | */ 19 | export const getGasPrice = async (provider: providers.Provider, gasPrice?: number): Promise => { 20 | if (gasPrice !== undefined) { 21 | return BigNumber.from(gasPrice).mul(gweiDecimals); 22 | } 23 | 24 | return getBiggerBigNumber(await provider.getGasPrice(), GasNow.latestGasPrice); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/wallet/ethereum/EthereumUtils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, providers } from 'ethers'; 2 | import GasNow from './GasNow'; 3 | import { gweiDecimals } from '../../consts/Consts'; 4 | import { getBiggerBigNumber, getHexBuffer } from '../../Utils'; 5 | 6 | /** 7 | * Removes the 0x prefix of the Ethereum bytes 8 | */ 9 | export const parseBuffer = (input: string): Buffer => { 10 | return getHexBuffer(input.slice(2)); 11 | }; 12 | 13 | /** 14 | * Formats the gas provided price or queries an estimation from the web3 provider 15 | * 16 | * @param provider web3 provider 17 | * @param gasPrice denominated in GWEI 18 | */ 19 | export const getGasPrice = async (provider: providers.Provider, gasPrice?: number): Promise => { 20 | if (gasPrice !== undefined) { 21 | return BigNumber.from(gasPrice).mul(gweiDecimals); 22 | } 23 | 24 | return getBiggerBigNumber(await provider.getGasPrice(), GasNow.latestGasPrice); 25 | }; 26 | -------------------------------------------------------------------------------- /test/integration/Nodes.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Logger from '../../lib/Logger'; 3 | import LndClient from '../../lib/lightning/LndClient'; 4 | import ChainClient from '../../lib/chain/ChainClient'; 5 | 6 | const host = process.platform === 'win32' ? '192.168.99.100' : '127.0.0.1'; 7 | 8 | const bitcoinCookieDataPath = `${path.resolve(__dirname, '..', '..')}/docker/regtest/data/core/cookies/.bitcoin-cookie`; 9 | 10 | export const bitcoinClient = new ChainClient(Logger.disabledLogger, { 11 | host, 12 | port: 18443, 13 | cookie: bitcoinCookieDataPath, 14 | }, 'BTC'); 15 | 16 | export const lndDataPath = `${path.resolve(__dirname, '..', '..')}/docker/regtest/data/lnd`; 17 | 18 | export const bitcoinLndClient = new LndClient(Logger.disabledLogger, 'BTC', { 19 | host, 20 | port: 10009, 21 | certpath: `${lndDataPath}/certificates/tls.cert`, 22 | macaroonpath: `${lndDataPath}/macaroons/admin.macaroon`, 23 | }); 24 | -------------------------------------------------------------------------------- /lib/db/models/KeyProvider.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type KeyProviderType = { 4 | symbol: string; 5 | 6 | derivationPath: string; 7 | highestUsedIndex: number; 8 | }; 9 | 10 | class KeyProvider extends Model implements KeyProviderType { 11 | public symbol!: string; 12 | 13 | public derivationPath!: string; 14 | public highestUsedIndex!: number; 15 | 16 | public static load = (sequelize: Sequelize): void => { 17 | KeyProvider.init({ 18 | symbol: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 19 | derivationPath: { type: new DataTypes.STRING(255), allowNull: false }, 20 | highestUsedIndex: { type: new DataTypes.INTEGER(), allowNull: false }, 21 | }, { 22 | sequelize, 23 | tableName: 'keys', 24 | timestamps: false, 25 | }); 26 | } 27 | } 28 | 29 | export default KeyProvider; 30 | export { KeyProviderType }; 31 | -------------------------------------------------------------------------------- /contracts/sip-010-trait.clar: -------------------------------------------------------------------------------- 1 | (define-trait sip-010-trait 2 | ( 3 | ;; Transfer from the caller to a new principal 4 | (transfer (uint principal principal (optional (buff 34))) (response bool uint)) 5 | 6 | ;; the human readable name of the token 7 | (get-name () (response (string-ascii 32) uint)) 8 | 9 | ;; the ticker symbol, or empty if none 10 | (get-symbol () (response (string-ascii 32) uint)) 11 | 12 | ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token 13 | (get-decimals () (response uint uint)) 14 | 15 | ;; the balance of the passed principal 16 | (get-balance (principal) (response uint uint)) 17 | 18 | ;; the current total supply (which does not need to be a constant) 19 | (get-total-supply () (response uint uint)) 20 | 21 | ;; an optional URI that represents metadata of this token 22 | (get-token-uri () (response (optional (string-utf8 256)) uint)) 23 | ) 24 | ) -------------------------------------------------------------------------------- /docker/lnd/patches/btcutil.patch: -------------------------------------------------------------------------------- 1 | --- a/address.go 2 | +++ b/address.go 3 | @@ -137,12 +137,12 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) { 4 | // Bech32 encoded segwit addresses start with a human-readable part 5 | // (hrp) followed by '1'. For Bitcoin mainnet the hrp is "bc", and for 6 | // testnet it is "tb". If the address string has a prefix that matches 7 | - // one of the prefixes for the known networks, we try to decode it as 8 | - // a segwit address. 9 | + // the one defined in the defaultNet parameter, we try to decode it 10 | + // as a segwit address. 11 | oneIndex := strings.LastIndexByte(addr, '1') 12 | if oneIndex > 1 { 13 | prefix := addr[:oneIndex+1] 14 | - if chaincfg.IsBech32SegwitPrefix(prefix) { 15 | + if strings.ToLower(prefix) == defaultNet.Bech32HRPSegwit+"1" { 16 | witnessVer, witnessProg, err := decodeSegWitAddress(addr) 17 | if err != nil { 18 | return nil, err 19 | -------------------------------------------------------------------------------- /lib/cli/commands/UpdateTimeoutBlockDelta.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { loadBoltzClient, callback } from '../Command'; 3 | import { UpdateTimeoutBlockDeltaRequest } from '../../proto/boltzrpc_pb'; 4 | 5 | const command = 'updatetimeout '; 6 | 7 | const describe = 'updates the timeout block delta of a pair'; 8 | 9 | const builder = { 10 | pair: { 11 | describe: 'id of the pair', 12 | type: 'string', 13 | }, 14 | new_delta: { 15 | describe: 'new timeout block delta in minutes', 16 | type: 'number', 17 | }, 18 | }; 19 | 20 | const handler = (argv: Arguments): void => { 21 | const request = new UpdateTimeoutBlockDeltaRequest(); 22 | 23 | request.setPair(argv.pair); 24 | request.setNewDelta(argv.new_delta); 25 | 26 | loadBoltzClient(argv).updateTimeoutBlockDelta(request, callback); 27 | }; 28 | 29 | export { 30 | builder, 31 | command, 32 | handler, 33 | describe, 34 | }; 35 | -------------------------------------------------------------------------------- /tools/requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | astroid==2.5.1 3 | CacheControl==0.12.6 4 | certifi==2020.12.5 5 | chardet==4.0.0 6 | click==7.1.2 7 | colorama==0.4.4 8 | contextlib2==0.6.0 9 | distlib==0.3.1 10 | distro==1.5.0 11 | future==0.18.2 12 | html5lib==1.1 13 | idna==2.10 14 | ipaddr==2.2.0 15 | isort==5.7.0 16 | Jinja2==2.11.3 17 | joblib==1.0.1 18 | lazy-object-proxy==1.5.2 19 | livereload==2.6.3 20 | lockfile==0.12.2 21 | lunr==0.5.8 22 | Markdown==3.3.4 23 | MarkupSafe==1.1.1 24 | mccabe==0.6.1 25 | mkdocs==1.1.2 26 | msgpack==1.0.2 27 | nltk==3.5 28 | packaging==20.9 29 | pep517==0.10.0 30 | pkg-resources==0.0.0 31 | progress==1.5 32 | pylint==2.7.2 33 | pyotp==2.6.0 34 | pyparsing==2.4.7 35 | python-bitcoinrpc==1.0 36 | pytoml==0.1.21 37 | PyYAML==5.4.1 38 | regex==2021.3.17 39 | requests==2.25.1 40 | retrying==1.3.3 41 | six==1.15.0 42 | sseclient==0.0.27 43 | toml==0.10.2 44 | tornado==6.1 45 | tqdm==4.59.0 46 | urllib3==1.26.5 47 | webencodings==0.5.1 48 | wrapt==1.12.1 49 | -------------------------------------------------------------------------------- /docker/regtest/data/lnd/certificates/tls.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICiTCCAi+gAwIBAgIQZv1RZHQdFXjY8s9sQxl1KDAKBggqhkjOPQQDAjAuMR8w 3 | HQYDVQQKExZsbmQgYXV0b2dlbmVyYXRlZCBjZXJ0MQswCQYDVQQDEwJwYzAeFw0y 4 | MTA2MDExMDUyNDBaFw0yMjA3MjcxMDUyNDBaMC4xHzAdBgNVBAoTFmxuZCBhdXRv 5 | Z2VuZXJhdGVkIGNlcnQxCzAJBgNVBAMTAnBjMFkwEwYHKoZIzj0CAQYIKoZIzj0D 6 | AQcDQgAE/6uZ2iFvJAysD2Q2gdFOijBxq9ZlQKGInVz2j8qHXmmp54PBEbrUO5KM 7 | FgEnkbCzMh9YfSoCRTsFV2S50wVghqOCAS0wggEpMA4GA1UdDwEB/wQEAwICpDAT 8 | BgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT4 9 | h4qtd4UGhXWHCYJ+u5avs8FHVjCB0QYDVR0RBIHJMIHGggJwY4IJbG9jYWxob3N0 10 | ggR1bml4ggp1bml4cGFja2V0ggdidWZjb25uhwR/AAABhxAAAAAAAAAAAAAAAAAA 11 | AAABhwQKAAEGhwTAqHoBhwSsEgABhwSsEQABhxD9AAAAAAAAANYf3SOR8ARThxD9 12 | AAAAAAAAAKLxwPzoX67zhxD+gAAAAAAAAJu73GSvPP4/hxD+gAAAAAAAAABCt//+ 13 | tZlshxD+gAAAAAAAAABCkf/+rB6BhxD+gAAAAAAAALQW9//+2HEVMAoGCCqGSM49 14 | BAMCA0gAMEUCIDk/uOIa/AbyGAnvWcawYmT/WtiZ8tzvpBKtFk8jEovSAiEA8ER+ 15 | xNMkyCqNr7/noMAHuFByjo2GpYjfJzWQc7zkOnA= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /lib/db/KeyRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import KeyProvider, { KeyProviderType } from './models/KeyProvider'; 3 | 4 | class KeyRepository { 5 | public getKeyProviders = (): Promise => { 6 | return KeyProvider.findAll(); 7 | } 8 | 9 | public getKeyProvider = (symbol: string): Promise => { 10 | return KeyProvider.findOne({ 11 | where: { 12 | symbol: { 13 | [Op.eq]: symbol, 14 | }, 15 | }, 16 | }); 17 | } 18 | 19 | public addKeyProvider = (wallet: KeyProviderType): Promise => { 20 | return KeyProvider.create(wallet); 21 | } 22 | 23 | public setHighestUsedIndex = (symbol: string, highestUsedIndex: number): Promise<[number, KeyProviderType[]]> => { 24 | return KeyProvider.update({ 25 | highestUsedIndex, 26 | }, { 27 | where: { 28 | symbol: { 29 | [Op.eq]: symbol, 30 | }, 31 | }, 32 | }); 33 | } 34 | } 35 | 36 | export default KeyRepository; 37 | -------------------------------------------------------------------------------- /lib/db/models/PendingEthereumTransaction.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | type PendingEthereumTransactionType = { 4 | hash: string; 5 | }; 6 | 7 | class PendingEthereumTransaction extends Model implements PendingEthereumTransactionType { 8 | public hash!: string; 9 | 10 | public static load = (sequelize: Sequelize): void => { 11 | PendingEthereumTransaction.init({ 12 | hash: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 13 | nonce: { type: new DataTypes.INTEGER(), unique: true, allowNull: false }, 14 | }, { 15 | sequelize, 16 | timestamps: false, 17 | tableName: 'pendingEthereumTransactions', 18 | indexes: [ 19 | { 20 | unique: true, 21 | fields: ['hash'], 22 | }, 23 | { 24 | unique: true, 25 | fields: ['nonce'], 26 | }, 27 | ], 28 | }); 29 | } 30 | } 31 | 32 | export default PendingEthereumTransaction; 33 | export { PendingEthereumTransactionType }; 34 | -------------------------------------------------------------------------------- /lib/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { ClientStatus } from './consts/Enums'; 3 | 4 | class BaseClient extends EventEmitter { 5 | protected status = ClientStatus.Disconnected; 6 | 7 | protected readonly RECONNECT_INTERVAL = 5000; 8 | protected reconnectionTimer?: any; 9 | 10 | constructor() { 11 | super(); 12 | } 13 | 14 | public isConnected(): boolean { 15 | return this.status === ClientStatus.Connected; 16 | } 17 | 18 | public isDisconnected(): boolean { 19 | return this.status === ClientStatus.Disconnected; 20 | } 21 | 22 | public isOutOfSync(): boolean { 23 | return this.status === ClientStatus.OutOfSync; 24 | } 25 | 26 | protected setClientStatus = (status: ClientStatus): void => { 27 | this.status = status; 28 | } 29 | 30 | protected clearReconnectTimer = (): void => { 31 | if (this.reconnectionTimer) { 32 | clearInterval(this.reconnectionTimer); 33 | this.reconnectionTimer = undefined; 34 | } 35 | } 36 | } 37 | 38 | export default BaseClient; 39 | -------------------------------------------------------------------------------- /lib/cli/rsk/commands/Mine.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { connectEthereum } from '../EthereumUtils'; 3 | 4 | export const command = 'mine '; 5 | 6 | export const describe = 'mines the specified number of blocks on the Ganache chain'; 7 | 8 | export const builder = { 9 | blocks: { 10 | describe: 'number of blocks to mine', 11 | type: 'number', 12 | }, 13 | }; 14 | 15 | export const handler = async (argv: Arguments): Promise => { 16 | const signer = connectEthereum(argv.provider, argv.signer); 17 | const signerAddress = await signer.getAddress(); 18 | 19 | // Since Ganache mines a block whenever a transaction is sent, we are just going to send transactions 20 | // and wait for a confirmation until the specified number of blocks is mined 21 | for (let i = 0; i < argv.blocks; i += 1) { 22 | const transaction = await signer.sendTransaction({ 23 | to: signerAddress, 24 | }); 25 | await transaction.wait(1); 26 | } 27 | 28 | console.log(`Mined ${argv.blocks} blocks`); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/cli/stacks/commands/Mine.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { connectEthereum } from '../StacksUtils'; 3 | 4 | export const command = 'mine '; 5 | 6 | export const describe = 'mines the specified number of blocks on the Ganache chain'; 7 | 8 | export const builder = { 9 | blocks: { 10 | describe: 'number of blocks to mine', 11 | type: 'number', 12 | }, 13 | }; 14 | 15 | export const handler = async (argv: Arguments): Promise => { 16 | const signer = connectEthereum(argv.provider, argv.signer); 17 | const signerAddress = await signer.getAddress(); 18 | 19 | // Since Ganache mines a block whenever a transaction is sent, we are just going to send transactions 20 | // and wait for a confirmation until the specified number of blocks is mined 21 | for (let i = 0; i < argv.blocks; i += 1) { 22 | const transaction = await signer.sendTransaction({ 23 | to: signerAddress, 24 | }); 25 | await transaction.wait(1); 26 | } 27 | 28 | console.log(`Mined ${argv.blocks} blocks`); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/cli/ethereum/commands/Mine.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { connectEthereum } from '../EthereumUtils'; 3 | 4 | export const command = 'mine '; 5 | 6 | export const describe = 'mines the specified number of blocks on the Ganache chain'; 7 | 8 | export const builder = { 9 | blocks: { 10 | describe: 'number of blocks to mine', 11 | type: 'number', 12 | }, 13 | }; 14 | 15 | export const handler = async (argv: Arguments): Promise => { 16 | const signer = connectEthereum(argv.provider, argv.signer); 17 | const signerAddress = await signer.getAddress(); 18 | 19 | // Since Ganache mines a block whenever a transaction is sent, we are just going to send transactions 20 | // and wait for a confirmation until the specified number of blocks is mined 21 | for (let i = 0; i < argv.blocks; i += 1) { 22 | const transaction = await signer.sendTransaction({ 23 | to: signerAddress, 24 | }); 25 | await transaction.wait(1); 26 | } 27 | 28 | console.log(`Mined ${argv.blocks} blocks`); 29 | }; 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Boltz v3.1.0 Documentation 2 | 3 | This is the documentation of [Boltz](https://boltz.exchange) version 3.1.0. 4 | 5 | ## What is Boltz 6 | 7 | Boltz is a privacy first, account-free crypto exchange built on top of second layer scaling technologies like the [lightning network](http://lightning.network/). Boltz doesn't track any data that could potentially be traced back to the identity of our users. 8 | 9 | ## Instances 10 | 11 | We are running and maintaining two Boltz instances that can be used - one on [testnet](https://testnet.boltz.exchange) and one on [mainnet](https://boltz.exchange). 12 | 13 | The Rest API can be accessed at: 14 | 15 | * [Testnet](https://testnet.boltz.exchange/api) 16 | * [Mainnet](https://boltz.exchange/api) 17 | 18 | ## Useful Links 19 | 20 | * [Source Code](https://github.com/boltzexchange) 21 | * [Frequently Asked Questions](https://boltz.exchange/faq) 22 | * [Discord](https://discordapp.com/invite/QBvZGcW) [![Discord](https://img.shields.io/discord/547454030801272832.svg)](https://discordapp.com/invite/QBvZGcW) 23 | -------------------------------------------------------------------------------- /tools/ci backup.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest] 10 | node-version: [14] 11 | 12 | runs-on: ${{ matrix.platform }} 13 | 14 | steps: 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Start containers 27 | run: npm run docker:regtest && npm run docker:geth 28 | 29 | - name: Deploy Ethereum contracts 30 | run: sleep 15 && npm run docker:geth:deploy 31 | 32 | - name: Compile 33 | run: npm run compile 34 | 35 | - name: Lint 36 | run: npm run lint 37 | 38 | - name: Unit tests 39 | run: npm run test:unit 40 | 41 | - name: Integration tests 42 | run: npm run test:int 43 | -------------------------------------------------------------------------------- /tools/sse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module allows listening to Server-Send events via the CLI""" 3 | from argparse import ArgumentParser 4 | from sseclient import SSEClient 5 | 6 | def stream_server_side_events(url: str): 7 | """Listens to a stream of events and prints them to the console""" 8 | 9 | print("Listening to: {}".format(url)) 10 | 11 | try: 12 | client = SSEClient(url) 13 | 14 | for message in client: 15 | print(message) 16 | 17 | except Exception as exception: # pylint: disable=broad-except 18 | print("Could not listen to {url}: {exception}".format( 19 | url=url, 20 | exception=exception 21 | )) 22 | except KeyboardInterrupt: 23 | print("Cancelling stream") 24 | 25 | if __name__ == '__main__': 26 | PARSER = ArgumentParser(description="Stream Server-Sent events") 27 | 28 | # CLI arguments 29 | PARSER.add_argument("url", help="URL of the Server-Sent event stream", type=str, nargs=1) 30 | 31 | ARGS = PARSER.parse_args() 32 | 33 | stream_server_side_events(ARGS.url[0]) 34 | -------------------------------------------------------------------------------- /lib/wallet/providers/WalletProviderInterface.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from 'bitcoinjs-lib'; 2 | 3 | type WalletBalance = { 4 | totalBalance: number; 5 | confirmedBalance: number; 6 | unconfirmedBalance: number; 7 | }; 8 | 9 | type SentTransaction = { 10 | fee?: number; 11 | 12 | vout?: number; 13 | transactionId: string; 14 | 15 | transaction?: Transaction; 16 | }; 17 | 18 | interface WalletProviderInterface { 19 | readonly symbol: string; 20 | 21 | getBalance: () => Promise; 22 | 23 | getAddress: () => Promise; 24 | 25 | /** 26 | * Sends coins from the wallet 27 | * 28 | * @param address 29 | * @param amount 30 | * @param relativeFee 31 | */ 32 | sendToAddress: (address: string, amount: number, relativeFee?: number) => Promise; 33 | 34 | /** 35 | * Sweeps the wallet 36 | * 37 | * @param address 38 | * @param relativeFee 39 | */ 40 | sweepWallet: (address: string, relativeFee?: number) => Promise; 41 | } 42 | 43 | export default WalletProviderInterface; 44 | export { WalletBalance, SentTransaction }; 45 | -------------------------------------------------------------------------------- /docker/geth/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | ARG GOLANG_VERSION 3 | 4 | FROM golang:${GOLANG_VERSION} AS builder 5 | 6 | ARG VERSION 7 | 8 | RUN apt-get update && apt-get -y upgrade 9 | RUN apt-get -y install git make 10 | 11 | RUN git clone https://github.com/ethereum/go-ethereum.git $GOPATH/src/github.com/ethereum/go-ethereum 12 | 13 | WORKDIR $GOPATH/src/github.com/ethereum/go-ethereum 14 | 15 | RUN git checkout v${VERSION} 16 | 17 | RUN make geth 18 | 19 | # Start again with a new image to reduce the size 20 | FROM ubuntu:${UBUNTU_VERSION} 21 | 22 | RUN apt-get update && apt-get -y upgrade 23 | RUN apt-get -y install psmisc 24 | 25 | # Expose the RPC and websocket port 26 | EXPOSE 8545 8546 27 | 28 | COPY --from=builder /go/src/github.com/ethereum/go-ethereum/build/bin/geth /bin/ 29 | 30 | COPY geth/setup.sh /setup.sh 31 | 32 | # Start GETH in dev mode to save the genesis block and the first account 33 | RUN bash setup.sh 34 | 35 | ENTRYPOINT ["geth", "--datadir", "/gethData", "--dev", "--http", "--http.addr", "0.0.0.0", "--http.corsdomain", "*", "--ws", "--ws.addr", "0.0.0.0", "--miner.gasprice", "10000000000"] 36 | -------------------------------------------------------------------------------- /docker/stacks/start.sh: -------------------------------------------------------------------------------- 1 | mkdir -p persistent-data/postgres 2 | mkdir -p persistent-data/stacks-blockchain 3 | mkdir -p persistent-data/bns 4 | export POSTGRES_PASSWORD=postgres 5 | docker network create stacks-blockchain 6 | sleep 2 7 | docker run -d --rm \ 8 | --name postgres \ 9 | --net=stacks-blockchain \ 10 | -e POSTGRES_PASSWORD=postgres \ 11 | -v $(pwd)/persistent-data/postgres:/var/lib/postgresql/data \ 12 | -p 5432:5432 \ 13 | postgres:alpine 14 | sleep 5 15 | docker run -d --rm \ 16 | --name stacks-blockchain-api \ 17 | --net=stacks-blockchain \ 18 | --env-file $(pwd)/.env \ 19 | -v $(pwd)/bns:/bns-data \ 20 | -p 3700:3700 \ 21 | -p 3999:3999 \ 22 | blockstack/stacks-blockchain-api:2.1.1 23 | sleep 5 24 | docker run -d --rm \ 25 | --name stacks-blockchain \ 26 | --net=stacks-blockchain \ 27 | -v $(pwd)/persistent-data/stacks-blockchain:/root/stacks-node/data \ 28 | -v $(pwd)/config:/src/stacks-node \ 29 | -p 20443:20443 \ 30 | -p 20444:20444 \ 31 | blockstack/stacks-blockchain \ 32 | /bin/stacks-node start --config /src/stacks-node/Config.toml 33 | -------------------------------------------------------------------------------- /docker/berkeley-db/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | 3 | # Build BerkeleyDB 4 | FROM ubuntu:${UBUNTU_VERSION} as berkeley-db 5 | 6 | ARG VERSION 7 | 8 | RUN apt-get update && apt-get -y upgrade 9 | RUN apt-get -y install \ 10 | wget \ 11 | openssl \ 12 | autoconf \ 13 | automake \ 14 | build-essential 15 | 16 | ENV BERKELEYDB_VERSION=db-${VERSION}.NC 17 | ENV BERKELEYDB_PREFIX=/opt/${BERKELEYDB_VERSION} 18 | 19 | RUN wget https://download.oracle.com/berkeley-db/${BERKELEYDB_VERSION}.tar.gz 20 | RUN tar -xzf *.tar.gz 21 | 22 | RUN if [ "${VERSION}" = "4.8.30" ]; then \ 23 | sed s/__atomic_compare_exchange/__atomic_compare_exchange_db/g -i ${BERKELEYDB_VERSION}/dbinc/atomic.h ; else \ 24 | sed s/__atomic_compare_exchange/__atomic_compare_exchange_db/g -i ${BERKELEYDB_VERSION}/src/dbinc/atomic.h ; fi 25 | 26 | RUN mkdir -p ${BERKELEYDB_PREFIX} 27 | 28 | WORKDIR /${BERKELEYDB_VERSION}/build_unix 29 | 30 | RUN ../dist/configure --enable-cxx --disable-shared --with-pic --prefix=${BERKELEYDB_PREFIX} 31 | RUN make -j$(nproc) 32 | RUN make install 33 | 34 | # Assemble the final image 35 | FROM ubuntu:${UBUNTU_VERSION} 36 | 37 | COPY --from=berkeley-db /opt /opt 38 | -------------------------------------------------------------------------------- /docker/eclair/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | 3 | FROM ubuntu:${UBUNTU_VERSION} AS builder 4 | 5 | ARG VERSION 6 | 7 | RUN apt-get update && apt-get -y upgrade 8 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install git openjdk-11-jdk openjdk-11-jdk-headless maven 9 | 10 | RUN git clone https://github.com/ACINQ/eclair.git 11 | 12 | WORKDIR /eclair 13 | 14 | RUN git checkout v${VERSION} 15 | 16 | RUN mvn package -pl eclair-node -am -Dmaven.test.skip=true 17 | 18 | RUN ls /eclair/eclair-node/target 19 | 20 | # Start again with a new image to reduce the size 21 | FROM ubuntu:${UBUNTU_VERSION} 22 | 23 | # Copy the executables first to avoid caching of the apt repositories 24 | 25 | # Copy eclair-cli executable 26 | COPY --from=builder /eclair/eclair-core/eclair-cli /usr/local/bin/ 27 | 28 | # Copy the actual node 29 | COPY --from=builder /eclair/eclair-node/target/eclair-node-*.zip /eclair-node.zip 30 | 31 | RUN apt-get update && apt-get -y upgrade 32 | RUN apt-get install -y jq curl unzip openjdk-11-jdk openjdk-11-jdk-headless 33 | 34 | RUN unzip eclair-node.zip && rm eclair-node.zip && mv eclair-node-* eclair-node 35 | 36 | ENTRYPOINT /eclair-node/bin/eclair-node.sh 37 | -------------------------------------------------------------------------------- /docker/c-lightning/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | 3 | FROM ubuntu:${UBUNTU_VERSION} AS builder 4 | 5 | ARG VERSION 6 | 7 | RUN apt-get update && apt-get -y upgrade 8 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y \ 9 | git \ 10 | swig \ 11 | gettext \ 12 | libtool \ 13 | python3 \ 14 | autoconf \ 15 | automake \ 16 | net-tools \ 17 | libgmp-dev \ 18 | zlib1g-dev \ 19 | python3-mako \ 20 | libsodium-dev \ 21 | libsqlite3-dev \ 22 | build-essential \ 23 | python-is-python3 24 | 25 | RUN git clone https://github.com/ElementsProject/lightning.git 26 | WORKDIR /lightning 27 | 28 | RUN git checkout v${VERSION} 29 | 30 | RUN ./configure 31 | RUN make install 32 | 33 | RUN strip --strip-all /usr/local/bin/* 34 | 35 | # Start again with a new image to reduce the size 36 | FROM ubuntu:${UBUNTU_VERSION} 37 | 38 | RUN apt-get update && apt-get -y upgrade 39 | RUN apt-get -y install sqlite3 libsodium23 40 | 41 | # Copy the binaries and entrypoint from the builder image. 42 | COPY --from=builder /usr/local/bin/lightning-cli /bin/ 43 | COPY --from=builder /usr/local/bin/lightningd /bin/ 44 | COPY --from=builder /usr/local/libexec /usr/libexec 45 | 46 | ENTRYPOINT ["lightningd"] 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest] 10 | node-version: [14] 11 | 12 | runs-on: ${{ matrix.platform }} 13 | 14 | steps: 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | 23 | - name: Install Global Dependencies 24 | run: npm -g install @stacks/cli 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Start containers 30 | run: npm run docker:regtest && npm run stacks:mocknet 31 | 32 | - name: Deploy Stacks contracts 33 | run: sleep 240 && npm run stacks:fund:old && sleep 30 && npm run stacks:deploy 34 | 35 | - name: Compile 36 | run: npm run compile 37 | 38 | - name: Lint 39 | run: npm run lint 40 | 41 | - name: Unit tests 42 | run: npm run test:unit 43 | 44 | # - name: Integration tests - disabled for now 45 | # run: npm run test:int 46 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: [ main ] 9 | 10 | # build and push the image 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Get current date 16 | id: date 17 | run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')" 18 | - 19 | name: Checkout 20 | uses: actions/checkout@v2 21 | - 22 | name: Set up QEMU 23 | uses: docker/setup-qemu-action@v1 24 | - 25 | name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v1 27 | - 28 | name: Login to DockerHub 29 | uses: docker/login-action@v1 30 | with: 31 | username: ${{ secrets.DOCKER_USERNAME }} 32 | password: ${{ secrets.DOCKER_PASSWORD }} 33 | - 34 | name: Build and push 35 | uses: docker/build-push-action@v2 36 | with: 37 | context: . 38 | platforms: linux/amd64 39 | push: true 40 | tags: | 41 | pseudozach/lnstxbridge:latest 42 | pseudozach/lnstxbridge:${{ steps.date.outputs.date }} -------------------------------------------------------------------------------- /lib/db/DirectSwapRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op, WhereOptions } from 'sequelize'; 2 | import DirectSwap, { DirectSwapType } from './models/DirectSwap'; 3 | 4 | class DirectSwapRepository { 5 | public getDirectSwaps = (): Promise => { 6 | return DirectSwap.findAll({}); 7 | } 8 | 9 | public getSwap = (options: WhereOptions): Promise => { 10 | return DirectSwap.findOne({ 11 | where: options, 12 | }); 13 | } 14 | 15 | public addDirectSwap = (directSwap: DirectSwapType): Promise => { 16 | return DirectSwap.create(directSwap); 17 | } 18 | 19 | public setSwapStatus = (directSwap: DirectSwap, status: string, failureReason?: string, txId?: string): Promise => { 20 | return directSwap.update({ 21 | status, 22 | failureReason, 23 | txId, 24 | }); 25 | } 26 | 27 | public removeDirectSwap = (id: string): Promise => { 28 | return DirectSwap.destroy({ 29 | where: { 30 | id: { 31 | [Op.eq]: id, 32 | }, 33 | }, 34 | }); 35 | } 36 | 37 | public dropTable = async (): Promise => { 38 | return DirectSwap.drop(); 39 | } 40 | } 41 | 42 | export default DirectSwapRepository; 43 | -------------------------------------------------------------------------------- /parseGitCommit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const childProcess = require('child_process'); 3 | 4 | const executeCommand = (command) => { 5 | return childProcess.execSync(command, { encoding: 'utf8' }); 6 | }; 7 | 8 | const getCommitHash = () => { 9 | const result = executeCommand('git reflog --decorate -1'); 10 | 11 | // Do not show the commit hash if that commit is also a tag 12 | const tag = result.match(/tag\:\s[a-zA-Z0-9\-\.]+\,/g); 13 | if (tag && tag.length > 0) { 14 | return ''; 15 | } else { 16 | const match = result.match(/[a-z0-9]+\s\(HEAD/g); 17 | 18 | if (match) { 19 | return match[0].slice(0, -6); 20 | } else { 21 | return ''; 22 | } 23 | } 24 | }; 25 | 26 | const isDirty = () => { 27 | const result = executeCommand('git status --short'); 28 | return result.length > 0; 29 | }; 30 | 31 | const versionFilePath = `${__dirname}/lib/Version.ts`; 32 | 33 | try { 34 | // Delete the version file if it exists 35 | fs.unlinkSync(versionFilePath); 36 | } catch (error) {} 37 | 38 | const commitHash = getCommitHash(); 39 | 40 | fs.writeFileSync( 41 | versionFilePath, 42 | `export default '${commitHash === '' ? '' : '-' + commitHash}${isDirty() ? '-dirty' : ''}';\n`, 43 | ); 44 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Boltz tools 2 | 3 | This scripts were written and are used with `Python 3.7.3` or higher. Older versions *might* work too but I wouldn't rely on that. 4 | 5 | Unlike the `docker/build.py` script these ones have dependencies that have to be installed. To do that a virtual environment has to be created with: 6 | 7 | ```bash 8 | virtualenv .venv 9 | source .venv/bin/activate 10 | ``` 11 | 12 | To install the required dependencies: 13 | 14 | ```bash 15 | npm run python:install 16 | ``` 17 | 18 | ## Streaming Server-Sent Events 19 | 20 | Streaming Server-Sent Events is a little tedious to do in the browser and there are hardly any tools available for it. To make this a little more convenient there is the `sse.py` script that allows for stream Server-Sent Events via the CLI: 21 | 22 | ```bash 23 | ./sse.py 24 | ``` 25 | 26 | ## Generating OTP tokens 27 | 28 | To generate OTP tokens for the `withdraw` Discord command without having to setup an app the script `otp.py` can be used: 29 | 30 | ```bash 31 | ./otp.py 32 | ``` 33 | 34 | ## Calculating the miner fee of transactions 35 | 36 | To calculate the miner fee of a transaction `miner_fee.py` can be used: 37 | 38 | ```bash 39 | ./miner_fee.py 40 | ``` 41 | -------------------------------------------------------------------------------- /lib/db/models/ProviderSwap.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type ProviderSwapType = { 4 | id: string; 5 | providerUrl: string; 6 | status?: string; 7 | txId?: string; 8 | failureReason?: string; 9 | }; 10 | 11 | class ProviderSwap extends Model implements ProviderSwapType { 12 | public id!: string; 13 | public providerUrl!: string; 14 | public status?: string; 15 | public txId?: string; 16 | public failureReason?: string; 17 | 18 | public createdAt!: Date; 19 | public updatedAt!: Date; 20 | 21 | public static load = (sequelize: Sequelize): void => { 22 | ProviderSwap.init({ 23 | id: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 24 | status: { type: new DataTypes.STRING(255), }, 25 | providerUrl: { type: new DataTypes.STRING(255), }, 26 | txId: { type: new DataTypes.STRING(255), }, 27 | failureReason: { type: new DataTypes.STRING(255), }, 28 | }, { 29 | sequelize, 30 | tableName: 'providerSwaps', 31 | timestamps: true, 32 | indexes: [ 33 | { 34 | unique: true, 35 | fields: ['id'], 36 | }, 37 | ] 38 | }); 39 | } 40 | } 41 | 42 | export default ProviderSwap; 43 | export { ProviderSwapType }; 44 | -------------------------------------------------------------------------------- /lib/db/ProviderSwapRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op, WhereOptions } from 'sequelize'; 2 | import ProviderSwap, { ProviderSwapType } from './models/ProviderSwap'; 3 | 4 | class ProviderSwapRepository { 5 | public getProviderSwaps = (): Promise => { 6 | return ProviderSwap.findAll({}); 7 | } 8 | 9 | public getSwap = (options: WhereOptions): Promise => { 10 | return ProviderSwap.findOne({ 11 | where: options, 12 | }); 13 | } 14 | 15 | public addProviderSwap = (providerSwap: ProviderSwapType): Promise => { 16 | return ProviderSwap.create(providerSwap); 17 | } 18 | 19 | public setSwapStatus = (providerSwap: ProviderSwap, status: string, failureReason?: string, txId?: string): Promise => { 20 | return providerSwap.update({ 21 | status, 22 | failureReason, 23 | txId, 24 | }); 25 | } 26 | 27 | public removeProviderSwap = (id: string): Promise => { 28 | return ProviderSwap.destroy({ 29 | where: { 30 | id: { 31 | [Op.eq]: id, 32 | }, 33 | }, 34 | }); 35 | } 36 | 37 | public dropTable = async (): Promise => { 38 | return ProviderSwap.drop(); 39 | } 40 | } 41 | 42 | export default ProviderSwapRepository; 43 | -------------------------------------------------------------------------------- /lib/cli/Command.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import grpc from 'grpc'; 4 | import { Arguments } from 'yargs'; 5 | import { getServiceDataDir } from '../Utils'; 6 | import { BoltzClient } from '../proto/boltzrpc_grpc_pb'; 7 | 8 | export interface GrpcResponse { 9 | toObject: () => any; 10 | } 11 | 12 | export const loadBoltzClient = (argv: Arguments): BoltzClient => { 13 | const certPath = argv.tlscertpath ? argv.tlscertpath : path.join(getServiceDataDir('lnstx'), 'tls.cert'); 14 | const cert = fs.readFileSync(certPath); 15 | 16 | return new BoltzClient(`${argv.rpc.host}:${argv.rpc.port}`, grpc.credentials.createSsl(cert)); 17 | }; 18 | 19 | export const callback = (error: Error | null, response: GrpcResponse): void => { 20 | if (error) { 21 | printError(error); 22 | } else { 23 | const responseObj = response.toObject(); 24 | if (Object.keys(responseObj).length === 0) { 25 | console.log('success'); 26 | } else { 27 | printResponse(responseObj); 28 | } 29 | } 30 | }; 31 | 32 | export const printResponse = (response: unknown): void => { 33 | console.log(JSON.stringify(response, undefined, 2)); 34 | }; 35 | 36 | export const printError = (error: Error): void => { 37 | console.error(`${error.name}: ${error.message}`); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/chain/Errors.ts: -------------------------------------------------------------------------------- 1 | import { Error } from '../consts/Types'; 2 | import { concatErrorCode } from '../Utils'; 3 | import { ErrorCodePrefix } from '../consts/Enums'; 4 | 5 | export default { 6 | NO_RAWTX: (): Error => ({ 7 | message: 'pubrawtx ZMQ notifications are not enabled', 8 | code: concatErrorCode(ErrorCodePrefix.Chain, 1), 9 | }), 10 | NO_BLOCK_NOTIFICATIONS: (): Error => ({ 11 | message: 'no ZMQ block notifications are enabled', 12 | code: concatErrorCode(ErrorCodePrefix.Chain, 2), 13 | }), 14 | NO_BLOCK_FALLBACK: (): Error => ({ 15 | message: 'could not fall back to pubhashblock ZMQ filter because it is not enabled', 16 | code: concatErrorCode(ErrorCodePrefix.Chain, 3), 17 | }), 18 | INVALID_COOKIE_FILE: (path: string): Error => ({ 19 | message: `invalid cookie authentication file: ${path}`, 20 | code: concatErrorCode(ErrorCodePrefix.Chain, 4), 21 | }), 22 | NO_AUTHENTICATION: (): Error => ({ 23 | message: 'no or invalid authentication specified', 24 | code: concatErrorCode(ErrorCodePrefix.Chain, 5), 25 | }), 26 | ZMQ_CONNECTION_TIMEOUT: (symbol: string, filter: string, address: string): Error => ({ 27 | message: `connection attempt to ${symbol} ZMQ filter ${filter} (${address}) timed out`, 28 | code: concatErrorCode(ErrorCodePrefix.Chain, 6), 29 | }), 30 | }; 31 | -------------------------------------------------------------------------------- /lib/db/models/StacksTransaction.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model, Sequelize } from 'sequelize'; 2 | 3 | type StacksTransactionType = { 4 | txId: string; 5 | preimageHash: string; 6 | claimPrincipal?: string; 7 | swapContractAddress: string; 8 | event: string; 9 | }; 10 | 11 | class StacksTransaction extends Model implements StacksTransactionType { 12 | public txId!: string; 13 | public preimageHash!: string; 14 | public claimPrincipal?: string; 15 | public swapContractAddress!: string; 16 | public event!: string; 17 | 18 | public static load = (sequelize: Sequelize): void => { 19 | StacksTransaction.init({ 20 | txId: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 21 | preimageHash: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 22 | claimPrincipal: { type: new DataTypes.STRING(255), }, 23 | swapContractAddress: { type: new DataTypes.STRING(255), }, 24 | event: { type: new DataTypes.STRING(255), }, 25 | // nonce: { type: new DataTypes.INTEGER(), unique: true, allowNull: false }, 26 | }, { 27 | sequelize, 28 | timestamps: false, 29 | tableName: 'stacksTransactions', 30 | indexes: [ 31 | ], 32 | }); 33 | } 34 | } 35 | 36 | export default StacksTransaction; 37 | export { StacksTransactionType }; 38 | -------------------------------------------------------------------------------- /contracts/triggerswap_v2.clar: -------------------------------------------------------------------------------- 1 | ;; triggers claimstx from lnswap and claim-for from any nft for trustless LN purchases. 2 | 3 | ;; claim/mint an nft for a principal 4 | (define-trait claim-for-trait 5 | ( 6 | (claim-for (principal) (response uint uint)) 7 | ) 8 | ) 9 | 10 | ;; mainnet 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait 11 | (use-trait ft-trait .sip-010-trait.sip-010-trait) 12 | 13 | (define-public (triggerStx (preimage (buff 32)) (amount (buff 16)) (claimAddress (buff 42)) (refundAddress (buff 42)) (timelock (buff 16)) (nftPrincipal ) (userPrincipal principal)) 14 | (begin 15 | (try! (contract-call? .stxswap claimStx preimage amount claimAddress refundAddress timelock)) 16 | (try! (contract-call? nftPrincipal claim-for userPrincipal)) 17 | (ok true) 18 | ) 19 | ) 20 | 21 | (define-public (triggerSip10 (preimage (buff 32)) (amount (buff 16)) (claimAddress (buff 42)) (refundAddress (buff 42)) (timelock (buff 16)) (tokenPrincipal ) (nftPrincipal ) (userPrincipal principal)) 22 | (begin 23 | (try! (contract-call? .sip10swap claimToken preimage amount claimAddress refundAddress timelock tokenPrincipal)) 24 | (try! (contract-call? nftPrincipal claim-for userPrincipal)) 25 | (ok true) 26 | ) 27 | ) -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'node': true 4 | }, 5 | rules: { 6 | '@typescript-eslint/ban-ts-comment': 'off', 7 | 'jest/expect-expect': 'off', 8 | 'node/no-missing-import': 'off', 9 | 'node/no-extraneous-import': 'off', 10 | 'jest/no-conditional-expect': 'off', 11 | '@typescript-eslint/no-explicit-any': 'off', 12 | '@typescript-eslint/no-empty-function': 'off', 13 | 'node/no-unsupported-features/es-syntax': 'off', 14 | '@typescript-eslint/no-non-null-assertion': 'off', 15 | '@typescript-eslint/adjacent-overload-signatures': 'off', 16 | 17 | 'semi': 'error', 18 | 'no-trailing-spaces': 'error', 19 | 'quotes': ['error', 'single', { 'avoidEscape': true }], 20 | }, 21 | root: true, 22 | parser: '@typescript-eslint/parser', 23 | ignorePatterns: [ 24 | 'dist/', 25 | 'lib/proto/', 26 | 'contracts/', 27 | 'node_modules/', 28 | ], 29 | plugins: [ 30 | 'jest', 31 | 'eslint-plugin-import', 32 | 'eslint-plugin-node', 33 | '@typescript-eslint', 34 | ], 35 | extends: [ 36 | 'eslint:recommended', 37 | 'plugin:import/errors', 38 | 'plugin:import/warnings', 39 | 'plugin:jest/recommended', 40 | 'plugin:node/recommended', 41 | 'plugin:import/typescript', 42 | 'plugin:@typescript-eslint/recommended', 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /lib/cli/commands/SendCoins.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import BuilderComponents from '../BuilderComponents'; 3 | import { callback, loadBoltzClient } from '../Command'; 4 | import { SendCoinsRequest } from '../../proto/boltzrpc_pb'; 5 | 6 | export const command = 'sendcoins
[fee] [send_all]'; 7 | 8 | export const describe = 'sends coins to a specified address'; 9 | 10 | export const builder = { 11 | symbol: BuilderComponents.symbol, 12 | address: { 13 | describe: 'address to which the funds should be sent', 14 | type: 'string', 15 | }, 16 | amount: { 17 | describe: 'amount that should be sent', 18 | type: 'number', 19 | }, 20 | fee: { 21 | describe: 'sat/vbyte or gas price in gwei that should be paid as fee', 22 | type: 'number', 23 | default: 2, 24 | }, 25 | send_all: { 26 | describe: 'ignores the amount and sends the whole balance of the wallet', 27 | type: 'boolean', 28 | }, 29 | }; 30 | 31 | export const handler = (argv: Arguments): void => { 32 | const request = new SendCoinsRequest(); 33 | 34 | request.setSymbol(argv.symbol); 35 | request.setAddress(argv.address); 36 | request.setAmount(argv.amount); 37 | request.setFee(argv.fee); 38 | request.setSendAll(argv.send_all); 39 | 40 | loadBoltzClient(argv).sendCoins(request, callback); 41 | }; 42 | -------------------------------------------------------------------------------- /docker/zcash/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | 3 | # Build Zcash 4 | FROM ubuntu:${UBUNTU_VERSION} as zcash 5 | 6 | ARG VERSION 7 | 8 | RUN apt-get update && apt-get upgrade -y 9 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ 10 | m4 \ 11 | git \ 12 | curl \ 13 | wget \ 14 | unzip \ 15 | python3 \ 16 | libtool \ 17 | autoconf \ 18 | automake \ 19 | libtinfo5 \ 20 | libc6-dev \ 21 | pkg-config \ 22 | zlib1g-dev \ 23 | ncurses-dev \ 24 | python3-zmq \ 25 | bsdmainutils \ 26 | g++-multilib \ 27 | build-essential 28 | 29 | RUN wget https://github.com/zcash/zcash/archive/v${VERSION}.tar.gz 30 | RUN tar -xzf *.tar.gz 31 | 32 | WORKDIR /zcash-${VERSION} 33 | 34 | RUN ./zcutil/fetch-params.sh 35 | RUN ./zcutil/build.sh -j$(nproc) 36 | 37 | RUN mkdir bin 38 | 39 | RUN strip --strip-all src/zcashd 40 | RUN strip --strip-all src/zcash-tx 41 | RUN strip --strip-all src/zcash-cli 42 | 43 | RUN cp src/zcashd bin 44 | RUN cp src/zcash-tx bin 45 | RUN cp src/zcash-cli bin 46 | 47 | # Assemble the final image 48 | FROM ubuntu:${UBUNTU_VERSION} 49 | 50 | ARG VERSION 51 | 52 | RUN apt-get update && apt-get upgrade -y 53 | RUN apt-get -y install libgomp1 54 | 55 | COPY --from=zcash /zcash-${VERSION}/bin /bin 56 | COPY --from=zcash /root/.zcash-params/ /root/.zcash-params/ 57 | 58 | EXPOSE 8232 18232 59 | 60 | ENTRYPOINT ["zcashd"] 61 | -------------------------------------------------------------------------------- /test/unit/service/InvoiceExpiryHelper.spec.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyConfig } from '../../../lib/Config'; 2 | import InvoiceExpiryHelper from '../../../lib/service/InvoiceExpiryHelper'; 3 | 4 | describe('InvoiceExpiryHelper', () => { 5 | const currencies = [ 6 | { 7 | symbol: 'BTC', 8 | invoiceExpiry: 123, 9 | }, 10 | { 11 | symbol: 'LTC', 12 | invoiceExpiry: 210, 13 | }, 14 | ] as any as CurrencyConfig[]; 15 | 16 | const helper = new InvoiceExpiryHelper(currencies); 17 | 18 | test('should get expiry of invoices', () => { 19 | // Defined in the currency array 20 | expect(helper.getExpiry(currencies[0].symbol)).toEqual(currencies[0].invoiceExpiry); 21 | expect(helper.getExpiry(currencies[1].symbol)).toEqual(currencies[1].invoiceExpiry); 22 | 23 | // Default value 24 | expect(helper.getExpiry('DOGE')).toEqual(3600); 25 | }); 26 | 27 | test('should calculate expiry of invoices', () => { 28 | // Should use expiry date when defined 29 | expect(InvoiceExpiryHelper.getInvoiceExpiry(120, 360)).toEqual(360); 30 | 31 | // Should add default expiry to timestamp when expiry is not defined 32 | expect(InvoiceExpiryHelper.getInvoiceExpiry(120)).toEqual(3720); 33 | 34 | // should use 0 as timestamp when not defined 35 | expect(InvoiceExpiryHelper.getInvoiceExpiry()).toEqual(3600); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/wallet/rsk/Errors.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | import { Error } from '../../consts/Types'; 3 | import { concatErrorCode } from '../../Utils'; 4 | import { ErrorCodePrefix } from '../../consts/Enums'; 5 | 6 | export default { 7 | NO_PROVIDER_SPECIFIED: (): Error => ({ 8 | message: 'no Web3 provider was specified', 9 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 0), 10 | }), 11 | NO_LOCKUP_FOUND: (): Error => ({ 12 | message: 'no lockup transaction found', 13 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 1), 14 | }), 15 | INVALID_LOCKUP_TRANSACTION: (transactionHash: string): Error => ({ 16 | message: `lockup transaction is invalid: ${transactionHash}`, 17 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 2), 18 | }), 19 | UNEQUAL_PROVIDER_NETWORKS: (networks: providers.Network[]): Error => { 20 | const networkStrings: number[] = []; 21 | networks.forEach((network) => networkStrings.push(network.chainId)); 22 | 23 | return { 24 | message: `not all web3 provider networks are equal: ${networkStrings.join(', ')}`, 25 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 3), 26 | }; 27 | }, 28 | REQUESTS_TO_PROVIDERS_FAILED: (errors: string[]): Error => ({ 29 | message: `requests to all providers failed:\n - ${errors.join('\n - ')}`, 30 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 4), 31 | }), 32 | }; 33 | -------------------------------------------------------------------------------- /lib/wallet/stacks/Errors.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | import { Error } from '../../consts/Types'; 3 | import { concatErrorCode } from '../../Utils'; 4 | import { ErrorCodePrefix } from '../../consts/Enums'; 5 | 6 | export default { 7 | NO_PROVIDER_SPECIFIED: (): Error => ({ 8 | message: 'no Web3 provider was specified', 9 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 0), 10 | }), 11 | NO_LOCKUP_FOUND: (): Error => ({ 12 | message: 'no lockup transaction found', 13 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 1), 14 | }), 15 | INVALID_LOCKUP_TRANSACTION: (transactionHash: string): Error => ({ 16 | message: `lockup transaction is invalid: ${transactionHash}`, 17 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 2), 18 | }), 19 | UNEQUAL_PROVIDER_NETWORKS: (networks: providers.Network[]): Error => { 20 | const networkStrings: number[] = []; 21 | networks.forEach((network) => networkStrings.push(network.chainId)); 22 | 23 | return { 24 | message: `not all web3 provider networks are equal: ${networkStrings.join(', ')}`, 25 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 3), 26 | }; 27 | }, 28 | REQUESTS_TO_PROVIDERS_FAILED: (errors: string[]): Error => ({ 29 | message: `requests to all providers failed:\n - ${errors.join('\n - ')}`, 30 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 4), 31 | }), 32 | }; 33 | -------------------------------------------------------------------------------- /lib/wallet/ethereum/Errors.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | import { Error } from '../../consts/Types'; 3 | import { concatErrorCode } from '../../Utils'; 4 | import { ErrorCodePrefix } from '../../consts/Enums'; 5 | 6 | export default { 7 | NO_PROVIDER_SPECIFIED: (): Error => ({ 8 | message: 'no Web3 provider was specified', 9 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 0), 10 | }), 11 | NO_LOCKUP_FOUND: (): Error => ({ 12 | message: 'no lockup transaction found', 13 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 1), 14 | }), 15 | INVALID_LOCKUP_TRANSACTION: (transactionHash: string): Error => ({ 16 | message: `lockup transaction is invalid: ${transactionHash}`, 17 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 2), 18 | }), 19 | UNEQUAL_PROVIDER_NETWORKS: (networks: providers.Network[]): Error => { 20 | const networkStrings: number[] = []; 21 | networks.forEach((network) => networkStrings.push(network.chainId)); 22 | 23 | return { 24 | message: `not all web3 provider networks are equal: ${networkStrings.join(', ')}`, 25 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 3), 26 | }; 27 | }, 28 | REQUESTS_TO_PROVIDERS_FAILED: (errors: string[]): Error => ({ 29 | message: `requests to all providers failed:\n - ${errors.join('\n - ')}`, 30 | code: concatErrorCode(ErrorCodePrefix.Ethereum, 4), 31 | }), 32 | }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LN - STX Bridge with Provider Swap Network 2 | ## Submarine/Atomic Swaps between assets on Stacks <-> Bitcoin (onchain and Lightning Network) 3 | 4 | * This version adds independent provider support such that any entity can register to serve swaps. This app will act as an aggregator to route swap requests to registered swap providers. 5 | 6 | * This app enables functionality as described in https://github.com/stacksgov/Stacks-Grants/issues/172 and https://github.com/stacksgov/Stacks-Grants/issues/204 7 | * Running on https://lnswap.org 8 | 9 | ## install 10 | * clone the repo, install requirements and compile 11 | `git clone https://github.com/pseudozach/lnstxbridge.git` 12 | `cd lnstxbridge && npm i && npm run compile` 13 | * start btc & lnd 14 | `npm run docker:regtest` 15 | * start stacks 16 | `npm run stacks:mocknet` 17 | * fund a regtest account and deploy latest Clarity contract under contracts/ 18 | `npm run stacks:fund && npm run stacks:deploy` 19 | * copy boltz.conf to ~/.lnstx/boltz.conf and modify as needed 20 | * start the app 21 | `npm run start` 22 | 23 | * clone [lnstxbridge-client](https://github.com/pseudozach/lnstxbridge-client.git) repo and start it with `aggregatorUrl` parameter set to this bridge IP:port. 24 | 25 | ## use 26 | * API is available at http://localhost:9007, e.g. `curl http://localhost:9007/getpairs` 27 | refer to [API docs](https://docs.boltz.exchange/en/latest/api/) -------------------------------------------------------------------------------- /lib/service/InvoiceExpiryHelper.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyConfig } from '../Config'; 2 | 3 | class InvoiceExpiryHelper { 4 | private static readonly defaultInvoiceExpiry = 3600; 5 | 6 | private readonly invoiceExpiry = new Map(); 7 | 8 | constructor(currencies: CurrencyConfig[]) { 9 | for (const currency of currencies) { 10 | console.log(`currency loop inside invoiceexpiryhelper ${currency.symbol}`); 11 | if (currency.invoiceExpiry) { 12 | this.invoiceExpiry.set(currency.symbol, currency.invoiceExpiry); 13 | } 14 | } 15 | } 16 | 17 | public getExpiry = (symbol: string): number => { 18 | return this.invoiceExpiry.get(symbol) || InvoiceExpiryHelper.defaultInvoiceExpiry; 19 | } 20 | 21 | /** 22 | * Calculates the expiry of an invoice 23 | * Reference: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields 24 | * 25 | * @param timestamp creation timestamp of the invoice 26 | * @param timeExpireDate expiry timestamp of the invoice 27 | */ 28 | public static getInvoiceExpiry = (timestamp?: number, timeExpireDate?: number): number => { 29 | let invoiceExpiry = timestamp || 0; 30 | 31 | if (timeExpireDate) { 32 | invoiceExpiry = timeExpireDate; 33 | } else { 34 | invoiceExpiry += 3600; 35 | } 36 | 37 | return invoiceExpiry; 38 | } 39 | } 40 | 41 | export default InvoiceExpiryHelper; 42 | -------------------------------------------------------------------------------- /docker/lnd/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | ARG GOLANG_VERSION 3 | 4 | FROM golang:${GOLANG_VERSION} AS builder 5 | 6 | ARG VERSION 7 | 8 | RUN apt-get update && apt-get -y upgrade 9 | RUN apt-get -y install git make gcc libc-dev patch 10 | 11 | RUN git clone https://github.com/lightningnetwork/lnd.git $GOPATH/src/github.com/lightningnetwork/lnd 12 | 13 | WORKDIR $GOPATH/src/github.com/lightningnetwork/lnd 14 | 15 | RUN git checkout v${VERSION} 16 | 17 | # Apply patches for Litecoin LND 18 | RUN go mod vendor 19 | 20 | COPY lnd/patches /patches 21 | 22 | RUN cd vendor/github.com/ltcsuite/ltcd && patch -p1 < /patches/ltcd.patch 23 | RUN cd vendor/github.com/btcsuite/btcutil && patch -p1 < /patches/btcutil.patch 24 | 25 | RUN go build -mod vendor -v -trimpath -tags="autopilotrpc signrpc walletrpc chainrpc invoicesrpc routerrpc watchtowerrpc" github.com/lightningnetwork/lnd/cmd/lnd 26 | RUN go build -mod vendor -v -trimpath -tags="autopilotrpc invoicesrpc walletrpc routerrpc watchtowerrpc" github.com/lightningnetwork/lnd/cmd/lncli 27 | 28 | # Start again with a new image to reduce the size 29 | FROM ubuntu:${UBUNTU_VERSION} 30 | 31 | # Expose LND ports (server, gRPC) 32 | EXPOSE 9735 10009 33 | 34 | # Copy the binaries and entrypoint from the builder image. 35 | COPY --from=builder /go/src/github.com/lightningnetwork/lnd/lnd /bin/ 36 | COPY --from=builder /go/src/github.com/lightningnetwork/lnd/lncli /bin/ 37 | 38 | ENTRYPOINT ["lnd"] 39 | -------------------------------------------------------------------------------- /test/unit/wallet/ethereum/EthereumUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, providers } from 'ethers'; 2 | import { getHexBuffer } from '../../../../lib/Utils'; 3 | import GasNow from '../../../../lib/wallet/ethereum/GasNow'; 4 | import { gweiDecimals } from '../../../../lib/consts/Consts'; 5 | import { getGasPrice, parseBuffer } from '../../../../lib/wallet/ethereum/EthereumUtils'; 6 | 7 | const mockGetGasPriceResult = BigNumber.from(1); 8 | const mockGetGasPrice = jest.fn().mockResolvedValue(mockGetGasPriceResult); 9 | 10 | const MockedProvider = >jest.fn().mockImplementation(() => ({ 11 | getGasPrice: mockGetGasPrice, 12 | })); 13 | 14 | GasNow.latestGasPrice = BigNumber.from(2); 15 | 16 | describe('EthereumUtils', () => { 17 | const provider = new MockedProvider(); 18 | 19 | test('should parse buffers', () => { 20 | const data = '40fee37b911579bdd107e57add77c9351ace6692cd01dee36fd7879c6a7cf9fe'; 21 | 22 | expect(parseBuffer(`0x${data}`)).toEqual(getHexBuffer(data)); 23 | }); 24 | 25 | test('should get gas price', async () => { 26 | const providedGasPrice = 20; 27 | 28 | expect(await getGasPrice(provider, providedGasPrice)).toEqual(BigNumber.from(providedGasPrice).mul(gweiDecimals)); 29 | expect(await getGasPrice(provider)).toEqual(GasNow.latestGasPrice); 30 | 31 | GasNow.latestGasPrice = BigNumber.from(0); 32 | 33 | expect(await getGasPrice(provider)).toEqual(mockGetGasPriceResult); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/rates/RateCalculator.ts: -------------------------------------------------------------------------------- 1 | import Errors from './Errors'; 2 | import { getPairId } from '../Utils'; 3 | import DataAggregator from './data/DataAggregator'; 4 | 5 | class RateCalculator { 6 | constructor(private aggregator: DataAggregator) {} 7 | 8 | public calculateRate = (base: string, quote: string): number => { 9 | const pair = getPairId({ 10 | base, 11 | quote, 12 | }); 13 | 14 | let rate = this.aggregator.latestRates.get(pair); 15 | 16 | if (rate === undefined) { 17 | // Try the reverse rate 18 | rate = this.aggregator.latestRates.get(getPairId({ 19 | base: quote, 20 | quote: base, 21 | })); 22 | 23 | if (rate !== undefined) { 24 | rate = 1 / rate; 25 | } else { 26 | // TODO: also try routing via USD(T) 27 | // Get the route via BTC 28 | const baseBtc = this.aggregator.latestRates.get(getPairId({ 29 | base, 30 | quote: 'BTC', 31 | })); 32 | 33 | if (baseBtc === undefined) { 34 | throw Errors.COULD_NOT_FIND_RATE(pair); 35 | } 36 | 37 | const quoteBtc = this.aggregator.latestRates.get(getPairId({ 38 | base: quote, 39 | quote: 'BTC', 40 | })); 41 | 42 | if (quoteBtc === undefined) { 43 | throw Errors.COULD_NOT_FIND_RATE(pair); 44 | } 45 | 46 | return baseBtc / quoteBtc; 47 | } 48 | } 49 | 50 | return rate; 51 | } 52 | } 53 | 54 | export default RateCalculator; 55 | -------------------------------------------------------------------------------- /lib/VersionCheck.ts: -------------------------------------------------------------------------------- 1 | type Version = string | number; 2 | 3 | class VersionCheck { 4 | private static chainClientVersionLimits = { 5 | minimal: 180100, 6 | maximal: 250100, 7 | }; 8 | 9 | private static lndVersionLimits = { 10 | minimal: '0.12.0', 11 | maximal: '0.16.3', 12 | }; 13 | 14 | public static checkChainClientVersion = (symbol: string, version: number): void => { 15 | const { maximal, minimal } = VersionCheck.chainClientVersionLimits; 16 | 17 | if (version > maximal || version < minimal) { 18 | throw VersionCheck.unsupportedVersionError(`${symbol} Core`, version, maximal, minimal); 19 | } 20 | } 21 | 22 | public static checkLndVersion = (symbol: string, version: string): void => { 23 | const parseStringVersion = (version: string) => { 24 | return Number(version.split('-')[0].replace('.', '')); 25 | }; 26 | 27 | const { maximal, minimal } = VersionCheck.lndVersionLimits; 28 | const versionNumber = parseStringVersion(version); 29 | 30 | if (versionNumber > parseStringVersion(maximal) || versionNumber < parseStringVersion(minimal)) { 31 | throw VersionCheck.unsupportedVersionError(`${symbol} LND`, version, maximal, minimal); 32 | } 33 | } 34 | 35 | private static unsupportedVersionError = (service: string, actual: Version, maximal: Version, minimal: Version) => { 36 | return `unsupported ${service} version: ${actual}; max version ${maximal}; min version ${minimal}`; 37 | } 38 | } 39 | 40 | export default VersionCheck; 41 | -------------------------------------------------------------------------------- /test/integration/rates/data/DataAggregator.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPairId } from '../../../../lib/Utils'; 2 | import { baseAsset, quoteAsset, checkPrice } from './Consts'; 3 | import DataAggregator from '../../../../lib/rates/data/DataAggregator'; 4 | 5 | describe('DataProvider', () => { 6 | const dataAggregator = new DataAggregator(); 7 | 8 | test('should register pairs', () => { 9 | dataAggregator.registerPair(baseAsset, quoteAsset); 10 | 11 | expect(dataAggregator.pairs.size).toEqual(1); 12 | expect(Array.from(dataAggregator.pairs.values())[0]).toEqual([baseAsset, quoteAsset]); 13 | }); 14 | 15 | test('should calculate the median price', async () => { 16 | const result = await dataAggregator.fetchPairs(); 17 | 18 | expect(result).toEqual(dataAggregator.latestRates); 19 | 20 | checkPrice(result.get(getPairId({ base: baseAsset, quote: quoteAsset }))!); 21 | }); 22 | 23 | test('should fallback to latest rate when current cannot be fetched', async () => { 24 | const mockBaseAsset = 'SOME'; 25 | const mockQuoteAsset = 'QueryFailure'; 26 | 27 | const mockPair = getPairId({ base: mockBaseAsset, quote: mockQuoteAsset }); 28 | 29 | dataAggregator.registerPair(mockBaseAsset, mockQuoteAsset); 30 | 31 | const latestMockPrice = 123; 32 | dataAggregator.latestRates.set(mockPair, latestMockPrice); 33 | 34 | const result = await dataAggregator.fetchPairs(); 35 | 36 | expect(result.size).toEqual(2); 37 | expect(result.get(mockPair)).toEqual(latestMockPrice); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/notifications/DiskUsageChecker.ts: -------------------------------------------------------------------------------- 1 | import { platform } from 'os'; 2 | import { check } from 'diskusage'; 3 | import Logger from '../Logger'; 4 | import { Emojis } from './Markup'; 5 | import DiscordClient from './DiscordClient'; 6 | 7 | class DiskUsageChecker { 8 | private alertSent = false; 9 | 10 | private static warningThreshold = 0.9; 11 | private static rootDir = platform() !== 'win32' ? '/' : 'C:'; 12 | 13 | constructor(private logger: Logger, private discord: DiscordClient) {} 14 | 15 | public checkUsage = async (): Promise => { 16 | const { available, total } = await check(DiskUsageChecker.rootDir); 17 | 18 | const used = total - available; 19 | const usedPercentage = used / total; 20 | 21 | if (usedPercentage >= DiskUsageChecker.warningThreshold) { 22 | if (!this.alertSent) { 23 | const message = `${Emojis.RotatingLight} Disk usage is **${this.formatNumber(usedPercentage * 100)}%**: ` + 24 | `**${this.formatNumber(this.convertToGb(available))} GB** of **${this.formatNumber(this.convertToGb(total))} GB** available ${Emojis.RotatingLight}`; 25 | 26 | this.logger.warn(message); 27 | await this.discord.sendMessage(message); 28 | 29 | this.alertSent = true; 30 | } 31 | } else { 32 | this.alertSent = false; 33 | } 34 | } 35 | 36 | private formatNumber = (toFormat: number) => { 37 | return Number(toFormat.toFixed(2)); 38 | } 39 | 40 | private convertToGb = (bytes: number) => { 41 | return bytes / 1073741824; 42 | } 43 | } 44 | 45 | export default DiskUsageChecker; 46 | -------------------------------------------------------------------------------- /test/unit/rates/RateCalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import RateCalculator from '../../../lib/rates/RateCalculator'; 2 | import DataAggregator from '../../../lib/rates/data/DataAggregator'; 3 | import Errors from '../../../lib/rates/Errors'; 4 | 5 | const ethBtcRate = 0.04269969; 6 | const ltcBtcRate = 0.00432060; 7 | 8 | const aggregator = { 9 | latestRates: new Map([ 10 | [ 11 | 'ETH/BTC', 12 | ethBtcRate, 13 | ], 14 | [ 15 | 'LTC/BTC', 16 | ltcBtcRate, 17 | ], 18 | ]), 19 | } as any as DataAggregator; 20 | 21 | describe('RateCalculator', () => { 22 | const calculator = new RateCalculator(aggregator); 23 | 24 | test('should get rate if pair exists', () => { 25 | expect(calculator.calculateRate('ETH', 'BTC')).toEqual(ethBtcRate); 26 | }); 27 | 28 | test('should get rate if the inverse pair exists', () => { 29 | expect(calculator.calculateRate('BTC', 'ETH')).toEqual(1 / ethBtcRate); 30 | }); 31 | 32 | test('should route rate calculations via the BTC pairs', () => { 33 | expect(calculator.calculateRate('ETH', 'LTC')).toEqual(ethBtcRate / ltcBtcRate); 34 | }); 35 | 36 | test('should throw when rate cannot be calculated', () => { 37 | const notFoundSymbol = 'NOT'; 38 | 39 | expect(() => calculator.calculateRate(notFoundSymbol, 'ETH')).toThrow( 40 | Errors.COULD_NOT_FIND_RATE(`${notFoundSymbol}/ETH`).message, 41 | ); 42 | 43 | expect(() => calculator.calculateRate('ETH', notFoundSymbol)).toThrow( 44 | Errors.COULD_NOT_FIND_RATE(`ETH/${notFoundSymbol}`).message, 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/integration/rates/data/Exchanges.spec.ts: -------------------------------------------------------------------------------- 1 | import { baseAsset, checkPrice, quoteAsset } from './Consts'; 2 | import Kraken from '../../../../lib/rates/data/exchanges/Kraken'; 3 | import Binance from '../../../../lib/rates/data/exchanges/Binance'; 4 | import Bitfinex from '../../../../lib/rates/data/exchanges/Bitfinex'; 5 | import Poloniex from '../../../../lib/rates/data/exchanges/Poloniex'; 6 | import CoinbasePro from '../../../../lib/rates/data/exchanges/CoinbasePro'; 7 | 8 | describe('Exchanges', () => { 9 | test('should get price from Binance', async () => { 10 | const binance = new Binance(); 11 | const price = await binance.getPrice(baseAsset, quoteAsset); 12 | 13 | checkPrice(price); 14 | }); 15 | 16 | test('should get price from Bitfinex', async () => { 17 | const bitfinex = new Bitfinex(); 18 | const price = await bitfinex.getPrice(baseAsset, quoteAsset); 19 | 20 | checkPrice(price); 21 | }); 22 | 23 | test('should get price from Coinbase Pro', async () => { 24 | const coinbase = new CoinbasePro(); 25 | const price = await coinbase.getPrice(baseAsset, quoteAsset); 26 | 27 | checkPrice(price); 28 | }); 29 | 30 | test('should get price from Kraken', async () => { 31 | const kraken = new Kraken(); 32 | const price = await kraken.getPrice(baseAsset, quoteAsset); 33 | 34 | checkPrice(price); 35 | }); 36 | 37 | test('should get price from Poloniex', async () => { 38 | const poloniex = new Poloniex(); 39 | const price = await poloniex.getPrice(baseAsset, quoteAsset); 40 | 41 | checkPrice(price); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /contracts/triggerswap_v5.clar: -------------------------------------------------------------------------------- 1 | ;; LNSwap external atomic swap triggers 2 | ;; triggers claim from lnswap contracts and mint/transfer to any contract/principal for trustless LN -> STX interaction. 3 | 4 | (define-trait claim-trait 5 | ( 6 | (claim () (response uint uint)) 7 | ) 8 | ) 9 | 10 | (define-trait claim-usda-trait 11 | ( 12 | (claim-usda () (response uint uint)) 13 | ) 14 | ) 15 | 16 | ;; TODO: update .stxswap -> .stxswap_v10/sip10swap_v3 17 | ;; mainnet 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait 18 | (use-trait ft-trait .sip-010-trait.sip-010-trait) 19 | 20 | (define-public (triggerStx (preimage (buff 32)) (amount uint) (nftPrincipal )) 21 | (begin 22 | (try! (contract-call? .stxswap claimStx preimage amount)) 23 | (try! (contract-call? nftPrincipal claim)) 24 | (ok true) 25 | ) 26 | ) 27 | 28 | (define-public (triggerTransferStx (preimage (buff 32)) (amount uint) (receiver principal) (memo (string-ascii 40))) 29 | (begin 30 | (try! (contract-call? .stxswap claimStx preimage amount)) 31 | (try! (stx-transfer? amount tx-sender receiver)) 32 | (print {action: "transfer", address: tx-sender, memo: memo}) 33 | (ok true) 34 | ) 35 | ) 36 | 37 | (define-public (triggerSip10 (preimage (buff 32)) (amount uint) (tokenPrincipal ) (nftPrincipal )) 38 | (begin 39 | (try! (contract-call? .sip10swap claimToken preimage amount tokenPrincipal)) 40 | (try! (contract-call? nftPrincipal claim-usda)) 41 | (ok true) 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /Clarinet.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "lnstxbridge" 3 | authors = [] 4 | description = "" 5 | telemetry = false 6 | requirements = [] 7 | boot_contracts = ["pox", "costs-v2"] 8 | 9 | [contracts.Wrapped-USD] 10 | path = "contracts/Wrapped-USD.clar" 11 | depends_on = ["restricted-token-trait", "sip-010-trait"] 12 | 13 | [contracts.claim-for-trait] 14 | path = "contracts/claim-for-trait.clar" 15 | depends_on = [] 16 | 17 | [contracts.nft] 18 | path = "contracts/nft.clar" 19 | depends_on = ["nft-trait", "usda-token"] 20 | 21 | [contracts.nft-trait] 22 | path = "contracts/nft-trait.clar" 23 | depends_on = [] 24 | 25 | [contracts.restricted-token-trait] 26 | path = "contracts/restricted-token-trait.clar" 27 | depends_on = [] 28 | 29 | [contracts.sip-010-trait] 30 | path = "contracts/sip-010-trait.clar" 31 | depends_on = [] 32 | 33 | [contracts.sip10swap] 34 | path = "contracts/sip10swap_v3.clar" 35 | depends_on = ["usda-token"] 36 | 37 | [contracts.stxswap] 38 | path = "contracts/stxswap_v10.clar" 39 | depends_on = [] 40 | 41 | [contracts.triggerswap] 42 | path = "contracts/triggerswap-v7.clar" 43 | depends_on = ["sip-010-trait", "usda-token", "nft"] 44 | 45 | [contracts.trustless-rewards] 46 | path = "contracts/trustless-rewards.clar" 47 | depends_on = [] 48 | 49 | [contracts.usda-token] 50 | path = "contracts/usda-token.clar" 51 | depends_on = ["sip-010-trait"] 52 | 53 | [repl] 54 | costs_version = 2 55 | parser_version = 2 56 | 57 | [repl.analysis] 58 | passes = ["check_checker"] 59 | 60 | [repl.analysis.check_checker] 61 | strict = false 62 | trusted_sender = false 63 | trusted_caller = false 64 | callee_filter = false 65 | -------------------------------------------------------------------------------- /bin/boltzd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Boltz = require('../dist/lib/Boltz').default; 4 | 5 | const { argv } = require('yargs') 6 | .options({ 7 | datadir: { 8 | describe: 'Data directory of Boltz', 9 | alias: 'w', 10 | type: 'string', 11 | }, 12 | configpath: { 13 | type: 'string', 14 | describe: 'Path to the config file', 15 | }, 16 | logpath: { 17 | describe: 'Path to the logfile', 18 | type: 'string', 19 | }, 20 | loglevel: { 21 | describe: 'Verbosity of the logger', 22 | alias: 'l', 23 | type: 'string', 24 | choices: ['error', 'warn', 'info', 'verbose', 'debug', 'silly'], 25 | }, 26 | walletpath: { 27 | type: 'string', 28 | describe: 'Path to the wallet file', 29 | }, 30 | 'grpc.host': { 31 | describe: 'Host of the Boltz gRPC interface', 32 | type: 'string', 33 | }, 34 | 'grpc.port': { 35 | describe: 'Port of the Boltz gRPC interface', 36 | type: 'number', 37 | }, 38 | 'grpc.certpath': { 39 | describe: 'Path to the TLS certificate of the Boltz gRPC interface', 40 | type: 'string', 41 | }, 42 | 'grpc.keypath': { 43 | describe: 'Path to the key of the TLS certificate of the Boltz gRPC interface', 44 | type: 'string', 45 | }, 46 | currencies: { 47 | describe: 'Currencies that Boltz should support', 48 | type: 'array', 49 | }, 50 | }); 51 | 52 | // Delete non-config keys from argv 53 | delete argv._; 54 | delete argv.version; 55 | delete argv.help; 56 | delete argv.$0; 57 | 58 | const boltz = new Boltz(argv); 59 | boltz.start(); 60 | -------------------------------------------------------------------------------- /docs/swap-files.md: -------------------------------------------------------------------------------- 1 | # Swap files 2 | 3 | All applications that save files for Swaps to the disk should format them in a standardized way. This is especially important for Normal Submarine Swaps so that they can be refunded not only in the application but also in our Boltz frontend. 4 | 5 | ## Refund Files 6 | 7 | The refund files our frontend generates are PNG QR codes because iOS browsers don't allow any other files than images to be downloaded to the device. This might not be applicable to you and therefore our frontend also parses files with any other extension and treats them as raw JSON. 8 | 9 | The data that should be in the file or encoded in the QR code is a JSON object with the following values: 10 | 11 | - `id`: the ID of the swap 12 | - `currency`: symbol of the chain on which coins were locked up 13 | - `redeemScript`: the redeem script of the lockup address 14 | - `privateKey`: the private key of the refund key pair 15 | - `timeoutBlockHeight`: block height at which the Swaps times out 16 | 17 | The values of `id`, `redeemScript` and `timeoutBlockHeight` are returned by the Boltz API when the Swap gets created. `currency` and `privateKey` are obviously known by the application anyways. 18 | 19 | Example: 20 | 21 | ```json 22 | { 23 | "id": "qZf1Zb", 24 | "currency": "BTC", 25 | "redeemScript": "a9146a24b142de20b50871a247c1c66a6e41ee199017876321038ce1d1be5a22b396ccafc109c86717bc081301fe58d1958546d5aba647047af3670381a81ab1752102d23a7d39395f40a71a490cf79e0f2df5da2fb006fdab660bc0c78ef0c9ba457668ac", 26 | "privateKey": "1736eb52267524619289a5c9b58dab9339b2bb389764ad5c5be8955d9aadeeab", 27 | "timeoutBlockHeight": 1747073 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | history.txt 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Typescript compilation output 45 | dist/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # Webstorm config 60 | .idea/ 61 | 62 | # typedoc 63 | typedoc/ 64 | 65 | # Built API documentation 66 | site/ 67 | 68 | # Virtual Python environment 69 | .venv 70 | 71 | # Visual Studio Code configuration 72 | .vscode/ 73 | 74 | # Version file generated in build process 75 | lib/Version.ts 76 | 77 | # # Folder from which regtest contracts are deployed 78 | # contracts/ 79 | 80 | # Bitcoin and Litecoin Core RPC cookies 81 | docker/regtest/data/core/cookies/ 82 | 83 | # mocknet files 84 | docker/stacks/persistent-data 85 | docker/stacks/bns 86 | 87 | _clarinet -------------------------------------------------------------------------------- /lib/cli/commands/Claim.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { address, ECPair, Transaction } from 'bitcoinjs-lib'; 3 | import { Networks, constructClaimTransaction, detectSwap } from 'boltz-core'; 4 | import BuilderComponents from '../BuilderComponents'; 5 | import { getHexBuffer, stringify } from '../../Utils'; 6 | 7 | export const command = 'claim '; 8 | 9 | export const describe = 'claims reverse submarine or chain to chain swaps'; 10 | 11 | export const builder = { 12 | network: BuilderComponents.network, 13 | privateKey: BuilderComponents.privateKey, 14 | redeemScript: BuilderComponents.redeemScript, 15 | rawTransaction: BuilderComponents.rawTransaction, 16 | destinationAddress: BuilderComponents.destinationAddress, 17 | preimage: { 18 | describe: 'preimage of the swap', 19 | type: 'string', 20 | }, 21 | }; 22 | 23 | export const handler = (argv: Arguments): void => { 24 | const network = Networks[argv.network]; 25 | 26 | const redeemScript = getHexBuffer(argv.redeemScript); 27 | const transaction = Transaction.fromHex(argv.rawTransaction); 28 | 29 | const swapOutput = detectSwap(redeemScript, transaction)!; 30 | 31 | const claimTransaction = constructClaimTransaction( 32 | [{ 33 | ...swapOutput, 34 | txHash: transaction.getHash(), 35 | preimage: getHexBuffer(argv.preimage), 36 | redeemScript: getHexBuffer(argv.redeemScript), 37 | keys: ECPair.fromPrivateKey(getHexBuffer(argv.privateKey)), 38 | }], 39 | address.toOutputScript(argv.destinationAddress, network), 40 | 2, 41 | true, 42 | ).toHex(); 43 | 44 | console.log(stringify({ claimTransaction })); 45 | }; 46 | -------------------------------------------------------------------------------- /tools/miner_fee.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Calculate the miner fee of a transaction""" 3 | from argparse import ArgumentParser 4 | from bitcoinrpc.authproxy import AuthServiceProxy 5 | 6 | def get_rpc_connection(rpc_port: int): 7 | """Initialize the RPC connection to the daemon""" 8 | return AuthServiceProxy("http://kek:kek@127.0.0.1:{}".format(rpc_port)) 9 | 10 | def get_raw_transaction(rpc_connection: AuthServiceProxy, transaction_id: str): 11 | """Query a raw transaction verbosely""" 12 | return rpc_connection.getrawtransaction(transaction_id, 1) 13 | 14 | def calculate_miner_fee(rpc_connection: AuthServiceProxy, transaction_id: str): 15 | """Calcalute the miner fee of a transaction""" 16 | miner_fee = 0 17 | raw_transaction = get_raw_transaction(rpc_connection, transaction_id) 18 | 19 | for tx_input in raw_transaction["vin"]: 20 | input_transaction = get_raw_transaction(rpc_connection, tx_input["txid"]) 21 | miner_fee += input_transaction["vout"][tx_input["vout"]]["value"] 22 | 23 | for output in raw_transaction["vout"]: 24 | miner_fee -= output["value"] 25 | 26 | return miner_fee 27 | 28 | if __name__ == "__main__": 29 | PARSER = ArgumentParser(description="Calculate the miner fee of a transaction") 30 | 31 | # CLI arguments 32 | PARSER.add_argument("rpcport", help="RPC port of the Bitcoin or Litecoin Core daemon", type=int) 33 | PARSER.add_argument( 34 | "transaction", 35 | help="Transaction of which the miner fee should be calculated", 36 | type=str, 37 | ) 38 | 39 | ARGS = PARSER.parse_args() 40 | 41 | RPC_CONNECTION = get_rpc_connection(ARGS.rpcport) 42 | 43 | print(calculate_miner_fee(RPC_CONNECTION, ARGS.transaction)) 44 | -------------------------------------------------------------------------------- /test/unit/VersionCheck.spec.ts: -------------------------------------------------------------------------------- 1 | import VersionCheck from '../../lib/VersionCheck'; 2 | 3 | describe('VersionCheck', () => { 4 | test('should check version of chain clients', () => { 5 | const symbol = 'BTC'; 6 | const limits = VersionCheck['chainClientVersionLimits']; 7 | 8 | let unsupportedVersion = limits.minimal - 1; 9 | expect(() => VersionCheck.checkChainClientVersion(symbol, unsupportedVersion)) 10 | .toThrow(`unsupported BTC Core version: ${unsupportedVersion}; max version ${limits.maximal}; min version ${limits.minimal}`); 11 | 12 | unsupportedVersion = limits.maximal + 1; 13 | expect(() => VersionCheck.checkChainClientVersion(symbol, unsupportedVersion)) 14 | .toThrow(`unsupported BTC Core version: ${unsupportedVersion}; max version ${limits.maximal}; min version ${limits.minimal}`); 15 | 16 | expect(() => VersionCheck.checkChainClientVersion(symbol, limits.maximal)).not.toThrow(); 17 | expect(() => VersionCheck.checkChainClientVersion(symbol, limits.minimal)).not.toThrow(); 18 | }); 19 | 20 | test('should check version of LND clients', () => { 21 | const symbol = 'BTC'; 22 | 23 | expect(() => VersionCheck.checkLndVersion(symbol, '0.10.4-beta')) 24 | .toThrow('unsupported BTC LND version: 0.10.4-beta; max version 0.14.1; min version 0.12.0'); 25 | 26 | expect(() => VersionCheck.checkLndVersion(symbol, '0.14.2-beta')) 27 | .toThrow('unsupported BTC LND version: 0.14.2-beta; max version 0.14.1; min version 0.12.0'); 28 | 29 | const limits = VersionCheck['lndVersionLimits']; 30 | 31 | expect(() => VersionCheck.checkLndVersion(symbol, limits.maximal)).not.toThrow(); 32 | expect(() => VersionCheck.checkLndVersion(symbol, limits.minimal)).not.toThrow(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.git-commit-template.txt: -------------------------------------------------------------------------------- 1 | # HEADER 2 | # |<---- Using a Maximum Of 50 Characters ---->| 3 | (): 4 | # Type can be 5 | # feat (A new feature) 6 | # fix (A bug fix) 7 | # docs (Documentation only changes) 8 | # style (Changes that do not affect the meaning of the code e.g. white-space, formatting, missing semi-colons, etc) 9 | # refactor (A code change that neither fixes a bug nor adds a feature) 10 | # perf (A code change that improves performance) 11 | # test (Adding missing or correcting existing tests) 12 | # chore (Changes to the build process or auxiliary tools and libraries such as documentation generation) 13 | # An optional scope specifies the place of the commit change, e.g. orderbook, p2p, proto, swaps, etc... 14 | # You can use * when the change affects more than a single scope. 15 | # The subject contains succinct description of the change: 16 | # Use the imperative, present tense: "change" not "changed" nor "changes" 17 | # Don't capitalize first letter 18 | # No dot (.) at the end 19 | 20 | # BODY 21 | # Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes". 22 | # The body should include the motivation for the change and contrast this with previous behavior. 23 | # |<---- Try To Limit Each Line to a Maximum Of 72 Characters ----> 24 | 25 | 26 | # FOOTER 27 | # The footer should contain any information about Breaking Changes and is also the place to reference GitHub issues that this commit closes. 28 | # Breaking Changes should start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this. 29 | # |<---- Try To Limit Each Line to a Maximum Of 72 Characters ----> 30 | 31 | # --- COMMIT END --- 32 | -------------------------------------------------------------------------------- /lib/lightning/ConnectionHelper.ts: -------------------------------------------------------------------------------- 1 | import Logger from '../Logger'; 2 | import LndClient from './LndClient'; 3 | import { formatError } from '../Utils'; 4 | 5 | class ConnectionHelper { 6 | constructor(private logger: Logger) {} 7 | 8 | public connectByPublicKey = async (lndClient: LndClient, publicKey: string): Promise => { 9 | this.logger.verbose(`Trying to connect to LND ${lndClient.symbol} node: ${publicKey}`); 10 | 11 | // Fetch the publicly advertised addresses of the other LND node 12 | const nodeInfo = await lndClient.getNodeInfo(publicKey); 13 | 14 | if (nodeInfo.node === undefined || nodeInfo.node.addressesList.length === 0) { 15 | throw 'node does not advertise addresses'; 16 | } 17 | 18 | let addressesMessage = `Found ${nodeInfo.node.addressesList.length} public addresses of ${publicKey}:`; 19 | 20 | nodeInfo.node.addressesList.forEach((address) => { 21 | addressesMessage += `\n - ${address.addr}`; 22 | }); 23 | 24 | this.logger.debug(addressesMessage); 25 | 26 | for (const address of nodeInfo.node.addressesList) { 27 | this.logger.debug(`Trying to connect to LND ${lndClient.symbol} addresses: ${publicKey}@${address.addr}`); 28 | 29 | try { 30 | await lndClient.connectPeer(publicKey, address.addr); 31 | } catch (error) { 32 | this.logger.debug(`Could not connect to to LND ${lndClient.symbol} address ${publicKey}@${address.addr}: ${formatError(error)}`); 33 | 34 | // Try the next URI if that one did not work 35 | continue; 36 | } 37 | 38 | this.logger.verbose(`Connected to LND ${lndClient.symbol} node: ${publicKey}`); 39 | return; 40 | } 41 | 42 | throw 'could not connect to any of the advertised addresses'; 43 | } 44 | } 45 | 46 | export default ConnectionHelper; 47 | -------------------------------------------------------------------------------- /lib/cli/rsk/commands/Send.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { BigNumber } from 'ethers'; 3 | import { etherDecimals } from '../../../consts/Consts'; 4 | import BuilderComponents from '../../BuilderComponents'; 5 | import { connectEthereum, getBoltzAddress, getContracts } from '../EthereumUtils'; 6 | 7 | export const command = 'send [destination] [token]'; 8 | 9 | export const describe = 'sends Rbtc or a ERC20 token to the destined address'; 10 | 11 | export const builder = { 12 | amount: { 13 | describe: 'amount of tokens that should be sent', 14 | type: 'number', 15 | }, 16 | destination: { 17 | describe: 'address to which the coins should be sent', 18 | type: 'string', 19 | }, 20 | token: BuilderComponents.token, 21 | }; 22 | 23 | export const handler = async (argv: Arguments): Promise => { 24 | const signer = connectEthereum(argv.provider, argv.signer); 25 | const { token } = getContracts(signer); 26 | 27 | const amount = BigNumber.from(argv.amount).mul(etherDecimals); 28 | 29 | let transactionHash: string; 30 | 31 | const destination = argv.destination || await getBoltzAddress(); 32 | 33 | if (destination === undefined) { 34 | console.log('Did not send coins because no onchain address was specified'); 35 | return; 36 | } 37 | 38 | if (argv.token) { 39 | const transaction = await token.transfer(destination, amount); 40 | await transaction.wait(1); 41 | 42 | transactionHash = transaction.hash; 43 | } else { 44 | const transaction = await signer.sendTransaction({ 45 | value: amount, 46 | to: destination, 47 | }); 48 | await transaction.wait(1); 49 | 50 | transactionHash = transaction.hash; 51 | } 52 | 53 | console.log(`Sent ${argv.token ? 'ERC20 token' : 'Rbtc'} in: ${transactionHash}`); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/cli/stacks/commands/Send.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { BigNumber } from 'ethers'; 3 | import { etherDecimals } from '../../../consts/Consts'; 4 | import BuilderComponents from '../../BuilderComponents'; 5 | import { connectEthereum, getBoltzAddress, getContracts } from '../StacksUtils'; 6 | 7 | export const command = 'send [destination] [token]'; 8 | 9 | export const describe = 'sends Rbtc or a ERC20 token to the destined address'; 10 | 11 | export const builder = { 12 | amount: { 13 | describe: 'amount of tokens that should be sent', 14 | type: 'number', 15 | }, 16 | destination: { 17 | describe: 'address to which the coins should be sent', 18 | type: 'string', 19 | }, 20 | token: BuilderComponents.token, 21 | }; 22 | 23 | export const handler = async (argv: Arguments): Promise => { 24 | const signer = connectEthereum(argv.provider, argv.signer); 25 | const { token } = getContracts(signer); 26 | 27 | const amount = BigNumber.from(argv.amount).mul(etherDecimals); 28 | 29 | let transactionHash: string; 30 | 31 | const destination = argv.destination || await getBoltzAddress(); 32 | 33 | if (destination === undefined) { 34 | console.log('Did not send coins because no onchain address was specified'); 35 | return; 36 | } 37 | 38 | if (argv.token) { 39 | const transaction = await token.transfer(destination, amount); 40 | await transaction.wait(1); 41 | 42 | transactionHash = transaction.hash; 43 | } else { 44 | const transaction = await signer.sendTransaction({ 45 | value: amount, 46 | to: destination, 47 | }); 48 | await transaction.wait(1); 49 | 50 | transactionHash = transaction.hash; 51 | } 52 | 53 | console.log(`Sent ${argv.token ? 'ERC20 token' : 'Rbtc'} in: ${transactionHash}`); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/cli/ethereum/commands/Send.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { BigNumber } from 'ethers'; 3 | import { etherDecimals } from '../../../consts/Consts'; 4 | import BuilderComponents from '../../BuilderComponents'; 5 | import { connectEthereum, getBoltzAddress, getContracts } from '../EthereumUtils'; 6 | 7 | export const command = 'send [destination] [token]'; 8 | 9 | export const describe = 'sends Ether or a ERC20 token to the destined address'; 10 | 11 | export const builder = { 12 | amount: { 13 | describe: 'amount of tokens that should be sent', 14 | type: 'number', 15 | }, 16 | destination: { 17 | describe: 'address to which the coins should be sent', 18 | type: 'string', 19 | }, 20 | token: BuilderComponents.token, 21 | }; 22 | 23 | export const handler = async (argv: Arguments): Promise => { 24 | const signer = connectEthereum(argv.provider, argv.signer); 25 | const { token } = getContracts(signer); 26 | 27 | const amount = BigNumber.from(argv.amount).mul(etherDecimals); 28 | 29 | let transactionHash: string; 30 | 31 | const destination = argv.destination || await getBoltzAddress(); 32 | 33 | if (destination === undefined) { 34 | console.log('Did not send coins because no onchain address was specified'); 35 | return; 36 | } 37 | 38 | if (argv.token) { 39 | const transaction = await token.transfer(destination, amount); 40 | await transaction.wait(1); 41 | 42 | transactionHash = transaction.hash; 43 | } else { 44 | const transaction = await signer.sendTransaction({ 45 | value: amount, 46 | to: destination, 47 | }); 48 | await transaction.wait(1); 49 | 50 | transactionHash = transaction.hash; 51 | } 52 | 53 | console.log(`Sent ${argv.token ? 'ERC20 token' : 'Ether'} in: ${transactionHash}`); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/db/models/DirectSwap.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | type DirectSwapType = { 4 | id: string; 5 | nftAddress: string; 6 | userAddress: string; 7 | contractSignature?: string; 8 | invoice?: string; 9 | status: string; 10 | txId?: string; 11 | mintCostStx?: number, 12 | }; 13 | 14 | class DirectSwap extends Model implements DirectSwapType { 15 | public id!: string; 16 | public nftAddress!: string; 17 | public userAddress!: string; 18 | public contractSignature?: string; 19 | public invoice?: string; 20 | public status!: string; 21 | public txId?: string; 22 | public mintCostStx?: number; 23 | 24 | public createdAt!: Date; 25 | public updatedAt!: Date; 26 | 27 | public static load = (sequelize: Sequelize): void => { 28 | DirectSwap.init({ 29 | id: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 30 | nftAddress: { type: new DataTypes.STRING(255), allowNull: false }, 31 | userAddress: { type: new DataTypes.STRING(255), allowNull: false }, 32 | contractSignature: { type: new DataTypes.STRING(255), allowNull: true }, 33 | invoice: { type: new DataTypes.STRING(255), allowNull: true, unique: true }, 34 | status: { type: new DataTypes.STRING(255), allowNull: true }, 35 | txId: { type: new DataTypes.STRING(255), allowNull: true }, 36 | mintCostStx: { type: new DataTypes.INTEGER(), allowNull: true }, 37 | }, { 38 | sequelize, 39 | tableName: 'directSwaps', 40 | timestamps: true, 41 | indexes: [ 42 | { 43 | unique: true, 44 | fields: ['id'], 45 | }, 46 | { 47 | unique: true, 48 | fields: ['invoice'], 49 | }, 50 | ] 51 | }); 52 | } 53 | } 54 | 55 | export default DirectSwap; 56 | export { DirectSwapType }; 57 | -------------------------------------------------------------------------------- /lib/db/StacksTransactionRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import StacksTransaction from './models/StacksTransaction'; 3 | // import { crypto } from 'bitcoinjs-lib'; 4 | // import { getHexString } from '../../lib/Utils'; 5 | 6 | class StacksTransactionRepository { 7 | public findByTxId = (txId: string): Promise => { 8 | return StacksTransaction.findAll({ 9 | where: { 10 | txId: { 11 | [Op.lte]: txId, 12 | }, 13 | } 14 | }); 15 | } 16 | public findByPreimageHash = (preimageHash: string): Promise => { 17 | return StacksTransaction.findAll({ 18 | where: { 19 | preimageHash: { 20 | [Op.eq]: preimageHash, 21 | }, 22 | } 23 | }); 24 | } 25 | public findByPrincipal = (claimPrincipal: string): Promise => { 26 | return StacksTransaction.findAll({ 27 | where: { 28 | claimPrincipal: { 29 | [Op.lte]: claimPrincipal, 30 | }, 31 | } 32 | }); 33 | } 34 | 35 | public addTransaction = (txId: string, preimageHash: string, claimPrincipal: string, event: string, swapContractAddress: string): Promise => { 36 | return StacksTransaction.create({ 37 | txId, 38 | preimageHash, 39 | claimPrincipal, 40 | event, 41 | swapContractAddress 42 | }); 43 | } 44 | 45 | public addClaimTransaction = (txId: string, preimageHash: string, event: string, swapContractAddress: string): Promise => { 46 | // const preimageHash = getHexString(crypto.sha256(Buffer.from(preimage, 'hex'))); 47 | return StacksTransaction.create({ 48 | txId, 49 | preimageHash, 50 | event, 51 | swapContractAddress 52 | }); 53 | } 54 | } 55 | 56 | export default StacksTransactionRepository; 57 | -------------------------------------------------------------------------------- /lib/cli/rsk/commands/Refund.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { ContractTransaction } from 'ethers'; 3 | import { getHexBuffer } from '../../../Utils'; 4 | import BuilderComponents from '../../BuilderComponents'; 5 | import { connectEthereum, getContracts } from '../EthereumUtils'; 6 | import { queryERC20SwapValues, queryEtherSwapValues } from '../../../wallet/rsk/ContractUtils'; 7 | 8 | export const command = 'refund [token]'; 9 | 10 | export const describe = 'refunds Rbtc or a ERC20 token from the corresponding swap contract'; 11 | 12 | export const builder = { 13 | preimageHash: { 14 | describe: 'preimage hash with which the funds have been locked', 15 | type: 'string', 16 | }, 17 | token: BuilderComponents.token, 18 | }; 19 | 20 | export const handler = async (argv: Arguments): Promise => { 21 | const signer = connectEthereum(argv.provider, argv.signer); 22 | const { etherSwap, erc20Swap } = getContracts(signer); 23 | 24 | const preimageHash = getHexBuffer(argv.preimageHash); 25 | 26 | let transaction: ContractTransaction; 27 | 28 | if (argv.token) { 29 | const erc20SwapValues = await queryERC20SwapValues(erc20Swap, preimageHash); 30 | transaction = await erc20Swap.refund( 31 | preimageHash, 32 | erc20SwapValues.amount, 33 | erc20SwapValues.tokenAddress, 34 | erc20SwapValues.claimAddress, 35 | erc20SwapValues.timelock, 36 | ); 37 | } else { 38 | const etherSwapValues = await queryEtherSwapValues(etherSwap, preimageHash); 39 | transaction = await etherSwap.refund( 40 | preimageHash, 41 | etherSwapValues.amount, 42 | etherSwapValues.claimAddress, 43 | etherSwapValues.timelock, 44 | ); 45 | } 46 | 47 | await transaction.wait(1); 48 | 49 | console.log(`Refunded ${argv.token ? 'ERC20 token' : 'Rbtc'} in: ${transaction.hash}`); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/cli/ethereum/commands/Refund.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { ContractTransaction } from 'ethers'; 3 | import { getHexBuffer } from '../../../Utils'; 4 | import BuilderComponents from '../../BuilderComponents'; 5 | import { connectEthereum, getContracts } from '../EthereumUtils'; 6 | import { queryERC20SwapValues, queryEtherSwapValues } from '../../../wallet/ethereum/ContractUtils'; 7 | 8 | export const command = 'refund [token]'; 9 | 10 | export const describe = 'refunds Ether or a ERC20 token from the corresponding swap contract'; 11 | 12 | export const builder = { 13 | preimageHash: { 14 | describe: 'preimage hash with which the funds have been locked', 15 | type: 'string', 16 | }, 17 | token: BuilderComponents.token, 18 | }; 19 | 20 | export const handler = async (argv: Arguments): Promise => { 21 | const signer = connectEthereum(argv.provider, argv.signer); 22 | const { etherSwap, erc20Swap } = getContracts(signer); 23 | 24 | const preimageHash = getHexBuffer(argv.preimageHash); 25 | 26 | let transaction: ContractTransaction; 27 | 28 | if (argv.token) { 29 | const erc20SwapValues = await queryERC20SwapValues(erc20Swap, preimageHash); 30 | transaction = await erc20Swap.refund( 31 | preimageHash, 32 | erc20SwapValues.amount, 33 | erc20SwapValues.tokenAddress, 34 | erc20SwapValues.claimAddress, 35 | erc20SwapValues.timelock, 36 | ); 37 | } else { 38 | const etherSwapValues = await queryEtherSwapValues(etherSwap, preimageHash); 39 | transaction = await etherSwap.refund( 40 | preimageHash, 41 | etherSwapValues.amount, 42 | etherSwapValues.claimAddress, 43 | etherSwapValues.timelock, 44 | ); 45 | } 46 | 47 | await transaction.wait(1); 48 | 49 | console.log(`Refunded ${argv.token ? 'ERC20 token' : 'Ether'} in: ${transaction.hash}`); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/db/models/ChannelCreation.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | import Swap from './Swap'; 3 | 4 | type ChannelCreationType = { 5 | swapId: string; 6 | 7 | status?: string; 8 | 9 | type: string; 10 | private: boolean; 11 | inboundLiquidity: number; 12 | 13 | nodePublicKey?: string; 14 | fundingTransactionId?: string; 15 | fundingTransactionVout?: number; 16 | }; 17 | 18 | class ChannelCreation extends Model implements ChannelCreationType { 19 | public swapId!: string; 20 | 21 | public status?: string; 22 | 23 | public type!: string; 24 | public private!: boolean; 25 | public inboundLiquidity!: number; 26 | 27 | public nodePublicKey?: string; 28 | public fundingTransactionId?: string; 29 | public fundingTransactionVout?: number; 30 | 31 | public static load = (sequelize: Sequelize): void => { 32 | ChannelCreation.init({ 33 | swapId: { type: new DataTypes.STRING(255), primaryKey: true, allowNull: false }, 34 | status: { type: new DataTypes.STRING(255), allowNull: true }, 35 | type: { type: new DataTypes.STRING(255), allowNull: false }, 36 | private: { type: DataTypes.BOOLEAN, allowNull: false }, 37 | nodePublicKey: { type: new DataTypes.STRING(255), allowNull: true }, 38 | inboundLiquidity: { type: new DataTypes.INTEGER(), allowNull: false }, 39 | fundingTransactionId: { type: new DataTypes.STRING(255), allowNull: true }, 40 | fundingTransactionVout: { type: new DataTypes.INTEGER(), allowNull: true }, 41 | }, { 42 | sequelize, 43 | tableName: 'channelCreations', 44 | indexes: [ 45 | { 46 | unique: true, 47 | fields: ['swapId'], 48 | }, 49 | ], 50 | }); 51 | 52 | ChannelCreation.belongsTo(Swap, { 53 | foreignKey: 'swapId', 54 | }); 55 | } 56 | } 57 | 58 | export default ChannelCreation; 59 | export { ChannelCreationType }; 60 | -------------------------------------------------------------------------------- /docker/stacks/config/Config.toml: -------------------------------------------------------------------------------- 1 | [node] 2 | working_dir = "/root/stacks-node/data" 3 | rpc_bind = "0.0.0.0:20443" 4 | p2p_bind = "0.0.0.0:20444" 5 | seed = "0000000000000000000000000000000000000000000000000000000000000000" 6 | local_peer_seed = "0000000000000000000000000000000000000000000000000000000000000000" 7 | miner = true 8 | mine_microblocks = true 9 | microblock_frequency = 100 10 | wait_time_for_microblocks = 0 11 | use_test_genesis_chainstate = true 12 | microblock_attempt_time_ms = 1_000 13 | 14 | # https://github.com/stacks-network/stacks-blockchain/blob/7dc20797fb5d815ff4779063c0c0902443a44bb7/testnet/stacks-node/src/tests/neon_integrations.rs#L2811 15 | #conf.node.mine_microblocks = true; 16 | #conf.node.wait_time_for_microblocks = 1000; 17 | #conf.node.microblock_frequency = 1000; 18 | #conf.miner.microblock_attempt_time_ms = 120_000; 19 | #conf.node.max_microblocks = 65536; 20 | #conf.burnchain.max_rbf = 1000000; 21 | 22 | #commit_anchor_block_within = 1_000_000 23 | #mock_mining: false, 24 | #mine_microblocks: true, 25 | #microblock_frequency: 30_000, 26 | #max_microblocks: u16::MAX as u64, 27 | #wait_time_for_microblocks: 30_000, 28 | 29 | [[events_observer]] 30 | endpoint = "stacks-blockchain-api:3700" 31 | retry_count = 255 32 | events_keys = ["*"] 33 | 34 | [burnchain] 35 | chain = "bitcoin" 36 | mode = "mocknet" 37 | commit_anchor_block_within = 120_000 38 | 39 | [[ustx_balance]] 40 | address = "ST3EQ88S02BXXD0T5ZVT3KW947CRMQ1C6DMQY8H19" 41 | amount = 100000000000000 42 | 43 | [[ustx_balance]] 44 | address = "ST3KCNDSWZSFZCC6BE4VA9AXWXC9KEB16FBTRK36T" 45 | amount = 100000000000000 46 | 47 | [[ustx_balance]] 48 | address = "ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK" 49 | amount = 100000000000000 50 | 51 | [[ustx_balance]] 52 | address = "ST27SD3H5TTZXPBFXHN1ZNMFJ3HNE2070QX7ZN4FF" 53 | amount = 100000000000000 54 | 55 | [[ustx_balance]] 56 | address = "ST1N28QCRR03EW37S470PND4SPECCXQ22ZZHF97GP" 57 | amount = 100000000000000 -------------------------------------------------------------------------------- /docs/channel-creation.md: -------------------------------------------------------------------------------- 1 | # Channel Creation Docs 2 | 3 | The Boltz Submarine Swaps can be configured to create a channel to your node before paying the invoice. This document elaborates on the way we do this and how you can enforce that the requested channel is actually opened. 4 | 5 | ## Enforcing a Channel Creation 6 | 7 | As of writing this, no Lightning implementation does support setting the channel through which an invoice should be paid or even specifying that an invoice has to be paid through a new channel to a specific node. Therefore you will have to get creative if you want to enforce that the channel Boltz agreed to open is really created. 8 | 9 | ### External Daemon 10 | 11 | One way to enforce that the invoice is paid through a freshly created channel with the requested properties and balances is by using [hold invoices](https://wiki.ion.radar.tech/tech/research/hodl-invoice) in combination with an external daemon. Such a daemon should have all the information required to enforce the channel creation. This is the hold invoice that should be paid, the preimage of said hold invoice and the properties of the channel that should be created. If the invoice is paid through a new channel with the aspired properties, the daemon should settle the invoice. 12 | 13 | Such a daemon could also utilize the [ChannelAcceptor of LND](https://api.lightning.community/#channelacceptor) to prevent channels with properties other than the requested ones being opened. 14 | 15 | ### Modifying the Lightning Client 16 | 17 | Another way is incorporating a logic similar to the one of the external daemon directly into the Lightning implementation. This can be done by modifying the source code of the client or by creating a plugin. We have built a [c-lightning](https://github.com/ElementsProject/lightning) plugin for this exact use case. The full source code for this plugin can be found in [this repository](https://github.com/BoltzExchange/channel-creation-plugin). 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb", "tslint-no-circular-imports"], 3 | "rules": { 4 | "variable-name": false, 5 | "import-name": false, 6 | "max-line-length": [true, 150], 7 | "no-else-after-return": false, 8 | "function-name": false, 9 | "align": false, 10 | "typedef-whitespace": [ 11 | true, { 12 | "call-signature": "nospace", 13 | "index-signature": "nospace", 14 | "parameter": "nospace", 15 | "property-declaration": "nospace", 16 | "variable-declaration": "nospace" 17 | }, 18 | { 19 | "call-signature": "onespace", 20 | "index-signature": "onespace", 21 | "parameter": "onespace", 22 | "property-declaration": "onespace", 23 | "variable-declaration": "onespace" 24 | } 25 | ], 26 | "whitespace": [true, 27 | "check-type-operator", 28 | "check-module", 29 | "check-rest-spread", 30 | "check-type", 31 | "check-preblock", 32 | "check-branch", "check-decl", "check-operator", "check-preblock", "check-separator" // preserved from tslint-config-airbnb 33 | ], 34 | "member-access": true, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "public-instance-field", 40 | "public-static-field", 41 | "protected-instance-field", 42 | "protected-static-field", 43 | "private-instance-field", 44 | "private-static-field", 45 | "public-constructor", 46 | "protected-constructor", 47 | "private-constructor", 48 | "static-method", 49 | "instance-method" 50 | ] 51 | } 52 | ], 53 | "no-floating-promises": [true, "Bluebird"], 54 | "no-null-keyword": true, 55 | "jsdoc-format": true, 56 | "no-unnecessary-type-assertion": false, 57 | "await-promise": [true, "Bluebird"], 58 | "no-inferrable-types": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/CliTools.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { Arguments } from 'yargs'; 4 | import Config from './Config'; 5 | import Logger from './Logger'; 6 | import Stats from './data/Stats'; 7 | import Report from './data/Report'; 8 | import Database from './db/Database'; 9 | import { resolveHome, stringify } from './Utils'; 10 | import SwapRepository from './db/SwapRepository'; 11 | import SwapFailureChecker from './data/SwapFailureChecker'; 12 | import ReverseSwapRepository from './db/ReverseSwapRepository'; 13 | 14 | const initDatabase = async (argv: Arguments): Promise => { 15 | // Get the path to the database from the command line arguments or 16 | // use a default one if none was specified 17 | const dbPath = argv.dbpath || path.join(Config.defaultDataDir, Config.defaultDbPath); 18 | 19 | const db = new Database(Logger.disabledLogger, resolveHome(dbPath)); 20 | await db.init(); 21 | }; 22 | 23 | const generateReport = async (argv: Arguments): Promise => { 24 | await initDatabase(argv); 25 | 26 | const report = new Report(new SwapRepository(), new ReverseSwapRepository()); 27 | const csv = await report.generate(); 28 | 29 | if (argv.reportpath) { 30 | fs.writeFileSync(resolveHome(argv.reportpath), csv); 31 | } else { 32 | console.log(csv); 33 | } 34 | }; 35 | 36 | const generateStats = async (argv: Arguments): Promise => { 37 | await initDatabase(argv); 38 | 39 | const stats = new Stats(new SwapRepository(), new ReverseSwapRepository()); 40 | 41 | console.log(await stats.generate()); 42 | }; 43 | 44 | const checkFailedSwaps = async (argv: Arguments): Promise => { 45 | await initDatabase(argv); 46 | 47 | const failedSwaps = new SwapFailureChecker(new SwapRepository(), new ReverseSwapRepository()); 48 | 49 | console.log(stringify(await failedSwaps.check())); 50 | }; 51 | 52 | export { 53 | generateStats, 54 | generateReport, 55 | checkFailedSwaps, 56 | }; 57 | -------------------------------------------------------------------------------- /contracts/usda-token.clar: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;; SIP 010 ;;;;;;;;;;;;;;;;;;;;;; 2 | ;; mainnet sip10 trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE 3 | (impl-trait .sip-010-trait.sip-010-trait) 4 | 5 | (define-constant contract-creator tx-sender) 6 | 7 | ;; Defines the USDA Stablecoin according to the SIP-010 Standard 8 | (define-fungible-token usda) 9 | 10 | ;; errors 11 | (define-constant ERR-NOT-AUTHORIZED u401) 12 | 13 | ;; Mint initial token 14 | (ft-mint? usda u1000000000 contract-creator) 15 | (ft-mint? usda u1000000000 'ST27SD3H5TTZXPBFXHN1ZNMFJ3HNE2070QX7ZN4FF) 16 | (ft-mint? usda u1000000000 'ST1N28QCRR03EW37S470PND4SPECCXQ22ZZHF97GP) 17 | (ft-mint? usda u1000000000 'ST3RXC1Q8MXYKPWRNQ6ZFAWSRY8PKRK8H4BW2F8F) 18 | 19 | ;; --------------------------------------------------------- 20 | ;; SIP-10 Functions 21 | ;; --------------------------------------------------------- 22 | 23 | (define-read-only (get-total-supply) 24 | (ok (ft-get-supply usda)) 25 | ) 26 | 27 | (define-read-only (get-name) 28 | (ok "USDA") 29 | ) 30 | 31 | (define-read-only (get-symbol) 32 | (ok "USDA") 33 | ) 34 | 35 | (define-read-only (get-decimals) 36 | (ok u6) 37 | ) 38 | 39 | (define-read-only (get-balance (account principal)) 40 | (ok (ft-get-balance usda account)) 41 | ) 42 | 43 | (define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) 44 | (begin 45 | (asserts! (is-eq tx-sender sender) (err ERR-NOT-AUTHORIZED)) 46 | 47 | (match (ft-transfer? usda amount sender recipient) 48 | response (begin 49 | (print memo) 50 | (ok response) 51 | ) 52 | error (err error) 53 | ) 54 | ) 55 | ) 56 | 57 | ;; dummy token-uri 58 | (define-public (get-token-uri) 59 | (ok (some u"https://some.token/token-metadata.json"))) 60 | 61 | (define-public (gift-tokens (recipient principal)) 62 | (begin 63 | (asserts! (is-eq tx-sender recipient) (err u0)) 64 | (ft-mint? usda u1000000000 recipient) 65 | ) 66 | ) -------------------------------------------------------------------------------- /lib/notifications/OtpManager.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path'; 2 | import { authenticator } from 'otplib'; 3 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 4 | import Logger from '../Logger'; 5 | import { NotificationConfig } from '../Config'; 6 | 7 | class OtpManager { 8 | private readonly secret: string; 9 | 10 | // This variable keeps track of the last used token to prevent that the same token can be used multiple times 11 | private lastUsedToken = ''; 12 | 13 | private static service = 'Boltz'; 14 | private static uriFile = 'otpUri.txt'; 15 | 16 | constructor(private logger: Logger, config: NotificationConfig) { 17 | if (existsSync(config.otpsecretpath)) { 18 | this.secret = readFileSync(config.otpsecretpath, { encoding: 'utf8' }); 19 | 20 | this.logger.info('Loaded existing OTP secret'); 21 | } else { 22 | this.secret = authenticator.generateSecret(); 23 | writeFileSync(config.otpsecretpath, this.secret); 24 | 25 | this.logger.warn('Generated new OTP secret'); 26 | 27 | this.generateUri(config.prefix, config.otpsecretpath); 28 | } 29 | } 30 | 31 | public verify = (token: string): boolean => { 32 | try { 33 | if (token !== this.lastUsedToken) { 34 | const valid = authenticator.check(token, this.secret); 35 | 36 | if (valid) { 37 | this.lastUsedToken = token; 38 | } 39 | 40 | return valid; 41 | } 42 | } catch (error) { 43 | this.logger.debug(`Could not check OTP token ${token}: ${error}`); 44 | } 45 | 46 | return false; 47 | } 48 | 49 | private generateUri = (prefix: string, secretPath: string) => { 50 | const uri = authenticator.keyuri( 51 | encodeURIComponent(prefix), 52 | encodeURIComponent(OtpManager.service), 53 | this.secret, 54 | ); 55 | 56 | const path = join(dirname(secretPath), OtpManager.uriFile); 57 | 58 | writeFileSync(path, uri); 59 | } 60 | } 61 | 62 | export default OtpManager; 63 | -------------------------------------------------------------------------------- /lib/cli/commands/Refund.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { address, ECPair, Transaction, networks } from 'bitcoinjs-lib'; 3 | // Networks, 4 | import { constructRefundTransaction, detectSwap } from 'boltz-core'; 5 | import BuilderComponents from '../BuilderComponents'; 6 | import { getHexBuffer, stringify } from '../../Utils'; 7 | 8 | export const command = 'refund '; 9 | 10 | export const describe = 'refunds submarine or chain to chain swaps'; 11 | 12 | export const builder = { 13 | network: BuilderComponents.network, // bitcoinMainnet 14 | privateKey: BuilderComponents.privateKey, 15 | redeemScript: BuilderComponents.redeemScript, 16 | rawTransaction: BuilderComponents.rawTransaction, 17 | destinationAddress: BuilderComponents.destinationAddress, 18 | timeoutBlockHeight: BuilderComponents.timeoutBlockHeight, 19 | }; 20 | 21 | export const handler = (argv: Arguments): void => { 22 | // const network = Networks[argv.network]; 23 | const network = networks.bitcoin; 24 | 25 | const redeemScript = getHexBuffer(argv.redeemScript); 26 | const transaction = Transaction.fromHex(argv.rawTransaction); 27 | const swapOutput = detectSwap(redeemScript, transaction)!; 28 | 29 | console.log('refund constructRefundTransaction ', 30 | swapOutput, transaction.getHash(), 31 | address.toOutputScript(argv.destinationAddress, 32 | network)); 33 | 34 | const refundTransaction = constructRefundTransaction( 35 | [{ 36 | ...swapOutput, 37 | txHash: transaction.getHash(), 38 | redeemScript: getHexBuffer(argv.redeemScript), 39 | keys: ECPair.fromPrivateKey(getHexBuffer(argv.privateKey)), 40 | }], 41 | address.toOutputScript(argv.destinationAddress, network), 42 | parseInt(argv.timeoutBlockHeight), // 719579, // timeoutblockheight!!! 43 | 1, 44 | true, 45 | ).toHex(); 46 | 47 | console.log(stringify({ refundTransaction })); 48 | }; 49 | -------------------------------------------------------------------------------- /lib/wallet/rsk/EthereumTransactionTracker.ts: -------------------------------------------------------------------------------- 1 | import { providers, Signer } from 'ethers'; 2 | import Logger from '../../Logger'; 3 | import PendingEthereumTransactionRepository from '../../db/PendingEthereumTransactionRepository'; 4 | 5 | class EthereumTransactionTracker { 6 | private pendingEthereumTransactionRepository = new PendingEthereumTransactionRepository(); 7 | 8 | private walletAddress!: string; 9 | 10 | constructor( 11 | private logger: Logger, 12 | private provider: providers.Provider, 13 | private wallet: Signer, 14 | ) {} 15 | 16 | public init = async (): Promise => { 17 | this.walletAddress = await this.wallet.getAddress(); 18 | this.logger.info(`Starting Rsk transaction tracker for address: ${this.walletAddress}`); 19 | 20 | await this.scanBlock(await this.provider.getBlockNumber()); 21 | } 22 | 23 | /** 24 | * Scans a block and removes pending transactions from the database in case they were confirmed 25 | * This method is public and gets called from "EthereumManager" because there is a block subscription 26 | * in that class already 27 | */ 28 | public scanBlock = async (blockNumber: number): Promise => { 29 | const block = await this.provider.getBlockWithTransactions(blockNumber); 30 | 31 | for (const transaction of block.transactions) { 32 | if (transaction.from === this.walletAddress) { 33 | const confirmedTransactions = await this.pendingEthereumTransactionRepository.findByNonce(transaction.nonce); 34 | 35 | if (confirmedTransactions.length > 0) { 36 | this.logger.debug(`Removing ${confirmedTransactions.length} confirmed Rsk transactions`); 37 | 38 | for (const confirmedTransaction of confirmedTransactions) { 39 | this.logger.silly(`Removing confirmed Rsk transaction: ${confirmedTransaction.hash}`); 40 | await confirmedTransaction.destroy(); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | export default EthereumTransactionTracker; 49 | -------------------------------------------------------------------------------- /lib/wallet/stacks/EthereumTransactionTracker.ts: -------------------------------------------------------------------------------- 1 | import { providers, Signer } from 'ethers'; 2 | import Logger from '../../Logger'; 3 | import PendingEthereumTransactionRepository from '../../db/PendingEthereumTransactionRepository'; 4 | 5 | class EthereumTransactionTracker { 6 | private pendingEthereumTransactionRepository = new PendingEthereumTransactionRepository(); 7 | 8 | private walletAddress!: string; 9 | 10 | constructor( 11 | private logger: Logger, 12 | private provider: providers.Provider, 13 | private wallet: Signer, 14 | ) {} 15 | 16 | public init = async (): Promise => { 17 | this.walletAddress = await this.wallet.getAddress(); 18 | this.logger.info(`Starting Rsk transaction tracker for address: ${this.walletAddress}`); 19 | 20 | await this.scanBlock(await this.provider.getBlockNumber()); 21 | } 22 | 23 | /** 24 | * Scans a block and removes pending transactions from the database in case they were confirmed 25 | * This method is public and gets called from "EthereumManager" because there is a block subscription 26 | * in that class already 27 | */ 28 | public scanBlock = async (blockNumber: number): Promise => { 29 | const block = await this.provider.getBlockWithTransactions(blockNumber); 30 | 31 | for (const transaction of block.transactions) { 32 | if (transaction.from === this.walletAddress) { 33 | const confirmedTransactions = await this.pendingEthereumTransactionRepository.findByNonce(transaction.nonce); 34 | 35 | if (confirmedTransactions.length > 0) { 36 | this.logger.debug(`Removing ${confirmedTransactions.length} confirmed Rsk transactions`); 37 | 38 | for (const confirmedTransaction of confirmedTransactions) { 39 | this.logger.silly(`Removing confirmed Rsk transaction: ${confirmedTransaction.hash}`); 40 | await confirmedTransaction.destroy(); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | export default EthereumTransactionTracker; 49 | -------------------------------------------------------------------------------- /lib/rates/data/exchanges/Coingecko.ts: -------------------------------------------------------------------------------- 1 | import Exchange, { makeRequest } from '../Exchange'; 2 | 3 | class Coingecko implements Exchange { 4 | // curl -X GET "https://api.coingecko.com/api/v3/simple/price?ids=sovryn&vs_currencies=btc" -H "accept: application/json" 5 | private static readonly API = 'https://api.coingecko.com/api/v3'; 6 | 7 | public async getPrice(baseAsset: string, quoteAsset: string): Promise { 8 | // BTC SOV 9 | // console.log("getPrice baseAsset quoteAsset: ", baseAsset, quoteAsset); 10 | const longerquoteasset = this.longerName(quoteAsset); 11 | const lowerbaseasset = baseAsset.toLowerCase(); 12 | const pair = `${this.longerName(quoteAsset)}&vs_currencies=${baseAsset}`; 13 | const response = await makeRequest(`${Coingecko.API}/simple/price?ids=${pair}`); 14 | // console.log("response: ", response, response[longerquoteasset]); 15 | const lastprice = response[longerquoteasset][lowerbaseasset]; 16 | // console.log("coingecko 1/lastprice: ", 1/lastprice); 17 | // 1 stx = 3000 sats 18 | // 1 btc = 33156.498673740054 STX -> this is returned from here which is correct on frontend UI 19 | return Number(1/lastprice); 20 | 21 | // const lastTrade = (Object.values(response['result'])[0] as Record)['c']; 22 | 23 | // return Number(lastTrade[0]); 24 | } 25 | 26 | private longerName = (asset: string) => { 27 | switch (asset) { 28 | case 'SOV': return 'sovryn'; 29 | case 'ETH': return 'ethereum'; 30 | case 'BTC': return 'bitcoin'; 31 | case 'RBTC': return 'rootstock'; 32 | case 'STX': return 'blockstack'; 33 | case 'XUSD': return 'usd-coin'; 34 | 35 | default: return asset; 36 | } 37 | } 38 | 39 | // private parseAsset = (asset: string) => { 40 | // const assetUpperCase = asset.toUpperCase(); 41 | 42 | // switch (assetUpperCase) { 43 | // case 'BTC': return 'XBT'; 44 | // default: return assetUpperCase; 45 | // } 46 | // } 47 | } 48 | 49 | export default Coingecko; 50 | -------------------------------------------------------------------------------- /lib/wallet/ethereum/EthereumTransactionTracker.ts: -------------------------------------------------------------------------------- 1 | import { providers, Signer } from 'ethers'; 2 | import Logger from '../../Logger'; 3 | import PendingEthereumTransactionRepository from '../../db/PendingEthereumTransactionRepository'; 4 | 5 | class EthereumTransactionTracker { 6 | private pendingEthereumTransactionRepository = new PendingEthereumTransactionRepository(); 7 | 8 | private walletAddress!: string; 9 | 10 | constructor( 11 | private logger: Logger, 12 | private provider: providers.Provider, 13 | private wallet: Signer, 14 | ) {} 15 | 16 | public init = async (): Promise => { 17 | this.walletAddress = await this.wallet.getAddress(); 18 | this.logger.info(`Starting Ethereum transaction tracker for address: ${this.walletAddress}`); 19 | 20 | await this.scanBlock(await this.provider.getBlockNumber()); 21 | } 22 | 23 | /** 24 | * Scans a block and removes pending transactions from the database in case they were confirmed 25 | * This method is public and gets called from "EthereumManager" because there is a block subscription 26 | * in that class already 27 | */ 28 | public scanBlock = async (blockNumber: number): Promise => { 29 | const block = await this.provider.getBlockWithTransactions(blockNumber); 30 | 31 | for (const transaction of block.transactions) { 32 | if (transaction.from === this.walletAddress) { 33 | const confirmedTransactions = await this.pendingEthereumTransactionRepository.findByNonce(transaction.nonce); 34 | 35 | if (confirmedTransactions.length > 0) { 36 | this.logger.debug(`Removing ${confirmedTransactions.length} confirmed Ethereum transactions`); 37 | 38 | for (const confirmedTransaction of confirmedTransactions) { 39 | this.logger.silly(`Removing confirmed Ethereum transaction: ${confirmedTransaction.hash}`); 40 | await confirmedTransaction.destroy(); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | export default EthereumTransactionTracker; 49 | -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Instructions for running lnstxbridge through docker-compose. 3 | The docker-compose consist of the following services: 4 | - lnstxbridge backend API (running on port 9002) 5 | - lnstxbridge frontend (running on port 3000) 6 | 7 | You can build your own images, or use images built by us (default latest version in docker-compose) 8 | 9 | # Docker build 10 | ``` 11 | git clone https://github.com/pseudozach/lnstxbridge 12 | cd lnstxbridge 13 | docker buildx build --platform linux/amd64 -t your_tag_name . 14 | 15 | 16 | git clone https://github.com/pseudozach/lnstxbridge-frontend 17 | cd lnstxbridge-frontend 18 | docker buildx build --platform linux/amd64 -t your_tag_name . 19 | ``` 20 | # Configuration 21 | 22 | Each services needs it own configuration file to be mounted inside docker container at startup. 23 | You should edit configuration files with your own values. 24 | 25 | ## lnstxbridge backend API 26 | Open and edit the `boltz.conf` with your values 27 | ### LND 28 | - lnd endpoint 29 | - macaroonpath 30 | - certpath 31 | ### BTC 32 | - bitcoin node endpoint 33 | - cookie 34 | - rpcuser 35 | - rpcpass 36 | ### Stacks 37 | - stacks node endpoint, if you cannot host your own node, you can get in touch with us for API access. 38 | #### Onchain data 39 | We need to provide information about smart contracts that service will be interacting with 40 | ##### Swap contracts 41 | These contracts should be deployed for every deployment of this service and should not be shared with other deployments 42 | - stxswap contract address - latest version under /contracts folder 43 | - sip10swap contract address - latest version under /contracts folder 44 | ##### USDA token contract address 45 | Currently alongside STX and Lightning we support swapping of USDA tokens. We need to provide deployment of the USDA token of the chain we are deploying to: 46 | - contractAddress 47 | 48 | # lnstxbridge frontend 49 | Open and edit `frontend/.env` with your values 50 | Contracts you deployed in step above for the backend will also be needed in here -------------------------------------------------------------------------------- /lib/cli/rsk/EthereumUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { join } from 'path'; 3 | import { ContractABIs } from 'boltz-core'; 4 | import { existsSync, readFileSync } from 'fs'; 5 | import { ERC20 } from 'boltz-core/typechain/ERC20'; 6 | import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; 7 | import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; 8 | import { Signer, providers, Contract, Wallet } from 'ethers'; 9 | 10 | const Constants = { 11 | erc20TokenAddress: '0x9f84F92d952f90027618089F6F2a3481f1a3fa0F', 12 | rbtcSwapAddress: '0x4efc8b4323e532db6cd78d70a97f83bc7559cef3', 13 | erc20SwapAddress: '0x3b15af794d4e39589a31089ce0b53a9e1994930f', 14 | }; 15 | 16 | const connectEthereum = (providerUrl: string, signerAddress: string): Signer => { 17 | const provider = new providers.JsonRpcProvider(providerUrl); 18 | console.log('rsk connectEthereum signerAddress: ', signerAddress); 19 | return provider.getSigner(signerAddress); 20 | }; 21 | 22 | const getContracts = (signer: Signer): { token: ERC20, etherSwap: EtherSwap, erc20Swap: ERC20Swap } => { 23 | return { 24 | token: new Contract( 25 | Constants.erc20TokenAddress, 26 | ContractABIs.ERC20, 27 | signer, 28 | ) as any as ERC20, 29 | 30 | etherSwap: new Contract( 31 | Constants.rbtcSwapAddress, 32 | ContractABIs.EtherSwap, 33 | signer, 34 | ) as any as EtherSwap, 35 | erc20Swap: new Contract( 36 | Constants.erc20SwapAddress, 37 | ContractABIs.ERC20Swap, 38 | signer, 39 | ) as any as ERC20Swap, 40 | }; 41 | }; 42 | 43 | const getBoltzAddress = async (): Promise => { 44 | const filePath = join(process.env.HOME!, '.boltz/seed.dat'); 45 | 46 | if (existsSync(filePath)) { 47 | return Wallet.fromMnemonic(readFileSync( 48 | filePath, 49 | { 50 | encoding: 'utf-8', 51 | }, 52 | )).getAddress(); 53 | } 54 | 55 | return; 56 | }; 57 | 58 | export { 59 | Constants, 60 | 61 | getContracts, 62 | connectEthereum, 63 | getBoltzAddress, 64 | }; 65 | -------------------------------------------------------------------------------- /lib/cli/ethereum/EthereumUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { join } from 'path'; 3 | import { ContractABIs } from 'boltz-core'; 4 | import { existsSync, readFileSync } from 'fs'; 5 | import { ERC20 } from 'boltz-core/typechain/ERC20'; 6 | import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; 7 | import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; 8 | import { Signer, providers, Contract, Wallet } from 'ethers'; 9 | 10 | const Constants = { 11 | erc20TokenAddress: '0x23d5395De7862A174b0cCbf9f7A350b4b8afC720', 12 | 13 | etherSwapAddress: '0xCc2dd65Bc10AdbE3C6cF341aaFB5d9440E72cc20', 14 | erc20SwapAddress: '0xf90664Edb44fd966089673f289834DaDb9888203', 15 | }; 16 | 17 | const connectEthereum = (providerUrl: string, signerAddress: string): Signer => { 18 | const provider = new providers.JsonRpcProvider(providerUrl); 19 | console.log('connectEthereum signerAddress: ', signerAddress); 20 | return provider.getSigner(signerAddress); 21 | }; 22 | 23 | const getContracts = (signer: Signer): { token: ERC20, etherSwap: EtherSwap, erc20Swap: ERC20Swap } => { 24 | return { 25 | token: new Contract( 26 | Constants.erc20TokenAddress, 27 | ContractABIs.ERC20, 28 | signer, 29 | ) as any as ERC20, 30 | 31 | etherSwap: new Contract( 32 | Constants.etherSwapAddress, 33 | ContractABIs.EtherSwap, 34 | signer, 35 | ) as any as EtherSwap, 36 | erc20Swap: new Contract( 37 | Constants.erc20SwapAddress, 38 | ContractABIs.ERC20Swap, 39 | signer, 40 | ) as any as ERC20Swap, 41 | }; 42 | }; 43 | 44 | const getBoltzAddress = async (): Promise => { 45 | const filePath = join(process.env.HOME!, '.boltz/seed.dat'); 46 | 47 | if (existsSync(filePath)) { 48 | return Wallet.fromMnemonic(readFileSync( 49 | filePath, 50 | { 51 | encoding: 'utf-8', 52 | }, 53 | )).getAddress(); 54 | } 55 | 56 | return; 57 | }; 58 | 59 | export { 60 | Constants, 61 | 62 | getContracts, 63 | connectEthereum, 64 | getBoltzAddress, 65 | }; 66 | -------------------------------------------------------------------------------- /docker-compose/frontend/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | 3 | # production 4 | HTTPS=false 5 | PORT=3000 6 | #PORT = 3001 7 | SSL_CRT_FILE=fullchain.pem 8 | SSL_KEY_FILE=privkey.pem 9 | 10 | # Onion URL 11 | REACT_APP_BOLTZ_ONION=http://random.onion 12 | 13 | # API endpoint 14 | # REACT_APP_BOLTZ_API=http://127.0.0.1:9002 15 | REACT_APP_BOLTZ_API=${REACT_APP_BOLTZ_API} 16 | REACT_APP_BOLTZ_API_ONION=http://random.onion/api 17 | 18 | # LND node URIs 19 | REACT_APP_BITCOIN_LND=${REACT_APP_BITCOIN_LND} 20 | REACT_APP_BITCOIN_LND_ONION=${REACT_APP_BITCOIN_LND_ONION} 21 | 22 | REACT_APP_LITECOIN_LND=030bbf3ecfc370d2caff64858485c45b01b92b74a92c81d395589ed394d5e0d236@127.0.0.1:10735 23 | REACT_APP_LITECOIN_LND_ONION=030bbf3ecfc370d2caff64858485c45b01b92b74a92c81d395589ed394d5e0d236@random.onion:10735 24 | 25 | # Network configurations 26 | REACT_APP_NETWORK=${REACT_APP_NETWORK} 27 | REACT_APP_STACKS_NETWORK_TYPE=${REACT_APP_STACKS_NETWORK} 28 | 29 | REACT_APP_BITCOIN_EXPLORER=https://mempool.space 30 | REACT_APP_LITECOIN_EXPLORER=https://127.0.0.1 31 | # REACT_APP_STACKS_EXPLORER=http://localhost:3000 32 | REACT_APP_STACKS_EXPLORER=https://explorer.stacks.co 33 | 34 | # Sample values 35 | REACT_APP_LOCKUP_TRANSACTION_HASH=0000000000000000000000000000000000000000000000000000000000000000 36 | 37 | REACT_APP_BITCOIN_ADDRESS=bcrt1qma2jy0pe527wngc6lysplrnwhj8uvwljyykg88 38 | REACT_APP_LITECOIN_ADDRESS=ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK 39 | 40 | REACT_APP_BITCOIN_INVOICE=lnbcrt1pwgl58kpp52wvwnmya9xja68nqu3e3mc7x3rag52nsp7e0j6r5r2vte6wvp0nqdqqcqzystg08vaszwxjrgsazajs56xcc7k6rmzaq3wrnqjncsjglutkzxge9fsux675vqq36wq29jgc9y5rw0lggl4268zvl20g48sqr2neh88qpujs498 41 | REACT_APP_LITECOIN_INVOICE=ST27SD3H5TTZXPBFXHN1ZNMFJ3HNE2070QX7ZN4FF 42 | 43 | REACT_APP_RBTCSWAP_ADDRESS=0x4efc8b4323e532db6cd78d70a97f83bc7559cef3 44 | REACT_APP_ERC20SWAP_ADDRESS=0x3b15af794d4e39589a31089ce0b53a9e1994930f 45 | REACT_APP_ERC20TOKEN_ADDRESS=0x9f84F92d952f90027618089F6F2a3481f1a3fa0F 46 | REACT_APP_BOLTZ_ADDRESS=ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK 47 | REACT_APP_STXSWAP_ADDRESS=STR187KT73T0A8M0DEWDX06TJR2B8WM0WP9VGZY3.stxswap_v3 48 | -------------------------------------------------------------------------------- /test/integration/wallet/providers/EtherWalletProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | import Logger from '../../../../lib/Logger'; 3 | import { etherDecimals } from '../../../../lib/consts/Consts'; 4 | import { fundSignerWallet, getSigner } from '../EthereumTools'; 5 | import EtherWalletProvider from '../../../../lib/wallet/providers/EtherWalletProvider'; 6 | 7 | describe('EtherWalletProvider', () => { 8 | const { provider, signer, etherBase } = getSigner(); 9 | const wallet = new EtherWalletProvider(Logger.disabledLogger, signer); 10 | 11 | test('should get address', async () => { 12 | expect(await wallet.getAddress()).toEqual(await signer.getAddress()); 13 | }); 14 | 15 | test('should get balance', async () => { 16 | await fundSignerWallet(signer, etherBase); 17 | 18 | const balance = (await signer.getBalance()).div(etherDecimals).toNumber(); 19 | 20 | expect(await wallet.getBalance()).toEqual({ 21 | totalBalance: balance, 22 | confirmedBalance: balance, 23 | unconfirmedBalance: 0, 24 | }); 25 | }); 26 | 27 | test('should send to address', async () => { 28 | const amount = 1000000; 29 | const { transactionId } = await wallet.sendToAddress(await signer.getAddress(), amount); 30 | 31 | const transaction = await provider.getTransaction(transactionId); 32 | expect(transaction.value).toEqual(BigNumber.from(amount).mul(etherDecimals)); 33 | }); 34 | 35 | test('should sweep wallet', async () => { 36 | const balance = await signer.getBalance(); 37 | 38 | const { transactionId } = await wallet.sweepWallet(await etherBase.getAddress()); 39 | 40 | const transaction = await provider.getTransaction(transactionId); 41 | const receipt = await provider.getTransactionReceipt(transactionId); 42 | 43 | const sentInTransaction = transaction.value.add(receipt.gasUsed.mul(transaction.gasPrice)); 44 | 45 | expect(balance).toEqual(sentInTransaction); 46 | expect((await signer.getBalance()).toNumber()).toEqual(0); 47 | }); 48 | 49 | afterAll(async () => { 50 | await provider.destroy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/chain/MempoolSpace.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosResponse } from 'axios'; 2 | import Logger from '../Logger'; 3 | import { formatError, stringify } from '../Utils'; 4 | 5 | type RecommendedFees = { 6 | fastestFee?: number; 7 | halfHourFee?: number; 8 | hourFee?: number; 9 | minimumFee?: number; 10 | } 11 | 12 | class MempoolSpace { 13 | private static readonly fetchInterval = 30000; 14 | 15 | // Undefined in case the latest request failed 16 | public latestFee?: number; 17 | 18 | private fetchInterval?: any; 19 | 20 | constructor( 21 | private logger: Logger, 22 | private symbol: string, 23 | private apiUrl: string, 24 | ) { 25 | this.logger.info(`Enabling MempoolSpace fee estimations for ${this.symbol}: ${this.apiUrl}`); 26 | } 27 | 28 | public init = async (): Promise => { 29 | await this.fetchRecommendedFees(); 30 | 31 | this.fetchInterval = setInterval(async () => { 32 | await this.fetchRecommendedFees(); 33 | }, MempoolSpace.fetchInterval); 34 | } 35 | 36 | public stop = (): void => { 37 | if (this.fetchInterval) { 38 | clearInterval(this.fetchInterval); 39 | this.fetchInterval = undefined; 40 | } 41 | 42 | this.latestFee = undefined; 43 | } 44 | 45 | private fetchRecommendedFees = async () => { 46 | try { 47 | const response = await Axios.get>(`${this.apiUrl}/v1/fees/recommended`); 48 | 49 | if (typeof response.data.fastestFee !== 'number') { 50 | this.handleCouldNotFetch('invalid response'); 51 | return; 52 | } 53 | 54 | this.logger.silly(`Fetched latest ${this.symbol} MempoolSpace fee estimations: ${stringify(response.data)}`); 55 | 56 | this.latestFee = response.data.fastestFee; 57 | } catch (error) { 58 | this.handleCouldNotFetch(error); 59 | } 60 | } 61 | 62 | private handleCouldNotFetch = (error: any) => { 63 | this.latestFee = undefined; 64 | this.logger.warn(`Could not fetch ${this.symbol} MempoolSpace fee estimations: ${formatError(error)}`); 65 | } 66 | } 67 | 68 | export default MempoolSpace; 69 | -------------------------------------------------------------------------------- /docker/dogecoin-core/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | ARG BERKELEY_VERSION 3 | 4 | FROM boltz/berkeley-db:${BERKELEY_VERSION} AS berkeley-db 5 | 6 | # Build Dogecoin Core 7 | FROM ubuntu:${UBUNTU_VERSION} as dogecoin-core 8 | 9 | ARG VERSION 10 | 11 | COPY --from=berkeley-db /opt /opt 12 | 13 | RUN apt-get update && apt-get -y upgrade 14 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ 15 | wget \ 16 | libtool \ 17 | python3 \ 18 | automake \ 19 | pkg-config \ 20 | libssl-dev \ 21 | libzmq3-dev \ 22 | bsdmainutils \ 23 | libevent-dev \ 24 | autotools-dev \ 25 | build-essential \ 26 | libboost-test-dev \ 27 | libboost-chrono-dev \ 28 | libboost-system-dev \ 29 | libboost-thread-dev \ 30 | libboost-filesystem-dev \ 31 | libboost-program-options-dev 32 | 33 | ENV DOGECOIN_PREFIX=/opt/dogecoin-${VERSION} 34 | 35 | RUN wget https://github.com/dogecoin/dogecoin/archive/v${VERSION}.tar.gz 36 | 37 | RUN tar -xzf *.tar.gz 38 | 39 | WORKDIR /dogecoin-${VERSION} 40 | 41 | RUN ./autogen.sh 42 | RUN ./configure LDFLAGS=-L`ls -d /opt/db*`/lib/ CPPFLAGS=-I`ls -d /opt/db*`/include/ \ 43 | --prefix=${DOGECOIN_PREFIX} \ 44 | --mandir=/usr/share/man \ 45 | --disable-ccache \ 46 | --disable-tests \ 47 | --disable-bench \ 48 | --without-gui \ 49 | --with-daemon \ 50 | --with-utils \ 51 | --with-libs 52 | 53 | RUN make -j$(nproc) 54 | RUN make install 55 | 56 | RUN strip --strip-all ${DOGECOIN_PREFIX}/bin/dogecoind 57 | RUN strip --strip-all ${DOGECOIN_PREFIX}/bin/dogecoin-tx 58 | RUN strip --strip-all ${DOGECOIN_PREFIX}/bin/dogecoin-cli 59 | 60 | # Assemble the final image 61 | FROM ubuntu:${UBUNTU_VERSION} 62 | 63 | ARG VERSION 64 | 65 | RUN apt-get update && apt-get -y upgrade 66 | RUN apt-get -y install \ 67 | openssl \ 68 | libzmq3-dev \ 69 | libevent-dev \ 70 | libboost-chrono-dev \ 71 | libboost-system-dev \ 72 | libboost-thread-dev \ 73 | libboost-filesystem-dev \ 74 | libboost-program-options-dev 75 | 76 | COPY --from=dogecoin-core /opt/dogecoin-${VERSION}/bin /bin 77 | 78 | EXPOSE 22555 44555 18332 79 | 80 | ENTRYPOINT ["dogecoind"] 81 | -------------------------------------------------------------------------------- /docker/litecoin-core/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | ARG BERKELEY_VERSION 3 | 4 | FROM boltz/berkeley-db:${BERKELEY_VERSION} AS berkeley-db 5 | 6 | # Build Litecoin Core 7 | FROM ubuntu:${UBUNTU_VERSION} as litecoin-core 8 | 9 | ARG VERSION 10 | 11 | COPY --from=berkeley-db /opt /opt 12 | 13 | RUN apt-get update && apt-get -y upgrade 14 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ 15 | wget \ 16 | libtool \ 17 | python3 \ 18 | automake \ 19 | pkg-config \ 20 | libssl-dev \ 21 | libzmq3-dev \ 22 | bsdmainutils \ 23 | libevent-dev \ 24 | autotools-dev \ 25 | build-essential \ 26 | libboost-test-dev \ 27 | libboost-chrono-dev \ 28 | libboost-system-dev \ 29 | libboost-thread-dev \ 30 | libboost-filesystem-dev 31 | 32 | ENV LITECOIN_PREFIX=/opt/litecoin-${VERSION} 33 | 34 | RUN wget https://github.com/litecoin-project/litecoin/archive/v${VERSION}.tar.gz 35 | 36 | RUN tar -xzf *.tar.gz 37 | 38 | WORKDIR /litecoin-${VERSION} 39 | 40 | RUN ./autogen.sh 41 | RUN ./configure LDFLAGS=-L`ls -d /opt/db*`/lib/ CPPFLAGS=-I`ls -d /opt/db*`/include/ \ 42 | --prefix=${LITECOIN_PREFIX} \ 43 | --mandir=/usr/share/man \ 44 | --disable-ccache \ 45 | --disable-tests \ 46 | --disable-bench \ 47 | --without-gui \ 48 | --with-daemon \ 49 | --with-utils \ 50 | --with-libs 51 | 52 | RUN make -j$(nproc) 53 | RUN make install 54 | 55 | RUN strip --strip-all ${LITECOIN_PREFIX}/bin/litecoind 56 | RUN strip --strip-all ${LITECOIN_PREFIX}/bin/litecoin-tx 57 | RUN strip --strip-all ${LITECOIN_PREFIX}/bin/litecoin-cli 58 | RUN strip --strip-all ${LITECOIN_PREFIX}/bin/litecoin-wallet 59 | 60 | # Assemble the final image 61 | FROM ubuntu:${UBUNTU_VERSION} 62 | 63 | ARG VERSION 64 | 65 | RUN apt-get update && apt-get -y upgrade 66 | RUN apt-get -y install \ 67 | openssl \ 68 | libzmq3-dev \ 69 | libevent-dev \ 70 | libboost-chrono-dev \ 71 | libboost-system-dev \ 72 | libboost-thread-dev \ 73 | libboost-filesystem-dev 74 | 75 | COPY --from=litecoin-core /opt/litecoin-${VERSION}/bin /bin 76 | 77 | EXPOSE 19332 19333 19444 19443 78 | 79 | ENTRYPOINT ["litecoind"] 80 | -------------------------------------------------------------------------------- /lib/cli/stacks/StacksUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { join } from 'path'; 3 | import { ContractABIs } from 'boltz-core'; 4 | import { existsSync, readFileSync } from 'fs'; 5 | import { ERC20 } from 'boltz-core/typechain/ERC20'; 6 | import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; 7 | import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; 8 | import { Signer, providers, Wallet, Contract } from 'ethers'; 9 | 10 | const Constants = { 11 | erc20TokenAddress: '0x9f84F92d952f90027618089F6F2a3481f1a3fa0F', 12 | rbtcSwapAddress: '0x4efc8b4323e532db6cd78d70a97f83bc7559cef3', 13 | erc20SwapAddress: '0x3b15af794d4e39589a31089ce0b53a9e1994930f', 14 | stxSwapAddress: 'STR187KT73T0A8M0DEWDX06TJR2B8WM0WP9VGZY3.stxswap_v3' 15 | }; 16 | 17 | const connectEthereum = (providerUrl: string, signerAddress: string): Signer => { 18 | const provider = new providers.JsonRpcProvider(providerUrl); 19 | console.log('rsk connectEthereum signerAddress: ', signerAddress); 20 | return provider.getSigner(signerAddress); 21 | }; 22 | 23 | const getContracts = (signer: Signer): { token: ERC20, etherSwap: EtherSwap, erc20Swap: ERC20Swap } => { 24 | return { 25 | token: new Contract( 26 | Constants.erc20TokenAddress, 27 | ContractABIs.ERC20, 28 | signer, 29 | ) as any as ERC20, 30 | 31 | etherSwap: new Contract( 32 | Constants.rbtcSwapAddress, 33 | ContractABIs.EtherSwap, 34 | signer, 35 | ) as any as EtherSwap, 36 | erc20Swap: new Contract( 37 | Constants.erc20SwapAddress, 38 | ContractABIs.ERC20Swap, 39 | signer, 40 | ) as any as ERC20Swap, 41 | }; 42 | }; 43 | 44 | const getBoltzAddress = async (): Promise => { 45 | const filePath = join(process.env.HOME!, '.boltz/seed.dat'); 46 | 47 | if (existsSync(filePath)) { 48 | return Wallet.fromMnemonic(readFileSync( 49 | filePath, 50 | { 51 | encoding: 'utf-8', 52 | }, 53 | )).getAddress(); 54 | } 55 | 56 | return; 57 | }; 58 | 59 | export { 60 | Constants, 61 | getContracts, 62 | connectEthereum, 63 | getBoltzAddress, 64 | }; 65 | -------------------------------------------------------------------------------- /lib/db/ChannelCreationRepository.ts: -------------------------------------------------------------------------------- 1 | import { WhereOptions } from 'sequelize'; 2 | import { ChannelCreationStatus } from '../consts/Enums'; 3 | import ChannelCreation, { ChannelCreationType } from './models/ChannelCreation'; 4 | 5 | class ChannelCreationRepository { 6 | public getChannelCreation = (options?: WhereOptions): Promise => { 7 | return ChannelCreation.findOne({ 8 | where: options, 9 | }); 10 | } 11 | 12 | public getChannelCreations = (options?: WhereOptions): Promise => { 13 | return ChannelCreation.findAll({ 14 | where: options, 15 | }); 16 | } 17 | 18 | public addChannelCreation = (channelCreation: ChannelCreationType): Promise => { 19 | return ChannelCreation.create(channelCreation); 20 | } 21 | 22 | public setAttempted = (channelCreation: ChannelCreation): Promise => { 23 | return channelCreation.update({ 24 | status: ChannelCreationStatus.Attempted, 25 | }); 26 | } 27 | 28 | public setFundingTransaction = ( 29 | channelCreation: ChannelCreation, 30 | fundingTransactionId: string, 31 | fundingTransactionVout: number, 32 | ): Promise => { 33 | return channelCreation.update({ 34 | fundingTransactionId, 35 | fundingTransactionVout, 36 | status: ChannelCreationStatus.Created, 37 | }); 38 | } 39 | 40 | public setNodePublicKey = (channelCreation: ChannelCreation, nodePublicKey: string): Promise => { 41 | return channelCreation.update({ 42 | nodePublicKey, 43 | }); 44 | } 45 | 46 | public setSettled = (channelCreation: ChannelCreation): Promise => { 47 | return channelCreation.update({ 48 | status: ChannelCreationStatus.Settled, 49 | }); 50 | } 51 | 52 | public setAbandoned = (channelCreation: ChannelCreation): Promise => { 53 | return channelCreation.update({ 54 | status: ChannelCreationStatus.Abandoned, 55 | }); 56 | } 57 | 58 | public dropTable = (): Promise => { 59 | return ChannelCreation.drop(); 60 | } 61 | } 62 | 63 | export default ChannelCreationRepository; 64 | -------------------------------------------------------------------------------- /test/integration/wallet/EthereumTools.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { ContractABIs } from 'boltz-core'; 3 | import { ERC20 } from 'boltz-core/typechain/ERC20'; 4 | import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; 5 | import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; 6 | import { BigNumber, Contract, providers, Signer, Wallet } from 'ethers'; 7 | 8 | export const getSigner = (): { provider: providers.WebSocketProvider, signer: Signer, etherBase: Signer } => { 9 | const provider = new providers.WebSocketProvider('http://127.0.0.1:8546'); 10 | 11 | return { 12 | provider, 13 | signer: Wallet.createRandom().connect(provider), 14 | etherBase: provider.getSigner(0), 15 | }; 16 | }; 17 | 18 | export const getTokenContract = (signer: Signer): ERC20 => { 19 | return new Contract('0x23d5395De7862A174b0cCbf9f7A350b4b8afC720', ContractABIs.ERC20, signer) as any as ERC20; 20 | }; 21 | 22 | export const getSwapContracts = (signer: Signer): { etherSwap: EtherSwap, erc20Swap: ERC20Swap } => { 23 | return { 24 | etherSwap: new Contract('0xCc2dd65Bc10AdbE3C6cF341aaFB5d9440E72cc20', ContractABIs.EtherSwap, signer) as any as EtherSwap, 25 | erc20Swap: new Contract('0xf90664Edb44fd966089673f289834DaDb9888203', ContractABIs.ERC20Swap, signer) as any as ERC20Swap, 26 | }; 27 | }; 28 | 29 | export const fundSignerWallet = async (signer: Signer, etherBase: Signer, token?: ERC20): Promise => { 30 | const signerAddress = await signer.getAddress(); 31 | 32 | const etherFundingTransaction = await etherBase.sendTransaction({ 33 | to: signerAddress, 34 | value: BigNumber.from(10).pow(18), 35 | }); 36 | 37 | await etherFundingTransaction.wait(1); 38 | 39 | if (token) { 40 | const tokenFundingTransaction = await token.connect(etherBase).transfer( 41 | signerAddress, 42 | BigNumber.from(10).pow(18), 43 | ); 44 | 45 | await tokenFundingTransaction.wait(1); 46 | } 47 | }; 48 | 49 | export const waitForTransactionHash = async (provider: providers.WebSocketProvider, transactionHash: string): Promise => { 50 | const transaction = await provider.getTransaction(transactionHash); 51 | await transaction.wait(1); 52 | }; 53 | -------------------------------------------------------------------------------- /test/Utils.ts: -------------------------------------------------------------------------------- 1 | import { OutputType, Networks, Scripts } from 'boltz-core'; 2 | import { TransactionInput } from 'bip174/src/lib/interfaces'; 3 | import { ECPair, address, crypto, Psbt, Transaction } from 'bitcoinjs-lib'; 4 | import { getPubkeyHashFunction } from '../lib/Utils'; 5 | 6 | export const randomRange = (max: number): number => { 7 | return Math.floor(Math.random() * Math.floor(max)); 8 | }; 9 | 10 | export const wait = (ms: number): Promise => { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | }; 13 | 14 | export const waitForFunctionToBeTrue = (func: () => boolean): Promise => { 15 | return new Promise((resolve) => { 16 | const interval = setInterval(() => { 17 | if (func()) { 18 | clearInterval(interval); 19 | resolve(); 20 | } 21 | }, 25); 22 | }); 23 | }; 24 | 25 | export const generateAddress = (outputType: OutputType): { outputScript: Buffer, address: string } => { 26 | const keys = ECPair.makeRandom({ network: Networks.bitcoinRegtest }); 27 | const encodeFunction = getPubkeyHashFunction(outputType); 28 | 29 | const outputScript = encodeFunction(crypto.hash160(keys.publicKey)) as Buffer; 30 | 31 | return { 32 | outputScript, 33 | address: address.fromOutputScript(outputScript, Networks.bitcoinRegtest), 34 | }; 35 | }; 36 | 37 | export const constructTransaction = (rbf: boolean, input: string, outputAmount = 1): Transaction => { 38 | const { outputScript } = generateAddress(OutputType.Bech32); 39 | const keys = ECPair.makeRandom({ network: Networks.bitcoinRegtest }); 40 | 41 | const psbt = new Psbt({ 42 | network: Networks.bitcoinRegtest, 43 | }); 44 | 45 | psbt.addInput({ 46 | hash: input, 47 | index: 0, 48 | sequence: rbf ? 0xfffffffd : 0xffffffff, 49 | witnessUtxo: { 50 | value: outputAmount + 1, 51 | script: Scripts.p2wpkhOutput(crypto.hash160(keys.publicKey)), 52 | }, 53 | } as any as TransactionInput); 54 | psbt.addOutput({ 55 | script: outputScript, 56 | value: outputAmount, 57 | }); 58 | 59 | psbt.signInput(0, keys); 60 | 61 | psbt.validateSignaturesOfAllInputs(); 62 | psbt.finalizeAllInputs(); 63 | 64 | return psbt.extractTransaction(); 65 | }; 66 | -------------------------------------------------------------------------------- /lib/cli/ethereum/commands/Lock.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | import { BigNumber, ContractTransaction } from 'ethers'; 3 | import { getHexBuffer } from '../../../Utils'; 4 | import { etherDecimals } from '../../../consts/Consts'; 5 | import BuilderComponents from '../../BuilderComponents'; 6 | import { Constants, connectEthereum, getContracts, getBoltzAddress } from '../EthereumUtils'; 7 | 8 | export const command = 'lock [token]'; 9 | 10 | export const describe = 'locks Ether or a ERC20 token in the corresponding swap contract'; 11 | 12 | export const builder = { 13 | preimageHash: { 14 | describe: 'preimage hash with which the funds should be locked', 15 | type: 'string', 16 | }, 17 | amount: { 18 | describe: 'amount of tokens that should be locked up', 19 | type: 'number', 20 | }, 21 | timelock: { 22 | describe: 'timelock delta in blocks', 23 | type: 'number', 24 | }, 25 | token: BuilderComponents.token, 26 | }; 27 | 28 | export const handler = async (argv: Arguments): Promise => { 29 | const signer = connectEthereum(argv.provider, argv.signer); 30 | const { etherSwap, erc20Swap, token } = getContracts(signer); 31 | 32 | const preimageHash = getHexBuffer(argv.preimageHash); 33 | const amount = BigNumber.from(argv.amount).mul(etherDecimals); 34 | 35 | const boltzAddress = await getBoltzAddress(); 36 | 37 | if (boltzAddress === undefined) { 38 | console.log('Could not lock coins because the address of Boltz could not be queried'); 39 | return; 40 | } 41 | 42 | let transaction: ContractTransaction; 43 | 44 | if (argv.token) { 45 | await token.approve(Constants.erc20SwapAddress, amount); 46 | 47 | transaction = await erc20Swap.lock( 48 | preimageHash, 49 | amount, 50 | Constants.erc20TokenAddress, 51 | boltzAddress, 52 | argv.timelock, 53 | ); 54 | } else { 55 | transaction = await etherSwap.lock( 56 | preimageHash, 57 | boltzAddress, 58 | argv.timelock, 59 | { 60 | value: amount, 61 | }, 62 | ); 63 | } 64 | 65 | await transaction.wait(1); 66 | 67 | console.log(`Sent ${argv.token ? 'ERC20 token' : 'Ether'} in: ${transaction.hash}`); 68 | }; 69 | -------------------------------------------------------------------------------- /docker/regtest/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source utils.sh 4 | 5 | function waitForLndToSync () { 6 | while true; do 7 | if $1 getinfo 2>&1 | grep synced_to_chain.*true > /dev/null 2>&1; then 8 | break 9 | fi 10 | sleep 1 11 | done 12 | 13 | sleep 5 14 | } 15 | 16 | function openChannel () { 17 | nodeAddress=$($1 getnewaddress) 18 | lndAddress=$($2 newaddress p2wkh | jq -r '.address') 19 | 20 | $1 sendtoaddress ${lndAddress} 10 > /dev/null 21 | 22 | $1 generatetoaddress 1 ${nodeAddress} > /dev/null 23 | 24 | lnd2Pubkey=$($3 --network=regtest getinfo | jq -r '.identity_pubkey') 25 | 26 | waitForLndToSync "$2" 27 | 28 | $2 connect ${lnd2Pubkey}@127.0.0.1:$4 > /dev/null 29 | $2 openchannel --node_key ${lnd2Pubkey} --local_amt 100000000 --push_amt 50000000 > /dev/null 30 | 31 | $1 generatetoaddress 6 ${nodeAddress} > /dev/null 32 | 33 | while true; do 34 | numActiveChannels="$($2 getinfo | jq -r ".num_active_channels")" 35 | 36 | if [[ ${numActiveChannels} == "1" ]]; then 37 | break 38 | fi 39 | sleep 1 40 | done 41 | } 42 | 43 | startNodes 44 | 45 | bitcoin-cli createwallet $DEFAULT_WALLET_NAME > /dev/null 46 | 47 | # Mine 101 blocks so that the coinbase of the first block is spendable 48 | bitcoinAddress=$(bitcoin-cli getnewaddress) 49 | litecoinAddress=$(litecoin-cli getnewaddress) 50 | 51 | bitcoin-cli generatetoaddress 101 ${bitcoinAddress} > /dev/null 52 | litecoin-cli generatetoaddress 101 ${litecoinAddress} > /dev/null 53 | 54 | echo "Restarting nodes" 55 | 56 | stopNodes 57 | 58 | sleep 5 59 | 60 | startNodes 61 | bitcoin-cli loadwallet $DEFAULT_WALLET_NAME > /dev/null 62 | 63 | startLnds 64 | 65 | echo "Opening BTC channel" 66 | openChannel bitcoin-cli \ 67 | "lncli --lnddir=/root/.lnd-btc --rpcserver=127.0.0.1:10009 --network=regtest" \ 68 | "lncli --lnddir=/root/.lnd-btc --rpcserver=127.0.0.1:10011 --network=regtest" \ 69 | 9736 70 | 71 | echo "Opening LTC channel" 72 | openChannel litecoin-cli \ 73 | "lncli --lnddir=/root/.lnd-ltc --rpcserver=127.0.0.1:11009 --chain=litecoin --network=regtest" \ 74 | "lncli --lnddir=/root/.lnd-ltc2 --rpcserver=127.0.0.1:11010 --chain=litecoin --network=regtest" \ 75 | 10736 76 | 77 | stopLnds 78 | stopNodes 79 | 80 | sleep 5 81 | -------------------------------------------------------------------------------- /lib/wallet/providers/RskWalletProvider.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Signer } from 'ethers'; 2 | import Logger from '../../Logger'; 3 | import { etherDecimals } from '../../consts/Consts'; 4 | import { getGasPrice } from '../ethereum/EthereumUtils'; 5 | import WalletProviderInterface, { SentTransaction, WalletBalance } from './WalletProviderInterface'; 6 | 7 | class RskWalletProvider implements WalletProviderInterface { 8 | public readonly symbol: string; 9 | 10 | // The gas needed for sending Rsk is 21000? 11 | private readonly ethTransferGas = BigNumber.from(21000); 12 | 13 | constructor(private logger: Logger, private signer: Signer) { 14 | this.symbol = 'RBTC'; 15 | this.logger.info('Initialized Rsk wallet'); 16 | } 17 | 18 | public getAddress = (): Promise => { 19 | return this.signer.getAddress(); 20 | } 21 | 22 | public getBalance = async (): Promise => { 23 | const balance = (await this.signer.getBalance()).div(etherDecimals).toNumber(); 24 | 25 | return { 26 | totalBalance: balance, 27 | confirmedBalance: balance, 28 | unconfirmedBalance: 0, 29 | }; 30 | } 31 | 32 | public sendToAddress = async (address: string, amount: number, gasPrice?: number): Promise => { 33 | const transaction = await this.signer.sendTransaction({ 34 | to: address, 35 | value: BigNumber.from(amount).mul(etherDecimals), 36 | gasPrice: await getGasPrice(this.signer.provider!, gasPrice), 37 | }); 38 | 39 | return { 40 | transactionId: transaction.hash, 41 | }; 42 | } 43 | 44 | public sweepWallet = async (address: string, gasPrice?: number): Promise => { 45 | const balance = await this.signer.getBalance(); 46 | 47 | const actualGasPrice = await getGasPrice(this.signer.provider!, gasPrice); 48 | const gasCost = this.ethTransferGas.mul(actualGasPrice); 49 | 50 | const value = balance.sub(gasCost); 51 | 52 | const transaction = await this.signer.sendTransaction({ 53 | value, 54 | to: address, 55 | gasPrice: actualGasPrice, 56 | gasLimit: this.ethTransferGas, 57 | }); 58 | 59 | return { 60 | transactionId: transaction.hash, 61 | }; 62 | } 63 | } 64 | 65 | export default RskWalletProvider; 66 | -------------------------------------------------------------------------------- /docker-compose/lnstx/boltz.conf: -------------------------------------------------------------------------------- 1 | [[pairs]] 2 | base = "BTC" 3 | quote = "STX" 4 | fee = 5 5 | timeoutDelta = 1_240 6 | 7 | [[pairs]] 8 | base = "BTC" 9 | quote = "USDA" 10 | fee = 5 11 | timeoutDelta = 1_240 12 | 13 | [[pairs]] 14 | base = "BTC" 15 | quote = "XUSD" 16 | fee = 5 17 | timeoutDelta = 1_240 18 | 19 | [[currencies]] 20 | symbol = "BTC" 21 | network = "bitcoinRegtest" 22 | minWalletBalance = 10_000_000 23 | minChannelBalance = 10_000_000 24 | maxSwapAmount = 4_294_967 25 | minSwapAmount = 10_000 26 | maxZeroConfAmount = 10_000_000 27 | 28 | [currencies.chain] 29 | host = "127.0.0.1" 30 | port = 18_443 31 | cookie = "docker/regtest/data/core/cookies/.bitcoin-cookie" 32 | rpcuser = "kek" 33 | rpcpass = "kek" 34 | 35 | [currencies.lnd] 36 | host = "127.0.0.1" 37 | port = 10_009 38 | certpath = "docker/regtest/data/lnd/certificates/tls.cert" 39 | macaroonpath = "docker/regtest/data/lnd/macaroons/admin.macaroon" 40 | 41 | [stacks] 42 | # mainnet 43 | # providerEndpoint = "https://stacks-node-api.mainnet.stacks.co" 44 | # stxSwapAddress = "ST15RGYVK9ACFQWMFFA2TVASDVZH38B4VAV4WF6BJ.stxswap_v6" 45 | 46 | # testnet 47 | # providerEndpoint = "https://stacks-node-api.testnet.stacks.co" 48 | # stxSwapAddress = "ST15RGYVK9ACFQWMFFA2TVASDVZH38B4VAV4WF6BJ.stxswap_v3" 49 | 50 | # mocknet 51 | providerEndpoint = "http://localhost:3999" 52 | stxSwapAddress = "ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK.stxswap_v10" 53 | sip10SwapAddress = "ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK.sip10swap_v3" 54 | 55 | [[stacks.tokens]] 56 | symbol = "STX" 57 | 58 | maxSwapAmount = 1_294_967000 59 | minSwapAmount = 10000 60 | 61 | [[stacks.tokens]] 62 | symbol = "USDA" 63 | 64 | maxSwapAmount = 1_294_967000 65 | minSwapAmount = 10000 66 | # mocknet 67 | contractAddress = "ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK.usda-token" 68 | # mainnet 69 | # contractAddress = "SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.usda-token" 70 | decimals = 6 71 | 72 | [[stacks.tokens]] 73 | symbol = "XUSD" 74 | 75 | maxSwapAmount = 8_294_967000 76 | minSwapAmount = 10000 77 | # mocknet 78 | contractAddress = "ST30VXWG00R13WK8RDXBSTHXNWGNKCAQTRYEMA9FK.Wrapped-USD" 79 | # mainnet 80 | # contractAddress = "SP2TZK01NKDC89J6TA56SA47SDF7RTHYEQ79AAB9A.Wrapped-USD" 81 | decimals = 8 -------------------------------------------------------------------------------- /lib/wallet/providers/EtherWalletProvider.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Signer } from 'ethers'; 2 | import Logger from '../../Logger'; 3 | import { etherDecimals } from '../../consts/Consts'; 4 | import { getGasPrice } from '../ethereum/EthereumUtils'; 5 | import WalletProviderInterface, { SentTransaction, WalletBalance } from './WalletProviderInterface'; 6 | 7 | class EtherWalletProvider implements WalletProviderInterface { 8 | public readonly symbol: string; 9 | 10 | // The gas needed for sending Ether is 21000 11 | private readonly ethTransferGas = BigNumber.from(21000); 12 | 13 | constructor(private logger: Logger, private signer: Signer) { 14 | this.symbol = 'ETH'; 15 | this.logger.info('Initialized Ether wallet'); 16 | } 17 | 18 | public getAddress = (): Promise => { 19 | return this.signer.getAddress(); 20 | } 21 | 22 | public getBalance = async (): Promise => { 23 | const balance = (await this.signer.getBalance()).div(etherDecimals).toNumber(); 24 | 25 | return { 26 | totalBalance: balance, 27 | confirmedBalance: balance, 28 | unconfirmedBalance: 0, 29 | }; 30 | } 31 | 32 | public sendToAddress = async (address: string, amount: number, gasPrice?: number): Promise => { 33 | const transaction = await this.signer.sendTransaction({ 34 | to: address, 35 | value: BigNumber.from(amount).mul(etherDecimals), 36 | gasPrice: await getGasPrice(this.signer.provider!, gasPrice), 37 | }); 38 | 39 | return { 40 | transactionId: transaction.hash, 41 | }; 42 | } 43 | 44 | public sweepWallet = async (address: string, gasPrice?: number): Promise => { 45 | const balance = await this.signer.getBalance(); 46 | 47 | const actualGasPrice = await getGasPrice(this.signer.provider!, gasPrice); 48 | const gasCost = this.ethTransferGas.mul(actualGasPrice); 49 | 50 | const value = balance.sub(gasCost); 51 | 52 | const transaction = await this.signer.sendTransaction({ 53 | value, 54 | to: address, 55 | gasPrice: actualGasPrice, 56 | gasLimit: this.ethTransferGas, 57 | }); 58 | 59 | return { 60 | transactionId: transaction.hash, 61 | }; 62 | } 63 | } 64 | 65 | export default EtherWalletProvider; 66 | -------------------------------------------------------------------------------- /test/unit/notifications/OtpManager.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { authenticator } from 'otplib'; 3 | import { unlinkSync, existsSync, readFileSync } from 'fs'; 4 | import Logger from '../../../lib/Logger'; 5 | import { NotificationConfig } from '../../../lib/Config'; 6 | import OtpManager from '../../../lib/notifications/OtpManager'; 7 | 8 | const config: NotificationConfig = { 9 | prefix: 'prefix', 10 | otpsecretpath: `${__dirname}/otpSecret.dat`, 11 | 12 | token: '', 13 | channel: '', 14 | interval: 0, 15 | }; 16 | 17 | const uriFilePath = join(__dirname, OtpManager['uriFile']); 18 | 19 | const clearFiles = () => { 20 | if (existsSync(uriFilePath)) { 21 | unlinkSync(uriFilePath); 22 | } 23 | 24 | if (existsSync(config.otpsecretpath)) { 25 | unlinkSync(config.otpsecretpath); 26 | } 27 | }; 28 | 29 | describe('OtpManager', () => { 30 | let secret: string; 31 | 32 | let otpManager: OtpManager; 33 | 34 | beforeAll(() => { 35 | clearFiles(); 36 | }); 37 | 38 | test('should create OTP secrets', () => { 39 | otpManager = new OtpManager(Logger.disabledLogger, config); 40 | secret = otpManager['secret']; 41 | 42 | expect(existsSync(config.otpsecretpath)).toBeTruthy(); 43 | expect(existsSync(uriFilePath)).toBeTruthy(); 44 | 45 | expect(readFileSync(config.otpsecretpath, { encoding: 'utf8' })).toEqual(secret); 46 | 47 | const uriFile = readFileSync(uriFilePath, { encoding: 'utf8' }); 48 | expect(uriFile.includes(secret)).toBeTruthy(); 49 | }); 50 | 51 | test('should load OTP secrets from file', () => { 52 | otpManager = new OtpManager(Logger.disabledLogger, config); 53 | 54 | expect(otpManager['secret']).toBeTruthy(); 55 | }); 56 | 57 | test('should verify OTP tokens', () => { 58 | const token = authenticator.generate(secret); 59 | expect(otpManager.verify(token)).toBeTruthy(); 60 | }); 61 | 62 | test('should not accept the same OTP token twice', () => { 63 | const token = authenticator.generate(secret); 64 | expect(otpManager.verify(token)).toBeFalsy(); 65 | }); 66 | 67 | test('should not throw when checking invalid tokens', () => { 68 | expect(otpManager.verify('invalid')).toBeFalsy(); 69 | }); 70 | 71 | afterAll(() => { 72 | clearFiles(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /lib/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { red, yellow, green, cyan, blue, magenta } from 'colors/safe'; 3 | import { getTsString } from './Utils'; 4 | 5 | class Logger { 6 | public static readonly disabledLogger = new Logger('', undefined, true); 7 | 8 | constructor(level: string, filename?: string, private disabled = false) { 9 | if (disabled) { 10 | return; 11 | } 12 | 13 | const transports: winston.transport[] = [ 14 | new winston.transports.Console({ 15 | format: this.getLogFormat(true), 16 | }), 17 | ]; 18 | 19 | if (filename) { 20 | transports.push(new winston.transports.File({ 21 | filename, 22 | format: this.getLogFormat(false), 23 | })); 24 | } 25 | 26 | winston.configure({ 27 | level, 28 | transports, 29 | }); 30 | } 31 | 32 | private getLogFormat = (colorize: boolean) => { 33 | return winston.format.printf(info => `${getTsString()} ${this.getLevel(info.level, colorize)}: ${info.message}`); 34 | } 35 | 36 | private getLevel = (level: string, colorize: boolean) => { 37 | if (colorize) { 38 | switch (level) { 39 | case 'error': return red(level); 40 | case 'warn': return yellow(level); 41 | case 'info': return green(level); 42 | case 'verbose': return cyan(level); 43 | case 'debug': return blue(level); 44 | case 'silly': return magenta(level); 45 | } 46 | } 47 | return level; 48 | } 49 | 50 | public error = (message: string): void => { 51 | this.log('error', message + '\n'); 52 | } 53 | 54 | public warn = (message: string): void => { 55 | this.log('warn', message); 56 | } 57 | 58 | public info = (message: string): void => { 59 | this.log('info', message); 60 | } 61 | 62 | public verbose = (message: string): void => { 63 | this.log('verbose', message + '\n'); 64 | } 65 | 66 | public debug = (message: string): void => { 67 | this.log('debug', message + '\n'); 68 | } 69 | 70 | public silly = (message: string): void => { 71 | this.log('silly', message); 72 | } 73 | 74 | private log = (level: string, message: string) => { 75 | if (!this.disabled) { 76 | winston.log(level, message); 77 | } 78 | } 79 | } 80 | 81 | export default Logger; 82 | -------------------------------------------------------------------------------- /lib/db/models/Client.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | // import { PairType } from './Pair'; 3 | // import { PairType } from '../../rates/RateProvider'; 4 | 5 | type ClientType = { 6 | id: string; 7 | stacksAddress: string; 8 | nodeId: string; 9 | url: string; 10 | // pairs: Map; 11 | pairs: string; 12 | success: number; 13 | fail: number; 14 | 15 | localLNBalance?: number; 16 | remoteLNBalance?: number; 17 | onchainBalance?: number; 18 | StxBalance?: number; 19 | tokenBalances?: string; 20 | }; 21 | 22 | class Client extends Model implements ClientType { 23 | public id!: string; 24 | public stacksAddress!: string; 25 | public nodeId!: string; 26 | public url!: string; 27 | // public pairs!: Map; 28 | public pairs!: string; 29 | public success!: number; 30 | public fail!: number; 31 | 32 | public localLNBalance?: number; 33 | public remoteLNBalance?: number; 34 | public onchainBalance?: number; 35 | public StxBalance?: number; 36 | public tokenBalances?: string; 37 | 38 | // public createdAt!: Date; 39 | // public updatedAt!: Date; 40 | 41 | public static load = (sequelize: Sequelize): void => { 42 | Client.init({ 43 | id: { type: new DataTypes.STRING(255), primaryKey: true }, 44 | stacksAddress: { type: new DataTypes.STRING(255), allowNull: false }, 45 | nodeId: { type: new DataTypes.STRING(255), allowNull: false }, 46 | url: { type: new DataTypes.STRING(255), allowNull: false }, 47 | pairs: { type: new DataTypes.STRING(1255), allowNull: false }, 48 | success: { type: new DataTypes.INTEGER(), allowNull: true }, 49 | fail: { type: new DataTypes.INTEGER(), allowNull: true }, 50 | localLNBalance: { type: new DataTypes.INTEGER(), allowNull: true }, 51 | remoteLNBalance: { type: new DataTypes.INTEGER(), allowNull: true }, 52 | onchainBalance: { type: new DataTypes.INTEGER(), allowNull: true }, 53 | StxBalance: { type: new DataTypes.INTEGER(), allowNull: true }, 54 | tokenBalances: { type: new DataTypes.STRING(1255), allowNull: true }, 55 | }, { 56 | sequelize, 57 | tableName: 'clients', 58 | timestamps: true, 59 | }); 60 | } 61 | } 62 | 63 | export default Client; 64 | export { ClientType }; 65 | -------------------------------------------------------------------------------- /test/unit/db/Migration.spec.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import Logger from '../../../lib/Logger'; 3 | import Migration from '../../../lib/db/Migration'; 4 | import DatabaseVersion from '../../../lib/db/models/DatabaseVersion'; 5 | 6 | const MockedSequelize = jest.fn().mockImplementation(() => {}); 7 | 8 | jest.mock('../../../lib/db/models/DatabaseVersion'); 9 | 10 | DatabaseVersion.sync = jest.fn().mockResolvedValue(undefined); 11 | 12 | let mockGetVersionResult: any = undefined; 13 | const mockGetVersion = jest.fn().mockImplementation(async () => { 14 | return mockGetVersionResult; 15 | }); 16 | 17 | const mockCreateVersion = jest.fn().mockResolvedValue(undefined); 18 | 19 | jest.mock('../../../lib/db/DatabaseVersionRepository', () => { 20 | return jest.fn().mockImplementation(() => { 21 | return { 22 | getVersion: mockGetVersion, 23 | createVersion: mockCreateVersion, 24 | }; 25 | }); 26 | }); 27 | 28 | describe('Migration', () => { 29 | const emptyCurrenciesMap = new Map(); 30 | 31 | const migration = new Migration(Logger.disabledLogger, MockedSequelize); 32 | 33 | beforeEach(() => { 34 | mockGetVersionResult = undefined; 35 | 36 | jest.clearAllMocks(); 37 | }); 38 | 39 | test('should insert the latest database schema version in case there none already', async () => { 40 | await migration.migrate(emptyCurrenciesMap); 41 | 42 | expect(mockGetVersion).toHaveBeenCalledTimes(1); 43 | 44 | expect(mockCreateVersion).toHaveBeenCalledTimes(1); 45 | expect(mockCreateVersion).toHaveBeenCalledWith(Migration['latestSchemaVersion']); 46 | }); 47 | 48 | test('should do nothing in case the database has already the latest schema version', async () => { 49 | mockGetVersionResult = { 50 | version: Migration['latestSchemaVersion'], 51 | }; 52 | 53 | await migration.migrate(emptyCurrenciesMap); 54 | 55 | expect(mockGetVersion).toHaveBeenCalledTimes(1); 56 | }); 57 | 58 | test('should throw when there is an unexpected database schema version', async () => { 59 | mockGetVersionResult = { 60 | version: -42, 61 | }; 62 | 63 | await expect(migration.migrate(emptyCurrenciesMap)).rejects.toEqual(`found unexpected database version ${mockGetVersionResult.version}`); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /docker/bitcoin-core/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION 2 | ARG BERKELEY_VERSION 3 | 4 | FROM boltz/berkeley-db:${BERKELEY_VERSION} AS berkeley-db 5 | 6 | # Build Bitcoin Core 7 | FROM ubuntu:${UBUNTU_VERSION} AS bitcoin-core 8 | 9 | ARG VERSION 10 | 11 | COPY --from=berkeley-db /opt /opt 12 | 13 | RUN apt-get update && apt-get -y upgrade 14 | RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \ 15 | wget \ 16 | libtool \ 17 | python3 \ 18 | automake \ 19 | pkg-config \ 20 | libzmq3-dev \ 21 | bsdmainutils \ 22 | libevent-dev \ 23 | autotools-dev \ 24 | build-essential \ 25 | libboost-test-dev \ 26 | libboost-chrono-dev \ 27 | libboost-system-dev \ 28 | libboost-thread-dev \ 29 | libboost-filesystem-dev 30 | 31 | RUN gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "90C8019E36C2E964" 32 | 33 | ENV BITCOIN_PREFIX=/opt/bitcoin-${VERSION} 34 | 35 | RUN wget https://bitcoincore.org/bin/bitcoin-core-${VERSION}/SHA256SUMS.asc 36 | RUN wget https://bitcoincore.org/bin/bitcoin-core-${VERSION}/bitcoin-${VERSION}.tar.gz 37 | 38 | RUN gpg --verify SHA256SUMS.asc 39 | RUN grep "bitcoin-${VERSION}.tar.gz\$" SHA256SUMS.asc | sha256sum -c - 40 | RUN tar -xzf *.tar.gz 41 | 42 | WORKDIR /bitcoin-${VERSION} 43 | 44 | RUN ./autogen.sh 45 | RUN ./configure LDFLAGS=-L`ls -d /opt/db*`/lib/ CPPFLAGS=-I`ls -d /opt/db*`/include/ \ 46 | --prefix=${BITCOIN_PREFIX} \ 47 | --enable-endomorphism \ 48 | --mandir=/usr/share/man \ 49 | --disable-ccache \ 50 | --disable-tests \ 51 | --disable-bench \ 52 | --without-gui \ 53 | --with-daemon \ 54 | --with-utils \ 55 | --with-libs 56 | 57 | RUN make -j$(nproc) 58 | RUN make install 59 | 60 | RUN strip --strip-all ${BITCOIN_PREFIX}/bin/bitcoind 61 | RUN strip --strip-all ${BITCOIN_PREFIX}/bin/bitcoin-tx 62 | RUN strip --strip-all ${BITCOIN_PREFIX}/bin/bitcoin-cli 63 | RUN strip --strip-all ${BITCOIN_PREFIX}/bin/bitcoin-wallet 64 | 65 | # Assemble the final image 66 | FROM ubuntu:${UBUNTU_VERSION} 67 | 68 | ARG VERSION 69 | 70 | RUN apt-get update && apt-get -y upgrade 71 | RUN apt-get -y install \ 72 | libzmq3-dev \ 73 | libevent-dev \ 74 | libboost-chrono-dev \ 75 | libboost-system-dev \ 76 | libboost-thread-dev \ 77 | libboost-filesystem-dev 78 | 79 | COPY --from=bitcoin-core /opt/bitcoin-${VERSION}/bin /bin 80 | 81 | EXPOSE 18332 18333 18444 18443 82 | 83 | ENTRYPOINT ["bitcoind"] 84 | -------------------------------------------------------------------------------- /docker/regtest/scripts/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_WALLET_NAME="default" 4 | export DEFAULT_WALLET_NAME 5 | 6 | function waitForNode () { 7 | while true; do 8 | if $1 getblockchaininfo 2>&1 | grep blocks > /dev/null 2>&1; then 9 | break 10 | fi 11 | sleep 1 12 | done 13 | } 14 | 15 | function waitForLnd () { 16 | while true; do 17 | if $1 getinfo 2>&1 | grep version > /dev/null 2>&1; then 18 | break 19 | fi 20 | sleep 1 21 | done 22 | } 23 | 24 | function startNodes () { 25 | echo "Starting nodes" 26 | 27 | bitcoind 28 | litecoind 29 | 30 | # Wait for the nodes to start 31 | waitForNode bitcoin-cli 32 | waitForNode litecoin-cli 33 | 34 | echo "Started nodes" 35 | } 36 | 37 | function startLnds () { 38 | echo "Starting LNDs" 39 | 40 | # Start the LNDs 41 | nohup lnd --lnddir=/root/.lnd-btc --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=127.0.0.1:18443 > /dev/null 2>&1 & num="0" 42 | nohup lnd --lnddir=/root/.lnd-btc2 --listen=127.0.0.1:9736 --rpclisten=0.0.0.0:10011 --restlisten=0.0.0.0:8081 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=127.0.0.1:18443 > /dev/null 2>&1 & num="0" 43 | 44 | nohup lnd --lnddir=/root/.lnd-ltc --listen=0.0.0.0:10735 --rpclisten=0.0.0.0:11009 --restlisten=0.0.0.0:9080 --litecoin.active --litecoin.regtest --litecoin.node=litecoind --litecoind.rpchost=127.0.0.1:19443 > /dev/null 2>&1 & num="0" 45 | nohup lnd --lnddir=/root/.lnd-ltc2 --listen=127.0.0.1:10736 --rpclisten=0.0.0.0:11010 --restlisten=0.0.0.0:9081 --litecoin.active --litecoin.regtest --litecoin.node=litecoind --litecoind.rpchost=127.0.0.1:19443 > /dev/null 2>&1 & num="0" 46 | 47 | # Wait for the LNDs to start 48 | waitForLnd "lncli --lnddir=/root/.lnd-btc --rpcserver=127.0.0.1:10009 --network=regtest" 49 | waitForLnd "lncli --lnddir=/root/.lnd-btc2 --rpcserver=127.0.0.1:10011 --network=regtest" 50 | 51 | waitForLnd "lncli --lnddir=/root/.lnd-ltc --rpcserver=127.0.0.1:11009 --chain=litecoin --network=regtest" 52 | waitForLnd "lncli --lnddir=/root/.lnd-ltc2 --rpcserver=127.0.0.1:11010 --chain=litecoin --network=regtest" 53 | 54 | echo "Started LNDs" 55 | } 56 | 57 | function stopNodes () { 58 | killall bitcoind 59 | killall litecoind 60 | } 61 | 62 | function stopLnds () { 63 | killall lnd 64 | } 65 | -------------------------------------------------------------------------------- /docs/0-confirmation.md: -------------------------------------------------------------------------------- 1 | # 0-confirmation 2 | 3 | The use of 0-confirmation can make swaps a lot faster by utilizing transactions that are not included in a block yet. But accepting 0-conf transactions doesn't come without unwarranted risk. Therefore, as a precautionary measure, Boltz enforces a few rules when it comes to 0-conf. 4 | 5 | It is important to note that for: 6 | 7 | - normal swaps in which the user sends the onchain transaction, a Boltz service provider is taking the risk by accepting the 0-conf transaction 8 | - reverse swaps where the user receives the onchain coins from Boltz, the user is at risk for accepting the unconfirmed transaction 9 | 10 | *And 0-confirmation Swaps are only available on UTXO based blockchains like Bitcoin.* 11 | 12 | ## Limits 13 | 14 | When it comes to accepting 0-conf transactions, Boltz has configurable limits in place. These limits can be found in the [`getpairs` endpoint](/api/#getting-pairs) and are just enforced for normal swaps. When the user receives onchain coins from Boltz, he can accept any amount of coins with 0-conf he is comfortable with. 15 | 16 | ## BIP 125 - Replace-By-Fee 17 | 18 | If a transaction locking up coins is signalling Replace-By-Fee either explicitly or inherently (unconfirmed inputs of the transaction signal RBF) Boltz will not accept 0-conf for that transaction. Boltz itself will never send transactions that signal RBF, which means that the user doesn't have to worry about a lockup transaction of a reverse swap being replaceable. 19 | 20 | For more information about RBF please read the [BIP 125 - Opt-in Full Replace-by-Fee Signaling](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki) 21 | 22 | ## Miner fees 23 | 24 | Swaps on Boltz are based on HTLCs (*Hash Time Locked Contracts*). In order to be able to deal with the *time locked* component of these contracts, in scenarios where not all transactions from the mempool are getting included in the very next block all the time, transactions locking and claiming coins from such contracts have to pay a *reasonably high miner fee* in order to be included in a block quickly. 25 | 26 | Boltz considers fees that are equal or higher than 80% of the `sat/vbyte` estimations of the [`getfeeestimation`](/api/#getting-fee-estimations) endpoint as *reasonably high*. If the miner fee paid by the transaction is less than that, Boltz will not accept 0-conf and wait for the transaction to be included in a block. 27 | --------------------------------------------------------------------------------