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 @@ -30,6 +30,15 @@ notificationDurationShort: 3, notificationDurationLong: 5, newTokenDefaultUrl: 'https://cashtab.com/', + opReturn: { + opReturnPrefixHex: '6a', + opReturnPushDataHex: '04', + opReturnAppPrefixLengthHex: '04', + appPrefixesHex: { + eToken: '534c5000', + cashtab: '00746162', + }, + }, settingsValidation: { fiatCurrency: [ 'usd', @@ -74,6 +83,55 @@ }, }; +export function getETokenEncodingSubstring() { + let encodingStr = + currency.opReturn.opReturnPrefixHex + // 6a + currency.opReturn.opReturnAppPrefixLengthHex + // 04 + currency.opReturn.appPrefixesHex.eToken; // 534c5000 + + return encodingStr; +} + +export function getCashtabEncodingSubstring() { + let encodingStr = + currency.opReturn.opReturnPrefixHex + // 6a + currency.opReturn.opReturnAppPrefixLengthHex + // 04 + currency.opReturn.appPrefixesHex.cashtab; // 00746162 + + return encodingStr; +} + +export function isCashtabOutput(hexStr) { + if (!hexStr || typeof hexStr !== 'string') { + return false; + } + return hexStr.startsWith(getCashtabEncodingSubstring()); +} + +export function isEtokenOutput(hexStr) { + if (!hexStr || typeof hexStr !== 'string') { + return false; + } + return hexStr.startsWith(getETokenEncodingSubstring()); +} + +export function extractCashtabMessage(hexSubstring) { + if (!hexSubstring || typeof hexSubstring !== 'string') { + return ''; + } + let substring = hexSubstring.replace(getCashtabEncodingSubstring(), ''); // remove the cashtab encoding + substring = substring.slice(2); // remove the 2 bytes indicating the size of the next element on the stack e.g. a0 -> 160 bytes + return substring; +} + +export function extractExternalMessage(hexSubstring) { + if (!hexSubstring || typeof hexSubstring !== 'string') { + return ''; + } + let substring = hexSubstring.slice(4); // remove the preceding OP_RETURN prefixes + return substring; +} + export function isValidCashPrefix(addressString) { // Note that this function validates prefix only // Check for prefix included in currency.prefixes array diff --git a/web/cashtab/src/components/Common/__tests__/Ticker.test.js b/web/cashtab/src/components/Common/__tests__/Ticker.test.js --- a/web/cashtab/src/components/Common/__tests__/Ticker.test.js +++ b/web/cashtab/src/components/Common/__tests__/Ticker.test.js @@ -1,5 +1,15 @@ import { ValidationError } from 'ecashaddrjs'; -import { isValidCashPrefix, isValidTokenPrefix, toLegacy } from '../Ticker'; +import { + isValidCashPrefix, + isValidTokenPrefix, + toLegacy, + isCashtabOutput, + isEtokenOutput, + extractCashtabMessage, + extractExternalMessage, + getETokenEncodingSubstring, + getCashtabEncodingSubstring, +} from '../Ticker'; test('Rejects cash address with bitcoincash: prefix', async () => { const result = isValidCashPrefix( @@ -106,3 +116,76 @@ ), ); }); + +test('getCashtabEncodingSubstring() returns the appropriate substring for cashtab message outputs', async () => { + const result = getCashtabEncodingSubstring(); + expect(result).toStrictEqual('6a0400746162'); +}); + +test('getETokenEncodingSubstring() returns the appropriate substring for eToken outputs', async () => { + const result = getETokenEncodingSubstring(); + expect(result).toStrictEqual('6a04534c5000'); +}); + +test('isCashtabOutput() correctly validates a cashtab message output hex', async () => { + const result = isCashtabOutput('6a04007461620b63617368746162756c6172'); + expect(result).toStrictEqual(true); +}); + +test('isCashtabOutput() correctly invalidates an external message output hex', async () => { + const result = isCashtabOutput('6a0c7069616e6f74656e6e697332'); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly handles null input', async () => { + const result = isCashtabOutput(null); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly handles non-string input', async () => { + const result = isCashtabOutput(7623723323); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly invalidates an external message output hex', async () => { + const result = isCashtabOutput( + '6a202731afddf3b83747943f0e650b938ea0670dcae2e08c415f53bd4c6acfd15e09', + ); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly validates an eToken output hex', async () => { + const result = isEtokenOutput( + '6a04534c500001010453454e442069b8431ddecf775393b1b36aa1d0ddcd7b342f1157b9671a03747378ed35ea0d08000000000000012c080000000000002008', + ); + expect(result).toStrictEqual(true); +}); + +test('isEtokenOutput() correctly invalidates an eToken output hex', async () => { + const result = isEtokenOutput( + '5434c500001010453454e442069b8431ddecf775393b1b36aa1d0ddcd7b342f1157b9671a03747378ed35ea0d08000000000000012c080000000000002008', + ); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly handles null input', async () => { + const result = isEtokenOutput(null); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly handles non-string input', async () => { + const result = isEtokenOutput(7623723323); + expect(result).toStrictEqual(false); +}); + +test('extractCashtabMessage() correctly extracts a Cashtab message', async () => { + const result = extractCashtabMessage( + '6a04007461620b63617368746162756c6172', + ); + expect(result).toStrictEqual('63617368746162756c6172'); +}); + +test('extractExternalMessage() correctly extracts an external message', async () => { + const result = extractExternalMessage('6a0d62696e676f656c65637472756d'); + expect(result).toStrictEqual('62696e676f656c65637472756d'); +}); diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -388,7 +388,7 @@ ,