Merge pull request #19 from shocknet/spontaneous-payments
Spontaneous payments
This commit is contained in:
commit
0030b62ba3
11 changed files with 469 additions and 35 deletions
|
|
@ -62,7 +62,10 @@
|
|||
|
||||
"no-shadow": "off",
|
||||
// We're usually throwing objects throughout the API to allow for more detailed error messages
|
||||
"no-throw-literal": "off"
|
||||
"no-throw-literal": "off",
|
||||
|
||||
// lightning has sync methods and this rule bans them
|
||||
"no-sync": "off"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ const authenticate = async (alias, pass) => {
|
|||
}
|
||||
// move this to a subscription; implement off() ? todo
|
||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||
API.Jobs.onOrders(user, gun, mySEA)
|
||||
return user._.sea.pub
|
||||
}
|
||||
|
||||
|
|
@ -177,6 +178,7 @@ const authenticate = async (alias, pass) => {
|
|||
throw new Error(ack.err)
|
||||
} else if (typeof ack.sea === 'object') {
|
||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||
API.Jobs.onOrders(user, gun, mySEA)
|
||||
|
||||
const mySec = await mySEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySec !== 'string') {
|
||||
|
|
@ -294,6 +296,7 @@ class Mediator {
|
|||
this.sendHRWithInitialMsg
|
||||
)
|
||||
socket.on(Action.SEND_MESSAGE, this.sendMessage)
|
||||
socket.on(Action.SEND_PAYMENT, this.sendPayment)
|
||||
socket.on(Action.SET_AVATAR, this.setAvatar)
|
||||
socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName)
|
||||
|
||||
|
|
@ -561,6 +564,39 @@ class Mediator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ uuid: string, recipientPub: string, amount: number, memo: string, token: string }>} reqBody
|
||||
*/
|
||||
sendPayment = async reqBody => {
|
||||
try {
|
||||
const { recipientPub, amount, memo, token } = reqBody
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.sendPayment(
|
||||
recipientPub,
|
||||
amount,
|
||||
memo,
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
|
||||
this.socket.emit(Action.SEND_PAYMENT, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: reqBody
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.SEND_PAYMENT, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: reqBody
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ avatar: string|null , token: string }>} body
|
||||
*/
|
||||
|
|
@ -915,6 +951,7 @@ const register = async (alias, pass) => {
|
|||
return authenticate(alias, pass).then(async pub => {
|
||||
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
|
||||
await API.Actions.generateHandshakeAddress(user)
|
||||
await API.Actions.generateOrderAddress(user)
|
||||
return pub
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const Actions = {
|
|||
SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST",
|
||||
SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG",
|
||||
SEND_MESSAGE: "SEND_MESSAGE",
|
||||
SEND_PAYMENT: "SEND_PAYMENT",
|
||||
SET_AVATAR: "SET_AVATAR",
|
||||
SET_DISPLAY_NAME: "SET_DISPLAY_NAME"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @format
|
||||
*/
|
||||
const uuidv1 = require('uuid/v1')
|
||||
|
||||
const LightningServices = require('../../../utils/lightningServices')
|
||||
|
||||
const ErrorCode = require('./errorCode')
|
||||
const Getters = require('./getters')
|
||||
const Key = require('./key')
|
||||
const Utils = require('./utils')
|
||||
const { isHandshakeRequest } = require('./schema')
|
||||
|
|
@ -15,6 +19,8 @@ const { isHandshakeRequest } = require('./schema')
|
|||
* @typedef {import('./schema').Message} Message
|
||||
* @typedef {import('./schema').Outgoing} Outgoing
|
||||
* @typedef {import('./schema').PartialOutgoing} PartialOutgoing
|
||||
* @typedef {import('./schema').Order} Order
|
||||
* @typedef {import('./SimpleGUN').Ack} Ack
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -408,18 +414,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
|||
console.log('sendHR() -> before mySecret')
|
||||
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError(
|
||||
"sendHandshakeRequest() -> typeof mySecret !== 'string'"
|
||||
)
|
||||
}
|
||||
console.log('sendHR() -> before ourSecret')
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError(
|
||||
"sendHandshakeRequest() -> typeof ourSecret !== 'string'"
|
||||
)
|
||||
}
|
||||
|
||||
// check if successful handshake is present
|
||||
|
||||
|
|
@ -660,7 +656,65 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => {
|
|||
.get(Key.MESSAGES)
|
||||
.set(newMessage, ack => {
|
||||
if (ack.err) {
|
||||
rej(ack.err)
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} recipientPub
|
||||
* @param {string} msgID
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteMessage = async (recipientPub, msgID, user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
if (typeof recipientPub !== 'string') {
|
||||
throw new TypeError(
|
||||
`expected recipientPublicKey to be an string, but instead got: ${typeof recipientPub}`
|
||||
)
|
||||
}
|
||||
|
||||
if (recipientPub.length === 0) {
|
||||
throw new TypeError(
|
||||
'expected recipientPublicKey to be an string of length greater than zero'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof msgID !== 'string') {
|
||||
throw new TypeError(
|
||||
`expected msgID to be an string, instead got: ${typeof msgID}`
|
||||
)
|
||||
}
|
||||
|
||||
if (msgID.length === 0) {
|
||||
throw new TypeError(
|
||||
'expected msgID to be an string of length greater than zero'
|
||||
)
|
||||
}
|
||||
|
||||
const outgoingID = await Utils.recipientToOutgoingID(recipientPub, user, SEA)
|
||||
|
||||
if (outgoingID === null) {
|
||||
throw new Error(`Could not fetch an outgoing id for user: ${recipientPub}`)
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(outgoingID)
|
||||
.get(Key.MESSAGES)
|
||||
.get(msgID)
|
||||
.put(null, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
|
|
@ -783,6 +837,158 @@ const sendHRWithInitialMsg = async (
|
|||
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} to
|
||||
* @param {number} amount
|
||||
* @param {string} memo
|
||||
* @param {GUNNode} gun
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
||||
* lightning cannot find a route for the payment.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendPayment = async (to, amount, memo, gun, user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const recipientEpub = await Utils.pubToEpub(to)
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
|
||||
if (amount < 1) {
|
||||
throw new RangeError('Amount must be at least 1 sat.')
|
||||
}
|
||||
|
||||
/** @type {Order} */
|
||||
const order = {
|
||||
amount: await SEA.encrypt(amount.toString(), ourSecret),
|
||||
from: user._.sea.pub,
|
||||
memo: await SEA.encrypt(memo || 'no memo', ourSecret),
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
const currOrderAddress = await Getters.currentOrderAddress(to)
|
||||
|
||||
order.timestamp = Date.now()
|
||||
|
||||
/** @type {string} */
|
||||
const orderID = await new Promise((res, rej) => {
|
||||
const ord = gun
|
||||
.get(Key.ORDER_NODES)
|
||||
.get(currOrderAddress)
|
||||
.set(order, ack => {
|
||||
if (ack.err) {
|
||||
rej(
|
||||
new Error(
|
||||
`Error writing order to order node: ${currOrderAddress} for pub: ${to}: ${ack.err}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res(ord._.get)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const bob = gun.user(to)
|
||||
|
||||
/** @type {string} */
|
||||
const invoice = await Promise.race([
|
||||
new Promise((res, rej) => {
|
||||
bob
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
.on(data => {
|
||||
if (typeof data !== 'string') {
|
||||
rej(
|
||||
`Expected order response from pub ${to} to be an string, instead got: ${typeof data}`
|
||||
)
|
||||
} else {
|
||||
res(data)
|
||||
}
|
||||
})
|
||||
}),
|
||||
new Promise((_, rej) => {
|
||||
setTimeout(() => {
|
||||
rej(new Error(ErrorCode.ORDER_NOT_ANSWERED_IN_TIME))
|
||||
}, 20000)
|
||||
})
|
||||
])
|
||||
|
||||
const decInvoice = await SEA.decrypt(invoice, ourSecret)
|
||||
|
||||
const {
|
||||
services: { lightning }
|
||||
} = LightningServices
|
||||
|
||||
/**
|
||||
* @typedef {object} SendErr
|
||||
* @prop {string} details
|
||||
*/
|
||||
|
||||
/**
|
||||
* Partial
|
||||
* https://api.lightning.community/#grpc-response-sendresponse-2
|
||||
* @typedef {object} SendResponse
|
||||
* @prop {string|null} payment_error
|
||||
* @prop {any[]|null} payment_route
|
||||
*/
|
||||
|
||||
await new Promise((resolve, rej) => {
|
||||
lightning.sendPaymentSync(
|
||||
{
|
||||
payment_request: decInvoice
|
||||
},
|
||||
(/** @type {SendErr=} */ err, /** @type {SendResponse} */ res) => {
|
||||
if (err) {
|
||||
rej(new Error(err.details))
|
||||
} else if (res) {
|
||||
if (res.payment_error) {
|
||||
rej(
|
||||
new Error(
|
||||
`sendPaymentSync error response: ${JSON.stringify(res)}`
|
||||
)
|
||||
)
|
||||
} else if (!res.payment_route) {
|
||||
rej(
|
||||
new Error(
|
||||
`sendPaymentSync no payment route response: ${JSON.stringify(
|
||||
res
|
||||
)}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
} else {
|
||||
rej(new Error('no error or response received from sendPaymentSync'))
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UserGUNNode} user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const generateOrderAddress = user =>
|
||||
new Promise((res, rej) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const address = uuidv1()
|
||||
|
||||
user.get(Key.CURRENT_ORDER_ADDRESS).put(address, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
INITIAL_MSG,
|
||||
__createOutgoingFeed,
|
||||
|
|
@ -791,8 +997,11 @@ module.exports = {
|
|||
blacklist,
|
||||
generateHandshakeAddress,
|
||||
sendHandshakeRequest,
|
||||
deleteMessage,
|
||||
sendMessage,
|
||||
sendHRWithInitialMsg,
|
||||
setAvatar,
|
||||
setDisplayName
|
||||
setDisplayName,
|
||||
sendPayment,
|
||||
generateOrderAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,3 +41,5 @@ exports.ALREADY_REQUESTED_HANDSHAKE = 'ALREADY_REQUESTED_HANDSHAKE'
|
|||
exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS'
|
||||
|
||||
exports.TIMEOUT_ERR = 'TIMEOUT_ERR'
|
||||
|
||||
exports.ORDER_NOT_ANSWERED_IN_TIME = 'ORDER_NOT_ANSWERED_IN_TIME'
|
||||
|
|
|
|||
16
services/gunDB/contact-api/getters.js
Normal file
16
services/gunDB/contact-api/getters.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const Key = require('./key')
|
||||
const Utils = require('./utils')
|
||||
|
||||
/**
|
||||
* @param {string} pub
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.currentOrderAddress = async (pub) => {
|
||||
const currAddr = await Utils.tryAndWait((gun) => gun.user(pub).get(Key.CURRENT_ORDER_ADDRESS).then())
|
||||
|
||||
if (typeof currAddr !== 'string') {
|
||||
throw new TypeError('Expected user.currentOrderAddress to be an string')
|
||||
}
|
||||
|
||||
return currAddr
|
||||
}
|
||||
18
services/gunDB/contact-api/jobs/index.js
Normal file
18
services/gunDB/contact-api/jobs/index.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @format
|
||||
* Jobs are subscriptions to events that perform actions (write to GUN) on
|
||||
* response to certain ways events can happen. These tasks need to be fired up
|
||||
* at app launch otherwise certain features won't work as intended. Tasks should
|
||||
* ideally be idempotent, that is, if they were to be fired up after a certain
|
||||
* amount of time after app launch, everything should work as intended. For this
|
||||
* to work, special care has to be put into how these respond to events. These
|
||||
* tasks accept factories that are homonymous to the events on this same module.
|
||||
*/
|
||||
|
||||
const onAcceptedRequests = require('./onAcceptedRequests')
|
||||
const onOrders = require('./onOrders')
|
||||
|
||||
module.exports = {
|
||||
onAcceptedRequests,
|
||||
onOrders
|
||||
}
|
||||
|
|
@ -1,24 +1,15 @@
|
|||
/**
|
||||
* @prettier
|
||||
* Taks are subscriptions to events that perform actions (write to GUN) on
|
||||
* response to certain ways events can happen. These tasks need to be fired up
|
||||
* at app launch otherwise certain features won't work as intended. Tasks should
|
||||
* ideally be idempotent, that is, if they were to be fired up after a certain
|
||||
* amount of time after app launch, everything should work as intended. For this
|
||||
* to work, special care has to be put into how these respond to events. These
|
||||
* tasks could be hardcoded inside events but then they wouldn't be easily
|
||||
* auto-testable. These tasks accept factories that are homonymous to the events
|
||||
* on the same
|
||||
* @format
|
||||
*/
|
||||
const ErrorCode = require('./errorCode')
|
||||
const Key = require('./key')
|
||||
const Schema = require('./schema')
|
||||
const Utils = require('./utils')
|
||||
const ErrorCode = require('../errorCode')
|
||||
const Key = require('../key')
|
||||
const Schema = require('../schema')
|
||||
const Utils = require('../utils')
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -148,6 +139,4 @@ const onAcceptedRequests = async (user, SEA) => {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
onAcceptedRequests
|
||||
}
|
||||
module.exports = onAcceptedRequests
|
||||
116
services/gunDB/contact-api/jobs/onOrders.js
Normal file
116
services/gunDB/contact-api/jobs/onOrders.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
const LightningServices = require('../../../../utils/lightningServices')
|
||||
|
||||
const ErrorCode = require('../errorCode')
|
||||
const Key = require('../key')
|
||||
const Schema = require('../schema')
|
||||
const Utils = require('../utils')
|
||||
|
||||
/**
|
||||
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('../SimpleGUN').ListenerData} ListenerData
|
||||
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||
*/
|
||||
|
||||
let currentOrderAddr = ''
|
||||
|
||||
/**
|
||||
* @param {string} addr
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {(order: ListenerData, orderID: string) => void}
|
||||
*/
|
||||
const listenerForAddr = (addr, user, SEA) => async (order, orderID) => {
|
||||
try {
|
||||
if (addr !== currentOrderAddr) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Schema.isOrder(order)) {
|
||||
throw new Error(`Expected an order instead got: ${JSON.stringify(order)}`)
|
||||
}
|
||||
|
||||
const orderToResponse = user.get(Key.ORDER_TO_RESPONSE)
|
||||
|
||||
if (await orderToResponse.get(orderID).then()) {
|
||||
return
|
||||
}
|
||||
|
||||
const senderEpub = await Utils.pubToEpub(order.from)
|
||||
const secret = await SEA.secret(senderEpub, user._.sea)
|
||||
|
||||
const amount = Number(await SEA.decrypt(order.amount, secret))
|
||||
const memo = await SEA.decrypt(order.memo, secret)
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
const invoice = await new Promise((resolve, rej) => {
|
||||
const {
|
||||
services: { lightning }
|
||||
} = LightningServices
|
||||
|
||||
lightning.addInvoice(
|
||||
{
|
||||
expiry: 36000,
|
||||
memo,
|
||||
value: amount
|
||||
},
|
||||
(
|
||||
/** @type {any} */ error,
|
||||
/** @type {{ payment_request: string }} */ response
|
||||
) => {
|
||||
if (error) {
|
||||
rej(error)
|
||||
} else {
|
||||
resolve(response.payment_request)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const encInvoice = await SEA.encrypt(invoice, secret)
|
||||
|
||||
orderToResponse.get(orderID).put(encInvoice, ack => {
|
||||
if (ack.err) {
|
||||
console.error(`error saving order response: ${ack.err}`)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('error inside onOrders:')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UserGUNNode} user
|
||||
* @param {GUNNode} gun
|
||||
* @param {ISEA} SEA
|
||||
* @throws {Error} NOT_AUTH
|
||||
* @returns {void}
|
||||
*/
|
||||
const onOrders = (user, gun, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
user.get(Key.CURRENT_ORDER_ADDRESS).on(addr => {
|
||||
if (typeof addr !== 'string') {
|
||||
throw new TypeError('Expected current order address to be an string')
|
||||
}
|
||||
|
||||
currentOrderAddr = addr
|
||||
|
||||
gun
|
||||
.get(Key.ORDER_NODES)
|
||||
.get(addr)
|
||||
.map()
|
||||
.on(listenerForAddr(currentOrderAddr, user, SEA))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = onOrders
|
||||
|
|
@ -24,3 +24,9 @@ exports.DISPLAY_NAME = 'displayName'
|
|||
* Maps user to the last request sent to them.
|
||||
*/
|
||||
exports.USER_TO_LAST_REQUEST_SENT = 'USER_TO_LAST_REQUEST_SENT'
|
||||
|
||||
exports.CURRENT_ORDER_ADDRESS = 'currentOrderAddress'
|
||||
|
||||
exports.ORDER_NODES = 'orderNodes'
|
||||
|
||||
exports.ORDER_TO_RESPONSE = 'orderToResponse'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* @prettier
|
||||
* @format
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} HandshakeRequest
|
||||
|
|
@ -316,7 +316,6 @@ exports.isPartialOutgoing = item => {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} item
|
||||
* @returns {item is Outgoing}
|
||||
*/
|
||||
|
|
@ -337,3 +336,41 @@ exports.isOutgoing = item => {
|
|||
|
||||
return typeof obj.with === 'string' && messagesAreMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Order
|
||||
* @prop {string} from Public key of sender.
|
||||
* @prop {string} amount Encrypted
|
||||
* @prop {string} memo Encrypted
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is Order}
|
||||
*/
|
||||
exports.isOrder = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {Order} */ (item)
|
||||
|
||||
if (typeof obj.amount !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.from !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.memo !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return typeof obj.timestamp === 'number'
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue