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 @@ -38,6 +38,7 @@ eToken: '534c5000', cashtab: '00746162', cashtabEncrypted: '65746162', + airdrop: '64726f70', }, encryptedMsgCharLimit: 94, unencryptedMsgCharLimit: 160, diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -161,6 +161,8 @@ const [messageVerificationSigError, setMessageVerificationSigError] = useState(false); + const [airdropFlag, setAirdropFlag] = useState(false); + const userLocale = navigator.language; const clearInputForms = () => { setFormData({ @@ -236,6 +238,8 @@ value: location.state.airdropRecipients, }, }); + + setAirdropFlag(true); } // Do not set txInfo in state if query strings are not present @@ -339,9 +343,14 @@ opReturnMsg, true, // indicate send mode is one to many cleanAddressAndValueArray, + null, + null, + false, // one to many tx msg can't be encrypted + airdropFlag, ); sendXecNotification(link); clearInputForms(); + setAirdropFlag(false); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } diff --git a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js --- a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js +++ b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js @@ -15,6 +15,7 @@ replyAddress: null, tokenTx: false, decryptionSuccess: false, + airdropFlag: false, txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', }, ]; @@ -35,6 +36,7 @@ replyAddress: null, tokenTx: false, decryptionSuccess: false, + airdropFlag: false, txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', }, ]; @@ -55,6 +57,7 @@ replyAddress: null, tokenTx: true, decryptionSuccess: false, + airdropFlag: false, txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', }, ]; @@ -74,6 +77,7 @@ replyAddress: null, tokenTx: true, decryptionSuccess: false, + airdropFlag: false, txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', }, ]; @@ -92,6 +96,7 @@ isCashtabMessage: false, isEncryptedMessage: false, decryptionSuccess: false, + airdropFlag: false, txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', }, ]; @@ -110,6 +115,7 @@ isCashtabMessage: true, isEncryptedMessage: false, decryptionSuccess: false, + airdropFlag: false, txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', }, ]; @@ -128,6 +134,7 @@ outgoingTx: false, replyAddress: null, tokenTx: true, + airdropFlag: false, txid: '8b569d64a7e51d1d3cf1cf2b99d8b34451bbebc7df6b67232e5b770418b0428c', }, ]; 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 @@ -128,6 +128,7 @@ let outgoingTx = false; let tokenTx = false; let substring = ''; + let airdropFlag = false; // If vin's scriptSig contains one of the publicKeys of this wallet // This is an outgoing tx @@ -184,6 +185,16 @@ let message = ''; let txType = parsedOpReturnArray[0]; + + if (txType === currency.opReturn.appPrefixesHex.airdrop) { + // this is to facilitate special Cashtab-specific cases of airdrop txs, both with and without msgs + // The UI via Tx.js can check this airdropFlag attribute in the parsedTx object to conditionally render airdrop-specific formatting if it's true + airdropFlag = true; + txType = parsedOpReturnArray[1]; + // remove the first airdrop prefix from array so the array parsing logic below can remain unchanged + parsedOpReturnArray.shift(); + } + if (txType === currency.opReturn.appPrefixesHex.eToken) { // this is an eToken transaction tokenTx = true; @@ -330,6 +341,7 @@ parsedTx.isCashtabMessage = isCashtabMessage; parsedTx.isEncryptedMessage = isEncryptedMessage; parsedTx.decryptionSuccess = decryptionSuccess; + parsedTx.airdropFlag = airdropFlag; parsedTxHistory.push(parsedTx); } return parsedTxHistory; @@ -1386,6 +1398,7 @@ destinationAddress, sendAmount, encryptionFlag, + airdropFlag, ) => { try { let value = new BigNumber(0); @@ -1469,9 +1482,10 @@ // Start of building the OP_RETURN output. // only build the OP_RETURN output if the user supplied it if ( - optionalOpReturnMsg && - typeof optionalOpReturnMsg !== 'undefined' && - optionalOpReturnMsg.trim() !== '' + (optionalOpReturnMsg && + typeof optionalOpReturnMsg !== 'undefined' && + optionalOpReturnMsg.trim() !== '') || + airdropFlag ) { if (encryptionFlag) { // if the user has opted to encrypt this message @@ -1498,14 +1512,48 @@ ]; } else { // this is an un-encrypted message - script = [ - BCH.Script.opcodes.OP_RETURN, // 6a - Buffer.from( - currency.opReturn.appPrefixesHex.cashtab, - 'hex', - ), // 00746162 - Buffer.from(optionalOpReturnMsg), - ]; + + if (airdropFlag) { + // un-encrypted airdrop tx + if (optionalOpReturnMsg) { + // airdrop tx with message + script = [ + BCH.Script.opcodes.OP_RETURN, // 6a + Buffer.from( + currency.opReturn.appPrefixesHex.airdrop, + 'hex', + ), // drop + Buffer.from( + currency.opReturn.appPrefixesHex.cashtab, + 'hex', + ), // 00746162 + Buffer.from(optionalOpReturnMsg), + ]; + } else { + // airdrop tx with no message + script = [ + BCH.Script.opcodes.OP_RETURN, // 6a + Buffer.from( + currency.opReturn.appPrefixesHex.airdrop, + 'hex', + ), // drop + Buffer.from( + currency.opReturn.appPrefixesHex.cashtab, + 'hex', + ), // 00746162 + ]; + } + } else { + // non-airdrop un-encrypted message + script = [ + BCH.Script.opcodes.OP_RETURN, // 6a + Buffer.from( + currency.opReturn.appPrefixesHex.cashtab, + 'hex', + ), // 00746162 + Buffer.from(optionalOpReturnMsg), + ]; + } } const data = BCH.Script.encode(script); transactionBuilder.addOutput(data, 0); diff --git a/web/cashtab/src/utils/__mocks__/mockOpReturnParsedArray.js b/web/cashtab/src/utils/__mocks__/mockOpReturnParsedArray.js --- a/web/cashtab/src/utils/__mocks__/mockOpReturnParsedArray.js +++ b/web/cashtab/src/utils/__mocks__/mockOpReturnParsedArray.js @@ -50,3 +50,12 @@ export const mockParsedETokenOutputArray = [ currency.opReturn.appPrefixesHex.eToken, ]; + +export const mockAirdropHexOutput = + '6a0464726f7004007461620f61697264726f70206d657373616765'; + +export const mockParsedAirdropMessageArray = [ + '64726f70', + '00746162', + '61697264726f70206d657373616765', +]; 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 @@ -25,6 +25,7 @@ areAllUtxosIncludedInIncrementallyHydratedUtxos, convertEcashtoEtokenAddr, } from 'utils/cashMethods'; +import { currency } from 'components/Common/Ticker'; import { unbatchedArray, arrayBatchedByThree, @@ -84,6 +85,8 @@ mockParsedMixedSegmentedExternalMessageArray, eTokenInputHex, mockParsedETokenOutputArray, + mockAirdropHexOutput, + mockParsedAirdropMessageArray, } from '../__mocks__/mockOpReturnParsedArray'; import { @@ -422,6 +425,18 @@ expect(result).toStrictEqual(mockParsedETokenOutputArray); }); + test('parseOpReturn() successfully parses an airdrop transaction', async () => { + const result = parseOpReturn(mockAirdropHexOutput); + // verify the hex output is parsed correctly + expect(result).toStrictEqual(mockParsedAirdropMessageArray); + // verify airdrop hex prefix is contained in the array returned from parseOpReturn() + expect( + result.find( + element => element === currency.opReturn.appPrefixesHex.airdrop, + ), + ).toStrictEqual(currency.opReturn.appPrefixesHex.airdrop); + }); + test('isExcludedUtxo returns true for a utxo with different tx_pos and same txid as an existing utxo in the set', async () => { expect( isExcludedUtxo(excludedUtxoAlpha, previousUtxosObjUtxoArray), 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 @@ -62,6 +62,12 @@ ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; + } else if ( + i === 0 && + message === currency.opReturn.appPrefixesHex.airdrop + ) { + // add the airdrop prefix to array + resultArray[i] = currency.opReturn.appPrefixesHex.airdrop; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message; diff --git a/web/standards/op_return-prefix-guideline.md b/web/standards/op_return-prefix-guideline.md --- a/web/standards/op_return-prefix-guideline.md +++ b/web/standards/op_return-prefix-guideline.md @@ -17,6 +17,7 @@ | 46555a00 | CashFusion | Jonald Fyookball and Mark Lundeberg | ecash:qqqxxmjyavdkwdj6npa5w6xl0fzq3wc5fu6s5x69jj | https://github.com/cashshuffle/spec/blob/master/CASHFUSION.md | n/a | | 00746162 | Cashtab | Bitcoin ABC | ecash:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdg2jj94l5j | https://cashtab.com/ | n/a | | 65746162 | Cashtab Encrypted | Bitcoin ABC | ecash:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdg2jj94l5j | https://cashtab.com/ | n/a | +| 64726f70 | Airdrop | Bitcoin ABC | ecash:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdg2jj94l5j | https://cashtab.com/ | n/a | ---