Skip to content

Commit

Permalink
feat: spend reducers and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
erictaylor committed Sep 20, 2024
1 parent 7183cfe commit 4a29afc
Show file tree
Hide file tree
Showing 8 changed files with 800 additions and 453 deletions.
3 changes: 3 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ module.exports = {
testEnvironment: 'node',
coverageProvider: 'v8',
extensionsToTreatAsEsm: ['.ts'],
// Experimental to fix issues with BigInt serialization
// See: https://jestjs.io/docs/configuration#workerthreads
workerThreads: true,
};
63 changes: 12 additions & 51 deletions src/vms/pvm/etna-builder/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ import {
getOwnerComplexity,
getSignerComplexity,
} from '../txs/fee';
import { spend, useSpendableLockedUTXOs, useUnlockedUTXOs } from './spend';
import { spend } from './spend';
import { useSpendableLockedUTXOs, useUnlockedUTXOs } from './spend-reducers';

const getAddressMaps = ({
inputs,
Expand Down Expand Up @@ -156,7 +157,7 @@ export const newBaseTx: TxBuilderFn<NewBaseTxProps> = (
outputComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses,
Expand All @@ -170,10 +171,6 @@ export const newBaseTx: TxBuilderFn<NewBaseTxProps> = (
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -319,7 +316,7 @@ export const newImportTx: TxBuilderFn<NewImportTxProps> = (
outputComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: importedAvax,
fromAddresses,
Expand All @@ -332,10 +329,6 @@ export const newImportTx: TxBuilderFn<NewImportTxProps> = (
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;

return new UnsignedTx(
Expand Down Expand Up @@ -397,7 +390,7 @@ export const newExportTx: TxBuilderFn<NewExportTxProps> = (
outputComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses,
Expand All @@ -410,10 +403,6 @@ export const newExportTx: TxBuilderFn<NewExportTxProps> = (
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -476,7 +465,7 @@ export const newCreateSubnetTx: TxBuilderFn<NewCreateSubnetTxProps> = (
ownerComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -488,10 +477,6 @@ export const newCreateSubnetTx: TxBuilderFn<NewCreateSubnetTxProps> = (
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -588,7 +573,7 @@ export const newCreateChainTx: TxBuilderFn<NewCreateChainTxProps> = (
authComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -600,10 +585,6 @@ export const newCreateChainTx: TxBuilderFn<NewCreateChainTxProps> = (
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -682,7 +663,7 @@ export const newAddSubnetValidatorTx: TxBuilderFn<
authComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -694,10 +675,6 @@ export const newAddSubnetValidatorTx: TxBuilderFn<
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -765,7 +742,7 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn<
authComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -777,10 +754,6 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn<
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -933,7 +906,7 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn<
delegatorOwnerComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -947,10 +920,6 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn<
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs, stakeOutputs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -1083,7 +1052,7 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn<
ownerComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -1097,10 +1066,6 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn<
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs, stakeOutputs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down Expand Up @@ -1198,7 +1163,7 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn<
ownerComplexity,
);

const [error, spendResults] = spend(
const spendResults = spend(
{
excessAVAX: 0n,
fromAddresses: addressesFromBytes(fromAddressesBytes),
Expand All @@ -1210,10 +1175,6 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn<
context,
);

if (error) {
throw error;
}

const { changeOutputs, inputs, inputUTXOs } = spendResults;
const addressMaps = getAddressMaps({
inputs,
Expand Down
138 changes: 138 additions & 0 deletions src/vms/pvm/etna-builder/spend-reducers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { jest } from '@jest/globals';

import { testContext } from '../../../fixtures/context';
import { Address, OutputOwners } from '../../../serializable';
import { defaultSpendOptions } from '../../common/defaultSpendOptions';
import { createDimensions } from '../../common/fees/dimensions';
import type { SpendHelperProps } from './spendHelper';
import { SpendHelper } from './spendHelper';
import type { SpendReducerState } from './spend-reducers';
import { handleFeeAndChange } from './spend-reducers';

const CHANGE_ADDRESS = Address.fromString(
'P-fuji1y50xa9363pn3d5gjhcz3ltp3fj6vq8x8a5txxg',
);
const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([
CHANGE_ADDRESS.toBytes(),
]);

const getInitialReducerState = (
state: Partial<SpendReducerState> = {},
): SpendReducerState => ({
excessAVAX: 0n,
initialComplexity: createDimensions(1, 1, 1, 1),
fromAddresses: [CHANGE_ADDRESS],
ownerOverride: null,
spendOptions: defaultSpendOptions(
state?.fromAddresses?.map((address) => address.toBytes()) ?? [
CHANGE_ADDRESS.toBytes(),
],
),
toBurn: new Map(),
toStake: new Map(),
utxos: [],
...state,
});

const getSpendHelper = ({
initialComplexity = createDimensions(1, 1, 1, 1),
shouldConsolidateOutputs = false,
toBurn = new Map(),
toStake = new Map(),
}: Partial<
Pick<
SpendHelperProps,
'initialComplexity' | 'shouldConsolidateOutputs' | 'toBurn' | 'toStake'
>
> = {}) => {
return new SpendHelper({
changeOutputs: [],
gasPrice: testContext.gasPrice,
initialComplexity,
inputs: [],
shouldConsolidateOutputs,
stakeOutputs: [],
toBurn,
toStake,
weights: testContext.complexityWeights,
});
};

describe('./src/vms/pvm/etna-builder/spend-reducers.test.ts', () => {
describe('handleFeeAndChange', () => {
test('throws an error if excessAVAX is less than the required fee', () => {
expect(() =>
handleFeeAndChange(
getInitialReducerState(),
getSpendHelper(),
testContext,
),
).toThrow(
`Insufficient funds: provided UTXOs need 4 more nAVAX (asset id: ${testContext.avaxAssetID})`,
);
});

test('returns original state if excessAVAX equals the required fee', () => {
const state = getInitialReducerState({ excessAVAX: 4n });
const spendHelper = getSpendHelper();
const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');
const calculateFeeWithTemporaryOutputComplexitySpy = jest.spyOn(
spendHelper,
'calculateFeeWithTemporaryOutputComplexity',
);

expect(handleFeeAndChange(state, getSpendHelper(), testContext)).toEqual(
state,
);
expect(
calculateFeeWithTemporaryOutputComplexitySpy,
).not.toHaveBeenCalled();
expect(addChangeOutputSpy).not.toHaveBeenCalled();
});

test('adds a change output if excessAVAX is greater than the required fee', () => {
const excessAVAX = 1_000n;
const state = getInitialReducerState({
excessAVAX,
});
const spendHelper = getSpendHelper();

const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');
const calculateFeeWithTemporaryOutputComplexitySpy = jest.spyOn(
spendHelper,
'calculateFeeWithTemporaryOutputComplexity',
);

expect(handleFeeAndChange(state, spendHelper, testContext)).toEqual({
...state,
excessAVAX,
});
expect(
calculateFeeWithTemporaryOutputComplexitySpy,
).toHaveBeenCalledTimes(1);
expect(addChangeOutputSpy).toHaveBeenCalledTimes(1);

expect(
spendHelper.hasChangeOutput(testContext.avaxAssetID, CHANGE_OWNERS),
).toBe(true);

expect(spendHelper.getInputsOutputs().changeOutputs).toHaveLength(1);
});

test('does not add change output if fee with temporary output complexity and excessAVAX are equal or less', () => {
const excessAVAX = 5n;
const state = getInitialReducerState({
excessAVAX,
});
const spendHelper = getSpendHelper();

const addChangeOutputSpy = jest.spyOn(spendHelper, 'addChangeOutput');

expect(handleFeeAndChange(state, spendHelper, testContext)).toEqual(
state,
);

expect(addChangeOutputSpy).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 4a29afc

Please sign in to comment.