diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js --- a/cashtab/src/components/Send/SendXec.js +++ b/cashtab/src/components/Send/SendXec.js @@ -24,6 +24,7 @@ sendXec, getMultisendTargetOutputs, getMaxSendAmountSatoshis, + addressToScript, } from 'transactions'; import { getCashtabMsgTargetOutput, @@ -473,7 +474,7 @@ : fiatToSatoshis(formData.amount, fiatPrice); targetOutputs.push({ - address: cleanAddress, + script: addressToScript(cleanAddress), value: satoshisToSend, }); diff --git a/cashtab/src/slpv1/fixtures/vectors.js b/cashtab/src/slpv1/fixtures/vectors.js --- a/cashtab/src/slpv1/fixtures/vectors.js +++ b/cashtab/src/slpv1/fixtures/vectors.js @@ -11,6 +11,7 @@ } from 'slpv1'; import { Script, fromHex } from 'ecash-lib'; import { undecimalizeTokenAmount } from 'wallet'; +import { addressToScript } from 'transactions'; const GENESIS_MINT_ADDRESS = 'ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y'; export const SEND_DESTINATION_ADDRESS = @@ -540,7 +541,7 @@ }, { value: appConfig.dustSats, - address: SEND_DESTINATION_ADDRESS, + script: addressToScript(SEND_DESTINATION_ADDRESS), }, { value: appConfig.dustSats, @@ -610,7 +611,7 @@ }, { value: appConfig.dustSats, - address: SEND_DESTINATION_ADDRESS, + script: addressToScript(SEND_DESTINATION_ADDRESS), }, ], }, @@ -670,7 +671,7 @@ }, { value: appConfig.dustSats, - address: SEND_DESTINATION_ADDRESS, + script: addressToScript(SEND_DESTINATION_ADDRESS), }, { value: appConfig.dustSats, @@ -1865,7 +1866,7 @@ ), }, { - address: SEND_DESTINATION_ADDRESS, + script: addressToScript(SEND_DESTINATION_ADDRESS), value: appConfig.dustSats, }, ], diff --git a/cashtab/src/slpv1/index.js b/cashtab/src/slpv1/index.js --- a/cashtab/src/slpv1/index.js +++ b/cashtab/src/slpv1/index.js @@ -5,6 +5,7 @@ import appConfig from 'config/app'; import { undecimalizeTokenAmount, decimalizeTokenAmount } from 'wallet'; import { slpGenesis, slpSend, slpMint } from 'ecash-lib'; +import { addressToScript } from 'transactions'; // Constants for SLP 1 token types as returned by chronik-client export const SLP_1_PROTOCOL_NUMBER = 1; @@ -106,7 +107,7 @@ // Add first 'to' amount to 1 index. This could be any index between 1 and 19. targetOutputs.push({ value: appConfig.dustSats, - address: destinationAddress, + script: addressToScript(destinationAddress), }); // sendAmounts can only be length 1 or 2 @@ -601,6 +602,6 @@ // Therefore, we will have no change, and every send tx will have only one token utxo output return [ { value: 0, script }, - { address: destinationAddress, value: 546 }, + { script: addressToScript(destinationAddress), value: 546 }, ]; }; diff --git a/cashtab/src/transactions/__tests__/index.test.js b/cashtab/src/transactions/__tests__/index.test.js --- a/cashtab/src/transactions/__tests__/index.test.js +++ b/cashtab/src/transactions/__tests__/index.test.js @@ -7,6 +7,7 @@ getMultisendTargetOutputs, ignoreUnspendableUtxos, getMaxSendAmountSatoshis, + addressToScript, } from 'transactions'; import { getSendTokenInputs, @@ -14,7 +15,7 @@ getSlpBurnTargetOutputs, } from 'slpv1'; import { MockChronikClient } from '../../../../modules/mock-chronik-client'; -import { +import vectors, { sendXecVectors, getMultisendTargetOutputsVectors, ignoreUnspendableUtxosVectors, @@ -364,4 +365,19 @@ ).toStrictEqual(999630); }); }); + describe('We can convert cashaddr addresses to ecash-lib expected Script types', () => { + const { expectedReturns, expectedErrors } = vectors.addressToScript; + expectedReturns.forEach(expectedReturn => { + const { description, address, returned } = expectedReturn; + it(`addressToScript: ${description}`, () => { + expect(addressToScript(address)).toStrictEqual(returned); + }); + }); + expectedErrors.forEach(expectedError => { + const { description, address, errorMsg } = expectedError; + it(`addressToScript throws error for: ${description}`, () => { + expect(() => addressToScript(address)).toThrow(errorMsg); + }); + }); + }); }); diff --git a/cashtab/src/transactions/fixtures/vectors.js b/cashtab/src/transactions/fixtures/vectors.js --- a/cashtab/src/transactions/fixtures/vectors.js +++ b/cashtab/src/transactions/fixtures/vectors.js @@ -12,7 +12,8 @@ allTheXecWallet, walletWithTokensInNode, } from './mocks'; - +import { Script, fromHex } from 'ecash-lib'; +import { addressToScript } from 'transactions'; import { getCashtabMsgTargetOutput } from 'opreturn'; const OP_RETURN_CASHTAB_MSG_TEST = getCashtabMsgTargetOutput('test'); @@ -25,7 +26,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -40,7 +43,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -55,7 +60,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, OP_RETURN_CASHTAB_MSG_TEST, ], @@ -72,7 +79,9 @@ OP_RETURN_CASHTAB_MSG_TEST, { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -87,7 +96,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, ], satsPerKb: 1000, @@ -101,7 +112,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 10000, @@ -115,7 +128,9 @@ targetOutputs: [ { value: 24808, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -130,7 +145,9 @@ targetOutputs: [ { value: 24808, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -145,7 +162,9 @@ targetOutputs: [ { value: 88800, - address: 'ecash:qzr03ye2jhrxmw97g9hv4s05364qgsdqzsjre4krry', + script: addressToScript( + 'ecash:qzr03ye2jhrxmw97g9hv4s05364qgsdqzsjre4krry', + ), }, ], satsPerKb: 1000, @@ -160,7 +179,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -175,23 +196,33 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, { value: 2000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, { value: 3000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, { value: 4000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, { value: 5000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -206,23 +237,33 @@ targetOutputs: [ { value: 1000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, { value: 2000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, { value: 3000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, { value: 4000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, { value: 5000, - address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + script: addressToScript( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), }, ], satsPerKb: 1000, @@ -238,7 +279,9 @@ targetOutputs: [ { value: 545, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -252,7 +295,9 @@ targetOutputs: [ { value: wallet.state.balanceSats + 1, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -267,7 +312,9 @@ targetOutputs: [ { value: wallet.state.balanceSats - 50, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -282,7 +329,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -296,7 +345,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -310,7 +361,9 @@ targetOutputs: [ { value: 1000, - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), }, ], satsPerKb: 1000, @@ -328,27 +381,39 @@ userMultisendInput: `ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr,150\necash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035,50\necash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6,150\necash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj,4400\necash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly,50\necash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m,200`, targetOutputs: [ { - address: 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + script: addressToScript( + 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + ), value: 15000, }, { - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), value: 5000, }, { - address: 'ecash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6', + script: addressToScript( + 'ecash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6', + ), value: 15000, }, { - address: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + script: addressToScript( + 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + ), value: 440000, }, { - address: 'ecash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly', + script: addressToScript( + 'ecash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly', + ), value: 5000, }, { - address: 'ecash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m', + script: addressToScript( + 'ecash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m', + ), value: 20000, }, ], @@ -359,11 +424,15 @@ userMultisendInput: ` ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr , 150\n ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035 , 50 `, targetOutputs: [ { - address: 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + script: addressToScript( + 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + ), value: 15000, }, { - address: 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + script: addressToScript( + 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', + ), value: 5000, }, ], @@ -373,7 +442,9 @@ userMultisendInput: `ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr,150`, targetOutputs: [ { - address: 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + script: addressToScript( + 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + ), value: 15000, }, ], @@ -383,23 +454,33 @@ userMultisendInput: `ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr,151.52\necash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6,151.52\necash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj,4444.44\necash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly,50.51\necash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m,202.02`, targetOutputs: [ { - address: 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + script: addressToScript( + 'ecash:qzj5zu6fgg8v2we82gh76xnrk9njcreglum9ffspnr', + ), value: 15152, }, { - address: 'ecash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6', + script: addressToScript( + 'ecash:qr204yfphngxthvnukyrz45u7500tf60vyqspva5a6', + ), value: 15152, }, { - address: 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + script: addressToScript( + 'ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj', + ), value: 444444, }, { - address: 'ecash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly', + script: addressToScript( + 'ecash:qrq64hyel9hulnl9vsk29xjnuuqlpwqpcv6mk9pqly', + ), value: 5051, }, { - address: 'ecash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m', + script: addressToScript( + 'ecash:qzn3gqf7vvm2qdu2rac6m6r4kgfcsyaras7jfqja3m', + ), value: 20202, }, ], @@ -606,3 +687,35 @@ }, ], }; + +export default { + addressToScript: { + expectedReturns: [ + { + description: + 'We can convert a valid p2pkh address to an ecash-lib Script', + address: 'ecash:qzppgpav9xfls6zzyuqy7syxpqhnlqqa5u68m4qw6l', + returned: Script.p2pkh( + fromHex('821407ac2993f8684227004f4086082f3f801da7'), + ), + }, + { + description: + 'We can convert a valid p2sh address to an ecash-lib Script', + address: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + returned: Script.p2sh( + fromHex('d37c4c809fe9840e7bfa77b86bd47163f6fb6c60'), + ), + }, + ], + expectedErrors: [ + { + description: + 'We throw the ecashaddrjs decode error for an address that cannot be decoded', + address: + '41047fa64f6874fb7213776b24c40bc915451b57ef7f17ad7b982561f99f7cdc7010d141b856a092ee169c5405323895e1962c6b0d7c101120d360164c9e4b3997bdac', + errorMsg: 'Invalid value: 1.', + }, + ], + }, +}; diff --git a/cashtab/src/transactions/index.js b/cashtab/src/transactions/index.js --- a/cashtab/src/transactions/index.js +++ b/cashtab/src/transactions/index.js @@ -68,29 +68,6 @@ }); continue; } - // We must convert address to the appropriate outputScript - const { type, hash } = cashaddr.decode(targetOutput.address, true); - switch (type) { - case 'p2pkh': { - outputs.push({ - value: targetOutput.value, - script: Script.p2pkh(fromHex(hash)), - }); - break; - } - case 'p2sh': { - outputs.push({ - value: targetOutput.value, - script: Script.p2sh(fromHex(hash)), - }); - break; - } - default: { - throw new Error( - `Unsupported address type for ${targetOutput.address}`, - ); - } - } } // Get the total amount of satoshis being sent in this tx @@ -285,7 +262,7 @@ // targetOutputs expects satoshis at value key const valueSats = toSatoshis(valueXec); targetOutputs.push({ - address: addressValueLineArray[0].trim(), + script: addressToScript(addressValueLineArray[0].trim()), value: valueSats, }); } @@ -313,3 +290,27 @@ ); }); }; + +/** + * Convert an ecash address to an ecash-lib Script + * ecash-lib transaction builder uses Script for outputs and not an address + * @param {string} address a valid cash address + * @throws {Error} on invalid or unsupported address input + * @returns {Script} address translated to ecash-lib ready Script + */ +export const addressToScript = address => { + const { type, hash } = cashaddr.decode(address, true); + switch (type) { + case 'p2pkh': { + return Script.p2pkh(fromHex(hash)); + } + case 'p2sh': { + return Script.p2sh(fromHex(hash)); + } + default: { + // Note we should never get here, as ecashaddrjs decode method + // only supports p2pkh and p2sh + throw new Error(`Unsupported address type: ${address}`); + } + } +};