From a9e144bb0c68987e677228389b7c6ad59139fffe Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 4 Jul 2026 11:44:03 +0200 Subject: [PATCH] fix regresssion of json wallet serialization with jdk25 --- build.gradle | 4 +- .../sparrow/io/JsonPersistence.java | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b6e172ae..9e53dcd2 100644 --- a/build.gradle +++ b/build.gradle @@ -126,7 +126,7 @@ compileJava { test { useJUnitPlatform() - jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"] + jvmArgs = ["--enable-native-access=ALL-UNNAMED"] } application { @@ -153,7 +153,6 @@ application { "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow", - "--add-opens=java.base/java.io=com.google.gson", "--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow", "--add-reads=kotlin.stdlib=kotlinx.coroutines.core", "--add-reads=org.flywaydb.core=java.desktop"] @@ -231,7 +230,6 @@ jlink { "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow", - "--add-opens=java.base/java.io=com.google.gson", "--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow", "--add-reads=com.sparrowwallet.merged.module=java.desktop", "--add-reads=com.sparrowwallet.merged.module=java.sql", diff --git a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java index 72b40051..46f12482 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java @@ -4,18 +4,30 @@ import com.google.gson.*; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.FileType; import com.sparrowwallet.drongo.IOUtils; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.InvalidAddressException; +import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver; import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver; import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.crypto.EncryptedData; +import com.sparrowwallet.drongo.crypto.EncryptionType; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.drongo.policy.Miniscript; +import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey; +import com.sparrowwallet.drongo.wallet.UtxoMixData; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.drongo.wallet.WalletTable; import java.io.*; import java.lang.reflect.Type; @@ -340,6 +352,26 @@ public class JsonPersistence implements Persistence { gsonBuilder.registerTypeAdapter(Address.class, new AddressDeserializer()); gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeSerializer()); gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeDeserializer()); + gsonBuilder.registerTypeAdapter(File.class, new FileSerializer()); + gsonBuilder.registerTypeAdapter(File.class, new FileDeserializer()); + + //Instance creators are required for classes without no-args constructors, since Gson's fallback of sun.misc.Unsafe is unavailable on the module path (jdk.unsupported is not resolved) + //Unsafe is also explicitly disabled below so that classpath-based tests fail in the same way production does if a class is missed here + gsonBuilder.disableJdkUnsafe(); + gsonBuilder.registerTypeAdapter(Policy.class, (InstanceCreator) type -> new Policy(null)); + gsonBuilder.registerTypeAdapter(Miniscript.class, (InstanceCreator) type -> new Miniscript(null)); + gsonBuilder.registerTypeAdapter(KeyDerivation.class, (InstanceCreator) type -> new KeyDerivation(null, (String)null)); + gsonBuilder.registerTypeAdapter(WalletNode.class, (InstanceCreator) type -> new WalletNode("m/0")); + gsonBuilder.registerTypeAdapter(BlockTransaction.class, (InstanceCreator) type -> new BlockTransaction(null, 0, null, null, null)); + gsonBuilder.registerTypeAdapter(BlockTransactionHashIndex.class, (InstanceCreator) type -> new BlockTransactionHashIndex(null, 0, null, null, 0, 0)); + gsonBuilder.registerTypeAdapter(DeterministicSeed.class, (InstanceCreator) type -> new DeterministicSeed((EncryptedData)null, false, 0, null)); + gsonBuilder.registerTypeAdapter(MasterPrivateExtendedKey.class, (InstanceCreator) type -> new MasterPrivateExtendedKey((EncryptedData)null)); + gsonBuilder.registerTypeAdapter(EncryptedData.class, (InstanceCreator) type -> new EncryptedData(new byte[0], new byte[0], null, (EncryptionType)null)); + gsonBuilder.registerTypeAdapter(EncryptionType.class, (InstanceCreator) type -> new EncryptionType(null, null)); + gsonBuilder.registerTypeAdapter(PaymentCode.class, (InstanceCreator) type -> new PaymentCode(new byte[33], new byte[32])); + gsonBuilder.registerTypeAdapter(UtxoMixData.class, (InstanceCreator) type -> new UtxoMixData(0, null)); + gsonBuilder.registerTypeAdapter(WalletTable.class, (InstanceCreator) type -> new WalletTable(null, null, 0, null)); + if(includeWalletSerializers) { gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer()); gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer()); @@ -403,6 +435,23 @@ public class JsonPersistence implements Persistence { } } + //Maintains the same format as the previous reflective serialization of java.io.File, which requires java.base/java.io to be opened to Gson + private static class FileSerializer implements JsonSerializer { + @Override + public JsonElement serialize(File src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("path", src.getPath()); + return jsonObject; + } + } + + private static class FileDeserializer implements JsonDeserializer { + @Override + public File deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new File(json.isJsonObject() ? json.getAsJsonObject().get("path").getAsString() : json.getAsJsonPrimitive().getAsString()); + } + } + private static class DateSerializer implements JsonSerializer { @Override public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {