diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -181,6 +181,140 @@ ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); + it('generateOpReturnScript() correctly generates an encrypted message script', () => { + const { generateOpReturnScript } = useBCH(); + 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 { generateOpReturnScript } = useBCH(); + 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 { generateOpReturnScript } = useBCH(); + 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( + '6a0464726f70403163366339633634643730623238356265666537333366313735643066333834353338353736383736626432383062313035383764663831323739643366356504007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', + ); + }); + + it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => { + const { generateOpReturnScript } = useBCH(); + 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( + '6a0464726f7040316336633963363464373062323835626566653733336631373564306633383435333835373638373662643238306231303538376466383132373964336635650400746162', + ); + }); + + it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => { + const { generateOpReturnScript } = useBCH(); + 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 { generateOpReturnScript } = useBCH(); + 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', + ); + }); + it('sends one to many XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); 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 @@ -1395,6 +1395,83 @@ return encryptedEj; }; + /* + * 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 + */ + 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)); + } + + // 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; + }; + const sendXec = async ( BCH, wallet, @@ -1730,5 +1807,6 @@ handleEncryptedOpReturn, getRecipientPublicKey, burnEtoken, + generateOpReturnScript, }; }