Skip to content

Commit

Permalink
Merge pull request #300 from input-output-hk/fix/chained-tx-doublespend
Browse files Browse the repository at this point in the history
fix(wallet): consume chained tx outputs
  • Loading branch information
rhyslbw committed Jun 24, 2022
2 parents 882edbf + f051351 commit db38b1c
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 28 deletions.
12 changes: 10 additions & 2 deletions packages/wallet/src/services/UtxoTracker.ts
Expand Up @@ -57,9 +57,17 @@ export const createUtxoTracker = (
inputs.some((input) => input.txId === utxoTxIn.txId && input.index === utxoTxIn.index)
)
),
...transactionsInFlight.flatMap((tx) =>
...transactionsInFlight.flatMap((tx, txInFlightIndex) =>
tx.body.outputs
.filter(({ address }) => ownAddresses.includes(address))
.filter(
({ address }, outputIndex) =>
ownAddresses.includes(address) &&
// not already consumed by another tx in flight
!transactionsInFlight.some(
({ body: { inputs } }, i) =>
txInFlightIndex !== i && inputs.some((txIn) => txIn.txId === tx.id && txIn.index === outputIndex)
)
)
.map(
(txOut): Cardano.Utxo => [
{
Expand Down
71 changes: 45 additions & 26 deletions packages/wallet/test/services/UtxoTracker.test.ts
Expand Up @@ -12,39 +12,62 @@ describe('createUtxoTracker', () => {
let tipBlockHeight$: Observable<number>;
let utxoProvider: UtxoProvider;

it('fetches utxo from WalletProvider and includes change from a transaction in flight', () => {
it(`fetches utxo from UtxoProvider;
includes change from a transaction in flight;
deeply chained transactions consumes chainable utxo`, () => {
const store = new InMemoryUtxoStore();
const ownAddress = utxo[0][1].address;
const spendAddress = utxo[1][0].address;
const spendUtxo = utxo[0];
const spendTotalCoins = spendUtxo[1].value.coins;
const spendCoins = spendTotalCoins / 2n;
const spendOutput: Cardano.TxOut = {
const tx1SpendOutput: Cardano.TxOut = {
address: spendAddress,
value: {
coins: spendCoins
}
};
const changeOutput: Cardano.TxOut = {
const tx1ChangeOutput: Cardano.TxOut = {
address: ownAddress,
value: {
coins: spendTotalCoins - spendCoins
}
};
const transactionId = Cardano.TransactionId('4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6');
const outputs = [spendOutput, changeOutput];
const transactionId1 = Cardano.TransactionId('4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6');
const transactionId2 = Cardano.TransactionId('4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d7');
const tx1Outputs = [tx1SpendOutput, tx1ChangeOutput];
const tx2Output = {
address: ownAddress,
value: tx1ChangeOutput.value
};
const tx2Outputs = [tx2Output];
createTestScheduler().run(({ cold, expectObservable }) => {
const transactionsInFlight$ = cold('-a-b|', {
const inFlightTx1 = {
body: {
inputs: [spendUtxo[0]],
outputs: tx1Outputs
},
id: transactionId1
} as unknown as Cardano.NewTxAlonzo;
const chainableUtxoFromTx1 = [
{ address: ownAddress, index: tx1Outputs.indexOf(tx1ChangeOutput), txId: transactionId1 },
tx1ChangeOutput
] as Cardano.Utxo;
const chainableUtxoFromTx2 = [
{ address: ownAddress, index: tx2Outputs.indexOf(tx2Output), txId: transactionId2 },
tx2Output
] as Cardano.Utxo;
const inFlightTx2 = {
body: {
inputs: [chainableUtxoFromTx1[0]],
outputs: tx2Outputs
},
id: transactionId2
} as unknown as Cardano.NewTxAlonzo;
const transactionsInFlight$ = cold('-a-b-c|', {
a: [],
b: [
{
body: {
inputs: [spendUtxo[0]],
outputs
},
id: transactionId
} as unknown as Cardano.NewTxAlonzo
]
b: [inFlightTx1],
c: [inFlightTx1, inFlightTx2]
});
const utxoTracker = createUtxoTracker(
{
Expand All @@ -69,23 +92,19 @@ describe('createUtxoTracker', () => {
utxoSource$: cold('a---|', { a: utxo }) as unknown as PersistentCollectionTrackerSubject<Cardano.Utxo>
}
);
const utxoWithTxInFlight = [
...utxo.slice(1),
[
{ address: ownAddress, index: outputs.indexOf(changeOutput), txId: transactionId },
changeOutput
] as Cardano.Utxo
];
expectObservable(utxoTracker.total$).toBe('-a-b|', { a: utxo, b: utxoWithTxInFlight });
const utxoWithTx1InFlight = [...utxo.slice(1), chainableUtxoFromTx1];
const utxoWithTx2InFlight = [...utxo.slice(1), chainableUtxoFromTx2];
expectObservable(utxoTracker.total$).toBe('-a-b-c|', { a: utxo, b: utxoWithTx1InFlight, c: utxoWithTx2InFlight });
expectObservable(utxoTracker.unspendable$).toBe('a---|', { a: [] });
expectObservable(utxoTracker.available$).toBe('-a-b|', {
expectObservable(utxoTracker.available$).toBe('-a-b-c|', {
a: utxo,
b: utxoWithTxInFlight
b: utxoWithTx1InFlight,
c: utxoWithTx2InFlight
});
});
});

it('fetches utxo from WalletProvider and locks unspendable ones', () => {
it('fetches utxo from UtxoProvider and locks unspendable ones', () => {
const store = new InMemoryUtxoStore();
const address = utxo[0][0].address;
createTestScheduler().run(({ cold, expectObservable }) => {
Expand Down

0 comments on commit db38b1c

Please sign in to comment.