diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java
index cdec47683..f8c3f6c6d 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java
@@ -88,7 +88,21 @@ import net.fabricmc.fabric.impl.transfer.context.SingleSlotContainerItemContext;
 @ApiStatus.Experimental
 public interface ContainerItemContext {
 	/**
-	 * Return a context for the passed player's hand. This is recommended for item use interactions.
+	 * Returns a context for interaction with a player's hand. This is recommended for item use interactions.
+	 *
+	 * <p>In creative mode, {@link #withInitial(ItemStack)} is used to avoid modifying the item in hand.
+	 * Otherwise, {@link #ofPlayerHand} is used.
+	 */
+	static ContainerItemContext forPlayerInteraction(PlayerEntity player, Hand hand) {
+		if (player.getAbilities().creativeMode) {
+			return withInitial(player.getStackInHand(hand));
+		} else {
+			return ofPlayerHand(player, hand);
+		}
+	}
+
+	/**
+	 * Return a context for the passed player's hand.
 	 */
 	static ContainerItemContext ofPlayerHand(PlayerEntity player, Hand hand) {
 		return new PlayerContainerItemContext(player, hand);
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java
index be3476277..d6ac5e77b 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java
@@ -117,7 +117,7 @@ public final class FluidStorage {
 	 * This means that per-item combined providers registered through {@code combinedItemApiProvider} DO NOT prevent these general providers from running,
 	 * however regular providers registered through {@code ItemApiLookup#register...} that return a non-null API instance DO prevent it.
 	 */
-	public static Event<CombinedItemApiProvider> GENERAL_COMBINED_PROVIDER = CombinedProvidersImpl.createEvent(false);
+	public static final Event<CombinedItemApiProvider> GENERAL_COMBINED_PROVIDER = CombinedProvidersImpl.createEvent(false);
 
 	@FunctionalInterface
 	public interface CombinedItemApiProvider {
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorageUtil.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorageUtil.java
new file mode 100644
index 000000000..7abef4c65
--- /dev/null
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorageUtil.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.transfer.v1.fluid;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.fluid.Fluids;
+import net.minecraft.item.Item;
+import net.minecraft.item.Items;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.Hand;
+
+import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
+import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
+import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
+import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
+
+/**
+ * Helper functions to work with fluid storages.
+ *
+ * <p><b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
+ * The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
+ */
+@ApiStatus.Experimental
+public final class FluidStorageUtil {
+	/**
+	 * Try to make the item in a player hand "interact" with a fluid storage.
+	 * This can be used when a player right-clicks a tank, for example.
+	 *
+	 * <p>More specifically, this function tries to find a fluid storing item in the player's hand.
+	 * Then, it tries to fill that item from the storage. If that fails, it tries to fill the storage from that item.
+	 *
+	 * <p>Only up to one fluid variant will be moved, and the corresponding emptying/filling sound will be played.
+	 * In creative mode, the player's inventory will not be modified.
+	 *
+	 * @param storage The storage that the player is interacting with.
+	 * @param player The player.
+	 * @param hand The hand that the player used.
+	 * @return True if some fluid was moved.
+	 */
+	public static boolean interactWithFluidStorage(Storage<FluidVariant> storage, PlayerEntity player, Hand hand) {
+		// Check if hand is a fluid container.
+		Storage<FluidVariant> handStorage = ContainerItemContext.forPlayerInteraction(player, hand).find(FluidStorage.ITEM);
+		if (handStorage == null) return false;
+
+		// Try to fill hand first, otherwise try to empty it.
+		Item handItem = player.getStackInHand(hand).getItem();
+		return moveWithSound(storage, handStorage, player, true, handItem) || moveWithSound(handStorage, storage, player, false, handItem);
+	}
+
+	private static boolean moveWithSound(Storage<FluidVariant> from, Storage<FluidVariant> to, PlayerEntity player, boolean fill, Item handItem) {
+		for (StorageView<FluidVariant> view : from) {
+			if (view.isResourceBlank()) continue;
+			FluidVariant resource = view.getResource();
+			long maxExtracted;
+
+			// check how much can be extracted
+			try (Transaction extractionTestTransaction = Transaction.openOuter()) {
+				maxExtracted = view.extract(resource, Long.MAX_VALUE, extractionTestTransaction);
+				extractionTestTransaction.abort();
+			}
+
+			try (Transaction transferTransaction = Transaction.openOuter()) {
+				// check how much can be inserted
+				long accepted = to.insert(resource, maxExtracted, transferTransaction);
+
+				// extract it, or rollback if the amounts don't match
+				if (accepted > 0 && view.extract(resource, accepted, transferTransaction) == accepted) {
+					transferTransaction.commit();
+
+					SoundEvent sound = fill ? FluidVariantAttributes.getFillSound(resource) : FluidVariantAttributes.getEmptySound(resource);
+
+					// Temporary workaround to use the correct sound for water bottles.
+					// TODO: Look into providing a proper item-aware fluid sound API.
+					if (resource.isOf(Fluids.WATER)) {
+						if (fill && handItem == Items.GLASS_BOTTLE) sound = SoundEvents.ITEM_BOTTLE_FILL;
+						if (!fill && handItem == Items.POTION) sound = SoundEvents.ITEM_BOTTLE_EMPTY;
+					}
+
+					player.playSound(sound, SoundCategory.BLOCKS, 1, 1);
+
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	private FluidStorageUtil() {
+	}
+}
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidVariantAttributes.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidVariantAttributes.java
index f6bc8184f..49d49de23 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidVariantAttributes.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidVariantAttributes.java
@@ -21,12 +21,15 @@ import java.util.Optional;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.Nullable;
 
+import net.minecraft.block.Blocks;
 import net.minecraft.fluid.FlowableFluid;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.fluid.Fluids;
 import net.minecraft.sound.SoundEvent;
 import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.Style;
 import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
 import net.minecraft.world.World;
 
 import net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap;
@@ -42,6 +45,7 @@ import net.fabricmc.fabric.impl.transfer.TransferApiImpl;
 public final class FluidVariantAttributes {
 	private static final ApiProviderMap<Fluid, FluidVariantAttributeHandler> HANDLERS = ApiProviderMap.create();
 	private static final FluidVariantAttributeHandler DEFAULT_HANDLER = new FluidVariantAttributeHandler() { };
+	private static volatile boolean coloredVanillaFluidNames = false;
 
 	private FluidVariantAttributes() {
 	}
@@ -55,6 +59,13 @@ public final class FluidVariantAttributes {
 		}
 	}
 
+	/**
+	 * Enable blue- and red-colored names for water and lava respectively.
+	 */
+	public static void enableColoredVanillaFluidNames() {
+		coloredVanillaFluidNames = true;
+	}
+
 	/**
 	 * Return the attribute handler for the passed fluid, if available, and {@code null} otherwise.
 	 */
@@ -157,12 +168,30 @@ public final class FluidVariantAttributes {
 
 	static {
 		register(Fluids.WATER, new FluidVariantAttributeHandler() {
+			@Override
+			public Text getName(FluidVariant fluidVariant) {
+				if (coloredVanillaFluidNames) {
+					return Blocks.WATER.getName().setStyle(Style.EMPTY.withColor(Formatting.BLUE));
+				} else {
+					return FluidVariantAttributeHandler.super.getName(fluidVariant);
+				}
+			}
+
 			@Override
 			public Optional<SoundEvent> getEmptySound(FluidVariant variant) {
 				return Optional.of(SoundEvents.ITEM_BUCKET_EMPTY);
 			}
 		});
 		register(Fluids.LAVA, new FluidVariantAttributeHandler() {
+			@Override
+			public Text getName(FluidVariant fluidVariant) {
+				if (coloredVanillaFluidNames) {
+					return Blocks.LAVA.getName().setStyle(Style.EMPTY.withColor(Formatting.RED));
+				} else {
+					return FluidVariantAttributeHandler.super.getName(fluidVariant);
+				}
+			}
+
 			@Override
 			public Optional<SoundEvent> getFillSound(FluidVariant variant) {
 				return Optional.of(SoundEvents.ITEM_BUCKET_FILL_LAVA);
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java
new file mode 100644
index 000000000..e30742af5
--- /dev/null
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.transfer.v1.fluid.base;
+
+import java.util.Objects;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.nbt.NbtCompound;
+
+import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
+import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
+import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage;
+
+/**
+ * A storage that can store a single fluid variant at any given time.
+ * Implementors should at least override {@link #getCapacity(FluidVariant)},
+ * and probably {@link #onFinalCommit} as well for {@code markDirty()} and similar calls.
+ *
+ * <p>This is a convenient specialization of {@link SingleVariantStorage} for fluids that additionally offers methods
+ * to read the contents of the storage from NBT.
+ *
+ * <p><b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
+ * The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
+ */
+@ApiStatus.Experimental
+public abstract class SingleFluidStorage extends SingleVariantStorage<FluidVariant> {
+	/**
+	 * Create a fluid storage with a fixed capacity and a change handler.
+	 *
+	 * @param capacity Fixed capacity of the fluid storage. Must be nonnegative.
+	 * @param onChange Change handler, generally for {@code markDirty()} or similar calls. May not be null.
+	 */
+	public static SingleFluidStorage withFixedCapacity(long capacity, Runnable onChange) {
+		StoragePreconditions.notNegative(capacity);
+		Objects.requireNonNull(onChange, "onChange may not be null");
+
+		return new SingleFluidStorage() {
+			@Override
+			protected long getCapacity(FluidVariant variant) {
+				return capacity;
+			}
+
+			@Override
+			protected void onFinalCommit() {
+				onChange.run();
+			}
+		};
+	}
+
+	@Override
+	protected final FluidVariant getBlankVariant() {
+		return FluidVariant.blank();
+	}
+
+	/**
+	 * Simple implementation of reading from NBT, to match what is written by {@link #writeNbt}.
+	 * Other formats are allowed, this is just a suggestion.
+	 */
+	public void readNbt(NbtCompound nbt) {
+		variant = FluidVariant.fromNbt(nbt.getCompound("variant"));
+		amount = nbt.getLong("amount");
+	}
+}
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleItemStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleItemStorage.java
new file mode 100644
index 000000000..c1141420f
--- /dev/null
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleItemStorage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.transfer.v1.item.base;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.nbt.NbtCompound;
+
+import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
+import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage;
+
+/**
+ * A storage that can store a single item variant at any given time.
+ * Implementors should at least override {@link #getCapacity(ItemVariant)},
+ * and probably {@link #onFinalCommit} as well for {@code markDirty()} and similar calls.
+ *
+ * <p>This is a convenient specialization of {@link SingleVariantStorage} for items that additionally offers methods
+ * to read the contents of the storage from NBT.
+ *
+ * <p><b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
+ * The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
+ */
+@ApiStatus.Experimental
+public abstract class SingleItemStorage extends SingleVariantStorage<ItemVariant> {
+	@Override
+	protected final ItemVariant getBlankVariant() {
+		return ItemVariant.blank();
+	}
+
+	/**
+	 * Simple implementation of reading from NBT, to match what is written by {@link #writeNbt}.
+	 * Other formats are allowed, this is just a suggestion.
+	 */
+	public void readNbt(NbtCompound nbt) {
+		variant = ItemVariant.fromNbt(nbt.getCompound("variant"));
+		amount = nbt.getLong("amount");
+	}
+}
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java
index 2aa70bfaa..d0c3ee704 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java
@@ -159,18 +159,23 @@ public interface Storage<T> extends Iterable<StorageView<T>> {
 	 * If returning the requested view would require iteration through a potentially large number of views,
 	 * {@code null} should be returned instead.
 	 *
-	 * <p>The returned view is tied to the passed transaction,
-	 * and may never be used once the passed transaction has been closed.
-	 *
-	 * @param transaction The transaction to which the scope of the returned storage view is tied.
 	 * @param resource The resource for which a storage view is requested. May be blank, for example to estimate capacity.
 	 * @return A view over this storage for the passed resource, or {@code null} if none is quickly available.
 	 */
 	@Nullable
-	default StorageView<T> exactView(TransactionContext transaction, T resource) {
+	default StorageView<T> exactView(T resource) {
 		return null;
 	}
 
+	/**
+	 * @deprecated Use and implement the overload without the transaction parameter.
+	 */
+	@Deprecated(forRemoval = true)
+	@Nullable
+	default StorageView<T> exactView(TransactionContext transaction, T resource) {
+		return exactView(resource);
+	}
+
 	/**
 	 * Return an integer representing the current version of this storage instance to allow for fast change detection:
 	 * if the version hasn't changed since the last time, <b>and the storage instance is the same</b>, the storage has the same contents.
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageView.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageView.java
index 11518d95c..56a5db98f 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageView.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageView.java
@@ -23,8 +23,6 @@ import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
 /**
  * A view of a single stored resource in a {@link Storage}, for use with {@link Storage#iterator} or {@link Storage#exactView}.
  *
- * <p>A view is always tied to a specific transaction, and should not be accessed outside of it.
- *
  * @param <T> The type of the stored resource.
  *
  * <b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/TransferVariant.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/TransferVariant.java
index 67b0a03ce..9cd879522 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/TransferVariant.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/TransferVariant.java
@@ -92,6 +92,14 @@ public interface TransferVariant<O> {
 		return nbt == null ? null : nbt.copy();
 	}
 
+	/**
+	 * Return a copy of the tag of this variant, or a new compound if this variant doesn't have a tag.
+	 */
+	default NbtCompound copyOrCreateNbt() {
+		NbtCompound nbt = getNbt();
+		return nbt == null ? new NbtCompound() : nbt.copy();
+	}
+
 	/**
 	 * Save this variant into an NBT compound tag. Subinterfaces should have a matching static {@code fromNbt}.
 	 *
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/FilteringStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/FilteringStorage.java
index 05bd7c4df..506388047 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/FilteringStorage.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/FilteringStorage.java
@@ -165,6 +165,19 @@ public abstract class FilteringStorage<T> implements Storage<T> {
 		return Iterators.transform(backingStorage.get().iterator(), FilteringStorageView::new);
 	}
 
+	@Override
+	@Nullable
+	public StorageView<T> exactView(T resource) {
+		StorageView<T> exact = backingStorage.get().exactView(resource);
+
+		if (exact != null) {
+			return new FilteringStorageView(exact);
+		} else {
+			return null;
+		}
+	}
+
+	@Deprecated(forRemoval = true)
 	@Override
 	@Nullable
 	public StorageView<T> exactView(TransactionContext transaction, T resource) {
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/InsertionOnlyStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/InsertionOnlyStorage.java
index 2fa5f9ad2..79b29d729 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/InsertionOnlyStorage.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/InsertionOnlyStorage.java
@@ -16,13 +16,17 @@
 
 package net.fabricmc.fabric.api.transfer.v1.storage.base;
 
+import java.util.Collections;
+import java.util.Iterator;
+
 import org.jetbrains.annotations.ApiStatus;
 
 import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
+import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
 import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
 
 /**
- * A {@link Storage} that supports insertion, and not extraction.
+ * A {@link Storage} that supports insertion, and not extraction. By default, it doesn't have any storage view either.
  *
  * <p><b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
  * The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
@@ -38,4 +42,9 @@ public interface InsertionOnlyStorage<T> extends Storage<T> {
 	default long extract(T resource, long maxAmount, TransactionContext transaction) {
 		return 0;
 	}
+
+	@Override
+	default Iterator<StorageView<T>> iterator() {
+		return Collections.emptyIterator();
+	}
 }
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java
index 6dc36355b..759de3086 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java
@@ -18,6 +18,8 @@ package net.fabricmc.fabric.api.transfer.v1.storage.base;
 
 import org.jetbrains.annotations.ApiStatus;
 
+import net.minecraft.nbt.NbtCompound;
+
 import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
 import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
 import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
@@ -34,6 +36,9 @@ import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
  *
  * <p><b>Experimental feature</b>, we reserve the right to remove or change it without further notice.
  * The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
+ *
+ * @see net.fabricmc.fabric.api.transfer.v1.fluid.base.SingleFluidStorage SingleFluidStorage for fluid variants.
+ * @see net.fabricmc.fabric.api.transfer.v1.item.base.SingleItemStorage SingleItemStorage for item variants.
  */
 @ApiStatus.Experimental
 public abstract class SingleVariantStorage<T extends TransferVariant<?>> extends SnapshotParticipant<ResourceAmount<T>> implements SingleSlotStorage<T> {
@@ -65,6 +70,15 @@ public abstract class SingleVariantStorage<T extends TransferVariant<?>> extends
 		return true;
 	}
 
+	/**
+	 * Simple implementation of writing to NBT. Other formats are allowed, this is just a convenient suggestion.
+	 */
+	// Reading from NBT is not provided because it would need to call the static FluidVariant/ItemVariant.fromNbt
+	public void writeNbt(NbtCompound nbt) {
+		nbt.put("variant", variant.toNbt());
+		nbt.putLong("amount", amount);
+	}
+
 	@Override
 	public long insert(T insertedVariant, long maxAmount, TransactionContext transaction) {
 		StoragePreconditions.notBlankNotNegative(insertedVariant, maxAmount);
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java
index a0b10da6a..19b5b1fd4 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java
@@ -18,8 +18,6 @@ package net.fabricmc.fabric.impl.transfer.item;
 
 import static net.minecraft.util.math.Direction.UP;
 
-import java.util.Collections;
-import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 
@@ -39,7 +37,6 @@ import net.minecraft.world.WorldEvents;
 import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
 import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
 import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
-import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
 import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage;
 import net.fabricmc.fabric.api.transfer.v1.storage.base.InsertionOnlyStorage;
 import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
@@ -152,11 +149,6 @@ public class ComposterWrapper extends SnapshotParticipant<Float> {
 			increaseProbability = insertedIncreaseProbability;
 			return 1;
 		}
-
-		@Override
-		public Iterator<StorageView<ItemVariant>> iterator() {
-			return Collections.emptyIterator();
-		}
 	}
 
 	private class BottomStorage implements ExtractionOnlyStorage<ItemVariant>, SingleSlotStorage<ItemVariant> {
diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlock.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlock.java
index ed3f186c8..cfde70216 100644
--- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlock.java
+++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlock.java
@@ -26,12 +26,20 @@ import net.minecraft.block.ShapeContext;
 import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.block.entity.BlockEntityTicker;
 import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.util.shape.VoxelShapes;
 import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
 
+import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorageUtil;
+import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariantAttributes;
+
 public class FluidChuteBlock extends Block implements BlockEntityProvider {
 	public FluidChuteBlock() {
 		super(Settings.of(Material.METAL));
@@ -55,4 +63,20 @@ public class FluidChuteBlock extends Block implements BlockEntityProvider {
 	public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
 		return SHAPE;
 	}
+
+	@Override
+	public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
+		if (!world.isClient() && world.getBlockEntity(pos) instanceof FluidChuteBlockEntity chute) {
+			if (!FluidStorageUtil.interactWithFluidStorage(chute.storage, player, hand)) {
+				player.sendMessage(
+						Text.literal("Fluid: ")
+								.append(FluidVariantAttributes.getName(chute.storage.variant))
+								.append(", amount: " + chute.storage.amount),
+						false
+				);
+			}
+		}
+
+		return ActionResult.success(world.isClient());
+	}
 }
diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlockEntity.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlockEntity.java
index 2cc6411b4..d88825fdd 100644
--- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlockEntity.java
+++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/FluidChuteBlockEntity.java
@@ -18,16 +18,18 @@ package net.fabricmc.fabric.test.transfer.ingame;
 
 import net.minecraft.block.BlockState;
 import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.nbt.NbtCompound;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Direction;
 
 import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants;
 import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage;
-import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
+import net.fabricmc.fabric.api.transfer.v1.fluid.base.SingleFluidStorage;
 import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
-import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
 
 public class FluidChuteBlockEntity extends BlockEntity {
+	final SingleFluidStorage storage = SingleFluidStorage.withFixedCapacity(FluidConstants.BUCKET * 4, this::markDirty);
+
 	private int tickCounter = 0;
 
 	public FluidChuteBlockEntity(BlockPos pos, BlockState state) {
@@ -37,12 +39,32 @@ public class FluidChuteBlockEntity extends BlockEntity {
 	@SuppressWarnings("ConstantConditions")
 	public void tick() {
 		if (!world.isClient() && tickCounter++ % 20 == 0) {
-			Storage<FluidVariant> top = FluidStorage.SIDED.find(world, pos.offset(Direction.UP), Direction.DOWN);
-			Storage<FluidVariant> bottom = FluidStorage.SIDED.find(world, pos.offset(Direction.DOWN), Direction.UP);
-
-			if (top != null && bottom != null) {
-				StorageUtil.move(top, bottom, fluid -> true, FluidConstants.BUCKET, null);
-			}
+			StorageUtil.move(
+					FluidStorage.SIDED.find(world, pos.offset(Direction.UP), Direction.DOWN),
+					storage,
+					fluid -> true,
+					FluidConstants.BUCKET,
+					null
+			);
+			StorageUtil.move(
+					storage,
+					FluidStorage.SIDED.find(world, pos.offset(Direction.DOWN), Direction.UP),
+					fluid -> true,
+					FluidConstants.BUCKET,
+					null
+			);
 		}
 	}
+
+	@Override
+	protected void writeNbt(NbtCompound nbt) {
+		super.writeNbt(nbt);
+		storage.writeNbt(nbt);
+	}
+
+	@Override
+	public void readNbt(NbtCompound nbt) {
+		super.readNbt(nbt);
+		storage.readNbt(nbt);
+	}
 }
diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/TrashingStorage.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/TrashingStorage.java
index 69dbe2a49..86f5c79ca 100644
--- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/TrashingStorage.java
+++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/TrashingStorage.java
@@ -16,12 +16,8 @@
 
 package net.fabricmc.fabric.test.transfer.ingame;
 
-import java.util.Collections;
-import java.util.Iterator;
-
 import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
 import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
-import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
 import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
 import net.fabricmc.fabric.api.transfer.v1.storage.base.InsertionOnlyStorage;
 import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
@@ -36,9 +32,4 @@ public class TrashingStorage<T extends TransferVariant<?>> implements InsertionO
 		// Insertion always succeeds.
 		return maxAmount;
 	}
-
-	@Override
-	public Iterator<StorageView<T>> iterator() {
-		return Collections.emptyIterator();
-	}
 }
diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/client/FluidVariantRenderTest.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/client/FluidVariantRenderTest.java
index 2a3df3bb4..687e9c871 100644
--- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/client/FluidVariantRenderTest.java
+++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/ingame/client/FluidVariantRenderTest.java
@@ -40,6 +40,7 @@ import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
 import net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRendering;
 import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
+import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariantAttributes;
 
 /**
  * Renders the water sprite in the top left of the screen, to make sure that it correctly depends on the position.
@@ -47,6 +48,8 @@ import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
 public class FluidVariantRenderTest implements ClientModInitializer {
 	@Override
 	public void onInitializeClient() {
+		FluidVariantAttributes.enableColoredVanillaFluidNames();
+
 		HudRenderCallback.EVENT.register((matrices, tickDelta) -> {
 			PlayerEntity player = MinecraftClient.getInstance().player;
 			if (player == null) return;