Skip to content

Commit 4a29afc

Browse files
committed
feat: spend reducers and tests
1 parent 7183cfe commit 4a29afc

File tree

8 files changed

+800
-453
lines changed

8 files changed

+800
-453
lines changed

jest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ module.exports = {
1515
testEnvironment: 'node',
1616
coverageProvider: 'v8',
1717
extensionsToTreatAsEsm: ['.ts'],
18+
// Experimental to fix issues with BigInt serialization
19+
// See: https://jestjs.io/docs/configuration#workerthreads
20+
workerThreads: true,
1821
};

src/vms/pvm/etna-builder/builder.ts

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ import {
6565
getOwnerComplexity,
6666
getSignerComplexity,
6767
} from '../txs/fee';
68-
import { spend, useSpendableLockedUTXOs, useUnlockedUTXOs } from './spend';
68+
import { spend } from './spend';
69+
import { useSpendableLockedUTXOs, useUnlockedUTXOs } from './spend-reducers';
6970

7071
const getAddressMaps = ({
7172
inputs,
@@ -156,7 +157,7 @@ export const newBaseTx: TxBuilderFn<NewBaseTxProps> = (
156157
outputComplexity,
157158
);
158159

159-
const [error, spendResults] = spend(
160+
const spendResults = spend(
160161
{
161162
excessAVAX: 0n,
162163
fromAddresses,
@@ -170,10 +171,6 @@ export const newBaseTx: TxBuilderFn<NewBaseTxProps> = (
170171
context,
171172
);
172173

173-
if (error) {
174-
throw error;
175-
}
176-
177174
const { changeOutputs, inputs, inputUTXOs } = spendResults;
178175
const addressMaps = getAddressMaps({
179176
inputs,
@@ -319,7 +316,7 @@ export const newImportTx: TxBuilderFn<NewImportTxProps> = (
319316
outputComplexity,
320317
);
321318

322-
const [error, spendResults] = spend(
319+
const spendResults = spend(
323320
{
324321
excessAVAX: importedAvax,
325322
fromAddresses,
@@ -332,10 +329,6 @@ export const newImportTx: TxBuilderFn<NewImportTxProps> = (
332329
context,
333330
);
334331

335-
if (error) {
336-
throw error;
337-
}
338-
339332
const { changeOutputs, inputs, inputUTXOs } = spendResults;
340333

341334
return new UnsignedTx(
@@ -397,7 +390,7 @@ export const newExportTx: TxBuilderFn<NewExportTxProps> = (
397390
outputComplexity,
398391
);
399392

400-
const [error, spendResults] = spend(
393+
const spendResults = spend(
401394
{
402395
excessAVAX: 0n,
403396
fromAddresses,
@@ -410,10 +403,6 @@ export const newExportTx: TxBuilderFn<NewExportTxProps> = (
410403
context,
411404
);
412405

413-
if (error) {
414-
throw error;
415-
}
416-
417406
const { changeOutputs, inputs, inputUTXOs } = spendResults;
418407
const addressMaps = getAddressMaps({
419408
inputs,
@@ -476,7 +465,7 @@ export const newCreateSubnetTx: TxBuilderFn<NewCreateSubnetTxProps> = (
476465
ownerComplexity,
477466
);
478467

479-
const [error, spendResults] = spend(
468+
const spendResults = spend(
480469
{
481470
excessAVAX: 0n,
482471
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -488,10 +477,6 @@ export const newCreateSubnetTx: TxBuilderFn<NewCreateSubnetTxProps> = (
488477
context,
489478
);
490479

491-
if (error) {
492-
throw error;
493-
}
494-
495480
const { changeOutputs, inputs, inputUTXOs } = spendResults;
496481
const addressMaps = getAddressMaps({
497482
inputs,
@@ -588,7 +573,7 @@ export const newCreateChainTx: TxBuilderFn<NewCreateChainTxProps> = (
588573
authComplexity,
589574
);
590575

591-
const [error, spendResults] = spend(
576+
const spendResults = spend(
592577
{
593578
excessAVAX: 0n,
594579
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -600,10 +585,6 @@ export const newCreateChainTx: TxBuilderFn<NewCreateChainTxProps> = (
600585
context,
601586
);
602587

603-
if (error) {
604-
throw error;
605-
}
606-
607588
const { changeOutputs, inputs, inputUTXOs } = spendResults;
608589
const addressMaps = getAddressMaps({
609590
inputs,
@@ -682,7 +663,7 @@ export const newAddSubnetValidatorTx: TxBuilderFn<
682663
authComplexity,
683664
);
684665

685-
const [error, spendResults] = spend(
666+
const spendResults = spend(
686667
{
687668
excessAVAX: 0n,
688669
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -694,10 +675,6 @@ export const newAddSubnetValidatorTx: TxBuilderFn<
694675
context,
695676
);
696677

697-
if (error) {
698-
throw error;
699-
}
700-
701678
const { changeOutputs, inputs, inputUTXOs } = spendResults;
702679
const addressMaps = getAddressMaps({
703680
inputs,
@@ -765,7 +742,7 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn<
765742
authComplexity,
766743
);
767744

768-
const [error, spendResults] = spend(
745+
const spendResults = spend(
769746
{
770747
excessAVAX: 0n,
771748
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -777,10 +754,6 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn<
777754
context,
778755
);
779756

780-
if (error) {
781-
throw error;
782-
}
783-
784757
const { changeOutputs, inputs, inputUTXOs } = spendResults;
785758
const addressMaps = getAddressMaps({
786759
inputs,
@@ -933,7 +906,7 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn<
933906
delegatorOwnerComplexity,
934907
);
935908

936-
const [error, spendResults] = spend(
909+
const spendResults = spend(
937910
{
938911
excessAVAX: 0n,
939912
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -947,10 +920,6 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn<
947920
context,
948921
);
949922

950-
if (error) {
951-
throw error;
952-
}
953-
954923
const { changeOutputs, inputs, inputUTXOs, stakeOutputs } = spendResults;
955924
const addressMaps = getAddressMaps({
956925
inputs,
@@ -1083,7 +1052,7 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn<
10831052
ownerComplexity,
10841053
);
10851054

1086-
const [error, spendResults] = spend(
1055+
const spendResults = spend(
10871056
{
10881057
excessAVAX: 0n,
10891058
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -1097,10 +1066,6 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn<
10971066
context,
10981067
);
10991068

1100-
if (error) {
1101-
throw error;
1102-
}
1103-
11041069
const { changeOutputs, inputs, inputUTXOs, stakeOutputs } = spendResults;
11051070
const addressMaps = getAddressMaps({
11061071
inputs,
@@ -1198,7 +1163,7 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn<
11981163
ownerComplexity,
11991164
);
12001165

1201-
const [error, spendResults] = spend(
1166+
const spendResults = spend(
12021167
{
12031168
excessAVAX: 0n,
12041169
fromAddresses: addressesFromBytes(fromAddressesBytes),
@@ -1210,10 +1175,6 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn<
12101175
context,
12111176
);
12121177

1213-
if (error) {
1214-
throw error;
1215-
}
1216-
12171178
const { changeOutputs, inputs, inputUTXOs } = spendResults;
12181179
const addressMaps = getAddressMaps({
12191180
inputs,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { jest } from '@jest/globals';
2+
3+
import { testContext } from '../../../fixtures/context';
4+
import { Address, OutputOwners } from '../../../serializable';
5+
import { defaultSpendOptions } from '../../common/defaultSpendOptions';
6+
import { createDimensions } from '../../common/fees/dimensions';
7+
import type { SpendHelperProps } from './spendHelper';
8+
import { SpendHelper } from './spendHelper';
9+
import type { SpendReducerState } from './spend-reducers';
10+
import { handleFeeAndChange } from './spend-reducers';
11+
12+
const CHANGE_ADDRESS = Address.fromString(
13+
'P-fuji1y50xa9363pn3d5gjhcz3ltp3fj6vq8x8a5txxg',
14+
);
15+
const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([
16+
CHANGE_ADDRESS.toBytes(),
17+
]);
18+
19+
const getInitialReducerState = (
20+
state: Partial<SpendReducerState> = {},
21+
): SpendReducerState => ({
22+
excessAVAX: 0n,
23+
initialComplexity: createDimensions(1, 1, 1, 1),
24+
fromAddresses: [CHANGE_ADDRESS],
25+
ownerOverride: null,
26+
spendOptions: defaultSpendOptions(
27+
state?.fromAddresses?.map((address) => address.toBytes()) ?? [
28+
CHANGE_ADDRESS.toBytes(),
29+
],
30+
),
31+
toBurn: new Map(),
32+
toStake: new Map(),
33+
utxos: [],
34+
...state,
35+
});
36+
37+
const getSpendHelper = ({
38+
initialComplexity = createDimensions(1, 1, 1, 1),
39+
shouldConsolidateOutputs = false,
40+
toBurn = new Map(),
41+
toStake = new Map(),
42+
}: Partial<
43+
Pick<
44+
SpendHelperProps,
45+
'initialComplexity' | 'shouldConsolidateOutputs' | 'toBurn' | 'toStake'
46+
>
47+
> = {}) => {
48+
return new SpendHelper({
49+
changeOutputs: [],
50+
gasPrice: testContext.gasPrice,
51+
initialComplexity,
52+
inputs: [],
53+
shouldConsolidateOutputs,
54+
stakeOutputs: [],
55+
toBurn,
56+
toStake,
57+
weights: testContext.complexityWeights,
58+
});
59+
};
60+
61+
describe('./src/vms/pvm/etna-builder/spend-reducers.test.ts', () => {
62+
describe('handleFeeAndChange', () => {
63+
test('throws an error if excessAVAX is less than the required fee', () => {
64+
expect(() =>
65+
handleFeeAndChange(
66+
getInitialReducerState(),
67+
getSpendHelper(),
68+
testContext,
69+
),
70+
).toThrow(
71+
`Insufficient funds: provided UTXOs need 4 more nAVAX (asset id: ${testContext.avaxAssetID})`,
72+
);
73+
});
74+
75+
test('returns original state if excessAVAX equals the required fee', () => {
76+
const state = getInitialReducerState({ excessAVAX: 4n });
77+
const spendHelper = getSpendHelper();
78+
const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');
79+
const calculateFeeWithTemporaryOutputComplexitySpy = jest.spyOn(
80+
spendHelper,
81+
'calculateFeeWithTemporaryOutputComplexity',
82+
);
83+
84+
expect(handleFeeAndChange(state, getSpendHelper(), testContext)).toEqual(
85+
state,
86+
);
87+
expect(
88+
calculateFeeWithTemporaryOutputComplexitySpy,
89+
).not.toHaveBeenCalled();
90+
expect(addChangeOutputSpy).not.toHaveBeenCalled();
91+
});
92+
93+
test('adds a change output if excessAVAX is greater than the required fee', () => {
94+
const excessAVAX = 1_000n;
95+
const state = getInitialReducerState({
96+
excessAVAX,
97+
});
98+
const spendHelper = getSpendHelper();
99+
100+
const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');
101+
const calculateFeeWithTemporaryOutputComplexitySpy = jest.spyOn(
102+
spendHelper,
103+
'calculateFeeWithTemporaryOutputComplexity',
104+
);
105+
106+
expect(handleFeeAndChange(state, spendHelper, testContext)).toEqual({
107+
...state,
108+
excessAVAX,
109+
});
110+
expect(
111+
calculateFeeWithTemporaryOutputComplexitySpy,
112+
).toHaveBeenCalledTimes(1);
113+
expect(addChangeOutputSpy).toHaveBeenCalledTimes(1);
114+
115+
expect(
116+
spendHelper.hasChangeOutput(testContext.avaxAssetID, CHANGE_OWNERS),
117+
).toBe(true);
118+
119+
expect(spendHelper.getInputsOutputs().changeOutputs).toHaveLength(1);
120+
});
121+
122+
test('does not add change output if fee with temporary output complexity and excessAVAX are equal or less', () => {
123+
const excessAVAX = 5n;
124+
const state = getInitialReducerState({
125+
excessAVAX,
126+
});
127+
const spendHelper = getSpendHelper();
128+
129+
const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');
130+
131+
expect(handleFeeAndChange(state, spendHelper, testContext)).toEqual(
132+
state,
133+
);
134+
135+
expect(addChangeOutputSpy).not.toHaveBeenCalled();
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)