Skip to content

Commit

Permalink
feat: add changeAddressesBytes to etna builder
Browse files Browse the repository at this point in the history
  • Loading branch information
erictaylor committed Oct 11, 2024
1 parent d5f5847 commit c2b4df2
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 108 deletions.
87 changes: 31 additions & 56 deletions src/vms/pvm/etna-builder/builder.ts

Large diffs are not rendered by default.

18 changes: 5 additions & 13 deletions src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { testContext } from '../../../../../fixtures/context';
import { feeState as testFeeState } from '../../../../../fixtures/pvm';
import { Address, OutputOwners } from '../../../../../serializable';
import type { SpendOptions } from '../../../../common';
import { defaultSpendOptions } from '../../../../common/defaultSpendOptions';
import { createDimensions } from '../../../../common/fees/dimensions';
import type { FeeState } from '../../../models';
import type { SpendHelperProps } from '../../spendHelper';
Expand All @@ -17,11 +15,12 @@ export const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([
]);

export const getInitialReducerState = ({
spendOptions,
...state
}: Partial<Omit<SpendReducerState, 'spendOptions'>> & {
spendOptions?: SpendOptions;
} = {}): SpendReducerState => ({
}: Partial<SpendReducerState> = {}): SpendReducerState => ({
changeAddressesBytes: state?.fromAddresses?.map((address) =>
address.toBytes(),
) ?? [CHANGE_ADDRESS.toBytes()],
changeOwnerOverride: null,
excessAVAX: 0n,
initialComplexity: createDimensions({
bandwidth: 1,
Expand All @@ -31,13 +30,6 @@ export const getInitialReducerState = ({
}),
fromAddresses: [CHANGE_ADDRESS],
minIssuanceTime: BigInt(Math.floor(new Date().getTime() / 1000)),
ownerOverride: null,
spendOptions: defaultSpendOptions(
state?.fromAddresses?.map((address) => address.toBytes()) ?? [
CHANGE_ADDRESS.toBytes(),
],
spendOptions,
),
toBurn: new Map(),
toStake: new Map(),
utxos: [],
Expand Down
4 changes: 2 additions & 2 deletions src/vms/pvm/etna-builder/spend-reducers/handleFeeAndChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export const handleFeeAndChange: SpendReducerFunction = (
// Use the change owner override if it exists, otherwise use the default change owner.
// This is used on "import" transactions.
const changeOwners =
state.ownerOverride ??
OutputOwners.fromNative(state.spendOptions.changeAddresses);
state.changeOwnerOverride ??
OutputOwners.fromNative(state.changeAddressesBytes);

const requiredFee = spendHelper.calculateFee();

Expand Down
9 changes: 8 additions & 1 deletion src/vms/pvm/etna-builder/spend-reducers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import type { SpendProps } from '../spend';
import type { SpendHelper } from '../spendHelper';

export type SpendReducerState = Readonly<
Required<Omit<SpendProps, 'feeState' | 'shouldConsolidateOutputs'>>
Required<
Omit<
SpendProps,
'changeAddressesBytes' | 'feeState' | 'shouldConsolidateOutputs'
>
> & {
changeAddressesBytes: readonly Uint8Array[];
}
>;

export type SpendReducerFunction = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('useUnlockedUTXOs', () => {
const { inputs } = spendHelper.getInputsOutputs();

expect(state.excessAVAX).toEqual(10_000n - 4_900n - 4_900n);
expect(state.ownerOverride).toBe(null);
expect(state.changeOwnerOverride).toBe(null);
expect(inputs).toHaveLength(1);
expect(inputs[0].getAssetId()).toEqual(testContext.avaxAssetID);
});
Expand Down
10 changes: 6 additions & 4 deletions src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const useUnlockedUTXOs: SpendReducerFunction = (
);

const changeOwner = OutputOwners.fromNative(
state.spendOptions.changeAddresses,
state.changeAddressesBytes,
0n,
1,
);
Expand Down Expand Up @@ -147,7 +147,7 @@ export const useUnlockedUTXOs: SpendReducerFunction = (

// 5. Handle AVAX asset UTXOs last to account for fees.
let excessAVAX = state.excessAVAX;
let clearOwnerOverride = false;
let clearChangeOwnerOverride = false;
for (const { sigData, data: utxo } of avaxVerifiedUsableUTXOs) {
const requiredFee = spendHelper.calculateFee();

Expand Down Expand Up @@ -189,12 +189,14 @@ export const useUnlockedUTXOs: SpendReducerFunction = (
excessAVAX += remainingAmount;

// The ownerOverride is no longer needed. Clear it.
clearOwnerOverride = true;
clearChangeOwnerOverride = true;
}

return {
...state,
changeOwnerOverride: clearChangeOwnerOverride
? null
: state.changeOwnerOverride,
excessAVAX,
ownerOverride: clearOwnerOverride ? null : state.ownerOverride,
};
};
47 changes: 35 additions & 12 deletions src/vms/pvm/etna-builder/spend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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 {
verifyAssetsConsumed,
Expand All @@ -13,6 +12,7 @@ import {
import type { SpendProps } from './spend';
import { spend } from './spend';
import { feeState as testFeeState } from '../../../fixtures/pvm';
import { bech32ToBytes } from '../../../utils';

jest.mock('./spend-reducers', () => ({
verifyAssetsConsumed: jest.fn<SpendReducerFunction>((state) => state),
Expand All @@ -27,6 +27,7 @@ const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([
]);

const getSpendProps = (state: Partial<SpendReducerState> = {}): SpendProps => ({
changeOwnerOverride: null,
excessAVAX: 0n,
initialComplexity: createDimensions({
bandwidth: 1,
Expand All @@ -37,12 +38,6 @@ const getSpendProps = (state: Partial<SpendReducerState> = {}): SpendProps => ({
feeState: testFeeState(),
fromAddresses: [CHANGE_ADDRESS],
minIssuanceTime: BigInt(Math.floor(new Date().getTime() / 1000)),
ownerOverride: null,
spendOptions: defaultSpendOptions(
state?.fromAddresses?.map((address) => address.toBytes()) ?? [
CHANGE_ADDRESS.toBytes(),
],
),
toBurn: new Map(),
toStake: new Map(),
utxos: [],
Expand Down Expand Up @@ -82,13 +77,36 @@ describe('./src/vms/pvm/etna-builder/spend.test.ts', () => {
).toThrow('An unexpected error occurred during spend calculation');
});

test('change owners in state should default to change addresses', () => {
test('change owners in state should default to from addresses', () => {
expect.assertions(1);

const initialState = getSpendProps({ excessAVAX: 1_000n });
const testReducer = jest.fn<SpendReducerFunction>((state) => {
expect(state.ownerOverride).toEqual(
OutputOwners.fromNative(initialState.spendOptions.changeAddresses),
expect(state.changeOwnerOverride).toEqual(
OutputOwners.fromNative(
initialState.fromAddresses.map((address) => address.toBytes()),
),
);
return state;
});

spend(initialState, [testReducer], testContext);
});

test('change owners in state should be change addresses', () => {
expect.assertions(1);

const changeAddressesBytes = [
bech32ToBytes('P-fuji1t43hr35eu9enk7tfyqq4ukpww4stpzf74kxjfk'),
];

const initialState = getSpendProps({
changeAddressesBytes,
excessAVAX: 1_000n,
});
const testReducer = jest.fn<SpendReducerFunction>((state) => {
expect(state.changeOwnerOverride).toEqual(
OutputOwners.fromNative(changeAddressesBytes),
);
return state;
});
Expand All @@ -99,12 +117,17 @@ describe('./src/vms/pvm/etna-builder/spend.test.ts', () => {
test('change owners in state should be ownerOverride if provided', () => {
expect.assertions(1);

const changeAddressesBytes = [
bech32ToBytes('P-fuji1t43hr35eu9enk7tfyqq4ukpww4stpzf74kxjfk'),
];

const initialState = getSpendProps({
changeAddressesBytes,
changeOwnerOverride: CHANGE_OWNERS,
excessAVAX: 1_000n,
ownerOverride: CHANGE_OWNERS,
});
const testReducer = jest.fn<SpendReducerFunction>((state) => {
expect(state.ownerOverride).toBe(CHANGE_OWNERS);
expect(state.changeOwnerOverride).toBe(CHANGE_OWNERS);
return state;
});

Expand Down
50 changes: 35 additions & 15 deletions src/vms/pvm/etna-builder/spend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ import type { FeeState } from '../models';
import type { SpendReducerFunction, SpendReducerState } from './spend-reducers';
import { handleFeeAndChange, verifyAssetsConsumed } from './spend-reducers';
import { SpendHelper } from './spendHelper';
import type { BuilderSpendOptions } from './types';

const getChangeAddressesBytes = (
changeAddressesBytes: readonly Uint8Array[] | undefined,
fromAddresses: readonly Uint8Array[],
): readonly Uint8Array[] => {
if (changeAddressesBytes && changeAddressesBytes.length > 0) {
return changeAddressesBytes;
}

return [...fromAddresses];
};

type SpendResult = Readonly<{
/**
Expand All @@ -37,6 +47,20 @@ type SpendResult = Readonly<{
}>;

export type SpendProps = Readonly<{
/**
* List of addresses that are used for change outputs.
*
* Defaults to the addresses provided in `fromAddressesBytes`.
*/
changeAddressesBytes?: readonly Uint8Array[];
/**
* Optionally specifies the output owners to use for the unlocked
* AVAX change output if no additional AVAX was needed to be burned.
* If this value is `undefined` or `null`, the default change owner is used.
*
* Used in ImportTx.
*/
changeOwnerOverride?: OutputOwners | null;
/**
* The extra AVAX that spend can produce in
* the change outputs in addition to the consumed and not burned AVAX.
Expand All @@ -52,21 +76,12 @@ export type SpendProps = Readonly<{
*/
initialComplexity: Dimensions;
minIssuanceTime: bigint;
/**
* Optionally specifies the output owners to use for the unlocked
* AVAX change output if no additional AVAX was needed to be burned.
* If this value is `undefined` or `null`, the default change owner is used.
*
* Used in ImportTx.
*/
ownerOverride?: OutputOwners | null;
/**
* Whether to consolidate change and stake outputs.
*
* @default false
*/
shouldConsolidateOutputs?: boolean;
spendOptions: Required<BuilderSpendOptions>;
/**
* Maps `assetID` to the amount of the asset to spend without
* producing an output. This is typically used for fees.
Expand Down Expand Up @@ -103,14 +118,14 @@ export type SpendProps = Readonly<{
*/
export const spend = (
{
changeAddressesBytes: _changeAddressesBytes,
changeOwnerOverride,
excessAVAX = 0n,
feeState,
fromAddresses,
initialComplexity,
minIssuanceTime,
ownerOverride,
shouldConsolidateOutputs = false,
spendOptions,
toBurn = new Map(),
toStake = new Map(),
utxos,
Expand All @@ -119,8 +134,13 @@ export const spend = (
context: Context,
): SpendResult => {
try {
const changeAddressesBytes = getChangeAddressesBytes(
_changeAddressesBytes,
fromAddresses.map((address) => address.toBytes()),
);

const changeOwners =
ownerOverride || OutputOwners.fromNative(spendOptions.changeAddresses);
changeOwnerOverride || OutputOwners.fromNative(changeAddressesBytes);

const gasPrice: bigint = feeState.price;

Expand All @@ -137,12 +157,12 @@ export const spend = (
});

const initialState: SpendReducerState = {
changeAddressesBytes,
changeOwnerOverride: changeOwners,
excessAVAX,
initialComplexity,
fromAddresses,
minIssuanceTime,
ownerOverride: changeOwners,
spendOptions,
toBurn,
toStake,
utxos,
Expand Down
4 changes: 0 additions & 4 deletions src/vms/pvm/etna-builder/types.ts

This file was deleted.

0 comments on commit c2b4df2

Please sign in to comment.