Skip to content

Commit

Permalink
Merge pull request #826 from setlife-network/or/issue-812/implement-s…
Browse files Browse the repository at this point in the history
…end-payment

Implement Send Payment to Contributors API
  • Loading branch information
Ovifer13 committed Jun 23, 2023
2 parents 149af93 + 76ee884 commit d53e269
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ STRIPE_API_KEY=
STRIPE_SECRET=
TOGGL_API_KEY=
BTCPAYSERVER_API_KEY=
BTCPAYSERVER_API_ROOT=
BTCPAYSERVER_STORE_ID=
BTCPAYSERVER_WALLET_ID=
BTCPAYSERVER_SECRET=
OAUTH_REDIRECT_NEW_USERS=
OAUTH_REDIRECT_EXISTING_USERS=
Expand Down
1 change: 0 additions & 1 deletion api/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,4 @@ module.exports = {
'MXN',
'EUR'
],
BTCPAYSERVER_API_ROOT: 'https://btcpayserver.setlife.tech/api/v1'
}
4 changes: 3 additions & 1 deletion api/config/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ module.exports = {
},
BTCPAYSERVER: {
API_KEY: process.env.BTCPAYSERVER_API_KEY,
API_ROOT: process.env.BTCPAYSERVER_API_ROOT,
STORE_ID: process.env.BTCPAYSERVER_STORE_ID,
SECRET: process.env.BTCPAYSERVER_SECRET
SECRET: process.env.BTCPAYSERVER_SECRET,
WALLET_ID: process.env.BTCPAYSERVER_WALLET_ID
}
};
71 changes: 66 additions & 5 deletions api/handlers/btcPayServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ const axios = require('axios')
const {
BTCPAYSERVER
} = require('../config/credentials')
const { BTCPAYSERVER_API_ROOT } = require('../config/constants')
const crypto = require('crypto')

const BTCPAYSERVER_API_KEY = BTCPAYSERVER.API_KEY;
const BTCPAYSERVER_STORE_ID = BTCPAYSERVER.STORE_ID;
const BTCPAYSERVER_SECRET = BTCPAYSERVER.SECRET;
const BTCPAYSERVER_API_KEY = BTCPAYSERVER.API_KEY
const BTCPAYSERVER_API_ROOT = BTCPAYSERVER.API_ROOT
const BTCPAYSERVER_STORE_ID = BTCPAYSERVER.STORE_ID
const BTCPAYSERVER_SECRET = BTCPAYSERVER.SECRET
const BTCPAYSERVER_WALLET_ID = BTCPAYSERVER.WALLET_ID

let config = {
headers: {
Expand Down Expand Up @@ -75,9 +76,69 @@ const webhookSignatureIsValid = (body, signature) => {
return false
}

const getNodeInfo = async () => {
const response = await axios.get(
`${BTCPAYSERVER_API_ROOT}/server/lightning/BTC/info`,
config
)

return response.data
}

const getFee = async () => {
const response = await axios.get(
`${BTCPAYSERVER_API_ROOT}/stores/${BTCPAYSERVER_STORE_ID}/payment-methods/onchain/BTC/wallet/feerate`,
config
)

return response.data
}

const createOnChainTransaction = async (walletAddress, amount) => {
const fee = await getFee()
const body = {
destinations: [
{
destination: walletAddress,
amount: amount,
}
],
feerate: fee.feeRate,
proceedWithPayjoin: true,
proceedWithBroadcast: true,
noChange: false,
rbf: true,
excludeUnconfirmed: false,
}
const response = await axios.post(
`${BTCPAYSERVER_API_ROOT}/stores/${BTCPAYSERVER_STORE_ID}/payment-methods/onchain/BTC/wallet/transactions`,
body,
config
)

return response.data
}

const payLightningInvoice = async (payment_request) => {
const body = {
destination: payment_request
}
const response = await axios.post(
`${BTCPAYSERVER_API_ROOT}/lnbank/wallets/${BTCPAYSERVER_WALLET_ID}/send`,
body,
config
)

return response.data
}

module.exports = {
createBitcoinInvoice,
getAllInvoices,
getInvoiceById,
webhookSignatureIsValid
webhookSignatureIsValid,
getNodeInfo,
createOnChainTransaction,
getFee,
payLightningInvoice
};
82 changes: 82 additions & 0 deletions api/handlers/lnd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const axios = require('axios')

const sendPayment = async (host, port, macaroon, payment_request) => {
const options = {
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }),
headers: {
'Grpc-Metadata-macaroon': macaroon,
}
}

const body = {
payment_request: payment_request
}

response = await axios.post(
`https://${host}:${port}/v1/channels/transactions`,
body,
options
)

return response.data
}

const addInvoice = async (host, port, macaroon, value) => {
const options = {
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }),
headers: {
'Grpc-Metadata-macaroon': macaroon,
}
}

const body = {
value: value,
}

response = await axios.post(
`https://${host}:${port}/v1/invoices`,
body,
options
)

return response.data
}

const decodePaymentRequest = async (host, port, macaroon, payment_request) => {
const options = {
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }),
headers: {
'Grpc-Metadata-macaroon': macaroon,
}
}

response = await axios.get(
`https://${host}:${port}/v1/payreq/${payment_request}`,
options
)

return response.data
}

const listPayments = async (host, port, macaroon) => {
const options = {
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }),
headers: {
'Grpc-Metadata-macaroon': macaroon,
}
}

response = await axios.get(
`https://${host}:${port}/v1/payments`,
options
)

return response.data
}

module.exports = {
sendPayment,
addInvoice,
decodePaymentRequest,
listPayments
}
71 changes: 71 additions & 0 deletions api/schema/resolvers/PaymentResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const bitcoinConversion = require('bitcoin-conversion')
const { validateDatesFormat } = require('../helpers/inputValidation')
const apiModules = require('../../modules')
const { DEFAULT_STRIPE_CURRENCY, STRIPE_SUPPORTED_CURRENCIES } = require('../../config/constants')
const lnd = require('../../handlers/lnd')
const btcPayServer = require('../../handlers/btcPayServer')

module.exports = {

Expand Down Expand Up @@ -149,6 +151,75 @@ module.exports = {
} catch (error) {
throw new Error('Failed to convert USD to SATS: ', error);
}
},
sendPayment: async (root, { contributors }, { models }) => {
const results = []
const onChainAddresses = []

const contributorIds = contributors.map(c => c.id)
const amounts = contributors.map(c => c.amount)

const reflect = promise => promise.then(
value => ({ status: 'fulfilled', value }),
error => ({ status: 'rejected', reason: error })
)

const wallets = await models.Wallet.findAll({
where: {
contributor_id: contributorIds
}
})

const invoices = await Promise.all(wallets.map(async (wallet, i) => {
if (wallet.dataValues.invoice_macaroon) {
const invoice = await lnd.addInvoice(wallet.dataValues.lnd_host, wallet.dataValues.lnd_port, wallet.dataValues.invoice_macaroon, amounts[i])
return invoice.payment_request
} else {
onChainAddresses.push({
address: wallet.dataValues.onchain_address,
amount: amounts[i]
})
return null
}
}))

if (invoices) {
const lndInvoices = invoices.filter(invoice => invoice !== null)

const payLndInvoices = async () => lndInvoices.map(async invoice => {
return reflect(btcPayServer.payLightningInvoice(invoice))
.then(result => {
return result.status === 'fulfilled' ? result.value : { error: result.reason.message, status: result.status }
})
})
const lndInvocesResults = await Promise.all(await payLndInvoices())
results.push(...lndInvocesResults)
}

if (onChainAddresses) {
const payOnChain = async () => onChainAddresses.map(async receiver => {
return reflect(btcPayServer.createOnChainTransaction(receiver.address, String(receiver.amount / 100000000)))
.then(result => {
return result.status === 'fulfilled' ? result.value : { error: result.reason.message, status: result.status }
})
})
const onChainResults = await Promise.all(await payOnChain())
results.push(...onChainResults)
}

results.map(result => {
if (result && !result.error) {
models.Payment.create({
amount: result.paymentRequest ? result.amount / 1000 : result.amount,
external_uuid: result.paymentRequest ? result.paymentRequest : result.transactionHash,
date_incurred: moment.unix(result.createdAt ? result.createdAt : result.timestamp).utc().format('YYYY-MM-DD'),
external_uuid_type: 'bitcoin',
currency: 'SATS'
})
}
})

return results
}
}

Expand Down
16 changes: 16 additions & 0 deletions api/schema/types/PaymentType.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ module.exports = gql`
contributor: Contributor
}
type BTCPayment {
transactionHash: String
paymentRequest: String
amount: String
status: String
error: String
}
input PaymentInput {
amount: Int,
Expand All @@ -32,6 +39,11 @@ module.exports = gql`
date_paid: String
}
input ContributorInput {
id: Int,
amount: Int
}
type Query {
getPaymentById(id: Int!): Payment
getPayments: [Payment]
Expand Down Expand Up @@ -62,6 +74,10 @@ module.exports = gql`
convertUSDtoSATS(
amount: Int!
): Float!
sendPayment(
contributors: [ContributorInput]
): [BTCPayment]
}
`

0 comments on commit d53e269

Please sign in to comment.