diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index e61227444..57a4ef692 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -161,6 +161,8 @@ Utilities to manage contract aliases ###### **Subcommands:** * `remove` — Remove contract alias +* `add` — Add contract alias +* `show` — Show the contract id associated with a given alias @@ -184,6 +186,48 @@ Remove contract alias +## `stellar contract alias add` + +Add contract alias + +**Usage:** `stellar contract alias add [OPTIONS] --id ` + +###### **Arguments:** + +* `` — The contract alias that will be removed + +###### **Options:** + +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config +* `--overwrite` — Overwrite the contract alias if it already exists +* `--id ` — The contract id that will be associated with the alias + + + +## `stellar contract alias show` + +Show the contract id associated with a given alias + +**Usage:** `stellar contract alias show [OPTIONS] ` + +###### **Arguments:** + +* `` — The contract alias that will be displayed + +###### **Options:** + +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config + + + ## `stellar contract bindings` Generate code client bindings for a contract diff --git a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.d.ts b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.d.ts index 0b661d2c3..cd888cc04 100644 --- a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.d.ts +++ b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.d.ts @@ -51,6 +51,9 @@ export type ComplexEnum = { values: void; }; export declare const Errors: { + /** + * Please provide an odd number + */ 1: { message: string; }; @@ -186,7 +189,7 @@ export interface Client { simulate?: boolean; }) => Promise>; /** - * Construct and simulate a strukt_hel transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a strukt_hel transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Example contract method which takes a struct */ strukt_hel: ({ strukt }: { @@ -358,7 +361,7 @@ export interface Client { simulate?: boolean; }) => Promise>; /** - * Construct and simulate a not transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a not transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Negates a boolean value */ not: ({ boolean }: { @@ -493,7 +496,7 @@ export interface Client { simulate?: boolean; }) => Promise>; /** - * Construct and simulate a option transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a option transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Example of an optional argument */ option: ({ option }: { diff --git a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.js b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.js index 5b9e067c9..4286b2a68 100644 --- a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.js +++ b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/dist/index.js @@ -20,7 +20,10 @@ export var RoyalCard; RoyalCard[RoyalCard["King"] = 13] = "King"; })(RoyalCard || (RoyalCard = {})); export const Errors = { - 1: { message: "Please provide an odd number" } + /** + * Please provide an odd number + */ + 1: { message: "NumberMustBeOdd" } }; export class Client extends ContractClient { options; diff --git a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/package-lock.json b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/package-lock.json index 11ad043be..f45484ffb 100644 --- a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/package-lock.json +++ b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/package-lock.json @@ -17,13 +17,11 @@ }, "node_modules/@stellar/js-xdr": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.1.tgz", - "integrity": "sha512-3gnPjAz78htgqsNEDkEsKHKosV2BF2iZkoHCNxpmZwUxiPsw+2VaXMed8RRMe0rGk3d5GZe7RrSba8zV80J3Ag==" + "license": "Apache-2.0" }, "node_modules/@stellar/stellar-base": { "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.0.1.tgz", - "integrity": "sha512-g6c27MNsDgEdUmoNQJn7zCWoCY50WHt0OIIOq3PhWaJRtUaT++qs1Jpb8+1bny2GmhtfRGOfPUFSyQBuHT9Mvg==", + "license": "Apache-2.0", "dependencies": { "@stellar/js-xdr": "^3.1.1", "base32.js": "^0.1.0", @@ -38,8 +36,7 @@ }, "node_modules/@stellar/stellar-sdk": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-12.1.0.tgz", - "integrity": "sha512-Va0hu9SaPezmMbO5eMwL5D15Wrx1AGWRtxayUDRWV2Fr3ynY58mvCZS1vsgNQ4kE8MZe3nBVKv6T9Kzqwgx1PQ==", + "license": "Apache-2.0", "dependencies": { "@stellar/stellar-base": "^12.0.1", "axios": "^1.7.2", @@ -52,13 +49,12 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "license": "MIT" }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -67,16 +63,13 @@ }, "node_modules/base32.js": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", - "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -90,20 +83,18 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/bignumber.js": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", "engines": { "node": "*" } }, "node_modules/buffer": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -118,6 +109,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -125,8 +117,7 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -136,30 +127,27 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/eventsource": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", "engines": { "node": ">=12.0.0" } }, "node_modules/follow-redirects": { "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -171,8 +159,7 @@ }, "node_modules/form-data": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -184,8 +171,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -199,25 +184,23 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "license": "ISC" }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -227,8 +210,7 @@ }, "node_modules/node-gyp-build": { "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "license": "MIT", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -238,21 +220,17 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -266,12 +244,12 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -282,9 +260,8 @@ }, "node_modules/sodium-native": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.1.1.tgz", - "integrity": "sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { "node-gyp-build": "^4.8.0" @@ -292,13 +269,11 @@ }, "node_modules/toml": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + "license": "MIT" }, "node_modules/tweetnacl": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + "license": "Unlicense" }, "node_modules/typescript": { "version": "5.3.3", @@ -315,8 +290,7 @@ }, "node_modules/urijs": { "version": "1.19.11", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==" + "license": "MIT" } } } diff --git a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/src/index.ts b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/src/index.ts index b6ee3dbbc..7f2d5b000 100644 --- a/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/src/index.ts +++ b/cmd/crates/soroban-spec-typescript/fixtures/test_custom_types/src/index.ts @@ -59,7 +59,10 @@ export type TupleStruct = readonly [Test, SimpleEnum]; export type ComplexEnum = {tag: "Struct", values: readonly [Test]} | {tag: "Tuple", values: readonly [TupleStruct]} | {tag: "Enum", values: readonly [SimpleEnum]} | {tag: "Asset", values: readonly [string, i128]} | {tag: "Void", values: void}; export const Errors = { - 1: {message:"Please provide an odd number"} + /** + * Please provide an odd number + */ + 1: {message:"NumberMustBeOdd"} } export interface Client { @@ -204,7 +207,7 @@ export interface Client { }) => Promise> /** - * Construct and simulate a strukt_hel transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a strukt_hel transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Example contract method which takes a struct */ strukt_hel: ({strukt}: {strukt: Test}, options?: { @@ -385,7 +388,7 @@ export interface Client { }) => Promise> /** - * Construct and simulate a not transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a not transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Negates a boolean value */ not: ({boolean}: {boolean: boolean}, options?: { @@ -526,7 +529,7 @@ export interface Client { }) => Promise> /** - * Construct and simulate a option transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * + * Construct and simulate a option transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Example of an optional argument */ option: ({option}: {option: Option}, options?: { diff --git a/cmd/crates/soroban-spec-typescript/ts-tests/package-lock.json b/cmd/crates/soroban-spec-typescript/ts-tests/package-lock.json index 8e620a1a3..deee8f09d 100644 --- a/cmd/crates/soroban-spec-typescript/ts-tests/package-lock.json +++ b/cmd/crates/soroban-spec-typescript/ts-tests/package-lock.json @@ -700,9 +700,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dev": true, "dependencies": { "follow-redirects": "^1.15.6", @@ -782,12 +782,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" @@ -1610,9 +1610,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/cmd/crates/soroban-test/tests/it/help.rs b/cmd/crates/soroban-test/tests/it/help.rs index a66c449ed..ef84a361b 100644 --- a/cmd/crates/soroban-test/tests/it/help.rs +++ b/cmd/crates/soroban-test/tests/it/help.rs @@ -1,4 +1,4 @@ -use soroban_cli::commands::contract; +use soroban_cli::commands::contract::{self, arg_parsing}; use soroban_test::TestEnv; use crate::util::{invoke_custom as invoke, CUSTOM_TYPES, DEFAULT_CONTRACT_ID}; @@ -55,7 +55,7 @@ async fn complex_enum_help() { async fn multi_arg_failure() { assert!(matches!( invoke_custom("multi_args", "--b").await.unwrap_err(), - contract::invoke::Error::MissingArgument(_) + contract::invoke::Error::ArgParsing(arg_parsing::Error::MissingArgument(_)) )); } @@ -64,7 +64,9 @@ async fn handle_arg_larger_than_i32_failure() { let res = invoke_custom("i32_", &format!("--i32_={}", u32::MAX)).await; assert!(matches!( res, - Err(contract::invoke::Error::CannotParseArg { .. }) + Err(contract::invoke::Error::ArgParsing( + arg_parsing::Error::CannotParseArg { .. } + )) )); } @@ -73,7 +75,9 @@ async fn handle_arg_larger_than_i64_failure() { let res = invoke_custom("i64_", &format!("--i64_={}", u64::MAX)).await; assert!(matches!( res, - Err(contract::invoke::Error::CannotParseArg { .. }) + Err(contract::invoke::Error::ArgParsing( + arg_parsing::Error::CannotParseArg { .. } + )) )); } diff --git a/cmd/soroban-cli/src/commands/contract/alias.rs b/cmd/soroban-cli/src/commands/contract/alias.rs index 4dedd35ff..bea808cb4 100644 --- a/cmd/soroban-cli/src/commands/contract/alias.rs +++ b/cmd/soroban-cli/src/commands/contract/alias.rs @@ -1,23 +1,39 @@ use crate::commands::global; +pub mod add; pub mod remove; +pub mod show; #[derive(Debug, clap::Subcommand)] pub enum Cmd { /// Remove contract alias Remove(remove::Cmd), + + /// Add contract alias + Add(add::Cmd), + + /// Show the contract id associated with a given alias + Show(show::Cmd), } #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Remove(#[from] remove::Error), + + #[error(transparent)] + Add(#[from] add::Error), + + #[error(transparent)] + Show(#[from] show::Error), } impl Cmd { - pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { match &self { - Cmd::Remove(remove) => remove.run(global_args).await?, + Cmd::Remove(remove) => remove.run(global_args)?, + Cmd::Add(add) => add.run(global_args)?, + Cmd::Show(show) => show.run(global_args)?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/alias/add.rs b/cmd/soroban-cli/src/commands/contract/alias/add.rs new file mode 100644 index 000000000..d5c304452 --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/alias/add.rs @@ -0,0 +1,84 @@ +use std::fmt::Debug; + +use clap::{command, Parser}; + +use crate::commands::{config::network, global}; +use crate::config::locator; +use crate::print::Print; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub config_locator: locator::Args, + + #[command(flatten)] + network: network::Args, + + /// The contract alias that will be removed. + pub alias: String, + + /// Overwrite the contract alias if it already exists. + #[arg(long)] + pub overwrite: bool, + + /// The contract id that will be associated with the alias. + #[arg(long = "id")] + pub contract_id: stellar_strkey::Contract, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Network(#[from] network::Error), + + #[error( + "alias '{alias}' is already referencing contract '{contract}' on network '{network_passphrase}'" + )] + AlreadyExist { + alias: String, + network_passphrase: String, + contract: String, + }, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + let alias = &self.alias; + let network = self.network.get(&self.config_locator)?; + let network_passphrase = &network.network_passphrase; + + let contract = self + .config_locator + .get_contract_id(&self.alias, network_passphrase)?; + + if let Some(contract) = contract { + if contract != self.contract_id.to_string() && !self.overwrite { + return Err(Error::AlreadyExist { + alias: alias.to_string(), + network_passphrase: network_passphrase.to_string(), + contract, + }); + } + }; + + print.infoln(format!( + "Contract alias '{alias}' will reference {contract} on network '{network_passphrase}'", + contract = self.contract_id + )); + + self.config_locator.save_contract_id( + &network.network_passphrase, + &self.contract_id.to_string(), + alias, + )?; + + print.checkln(format!("Contract alias '{alias}' has been added")); + + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/contract/alias/remove.rs b/cmd/soroban-cli/src/commands/contract/alias/remove.rs index 27c92e6ea..7a4190595 100644 --- a/cmd/soroban-cli/src/commands/contract/alias/remove.rs +++ b/cmd/soroban-cli/src/commands/contract/alias/remove.rs @@ -27,13 +27,15 @@ pub enum Error { #[error(transparent)] Network(#[from] network::Error), - #[error("no contract found with alias `{alias}`")] - NoContract { alias: String }, + #[error("no contract found with alias '{alias}' for network '{network_passphrase}'")] + NoContract { + alias: String, + network_passphrase: String, + }, } impl Cmd { - #[allow(clippy::unused_async)] - pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { let print = Print::new(global_args.quiet); let alias = &self.alias; let network = self.network.get(&self.config_locator)?; @@ -45,6 +47,7 @@ impl Cmd { else { return Err(Error::NoContract { alias: alias.into(), + network_passphrase: network_passphrase.into(), }); }; diff --git a/cmd/soroban-cli/src/commands/contract/alias/show.rs b/cmd/soroban-cli/src/commands/contract/alias/show.rs new file mode 100644 index 000000000..fc233b613 --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/alias/show.rs @@ -0,0 +1,62 @@ +use std::fmt::Debug; + +use clap::{command, Parser}; + +use crate::commands::{config::network, global}; +use crate::config::locator; +use crate::print::Print; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub config_locator: locator::Args, + + #[command(flatten)] + network: network::Args, + + /// The contract alias that will be displayed. + pub alias: String, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Network(#[from] network::Error), + + #[error("no contract found with alias '{alias}' for network '{network_passphrase}'")] + NoContract { + alias: String, + network_passphrase: String, + }, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + let alias = &self.alias; + let network = self.network.get(&self.config_locator)?; + let network_passphrase = &network.network_passphrase; + + if let Some(contract) = self + .config_locator + .get_contract_id(&self.alias, network_passphrase)? + { + print.infoln(format!( + "Contract alias '{alias}' references {contract} on network '{network_passphrase}'" + )); + + println!("{contract}"); + + Ok(()) + } else { + Err(Error::NoContract { + alias: alias.into(), + network_passphrase: network_passphrase.into(), + }) + } + } +} diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs new file mode 100644 index 000000000..4a8af47e2 --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -0,0 +1,245 @@ +use std::collections::HashMap; +use std::convert::TryInto; +use std::ffi::OsString; +use std::fmt::Debug; +use std::path::PathBuf; + +use clap::value_parser; +use ed25519_dalek::SigningKey; +use heck::ToKebabCase; + +use soroban_env_host::xdr::{ + self, Hash, InvokeContractArgs, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, + ScVec, +}; + +use crate::commands::txn_result::TxnResult; +use crate::config::{self}; +use soroban_spec_tools::Spec; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("parsing argument {arg}: {error}")] + CannotParseArg { + arg: String, + error: soroban_spec_tools::Error, + }, + #[error("cannot print result {result:?}: {error}")] + CannotPrintResult { + result: ScVal, + error: soroban_spec_tools::Error, + }, + #[error("function {0} was not found in the contract")] + FunctionNotFoundInContractSpec(String), + #[error("function name {0} is too long")] + FunctionNameTooLong(String), + #[error("argument count ({current}) surpasses maximum allowed count ({maximum})")] + MaxNumberOfArgumentsReached { current: usize, maximum: usize }, + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error(transparent)] + StrVal(#[from] soroban_spec_tools::Error), + #[error("Missing argument {0}")] + MissingArgument(String), + #[error("")] + MissingFileArg(PathBuf), +} + +pub fn build_host_function_parameters( + contract_id: &stellar_strkey::Contract, + slop: &[OsString], + spec_entries: &[ScSpecEntry], + config: &config::Args, +) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { + let spec = Spec(Some(spec_entries.to_vec())); + let mut cmd = clap::Command::new(contract_id.to_string()) + .no_binary_name(true) + .term_width(300) + .max_term_width(300); + + for ScSpecFunctionV0 { name, .. } in spec.find_functions()? { + cmd = cmd.subcommand(build_custom_cmd(&name.to_utf8_string_lossy(), &spec)?); + } + cmd.build(); + let long_help = cmd.render_long_help(); + let mut matches_ = cmd.get_matches_from(slop); + let Some((function, matches_)) = &matches_.remove_subcommand() else { + println!("{long_help}"); + std::process::exit(1); + }; + + let func = spec.find_function(function)?; + // create parsed_args in same order as the inputs to func + let mut signers: Vec = vec![]; + let parsed_args = func + .inputs + .iter() + .map(|i| { + let name = i.name.to_utf8_string()?; + if let Some(mut val) = matches_.get_raw(&name) { + let mut s = val.next().unwrap().to_string_lossy().to_string(); + if matches!(i.type_, ScSpecTypeDef::Address) { + let cmd = crate::commands::keys::address::Cmd { + name: s.clone(), + hd_path: Some(0), + locator: config.locator.clone(), + }; + if let Ok(address) = cmd.public_key() { + s = address.to_string(); + } + if let Ok(key) = cmd.private_key() { + signers.push(key); + } + } + spec.from_string(&s, &i.type_) + .map_err(|error| Error::CannotParseArg { arg: name, error }) + } else if matches!(i.type_, ScSpecTypeDef::Option(_)) { + Ok(ScVal::Void) + } else if let Some(arg_path) = matches_.get_one::(&fmt_arg_file_name(&name)) { + if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) { + Ok(ScVal::try_from( + &std::fs::read(arg_path) + .map_err(|_| Error::MissingFileArg(arg_path.clone()))?, + ) + .map_err(|()| Error::CannotParseArg { + arg: name.clone(), + error: soroban_spec_tools::Error::Unknown, + })?) + } else { + let file_contents = std::fs::read_to_string(arg_path) + .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; + tracing::debug!( + "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}", + i.type_, + file_contents.len() + ); + spec.from_string(&file_contents, &i.type_) + .map_err(|error| Error::CannotParseArg { arg: name, error }) + } + } else { + Err(Error::MissingArgument(name)) + } + }) + .collect::, Error>>()?; + + let contract_address_arg = ScAddress::Contract(Hash(contract_id.0)); + let function_symbol_arg = function + .try_into() + .map_err(|()| Error::FunctionNameTooLong(function.clone()))?; + + let final_args = + parsed_args + .clone() + .try_into() + .map_err(|_| Error::MaxNumberOfArgumentsReached { + current: parsed_args.len(), + maximum: ScVec::default().max_len(), + })?; + + let invoke_args = InvokeContractArgs { + contract_address: contract_address_arg, + function_name: function_symbol_arg, + args: final_args, + }; + + Ok((function.clone(), spec, invoke_args, signers)) +} + +fn build_custom_cmd(name: &str, spec: &Spec) -> Result { + let func = spec + .find_function(name) + .map_err(|_| Error::FunctionNotFoundInContractSpec(name.to_string()))?; + + // Parse the function arguments + let inputs_map = &func + .inputs + .iter() + .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone())) + .collect::>(); + let name: &'static str = Box::leak(name.to_string().into_boxed_str()); + let mut cmd = clap::Command::new(name) + .no_binary_name(true) + .term_width(300) + .max_term_width(300); + let kebab_name = name.to_kebab_case(); + if kebab_name != name { + cmd = cmd.alias(kebab_name); + } + let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str()); + let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str()); + + cmd = cmd.about(Some(doc)).long_about(long_doc); + for (name, type_) in inputs_map { + let mut arg = clap::Arg::new(name); + let file_arg_name = fmt_arg_file_name(name); + let mut file_arg = clap::Arg::new(&file_arg_name); + arg = arg + .long(name) + .alias(name.to_kebab_case()) + .num_args(1) + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .long_help(spec.doc(name, type_)?); + + file_arg = file_arg + .long(&file_arg_name) + .alias(file_arg_name.to_kebab_case()) + .num_args(1) + .hide(true) + .value_parser(value_parser!(PathBuf)) + .conflicts_with(name); + + if let Some(value_name) = spec.arg_value_name(type_, 0) { + let value_name: &'static str = Box::leak(value_name.into_boxed_str()); + arg = arg.value_name(value_name); + } + + // Set up special-case arg rules + arg = match type_ { + ScSpecTypeDef::Bool => arg + .num_args(0..1) + .default_missing_value("true") + .default_value("false") + .num_args(0..=1), + ScSpecTypeDef::Option(_val) => arg.required(false), + ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => { + arg.allow_hyphen_values(true) + } + _ => arg, + }; + + cmd = cmd.arg(arg); + cmd = cmd.arg(file_arg); + } + Ok(cmd) +} + +fn fmt_arg_file_name(name: &str) -> String { + format!("{name}-file-path") +} + +fn arg_file_help(docs: &str) -> String { + format!( + r#"{docs} +Usage Notes: +Each arg has a corresponding ---file-path which is a path to a file containing the corresponding JSON argument. +Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"# + ) +} + +pub fn output_to_string( + spec: &Spec, + res: &ScVal, + function: &str, +) -> Result, Error> { + let mut res_str = String::new(); + if let Some(output) = spec.find_function(function)?.outputs.first() { + res_str = spec + .xdr_to_json(res, output) + .map_err(|e| Error::CannotPrintResult { + result: res.clone(), + error: e, + })? + .to_string(); + } + Ok(TxnResult::Res(res_str)) +} diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 03c7c70cf..a0e563e58 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -130,11 +130,7 @@ impl NetworkRunnable for Cmd { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); - let contract = config.locator.resolve_contract_id( - self.key.contract_id.as_ref().unwrap(), - &network.network_passphrase, - )?; - let keys = self.key.parse_keys(contract)?; + let keys = self.key.parse_keys(&config.locator, &network)?; let network = &config.get_network()?; let client = Client::new(&network.rpc_url)?; let key = config.key_pair()?; diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index a83504d96..0d451f6fe 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -1,6 +1,6 @@ use clap::{ builder::{PossibleValue, PossibleValuesParser, ValueParser}, - Parser, ValueEnum, + Parser, }; use gix::{clone, create, open, progress, remote}; use rust_embed::RustEmbed; @@ -14,24 +14,20 @@ use std::{ }, io::{self, Read, Write}, num::NonZeroU32, - path::Path, + path::{Path, PathBuf}, str, sync::atomic::AtomicBool, }; use toml_edit::{Document, TomlError}; use ureq::get; +use crate::{commands::global, print}; + const SOROBAN_EXAMPLES_URL: &str = "https://github.com/stellar/soroban-examples.git"; const GITHUB_URL: &str = "https://github.com"; const WITH_EXAMPLE_LONG_HELP_TEXT: &str = "An optional flag to specify Soroban example contracts to include. A hello-world contract will be included by default."; -#[derive(Clone, Debug, ValueEnum, PartialEq)] -pub enum FrontendTemplate { - Astro, - None, -} - #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { @@ -87,380 +83,415 @@ pub enum Error { impl Cmd { #[allow(clippy::unused_self)] - pub fn run(&self) -> Result<(), Error> { - println!("ℹ️ Initializing project at {}", self.project_path); - let project_path = Path::new(&self.project_path); - - init( - project_path, - &self.frontend_template, - &self.with_example, - self.overwrite, - )?; + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let runner = Runner { + args: self.clone(), + print: print::Print::new(global_args.quiet), + }; - Ok(()) + runner.run() } } #[derive(RustEmbed)] #[folder = "src/utils/contract-init-template"] struct TemplateFiles; +struct Runner { + args: Cmd, + print: print::Print, +} -fn init( - project_path: &Path, - frontend_template: &str, - with_examples: &[String], - overwrite: bool, -) -> Result<(), Error> { - // create a project dir, and copy the contents of the base template (contract-init-template) into it - create_dir_all(project_path).map_err(|e| { - eprintln!("Error creating new project directory: {project_path:?}"); - e - })?; - copy_template_files(project_path, overwrite)?; - - if !check_internet_connection() { - println!("⚠️ It doesn't look like you're connected to the internet. We're still able to initialize a new project, but additional examples and the frontend template will not be included."); - return Ok(()); - } - - if !frontend_template.is_empty() { - // create a temp dir for the template repo - let fe_template_dir = tempfile::tempdir().map_err(|e| { - eprintln!("Error creating temp dir for frontend template"); - e - })?; - - // clone the template repo into the temp dir - clone_repo(frontend_template, fe_template_dir.path())?; - - // copy the frontend template files into the project - copy_frontend_files(fe_template_dir.path(), project_path, overwrite)?; - } +impl Runner { + fn run(&self) -> Result<(), Error> { + let project_path = PathBuf::from(&self.args.project_path); + self.print + .infoln(format!("Initializing project at {project_path:?}")); - // if there are --with-example flags, include the example contracts - if include_example_contracts(with_examples) { - // create an examples temp dir - let examples_dir = tempfile::tempdir().map_err(|e| { - eprintln!("Error creating temp dir for soroban-examples"); + // create a project dir, and copy the contents of the base template (contract-init-template) into it + create_dir_all(&project_path).map_err(|e| { + self.print + .errorln("Error creating new project directory: {project_path:?}"); e })?; + self.copy_template_files()?; - // clone the soroban-examples repo into the temp dir - clone_repo(SOROBAN_EXAMPLES_URL, examples_dir.path())?; + if !Self::check_internet_connection() { + self.print.warnln("It doesn't look like you're connected to the internet. We're still able to initialize a new project, but additional examples and the frontend template will not be included."); + return Ok(()); + } - // copy the example contracts into the project - copy_example_contracts(examples_dir.path(), project_path, with_examples, overwrite)?; - } + if !self.args.frontend_template.is_empty() { + // create a temp dir for the template repo + let fe_template_dir = tempfile::tempdir().map_err(|e| { + self.print + .errorln("Error creating temp dir for frontend template"); + e + })?; - Ok(()) -} + // clone the template repo into the temp dir + self.clone_repo(&self.args.frontend_template, fe_template_dir.path())?; -fn copy_template_files(project_path: &Path, overwrite: bool) -> Result<(), Error> { - for item in TemplateFiles::iter() { - let mut to = project_path.join(item.as_ref()); - let exists = file_exists(&to); - if exists && !overwrite { - println!( - "ℹ️ Skipped creating {} as it already exists", - &to.to_string_lossy() - ); - continue; + // copy the frontend template files into the project + self.copy_frontend_files(fe_template_dir.path(), &project_path)?; } - create_dir_all(to.parent().unwrap()).map_err(|e| { - eprintln!("Error creating directory path for: {to:?}"); - e - })?; - let Some(file) = TemplateFiles::get(item.as_ref()) else { - println!("⚠️ Failed to read file: {}", item.as_ref()); - continue; - }; + // if there are --with-example flags, include the example contracts + if self.include_example_contracts() { + // create an examples temp dir + let examples_dir = tempfile::tempdir().map_err(|e| { + self.print + .errorln("Error creating temp dir for soroban-examples"); + e + })?; - let file_contents = std::str::from_utf8(file.data.as_ref()).map_err(|e| { - eprintln!( - "Error converting file contents in {:?} to string", - item.as_ref() - ); - e - })?; + // clone the soroban-examples repo into the temp dir + self.clone_repo(SOROBAN_EXAMPLES_URL, examples_dir.path())?; - // We need to include the Cargo.toml file as Cargo.toml.removeextension in the template so that it will be included the package. This is making sure that the Cargo file is written as Cargo.toml in the new project. This is a workaround for this issue: https://github.com/rust-lang/cargo/issues/8597. - let item_path = Path::new(item.as_ref()); - if item_path.file_name().unwrap() == "Cargo.toml.removeextension" { - let item_parent_path = item_path.parent().unwrap(); - to = project_path.join(item_parent_path).join("Cargo.toml"); + // copy the example contracts into the project + self.copy_example_contracts( + examples_dir.path(), + &project_path, + &self.args.with_example, + )?; } - if exists { - println!("🔄 Overwriting {}", &to.to_string_lossy()); - } else { - println!("➕ Writing {}", &to.to_string_lossy()); - } - write(&to, file_contents).map_err(|e| { - eprintln!("Error writing file: {to:?}"); - e - })?; + Ok(()) } - Ok(()) -} -fn copy_contents(from: &Path, to: &Path, overwrite: bool) -> Result<(), Error> { - let contents_to_exclude_from_copy = [ - ".git", - ".github", - "Makefile", - ".vscode", - "target", - "Cargo.lock", - ]; - for entry in read_dir(from).map_err(|e| { - eprintln!("Error reading directory: {from:?}"); - e - })? { - let entry = entry.map_err(|e| { - eprintln!("Error reading entry in directory: {from:?}"); - e - })?; - let path = entry.path(); - let entry_name = entry.file_name().to_string_lossy().to_string(); - let new_path = to.join(&entry_name); + fn copy_template_files(&self) -> Result<(), Error> { + let project_path = Path::new(&self.args.project_path); + for item in TemplateFiles::iter() { + let mut to = project_path.join(item.as_ref()); + let exists = Self::file_exists(&to); + if exists && !self.args.overwrite { + self.print + .infoln(format!("Skipped creating {to:?} as it already exists")); + continue; + } - if contents_to_exclude_from_copy.contains(&entry_name.as_str()) { - continue; - } + create_dir_all(to.parent().unwrap()).map_err(|e| { + self.print + .errorln(format!("Error creating directory path for: {to:?}")); + e + })?; - if path.is_dir() { - create_dir_all(&new_path).map_err(|e| { - eprintln!("Error creating directory: {new_path:?}"); + let Some(file) = TemplateFiles::get(item.as_ref()) else { + self.print + .warnln(format!("Failed to read file: {}", item.as_ref())); + continue; + }; + + let file_contents = std::str::from_utf8(file.data.as_ref()).map_err(|e| { + self.print.errorln(format!( + "Error converting file contents in {:?} to string", + item.as_ref() + )); e })?; - copy_contents(&path, &new_path, overwrite)?; - } else { - let exists = file_exists(&new_path); - let new_path_str = new_path.to_string_lossy(); - if exists { - let append = - new_path_str.contains(".gitignore") || new_path_str.contains("README.md"); - if append { - append_contents(&path, &new_path)?; - } - if overwrite && !append { - println!("🔄 Overwriting {new_path_str}"); - } else { - println!("ℹ️ Skipped creating {new_path_str} as it already exists"); - continue; - } + // We need to include the Cargo.toml file as Cargo.toml.removeextension in the template so that it will be included the package. This is making sure that the Cargo file is written as Cargo.toml in the new project. This is a workaround for this issue: https://github.com/rust-lang/cargo/issues/8597. + let item_path = Path::new(item.as_ref()); + if item_path.file_name().unwrap() == "Cargo.toml.removeextension" { + let item_parent_path = item_path.parent().unwrap(); + to = project_path.join(item_parent_path).join("Cargo.toml"); + } + + if exists { + self.print + .plusln(format!("Writing {to:?} (overwriting existing file)")); } else { - println!("➕ Writing {new_path_str}"); + self.print.plusln(format!("Writing {to:?}")); } - copy(&path, &new_path).map_err(|e| { - eprintln!( - "Error copying from {:?} to {:?}", - path.to_string_lossy(), - new_path - ); + write(&to, file_contents).map_err(|e| { + self.print.errorln(format!("Error writing file: {to:?}")); e })?; } + Ok(()) } - Ok(()) -} - -fn file_exists(file_path: &Path) -> bool { - metadata(file_path) - .as_ref() - .map(Metadata::is_file) - .unwrap_or(false) -} + fn copy_contents(&self, from: &Path, to: &Path) -> Result<(), Error> { + let contents_to_exclude_from_copy = [ + ".git", + ".github", + "Makefile", + ".vscode", + "target", + "Cargo.lock", + ]; + for entry in read_dir(from).map_err(|e| { + self.print + .errorln(format!("Error reading directory: {from:?}")); + e + })? { + let entry = entry.map_err(|e| { + self.print + .errorln(format!("Error reading entry in directory: {from:?}")); + e + })?; + let path = entry.path(); + let entry_name = entry.file_name().to_string_lossy().to_string(); + let new_path = to.join(&entry_name); -fn include_example_contracts(contracts: &[String]) -> bool { - !contracts.is_empty() -} + if contents_to_exclude_from_copy.contains(&entry_name.as_str()) { + continue; + } -fn clone_repo(from_url: &str, to_path: &Path) -> Result<(), Error> { - let mut prepare = clone::PrepareFetch::new( - from_url, - to_path, - create::Kind::WithWorktree, - create::Options { - destination_must_be_empty: false, - fs_capabilities: None, - }, - open::Options::isolated(), - ) - .map_err(|e| { - eprintln!("Error preparing fetch for {from_url:?}"); - Box::new(e) - })? - .with_shallow(remote::fetch::Shallow::DepthAtRemote( - NonZeroU32::new(1).unwrap(), - )); - - let (mut checkout, _outcome) = prepare - .fetch_then_checkout(progress::Discard, &AtomicBool::new(false)) - .map_err(|e| { - eprintln!("Error calling fetch_then_checkout with {from_url:?}"); - Box::new(e) - })?; + if path.is_dir() { + create_dir_all(&new_path).map_err(|e| { + self.print + .errorln(format!("Error creating directory: {new_path:?}")); + e + })?; + self.copy_contents(&path, &new_path)?; + } else { + let exists = Self::file_exists(&new_path); + let new_path_str = new_path.to_string_lossy(); + if exists { + let append = + new_path_str.contains(".gitignore") || new_path_str.contains("README.md"); + if append { + self.append_contents(&path, &new_path)?; + } + + if self.args.overwrite && !append { + self.print.plusln(format!( + "Writing {new_path_str} (overwriting existing file)" + )); + } else { + self.print.infoln(format!( + "Skipped creating {new_path_str} as it already exists" + )); + continue; + } + } else { + self.print.plus(format!("Writing {new_path_str}")); + } + copy(&path, &new_path).map_err(|e| { + self.print.errorln(format!( + "Error copying from {:?} to {:?}", + path.to_string_lossy(), + new_path + )); + e + })?; + } + } - let (_repo, _outcome) = checkout - .main_worktree(progress::Discard, &AtomicBool::new(false)) - .map_err(|e| { - eprintln!("Error calling main_worktree for {from_url:?}"); - e - })?; + Ok(()) + } - Ok(()) -} + fn file_exists(file_path: &Path) -> bool { + metadata(file_path) + .as_ref() + .map(Metadata::is_file) + .unwrap_or(false) + } -fn copy_example_contracts( - from: &Path, - to: &Path, - contracts: &[String], - overwrite: bool, -) -> Result<(), Error> { - let project_contracts_path = to.join("contracts"); - for contract in contracts { - println!("ℹ️ Initializing example contract: {contract}"); - let contract_as_string = contract.to_string(); - let contract_path = Path::new(&contract_as_string); - let from_contract_path = from.join(contract_path); - let to_contract_path = project_contracts_path.join(contract_path); - create_dir_all(&to_contract_path).map_err(|e| { - eprintln!("Error creating directory: {contract_path:?}"); - e - })?; + fn check_internet_connection() -> bool { + if let Ok(_req) = get(GITHUB_URL).call() { + return true; + } - copy_contents(&from_contract_path, &to_contract_path, overwrite)?; - edit_contract_cargo_file(&to_contract_path)?; + false } - Ok(()) -} + fn include_example_contracts(&self) -> bool { + !self.args.with_example.is_empty() + } -fn edit_contract_cargo_file(contract_path: &Path) -> Result<(), Error> { - let cargo_path = contract_path.join("Cargo.toml"); - let cargo_toml_str = read_to_string(&cargo_path).map_err(|e| { - eprint!("Error reading Cargo.toml file in: {contract_path:?}"); - e - })?; - - let cargo_toml_str = regex::Regex::new(r#"soroban-sdk = "[^\"]+""#) - .unwrap() - .replace_all( - cargo_toml_str.as_str(), - "soroban-sdk = { workspace = true }", - ); + fn clone_repo(&self, from_url: &str, to_path: &Path) -> Result<(), Error> { + let mut prepare = clone::PrepareFetch::new( + from_url, + to_path, + create::Kind::WithWorktree, + create::Options { + destination_must_be_empty: false, + fs_capabilities: None, + }, + open::Options::isolated(), + ) + .map_err(|e| { + self.print + .errorln(format!("Error preparing fetch for {from_url:?}")); + Box::new(e) + })? + .with_shallow(remote::fetch::Shallow::DepthAtRemote( + NonZeroU32::new(1).unwrap(), + )); + + let (mut checkout, _outcome) = prepare + .fetch_then_checkout(progress::Discard, &AtomicBool::new(false)) + .map_err(|e| { + self.print.errorln(format!( + "Error calling fetch_then_checkout with {from_url:?}" + )); + Box::new(e) + })?; - let cargo_toml_str = regex::Regex::new(r#"soroban-sdk = \{(.*) version = "[^"]+"(.+)}"#) - .unwrap() - .replace_all(&cargo_toml_str, "soroban-sdk = {$1 workspace = true$2}"); + let (_repo, _outcome) = checkout + .main_worktree(progress::Discard, &AtomicBool::new(false)) + .map_err(|e| { + self.print + .errorln(format!("Error calling main_worktree for {from_url:?}")); + e + })?; - let mut doc = cargo_toml_str.parse::().map_err(|e| { - eprintln!("Error parsing Cargo.toml file in: {contract_path:?}"); - e - })?; - doc.remove("profile"); + Ok(()) + } - write(&cargo_path, doc.to_string()).map_err(|e| { - eprintln!("Error writing to Cargo.toml file in: {contract_path:?}"); - e - })?; + fn copy_example_contracts( + &self, + from: &Path, + to: &Path, + contracts: &[String], + ) -> Result<(), Error> { + let project_contracts_path = to.join("contracts"); + for contract in contracts { + self.print + .infoln(format!("Initializing example contract: {contract}")); + let contract_as_string = contract.to_string(); + let contract_path = Path::new(&contract_as_string); + let from_contract_path = from.join(contract_path); + let to_contract_path = project_contracts_path.join(contract_path); + create_dir_all(&to_contract_path).map_err(|e| { + self.print + .errorln(format!("Error creating directory: {contract_path:?}")); + e + })?; - Ok(()) -} + self.copy_contents(&from_contract_path, &to_contract_path)?; + self.edit_contract_cargo_file(&to_contract_path)?; + } -fn copy_frontend_files(from: &Path, to: &Path, overwrite: bool) -> Result<(), Error> { - println!("ℹ️ Initializing with frontend template"); - copy_contents(from, to, overwrite)?; - edit_package_json_files(to) -} + Ok(()) + } -fn edit_package_json_files(project_path: &Path) -> Result<(), Error> { - let package_name = if let Some(name) = project_path.file_name() { - name.to_owned() - } else { - let current_dir = env::current_dir()?; - let file_name = current_dir - .file_name() - .unwrap_or(OsStr::new("soroban-astro-template")) - .to_os_string(); - file_name - }; + fn edit_contract_cargo_file(&self, contract_path: &Path) -> Result<(), Error> { + let cargo_path = contract_path.join("Cargo.toml"); + let cargo_toml_str = read_to_string(&cargo_path).map_err(|e| { + self.print.errorln(format!( + "Error reading Cargo.toml file in: {contract_path:?}" + )); + e + })?; - edit_package_name(project_path, &package_name, "package.json").map_err(|e| { - eprintln!("Error editing package.json file in: {project_path:?}"); - e - })?; - edit_package_name(project_path, &package_name, "package-lock.json") -} + let cargo_toml_str = regex::Regex::new(r#"soroban-sdk = "[^\"]+""#) + .unwrap() + .replace_all( + cargo_toml_str.as_str(), + "soroban-sdk = { workspace = true }", + ); -fn edit_package_name( - project_path: &Path, - package_name: &OsStr, - file_name: &str, -) -> Result<(), Error> { - let file_path = project_path.join(file_name); - let file_contents = read_to_string(&file_path)?; + let cargo_toml_str = regex::Regex::new(r#"soroban-sdk = \{(.*) version = "[^"]+"(.+)}"#) + .unwrap() + .replace_all(&cargo_toml_str, "soroban-sdk = {$1 workspace = true$2}"); - let mut doc: JsonValue = from_str(&file_contents).map_err(|e| { - eprintln!("Error parsing package.json file in: {project_path:?}"); - e - })?; + let mut doc = cargo_toml_str.parse::().map_err(|e| { + self.print.errorln(format!( + "Error parsing Cargo.toml file in: {contract_path:?}" + )); + e + })?; + doc.remove("profile"); - doc["name"] = json!(package_name.to_string_lossy()); + write(&cargo_path, doc.to_string()).map_err(|e| { + self.print.errorln(format!( + "Error writing to Cargo.toml file in: {contract_path:?}" + )); + e + })?; - let formatted_json = to_string_pretty(&doc)?; + Ok(()) + } - write(&file_path, formatted_json)?; + fn copy_frontend_files(&self, from: &Path, to: &Path) -> Result<(), Error> { + self.print.infoln("ℹ️ Initializing with frontend template"); + self.copy_contents(from, to)?; + self.edit_package_json_files(to) + } - Ok(()) -} + fn edit_package_json_files(&self, project_path: &Path) -> Result<(), Error> { + let package_name = if let Some(name) = project_path.file_name() { + name.to_owned() + } else { + let current_dir = env::current_dir()?; + let file_name = current_dir + .file_name() + .unwrap_or(OsStr::new("soroban-astro-template")) + .to_os_string(); + file_name + }; -fn check_internet_connection() -> bool { - if let Ok(_req) = get(GITHUB_URL).call() { - return true; + self.edit_package_name(project_path, &package_name, "package.json") + .map_err(|e| { + self.print.errorln(format!( + "Error editing package.json file in: {project_path:?}" + )); + e + })?; + self.edit_package_name(project_path, &package_name, "package-lock.json") } - false -} + fn edit_package_name( + &self, + project_path: &Path, + package_name: &OsStr, + file_name: &str, + ) -> Result<(), Error> { + let file_path = project_path.join(file_name); + let file_contents = read_to_string(&file_path)?; + + let mut doc: JsonValue = from_str(&file_contents).map_err(|e| { + self.print.errorln(format!( + "Error parsing {file_name} file in: {project_path:?}" + )); + e + })?; + + doc["name"] = json!(package_name.to_string_lossy()); -// Appends the contents of a file to another file, separated by a delimiter -fn append_contents(from: &Path, to: &Path) -> Result<(), Error> { - let mut from_file = File::open(from)?; - let mut from_content = String::new(); - from_file.read_to_string(&mut from_content)?; + let formatted_json = to_string_pretty(&doc)?; - let mut to_file = OpenOptions::new().read(true).append(true).open(to)?; - let mut to_content = String::new(); - to_file.read_to_string(&mut to_content)?; + write(&file_path, formatted_json)?; - let delimiter = get_merged_file_delimiter(to); - // if the to file already contains the delimiter, we don't need to append the contents again - if to_content.contains(&delimiter) { - return Ok(()); + Ok(()) } - to_file.write_all(delimiter.as_bytes())?; - to_file.write_all(from_content.as_bytes())?; + // Appends the contents of a file to another file, separated by a delimiter + fn append_contents(&self, from: &Path, to: &Path) -> Result<(), Error> { + let mut from_file = File::open(from)?; + let mut from_content = String::new(); + from_file.read_to_string(&mut from_content)?; - println!("ℹ️ Merging {} contents", &to.to_string_lossy()); - Ok(()) -} + let mut to_file = OpenOptions::new().read(true).append(true).open(to)?; + let mut to_content = String::new(); + to_file.read_to_string(&mut to_content)?; -fn get_merged_file_delimiter(file_path: &Path) -> String { - let comment = if file_path.to_string_lossy().contains("README.md") { - "---\n".to_string() - } else if file_path.to_string_lossy().contains("gitignore") { - "# The following is from the Frontend Template's .gitignore".to_string() - } else { - String::new() - }; + let delimiter = Self::get_merged_file_delimiter(to); + // if the to file already contains the delimiter, we don't need to append the contents again + if to_content.contains(&delimiter) { + return Ok(()); + } + + to_file.write_all(delimiter.as_bytes())?; + to_file.write_all(from_content.as_bytes())?; + + self.print.infoln(format!("Merging {to:?} contents")); + Ok(()) + } + + fn get_merged_file_delimiter(file_path: &Path) -> String { + let comment = if file_path.to_string_lossy().contains("README.md") { + "---\n".to_string() + } else if file_path.to_string_lossy().contains("gitignore") { + "# The following is from the Frontend Template's .gitignore".to_string() + } else { + String::new() + }; - format!("\n\n{comment}\n\n").to_string() + format!("\n\n{comment}\n\n").to_string() + } } #[cfg(test)] @@ -482,9 +513,16 @@ mod tests { fn test_init() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); - let with_examples = vec![]; - let overwrite = false; - init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: String::new(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -501,9 +539,16 @@ mod tests { fn test_init_including_example_contract() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); - let with_examples = ["alloc".to_owned()]; - let overwrite = false; - init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: ["alloc".to_owned()].to_vec(), + frontend_template: String::new(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -525,9 +570,16 @@ mod tests { fn test_init_including_multiple_example_contracts() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("project"); - let with_examples = ["account".to_owned(), "atomic_swap".to_owned()]; - let overwrite = false; - init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: ["account".to_owned(), "atomic_swap".to_owned()].to_vec(), + frontend_template: String::new(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -550,9 +602,16 @@ mod tests { fn test_init_with_invalid_example_contract() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("project"); - let with_examples = ["invalid_example".to_owned(), "atomic_swap".to_owned()]; - let overwrite = false; - assert!(init(project_dir.as_path(), "", &with_examples, overwrite).is_err()); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: ["invalid_example".to_owned(), "atomic_swap".to_owned()].to_vec(), + frontend_template: String::new(), + overwrite: false, + }, + print: print::Print::new(false), + }; + assert!(runner.run().is_err()); temp_dir.close().unwrap(); } @@ -561,15 +620,16 @@ mod tests { fn test_init_with_frontend_template() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); - let with_examples = vec![]; - let overwrite = false; - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - overwrite, - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -591,28 +651,33 @@ mod tests { fn test_init_with_overwrite() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); - let with_examples = vec![]; // First initialization - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - false, - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); // Get initial modification times let initial_mod_times = get_mod_times(&project_dir); // Second initialization with overwrite - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - true, // overwrite = true - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: true, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); // Get new modification times let new_mod_times = get_mod_times(&project_dir); @@ -647,15 +712,16 @@ mod tests { fn test_init_from_within_an_existing_project() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("./"); - let with_examples = vec![]; - let overwrite = false; - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - overwrite, - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -679,24 +745,28 @@ mod tests { fn test_init_does_not_duplicate_frontend_readme_contents_when_run_more_than_once() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); - let with_examples = vec![]; - let overwrite = false; - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - overwrite, - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); // call init again to make sure the README.md's contents are not duplicated - init( - project_dir.as_path(), - "https://github.com/stellar/soroban-astro-template", - &with_examples, - overwrite, - ) - .unwrap(); + let runner = Runner { + args: Cmd { + project_path: project_dir.to_string_lossy().to_string(), + with_example: vec![], + frontend_template: "https://github.com/stellar/soroban-astro-template".to_owned(), + overwrite: false, + }, + print: print::Print::new(false), + }; + runner.run().unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -743,7 +813,6 @@ mod tests { let cargo_toml_path = contract_dir.as_path().join("Cargo.toml"); let cargo_toml_str = read_to_string(cargo_toml_path.clone()).unwrap(); let doc = cargo_toml_str.parse::().unwrap(); - println!("{cargo_toml_path:?} contents:\n{cargo_toml_str}"); assert!( doc.get("dependencies") .unwrap() diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 6fcfb1e31..293867c76 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::convert::{Infallible, TryInto}; use std::ffi::OsString; use std::num::ParseIntError; @@ -6,18 +5,15 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{fmt::Debug, fs, io}; -use clap::{arg, command, value_parser, Parser, ValueEnum}; -use ed25519_dalek::SigningKey; -use heck::ToKebabCase; +use clap::{arg, command, Parser, ValueEnum}; use soroban_env_host::{ xdr::{ self, AccountEntry, AccountEntryExt, AccountId, ContractEvent, ContractEventType, - DiagnosticEvent, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, - LedgerEntryData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, - PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, - SequenceNumber, String32, StringM, Thresholds, Transaction, TransactionExt, Uint256, VecM, - WriteXdr, + DiagnosticEvent, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerEntryData, + Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, + ScSpecEntry, SequenceNumber, String32, StringM, Thresholds, Transaction, TransactionExt, + Uint256, VecM, WriteXdr, }, HostError, }; @@ -26,6 +22,8 @@ use soroban_rpc::{SimulateHostFunctionResult, SimulateTransactionResponse}; use soroban_spec::read::FromWasmError; use super::super::events; +use super::arg_parsing; +use crate::commands::contract::arg_parsing::{build_host_function_parameters, output_to_string}; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; @@ -35,7 +33,7 @@ use crate::{ config::{self, data, locator, network}, rpc, Pwd, }; -use soroban_spec_tools::{contract, Spec}; +use soroban_spec_tools::contract; #[derive(Parser, Debug, Default, Clone)] #[allow(clippy::struct_excessive_bools)] @@ -79,11 +77,6 @@ impl Pwd for Cmd { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("parsing argument {arg}: {error}")] - CannotParseArg { - arg: String, - error: soroban_spec_tools::Error, - }, #[error("cannot add contract to ledger entries: {0}")] CannotAddContractToLedgerEntries(xdr::Error), #[error(transparent)] @@ -97,19 +90,8 @@ pub enum Error { filepath: std::path::PathBuf, error: events::Error, }, - #[error("function {0} was not found in the contract")] - FunctionNotFoundInContractSpec(String), #[error("parsing contract spec: {0}")] CannotParseContractSpec(FromWasmError), - #[error("function name {0} is too long")] - FunctionNameTooLong(String), - #[error("argument count ({current}) surpasses maximum allowed count ({maximum})")] - MaxNumberOfArgumentsReached { current: usize, maximum: usize }, - #[error("cannot print result {result:?}: {error}")] - CannotPrintResult { - result: ScVal, - error: soroban_spec_tools::Error, - }, #[error(transparent)] Xdr(#[from] xdr::Error), #[error("error parsing int: {0}")] @@ -120,16 +102,12 @@ pub enum Error { UnexpectedContractCodeDataType(LedgerEntryData), #[error("missing operation result")] MissingOperationResult, - #[error(transparent)] - StrVal(#[from] soroban_spec_tools::Error), #[error("error loading signing key: {0}")] SignatureError(#[from] ed25519_dalek::SignatureError), #[error(transparent)] Config(#[from] config::Error), #[error("unexpected ({length}) simulate transaction result length")] UnexpectedSimulateTransactionResultSize { length: usize }, - #[error("Missing argument {0}")] - MissingArgument(String), #[error(transparent)] Clap(#[from] clap::Error), #[error(transparent)] @@ -140,8 +118,6 @@ pub enum Error { StrKey(#[from] stellar_strkey::DecodeError), #[error(transparent)] ContractSpec(#[from] contract::Error), - #[error("")] - MissingFileArg(PathBuf), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] @@ -150,6 +126,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] GetSpecError(#[from] get_spec::Error), + #[error(transparent)] + ArgParsing(#[from] arg_parsing::Error), } impl From for Error { @@ -159,108 +137,6 @@ impl From for Error { } impl Cmd { - fn build_host_function_parameters( - &self, - contract_id: [u8; 32], - spec_entries: &[ScSpecEntry], - config: &config::Args, - ) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { - let spec = Spec(Some(spec_entries.to_vec())); - let mut cmd = clap::Command::new(self.contract_id.clone()) - .no_binary_name(true) - .term_width(300) - .max_term_width(300); - - for ScSpecFunctionV0 { name, .. } in spec.find_functions()? { - cmd = cmd.subcommand(build_custom_cmd(&name.to_utf8_string_lossy(), &spec)?); - } - cmd.build(); - let long_help = cmd.render_long_help(); - let mut matches_ = cmd.get_matches_from(&self.slop); - let Some((function, matches_)) = &matches_.remove_subcommand() else { - println!("{long_help}"); - std::process::exit(1); - }; - - let func = spec.find_function(function)?; - // create parsed_args in same order as the inputs to func - let mut signers: Vec = vec![]; - let parsed_args = func - .inputs - .iter() - .map(|i| { - let name = i.name.to_utf8_string()?; - if let Some(mut val) = matches_.get_raw(&name) { - let mut s = val.next().unwrap().to_string_lossy().to_string(); - if matches!(i.type_, ScSpecTypeDef::Address) { - let cmd = crate::commands::keys::address::Cmd { - name: s.clone(), - hd_path: Some(0), - locator: config.locator.clone(), - }; - if let Ok(address) = cmd.public_key() { - s = address.to_string(); - } - if let Ok(key) = cmd.private_key() { - signers.push(key); - } - } - spec.from_string(&s, &i.type_) - .map_err(|error| Error::CannotParseArg { arg: name, error }) - } else if matches!(i.type_, ScSpecTypeDef::Option(_)) { - Ok(ScVal::Void) - } else if let Some(arg_path) = - matches_.get_one::(&fmt_arg_file_name(&name)) - { - if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) { - Ok(ScVal::try_from( - &std::fs::read(arg_path) - .map_err(|_| Error::MissingFileArg(arg_path.clone()))?, - ) - .map_err(|()| Error::CannotParseArg { - arg: name.clone(), - error: soroban_spec_tools::Error::Unknown, - })?) - } else { - let file_contents = std::fs::read_to_string(arg_path) - .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; - tracing::debug!( - "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}", - i.type_, - file_contents.len() - ); - spec.from_string(&file_contents, &i.type_) - .map_err(|error| Error::CannotParseArg { arg: name, error }) - } - } else { - Err(Error::MissingArgument(name)) - } - }) - .collect::, Error>>()?; - - let contract_address_arg = ScAddress::Contract(Hash(contract_id)); - let function_symbol_arg = function - .try_into() - .map_err(|()| Error::FunctionNameTooLong(function.clone()))?; - - let final_args = - parsed_args - .clone() - .try_into() - .map_err(|_| Error::MaxNumberOfArgumentsReached { - current: parsed_args.len(), - maximum: ScVec::default().max_len(), - })?; - - let invoke_args = InvokeContractArgs { - contract_address: contract_address_arg, - function_name: function_symbol_arg, - args: final_args, - }; - - Ok((function.clone(), spec, invoke_args, signers)) - } - pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let res = self.invoke(global_args).await?.to_envelope(); match res { @@ -326,12 +202,12 @@ impl NetworkRunnable for Cmd { let contract_id = self .config .locator - .resolve_contract_id(&self.contract_id, &network.network_passphrase)? - .0; + .resolve_contract_id(&self.contract_id, &network.network_passphrase)?; + let spec_entries = self.spec_entries()?; if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; + let _ = build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?; } let client = rpc::Client::new(&network.rpc_url)?; let account_details = if self.is_view { @@ -351,7 +227,7 @@ impl NetworkRunnable for Cmd { let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id; let spec_entries = get_remote_contract_spec( - &contract_id, + &contract_id.0, &config.locator, &config.network, global_args, @@ -362,7 +238,7 @@ impl NetworkRunnable for Cmd { // Get the ledger footprint let (function, spec, host_function_params, signers) = - self.build_host_function_parameters(contract_id, &spec_entries, config)?; + build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?; let tx = build_invoke_contract_tx( host_function_params.clone(), sequence + 1, @@ -411,7 +287,7 @@ impl NetworkRunnable for Cmd { } }; crate::log::events(&events); - output_to_string(&spec, &return_value, &function) + Ok(output_to_string(&spec, &return_value, &function)?) } } @@ -460,24 +336,6 @@ fn default_account_entry() -> AccountEntry { } } -pub fn output_to_string( - spec: &Spec, - res: &ScVal, - function: &str, -) -> Result, Error> { - let mut res_str = String::new(); - if let Some(output) = spec.find_function(function)?.outputs.first() { - res_str = spec - .xdr_to_json(res, output) - .map_err(|e| Error::CannotPrintResult { - result: res.clone(), - error: e, - })? - .to_string(); - } - Ok(TxnResult::Res(res_str)) -} - fn build_invoke_contract_tx( parameters: InvokeContractArgs, sequence: i64, @@ -502,87 +360,6 @@ fn build_invoke_contract_tx( }) } -fn build_custom_cmd(name: &str, spec: &Spec) -> Result { - let func = spec - .find_function(name) - .map_err(|_| Error::FunctionNotFoundInContractSpec(name.to_string()))?; - - // Parse the function arguments - let inputs_map = &func - .inputs - .iter() - .map(|i| (i.name.to_utf8_string().unwrap(), i.type_.clone())) - .collect::>(); - let name: &'static str = Box::leak(name.to_string().into_boxed_str()); - let mut cmd = clap::Command::new(name) - .no_binary_name(true) - .term_width(300) - .max_term_width(300); - let kebab_name = name.to_kebab_case(); - if kebab_name != name { - cmd = cmd.alias(kebab_name); - } - let doc: &'static str = Box::leak(func.doc.to_utf8_string_lossy().into_boxed_str()); - let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str()); - - cmd = cmd.about(Some(doc)).long_about(long_doc); - for (name, type_) in inputs_map { - let mut arg = clap::Arg::new(name); - let file_arg_name = fmt_arg_file_name(name); - let mut file_arg = clap::Arg::new(&file_arg_name); - arg = arg - .long(name) - .alias(name.to_kebab_case()) - .num_args(1) - .value_parser(clap::builder::NonEmptyStringValueParser::new()) - .long_help(spec.doc(name, type_)?); - - file_arg = file_arg - .long(&file_arg_name) - .alias(file_arg_name.to_kebab_case()) - .num_args(1) - .hide(true) - .value_parser(value_parser!(PathBuf)) - .conflicts_with(name); - - if let Some(value_name) = spec.arg_value_name(type_, 0) { - let value_name: &'static str = Box::leak(value_name.into_boxed_str()); - arg = arg.value_name(value_name); - } - - // Set up special-case arg rules - arg = match type_ { - ScSpecTypeDef::Bool => arg - .num_args(0..1) - .default_missing_value("true") - .default_value("false") - .num_args(0..=1), - ScSpecTypeDef::Option(_val) => arg.required(false), - ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => { - arg.allow_hyphen_values(true) - } - _ => arg, - }; - - cmd = cmd.arg(arg); - cmd = cmd.arg(file_arg); - } - Ok(cmd) -} - -fn fmt_arg_file_name(name: &str) -> String { - format!("{name}-file-path") -} - -fn arg_file_help(docs: &str) -> String { - format!( - r#"{docs} -Usage Notes: -Each arg has a corresponding ---file-path which is a path to a file containing the corresponding JSON argument. -Note: The only types which aren't JSON are Bytes and BytesN, which are raw bytes"# - ) -} - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum, Default)] pub enum Send { /// Send transaction if simulation indicates there are ledger writes, diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 45fd00b96..761784de9 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -1,4 +1,5 @@ pub mod alias; +pub mod arg_parsing; pub mod asset; pub mod bindings; pub mod build; @@ -141,11 +142,11 @@ impl Cmd { Cmd::Bindings(bindings) => bindings.run().await?, Cmd::Build(build) => build.run()?, Cmd::Extend(extend) => extend.run().await?, - Cmd::Alias(alias) => alias.run(global_args).await?, + Cmd::Alias(alias) => alias.run(global_args)?, Cmd::Deploy(deploy) => deploy.run(global_args).await?, Cmd::Id(id) => id.run()?, Cmd::Info(info) => info.run().await?, - Cmd::Init(init) => init.run()?, + Cmd::Init(init) => init.run(global_args)?, Cmd::Inspect(inspect) => inspect.run()?, Cmd::Install(install) => install.run(global_args).await?, Cmd::Invoke(invoke) => invoke.run(global_args).await?, diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index bef3f3737..3cb253bb1 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -186,11 +186,7 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; tracing::trace!(?network); let client = Client::new(&network.rpc_url)?; - let contract = config.locator.resolve_contract_id( - self.key.contract_id.as_ref().unwrap(), - &network.network_passphrase, - )?; - let keys = self.key.parse_keys(contract)?; + let keys = self.key.parse_keys(&config.locator, &network)?; Ok(client.get_full_ledger_entries(&keys).await?) } } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 192af3140..e3e8e65ae 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -133,11 +133,7 @@ impl NetworkRunnable for Cmd { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); - let contract = config.locator.resolve_contract_id( - self.key.contract_id.as_ref().unwrap(), - &network.network_passphrase, - )?; - let entry_keys = self.key.parse_keys(contract)?; + let entry_keys = self.key.parse_keys(&config.locator, &network)?; let client = Client::new(&network.rpc_url)?; let key = config.key_pair()?; diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index a755c33d0..5e2bda66f 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -176,8 +176,6 @@ impl Cmd { OutputFormat::Pretty => event.pretty_print()?, } } - println!("Latest Ledger: {}", response.latest_ledger); - Ok(()) } diff --git a/cmd/soroban-cli/src/key.rs b/cmd/soroban-cli/src/key.rs index 885295936..b4fd358aa 100644 --- a/cmd/soroban-cli/src/key.rs +++ b/cmd/soroban-cli/src/key.rs @@ -1,12 +1,14 @@ +use crate::{ + commands::contract::Durability, + config::{locator, network::Network}, + wasm, +}; use clap::arg; use soroban_env_host::xdr::{ self, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limits, ReadXdr, ScAddress, ScVal, }; use std::path::PathBuf; -use stellar_strkey::Contract; - -use crate::{commands::contract::Durability, wasm}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -18,6 +20,8 @@ pub enum Error { CannotParseContractId(String, stellar_strkey::DecodeError), #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] + Locator(#[from] locator::Error), } #[derive(Debug, clap::Args, Clone)] @@ -61,7 +65,13 @@ pub struct Args { } impl Args { - pub fn parse_keys(&self, contract: Contract) -> Result, Error> { + pub fn parse_keys( + &self, + locator: &locator::Args, + Network { + network_passphrase, .. + }: &Network, + ) -> Result, Error> { let keys = if let Some(keys) = &self.key { keys.iter() .map(|key| { @@ -87,6 +97,8 @@ impl Args { } else { vec![ScVal::LedgerKeyContractInstance] }; + let contract = + locator.resolve_contract_id(self.contract_id.as_ref().unwrap(), network_passphrase)?; Ok(keys .into_iter() diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index ca95f652c..2afa5ec03 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -86,6 +86,7 @@ create_print_functions!(error, errorln, "❌"); create_print_functions!(globe, globeln, "🌎"); create_print_functions!(info, infoln, "ℹ️"); create_print_functions!(link, linkln, "🔗"); +create_print_functions!(plus, plusln, "➕"); create_print_functions!(save, saveln, "💾"); create_print_functions!(search, searchln, "🔎"); create_print_functions!(warn, warnln, "⚠️"); diff --git a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs b/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs index fb23e5055..f3eb78a36 100644 --- a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs +++ b/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs @@ -1,13 +1,13 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec}; +use soroban_sdk::{contract, contractimpl, vec, Env, String, Vec}; #[contract] pub struct HelloContract; #[contractimpl] impl HelloContract { - pub fn hello(env: Env, to: Symbol) -> Vec { - vec![&env, symbol_short!("Hello"), to] + pub fn hello(env: Env, to: String) -> Vec { + vec![&env, String::from_str(&env, "Hello"), to] } } diff --git a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs b/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs index e72c6bb9f..4b250446a 100644 --- a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs +++ b/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs @@ -1,7 +1,7 @@ #![cfg(test)] use super::*; -use soroban_sdk::{symbol_short, vec, Env}; +use soroban_sdk::{vec, Env, String}; #[test] fn test() { @@ -9,9 +9,13 @@ fn test() { let contract_id = env.register_contract(None, HelloContract); let client = HelloContractClient::new(&env, &contract_id); - let words = client.hello(&symbol_short!("Dev")); + let words = client.hello(&String::from_str(&env, "Dev")); assert_eq!( words, - vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),] + vec![ + &env, + String::from_str(&env, "Hello"), + String::from_str(&env, "Dev"), + ] ); }