From d7bb8b000c548a619345ffa402baaaff19218a5f Mon Sep 17 00:00:00 2001 From: Christian Rogobete Date: Thu, 7 Aug 2025 16:47:14 +0200 Subject: [PATCH] feat: add contract binding generators for flutter, php and swift --- README.md | 142 +++- pyproject.toml | 3 + stellar_contract_bindings/cli.py | 21 + stellar_contract_bindings/flutter.py | 1011 ++++++++++++++++++++++++++ stellar_contract_bindings/php.py | 886 ++++++++++++++++++++++ stellar_contract_bindings/swift.py | 962 ++++++++++++++++++++++++ tests/test_flutter_generator.py | 356 +++++++++ tests/test_php_generator.py | 504 +++++++++++++ tests/test_swift_generator.py | 571 +++++++++++++++ web_interface/app.py | 26 +- 10 files changed, 4478 insertions(+), 4 deletions(-) create mode 100644 stellar_contract_bindings/flutter.py create mode 100644 stellar_contract_bindings/php.py create mode 100644 stellar_contract_bindings/swift.py create mode 100644 tests/test_flutter_generator.py create mode 100644 tests/test_php_generator.py create mode 100644 tests/test_swift_generator.py diff --git a/README.md b/README.md index 71f5401..93c9bd6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This tool simplifies the process of interacting with Soroban contracts by generating the necessary code to call contract methods directly from your preferred programming language. Currently, it supports -Python and Java. [stellar-cli](https://github.com/stellar/stellar-cli) provides support for TypeScript and Rust. +Python, Java, Flutter/Dart, PHP, and Swift/iOS. [stellar-cli](https://github.com/stellar/stellar-cli) provides support for TypeScript and Rust. ## Web Interface We have a web interface for generating bindings. You can access via [https://stellar-contract-bindings.fly.dev/](https://stellar-contract-bindings.fly.dev/). @@ -25,13 +25,34 @@ Please check the help message for the most up-to-date usage information: stellar-contract-bindings --help ``` -### Example +### Examples +#### Python ```shell stellar-contract-bindings python --contract-id CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF --rpc-url https://mainnet.sorobanrpc.com --output ./bindings ``` -This command will generate Python binding for the specified contract and save it in the `./bindings` directory. +#### Java +```shell +stellar-contract-bindings java --contract-id CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF --rpc-url https://mainnet.sorobanrpc.com --output ./bindings --package com.example +``` + +#### Flutter/Dart +```shell +stellar-contract-bindings flutter --contract-id CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF --rpc-url https://mainnet.sorobanrpc.com --output ./lib --class-name MyContract +``` + +#### PHP +```shell +stellar-contract-bindings php --contract-id CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF --rpc-url https://mainnet.sorobanrpc.com --output ./generated --namespace MyApp\\Contracts --class-name MyContractClient +``` + +#### Swift/iOS +```shell +stellar-contract-bindings swift --contract-id CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF --rpc-url https://mainnet.sorobanrpc.com --output ./Sources --class-name MyContract +``` + +These commands will generate language-specific bindings for the specified contract and save them in the respective directories. ### Using the Generated Binding @@ -64,6 +85,121 @@ public class Example extends ContractClient { } ``` +#### Flutter/Dart +```dart +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; +import 'lib/my_contract_client.dart'; // Import the generated bindings + +void main() async { + final sourceKeyPair = KeyPair.fromAccountId("GD5KKP3LHUDXLDCGKP55NLEOEHMS3Z4BS6IDDZFCYU3BDXUZTBWL7JNF"); + // or: final sourceKeyPair = KeyPair.fromSecretSeed("S..."); + + // Create client instance + final client = await MyContractClient.forContractId( + sourceAccountKeyPair: sourceKeyPair, + contractId: "CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF", + network: Network.PUBLIC, + rpcUrl: "https://mainnet.sorobanrpc.com", + ); + + // Call contract method directly + try { + final result = await client.hello(input: "World"); + print("Contract response: $result"); + } catch (e) { + print("Error calling contract: $e"); + } + + // Or build an assembled transaction for more control + final assembledTx = await client.buildHelloTx( + input: "World", + methodOptions: MethodOptions(), + ); +} +``` + +#### PHP +```php +hello("World"); + echo "Contract response: " . $result . "\n"; +} catch (Exception $e) { + echo "Error calling contract: " . $e->getMessage() . "\n"; +} + +// Or build an assembled transaction for more control +$methodOptions = new MethodOptions(); +$assembledTx = $client->buildHelloTx("World", $methodOptions); +``` + +#### Swift/iOS +```swift +import stellarsdk +import Foundation + +// Import the generated bindings +// Assuming the generated file is named MyContract.swift + +// Initialize +let sourceKeyPair = try! KeyPair.init(accountId: "GD5KKP3LHUDXLDCGKP55NLEOEHMS3Z4BS6IDDZFCYU3BDXUZTBWL7JNF") +// or: let sourceKeyPair = try! KeyPair.init(secretSeed: "S...") + +// Create client instance +let options = ClientOptions( + sourceAccountKeyPair: sourceKeyPair, + contractId: "CDOAW6D7NXAPOCO7TFAWZNJHK62E3IYRGNRVX3VOXNKNVOXCLLPJXQCF", + network: .public, + rpcUrl: "https://mainnet.sorobanrpc.com" +) + +Task { + do { + let client = try await MyContract.forClientOptions(options: options) + + // Call contract method directly + let result = try await client.hello( + to: "World", + methodOptions: nil, + force: false + ) + print("Contract response: \(result)") + + // Or build an assembled transaction for more control + let methodOptions = MethodOptions() + let assembledTx = try await client.buildHelloTx( + to: "World", + methodOptions: methodOptions + ) + + } catch { + print("Error calling contract: \(error)") + } +} +``` + ## License This project is licensed under the Apache-2.0 License. See the [LICENSE](LICENSE) file for details. diff --git a/pyproject.toml b/pyproject.toml index aa47e59..a756ef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,9 @@ dependencies = [ stellar-contract-bindings = "stellar_contract_bindings.cli:cli" stellar-contract-bindings-python = "stellar_contract_bindings.cli:cli_python" stellar-contract-bindings-java = "stellar_contract_bindings.cli:cli_java" +stellar-contract-bindings-flutter = "stellar_contract_bindings.cli:cli_flutter" +stellar-contract-bindings-php = "stellar_contract_bindings.cli:cli_php" +stellar-contract-bindings-swift = "stellar_contract_bindings.cli:cli_swift" [project.urls] Homepage = "https://github.com/lightsail-network/stellar-contract-bindings" diff --git a/stellar_contract_bindings/cli.py b/stellar_contract_bindings/cli.py index df2ca09..343b4f1 100644 --- a/stellar_contract_bindings/cli.py +++ b/stellar_contract_bindings/cli.py @@ -3,6 +3,9 @@ from stellar_contract_bindings import __version__ from stellar_contract_bindings.python import command as python_command from stellar_contract_bindings.java import command as java_command +from stellar_contract_bindings.flutter import command as flutter_command +from stellar_contract_bindings.php import command as php_command +from stellar_contract_bindings.swift import command as swift_command @click.group() @@ -13,6 +16,9 @@ def cli(): cli.add_command(python_command) cli.add_command(java_command) +cli.add_command(flutter_command) +cli.add_command(php_command) +cli.add_command(swift_command) # https://github.com/lightsail-network/stellar-contract-bindings/issues/14 @@ -26,5 +32,20 @@ def cli_java(): java_command() +def cli_flutter(): + """CLI for generating Stellar contract bindings (Flutter).""" + flutter_command() + + +def cli_php(): + """CLI for generating Stellar contract bindings (PHP).""" + php_command() + + +def cli_swift(): + """CLI for generating Stellar contract bindings (Swift).""" + swift_command() + + if __name__ == "__main__": cli() diff --git a/stellar_contract_bindings/flutter.py b/stellar_contract_bindings/flutter.py new file mode 100644 index 0000000..7e31f4b --- /dev/null +++ b/stellar_contract_bindings/flutter.py @@ -0,0 +1,1011 @@ +import os +from typing import List + +import click +from jinja2 import Template +from stellar_sdk import __version__ as stellar_sdk_version, StrKey +from stellar_sdk import xdr + +from stellar_contract_bindings import __version__ as stellar_contract_bindings_version +from stellar_contract_bindings.utils import get_specs_by_contract_id + + +def is_keywords(word: str) -> bool: + return word in [ + "abstract", + "as", + "assert", + "async", + "await", + "break", + "case", + "catch", + "class", + "const", + "continue", + "covariant", + "default", + "deferred", + "do", + "dynamic", + "else", + "enum", + "export", + "extends", + "extension", + "external", + "factory", + "false", + "final", + "finally", + "for", + "Function", + "get", + "hide", + "if", + "implements", + "import", + "in", + "interface", + "is", + "late", + "library", + "mixin", + "new", + "null", + "on", + "operator", + "part", + "rethrow", + "required", + "return", + "set", + "show", + "static", + "super", + "switch", + "sync", + "this", + "throw", + "true", + "try", + "typedef", + "var", + "void", + "while", + "with", + "yield", + ] + + +def is_tuple_struct(entry: xdr.SCSpecUDTStructV0) -> bool: + return all(f.name.isdigit() for f in entry.fields) + + +def prefixed_type_name(type_name: str, class_name: str) -> str: + """Prefix a type name with the class name to avoid conflicts. + + Args: + type_name: The original type name from the contract spec + class_name: The class name to use as prefix + + Returns: + The prefixed type name if it's a UDT, or the original if it's a built-in type + """ + # Don't prefix built-in types + if type_name in ['bool', 'int', 'BigInt', 'String', 'Uint8List', 'Address', 'Map', 'List', + 'XdrSCVal', 'void']: + return type_name + return f"{class_name}{type_name}" + + +def snake_to_camel(text: str, first_letter_lower: bool = True) -> str: + parts = text.split("_") + if first_letter_lower: + return parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) + else: + return "".join(part.capitalize() for part in parts) + +def camel_to_snake(text: str) -> str: + result = text[0].lower() + for char in text[1:]: + if char.isupper(): + result += "_" + char.lower() + else: + result += char + return result + +def to_dart_type(td: xdr.SCSpecTypeDef, nullable: bool = False, class_name: str = "") -> str: + t = td.type + nullable_suffix = "?" if nullable else "" + + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"XdrSCVal{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"bool{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return "void" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"int{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + return f"BigInt{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + return f"BigInt{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + return f"BigInt{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + return f"BigInt{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"Uint8List{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"String{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"String{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"Address{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"Address{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + return to_dart_type(td.option.value_type, nullable=True, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return to_dart_type(ok_t, nullable, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + return f"List<{to_dart_type(td.vec.element_type, class_name=class_name)}>{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + return f"Map<{to_dart_type(td.map.key_type, class_name=class_name)}, {to_dart_type(td.map.value_type, class_name=class_name)}>{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "void" + types = [to_dart_type(t, class_name=class_name) for t in td.tuple.value_types] + return f"({', '.join(types)}){nullable_suffix}" # Using Dart 3 records + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"Uint8List{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + udt_name = td.udt.name.decode() + return f"{prefixed_type_name(udt_name, class_name)}{nullable_suffix}" + raise ValueError(f"Unsupported SCValType: {t}") + + +def to_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "") -> str: + t = td.type + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"{name}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"XdrSCVal.forBool({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"XdrSCVal.forVoid()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"XdrSCVal.forU32({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"XdrSCVal.forI32({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"XdrSCVal.forU64({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"XdrSCVal.forI64({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"XdrSCVal.forTimePoint({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"XdrSCVal.forDuration({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + return f"XdrSCVal.forU128BigInt({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + return f"XdrSCVal.forI128BigInt({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + return f"XdrSCVal.forU256BigInt({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + return f"XdrSCVal.forI256BigInt({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"XdrSCVal.forBytes({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"XdrSCVal.forString({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"XdrSCVal.forSymbol({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"{name}.toXdrSCVal()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"{name}.toXdrSCVal()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + return f"{name} == null ? XdrSCVal.forVoid() : {to_scval(td.option.value_type, name, class_name)}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + return NotImplementedError("SC_SPEC_TYPE_RESULT is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + return f"XdrSCVal.forVec({name}.map((e) => {to_scval(td.vec.element_type, 'e', class_name)}).toList())" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + return f"XdrSCVal.forMap(Map.fromEntries({name}.entries.map((e) => MapEntry({to_scval(td.map.key_type, 'e.key', class_name)}, {to_scval(td.map.value_type, 'e.value', class_name)}))))" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + types = [ + to_scval(t, f"{name}.${{i+1}}", class_name) for i, t in enumerate(td.tuple.value_types) + ] + return f"XdrSCVal.forVec([{', '.join(types)}])" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"XdrSCVal.forBytes({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + return f"{name}.toScVal()" + raise ValueError(f"Unsupported SCValType: {t}") + + +def from_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "") -> str: + t = td.type + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"{name}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"{name}.b!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"null" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"{name}.u32!.uint32" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"{name}.i32!.int32" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"BigInt.from({name}.u64!.uint64)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"{name}.i64!.int64" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"BigInt.from({name}.u64!.uint64)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"BigInt.from({name}.u64!.uint64)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + return f"{name}.toBigInt()!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + return f"{name}.toBigInt()!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + return f"{name}.toBigInt()!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + return f"{name}.toBigInt()!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"{name}.bytes!.dataValue" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"{name}.str!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"{name}.sym!.toString()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"Address.fromXdrSCVal({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"Address.fromXdrSCVal({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + return f"{name}.discriminant == XdrSCValType.SCV_VOID ? null : {from_scval(td.option.value_type, name, class_name)}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return f"{from_scval(ok_t, name, class_name)}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + return ( + f"{name}.vec!.map((e) => {from_scval(td.vec.element_type, 'e', class_name)}).toList()" + ) + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + return f"Map.fromEntries({name}.map!.entries.map((e) => MapEntry({from_scval(td.map.key_type, 'e.key', class_name)}, {from_scval(td.map.value_type, 'e.val', class_name)})))" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "null" + elements = f"{name}.vec!" + types = [ + from_scval(t, f"{elements}[{i}]", class_name) + for i, t in enumerate(td.tuple.value_types) + ] + return f"({', '.join(types)})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"{name}.bytes!.dataValue" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + udt_name = td.udt.name.decode() + return f"{prefixed_type_name(udt_name, class_name)}.fromScVal({name})" + raise NotImplementedError(f"Unsupported SCValType: {t}") + + +def render_info(): + return f"// This file was generated by stellar_contract_bindings v{stellar_contract_bindings_version} and stellar_sdk v{stellar_sdk_version}." + + +def render_imports(): + template = """ +import 'dart:typed_data'; +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; +""" + return Template(template).render() + + +def render_enum(entry: xdr.SCSpecUDTEnumV0, class_name: str): + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/// {{ entry.doc.decode() if entry.doc else type_name + ' enum' }} +enum {{ type_name }} { + {%- for case in entry.cases %} + {{ snake_to_camel(case.name.decode(), False) }}({{ case.value.uint32 }}){% if loop.last %};{% else %},{% endif %} + {%- endfor %} + + final int value; + + const {{ type_name }}(this.value); + + factory {{ type_name }}.fromValue(int value) { + return {{ type_name }}.values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError('Unknown {{ type_name }} value: $value'), + ); + } + + XdrSCVal toScVal() { + return XdrSCVal.forU32(value); + } + + static {{ type_name }} fromScVal(XdrSCVal val) { + return {{ type_name }}.fromValue(val.u32!.uint32); + } +} +""" + rendered_code = Template(template).render( + entry=entry, type_name=type_name, snake_to_camel=snake_to_camel + ) + return rendered_code + + +def render_error_enum(entry: xdr.SCSpecUDTErrorEnumV0, class_name: str): + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/// {{ entry.doc.decode() if entry.doc else type_name + ' error enum' }} +enum {{ type_name }} { + {%- for case in entry.cases %} + {{ snake_to_camel(case.name.decode(), False) }}({{ case.value.uint32 }}){% if loop.last %};{% else %},{% endif %} + {%- endfor %} + + final int value; + + const {{ type_name }}(this.value); + + factory {{ type_name }}.fromValue(int value) { + return {{ type_name }}.values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError('Unknown {{ type_name }} value: $value'), + ); + } + + XdrSCVal toScVal() { + return XdrSCVal.forU32(value); + } + + static {{ type_name }} fromScVal(XdrSCVal val) { + return {{ type_name }}.fromValue(val.u32!.uint32); + } +} +""" + rendered_code = Template(template).render( + entry=entry, type_name=type_name, snake_to_camel=snake_to_camel + ) + return rendered_code + + +def render_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_dart_type_bound(td, nullable=False): + return to_dart_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name) + + template = """ +/// {{ entry.doc.decode() if entry.doc else type_name + ' struct' }} +class {{ type_name }} { + {%- for field in entry.fields %} + final {{ to_dart_type(field.type) }} {{ snake_to_camel(field.name.decode()) }}; + {%- endfor %} + + const {{ type_name }}({ + {%- for field in entry.fields %} + required this.{{ snake_to_camel(field.name.decode()) }}, + {%- endfor %} + }); + + XdrSCVal toScVal() { + final fields = []; + {%- for field in entry.fields %} + fields.add(XdrSCMapEntry( + XdrSCVal.forSymbol('{{ field.name_r.decode() if field.name_r else field.name.decode() }}'), + {{ to_scval(field.type, snake_to_camel(field.name.decode())) }}, + )); + {%- endfor %} + return XdrSCVal.forMap(fields); + } + + factory {{ type_name }}.fromScVal(XdrSCVal val) { + final map = val.map!; + final fieldsMap = {}; + for (final entry in map) { + fieldsMap[entry.key.sym!.toString()] = entry.val; + } + + return {{ type_name }}( + {%- for field in entry.fields %} + {{ snake_to_camel(field.name.decode()) }}: {{ from_scval(field.type, 'fieldsMap["' ~ (field.name_r.decode() if field.name_r else field.name.decode()) ~ '"]!') }}, + {%- endfor %} + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is {{ type_name }} && + {%- for field in entry.fields %} + {{ snake_to_camel(field.name.decode()) }} == other.{{ snake_to_camel(field.name.decode()) }}{% if not loop.last %} &&{% endif %} + {%- endfor %}; + + @override + int get hashCode => Object.hash( + {%- for field in entry.fields %} + {{ snake_to_camel(field.name.decode()) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ); +} +""" + rendered_code = Template(template).render( + entry=entry, + type_name=type_name, + to_dart_type=to_dart_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + snake_to_camel=snake_to_camel, + ) + return rendered_code + + +def render_tuple_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_dart_type_bound(td, nullable=False): + return to_dart_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name) + + template = """ +/// {{ entry.doc.decode() if entry.doc else type_name + ' tuple struct' }} +class {{ type_name }} { + final ({% for f in entry.fields %}{{ to_dart_type(f.type) }}{% if not loop.last %}, {% endif %}{% endfor %}) value; + + const {{ type_name }}(this.value); + + XdrSCVal toScVal() { + return XdrSCVal.forVec([ + {%- for f in entry.fields %} + {{ to_scval(f.type, 'value.$' + (loop.index0 + 1)|string) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]); + } + + factory {{ type_name }}.fromScVal(XdrSCVal val) { + final vec = val.vec!; + return {{ type_name }}(( + {%- for f in entry.fields %} + {{ from_scval(f.type, 'vec[' + loop.index0|string + ']') }}{% if not loop.last %},{% endif %} + {%- endfor %} + )); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is {{ type_name }} && value == other.value; + + @override + int get hashCode => value.hashCode; +} +""" + rendered_code = Template(template).render( + entry=entry, + type_name=type_name, + to_dart_type=to_dart_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound + ) + return rendered_code + + +def render_union(entry: xdr.SCSpecUDTUnionV0, class_name: str): + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_dart_type_bound(td, nullable=False): + return to_dart_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name) + + kind_enum_template = """ +/// Kind enum for {{ type_name }} +enum {{ type_name }}Kind { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + {{ snake_to_camel(case.void_case.name.decode(), False) }}('{{ case.void_case.name_r.decode() if case.void_case.name_r else case.void_case.name.decode() }}'){% if loop.last %};{% else %},{% endif %} + {%- else %} + {{ snake_to_camel(case.tuple_case.name.decode(), False) }}('{{ case.tuple_case.name.decode() if case.tuple_case.name_r else case.tuple_case.name.decode() }}'){% if loop.last %};{% else %},{% endif %} + {%- endif %} + {%- endfor %} + + final String value; + + const {{ type_name }}Kind(this.value); + + factory {{ type_name }}Kind.fromValue(String value) { + return {{ type_name }}Kind.values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError('Unknown {{ type_name }}Kind value: $value'), + ); + } +} +""" + kind_enum_rendered_code = Template(kind_enum_template).render( + entry=entry, type_name=type_name, xdr=xdr, snake_to_camel=snake_to_camel + ) + + template = """ +/// {{ entry.doc.decode() if entry.doc else type_name + ' union' }} +class {{ type_name }} { + final {{ type_name }}Kind kind; + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 %} + {%- if len(case.tuple_case.type) == 1 %} + final {{ to_dart_type(case.tuple_case.type[0], nullable=True) }} {{ snake_to_camel(case.tuple_case.name.decode()) }}; + {%- else %} + final ({% for f in case.tuple_case.type %}{{ to_dart_type(f) }}{% if not loop.last %}, {% endif %}{% endfor %})? {{ snake_to_camel(case.tuple_case.name.decode()) }}; + {%- endif %} + {%- endif %} + {%- endfor %} + + const {{ type_name }}._({ + required this.kind, + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 %} + this.{{ snake_to_camel(case.tuple_case.name.decode()) }}, + {%- endif %} + {%- endfor %} + }); + + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + factory {{ type_name }}.{{ snake_to_camel(case.void_case.name.decode()) }}() { + return {{ type_name }}._(kind: {{ type_name }}Kind.{{ snake_to_camel(case.void_case.name.decode(), False) }}); + } + {%- else %} + {%- if len(case.tuple_case.type) == 1 %} + factory {{ type_name }}.{{ snake_to_camel(case.tuple_case.name.decode()) }}({{ to_dart_type(case.tuple_case.type[0]) }} value) { + return {{ type_name }}._( + kind: {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}, + {{ snake_to_camel(case.tuple_case.name.decode()) }}: value, + ); + } + {%- else %} + factory {{ type_name }}.{{ snake_to_camel(case.tuple_case.name.decode()) }}(({% for f in case.tuple_case.type %}{{ to_dart_type(f) }}{% if not loop.last %}, {% endif %}{% endfor %}) value) { + return {{ type_name }}._( + kind: {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}, + {{ snake_to_camel(case.tuple_case.name.decode()) }}: value, + ); + } + {%- endif %} + {%- endif %} + {%- endfor %} + + XdrSCVal toScVal() { + switch (kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case {{ type_name }}Kind.{{ snake_to_camel(case.void_case.name.decode(), False) }}: + return XdrSCVal.forVec([XdrSCVal.forSymbol(kind.value)]); + {%- else %} + case {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}: + {%- if len(case.tuple_case.type) == 1 %} + return XdrSCVal.forVec([ + XdrSCVal.forSymbol(kind.value), + {{ to_scval(case.tuple_case.type[0], snake_to_camel(case.tuple_case.name.decode()) + '!') }}, + ]); + {%- else %} + final tuple = {{ snake_to_camel(case.tuple_case.name.decode()) }}!; + return XdrSCVal.forVec([ + XdrSCVal.forSymbol(kind.value), + {%- for t in case.tuple_case.type %} + {{ to_scval(t, 'tuple.$' + (loop.index)|string) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]); + {%- endif %} + {%- endif %} + {%- endfor %} + } + } + + factory {{ type_name }}.fromScVal(XdrSCVal val) { + final vec = val.vec!; + final kind = {{ type_name }}Kind.fromValue(vec[0].sym!.toString()); + + switch (kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case {{ type_name }}Kind.{{ snake_to_camel(case.void_case.name.decode(), False) }}: + return {{ type_name }}.{{ snake_to_camel(case.void_case.name.decode()) }}(); + {%- else %} + case {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}: + {%- if len(case.tuple_case.type) == 1 %} + return {{ type_name }}.{{ snake_to_camel(case.tuple_case.name.decode()) }}( + {{ from_scval(case.tuple_case.type[0], 'vec[1]') }} + ); + {%- else %} + return {{ type_name }}.{{ snake_to_camel(case.tuple_case.name.decode()) }}(( + {%- for i, t in enumerate(case.tuple_case.type) %} + {{ from_scval(t, 'vec[' + (i + 1)|string + ']') }}{% if not loop.last %},{% endif %} + {%- endfor %} + )); + {%- endif %} + {%- endif %} + {%- endfor %} + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! {{ type_name }}) return false; + if (kind != other.kind) return false; + + switch (kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case {{ type_name }}Kind.{{ snake_to_camel(case.void_case.name.decode(), False) }}: + return true; + {%- else %} + case {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}: + return {{ snake_to_camel(case.tuple_case.name.decode()) }} == other.{{ snake_to_camel(case.tuple_case.name.decode()) }}; + {%- endif %} + {%- endfor %} + } + } + + @override + int get hashCode { + switch (kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case {{ type_name }}Kind.{{ snake_to_camel(case.void_case.name.decode(), False) }}: + return kind.hashCode; + {%- else %} + case {{ type_name }}Kind.{{ snake_to_camel(case.tuple_case.name.decode(), False) }}: + return Object.hash(kind, {{ snake_to_camel(case.tuple_case.name.decode()) }}); + {%- endif %} + {%- endfor %} + } + } +} +""" + union_rendered_code = Template(template).render( + entry=entry, + type_name=type_name, + to_dart_type=to_dart_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + xdr=xdr, + len=len, + snake_to_camel=snake_to_camel, + enumerate=enumerate, + ) + return kind_enum_rendered_code + "\n" + union_rendered_code + + +def render_client(entries: List[xdr.SCSpecFunctionV0], class_name: str): + template = """ +/// Client for interacting with the {{ class_name }} contract +class {{ class_name }} { + /// The underlying SorobanClient instance + final SorobanClient _client; + + /// Creates a new {{ class_name }} for the given contract ID + static Future<{{ class_name }}> forContractId({ + required KeyPair sourceAccountKeyPair, + required String contractId, + required Network network, + required String rpcUrl, + bool enableServerLogging = false, + }) async { + final options = ClientOptions( + sourceAccountKeyPair: sourceAccountKeyPair, + contractId: contractId, + network: network, + rpcUrl: rpcUrl, + enableServerLogging: enableServerLogging, + ); + + final client = await SorobanClient.forClientOptions(options: options); + return {{ class_name }}._(client); + } + + /// Private constructor that wraps a SorobanClient + {{ class_name }}._(this._client); + + /// Gets the contract ID + String getContractId() => _client.getContractId(); + + /// Gets the client options + ClientOptions getOptions() => _client.getOptions(); + + /// Gets the contract specification + ContractSpec getContractSpec() => _client.getContractSpec(); + + {%- for entry in entries %} + + /// {{ entry.doc.decode() if entry.doc else 'Invokes the ' + entry.name.sc_symbol.decode() + ' method' }} + {%- if parse_result_type(entry.outputs) == 'void' %} + Future {{ snake_to_camel(entry.name.sc_symbol.decode()) }}({ + {%- else %} + Future<{{ parse_result_type(entry.outputs) }}> {{ snake_to_camel(entry.name.sc_symbol.decode()) }}({ + {%- endif %} + {%- for param in entry.inputs %} + required {{ to_dart_type(param.type) }} {{ snake_to_camel(param.name.decode()) }}, + {%- endfor %} + KeyPair? signer, + int baseFee = 100, + int transactionTimeout = 300, + int submitTimeout = 30, + bool simulate = true, + bool restore = true, + bool force = false, + }) async { + final List args = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, snake_to_camel(param.name.decode())) }}, + {%- endfor %} + ]; + + final methodOptions = MethodOptions(); + // You can customize method options here if needed + + final result = await _client.invokeMethod( + name: '{{ entry.name.sc_symbol_r.decode() if entry.name.sc_symbol_r else entry.name.sc_symbol.decode() }}', + args: args, + force: force, + methodOptions: methodOptions, + ); + + {%- if parse_result_type(entry.outputs) != 'void' %} + return {{ parse_result_from_scval(entry.outputs, 'result') }}; + {%- endif %} + } + + /// Builds an AssembledTransaction for the {{ entry.name.sc_symbol.decode() }} method. + /// This is useful if you need to manipulate the transaction before signing and sending. + Future build{{ snake_to_camel(entry.name.sc_symbol.decode(), False) }}Tx({ + {%- for param in entry.inputs %} + required {{ to_dart_type(param.type) }} {{ snake_to_camel(param.name.decode()) }}, + {%- endfor %} + MethodOptions? methodOptions, + }) async { + final List args = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, snake_to_camel(param.name.decode())) }}, + {%- endfor %} + ]; + + return await _client.buildInvokeMethodTx( + name: '{{ entry.name.sc_symbol_r.decode() if entry.name.sc_symbol_r else entry.name.sc_symbol.decode() }}', + args: args, + methodOptions: methodOptions, + ); + } + {%- endfor %} +} +""" + + # Create wrapper functions with class_name bound + def to_dart_type_bound(td, nullable=False): + return to_dart_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name) + + def parse_result_type(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "void" + elif len(output) == 1: + return to_dart_type_bound(output[0]) + else: + types = [to_dart_type_bound(t) for t in output] + return f"({', '.join(types)})" + + def parse_result_xdr(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "" + elif len(output) == 1: + return from_scval_bound(output[0], "result") + else: + # Handle tuple return + results = [] + for i, t in enumerate(output): + results.append(from_scval_bound(t, f"result.vec![{i}]")) + return f"({', '.join(results)})" + + def parse_result_from_scval(output: List[xdr.SCSpecTypeDef], var_name: str): + if len(output) == 0: + return "" + elif len(output) == 1: + return from_scval_bound(output[0], var_name) + else: + # Handle tuple return + results = [] + for i, t in enumerate(output): + results.append(from_scval_bound(t, f"{var_name}.vec![{i}]")) + return f"({', '.join(results)})" + + client_rendered_code = Template(template).render( + entries=entries, + to_dart_type=to_dart_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + parse_result_type=parse_result_type, + parse_result_xdr=parse_result_xdr, + parse_result_from_scval=parse_result_from_scval, + snake_to_camel=snake_to_camel, + class_name=class_name, + ) + return client_rendered_code + + +def append_underscore(specs: List[xdr.SCSpecEntry]): + for spec in specs: + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0: + assert spec.udt_struct_v0 is not None + if is_keywords(spec.udt_struct_v0.name.decode()): + spec.udt_struct_v0.name_r = spec.udt_struct_v0.name # type: ignore[attr-defined] + spec.udt_struct_v0.name = spec.udt_struct_v0.name + b"_" + for field in spec.udt_struct_v0.fields: + if is_keywords(field.name.decode()): + field.name_r = field.name # type: ignore[attr-defined] + field.name = field.name + b"_" + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0: + assert spec.udt_union_v0 is not None + if is_keywords(spec.udt_union_v0.name.decode()): + spec.udt_union_v0.name_r = spec.udt_union_v0.name # type: ignore[attr-defined] + spec.udt_union_v0.name = spec.udt_union_v0.name + b"_" + for union_case in spec.udt_union_v0.cases: + if ( + union_case.kind + == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 + ): + if is_keywords(union_case.tuple_case.name.decode()): + union_case.tuple_case.name_r = union_case.tuple_case.name # type: ignore[attr-defined] + union_case.tuple_case.name = union_case.tuple_case.name + b"_" + elif ( + union_case.kind + == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 + ): + if is_keywords(union_case.void_case.name.decode()): + union_case.void_case.name_r = union_case.void_case.name # type: ignore[attr-defined] + union_case.void_case.name = union_case.void_case.name + b"_" + else: + raise ValueError(f"Unsupported union case kind: {union_case.kind}") + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0: + assert spec.function_v0 is not None + if is_keywords(spec.function_v0.name.sc_symbol.decode()): + spec.function_v0.name.sc_symbol_r = spec.function_v0.name.sc_symbol # type: ignore[attr-defined] + spec.function_v0.name.sc_symbol = spec.function_v0.name.sc_symbol + b"_" + for param in spec.function_v0.inputs: + if is_keywords(param.name.decode()): + param.name = param.name + b"_" + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0: + assert spec.udt_enum_v0 is not None + if is_keywords(spec.udt_enum_v0.name.decode()): + spec.udt_enum_v0.name_r = spec.udt_enum_v0.name # type: ignore[attr-defined] + spec.udt_enum_v0.name = spec.udt_enum_v0.name + b"_" + for enum_case in spec.udt_enum_v0.cases: + if is_keywords(enum_case.name.decode()): + enum_case.name = enum_case.name + b"_" + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0: + assert spec.udt_error_enum_v0 is not None + if is_keywords(spec.udt_error_enum_v0.name.decode()): + spec.udt_error_enum_v0.name_r = spec.udt_error_enum_v0.name # type: ignore[attr-defined] + spec.udt_error_enum_v0.name = spec.udt_error_enum_v0.name + b"_" + for error_enum_case in spec.udt_error_enum_v0.cases: + if is_keywords(error_enum_case.name.decode()): + error_enum_case.name = error_enum_case.name + b"_" + + +def generate_binding(specs: List[xdr.SCSpecEntry], class_name: str) -> str: + append_underscore(specs) + + generated = [] + generated.append(render_info()) + generated.append(render_imports()) + + for spec in specs: + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0: + generated.append(render_enum(spec.udt_enum_v0, class_name)) + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0: + generated.append(render_error_enum(spec.udt_error_enum_v0, class_name)) + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0: + if is_tuple_struct(spec.udt_struct_v0): + generated.append(render_tuple_struct(spec.udt_struct_v0, class_name)) + else: + generated.append(render_struct(spec.udt_struct_v0, class_name)) + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0: + generated.append(render_union(spec.udt_union_v0, class_name)) + + function_specs: List[xdr.SCSpecFunctionV0] = [ + spec.function_v0 + for spec in specs + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0 + and not spec.function_v0.name.sc_symbol.decode().startswith("__") + ] + generated.append(render_client(function_specs, class_name)) + return "\n".join(generated) + + +@click.command(name="flutter") +@click.option( + "--contract-id", required=True, help="The contract ID to generate bindings for" +) +@click.option( + "--rpc-url", default="https://mainnet.sorobanrpc.com", help="Soroban RPC URL" +) +@click.option( + "--output", + default=None, + help="Output directory for generated bindings, defaults to current directory", +) +@click.option( + "--class-name", + default="Contract", + help="Class name prefix for generated bindings, defaults to 'Contract'", +) +def command(contract_id: str, rpc_url: str, output: str, class_name: str): + """Generate Flutter/Dart bindings for a Soroban contract""" + if not StrKey.is_valid_contract(contract_id): + click.echo(f"Invalid contract ID: {contract_id}", err=True) + raise click.Abort() + + # Use current directory if output is not specified + if output is None: + output = os.getcwd() + try: + specs = get_specs_by_contract_id(contract_id, rpc_url) + except Exception as e: + click.echo(f"Get contract specs failed: {e}", err=True) + raise click.Abort() + + click.echo("Generating Flutter bindings") + generated = generate_binding(specs, class_name=class_name) + + if not os.path.exists(output): + os.makedirs(output) + output_path = os.path.join( + output, f"{camel_to_snake(class_name)}_client.dart" + ) + with open(output_path, "w") as f: + f.write(generated) + click.echo(f"Generated Flutter bindings to {output_path}") + + +if __name__ == "__main__": + command() diff --git a/stellar_contract_bindings/php.py b/stellar_contract_bindings/php.py new file mode 100644 index 0000000..3c5c2f2 --- /dev/null +++ b/stellar_contract_bindings/php.py @@ -0,0 +1,886 @@ +import os +from typing import List + +import click +from jinja2 import Template +from stellar_sdk import __version__ as stellar_sdk_version, StrKey +from stellar_sdk import xdr + +from stellar_contract_bindings import __version__ as stellar_contract_bindings_version +from stellar_contract_bindings.utils import get_specs_by_contract_id + + +def is_php_keyword(word: str) -> bool: + """Check if a word is a PHP reserved keyword.""" + return word.lower() in [ + "abstract", "and", "array", "as", "break", "callable", "case", "catch", + "class", "clone", "const", "continue", "declare", "default", "die", "do", + "echo", "else", "elseif", "empty", "enddeclare", "endfor", "endforeach", + "endif", "endswitch", "endwhile", "eval", "exit", "extends", "final", + "finally", "fn", "for", "foreach", "function", "global", "goto", "if", + "implements", "include", "include_once", "instanceof", "insteadof", + "interface", "isset", "list", "match", "namespace", "new", "or", "print", + "private", "protected", "public", "readonly", "require", "require_once", + "return", "static", "switch", "throw", "trait", "try", "unset", "use", + "var", "while", "xor", "yield", "yield_from", + "__halt_compiler", "__class__", "__dir__", "__file__", "__function__", + "__line__", "__method__", "__namespace__", "__trait__", + "int", "float", "bool", "string", "true", "false", "null", "void", + "iterable", "object", "resource", "mixed", "never" + ] + + +def is_tuple_struct(entry: xdr.SCSpecUDTStructV0) -> bool: + """Check if a struct is a tuple struct (fields are numeric).""" + return all(f.name.isdigit() for f in entry.fields) + + +def snake_to_pascal(text: str) -> str: + """Convert snake_case to PascalCase.""" + parts = text.split("_") + return "".join(part.capitalize() for part in parts) + + +def snake_to_camel(text: str) -> str: + """Convert snake_case to camelCase.""" + parts = text.split("_") + return parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) + + +def camel_to_snake(text: str) -> str: + """Convert CamelCase to snake_case.""" + result = text[0].lower() + for char in text[1:]: + if char.isupper(): + result += "_" + char.lower() + else: + result += char + return result + + +def escape_keyword(name: str, context: str = "property") -> str: + """Escape PHP keywords by appending underscore.""" + if is_php_keyword(name): + return f"{name}_" + return name + + +def prefixed_type_name(type_name: str, class_name: str) -> str: + """Prefix a type name with the class name to avoid conflicts. + + Args: + type_name: The original type name from the contract spec + class_name: The class name to use as prefix + + Returns: + The prefixed type name (e.g., "DataKey" -> "TokenContractDataKey") + """ + # Don't prefix primitive types or SDK types + if type_name in ['string', 'bool', 'int', 'float', 'array', 'Address', + 'XdrSCVal', 'XdrSCMapEntry', 'XdrSCValType']: + return type_name + return f"{class_name}{type_name}" + + +def to_php_type(td: xdr.SCSpecTypeDef, nullable: bool = False, class_name: str = "") -> str: + """Convert Soroban type to PHP type hint.""" + t = td.type + nullable_prefix = "?" if nullable else "" + + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"{nullable_prefix}XdrSCVal" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"{nullable_prefix}bool" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return "void" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t in [xdr.SCSpecType.SC_SPEC_TYPE_U32, xdr.SCSpecType.SC_SPEC_TYPE_I32, + xdr.SCSpecType.SC_SPEC_TYPE_U64, xdr.SCSpecType.SC_SPEC_TYPE_I64, + xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT, xdr.SCSpecType.SC_SPEC_TYPE_DURATION]: + return f"{nullable_prefix}int" + if t in [xdr.SCSpecType.SC_SPEC_TYPE_U128, xdr.SCSpecType.SC_SPEC_TYPE_I128, + xdr.SCSpecType.SC_SPEC_TYPE_U256, xdr.SCSpecType.SC_SPEC_TYPE_I256]: + return f"{nullable_prefix}string" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"{nullable_prefix}string" # PHP uses string for bytes + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"{nullable_prefix}string" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"{nullable_prefix}string" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"{nullable_prefix}Address" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"{nullable_prefix}Address" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + return to_php_type(td.option.value_type, nullable=True, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return to_php_type(ok_t, nullable, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + inner_type = to_php_type(td.vec.element_type, class_name=class_name) + return f"{nullable_prefix}array" # PHP arrays + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + return f"{nullable_prefix}array" # PHP associative arrays + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "void" + return f"{nullable_prefix}array" # PHP arrays for tuples + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"{nullable_prefix}string" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + udt_name = td.udt.name.decode() + return f"{nullable_prefix}{prefixed_type_name(udt_name, class_name)}" + raise ValueError(f"Unsupported SCValType: {t}") + + +def to_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "") -> str: + """Generate PHP code to convert a value to XdrSCVal.""" + t = td.type + # Add $ prefix only if name doesn't already start with $ or this-> + if name.startswith('$') or name.startswith('this->'): + var_name = name + else: + var_name = f"${name}" + + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return var_name + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"XdrSCVal::forBool({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"XdrSCVal::forVoid()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"XdrSCVal::forU32({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"XdrSCVal::forI32({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"XdrSCVal::forU64({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"XdrSCVal::forI64({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"XdrSCVal::forTimepoint({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"XdrSCVal::forDuration({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + return f"XdrSCVal::forU128BigInt({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + return f"XdrSCVal::forI128BigInt({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + return f"XdrSCVal::forU256BigInt({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + return f"XdrSCVal::forI256BigInt({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"XdrSCVal::forBytes({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"XdrSCVal::forString({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"XdrSCVal::forSymbol({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"{var_name}->toXdrSCVal()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"{var_name}->toXdrSCVal()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + inner = to_scval(td.option.value_type, name, class_name) + return f"({var_name} !== null ? {inner} : XdrSCVal::forVoid())" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + return NotImplementedError("SC_SPEC_TYPE_RESULT is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + element_conversion = to_scval(td.vec.element_type, "item", class_name) + return f"XdrSCVal::forVec(array_map(fn($item) => {element_conversion}, {var_name}))" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + key_conv = to_scval(td.map.key_type, "k", class_name) + val_conv = to_scval(td.map.value_type, "v", class_name) + return f"XdrSCVal::forMap(array_map(fn($k, $v) => [{key_conv}, {val_conv}], array_keys({var_name}), {var_name}))" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "XdrSCVal::forVoid()" + conversions = [to_scval(td.tuple.value_types[i], f"{name}[{i}]", class_name) for i in range(len(td.tuple.value_types))] + return f"XdrSCVal::forTupleStruct([{', '.join(conversions)}])" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"XdrSCVal::forBytes({var_name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + return f"{var_name}->toSCVal()" + raise ValueError(f"Unsupported SCValType: {t}") + + +def from_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "") -> str: + """Generate PHP code to convert from XdrSCVal to a PHP value.""" + t = td.type + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"${name}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"${name}->b" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"null" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"${name}->u32" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"${name}->i32" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"${name}->u64" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"${name}->i64" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"${name}->timepoint" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"${name}->duration" + if t in [xdr.SCSpecType.SC_SPEC_TYPE_U128, xdr.SCSpecType.SC_SPEC_TYPE_I128]: + return f"gmp_strval(${name}->toBigInt())" + if t in [xdr.SCSpecType.SC_SPEC_TYPE_U256, xdr.SCSpecType.SC_SPEC_TYPE_I256]: + return f"gmp_strval(${name}->toBigInt())" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"${name}->bytes->getValue()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"${name}->str" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"${name}->sym" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"Address::fromXdrSCVal(${name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"Address::fromXdrSCVal(${name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + inner = from_scval(td.option.value_type, name, class_name) + return f"(${name}->type !== XdrSCValType::SCV_VOID ? {inner} : null)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return from_scval(ok_t, name, class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + element_conversion = from_scval(td.vec.element_type, "item", class_name) + return f"array_map(fn($item) => {element_conversion}, ${name}->vec)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + key_conv = from_scval(td.map.key_type, "entry->key", class_name) + val_conv = from_scval(td.map.value_type, "entry->val", class_name) + return f"array_combine(array_map(fn($entry) => {key_conv}, ${name}->map), array_map(fn($entry) => {val_conv}, ${name}->map))" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "null" + conversions = [from_scval(td.tuple.value_types[i], f"{name}->vec[{i}]", class_name) for i in range(len(td.tuple.value_types))] + return f"[{', '.join(conversions)}]" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"${name}->bytes->getValue()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + udt_name = td.udt.name.decode() + return f"{prefixed_type_name(udt_name, class_name)}::fromSCVal(${name})" + raise NotImplementedError(f"Unsupported SCValType: {t}") + + +def render_info(): + """Generate file header comment.""" + return f"""value); + } + + public static function fromSCVal(XdrSCVal $val): self + { + return self::from($val->u32); + } +} +""" + return Template(template).render(entry=entry, type_name=type_name) + + +def render_error_enum(entry: xdr.SCSpecUDTErrorEnumV0, class_name: str): + """Generate PHP error enum class.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/** +{%- if entry.doc %} + * {{ entry.doc.decode() }} (Error enum) +{%- else %} + * Generated error enum {{ type_name }} +{%- endif %} + */ +enum {{ type_name }}Error: int +{ + {%- for case in entry.cases %} + case {{ case.name.decode() }} = {{ case.value.uint32 }}; + {%- endfor %} + + public function toSCVal(): XdrSCVal + { + return XdrSCVal::forU32($this->value); + } + + public static function fromSCVal(XdrSCVal $val): self + { + return self::from($val->u32); + } +} +""" + return Template(template).render(entry=entry, type_name=type_name) + + +def render_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + """Generate PHP class for struct.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/** +{%- if entry.doc %} + * {{ entry.doc.decode() }} +{%- else %} + * Generated struct {{ type_name }} +{%- endif %} + */ +class {{ type_name }} +{ + {%- for field in entry.fields %} + public {{ to_php_type(field.type, False, class_name) }} ${{ escape_keyword(field.name.decode()) }}; + {%- endfor %} + + public function __construct( + {%- for field in entry.fields %} + {{ to_php_type(field.type, False, class_name) }} ${{ escape_keyword(field.name.decode()) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ) { + {%- for field in entry.fields %} + $this->{{ escape_keyword(field.name.decode()) }} = ${{ escape_keyword(field.name.decode()) }}; + {%- endfor %} + } + + public function toSCVal(): XdrSCVal + { + $mapEntries = []; + {%- for field in entry.fields %} + $mapEntries[] = new XdrSCMapEntry( + XdrSCVal::forSymbol('{{ field.name.decode() }}'), + {{ to_scval(field.type, '$this->' ~ escape_keyword(field.name.decode()), class_name) }} + ); + {%- endfor %} + return XdrSCVal::forMap($mapEntries); + } + + public static function fromSCVal(XdrSCVal $val): self + { + $map = []; + foreach ($val->map as $entry) { + $map[$entry->key->sym] = $entry->val; + } + return new self( + {%- for field in entry.fields %} + {{ escape_keyword(field.name.decode()) }}: {{ from_scval(field.type, 'map["' ~ field.name.decode() ~ '"]', class_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ); + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + class_name=class_name, + to_php_type=to_php_type, + to_scval=to_scval, + from_scval=from_scval, + escape_keyword=escape_keyword + ) + + +def render_tuple_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + """Generate PHP class for tuple struct.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/** +{%- if entry.doc %} + * {{ entry.doc.decode() }} +{%- else %} + * Generated tuple struct {{ type_name }} +{%- endif %} + */ +class {{ type_name }} +{ + public array $value; + + /** + * @param array{%- for f in entry.fields %}{% if loop.first %}<{% endif %}{{ to_php_type(f.type, False, class_name) }}{% if not loop.last %}, {% else %}>{% endif %}{% endfor %} $value + */ + public function __construct(array $value) + { + $this->value = $value; + } + + public function toSCVal(): XdrSCVal + { + return XdrSCVal::forVec([ + {%- for f in entry.fields %} + {{ to_scval(f.type, '$this->value[' ~ f.name.decode() ~ ']', class_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]); + } + + public static function fromSCVal(XdrSCVal $val): self + { + $elements = $val->vec; + return new self([ + {%- for f in entry.fields %} + {{ from_scval(f.type, 'elements[' ~ f.name.decode() ~ ']', class_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]); + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + class_name=class_name, + to_php_type=to_php_type, + to_scval=to_scval, + from_scval=from_scval + ) + + +def render_union(entry: xdr.SCSpecUDTUnionV0, class_name: str): + """Generate PHP class for union.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/** +{%- if entry.doc %} + * {{ entry.doc.decode() }} +{%- else %} + * Generated union {{ type_name }} +{%- endif %} + */ +class {{ type_name }} +{ + public const {% for case in entry.cases -%} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 -%} + {{ case.void_case.name.decode().upper() }} = '{{ case.void_case.name.decode() }}' + {%- else -%} + {{ case.tuple_case.name.decode().upper() }} = '{{ case.tuple_case.name.decode() }}' + {%- endif -%} + {%- if not loop.last %}, {% else %};{% endif -%} + {%- endfor %} + + public string $kind; + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 %} + {%- if len(case.tuple_case.type) == 1 %} + public {{ to_php_type(case.tuple_case.type[0], nullable=True, class_name=class_name) }} ${{ camel_to_snake(case.tuple_case.name.decode()) }} = null; + {%- else %} + public ?array ${{ camel_to_snake(case.tuple_case.name.decode()) }} = null; + {%- endif %} + {%- endif %} + {%- endfor %} + + public function __construct(string $kind{% for case in entry.cases -%} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 -%} + {%- if len(case.tuple_case.type) == 1 -%} + , ?{{ to_php_type(case.tuple_case.type[0], False, class_name) }} ${{ camel_to_snake(case.tuple_case.name.decode()) }} = null + {%- else -%} + , ?array ${{ camel_to_snake(case.tuple_case.name.decode()) }} = null + {%- endif -%} + {%- endif -%} + {%- endfor %}) + { + $this->kind = $kind; + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0 %} + $this->{{ camel_to_snake(case.tuple_case.name.decode()) }} = ${{ camel_to_snake(case.tuple_case.name.decode()) }}; + {%- endif %} + {%- endfor %} + } + + public function toSCVal(): XdrSCVal + { + switch ($this->kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case self::{{ case.void_case.name.decode().upper() }}: + return XdrSCVal::forVec([XdrSCVal::forSymbol($this->kind)]); + {%- else %} + case self::{{ case.tuple_case.name.decode().upper() }}: + {%- if len(case.tuple_case.type) == 1 %} + return XdrSCVal::forVec([ + XdrSCVal::forSymbol($this->kind), + {{ to_scval(case.tuple_case.type[0], '$this->' ~ camel_to_snake(case.tuple_case.name.decode()), class_name) }} + ]); + {%- else %} + return XdrSCVal::forVec([ + XdrSCVal::forSymbol($this->kind), + {%- for i, t in enumerate(case.tuple_case.type) %} + {{ to_scval(t, '$this->' ~ camel_to_snake(case.tuple_case.name.decode()) ~ '[' ~ i|string ~ ']', class_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]); + {%- endif %} + {%- endif %} + {%- endfor %} + default: + throw new Exception("Invalid union kind: {$this->kind}"); + } + } + + public static function fromSCVal(XdrSCVal $val): self + { + if ($val->vec === null || count($val->vec) < 1) { + throw new Exception("Invalid union value: expected vec with at least 1 element"); + } + + $kind = $val->vec[0]->sym; + + switch ($kind) { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case '{{ case.void_case.name.decode() }}': + return new self(self::{{ case.void_case.name.decode().upper() }}); + {%- else %} + case '{{ case.tuple_case.name.decode() }}': + {%- if len(case.tuple_case.type) == 1 %} + if (count($val->vec) !== 2) { + throw new Exception("Invalid union value for {{ case.tuple_case.name.decode() }}: expected 2 elements"); + } + return new self( + self::{{ case.tuple_case.name.decode().upper() }}, + {{ camel_to_snake(case.tuple_case.name.decode()) }}: {{ from_scval(case.tuple_case.type[0], 'val->vec[1]', class_name) }} + ); + {%- else %} + if (count($val->vec) !== {{ len(case.tuple_case.type) + 1 }}) { + throw new Exception("Invalid union value for {{ case.tuple_case.name.decode() }}: expected {{ len(case.tuple_case.type) + 1 }} elements"); + } + return new self( + self::{{ case.tuple_case.name.decode().upper() }}, + {{ camel_to_snake(case.tuple_case.name.decode()) }}: [ + {%- for i, t in enumerate(case.tuple_case.type) %} + {{ from_scval(t, 'val->vec[' ~ (i + 1)|string ~ ']', class_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ] + ); + {%- endif %} + {%- endif %} + {%- endfor %} + default: + throw new Exception("Unknown union kind: $kind"); + } + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + class_name=class_name, + to_php_type=to_php_type, + to_scval=to_scval, + from_scval=from_scval, + xdr=xdr, + len=len, + camel_to_snake=camel_to_snake, + enumerate=enumerate + ) + + +def render_client(entries: List[xdr.SCSpecFunctionV0], contract_name: str): + """Generate PHP client class.""" + template = ''' +/** + * Generated contract client for {{ contract_name }} + */ +class {{ contract_name }} +{ + /** + * The underlying SorobanClient instance + * @var SorobanClient + */ + private SorobanClient $client; + + /** + * Private constructor that wraps a SorobanClient + * @param SorobanClient $client + */ + private function __construct(SorobanClient $client) + { + $this->client = $client; + } + + /** + * Creates a new {{ contract_name }} for the given contract ID + * @param ClientOptions $options Client options for the contract + * @return {{ contract_name }} + * @throws Exception + * @throws GuzzleException + */ + public static function forClientOptions(ClientOptions $options): self + { + $client = SorobanClient::forClientOptions($options); + return new self($client); + } + + /** + * Gets the contract ID + * @return string + */ + public function getContractId(): string + { + return $this->client->getContractId(); + } + + /** + * Gets the client options + * @return ClientOptions + */ + public function getOptions(): ClientOptions + { + return $this->client->getOptions(); + } + + /** + * Gets the contract specification + * @return ContractSpec + */ + public function getContractSpec(): ContractSpec + { + return $this->client->getContractSpec(); + } + {%- for entry in entries %} + + /** + {%- if entry.doc %} + * {{ entry.doc.decode() }} + {%- else %} + * Invoke the {{ entry.name.sc_symbol.decode() }} method + {%- endif %} + * + {%- for param in entry.inputs %} + * @param {{ to_php_type(param.type, False, contract_name) }} ${{ escape_keyword(param.name.decode()) }} + {%- endfor %} + * @param MethodOptions|null $methodOptions Options for transaction + * @return {{ parse_result_type(entry.outputs) }} + * @throws Exception + * @throws GuzzleException + */ + public function {{ snake_to_camel(entry.name.sc_symbol.decode()) }}( + {%- for param in entry.inputs %} + {{ to_php_type(param.type, False, contract_name) }} ${{ escape_keyword(param.name.decode()) }}, + {%- endfor %} + ?MethodOptions $methodOptions = null + ){{ return_type_hint(entry.outputs) }} { + $args = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, escape_keyword(param.name.decode()), contract_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]; + + $result = $this->client->invokeMethod( + name: '{{ entry.name.sc_symbol.decode() }}', + args: $args, + methodOptions: $methodOptions + ); + {%- if len(entry.outputs) > 0 %} + return {{ parse_result_conversion(entry.outputs) }}; + {%- endif %} + } + + /** + * Build an AssembledTransaction for the {{ entry.name.sc_symbol.decode() }} method. + * This is useful if you need to manipulate the transaction before signing and sending. + * + {%- for param in entry.inputs %} + * @param {{ to_php_type(param.type, False, contract_name) }} ${{ escape_keyword(param.name.decode()) }} + {%- endfor %} + * @param MethodOptions|null $methodOptions Options for transaction + * @return AssembledTransaction + * @throws Exception + * @throws GuzzleException + */ + public function build{{ snake_to_pascal(entry.name.sc_symbol.decode()) }}Tx( + {%- for param in entry.inputs %} + {{ to_php_type(param.type, False, contract_name) }} ${{ escape_keyword(param.name.decode()) }}, + {%- endfor %} + ?MethodOptions $methodOptions = null + ): AssembledTransaction { + $args = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, escape_keyword(param.name.decode()), contract_name) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]; + + return $this->client->buildInvokeMethodTx( + name: '{{ entry.name.sc_symbol.decode() }}', + args: $args, + methodOptions: $methodOptions + ); + } + {%- endfor %} +} +''' + + def parse_result_type(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "void" + elif len(output) == 1: + return to_php_type(output[0], False, contract_name) + else: + return "array" + + def return_type_hint(output: List[xdr.SCSpecTypeDef]): + result_type = parse_result_type(output) + if result_type: + # Add colon and return type + return f": {result_type}" + else: + return "" + + def parse_result_conversion(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "" + elif len(output) == 1: + return from_scval(output[0], "result", contract_name) + else: + conversions = [from_scval(output[i], f"result[{i}]", contract_name) for i in range(len(output))] + return f"[{', '.join(conversions)}]" + + return Template(template).render( + entries=entries, + contract_name=contract_name, + to_php_type=to_php_type, + to_scval=to_scval, + from_scval=from_scval, + parse_result_type=parse_result_type, + return_type_hint=return_type_hint, + parse_result_conversion=parse_result_conversion, + escape_keyword=escape_keyword, + snake_to_camel=snake_to_camel, + snake_to_pascal=snake_to_pascal, + len=len + ) + + +def generate_binding(specs: List[xdr.SCSpecEntry], namespace: str = "GeneratedContracts", contract_name: str = "Contract") -> str: + """Generate complete PHP binding file.""" + generated = [] + generated.append(render_info()) + generated.append(render_imports(namespace)) + + # Generate types + for spec in specs: + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0: + generated.append(render_enum(spec.udt_enum_v0, contract_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0: + generated.append(render_error_enum(spec.udt_error_enum_v0, contract_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0: + if is_tuple_struct(spec.udt_struct_v0): + generated.append(render_tuple_struct(spec.udt_struct_v0, contract_name)) + else: + generated.append(render_struct(spec.udt_struct_v0, contract_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0: + generated.append(render_union(spec.udt_union_v0, contract_name)) + + # Generate client + function_specs: List[xdr.SCSpecFunctionV0] = [ + spec.function_v0 + for spec in specs + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0 + and not spec.function_v0.name.sc_symbol.decode().startswith("__") + ] + + if function_specs: + generated.append(render_client(function_specs, contract_name)) + + return "\n".join(generated) + + +@click.command(name="php") +@click.option( + "--contract-id", required=True, help="The contract ID to generate bindings for" +) +@click.option( + "--rpc-url", default="https://mainnet.sorobanrpc.com", help="Soroban RPC URL" +) +@click.option( + "--output", + default=None, + help="Output directory for generated bindings, defaults to current directory", +) +@click.option( + "--namespace", + default="GeneratedContracts", + help="PHP namespace for generated classes", +) +@click.option( + "--class-name", + default="ContractClient", + help="Name for the generated client class", +) +def command(contract_id: str, rpc_url: str, output: str, namespace: str, class_name: str): + """Generate PHP bindings for a Soroban contract""" + if not StrKey.is_valid_contract(contract_id): + click.echo(f"Invalid contract ID: {contract_id}", err=True) + raise click.Abort() + + # Use current directory if output is not specified + if output is None: + output = os.getcwd() + + try: + specs = get_specs_by_contract_id(contract_id, rpc_url) + except Exception as e: + click.echo(f"Get contract specs failed: {e}", err=True) + raise click.Abort() + + click.echo("Generating PHP bindings") + generated = generate_binding(specs, namespace=namespace, contract_name=class_name) + + if not os.path.exists(output): + os.makedirs(output) + + output_path = os.path.join(output, f"{class_name}.php") + with open(output_path, "w") as f: + f.write(generated) + + click.echo(f"Generated PHP bindings to {output_path}") + + +if __name__ == "__main__": + command() \ No newline at end of file diff --git a/stellar_contract_bindings/swift.py b/stellar_contract_bindings/swift.py new file mode 100644 index 0000000..7e96a1c --- /dev/null +++ b/stellar_contract_bindings/swift.py @@ -0,0 +1,962 @@ +import os +from typing import List + +import click +from jinja2 import Template +from stellar_sdk import __version__ as stellar_sdk_version, StrKey +from stellar_sdk import xdr + +from stellar_contract_bindings import __version__ as stellar_contract_bindings_version +from stellar_contract_bindings.utils import get_specs_by_contract_id + + +def is_swift_keyword(word: str) -> bool: + """Check if a word is a Swift reserved keyword.""" + return word in [ + "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", + "func", "import", "init", "inout", "internal", "let", "open", "operator", + "private", "protocol", "public", "static", "struct", "subscript", "typealias", + "var", "break", "case", "continue", "default", "defer", "do", "else", + "fallthrough", "for", "guard", "if", "in", "repeat", "return", "switch", + "where", "while", "as", "Any", "catch", "false", "is", "nil", "rethrows", + "super", "self", "Self", "throw", "throws", "true", "try", "#available", + "#colorLiteral", "#column", "#else", "#elseif", "#endif", "#file", + "#fileLiteral", "#function", "#if", "#imageLiteral", "#line", "#selector", + "#sourceLocation", "associativity", "convenience", "dynamic", "didSet", + "final", "get", "infix", "indirect", "lazy", "left", "mutating", "none", + "nonmutating", "optional", "override", "postfix", "precedence", "prefix", + "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet", + "actor", "async", "await", "nonisolated", "isolated", "some" + ] + + +def is_tuple_struct(entry: xdr.SCSpecUDTStructV0) -> bool: + """Check if a struct is a tuple struct (fields are numeric).""" + return all(f.name.isdigit() for f in entry.fields) + + +def snake_to_pascal(text: str) -> str: + """Convert snake_case to PascalCase.""" + parts = text.split("_") + return "".join(part.capitalize() for part in parts) + + +def snake_to_camel(text: str) -> str: + """Convert snake_case to camelCase.""" + parts = text.split("_") + return parts[0].lower() + "".join(part.capitalize() for part in parts[1:]) + + +def camel_to_snake(text: str) -> str: + """Convert CamelCase to snake_case.""" + result = text[0].lower() + for char in text[1:]: + if char.isupper(): + result += "_" + char.lower() + else: + result += char + return result + + +def escape_keyword(name: str) -> str: + """Escape Swift keywords by wrapping in backticks.""" + if is_swift_keyword(name): + return f"`{name}`" + return name + + +def prefixed_type_name(type_name: str, class_name: str) -> str: + """Prefix a type name with the class name to avoid conflicts. + + Args: + type_name: The original type name from the contract spec + class_name: The class name to use as prefix + + Returns: + The prefixed type name (e.g., "DataKey" -> "HelloContractDataKey") + """ + # Don't prefix primitive types or SDK types + if type_name in ['String', 'Bool', 'Data', 'SCAddressXDR', 'SCValXDR', + 'UInt32', 'UInt64', 'Int32', 'Int64', 'UInt128', 'Int128', 'UInt256', 'Int256']: + return type_name + return f"{class_name}{type_name}" + + +def to_swift_type(td: xdr.SCSpecTypeDef, nullable: bool = False, class_name: str = "") -> str: + """Convert Soroban type to Swift type.""" + t = td.type + nullable_suffix = "?" if nullable else "" + + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return f"SCValXDR{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"Bool{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return "Void" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"UInt32{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"Int32{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"UInt64{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"Int64{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"UInt64{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"UInt64{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + return f"String{nullable_suffix}" # BigInt as String + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + return f"String{nullable_suffix}" # BigInt as String + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + return f"String{nullable_suffix}" # BigInt as String + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + return f"String{nullable_suffix}" # BigInt as String + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"Data{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"String{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"String{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"SCAddressXDR{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"SCAddressXDR{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + return to_swift_type(td.option.value_type, nullable=True, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return to_swift_type(ok_t, nullable, class_name=class_name) + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + inner_type = to_swift_type(td.vec.element_type, class_name=class_name) + return f"[{inner_type}]{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + key_type = to_swift_type(td.map.key_type, class_name=class_name) + val_type = to_swift_type(td.map.value_type, class_name=class_name) + return f"[{key_type}: {val_type}]{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "Void" + types = [to_swift_type(t, class_name=class_name) for t in td.tuple.value_types] + return f"({', '.join(types)}){nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"Data{nullable_suffix}" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + udt_name = td.udt.name.decode() + return f"{prefixed_type_name(udt_name, class_name)}{nullable_suffix}" + raise ValueError(f"Unsupported SCValType: {t}") + + +def to_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "") -> str: + """Generate Swift code to convert a value to SCValXDR.""" + t = td.type + + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return name + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + return f"SCValXDR.bool({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"SCValXDR.void" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + return f"SCValXDR.u32({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + return f"SCValXDR.i32({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + return f"SCValXDR.u64({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + return f"SCValXDR.i64({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return f"SCValXDR.timepoint({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + return f"SCValXDR.duration({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + # Use SDK's built-in string to u128 conversion + return f"try SCValXDR.u128(stringValue: {name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + # Use SDK's built-in string to i128 conversion + return f"try SCValXDR.i128(stringValue: {name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + # Use SDK's built-in string to u256 conversion + return f"try SCValXDR.u256(stringValue: {name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + # Use SDK's built-in string to i256 conversion + return f"try SCValXDR.i256(stringValue: {name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + return f"SCValXDR.bytes({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + return f"SCValXDR.string({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + return f"SCValXDR.symbol({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + return f"SCValXDR.address({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + return f"SCValXDR.address({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + inner = to_scval(td.option.value_type, f"{name}!", class_name) + return f"({name} != nil ? {inner} : SCValXDR.void)" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + return NotImplementedError("SC_SPEC_TYPE_RESULT is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + element_conversion = to_scval(td.vec.element_type, "$0", class_name) + return f"SCValXDR.vec(try {name}.map {{ {element_conversion} }})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + key_conv = to_scval(td.map.key_type, "$0.key", class_name) + val_conv = to_scval(td.map.value_type, "$0.value", class_name) + return f"SCValXDR.map(try {name}.map {{ SCMapEntryXDR(key: {key_conv}, val: {val_conv}) }})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "SCValXDR.void" + conversions = [to_scval(td.tuple.value_types[i], f"{name}.{i}", class_name) for i in range(len(td.tuple.value_types))] + return f"SCValXDR.vec([{', '.join(conversions)}])" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + return f"SCValXDR.bytes({name})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + return f"try {name}.toSCVal()" + raise ValueError(f"Unsupported SCValType: {t}") + + +def from_scval(td: xdr.SCSpecTypeDef, name: str, class_name: str = "", throw_on_missing: bool = False) -> str: + """Generate Swift code to convert from SCValXDR to a Swift value. + + Args: + td: The type definition + name: The variable name + class_name: The class name for prefixing types + throw_on_missing: If True, throw errors instead of using default values + """ + t = td.type + if t == xdr.SCSpecType.SC_SPEC_TYPE_VAL: + return name + if t == xdr.SCSpecType.SC_SPEC_TYPE_BOOL: + if throw_on_missing: + return f"try {name}.bool ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid bool value\") }}()" + return f"{name}.bool ?? false" + if t == xdr.SCSpecType.SC_SPEC_TYPE_VOID: + return f"()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ERROR: + raise NotImplementedError("SC_SPEC_TYPE_ERROR is not supported") + if t == xdr.SCSpecType.SC_SPEC_TYPE_U32: + if throw_on_missing: + return f"try {name}.u32 ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid u32 value\") }}()" + return f"{name}.u32 ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I32: + if throw_on_missing: + return f"try {name}.i32 ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid i32 value\") }}()" + return f"{name}.i32 ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U64: + if throw_on_missing: + return f"try {name}.u64 ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid u64 value\") }}()" + return f"{name}.u64 ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I64: + if throw_on_missing: + return f"try {name}.i64 ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid i64 value\") }}()" + return f"{name}.i64 ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TIMEPOINT: + if throw_on_missing: + return f"try {name}.timepoint ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid timepoint value\") }}()" + return f"{name}.timepoint ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_DURATION: + if throw_on_missing: + return f"try {name}.duration ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid duration value\") }}()" + return f"{name}.duration ?? 0" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U128: + if throw_on_missing: + return f"try {name}.u128String ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid u128 value\") }}()" + return f"{name}.u128String ?? \"0\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I128: + if throw_on_missing: + return f"try {name}.i128String ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid i128 value\") }}()" + return f"{name}.i128String ?? \"0\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_U256: + if throw_on_missing: + return f"try {name}.u256String ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid u256 value\") }}()" + return f"{name}.u256String ?? \"0\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_I256: + if throw_on_missing: + return f"try {name}.i256String ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid i256 value\") }}()" + return f"{name}.i256String ?? \"0\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES: + if throw_on_missing: + return f"try {name}.bytes ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid bytes value\") }}()" + return f"{name}.bytes ?? Data()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_STRING: + if throw_on_missing: + return f"try {name}.string ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid string value\") }}()" + return f"{name}.string ?? \"\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL: + if throw_on_missing: + return f"try {name}.symbol ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid symbol value\") }}()" + return f"{name}.symbol ?? \"\"" + if t == xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS: + if throw_on_missing: + return f"try {name}.address ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid address value\") }}()" + return f"{name}.address!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS: + if throw_on_missing: + return f"try {name}.address ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid muxed address value\") }}()" + return f"{name}.address!" + if t == xdr.SCSpecType.SC_SPEC_TYPE_OPTION: + inner = from_scval(td.option.value_type, name, class_name, throw_on_missing) + # Use conditional to check for void + return f"(if case .void = {name} {{ nil }} else {{ {inner} }})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_RESULT: + ok_t = td.result.ok_type + return from_scval(ok_t, name, class_name, throw_on_missing) + if t == xdr.SCSpecType.SC_SPEC_TYPE_VEC: + element_conversion = from_scval(td.vec.element_type, "$0", class_name, throw_on_missing) + # Check if element conversion needs 'try' + if "try" in element_conversion: + return f"try {name}.vec?.compactMap {{ {element_conversion} }} ?? []" + else: + return f"{name}.vec?.map {{ {element_conversion} }} ?? []" + if t == xdr.SCSpecType.SC_SPEC_TYPE_MAP: + key_conv = from_scval(td.map.key_type, "$0.key", class_name, throw_on_missing) + val_conv = from_scval(td.map.value_type, "$0.val", class_name, throw_on_missing) + # Check if conversions need 'try' + if "try" in key_conv or "try" in val_conv: + return f"try Dictionary(uniqueKeysWithValues: {name}.map?.compactMap {{ ({key_conv}, {val_conv}) }} ?? [])" + else: + return f"Dictionary(uniqueKeysWithValues: {name}.map?.map {{ ({key_conv}, {val_conv}) }} ?? [])" + if t == xdr.SCSpecType.SC_SPEC_TYPE_TUPLE: + if len(td.tuple.value_types) == 0: + return "()" + vec_name = f"{name}.vec!" + conversions = [from_scval(td.tuple.value_types[i], f"{vec_name}[{i}]", class_name, throw_on_missing) for i in range(len(td.tuple.value_types))] + return f"({', '.join(conversions)})" + if t == xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N: + if throw_on_missing: + return f"try {name}.bytes ?? {{ throw {class_name}Error.conversionFailed(message: \"Missing or invalid bytes value\") }}()" + return f"{name}.bytes ?? Data()" + if t == xdr.SCSpecType.SC_SPEC_TYPE_UDT: + # UDT conversion always needs try as fromSCVal might throw + udt_name = td.udt.name.decode() + return f"try {prefixed_type_name(udt_name, class_name)}.fromSCVal({name})" + raise NotImplementedError(f"Unsupported SCValType: {t}") + + +def render_info(): + """Generate file header comment.""" + return f"""// +// This file was generated by stellar_contract_bindings v{stellar_contract_bindings_version} +// and stellar_sdk v{stellar_sdk_version}. +// +// @generated +// +""" + + +def render_imports(): + """Generate Swift import statements.""" + return """ +import Foundation +import stellarsdk +""" + + +def render_enum(entry: xdr.SCSpecUDTEnumV0, class_name: str): + """Generate Swift enum.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/// {{ entry.doc.decode() if entry.doc else 'Generated enum ' + type_name }} +public enum {{ type_name }}: UInt32, CaseIterable { + {%- for case in entry.cases %} + case {{ escape_keyword(case.name.decode()) }} = {{ case.value.uint32 }} + {%- endfor %} + + public func toSCVal() throws -> SCValXDR { + return .u32(self.rawValue) + } + + /// Converts an SCVal XDR value to a {{ type_name }} enum case + /// - Parameter val: The SCVal to convert + /// - Returns: The corresponding {{ type_name }} case + /// - Throws: {{ class_name }}Error.conversionFailed if the SCVal is not a u32 or the value doesn't match any case + public static func fromSCVal(_ val: SCValXDR) throws -> {{ type_name }} { + guard case .u32(let value) = val else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid SCVal type for {{ type_name }}") + } + guard let enumCase = {{ type_name }}(rawValue: value) else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid value for {{ type_name }}: \\(value)") + } + return enumCase + } +} +""" + return Template(template).render(entry=entry, type_name=type_name, escape_keyword=escape_keyword, class_name=class_name) + + +def render_error_enum(entry: xdr.SCSpecUDTErrorEnumV0, class_name: str): + """Generate Swift error enum.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + template = """ +/// {{ entry.doc.decode() if entry.doc else 'Generated error enum ' + type_name }} +public enum {{ type_name }}Error: UInt32, Error, CaseIterable { + {%- for case in entry.cases %} + case {{ escape_keyword(case.name.decode()) }} = {{ case.value.uint32 }} + {%- endfor %} + + public func toSCVal() throws -> SCValXDR { + return .u32(self.rawValue) + } + + /// Converts an SCVal XDR value to a {{ type_name }}Error enum case + /// - Parameter val: The SCVal to convert + /// - Returns: The corresponding {{ type_name }}Error case + /// - Throws: {{ class_name }}Error.conversionFailed if the SCVal is not a u32 or the value doesn't match any case + public static func fromSCVal(_ val: SCValXDR) throws -> {{ type_name }}Error { + guard case .u32(let value) = val else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid SCVal type for {{ type_name }}Error") + } + guard let errorCase = {{ type_name }}Error(rawValue: value) else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid value for {{ type_name }}Error: \\(value)") + } + return errorCase + } +} +""" + return Template(template).render(entry=entry, type_name=type_name, escape_keyword=escape_keyword, class_name=class_name) + + +def render_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + """Generate Swift struct.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_swift_type_bound(td, nullable=False): + return to_swift_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + # For struct fields, we want to throw on missing values to avoid force unwrapping + return from_scval(td, name, class_name, throw_on_missing=True) + + template = """ +/// {{ entry.doc.decode() if entry.doc else 'Generated struct ' + type_name }} +public struct {{ type_name }}: Codable { + {%- for field in entry.fields %} + public let {{ escape_keyword(field.name.decode()) }}: {{ to_swift_type(field.type) }} + {%- endfor %} + + public init( + {%- for field in entry.fields %} + {{ escape_keyword(field.name.decode()) }}: {{ to_swift_type(field.type) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ) { + {%- for field in entry.fields %} + self.{{ escape_keyword(field.name.decode()) }} = {{ escape_keyword(field.name.decode()) }} + {%- endfor %} + } + + public func toSCVal() throws -> SCValXDR { + var mapEntries: [SCMapEntryXDR] = [] + {%- for field in entry.fields %} + mapEntries.append(SCMapEntryXDR( + key: .symbol("{{ field.name.decode() }}"), + val: {{ to_scval(field.type, escape_keyword(field.name.decode())) }} + )) + {%- endfor %} + return .map(mapEntries) + } + + /// Converts an SCVal XDR value to a {{ type_name }} struct + /// - Parameter val: The SCVal to convert (must be a map) + /// - Returns: A new {{ type_name }} instance + /// - Throws: {{ class_name }}Error.conversionFailed if the SCVal is not a map or required fields are missing + public static func fromSCVal(_ val: SCValXDR) throws -> {{ type_name }} { + guard case .map(let mapEntries) = val else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid SCVal type for {{ type_name }}") + } + + var map: [String: SCValXDR] = [:] + for entry in mapEntries ?? [] { + if case .symbol(let key) = entry.key { + map[key] = entry.val + } + } + + {%- for field in entry.fields %} + guard let field_{{ loop.index0 }}_val = map["{{ field.name.decode() }}"] else { + throw {{ class_name }}Error.conversionFailed(message: "Missing required field '{{ field.name.decode() }}' in {{ type_name }}") + } + {%- endfor %} + + return {{ type_name }}( + {%- for field in entry.fields %} + {{ escape_keyword(field.name.decode()) }}: {{ from_scval(field.type, 'field_' ~ loop.index0 ~ '_val') }}{% if not loop.last %},{% endif %} + {%- endfor %} + ) + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + to_swift_type=to_swift_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + escape_keyword=escape_keyword, + class_name=class_name + ) + + +def render_tuple_struct(entry: xdr.SCSpecUDTStructV0, class_name: str): + """Generate Swift tuple struct.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_swift_type_bound(td, nullable=False): + return to_swift_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name, throw_on_missing=False) + + template = """ +/// {{ entry.doc.decode() if entry.doc else 'Generated tuple struct ' + type_name }} +public struct {{ type_name }}: Codable { + public let value: ({% for f in entry.fields %}{{ to_swift_type(f.type) }}{% if not loop.last %}, {% endif %}{% endfor %}) + + public init(value: ({% for f in entry.fields %}{{ to_swift_type(f.type) }}{% if not loop.last %}, {% endif %}{% endfor %})) { + self.value = value + } + + public func toSCVal() throws -> SCValXDR { + return .vec([ + {%- for f in entry.fields %} + {{ to_scval(f.type, 'value.' ~ f.name.decode()) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]) + } + + /// Converts an SCVal XDR value to a {{ type_name }} tuple struct + /// - Parameter val: The SCVal to convert (must be a vec) + /// - Returns: A new {{ type_name }} instance + /// - Throws: {{ class_name }}Error.conversionFailed if the SCVal is not a vec or has wrong number of elements + public static func fromSCVal(_ val: SCValXDR) throws -> {{ type_name }} { + guard case .vec(let elements) = val, let elements = elements else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid SCVal type for {{ type_name }}") + } + + return {{ type_name }}(value: ( + {%- for f in entry.fields %} + {{ from_scval(f.type, 'elements[' ~ f.name.decode() ~ ']') }}{% if not loop.last %},{% endif %} + {%- endfor %} + )) + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + to_swift_type=to_swift_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + class_name=class_name + ) + + +def render_union(entry: xdr.SCSpecUDTUnionV0, class_name: str): + """Generate Swift enum for union.""" + type_name = prefixed_type_name(entry.name.decode(), class_name) + + # Create wrapper functions with class_name bound + def to_swift_type_bound(td, nullable=False): + return to_swift_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + # For union conversions, we want to throw on missing values instead of using defaults/force unwrapping + return from_scval(td, name, class_name, throw_on_missing=True) + + template = """ +/// {{ entry.doc.decode() if entry.doc else 'Generated union ' + type_name }} +public enum {{ type_name }} { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case {{ escape_keyword(case.void_case.name.decode()) }} + {%- else %} + {%- if len(case.tuple_case.type) == 1 %} + case {{ escape_keyword(case.tuple_case.name.decode()) }}({{ to_swift_type(case.tuple_case.type[0]) }}) + {%- else %} + case {{ escape_keyword(case.tuple_case.name.decode()) }}({% for t in case.tuple_case.type %}{{ to_swift_type(t) }}{% if not loop.last %}, {% endif %}{% endfor %}) + {%- endif %} + {%- endif %} + {%- endfor %} + + public func toSCVal() throws -> SCValXDR { + switch self { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case .{{ escape_keyword(case.void_case.name.decode()) }}: + return .vec([.symbol("{{ case.void_case.name.decode() }}")]) + {%- else %} + {%- if len(case.tuple_case.type) == 1 %} + case .{{ escape_keyword(case.tuple_case.name.decode()) }}(let value): + return .vec([ + .symbol("{{ case.tuple_case.name.decode() }}"), + {{ to_scval(case.tuple_case.type[0], 'value') }} + ]) + {%- else %} + case .{{ escape_keyword(case.tuple_case.name.decode()) }}({% for i in range(len(case.tuple_case.type)) %}let value{{ i }}{% if not loop.last %}, {% endif %}{% endfor %}): + return .vec([ + .symbol("{{ case.tuple_case.name.decode() }}"), + {%- for i, t in enumerate(case.tuple_case.type) %} + {{ to_scval(t, 'value' ~ i|string) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ]) + {%- endif %} + {%- endif %} + {%- endfor %} + } + } + + /// Converts an SCVal XDR value to a {{ type_name }} union + /// - Parameter val: The SCVal to convert (must be a vec with discriminant as first element) + /// - Returns: The corresponding {{ type_name }} case + /// - Throws: {{ class_name }}Error.conversionFailed if: + /// - The SCVal is not a vec + /// - The vec is empty or missing the discriminant + /// - The discriminant is not a symbol or is unknown + /// - The number of elements doesn't match the expected case + public static func fromSCVal(_ val: SCValXDR) throws -> {{ type_name }} { + guard case .vec(let vec) = val, let vec = vec, vec.count >= 1 else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid union value: expected vec with at least 1 element") + } + + guard case .symbol(let kind) = vec[0] else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid union discriminant") + } + + switch kind { + {%- for case in entry.cases %} + {%- if case.kind == xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0 %} + case "{{ case.void_case.name.decode() }}": + return .{{ escape_keyword(case.void_case.name.decode()) }} + {%- else %} + case "{{ case.tuple_case.name.decode() }}": + {%- if len(case.tuple_case.type) == 1 %} + guard vec.count == 2 else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid union value for {{ case.tuple_case.name.decode() }}: expected 2 elements") + } + return .{{ escape_keyword(case.tuple_case.name.decode()) }}({{ from_scval(case.tuple_case.type[0], 'vec[1]') }}) + {%- else %} + guard vec.count == {{ len(case.tuple_case.type) + 1 }} else { + throw {{ class_name }}Error.conversionFailed(message: "Invalid union value for {{ case.tuple_case.name.decode() }}: expected {{ len(case.tuple_case.type) + 1 }} elements") + } + return .{{ escape_keyword(case.tuple_case.name.decode()) }}( + {%- for i, t in enumerate(case.tuple_case.type) %} + {{ from_scval(t, 'vec[' ~ (i + 1)|string ~ ']') }}{% if not loop.last %},{% endif %} + {%- endfor %} + ) + {%- endif %} + {%- endif %} + {%- endfor %} + default: + throw {{ class_name }}Error.conversionFailed(message: "Unknown union kind: \\(kind)") + } + } +} +""" + return Template(template).render( + entry=entry, + type_name=type_name, + to_swift_type=to_swift_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + xdr=xdr, + len=len, + class_name=class_name, + enumerate=enumerate, + escape_keyword=escape_keyword + ) + + +def render_client(entries: List[xdr.SCSpecFunctionV0], class_name: str): + """Generate Swift client class.""" + template = ''' +/// Generated contract client for {{ class_name }} +public class {{ class_name }} { + + /// The underlying SorobanClient instance + private let client: SorobanClient + + /// Private constructor that wraps a SorobanClient + private init(client: SorobanClient) { + self.client = client + } + + /// Creates a new {{ class_name }} for the given client options + /// - Parameter options: Client options for the contract + /// - Returns: A new {{ class_name }} instance + public static func forClientOptions(options: ClientOptions) async throws -> {{ class_name }} { + let client = try await SorobanClient.forClientOptions(options: options) + return {{ class_name }}(client: client) + } + + /// Gets the contract ID + /// - Returns: The contract ID as a string + public var contractId: String { + return client.contractId + } + + /// Gets the spec entries of the contract + /// - Returns: Array of SCSpecEntryXDR + public var specEntries: [SCSpecEntryXDR] { + return client.specEntries + } + + /// Gets the method names of the contract + /// - Returns: Array of method names + public var methodNames: [String] { + return client.methodNames + } + {%- for entry in entries %} + + /// {{ entry.doc.decode() if entry.doc else 'Invoke the ' + entry.name.sc_symbol.decode() + ' method' }} + /// {%- for param in entry.inputs %} + /// - Parameter {{ escape_keyword(param.name.decode()) }}: {{ param.doc.decode() if param.doc else to_swift_type(param.type) }} + {%- endfor %} + /// - Parameter methodOptions: Options for transaction (optional) + /// - Parameter force: Force signing and sending even if it's a read call (default: false) + /// - Returns: {{ parse_result_type(entry.outputs) }} + public func {{ snake_to_camel(entry.name.sc_symbol.decode()) }}( + {%- for param in entry.inputs %} + {{ escape_keyword(param.name.decode()) }}: {{ to_swift_type(param.type) }}, + {%- endfor %} + methodOptions: MethodOptions? = nil, + force: Bool = false + ) async throws{{ return_type_hint(entry.outputs) }} { + let args: [SCValXDR] = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, escape_keyword(param.name.decode())) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ] + + let result = try await client.invokeMethod( + name: "{{ entry.name.sc_symbol.decode() }}", + args: args, + force: force, + methodOptions: methodOptions + ) + {%- if len(entry.outputs) > 0 %} + return {{ parse_result_conversion(entry.outputs) }} + {%- endif %} + } + + /// Build an AssembledTransaction for the {{ entry.name.sc_symbol.decode() }} method. + /// This is useful if you need to manipulate the transaction before signing and sending. + /// {%- for param in entry.inputs %} + /// - Parameter {{ escape_keyword(param.name.decode()) }}: {{ param.doc.decode() if param.doc else to_swift_type(param.type) }} + {%- endfor %} + /// - Parameter methodOptions: Options for transaction (optional) + /// - Returns: AssembledTransaction + public func build{{ snake_to_pascal(entry.name.sc_symbol.decode()) }}Tx( + {%- for param in entry.inputs %} + {{ escape_keyword(param.name.decode()) }}: {{ to_swift_type(param.type) }}, + {%- endfor %} + methodOptions: MethodOptions? = nil + ) async throws -> AssembledTransaction { + let args: [SCValXDR] = [ + {%- for param in entry.inputs %} + {{ to_scval(param.type, escape_keyword(param.name.decode())) }}{% if not loop.last %},{% endif %} + {%- endfor %} + ] + + return try await client.buildInvokeMethodTx( + name: "{{ entry.name.sc_symbol.decode() }}", + args: args, + methodOptions: methodOptions + ) + } + {%- endfor %} +} + +// MARK: - Error Types + +/// Custom errors for {{ class_name }} operations +/// These errors are thrown when: +/// - Converting between Swift types and SCVal XDR types fails +/// - Type validation fails during deserialization +/// - Contract invocation fails +enum {{ class_name }}Error: Error { + /// Thrown when converting from SCVal to Swift types fails. + /// This typically happens when: + /// - The SCVal type doesn't match the expected type + /// - Required fields are missing in structs + /// - Union discriminants are unknown + /// - Enum values are out of range + case conversionFailed(message: String) + + /// Thrown when a contract method invocation fails. + /// This can happen when: + /// - The contract returns an error + /// - The transaction fails to submit + /// - Network or RPC errors occur + case invokeFailed(message: String) +} +''' + + def parse_result_type(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "Void" + elif len(output) == 1: + return to_swift_type(output[0], class_name=class_name) + else: + types = [to_swift_type(t, class_name=class_name) for t in output] + return f"({', '.join(types)})" + + def return_type_hint(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "" + result_type = parse_result_type(output) + return f" -> {result_type}" + + def parse_result_conversion(output: List[xdr.SCSpecTypeDef]): + if len(output) == 0: + return "" + elif len(output) == 1: + # Use throw_on_missing=True for method return values + conversion = from_scval(output[0], "result", class_name, throw_on_missing=True) + # Only wrap in try if the conversion actually needs it + if "try" in conversion: + return conversion + else: + return conversion + else: + # Use throw_on_missing=True for method return values + vec_conversions = [from_scval(output[i], f"resultVec[{i}]", class_name, throw_on_missing=True) for i in range(len(output))] + # Need to use the contract-specific error type here + # We'll pass it through template rendering + return f"""guard let resultVec = result.vec else {{ + throw {{{{ class_name }}}}Error.conversionFailed(message: "Expected tuple result") + }} + return ({', '.join(vec_conversions)})""" + + # Create wrapper functions with class_name bound + def to_swift_type_bound(td, nullable=False): + return to_swift_type(td, nullable, class_name) + + def to_scval_bound(td, name): + return to_scval(td, name, class_name) + + def from_scval_bound(td, name): + return from_scval(td, name, class_name, throw_on_missing=False) + + return Template(template).render( + entries=entries, + class_name=class_name, + to_swift_type=to_swift_type_bound, + to_scval=to_scval_bound, + from_scval=from_scval_bound, + parse_result_type=parse_result_type, + return_type_hint=return_type_hint, + parse_result_conversion=parse_result_conversion, + escape_keyword=escape_keyword, + snake_to_camel=snake_to_camel, + snake_to_pascal=snake_to_pascal, + len=len + ) + + +def generate_binding(specs: List[xdr.SCSpecEntry], class_name: str = "ContractClient") -> str: + """Generate complete Swift binding file.""" + generated = [] + generated.append(render_info()) + generated.append(render_imports()) + + # Generate types + for spec in specs: + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0: + generated.append(render_enum(spec.udt_enum_v0, class_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0: + generated.append(render_error_enum(spec.udt_error_enum_v0, class_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0: + if is_tuple_struct(spec.udt_struct_v0): + generated.append(render_tuple_struct(spec.udt_struct_v0, class_name)) + else: + generated.append(render_struct(spec.udt_struct_v0, class_name)) + elif spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0: + generated.append(render_union(spec.udt_union_v0, class_name)) + + # Generate client + function_specs: List[xdr.SCSpecFunctionV0] = [ + spec.function_v0 + for spec in specs + if spec.kind == xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0 + and not spec.function_v0.name.sc_symbol.decode().startswith("__") + ] + + if function_specs: + generated.append(render_client(function_specs, class_name)) + + return "\n".join(generated) + + +@click.command(name="swift") +@click.option( + "--contract-id", required=True, help="The contract ID to generate bindings for" +) +@click.option( + "--rpc-url", default="https://mainnet.sorobanrpc.com", help="Soroban RPC URL" +) +@click.option( + "--output", + default=None, + help="Output directory for generated bindings, defaults to current directory", +) +@click.option( + "--class-name", + default="ContractClient", + help="Name for the generated client class", +) +def command(contract_id: str, rpc_url: str, output: str, class_name: str): + """Generate Swift bindings for a Soroban contract""" + if not StrKey.is_valid_contract(contract_id): + click.echo(f"Invalid contract ID: {contract_id}", err=True) + raise click.Abort() + + # Use current directory if output is not specified + if output is None: + output = os.getcwd() + + try: + specs = get_specs_by_contract_id(contract_id, rpc_url) + except Exception as e: + click.echo(f"Get contract specs failed: {e}", err=True) + raise click.Abort() + + click.echo("Generating Swift bindings") + generated = generate_binding(specs, class_name=class_name) + + # Check if output is a file or directory + if output.endswith('.swift'): + # It's a file path + output_path = output + output_dir = os.path.dirname(output_path) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + else: + # It's a directory path + if not os.path.exists(output): + os.makedirs(output) + output_path = os.path.join(output, f"{class_name}.swift") + + with open(output_path, "w") as f: + f.write(generated) + + click.echo(f"Generated Swift bindings to {output_path}") + + +if __name__ == "__main__": + command() \ No newline at end of file diff --git a/tests/test_flutter_generator.py b/tests/test_flutter_generator.py new file mode 100644 index 0000000..96b10a0 --- /dev/null +++ b/tests/test_flutter_generator.py @@ -0,0 +1,356 @@ +import tempfile +import os +from pathlib import Path + +import pytest +from stellar_sdk import xdr + +from stellar_contract_bindings.flutter import ( + generate_binding, + to_dart_type, + to_scval, + from_scval, + snake_to_camel, + is_keywords, + render_enum, + render_struct, + render_tuple_struct, + render_union, + command, +) +from stellar_contract_bindings.metadata import parse_contract_metadata + + +class TestFlutterGenerator: + @classmethod + def setup_class(cls): + # Load test contract specs from wasm file + wasm_file = Path(__file__).parent / "contracts" / "target" / "wasm32-unknown-unknown" / "release" / "python.wasm" + if wasm_file.exists(): + with open(wasm_file, "rb") as f: + wasm = f.read() + cls.specs = parse_contract_metadata(wasm).spec + else: + cls.specs = [] + + def test_snake_to_camel(self): + assert snake_to_camel("hello_world") == "helloWorld" + assert snake_to_camel("test_case", first_letter_lower=False) == "TestCase" + assert snake_to_camel("simple") == "simple" + assert snake_to_camel("a_b_c_d") == "aBCD" + + def test_is_keywords(self): + assert is_keywords("class") == True + assert is_keywords("if") == True + assert is_keywords("async") == True + assert is_keywords("hello") == False + assert is_keywords("world") == False + + def test_to_dart_type(self): + # Test basic types + bool_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert to_dart_type(bool_type) == "bool" + assert to_dart_type(bool_type, nullable=True) == "bool?" + + u32_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert to_dart_type(u32_type) == "int" + + u64_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U64) + assert to_dart_type(u64_type) == "int" # U64 is int in Flutter, not BigInt + + string_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert to_dart_type(string_type) == "String" + + bytes_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + assert to_dart_type(bytes_type) == "Uint8List" + + # Test option type + option_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_OPTION) + option_type.option = xdr.SCSpecTypeOption(bool_type) + assert to_dart_type(option_type) == "bool?" + + # Test vector type + vec_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VEC) + vec_type.vec = xdr.SCSpecTypeVec(u32_type) + assert to_dart_type(vec_type) == "List" + + # Test map type + map_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_MAP) + map_type.map = xdr.SCSpecTypeMap(string_type, u32_type) + assert to_dart_type(map_type) == "Map" + + def test_to_scval(self): + bool_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert to_scval(bool_type, "value") == "XdrSCVal.forBool(value)" + + u32_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert to_scval(u32_type, "value") == "XdrSCVal.forU32(value)" + + string_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert to_scval(string_type, "value") == "XdrSCVal.forString(value)" + + bytes_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + assert to_scval(bytes_type, "value") == "XdrSCVal.forBytes(value)" + + def test_from_scval(self): + bool_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert from_scval(bool_type, "val") == "val.b!" + + u32_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert from_scval(u32_type, "val") == "val.u32!.uint32" + + string_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert from_scval(string_type, "val") == "val.str!" # Flutter SDK now returns string directly + + def test_render_enum(self): + # Create a mock enum entry + case1 = xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"First", + value=xdr.Uint32(0) + ) + case2 = xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Second", + value=xdr.Uint32(1) + ) + enum_entry = xdr.SCSpecUDTEnumV0( + doc=b"Test enum", + lib=b"", + name=b"TestEnum", + cases=[case1, case2] + ) + + result = render_enum(enum_entry, "TestContract") + + assert "enum TestContractTestEnum {" in result + assert "First(0)," in result + assert "Second(1);" in result + assert "factory TestContractTestEnum.fromValue(int value)" in result + assert "XdrSCVal toScVal()" in result + assert "static TestContractTestEnum fromScVal(XdrSCVal val)" in result + + def test_render_struct(self): + # Create a mock struct entry + field1_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + field2_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + + field1 = xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"field_one", + type=field1_type + ) + field2 = xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"field_two", + type=field2_type + ) + + struct_entry = xdr.SCSpecUDTStructV0( + doc=b"Test struct", + lib=b"", + name=b"TestStruct", + fields=[field1, field2] + ) + + result = render_struct(struct_entry, "TestContract") + + assert "class TestContractTestStruct {" in result + assert "final int fieldOne;" in result + assert "final bool fieldTwo;" in result + assert "const TestContractTestStruct({" in result + assert "required this.fieldOne," in result + assert "required this.fieldTwo," in result + assert "XdrSCVal toScVal()" in result + assert "factory TestContractTestStruct.fromScVal(XdrSCVal val)" in result + + def test_generate_binding_basic(self): + if not self.specs: + pytest.skip("No test contract specs available") + + generated = generate_binding(self.specs, "TestContract") + + # Check that the generated code contains expected elements + assert "// This file was generated by stellar_contract_bindings" in generated + assert "import 'dart:typed_data';" in generated + assert "import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart';" in generated + assert "class TestContract {" in generated + assert "static Future forContractId(" in generated + + def test_generate_binding_with_complex_types(self): + # Create a more complex spec with various types + # Enum + enum_case1 = xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Option1", + value=xdr.Uint32(0) + ) + enum_case2 = xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Option2", + value=xdr.Uint32(1) + ) + enum_entry = xdr.SCSpecUDTEnumV0( + doc=b"", + lib=b"", + name=b"Status", + cases=[enum_case1, enum_case2] + ) + + enum_spec = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0) + enum_spec.udt_enum_v0 = enum_entry + + # Struct + field_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + field = xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"name", + type=field_type + ) + struct_entry = xdr.SCSpecUDTStructV0( + doc=b"", + lib=b"", + name=b"Person", + fields=[field] + ) + + struct_spec = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0) + struct_spec.udt_struct_v0 = struct_entry + + # Function + param_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + param = xdr.SCSpecFunctionInputV0( + doc=b"", + name=b"input", + type=param_type + ) + + return_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + + func_name = xdr.SCSymbol(sc_symbol=b"test_function") + func_entry = xdr.SCSpecFunctionV0( + doc=b"", + name=func_name, + inputs=[param], + outputs=[return_type] + ) + + func_spec = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + func_spec.function_v0 = func_entry + + specs = [enum_spec, struct_spec, func_spec] + generated = generate_binding(specs, "TestContract") + + # Check enum generation with prefixed names + assert "enum TestContractStatus {" in generated + assert "Option1(0)," in generated + assert "Option2(1);" in generated + + # Check struct generation with prefixed names + assert "class TestContractPerson {" in generated + assert "final String name;" in generated + + # Check client generation + assert "class TestContract {" in generated + assert "Future testFunction({" in generated + assert "required String input," in generated + + def test_command_function(self): + """Test the CLI command function with mocked dependencies""" + # This is a basic test to ensure the command function can be called + # In a real scenario, you'd want to mock the dependencies + try: + # Test invalid contract ID + from click.testing import CliRunner + runner = CliRunner() + + result = runner.invoke(command, ['--contract-id', 'invalid']) + assert result.exit_code != 0 + assert "Invalid contract ID" in result.output + except ImportError: + pytest.skip("Click testing not available") + + def test_tuple_struct_rendering(self): + # Create a tuple struct (fields with numeric names) + field1_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + field2_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + + field1 = xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"0", # Numeric name indicates tuple + type=field1_type + ) + field2 = xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"1", + type=field2_type + ) + + tuple_struct_entry = xdr.SCSpecUDTStructV0( + doc=b"", + lib=b"", + name=b"TupleStruct", + fields=[field1, field2] + ) + + result = render_tuple_struct(tuple_struct_entry, "TestContract") + + assert "class TestContractTupleStruct {" in result + assert "final (int, bool) value;" in result + assert "const TestContractTupleStruct(this.value);" in result + assert "XdrSCVal toScVal()" in result + assert "factory TestContractTupleStruct.fromScVal(XdrSCVal val)" in result + + def test_union_rendering(self): + # Create a union with void and tuple cases + void_case = xdr.SCSpecUDTUnionCaseV0(xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0) + void_case.void_case = xdr.SCSpecUDTUnionCaseVoidV0( + doc=b"", + name=b"None" + ) + + tuple_case = xdr.SCSpecUDTUnionCaseV0(xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0) + tuple_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + tuple_case.tuple_case = xdr.SCSpecUDTUnionCaseTupleV0( + doc=b"", + name=b"Some", + type=[tuple_type] + ) + + union_entry = xdr.SCSpecUDTUnionV0( + doc=b"", + lib=b"", + name=b"Option", + cases=[void_case, tuple_case] + ) + + result = render_union(union_entry, "TestContract") + + assert "enum TestContractOptionKind {" in result + assert "None(" in result + assert "Some(" in result + assert "class TestContractOption {" in result + assert "final TestContractOptionKind kind;" in result + assert "factory TestContractOption.none()" in result + assert "factory TestContractOption.some(int value)" in result + + def test_file_output(self): + """Test that generated code can be written to a file""" + if not self.specs: + pytest.skip("No test contract specs available") + + with tempfile.TemporaryDirectory() as tmpdir: + generated = generate_binding(self.specs, "TestContract") + + output_path = os.path.join(tmpdir, "test_contract_client.dart") + with open(output_path, "w") as f: + f.write(generated) + + # Verify file was created and contains expected content + assert os.path.exists(output_path) + + with open(output_path, "r") as f: + content = f.read() + + assert "class TestContract {" in content + assert "import 'dart:typed_data';" in content \ No newline at end of file diff --git a/tests/test_php_generator.py b/tests/test_php_generator.py new file mode 100644 index 0000000..f243755 --- /dev/null +++ b/tests/test_php_generator.py @@ -0,0 +1,504 @@ +import unittest +from stellar_sdk import xdr +from stellar_contract_bindings.php import ( + is_php_keyword, + is_tuple_struct, + snake_to_pascal, + snake_to_camel, + camel_to_snake, + escape_keyword, + to_php_type, + to_scval, + from_scval, + render_enum, + render_struct, + render_tuple_struct, + generate_binding, +) + + +class TestPHPGenerator(unittest.TestCase): + + def test_is_php_keyword(self): + """Test PHP keyword detection.""" + self.assertTrue(is_php_keyword("class")) + self.assertTrue(is_php_keyword("function")) + self.assertTrue(is_php_keyword("return")) + self.assertTrue(is_php_keyword("if")) + self.assertFalse(is_php_keyword("myVariable")) + self.assertFalse(is_php_keyword("someMethod")) + + def test_snake_to_pascal(self): + """Test snake_case to PascalCase conversion.""" + self.assertEqual(snake_to_pascal("hello_world"), "HelloWorld") + self.assertEqual(snake_to_pascal("my_contract_method"), "MyContractMethod") + self.assertEqual(snake_to_pascal("single"), "Single") + self.assertEqual(snake_to_pascal("a_b_c_d"), "ABCD") + + def test_snake_to_camel(self): + """Test snake_case to camelCase conversion.""" + self.assertEqual(snake_to_camel("hello_world"), "helloWorld") + self.assertEqual(snake_to_camel("my_contract_method"), "myContractMethod") + self.assertEqual(snake_to_camel("single"), "single") + self.assertEqual(snake_to_camel("a_b_c_d"), "aBCD") + + def test_camel_to_snake(self): + """Test CamelCase to snake_case conversion.""" + self.assertEqual(camel_to_snake("HelloWorld"), "hello_world") + self.assertEqual(camel_to_snake("MyContractMethod"), "my_contract_method") + self.assertEqual(camel_to_snake("Single"), "single") + self.assertEqual(camel_to_snake("XMLHttpRequest"), "x_m_l_http_request") + + def test_escape_keyword(self): + """Test PHP keyword escaping.""" + self.assertEqual(escape_keyword("class"), "class_") + self.assertEqual(escape_keyword("function"), "function_") + self.assertEqual(escape_keyword("myVariable"), "myVariable") + self.assertEqual(escape_keyword("return", "property"), "return_") + + def test_to_php_type_primitives(self): + """Test conversion of primitive Soroban types to PHP types.""" + # Boolean + td_bool = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + self.assertEqual(to_php_type(td_bool), "bool") + self.assertEqual(to_php_type(td_bool, nullable=True), "?bool") + + # Integers + td_u32 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + self.assertEqual(to_php_type(td_u32), "int") + + td_i64 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_I64) + self.assertEqual(to_php_type(td_i64), "int") + + # Big integers (use string in PHP) + td_u128 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U128) + self.assertEqual(to_php_type(td_u128), "string") + + # String types + td_string = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + self.assertEqual(to_php_type(td_string), "string") + + td_symbol = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + self.assertEqual(to_php_type(td_symbol), "string") + + # Bytes + td_bytes = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + self.assertEqual(to_php_type(td_bytes), "string") + + # Address + td_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + self.assertEqual(to_php_type(td_address), "Address") + + # Muxed Address (same as Address in PHP) + td_muxed_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS) + self.assertEqual(to_php_type(td_muxed_address), "Address") + + # Void + td_void = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VOID) + self.assertEqual(to_php_type(td_void), "void") + + def test_to_php_type_complex(self): + """Test conversion of complex Soroban types to PHP types.""" + # Vector + td_u32 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + td_vec = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VEC) + td_vec.vec = xdr.SCSpecTypeVec(element_type=td_u32) + self.assertEqual(to_php_type(td_vec), "array") + + # Map + td_string = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + td_map = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_MAP) + td_map.map = xdr.SCSpecTypeMap(key_type=td_string, value_type=td_u32) + self.assertEqual(to_php_type(td_map), "array") + + # Option + td_option = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_OPTION) + td_option.option = xdr.SCSpecTypeOption(value_type=td_u32) + self.assertEqual(to_php_type(td_option), "?int") + + # UDT + td_udt = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_UDT) + td_udt.udt = xdr.SCSpecTypeUDT(name=b"MyContract") + self.assertEqual(to_php_type(td_udt), "MyContract") + + def test_to_scval_primitives(self): + """Test conversion to XdrSCVal for primitive types.""" + # Boolean + td_bool = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + self.assertEqual(to_scval(td_bool, "myBool"), "XdrSCVal::forBool($myBool)") + + # Integer types + td_u32 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + self.assertEqual(to_scval(td_u32, "myInt"), "XdrSCVal::forU32($myInt)") + + td_i64 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_I64) + self.assertEqual(to_scval(td_i64, "myLong"), "XdrSCVal::forI64($myLong)") + + # String + td_string = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + self.assertEqual(to_scval(td_string, "myString"), "XdrSCVal::forString($myString)") + + # Symbol + td_symbol = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + self.assertEqual(to_scval(td_symbol, "mySymbol"), "XdrSCVal::forSymbol($mySymbol)") + + # Address + td_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + self.assertEqual(to_scval(td_address, "myAddress"), "$myAddress->toXdrSCVal()") + + # Muxed Address + td_muxed_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS) + self.assertEqual(to_scval(td_muxed_address, "myMuxedAddress"), "$myMuxedAddress->toXdrSCVal()") + + # Void + td_void = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VOID) + self.assertEqual(to_scval(td_void, "ignored"), "XdrSCVal::forVoid()") + + def test_from_scval_primitives(self): + """Test conversion from XdrSCVal for primitive types.""" + # Boolean + td_bool = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + self.assertEqual(from_scval(td_bool, "val"), "$val->b") + + # Integer types + td_u32 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + self.assertEqual(from_scval(td_u32, "val"), "$val->u32") + + td_i64 = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_I64) + self.assertEqual(from_scval(td_i64, "val"), "$val->i64") + + # String + td_string = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + self.assertEqual(from_scval(td_string, "val"), "$val->str") + + # Symbol + td_symbol = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + self.assertEqual(from_scval(td_symbol, "val"), "$val->sym") + + # Bytes + td_bytes = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + self.assertEqual(from_scval(td_bytes, "val"), "$val->bytes->getValue()") + + # Bytes_N + td_bytes_n = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N) + td_bytes_n.bytes_n = xdr.SCSpecTypeBytesN(n=xdr.Uint32(32)) + self.assertEqual(from_scval(td_bytes_n, "val"), "$val->bytes->getValue()") + + # Address + td_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + self.assertEqual(from_scval(td_address, "val"), "Address::fromXdrSCVal($val)") + + # Muxed Address (same as Address in PHP) + td_muxed_address = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS) + self.assertEqual(from_scval(td_muxed_address, "val"), "Address::fromXdrSCVal($val)") + + # Void + td_void = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VOID) + self.assertEqual(from_scval(td_void, "val"), "null") + + def test_is_tuple_struct(self): + """Test tuple struct detection.""" + # Create a regular struct + regular_struct = xdr.SCSpecUDTStructV0( + doc=b"", + lib=b"", + name=b"RegularStruct", + fields=[ + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"field1", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"field2", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ), + ] + ) + self.assertFalse(is_tuple_struct(regular_struct)) + + # Create a tuple struct + tuple_struct = xdr.SCSpecUDTStructV0( + doc=b"", + lib=b"", + name=b"TupleStruct", + fields=[ + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"0", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"1", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ), + ] + ) + self.assertTrue(is_tuple_struct(tuple_struct)) + + def test_render_enum(self): + """Test enum rendering.""" + enum_entry = xdr.SCSpecUDTEnumV0( + doc=b"Test enum", + lib=b"", + name=b"Color", + cases=[ + xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Red", + value=xdr.Uint32(0) + ), + xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Green", + value=xdr.Uint32(1) + ), + xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Blue", + value=xdr.Uint32(2) + ), + ] + ) + + result = render_enum(enum_entry, "TestContract") + self.assertIn("enum TestContractColor: int", result) + self.assertIn("case Red = 0;", result) + self.assertIn("case Green = 1;", result) + self.assertIn("case Blue = 2;", result) + self.assertIn("public function toSCVal(): XdrSCVal", result) + self.assertIn("public static function fromSCVal(XdrSCVal $val): self", result) + + def test_render_struct(self): + """Test struct rendering.""" + struct_entry = xdr.SCSpecUDTStructV0( + doc=b"Test struct", + lib=b"", + name=b"Person", + fields=[ + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"name", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ), + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"age", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + ] + ) + + result = render_struct(struct_entry, "TestContract") + self.assertIn("class TestContractPerson", result) + self.assertIn("public string $name;", result) + self.assertIn("public int $age;", result) + self.assertIn("public function __construct(", result) + self.assertIn("public function toSCVal(): XdrSCVal", result) + self.assertIn("public static function fromSCVal(XdrSCVal $val): self", result) + + def test_generate_binding(self): + """Test complete binding generation.""" + # Create a simple spec with an enum and a function + enum_spec = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0) + enum_spec.udt_enum_v0 = xdr.SCSpecUDTEnumV0( + doc=b"", + lib=b"", + name=b"Status", + cases=[ + xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Active", + value=xdr.Uint32(0) + ), + xdr.SCSpecUDTEnumCaseV0( + doc=b"", + name=b"Inactive", + value=xdr.Uint32(1) + ), + ] + ) + + function_spec = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + function_spec.function_v0 = xdr.SCSpecFunctionV0( + doc=b"Get status", + name=xdr.SCSymbol(sc_symbol=b"get_status"), + inputs=[], + outputs=[xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_UDT)] + ) + function_spec.function_v0.outputs[0].udt = xdr.SCSpecTypeUDT(name=b"Status") + + specs = [enum_spec, function_spec] + + result = generate_binding(specs, namespace="Test", contract_name="TestContract") + + self.assertIn("namespace Test;", result) + self.assertIn("enum TestContractStatus: int", result) + self.assertIn("class TestContract", result) + self.assertIn("private SorobanClient $client;", result) + self.assertIn("public static function forClientOptions(ClientOptions $options): self", result) + self.assertIn("public function getStatus(", result) + + def test_return_type_hints(self): + """Test that functions have proper return type hints.""" + # Test function with void return + void_function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + void_function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"do_something"), + inputs=[], + outputs=[] # No outputs means void return + ) + + # Test function with int return + int_function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + int_function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"get_count"), + inputs=[], + outputs=[xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32)] + ) + + # Test function with string return (i128) + bigint_function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + bigint_function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"get_balance"), + inputs=[], + outputs=[xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_I128)] + ) + + # Test function with array return (vec type) + vec_type = xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_VEC) + vec_type.vec = xdr.SCSpecTypeVec(element_type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32)) + array_function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + array_function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"get_list"), + inputs=[], + outputs=[vec_type] + ) + + specs = [void_function, int_function, bigint_function, array_function] + result = generate_binding(specs, namespace="Test", contract_name="TestContract") + + # Check return type hints are present + self.assertIn("public function doSomething(\n ?MethodOptions $methodOptions = null\n ): void {", result) + self.assertIn("public function getCount(\n ?MethodOptions $methodOptions = null\n ): int {", result) + self.assertIn("public function getBalance(\n ?MethodOptions $methodOptions = null\n ): string {", result) + self.assertIn("public function getList(\n ?MethodOptions $methodOptions = null\n ): array {", result) + + def test_php_keyword_escaping_in_struct(self): + """Test that PHP keywords are properly escaped in struct fields.""" + struct_entry = xdr.SCSpecUDTStructV0( + doc=b"", + lib=b"", + name=b"TestStruct", + fields=[ + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"class", # PHP keyword + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ), + xdr.SCSpecUDTStructFieldV0( + doc=b"", + name=b"return", # PHP keyword + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + ] + ) + + result = render_struct(struct_entry, "TestContract") + self.assertIn("class TestContractTestStruct", result) + self.assertIn("public string $class_;", result) + self.assertIn("public int $return_;", result) + self.assertIn("string $class_", result) + self.assertIn("int $return_", result) + + def test_utility_methods(self): + """Test that utility methods (getOptions, getContractSpec) are generated correctly.""" + # Create a simple function for testing + function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"test_func"), + inputs=[], + outputs=[] + ) + + specs = [function] + result = generate_binding(specs, namespace="Test", contract_name="TestContract") + + # Check that getOptions method is present + self.assertIn("public function getOptions(): ClientOptions", result) + self.assertIn("return $this->client->getOptions();", result) + + # Check that getContractSpec method is present + self.assertIn("public function getContractSpec(): ContractSpec", result) + self.assertIn("return $this->client->getContractSpec();", result) + + # Check that ContractSpec is imported + self.assertIn("use Soneso\\StellarSDK\\Soroban\\Contract\\ContractSpec;", result) + + def test_build_tx_methods(self): + """Test that build transaction methods are generated correctly.""" + # Test function with parameters + function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"Transfer tokens", + name=xdr.SCSymbol(sc_symbol=b"transfer"), + inputs=[ + xdr.SCSpecFunctionInputV0( + doc=b"", + name=b"from", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + ), + xdr.SCSpecFunctionInputV0( + doc=b"", + name=b"to", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + ), + xdr.SCSpecFunctionInputV0( + doc=b"", + name=b"amount", + type=xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_I128) + ), + ], + outputs=[] + ) + + specs = [function] + result = generate_binding(specs, namespace="Test", contract_name="TestContract") + + # Check that both regular method and build method are generated + self.assertIn("public function transfer(", result) + self.assertIn("public function buildTransferTx(", result) + + # Check build method signature + self.assertIn("public function buildTransferTx(\n Address $from,\n Address $to,\n string $amount,\n ?MethodOptions $methodOptions = null\n ): AssembledTransaction {", result) + + # Check build method implementation + self.assertIn("return $this->client->buildInvokeMethodTx(", result) + self.assertIn("name: 'transfer',", result) + + # Test function with no parameters + simple_function = xdr.SCSpecEntry(xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0) + simple_function.function_v0 = xdr.SCSpecFunctionV0( + doc=b"", + name=xdr.SCSymbol(sc_symbol=b"get_info"), + inputs=[], + outputs=[xdr.SCSpecTypeDef(xdr.SCSpecType.SC_SPEC_TYPE_STRING)] + ) + + specs = [simple_function] + result = generate_binding(specs, namespace="Test", contract_name="TestContract") + + # Check build method for function with no parameters + self.assertIn("public function buildGetInfoTx(", result) + self.assertIn("?MethodOptions $methodOptions = null\n ): AssembledTransaction {", result) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_swift_generator.py b/tests/test_swift_generator.py new file mode 100644 index 0000000..71e961c --- /dev/null +++ b/tests/test_swift_generator.py @@ -0,0 +1,571 @@ +"""Tests for Swift binding generator.""" + +import pytest +from stellar_sdk import xdr + +from stellar_contract_bindings.swift import ( + is_swift_keyword, + is_tuple_struct, + snake_to_pascal, + snake_to_camel, + camel_to_snake, + escape_keyword, + to_swift_type, + to_scval, + from_scval, + render_enum, + render_error_enum, + render_struct, + render_tuple_struct, + render_union, + render_client, + generate_binding, +) + + +class TestSwiftUtilities: + """Test Swift utility functions.""" + + def test_is_swift_keyword(self): + assert is_swift_keyword("class") is True + assert is_swift_keyword("func") is True + assert is_swift_keyword("var") is True + assert is_swift_keyword("myVariable") is False + assert is_swift_keyword("hello") is False + + def test_snake_to_pascal(self): + assert snake_to_pascal("hello_world") == "HelloWorld" + assert snake_to_pascal("my_test_function") == "MyTestFunction" + assert snake_to_pascal("simple") == "Simple" + assert snake_to_pascal("") == "" + + def test_snake_to_camel(self): + assert snake_to_camel("hello_world") == "helloWorld" + assert snake_to_camel("my_test_function") == "myTestFunction" + assert snake_to_camel("simple") == "simple" + assert snake_to_camel("") == "" + + def test_camel_to_snake(self): + assert camel_to_snake("HelloWorld") == "hello_world" + assert camel_to_snake("MyTestFunction") == "my_test_function" + assert camel_to_snake("simple") == "simple" + assert camel_to_snake("S") == "s" + + def test_escape_keyword(self): + assert escape_keyword("class") == "`class`" + assert escape_keyword("func") == "`func`" + assert escape_keyword("myVariable") == "myVariable" + assert escape_keyword("hello") == "hello" + + +class TestSwiftTypeConversion: + """Test Swift type conversion functions.""" + + def test_to_swift_type_primitives(self): + # Boolean + bool_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert to_swift_type(bool_type) == "Bool" + assert to_swift_type(bool_type, nullable=True) == "Bool?" + + # Void + void_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_VOID) + assert to_swift_type(void_type) == "Void" + + # Integers + u32_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert to_swift_type(u32_type) == "UInt32" + + i32_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I32) + assert to_swift_type(i32_type) == "Int32" + + u64_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64) + assert to_swift_type(u64_type) == "UInt64" + + i64_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I64) + assert to_swift_type(i64_type) == "Int64" + + def test_to_swift_type_bigint(self): + # BigInt types (represented as String) + u128_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U128) + assert to_swift_type(u128_type) == "String" + + i128_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I128) + assert to_swift_type(i128_type) == "String" + + u256_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U256) + assert to_swift_type(u256_type) == "String" + + i256_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I256) + assert to_swift_type(i256_type) == "String" + + def test_to_swift_type_string_bytes(self): + # String + string_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert to_swift_type(string_type) == "String" + + # Symbol + symbol_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + assert to_swift_type(symbol_type) == "String" + + # Bytes + bytes_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + assert to_swift_type(bytes_type) == "Data" + + # Fixed-length bytes + bytes_n_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BYTES_N) + assert to_swift_type(bytes_n_type) == "Data" + + def test_to_swift_type_address(self): + # Address + address_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + assert to_swift_type(address_type) == "SCAddressXDR" + + # Muxed Address + muxed_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_MUXED_ADDRESS) + assert to_swift_type(muxed_type) == "SCAddressXDR" + + def test_to_swift_type_collections(self): + # Vector + element_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + vec_type = xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_VEC, + vec=xdr.SCSpecTypeVec(element_type=element_type) + ) + assert to_swift_type(vec_type) == "[UInt32]" + assert to_swift_type(vec_type, nullable=True) == "[UInt32]?" + + # Map + key_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + val_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64) + map_type = xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_MAP, + map=xdr.SCSpecTypeMap(key_type=key_type, value_type=val_type) + ) + assert to_swift_type(map_type) == "[String: UInt64]" + + def test_to_swift_type_option(self): + # Option + inner_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + option_type = xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_OPTION, + option=xdr.SCSpecTypeOption(value_type=inner_type) + ) + assert to_swift_type(option_type) == "UInt32?" + + def test_to_swift_type_tuple(self): + # Empty tuple + empty_tuple = xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_TUPLE, + tuple=xdr.SCSpecTypeTuple(value_types=[]) + ) + assert to_swift_type(empty_tuple) == "Void" + + # Multi-element tuple + type1 = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + type2 = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + multi_tuple = xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_TUPLE, + tuple=xdr.SCSpecTypeTuple(value_types=[type1, type2]) + ) + assert to_swift_type(multi_tuple) == "(UInt32, String)" + + +class TestSwiftSCValConversion: + """Test SCVal conversion functions.""" + + def test_to_scval_primitives(self): + # Boolean + bool_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert to_scval(bool_type, "myBool") == "SCValXDR.bool(myBool)" + + # Void + void_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_VOID) + assert to_scval(void_type, "ignored") == "SCValXDR.void" + + # U32 + u32_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert to_scval(u32_type, "myU32") == "SCValXDR.u32(myU32)" + + # I32 + i32_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I32) + assert to_scval(i32_type, "myI32") == "SCValXDR.i32(myI32)" + + def test_to_scval_bigint(self): + # U128 + u128_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U128) + assert to_scval(u128_type, "myBigInt") == "try SCValXDR.u128(stringValue: myBigInt)" + + # I256 + i256_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I256) + assert to_scval(i256_type, "myBigInt") == "try SCValXDR.i256(stringValue: myBigInt)" + + def test_to_scval_string_bytes(self): + # String + string_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert to_scval(string_type, "myString") == "SCValXDR.string(myString)" + + # Symbol + symbol_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + assert to_scval(symbol_type, "mySymbol") == "SCValXDR.symbol(mySymbol)" + + # Bytes + bytes_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + assert to_scval(bytes_type, "myBytes") == "SCValXDR.bytes(myBytes)" + + def test_to_scval_address(self): + # Address + address_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + assert to_scval(address_type, "myAddress") == "SCValXDR.address(myAddress)" + + def test_from_scval_primitives(self): + # Boolean + bool_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + assert from_scval(bool_type, "val") == "val.bool ?? false" + + # Void + void_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_VOID) + assert from_scval(void_type, "val") == "()" + + # U32 + u32_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + assert from_scval(u32_type, "val") == "val.u32 ?? 0" + + def test_from_scval_bigint(self): + # U128 + u128_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U128) + assert from_scval(u128_type, "val") == 'val.u128String ?? "0"' + + # I256 + i256_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_I256) + assert from_scval(i256_type, "val") == 'val.i256String ?? "0"' + + def test_from_scval_string_bytes(self): + # String + string_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + assert from_scval(string_type, "val") == 'val.string ?? ""' + + # Symbol + symbol_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_SYMBOL) + assert from_scval(symbol_type, "val") == 'val.symbol ?? ""' + + # Bytes + bytes_type = xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BYTES) + assert from_scval(bytes_type, "val") == "val.bytes ?? Data()" + + +class TestSwiftCodeGeneration: + """Test Swift code generation functions.""" + + def test_render_enum(self): + # Create a test enum + cases = [ + xdr.SCSpecUDTEnumCaseV0( + doc=None, + name=b"pending", + value=xdr.Uint32(0) + ), + xdr.SCSpecUDTEnumCaseV0( + doc=None, + name=b"completed", + value=xdr.Uint32(1) + ), + xdr.SCSpecUDTEnumCaseV0( + doc=None, + name=b"failed", + value=xdr.Uint32(2) + ) + ] + enum_entry = xdr.SCSpecUDTEnumV0( + doc=b"Status of a transaction", + lib=None, + name=b"Status", + cases=cases + ) + + result = render_enum(enum_entry, "TestContract") + assert "public enum TestContractStatus: UInt32, CaseIterable" in result + assert "case pending = 0" in result + assert "case completed = 1" in result + assert "case failed = 2" in result + assert "public func toSCVal() throws -> SCValXDR" in result + assert "public static func fromSCVal(_ val: SCValXDR) throws -> TestContractStatus" in result + + def test_render_error_enum(self): + # Create a test error enum + cases = [ + xdr.SCSpecUDTErrorEnumCaseV0( + doc=None, + name=b"not_found", + value=xdr.Uint32(0) + ), + xdr.SCSpecUDTErrorEnumCaseV0( + doc=None, + name=b"unauthorized", + value=xdr.Uint32(1) + ) + ] + error_enum = xdr.SCSpecUDTErrorEnumV0( + doc=b"Common errors", + lib=None, + name=b"Common", + cases=cases + ) + + result = render_error_enum(error_enum, "TestContract") + assert "public enum TestContractCommonError: UInt32, Error, CaseIterable" in result + assert "case not_found = 0" in result + assert "case unauthorized = 1" in result + + def test_render_struct(self): + # Create a test struct + fields = [ + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"name", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ), + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"age", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"active", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + ) + ] + struct_entry = xdr.SCSpecUDTStructV0( + doc=b"User information", + lib=None, + name=b"User", + fields=fields + ) + + result = render_struct(struct_entry, "TestContract") + assert "public struct TestContractUser: Codable" in result + assert "public let name: String" in result + assert "public let age: UInt32" in result + assert "public let active: Bool" in result + assert "public init(" in result + assert "public func toSCVal() throws -> SCValXDR" in result + assert "public static func fromSCVal(_ val: SCValXDR) throws -> TestContractUser" in result + + def test_render_tuple_struct(self): + # Create a test tuple struct + fields = [ + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"0", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32) + ), + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"1", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING) + ) + ] + tuple_struct = xdr.SCSpecUDTStructV0( + doc=b"A tuple struct", + lib=None, + name=b"MyTuple", + fields=fields + ) + + result = render_tuple_struct(tuple_struct, "TestContract") + assert "public struct TestContractMyTuple: Codable" in result + assert "public let value: (UInt32, String)" in result + assert "public init(value: (UInt32, String))" in result + + def test_is_tuple_struct(self): + # Test tuple struct (numeric field names) + tuple_fields = [ + xdr.SCSpecUDTStructFieldV0(doc=None, name=b"0", type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32)), + xdr.SCSpecUDTStructFieldV0(doc=None, name=b"1", type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING)) + ] + tuple_struct = xdr.SCSpecUDTStructV0(doc=None, lib=None, name=b"MyTuple", fields=tuple_fields) + assert is_tuple_struct(tuple_struct) is True + + # Test normal struct (non-numeric field names) + normal_fields = [ + xdr.SCSpecUDTStructFieldV0(doc=None, name=b"name", type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING)), + xdr.SCSpecUDTStructFieldV0(doc=None, name=b"age", type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32)) + ] + normal_struct = xdr.SCSpecUDTStructV0(doc=None, lib=None, name=b"User", fields=normal_fields) + assert is_tuple_struct(normal_struct) is False + + def test_render_union(self): + # Create a test union with both void and tuple cases + cases = [ + xdr.SCSpecUDTUnionCaseV0( + kind=xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0, + void_case=xdr.SCSpecUDTUnionCaseVoidV0( + doc=None, + name=b"none" + ) + ), + xdr.SCSpecUDTUnionCaseV0( + kind=xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0, + tuple_case=xdr.SCSpecUDTUnionCaseTupleV0( + doc=None, + name=b"some", + type=[xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U32)] + ) + ), + xdr.SCSpecUDTUnionCaseV0( + kind=xdr.SCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0, + tuple_case=xdr.SCSpecUDTUnionCaseTupleV0( + doc=None, + name=b"pair", + type=[ + xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_STRING), + xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL) + ] + ) + ) + ] + union_entry = xdr.SCSpecUDTUnionV0( + doc=b"Optional value", + name=b"OptionalValue", + lib=None, + cases=cases + ) + + result = render_union(union_entry, "TestContract") + assert "public enum TestContractOptionalValue" in result + assert "case `none`" in result # 'none' is a Swift keyword, so it's escaped + assert "case `some`(UInt32)" in result # 'some' is also a Swift keyword + assert "case pair(String, Bool)" in result + assert "public func toSCVal() throws -> SCValXDR" in result + assert "public static func fromSCVal(_ val: SCValXDR) throws -> TestContractOptionalValue" in result + assert "TestContractError.conversionFailed" in result # Check for contract-specific error + + def test_render_client(self): + # Create test function specs + input1 = xdr.SCSpecFunctionInputV0( + doc=None, + name=b"amount", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64) + ) + input2 = xdr.SCSpecFunctionInputV0( + doc=None, + name=b"recipient", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + ) + + function1 = xdr.SCSpecFunctionV0( + doc=b"Transfer tokens", + name=xdr.SCSymbol(sc_symbol=b"transfer"), + inputs=[input1, input2], + outputs=[xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_BOOL)] + ) + + function2 = xdr.SCSpecFunctionV0( + doc=b"Get balance", + name=xdr.SCSymbol(sc_symbol=b"balance_of"), + inputs=[xdr.SCSpecFunctionInputV0( + doc=None, + name=b"owner", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_ADDRESS) + )], + outputs=[xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64)] + ) + + result = render_client([function1, function2], "TokenContract") + + # Check class definition + assert "public class TokenContract" in result + assert "private let client: SorobanClient" in result + + # Check transfer method + assert "public func transfer(" in result + assert "amount: UInt64" in result + assert "recipient: SCAddressXDR" in result + assert "-> Bool" in result + + # Check balanceOf method (snake_case to camelCase conversion) + assert "public func balanceOf(" in result + assert "owner: SCAddressXDR" in result + assert "-> UInt64" in result + + # Check build methods + assert "public func buildTransferTx(" in result + assert "public func buildBalanceOfTx(" in result + assert "-> AssembledTransaction" in result + + def test_generate_binding(self): + # Create a complete set of specs + specs = [ + # Enum + xdr.SCSpecEntry( + kind=xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0, + udt_enum_v0=xdr.SCSpecUDTEnumV0( + doc=None, + lib=None, + name=b"Status", + cases=[ + xdr.SCSpecUDTEnumCaseV0(doc=None, name=b"active", value=xdr.Uint32(0)), + xdr.SCSpecUDTEnumCaseV0(doc=None, name=b"inactive", value=xdr.Uint32(1)) + ] + ) + ), + # Struct + xdr.SCSpecEntry( + kind=xdr.SCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0, + udt_struct_v0=xdr.SCSpecUDTStructV0( + doc=None, + lib=None, + name=b"User", + fields=[ + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"id", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64) + ), + xdr.SCSpecUDTStructFieldV0( + doc=None, + name=b"status", + type=xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_UDT, + udt=xdr.SCSpecTypeUDT(name=b"Status") + ) + ) + ] + ) + ), + # Function + xdr.SCSpecEntry( + kind=xdr.SCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0, + function_v0=xdr.SCSpecFunctionV0( + doc=None, + name=xdr.SCSymbol(sc_symbol=b"get_user"), + inputs=[ + xdr.SCSpecFunctionInputV0( + doc=None, + name=b"id", + type=xdr.SCSpecTypeDef(type=xdr.SCSpecType.SC_SPEC_TYPE_U64) + ) + ], + outputs=[ + xdr.SCSpecTypeDef( + type=xdr.SCSpecType.SC_SPEC_TYPE_UDT, + udt=xdr.SCSpecTypeUDT(name=b"User") + ) + ] + ) + ) + ] + + result = generate_binding(specs, "MyContract") + + # Check that all parts are generated + assert "import Foundation" in result + assert "import stellarsdk" in result + assert "public enum MyContractStatus: UInt32, CaseIterable" in result + assert "public struct MyContractUser: Codable" in result + assert "public class MyContract" in result + assert "public func getUser(" in result + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/web_interface/app.py b/web_interface/app.py index cfda1bd..542ce51 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -2,6 +2,9 @@ from typing import Literal from stellar_contract_bindings.java import generate_binding as generate_java_binding from stellar_contract_bindings.python import generate_binding as generate_python_binding +from stellar_contract_bindings.flutter import generate_binding as generate_flutter_binding +from stellar_contract_bindings.php import generate_binding as generate_php_binding +from stellar_contract_bindings.swift import generate_binding as generate_swift_binding from stellar_contract_bindings.utils import get_specs_by_contract_id import black @@ -12,13 +15,22 @@ "package": {"label": "Java Package", "default": "org.example", "type": "text"} }, "python": {}, + "flutter": { + "class_name": {"label": "Class Name", "default": "Contract", "type": "text"} + }, + "php": { + "class_name": {"label": "Class Name", "default": "Contract", "type": "text"} + }, + "swift": { + "class_name": {"label": "Class Name", "default": "Contract", "type": "text"} + }, } def generate_code( contract_id: str, rpc_url: str, - language: str = Literal["python", "java"], + language: str = Literal["python", "java", "flutter", "php", "swift"], extra_fields: dict = None, ) -> str: @@ -34,6 +46,18 @@ def generate_code( package = extra_fields.get("package", "org.example") code = generate_java_binding(specs, package) return code + elif language == "flutter": + class_name = extra_fields.get("class_name", "Contract") + code = generate_flutter_binding(specs, class_name) + return code + elif language == "php": + class_name = extra_fields.get("class_name", "Contract") + code = generate_php_binding(specs, class_name) + return code + elif language == "swift": + class_name = extra_fields.get("class_name", "Contract") + code = generate_swift_binding(specs, class_name) + return code else: return "Unsupported language selected."