From 86229796736f23af002cf2b1747c8e70da0b3153 Mon Sep 17 00:00:00 2001 From: PHCitizen <75726261+PHCitizen@users.noreply.github.com> Date: Thu, 30 May 2024 05:27:21 +0000 Subject: [PATCH 01/15] added generics for contract type-safety --- packages/cashscript/src/Contract.ts | 75 +++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 1a013bfb..5afc0d1c 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -22,7 +22,71 @@ import { import SignatureTemplate from './SignatureTemplate.js'; import { ElectrumNetworkProvider } from './network/index.js'; -export class Contract { +/** + * Merge intersection type + * {foo: "foo"} & {bar: "bar"} will become {foo: "foo", bar: "bar"} + */ +type Prettify = { [K in keyof T]: T[K] } & {}; + +type TypeMap = { + bool: boolean; + int: bigint; + string: string; + bytes: Uint8Array; + bytes4: Uint8Array; + bytes32: Uint8Array; + bytes64: Uint8Array; + bytes1: Uint8Array; + // TODO: use proper value for types below + pubkey: Uint8Array; + sig: Uint8Array; + datasig: Uint8Array; +}; + +/** + * Artifact format to tuple + * T = {name: string, type: keyof TypeMap}[] + * output = [ValueOfTypeMap, ...] + */ +type GetTypeAsTuple = T extends readonly [infer A, ...infer O] + ? A extends { type: infer Type } + ? Type extends keyof TypeMap + ? [TypeMap[Type], ...GetTypeAsTuple] + : [any, ...GetTypeAsTuple] + : [any, ...GetTypeAsTuple] + : T extends [] + ? [] + : any[]; + +/** + * Iterate to each function in artifact.abi + * then use GetTypeAsTuple passing the artifact.abi[number].inputs + * to get the parameters for the function + * + * T = {name: string, inputs: {name: string, type: keyof TypeMap}[] }[] + * Output = {[NameOfTheFunction]: (...params: GetTypeAsTuple) => any} + * + */ +type _InferContractFunction = T extends { length: infer L } + ? L extends number + ? T extends readonly [infer A, ...infer O] + ? A extends { name: string; inputs: readonly any[] } + ? { + [k in A['name']]: (...p: GetTypeAsTuple) => Transaction; + } & _InferContractFunction + : {} & _InferContractFunction + : T extends [] + ? {} + : { [k: string]: (...p: any[]) => Transaction } + : { [k: string]: (...p: any[]) => Transaction } + : never; +type InferContractFunction = Prettify<_InferContractFunction>; + +export class Contract< + const TArtifact extends Artifact = any, + TContractType extends { constructorArgs: (TypeMap[keyof TypeMap])[], functions: Record } + = { constructorArgs: GetTypeAsTuple, functions: InferContractFunction } + > { name: string; address: string; tokenAddress: string; @@ -30,7 +94,8 @@ export class Contract { bytesize: number; opcount: number; - functions: Record; + functions: TContractType["functions"]; + // ? Also do inferon unlock? unlock: Record; redeemScript: Script; @@ -38,8 +103,8 @@ export class Contract { private addressType: 'p2sh20' | 'p2sh32'; constructor( - private artifact: Artifact, - constructorArgs: Argument[], + private artifact: TArtifact, + constructorArgs: TContractType["constructorArgs"], private options?: ContractOptions, ) { this.provider = this.options?.provider ?? new ElectrumNetworkProvider(); @@ -74,9 +139,11 @@ export class Contract { this.functions = {}; if (artifact.abi.length === 1) { const f = artifact.abi[0]; + // @ts-ignore generic and can only be indexed for reading this.functions[f.name] = this.createFunction(f); } else { artifact.abi.forEach((f, i) => { + // @ts-ignore generic and can only be indexed for reading this.functions[f.name] = this.createFunction(f, i); }); } From 90191fbc8df943bf5c59b9e7c3312c2d34bdbb0b Mon Sep 17 00:00:00 2001 From: PHCitizen <75726261+PHCitizen@users.noreply.github.com> Date: Fri, 7 Jun 2024 05:03:36 +0000 Subject: [PATCH 02/15] also infer for unlock --- packages/cashscript/src/Contract.ts | 30 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 5afc0d1c..19437056 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -61,10 +61,9 @@ type GetTypeAsTuple = T extends readonly [infer A, ...infer O] /** * Iterate to each function in artifact.abi * then use GetTypeAsTuple passing the artifact.abi[number].inputs - * to get the parameters for the function * * T = {name: string, inputs: {name: string, type: keyof TypeMap}[] }[] - * Output = {[NameOfTheFunction]: (...params: GetTypeAsTuple) => any} + * Output = {[NameOfTheFunction]: GetTypeAsTuple} * */ type _InferContractFunction = T extends { length: infer L } @@ -72,20 +71,26 @@ type _InferContractFunction = T extends { length: infer L } ? T extends readonly [infer A, ...infer O] ? A extends { name: string; inputs: readonly any[] } ? { - [k in A['name']]: (...p: GetTypeAsTuple) => Transaction; + [k in A['name']]: GetTypeAsTuple; } & _InferContractFunction : {} & _InferContractFunction : T extends [] ? {} - : { [k: string]: (...p: any[]) => Transaction } - : { [k: string]: (...p: any[]) => Transaction } + : { [k: string]: any[] } + : { [k: string]: any[] } : never; type InferContractFunction = Prettify<_InferContractFunction>; +type KeyArgsToFunction, ReturnVal> = { + [K in keyof T]: (...p: T[K]) => ReturnVal +} + export class Contract< - const TArtifact extends Artifact = any, + const TArtifact extends Artifact = Artifact, TContractType extends { constructorArgs: (TypeMap[keyof TypeMap])[], functions: Record } - = { constructorArgs: GetTypeAsTuple, functions: InferContractFunction } + = { constructorArgs: GetTypeAsTuple, functions: InferContractFunction }, + TFunctions extends Record Transaction> = KeyArgsToFunction, + TUnlock extends Record Unlocker> = KeyArgsToFunction, > { name: string; address: string; @@ -94,9 +99,8 @@ export class Contract< bytesize: number; opcount: number; - functions: TContractType["functions"]; - // ? Also do inferon unlock? - unlock: Record; + functions: TFunctions; + unlock: TUnlock; redeemScript: Script; provider: NetworkProvider; @@ -136,7 +140,7 @@ export class Contract< // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.functions = {}; + this.functions = {} as TFunctions; if (artifact.abi.length === 1) { const f = artifact.abi[0]; // @ts-ignore generic and can only be indexed for reading @@ -150,12 +154,14 @@ export class Contract< // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.unlock = {}; + this.unlock = {} as TUnlock; if (artifact.abi.length === 1) { const f = artifact.abi[0]; + // @ts-ignore generic and can only be indexed for reading this.unlock[f.name] = this.createUnlocker(f); } else { artifact.abi.forEach((f, i) => { + // @ts-ignore generic and can only be indexed for reading this.unlock[f.name] = this.createUnlocker(f, i); }); } From e58586897f295be65020e1b61cd9b5fa4332ed49 Mon Sep 17 00:00:00 2001 From: PHCitizen <75726261+PHCitizen@users.noreply.github.com> Date: Fri, 7 Jun 2024 05:13:38 +0000 Subject: [PATCH 03/15] remove unlock and functions from generic and force type as any --- packages/cashscript/src/Contract.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 19437056..cf883b57 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -89,8 +89,6 @@ export class Contract< const TArtifact extends Artifact = Artifact, TContractType extends { constructorArgs: (TypeMap[keyof TypeMap])[], functions: Record } = { constructorArgs: GetTypeAsTuple, functions: InferContractFunction }, - TFunctions extends Record Transaction> = KeyArgsToFunction, - TUnlock extends Record Unlocker> = KeyArgsToFunction, > { name: string; address: string; @@ -99,8 +97,8 @@ export class Contract< bytesize: number; opcount: number; - functions: TFunctions; - unlock: TUnlock; + functions: KeyArgsToFunction; + unlock: KeyArgsToFunction; redeemScript: Script; provider: NetworkProvider; @@ -140,7 +138,7 @@ export class Contract< // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.functions = {} as TFunctions; + this.functions = {} as any; if (artifact.abi.length === 1) { const f = artifact.abi[0]; // @ts-ignore generic and can only be indexed for reading @@ -154,7 +152,7 @@ export class Contract< // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.unlock = {} as TUnlock; + this.unlock = {} as any; if (artifact.abi.length === 1) { const f = artifact.abi[0]; // @ts-ignore generic and can only be indexed for reading From 04302463d20f029232678b81dfd464f2493cb6b8 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 26 Nov 2024 11:29:03 +0100 Subject: [PATCH 04/15] Update TypeMap + temporarily disable linting --- packages/cashscript/src/Contract.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index cf883b57..4c0a44bc 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ import { binToHex } from '@bitauth/libauth'; import { AbiFunction, @@ -29,20 +30,20 @@ import { ElectrumNetworkProvider } from './network/index.js'; type Prettify = { [K in keyof T]: T[K] } & {}; type TypeMap = { + [k: `bytes${number}`]: Uint8Array | string; // Matches any "bytes" pattern +} & { + byte: Uint8Array | string; + bytes: Uint8Array | string; bool: boolean; int: bigint; string: string; - bytes: Uint8Array; - bytes4: Uint8Array; - bytes32: Uint8Array; - bytes64: Uint8Array; - bytes1: Uint8Array; - // TODO: use proper value for types below - pubkey: Uint8Array; - sig: Uint8Array; - datasig: Uint8Array; + pubkey: Uint8Array | string; + sig: SignatureTemplate | Uint8Array | string; + datasig: Uint8Array | string; }; +type TypeMapValue = TypeMap[keyof TypeMap]; + /** * Artifact format to tuple * T = {name: string, type: keyof TypeMap}[] @@ -61,10 +62,10 @@ type GetTypeAsTuple = T extends readonly [infer A, ...infer O] /** * Iterate to each function in artifact.abi * then use GetTypeAsTuple passing the artifact.abi[number].inputs - * + * * T = {name: string, inputs: {name: string, type: keyof TypeMap}[] }[] * Output = {[NameOfTheFunction]: GetTypeAsTuple} - * + * */ type _InferContractFunction = T extends { length: infer L } ? L extends number From 44b4baf014ea8b797435e9099ec44e83f2127454 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 26 Nov 2024 12:17:55 +0100 Subject: [PATCH 05/15] Add TypeScript tests for inferred Contract types + export 'const' .ts artifacts --- .../test/fixture/announcement.artifact.ts | 17 +++++ .../test/fixture/hodl_vault.artifact.ts | 47 ++++++++++++++ .../cashscript/test/fixture/p2pkh.artifact.ts | 31 +++++++++ .../test/types/Contract.types.test.ts | 64 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 packages/cashscript/test/fixture/announcement.artifact.ts create mode 100644 packages/cashscript/test/fixture/hodl_vault.artifact.ts create mode 100644 packages/cashscript/test/fixture/p2pkh.artifact.ts create mode 100644 packages/cashscript/test/types/Contract.types.test.ts diff --git a/packages/cashscript/test/fixture/announcement.artifact.ts b/packages/cashscript/test/fixture/announcement.artifact.ts new file mode 100644 index 00000000..1e148582 --- /dev/null +++ b/packages/cashscript/test/fixture/announcement.artifact.ts @@ -0,0 +1,17 @@ +export default { + contractName: 'Announcement', + constructorInputs: [], + abi: [ + { + name: 'announce', + inputs: [], + }, + ], + bytecode: '6a 6d02 OP_SIZE OP_SWAP OP_CAT OP_CAT 4120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e206265696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f7720612068756d616e206265696e6720746f20636f6d6520746f206861726d2e OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT OP_0 OP_OUTPUTVALUE OP_0 OP_NUMEQUALVERIFY OP_0 OP_OUTPUTBYTECODE OP_EQUALVERIFY e803 OP_INPUTINDEX OP_UTXOVALUE OP_OVER OP_SUB OP_DUP OP_ROT OP_GREATERTHANOREQUAL OP_IF OP_1 OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_OVER OP_NUMEQUALVERIFY OP_ENDIF OP_DROP OP_1', + source: 'pragma cashscript ^0.9.0;\n\n/* This is a contract showcasing covenants outside of regular transactional use.\n * It enforces the contract to make an "announcement" on Memo.cash, and send the\n * remainder of contract funds back to the contract.\n */\ncontract Announcement() {\n function announce() {\n // Create the memo.cash announcement output\n bytes announcement = new LockingBytecodeNullData([\n 0x6d02,\n bytes(\'A contract may not injure a human being or, through inaction, allow a human being to come to harm.\')\n ]);\n\n // Check that the first tx output matches the announcement\n require(tx.outputs[0].value == 0);\n require(tx.outputs[0].lockingBytecode == announcement);\n\n // Calculate leftover money after fee (1000 sats)\n // Check that the second tx output sends the change back if there\'s enough leftover for another announcement\n int minerFee = 1000;\n int changeAmount = tx.inputs[this.activeInputIndex].value - minerFee;\n if (changeAmount >= minerFee) {\n require(tx.outputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);\n require(tx.outputs[1].value == changeAmount);\n }\n }\n}\n', + compiler: { + name: 'cashc', + version: '0.8.0-next.0', + }, + updatedAt: '2023-02-10T15:22:49.703Z', +} as const; diff --git a/packages/cashscript/test/fixture/hodl_vault.artifact.ts b/packages/cashscript/test/fixture/hodl_vault.artifact.ts new file mode 100644 index 00000000..91d90149 --- /dev/null +++ b/packages/cashscript/test/fixture/hodl_vault.artifact.ts @@ -0,0 +1,47 @@ +export default { + contractName: 'HodlVault', + constructorInputs: [ + { + name: 'ownerPk', + type: 'pubkey', + }, + { + name: 'oraclePk', + type: 'pubkey', + }, + { + name: 'minBlock', + type: 'int', + }, + { + name: 'priceTarget', + type: 'int', + }, + ], + abi: [ + { + name: 'spend', + inputs: [ + { + name: 'ownerSig', + type: 'sig', + }, + { + name: 'oracleSig', + type: 'datasig', + }, + { + name: 'oracleMessage', + type: 'bytes8', + }, + ], + }, + ], + bytecode: 'OP_6 OP_PICK OP_4 OP_SPLIT OP_SWAP OP_BIN2NUM OP_SWAP OP_BIN2NUM OP_OVER OP_5 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_3 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY OP_3 OP_ROLL OP_4 OP_ROLL OP_3 OP_ROLL OP_CHECKDATASIGVERIFY OP_CHECKSIG', + source: '// This contract forces HODLing until a certain price target has been reached\n// A minimum block is provided to ensure that oracle price entries from before this block are disregarded\n// i.e. when the BCH price was $1000 in the past, an oracle entry with the old block number and price can not be used.\n// Instead, a message with a block number and price from after the minBlock needs to be passed.\n// This contract serves as a simple example for checkDataSig-based contracts.\ncontract HodlVault(\n pubkey ownerPk,\n pubkey oraclePk,\n int minBlock,\n int priceTarget\n) {\n function spend(sig ownerSig, datasig oracleSig, bytes8 oracleMessage) {\n // message: { blockHeight, price }\n bytes4 blockHeightBin, bytes4 priceBin = oracleMessage.split(4);\n int blockHeight = int(blockHeightBin);\n int price = int(priceBin);\n\n // Check that blockHeight is after minBlock and not in the future\n require(blockHeight >= minBlock);\n require(tx.time >= blockHeight);\n\n // Check that current price is at least priceTarget\n require(price >= priceTarget);\n\n // Handle necessary signature checks\n require(checkDataSig(oracleSig, oracleMessage, oraclePk));\n require(checkSig(ownerSig, ownerPk));\n }\n}\n', + compiler: { + name: 'cashc', + version: '0.8.0-next.0', + }, + updatedAt: '2023-02-10T15:22:49.985Z', +} as const; diff --git a/packages/cashscript/test/fixture/p2pkh.artifact.ts b/packages/cashscript/test/fixture/p2pkh.artifact.ts new file mode 100644 index 00000000..d679d513 --- /dev/null +++ b/packages/cashscript/test/fixture/p2pkh.artifact.ts @@ -0,0 +1,31 @@ +export default { + contractName: 'P2PKH', + constructorInputs: [ + { + name: 'pkh', + type: 'bytes20', + }, + ], + abi: [ + { + name: 'spend', + inputs: [ + { + name: 'pk', + type: 'pubkey', + }, + { + name: 's', + type: 'sig', + }, + ], + }, + ], + bytecode: 'OP_OVER OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG', + source: 'contract P2PKH(bytes20 pkh) {\n // Require pk to match stored pkh and signature to match\n function spend(pubkey pk, sig s) {\n require(hash160(pk) == pkh);\n require(checkSig(s, pk));\n }\n}\n', + compiler: { + name: 'cashc', + version: '0.8.0-next.0', + }, + updatedAt: '2023-02-10T15:22:51.429Z', +} as const; diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts new file mode 100644 index 00000000..2faf47a2 --- /dev/null +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -0,0 +1,64 @@ +/* eslint-disable */ +import { Contract } from 'cashscript'; +import p2pkhArtifact from '../fixture/p2pkh.artifact'; +import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json'; +import announcementArtifact from '../fixture/announcement.artifact'; +import hodlVaultArtifact from '../fixture/hodl_vault.artifact'; +import { alicePkh, alicePub, oraclePub } from '../fixture/vars'; +import { binToHex } from '@bitauth/libauth'; + +// describe('P2PKH contract | single constructor input') +{ + // it('should not give type errors when using correct constructor inputs') + new Contract(p2pkhArtifact, [alicePkh]); + new Contract(p2pkhArtifact, [binToHex(alicePkh)]); + + // it('should give type errors when using empty constructor inputs') + // @ts-expect-error + new Contract(p2pkhArtifact, []); + + // it('should give type errors when using incorrect constructor input type') + // @ts-expect-error + new Contract(p2pkhArtifact, [1000n]); + + // it('should give type errors when using incorrect constructor input length') + // @ts-expect-error + new Contract(p2pkhArtifact, [alicePkh, 1000n]); + + // it('should not perform type checking when cast to any') + new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + + // it('should not perform type checking when cannot infer type') + // Note: would be very nice if it *could* infer the type from static json + new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); +} + +// describe('Announcement contract | no constructor inputs') +{ + // it('should not give type errors when using correct constructor inputs') + new Contract(announcementArtifact, []); + + // it('should give type errors when using incorrect constructor input length') + // @ts-expect-error + new Contract(announcementArtifact, [1000n]); + + // it('should give type errors when passing in completely incorrect type') + // @ts-expect-error + new Contract(announcementArtifact, 'hello'); +} + +// describe('HodlVault contract | 4 constructor inputs') +{ + // it('should not give type errors when using correct constructor inputs') + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n]); + + // it('should give type errors when using too few constructor inputs') + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)]); + + // it('should give type errors when using incorrect constructor input type') + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello']); + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n]); +} From 227c503609d07759f1e7380ff523cdb403dfc6e8 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 26 Nov 2024 12:24:10 +0100 Subject: [PATCH 06/15] Fix broken type inference tests regarding constructor inputs --- packages/cashscript/src/Contract.ts | 61 ++++++++++++++++++----------- packages/utils/src/artifact.ts | 6 +-- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 4c0a44bc..40b26a0c 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -42,22 +42,29 @@ type TypeMap = { datasig: Uint8Array | string; }; -type TypeMapValue = TypeMap[keyof TypeMap]; - -/** - * Artifact format to tuple - * T = {name: string, type: keyof TypeMap}[] - * output = [ValueOfTypeMap, ...] - */ -type GetTypeAsTuple = T extends readonly [infer A, ...infer O] - ? A extends { type: infer Type } - ? Type extends keyof TypeMap - ? [TypeMap[Type], ...GetTypeAsTuple] - : [any, ...GetTypeAsTuple] - : [any, ...GetTypeAsTuple] - : T extends [] - ? [] - : any[]; +// Helper type to process a single parameter by mapping its `type` to a value in `TypeMap`. +// Example: { type: "pubkey" } -> Uint8Array +// Branches: +// - If `Param` is a known type, it maps the `type` to `TypeMap[Type]`. +// - If `Param` has an unknown `type`, it defaults to `any`. +// - If `Param` is not an object with `type`, it defaults to `any`. +type ProcessParam = Param extends { type: infer Type } + ? Type extends keyof TypeMap + ? TypeMap[Type] + : any + : any; + +// Main type to recursively convert an array of parameter definitions into a tuple. +// Example: [{ type: "pubkey" }, { type: "int" }] -> [Uint8Array, bigint] +// Branches: +// - If `Params` is a tuple with a `Head` that matches `ProcessParam`, it processes the head and recurses on the `Tail`. +// - If `Params` is an empty tuple, it returns []. +// - If `Params` is not an array or tuple, it defaults to any[]. +type ParamsToTuple = Params extends readonly [infer Head, ...infer Tail] + ? [ProcessParam, ...ParamsToTuple] + : Params extends readonly [] + ? [] + : any[]; /** * Iterate to each function in artifact.abi @@ -72,7 +79,7 @@ type _InferContractFunction = T extends { length: infer L } ? T extends readonly [infer A, ...infer O] ? A extends { name: string; inputs: readonly any[] } ? { - [k in A['name']]: GetTypeAsTuple; + [k in A['name']]: ParamsToTuple; } & _InferContractFunction : {} & _InferContractFunction : T extends [] @@ -86,10 +93,18 @@ type KeyArgsToFunction, ReturnVal> = { [K in keyof T]: (...p: T[K]) => ReturnVal } +// TODO: Update type inference for function calls + export class Contract< - const TArtifact extends Artifact = Artifact, - TContractType extends { constructorArgs: (TypeMap[keyof TypeMap])[], functions: Record } - = { constructorArgs: GetTypeAsTuple, functions: InferContractFunction }, + TArtifact extends Artifact = Artifact, + TResolved extends { + constructorInputs: any[]; + functions: Record; + } + = { + constructorInputs: ParamsToTuple; + functions: InferContractFunction; + } > { name: string; address: string; @@ -98,8 +113,8 @@ export class Contract< bytesize: number; opcount: number; - functions: KeyArgsToFunction; - unlock: KeyArgsToFunction; + functions: KeyArgsToFunction; + unlock: KeyArgsToFunction; redeemScript: Script; provider: NetworkProvider; @@ -107,7 +122,7 @@ export class Contract< constructor( private artifact: TArtifact, - constructorArgs: TContractType["constructorArgs"], + constructorArgs: TResolved["constructorInputs"], private options?: ContractOptions, ) { this.provider = this.options?.provider ?? new ElectrumNetworkProvider(); diff --git a/packages/utils/src/artifact.ts b/packages/utils/src/artifact.ts index 8ddf15f1..aa4bae4b 100644 --- a/packages/utils/src/artifact.ts +++ b/packages/utils/src/artifact.ts @@ -8,13 +8,13 @@ export interface AbiInput { export interface AbiFunction { name: string; covenant?: boolean; - inputs: AbiInput[]; + inputs: readonly AbiInput[]; } export interface Artifact { contractName: string; - constructorInputs: AbiInput[]; - abi: AbiFunction[]; + constructorInputs: readonly AbiInput[]; + abi: readonly AbiFunction[]; bytecode: string; source: string; compiler: { From 4539b221faf9329e7e094e4acb1b63c0733af061 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 28 Nov 2024 10:14:04 +0100 Subject: [PATCH 07/15] Add (broken) tests for contract function typings --- .../fixture/transfer_with_timeout.artifact.ts | 44 +++++ .../test/types/Contract.types.test.ts | 168 +++++++++++++----- 2 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts diff --git a/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts b/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts new file mode 100644 index 00000000..39add929 --- /dev/null +++ b/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts @@ -0,0 +1,44 @@ +export default { + contractName: 'TransferWithTimeout', + constructorInputs: [ + { + name: 'sender', + type: 'pubkey', + }, + { + name: 'recipient', + type: 'pubkey', + }, + { + name: 'timeout', + type: 'int', + }, + ], + abi: [ + { + name: 'transfer', + inputs: [ + { + name: 'recipientSig', + type: 'sig', + }, + ], + }, + { + name: 'timeout', + inputs: [ + { + name: 'senderSig', + type: 'sig', + }, + ], + }, + ], + bytecode: 'OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_4 OP_ROLL OP_ROT OP_CHECKSIG OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_2DROP OP_1 OP_ENDIF', + source: "contract TransferWithTimeout(\n pubkey sender,\n pubkey recipient,\n int timeout\n) {\n // Require recipient's signature to match\n function transfer(sig recipientSig) {\n require(checkSig(recipientSig, recipient));\n }\n\n // Require timeout time to be reached and sender's signature to match\n function timeout(sig senderSig) {\n require(checkSig(senderSig, sender));\n require(tx.time >= timeout);\n }\n}\n", + compiler: { + name: 'cashc', + version: '0.8.0-next.0', + }, + updatedAt: '2023-02-10T15:22:51.144Z', +}; diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index 2faf47a2..592ff119 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,64 +1,148 @@ /* eslint-disable */ -import { Contract } from 'cashscript'; +import { Contract, SignatureTemplate } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json'; import announcementArtifact from '../fixture/announcement.artifact'; import hodlVaultArtifact from '../fixture/hodl_vault.artifact'; -import { alicePkh, alicePub, oraclePub } from '../fixture/vars'; +import transferWithTimeoutArtifact from '../fixture/transfer_with_timeout.artifact'; +import { alicePkh, alicePriv, alicePub, bobPub, oraclePub } from '../fixture/vars'; import { binToHex } from '@bitauth/libauth'; -// describe('P2PKH contract | single constructor input') +// describe('P2PKH contract | single constructor input | single function (2 args)') { - // it('should not give type errors when using correct constructor inputs') - new Contract(p2pkhArtifact, [alicePkh]); - new Contract(p2pkhArtifact, [binToHex(alicePkh)]); + // describe('Contructor arguments') + { + // it('should not give type errors when using correct constructor inputs') + new Contract(p2pkhArtifact, [alicePkh]); + new Contract(p2pkhArtifact, [binToHex(alicePkh)]); - // it('should give type errors when using empty constructor inputs') - // @ts-expect-error - new Contract(p2pkhArtifact, []); + // it('should give type errors when using empty constructor inputs') + // @ts-expect-error + new Contract(p2pkhArtifact, []); - // it('should give type errors when using incorrect constructor input type') - // @ts-expect-error - new Contract(p2pkhArtifact, [1000n]); + // it('should give type errors when using incorrect constructor input type') + // @ts-expect-error + new Contract(p2pkhArtifact, [1000n]); - // it('should give type errors when using incorrect constructor input length') - // @ts-expect-error - new Contract(p2pkhArtifact, [alicePkh, 1000n]); + // it('should give type errors when using incorrect constructor input length') + // @ts-expect-error + new Contract(p2pkhArtifact, [alicePkh, 1000n]); - // it('should not perform type checking when cast to any') - new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + // it('should not perform type checking when cast to any') + new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); - // it('should not perform type checking when cannot infer type') - // Note: would be very nice if it *could* infer the type from static json - new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + // it('should not perform type checking when cannot infer type') + // Note: would be very nice if it *could* infer the type from static json + new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + } + + // describe('Contract functions') + { + const contract = new Contract(p2pkhArtifact, [alicePkh]); + + // it('should not give type errors when using correct function inputs') + contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)); + + // it('should give type errors when calling a function that does not exist') + // @ts-expect-error + contract.functions.notAFunction(); + + // it('should give type errors when using incorrect function input types') + // @ts-expect-error + contract.functions.spend(1000n, true); + + // it('should give type errors when using incorrect function input length') + // @ts-expect-error + contract.functions.spend(alicePub, new SignatureTemplate(alicePriv), 100n); + // @ts-expect-error + contract.functions.spend(alicePub); + } +} + +// describe('Announcement contract | no constructor inputs | single function (no args)') +{ + // describe('Constructor arguments') + { + // it('should not give type errors when using correct constructor inputs') + new Contract(announcementArtifact, []); + + // it('should give type errors when using incorrect constructor input length') + // @ts-expect-error + new Contract(announcementArtifact, [1000n]); + + // it('should give type errors when passing in completely incorrect type') + // @ts-expect-error + new Contract(announcementArtifact, 'hello'); + } + + // describe('Contract functions') + { + // it('should not give type errors when using correct function inputs') + const contract = new Contract(announcementArtifact, []); + + // it('should not give type errors when using correct function inputs') + contract.functions.announce(); + + // it('should give type errors when calling a function that does not exist') + // @ts-expect-error + contract.functions.notAFunction(); + + // it('should give type errors when using incorrect function input length') + // @ts-expect-error + contract.functions.announce('hello world'); + } } -// describe('Announcement contract | no constructor inputs') +// describe('HodlVault contract | 4 constructor inputs | single function (3 args)') { - // it('should not give type errors when using correct constructor inputs') - new Contract(announcementArtifact, []); + // describe('Constructor arguments') + { + // it('should not give type errors when using correct constructor inputs') + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n]); - // it('should give type errors when using incorrect constructor input length') - // @ts-expect-error - new Contract(announcementArtifact, [1000n]); + // it('should give type errors when using too few constructor inputs') + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)]); - // it('should give type errors when passing in completely incorrect type') - // @ts-expect-error - new Contract(announcementArtifact, 'hello'); + // it('should give type errors when using incorrect constructor input type') + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello']); + // @ts-expect-error + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n]); + } } -// describe('HodlVault contract | 4 constructor inputs') + +// describe('TransferWithTimeout contract | 3 constructor inputs | two functions (1 arg each)') { - // it('should not give type errors when using correct constructor inputs') - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n]); - - // it('should give type errors when using too few constructor inputs') - // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)]); - - // it('should give type errors when using incorrect constructor input type') - // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello']); - // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n]); + // describe('Constructor arguments') + { + // it('should not give type errors when using correct constructor inputs') + new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + } + + // describe('Contract functions') + { + const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + + // it('should not give type errors when using correct function inputs') + contract.functions.transfer(new SignatureTemplate(alicePriv)); + contract.functions.timeout(new SignatureTemplate(alicePriv)); + + // it('should give type errors when calling a function that does not exist') + // @ts-expect-error + contract.functions.notAFunction(); + + // it('should give type errors when using incorrect function input types') + // @ts-expect-error + contract.functions.transfer(1000n); + // @ts-expect-error + contract.functions.timeout('hello'); + + // it('should give type errors when using incorrect function input length') + // @ts-expect-error + contract.functions.transfer(new SignatureTemplate(alicePub), 100n); + // @ts-expect-error + contract.functions.timeout(); + } } From 5a6478b86d0aa9fff70ca9a0c8ff43762cef5fc4 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 28 Nov 2024 11:17:59 +0100 Subject: [PATCH 08/15] Fix some broken tests, add more tests, and refactor complex types --- packages/cashscript/src/Contract.ts | 80 +++++++++++-------- .../fixture/transfer_with_timeout.artifact.ts | 2 +- .../test/types/Contract.types.test.ts | 15 +++- 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 40b26a0c..a50d4361 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -23,12 +23,6 @@ import { import SignatureTemplate from './SignatureTemplate.js'; import { ElectrumNetworkProvider } from './network/index.js'; -/** - * Merge intersection type - * {foo: "foo"} & {bar: "bar"} will become {foo: "foo", bar: "bar"} - */ -type Prettify = { [K in keyof T]: T[K] } & {}; - type TypeMap = { [k: `bytes${number}`]: Uint8Array | string; // Matches any "bytes" pattern } & { @@ -66,32 +60,46 @@ type ParamsToTuple = Params extends readonly [infer Head, ...infer Tail] ? [] : any[]; -/** - * Iterate to each function in artifact.abi - * then use GetTypeAsTuple passing the artifact.abi[number].inputs - * - * T = {name: string, inputs: {name: string, type: keyof TypeMap}[] }[] - * Output = {[NameOfTheFunction]: GetTypeAsTuple} - * - */ -type _InferContractFunction = T extends { length: infer L } - ? L extends number - ? T extends readonly [infer A, ...infer O] - ? A extends { name: string; inputs: readonly any[] } - ? { - [k in A['name']]: ParamsToTuple; - } & _InferContractFunction - : {} & _InferContractFunction - : T extends [] - ? {} - : { [k: string]: any[] } - : { [k: string]: any[] } - : never; -type InferContractFunction = Prettify<_InferContractFunction>; - -type KeyArgsToFunction, ReturnVal> = { - [K in keyof T]: (...p: T[K]) => ReturnVal -} +// Processes a single function definition into a function mapping with parameters and return type. +// Example: { name: "transfer", inputs: [{ type: "int" }] } -> { transfer: (arg0: bigint) => ReturnType } +// Branches: +// - Branch 1: If `Function` is an object with `name` and `inputs`, it creates a function mapping. +// - Branch 2: If `Function` does not match the expected shape, it returns an empty object. +type ProcessFunction = Function extends { name: string; inputs: readonly any[] } + ? { + [functionName in Function["name"]]: (...functionParameters: ParamsToTuple) => ReturnType; + } + : {}; + +// Recursively converts an ABI into a function map with parameter typings and return type. +// Example: +// [ +// { name: "transfer", inputs: [{ type: "int" }] }, +// { name: "approve", inputs: [{ type: "address" }, { type: "int" }] } +// ] -> +// { transfer: (arg0: bigint) => ReturnType; approve: (arg0: string, arg1: bigint) => ReturnType } +// Branches: +// - Branch 1: If `Abi` is `unknown` or `any`, return a default function map with generic parameters and return type. +// - Branch 2: If `Abi` is a tuple with a `Head`, process `Head` using `ProcessFunction` and recurse on the `Tail`. +// - Branch 3: If `Abi` is an empty tuple, return an empty object. +// - Branch 4: If `Abi` is not an array or tuple, return a generic function map. +type _AbiToFunctionMap = + // Check if Abi is typed as `any`, in which case we return a default function map + unknown extends Abi + ? GenericFunctionMap + : Abi extends readonly [infer Head, ...infer Tail] + ? ProcessFunction & _AbiToFunctionMap + : Abi extends readonly [] + ? {} + : GenericFunctionMap; + +type GenericFunctionMap = { [functionName: string]: (...functionParameters: any[]) => ReturnType }; + +// Merge intersection type +// Example: {foo: "foo"} & {bar: "bar"} -> {foo: "foo", bar: "bar"} +type Prettify = { [K in keyof T]: T[K] } & {}; + +type AbiToFunctionMap = Prettify<_AbiToFunctionMap>; // TODO: Update type inference for function calls @@ -100,10 +108,12 @@ export class Contract< TResolved extends { constructorInputs: any[]; functions: Record; + unlock: Record; } = { constructorInputs: ParamsToTuple; - functions: InferContractFunction; + functions: AbiToFunctionMap; + unlock: AbiToFunctionMap; } > { name: string; @@ -113,8 +123,8 @@ export class Contract< bytesize: number; opcount: number; - functions: KeyArgsToFunction; - unlock: KeyArgsToFunction; + functions: TResolved["functions"]; + unlock: TResolved["unlock"]; redeemScript: Script; provider: NetworkProvider; diff --git a/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts b/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts index 39add929..8a16ce38 100644 --- a/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts +++ b/packages/cashscript/test/fixture/transfer_with_timeout.artifact.ts @@ -41,4 +41,4 @@ export default { version: '0.8.0-next.0', }, updatedAt: '2023-02-10T15:22:51.144Z', -}; +} as const; diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index 592ff119..54291818 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -56,6 +56,19 @@ import { binToHex } from '@bitauth/libauth'; contract.functions.spend(alicePub, new SignatureTemplate(alicePriv), 100n); // @ts-expect-error contract.functions.spend(alicePub); + + // it('should not perform type checking when cast to any') + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + contractAsAny.functions.notAFunction(); + contractAsAny.functions.spend(); + contractAsAny.functions.spend(1000n, true); + + // it('should not perform type checking when cannot infer type') + // Note: would be very nice if it *could* infer the type from static json + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + contractFromUnknown.functions.notAFunction(); + contractFromUnknown.functions.spend(); + contractFromUnknown.functions.spend(1000n, true); } } @@ -137,7 +150,7 @@ import { binToHex } from '@bitauth/libauth'; // @ts-expect-error contract.functions.transfer(1000n); // @ts-expect-error - contract.functions.timeout('hello'); + contract.functions.timeout(true); // it('should give type errors when using incorrect function input length') // @ts-expect-error From 15c6b746b995a2ee600f8585040b0a1a40cc2d22 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 28 Nov 2024 11:29:32 +0100 Subject: [PATCH 09/15] Add tests for unlockers and for correct return types --- packages/cashscript/src/Contract.ts | 2 - .../test/types/Contract.types.test.ts | 59 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index a50d4361..264cce85 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -101,8 +101,6 @@ type Prettify = { [K in keyof T]: T[K] } & {}; type AbiToFunctionMap = Prettify<_AbiToFunctionMap>; -// TODO: Update type inference for function calls - export class Contract< TArtifact extends Artifact = Artifact, TResolved extends { diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index 54291818..9062667b 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { Contract, SignatureTemplate } from 'cashscript'; +import { Contract, SignatureTemplate, Transaction } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json'; import announcementArtifact from '../fixture/announcement.artifact'; @@ -41,7 +41,7 @@ import { binToHex } from '@bitauth/libauth'; const contract = new Contract(p2pkhArtifact, [alicePkh]); // it('should not give type errors when using correct function inputs') - contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)); + contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)).build(); // it('should give type errors when calling a function that does not exist') // @ts-expect-error @@ -59,16 +59,67 @@ import { binToHex } from '@bitauth/libauth'; // it('should not perform type checking when cast to any') const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); - contractAsAny.functions.notAFunction(); + contractAsAny.functions.notAFunction().build(); contractAsAny.functions.spend(); contractAsAny.functions.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); - contractFromUnknown.functions.notAFunction(); + contractFromUnknown.functions.notAFunction().build(); contractFromUnknown.functions.spend(); contractFromUnknown.functions.spend(1000n, true); + + // it('should give type errors when calling methods that do not exist on the returned object') + // @ts-expect-error + contract.functions.spend().notAFunction(); + // @ts-expect-error + contractAsAny.functions.spend().notAFunction(); + // @ts-expect-error + contractFromUnknown.functions.spend().notAFunction(); + } + + // describe('Contract unlockers') + { + const contract = new Contract(p2pkhArtifact, [alicePkh]); + + // it('should not give type errors when using correct function inputs') + contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)).generateLockingBytecode(); + + // it('should give type errors when calling a function that does not exist') + // @ts-expect-error + contract.unlock.notAFunction(); + + // it('should give type errors when using incorrect function input types') + // @ts-expect-error + contract.unlock.spend(1000n, true); + + // it('should give type errors when using incorrect function input length') + // @ts-expect-error + contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv), 100n); + // @ts-expect-error + contract.unlock.spend(alicePub); + + // it('should not perform type checking when cast to any') + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + contractAsAny.unlock.notAFunction().generateLockingBytecode(); + contractAsAny.unlock.spend(); + contractAsAny.unlock.spend(1000n, true); + + // it('should not perform type checking when cannot infer type') + // Note: would be very nice if it *could* infer the type from static json + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + contractFromUnknown.unlock.notAFunction().generateLockingBytecode(); + contractFromUnknown.unlock.spend(); + contractFromUnknown.unlock.spend(1000n, true); + + // it('should give type errors when calling methods that do not exist on the returned object') + // @ts-expect-error + contract.unlock.spend().notAFunction(); + // @ts-expect-error + contractAsAny.unlock.spend().notAFunction(); + // @ts-expect-error + contractFromUnknown.unlock.spend().notAFunction(); } } From 2010edb9461297ae439258d62971e21d2e6731d4 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 28 Nov 2024 11:34:00 +0100 Subject: [PATCH 10/15] Move type inference types to separate file and re-enable linting --- packages/cashscript/src/Contract.ts | 96 ++----------------- .../cashscript/src/types/type-inference.ts | 79 +++++++++++++++ 2 files changed, 88 insertions(+), 87 deletions(-) create mode 100644 packages/cashscript/src/types/type-inference.ts diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 264cce85..570f38e2 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import { binToHex } from '@bitauth/libauth'; import { AbiFunction, @@ -22,84 +21,7 @@ import { } from './utils.js'; import SignatureTemplate from './SignatureTemplate.js'; import { ElectrumNetworkProvider } from './network/index.js'; - -type TypeMap = { - [k: `bytes${number}`]: Uint8Array | string; // Matches any "bytes" pattern -} & { - byte: Uint8Array | string; - bytes: Uint8Array | string; - bool: boolean; - int: bigint; - string: string; - pubkey: Uint8Array | string; - sig: SignatureTemplate | Uint8Array | string; - datasig: Uint8Array | string; -}; - -// Helper type to process a single parameter by mapping its `type` to a value in `TypeMap`. -// Example: { type: "pubkey" } -> Uint8Array -// Branches: -// - If `Param` is a known type, it maps the `type` to `TypeMap[Type]`. -// - If `Param` has an unknown `type`, it defaults to `any`. -// - If `Param` is not an object with `type`, it defaults to `any`. -type ProcessParam = Param extends { type: infer Type } - ? Type extends keyof TypeMap - ? TypeMap[Type] - : any - : any; - -// Main type to recursively convert an array of parameter definitions into a tuple. -// Example: [{ type: "pubkey" }, { type: "int" }] -> [Uint8Array, bigint] -// Branches: -// - If `Params` is a tuple with a `Head` that matches `ProcessParam`, it processes the head and recurses on the `Tail`. -// - If `Params` is an empty tuple, it returns []. -// - If `Params` is not an array or tuple, it defaults to any[]. -type ParamsToTuple = Params extends readonly [infer Head, ...infer Tail] - ? [ProcessParam, ...ParamsToTuple] - : Params extends readonly [] - ? [] - : any[]; - -// Processes a single function definition into a function mapping with parameters and return type. -// Example: { name: "transfer", inputs: [{ type: "int" }] } -> { transfer: (arg0: bigint) => ReturnType } -// Branches: -// - Branch 1: If `Function` is an object with `name` and `inputs`, it creates a function mapping. -// - Branch 2: If `Function` does not match the expected shape, it returns an empty object. -type ProcessFunction = Function extends { name: string; inputs: readonly any[] } - ? { - [functionName in Function["name"]]: (...functionParameters: ParamsToTuple) => ReturnType; - } - : {}; - -// Recursively converts an ABI into a function map with parameter typings and return type. -// Example: -// [ -// { name: "transfer", inputs: [{ type: "int" }] }, -// { name: "approve", inputs: [{ type: "address" }, { type: "int" }] } -// ] -> -// { transfer: (arg0: bigint) => ReturnType; approve: (arg0: string, arg1: bigint) => ReturnType } -// Branches: -// - Branch 1: If `Abi` is `unknown` or `any`, return a default function map with generic parameters and return type. -// - Branch 2: If `Abi` is a tuple with a `Head`, process `Head` using `ProcessFunction` and recurse on the `Tail`. -// - Branch 3: If `Abi` is an empty tuple, return an empty object. -// - Branch 4: If `Abi` is not an array or tuple, return a generic function map. -type _AbiToFunctionMap = - // Check if Abi is typed as `any`, in which case we return a default function map - unknown extends Abi - ? GenericFunctionMap - : Abi extends readonly [infer Head, ...infer Tail] - ? ProcessFunction & _AbiToFunctionMap - : Abi extends readonly [] - ? {} - : GenericFunctionMap; - -type GenericFunctionMap = { [functionName: string]: (...functionParameters: any[]) => ReturnType }; - -// Merge intersection type -// Example: {foo: "foo"} & {bar: "bar"} -> {foo: "foo", bar: "bar"} -type Prettify = { [K in keyof T]: T[K] } & {}; - -type AbiToFunctionMap = Prettify<_AbiToFunctionMap>; +import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js'; export class Contract< TArtifact extends Artifact = Artifact, @@ -109,11 +31,11 @@ export class Contract< unlock: Record; } = { - constructorInputs: ParamsToTuple; - functions: AbiToFunctionMap; - unlock: AbiToFunctionMap; - } - > { + constructorInputs: ParamsToTuple; + functions: AbiToFunctionMap; + unlock: AbiToFunctionMap; + }, +> { name: string; address: string; tokenAddress: string; @@ -121,8 +43,8 @@ export class Contract< bytesize: number; opcount: number; - functions: TResolved["functions"]; - unlock: TResolved["unlock"]; + functions: TResolved['functions']; + unlock: TResolved['unlock']; redeemScript: Script; provider: NetworkProvider; @@ -130,7 +52,7 @@ export class Contract< constructor( private artifact: TArtifact, - constructorArgs: TResolved["constructorInputs"], + constructorArgs: TResolved['constructorInputs'], private options?: ContractOptions, ) { this.provider = this.options?.provider ?? new ElectrumNetworkProvider(); diff --git a/packages/cashscript/src/types/type-inference.ts b/packages/cashscript/src/types/type-inference.ts new file mode 100644 index 00000000..bef3df50 --- /dev/null +++ b/packages/cashscript/src/types/type-inference.ts @@ -0,0 +1,79 @@ +import type SignatureTemplate from '../SignatureTemplate.js'; + +type TypeMap = { + [k: `bytes${number}`]: Uint8Array | string; // Matches any "bytes" pattern +} & { + byte: Uint8Array | string; + bytes: Uint8Array | string; + bool: boolean; + int: bigint; + string: string; + pubkey: Uint8Array | string; + sig: SignatureTemplate | Uint8Array | string; + datasig: Uint8Array | string; +}; + +// Helper type to process a single parameter by mapping its `type` to a value in `TypeMap`. +// Example: { type: "pubkey" } -> Uint8Array +// Branches: +// - If `Param` is a known type, it maps the `type` to `TypeMap[Type]`. +// - If `Param` has an unknown `type`, it defaults to `any`. +// - If `Param` is not an object with `type`, it defaults to `any`. +type ProcessParam = Param extends { type: infer Type } + ? Type extends keyof TypeMap + ? TypeMap[Type] + : any + : any; + +// Main type to recursively convert an array of parameter definitions into a tuple. +// Example: [{ type: "pubkey" }, { type: "int" }] -> [Uint8Array, bigint] +// Branches: +// - If `Params` is a tuple with a `Head` that matches `ProcessParam`, it processes the head and recurses on the `Tail`. +// - If `Params` is an empty tuple, it returns []. +// - If `Params` is not an array or tuple, it defaults to any[]. +export type ParamsToTuple = Params extends readonly [infer Head, ...infer Tail] + ? [ProcessParam, ...ParamsToTuple] + : Params extends readonly [] + ? [] + : any[]; + +// Processes a single function definition into a function mapping with parameters and return type. +// Example: { name: "transfer", inputs: [{ type: "int" }] } -> { transfer: (arg0: bigint) => ReturnType } +// Branches: +// - Branch 1: If `Function` is an object with `name` and `inputs`, it creates a function mapping. +// - Branch 2: If `Function` does not match the expected shape, it returns an empty object. +type ProcessFunction = Function extends { name: string; inputs: readonly any[] } + ? { + [functionName in Function['name']]: (...functionParameters: ParamsToTuple) => ReturnType; + } + : {}; + +// Recursively converts an ABI into a function map with parameter typings and return type. +// Example: +// [ +// { name: "transfer", inputs: [{ type: "int" }] }, +// { name: "approve", inputs: [{ type: "address" }, { type: "int" }] } +// ] -> +// { transfer: (arg0: bigint) => ReturnType; approve: (arg0: string, arg1: bigint) => ReturnType } +// Branches: +// - Branch 1: If `Abi` is `unknown` or `any`, return a default function map with generic parameters and return type. +// - Branch 2: If `Abi` is a tuple with a `Head`, process `Head` using `ProcessFunction` and recurse on the `Tail`. +// - Branch 3: If `Abi` is an empty tuple, return an empty object. +// - Branch 4: If `Abi` is not an array or tuple, return a generic function map. +type InternalAbiToFunctionMap = + // Check if Abi is typed as `any`, in which case we return a default function map + unknown extends Abi + ? GenericFunctionMap + : Abi extends readonly [infer Head, ...infer Tail] + ? ProcessFunction & InternalAbiToFunctionMap + : Abi extends readonly [] + ? {} + : GenericFunctionMap; + +type GenericFunctionMap = { [functionName: string]: (...functionParameters: any[]) => ReturnType }; + +// Merge intersection type +// Example: {foo: "foo"} & {bar: "bar"} -> {foo: "foo", bar: "bar"} +type Prettify = { [K in keyof T]: T[K] } & {}; + +export type AbiToFunctionMap = Prettify>; From 0e08f025ebf71858b486a6030955c0ff5f766f0f Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 3 Dec 2024 10:18:54 +0100 Subject: [PATCH 11/15] Add extra test for manual Artifact type --- .../test/types/Contract.types.test.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index 9062667b..ea1cc81c 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { Contract, SignatureTemplate, Transaction } from 'cashscript'; +import { Artifact, Contract, SignatureTemplate, Transaction, Unlocker } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json'; import announcementArtifact from '../fixture/announcement.artifact'; @@ -8,6 +8,30 @@ import transferWithTimeoutArtifact from '../fixture/transfer_with_timeout.artifa import { alicePkh, alicePriv, alicePub, bobPub, oraclePub } from '../fixture/vars'; import { binToHex } from '@bitauth/libauth'; +interface ManualArtifactType extends Artifact { + constructorInputs: [ + { + name: 'pkh', + type: 'bytes20', + }, + ], + abi: [ + { + name: 'spend', + inputs: [ + { + name: 'pk', + type: 'pubkey', + }, + { + name: 's', + type: 'sig', + }, + ], + }, + ] +} + // describe('P2PKH contract | single constructor input | single function (2 args)') { // describe('Contructor arguments') @@ -34,6 +58,10 @@ import { binToHex } from '@bitauth/libauth'; // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + + // it('should perform type checking when manually specifying a type + // @ts-expect-error + new Contract(p2pkhArtifactJsonNotConst as any, [alicePkh, 1000n]); } // describe('Contract functions') From 72bea1078aa3a3a55c46ed9c2c63bf30565430bc Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 3 Dec 2024 11:20:31 +0100 Subject: [PATCH 12/15] Add '--format' flag to cashc cli that allows formatting as json or ts --- packages/cashc/src/cashc-cli.ts | 12 ++++++-- packages/cashscript/src/Contract.ts | 2 +- packages/utils/src/artifact.ts | 47 +++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/cashc/src/cashc-cli.ts b/packages/cashc/src/cashc-cli.ts index 5ba31a45..7b353790 100644 --- a/packages/cashc/src/cashc-cli.ts +++ b/packages/cashc/src/cashc-cli.ts @@ -5,10 +5,11 @@ import { calculateBytesize, countOpcodes, exportArtifact, + formatArtifact, scriptToAsm, scriptToBytecode, } from '@cashscript/utils'; -import { program } from 'commander'; +import { program, Option } from 'commander'; import fs from 'fs'; import path from 'path'; import { compileFile, version } from './index.js'; @@ -23,6 +24,11 @@ program .option('-A, --asm', 'Compile the contract to ASM format rather than a full artifact.') .option('-c, --opcount', 'Display the number of opcodes in the compiled bytecode.') .option('-s, --size', 'Display the size in bytes of the compiled bytecode.') + .addOption( + new Option('-f, --format ', 'Specify the format of the output.') + .choices(['json', 'ts']) + .default('json'), + ) .helpOption('-?, --help', 'Display help') .parse(); @@ -82,10 +88,10 @@ function run(): void { if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } - exportArtifact(artifact, outputFile); + exportArtifact(artifact, outputFile, opts.format); } else { // Output artifact to STDOUT - console.log(JSON.stringify(artifact, null, 2)); + console.log(formatArtifact(artifact, opts.format)); } } catch (e: any) { abort(e.message); diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index 570f38e2..78a650b1 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -105,7 +105,7 @@ export class Contract< this.unlock[f.name] = this.createUnlocker(f); } else { artifact.abi.forEach((f, i) => { - // @ts-ignore generic and can only be indexed for reading + // @ts-ignore generic and can only be indexed for reading this.unlock[f.name] = this.createUnlocker(f, i); }); } diff --git a/packages/utils/src/artifact.ts b/packages/utils/src/artifact.ts index aa4bae4b..0655bd5b 100644 --- a/packages/utils/src/artifact.ts +++ b/packages/utils/src/artifact.ts @@ -28,7 +28,50 @@ export function importArtifact(artifactFile: PathLike): Artifact { return JSON.parse(fs.readFileSync(artifactFile, { encoding: 'utf-8' })); } -export function exportArtifact(artifact: Artifact, targetFile: string): void { - const jsonString = JSON.stringify(artifact, null, 2); +export function exportArtifact(artifact: Artifact, targetFile: string, format: 'json' | 'ts'): void { + const jsonString = formatArtifact(artifact, format); fs.writeFileSync(targetFile, jsonString); } + +export function formatArtifact(artifact: Artifact, format: 'json' | 'ts'): string { + if (format === 'ts') { + return `export default ${stringifyAsTs(artifact)} as const;\n`; + } + + return JSON.stringify(artifact, null, 2); +} + +const indent = (level: number): string => ' '.repeat(level); + +function stringifyAsTs(obj: any, indentationLevel: number = 1): string { + // For strings, we use JSON.stringify, but we convert double quotes to single quotes + if (typeof obj === 'string') { + return JSON.stringify(obj).replace(/'/g, "\\'").replace(/"/g, "'"); + } + + // Numbers and booleans are just converted to strings + if (typeof obj === 'number' || typeof obj === 'boolean') { + return JSON.stringify(obj); + } + + // Arrays are recursively formatted with indentation + if (Array.isArray(obj)) { + if (obj.length === 0) return '[]'; + const formattedItems = obj.map((item) => `${indent(indentationLevel)}${stringifyAsTs(item, indentationLevel + 1)}`); + return `[\n${formattedItems.join(',\n')},\n${indent(indentationLevel - 1)}]`; + } + + // Objects are recursively formatted with indentation + if (typeof obj === 'object') { + const entries = Object.entries(obj); + + if (entries.length === 0) return '{}'; + + const formattedEntries = entries.map(([key, value]) => ( + `${indent(indentationLevel)}${key}: ${stringifyAsTs(value, indentationLevel + 1)}` + )); + return `{\n${formattedEntries.join(',\n')},\n${indent(indentationLevel - 1)}}`; + } + + throw new Error(`Unsupported type: ${typeof obj}`); +} From 1435d4b0be107aa6a93559bf7a9ec95c9e6db101 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 3 Dec 2024 11:36:44 +0100 Subject: [PATCH 13/15] Update LibauthTemplate typings after merge with master --- packages/cashscript/src/Contract.ts | 2 +- packages/cashscript/src/LibauthTemplate.ts | 10 +++++----- packages/cashscript/src/utils.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index fbb4104c..da7110a7 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -55,7 +55,7 @@ export class Contract< public encodedConstructorArgs: Uint8Array[]; constructor( - private artifact: TArtifact, + public artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], private options?: ContractOptions, ) { diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts index e07faa14..b931c50d 100644 --- a/packages/cashscript/src/LibauthTemplate.ts +++ b/packages/cashscript/src/LibauthTemplate.ts @@ -105,14 +105,14 @@ export const buildTemplate = async ({ template.scripts[unlockScriptName] = { name: unlockScriptName, script: - `<${signatureString}>\n<${placeholderKeyName}.public_key>`, + `<${signatureString}>\n<${placeholderKeyName}.public_key>`, unlocks: lockScriptName, }; template.scripts[lockScriptName] = { lockingType: 'standard', name: lockScriptName, script: - `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, + `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, }; }); @@ -358,7 +358,7 @@ const generateTemplateScenarioBytecode = ( }; const generateTemplateScenarioParametersValues = ( - types: AbiInput[], + types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[], ): Record => { const typesAndArguments = zip(types, encodedArgs); @@ -376,7 +376,7 @@ const generateTemplateScenarioParametersValues = ( }; const generateTemplateScenarioKeys = ( - types: AbiInput[], + types: readonly AbiInput[], encodedArgs: EncodedFunctionArgument[], ): Record => { const typesAndArguments = zip(types, encodedArgs); @@ -388,7 +388,7 @@ const generateTemplateScenarioKeys = ( return Object.fromEntries(entries); }; -const formatParametersForDebugging = (types: AbiInput[], args: EncodedFunctionArgument[]): string => { +const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { if (types.length === 0) return '// none'; // We reverse the arguments because the order of the arguments in the bytecode is reversed diff --git a/packages/cashscript/src/utils.ts b/packages/cashscript/src/utils.ts index b9dca04b..0cbddf65 100644 --- a/packages/cashscript/src/utils.ts +++ b/packages/cashscript/src/utils.ts @@ -345,12 +345,12 @@ export function findLastIndex(array: Array, predicate: (value: T, index: n export const snakeCase = (str: string): string => ( str - && str - .match( - /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g, - )! - .map((s) => s.toLowerCase()) - .join('_') + && str + .match( + /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g, + )! + .map((s) => s.toLowerCase()) + .join('_') ); // JSON.stringify version that can serialize otherwise unsupported types (bigint and Uint8Array) @@ -368,6 +368,6 @@ export const extendedStringify = (obj: any, spaces?: number): string => JSON.str spaces, ); -export const zip = (a: T[], b: U[]): [T, U][] => ( +export const zip = (a: readonly T[], b: readonly U[]): [T, U][] => ( Array.from(Array(Math.max(b.length, a.length)), (_, i) => [a[i], b[i]]) ); From 9013850027eb6989e7e2d1571a4ec0257ae85048 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 3 Dec 2024 12:04:42 +0100 Subject: [PATCH 14/15] Add some TODOs to rework @ts-ignore'd code --- packages/cashscript/src/Contract.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index da7110a7..dfe428fd 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -78,28 +78,28 @@ export class Contract< // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.functions = {} as any; + this.functions = {}; if (artifact.abi.length === 1) { const f = artifact.abi[0]; - // @ts-ignore generic and can only be indexed for reading + // @ts-ignore TODO: see if we can use generics to make TypeScript happy this.functions[f.name] = this.createFunction(f); } else { artifact.abi.forEach((f, i) => { - // @ts-ignore generic and can only be indexed for reading + // @ts-ignore TODO: see if we can use generics to make TypeScript happy this.functions[f.name] = this.createFunction(f, i); }); } // Populate the functions object with the contract's functions // (with a special case for single function, which has no "function selector") - this.unlock = {} as any; + this.unlock = {}; if (artifact.abi.length === 1) { const f = artifact.abi[0]; - // @ts-ignore generic and can only be indexed for reading + // @ts-ignore TODO: see if we can use generics to make TypeScript happy this.unlock[f.name] = this.createUnlocker(f); } else { artifact.abi.forEach((f, i) => { - // @ts-ignore generic and can only be indexed for reading + // @ts-ignore TODO: see if we can use generics to make TypeScript happy this.unlock[f.name] = this.createUnlocker(f, i); }); } From 3eb324c00df64e2e0370f79c0ad6ef8046983a14 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 3 Dec 2024 12:05:24 +0100 Subject: [PATCH 15/15] Update docs and release notes for v0.10.3 --- README.md | 19 ++++++++++--------- packages/cashc/README.md | 17 +++++++++-------- packages/cashscript/README.md | 2 +- website/docs/compiler/artifacts.md | 2 +- website/docs/compiler/compiler.md | 25 +++++++++++++++---------- website/docs/releases/release-notes.md | 8 ++++++++ website/docs/sdk/instantiation.md | 8 +++++++- 7 files changed, 51 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 03114fda..f10c6c51 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ CashScript is a high-level language that allows you to write Bitcoin Cash smart ## The CashScript Compiler -CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. +CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` (or `.ts`) artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. ### Installation @@ -30,18 +30,19 @@ npm install -g cashc Usage: cashc [options] [source_file] Options: - -V, --version Output the version number. - -o, --output Specify a file to output the generated artifact. - -h, --hex Compile the contract to hex format rather than a full artifact. - -A, --asm Compile the contract to ASM format rather than a full artifact. - -c, --opcount Display the number of opcodes in the compiled bytecode. - -s, --size Display the size in bytes of the compiled bytecode. - -?, --help Display help + -V, --version Output the version number. + -o, --output Specify a file to output the generated artifact. + -h, --hex Compile the contract to hex format rather than a full artifact. + -A, --asm Compile the contract to ASM format rather than a full artifact. + -c, --opcount Display the number of opcodes in the compiled bytecode. + -s, --size Display the size in bytes of the compiled bytecode. + -f, --format Specify the format of the output. (choices: "json", "ts", default: "json") + -?, --help Display help ``` ## The CashScript SDK -The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). +The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` (or `.ts`) artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). ### Installation diff --git a/packages/cashc/README.md b/packages/cashc/README.md index dd1e0ada..674ef4ba 100644 --- a/packages/cashc/README.md +++ b/packages/cashc/README.md @@ -14,7 +14,7 @@ See the [GitHub repository](https://github.com/CashScript/cashscript) and the [C CashScript is a high-level language that allows you to write Bitcoin Cash smart contracts in a straightforward and familiar way. Its syntax is inspired by Ethereum's Solidity language, but its functionality is different since the underlying systems have very different fundamentals. See the [language documentation](https://cashscript.org/docs/language/) for a full reference of the language. ## The CashScript Compiler -CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. +CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` (or `.ts`)artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. ### Installation ```bash @@ -26,11 +26,12 @@ npm install -g cashc Usage: cashc [options] [source_file] Options: - -V, --version Output the version number. - -o, --output Specify a file to output the generated artifact. - -h, --hex Compile the contract to hex format rather than a full artifact. - -A, --asm Compile the contract to ASM format rather than a full artifact. - -c, --opcount Display the number of opcodes in the compiled bytecode. - -s, --size Display the size in bytes of the compiled bytecode. - -?, --help Display help + -V, --version Output the version number. + -o, --output Specify a file to output the generated artifact. + -h, --hex Compile the contract to hex format rather than a full artifact. + -A, --asm Compile the contract to ASM format rather than a full artifact. + -c, --opcount Display the number of opcodes in the compiled bytecode. + -s, --size Display the size in bytes of the compiled bytecode. + -f, --format Specify the format of the output. (choices: "json", "ts", default: "json") + -?, --help Display help ``` diff --git a/packages/cashscript/README.md b/packages/cashscript/README.md index bd8bdf1f..472c5f70 100644 --- a/packages/cashscript/README.md +++ b/packages/cashscript/README.md @@ -14,7 +14,7 @@ See the [GitHub repository](https://github.com/CashScript/cashscript) and the [C CashScript is a high-level language that allows you to write Bitcoin Cash smart contracts in a straightforward and familiar way. Its syntax is inspired by Ethereum's Solidity language, but its functionality is different since the underlying systems have very different fundamentals. See the [language documentation](https://cashscript.org/docs/language/) for a full reference of the language. ## The CashScript SDK -The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). +The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` (or `.ts`) artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). ### Installation ```bash diff --git a/website/docs/compiler/artifacts.md b/website/docs/compiler/artifacts.md index fcc144f0..3db42ff4 100644 --- a/website/docs/compiler/artifacts.md +++ b/website/docs/compiler/artifacts.md @@ -2,7 +2,7 @@ title: Artifacts --- -Compiled contracts can be represented by so-called artifacts. These artifacts contain all information that is needed to interact with the smart contracts on-chain. Artifacts are stored in `.json` files so they can be shared and stored for later usage without having to recompile the contract. +Compiled contracts can be represented by so-called artifacts. These artifacts contain all information that is needed to interact with the smart contracts on-chain. Artifacts are stored in `.json` (or `.ts`) files so they can be shared and stored for later usage without having to recompile the contract. :::tip Did you know? Artifacts allow any third-party SDKs to be developed, since these SDKs only need to import and use an artifact file, while the compilation of the contract is left to the official `cashc` compiler. diff --git a/website/docs/compiler/compiler.md b/website/docs/compiler/compiler.md index 493900f0..510ea0ba 100644 --- a/website/docs/compiler/compiler.md +++ b/website/docs/compiler/compiler.md @@ -2,12 +2,12 @@ title: Compiler --- -The CashScript compiler is called `cashc` and is used to compile CashScript `.cash` contract files into `.json` artifact files. +The CashScript compiler is called `cashc` and is used to compile CashScript `.cash` contract files into `.json` (or `.ts`) artifact files. These artifact files can be used to instantiate a CashScript contract with the help of the CashScript SDK. For more information on this artifact format refer to [Artifacts](/docs/compiler/artifacts). ## Command Line Interface -The `cashc` command line interface is used to compile CashScript `.cash` files into `.json` artifact files. +The `cashc` command line interface is used to compile CashScript `.cash` files into `.json` (or `.ts`) artifact files. ### Installation You can use `npm` to install the `cashc` command line tool globally. @@ -17,21 +17,26 @@ npm install -g cashc ``` ### CLI Usage -The `cashc` CLI tool can be used to compile `.cash` files to JSON artifact files. +The `cashc` CLI tool can be used to compile `.cash` files to JSON (or `.ts`) artifact files. ```bash Usage: cashc [options] [source_file] Options: - -V, --version Output the version number. - -o, --output Specify a file to output the generated artifact. - -h, --hex Compile the contract to hex format rather than a full artifact. - -A, --asm Compile the contract to ASM format rather than a full artifact. - -c, --opcount Display the number of opcodes in the compiled bytecode. - -s, --size Display the size in bytes of the compiled bytecode. - -?, --help Display help + -V, --version Output the version number. + -o, --output Specify a file to output the generated artifact. + -h, --hex Compile the contract to hex format rather than a full artifact. + -A, --asm Compile the contract to ASM format rather than a full artifact. + -c, --opcount Display the number of opcodes in the compiled bytecode. + -s, --size Display the size in bytes of the compiled bytecode. + -f, --format Specify the format of the output. (choices: "json", "ts", default: "json") + -?, --help Display help ``` +:::tip +To have the best TypeScript integration, we recommend generating the artifact in the `.ts` format and importing it into your TypeScript project from that `.ts` file. +::: + ## JavaScript Compilation Generally CashScript contracts are compiled to an Artifact JSON file using the CLI compiler. As an alternative to this, CashScript contracts can be compiled from within JavaScript apps using the `cashc` package. This package exports two compilation functions. diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index da27834f..5a9fb560 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -2,6 +2,14 @@ title: Release Notes --- +## v0.10.3 + +#### cashc compiler +- :sparkles: Add `--format ts` option to `cashc` CLI to generate TypeScript typings for the artifact. + +#### CashScript SDK +- :sparkles: Add automatic TypeScript typings for `Contract` class when artifact is generated using the `cashc` CLI with the `--format ts` option. + ## v0.10.2 #### cashc compiler diff --git a/website/docs/sdk/instantiation.md b/website/docs/sdk/instantiation.md index 29bdb91c..ff34a33c 100644 --- a/website/docs/sdk/instantiation.md +++ b/website/docs/sdk/instantiation.md @@ -26,7 +26,7 @@ new Contract( A CashScript contract can be instantiated by providing an `Artifact` object, a list of constructor arguments, and optionally an options object configuring `NetworkProvider` and `addressType`. -An `Artifact` object is the result of compiling a CashScript contract. See the [Language Documentation](/docs/compiler/artifacts) for more information on Artifacts. Compilation can be done using the standalone [`cashc` CLI](/docs/compiler) or programmatically with the `cashc` NPM package (see [CashScript Compiler](/docs/compiler#javascript-compilation)). +An `Artifact` object is the result of compiling a CashScript contract. Compilation can be done using the standalone [`cashc` CLI](/docs/compiler) or programmatically with the `cashc` NPM package (see [CashScript Compiler](/docs/compiler#javascript-compilation)). If compilation is done using the `cashc` CLI with the `--format ts` option, you will get explicit types and type checking for the constructor arguments and function arguments. The `NetworkProvider` option is used to manage network operations for the CashScript contract. By default, a mainnet `ElectrumNetworkProvider` is used, but the network providers can be configured. See the docs on [NetworkProvider](/docs/sdk/network-provider). @@ -54,6 +54,8 @@ const options = { provider, addressType} const contract = new Contract(P2PKH, contractArguments, options); ``` +### TypeScript Typings + ## Contract Properties ### address @@ -175,6 +177,10 @@ const tx = await contract.functions .send() ``` +:::tip +If the contract artifact is generated using the `cashc` CLI with the `--format ts` option, you will get explicit types and type checking for the function name and arguments. +::: + ### Contract unlockers ```ts