From 39440378269d466afac784e57fd418458eca9518 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Fri, 6 Mar 2026 09:27:47 +0530 Subject: [PATCH 1/4] feat: add support for frozen and inline interface unions in Kotlin data classes --- .../templates/data_class.mustache | 82 +++++++++++++- .../modelDiscriminatedUnion.mustache | 103 +++++++++++++++++- .../templates/modelGeneric.mustache | 7 ++ .../walletkit/walletkit-android-bridge.mjs | 2 +- 4 files changed, 187 insertions(+), 7 deletions(-) diff --git a/Scripts/generate-api/templates/data_class.mustache b/Scripts/generate-api/templates/data_class.mustache index db36f7de..1610b3c2 100644 --- a/Scripts/generate-api/templates/data_class.mustache +++ b/Scripts/generate-api/templates/data_class.mustache @@ -16,6 +16,21 @@ import kotlinx.serialization.builtins.serializer import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder {{/enumUnknownDefaultCase}} +{{#vendorExtensions.x-inline-interface-unions}} +{{#-first}} +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializer +{{/-first}} +{{/vendorExtensions.x-inline-interface-unions}} {{/kotlinx_serialization}} {{#parcelizeModels}} import android.os.Parcelable @@ -40,12 +55,77 @@ import kotlinx.parcelize.Parcelize {{#nonPublicApi}}internal {{/nonPublicApi}}{{#hasVars}}data {{/hasVars}}class {{classname}} ( {{#allVars}} -{{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{^-last}},{{/-last}} +{{#vendorExtensions.x-frozen}} + @SerialName("{{{baseName}}}") + private val {{{name}}}: {{{dataType}}}{{^required}}? = null{{/required}}{{/vendorExtensions.x-frozen}} +{{^vendorExtensions.x-frozen}} +{{#vendorExtensions.x-interface-union}} + @SerialName("{{{baseName}}}") + val {{{name}}}: {{#lambda.titlecase}}{{{name}}}{{/lambda.titlecase}}{{^required}}? = null{{/required}}{{/vendorExtensions.x-interface-union}} +{{^vendorExtensions.x-interface-union}} +{{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{/vendorExtensions.x-interface-union}} +{{/vendorExtensions.x-frozen}} +{{^-last}},{{/-last}} {{/allVars}} +{{#vendorExtensions.x-constant-fields}} +{{#-first}}{{#hasVars}},{{/hasVars}}{{/-first}} + @SerialName("{{{name}}}") + val {{{name}}}: kotlin.String = "{{{value}}}"{{^-last}},{{/-last}} +{{/vendorExtensions.x-constant-fields}} ){{#parent}} : {{{parent}}}(){{/parent}} { companion object +{{#vendorExtensions.x-inline-interface-unions}} + + @Serializable(with = {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}.Serializer::class) + sealed class {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}} { +{{#cases}} + + @Serializable + data class {{#lambda.titlecase}}{{{caseName}}}{{/lambda.titlecase}}( + val value: {{modelNamePrefix}}{{{typeName}}} + ) : {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}() +{{/cases}} + + internal object Serializer : KSerializer<{{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}> { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}.{{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}") + + @Suppress("UNCHECKED_CAST") + override fun serialize(encoder: Encoder, value: {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}) { + val jsonEncoder = encoder as? JsonEncoder + ?: throw SerializationException("{{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}} can only be serialized with JSON") + + val jsonElement = when (value) { +{{#cases}} + is {{#lambda.titlecase}}{{{caseName}}}{{/lambda.titlecase}} -> + jsonEncoder.json.encodeToJsonElement(serializer<{{modelNamePrefix}}{{{typeName}}}>(), value.value) +{{/cases}} + } + jsonEncoder.encodeJsonElement(jsonElement) + } + + override fun deserialize(decoder: Decoder): {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}} { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("{{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}} can only be deserialized from JSON") + + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val discriminatorValue = jsonObject["{{{discriminatorField}}}"]?.jsonPrimitive?.content + ?: throw SerializationException("Missing '{{{discriminatorField}}}' discriminator for {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}") + + return when (discriminatorValue) { +{{#cases}} + "{{{rawValue}}}" -> + {{#lambda.titlecase}}{{{caseName}}}{{/lambda.titlecase}}( + jsonDecoder.json.decodeFromJsonElement(serializer<{{modelNamePrefix}}{{{typeName}}}>(), jsonObject) + ) +{{/cases}} + else -> throw SerializationException("Unknown discriminator '$discriminatorValue' for {{#lambda.titlecase}}{{{propertyName}}}{{/lambda.titlecase}}") + } + } + } + } +{{/vendorExtensions.x-inline-interface-unions}} {{#hasEnums}} {{#vars}} {{#isEnum}} diff --git a/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache b/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache index 36dd25d6..24b938f5 100644 --- a/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache +++ b/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache @@ -1,13 +1,105 @@ {{!-- Template for discriminated union types (sealed classes in Kotlin). - - Uses vendor extensions from OpenAPI spec: + + Two variants: + 1. Inline object unions (x-interface-union is false/absent): + e.g., { type: 'null' } | { type: 'num'; value: string } + Uses type/value JSON wrapper with per-case value decoding. + + 2. Interface unions (x-interface-union is true): + e.g., /** @discriminator name */ export type Test = TestWithMessage | TestWithNumber; + Uses discriminator field from full JSON object, decodes full member type. + + Vendor extensions used: - x-discriminated-union: boolean indicating this is a discriminated union + - x-interface-union: boolean indicating this uses interface-based members + - x-discriminator-field: string name of the JSON discriminator property - x-enum-cases: array of {name, rawValue, hasAssociatedValue, valuePropertyName} - - Properties with x-enum-case-name extension become sealed class subclasses. - The discriminator is always "type" field. + - x-enum-case-name: camelCase Kotlin case name (on vars) + - x-enum-case-raw-value: original JSON value for encoding/decoding (on vars) --}} +{{#vendorExtensions.x-interface-union}} +{{!-- ====== Interface union variant ====== --}} +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializer +import io.ton.walletkit.model.TONBase64 +import io.ton.walletkit.model.TONUserFriendlyAddress + +/** + * {{{description}}} + * + * This is a discriminated union type. Use the appropriate subclass based on the `{{{vendorExtensions.x-discriminator-field}}}` field. + */ +@Serializable(with = {{classname}}.Serializer::class) +sealed class {{classname}} { + +{{#vars}} +{{#vendorExtensions.x-enum-case-name}} + /** + * {{description}} + */ + @Serializable + data class {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( + val value: {{{dataType}}} + ) : {{classname}}() + +{{/vendorExtensions.x-enum-case-name}} +{{/vars}} + internal object Serializer : KSerializer<{{classname}}> { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}") + + @Suppress("UNCHECKED_CAST") + override fun serialize(encoder: Encoder, value: {{classname}}) { + val jsonEncoder = encoder as? JsonEncoder + ?: throw SerializationException("{{classname}} can only be serialized with JSON") + + val jsonElement = when (value) { +{{#vars}} +{{#vendorExtensions.x-enum-case-name}} + is {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} -> + jsonEncoder.json.encodeToJsonElement(serializer<{{{dataType}}}>(), value.value) +{{/vendorExtensions.x-enum-case-name}} +{{/vars}} + } + jsonEncoder.encodeJsonElement(jsonElement) + } + + override fun deserialize(decoder: Decoder): {{classname}} { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("{{classname}} can only be deserialized from JSON") + + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val discriminatorValue = jsonObject["{{{vendorExtensions.x-discriminator-field}}}"]?.jsonPrimitive?.content + ?: throw SerializationException("Missing '{{{vendorExtensions.x-discriminator-field}}}' discriminator for {{classname}}") + + return when (discriminatorValue) { +{{#vars}} +{{#vendorExtensions.x-enum-case-name}} + "{{vendorExtensions.x-enum-case-raw-value}}" -> + {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( + jsonDecoder.json.decodeFromJsonElement(serializer<{{{dataType}}}>(), jsonObject) + ) +{{/vendorExtensions.x-enum-case-name}} +{{/vars}} + else -> throw SerializationException("Unknown discriminator '$discriminatorValue' for {{classname}}") + } + } + } +} +{{/vendorExtensions.x-interface-union}} +{{^vendorExtensions.x-interface-union}} +{{!-- ====== Type/value wrapper variant (original) ====== --}} import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName @@ -128,3 +220,4 @@ sealed class {{classname}} { } } } +{{/vendorExtensions.x-interface-union}} diff --git a/Scripts/generate-api/templates/modelGeneric.mustache b/Scripts/generate-api/templates/modelGeneric.mustache index 7db55ae9..6c12476c 100644 --- a/Scripts/generate-api/templates/modelGeneric.mustache +++ b/Scripts/generate-api/templates/modelGeneric.mustache @@ -4,6 +4,7 @@ Vendor extensions used: - x-generic-params: array of {name} for type parameters - x-generic-type-ref: string indicating a property uses a generic type parameter + - x-frozen: boolean indicating the field should use JsonElement type with private val access Generates Kotlin data classes with generic type parameters and kotlinx.serialization support. --}} @@ -23,6 +24,11 @@ import io.ton.walletkit.model.TONUserFriendlyAddress @Serializable {{#nonPublicApi}}internal {{/nonPublicApi}}data class {{{classname}}}<{{#vendorExtensions.x-generic-params}}{{{name}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-generic-params}}>( {{#vars}} +{{#vendorExtensions.x-frozen}} + @SerialName("{{{baseName}}}") + private val {{{name}}}: {{{dataType}}}{{^required}}? = null{{/required}}{{^-last}},{{/-last}} +{{/vendorExtensions.x-frozen}} +{{^vendorExtensions.x-frozen}} {{#vendorExtensions.x-generic-type-ref}} @SerialName("{{{baseName}}}") val {{{name}}}: {{{vendorExtensions.x-generic-type-ref}}}{{^required}}? = null{{/required}}{{^-last}},{{/-last}} @@ -31,6 +37,7 @@ import io.ton.walletkit.model.TONUserFriendlyAddress @SerialName("{{{baseName}}}") val {{{name}}}: {{{dataType}}}{{^required}}? = null{{/required}}{{^-last}},{{/-last}} {{/vendorExtensions.x-generic-type-ref}} +{{/vendorExtensions.x-frozen}} {{/vars}} ) { companion object diff --git a/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs b/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs index e4be5335..b8e3e5f3 100644 --- a/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs +++ b/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs @@ -35233,7 +35233,7 @@ class ApiClientTonApi extends BaseApiClient { return mapJettonMasters(raw); } async jettonsByOwnerAddress(request) { - const raw = await this.getJson(`/v2/accounts/${request.ownerAddress}/jettons`); + const raw = await this.getJson(`/v2/accounts/${request.ownerAddress}/jettons?currencies=usd`); return mapUserJettons(raw); } async nftItemsByAddress(request) { From b351471fbc260840d479f08018b838c84708dd6a Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Thu, 12 Mar 2026 13:55:57 +0530 Subject: [PATCH 2/4] feat: support typealias, skip-model, and empty/single-field union variants in codegen templates --- Scripts/generate-api/generate-api-models.sh | 11 +++ .../templates/data_class.mustache | 15 ++++ .../modelDiscriminatedUnion.mustache | 79 ++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/Scripts/generate-api/generate-api-models.sh b/Scripts/generate-api/generate-api-models.sh index d2cb3f70..311a6db0 100755 --- a/Scripts/generate-api/generate-api-models.sh +++ b/Scripts/generate-api/generate-api-models.sh @@ -175,6 +175,17 @@ rm -rf "$DEST_DIR" mkdir -p "$DEST_DIR" cp -R "$MODELS_DIR/"* "$DEST_DIR/" +# Remove empty generated files (from x-skip-model suppression) +echo "🧹 Removing empty generated files..." +find "$DEST_DIR" -name '*.kt' -type f -empty -delete +find "$DEST_DIR" -name '*.kt' -type f | while read -r file; do + # Check if file contains only whitespace/blank lines/comments/package/suppress but no actual code + if ! grep -qE '^\s*(class |data class |sealed class |object |interface |typealias |enum class |fun |val |var |abstract )' "$file"; then + echo " Removing boilerplate-only file: $(basename "$file")" + rm "$file" + fi +done + # Clean up generated directory echo "🧹 Cleaning up generated directory..." rm -rf "$OUTPUT_DIR" diff --git a/Scripts/generate-api/templates/data_class.mustache b/Scripts/generate-api/templates/data_class.mustache index 1610b3c2..c0ac6b38 100644 --- a/Scripts/generate-api/templates/data_class.mustache +++ b/Scripts/generate-api/templates/data_class.mustache @@ -1,7 +1,19 @@ +{{#vendorExtensions.x-skip-model}} +{{! Suppressed model (e.g., single-field variant inlined into parent union) }} +{{/vendorExtensions.x-skip-model}} +{{^vendorExtensions.x-skip-model}} +{{#vendorExtensions.x-type-alias}} +typealias {{{classname}}} = {{modelNamePrefix}}{{{vendorExtensions.x-alias-target}}} +{{/vendorExtensions.x-type-alias}} +{{^vendorExtensions.x-type-alias}} {{#vendorExtensions.x-discriminated-union}} {{>modelDiscriminatedUnion}} {{/vendorExtensions.x-discriminated-union}} {{^vendorExtensions.x-discriminated-union}} +{{#vendorExtensions.x-is-generic}} +{{>modelGeneric}} +{{/vendorExtensions.x-is-generic}} +{{^vendorExtensions.x-is-generic}} {{^multiplatform}} {{#kotlinx_serialization}} import kotlinx.serialization.Serializable @@ -150,4 +162,7 @@ import kotlinx.parcelize.Parcelize {{/vars}} {{/hasEnums}} } +{{/vendorExtensions.x-is-generic}} {{/vendorExtensions.x-discriminated-union}} +{{/vendorExtensions.x-type-alias}} +{{/vendorExtensions.x-skip-model}} diff --git a/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache b/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache index 24b938f5..2e6d2072 100644 --- a/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache +++ b/Scripts/generate-api/templates/modelDiscriminatedUnion.mustache @@ -32,6 +32,8 @@ import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.serializer import io.ton.walletkit.model.TONBase64 import io.ton.walletkit.model.TONUserFriendlyAddress @@ -44,8 +46,30 @@ import io.ton.walletkit.model.TONUserFriendlyAddress @Serializable(with = {{classname}}.Serializer::class) sealed class {{classname}} { + companion object { + internal const val DISCRIMINATOR_FIELD = "{{{vendorExtensions.x-discriminator-field}}}" + } + {{#vars}} {{#vendorExtensions.x-enum-case-name}} +{{#vendorExtensions.x-empty-variant}} + /** + * {{description}} + */ + object {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} : {{classname}}() + +{{/vendorExtensions.x-empty-variant}} +{{#vendorExtensions.x-single-field-variant}} + /** + * {{description}} + */ + @Serializable + data class {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( + val {{{vendorExtensions.x-single-field-name}}}: {{{dataType}}}{{#vendorExtensions.x-single-field-optional}}? = null{{/vendorExtensions.x-single-field-optional}} + ) : {{classname}}() + +{{/vendorExtensions.x-single-field-variant}} +{{^vendorExtensions.x-empty-variant}}{{^vendorExtensions.x-single-field-variant}} /** * {{description}} */ @@ -54,6 +78,7 @@ sealed class {{classname}} { val value: {{{dataType}}} ) : {{classname}}() +{{/vendorExtensions.x-single-field-variant}}{{/vendorExtensions.x-empty-variant}} {{/vendorExtensions.x-enum-case-name}} {{/vars}} internal object Serializer : KSerializer<{{classname}}> { @@ -67,8 +92,32 @@ sealed class {{classname}} { val jsonElement = when (value) { {{#vars}} {{#vendorExtensions.x-enum-case-name}} +{{#vendorExtensions.x-empty-variant}} + is {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} -> + buildJsonObject { + put(DISCRIMINATOR_FIELD, JsonPrimitive("{{vendorExtensions.x-enum-case-raw-value}}")) + } +{{/vendorExtensions.x-empty-variant}} +{{#vendorExtensions.x-single-field-variant}} +{{^vendorExtensions.x-single-field-optional}} + is {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} -> + buildJsonObject { + put(DISCRIMINATOR_FIELD, JsonPrimitive("{{vendorExtensions.x-enum-case-raw-value}}")) + put("{{{vendorExtensions.x-single-field-name}}}", jsonEncoder.json.encodeToJsonElement(serializer<{{{dataType}}}>(), value.{{{vendorExtensions.x-single-field-name}}})) + } +{{/vendorExtensions.x-single-field-optional}} +{{#vendorExtensions.x-single-field-optional}} + is {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} -> + buildJsonObject { + put(DISCRIMINATOR_FIELD, JsonPrimitive("{{vendorExtensions.x-enum-case-raw-value}}")) + value.{{{vendorExtensions.x-single-field-name}}}?.let { put("{{{vendorExtensions.x-single-field-name}}}", jsonEncoder.json.encodeToJsonElement(serializer<{{{dataType}}}>(), it)) } + } +{{/vendorExtensions.x-single-field-optional}} +{{/vendorExtensions.x-single-field-variant}} +{{^vendorExtensions.x-empty-variant}}{{^vendorExtensions.x-single-field-variant}} is {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} -> jsonEncoder.json.encodeToJsonElement(serializer<{{{dataType}}}>(), value.value) +{{/vendorExtensions.x-single-field-variant}}{{/vendorExtensions.x-empty-variant}} {{/vendorExtensions.x-enum-case-name}} {{/vars}} } @@ -80,16 +129,42 @@ sealed class {{classname}} { ?: throw SerializationException("{{classname}} can only be deserialized from JSON") val jsonObject = jsonDecoder.decodeJsonElement().jsonObject - val discriminatorValue = jsonObject["{{{vendorExtensions.x-discriminator-field}}}"]?.jsonPrimitive?.content - ?: throw SerializationException("Missing '{{{vendorExtensions.x-discriminator-field}}}' discriminator for {{classname}}") + val discriminatorValue = jsonObject[DISCRIMINATOR_FIELD]?.jsonPrimitive?.content + ?: throw SerializationException("Missing '$DISCRIMINATOR_FIELD' discriminator for {{classname}}") return when (discriminatorValue) { {{#vars}} {{#vendorExtensions.x-enum-case-name}} +{{#vendorExtensions.x-empty-variant}} + "{{vendorExtensions.x-enum-case-raw-value}}" -> + {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}} +{{/vendorExtensions.x-empty-variant}} +{{#vendorExtensions.x-single-field-variant}} +{{^vendorExtensions.x-single-field-optional}} + "{{vendorExtensions.x-enum-case-raw-value}}" -> + {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( + {{{vendorExtensions.x-single-field-name}}} = jsonDecoder.json.decodeFromJsonElement(serializer<{{{dataType}}}>(), jsonObject["{{{vendorExtensions.x-single-field-name}}}"] ?: throw SerializationException("Missing '{{{vendorExtensions.x-single-field-name}}}' for {{classname}}")) + ) +{{/vendorExtensions.x-single-field-optional}} +{{#vendorExtensions.x-single-field-optional}} + "{{vendorExtensions.x-enum-case-raw-value}}" -> { + val fieldElement = jsonObject["{{{vendorExtensions.x-single-field-name}}}"] + if (fieldElement != null) { + {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( + {{{vendorExtensions.x-single-field-name}}} = jsonDecoder.json.decodeFromJsonElement(serializer<{{{dataType}}}>(), fieldElement) + ) + } else { + {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}() + } + } +{{/vendorExtensions.x-single-field-optional}} +{{/vendorExtensions.x-single-field-variant}} +{{^vendorExtensions.x-empty-variant}}{{^vendorExtensions.x-single-field-variant}} "{{vendorExtensions.x-enum-case-raw-value}}" -> {{#lambda.titlecase}}{{vendorExtensions.x-enum-case-name}}{{/lambda.titlecase}}( jsonDecoder.json.decodeFromJsonElement(serializer<{{{dataType}}}>(), jsonObject) ) +{{/vendorExtensions.x-single-field-variant}}{{/vendorExtensions.x-empty-variant}} {{/vendorExtensions.x-enum-case-name}} {{/vars}} else -> throw SerializationException("Unknown discriminator '$discriminatorValue' for {{classname}}") From 2180e0a8454493115a6fdd448f2bf105a31ba9c2 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Fri, 13 Mar 2026 05:07:44 +0530 Subject: [PATCH 3/4] ci: use pixel_6 emulator profile for bigger screen in E2E tests --- .github/workflows/android-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index c1f8e650..0f5565dc 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -288,7 +288,7 @@ jobs: path: | ~/.android/avd/* ~/.android/adb* - key: avd-api-35-x86_64 + key: avd-api-35-x86_64-pixel6 - name: Create AVD and generate snapshot if: steps.avd-cache.outputs.cache-hit != 'true' @@ -297,6 +297,7 @@ jobs: api-level: 35 arch: x86_64 target: google_apis + profile: pixel_6 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true @@ -418,6 +419,7 @@ jobs: api-level: 35 arch: x86_64 target: google_apis + profile: pixel_6 force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true From cbc27819f4e177c015dd1bed625301c17db92c9f Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 16 Mar 2026 14:50:43 +0530 Subject: [PATCH 4/4] fix: use committed bridge bundle instead of rebuilding from stale branch The CI was checking out ton-connect/kit at fix/android-dto which has an older API (createSigner) that doesn't match the Android SDK's expected methods (createSignerFromMnemonic). This caused all wallet imports to fail silently with "Unknown method createSignerFromMnemonic". Co-Authored-By: Claude Opus 4.6 --- .github/workflows/android-ci.yml | 34 ++------------------------------ 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 0f5565dc..034718e2 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -69,16 +69,6 @@ jobs: - name: Checkout kit-android code uses: actions/checkout@v4 - - name: Checkout kit repository - uses: actions/checkout@v4 - with: - repository: ton-connect/kit - ref: fix/android-dto # Branch with RequestError and nullable fixes - path: ton-repo - sparse-checkout: | - packages/walletkit - packages/walletkit-android-bridge - - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -89,35 +79,15 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Set up pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Build TypeScript Bridge Bundle - run: | - cd ton-repo - pnpm install - pnpm turbo run build --filter=walletkit-android-bridge --force - - - name: Copy Bridge Bundle to dist-android + - name: Populate dist-android from committed assets run: | mkdir -p dist-android - cp ton-repo/packages/walletkit-android-bridge/dist/* dist-android/ + cp TONWalletKit-Android/impl/src/main/assets/walletkit/* dist-android/ - name: Grant execute permission for gradlew working-directory: TONWalletKit-Android run: chmod +x gradlew - - name: Sync WebView Assets - working-directory: TONWalletKit-Android - run: ./gradlew syncWalletKitWebViewAssets - - name: Run unit tests working-directory: TONWalletKit-Android run: ./gradlew :impl:testWebviewDebugUnitTest