Skip to content

Commit

Permalink
Marina Web Provider: Broadcast transaction (#346)
Browse files Browse the repository at this point in the history
* add marina.broadcastTransaction

* lock selected inputs

* lint

* select Utxos from all accounts

* slice hash to prevent mutation

* get addresses from all accounts

* prettier

* code refactor, dry code

* remove redux selectors from utils/utxos.ts

* prettier
  • Loading branch information
bordalix authored May 10, 2022
1 parent f3e6772 commit 23876c9
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 47 deletions.
130 changes: 125 additions & 5 deletions src/application/utils/utxos.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,142 @@
import { UnblindedOutput, unblindOutput } from 'ldk';
import { Transaction } from 'liquidjs-lib';
import {
AddressInterface,
getNetwork,
IdentityInterface,
NetworkString,
UnblindedOutput,
unblindOutput,
} from 'ldk';
import { address, networks, Transaction } from 'liquidjs-lib';
import { RawHex } from 'marina-provider';
import { Account } from '../../domain/account';
import { UnconfirmedOutput } from '../../domain/unconfirmed';

export const toStringOutpoint = (outpoint: { txid: string; vout: number }) => {
export const toStringOutpoint = (outpoint: { txid: string; vout: number }): string => {
return `${outpoint.txid}:${outpoint.vout}`;
};

// for each unconfirmed output get unblindData and return utxo
export const makeUnconfirmedUtxos = async (
txHex: string,
unconfirmedOutputs: UnconfirmedOutput[]
changeUtxos: UnconfirmedOutput[]
): Promise<UnblindedOutput[]> => {
const unconfirmedUtxos: UnblindedOutput[] = [];
const transaction = Transaction.fromHex(txHex);
for (const { txid, vout, blindPrivKey } of unconfirmedOutputs) {
for (const { txid, vout, blindPrivKey } of changeUtxos) {
const prevout = transaction.outs[vout];
const utxo = await unblindOutput({ txid, vout, prevout }, blindPrivKey);
unconfirmedUtxos.push(utxo);
}
return unconfirmedUtxos;
};

export interface UtxosFromTx {
selectedUtxos: UnblindedOutput[];
changeUtxos: UnconfirmedOutput[];
}

// given a signed tx hex, get selected utxos and change utxos
export const getUtxosFromTx = async (
accounts: Account[],
coins: UnblindedOutput[],
network: NetworkString,
signedTxHex: RawHex
): Promise<UtxosFromTx> => {
const tx = Transaction.fromHex(signedTxHex);
const txid = tx.getId();

// get selected utxos used in this transaction:
// - get all coins from marina
// - iterate over all transaction inputs and check if is a coin of ours

// array to store all utxos selected in this transaction
const selectedUtxos: UnblindedOutput[] = [];

// find all inputs that are a coin of ours
tx.ins.forEach((_in) => {
const { hash, index } = _in; // txid and vout
const txid = hash.slice().reverse().toString('hex');
for (const coin of coins) {
if (coin.txid === txid && coin.vout === index) {
selectedUtxos.push(coin);
break;
}
}
});

// get credit change utxos:
// - get all addresses used by marina
// - iterate over all transaction outputs and check if belongs to marina

// array to store all utxos to be found
const changeUtxos: UnconfirmedOutput[] = [];

// get all marina addresses (from all accounts)
let allAddresses: AddressInterface[] = [];
for (const account of accounts) {
const identity = await account.getWatchIdentity(network);
allAddresses = [...allAddresses, ...(await identity.getAddresses())];
}

// iterate over transaction outputs
tx.outs.forEach((out, vout) => {
try {
// get address from output script
const addressFromOutputScript = address.fromOutputScript(
Buffer.from(out.script),
networks[network]
);
// iterate over marina addresses
for (const addr of allAddresses) {
// get unconfidential address for addr
const unconfidentialAddress = address.fromConfidential(
addr.confidentialAddress
).unconfidentialAddress;
// compare with address from output script
if (
addressFromOutputScript === unconfidentialAddress ||
addressFromOutputScript === addr.confidentialAddress
) {
changeUtxos.push({
txid,
vout,
blindPrivKey: addr.blindingPrivateKey,
});
break;
}
}
} catch (_) {
// probably this output doesn't belong to us
}
});

return {
selectedUtxos,
changeUtxos,
};
};

export const getUtxosFromChangeAddresses = async (
changeAddresses: string[],
identities: IdentityInterface[],
network: NetworkString,
tx: RawHex
): Promise<UnconfirmedOutput[]> => {
const changeUtxos: UnconfirmedOutput[] = [];
if (changeAddresses && identities[0]) {
const transaction = Transaction.fromHex(tx);
const txid = transaction.getId();
for (const addr of changeAddresses) {
const changeOutputScript = address.toOutputScript(addr, getNetwork(network));
const vout = transaction.outs.findIndex(
(o: any) => o.script.toString() === changeOutputScript.toString()
);
if (vout !== -1 && transaction?.outs[vout]?.script) {
const script = transaction.outs[vout].script.toString('hex');
const blindPrivKey = await identities[0].getBlindingPrivateKey(script);
changeUtxos.push({ txid, vout, blindPrivKey });
}
}
}
return changeUtxos;
};
80 changes: 59 additions & 21 deletions src/content/marina/marinaBroker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
setTxData,
} from '../../application/redux/actions/connect';
import {
selectAllAccounts,
selectAllAccountsIDs,
selectMainAccount,
selectTransactions,
Expand All @@ -35,12 +36,12 @@ import {
} from '../../application/redux/actions/wallet';
import { selectBalances } from '../../application/redux/selectors/balance.selector';
import { assetGetterFromIAssets } from '../../domain/assets';
import { Balance, Recipient, Utxo } from 'marina-provider';
import { Balance, RawHex, Recipient, Utxo } from 'marina-provider';
import { SignTransactionPopupResponse } from '../../presentation/connect/sign-pset';
import { SpendPopupResponse } from '../../presentation/connect/spend';
import { SignMessagePopupResponse } from '../../presentation/connect/sign-msg';
import { AccountID, MainAccountID } from '../../domain/account';
import { getAsset, getSats } from 'ldk';
import { getAsset, getSats, UnblindedOutput } from 'ldk';
import { selectEsploraURL, selectNetwork } from '../../application/redux/selectors/app.selector';
import { broadcastTx, lbtcAssetByNetwork } from '../../application/utils/network';
import { sortRecipients } from '../../application/utils/transaction';
Expand All @@ -49,6 +50,8 @@ import { sleep } from '../../application/utils/common';
import { BrokerProxyStore } from '../brokerProxyStore';
import { updateTaskAction } from '../../application/redux/actions/updater';
import { addUnconfirmedUtxos, lockUtxo } from '../../application/redux/actions/utxos';
import { getUtxosFromTx } from '../../application/utils/utxos';
import { UnconfirmedOutput } from '../../domain/unconfirmed';

export default class MarinaBroker extends Broker {
private static NotSetUpError = new Error('proxy store and/or cache are not set up');
Expand Down Expand Up @@ -118,6 +121,31 @@ export default class MarinaBroker extends Broker {
return store.getState();
}

// locks utxos used on transaction
// credit change utxos to balance
private async lockAndLoadUtxos(
signedTxHex: RawHex,
selectedUtxos: UnblindedOutput[] | undefined,
changeUtxos: UnconfirmedOutput[] | undefined,
store: BrokerProxyStore
) {
const state = store.getState();

// lock utxos used on transaction
if (selectedUtxos) {
for (const utxo of selectedUtxos) {
await store.dispatchAsync(lockUtxo(utxo));
}
}

// credit change utxos to balance
if (changeUtxos && changeUtxos.length > 0) {
store.dispatch(
await addUnconfirmedUtxos(signedTxHex, changeUtxos, MainAccountID, selectNetwork(state))
);
}
}

private marinaMessageHandler: MessageHandler = async ({ id, name, params }: RequestMessage) => {
if (!this.store || !this.hostname) throw MarinaBroker.NotSetUpError;
let state = this.store.getState();
Expand Down Expand Up @@ -235,24 +263,8 @@ export default class MarinaBroker extends Broker {
const txid = await broadcastTx(selectEsploraURL(state), signedTxHex);
if (!txid) throw new Error('something went wrong with the tx broadcasting');

// lock utxos used in successful broadcast
if (selectedUtxos) {
for (const utxo of selectedUtxos) {
await this.store.dispatchAsync(lockUtxo(utxo));
}
}

// add unconfirmed utxos from change addresses to utxo set
if (unconfirmedOutputs && unconfirmedOutputs.length > 0) {
this.store.dispatch(
await addUnconfirmedUtxos(
signedTxHex,
unconfirmedOutputs,
MainAccountID,
selectNetwork(state)
)
);
}
// lock selected utxos and credit change utxos (aka unconfirmed outputs)
await this.lockAndLoadUtxos(signedTxHex, selectedUtxos, unconfirmedOutputs, this.store);

return successMsg({ txid, hex: signedTxHex });
}
Expand Down Expand Up @@ -280,7 +292,7 @@ export default class MarinaBroker extends Broker {

case Marina.prototype.getCoins.name: {
this.checkHostnameAuthorization(state);
const coins = selectUtxos(MainAccountID)(state);
const coins = selectUtxos(...selectAllAccountsIDs(state))(state);
const results: Utxo[] = coins.map((unblindedOutput) => ({
txid: unblindedOutput.txid,
vout: unblindedOutput.vout,
Expand Down Expand Up @@ -326,6 +338,32 @@ export default class MarinaBroker extends Broker {
return successMsg();
}

case Marina.prototype.broadcastTransaction.name: {
this.checkHostnameAuthorization(state);
const [signedTxHex] = params as [string];
const network = selectNetwork(state);

// broadcast tx
const txid = await broadcastTx(selectEsploraURL(state), signedTxHex);
if (!txid) throw new Error('something went wrong with the tx broadcasting');

// get selected and change utxos from transaction
const accounts = selectAllAccounts(state);
const coins = selectUtxos(...selectAllAccountsIDs(state))(state);

const { selectedUtxos, changeUtxos } = await getUtxosFromTx(
accounts,
coins,
network,
signedTxHex
);

// lock selected utxos and credit change utxos
await this.lockAndLoadUtxos(signedTxHex, selectedUtxos, changeUtxos, this.store);

return successMsg({ txid, hex: signedTxHex });
}

default:
return newErrorResponseMessage(id, new Error('Method not implemented.'));
}
Expand Down
4 changes: 4 additions & 0 deletions src/inject/marina/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,8 @@ export default class Marina extends WindowProxy implements MarinaProvider {
reloadCoins(): Promise<void> {
return this.proxy(this.reloadCoins.name, []);
}

broadcastTransaction(signedTxHex: string): Promise<SentTransaction> {
return this.proxy(this.broadcastTransaction.name, [signedTxHex]);
}
}
32 changes: 11 additions & 21 deletions src/presentation/wallet/send/end-of-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ import { debounce } from 'lodash';
import { createPassword } from '../../../domain/password';
import { extractErrorMessage } from '../../utils/error';
import { Account } from '../../../domain/account';
import { address, getNetwork, NetworkString, UnblindedOutput } from 'ldk';
import { NetworkString, UnblindedOutput } from 'ldk';
import { updateTaskAction } from '../../../application/redux/actions/updater';
import { useDispatch } from 'react-redux';
import { ProxyStoreDispatch } from '../../../application/redux/proxyStore';
import { flushPendingTx } from '../../../application/redux/actions/transaction';
import { broadcastTx } from '../../../application/utils/network';
import { blindAndSignPset } from '../../../application/utils/transaction';
import { addUnconfirmedUtxos, lockUtxo } from '../../../application/redux/actions/utxos';
import { UnconfirmedOutput } from '../../../domain/unconfirmed';
import { Transaction } from 'liquidjs-lib';
import { getUtxosFromChangeAddresses } from '../../../application/utils/utxos';

export interface EndOfFlowProps {
accounts: Account[];
Expand Down Expand Up @@ -72,26 +71,17 @@ const EndOfFlow: React.FC<EndOfFlowProps> = ({
}

// find unconfirmed utxos from change addresses
const unconfirmedOutputs: UnconfirmedOutput[] = [];
if (changeAddresses && identities[0]) {
const transaction = Transaction.fromHex(tx);
for (const addr of changeAddresses) {
const changeOutputScript = address.toOutputScript(addr, getNetwork(network));
const vout = transaction.outs.findIndex(
(o: any) => o.script.toString() === changeOutputScript.toString()
);
if (vout !== -1 && transaction?.outs[vout]?.script) {
const script = transaction.outs[vout].script.toString('hex');
const blindPrivKey = await identities[0].getBlindingPrivateKey(script);
unconfirmedOutputs.push({ txid, vout, blindPrivKey });
}
}
}
const changeUtxos = await getUtxosFromChangeAddresses(
changeAddresses,
identities,
network,
tx
);

// add unconfirmed utxos from change addresses to utxo set
if (unconfirmedOutputs && unconfirmedOutputs.length > 0) {
// credit change utxos to balance
if (changeUtxos && changeUtxos.length > 0) {
await dispatch(
await addUnconfirmedUtxos(tx, unconfirmedOutputs, accounts[0].getAccountID(), network)
await addUnconfirmedUtxos(tx, changeUtxos, accounts[0].getAccountID(), network)
);
}

Expand Down

0 comments on commit 23876c9

Please sign in to comment.