Skip to content

Commit 48a7918

Browse files
mainnet-patrkalis
authored andcommitted
Throw on an attempt to submit the same transaction twice
1 parent fdd9672 commit 48a7918

File tree

2 files changed

+37
-16
lines changed

2 files changed

+37
-16
lines changed

packages/cashscript/src/network/MockNetworkProvider.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const bobAddress = 'bchtest:qz6q5gqnxdldkr07xpls5474mmzmlesd6qnux4skuc';
1010
const carolAddress = 'bchtest:qqsr7nqwe6rq5crj63gy5gdqchpnwmguusmr7tfmsj';
1111

1212
export default class MockNetworkProvider implements NetworkProvider {
13-
// we use lockingBytecode as the key for utxoMap to make cashaddresses and tokenaddresses interchangeable
13+
// we use lockingBytecode hex as the key for utxoMap to make cash addresses and token addresses interchangeable
1414
private utxoMap: Record<string, Utxo[]> = {};
1515
private transactionMap: Record<string, string> = {};
1616
public network: Network = Network.MOCKNET;
@@ -45,6 +45,11 @@ export default class MockNetworkProvider implements NetworkProvider {
4545
const transactionBin = hexToBin(txHex);
4646

4747
const txid = binToHex(sha256(sha256(transactionBin)).reverse());
48+
49+
if (this.transactionMap[txid]) {
50+
throw new Error(`Transaction with txid ${txid} was already submitted: txn-mempool-conflict`);
51+
}
52+
4853
this.transactionMap[txid] = txHex;
4954

5055
const decoded = decodeTransaction(transactionBin);
@@ -54,21 +59,23 @@ export default class MockNetworkProvider implements NetworkProvider {
5459

5560
// remove (spend) UTXOs from the map
5661
for (const input of decoded.inputs) {
57-
for (const address of Object.keys(this.utxoMap)) {
58-
const utxos = this.utxoMap[address];
62+
for (const lockingBytecodeHex of Object.keys(this.utxoMap)) {
63+
const utxos = this.utxoMap[lockingBytecodeHex];
5964
const index = utxos.findIndex(
60-
(utxo) => utxo.txid === binToHex(input.outpointTransactionHash) && utxo.vout === input.outpointIndex
65+
(utxo) => utxo.txid === binToHex(input.outpointTransactionHash) && utxo.vout === input.outpointIndex,
6166
);
6267

6368
if (index !== -1) {
6469
// Remove the UTXO from the map
6570
utxos.splice(index, 1);
66-
this.utxoMap[address] = utxos;
71+
this.utxoMap[lockingBytecodeHex] = utxos;
72+
73+
if (utxos.length === 0) {
74+
delete this.utxoMap[lockingBytecodeHex]; // Clean up empty address entries
75+
}
76+
6777
break; // Exit loop after finding and removing the UTXO
6878
}
69-
if (utxos.length === 0) {
70-
delete this.utxoMap[address]; // Clean up empty address entries
71-
}
7279
}
7380
}
7481

@@ -93,7 +100,8 @@ export default class MockNetworkProvider implements NetworkProvider {
93100
}
94101

95102
addUtxo(addressOrLockingBytecode: string, utxo: Utxo): void {
96-
const lockingBytecode = isHex(addressOrLockingBytecode) ? addressOrLockingBytecode : binToHex(addressToLockScript(addressOrLockingBytecode));
103+
const lockingBytecode = isHex(addressOrLockingBytecode) ?
104+
addressOrLockingBytecode : binToHex(addressToLockScript(addressOrLockingBytecode));
97105
if (!this.utxoMap[lockingBytecode]) {
98106
this.utxoMap[lockingBytecode] = [];
99107
}

packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ describe('Transaction Builder', () => {
3030
expect(await provider.getUtxos(p2pkhInstance.address)).toHaveLength(0);
3131

3232
// add by address
33-
provider.addUtxo(aliceAddress, randomUtxo());
33+
provider.addUtxo(aliceAddress, randomUtxo({
34+
satoshis: 1100n,
35+
}));
3436
// add by locking bytecode
35-
provider.addUtxo(binToHex(addressToLockScript(p2pkhInstance.address)), randomUtxo());
37+
provider.addUtxo(binToHex(addressToLockScript(p2pkhInstance.address)), randomUtxo({
38+
satoshis: 1100n,
39+
}));
3640

3741
const aliceUtxos = await provider.getUtxos(aliceAddress);
3842
const p2pkhUtxos = await provider.getUtxos(p2pkhInstance.address);
@@ -43,17 +47,26 @@ describe('Transaction Builder', () => {
4347
const sigTemplate = new SignatureTemplate(alicePriv);
4448

4549
// spend both utxos to bob
46-
new TransactionBuilder({provider})
47-
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(alicePub, sigTemplate))
48-
.addInput(aliceUtxos[0], sigTemplate.unlockP2PKH())
49-
.addOutput({ to: bobAddress, amount: 1000n })
50-
.send();
50+
const builder = new TransactionBuilder({ provider })
51+
.addInputs(p2pkhUtxos, p2pkhInstance.unlock.spend(alicePub, sigTemplate))
52+
.addInputs(aliceUtxos, sigTemplate.unlockP2PKH())
53+
.addOutput({ to: bobAddress, amount: 2000n });
54+
55+
const tx = builder.build();
56+
57+
// try to send invalid transaction
58+
await expect(provider.sendRawTransaction(tx.slice(0, -2))).rejects.toThrow('Error reading transaction.');
59+
60+
// send valid transaction
61+
await expect(provider.sendRawTransaction(tx)).resolves.not.toThrow();
5162

5263
// utxos should be removed from the provider
5364
expect(await provider.getUtxos(aliceAddress)).toHaveLength(0);
5465
expect(await provider.getUtxos(p2pkhInstance.address)).toHaveLength(0);
5566

5667
// utxo should be added to bob
5768
expect(await provider.getUtxos(bobAddress)).toHaveLength(1);
69+
70+
await expect(provider.sendRawTransaction(tx)).rejects.toThrow('txn-mempool-conflict');
5871
});
5972
});

0 commit comments

Comments
 (0)