From 7a282c8c08a6278edb1b2e4334cc11922b872932 Mon Sep 17 00:00:00 2001 From: Al Date: Wed, 12 Jun 2024 15:08:56 +0200 Subject: [PATCH] fix: duplicate structs bug (#114) * fix: duplicate structs bug * chore: fixing audit warning --- examples/__main__.py | 2 +- examples/duplicate_structs/application.json | 92 +++ examples/duplicate_structs/client.ts | 547 ++++++++++++++++++ .../duplicate_structs/duplicate_structs.py | 31 + package-lock.json | 14 +- src/client/app-types.ts | 5 + src/tests/approval-tests.spec.ts | 2 +- 7 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 examples/duplicate_structs/application.json create mode 100644 examples/duplicate_structs/client.ts create mode 100644 examples/duplicate_structs/duplicate_structs.py diff --git a/examples/__main__.py b/examples/__main__.py index cb65d86..7942e53 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -24,7 +24,7 @@ def cwd(path: Path) -> Generator[None, None, None]: def main(action: str) -> None: match action: case "build": - example_dirs = filter(lambda file: file.is_dir() and "__" not in file.name, root_path.glob("*")) + example_dirs = filter(lambda file: file.is_dir() and "__" not in file.name and file.name != "duplicate_structs", root_path.glob("*")) for example in example_dirs: logger.info(f"Building example {example.name}") with cwd(root_path): diff --git a/examples/duplicate_structs/application.json b/examples/duplicate_structs/application.json new file mode 100644 index 0000000..53358f7 --- /dev/null +++ b/examples/duplicate_structs/application.json @@ -0,0 +1,92 @@ +{ + "hints": { + "method_a_that_uses_struct()(uint64,uint64)": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "output": { + "name": "SomeStruct", + "elements": [ + [ + "a", + "uint64" + ], + [ + "b", + "uint64" + ] + ] + } + } + }, + "method_b_that_uses_same_struct()(uint64,uint64)": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "output": { + "name": "SomeStruct", + "elements": [ + [ + "a", + "uint64" + ], + [ + "b", + "uint64" + ] + ] + } + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5hcHByb3ZhbF9wcm9ncmFtOgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IG1haW5fYmFyZV9yb3V0aW5nQDYKICAgIG1ldGhvZCAibWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdCgpKHVpbnQ2NCx1aW50NjQpIgogICAgbWV0aG9kICJtZXRob2RfYl90aGF0X3VzZXNfc2FtZV9zdHJ1Y3QoKSh1aW50NjQsdWludDY0KSIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIG1haW5fbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdF9yb3V0ZUAyIG1haW5fbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0X3JvdXRlQDMKICAgIGVyciAvLyByZWplY3QgdHJhbnNhY3Rpb24KCm1haW5fbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdF9yb3V0ZUAyOgogICAgLy8gY29udHJhY3QucHk6MTkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gaXMgbm90IGNyZWF0aW5nCiAgICBjYWxsc3ViIG1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3QKICAgIGJ5dGUgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludCAxCiAgICByZXR1cm4KCm1haW5fbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0X3JvdXRlQDM6CiAgICAvLyBjb250cmFjdC5weToyNgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBpcyBub3QgY3JlYXRpbmcKICAgIGNhbGxzdWIgbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0CiAgICBieXRlIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnQgMQogICAgcmV0dXJuCgptYWluX2JhcmVfcm91dGluZ0A2OgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIHJlamVjdCB0cmFuc2FjdGlvbgogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgICEKICAgIGFzc2VydCAvLyBpcyBjcmVhdGluZwogICAgaW50IDEKICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5kZWxlZ2F0b3JfY29udHJhY3QuY29udHJhY3QuRHVwbGljYXRlU3RydWN0c0NvbnRyYWN0Lm1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3QoKSAtPiBieXRlczoKbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdDoKICAgIC8vIGNvbnRyYWN0LnB5OjE5LTIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIG1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3Qoc2VsZikgLT4gU29tZVN0cnVjdDoKICAgIHByb3RvIDAgMQogICAgLy8gY29udHJhY3QucHk6MjEtMjQKICAgIC8vIHJldHVybiBTb21lU3RydWN0KAogICAgLy8gICAgIGFyYzQuVUludDY0KDEpLAogICAgLy8gICAgIGFyYzQuVUludDY0KDIpLAogICAgLy8gKQogICAgYnl0ZSAweDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAyCiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5tZXRob2RfYl90aGF0X3VzZXNfc2FtZV9zdHJ1Y3QoKSAtPiBieXRlczoKbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0OgogICAgLy8gY29udHJhY3QucHk6MjYtMjcKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICAvLyBkZWYgbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0KHNlbGYpIC0+IFNvbWVTdHJ1Y3Q6CiAgICBwcm90byAwIDEKICAgIC8vIGNvbnRyYWN0LnB5OjI4LTMxCiAgICAvLyByZXR1cm4gU29tZVN0cnVjdCgKICAgIC8vICAgICBhcmM0LlVJbnQ2NCgzKSwKICAgIC8vICAgICBhcmM0LlVJbnQ2NCg0KSwKICAgIC8vICkKICAgIGJ5dGUgMHgwMDAwMDAwMDAwMDAwMDAzMDAwMDAwMDAwMDAwMDAwNAogICAgcmV0c3ViCg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgaW50IDEKICAgIHJldHVybgo=" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "DuplicateStructsContract", + "desc": "\n This contract is to be used as a test artifact to verify behavior around struct generation to ensure that \n the scenarios similar to whats outlined in this contract can not result in a typed client with duplicate struct \n definitions.\n ", + "methods": [ + { + "name": "method_a_that_uses_struct", + "args": [], + "returns": { + "type": "(uint64,uint64)" + } + }, + { + "name": "method_b_that_uses_same_struct", + "args": [], + "returns": { + "type": "(uint64,uint64)" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} diff --git a/examples/duplicate_structs/client.ts b/examples/duplicate_structs/client.ts new file mode 100644 index 0000000..5a3978c --- /dev/null +++ b/examples/duplicate_structs/client.ts @@ -0,0 +1,547 @@ +/* eslint-disable */ +/** + * This file was automatically generated by @algorandfoundation/algokit-client-generator. + * DO NOT MODIFY IT BY HAND. + * requires: @algorandfoundation/algokit-utils: ^2 + */ +import * as algokit from '@algorandfoundation/algokit-utils' +import type { + ABIAppCallArg, + AppCallTransactionResult, + AppCallTransactionResultOfType, + AppCompilationResult, + AppReference, + AppState, + AppStorageSchema, + CoreAppCallArgs, + RawAppCallArgs, + TealTemplateParams, +} from '@algorandfoundation/algokit-utils/types/app' +import type { + AppClientCallCoreParams, + AppClientCompilationParams, + AppClientDeployCoreParams, + AppDetails, + ApplicationClient, +} from '@algorandfoundation/algokit-utils/types/app-client' +import type { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec' +import type { SendTransactionResult, TransactionToSign, SendTransactionFrom, SendTransactionParams } from '@algorandfoundation/algokit-utils/types/transaction' +import type { ABIResult, TransactionWithSigner } from 'algosdk' +import { Algodv2, OnApplicationComplete, Transaction, AtomicTransactionComposer, modelsv2 } from 'algosdk' +export const APP_SPEC: AppSpec = { + "hints": { + "method_a_that_uses_struct()(uint64,uint64)": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "output": { + "name": "SomeStruct", + "elements": [ + [ + "a", + "uint64" + ], + [ + "b", + "uint64" + ] + ] + } + } + }, + "method_b_that_uses_same_struct()(uint64,uint64)": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "output": { + "name": "SomeStruct", + "elements": [ + [ + "a", + "uint64" + ], + [ + "b", + "uint64" + ] + ] + } + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5hcHByb3ZhbF9wcm9ncmFtOgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IG1haW5fYmFyZV9yb3V0aW5nQDYKICAgIG1ldGhvZCAibWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdCgpKHVpbnQ2NCx1aW50NjQpIgogICAgbWV0aG9kICJtZXRob2RfYl90aGF0X3VzZXNfc2FtZV9zdHJ1Y3QoKSh1aW50NjQsdWludDY0KSIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIG1haW5fbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdF9yb3V0ZUAyIG1haW5fbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0X3JvdXRlQDMKICAgIGVyciAvLyByZWplY3QgdHJhbnNhY3Rpb24KCm1haW5fbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdF9yb3V0ZUAyOgogICAgLy8gY29udHJhY3QucHk6MTkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gaXMgbm90IGNyZWF0aW5nCiAgICBjYWxsc3ViIG1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3QKICAgIGJ5dGUgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludCAxCiAgICByZXR1cm4KCm1haW5fbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0X3JvdXRlQDM6CiAgICAvLyBjb250cmFjdC5weToyNgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBpcyBub3QgY3JlYXRpbmcKICAgIGNhbGxzdWIgbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0CiAgICBieXRlIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnQgMQogICAgcmV0dXJuCgptYWluX2JhcmVfcm91dGluZ0A2OgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIHJlamVjdCB0cmFuc2FjdGlvbgogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgICEKICAgIGFzc2VydCAvLyBpcyBjcmVhdGluZwogICAgaW50IDEKICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5kZWxlZ2F0b3JfY29udHJhY3QuY29udHJhY3QuRHVwbGljYXRlU3RydWN0c0NvbnRyYWN0Lm1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3QoKSAtPiBieXRlczoKbWV0aG9kX2FfdGhhdF91c2VzX3N0cnVjdDoKICAgIC8vIGNvbnRyYWN0LnB5OjE5LTIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIG1ldGhvZF9hX3RoYXRfdXNlc19zdHJ1Y3Qoc2VsZikgLT4gU29tZVN0cnVjdDoKICAgIHByb3RvIDAgMQogICAgLy8gY29udHJhY3QucHk6MjEtMjQKICAgIC8vIHJldHVybiBTb21lU3RydWN0KAogICAgLy8gICAgIGFyYzQuVUludDY0KDEpLAogICAgLy8gICAgIGFyYzQuVUludDY0KDIpLAogICAgLy8gKQogICAgYnl0ZSAweDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAyCiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5tZXRob2RfYl90aGF0X3VzZXNfc2FtZV9zdHJ1Y3QoKSAtPiBieXRlczoKbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0OgogICAgLy8gY29udHJhY3QucHk6MjYtMjcKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICAvLyBkZWYgbWV0aG9kX2JfdGhhdF91c2VzX3NhbWVfc3RydWN0KHNlbGYpIC0+IFNvbWVTdHJ1Y3Q6CiAgICBwcm90byAwIDEKICAgIC8vIGNvbnRyYWN0LnB5OjI4LTMxCiAgICAvLyByZXR1cm4gU29tZVN0cnVjdCgKICAgIC8vICAgICBhcmM0LlVJbnQ2NCgzKSwKICAgIC8vICAgICBhcmM0LlVJbnQ2NCg0KSwKICAgIC8vICkKICAgIGJ5dGUgMHgwMDAwMDAwMDAwMDAwMDAzMDAwMDAwMDAwMDAwMDAwNAogICAgcmV0c3ViCg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuZGVsZWdhdG9yX2NvbnRyYWN0LmNvbnRyYWN0LkR1cGxpY2F0ZVN0cnVjdHNDb250cmFjdC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgLy8gY29udHJhY3QucHk6MTIKICAgIC8vIGNsYXNzIER1cGxpY2F0ZVN0cnVjdHNDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgaW50IDEKICAgIHJldHVybgo=" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "DuplicateStructsContract", + "desc": "\n This contract is to be used as a test artifact to verify behavior around struct generation to ensure that \n the scenarios similar to whats outlined in this contract can not result in a typed client with duplicate struct \n definitions.\n ", + "methods": [ + { + "name": "method_a_that_uses_struct", + "args": [], + "returns": { + "type": "(uint64,uint64)" + } + }, + { + "name": "method_b_that_uses_same_struct", + "args": [], + "returns": { + "type": "(uint64,uint64)" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} + +/** + * Defines an onCompletionAction of 'no_op' + */ +export type OnCompleteNoOp = { onCompleteAction?: 'no_op' | OnApplicationComplete.NoOpOC } +/** + * Defines an onCompletionAction of 'opt_in' + */ +export type OnCompleteOptIn = { onCompleteAction: 'opt_in' | OnApplicationComplete.OptInOC } +/** + * Defines an onCompletionAction of 'close_out' + */ +export type OnCompleteCloseOut = { onCompleteAction: 'close_out' | OnApplicationComplete.CloseOutOC } +/** + * Defines an onCompletionAction of 'delete_application' + */ +export type OnCompleteDelApp = { onCompleteAction: 'delete_application' | OnApplicationComplete.DeleteApplicationOC } +/** + * Defines an onCompletionAction of 'update_application' + */ +export type OnCompleteUpdApp = { onCompleteAction: 'update_application' | OnApplicationComplete.UpdateApplicationOC } +/** + * A state record containing a single unsigned integer + */ +export type IntegerState = { + /** + * Gets the state value as a BigInt. + */ + asBigInt(): bigint + /** + * Gets the state value as a number. + */ + asNumber(): number +} +/** + * A state record containing binary data + */ +export type BinaryState = { + /** + * Gets the state value as a Uint8Array + */ + asByteArray(): Uint8Array + /** + * Gets the state value as a string + */ + asString(): string +} + +export type AppCreateCallTransactionResult = AppCallTransactionResult & Partial & AppReference +export type AppUpdateCallTransactionResult = AppCallTransactionResult & Partial + +export type AppClientComposeCallCoreParams = Omit & { + sendParams?: Omit +} +export type AppClientComposeExecuteParams = Pick + +export type IncludeSchema = { + /** + * Any overrides for the storage schema to request for the created app; by default the schema indicated by the app spec is used. + */ + schema?: Partial +} + +/** + * Defines the types of available calls and state of the DuplicateStructsContract smart contract. + */ +export type DuplicateStructsContract = { + /** + * Maps method signatures / names to their argument and return types. + */ + methods: + & Record<'method_a_that_uses_struct()(uint64,uint64)' | 'method_a_that_uses_struct', { + argsObj: { + } + argsTuple: [] + returns: SomeStruct + }> + & Record<'method_b_that_uses_same_struct()(uint64,uint64)' | 'method_b_that_uses_same_struct', { + argsObj: { + } + argsTuple: [] + returns: SomeStruct + }> +} +/** + * Defines the possible abi call signatures + */ +export type DuplicateStructsContractSig = keyof DuplicateStructsContract['methods'] +/** + * Defines an object containing all relevant parameters for a single call to the contract. Where TSignature is undefined, a bare call is made + */ +export type TypedCallParams = { + method: TSignature + methodArgs: TSignature extends undefined ? undefined : Array +} & AppClientCallCoreParams & CoreAppCallArgs +/** + * Defines the arguments required for a bare call + */ +export type BareCallArgs = Omit +/** + * Represents a SomeStruct result as a struct + */ +export type SomeStruct = { + a: bigint + b: bigint +} +/** + * Converts the tuple representation of a SomeStruct to the struct representation + */ +export function SomeStruct([a, b]: [bigint, bigint] ) { + return { + a, + b, + } +} +/** + * Maps a method signature from the DuplicateStructsContract smart contract to the method's arguments in either tuple of struct form + */ +export type MethodArgs = DuplicateStructsContract['methods'][TSignature]['argsObj' | 'argsTuple'] +/** + * Maps a method signature from the DuplicateStructsContract smart contract to the method's return type + */ +export type MethodReturn = DuplicateStructsContract['methods'][TSignature]['returns'] + +/** + * A factory for available 'create' calls + */ +export type DuplicateStructsContractCreateCalls = (typeof DuplicateStructsContractCallFactory)['create'] +/** + * Defines supported create methods for this smart contract + */ +export type DuplicateStructsContractCreateCallParams = + | (TypedCallParams & (OnCompleteNoOp)) +/** + * Defines arguments required for the deploy method. + */ +export type DuplicateStructsContractDeployArgs = { + deployTimeParams?: TealTemplateParams + /** + * A delegate which takes a create call factory and returns the create call params for this smart contract + */ + createCall?: (callFactory: DuplicateStructsContractCreateCalls) => DuplicateStructsContractCreateCallParams +} + + +/** + * Exposes methods for constructing all available smart contract calls + */ +export abstract class DuplicateStructsContractCallFactory { + /** + * Gets available create call factories + */ + static get create() { + return { + /** + * Constructs a create call for the DuplicateStructsContract smart contract using a bare call + * + * @param params Any parameters for the call + * @returns A TypedCallParams object for the call + */ + bare(params: BareCallArgs & AppClientCallCoreParams & CoreAppCallArgs & AppClientCompilationParams & (OnCompleteNoOp) = {}) { + return { + method: undefined, + methodArgs: undefined, + ...params, + } + }, + } + } + + /** + * Constructs a no op call for the method_a_that_uses_struct()(uint64,uint64) ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static methodAThatUsesStruct(args: MethodArgs<'method_a_that_uses_struct()(uint64,uint64)'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'method_a_that_uses_struct()(uint64,uint64)' as const, + methodArgs: Array.isArray(args) ? args : [], + ...params, + } + } + /** + * Constructs a no op call for the method_b_that_uses_same_struct()(uint64,uint64) ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static methodBThatUsesSameStruct(args: MethodArgs<'method_b_that_uses_same_struct()(uint64,uint64)'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'method_b_that_uses_same_struct()(uint64,uint64)' as const, + methodArgs: Array.isArray(args) ? args : [], + ...params, + } + } +} + +/** + * A client to make calls to the DuplicateStructsContract smart contract + */ +export class DuplicateStructsContractClient { + /** + * The underlying `ApplicationClient` for when you want to have more flexibility + */ + public readonly appClient: ApplicationClient + + private readonly sender: SendTransactionFrom | undefined + + /** + * Creates a new instance of `DuplicateStructsContractClient` + * + * @param appDetails appDetails The details to identify the app to deploy + * @param algod An algod client instance + */ + constructor(appDetails: AppDetails, private algod: Algodv2) { + this.sender = appDetails.sender + this.appClient = algokit.getAppClient({ + ...appDetails, + app: APP_SPEC + }, algod) + } + + /** + * Checks for decode errors on the AppCallTransactionResult and maps the return value to the specified generic type + * + * @param result The AppCallTransactionResult to be mapped + * @param returnValueFormatter An optional delegate to format the return value if required + * @returns The smart contract response with an updated return value + */ + protected mapReturnValue(result: AppCallTransactionResult, returnValueFormatter?: (value: any) => TReturn): AppCallTransactionResultOfType & TResult { + if(result.return?.decodeError) { + throw result.return.decodeError + } + const returnValue = result.return?.returnValue !== undefined && returnValueFormatter !== undefined + ? returnValueFormatter(result.return.returnValue) + : result.return?.returnValue as TReturn | undefined + return { ...result, return: returnValue } as AppCallTransactionResultOfType & TResult + } + + /** + * Calls the ABI method with the matching signature using an onCompletion code of NO_OP + * + * @param typedCallParams An object containing the method signature, args, and any other relevant parameters + * @param returnValueFormatter An optional delegate which when provided will be used to map non-undefined return values to the target type + * @returns The result of the smart contract call + */ + public async call(typedCallParams: TypedCallParams, returnValueFormatter?: (value: any) => MethodReturn) { + return this.mapReturnValue>(await this.appClient.call(typedCallParams), returnValueFormatter) + } + + /** + * Idempotently deploys the DuplicateStructsContract smart contract. + * + * @param params The arguments for the contract calls and any additional parameters for the call + * @returns The deployment result + */ + public deploy(params: DuplicateStructsContractDeployArgs & AppClientDeployCoreParams & IncludeSchema = {}): ReturnType { + const createArgs = params.createCall?.(DuplicateStructsContractCallFactory.create) + return this.appClient.deploy({ + ...params, + createArgs, + createOnCompleteAction: createArgs?.onCompleteAction, + }) + } + + /** + * Gets available create methods + */ + public get create() { + const $this = this + return { + /** + * Creates a new instance of the DuplicateStructsContract smart contract using a bare call. + * + * @param args The arguments for the bare call + * @returns The create result + */ + async bare(args: BareCallArgs & AppClientCallCoreParams & AppClientCompilationParams & IncludeSchema & CoreAppCallArgs & (OnCompleteNoOp) = {}) { + return $this.mapReturnValue(await $this.appClient.create(args)) + }, + } + } + + /** + * Makes a clear_state call to an existing instance of the DuplicateStructsContract smart contract. + * + * @param args The arguments for the bare call + * @returns The clear_state result + */ + public clearState(args: BareCallArgs & AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.appClient.clearState(args) + } + + /** + * Calls the method_a_that_uses_struct()(uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public methodAThatUsesStruct(args: MethodArgs<'method_a_that_uses_struct()(uint64,uint64)'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(DuplicateStructsContractCallFactory.methodAThatUsesStruct(args, params), SomeStruct) + } + + /** + * Calls the method_b_that_uses_same_struct()(uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public methodBThatUsesSameStruct(args: MethodArgs<'method_b_that_uses_same_struct()(uint64,uint64)'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(DuplicateStructsContractCallFactory.methodBThatUsesSameStruct(args, params), SomeStruct) + } + + public compose(): DuplicateStructsContractComposer { + const client = this + const atc = new AtomicTransactionComposer() + let promiseChain:Promise = Promise.resolve() + const resultMappers: Array any)> = [] + return { + methodAThatUsesStruct(args: MethodArgs<'method_a_that_uses_struct()(uint64,uint64)'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => client.methodAThatUsesStruct(args, {...params, sendParams: {...params?.sendParams, skipSending: true, atc}})) + resultMappers.push(SomeStruct) + return this + }, + methodBThatUsesSameStruct(args: MethodArgs<'method_b_that_uses_same_struct()(uint64,uint64)'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => client.methodBThatUsesSameStruct(args, {...params, sendParams: {...params?.sendParams, skipSending: true, atc}})) + resultMappers.push(SomeStruct) + return this + }, + clearState(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => client.clearState({...args, sendParams: {...args?.sendParams, skipSending: true, atc}})) + resultMappers.push(undefined) + return this + }, + addTransaction(txn: TransactionWithSigner | TransactionToSign | Transaction | Promise, defaultSender?: SendTransactionFrom) { + promiseChain = promiseChain.then(async () => atc.addTransaction(await algokit.getTransactionWithSigner(txn, defaultSender ?? client.sender))) + return this + }, + async atc() { + await promiseChain + return atc + }, + async simulate(options?: SimulateOptions) { + await promiseChain + const result = await atc.simulate(client.algod, new modelsv2.SimulateRequest({ txnGroups: [], ...options })) + return { + ...result, + returns: result.methodResults?.map((val, i) => resultMappers[i] !== undefined ? resultMappers[i]!(val.returnValue) : val.returnValue) + } + }, + async execute(sendParams?: AppClientComposeExecuteParams) { + await promiseChain + const result = await algokit.sendAtomicTransactionComposer({ atc, sendParams }, client.algod) + return { + ...result, + returns: result.returns?.map((val, i) => resultMappers[i] !== undefined ? resultMappers[i]!(val.returnValue) : val.returnValue) + } + } + } as unknown as DuplicateStructsContractComposer + } +} +export type DuplicateStructsContractComposer = { + /** + * Calls the method_a_that_uses_struct()(uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + methodAThatUsesStruct(args: MethodArgs<'method_a_that_uses_struct()(uint64,uint64)'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs): DuplicateStructsContractComposer<[...TReturns, MethodReturn<'method_a_that_uses_struct()(uint64,uint64)'>]> + + /** + * Calls the method_b_that_uses_same_struct()(uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + methodBThatUsesSameStruct(args: MethodArgs<'method_b_that_uses_same_struct()(uint64,uint64)'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs): DuplicateStructsContractComposer<[...TReturns, MethodReturn<'method_b_that_uses_same_struct()(uint64,uint64)'>]> + + /** + * Makes a clear_state call to an existing instance of the DuplicateStructsContract smart contract. + * + * @param args The arguments for the bare call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + clearState(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs): DuplicateStructsContractComposer<[...TReturns, undefined]> + + /** + * Adds a transaction to the composer + * + * @param txn One of: A TransactionWithSigner object (returned as is), a TransactionToSign object (signer is obtained from the signer property), a Transaction object (signer is extracted from the defaultSender parameter), an async SendTransactionResult returned by one of algokit utils helpers (signer is obtained from the defaultSender parameter) + * @param defaultSender The default sender to be used to obtain a signer where the object provided to the transaction parameter does not include a signer. + */ + addTransaction(txn: TransactionWithSigner | TransactionToSign | Transaction | Promise, defaultSender?: SendTransactionFrom): DuplicateStructsContractComposer + /** + * Returns the underlying AtomicTransactionComposer instance + */ + atc(): Promise + /** + * Simulates the transaction group and returns the result + */ + simulate(options?: SimulateOptions): Promise> + /** + * Executes the transaction group and returns the results + */ + execute(sendParams?: AppClientComposeExecuteParams): Promise> +} +export type SimulateOptions = Omit[0], 'txnGroups'> +export type DuplicateStructsContractComposerSimulateResult = { + returns: TReturns + methodResults: ABIResult[] + simulateResponse: modelsv2.SimulateResponse +} +export type DuplicateStructsContractComposerResults = { + returns: TReturns + groupId: string + txIds: string[] + transactions: Transaction[] +} diff --git a/examples/duplicate_structs/duplicate_structs.py b/examples/duplicate_structs/duplicate_structs.py new file mode 100644 index 0000000..93f095d --- /dev/null +++ b/examples/duplicate_structs/duplicate_structs.py @@ -0,0 +1,31 @@ +# TODO: revisit upon deciding on what to do with pyteal examples (separate PR) + +# # pyright: reportMissingModuleSource=false +# from algopy import ARC4Contract, arc4 + + +# class SomeStruct(arc4.Struct): +# """Struct with two arc4.UInt64 for returning multiple values""" + +# a: arc4.UInt64 +# b: arc4.UInt64 + + +# class DuplicateStructsContract(ARC4Contract): +# """ +# Used for snapshot testing to ensure no duplicate struct definitions in typed clients. +# """ + +# @arc4.abimethod() +# def method_a_that_uses_struct(self) -> SomeStruct: +# return SomeStruct( +# arc4.UInt64(1), +# arc4.UInt64(2), +# ) + +# @arc4.abimethod() +# def method_b_that_uses_same_struct(self) -> SomeStruct: +# return SomeStruct( +# arc4.UInt64(3), +# arc4.UInt64(4), +# ) diff --git a/package-lock.json b/package-lock.json index 53ded09..6230992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2874,12 +2874,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4703,9 +4703,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/src/client/app-types.ts b/src/client/app-types.ts index 54d7209..470204c 100644 --- a/src/client/app-types.ts +++ b/src/client/app-types.ts @@ -86,9 +86,14 @@ export function* appTypes(ctx: GeneratorContext): DocumentParts { function* structs({ app, sanitizer }: GeneratorContext): DocumentParts { if (app.hints === undefined) return + const definedStructs = new Set() + for (const methodHint of Object.values(app.hints)) { if (methodHint.structs === undefined) continue for (const struct of Object.values(methodHint.structs)) { + if (definedStructs.has(struct.name)) continue + definedStructs.add(struct.name) + yield* jsDoc(`Represents a ${struct.name} result as a struct`) yield `export type ${sanitizer.makeSafeTypeIdentifier(struct.name)} = {` yield IncIndent diff --git a/src/tests/approval-tests.spec.ts b/src/tests/approval-tests.spec.ts index 9f9ba0d..9882f74 100644 --- a/src/tests/approval-tests.spec.ts +++ b/src/tests/approval-tests.spec.ts @@ -7,7 +7,7 @@ import { expect, test, describe } from 'vitest' const writeActual = process.env.TEST_ENV !== 'ci' -const testContracts = ['helloworld', 'lifecycle', 'state', 'voting'] as const +const testContracts = ['helloworld', 'lifecycle', 'state', 'voting', 'duplicate_structs'] as const describe('When generating a ts client for a the contract', () => { test.each(testContracts)('%s approval', async (contractName) => {