diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json --- a/web/cashtab/package-lock.json +++ b/web/cashtab/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "cashtab", "version": "1.0.0", "dependencies": { "@ant-design/icons": "^4.3.0", @@ -31,6 +32,7 @@ "react-easy-crop": "^3.5.3", "react-ga": "^3.3.0", "react-image": "^4.0.3", + "react-input-emoji": "^4.0.9", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "styled-components": "^4.4.0", @@ -1892,7 +1894,6 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -3528,7 +3529,6 @@ "version": "0.8.0", "license": "MIT", "dependencies": { - "text-encoding": "^0.6.4", "ts-custom-error": "^2.2.1" }, "optionalDependencies": { @@ -3908,7 +3908,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -3961,7 +3960,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -5874,7 +5872,6 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -8246,6 +8243,18 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, + "node_modules/emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "dev": true, @@ -8503,8 +8512,7 @@ "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" + "optionator": "^0.8.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -10732,7 +10740,6 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", - "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "bin": { @@ -13062,8 +13069,7 @@ "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" + "optionator": "^0.8.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -13202,7 +13208,6 @@ "@jest/types": "^24.9.0", "anymatch": "^2.0.0", "fb-watchman": "^2.0.0", - "fsevents": "^1.2.7", "graceful-fs": "^4.1.15", "invariant": "^2.2.4", "jest-serializer": "^24.9.0", @@ -13712,7 +13717,6 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", @@ -15039,9 +15043,6 @@ "version": "4.0.0", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.6" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15184,13 +15185,6 @@ "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "native-request": "^1.0.5", - "source-map": "~0.6.0", "tslib": "^1.10.0" }, "bin": { @@ -22461,6 +22455,23 @@ "react-dom": ">=16.8" } }, + "node_modules/react-input-emoji": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/react-input-emoji/-/react-input-emoji-4.0.9.tgz", + "integrity": "sha512-IkgiaOMNXMYrSJETivEfARHYjZrZ3kVAhjKtV6htYX/cQ3wz0sOiKiGQdHOAPuBRbNdfDHr1WaWoFWT4lCJvwg==", + "dependencies": { + "emoji-mart": "3.0.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "prop-types": ">=15.7.2", + "react": ">=17.0.x", + "react-dom": ">=17.0.x" + } + }, "node_modules/react-is": { "version": "17.0.1", "dev": true, @@ -27048,10 +27059,8 @@ "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" + "neo-async": "^2.5.0" }, "optionalDependencies": { "chokidar": "^3.4.1", @@ -27140,7 +27149,6 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", - "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", @@ -27598,7 +27606,6 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", - "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", @@ -35021,6 +35028,15 @@ "version": "0.7.2", "dev": true }, + "emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "requires": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + } + }, "emoji-regex": { "version": "9.2.2", "dev": true @@ -44437,6 +44453,14 @@ "version": "4.0.3", "requires": {} }, + "react-input-emoji": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/react-input-emoji/-/react-input-emoji-4.0.9.tgz", + "integrity": "sha512-IkgiaOMNXMYrSJETivEfARHYjZrZ3kVAhjKtV6htYX/cQ3wz0sOiKiGQdHOAPuBRbNdfDHr1WaWoFWT4lCJvwg==", + "requires": { + "emoji-mart": "3.0.1" + } + }, "react-is": { "version": "17.0.1", "dev": true diff --git a/web/cashtab/package.json b/web/cashtab/package.json --- a/web/cashtab/package.json +++ b/web/cashtab/package.json @@ -28,6 +28,7 @@ "react-easy-crop": "^3.5.3", "react-ga": "^3.3.0", "react-image": "^4.0.3", + "react-input-emoji": "^4.0.9", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "styled-components": "^4.4.0", diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -10,12 +10,14 @@ CaretRightOutlined, SettingFilled, AppstoreAddOutlined, + TeamOutlined, } from '@ant-design/icons'; import Wallet from '@components/Wallet/Wallet'; import Tokens from '@components/Tokens/Tokens'; import Send from '@components/Send/Send'; import SendToken from '@components/Send/SendToken'; import Configure from '@components/Configure/Configure'; +import Social from '@components/Social/Social'; import NotFound from '@components/NotFound'; import CashTab from '@assets/cashtab_xec.png'; import './App.css'; @@ -106,10 +108,10 @@ outline: none; } cursor: pointer; - padding: 24px 12px 12px 12px; - margin: 0 28px; + padding: 24px 6px 12px 12px; + margin: 0 18px; @media (max-width: 475px) { - margin: 0 20px; + margin: 0 10px; } @media (max-width: 420px) { margin: 0 12px; @@ -119,7 +121,7 @@ } background-color: ${props => props.theme.footer.background}; border: none; - font-size: 10.5px; + font-size: 10px; font-weight: bold; .anticon { display: block; @@ -306,6 +308,13 @@ /> )} /> + <Route path="/social"> + <Social + passLoadingStatus={ + setLoadingUtxosAfterSend + } + /> + </Route> <Route path="/configure"> <Configure /> </Route> @@ -339,6 +348,15 @@ <CaretRightOutlined /> Send </NavButton> + + <NavButton + active={selectedKey === 'social'} + onClick={() => history.push('/social')} + > + <TeamOutlined /> + Social + </NavButton> + <NavButton active={selectedKey === 'configure'} onClick={() => history.push('/configure')} diff --git a/web/cashtab/src/components/Common/Notifications.js b/web/cashtab/src/components/Common/Notifications.js --- a/web/cashtab/src/components/Common/Notifications.js +++ b/web/cashtab/src/components/Common/Notifications.js @@ -43,6 +43,25 @@ }); }; +// Success Notifications: +const postSocialMsgNotification = link => { + const notificationStyle = getDeviceNotificationStyle(); + notification.success({ + message: 'Success', + description: ( + <a href={link} target="_blank" rel="noopener noreferrer"> + <Paragraph> + Social post submitted successfully. Click to view in block + explorer. + </Paragraph> + </a> + ), + duration: currency.notificationDurationShort, + icon: <CashReceivedNotificationIcon />, + style: notificationStyle, + }); +}; + const createTokenNotification = link => { const notificationStyle = getDeviceNotificationStyle(); notification.success({ @@ -179,4 +198,5 @@ eTokenReceivedNotification, errorNotification, messageSignedNotification, + postSocialMsgNotification, }; diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -15,6 +15,8 @@ cashDecimals: 2, blockExplorerUrl: 'https://explorer.bitcoinabc.org', tokenExplorerUrl: 'https://explorer.be.cash', + cashtabSocialHubAddr: + 'bitcoincash:qz305assdkl639zvem0d3g5vj5wtpyzr55t0qhm676', // ecash:qz305assdkl639zvem0d3g5vj5wtpyzr55jz5uqqcd blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', tokenName: 'eToken', tokenTicker: 'eToken', @@ -36,6 +38,7 @@ eToken: '534c5000', cashtab: '00746162', cashtabEncrypted: '65746162', + cashtabSocial: '73746162', }, encryptedMsgCharLimit: 94, unencryptedMsgCharLimit: 160, diff --git a/web/cashtab/src/components/Social/Social.js b/web/cashtab/src/components/Social/Social.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/components/Social/Social.js @@ -0,0 +1,261 @@ +import React, { useState, useEffect, createElement } from 'react'; +import InputEmoji from 'react-input-emoji'; +import PropTypes from 'prop-types'; +import { WalletContext } from '@utils/context'; +import { AntdFormWrapper } from '@components/Common/EnhancedInputs'; +import { + Form, + Modal, + Spin, + Button, + Comment, + Tooltip, + Avatar, + Divider, + Drawer, +} from 'antd'; +import moment from 'moment'; +import useBCH from '@hooks/useBCH'; +import { getWalletState } from '@utils/cashMethods'; +import { + UserOutlined, + DislikeOutlined, + LikeOutlined, + DislikeFilled, + LikeFilled, +} from '@ant-design/icons'; +import styled, { css } from 'styled-components'; +import { + femaleAvatarSrc, + maleAvatarSrc, + ExampleReply, + socialActions, +} from '@components/Social/__mocks__/mockSocialInputs'; +import { postSocialMsgNotification } from '@components/Common/Notifications'; +import { currency } from '@components/Common/Ticker.js'; + +const SocialMessage = styled.div` + text-align: left; +`; + +// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest +const Social = ({ jestBCH, passLoadingStatus }) => { + const ContextValue = React.useContext(WalletContext); + const { wallet, cashtabSettings } = ContextValue; + const walletState = getWalletState(wallet); + const { balances, slpBalancesAndUtxos } = walletState; + + const [bchObj, setBchObj] = useState(false); + + useEffect(() => { + // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); + const BCH = jestBCH ? jestBCH : getBCH(); + + // set the BCH instance to state, for other functions to reference + setBchObj(BCH); + }, []); + + const [socialMessage, setSocialMessage] = useState(''); + const [socialFeedMode, setSocialFeedMode] = useState(''); + + // social action state variables + const [likes, setLikes] = useState(0); + const [dislikes, setDislikes] = useState(0); + const [action, setAction] = useState(null); + + // identity management drawer state variables + const [identityDrawVisible, setIdentityDrawVisible] = useState(false); + + const showIdentityDrawer = () => { + setIdentityDrawVisible(true); + }; + + const onIdentityDrawerClose = () => { + setIdentityDrawVisible(false); + }; + + // social interaction functions + const like = () => { + setLikes(1); + setDislikes(0); + setAction('liked'); + // increment Like-count offchain based on tx hash of post + }; + + const dislike = () => { + setLikes(0); + setDislikes(1); + setAction('disliked'); + // increment Dislike-count offchain based on tx hash of post + }; + + const { getBCH, sendSocialPost } = useBCH(); + + const postSocialMsg = async () => { + try { + const link = await sendSocialPost( + bchObj, + wallet, + slpBalancesAndUtxos.nonSlpUtxos, + currency.defaultFee, + socialMessage.toString(), + ); + postSocialMsgNotification(link); + } catch (err) { + console.log('Social.postSocialMsg() error: ' + err); + throw err; + } + }; + + const handleSocialPostChange = e => { + const { value } = e.target; + setSocialMessage(value); + }; + + return ( + <> + {/* Identity Management drawer*/} +   + <Avatar + size="medium" + icon={<UserOutlined />} + onClick={showIdentityDrawer} + /> + <Drawer + title="Cashtab Social Profile" + placement="right" + onClose={onIdentityDrawerClose} + visible={identityDrawVisible} + > + <p> + <b>Profile Photo:</b>{' '} + <Avatar size="medium" icon={<UserOutlined />} /> + </p> + <p> + <b>Name:</b> Cain{' '} + </p> + <p> + <b>Twitter:</b> @cainbcha + </p> + <p> + <b>Telegram:</b> @cainbcha + </p> + </Drawer> + {/* end of Identity Management drawer*/} + <br /> + <br /> + <AntdFormWrapper> + <Form + style={{ + width: 'auto', + }} + > + <Form.Item> + <InputEmoji + onChange={e => + handleSocialPostChange({ + target: { + name: 'socialPostContent', + value: e, + }, + }) + } + required={true} + placeholder="What's up?" + value={socialMessage} + /> + <Button + onClick={postSocialMsg} + style={{ + color: 'white', + backgroundColor: '#CD0BC3', + }} + > + Post + </Button> + </Form.Item> + </Form> + </AntdFormWrapper> + <Divider plain>Cashtab Socials</Divider> + <div value={socialFeedMode}> + <Button + type="link" + style={{ color: 'white', backgroundColor: '#CD0BC3' }} + > + Latest + </Button> + <Button type="link">Top</Button> + <Button type="link">Replies</Button> + <Button type="link">Posts</Button> + </div> + <br /> + {/* the real diff will call retrieveSocialFeed() in useBCH.js to retrieve the social feed */} + {/* mock social messages - real diff will be iterating through an array of messages */} + <SocialMessage> + <Comment + actions={socialActions} + author={<a style={{ color: 'Black' }}>Pierre</a>} + avatar={<Avatar src={maleAvatarSrc} />} + content={ + <p>Thinking of going for a walk and tend to my goats</p> + } + datetime={ + <Tooltip title={moment().format('YYYY-MM-DD HH:mm:ss')}> + <span>{moment().fromNow()}</span> + </Tooltip> + } + style={{ color: 'Black' }} + /> + </SocialMessage> + <SocialMessage> + <Comment + actions={socialActions} + author={<a style={{ color: 'Black' }}>Alita</a>} + avatar={<Avatar src={femaleAvatarSrc} />} + content={ + <p> + I accidentally scolded myself with hot coffee + again....😑 + </p> + } + datetime={ + <Tooltip title={moment().format('YYYY-MM-DD HH:mm:ss')}> + <span>{moment().fromNow()}</span> + </Tooltip> + } + style={{ color: 'Black' }} + > + <ExampleReply /> + </Comment> + </SocialMessage> + <SocialMessage> + <Comment + actions={socialActions} + author={<a style={{ color: 'Black' }}>New Guy</a>} + avatar={<Avatar src={maleAvatarSrc} />} + content={<p>Please burn 99% of supply!</p>} + datetime={ + <Tooltip title={moment().format('YYYY-MM-DD HH:mm:ss')}> + <span>{moment().fromNow()}</span> + </Tooltip> + } + style={{ color: 'Black' }} + /> + </SocialMessage> + {/* end of mock social messages */} + </> + ); +}; + +Social.defaultProps = { + passLoadingStatus: status => { + console.log(status); + }, +}; + +Social.propTypes = { + jestBCH: PropTypes.object, + passLoadingStatus: PropTypes.func, +}; + +export default Social; diff --git a/web/cashtab/src/components/Social/__mocks__/mockSocialInputs.js b/web/cashtab/src/components/Social/__mocks__/mockSocialInputs.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/components/Social/__mocks__/mockSocialInputs.js @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Comment, Tooltip, Avatar } from 'antd'; +export const femaleAvatarSrc = + 'https://avataaars.io/?avatarStyle=Transparent&topType=LongHairStraight&accessoriesType=Blank&hairColor=Black&facialHairType=Blank&clotheType=Hoodie&clotheColor=Gray01&eyeType=Squint&eyebrowType=Default&mouthType=Default&skinColor=Light'; +export const maleAvatarSrc = + 'https://avataaars.io/?avatarStyle=Transparent&topType=ShortHairFrizzle&accessoriesType=Blank&hairColor=Black&facialHairType=Blank&clotheType=BlazerShirt&clotheColor=Gray01&eyeType=Squint&eyebrowType=Default&mouthType=Default&skinColor=Light'; +export const maleAvatarSrcAlt = + 'https://avataaars.io/?avatarStyle=Transparent&topType=ShortHairShortFlat&accessoriesType=Blank&hairColor=Black&facialHairType=Blank&clotheType=ShirtVNeck&clotheColor=Gray01&eyeType=Squint&eyebrowType=Default&mouthType=Default&skinColor=Light'; + +/* +export const [likes, setLikes] = useState(0); +export const [dislikes, setDislikes] = useState(0); +export const [action, setAction] = useState(null); + +export const like = () => { + setLikes(1); + setDislikes(0); + setAction('liked'); + // increment Like-count offchain +}; + +export const dislike = () => { + setLikes(0); + setDislikes(1); + setAction('disliked'); + // increment Dislike-count offchain +}; +*/ + +export const ExampleReply = ({ children }) => ( + <Comment + actions={socialActions} + author={<a>Luhushan</a>} + avatar={<Avatar src={maleAvatarSrcAlt} />} + content={<p>Lolz!</p>} + > + {children} + </Comment> +); + +export const socialActions = [ + <Tooltip key="comment-basic-like" title="Like"> + <span onClick={'#'}> + {/*createElement(action === 'liked' ? LikeFilled : LikeOutlined)*/} + <span className="comment-action">{/*likes*/}</span> + </span> + </Tooltip>, + <Tooltip key="comment-basic-dislike" title="Dislike"> + <span onClick={'#'}> + {/*React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)*/} + <span className="comment-action">{/*dislikes*/}</span> + </span> + </Tooltip>, + <span key="comment-basic-reply-to">Reply</span>, + <span key="comment-basic-reply-to">Follow</span>, + <span key="comment-basic-reply-to">Tip XEC</span>, + <span key="comment-basic-reply-to">Tip eToken</span>, +]; diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1480,6 +1480,181 @@ } }; + const sendSocialPost = async ( + BCH, + wallet, + utxos, + feeInSatsPerByte, + socialPostMsg, + ) => { + try { + const inputUtxos = []; + let transactionBuilder; + + // instance of transaction builder + if (process.env.REACT_APP_NETWORK === `mainnet`) + transactionBuilder = new BCH.TransactionBuilder(); + else transactionBuilder = new BCH.TransactionBuilder('testnet'); + + const satoshisToSend = currency.dustSats; + + // Start of building the OP_RETURN output. + if ( + socialPostMsg && + typeof socialPostMsg !== 'undefined' && + socialPostMsg.trim() !== '' + ) { + // only build the script if the social post content is not empty + const script = [ + BCH.Script.opcodes.OP_RETURN, // 6a + Buffer.from( + currency.opReturn.appPrefixesHex.cashtabSocial, + 'hex', + ), // 73746162 + // Additional prefix hex here for replies to parent posts + Buffer.from(socialPostMsg), + ]; + const data = BCH.Script.encode(script); + transactionBuilder.addOutput(data, 0); + } else { + throw new Error( + 'useBCH.sendSocialPost() error: invalid post content', + ); + } + // End of building the OP_RETURN output. + + let originalAmount = new BigNumber(0); + let txFee = 0; + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i]; + originalAmount = originalAmount.plus(utxo.value); + const vout = utxo.vout; + const txid = utxo.txid; + // add input with txid and index of vout + transactionBuilder.addInput(txid, vout); + + inputUtxos.push(utxo); + txFee = calcFee(BCH, inputUtxos, 2, feeInSatsPerByte); + + if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { + break; + } + } + + // Get change address from sending utxos + // fall back to what is stored in wallet + let REMAINDER_ADDR; + + // Validate address + let isValidChangeAddress; + try { + REMAINDER_ADDR = inputUtxos[0].address; + isValidChangeAddress = + BCH.Address.isCashAddress(REMAINDER_ADDR); + } catch (err) { + isValidChangeAddress = false; + } + if (!isValidChangeAddress) { + REMAINDER_ADDR = wallet.Path1899.cashAddress; + } + + // amount to send back to the remainder address. + const remainder = originalAmount.minus(satoshisToSend).minus(txFee); + + if (remainder.lt(0)) { + const error = new Error(`Insufficient funds`); + error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS; + throw error; + } + + transactionBuilder.addOutput( + BCH.Address.toCashAddress(currency.cashtabSocialHubAddr), + parseInt(satoshisToSend), + ); + + if (remainder.gte(new BigNumber(currency.dustSats))) { + transactionBuilder.addOutput( + REMAINDER_ADDR, + parseInt(remainder), + ); + } + + // Sign the transactions with the HD node. + for (let i = 0; i < inputUtxos.length; i++) { + const utxo = inputUtxos[i]; + transactionBuilder.sign( + i, + BCH.ECPair.fromWIF(utxo.wif), + undefined, + transactionBuilder.hashTypes.SIGHASH_ALL, + utxo.value, + ); + } + + // build tx + const tx = transactionBuilder.build(); + // output rawhex + const hex = tx.toHex(); + + // Broadcast transaction to the network + const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); + + if (txidStr && txidStr[0]) { + console.log(`${currency.ticker} txid`, txidStr[0]); + } + let link; + if (process.env.REACT_APP_NETWORK === `mainnet`) { + link = `${currency.blockExplorerUrl}/tx/${txidStr}`; + } else { + link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; + } + return link; + } catch (err) { + if (err.error === 'insufficient priority (code 66)') { + err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; + } else if (err.error === 'txn-mempool-conflict (code 18)') { + err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; + } else if (err.error === 'Network Error') { + err.code = SEND_BCH_ERRORS.NETWORK_ERROR; + } else if ( + err.error === + 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)' + ) { + err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; + } + console.log(`error: `, err); + throw err; + } + }; + + /* pseudo code + * @param socialFeedMode: sort feed by Latest, Top, Replies and Posts + const retrieveSocialFeed = async ( socialFeedMode, wallet ) => { + + const feedRepositoryAddress = currency.cashtabSocialHubAddr; + let socialFeedArray = []; + + try { + if (socialFeedMode === 'latest') { + socialFeedArray = bch-api call result passing in feedRepositoryAddress + } else if (socialFeedMode === 'top') { + socialFeedArray = bch-api call result passing in feedRepositoryAddress + } else if (socialFeedMode === 'replies') { + socialFeedArray = bch-api call result passing in feedRepositoryAddress + } else if (socialFeedMode === 'posts') { + socialFeedArray = bch-api call result passing in feedRepositoryAddress + } else { + throw error('unrecognized social feed mode'); + } + } catch (err) { + throw err; + } + + return socialFeedArray; + }; + + */ + const getBCH = (apiIndex = 0) => { let ConstructedSlpWallet; @@ -1509,5 +1684,7 @@ getTokenStats, handleEncryptedOpReturn, getRecipientPublicKey, + sendSocialPost, + //retrieveSocialFeed, }; } diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -57,6 +57,12 @@ ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; + } else if ( + i === 0 && + message === currency.opReturn.appPrefixesHex.cashtabSocial + ) { + // add the Cashtab encryption prefix to array + resultArray[i] = currency.opReturn.appPrefixesHex.cashtabSocial; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message;