├── test ├── example │ ├── .gitignore │ ├── Scarb.lock │ ├── Scarb.toml │ └── src │ │ └── lib.cairo ├── example.ts └── kanabi.test-d.ts ├── FUNDING.json ├── .vscode └── settings.json ├── .github └── workflows │ └── realise-package.yml ├── index.ts ├── LICENSE.md ├── rome.json ├── package.json ├── generate.ts ├── .gitignore ├── config.ts ├── tsconfig.json ├── README.md └── kanabi.ts /test/example/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /test/example/Scarb.lock: -------------------------------------------------------------------------------- 1 | # Code generated by scarb DO NOT EDIT. 2 | version = 1 3 | 4 | [[package]] 5 | name = "example" 6 | version = "0.1.0" 7 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x0d32c347706271DFd140f82Ba41fE4307b2AB6a3" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/example/Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | 5 | # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html 6 | 7 | [[target.starknet-contract]] 8 | sierra = true 9 | 10 | [dependencies] 11 | starknet = ">=2.4.4" 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rome.rome", 3 | "editor.formatOnSave": false, 4 | "typescript.enablePromptUseWorkspaceTsdk": true, 5 | "typescript.preferences.importModuleSpecifier": "shortest", 6 | "typescript.tsdk": "node_modules/typescript/lib", 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports.rome": "explicit" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "rome.rome" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "rome.rome" 15 | }, 16 | "[javascriptreact]": { 17 | "editor.defaultFormatter": "rome.rome" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "rome.rome" 21 | }, 22 | "[typescriptreact]": { 23 | "editor.defaultFormatter": "rome.rome" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/realise-package.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-gpr: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | permissions: 22 | packages: write 23 | contents: read 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 12 29 | registry-url: https://npm.pkg.github.com/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export type { Config, DefaultConfig, ResolvedConfig } from './config' 2 | import { 3 | Abi, 4 | Call, 5 | ContractFunctions, 6 | ContractFunctionsPopulateTransaction, 7 | ExtractAbiFunctionNames, 8 | FunctionArgs, 9 | FunctionRet, 10 | } from './kanabi' 11 | 12 | export { type Abi } from './kanabi' 13 | 14 | export function call< 15 | TAbi extends Abi, 16 | TFunctionName extends ExtractAbiFunctionNames, 17 | >( 18 | abi: TAbi, 19 | f: TFunctionName, 20 | args: FunctionArgs, 21 | ): FunctionRet { 22 | throw new Error('todo') 23 | } 24 | 25 | type TypedContractActions = { 26 | call>( 27 | method: TFunctionName, 28 | args?: FunctionArgs, 29 | ): Promise> 30 | populate>( 31 | method: TFunctionName, 32 | args?: FunctionArgs, 33 | ): Call 34 | populateTransaction: ContractFunctionsPopulateTransaction 35 | } 36 | 37 | export type TypedContract = TypedContractActions & 38 | ContractFunctions 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /rome.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "ignore": [ 4 | "CHANGELOG.md", 5 | "pnpm-lock.yaml", 6 | "tsconfig.json", 7 | "tsconfig.*.json" 8 | ] 9 | }, 10 | "formatter": { 11 | "enabled": true, 12 | "formatWithErrors": false, 13 | "indentStyle": "space", 14 | "indentSize": 2, 15 | "lineWidth": 80 16 | }, 17 | "linter": { 18 | "enabled": true, 19 | "rules": { 20 | "recommended": true, 21 | "complexity": { 22 | "noExtraSemicolon": "off" 23 | }, 24 | "correctness": { 25 | "noUnusedVariables": "error" 26 | }, 27 | "nursery": { 28 | "noBannedTypes": "off", 29 | "useExhaustiveDependencies": "error", 30 | "useGroupedTypeImport": "off" 31 | }, 32 | "performance": { 33 | "noDelete": "off" 34 | }, 35 | "style": { 36 | "noNonNullAssertion": "off", 37 | "useShorthandArrayType": "error" 38 | }, 39 | "suspicious": { 40 | "noArrayIndexKey": "off", 41 | "noExplicitAny": "off", 42 | "noRedeclare": "off" 43 | } 44 | } 45 | }, 46 | "javascript": { 47 | "formatter": { 48 | "quoteStyle": "single", 49 | "trailingComma": "all", 50 | "semicolons": "asNeeded" 51 | } 52 | }, 53 | "organizeImports": { 54 | "enabled": true 55 | }, 56 | "vcs": { 57 | "enabled": true, 58 | "clientKind": "git", 59 | "useIgnoreFile": true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abi-wan-kanabi", 3 | "version": "2.2.4", 4 | "description": "Abi parser for Cairo smart contracts, based on wagmi abitype", 5 | "main": "./dist/index.js", 6 | "bin": { 7 | "generate": "./dist/generate.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "clean": "rm -rf dist", 12 | "prepack": "npm run clean && npm run build", 13 | "generate": "npm run build && node dist/generate.js", 14 | "test": "vitest", 15 | "coverage": "vitest run --coverage", 16 | "typecheck": "vitest typecheck", 17 | "format": "rome format . --write" 18 | }, 19 | "files": ["dist"], 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.js", 24 | "default": "./dist/index.js" 25 | }, 26 | "./kanabi": { 27 | "types": "./dist/kanabi.d.ts", 28 | "import": "./dist/kanabi.js", 29 | "default": "./dist/kanabi.js" 30 | } 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/keep-starknet-strange/abi-wan-kanabi.git" 35 | }, 36 | "keywords": ["abi", "cairo"], 37 | "author": "", 38 | "license": "ISC", 39 | "bugs": { 40 | "url": "https://github.com/keep-starknet-strange/abi-wan-kanabi/issues" 41 | }, 42 | "homepage": "https://github.com/keep-starknet-strange/abi-wan-kanabi#readme", 43 | "publishConfig": { 44 | "ivpavici:registry": "https://npm.pkg.github.com" 45 | }, 46 | "dependencies": { 47 | "ansicolors": "^0.3.2", 48 | "cardinal": "^2.1.1", 49 | "fs-extra": "^10.0.0", 50 | "yargs": "^17.7.2" 51 | }, 52 | "devDependencies": { 53 | "@types/ansicolors": "^0.0.37", 54 | "@types/cardinal": "^2.1.1", 55 | "@types/fs-extra": "^11.0.1", 56 | "@types/yargs": "^17.0.24", 57 | "vitest": "^0.30.1", 58 | "rome": "^12.1.3", 59 | "typescript": "^5.2.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /generate.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import colors from 'ansicolors' 3 | import * as cardinal from 'cardinal' 4 | import * as fs from 'fs-extra' 5 | import * as path from 'path' 6 | import * as yargs from 'yargs' 7 | 8 | const argv = yargs 9 | .option('input', { 10 | alias: 'i', 11 | demandOption: true, 12 | describe: 'Input ABI file', 13 | type: 'string', 14 | }) 15 | .option('output', { 16 | alias: 'o', 17 | demandOption: true, 18 | describe: 'Output TypeScript file', 19 | type: 'string', 20 | }) 21 | .help() 22 | .alias('help', 'h') 23 | .parseSync() 24 | 25 | function usage(module: string) { 26 | return `import { Contract, RpcProvider, constants } from 'starknet'; 27 | import { ABI } from './${module}'; 28 | 29 | async function main() { 30 | const address = "CONTRACT_ADDRESS_HERE"; 31 | const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN }); 32 | const contract = new Contract(ABI, address, provider).typedv2(ABI); 33 | 34 | const version = await contract.getVersion(); 35 | console.log("version", version) 36 | 37 | // Abiwan is now successfully installed, just start writing your contract 38 | // function calls (\`const ret = contract.your_function()\`) and you'll get 39 | // helpful editor autocompletion, linting errors ... for free ! Enjoy ! 40 | } 41 | main().catch(console.error)` 42 | } 43 | 44 | async function run() { 45 | const json: { abi: object } = await fs.readJson(argv.input) 46 | let abi = json.abi 47 | 48 | if (typeof abi === 'string') { 49 | abi = JSON.parse(abi) 50 | } 51 | 52 | const content = `export const ABI = ${JSON.stringify( 53 | abi, 54 | null, 55 | 2, 56 | )} as const;\n` 57 | await fs.writeFile(argv.output, content) 58 | 59 | const output_path = path.parse(argv.output) 60 | const usage_snippet = usage(output_path.name) 61 | const usage_snippet_highlighted = cardinal.highlight(usage_snippet) 62 | 63 | console.log(`✅ Successfully generated ${colors.red(argv.output)}`) 64 | console.log(`💡 Here's a code snippet to get you started:\n`) 65 | console.log(usage_snippet_highlighted) 66 | } 67 | 68 | run().catch(console.error) 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | starknet-artifacts/ 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | .npmrc 109 | 110 | *.js 111 | 112 | # local folder for tests 113 | local 114 | 115 | # VSCode settings 116 | .vscode/ 117 | -------------------------------------------------------------------------------- /test/example/src/lib.cairo: -------------------------------------------------------------------------------- 1 | use starknet::ContractAddress; 2 | use starknet::EthAddress; 3 | 4 | #[derive(Drop, Serde)] 5 | struct TestStruct { 6 | int128: u128, 7 | felt: felt252, 8 | tuple: (u32, u32) 9 | } 10 | 11 | #[derive(Copy, Drop, Serde)] 12 | enum TestEnum { 13 | int128: u128, 14 | felt: felt252, 15 | tuple: (u32, u32), 16 | novalue, 17 | } 18 | 19 | #[starknet::interface] 20 | trait IExampleContract { 21 | fn fn_felt(self: @TContractState, felt: felt252); 22 | 23 | fn fn_felt_u8_bool(self: @TContractState, felt: felt252, int8: u8, b: bool); 24 | 25 | fn fn_felt_u8_bool_out_address_felt_u8_bool( 26 | self: @TContractState, felt: felt252, int8: u8, boolean: bool 27 | ) -> (ContractAddress, felt252, u8, bool); 28 | 29 | fn fn_felt_out_felt(self: @TContractState, felt: felt252) -> felt252; 30 | 31 | fn fn_out_simple_option(self: @TContractState) -> Option; 32 | 33 | fn fn_out_nested_option(self: @TContractState) -> Option>; 34 | 35 | fn fn_simple_array(self: @TContractState, arg: Array); 36 | 37 | fn fn_out_simple_array(self: @TContractState) -> Array; 38 | 39 | fn fn_struct(self: @TContractState, arg: TestStruct); 40 | 41 | fn fn_struct_array(self: @TContractState, arg: Array); 42 | 43 | fn fn_enum(self: @TContractState, arg: TestEnum); 44 | 45 | fn fn_enum_array(self: @TContractState, arg: Array); 46 | 47 | fn fn_out_enum_array(self: @TContractState, ) -> Array; 48 | 49 | fn fn_result(self: @TContractState, arg: Result); 50 | 51 | fn fn_out_result(self: @TContractState) -> Result; 52 | 53 | fn fn_nested_result(self: @TContractState, arg: Result, u8>); 54 | 55 | fn fn_eth_address(self: @TContractState, arg: EthAddress); 56 | 57 | fn fn_span(self: @TContractState, arg: Span); 58 | 59 | fn fn_bytes31(self: @TContractState, arg: bytes31); 60 | 61 | fn fn_byte_array(self: @TContractState, arg: ByteArray); 62 | } 63 | 64 | #[starknet::contract] 65 | mod example_contract { 66 | use starknet::ContractAddress; 67 | use starknet::EthAddress; 68 | use starknet::get_caller_address; 69 | 70 | use example::TestStruct; 71 | use example::TestEnum; 72 | #[storage] 73 | struct Storage {} 74 | 75 | //high-level code defining the events 76 | 77 | #[derive(Drop, starknet::Event)] 78 | #[event] 79 | enum Event { 80 | // ComponentEvent: test_component::Event, 81 | TestCounterIncreased: CounterIncreased, 82 | TestCounterDecreased: CounterDecreased, 83 | TestEnum: MyEnum 84 | } 85 | 86 | #[derive(Drop, starknet::Event)] 87 | struct CounterIncreased { 88 | amount: u128 89 | } 90 | 91 | #[derive(Drop, starknet::Event)] 92 | struct CounterDecreased { 93 | amount: u128 94 | } 95 | 96 | #[derive(Copy, Drop, starknet::Event)] 97 | enum MyEnum { 98 | Var1: MyStruct 99 | } 100 | 101 | #[derive(Copy, Drop, Serde, starknet::Event)] 102 | struct MyStruct { 103 | member: u128 104 | } 105 | 106 | #[external(v0)] 107 | impl ExampleContract of super::IExampleContract { 108 | fn fn_felt(self: @ContractState, felt: felt252) {} 109 | 110 | fn fn_felt_u8_bool(self: @ContractState, felt: felt252, int8: u8, b: bool) {} 111 | 112 | fn fn_felt_u8_bool_out_address_felt_u8_bool( 113 | self: @ContractState, felt: felt252, int8: u8, boolean: bool 114 | ) -> (ContractAddress, felt252, u8, bool) { 115 | let caller = get_caller_address(); 116 | (caller, felt, int8, boolean) 117 | } 118 | 119 | fn fn_felt_out_felt(self: @ContractState, felt: felt252) -> felt252 { 120 | felt 121 | } 122 | 123 | fn fn_out_simple_option(self: @ContractState) -> Option { 124 | Option::Some(1) 125 | } 126 | 127 | fn fn_out_nested_option(self: @ContractState) -> Option> { 128 | Option::Some(Option::Some(1)) 129 | } 130 | 131 | fn fn_simple_array(self: @ContractState, arg: Array) { 132 | } 133 | 134 | fn fn_out_simple_array(self: @ContractState) -> Array { 135 | let mut a = ArrayTrait::::new(); 136 | a.append(0); 137 | a.append(1); 138 | a.append(2); 139 | ArrayTrait::new() 140 | } 141 | 142 | fn fn_struct(self: @ContractState, arg: TestStruct) {} 143 | 144 | fn fn_struct_array(self: @ContractState, arg: Array) {} 145 | 146 | fn fn_enum(self: @ContractState, arg: TestEnum) {} 147 | 148 | fn fn_enum_array(self: @ContractState, arg: Array) {} 149 | 150 | fn fn_out_enum_array(self: @ContractState, ) -> Array { 151 | let mut messages: Array = ArrayTrait::new(); 152 | messages.append(TestEnum::int128(100_u128)); 153 | messages.append(TestEnum::felt('hello world')); 154 | messages.append(TestEnum::tuple((10_u32, 30_u32))); 155 | messages 156 | } 157 | 158 | fn fn_result(self: @ContractState, arg: Result) {} 159 | fn fn_out_result(self: @ContractState) -> Result { 160 | Result::Ok(0) 161 | } 162 | fn fn_nested_result(self: @ContractState, arg: Result, u8>) {} 163 | 164 | fn fn_eth_address(self: @ContractState, arg: EthAddress) {} 165 | 166 | fn fn_span(self: @ContractState, arg: Span) {} 167 | 168 | fn fn_bytes31(self: @ContractState, arg: bytes31) {} 169 | 170 | fn fn_byte_array(self: @ContractState, arg: ByteArray) {} 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | export type Ok = { Ok: T } 2 | export type Err = { Err: E } 3 | export type Result = Ok | Err 4 | // Note that Option> = T | undefined | undefined which is the same 5 | // as Option, is this what we want for Option ? 6 | export type Option = T | undefined 7 | export type U256 = { 8 | low: bigint 9 | high: bigint 10 | } 11 | export type Calldata = string[] & { readonly __compiled__?: boolean } 12 | export type Call = { 13 | contractAddress: string 14 | entrypoint: string 15 | calldata?: Calldata 16 | } 17 | 18 | export interface Config {} 19 | 20 | export type ResolvedConfig = { 21 | /** 22 | * TypeScript type to use for `ContractAddress` and `EthAddress` values 23 | * @default `0x${string}` 24 | */ 25 | AddressType: Config extends { AddressType: infer type } 26 | ? type 27 | : DefaultConfig['AddressType'] 28 | /** 29 | * TypeScript type to use for `ClassHash` values 30 | * @default `0x${string}` 31 | */ 32 | ClassHashType: Config extends { ClassHashType: infer type } 33 | ? type 34 | : DefaultConfig['ClassHashType'] 35 | /** 36 | * TypeScript type to use for `felt` values 37 | * @default `0x${string}` 38 | */ 39 | FeltType: Config extends { FeltType: infer type } 40 | ? type 41 | : DefaultConfig['FeltType'] 42 | /** 43 | * TypeScript type to use for `u64` and `u128` values 44 | * @default bigint 45 | */ 46 | BigIntType: Config extends { BigIntType: infer type } 47 | ? type 48 | : DefaultConfig['BigIntType'] 49 | /** 50 | * TypeScript type to use for `u265` values 51 | * @default bigint 52 | */ 53 | U256Type: Config extends { U256Type: infer type } 54 | ? type 55 | : DefaultConfig['U256Type'] 56 | /** 57 | * TypeScript type to use for `u512` values 58 | * @default string 59 | */ 60 | U512Type: Config extends { U512Type: infer type } 61 | ? type 62 | : DefaultConfig['U512Type'] 63 | /** 64 | * TypeScript type to use for `u8`, `u16` and `u32` values 65 | * @default number 66 | */ 67 | IntType: Config extends { IntType: infer type } 68 | ? type 69 | : DefaultConfig['IntType'] 70 | /** 71 | * TypeScript type to use for `Option::` values 72 | * @default T | undefined 73 | */ 74 | Option: Config extends { Option: infer type } 75 | ? type 76 | : DefaultConfig['Option'] 77 | /** 78 | * TypeScript type to use for tuples `(T1, T2, ...)` values 79 | * @default infer the types of the tuple element and return a TS tuple 80 | */ 81 | Tuple: Config extends { Tuple: infer type } ? type : DefaultConfig['Tuple'] 82 | /** 83 | * TypeScript type to use for tuples `Result` values 84 | * @default Ok | Err 85 | */ 86 | Result: Config extends { Result: infer type } 87 | ? type 88 | : DefaultConfig['Result'] 89 | /** 90 | * TypeScript type to use for enums 91 | * @default infer the types of the enum and return a union of objects 92 | */ 93 | Enum: Config extends { Enum: infer type } ? type : DefaultConfig['Enum'] 94 | /** 95 | * TypeScript type to use for bytes31 96 | * @default string 97 | */ 98 | Bytes31Type: Config extends { Bytes31Type: infer type } 99 | ? type 100 | : DefaultConfig['Bytes31Type'] 101 | /** 102 | * TypeScript type to use for ByteArray 103 | * @default string 104 | */ 105 | ByteArray: Config extends { ByteArray: infer type } 106 | ? type 107 | : DefaultConfig['ByteArrayType'] 108 | /** 109 | * TypeScript type to use for Secp256k1Point 110 | * @default string 111 | */ 112 | Secp256k1PointType: Config extends { ByteArray: infer type } 113 | ? type 114 | : DefaultConfig['Secp256k1PointType'] 115 | 116 | /** 117 | * TypeScript type to use for Calldata used in function calls 118 | * @default decimal-string array 119 | */ 120 | Calldata: Config extends { Calldata: infer type } 121 | ? type 122 | : DefaultConfig['Calldata'] 123 | 124 | /** 125 | * TypeScript type to use for populate return values 126 | * @default 127 | * { 128 | * contractAddress: string 129 | * entrypoint: string 130 | * calldata?: Calldata 131 | * } 132 | */ 133 | Call: Config extends { Call: infer type } 134 | ? type 135 | : DefaultConfig['Call'] 136 | 137 | /** 138 | * TypeScript type to use for CallOptions used in function calls 139 | * @default unknown 140 | */ 141 | CallOptions: Config extends { CallOptions: infer type } 142 | ? type 143 | : DefaultConfig['CallOptions'] 144 | 145 | /** 146 | * TypeScript type to use for InvokeOptions used in function calls 147 | * @default unknown 148 | */ 149 | InvokeOptions: Config extends { InvokeOptions: infer type } 150 | ? type 151 | : DefaultConfig['InvokeOptions'] 152 | 153 | /** 154 | * TypeScript type to use for invoke function return values 155 | * @default unknown 156 | */ 157 | InvokeFunctionResponse: Config extends { InvokeFunctionResponse: infer type } 158 | ? type 159 | : DefaultConfig['InvokeFunctionResponse'] 160 | } 161 | 162 | export type DefaultConfig = { 163 | AddressType: string 164 | ClassHashType: string 165 | FeltType: number | bigint | string 166 | BigIntType: number | bigint 167 | U256Type: number | bigint | U256 168 | U512Type: string 169 | IntType: number | bigint 170 | Option: Option 171 | /** By default, abiwan will infer the types of the tuple element and return a TS tuple */ 172 | Tuple: never 173 | Result: Result 174 | /** By default, abiwan will infer the types of the enum and return a union of objects */ 175 | Enum: never 176 | Bytes31Type: string 177 | ByteArrayType: string 178 | Secp256k1PointType: string 179 | 180 | Calldata: Calldata 181 | Call: Call 182 | CallOptions: unknown 183 | InvokeOptions: unknown 184 | InvokeFunctionResponse: unknown 185 | } 186 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | "lib": [ 14 | "ES2020", 15 | "ESNext" 16 | ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | /* JavaScript Support */ 41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 74 | /* Type Checking */ 75 | "strict": true, /* Enable all strict type-checking options. */ 76 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 77 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 78 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 79 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 80 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 81 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 82 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 83 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 84 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 85 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 86 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 87 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 88 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 89 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 90 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 91 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 92 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 93 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 94 | /* Completeness */ 95 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 96 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 97 | "resolveJsonModule": true, 98 | "esModuleInterop": true, 99 | "noErrorTruncation": true, 100 | }, 101 | "exclude": ["node_modules", "test", "dist"], 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abiwan 2 | 3 | npm version 4 | 5 | [![Exploration_Team](https://img.shields.io/badge/Exploration_Team-29296E.svg?&style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJhIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxODEgMTgxIj48ZGVmcz48c3R5bGU+LmJ7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYiIgZD0iTTE3Ni43Niw4OC4xOGwtMzYtMzcuNDNjLTEuMzMtMS40OC0zLjQxLTIuMDQtNS4zMS0xLjQybC0xMC42MiwyLjk4LTEyLjk1LDMuNjNoLjc4YzUuMTQtNC41Nyw5LjktOS41NSwxNC4yNS0xNC44OSwxLjY4LTEuNjgsMS44MS0yLjcyLDAtNC4yN0w5Mi40NSwuNzZxLTEuOTQtMS4wNC00LjAxLC4xM2MtMTIuMDQsMTIuNDMtMjMuODMsMjQuNzQtMzYsMzcuNjktMS4yLDEuNDUtMS41LDMuNDQtLjc4LDUuMThsNC4yNywxNi41OGMwLDIuNzIsMS40Miw1LjU3LDIuMDcsOC4yOS00LjczLTUuNjEtOS43NC0xMC45Ny0xNS4wMi0xNi4wNi0xLjY4LTEuODEtMi41OS0xLjgxLTQuNCwwTDQuMzksODguMDVjLTEuNjgsMi4zMy0xLjgxLDIuMzMsMCw0LjUzbDM1Ljg3LDM3LjNjMS4zNiwxLjUzLDMuNSwyLjEsNS40NCwxLjQybDExLjQtMy4xMSwxMi45NS0zLjYzdi45MWMtNS4yOSw0LjE3LTEwLjIyLDguNzYtMTQuNzYsMTMuNzNxLTMuNjMsMi45OC0uNzgsNS4zMWwzMy40MSwzNC44NGMyLjIsMi4yLDIuOTgsMi4yLDUuMTgsMGwzNS40OC0zNy4xN2MxLjU5LTEuMzgsMi4xNi0zLjYsMS40Mi01LjU3LTEuNjgtNi4wOS0zLjI0LTEyLjMtNC43OS0xOC4zOS0uNzQtMi4yNy0xLjIyLTQuNjItMS40Mi02Ljk5LDQuMyw1LjkzLDkuMDcsMTEuNTIsMTQuMjUsMTYuNzEsMS42OCwxLjY4LDIuNzIsMS42OCw0LjQsMGwzNC4zMi0zNS43NHExLjU1LTEuODEsMC00LjAxWm0tNzIuMjYsMTUuMTVjLTMuMTEtLjc4LTYuMDktMS41NS05LjE5LTIuNTktMS43OC0uMzQtMy42MSwuMy00Ljc5LDEuNjhsLTEyLjk1LDEzLjg2Yy0uNzYsLjg1LTEuNDUsMS43Ni0yLjA3LDIuNzJoLS42NWMxLjMtNS4zMSwyLjcyLTEwLjYyLDQuMDEtMTUuOGwxLjY4LTYuNzNjLjg0LTIuMTgsLjE1LTQuNjUtMS42OC02LjA5bC0xMi45NS0xNC4xMmMtLjY0LS40NS0xLjE0LTEuMDgtMS40Mi0xLjgxbDE5LjA0LDUuMTgsMi41OSwuNzhjMi4wNCwuNzYsNC4zMywuMTQsNS43LTEuNTVsMTIuOTUtMTQuMzhzLjc4LTEuMDQsMS42OC0xLjE3Yy0xLjgxLDYuNi0yLjk4LDE0LjEyLTUuNDQsMjAuNDYtMS4wOCwyLjk2LS4wOCw2LjI4LDIuNDYsOC4xNiw0LjI3LDQuMTQsOC4yOSw4LjU1LDEyLjk1LDEyLjk1LDAsMCwxLjMsLjkxLDEuNDIsMi4wN2wtMTMuMzQtMy42M1oiLz48L3N2Zz4=)](https://github.com/keep-starknet-strange) 6 | 7 |
8 | Table of Contents 9 | 10 | - [About](#about) 11 | - [Getting Started](#getting-started) 12 | - [Demo](#demo) 13 | - [Prerequisites](#prerequisites) 14 | - [Usage standalone](#usage-standalone) 15 | - [Usage with starknet.js](#usage-with-starknetjs) 16 | - [Supported Cairo Types](#supported-cairo-types) 17 | - [Contributing](#contributing) 18 | - [Acknowledgements](#acknowledgements) 19 | 20 |
21 | 22 | ## About 23 | 24 | Abiwan is an UNLICENSE standalone TypeScript parser for Cairo smart contracts. 25 | It enables on the fly typechecking and autocompletion for contract calls directly in TypeScript. 26 | Developers can now catch typing mistakes early, prior to executing the call on-chain, and thus enhancing the overall Dapp development experience. 27 | 28 | ### Cairo versions 29 | 30 | Abiwan will support multiple Cairo compiler versions, but not in parallel - different package versions will support different Cairo versions. 31 | 32 | | Abiwan | Cairo compiler | 33 | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | 34 | | [1.0.3](https://www.npmjs.com/package/abi-wan-kanabi/v/1.0.3) | [Cairo v1.0.0](https://github.com/starkware-libs/cairo/releases/tag/v1.0.0)
[Cairo v1.1.0](https://github.com/starkware-libs/cairo/releases/tag/v1.1.0) | 35 | | [2.1.1](https://www.npmjs.com/package/abi-wan-kanabi/v/2.1.1) | [Cairo v2.3.0](https://github.com/starkware-libs/cairo/releases/tag/v2.3.0) | 36 | | [2.2.3](https://www.npmjs.com/package/abi-wan-kanabi/v/2.2.3) | [Cairo v2.4.4](https://github.com/starkware-libs/cairo/releases/tag/v2.4.4) | 37 | 38 | ## Getting Started 39 | 40 | 41 | ### Demo 42 | 43 | https://github.com/haroune-mohammedi/abi-wan-kanabi/assets/118889688/b7e20ab0-7314-402d-99fa-2888c20136c9 44 | 45 | 46 | ### Prerequisites 47 | 48 | Abiwan dependence only on typescript version 4.9.5 or higher. 49 | Also, it makes use of BigInt, so the `tsconfig.json` should target at least `ES2020`: 50 | 51 | ```json 52 | // tsconfig.json 53 | { 54 | "compilerOptions": { 55 | "target": "ES2020", 56 | "lib": ["ES2020", "ESNext"] 57 | } 58 | } 59 | ``` 60 | 61 | ### Usage standalone 62 | 63 | To use Abiwan, you must first export your ABI as const in a typescript file 64 | 65 | ```typescript 66 | export const ABI = [ 67 | //Your ABI here 68 | ] as const; 69 | ``` 70 | 71 | If you have a json file containing your contract class, you can use the CLI to generate the typescript file for you: 72 | 73 | ```bash 74 | npx abi-wan-kanabi --input /path/to/contract_class.json --output /path/to/abi.ts 75 | ``` 76 | 77 | You can then import it in any script and you are set to go: 78 | 79 | ```typescript 80 | import ABI from "./path/to/abi"; 81 | import { call } from "abi-wan-kanabi"; 82 | // You'll notice the editor is able to infer the types of the contract's functions 83 | // It'll give you autocompletion and typechecking 84 | const balance = call(ABI, "your_function_name", ["your", "function", "args"]); 85 | ``` 86 | 87 | > If you think that we should be able to import the ABI directly from the json files, we think so too! 88 | > See this typescript [issue](https://github.com/microsoft/TypeScript/issues/32063) and thumb it up! 89 | 90 | ### Usage with `starknet.js` 91 | 92 | Let's say you want to interact with the [Ekubo: Core](https://starkscan.co/contract/0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b) contract using starknet.js 93 | 94 | You need to first get the **ABI** of the contract and export it in a typescript file, you can do so using one command combining both [`starkli`](https://github.com/xJonathanLEI/starkli) (tested with version 0.2.3) and `npx abi-wan-kanabi`, the command will also print a helpful snippet that you can use to get started 95 | 96 | ```bash 97 | starkli class-at "0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b" --network mainnet | npx abi-wan-kanabi --input /dev/stdin --output abi.ts 98 | ``` 99 | 100 | ```javascript 101 | import { Contract, RpcProvider, constants } from "starknet"; 102 | import { ABI } from "./abi"; 103 | 104 | async function main() { 105 | const address = 106 | "0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b"; 107 | const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN }); 108 | const contract = new Contract(ABI, address, provider).typedv2(ABI); 109 | 110 | const version = await contract.getVersion(); 111 | console.log("version", version); 112 | 113 | // Abiwan is now successfully installed, just start writing your contract 114 | // function calls (`const ret = contract.your_function()`) and you'll get 115 | // helpful editor autocompletion, linting errors ... 116 | const primary_inteface_id = contract.get_primary_interface_id(); 117 | const protocol_fees_collected = contract.get_protocol_fees_collected("0x1"); 118 | } 119 | main().catch(console.error); 120 | ``` 121 | 122 | ## Configuration 123 | 124 | Abiwan's types are customizable using [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). Just extend the `Config` interface and override the types you want to change, see how `starknet.js` is doing it [here](https://github.com/starknet-io/starknet.js/blob/602a131d4abe05ada9c59aecf6bf165968c15c97/src/contract/interface.ts#L30:L43) 125 | 126 | ```typescript 127 | declare module "abi-wan-kanabi" { 128 | interface Config { 129 | FeltType: string; 130 | IntType: number; 131 | // ... 132 | } 133 | } 134 | ``` 135 | 136 | Check [`config.ts`](./config.ts) for all the available options and the their default values. 137 | 138 | ##  Supported Cairo Types 139 | 140 | Abiwan supports all of Cairo types, here's the mapping between Cairo types and Typescript types 141 | 142 | ### Primitive Types 143 | 144 | | Cairo | TypeScript | 145 | | ------------------ | ---------------------------- | 146 | | `felt252` | `string \| number \| bigint` | 147 | | `u8 - u32` | `number \| bigint`  | 148 | | `u64 - u256` | `number \| bigint \| U256`  | 149 | | `ContractAddress`  | `string` | 150 | | `EthAddress`  | `string` | 151 | | `ClassHash`  | `string` | 152 | | `bytes31`  | `string` | 153 | | `ByteArray`  | `string` | 154 | | `bool` | `boolean`  | 155 | | `()`  | `void` | 156 | 157 | ###  Complex Types 158 | 159 | | Cairo | TypeScript | 160 | | ------------------------- | --------------------------------------------------- | 161 | | `Option` | `T \| undefined` | 162 | | `Array` | `T[]` | 163 | | `Span` | `T[]` | 164 | | `tuple (T1, T2, ..., Tn)` | `[T1, T2, ..., Tn]`  | 165 | | `struct`  | an object where keys are struct member names | 166 | | `enum` | a union of objects, each enum variant is an object  | 167 | 168 | #### Struct example 169 | 170 | **Cairo:** 171 | 172 | ```cairo 173 | struct TestStruct { 174 | int128: u128, 175 | felt: felt252, 176 | tuple: (u32, u32) 177 | } 178 | ``` 179 | 180 | **Typescript:** 181 | 182 | ```typescript 183 | { 184 | int128: number | bigint | Uint256; 185 | felt: string | number | bigint; 186 | tuple: [number | bigint, number | bigint]; 187 | } 188 | ``` 189 | 190 | #### Enum example 191 | 192 | **Cairo:** 193 | 194 | ```cairo 195 | enum TestEnum { 196 | int128: u128, 197 | felt: felt252, 198 | tuple: (u32, u32), 199 | } 200 | ``` 201 | 202 | **Typescript:** 203 | 204 | ```typescript 205 | { int128: number | bigint | Uint256 } | 206 | { felt: string | number | bigint } | 207 | { tuple: [number | bigint, number | bigint]} 208 | ``` 209 | 210 | ## Contributing 211 | 212 | ### Run tests 213 | 214 | ```bash 215 | npm run typecheck 216 | ``` 217 | 218 | ### Generate `test/example.ts` 219 | 220 | ```bash 221 | # First build the example project with `scarb` 222 | cd test/example 223 | scarb build 224 | # Then generate test/example.ts 225 | cd ../.. 226 | npm run generate -- --input test/example/target/dev/example_example_contract.contract_class.json --output test/example.ts 227 | ``` 228 | 229 | Contributions on Abiwan are most welcome! 230 | If you are willing to contribute, please get in touch with one of the project leads or via the repositories [Discussions](https://github.com/keep-starknet-strange/abi-wan-kanabi/discussions/categories/general) 231 | 232 | ## Acknowledgements 233 | 234 | ### Authors and Contributors 235 | 236 | For a full list of all authors and contributors, see [the contributors page](https://github.com/keep-starknet-strange/abi-wan-kanabi/contributors). 237 | 238 | ### Special mentions 239 | 240 | Big thanks and shoutout to [Francesco](https://github.com/fracek)! :clap: who is at the origin of the project! 241 | 242 | Also thanks to the awesome Haroune ([@haroune-mohammedi](https://github.com/haroune-mohammedi)) and Thomas ([@thomas-quadratic](https://github.com/thomas-quadratic)) from [Quadratic](https://en.quadratic-labs.com/)! 243 | 244 | ### Other projects 245 | 246 | Abiwan is greatly influenced by the similar project for EVM-compatible contracts [wagmi/abitype](https://github.com/wagmi-dev/abitype). 247 | -------------------------------------------------------------------------------- /test/example.ts: -------------------------------------------------------------------------------- 1 | export const ABI = [ 2 | { 3 | type: 'impl', 4 | name: 'ExampleContract', 5 | interface_name: 'example::IExampleContract', 6 | }, 7 | { 8 | type: 'enum', 9 | name: 'core::bool', 10 | variants: [ 11 | { 12 | name: 'False', 13 | type: '()', 14 | }, 15 | { 16 | name: 'True', 17 | type: '()', 18 | }, 19 | ], 20 | }, 21 | { 22 | type: 'enum', 23 | name: 'core::option::Option::', 24 | variants: [ 25 | { 26 | name: 'Some', 27 | type: 'core::integer::u8', 28 | }, 29 | { 30 | name: 'None', 31 | type: '()', 32 | }, 33 | ], 34 | }, 35 | { 36 | type: 'enum', 37 | name: 'core::option::Option::>', 38 | variants: [ 39 | { 40 | name: 'Some', 41 | type: 'core::option::Option::', 42 | }, 43 | { 44 | name: 'None', 45 | type: '()', 46 | }, 47 | ], 48 | }, 49 | { 50 | type: 'struct', 51 | name: 'example::TestStruct', 52 | members: [ 53 | { 54 | name: 'int128', 55 | type: 'core::integer::u128', 56 | }, 57 | { 58 | name: 'felt', 59 | type: 'core::felt252', 60 | }, 61 | { 62 | name: 'tuple', 63 | type: '(core::integer::u32, core::integer::u32)', 64 | }, 65 | ], 66 | }, 67 | { 68 | type: 'enum', 69 | name: 'example::TestEnum', 70 | variants: [ 71 | { 72 | name: 'int128', 73 | type: 'core::integer::u128', 74 | }, 75 | { 76 | name: 'felt', 77 | type: 'core::felt252', 78 | }, 79 | { 80 | name: 'tuple', 81 | type: '(core::integer::u32, core::integer::u32)', 82 | }, 83 | { 84 | name: 'novalue', 85 | type: '()', 86 | }, 87 | ], 88 | }, 89 | { 90 | type: 'enum', 91 | name: 'core::result::Result::', 92 | variants: [ 93 | { 94 | name: 'Ok', 95 | type: 'core::integer::u8', 96 | }, 97 | { 98 | name: 'Err', 99 | type: 'core::integer::u8', 100 | }, 101 | ], 102 | }, 103 | { 104 | type: 'enum', 105 | name: 'core::result::Result::, core::integer::u8>', 106 | variants: [ 107 | { 108 | name: 'Ok', 109 | type: 'core::option::Option::', 110 | }, 111 | { 112 | name: 'Err', 113 | type: 'core::integer::u8', 114 | }, 115 | ], 116 | }, 117 | { 118 | type: 'struct', 119 | name: 'core::starknet::eth_address::EthAddress', 120 | members: [ 121 | { 122 | name: 'address', 123 | type: 'core::felt252', 124 | }, 125 | ], 126 | }, 127 | { 128 | type: 'struct', 129 | name: 'core::array::Span::', 130 | members: [ 131 | { 132 | name: 'snapshot', 133 | type: '@core::array::Array::', 134 | }, 135 | ], 136 | }, 137 | { 138 | type: 'struct', 139 | name: 'core::byte_array::ByteArray', 140 | members: [ 141 | { 142 | name: 'data', 143 | type: 'core::array::Array::', 144 | }, 145 | { 146 | name: 'pending_word', 147 | type: 'core::felt252', 148 | }, 149 | { 150 | name: 'pending_word_len', 151 | type: 'core::integer::u32', 152 | }, 153 | ], 154 | }, 155 | { 156 | type: 'interface', 157 | name: 'example::IExampleContract', 158 | items: [ 159 | { 160 | type: 'function', 161 | name: 'fn_felt', 162 | inputs: [ 163 | { 164 | name: 'felt', 165 | type: 'core::felt252', 166 | }, 167 | ], 168 | outputs: [], 169 | state_mutability: 'view', 170 | }, 171 | { 172 | type: 'function', 173 | name: 'fn_felt_u8_bool', 174 | inputs: [ 175 | { 176 | name: 'felt', 177 | type: 'core::felt252', 178 | }, 179 | { 180 | name: 'int8', 181 | type: 'core::integer::u8', 182 | }, 183 | { 184 | name: 'b', 185 | type: 'core::bool', 186 | }, 187 | ], 188 | outputs: [], 189 | state_mutability: 'view', 190 | }, 191 | { 192 | type: 'function', 193 | name: 'fn_felt_u8_bool_out_address_felt_u8_bool', 194 | inputs: [ 195 | { 196 | name: 'felt', 197 | type: 'core::felt252', 198 | }, 199 | { 200 | name: 'int8', 201 | type: 'core::integer::u8', 202 | }, 203 | { 204 | name: 'boolean', 205 | type: 'core::bool', 206 | }, 207 | ], 208 | outputs: [ 209 | { 210 | type: '(core::starknet::contract_address::ContractAddress, core::felt252, core::integer::u8, core::bool)', 211 | }, 212 | ], 213 | state_mutability: 'view', 214 | }, 215 | { 216 | type: 'function', 217 | name: 'fn_felt_out_felt', 218 | inputs: [ 219 | { 220 | name: 'felt', 221 | type: 'core::felt252', 222 | }, 223 | ], 224 | outputs: [ 225 | { 226 | type: 'core::felt252', 227 | }, 228 | ], 229 | state_mutability: 'view', 230 | }, 231 | { 232 | type: 'function', 233 | name: 'fn_out_simple_option', 234 | inputs: [], 235 | outputs: [ 236 | { 237 | type: 'core::option::Option::', 238 | }, 239 | ], 240 | state_mutability: 'view', 241 | }, 242 | { 243 | type: 'function', 244 | name: 'fn_out_nested_option', 245 | inputs: [], 246 | outputs: [ 247 | { 248 | type: 'core::option::Option::>', 249 | }, 250 | ], 251 | state_mutability: 'view', 252 | }, 253 | { 254 | type: 'function', 255 | name: 'fn_simple_array', 256 | inputs: [ 257 | { 258 | name: 'arg', 259 | type: 'core::array::Array::', 260 | }, 261 | ], 262 | outputs: [], 263 | state_mutability: 'view', 264 | }, 265 | { 266 | type: 'function', 267 | name: 'fn_out_simple_array', 268 | inputs: [], 269 | outputs: [ 270 | { 271 | type: 'core::array::Array::', 272 | }, 273 | ], 274 | state_mutability: 'view', 275 | }, 276 | { 277 | type: 'function', 278 | name: 'fn_struct', 279 | inputs: [ 280 | { 281 | name: 'arg', 282 | type: 'example::TestStruct', 283 | }, 284 | ], 285 | outputs: [], 286 | state_mutability: 'view', 287 | }, 288 | { 289 | type: 'function', 290 | name: 'fn_struct_array', 291 | inputs: [ 292 | { 293 | name: 'arg', 294 | type: 'core::array::Array::', 295 | }, 296 | ], 297 | outputs: [], 298 | state_mutability: 'view', 299 | }, 300 | { 301 | type: 'function', 302 | name: 'fn_enum', 303 | inputs: [ 304 | { 305 | name: 'arg', 306 | type: 'example::TestEnum', 307 | }, 308 | ], 309 | outputs: [], 310 | state_mutability: 'view', 311 | }, 312 | { 313 | type: 'function', 314 | name: 'fn_enum_array', 315 | inputs: [ 316 | { 317 | name: 'arg', 318 | type: 'core::array::Array::', 319 | }, 320 | ], 321 | outputs: [], 322 | state_mutability: 'view', 323 | }, 324 | { 325 | type: 'function', 326 | name: 'fn_out_enum_array', 327 | inputs: [], 328 | outputs: [ 329 | { 330 | type: 'core::array::Array::', 331 | }, 332 | ], 333 | state_mutability: 'view', 334 | }, 335 | { 336 | type: 'function', 337 | name: 'fn_result', 338 | inputs: [ 339 | { 340 | name: 'arg', 341 | type: 'core::result::Result::', 342 | }, 343 | ], 344 | outputs: [], 345 | state_mutability: 'view', 346 | }, 347 | { 348 | type: 'function', 349 | name: 'fn_out_result', 350 | inputs: [], 351 | outputs: [ 352 | { 353 | type: 'core::result::Result::', 354 | }, 355 | ], 356 | state_mutability: 'view', 357 | }, 358 | { 359 | type: 'function', 360 | name: 'fn_nested_result', 361 | inputs: [ 362 | { 363 | name: 'arg', 364 | type: 'core::result::Result::, core::integer::u8>', 365 | }, 366 | ], 367 | outputs: [], 368 | state_mutability: 'view', 369 | }, 370 | { 371 | type: 'function', 372 | name: 'fn_eth_address', 373 | inputs: [ 374 | { 375 | name: 'arg', 376 | type: 'core::starknet::eth_address::EthAddress', 377 | }, 378 | ], 379 | outputs: [], 380 | state_mutability: 'view', 381 | }, 382 | { 383 | type: 'function', 384 | name: 'fn_span', 385 | inputs: [ 386 | { 387 | name: 'arg', 388 | type: 'core::array::Span::', 389 | }, 390 | ], 391 | outputs: [], 392 | state_mutability: 'view', 393 | }, 394 | { 395 | type: 'function', 396 | name: 'fn_bytes31', 397 | inputs: [ 398 | { 399 | name: 'arg', 400 | type: 'core::bytes_31::bytes31', 401 | }, 402 | ], 403 | outputs: [], 404 | state_mutability: 'view', 405 | }, 406 | { 407 | type: 'function', 408 | name: 'fn_byte_array', 409 | inputs: [ 410 | { 411 | name: 'arg', 412 | type: 'core::byte_array::ByteArray', 413 | }, 414 | ], 415 | outputs: [], 416 | state_mutability: 'view', 417 | }, 418 | ], 419 | }, 420 | { 421 | type: 'event', 422 | name: 'example::example_contract::CounterIncreased', 423 | kind: 'struct', 424 | members: [ 425 | { 426 | name: 'amount', 427 | type: 'core::integer::u128', 428 | kind: 'data', 429 | }, 430 | ], 431 | }, 432 | { 433 | type: 'event', 434 | name: 'example::example_contract::CounterDecreased', 435 | kind: 'struct', 436 | members: [ 437 | { 438 | name: 'amount', 439 | type: 'core::integer::u128', 440 | kind: 'data', 441 | }, 442 | ], 443 | }, 444 | { 445 | type: 'event', 446 | name: 'example::example_contract::MyStruct', 447 | kind: 'struct', 448 | members: [ 449 | { 450 | name: 'member', 451 | type: 'core::integer::u128', 452 | kind: 'data', 453 | }, 454 | ], 455 | }, 456 | { 457 | type: 'event', 458 | name: 'example::example_contract::MyEnum', 459 | kind: 'enum', 460 | variants: [ 461 | { 462 | name: 'Var1', 463 | type: 'example::example_contract::MyStruct', 464 | kind: 'nested', 465 | }, 466 | ], 467 | }, 468 | { 469 | type: 'event', 470 | name: 'example::example_contract::Event', 471 | kind: 'enum', 472 | variants: [ 473 | { 474 | name: 'TestCounterIncreased', 475 | type: 'example::example_contract::CounterIncreased', 476 | kind: 'nested', 477 | }, 478 | { 479 | name: 'TestCounterDecreased', 480 | type: 'example::example_contract::CounterDecreased', 481 | kind: 'nested', 482 | }, 483 | { 484 | name: 'TestEnum', 485 | type: 'example::example_contract::MyEnum', 486 | kind: 'nested', 487 | }, 488 | ], 489 | }, 490 | ] as const 491 | -------------------------------------------------------------------------------- /kanabi.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedConfig } from './config' 2 | 3 | export type CairoFelt = 'core::felt252' 4 | type MBits = 8 | 16 | 32 5 | type BigMBits = 64 | 128 6 | export type CairoInt = `${'core::integer::u'}${MBits}` 7 | export type CairoBigInt = `${'core::integer::u'}${BigMBits}` 8 | export type CairoU256 = 'core::integer::u256' 9 | export type CairoU512 = 'core::integer::u512' 10 | export type CairoContractAddress = 11 | 'core::starknet::contract_address::ContractAddress' 12 | export type CairoEthAddress = 'core::starknet::eth_address::EthAddress' 13 | export type CairoClassHash = 'core::starknet::class_hash::ClassHash' 14 | export type CairoFunction = 'function' 15 | export type CairoVoid = '()' 16 | export type CairoBool = 'core::bool' 17 | export type CairoBytes31 = 'core::bytes_31::bytes31' 18 | export type CairoByteArray = 'core::byte_array::ByteArray' 19 | export type CairoSecp256k1Point = 'core::starknet::secp256k1::Secp256k1Point' 20 | 21 | /// Implementation of tuples 22 | type MAX_TUPLE_SIZE = 20 23 | 24 | // Question: why do we need both R and A here ? 25 | type _BuildTuple< 26 | R extends unknown = never, 27 | A extends string = '', 28 | D extends readonly number[] = [], 29 | > = D['length'] extends MAX_TUPLE_SIZE 30 | ? `${A})` | R 31 | : A extends '' 32 | ? _BuildTuple 33 | : _BuildTuple<`${A})` | R, `${A}, ${string}`, [...D, 1]> 34 | 35 | export type CairoTuple = _BuildTuple 36 | 37 | type AbiType = 38 | | CairoFelt 39 | | CairoFunction 40 | | CairoInt 41 | | CairoBigInt 42 | | CairoU256 43 | | CairoU512 44 | | CairoContractAddress 45 | | CairoEthAddress 46 | | CairoClassHash 47 | | CairoBool 48 | | CairoVoid 49 | | CairoBytes31 50 | | CairoByteArray 51 | | CairoSecp256k1Point 52 | 53 | // We have to use string to support nesting 54 | type CairoOptionGeneric = `core::option::Option::<${T}>` 55 | type CairoArrayGeneric = 56 | | `core::array::Array::<${T}>` 57 | | `core::array::Span::<${T}>` 58 | type CairoResultGeneric< 59 | T extends string, 60 | E extends string, 61 | > = `core::result::Result::<${T}, ${E}>` 62 | type CairoGeneric = 63 | | CairoOptionGeneric 64 | | CairoArrayGeneric 65 | | CairoResultGeneric 66 | 67 | export type Option = ResolvedConfig['Option'] 68 | export type Tuple = ResolvedConfig['Tuple'] 69 | export type Result = ResolvedConfig['Result'] 70 | export type Enum = ResolvedConfig['Enum'] 71 | export type Calldata = ResolvedConfig['Calldata'] 72 | export type InvokeOptions = ResolvedConfig['InvokeOptions'] 73 | export type CallOptions = ResolvedConfig['CallOptions'] 74 | export type InvokeFunctionResponse = ResolvedConfig['InvokeFunctionResponse'] 75 | export type Call = ResolvedConfig['Call'] 76 | 77 | type AbiParameter = { 78 | name: string 79 | type: string 80 | } 81 | 82 | type AbiOutput = { 83 | type: string 84 | } 85 | 86 | type AbiStateMutability = 'view' | 'external' 87 | 88 | type AbiImpl = { 89 | type: 'impl' 90 | name: string 91 | interface_name: string 92 | } 93 | 94 | type AbiInterface = { 95 | type: 'interface' 96 | name: string 97 | items: readonly AbiFunction[] 98 | } 99 | 100 | type AbiConstructor = { 101 | type: 'constructor' 102 | name: 'constructor' 103 | inputs: readonly AbiParameter[] 104 | } 105 | 106 | type AbiFunction = { 107 | type: 'function' 108 | name: string 109 | inputs: readonly AbiParameter[] 110 | outputs: readonly AbiOutput[] 111 | state_mutability: AbiStateMutability 112 | } 113 | 114 | // TODO: Do we need to handle 'key' and 'data' differently ? 115 | // TODO: 'flat' is found in some ABIs but it's not mentioned in the ABI spec: 116 | // https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_metadata.json#L507:L509 117 | type AbiEventKind = 'nested' | 'data' | 'key' | 'flat' 118 | 119 | export type AbiEventMember = { 120 | name: string 121 | type: string 122 | kind: AbiEventKind 123 | } 124 | 125 | type AbiEventStruct = { 126 | type: 'event' 127 | name: string 128 | kind: 'struct' 129 | members: readonly AbiEventMember[] 130 | } 131 | 132 | type AbiEventEnum = { 133 | type: 'event' 134 | name: string 135 | kind: 'enum' 136 | variants: readonly AbiEventMember[] 137 | } 138 | 139 | type AbiEvent = AbiEventStruct | AbiEventEnum 140 | 141 | type AbiMember = { 142 | name: string 143 | type: string 144 | } 145 | 146 | type AbiStruct = { 147 | type: 'struct' 148 | name: string 149 | members: readonly AbiMember[] 150 | } 151 | 152 | type AbiEnum = { 153 | type: 'enum' 154 | name: string 155 | variants: readonly AbiParameter[] 156 | } 157 | 158 | export type Abi = readonly ( 159 | | AbiImpl 160 | | AbiInterface 161 | | AbiConstructor 162 | | AbiFunction 163 | | AbiStruct 164 | | AbiEnum 165 | | AbiEvent 166 | )[] 167 | 168 | /// Implement 169 | type _BuildArgs< 170 | TAbi extends Abi, 171 | TAbiParam extends readonly AbiParameter[], 172 | R extends unknown[], 173 | > = R['length'] extends TAbiParam['length'] 174 | ? R 175 | : _BuildArgs< 176 | TAbi, 177 | TAbiParam, 178 | [...R, StringToPrimitiveType] 179 | > 180 | 181 | export type FunctionArgs< 182 | TAbi extends Abi, 183 | TFunctionName extends ExtractAbiFunctionNames, 184 | > = ExtractAbiFunction['inputs'] extends readonly [] 185 | ? [] 186 | : _BuildArgs< 187 | TAbi, 188 | ExtractAbiFunction['inputs'], 189 | [] 190 | > extends [infer T] 191 | ? T 192 | : _BuildArgs['inputs'], []> 193 | 194 | export type FunctionRet< 195 | TAbi extends Abi, 196 | TFunctionName extends ExtractAbiFunctionNames, 197 | > = ExtractAbiFunction['outputs'] extends readonly [] 198 | ? void 199 | : StringToPrimitiveType< 200 | TAbi, 201 | ExtractAbiFunction['outputs'][0]['type'] 202 | > 203 | 204 | export type ExtractAbiImpls = Extract< 205 | TAbi[number], 206 | { type: 'impl' } 207 | > 208 | 209 | export type ExtractAbiInterfaces = Extract< 210 | TAbi[number], 211 | { type: 'interface' } 212 | > 213 | 214 | export type ExtractAbiFunctions = 215 | | Extract['items'][number], { type: 'function' }> 216 | | Extract 217 | 218 | export type ExtractAbiFunctionNames = 219 | ExtractAbiFunctions['name'] 220 | 221 | export type ExtractAbiFunction< 222 | TAbi extends Abi, 223 | TFunctionName extends ExtractAbiFunctionNames, 224 | > = Extract, { name: TFunctionName }> 225 | 226 | export type ExtractAbiStructs = Extract< 227 | TAbi[number], 228 | { type: 'struct' } 229 | > 230 | 231 | export type ExtractAbiStructNames = 232 | ExtractAbiStructs['name'] 233 | 234 | export type ExtractAbiStruct< 235 | TAbi extends Abi, 236 | TStructName extends ExtractAbiStructNames, 237 | > = Extract, { name: TStructName }> 238 | 239 | export type ExtractAbiEnums = Extract< 240 | TAbi[number], 241 | { type: 'enum' } 242 | > 243 | 244 | export type ExtractAbiEnumNames = 245 | ExtractAbiEnums['name'] 246 | 247 | export type ExtractAbiEnum< 248 | TAbi extends Abi, 249 | TEnumName extends ExtractAbiEnumNames, 250 | > = Extract, { name: TEnumName }> 251 | 252 | export type ExtractAbiEvents = Extract< 253 | TAbi[number], 254 | { type: 'event' } 255 | > 256 | 257 | export type ExtractAbiEventNames = 258 | ExtractAbiEvents['name'] 259 | 260 | export type ExtractAbiEvent< 261 | TAbi extends Abi, 262 | TEventName extends ExtractAbiEventNames, 263 | > = Extract, { name: TEventName }> 264 | 265 | // Question: why do we need TAbi extends Abi here, it's not used ? 266 | type PrimitiveTypeLookup = { 267 | [_ in CairoFelt]: ResolvedConfig['FeltType'] 268 | } & { 269 | [_ in CairoFunction]: number 270 | } & { 271 | [_ in CairoInt]: ResolvedConfig['IntType'] 272 | } & { 273 | [_ in CairoU256]: ResolvedConfig['U256Type'] 274 | } & { 275 | [_ in CairoU512]: ResolvedConfig['U512Type'] 276 | } & { 277 | [_ in CairoBigInt]: ResolvedConfig['BigIntType'] 278 | } & { 279 | [_ in CairoContractAddress]: ResolvedConfig['AddressType'] 280 | } & { 281 | [_ in CairoEthAddress]: ResolvedConfig['AddressType'] 282 | } & { 283 | [_ in CairoClassHash]: ResolvedConfig['ClassHashType'] 284 | } & { 285 | [_ in CairoVoid]: void 286 | } & { 287 | [_ in CairoBool]: boolean 288 | } & { 289 | [_ in CairoBytes31]: ResolvedConfig['Bytes31Type'] 290 | } & { 291 | [_ in CairoByteArray]: ResolvedConfig['ByteArray'] 292 | } & { 293 | [_ in CairoSecp256k1Point]: ResolvedConfig['Secp256k1PointType'] 294 | } 295 | 296 | export type AbiTypeToPrimitiveType< 297 | TAbi extends Abi, 298 | TAbiType extends AbiType, 299 | > = PrimitiveTypeLookup[TAbiType] 300 | 301 | export type GenericTypeToPrimitiveType< 302 | TAbi extends Abi, 303 | G extends string, 304 | > = G extends CairoOptionGeneric 305 | ? T extends AbiType 306 | ? Option> 307 | : Option> 308 | : G extends CairoArrayGeneric 309 | ? T extends AbiType 310 | ? AbiTypeToPrimitiveType[] 311 | : StringToPrimitiveType[] 312 | : G extends CairoResultGeneric 313 | ? T extends AbiType 314 | ? E extends AbiType 315 | ? Result, AbiTypeToPrimitiveType> 316 | : Result, StringToPrimitiveType> 317 | : Result, StringToPrimitiveType> 318 | : unknown 319 | 320 | export type CairoTupleToPrimitive< 321 | TAbi extends Abi, 322 | T extends string, 323 | > = T extends `(${infer first}, ${infer remaining})` 324 | ? [ 325 | StringToPrimitiveType, 326 | ...CairoTupleToPrimitive, 327 | ] 328 | : T extends `(${infer first})` 329 | ? [StringToPrimitiveType] 330 | : [unknown] 331 | 332 | // Convert an object {k1: v1, k2: v2, ...} to a union type of objects with each 333 | // a single element {k1: v1} | {k2: v2} | ... 334 | type ObjectToUnion> = { 335 | [K in keyof T]: { [Key in K]: T[K] } 336 | }[keyof T] 337 | 338 | export type EventToPrimitiveType< 339 | TAbi extends Abi, 340 | TEventName extends ExtractAbiEventNames, 341 | > = ExtractAbiEvent extends { 342 | type: 'event' 343 | kind: 'struct' 344 | members: infer TMembers extends readonly AbiEventMember[] 345 | } 346 | ? { 347 | [Member in TMembers[number] as Member['name']]: StringToPrimitiveType< 348 | TAbi, 349 | Member['type'] 350 | > 351 | } 352 | : ExtractAbiEvent extends { 353 | type: 'event' 354 | kind: 'enum' 355 | variants: infer TVariants extends readonly AbiEventMember[] 356 | } 357 | ? ObjectToUnion<{ 358 | [Variant in TVariants[number] as Variant['name']]: StringToPrimitiveType< 359 | TAbi, 360 | Variant['type'] 361 | > 362 | }> 363 | : never 364 | 365 | export type StringToPrimitiveTypeS< 366 | TAbi extends Abi, 367 | T extends string, 368 | > = ExtractAbiEnum 369 | 370 | export type StringToPrimitiveType< 371 | TAbi extends Abi, 372 | T extends string, 373 | > = T extends AbiType 374 | ? AbiTypeToPrimitiveType 375 | : T extends CairoGeneric 376 | ? GenericTypeToPrimitiveType 377 | : T extends CairoTuple 378 | ? Tuple extends never 379 | ? CairoTupleToPrimitive 380 | : Tuple 381 | : ExtractAbiStruct extends never 382 | ? ExtractAbiEnum extends never 383 | ? unknown 384 | : Enum extends never 385 | ? ExtractAbiEnum extends { 386 | type: 'enum' 387 | variants: infer TVariants extends readonly AbiParameter[] 388 | } 389 | ? ObjectToUnion<{ 390 | [Variant in 391 | TVariants[number] as Variant['name']]: StringToPrimitiveType< 392 | TAbi, 393 | Variant['type'] 394 | > 395 | }> 396 | : // We should never have a type T where ExtractAbiEnum 397 | // return something different than an enum 398 | never 399 | : Enum 400 | : ExtractAbiStruct extends { 401 | type: 'struct' 402 | members: infer TMembers extends readonly AbiMember[] 403 | } 404 | ? { 405 | [Member in TMembers[number] as Member['name']]: StringToPrimitiveType< 406 | TAbi, 407 | Member['type'] 408 | > 409 | } 410 | : // We should never have a type T where ExtractAbiStruct 411 | // return something different than a struct 412 | never 413 | 414 | type UnionToIntersection = ( 415 | Union extends unknown 416 | ? (arg: Union) => unknown 417 | : never 418 | ) extends (arg: infer R) => unknown 419 | ? R 420 | : never 421 | 422 | export type FunctionCallWithCallData< 423 | TAbi extends Abi, 424 | TAbiFunction extends AbiFunction, 425 | > = ( 426 | calldata: Calldata, 427 | ) => TAbiFunction['state_mutability'] extends 'view' 428 | ? Promise> 429 | : InvokeFunctionResponse 430 | 431 | export type ExtractArgs< 432 | TAbi extends Abi, 433 | TAbiFunction extends AbiFunction, 434 | > = TAbiFunction['inputs'] extends infer TInput extends readonly AbiParameter[] 435 | ? { 436 | [K3 in 437 | keyof TInput]: TInput[K3] extends infer TInputParam extends AbiParameter 438 | ? StringToPrimitiveType 439 | : never 440 | } 441 | : never 442 | 443 | export type FunctionCallWithArgs< 444 | TAbi extends Abi, 445 | TAbiFunction extends AbiFunction, 446 | > = ( 447 | ...args: ExtractArgs 448 | ) => TAbiFunction['state_mutability'] extends 'view' 449 | ? Promise> 450 | : InvokeFunctionResponse 451 | 452 | export type FunctionCallWithOptions< 453 | TAbi extends Abi, 454 | TAbiFunction extends AbiFunction, 455 | > = TAbiFunction['state_mutability'] extends 'view' 456 | ? ( 457 | ...args: [...ExtractArgs, CallOptions] 458 | ) => Promise> 459 | : ( 460 | ...args: [...ExtractArgs, InvokeOptions] 461 | ) => InvokeFunctionResponse 462 | 463 | export type FunctionCall< 464 | TAbi extends Abi, 465 | TAbiFunction extends AbiFunction, 466 | > = FunctionCallWithArgs & 467 | FunctionCallWithCallData & 468 | FunctionCallWithOptions 469 | 470 | export type ContractFunctions = { 471 | [K in ExtractAbiFunctionNames]: FunctionCall< 472 | TAbi, 473 | ExtractAbiFunction 474 | > 475 | } 476 | 477 | export type FunctionPopulateTransaction< 478 | TAbi extends Abi, 479 | TAbiFunction extends AbiFunction, 480 | > = (...args: [...ExtractArgs]) => Call 481 | 482 | export type ContractFunctionsPopulateTransaction = { 483 | [K in ExtractAbiFunctionNames]: FunctionPopulateTransaction< 484 | TAbi, 485 | ExtractAbiFunction 486 | > 487 | } 488 | -------------------------------------------------------------------------------- /test/kanabi.test-d.ts: -------------------------------------------------------------------------------- 1 | import { TypedContract } from '..' 2 | import { 3 | AbiTypeToPrimitiveType, 4 | CairoBigInt, 5 | CairoByteArray, 6 | CairoBytes31, 7 | CairoContractAddress, 8 | CairoEthAddress, 9 | CairoFelt, 10 | CairoFunction, 11 | CairoInt, 12 | CairoSecp256k1Point, 13 | CairoTuple, 14 | CairoU256, 15 | CairoU512, 16 | CairoVoid, 17 | EventToPrimitiveType, 18 | ExtractAbiFunction, 19 | ExtractAbiFunctionNames, 20 | FunctionArgs, 21 | FunctionRet, 22 | StringToPrimitiveType, 23 | } from '../kanabi' 24 | import { ABI } from './example' 25 | import { assertType, expectTypeOf, test } from 'vitest' 26 | 27 | type TAbi = typeof ABI 28 | 29 | function returnVoid() {} 30 | 31 | const voidValue = returnVoid() 32 | const bigIntValue = 105n 33 | const intValue = 10 34 | const stringValue = 's' 35 | const emptyArray: [] = [] 36 | const boolValue = true 37 | 38 | test('Cairo Types', () => { 39 | assertType('core::felt252') 40 | assertType('core::integer::u8') 41 | assertType('core::integer::u16') 42 | assertType('core::integer::u32') 43 | assertType('core::integer::u64') 44 | assertType('core::integer::u128') 45 | assertType('core::integer::u256') 46 | assertType('core::integer::u512') 47 | assertType( 48 | 'core::starknet::contract_address::ContractAddress', 49 | ) 50 | assertType('core::starknet::eth_address::EthAddress') 51 | assertType('core::starknet::secp256k1::Secp256k1Point') 52 | assertType('function') 53 | assertType('()') 54 | assertType('()') 55 | assertType('(1)') 56 | assertType('(1, 2n)') 57 | assertType("(1, 2n, 'string')") 58 | assertType('(1, 2, 3, 4, 5)') 59 | }) 60 | 61 | test('Cairo Types Errors', () => { 62 | // @ts-expect-error 63 | assertType('core::integer:u8') 64 | // @ts-expect-error 65 | assertType('core::integer::u64') 66 | // @ts-expect-error 67 | assertType('core::integer::u128') 68 | // @ts-expect-error 69 | assertType('core::integer::u256') 70 | // @ts-expect-error 71 | assertType('core::integer::u8') 72 | // @ts-expect-error 73 | assertType('core::integer::u16') 74 | // @ts-expect-error 75 | assertType('core::integer::u32') 76 | // @ts-expect-error 77 | assertType('core::felt') 78 | // @ts-expect-error 79 | assertType('core::integer::u8') 80 | // @ts-expect-error 81 | assertType('core::integer::u8') 82 | // @ts-expect-error 83 | assertType('function') 84 | // @ts-expect-error 85 | assertType('(') 86 | }) 87 | 88 | test('FunctionArgs', () => { 89 | assertType>(bigIntValue) 90 | assertType>([ 91 | bigIntValue, 92 | intValue, 93 | boolValue, 94 | ]) 95 | assertType>([ 96 | bigIntValue, 97 | intValue, 98 | boolValue, 99 | ]) 100 | 101 | assertType>({ 102 | felt: bigIntValue, 103 | int128: bigIntValue, 104 | tuple: [intValue, intValue], 105 | }) 106 | assertType>([ 107 | { felt: bigIntValue, int128: bigIntValue, tuple: [intValue, intValue] }, 108 | ]) 109 | 110 | assertType>([intValue, intValue]) 111 | 112 | assertType>([]) 113 | 114 | assertType>({ felt: bigIntValue }) 115 | assertType>({ int128: bigIntValue }) 116 | assertType>({ tuple: [intValue, intValue] }) 117 | 118 | assertType>([ 119 | { felt: bigIntValue }, 120 | { int128: bigIntValue }, 121 | { tuple: [intValue, intValue] }, 122 | ]) 123 | 124 | assertType>({ Ok: bigIntValue }) 125 | assertType>({ Err: intValue }) 126 | 127 | assertType>({ Ok: intValue }) 128 | assertType>({ Ok: undefined }) 129 | assertType>({ Err: bigIntValue }) 130 | 131 | assertType>(stringValue) 132 | 133 | assertType>([ 134 | { felt: bigIntValue, int128: bigIntValue, tuple: [intValue, intValue] }, 135 | ]) 136 | 137 | assertType>(stringValue) 138 | assertType>(stringValue) 139 | }) 140 | 141 | test('FunctionArgs Errors', () => { 142 | assertType>( 143 | // @ts-expect-error 144 | [bigIntValue, bigIntValue], 145 | ) 146 | // @ts-expect-error 147 | assertType>(boolValue) 148 | assertType>([ 149 | bigIntValue, 150 | intValue, 151 | // @ts-expect-error 152 | intValue, 153 | ]) 154 | // @ts-expect-error 155 | assertType>([ 156 | bigIntValue, 157 | intValue, 158 | ]) 159 | assertType>({ 160 | felt: bigIntValue, 161 | int128: bigIntValue, 162 | // @ts-expect-error 163 | tuple: [intValue, boolValue], 164 | }) 165 | // @ts-expect-error 166 | assertType>({ 167 | int128: bigIntValue, 168 | tuple: [intValue, intValue], 169 | }) 170 | // @ts-expect-error 171 | assertType>([intValue]) 172 | // @ts-expect-error 173 | assertType>({ Ok: voidValue }) 174 | // @ts-expect-error 175 | assertType>({ Err: boolValue }) 176 | // @ts-expect-error 177 | assertType>({ Ok: emptyArray }) 178 | // @ts-expect-error 179 | assertType>({ Err: stringValue }) 180 | // @ts-expect-error 181 | assertType>(voidValue) 182 | // @ts-expect-error 183 | assertType>([bigIntValue]) 184 | // @ts-expect-error 185 | assertType>(voidValue) 186 | // @ts-expect-error 187 | assertType>(intValue) 188 | }) 189 | 190 | test('FunctionRet', () => { 191 | assertType>(bigIntValue) 192 | 193 | assertType>([ 194 | stringValue, 195 | bigIntValue, 196 | intValue, 197 | boolValue, 198 | ]) 199 | assertType>([intValue, intValue]) 200 | 201 | assertType>(intValue) 202 | assertType>(undefined) 203 | 204 | assertType>(intValue) 205 | assertType>(undefined) 206 | 207 | assertType>([]) 208 | assertType>([ 209 | { felt: bigIntValue }, 210 | { int128: bigIntValue }, 211 | { tuple: [intValue, intValue] }, 212 | ]) 213 | 214 | assertType>({ Ok: intValue }) 215 | assertType>({ Err: bigIntValue }) 216 | }) 217 | 218 | test('FunctionRet Errors', () => { 219 | // @ts-expect-error 220 | assertType>(intValue) 221 | // @ts-expect-error 222 | assertType>(bigIntValue) 223 | assertType>( 224 | // @ts-expect-error 225 | emptyArray, 226 | ) 227 | // @ts-expect-error 228 | assertType>(intValue) 229 | // @ts-expect-error 230 | assertType>(emptyArray) 231 | assertType>([ 232 | { felt: bigIntValue }, 233 | { int128: bigIntValue }, 234 | { tuple: [intValue, intValue] }, 235 | // @ts-expect-error 236 | { x: 1 }, 237 | ]) 238 | // @ts-expect-error 239 | assertType>([intValue, intValue]) 240 | // @ts-expect-error 241 | assertType>(voidValue) 242 | // @ts-expect-error 243 | assertType>({ Ok: stringValue }) 244 | // @ts-expect-error 245 | assertType>({ Err: emptyArray }) 246 | }) 247 | 248 | test('ExtractAbiFunction', () => { 249 | const fnValue = { 250 | type: 'function', 251 | name: 'fn_felt_out_felt', 252 | inputs: [ 253 | { 254 | name: 'felt', 255 | type: 'core::felt252', 256 | }, 257 | ], 258 | outputs: [ 259 | { 260 | type: 'core::felt252', 261 | }, 262 | ], 263 | state_mutability: 'view', 264 | } as const 265 | assertType>(fnValue) 266 | }) 267 | 268 | test('ExtractAbiFunctionName', () => { 269 | type Expected = 270 | | 'fn_felt' 271 | | 'fn_felt_u8_bool' 272 | | 'fn_felt_u8_bool_out_address_felt_u8_bool' 273 | | 'fn_felt_out_felt' 274 | | 'fn_out_simple_option' 275 | | 'fn_out_nested_option' 276 | | 'fn_simple_array' 277 | | 'fn_out_simple_array' 278 | | 'fn_struct_array' 279 | | 'fn_struct' 280 | | 'fn_enum' 281 | | 'fn_enum_array' 282 | | 'fn_out_enum_array' 283 | | 'fn_result' 284 | | 'fn_out_result' 285 | | 'fn_nested_result' 286 | | 'fn_eth_address' 287 | | 'fn_span' 288 | | 'fn_bytes31' 289 | | 'fn_byte_array' 290 | 291 | expectTypeOf>().toEqualTypeOf() 292 | }) 293 | 294 | test('AbiTypeToPrimitiveType', () => { 295 | assertType>(intValue) 296 | assertType>(bigIntValue) 297 | assertType>(stringValue) 298 | 299 | assertType>(intValue) 300 | assertType>(bigIntValue) 301 | 302 | assertType>(intValue) 303 | assertType>(bigIntValue) 304 | 305 | assertType>(intValue) 306 | assertType>(stringValue) 307 | assertType>(stringValue) 308 | assertType>(stringValue) 309 | assertType>(intValue) 310 | assertType>(voidValue) 311 | 312 | assertType>(stringValue) 313 | assertType>(stringValue) 314 | assertType>(stringValue) 315 | }) 316 | 317 | test('AbiTypeToPrimitiveType Errors', () => { 318 | // @ts-expect-error CairoFelt should be number, bigint or string 319 | assertType>(boolValue) 320 | // @ts-expect-error CairoInt should be number or bigint 321 | assertType>(voidValue) 322 | // @ts-expect-error CairoBigInt should be number or bigint 323 | assertType>(voidValue) 324 | // @ts-expect-error ContractAddress should be string 325 | assertType>(intValue) 326 | // @ts-expect-error EthAddress should be string 327 | assertType>(intValue) 328 | // @ts-expect-error CairoFunction should be int 329 | assertType>(bigIntValue) 330 | // @ts-expect-error CairoVoid should be void 331 | assertType>(intValue) 332 | // @ts-expect-error CairoBytes31 should be string 333 | assertType>(bigIntValue) 334 | // @ts-expect-error CairoByteArray should be string 335 | assertType>(voidValue) 336 | }) 337 | 338 | test('StringToPrimitiveType', () => { 339 | // TODO: add tests for struct, enum and tuple 340 | assertType>(bigIntValue) 341 | assertType>(bigIntValue) 342 | assertType>(intValue) 343 | assertType>(bigIntValue) 344 | assertType>(bigIntValue) 345 | assertType>(stringValue) 346 | assertType>(stringValue) 347 | assertType>(intValue) 348 | assertType>(voidValue) 349 | assertType>(stringValue) 350 | assertType>(stringValue) 351 | }) 352 | 353 | test('StringToPrimitiveType Errors', () => { 354 | // TODO: add tests for struct, enum and tuple 355 | // @ts-expect-error CairoFelt should be bigint 356 | assertType>(voidValue) 357 | // @ts-expect-error CairoInt should be number or bigint 358 | assertType>(voidValue) 359 | // @ts-expect-error CairoBigInt should be bigint 360 | assertType>(boolValue) 361 | // @ts-expect-error ContractAddress should be bigint 362 | assertType>(intValue) 363 | // @ts-expect-error EthAddress should be bigint 364 | assertType>(intValue) 365 | // @ts-expect-error CairoFunction should be int 366 | assertType>(bigIntValue) 367 | // @ts-expect-error CairoVoid should be void 368 | assertType>(intValue) 369 | // @ts-expect-error CairoBytes31 should be string 370 | assertType>(intValue) 371 | // @ts-expect-error CairoByteArray should be string 372 | assertType>(bigIntValue) 373 | }) 374 | 375 | test('ContractFunctions', () => { 376 | // @ts-expect-error 377 | const contract: TypedContract = never 378 | 379 | contract.fn_felt(1) // Call with args 380 | contract.fn_felt(1, { parseRequest: true }) // Call withe invokeOptions 381 | contract.fn_felt(['0x0', '0x1']) // call with CallData 382 | 383 | // @ts-expect-error fn_felt argument should be string | number | bigint 384 | contract.fn_felt(true) 385 | 386 | contract.fn_out_simple_option() 387 | contract.fn_out_simple_option({ parseResponse: true }) 388 | contract.fn_out_simple_option(['0x0', '0x1']) 389 | }) 390 | 391 | test('ContractFunctionsPopulateTransaction', () => { 392 | // @ts-expect-error 393 | const contract: TypedContract = never 394 | 395 | // @ts-expect-error 396 | contract.populateTransaction.fn_felt() 397 | contract.populateTransaction.fn_felt(1) 398 | contract.populateTransaction.fn_simple_array([1, 2, 3]) 399 | }) 400 | 401 | test('populate', () => { 402 | // @ts-expect-error 403 | const contract: TypedContract = never 404 | 405 | contract.populate('fn_felt', 1n) 406 | contract.populate('fn_felt_u8_bool', [1n, 2, true]) 407 | }) 408 | 409 | test('StringToPrimitiveTypeEvent', () => { 410 | // TODO: add tests for struct, enum and tuple 411 | // Accept everything (unknown) for wrong types, this is done intentionally to 412 | // avoid rejecting types when abiwan make a mistake, we'll probably alter this 413 | // behavior when it gets mature enough 414 | assertType>({ 415 | TestCounterIncreased: { 416 | amount: 1, 417 | }, 418 | }) 419 | 420 | assertType>({ 421 | TestCounterIncreased: { 422 | amount: 1, 423 | }, 424 | }) 425 | assertType>({ 426 | TestCounterDecreased: { 427 | amount: 1, 428 | }, 429 | }) 430 | assertType>({ 431 | TestEnum: { 432 | Var1: { 433 | member: 1, 434 | }, 435 | }, 436 | }) 437 | 438 | // @ts-expect-error 439 | assertType>() 440 | }) 441 | --------------------------------------------------------------------------------