From 49bd2d16e34f3ce02844baedbf0ae84a70847c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Wed, 23 Mar 2022 23:48:49 +0000 Subject: [PATCH] feat: galoy wallet plugin --- lib/plugins/wallet/galoy/galoy.js | 253 +++++++++++++++++++++++++++++- 1 file changed, 245 insertions(+), 8 deletions(-) diff --git a/lib/plugins/wallet/galoy/galoy.js b/lib/plugins/wallet/galoy/galoy.js index 49c849c5..0c771602 100644 --- a/lib/plugins/wallet/galoy/galoy.js +++ b/lib/plugins/wallet/galoy/galoy.js @@ -1,5 +1,38 @@ + +const _ = require('lodash/fp') + +const axios = require('axios') const NAME = 'LN' const SUPPORTED_COINS = ['LN'] +const PHONE = '+3500000000000' + +const URI = 'https://api.staging.galoy.io/graphql' + +const BN = require('../../../bn') + +function request (graphqlQuery, token) { + const headers = { + 'content-type': 'application/json', + 'Authorization': token || '' + } + return Promise.resolve(true) + .then(() => { + return axios({ + method: 'post', + url: URI, + headers: headers, + data: graphqlQuery + }) + }) + .then(r => { + console.log(r) + if (r.error) throw r.error + return r.data + }) + .catch(err => { + throw new Error(err) + }) +} function checkCryptoCode (cryptoCode) { if (!SUPPORTED_COINS.includes(cryptoCode)) { @@ -9,36 +42,234 @@ function checkCryptoCode (cryptoCode) { return Promise.resolve() } -function getWallet () { - // Create wallet instance +function getGaloyAccount (token) { + const accountInfo = { + 'operationName': 'me', + 'query': `query me { + me { + createdAt + defaultAccount { + ...AccountFragment + } + id + phone + twoFAEnabled + username + } + }`, + 'variables': {} + } + return request(accountInfo, token) + .then(r => { + return r.data.data.defaultAccount + }) + .catch(err => { + throw new Error(err) + }) +} + +function fetchAuthToken (config) { + const phone = config.phone + // userRequestAuthCode deprecated? + const regularRequestAuthCode = { + 'operationName': 'userRequestAuthCode', + 'query': `mutation userRequestAuthCode($input: UserRequestAuthCodeInput!) { + userRequestAuthCode(input: $input) { + errors { + ...ErrorFragment + } + success + } + }`, + 'variables': { 'input': { 'phone': `${phone}` } } + } + const createCaptcha = { + 'operationName': 'captchaCreateChallenge', + 'query': `mutation captchaCreateChallenge { + captchaCreateChallenge { + errors { + ...ErrorFragment + } + result { + ...CaptchaCreateChallengeResultFragment + } + } + }`, + 'variables': {} + } + const captchaRequestAuthCode = { + 'operationName': 'captchaRequestAuthCode', + 'query': `mutation captchaRequestAuthCode($input: CaptchaRequestAuthCodeInput!) { + captchaRequestAuthCode(input: $input) { + errors { + ...ErrorFragment + } + success + } + }`, + 'variables': {} + } + + return request(regularRequestAuthCode) + .then(r => { + console.log(r) + return r + }) +} + +function isLightning (address) { + return address.substr(0, 2) === 'ln' } function sendCoins (account, tx, settings, operatorId) { - // const { toAddress, cryptoAtoms, cryptoCode } = tx - return {} + const { toAddress, cryptoAtoms, cryptoCode } = tx + // Is walletId a mandatory field? + const sendOnChain = { + 'operationName': 'onChainPaymentSend', + 'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { + onChainPaymentSend(input: $input) { + errors { + ...ErrorFragment + } + status + } + }`, + 'variables': { 'input': { 'address': `${toAddress}`, 'amount': `${cryptoAtoms}` } } + } + const sendLN = { + 'operationName': 'lnInvoicePaymentSend', + 'query': `mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) { + lnInvoicePaymentSend(input: $input) { + errors { + ...ErrorFragment + } + status + } + }`, + 'variables': { 'input': { 'paymentRequest': `${toAddress}` } } + } + return checkCryptoCode(cryptoCode) + .then(() => fetchAuthToken({ phone: PHONE })) + .then(authToken => { + if (isLightning) { + return request(sendLN, authToken) + } + return request(sendOnChain, authToken) + }) + .then(result => { + return result.data + }) + .catch(err => { + throw err + }) +} + +function newOnChainAddress (walletId, token) { + const createOnChainAddress = { + 'operationName': 'onChainAddressCreate', + 'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) { + onChainAddressCreate(input: $input) { + address + errors { + ...ErrorFragment + } + } + }`, + 'variables': { 'input': { 'walletId': `${walletId}` } } + } + return request(createOnChainAddress, token) + .then(result => { + return result.data.onChainAddressCreate.address + }) + .catch(err => { + throw err + }) +} + +function newInvoice (walletId, cryptoAtoms, token) { + const createInvoice = { + 'operationName': 'lnInvoiceCreate', + 'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { + lnInvoiceCreate(input: $input) { + errors { + ...ErrorFragment + } + invoice { + ...LnInvoiceFragment + } + } + }`, + 'variables': { 'input': { 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } } + } + return request(createInvoice, token) + .then(result => { + return result.data.lnInvoiceCreate.invoice.paymentRequest + }) + .catch(err => { + throw err + }) } function balance (account, cryptoCode, settings, operatorId) { return checkCryptoCode(cryptoCode) - .then(() => getWallet(account, cryptoCode)) + .then(() => fetchAuthToken({ phone: PHONE })) + .then(authToken => getGaloyAccount(authToken)) + .then(account => { + // account has a list of wallets, should we consider the balance of each one? + // for now we'll pick the first BTC wallet that matches the defaultWalletId + const wallet = _.filter(wallet => wallet.walletCurrency === cryptoCode && wallet.id === account.defaultWalletId)(account.wallets) + return new BN(wallet.balance || 0) + }) + .catch(err => { + throw err + }) } function newAddress (account, info, tx, settings, operatorId) { + const { cryptoAtoms, cryptoCode } = tx return checkCryptoCode(info.cryptoCode) + .then(() => fetchAuthToken({ phone: PHONE })) + .then(authToken => [getGaloyAccount(authToken), authToken]) + .then(([account, authToken]) => { + const wallet = _.filter(wallet => wallet.walletCurrency === cryptoCode && wallet.id === account.defaultWalletId)(account.wallets) + const promises = [ + newOnChainAddress(wallet.id, authToken), + newInvoice(wallet.id, cryptoAtoms, authToken) + ] + return Promise.all(promises) + }) + .then(([onChainAddress, invoice]) => { + return `bitcoin:${onChainAddress}?amount=${cryptoAtoms}?lightning=${invoice}` + }) } function getStatus (account, tx, requested, settings, operatorId) { - const { cryptoCode } = tx - return checkCryptoCode(cryptoCode).then(() => {}) + // Type Transaction has a field status + // but we're not sure if it's interchangeable with our status definition } function newFunding (account, cryptoCode, settings, operatorId) { + // Has to be a regular BTC address return checkCryptoCode(cryptoCode) + .then(() => fetchAuthToken({ phone: PHONE })) + .then(authToken => [getGaloyAccount(authToken), authToken]) + .then(([account, authToken]) => { + const wallet = _.filter(wallet => wallet.walletCurrency === cryptoCode && wallet.id === account.defaultWalletId)(account.wallets) + return newOnChainAddress(wallet.id, authToken) + .then(onChainAddress => [onChainAddress, wallet.balance]) + }) + .then(([onChainAddress, balance]) => { + // Missing pending balance + return { + fundingConfirmedBalance: new BN(balance), + fundingAddress: onChainAddress + } + }) } function cryptoNetwork (account, cryptoCode, settings, operatorId) { return checkCryptoCode(cryptoCode) - .then(() => {}) + .then(() => account.environment === 'test' ? 'test' : 'main') } function checkBlockchainStatus (cryptoCode) { @@ -46,6 +277,12 @@ function checkBlockchainStatus (cryptoCode) { .then(() => Promise.resolve('ready')) } +sendCoins({}, { + toAddress: 'lnbc10n1p3z7tpkpp5fjkptx4xtnh5n5vyrrhrw25f86mv0rwyu9a8tm3lrwnkl4u58zqdqg23jhxap3cqzpgxqyz5vqsp5g239kch2r7q9dty8rgs2h94h0d6tp8ssws9zte9qzvss2fr729zs9qyyssq86khhqz86dhftteqd0ymad32dfrdfdmdac8jw359wn4s0fx5gfyrectl4e49pt38gculfk3xeljv5ygd8ddry0m0z38lqt23j8aytycqrd4med', + cryptoCode: 'LN', + cryptoAtoms: 123123 +}, {}, {}) + module.exports = { NAME, balance,