diff --git a/README.md b/README.md index 4719288..818bcf6 100644 --- a/README.md +++ b/README.md @@ -45,32 +45,75 @@ import { Example: ```typescript -import { ParameterObjOfDescriptor, processContractCall } from "clarity-codegen"; +import { callReadOnlyFunction } from "@stacks/transactions"; +import { tupleT, stringT } from "clarity-codegen"; import { AlexContracts } from "./generated/contracts_Alex"; -import { Operation } from "./Operation"; -export type Contracts = typeof AlexContracts; -export type ContractName = keyof Contracts; - -export const callPublic = < - T extends ContractName, - F extends keyof Contracts[T], - Descriptor extends Contracts[T][F] ->( - contractOrType: T, - functionName: F, - args: ParameterObjOfDescriptor -): Operation.PublicCall => { - const contractCall = processContractCall( - AlexContracts, - contractOrType, - functionName - ); - const input = contractCall.encodeInput(args); - // Broadcast public contract or send readonly call - const output = contractCall.decodeOutput(response.output); - return output; +/** + * Let's call a readonly function + */ +const contractDeployerAddress = "..."; +const contractName = "..."; +const readonlyFunctionName = "..."; +const readonlyFunctionArgs = { + /* ... */ }; +const functionDescriptor = AlexContracts[contractName][readonlyFunctionName]; +const clarityArgs = functionDescriptor.input.map((arg) => + arg.type.encode(readonlyFunctionArgs[arg.name]) +); +const result = await callReadOnlyFunction({ + contractName, + functionName: readonlyFunctionName, + functionArgs: clarityArgs, + contractAddress: contractDeployerAddress, + senderAddress: contractDeployerAddress, +}); +console.log("result", functionDescriptor.output.decode(result)); + +/** + * Let's simply encode/decode some clarity value + */ +const schema = tupleT({ + hello: stringT, +}); +const encoded = schema.encode({ hello: "world" }); +console.log("serialized clarityValue", encoded); +console.log("deserialized clarityValue", schema.decode(encoded)); +``` + +### Make contract calls + +```typescript +import { makeContractCall, broadcastTransaction } from "@stacks/transactions"; +import { composeTxOptionsFactory, executeReadonlyCallFactory } from "clarity-codegen"; +import { AlexContracts } from "./generated/contracts_Alex"; + +const composeTxOptions = composeTxOptionsFactory(AlexContracts, { + deployerAddress: "...", +}); +const executeReadonlyCall = executeReadonlyCallFactory(AlexContracts, { + deployerAddress: "...", +}); + +// make a readonly call +console.log("decoded readonly call result", await executeReadonlyCall({ + "contract name", + "function name", + { /* arguments */ }, +})); + +// create a public call +const txOptions = composeTxOptions({ + "contract name", + "function name", + { /* arguments */ }, + { + postConditions: [ /* post conditions */ ], + }, +}); +const tx = makeContractCall({ ...txOptions, senderKey: "..." }); +await broadcastTransaction(tx); ``` ### Processing Historical Transactions diff --git a/src/runtime/composeTxOptions.ts b/src/runtime/composeTxOptions.ts new file mode 100644 index 0000000..7fe0427 --- /dev/null +++ b/src/runtime/composeTxOptions.ts @@ -0,0 +1,65 @@ +import { + AnchorMode, + ContractCallOptions, + FungiblePostCondition, + PostConditionMode, + STXPostCondition, +} from "@stacks/transactions"; +import { StringOnly } from "../utils/helpers"; +import { + ContractBaseType, + OpenCallFunctionDescriptor, + ParameterObjOfDescriptor, +} from "./contractBase"; + +export type ComposeTxOptionsFn = < + T extends StringOnly, + F extends StringOnly, + Descriptor extends Contracts[T][F] +>( + contractName: T, + functionName: F, + args: Descriptor extends OpenCallFunctionDescriptor + ? ParameterObjOfDescriptor + : never, + options?: { + postConditions?: (FungiblePostCondition | STXPostCondition)[]; + } +) => ContractCallOptions; + +export const composeTxOptionsFactory = + ( + contracts: T, + factoryOptions: { + deployerAddress: string; + } + ): ComposeTxOptionsFn => + (contractName, functionName, args, options = {}) => { + const functionDescriptor = contracts[contractName][functionName]; + + if (functionDescriptor.mode !== "public") { + throw new Error( + `[composeTx] function ${contractName}.${functionName} should be a public function` + ); + } + + const clarityArgs = functionDescriptor.input.map((arg) => + arg.type.encode(args[arg.name]) + ); + + const postConditionMode = + options.postConditions == null + ? PostConditionMode.Allow + : PostConditionMode.Deny; + const postConditions = options.postConditions; + + return { + contractName, + functionName, + functionArgs: clarityArgs, + contractAddress: factoryOptions.deployerAddress, + anchorMode: AnchorMode.Any, + postConditionMode, + postConditions, + }; + }; diff --git a/src/runtime/executeReadonlyCall.ts b/src/runtime/executeReadonlyCall.ts new file mode 100644 index 0000000..31e601d --- /dev/null +++ b/src/runtime/executeReadonlyCall.ts @@ -0,0 +1,73 @@ +import { callReadOnlyFunction } from "@stacks/transactions"; +import { StringOnly } from "../utils/helpers"; +import { + ContractBaseType, + ParameterObjOfDescriptor, + ReadonlyFunctionDescriptor, + ReturnTypeOfDescriptor, +} from "./contractBase"; + +export type CallReadOnlyFunctionFn = typeof callReadOnlyFunction; + +export type ExecuteReadonlyCallFn = < + T extends StringOnly, + F extends StringOnly, + Descriptor extends Contracts[T][F] +>( + contractName: T, + functionName: F, + args: Descriptor extends ReadonlyFunctionDescriptor + ? ParameterObjOfDescriptor + : never, + options?: { + senderAddress?: string; + callReadOnlyFunction?: CallReadOnlyFunctionFn; + } +) => Promise< + Descriptor extends ReadonlyFunctionDescriptor + ? ReturnTypeOfDescriptor + : never +>; + +export const executeReadonlyCallFactory = + ( + contracts: T, + factoryOptions: { + deployerAddress: string; + defaultSenderAddress?: string; + callReadOnlyFunction?: CallReadOnlyFunctionFn; + } + ): ExecuteReadonlyCallFn => + async (contractName, functionName, args, options = {}) => { + const functionDescriptor = contracts[contractName][functionName]; + + if (functionDescriptor.mode !== "readonly") { + throw new Error( + `[composeTx] function ${contractName}.${functionName} should be a readonly function` + ); + } + + const clarityArgs = functionDescriptor.input.map((arg) => + arg.type.encode(args[arg.name]) + ); + + const senderAddress = + options.senderAddress ?? + factoryOptions.defaultSenderAddress ?? + factoryOptions.deployerAddress; + + const _callReadOnlyFunction = + options.callReadOnlyFunction ?? + factoryOptions.callReadOnlyFunction ?? + callReadOnlyFunction; + + const result = await _callReadOnlyFunction({ + contractName, + functionName, + functionArgs: clarityArgs, + contractAddress: factoryOptions.deployerAddress, + senderAddress, + }); + + return functionDescriptor.output.decode(result); + }; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 3ed0aec..d875431 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,7 @@ import { Response } from "../runtime/types"; +export type StringOnly = Extract + export function mapValues, VO>( obj: T, mapping: (value: T[K], key: K) => VO