Skip to content

Commit fdd9672

Browse files
mainnet-patrkalis
authored andcommitted
Track UTXOs in MockNetworkProvider
1 parent 280c566 commit fdd9672

File tree

2 files changed

+106
-3
lines changed

2 files changed

+106
-3
lines changed

packages/cashscript/src/network/MockNetworkProvider.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { binToHex, hexToBin } from '@bitauth/libauth';
1+
import { binToHex, decodeTransaction, hexToBin, isHex } from '@bitauth/libauth';
22
import { sha256 } from '@cashscript/utils';
33
import { Utxo, Network } from '../interfaces.js';
44
import NetworkProvider from './NetworkProvider.js';
@@ -10,6 +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
1314
private utxoMap: Record<string, Utxo[]> = {};
1415
private transactionMap: Record<string, string> = {};
1516
public network: Network = Network.MOCKNET;
@@ -45,11 +46,54 @@ export default class MockNetworkProvider implements NetworkProvider {
4546

4647
const txid = binToHex(sha256(sha256(transactionBin)).reverse());
4748
this.transactionMap[txid] = txHex;
49+
50+
const decoded = decodeTransaction(transactionBin);
51+
if (typeof decoded === 'string') {
52+
throw new Error(`${decoded}`);
53+
}
54+
55+
// remove (spend) UTXOs from the map
56+
for (const input of decoded.inputs) {
57+
for (const address of Object.keys(this.utxoMap)) {
58+
const utxos = this.utxoMap[address];
59+
const index = utxos.findIndex(
60+
(utxo) => utxo.txid === binToHex(input.outpointTransactionHash) && utxo.vout === input.outpointIndex
61+
);
62+
63+
if (index !== -1) {
64+
// Remove the UTXO from the map
65+
utxos.splice(index, 1);
66+
this.utxoMap[address] = utxos;
67+
break; // Exit loop after finding and removing the UTXO
68+
}
69+
if (utxos.length === 0) {
70+
delete this.utxoMap[address]; // Clean up empty address entries
71+
}
72+
}
73+
}
74+
75+
// add new UTXOs to the map
76+
for (const [index, output] of decoded.outputs.entries()) {
77+
this.addUtxo(binToHex(output.lockingBytecode), {
78+
txid: txid,
79+
vout: index,
80+
satoshis: output.valueSatoshis,
81+
token: output.token && {
82+
...output.token,
83+
category: binToHex(output.token.category),
84+
nft: output.token.nft && {
85+
...output.token.nft,
86+
commitment: binToHex(output.token.nft.commitment),
87+
},
88+
},
89+
});
90+
}
91+
4892
return txid;
4993
}
5094

51-
addUtxo(address: string, utxo: Utxo): void {
52-
const lockingBytecode = binToHex(addressToLockScript(address));
95+
addUtxo(addressOrLockingBytecode: string, utxo: Utxo): void {
96+
const lockingBytecode = isHex(addressOrLockingBytecode) ? addressOrLockingBytecode : binToHex(addressToLockScript(addressOrLockingBytecode));
5397
if (!this.utxoMap[lockingBytecode]) {
5498
this.utxoMap[lockingBytecode] = [];
5599
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { binToHex } from '@bitauth/libauth';
2+
import { Contract, MockNetworkProvider, SignatureTemplate } from '../../../src/index.js';
3+
import { TransactionBuilder } from '../../../src/TransactionBuilder.js';
4+
import { addressToLockScript, randomUtxo } from '../../../src/utils.js';
5+
import p2pkhArtifact from '../../fixture/p2pkh.artifact.js';
6+
import {
7+
aliceAddress,
8+
alicePkh,
9+
alicePriv,
10+
alicePub,
11+
bobAddress,
12+
} from '../../fixture/vars.js';
13+
import { itOrSkip } from '../../test-util.js';
14+
15+
describe('Transaction Builder', () => {
16+
const provider = new MockNetworkProvider();
17+
18+
let p2pkhInstance: Contract<typeof p2pkhArtifact>;
19+
20+
beforeAll(() => {
21+
p2pkhInstance = new Contract(p2pkhArtifact, [alicePkh], { provider });
22+
});
23+
24+
beforeEach(() => {
25+
provider.reset();
26+
});
27+
28+
itOrSkip(!process.env.TESTS_USE_MOCKNET, 'MockNetworkProvider should keep track of utxo set - remove spent utxos and add newly created', async () => {
29+
expect(await provider.getUtxos(aliceAddress)).toHaveLength(0);
30+
expect(await provider.getUtxos(p2pkhInstance.address)).toHaveLength(0);
31+
32+
// add by address
33+
provider.addUtxo(aliceAddress, randomUtxo());
34+
// add by locking bytecode
35+
provider.addUtxo(binToHex(addressToLockScript(p2pkhInstance.address)), randomUtxo());
36+
37+
const aliceUtxos = await provider.getUtxos(aliceAddress);
38+
const p2pkhUtxos = await provider.getUtxos(p2pkhInstance.address);
39+
40+
expect(aliceUtxos).toHaveLength(1);
41+
expect(p2pkhUtxos).toHaveLength(1);
42+
43+
const sigTemplate = new SignatureTemplate(alicePriv);
44+
45+
// 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();
51+
52+
// utxos should be removed from the provider
53+
expect(await provider.getUtxos(aliceAddress)).toHaveLength(0);
54+
expect(await provider.getUtxos(p2pkhInstance.address)).toHaveLength(0);
55+
56+
// utxo should be added to bob
57+
expect(await provider.getUtxos(bobAddress)).toHaveLength(1);
58+
});
59+
});

0 commit comments

Comments
 (0)