From ad65b27b85a2a3714ca3c45826bdd196bddbe4c8 Mon Sep 17 00:00:00 2001 From: Eric Taylor Date: Mon, 14 Oct 2024 14:36:57 -0600 Subject: [PATCH] feat: adjust etna builder props (#882) * feat: move memo and minIssuanceTime props on etna builder * feat: add changeAddressesBytes to etna builder * fix: optional signature and public key on NewAddPermissionlessValidatorTxProps * feat: consolidate spend change props --- examples/p-chain/etna/import.ts | 2 +- .../useAvmAndCorethUTXOs.ts | 2 +- src/vms/common/models.ts | 2 +- src/vms/pvm/etna-builder/builder.test.ts | 60 +-- src/vms/pvm/etna-builder/builder.ts | 369 +++++++++++------- .../spend-reducers/fixtures/reducers.ts | 16 +- .../spend-reducers/handleFeeAndChange.ts | 15 +- .../useSpendableLockedUTXOs.test.ts | 32 +- .../spend-reducers/useSpendableLockedUTXOs.ts | 4 +- .../spend-reducers/useUnlockedUTXOs.test.ts | 21 +- .../spend-reducers/useUnlockedUTXOs.ts | 32 +- src/vms/pvm/etna-builder/spend.test.ts | 30 +- src/vms/pvm/etna-builder/spend.ts | 28 +- .../useSpendableLockedUTXOs.ts | 2 +- .../utxoCalculationFns/useUnlockedUTXOs.ts | 2 +- .../utils/verifySignaturesMatch.ts | 7 +- 16 files changed, 317 insertions(+), 307 deletions(-) diff --git a/examples/p-chain/etna/import.ts b/examples/p-chain/etna/import.ts index a3a93000c..b7329edd0 100644 --- a/examples/p-chain/etna/import.ts +++ b/examples/p-chain/etna/import.ts @@ -18,7 +18,7 @@ const main = async () => { feeState, fromAddressesBytes: [utils.bech32ToBytes(X_CHAIN_ADDRESS)], sourceChainId: context.xBlockchainID, - toAddresses: [utils.bech32ToBytes(P_CHAIN_ADDRESS)], + toAddressesBytes: [utils.bech32ToBytes(P_CHAIN_ADDRESS)], utxos, }, context, diff --git a/src/vms/avm/utxoCalculationFns/useAvmAndCorethUTXOs.ts b/src/vms/avm/utxoCalculationFns/useAvmAndCorethUTXOs.ts index f43b4cb46..20df46495 100644 --- a/src/vms/avm/utxoCalculationFns/useAvmAndCorethUTXOs.ts +++ b/src/vms/avm/utxoCalculationFns/useAvmAndCorethUTXOs.ts @@ -35,7 +35,7 @@ export const useAvmAndCorethUTXOs = ({ .filter((utxo) => !!isTransferOut(utxo.output as TransferOutput)), (utxo) => utxo.output as TransferOutput, fromAddresses, - options, + options.minIssuanceTime, ).forEach(({ sigData, data: utxo }) => { const utxoTransferout = utxo.output as TransferOutput; diff --git a/src/vms/common/models.ts b/src/vms/common/models.ts index 2358725df..57e2c02b1 100644 --- a/src/vms/common/models.ts +++ b/src/vms/common/models.ts @@ -8,6 +8,6 @@ export type SpendOptions = { export type SpendOptionsRequired = Required; -//the strign is address in hex +//the string is address in hex export type SigMapping = Map; export type SigMappings = SigMapping[]; diff --git a/src/vms/pvm/etna-builder/builder.test.ts b/src/vms/pvm/etna-builder/builder.test.ts index 080267d92..8859ead61 100644 --- a/src/vms/pvm/etna-builder/builder.test.ts +++ b/src/vms/pvm/etna-builder/builder.test.ts @@ -204,9 +204,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { fromAddressesBytes, feeState, outputs: [transferableOutput], - options: { - memo, - }, + memo, utxos, }, testContext, @@ -244,11 +242,9 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { { fromAddressesBytes, feeState, - options: { - memo, - }, + memo, sourceChainId: testContext.cBlockchainID, - toAddresses: [testAddress1], + toAddressesBytes: [testAddress1], utxos, }, testContext, @@ -310,9 +306,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { destinationChainId: testContext.cBlockchainID, feeState, fromAddressesBytes, - options: { - memo, - }, + memo, outputs: [tnsOut], utxos, }, @@ -363,9 +357,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { { fromAddressesBytes, feeState, - options: { - memo, - }, + memo, subnetOwners: [toAddress], utxos: [getValidUtxo(new BigIntPr(utxoInputAmt))], }, @@ -406,9 +398,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { fromAddressesBytes, fxIds: [], genesisData: testGenesisData, - options: { - memo, - }, + memo, subnetAuth: [0], subnetId: Id.fromHex(testSubnetId).toString(), utxos: [getValidUtxo(new BigIntPr(utxoInputAmt))], @@ -455,9 +445,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, subnetAuth: [0], subnetId: Id.fromHex(testSubnetId).toString(), start: 100n, @@ -506,9 +494,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { fromAddressesBytes, feeState, nodeId, - options: { - memo, - }, + memo, subnetAuth: [0], subnetId: Id.fromHex(testSubnetId).toString(), utxos: [getValidUtxo(new BigIntPr(utxoInputAmt))], @@ -553,9 +539,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, publicKey: blsPublicKeyBytes(), rewardAddresses: [], shares: 1, @@ -621,9 +605,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, publicKey: blsPublicKeyBytes(), rewardAddresses: [], shares: 1, @@ -690,9 +672,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, publicKey: blsPublicKeyBytes(), rewardAddresses: [], shares: 1, @@ -750,9 +730,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, rewardAddresses: [], start: 0n, subnetId: PrimaryNetworkID.toString(), @@ -811,9 +789,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, rewardAddresses: [], start: 0n, subnetId: Id.fromHex(testSubnetId).toString(), @@ -873,9 +849,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { feeState, fromAddressesBytes, nodeId, - options: { - memo, - }, + memo, rewardAddresses: [], stakingAssetId: stakingAssetId.toString(), start: 0n, @@ -930,9 +904,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { { fromAddressesBytes, feeState, - options: { - memo, - }, + memo, subnetAuth, subnetId: Id.fromHex(testSubnetId).toString(), subnetOwners: [toAddress], @@ -988,7 +960,7 @@ describe('./src/vms/pvm/etna-builder/builder.test.ts', () => { fromAddressesBytes, feeState, sourceChainId: testContext.cBlockchainID, - toAddresses: [testAddress1], + toAddressesBytes: [testAddress1], utxos, }, testContext, diff --git a/src/vms/pvm/etna-builder/builder.ts b/src/vms/pvm/etna-builder/builder.ts index 8eca44a7b..8ee235f9d 100644 --- a/src/vms/pvm/etna-builder/builder.ts +++ b/src/vms/pvm/etna-builder/builder.ts @@ -44,8 +44,7 @@ import { createSignerOrSignerEmptyFromStrings } from '../../../serializable/pvm/ import { AddressMaps, addressesFromBytes, isTransferOut } from '../../../utils'; import { matchOwners } from '../../../utils/matchOwners'; import { compareTransferableOutputs } from '../../../utils/sort'; -import { baseTxUnsafePvm, type SpendOptions, UnsignedTx } from '../../common'; -import { defaultSpendOptions } from '../../common/defaultSpendOptions'; +import { baseTxUnsafePvm, UnsignedTx } from '../../common'; import type { Dimensions } from '../../common/fees/dimensions'; import { addDimensions, createDimensions } from '../../common/fees/dimensions'; import type { Context } from '../../context'; @@ -70,6 +69,24 @@ import { import { spend } from './spend'; import { useSpendableLockedUTXOs, useUnlockedUTXOs } from './spend-reducers'; +/** + * Creates OutputOwners used for change outputs with the specified + * `changeAddressBytes` if provided, otherwise uses the `fromAddressesBytes`. + */ +const getChangeOutputOwners = ({ + fromAddressesBytes, + changeAddressesBytes, +}: { + fromAddressesBytes: readonly Uint8Array[]; + changeAddressesBytes?: readonly Uint8Array[]; +}): OutputOwners => { + return OutputOwners.fromNative( + changeAddressesBytes ?? fromAddressesBytes, + 0n, + 1, + ); +}; + const getAddressMaps = ({ inputs, inputUTXOs, @@ -89,27 +106,49 @@ const getAddressMaps = ({ ); }; -const getMemoComplexity = ( - spendOptions: Required, -): Dimensions => { +const getMemoComplexity = (memo: Uint8Array): Dimensions => { return createDimensions({ - bandwidth: spendOptions.memo.length, + bandwidth: memo.length, dbRead: 0, dbWrite: 0, compute: 0, }); }; +const getDefaultMinIssuanceTime = (): bigint => { + return BigInt(Math.floor(new Date().getTime() / 1000)); +}; + /** * Common properties used in all PVM transaction builder functions. */ type CommonTxProps = Readonly<{ + /** + * List of addresses that are used for change outputs. + * + * Defaults to the addresses provided in `fromAddressesBytes`. + */ + changeAddressesBytes?: readonly Uint8Array[]; + /** + * The current fee state returned from `PVMApi.getFeeState()`. + */ feeState: FeeState; /** * List of addresses that are used for selecting which UTXOs are signable. */ fromAddressesBytes: readonly Uint8Array[]; - options?: SpendOptions; + /** + * Contains arbitrary bytes (up to 256 bytes). + * + * Defaults to an empty byte array. + */ + memo?: Uint8Array; + /** + * Minimum time in Unix seconds. + * + * Defaults to the current time in Unix seconds. + */ + minIssuanceTime?: bigint; /** * List of UTXOs that are available to be spent. */ @@ -133,19 +172,23 @@ export type NewBaseTxProps = TxProps<{ /** * Creates a new unsigned PVM base transaction (`BaseTx`) using calculated dynamic fees. * - * @param props {NewBaseTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newBaseTx: TxBuilderFn = ( - { feeState, fromAddressesBytes, options, outputs, utxos }, + { + changeAddressesBytes, + feeState, + fromAddressesBytes, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), + outputs, + utxos, + }, context, ) => { const fromAddresses = addressesFromBytes(fromAddressesBytes); - const defaultedOptions = defaultSpendOptions( - [...fromAddressesBytes], - options, - ); const toBurn = new Map(); outputs.forEach((out) => { @@ -155,7 +198,7 @@ export const newBaseTx: TxBuilderFn = ( toBurn.set(assetId, amountToBurn); }); - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const outputComplexity = getOutputComplexity(outputs); @@ -167,12 +210,16 @@ export const newBaseTx: TxBuilderFn = ( const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses, initialComplexity: complexity, + minIssuanceTime, shouldConsolidateOutputs: true, - spendOptions: defaultedOptions, toBurn, utxos, }, @@ -184,7 +231,7 @@ export const newBaseTx: TxBuilderFn = ( const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -193,55 +240,56 @@ export const newBaseTx: TxBuilderFn = ( ); return new UnsignedTx( - new BaseTx( - baseTxUnsafePvm(context, allOutputs, inputs, defaultedOptions.memo), - ), + new BaseTx(baseTxUnsafePvm(context, allOutputs, inputs, memo)), inputUTXOs, addressMaps, ); }; -export type NewImportTxProps = TxProps<{ - /** - * The locktime to write onto the UTXO. - */ - locktime?: bigint; - /** - * Base58 string of the source chain ID. - */ - sourceChainId: string; - /** - * The threshold to write on the UTXO. - */ - threshold?: number; - /** - * List of addresses to import into. - */ - toAddresses: readonly Uint8Array[]; -}>; +export type NewImportTxProps = Omit< + TxProps<{ + /** + * The locktime to write onto the UTXO. + */ + locktime?: bigint; + /** + * Base58 string of the source chain ID. + */ + sourceChainId: string; + /** + * The threshold to write on the UTXO. + */ + threshold?: number; + /** + * List of addresses to import into. + */ + toAddressesBytes: readonly Uint8Array[]; + }>, + 'changeAddressesBytes' +>; /** * Creates a new unsigned PVM import transaction (`ImportTx`) using calculated dynamic fees. * - * @param props {NewImportTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newImportTx: TxBuilderFn = ( { feeState, fromAddressesBytes, locktime, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), sourceChainId, threshold, - toAddresses, + toAddressesBytes, utxos, }, context, ) => { const fromAddresses = addressesFromBytes(fromAddressesBytes); - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); const { importedInputs, importedAmounts } = utxos .filter( @@ -256,11 +304,8 @@ export const newImportTx: TxBuilderFn = ( }>( (acc, utxo) => { const { sigIndicies: inputSigIndices } = - matchOwners( - utxo.getOutputOwners(), - fromAddresses, - defaultedOptions.minIssuanceTime, - ) || {}; + matchOwners(utxo.getOutputOwners(), fromAddresses, minIssuanceTime) || + {}; if (inputSigIndices === undefined) { // We couldn't spend this UTXO, so we skip to the next one. @@ -300,23 +345,17 @@ export const newImportTx: TxBuilderFn = ( const addressMaps = AddressMaps.fromTransferableInputs( importedInputs, utxos, - defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, ); const outputs: TransferableOutput[] = Object.entries(importedAmounts) .filter(([assetID]) => assetID !== context.avaxAssetID) .map(([assetID, amount]) => - TransferableOutput.fromNative( - assetID, - amount, - toAddresses, - locktime, - threshold, - ), + TransferableOutput.fromNative(assetID, amount, toAddressesBytes), ); - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const inputComplexity = getInputComplexity(importedInputs); @@ -331,12 +370,16 @@ export const newImportTx: TxBuilderFn = ( const spendResults = spend( { + changeOutputOwners: OutputOwners.fromNative( + toAddressesBytes, + locktime, + threshold, + ), excessAVAX: importedAvax, feeState, fromAddresses, initialComplexity: complexity, - ownerOverride: OutputOwners.fromNative(toAddresses, locktime, threshold), - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -352,7 +395,7 @@ export const newImportTx: TxBuilderFn = ( PlatformChainID, [...outputs, ...changeOutputs].sort(compareTransferableOutputs), inputs, - new Bytes(defaultedOptions.memo), + new Bytes(memo), ), Id.fromString(sourceChainId), importedInputs.sort(TransferableInput.compare), @@ -376,17 +419,25 @@ export type NewExportTxProps = TxProps<{ /** * Creates a new unsigned PVM export transaction (`ExportTx`) using calculated dynamic fees. * - * @param props {NewExportTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newExportTx: TxBuilderFn = ( - { destinationChainId, feeState, fromAddressesBytes, options, outputs, utxos }, + { + changeAddressesBytes, + destinationChainId, + feeState, + fromAddressesBytes, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), + outputs, + utxos, + }, context, ) => { const fromAddresses = addressesFromBytes(fromAddressesBytes); - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); const toBurn = new Map(); outputs.forEach((output) => { @@ -394,7 +445,7 @@ export const newExportTx: TxBuilderFn = ( toBurn.set(assetId, (toBurn.get(assetId) ?? 0n) + output.output.amount()); }); - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const outputComplexity = getOutputComplexity(outputs); @@ -406,11 +457,15 @@ export const newExportTx: TxBuilderFn = ( const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses, initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, toBurn, utxos, }, @@ -422,7 +477,7 @@ export const newExportTx: TxBuilderFn = ( const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -433,7 +488,7 @@ export const newExportTx: TxBuilderFn = ( PlatformChainID, changeOutputs, inputs, - new Bytes(defaultedOptions.memo), + new Bytes(memo), ), Id.fromString(destinationChainId), [...outputs].sort(compareTransferableOutputs), @@ -458,25 +513,25 @@ export type NewCreateSubnetTxProps = TxProps<{ /** * Creates a new unsigned PVM create subnet transaction (`CreateSubnetTx`) using calculated dynamic fees. * - * @param props {NewCreateSubnetTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newCreateSubnetTx: TxBuilderFn = ( { + changeAddressesBytes, fromAddressesBytes, feeState, locktime, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), subnetOwners, threshold, utxos, }, context, ) => { - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const ownerComplexity = getOwnerComplexity( OutputOwners.fromNative(subnetOwners, locktime, threshold), @@ -490,11 +545,15 @@ export const newCreateSubnetTx: TxBuilderFn = ( const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -505,7 +564,7 @@ export const newCreateSubnetTx: TxBuilderFn = ( const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -515,7 +574,7 @@ export const newCreateSubnetTx: TxBuilderFn = ( context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), OutputOwners.fromNative(subnetOwners, locktime, threshold), ); @@ -553,18 +612,20 @@ export type NewCreateChainTxProps = TxProps<{ /** * Creates a new unsigned PVM create chain transaction (`CreateChainTx`) using calculated dynamic fees. * - * @param props {NewCreateChainTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newCreateChainTx: TxBuilderFn = ( { + changeAddressesBytes, chainName, feeState, fromAddressesBytes, fxIds, genesisData, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), subnetAuth, subnetId, utxos, @@ -572,8 +633,6 @@ export const newCreateChainTx: TxBuilderFn = ( }, context, ) => { - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - const genesisBytes = new Bytes( new TextEncoder().encode(JSON.stringify(genesisData)), ); @@ -585,7 +644,7 @@ export const newCreateChainTx: TxBuilderFn = ( fxIds.length * ID_LEN + chainName.length + genesisBytes.length + - defaultedOptions.memo.length, + memo.length, dbRead: 0, dbWrite: 0, compute: 0, @@ -601,11 +660,15 @@ export const newCreateChainTx: TxBuilderFn = ( const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -616,7 +679,7 @@ export const newCreateChainTx: TxBuilderFn = ( const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -626,7 +689,7 @@ export const newCreateChainTx: TxBuilderFn = ( context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), Id.fromString(subnetId), new Stringpr(chainName), @@ -658,19 +721,21 @@ export type NewAddSubnetValidatorTxProps = TxProps<{ * Creates a new unsigned PVM add subnet validator transaction * (`AddSubnetValidatorTx`) using calculated dynamic fees. * - * @param props {NewAddSubnetValidatorTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newAddSubnetValidatorTx: TxBuilderFn< NewAddSubnetValidatorTxProps > = ( { + changeAddressesBytes, end, feeState, fromAddressesBytes, nodeId, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), start, subnetAuth, subnetId, @@ -679,9 +744,7 @@ export const newAddSubnetValidatorTx: TxBuilderFn< }, context, ) => { - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const authComplexity = getAuthComplexity(Input.fromNative(subnetAuth)); @@ -693,11 +756,15 @@ export const newAddSubnetValidatorTx: TxBuilderFn< const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -708,7 +775,7 @@ export const newAddSubnetValidatorTx: TxBuilderFn< const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -718,7 +785,7 @@ export const newAddSubnetValidatorTx: TxBuilderFn< context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), SubnetValidator.fromNative( nodeId, @@ -749,27 +816,27 @@ export type NewRemoveSubnetValidatorTxProps = TxProps<{ * Creates a new unsigned PVM remove subnet validator transaction * (`RemoveSubnetValidatorTx`) using calculated dynamic fees. * - * @param props {NewRemoveSubnetValidatorTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newRemoveSubnetValidatorTx: TxBuilderFn< NewRemoveSubnetValidatorTxProps > = ( { + changeAddressesBytes, fromAddressesBytes, feeState, nodeId, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), subnetAuth, subnetId, utxos, }, context, ) => { - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const authComplexity = getAuthComplexity(Input.fromNative(subnetAuth)); @@ -781,11 +848,15 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn< const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -796,7 +867,7 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn< const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -806,7 +877,7 @@ export const newRemoveSubnetValidatorTx: TxBuilderFn< context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), NodeId.fromString(nodeId), Id.fromString(subnetId), @@ -835,7 +906,7 @@ export type NewAddPermissionlessValidatorTxProps = TxProps<{ /** * The BLS public key. */ - publicKey: Uint8Array; + publicKey?: Uint8Array; /** * The addresses which will receive the rewards from the delegated stake. * Given addresses will share the reward UTXO. @@ -849,7 +920,7 @@ export type NewAddPermissionlessValidatorTxProps = TxProps<{ /** * The BLS signature. */ - signature: Uint8Array; + signature?: Uint8Array; /** * Which asset to stake. Defaults to AVAX. */ @@ -880,21 +951,23 @@ export type NewAddPermissionlessValidatorTxProps = TxProps<{ * Creates a new unsigned PVM add permissionless validator transaction * (`AddPermissionlessValidatorTx`) using calculated dynamic fees. * - * @param props {NewAddPermissionlessValidatorTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newAddPermissionlessValidatorTx: TxBuilderFn< NewAddPermissionlessValidatorTxProps > = ( { + changeAddressesBytes, delegatorRewardsOwner, end, feeState, fromAddressesBytes, locktime = 0n, nodeId, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), publicKey, rewardAddresses, shares, @@ -918,8 +991,6 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn< const toStake = new Map([[assetId, weight]]); - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - const signer = createSignerOrSignerEmptyFromStrings(publicKey, signature); const validatorOutputOwners = OutputOwners.fromNative( rewardAddresses, @@ -931,7 +1002,7 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn< 0n, ); - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const signerComplexity = getSignerComplexity(signer); const validatorOwnerComplexity = getOwnerComplexity(validatorOutputOwners); @@ -947,12 +1018,16 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn< const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, + minIssuanceTime, shouldConsolidateOutputs: true, - spendOptions: defaultedOptions, toStake, utxos, }, @@ -964,7 +1039,7 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn< const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -974,7 +1049,7 @@ export const newAddPermissionlessValidatorTx: TxBuilderFn< context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), SubnetValidator.fromNative( nodeId, @@ -1041,20 +1116,22 @@ export type NewAddPermissionlessDelegatorTxProps = TxProps<{ * Creates a new unsigned PVM add permissionless delegator transaction * (`AddPermissionlessDelegatorTx`) using calculated dynamic fees. * - * @param props {NewAddPermissionlessDelegatorTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newAddPermissionlessDelegatorTx: TxBuilderFn< NewAddPermissionlessDelegatorTxProps > = ( { + changeAddressesBytes, end, feeState, fromAddressesBytes, locktime = 0n, nodeId, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), rewardAddresses, stakingAssetId, start, @@ -1075,15 +1152,13 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn< const toStake = new Map([[assetId, weight]]); - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - const delegatorRewardsOwner = OutputOwners.fromNative( rewardAddresses, locktime, threshold, ); - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const ownerComplexity = getOwnerComplexity(delegatorRewardsOwner); @@ -1095,12 +1170,16 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn< const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, + minIssuanceTime, shouldConsolidateOutputs: true, - spendOptions: defaultedOptions, toStake, utxos, }, @@ -1112,7 +1191,7 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn< const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -1122,7 +1201,7 @@ export const newAddPermissionlessDelegatorTx: TxBuilderFn< context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), SubnetValidator.fromNative( nodeId, @@ -1169,18 +1248,20 @@ export type NewTransferSubnetOwnershipTxProps = TxProps<{ * Creates a new unsigned PVM transfer subnet ownership transaction * (`TransferSubnetOwnershipTx`) using calculated dynamic fees. * - * @param props {NewTransferSubnetOwnershipTxProps} - * @param context {Context} - * @returns {UnsignedTx} An UnsignedTx. + * @param props + * @param context + * @returns An UnsignedTx. */ export const newTransferSubnetOwnershipTx: TxBuilderFn< NewTransferSubnetOwnershipTxProps > = ( { + changeAddressesBytes, fromAddressesBytes, feeState, locktime = 0n, - options, + memo = new Uint8Array(), + minIssuanceTime = getDefaultMinIssuanceTime(), subnetAuth, subnetId, subnetOwners, @@ -1189,9 +1270,7 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn< }, context, ) => { - const defaultedOptions = defaultSpendOptions(fromAddressesBytes, options); - - const memoComplexity = getMemoComplexity(defaultedOptions); + const memoComplexity = getMemoComplexity(memo); const authComplexity = getAuthComplexity(Input.fromNative(subnetAuth)); @@ -1208,11 +1287,15 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn< const spendResults = spend( { + changeOutputOwners: getChangeOutputOwners({ + changeAddressesBytes, + fromAddressesBytes, + }), excessAVAX: 0n, feeState, fromAddresses: addressesFromBytes(fromAddressesBytes), initialComplexity: complexity, - spendOptions: defaultedOptions, + minIssuanceTime, utxos, }, [useUnlockedUTXOs], @@ -1223,7 +1306,7 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn< const addressMaps = getAddressMaps({ inputs, inputUTXOs, - minIssuanceTime: defaultedOptions.minIssuanceTime, + minIssuanceTime, fromAddressesBytes, }); @@ -1234,7 +1317,7 @@ export const newTransferSubnetOwnershipTx: TxBuilderFn< context.pBlockchainID, changeOutputs, inputs, - defaultedOptions.memo, + memo, ), Id.fromString(subnetId), Input.fromNative(subnetAuth), diff --git a/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts b/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts index 6d17e4f1c..038485043 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts @@ -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'; @@ -17,11 +15,9 @@ export const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([ ]); export const getInitialReducerState = ({ - spendOptions, ...state -}: Partial> & { - spendOptions?: SpendOptions; -} = {}): SpendReducerState => ({ +}: Partial = {}): SpendReducerState => ({ + changeOutputOwners: CHANGE_OWNERS, excessAVAX: 0n, initialComplexity: createDimensions({ bandwidth: 1, @@ -30,13 +26,7 @@ export const getInitialReducerState = ({ compute: 1, }), fromAddresses: [CHANGE_ADDRESS], - ownerOverride: null, - spendOptions: defaultSpendOptions( - state?.fromAddresses?.map((address) => address.toBytes()) ?? [ - CHANGE_ADDRESS.toBytes(), - ], - spendOptions, - ), + minIssuanceTime: BigInt(Math.floor(new Date().getTime() / 1000)), toBurn: new Map(), toStake: new Map(), utxos: [], diff --git a/src/vms/pvm/etna-builder/spend-reducers/handleFeeAndChange.ts b/src/vms/pvm/etna-builder/spend-reducers/handleFeeAndChange.ts index 94f7f9a08..5ad15ff49 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/handleFeeAndChange.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/handleFeeAndChange.ts @@ -1,7 +1,6 @@ import { BigIntPr, Id, - OutputOwners, TransferOutput, TransferableOutput, } from '../../../../serializable'; @@ -42,18 +41,12 @@ export const handleFeeAndChange: SpendReducerFunction = ( spendHelper, context, ) => { - // 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); - const requiredFee = spendHelper.calculateFee(); // Checks for an existing change output that is for the AVAX asset assigned to the change owner. const hasExistingChangeOutput: boolean = spendHelper.hasChangeOutput( context.avaxAssetID, - changeOwners, + state.changeOutputOwners, ); if (canPayFeeAndNeedsChange(state.excessAVAX, requiredFee, context)) { @@ -65,7 +58,7 @@ export const handleFeeAndChange: SpendReducerFunction = ( Id.fromString(context.avaxAssetID), new TransferOutput( new BigIntPr(state.excessAVAX - requiredFee), - changeOwners, + state.changeOutputOwners, ), ), ); @@ -75,7 +68,7 @@ export const handleFeeAndChange: SpendReducerFunction = ( const requiredFeeWithChangeOutput = spendHelper.calculateFee( new TransferableOutput( Id.fromString(context.avaxAssetID), - new TransferOutput(new BigIntPr(0n), changeOwners), + new TransferOutput(new BigIntPr(0n), state.changeOutputOwners), ), ); @@ -88,7 +81,7 @@ export const handleFeeAndChange: SpendReducerFunction = ( Id.fromString(context.avaxAssetID), new TransferOutput( new BigIntPr(state.excessAVAX - requiredFeeWithChangeOutput), - changeOwners, + state.changeOutputOwners, ), ), ); diff --git a/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.test.ts b/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.test.ts index b83793049..0870d0234 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.test.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.test.ts @@ -39,9 +39,7 @@ describe('useSpendableLockedUTXOs', () => { test('returns `false` if UTXO output is a stakeable lockout but locktime is greater than minIssuanceTime', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const utxo = getStakeableLockoutOutput(testUTXOID1, 50n, 200n); @@ -51,9 +49,7 @@ describe('useSpendableLockedUTXOs', () => { test('returns `false` if UTXO output is a stakeable lockout with valid locktime but not used in toStake', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 300n, - }, + minIssuanceTime: 300n, }); const utxo = getStakeableLockoutOutput(testUTXOID1, 50n, 100n); @@ -65,9 +61,7 @@ describe('useSpendableLockedUTXOs', () => { const testAssetId = Id.fromString('testasset'); const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, toStake: new Map([[testAssetId.toString(), 100n]]), }); @@ -83,9 +77,7 @@ describe('useSpendableLockedUTXOs', () => { test('throws an error if UTXO output is a StakeableLockOut and the transferOut is not a TransferOutput', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const invalidUTXO = new Utxo( @@ -117,9 +109,7 @@ describe('useSpendableLockedUTXOs', () => { Address.fromString('P-fuji1y50xa9363pn3d5gjhcz3ltp3fj6vq8x8a5txxg'), ], excessAVAX: 0n, - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, toBurn, toStake, utxos: [getStakeableLockoutOutput(testUTXOID1, 10_000n, 300n)], @@ -138,9 +128,7 @@ describe('useSpendableLockedUTXOs', () => { const initialState = getInitialReducerState({ excessAVAX: 0n, - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, toBurn, toStake, utxos: [getStakeableLockoutOutput(testUTXOID1, 10_000n, 300n)], @@ -169,9 +157,7 @@ describe('useSpendableLockedUTXOs', () => { const initialState = getInitialReducerState({ fromAddresses: [testOwnerXAddress], excessAVAX: 0n, - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, toBurn, toStake, utxos: [ @@ -202,9 +188,7 @@ describe('useSpendableLockedUTXOs', () => { const initialState = getInitialReducerState({ fromAddresses: [testOwnerXAddress], excessAVAX: 0n, - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, toBurn, toStake, utxos: [ diff --git a/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.ts b/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.ts index 4f2870230..c2df41916 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/useSpendableLockedUTXOs.ts @@ -33,7 +33,7 @@ export const getUsableUTXOsFilter = } // 1b. Ensure UTXO is stakeable. - if (state.spendOptions.minIssuanceTime >= utxo.output.getLocktime()) { + if (state.minIssuanceTime >= utxo.output.getLocktime()) { return false; } @@ -64,7 +64,7 @@ export const useSpendableLockedUTXOs: SpendReducerFunction = ( usableUTXOs, (utxo) => utxo.output.transferOut, state.fromAddresses, - state.spendOptions, + state.minIssuanceTime, ); // 3. Do all the logic for spending based on the UTXOs. diff --git a/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.test.ts b/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.test.ts index ad85fafc8..ce8324f74 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.test.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.test.ts @@ -32,9 +32,7 @@ describe('useUnlockedUTXOs', () => { describe('getUsableUTXOsFilter', () => { test('returns `true` if UTXO output is a TransferOutput and the locktime is less than the minIssuanceTime', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const utxo = getValidUtxo(); expect(getUsableUTXOsFilter(state)(utxo)).toBe(true); @@ -42,9 +40,7 @@ describe('useUnlockedUTXOs', () => { test('returns `false` if UTXO output is a TransferOutput and the locktime is equal or greater than the minIssuanceTime', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const utxo = getLockedUTXO(new BigIntPr(100n), 100n); expect(getUsableUTXOsFilter(state)(utxo)).toBe(false); @@ -52,9 +48,7 @@ describe('useUnlockedUTXOs', () => { test('returns `true` if UTXO output is a StakeableLockOut and the locktime is less than the minIssuanceTime', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const utxo = getStakeableLockoutOutput(testUTXOID1, 100n, 50n); @@ -64,9 +58,7 @@ describe('useUnlockedUTXOs', () => { test('returns `false` if UTXO output is a StakeableLockOut and the locktime is equal or greater than the minIssuanceTime', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const utxo = getStakeableLockoutOutput(testUTXOID1, 100n, 100n); @@ -76,9 +68,7 @@ describe('useUnlockedUTXOs', () => { test('throws an error if UTXO output is a StakeableLockOut and the transferOut is not a TransferOutput', () => { const state = getInitialReducerState({ - spendOptions: { - minIssuanceTime: 100n, - }, + minIssuanceTime: 100n, }); const invalidUTXO = new Utxo( @@ -125,7 +115,6 @@ describe('useUnlockedUTXOs', () => { const { inputs } = spendHelper.getInputsOutputs(); expect(state.excessAVAX).toEqual(10_000n - 4_900n - 4_900n); - expect(state.ownerOverride).toBe(null); expect(inputs).toHaveLength(1); expect(inputs[0].getAssetId()).toEqual(testContext.avaxAssetID); }); diff --git a/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.ts b/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.ts index c95406269..257e20bac 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/useUnlockedUTXOs.ts @@ -1,6 +1,5 @@ import { BigIntPr, - OutputOwners, TransferInput, TransferOutput, TransferableInput, @@ -38,7 +37,7 @@ export const getUsableUTXOsFilter = throw IncorrectStakeableLockOutError; } - return utxo.output.getLocktime() < state.spendOptions.minIssuanceTime; + return utxo.output.getLocktime() < state.minIssuanceTime; }; export const useUnlockedUTXOs: SpendReducerFunction = ( @@ -58,7 +57,7 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( (utxo) => isTransferOut(utxo.output) ? utxo.output : utxo.output.transferOut, state.fromAddresses, - state.spendOptions, + state.minIssuanceTime, ); // 3. Split verified usable UTXOs into AVAX assetId UTXOs and other assetId UTXOs. @@ -89,12 +88,6 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( { otherVerifiedUsableUTXOs: [], avaxVerifiedUsableUTXOs: [] }, ); - const changeOwner = OutputOwners.fromNative( - state.spendOptions.changeAddresses, - 0n, - 1, - ); - // 4. Handle all the non-AVAX asset UTXOs first. for (const { sigData, data: utxo } of otherVerifiedUsableUTXOs) { const utxoInfo = getUtxoInfo(utxo); @@ -129,7 +122,10 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( spendHelper.addStakedOutput( new TransferableOutput( utxo.assetId, - new TransferOutput(new BigIntPr(amountToStake), changeOwner), + new TransferOutput( + new BigIntPr(amountToStake), + state.changeOutputOwners, + ), ), ); } @@ -139,7 +135,10 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( spendHelper.addChangeOutput( new TransferableOutput( utxo.assetId, - new TransferOutput(new BigIntPr(remainingAmount), changeOwner), + new TransferOutput( + new BigIntPr(remainingAmount), + state.changeOutputOwners, + ), ), ); } @@ -147,7 +146,7 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( // 5. Handle AVAX asset UTXOs last to account for fees. let excessAVAX = state.excessAVAX; - let clearOwnerOverride = false; + for (const { sigData, data: utxo } of avaxVerifiedUsableUTXOs) { const requiredFee = spendHelper.calculateFee(); @@ -181,20 +180,19 @@ export const useUnlockedUTXOs: SpendReducerFunction = ( spendHelper.addStakedOutput( new TransferableOutput( utxo.assetId, - new TransferOutput(new BigIntPr(amountToStake), changeOwner), + new TransferOutput( + new BigIntPr(amountToStake), + state.changeOutputOwners, + ), ), ); } excessAVAX += remainingAmount; - - // The ownerOverride is no longer needed. Clear it. - clearOwnerOverride = true; } return { ...state, excessAVAX, - ownerOverride: clearOwnerOverride ? null : state.ownerOverride, }; }; diff --git a/src/vms/pvm/etna-builder/spend.test.ts b/src/vms/pvm/etna-builder/spend.test.ts index 41a48d6e9..bdcbe0f0b 100644 --- a/src/vms/pvm/etna-builder/spend.test.ts +++ b/src/vms/pvm/etna-builder/spend.test.ts @@ -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, @@ -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((state) => state), @@ -27,6 +27,7 @@ const CHANGE_OWNERS: OutputOwners = OutputOwners.fromNative([ ]); const getSpendProps = (state: Partial = {}): SpendProps => ({ + changeOutputOwners: CHANGE_OWNERS, excessAVAX: 0n, initialComplexity: createDimensions({ bandwidth: 1, @@ -36,12 +37,7 @@ const getSpendProps = (state: Partial = {}): SpendProps => ({ }), feeState: testFeeState(), fromAddresses: [CHANGE_ADDRESS], - ownerOverride: null, - spendOptions: defaultSpendOptions( - state?.fromAddresses?.map((address) => address.toBytes()) ?? [ - CHANGE_ADDRESS.toBytes(), - ], - ), + minIssuanceTime: BigInt(Math.floor(new Date().getTime() / 1000)), toBurn: new Map(), toStake: new Map(), utxos: [], @@ -81,13 +77,15 @@ 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 output owners in state should default to from addresses', () => { expect.assertions(1); const initialState = getSpendProps({ excessAVAX: 1_000n }); const testReducer = jest.fn((state) => { - expect(state.ownerOverride).toEqual( - OutputOwners.fromNative(initialState.spendOptions.changeAddresses), + expect(state.changeOutputOwners).toEqual( + OutputOwners.fromNative( + initialState.fromAddresses.map((address) => address.toBytes()), + ), ); return state; }); @@ -95,15 +93,21 @@ describe('./src/vms/pvm/etna-builder/spend.test.ts', () => { spend(initialState, [testReducer], testContext); }); - test('change owners in state should be ownerOverride if provided', () => { + test('change output owners in state should be value provided', () => { expect.assertions(1); + const changeAddressesBytes = [ + bech32ToBytes('P-fuji1t43hr35eu9enk7tfyqq4ukpww4stpzf74kxjfk'), + ]; + + const OWNERS = OutputOwners.fromNative(changeAddressesBytes); + const initialState = getSpendProps({ + changeOutputOwners: OWNERS, excessAVAX: 1_000n, - ownerOverride: CHANGE_OWNERS, }); const testReducer = jest.fn((state) => { - expect(state.ownerOverride).toBe(CHANGE_OWNERS); + expect(state.changeOutputOwners).toBe(OWNERS); return state; }); diff --git a/src/vms/pvm/etna-builder/spend.ts b/src/vms/pvm/etna-builder/spend.ts index 1c42dc645..3467640e8 100644 --- a/src/vms/pvm/etna-builder/spend.ts +++ b/src/vms/pvm/etna-builder/spend.ts @@ -5,7 +5,6 @@ import type { } from '../../../serializable'; import { OutputOwners } from '../../../serializable'; import type { Utxo } from '../../../serializable/avax/utxo'; -import type { SpendOptions } from '../../common'; import type { Dimensions } from '../../common/fees/dimensions'; import type { Context } from '../../context'; import type { FeeState } from '../models'; @@ -37,6 +36,10 @@ type SpendResult = Readonly<{ }>; export type SpendProps = Readonly<{ + /** + * Output owners for the change outputs. + */ + changeOutputOwners: OutputOwners; /** * The extra AVAX that spend can produce in * the change outputs in addition to the consumed and not burned AVAX. @@ -51,21 +54,13 @@ export type SpendProps = Readonly<{ * The initial complexity of the transaction. */ initialComplexity: Dimensions; - /** - * 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; + minIssuanceTime: bigint; /** * Whether to consolidate change and stake outputs. * * @default false */ shouldConsolidateOutputs?: boolean; - spendOptions: Required; /** * Maps `assetID` to the amount of the asset to spend without * producing an output. This is typically used for fees. @@ -102,13 +97,13 @@ export type SpendProps = Readonly<{ */ export const spend = ( { + changeOutputOwners, excessAVAX = 0n, feeState, fromAddresses, initialComplexity, - ownerOverride, + minIssuanceTime, shouldConsolidateOutputs = false, - spendOptions, toBurn = new Map(), toStake = new Map(), utxos, @@ -118,7 +113,10 @@ export const spend = ( ): SpendResult => { try { const changeOwners = - ownerOverride || OutputOwners.fromNative(spendOptions.changeAddresses); + changeOutputOwners || + OutputOwners.fromNative( + fromAddresses.map((address) => address.toBytes()), + ); const gasPrice: bigint = feeState.price; @@ -135,11 +133,11 @@ export const spend = ( }); const initialState: SpendReducerState = { + changeOutputOwners: changeOwners, excessAVAX, initialComplexity, fromAddresses, - ownerOverride: changeOwners, - spendOptions, + minIssuanceTime, toBurn, toStake, utxos, diff --git a/src/vms/pvm/utxoCalculationFns/useSpendableLockedUTXOs.ts b/src/vms/pvm/utxoCalculationFns/useSpendableLockedUTXOs.ts index 7924de498..6f9ddc401 100644 --- a/src/vms/pvm/utxoCalculationFns/useSpendableLockedUTXOs.ts +++ b/src/vms/pvm/utxoCalculationFns/useSpendableLockedUTXOs.ts @@ -75,7 +75,7 @@ export function useSpendableLockedUTXOs({ return lockedOutput.transferOut as TransferOutput; }, fromAddresses, - options, + options.minIssuanceTime, ).forEach(({ sigData, data: { utxo, assetId, lockedOutput } }) => { const out = lockedOutput.transferOut as TransferOutput; const remainingAmountToStake = amountsToStake.get(assetId) ?? 0n; diff --git a/src/vms/pvm/utxoCalculationFns/useUnlockedUTXOs.ts b/src/vms/pvm/utxoCalculationFns/useUnlockedUTXOs.ts index 58144ac48..9574f88c2 100644 --- a/src/vms/pvm/utxoCalculationFns/useUnlockedUTXOs.ts +++ b/src/vms/pvm/utxoCalculationFns/useUnlockedUTXOs.ts @@ -54,7 +54,7 @@ export function useUnlockedUTXOs({ ) as TransferOutput; }, fromAddresses, - options, + options.minIssuanceTime, ).forEach(({ sigData, data: utxo }) => { const remainingAmountToBurn = amountsToBurn.get(utxo.assetId.value()) ?? 0n; diff --git a/src/vms/utils/calculateSpend/utils/verifySignaturesMatch.ts b/src/vms/utils/calculateSpend/utils/verifySignaturesMatch.ts index cc6f970a8..67f13ebd5 100644 --- a/src/vms/utils/calculateSpend/utils/verifySignaturesMatch.ts +++ b/src/vms/utils/calculateSpend/utils/verifySignaturesMatch.ts @@ -1,7 +1,6 @@ import type { MatchOwnerResult } from '../../../../utils/matchOwners'; import { matchOwners } from '../../../../utils/matchOwners'; import type { Address, TransferOutput } from '../../../../serializable'; -import type { SpendOptionsRequired } from '../../../common'; export type verifySigMatchItem = Required<{ sigData: MatchOwnerResult; @@ -18,7 +17,7 @@ export const NoSigMatchError = new Error('No addresses match UTXO owners'); * @param set the utxo or data set, this can change depending on the calcFn * @param getTransferOutput a callback that takes a utxo and gets the output * @param fromAddresses the addresses the utxos should belong to - * @param options + * @param minIssuanceTime the minimum issuance time for the tx * @returns T[] * @throws Error */ @@ -26,7 +25,7 @@ export function verifySignaturesMatch( set: T[], getTransferOutput: (utxo: T) => TransferOutput, fromAddresses: readonly Address[], - options: SpendOptionsRequired, + minIssuanceTime: bigint, ): readonly verifySigMatchItem[] { const outs = set.reduce((acc, data) => { const out = getTransferOutput(data); @@ -34,7 +33,7 @@ export function verifySignaturesMatch( const sigData = matchOwners( out.outputOwners, [...fromAddresses], - options.minIssuanceTime, + minIssuanceTime, ); return sigData ? [...acc, { sigData, data }] : acc;