Skip to content
This repository has been archived by the owner on Apr 7, 2020. It is now read-only.

Commit

Permalink
Publishing client to sparkswap-desktop: v0.3.6
Browse files Browse the repository at this point in the history
  • Loading branch information
Sparkswap committed Jan 3, 2020
1 parent 0c0151d commit 4965494
Show file tree
Hide file tree
Showing 51 changed files with 1,048 additions and 83 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "Sparkswap <dev@sparkswap.com> (https://github.com/sparkswap)",
"description": "Sparkswap Desktop: the only way to buy Bitcoin instantly",
"productName": "Sparkswap",
"version": "0.3.5",
"version": "0.3.6",
"license": "MIT",
"private": true,
"main": "./build/electron.js",
Expand Down
1 change: 1 addition & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const IS_TEST = process.env.REACT_APP_ENV === 'test'

export const API_URL = IS_PRODUCTION ? 'https://stack.sparkswap.com' : 'http://localhost:3000'
export const WEBSOCKETS_URL = IS_PRODUCTION ? 'wss://stack.sparkswap.com' : 'ws://localhost:3000'
export const PROOF_HOST = IS_PRODUCTION ? 'https://sparkswap.com' : 'http://localhost:3001'

export const ZAPIER_HOOK = 'https://hooks.zapier.com/hooks/catch/5808043/o2fl9bt/'
export const IP_API_URL = 'https://ipapi.co/json'
Expand Down
13 changes: 10 additions & 3 deletions src/global-shared/anchor-engine/anchor-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,16 @@ export class AnchorEngine {
`timeFromNow: ${timeFromNow}, finalDelta: ${finalDelta}`)
}

const newEscrow = await api.createEscrow(this.apiKey, hash, recipientId, amount, expiration)
this.logger.debug(`Escrow for swap (${hash}) created`)
return newEscrow
try {
const newEscrow = await api.createEscrow(this.apiKey, hash, recipientId, amount, expiration)
this.logger.debug(`Escrow for swap (${hash}) created`)
return newEscrow
} catch (e) {
if (e.code && Object.values(api.CreateEscrowErrorCodes).includes(e.code)) {
throw new PermanentSwapError(`Error while creating escrow: ${e.reason}`)
}
throw e
}
}

private async waitForEscrowEnd (startedEscrow: api.Escrow): Promise<api.Escrow> {
Expand Down
21 changes: 20 additions & 1 deletion src/global-shared/anchor-engine/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ export enum EscrowStatus {
complete = 'complete'
}

export class AnchorError extends Error {
param?: string
code?: string
error?: string
reason?: string
}

// see: https://www.anchorusd.com/9c0cba91e667a08e467f038b6e23e3c4/api/index.html#/?id=create-escrow
export enum CreateEscrowErrorCodes {
insufficientFunds = 'insufficient_funds',
recipientNotFound = 'not_found',
positiveDurationRequired = 'positive_duration_required',
duplicateTimeout = 'duplicate_timeout_parameters',
invalidHashFormat = 'invalid_format',
futureTimeoutRequired = 'future_timestamp_required'
}

// see: https://www.anchorusd.com/9c0cba91e667a08e467f038b6e23e3c4/api/index.html#/?id=the-escrow-object
// Note: Hashes and preimages are hex on Anchor, but we use base64. So there needs to be a conversion
// prior to sending to the Anchor API or receiving from it.
Expand Down Expand Up @@ -173,7 +190,9 @@ export async function createEscrow (apiKey: string, hash: SwapHash, recipientId:
amount: Amount, expiration: Date): Promise<Escrow> {
const duration = Math.floor((expiration.getTime() - (new Date()).getTime()) / 1000)
if (duration <= 0) {
throw new Error(`Escrow duration is too short (${duration}s)`)
const error = new AnchorError(`Escrow duration is too short (${duration}s)`)
error.code = CreateEscrowErrorCodes.positiveDurationRequired
throw error
}
const res = await request(
apiKey,
Expand Down
3 changes: 2 additions & 1 deletion src/global-shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export const API_ENDPOINTS: { [key: string]: string } = {
VERIFY_PHONE: '/verify-phone',
START_BERBIX: '/start-berbix',
FINISH_BERBIX: '/finish-berbix',
SUBMIT_PHOTO_ID: '/submit-photo-id'
SUBMIT_PHOTO_ID: '/submit-photo-id',
GET_PROOF: '/proof-of-keys'
}
33 changes: 28 additions & 5 deletions src/global-shared/fetch-json.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import fetch, { Headers, RequestInit } from 'node-fetch'
import logger from './logger'

interface FetchJsonErrorOpts {
status?: number,
code?: string,
error?: string,
reason?: string
}

class FetchJsonError extends Error {
statusCode?: number
status?: number
code?: string
error?: string
reason?: string

constructor (message?: string, statusCode?: number) {
constructor (message?: string, opts?: FetchJsonErrorOpts) {
super(message)
this.statusCode = statusCode
if (opts) {
this.status = opts.status
this.code = opts.code
this.error = opts.error
this.reason = opts.reason
}
}
}

Expand Down Expand Up @@ -46,7 +61,7 @@ Promise<{ ok: boolean, status: number, json: unknown }> {
return { ok: res.ok, status: res.status, json }
} catch (e) {
const message = `Error while requesting "${httpOptions.method} ${url}": ${res.status} ${res.statusText}`
throw new FetchJsonError(message, res.status)
throw new FetchJsonError(message, { status: res.status })
}
}

Expand All @@ -71,7 +86,15 @@ export default async function fetchJSON (url: string, httpOptions: RequestInit,

if (!ok && !(options.ignoreCodes && options.ignoreCodes.includes(status))) {
const message = isUnknownJSON(json) ? getErrorMessage(json) : ''
throw new FetchJsonError(`Error while requesting "${httpOptions.method} ${url}": ${message}`, status)
const opts = { status }
if (isUnknownJSON(json)) {
Object.assign(opts, {
code: json.code,
error: json.error,
reason: json.reason
})
}
throw new FetchJsonError(`Error while requesting "${httpOptions.method} ${url}": ${message}`, opts)
}

if (isUnknownJSON(json)) {
Expand Down
1 change: 1 addition & 0 deletions src/global-shared/lnd-engine/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SECONDS_PER_BLOCK = 600
25 changes: 25 additions & 0 deletions src/global-shared/lnd-engine/connect-peer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { LndActionOptions, GrpcError } from '../types/lnd-engine/client'
import { deadline } from './deadline'
import { promisify } from 'util'

function alreadyConnected (err: GrpcError): boolean {
return (err && err.code === 2 && err.details != null && err.details.includes('already connected to peer'))
}

export async function connectPeer (publicKey: string, host: string, { client, logger }: LndActionOptions): Promise<void> {
const connect = promisify(client.connectPeer).bind(client)
const addr = {
pubkey: publicKey,
host
}

try {
await connect({ addr }, { deadline: deadline() })
} catch (e) {
if (alreadyConnected(e)) {
logger.debug(`Peer already connected: ${publicKey}`)
return
}
throw e
}
}
73 changes: 73 additions & 0 deletions src/global-shared/lnd-engine/create-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
LndActionOptions,
OpenChannelResponse,
PendingUpdateResponse,
ChannelOpenResponse
} from '../types/lnd-engine/client'
import { SECONDS_PER_BLOCK } from './config'
import { connectPeer } from './connect-peer'
import {
parse as parseAddress,
loggablePubKey
} from './utils'

// Default number of seconds before our first confirmation. (30 minutes)
const DEFAULT_CONFIRMATION_DELAY = 1800

interface CreateChannelOptions {
targetTime?: number,
privateChan?: boolean
}

function isPendingUpdateResponse (res: OpenChannelResponse): res is PendingUpdateResponse {
return Object.keys(res).includes('chanPending')
}

function isChannelOpenUpdateResponse (res: OpenChannelResponse): res is ChannelOpenResponse {
return Object.keys(res).includes('chanOpen')
}

export async function createChannel (paymentChannelNetworkAddress: string, fundingAmount: number, {
targetTime = DEFAULT_CONFIRMATION_DELAY,
privateChan = false
}: CreateChannelOptions,
{ client, logger }: LndActionOptions): Promise<void> {
const targetConf = Math.max(Math.floor(targetTime / SECONDS_PER_BLOCK), 1)
const { publicKey, host } = parseAddress(paymentChannelNetworkAddress)
const loggablePublicKey = loggablePubKey(publicKey)

logger.debug(`Attempting to create a channel with ${loggablePublicKey}`)

if (host) {
await connectPeer(publicKey, host, { client, logger })
} else {
logger.debug(`Skipping connect peer. Host is missing for pubkey ${loggablePublicKey}`)
}

logger.debug(`Successfully connected to peer: ${loggablePublicKey}`)

return new Promise((resolve, reject) => {
try {
const call = client.openChannel({
nodePubkey: Buffer.from(publicKey, 'hex'),
localFundingAmount: fundingAmount,
targetConf,
private: privateChan
})

call.on('data', data => {
if (isPendingUpdateResponse(data)) {
return resolve()
}

if (isChannelOpenUpdateResponse(data)) {
return resolve()
}
})

call.on('error', reject)
} catch (e) {
reject(e)
}
})
}
3 changes: 3 additions & 0 deletions src/global-shared/lnd-engine/deadline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function deadline (duration = 30): number {
return new Date().setSeconds(new Date().getSeconds() + duration)
}
2 changes: 2 additions & 0 deletions src/global-shared/lnd-engine/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { deadline } from './deadline'
export { payInvoice } from './pay-invoice'
export { SECONDS_PER_BLOCK } from './config'
8 changes: 1 addition & 7 deletions src/global-shared/lnd-engine/pay-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { LndEngineClient } from 'lnd-engine'
import { LoggerInterface } from '../logger'
import { SendPaymentState } from '../types/lnd-engine/client/router'
import { LndActionOptions } from '../types/lnd-engine/client'

// Default value of invoice timeout taken from the original `sendPayment` Lightning rpc
// which is 60 seconds.
Expand All @@ -10,11 +9,6 @@ const INVOICE_TIMEOUT_IN_SECONDS = 60
// value for an `unlimited` fee limit from LND
const INVOICE_FEE_LIMIT = '9223372036854775807'

interface LndActionOptions {
client: LndEngineClient,
logger: LoggerInterface
}

export function payInvoice (paymentRequest: string, { client, logger }: LndActionOptions): Promise<void> {
return new Promise((resolve, reject) => {
logger.debug('Attempting to pay invoice')
Expand Down
2 changes: 2 additions & 0 deletions src/global-shared/lnd-engine/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { parse } from './network-address-formatter'
export { loggablePubKey } from './loggable-pubkey'
5 changes: 5 additions & 0 deletions src/global-shared/lnd-engine/utils/loggable-pubkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Nullable } from '../../types'

export function loggablePubKey (pubkey: Nullable<string>): Nullable<string> {
return pubkey ? `${pubkey.slice(0, 15)}...` : null
}
17 changes: 17 additions & 0 deletions src/global-shared/lnd-engine/utils/network-address-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const DELIMITER = ':'
const NETWORK_TYPE = 'bolt'

interface ParsedAddress {
publicKey: string,
host?: string
}

export function parse (paymentChannelNetworkAddress: string): ParsedAddress {
const [networkType, networkAddress] = paymentChannelNetworkAddress.split(DELIMITER, 2)
if (networkType !== NETWORK_TYPE) {
throw new Error(`Unable to parse address for payment channel network type of '${networkType}'`)
}

const [publicKey, host] = networkAddress.split('@', 2)
return { publicKey, host }
}
9 changes: 9 additions & 0 deletions src/global-shared/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function parsePhoneNumber (phone: string): string {
const digits = phone.split('-').join('').split(' ').join('')
return digits.startsWith('+') ? digits : '+1' + digits
}

export function parseSSN (ssn: string): string {
const digits = ssn.split('-').join('').split(' ').join('')
return [digits.slice(0, 3), digits.slice(3, 5), digits.slice(5)].join('-')
}
Loading

0 comments on commit 4965494

Please sign in to comment.