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,103 @@ ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); + it('generateOpReturnScript() correctly generates an encrypted message script', async () => { + const { generateOpReturnScript } = useBCH(); + const BCH = new BCHJS(); + const { destinationAddress } = sendBCHMock; + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = true; + const airdropFlag = false; + const airdropTokenId = null; + const expectedPubKeyResponse = { + success: true, + publicKey: + '03451a3e61ae8eb76b8d4cd6057e4ebaf3ef63ae3fe5f441b72c743b5810b6a389', + }; + + BCH.encryption.getPubKey = jest + .fn() + .mockResolvedValue(expectedPubKeyResponse); + const encodedScript = await generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + destinationAddress, + airdropFlag, + airdropTokenId, + ); + expect(encodedScript.toString('hex').slice(0, 12)).toBe('6a0465746162'); + }); + + it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', async () => { + const { generateOpReturnScript } = useBCH(); + const BCH = new BCHJS(); + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = false; + const destinationAddress = + 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed'; + const airdropFlag = false; + + const encodedScript = await generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + destinationAddress, + airdropFlag, + ); + expect(encodedScript.toString('hex')).toBe( + '6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', + ); + }); + + it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', async () => { + const { generateOpReturnScript } = useBCH(); + const BCH = new BCHJS(); + const optionalOpReturnMsg = 'testing generateOpReturnScript()'; + const encryptionFlag = false; + const destinationAddress = + 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed'; + const airdropFlag = true; + const airdropTokenId = + '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; + + const encodedScript = await generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + destinationAddress, + airdropFlag, + airdropTokenId, + ); + expect(encodedScript.toString('hex')).toBe( + '6a0464726f70403163366339633634643730623238356265666537333366313735643066333834353338353736383736626432383062313035383764663831323739643366356504007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', + ); + }); + + it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', async () => { + const { generateOpReturnScript } = useBCH(); + const BCH = new BCHJS(); + const optionalOpReturnMsg = null; + const encryptionFlag = false; + const destinationAddress = + 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed'; + const airdropFlag = true; + const airdropTokenId = + '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; + + const encodedScript = await generateOpReturnScript( + BCH, + optionalOpReturnMsg, + encryptionFlag, + destinationAddress, + airdropFlag, + airdropTokenId, + ); + expect(encodedScript.toString('hex')).toBe( + '6a0464726f7040316336633963363464373062323835626566653733336631373564306633383435333835373638373662643238306231303538376466383132373964336635650400746162', + ); + }); + 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,86 @@ 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 = async ( + BCH, + optionalOpReturnMsg, + encryptionFlag, + destinationAddress, + airdropFlag, + airdropTokenId, + ) => { + 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 + let encryptedEj; + try { + encryptedEj = await handleEncryptedOpReturn( + BCH, + destinationAddress, + optionalOpReturnMsg, + ); + } catch (err) { + console.log( + `useBCH.generateOpReturnScript() encryption error.`, + ); + throw err; + } + + // add the encrypted cashtab messaging prefix 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); + return null; + } + + const data = BCH.Script.encode(script); + return data; + }; + const sendXec = async ( BCH, wallet, @@ -1730,5 +1810,6 @@ handleEncryptedOpReturn, getRecipientPublicKey, burnEtoken, + generateOpReturnScript, }; }