Skip to content

Commit 1653c2f

Browse files
committed
Update P2PKH-tokens tests to use new Transaction Builder
1 parent b5ddafc commit 1653c2f

File tree

2 files changed

+85
-155
lines changed

2 files changed

+85
-155
lines changed

packages/cashscript/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"@cashscript/utils": "^0.11.5",
5050
"@electrum-cash/network": "^4.1.3",
5151
"@mr-zwets/bchn-api-wrapper": "^1.0.1",
52-
"fast-deep-equal": "^3.1.3",
5352
"pako": "^2.1.0",
5453
"semver": "^7.7.2"
5554
},

packages/cashscript/test/e2e/P2PKH-tokens.test.ts

Lines changed: 85 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { randomUtxo, randomToken, randomNFT } from '../../src/utils.js';
22
import {
33
Contract, SignatureTemplate, ElectrumNetworkProvider, MockNetworkProvider,
4+
TransactionBuilder,
5+
NetworkProvider,
46
} from '../../src/index.js';
57
import {
68
alicePkh,
@@ -14,9 +16,10 @@ import artifact from '../fixture/p2pkh.artifact.js';
1416
// TODO: Replace this with unlockers
1517
describe('P2PKH-tokens', () => {
1618
let p2pkhInstance: Contract<typeof artifact>;
19+
let provider: NetworkProvider;
1720

1821
beforeAll(() => {
19-
const provider = process.env.TESTS_USE_CHIPNET
22+
provider = process.env.TESTS_USE_CHIPNET
2023
? new ElectrumNetworkProvider(Network.CHIPNET)
2124
: new MockNetworkProvider();
2225

@@ -61,15 +64,19 @@ describe('P2PKH-tokens', () => {
6164
throw new Error('No token UTXO found with fungible tokens');
6265
}
6366

67+
const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis;
68+
6469
const to = p2pkhInstance.tokenAddress;
6570
const amount = 1000n;
6671
const { token } = tokenUtxo;
67-
68-
const tx = await p2pkhInstance.functions
69-
.spend(alicePub, new SignatureTemplate(alicePriv))
70-
.from(nonTokenUtxos)
71-
.from(tokenUtxo)
72-
.to(to, amount, token)
72+
const fee = 1000n;
73+
const changeAmount = fullBchBalance - fee - amount;
74+
75+
const tx = await new TransactionBuilder({ provider })
76+
.addInputs(nonTokenUtxos, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
77+
.addInput(tokenUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
78+
.addOutput({ to, amount, token })
79+
.addOutput({ to, amount: changeAmount })
7380
.send();
7481

7582
const txOutputs = getTxOutputs(tx);
@@ -87,24 +94,21 @@ describe('P2PKH-tokens', () => {
8794

8895
const to = p2pkhInstance.tokenAddress;
8996
const amount = 1000n;
97+
const fee = 1000n;
98+
const fullBchBalance = nftUtxo1.satoshis + nftUtxo2.satoshis + nonTokenUtxos.reduce(
99+
(total, utxo) => total + utxo.satoshis, 0n,
100+
);
101+
const changeAmount = fullBchBalance - fee - amount;
90102

91-
// We ran into a bug with the order of the properties, so we re-order the properties here to test that it works
92-
const reorderedToken1 = {
93-
nft: {
94-
commitment: nftUtxo1.token!.nft!.commitment,
95-
capability: nftUtxo1.token!.nft!.capability,
96-
},
97-
category: nftUtxo1.token!.category,
98-
amount: 0n,
99-
};
103+
const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv));
100104

101-
const tx = await p2pkhInstance.functions
102-
.spend(alicePub, new SignatureTemplate(alicePriv))
103-
.from(nonTokenUtxos)
104-
.from(nftUtxo1)
105-
.from(nftUtxo2)
106-
.to(to, amount, reorderedToken1)
107-
.to(to, amount, nftUtxo2.token)
105+
const tx = await new TransactionBuilder({ provider })
106+
.addInputs(nonTokenUtxos, unlocker)
107+
.addInput(nftUtxo1, unlocker)
108+
.addInput(nftUtxo2, unlocker)
109+
.addOutput({ to, amount, token: nftUtxo1.token })
110+
.addOutput({ to, amount, token: nftUtxo2.token })
111+
.addOutput({ to, amount: changeAmount })
108112
.send();
109113

110114
const txOutputs = getTxOutputs(tx);
@@ -113,59 +117,23 @@ describe('P2PKH-tokens', () => {
113117
);
114118
});
115119

116-
it('can automatically select UTXOs for fungible tokens', async () => {
117-
const contractUtxos = await p2pkhInstance.getUtxos();
118-
const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo);
119-
120-
if (!tokenUtxo) {
121-
throw new Error('No token UTXO found with fungible tokens');
122-
}
123-
120+
it('can create new token category (NFT and fungible token)', async () => {
121+
const fee = 1000n;
124122
const to = p2pkhInstance.tokenAddress;
125-
const amount = 1000n;
126-
const { token } = tokenUtxo;
127-
128-
const tx = await p2pkhInstance.functions
129-
.spend(alicePub, new SignatureTemplate(alicePriv))
130-
.to(to, amount, token)
131-
.send();
132-
133-
const txOutputs = getTxOutputs(tx);
134-
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }]));
135-
});
136-
137-
it('adds automatic change output for fungible tokens', async () => {
138-
const contractUtxos = await p2pkhInstance.getUtxos();
139-
const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo);
140-
const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo);
141123

142-
if (!tokenUtxo) {
143-
throw new Error('No token UTXO found with fungible tokens');
144-
}
145-
146-
const to = p2pkhInstance.tokenAddress;
147-
const amount = 1000n;
148-
const { token } = tokenUtxo;
124+
// As a prerequisite to creating a new token category, we need a vout0 UTXO, so we create one here
125+
const nonTokenUtxosBeforeGenesis = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo);
126+
const preGenesisAmount = 10_000n;
127+
const fullBchBalance = nonTokenUtxosBeforeGenesis.reduce((total, utxo) => total + utxo.satoshis, 0n);
128+
const preGenesisChangeAmount = fullBchBalance - fee - preGenesisAmount;
149129

150-
const tx = await p2pkhInstance.functions
151-
.spend(alicePub, new SignatureTemplate(alicePriv))
152-
.from(nonTokenUtxos)
153-
.from(tokenUtxo)
154-
.to(to, amount)
130+
await new TransactionBuilder({ provider })
131+
.addInputs(nonTokenUtxosBeforeGenesis, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
132+
.addOutput({ to, amount: preGenesisAmount })
133+
.addOutput({ to, amount: preGenesisChangeAmount })
155134
.send();
156135

157-
const txOutputs = getTxOutputs(tx);
158-
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }]));
159-
});
160-
161-
it('can create new token categories', async () => {
162-
const to = p2pkhInstance.tokenAddress;
163-
164-
// Send a transaction to be used as the genesis UTXO
165-
await p2pkhInstance.functions
166-
.spend(alicePub, new SignatureTemplate(alicePriv))
167-
.to(to, 10_000n)
168-
.send();
136+
//////////////////////////////////////////////////////////////////////////////////////////////////
169137

170138
const contractUtxos = await p2pkhInstance.getUtxos();
171139
const [genesisUtxo] = contractUtxos.filter((utxo) => utxo.vout === 0 && utxo.satoshis > 2000);
@@ -175,6 +143,7 @@ describe('P2PKH-tokens', () => {
175143
}
176144

177145
const amount = 1000n;
146+
const changeAmount = genesisUtxo.satoshis - fee - amount;
178147
const token: TokenDetails = {
179148
amount: 1000n,
180149
category: genesisUtxo.txid,
@@ -184,72 +153,16 @@ describe('P2PKH-tokens', () => {
184153
},
185154
};
186155

187-
const tx = await p2pkhInstance.functions
188-
.spend(alicePub, new SignatureTemplate(alicePriv))
189-
.from(genesisUtxo)
190-
.to(to, amount, token)
191-
.send();
192-
193-
const txOutputs = getTxOutputs(tx);
194-
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }]));
195-
});
196-
197-
it('adds automatic change output for NFTs', async () => {
198-
const contractUtxos = await p2pkhInstance.getUtxos();
199-
const nftUtxo = contractUtxos.find(isNftUtxo);
200-
const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo);
201-
202-
if (!nftUtxo) {
203-
throw new Error('No token UTXO found with an NFT');
204-
}
205-
206-
const to = p2pkhInstance.tokenAddress;
207-
const amount = 1000n;
208-
const { token } = nftUtxo;
209-
210-
const tx = await p2pkhInstance.functions
211-
.spend(alicePub, new SignatureTemplate(alicePriv))
212-
.from(nonTokenUtxos)
213-
.from(nftUtxo)
214-
.to(to, amount)
215-
.send();
216-
217-
const txOutputs = getTxOutputs(tx);
218-
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }]));
219-
});
220-
221-
it('can disable automatic change output for fungible tokens', async () => {
222-
const contractUtxos = await p2pkhInstance.getUtxos();
223-
const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo);
224-
const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo);
225-
226-
if (!tokenUtxo) {
227-
throw new Error('No token UTXO found with fungible tokens');
228-
}
229-
230-
const to = p2pkhInstance.tokenAddress;
231-
const amount = 1000n;
232-
const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n };
233-
234-
const tx = await p2pkhInstance.functions
235-
.spend(alicePub, new SignatureTemplate(alicePriv))
236-
.from(nonTokenUtxos)
237-
.from(tokenUtxo)
238-
.to(to, amount, token)
239-
.withoutTokenChange()
156+
const tx = await new TransactionBuilder({ provider })
157+
.addInput(genesisUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
158+
.addOutput({ to, amount, token })
159+
.addOutput({ to, amount: changeAmount })
240160
.send();
241161

242162
const txOutputs = getTxOutputs(tx);
243163
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }]));
244-
245-
// Check that the change output is not present
246-
txOutputs.forEach((output) => {
247-
expect(output.token?.amount).not.toEqual(1n);
248-
});
249164
});
250165

251-
it.todo('can disable automatic change output for NFTs');
252-
253166
it('should throw an error when trying to send more tokens than the contract has', async () => {
254167
const contractUtxos = await p2pkhInstance.getUtxos();
255168
const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo);
@@ -262,15 +175,22 @@ describe('P2PKH-tokens', () => {
262175
const to = p2pkhInstance.tokenAddress;
263176
const amount = 1000n;
264177
const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount + 1n };
178+
const fee = 1000n;
179+
const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis;
180+
const changeAmount = fullBchBalance - fee - amount;
181+
182+
const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv));
265183

266-
const txPromise = p2pkhInstance.functions
267-
.spend(alicePub, new SignatureTemplate(alicePriv))
268-
.from(nonTokenUtxos)
269-
.from(tokenUtxo)
270-
.to(to, amount, token)
184+
const txPromise = new TransactionBuilder({ provider })
185+
.addInputs(nonTokenUtxos, unlocker)
186+
.addInput(tokenUtxo, unlocker)
187+
.addOutput({ to, amount, token })
188+
.addOutput({ to, amount: changeAmount })
271189
.send();
272190

273-
await expect(txPromise).rejects.toThrow(/Insufficient funds for token/);
191+
await expect(txPromise).rejects.toThrow(
192+
/the sum of fungible tokens in the transaction outputs exceed that of the transaction inputs for a category/,
193+
);
274194
});
275195

276196
it('should throw an error when trying to send a token the contract doesn\'t have', async () => {
@@ -285,15 +205,21 @@ describe('P2PKH-tokens', () => {
285205
const to = p2pkhInstance.tokenAddress;
286206
const amount = 1000n;
287207
const token = { ...tokenUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' };
288-
289-
const txPromise = p2pkhInstance.functions
290-
.spend(alicePub, new SignatureTemplate(alicePriv))
291-
.from(nonTokenUtxos)
292-
.from(tokenUtxo)
293-
.to(to, amount, token)
208+
const fee = 1000n;
209+
const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis;
210+
const changeAmount = fullBchBalance - fee - amount;
211+
212+
const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv));
213+
const txPromise = new TransactionBuilder({ provider })
214+
.addInputs(nonTokenUtxos, unlocker)
215+
.addInput(tokenUtxo, unlocker)
216+
.addOutput({ to, amount, token })
217+
.addOutput({ to, amount: changeAmount })
294218
.send();
295219

296-
await expect(txPromise).rejects.toThrow(/Insufficient funds for token/);
220+
await expect(txPromise).rejects.toThrow(
221+
/the transaction creates new fungible tokens for a category without a matching genesis input/,
222+
);
297223
});
298224

299225
it('should throw an error when trying to send an NFT the contract doesn\'t have', async () => {
@@ -308,20 +234,25 @@ describe('P2PKH-tokens', () => {
308234
const to = p2pkhInstance.tokenAddress;
309235
const amount = 1000n;
310236
const token = { ...nftUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' };
311-
312-
const txPromise = p2pkhInstance.functions
313-
.spend(alicePub, new SignatureTemplate(alicePriv))
314-
.from(nonTokenUtxos)
315-
.from(nftUtxo)
316-
.to(to, amount, token)
237+
const fee = 1000n;
238+
const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + nftUtxo.satoshis;
239+
const changeAmount = fullBchBalance - fee - amount;
240+
241+
const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv));
242+
const txPromise = new TransactionBuilder({ provider })
243+
.addInputs(nonTokenUtxos, unlocker)
244+
.addInput(nftUtxo, unlocker)
245+
.addOutput({ to, amount, token })
246+
.addOutput({ to, amount: changeAmount })
317247
.send();
318248

319-
await expect(txPromise).rejects.toThrow(/NFT output with token category .* does not have corresponding input/);
249+
await expect(txPromise).rejects.toThrow(
250+
/the transaction creates an immutable token for a category without a matching minting token/,
251+
);
320252
});
321253

322-
it.todo('can mint new NFTs if the NFT has minting capabilities');
323-
it.todo('can change the NFT commitment if the NFT has mutable capabilities');
324-
// TODO: Add more edge case tests for NFTs (minting, mutable, change outputs with multiple kinds of NFTs)
254+
it.todo('cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)');
255+
it.todo('can burn fungible tokens when allowImplicitFungibleTokenBurn is true');
325256
});
326257
});
327258

0 commit comments

Comments
 (0)