diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -28,6 +28,7 @@ parseChronikTx, checkWalletForTokenInfo, isActiveWebsocket, + generateOpReturnScript, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { @@ -172,6 +173,131 @@ disconnectedWebsocketAlpha, unsubscribedWebsocket, } from '../__mocks__/chronikWs'; +import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore + +it('generateOpReturnScript() correctly generates an encrypted message script', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = true; + const airdropFlag = false; + const airdropTokenId = null; + const mockEncryptedEj = + '04688f9907fe3c7c0b78a73c4ab4f75e15e7e2b79641add519617086126fe6f6b1405a14eed48e90c9c8c0fc77f0f36984a78173e76ce51f0a44af94b59e9da703c9ff82758cfdb9cc46437d662423400fb731d3bfc1df0599279356ca261213fbb40d398c041e1bac966afed1b404581ab1bcfcde1fa039d53b7c7b70e8edf26d64bea9fbeed24cc80909796e6af5863707fa021f2a2ebaa2fe894904702be19d'; + + const encodedScript = generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + mockEncryptedEj, + ); + expect(encodedScript.toString('hex').slice(0, 12)).toBe('6a0465746162'); +}); + +it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = false; + const airdropFlag = false; + + const encodedScript = generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + ); + expect(encodedScript.toString('hex')).toBe( + '6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', + ); +}); + +it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = false; + const airdropFlag = true; + const airdropTokenId = + '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; + + const encodedScript = generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + ); + expect(encodedScript.toString('hex')).toBe( + '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', + ); +}); + +it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = null; + const encryptionFlag = false; + const airdropFlag = true; + const airdropTokenId = + '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; + + const encodedScript = generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + ); + expect(encodedScript.toString('hex')).toBe( + '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', + ); +}); + +it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = null; + const encryptionFlag = true; + const airdropFlag = false; + const airdropTokenId = null; + const mockEncryptedEj = null; // invalid given encryptionFlag is true + let thrownError; + + try { + generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + mockEncryptedEj, + ); + } catch (err) { + thrownError = err; + } + expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input'); +}); + +it('generateOpReturnScript() correctly throws an error on an invalid airdrop input', () => { + const BCH = new BCHJS(); + const optionalOpReturnMsg = null; + const encryptionFlag = false; + const airdropFlag = true; + const airdropTokenId = null; // invalid given airdropFlag is true + + let thrownError; + + try { + generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + ); + } catch (err) { + thrownError = err; + } + expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input'); +}); describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { 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 @@ -8,6 +8,80 @@ import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; +/* + * Generates an OP_RETURN script to reflect the various send XEC permutations + * involving messaging, encryption, eToken IDs and airdrop flags. + * + * Returns the final encoded script object + */ +export const generateOpReturnScript = ( + BCH, + optionalOpReturnMsg, + encryptionFlag, + airdropFlag, + airdropTokenId, + encryptedEj, +) => { + // encrypted mesage is mandatory when encryptionFlag is true + // airdrop token id is mandatory when airdropFlag is true + if ( + !BCH || + (encryptionFlag && !encryptedEj) || + (airdropFlag && !airdropTokenId) + ) { + throw new Error('Invalid OP RETURN script input'); + } + + let script = [BCH.Script.opcodes.OP_RETURN]; // initialize script with the OP_RETURN op code (6a) + try { + if (encryptionFlag) { + // if the user has opted to encrypt this message + + // add the encrypted cashtab messaging prefix and encrypted msg to script + script.push( + Buffer.from( + currency.opReturn.appPrefixesHex.cashtabEncrypted, + 'hex', + ), // 65746162 + ); + + // add the encrypted message to script + script.push(Buffer.from(encryptedEj)); + } else { + // this is an un-encrypted message + + if (airdropFlag) { + // if this was routed from the airdrop component + // add the airdrop prefix to script + script.push( + Buffer.from( + currency.opReturn.appPrefixesHex.airdrop, + 'hex', + ), // drop + ); + // add the airdrop token ID to script + script.push(Buffer.from(airdropTokenId, 'hex')); + } + + // add the cashtab prefix to script + script.push( + Buffer.from(currency.opReturn.appPrefixesHex.cashtab, 'hex'), // 00746162 + ); + + // add the un-encrypted message to script if supplied + if (optionalOpReturnMsg) { + script.push(Buffer.from(optionalOpReturnMsg)); + } + } + } catch (err) { + console.log('Error in generateOpReturnScript(): ' + err); + throw err; + } + + const data = BCH.Script.encode(script); + return data; +}; + export function parseOpReturn(hexStr) { if ( !hexStr ||