diff --git a/cashtab/src/utils/__tests__/cashMethods.test.js b/cashtab/src/utils/__tests__/cashMethods.test.js --- a/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/cashtab/src/utils/__tests__/cashMethods.test.js @@ -16,6 +16,7 @@ isActiveWebsocket, parseXecSendValue, getChangeAddressFromInputUtxos, + generateAliasOpReturnScript, generateOpReturnScript, generateTxInput, generateTxOutput, @@ -116,29 +117,91 @@ it(`OP_RETURN msg byte length matches for an encrypted msg input with a single emoji`, () => { const msgInput = '🙈'; - const encryptedEjMock = {"type":"Buffer","data":[2,241,30,211,127,184,181,145,219,158,127,99,178,221,90,234,194,108,152,147,60,77,74,176,112,249,23,170,186,204,20,209,135,98,156,215,47,144,123,71,111,123,199,26,89,67,76,135,250,112,226,74,182,186,79,52,15,88,214,142,141,145,103,89,66,158,107,191,144,255,139,65,21,141,128,61,33,172,31,246,145,72,62,161,173,23,249,4,79,245,183,202,115,140,0,83,42]}; - const opReturnMsgByteLength = getMessageByteSize(msgInput, true, encryptedEjMock); + const encryptedEjMock = { + type: 'Buffer', + data: [ + 2, 241, 30, 211, 127, 184, 181, 145, 219, 158, 127, 99, 178, 221, + 90, 234, 194, 108, 152, 147, 60, 77, 74, 176, 112, 249, 23, 170, + 186, 204, 20, 209, 135, 98, 156, 215, 47, 144, 123, 71, 111, 123, + 199, 26, 89, 67, 76, 135, 250, 112, 226, 74, 182, 186, 79, 52, 15, + 88, 214, 142, 141, 145, 103, 89, 66, 158, 107, 191, 144, 255, 139, + 65, 21, 141, 128, 61, 33, 172, 31, 246, 145, 72, 62, 161, 173, 23, + 249, 4, 79, 245, 183, 202, 115, 140, 0, 83, 42, + ], + }; + const opReturnMsgByteLength = getMessageByteSize( + msgInput, + true, + encryptedEjMock, + ); expect(opReturnMsgByteLength).toStrictEqual(97); }); it(`OP_RETURN msg byte length matches for an encrypted msg input with characters and emojis`, () => { const msgInput = 'monkey🙈'; - const encryptedEjMock = {"type":"Buffer","data":[2,74,145,240,12,210,143,66,224,155,246,106,238,186,167,192,123,39,44,165,231,97,166,149,93,121,10,107,45,12,235,45,158,251,183,245,6,206,9,153,146,208,40,156,106,3,140,137,68,126,240,70,87,131,54,91,115,164,223,109,199,173,127,106,94,82,200,83,77,157,55,195,16,17,99,1,148,226,150,243,120,133,80,17,226,109,17,154,226,59,203,36,203,230,236,12,104]}; - const opReturnMsgByteLength = getMessageByteSize(msgInput, true, encryptedEjMock); + const encryptedEjMock = { + type: 'Buffer', + data: [ + 2, 74, 145, 240, 12, 210, 143, 66, 224, 155, 246, 106, 238, 186, + 167, 192, 123, 39, 44, 165, 231, 97, 166, 149, 93, 121, 10, 107, 45, + 12, 235, 45, 158, 251, 183, 245, 6, 206, 9, 153, 146, 208, 40, 156, + 106, 3, 140, 137, 68, 126, 240, 70, 87, 131, 54, 91, 115, 164, 223, + 109, 199, 173, 127, 106, 94, 82, 200, 83, 77, 157, 55, 195, 16, 17, + 99, 1, 148, 226, 150, 243, 120, 133, 80, 17, 226, 109, 17, 154, 226, + 59, 203, 36, 203, 230, 236, 12, 104, + ], + }; + const opReturnMsgByteLength = getMessageByteSize( + msgInput, + true, + encryptedEjMock, + ); expect(opReturnMsgByteLength).toStrictEqual(97); }); it(`OP_RETURN msg byte length matches for an encrypted msg input with special characters`, () => { const msgInput = 'monkey©®ʕ•́ᴥ•̀ʔっ♡'; - const encryptedEjMock = {"type":"Buffer","data":[2,137,237,42,23,72,146,79,69,190,11,115,20,173,218,99,121,188,45,14,219,135,46,91,165,121,166,149,100,140,231,143,38,1,169,226,26,136,124,82,59,223,210,65,50,241,86,155,225,85,167,213,235,24,143,118,136,87,38,161,153,18,110,198,168,196,77,250,255,2,132,13,44,44,220,93,61,73,89,160,16,247,115,174,238,80,102,26,158,44,28,173,174,3,120,130,221,220,147,143,252,137,109,143,28,106,73,253,145,161,118,109,54,95,13,137,214,253,11,238,115,89,84,241,227,103,78,246,22]}; - const opReturnMsgByteLength = getMessageByteSize(msgInput, true, encryptedEjMock); + const encryptedEjMock = { + type: 'Buffer', + data: [ + 2, 137, 237, 42, 23, 72, 146, 79, 69, 190, 11, 115, 20, 173, 218, + 99, 121, 188, 45, 14, 219, 135, 46, 91, 165, 121, 166, 149, 100, + 140, 231, 143, 38, 1, 169, 226, 26, 136, 124, 82, 59, 223, 210, 65, + 50, 241, 86, 155, 225, 85, 167, 213, 235, 24, 143, 118, 136, 87, 38, + 161, 153, 18, 110, 198, 168, 196, 77, 250, 255, 2, 132, 13, 44, 44, + 220, 93, 61, 73, 89, 160, 16, 247, 115, 174, 238, 80, 102, 26, 158, + 44, 28, 173, 174, 3, 120, 130, 221, 220, 147, 143, 252, 137, 109, + 143, 28, 106, 73, 253, 145, 161, 118, 109, 54, 95, 13, 137, 214, + 253, 11, 238, 115, 89, 84, 241, 227, 103, 78, 246, 22, + ], + }; + const opReturnMsgByteLength = getMessageByteSize( + msgInput, + true, + encryptedEjMock, + ); expect(opReturnMsgByteLength).toStrictEqual(129); }); it(`OP_RETURN msg byte length matches for an encrypted msg input with a mixture of symbols, multilingual characters and emojis`, () => { const msgInput = '🙈©冰소주'; - const encryptedEjMock = {"type":"Buffer","data":[3,237,190,133,5,192,187,247,209,218,154,239,194,148,24,151,26,150,97,190,245,27,226,249,75,203,36,128,170,209,250,181,239,253,242,53,181,198,37,123,236,120,192,179,194,103,119,70,108,242,144,120,52,205,123,158,244,27,127,232,106,215,201,88,22,146,129,6,35,160,147,198,131,236,202,200,137,39,80,241,168,158,211,113,123,76,89,81,82,250,220,162,226,63,154,76,23]}; - const opReturnMsgByteLength = getMessageByteSize(msgInput, true, encryptedEjMock); + const encryptedEjMock = { + type: 'Buffer', + data: [ + 3, 237, 190, 133, 5, 192, 187, 247, 209, 218, 154, 239, 194, 148, + 24, 151, 26, 150, 97, 190, 245, 27, 226, 249, 75, 203, 36, 128, 170, + 209, 250, 181, 239, 253, 242, 53, 181, 198, 37, 123, 236, 120, 192, + 179, 194, 103, 119, 70, 108, 242, 144, 120, 52, 205, 123, 158, 244, + 27, 127, 232, 106, 215, 201, 88, 22, 146, 129, 6, 35, 160, 147, 198, + 131, 236, 202, 200, 137, 39, 80, 241, 168, 158, 211, 113, 123, 76, + 89, 81, 82, 250, 220, 162, 226, 63, 154, 76, 23, + ], + }; + const opReturnMsgByteLength = getMessageByteSize( + msgInput, + true, + encryptedEjMock, + ); expect(opReturnMsgByteLength).toStrictEqual(97); }); @@ -593,7 +656,89 @@ } expect(errorThrown.message).toStrictEqual('dust'); }); - +it('generateAliasOpReturnScript() correctly generates OP_RETURN script for a valid alias registration for a p2pkh address', () => { + const alias = 'test'; + const address = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; + const { hash } = cashaddr.decode(address, true); + + // Manually build the expected outputScript + const opReturn = '6a'; + // push protocol identifier + const prefixBytesHex = '04'; + const aliasIdentifier = '2e786563'; + + // push alias tx version + const aliasProtocolVersionNumberHex = '00'; + + // push the alias + const aliasHexBytes = '04'; // alias.length in one byte of hex + const aliasHex = Buffer.from(alias).toString('hex'); + + // push the address + const aliasAddressBytesHex = '15'; // (1 + 20) in one byte of hex + const p2pkhVersionByteHex = '00'; + + const aliasTxOpReturnOutputScript = [ + opReturn, + prefixBytesHex, + aliasIdentifier, + aliasProtocolVersionNumberHex, + aliasHexBytes, + aliasHex, + aliasAddressBytesHex, + p2pkhVersionByteHex, + hash, + ].join(''); + + // Calculate the expected outputScript with the tested function + const aliasOutputScript = generateAliasOpReturnScript(alias, address); + // aliasOutputScript.toString('hex') + // 6a042e78656301000474657374150095e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d + + expect(aliasOutputScript.toString('hex')).toBe(aliasTxOpReturnOutputScript); +}); +it('generateAliasOpReturnScript() correctly generates OP_RETURN script for a valid alias registration for a p2sh address', () => { + const alias = 'testtwo'; + const address = 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07'; + const { hash } = cashaddr.decode(address, true); + + // Manually build the expected outputScript + const opReturn = '6a'; + + // push protocol identifier + const prefixBytesHex = '04'; + const aliasIdentifier = '2e786563'; + + // push alias tx version + const aliasProtocolVersionNumberHex = '00'; + + // push the alias + const aliasHexBytes = '07'; // alias.length in one byte of hex + const aliasHex = Buffer.from(alias).toString('hex'); + + // push the address + const aliasAddressBytesHex = '15'; // (1 + 20) in one byte of hex + const p2shVersionByteHex = '08'; + + const aliasTxOpReturnOutputScript = [ + opReturn, + prefixBytesHex, + aliasIdentifier, + aliasProtocolVersionNumberHex, + aliasHexBytes, + aliasHex, + aliasAddressBytesHex, + p2shVersionByteHex, + hash, + ].join(''); + + // Calculate the expected outputScript with the tested function + const aliasOutputScript = generateAliasOpReturnScript(alias, address); + // aliasOutputScript.toString('hex') + // 6a042e7865630100077465737474776f1508d37c4c809fe9840e7bfa77b86bd47163f6fb6c60 + + expect(aliasOutputScript.toString('hex')).toBe(aliasTxOpReturnOutputScript); +}); it('generateOpReturnScript() correctly generates an encrypted message script', () => { const optionalOpReturnMsg = 'testing generateOpReturnScript()'; const encryptionFlag = true; @@ -737,9 +882,7 @@ txBuilder, ); - expect(tokenInputObj.inputXecUtxos).toStrictEqual( - [mockNonSlpUtxos[0]], - ); + expect(tokenInputObj.inputXecUtxos).toStrictEqual([mockNonSlpUtxos[0]]); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockCreateTokenTxBuilderObj.toString(), ); diff --git a/cashtab/src/utils/cashMethods.js b/cashtab/src/utils/cashMethods.js --- a/cashtab/src/utils/cashMethods.js +++ b/cashtab/src/utils/cashMethods.js @@ -11,7 +11,11 @@ import * as slpMdm from 'slp-mdm'; import * as utxolib from '@bitgo/utxo-lib'; -export const getMessageByteSize = (msgInputStr, encryptionFlag, encryptedEj) => { +export const getMessageByteSize = ( + msgInputStr, + encryptionFlag, + encryptedEj, +) => { if (!msgInputStr || msgInputStr.trim() === '') { return 0; } @@ -414,7 +418,12 @@ txBuilder.addInput(txid, vout); inputUtxos.push(utxo); - txFee = calcFee(inputUtxos, txOutputs, feeInSatsPerByte, opReturnByteCount); + txFee = calcFee( + inputUtxos, + txOutputs, + feeInSatsPerByte, + opReturnByteCount, + ); if (totalInputUtxoValue.minus(satoshisToSend).minus(txFee).gte(0)) { break; @@ -646,6 +655,49 @@ return utxolib.script.compile(arr); }; +/* + * Generates an OP_RETURN script for a version 0 alias registration tx + * + * Returns the final encoded script object ready to be added as a transaction output + */ +export const generateAliasOpReturnScript = (alias, address) => { + // Note: utxolib.script.compile(script) will add pushdata bytes for each buffer + // utxolib.script.compile(script) will not add pushdata bytes for raw data + + // Initialize script array with OP_RETURN byte (6a) as rawdata (i.e. you want compiled result of 6a, not 016a) + let script = [currency.opReturn.opReturnPrefixDec]; + + // Push alias protocol identifier + script.push( + Buffer.from(currency.opReturn.appPrefixesHex.aliasRegistration, 'hex'), // '.xec' + ); + + // Push alias protocol tx version to stack + // Per spec, push this as OP_0 + script.push(0); + + // Push alias to the stack + script.push(Buffer.from(alias, 'utf8')); + + // Get the type and hash of the address in string format + const { type, hash } = cashaddr.decode(address, true); + + // Determine address type and corresponding address version byte + let addressVersionByte; + // Version bytes per cashaddr spec,https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md + if (type === 'p2pkh') { + addressVersionByte = '00'; // one byte 0 in hex + } else if (type === 'p2sh') { + addressVersionByte = '08'; // one byte 8 in hex + } else { + throw new Error('Unsupported address type'); + } + + // Push and + script.push(Buffer.from(`${addressVersionByte}${hash}`, 'hex')); + + return utxolib.script.compile(script); +}; /* * Generates an OP_RETURN script to reflect the various send XEC permutations * involving messaging, encryption, eToken IDs and airdrop flags.