diff --git a/package.json b/package.json index 4e334e4..cbf95cf 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,15 @@ "lint": "next lint" }, "dependencies": { - "@cashscript/utils": "^0.13.1", + "@cashscript/utils": "^0.14.0-next.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@monaco-editor/react": "^4.7.0", "bootstrap": "^5.3.7", - "cashc": "^0.13.1", + "cashc": "^0.14.0-next.3", "cashc-v0.12": "npm:cashc@^0.12.2", - "cashscript": "^0.13.1", + "cashc-v0.13": "npm:cashc@^0.13.2", + "cashscript": "^0.14.0-next.3", "next": "15.5.9", "react": "18.2.0", "react-bootstrap": "^2.10.10", diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 1dc1cb6..be89acf 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -94,6 +94,7 @@ const Editor: React.FC = ({ code, setCode, compile, compilerVersion, setC onChange={(e) => setCompilerVersion(e.target.value as CashScriptVersion)} style={{ width: '170px', borderRadius: '30px' }} > + diff --git a/src/components/Main.tsx b/src/components/Main.tsx index f4f5496..944f52c 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -8,6 +8,7 @@ import { exampleTimeoutContract, exampleEscrowContract, exampleStramingMecenasContract, + exampleSharedFunctionsContract, exampleDexContract } from '../exampleContracts/examples'; import type { CashScriptVersion } from '@/editor/cashscript/version'; @@ -28,7 +29,7 @@ const Main: React.FC = ({ }) => { const [initializeContracts, setInitializeContracts] = useState<0 | 1 | 2>(0); - const [compilerVersion, setCompilerVersion] = useState('0.13'); + const [compilerVersion, setCompilerVersion] = useState('0.14'); useEffect(() => { const codeLocalStorage = localStorage.getItem("code"); @@ -44,11 +45,12 @@ const Main: React.FC = ({ } else { // add default example contracts to local storage try { - const artifactExampleTimeout = compileCashScript(exampleTimeoutContract, '0.13') - const artifactExampleEscrow = compileCashScript(exampleEscrowContract, '0.13') - const artifactExampleStramingMecenas = compileCashScript(exampleStramingMecenasContract, '0.13') - const artifactExampleDex = compileCashScript(exampleDexContract, '0.13') - const defaultArtifacts = [artifactExampleTimeout, artifactExampleEscrow, artifactExampleStramingMecenas, artifactExampleDex] + const artifactExampleTimeout = compileCashScript(exampleTimeoutContract, '0.14') + const artifactExampleEscrow = compileCashScript(exampleEscrowContract, '0.14') + const artifactExampleStramingMecenas = compileCashScript(exampleStramingMecenasContract, '0.14') + const artifactExampleSharedFunctions = compileCashScript(exampleSharedFunctionsContract, '0.14') + const artifactExampleDex = compileCashScript(exampleDexContract, '0.14') + const defaultArtifacts = [artifactExampleTimeout, artifactExampleEscrow, artifactExampleStramingMecenas, artifactExampleSharedFunctions, artifactExampleDex] setArtifacts(defaultArtifacts) localStorage.setItem("artifacts", JSON.stringify(defaultArtifacts, null, 2)); } catch (error) { console.log(error) } diff --git a/src/components/TransactionBuilder.tsx b/src/components/TransactionBuilder.tsx index 7d2537f..7766a76 100644 --- a/src/components/TransactionBuilder.tsx +++ b/src/components/TransactionBuilder.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react' +import React, {useState, useCallback, useMemo} from 'react' import { NetworkProvider, Output, SignatureTemplate, TransactionBuilder, Unlocker } from 'cashscript' import { Wallet, ContractInfo, ExplorerString, ContractUtxo, WalletUtxo } from './shared' import { Button, Card, Form } from 'react-bootstrap' @@ -48,32 +48,51 @@ const TransactionBuilderPage: React.FC = ({ provider, wallets, contracts, setInputs(inputsCopy) } - async function sendTransaction() { - // try to send transaction and alert result + // Construct a TransactionBuilder from the current inputs, outputs and options. + // Shared by sendTransaction and the live fee calculation. Throws if the + // transaction is incomplete (e.g. an undefined input or missing unlocker). + const buildTransaction = useCallback(() => { + // start constructing transaction + const transaction = new TransactionBuilder({ + provider, + allowImplicitFungibleTokenBurn, + ...(enableMaxFeeSatoshis && maximumFeeSatoshis ? { maximumFeeSatoshis: BigInt(maximumFeeSatoshis) } : {}), + ...(enableMaxFeeSatsPerByte && maximumFeeSatsPerByte ? { maximumFeeSatsPerByte: Number(maximumFeeSatsPerByte) } : {}), + }) + + // add inputs to transaction in the user-defined order + inputs.forEach((input, inputIndex) => { + if(!input) throw new Error("Undefined input provided") + if('walletIndex' in input){ + const walletIndex = input.walletIndex + const sigTemplate = new SignatureTemplate(wallets[walletIndex].privKey) + transaction.addInput(input, sigTemplate.unlockP2PKH()) + } else { + const inputUnlocker = inputUnlockers[inputIndex] + if(!inputUnlocker) throw new Error("Missing unlocker for input") + transaction.addInput(input, inputUnlocker) + } + }) + + transaction.addOutputs(outputs) + if(enableLocktime) transaction.setLocktime(Number(locktime)) + return transaction + }, [provider, wallets, allowImplicitFungibleTokenBurn, enableMaxFeeSatoshis, maximumFeeSatoshis, enableMaxFeeSatsPerByte, maximumFeeSatsPerByte, inputs, inputUnlockers, outputs, enableLocktime, locktime]) + + // Reactively calculate the transaction fee and fee rate as the user edits the + // inputs/outputs. Returns null while the transaction can't yet be built. + const feeData = useMemo(() => { try { - // start constructing transaction - const transaction = new TransactionBuilder({ - provider, - allowImplicitFungibleTokenBurn, - ...(enableMaxFeeSatoshis && maximumFeeSatoshis ? { maximumFeeSatoshis: BigInt(maximumFeeSatoshis) } : {}), - ...(enableMaxFeeSatsPerByte && maximumFeeSatsPerByte ? { maximumFeeSatsPerByte: Number(maximumFeeSatsPerByte) } : {}), - }) - - // add inputs to transaction in the user-defined order - inputs.forEach((input, inputIndex) => { - if(!input) throw new Error("Undefined input provided") - if('walletIndex' in input){ - const walletIndex = input.walletIndex - const sigTemplate = new SignatureTemplate(wallets[walletIndex].privKey) - transaction.addInput(input, sigTemplate.unlockP2PKH()) - } else { - const inputUnlocker = inputUnlockers[inputIndex] - transaction.addInput(input, inputUnlocker) - } - }) + return buildTransaction().calculateTransactionFee() + } catch { + return null + } + }, [buildTransaction]) - transaction.addOutputs(outputs) - if(enableLocktime) transaction.setLocktime(Number(locktime)) + async function sendTransaction() { + // try to send transaction and alert result + try { + const transaction = buildTransaction() // check for mocknet if(provider.network == "mocknet"){ @@ -205,6 +224,19 @@ const TransactionBuilderPage: React.FC = ({ provider, wallets, contracts, +
+ {feeData ? ( + + Calculated fee: {feeData.feeSats.toString()} sats + {' '}({feeData.feeSatsPerByte.toFixed(2)} sats/byte) + + ) : ( + + Calculated fee: add valid inputs and outputs to calculate the fee + + )} +
+ diff --git a/src/editor/cashscript/completionData.ts b/src/editor/cashscript/completionData.ts index b3f1e6f..c616bab 100644 --- a/src/editor/cashscript/completionData.ts +++ b/src/editor/cashscript/completionData.ts @@ -323,12 +323,40 @@ export const valueUnits: CompletionItemData[] = [ // Keywords export const keywords: CompletionItemData[] = [ + // The pragma snippet should suggest a version constraint matching the + // selected compiler, so there is one gated variant per supported compiler. + { + label: 'pragma', + kind: 'Keyword', + detail: 'Pragma directive', + documentation: 'Specifies the CashScript version. Example: pragma cashscript ^0.12.0;', + insertText: 'pragma cashscript ^${1:0.12.0};', + maxVersion: '0.12', + }, { label: 'pragma', kind: 'Keyword', detail: 'Pragma directive', documentation: 'Specifies the CashScript version. Example: pragma cashscript ^0.13.0;', insertText: 'pragma cashscript ^${1:0.13.0};', + minVersion: '0.13.0', + maxVersion: '0.13', + }, + { + label: 'pragma', + kind: 'Keyword', + detail: 'Pragma directive', + documentation: 'Specifies the CashScript version. Example: pragma cashscript ^0.14.0;', + insertText: 'pragma cashscript ^${1:0.14.0};', + minVersion: '0.14.0', + }, + { + label: 'import', + kind: 'Keyword', + detail: 'Import directive', + documentation: 'Imports top-level functions and constants from another CashScript file, making them available as if they were declared locally. Import directives must appear at the top of the file, after any pragma directives. Paths starting with `./`, `../` or `/` are resolved relative to the importing file; bare specifiers (e.g. `"pkg/math.cash"`) are resolved from `node_modules`.', + insertText: 'import "${1:./file.cash}";', + minVersion: '0.14.0', }, { label: 'contract', @@ -344,6 +372,14 @@ export const keywords: CompletionItemData[] = [ documentation: 'Defines a new function within a contract.', insertText: 'function ${1:functionName}(${2:params}) {\n\t$0\n}', }, + { + label: 'function', + kind: 'Keyword', + detail: 'Global function definition (with return values)', + documentation: 'Defines a reusable top-level function, declared outside the contract. It can perform `require` checks and return one or more values with a `returns (...)` clause, and can be called from contract functions or other top-level functions.', + insertText: 'function ${1:functionName}(${2:params}) returns (${3:int}) {\n\treturn $0;\n}', + minVersion: '0.14.0', + }, { label: 'if', kind: 'Keyword', @@ -386,7 +422,30 @@ export const keywords: CompletionItemData[] = [ label: 'constant', kind: 'Keyword', detail: 'Constant modifier', - documentation: 'Declares a compile-time constant value.', + documentation: 'Declares a compile-time constant value. From 0.14 constants can also be declared at the top level of a file (e.g. `int constant FEE = 1000;`) and shared between functions and contracts.', + }, + { + label: 'return', + kind: 'Keyword', + detail: 'Return statement', + documentation: 'Returns one or more comma-separated values from a user-defined function. A value-returning function must end with a single `return` statement — early or conditional returns are not allowed.', + insertText: 'return ${1:value};', + minVersion: '0.14.0', + }, + { + label: 'returns', + kind: 'Keyword', + detail: 'Return type declaration', + documentation: 'Declares the return type(s) of a user-defined function, e.g. `function double(int a) returns (int)`. Multiple return values are declared as `returns (T1, T2, ...)` and destructured at the call site: `int q, int r = divmod(a, b);`.', + insertText: 'returns (${1:int}) ', + minVersion: '0.14.0', + }, + { + label: 'unused', + kind: 'Keyword', + detail: 'Unused modifier', + documentation: 'Marks a parameter or variable as intentionally unused, suppressing the unused-symbol compiler warning.', + minVersion: '0.14.0', }, { label: 'true', diff --git a/src/editor/cashscript/completionProvider.ts b/src/editor/cashscript/completionProvider.ts index 37aac83..6364cdf 100644 --- a/src/editor/cashscript/completionProvider.ts +++ b/src/editor/cashscript/completionProvider.ts @@ -20,7 +20,7 @@ import { CompletionItemData, } from './completionData'; import { isAvailableInVersion } from './version'; -import { getAvailableVariables, ExtractedVariable } from './variableExtractor'; +import { getAvailableVariables, extractGlobalFunctions, ExtractedVariable, ExtractedFunction } from './variableExtractor'; export function registerCompletionProvider(monaco: typeof Monaco): void { monaco.languages.registerCompletionItemProvider(CASHSCRIPT_LANGUAGE_ID, { @@ -78,6 +78,13 @@ export function registerCompletionProvider(monaco: typeof Monaco): void { suggestions.push(createVariableCompletionItem(variable, range, monaco)); } + // Add user-defined global functions (0.14+) + if (isAvailableInVersion('0.14.0')) { + for (const fn of extractGlobalFunctions(sourceCode)) { + suggestions.push(createGlobalFunctionCompletionItem(fn, range, monaco)); + } + } + return { suggestions }; }, }); @@ -146,16 +153,43 @@ function createVariableCompletionItem( scopeLabel = ' (contract parameter)'; } else if (variable.scope === 'function') { scopeLabel = ` (${variable.functionName} parameter)`; + } else if (variable.scope === 'global') { + scopeLabel = ' (global constant)'; } else { scopeLabel = ' (local variable)'; } return { label: variable.name, - kind: monaco.languages.CompletionItemKind.Variable, + kind: variable.scope === 'global' + ? monaco.languages.CompletionItemKind.Constant + : monaco.languages.CompletionItemKind.Variable, detail: `${variable.type} ${variable.name}${scopeLabel}`, - documentation: `User-declared variable of type ${variable.type}.`, + documentation: variable.scope === 'global' + ? `User-declared global constant of type ${variable.type}.` + : `User-declared variable of type ${variable.type}.`, insertText: variable.name, range, }; } + +/** + * Creates a completion item for a user-defined global function (0.14+). + */ +function createGlobalFunctionCompletionItem( + fn: ExtractedFunction, + range: Monaco.IRange, + monaco: typeof Monaco +): Monaco.languages.CompletionItem { + const returnsSuffix = fn.returnTypes ? ` returns (${fn.returnTypes})` : ''; + + return { + label: fn.name, + kind: monaco.languages.CompletionItemKind.Function, + detail: `${fn.name}(${fn.parameters})${returnsSuffix}`, + documentation: 'User-defined function.', + insertText: `${fn.name}(\${1})`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + }; +} diff --git a/src/editor/cashscript/diagnostics.ts b/src/editor/cashscript/diagnostics.ts index 4a407a5..8416795 100644 --- a/src/editor/cashscript/diagnostics.ts +++ b/src/editor/cashscript/diagnostics.ts @@ -1,5 +1,6 @@ import type { Artifact } from 'cashscript'; import { compileString } from 'cashc'; +import { compileString as compileStringV013 } from 'cashc-v0.13'; import { compileString as compileStringV012 } from 'cashc-v0.12'; import type { CashScriptVersion } from './version'; @@ -62,7 +63,8 @@ export interface CashScriptDiagnostic { } const compileStringByVersion: Record = { - '0.13': compileString, + '0.14': compileString, + '0.13': compileStringV013, '0.12': compileStringV012, }; diff --git a/src/editor/cashscript/hoverProvider.ts b/src/editor/cashscript/hoverProvider.ts index 2ef96d9..230fa0d 100644 --- a/src/editor/cashscript/hoverProvider.ts +++ b/src/editor/cashscript/hoverProvider.ts @@ -19,10 +19,13 @@ import { CompletionItemData, } from './completionData'; import { isAvailableInVersion } from './version'; -import { extractVariables, ExtractedVariable } from './variableExtractor'; +import { extractVariables, extractGlobalFunctions, ExtractedVariable, ExtractedFunction } from './variableExtractor'; -// Build lookup maps for efficient hover lookup -const hoverDataMap = new Map(); +// Build lookup maps for efficient hover lookup. A label can appear multiple +// times with different version ranges (e.g. the pragma snippet per compiler +// version), so each label maps to all its variants and lookups pick the first +// variant available in the selected compiler version. +const hoverDataMap = new Map(); function buildHoverDataMap(): void { const allItems: CompletionItemData[] = [ @@ -38,13 +41,25 @@ function buildHoverDataMap(): void { ]; for (const item of allItems) { - hoverDataMap.set(item.label, item); + const existing = hoverDataMap.get(item.label); + if (existing) { + existing.push(item); + } else { + hoverDataMap.set(item.label, [item]); + } } } // Build the map once buildHoverDataMap(); +// Returns the hover item for a label that is available in the selected +// compiler version, if any. +function getAvailableHoverItem(label: string): CompletionItemData | undefined { + const items = hoverDataMap.get(label); + return items?.find(item => isAvailableInVersion(item.minVersion, item.maxVersion)); +} + // Create maps for contextual properties const txPropertyMap = new Map(txProperties.map(p => [p.label, p])); const inputPropertyMap = new Map(inputProperties.map(p => [p.label, p])); @@ -142,15 +157,15 @@ function getHoverContent( // bytes methods (after any identifier followed by dot, excluding tx/this/console) if (/\b\w+\s*\.\s*$/.test(lineUntilWord) && !/\b(?:tx|this|console)\s*\.\s*$/.test(lineUntilWord)) { - const method = hoverDataMap.get(word); + const method = getAvailableHoverItem(word); if (method && (method.kind === 'Method' || method.kind === 'Property')) { return formatHoverContent(method); } } // Check for global items (gated by the selected compiler version) - const globalItem = hoverDataMap.get(word); - if (globalItem && isAvailableInVersion(globalItem.minVersion, globalItem.maxVersion)) { + const globalItem = getAvailableHoverItem(word); + if (globalItem) { return formatHoverContent(globalItem); } @@ -178,6 +193,14 @@ function getHoverContent( }); } + // Check for user-defined global functions (0.14+) + if (isAvailableInVersion('0.14.0')) { + const userFunction = extractGlobalFunctions(sourceCode).find(fn => fn.name === word); + if (userFunction) { + return formatGlobalFunctionHover(userFunction); + } + } + // Check for user-declared variables const variables = extractVariables(sourceCode); const userVariable = variables.find(v => v.name === word); @@ -208,6 +231,15 @@ function formatHoverContent(item: CompletionItemData): Monaco.IMarkdownString[] return contents; } +function formatGlobalFunctionHover(fn: ExtractedFunction): Monaco.IMarkdownString[] { + const returnsSuffix = fn.returnTypes ? ` returns (${fn.returnTypes})` : ''; + + return [ + { value: `\`\`\`cashscript\nfunction ${fn.name}(${fn.parameters})${returnsSuffix}\n\`\`\`` }, + { value: 'User-defined function' }, + ]; +} + function formatVariableHover(variable: ExtractedVariable): Monaco.IMarkdownString[] { const contents: Monaco.IMarkdownString[] = []; @@ -216,6 +248,8 @@ function formatVariableHover(variable: ExtractedVariable): Monaco.IMarkdownStrin scopeDescription = 'Contract parameter'; } else if (variable.scope === 'function') { scopeDescription = `Parameter of function \`${variable.functionName}\``; + } else if (variable.scope === 'global') { + scopeDescription = 'Global constant'; } else { scopeDescription = 'Local variable'; } diff --git a/src/editor/cashscript/languageDefinition.ts b/src/editor/cashscript/languageDefinition.ts index eab532f..d020bbb 100644 --- a/src/editor/cashscript/languageDefinition.ts +++ b/src/editor/cashscript/languageDefinition.ts @@ -21,10 +21,13 @@ interface CashScriptMonarchLanguage extends Monaco.languages.IMonarchLanguage { // gated by version the same way completions/hover are: features added in 0.13 // (loops, unsafe casts, toPaddedBytes, and the bitwise-inversion / shift / // compound-assignment / increment operators) are only highlighted under a 0.13+ -// compiler. (bytesN remains a valid type in every version — only its *cast* -// form was removed in 0.13 — so the bytesN tokens are highlighted throughout.) +// compiler, and features added in 0.14 (imports, user-defined functions with +// return values, and the unused modifier) only under a 0.14+ compiler. +// (bytesN remains a valid type in every version — only its *cast* form was +// removed in 0.13 — so the bytesN tokens are highlighted throughout.) function buildMonarchLanguage(version: string): CashScriptMonarchLanguage { const since013 = isAvailableInVersion('0.13.0', undefined, version); + const since014 = isAvailableInVersion('0.14.0', undefined, version); return { defaultToken: '', @@ -35,6 +38,8 @@ function buildMonarchLanguage(version: string): CashScriptMonarchLanguage { 'if', 'else', 'require', 'new', 'constant', // Loops (0.13+) ...(since013 ? ['for', 'while', 'do'] : []), + // Imports, user-defined functions and the unused modifier (0.14+) + ...(since014 ? ['import', 'return', 'returns', 'unused'] : []), ], typeKeywords: [ diff --git a/src/editor/cashscript/variableExtractor.ts b/src/editor/cashscript/variableExtractor.ts index 60dea9e..6945116 100644 --- a/src/editor/cashscript/variableExtractor.ts +++ b/src/editor/cashscript/variableExtractor.ts @@ -1,10 +1,17 @@ export interface ExtractedVariable { name: string; type: string; - scope: 'contract' | 'function' | 'local'; + scope: 'contract' | 'function' | 'local' | 'global'; functionName?: string; } +// A user-defined global (top-level) function, introduced in CashScript 0.14. +export interface ExtractedFunction { + name: string; + parameters: string; + returnTypes?: string; +} + /** * Extracts user-declared variables from CashScript source code. * This includes contract parameters, function parameters, and local variable declarations. @@ -46,11 +53,13 @@ export function extractVariables(sourceCode: string): ExtractedVariable[] { }); } - // Extract local variable declarations - // Pattern: type varName = expression; + // Extract local variable declarations and top-level (global) constants + // Pattern: type [constant|unused] varName = expression; // CashScript types: int, bool, string, bytes, bytes1-32, pubkey, sig, datasig + // Declarations at brace depth 0 are global constants (0.14+); anything deeper + // is a local variable. const typePattern = '(?:int|bool|string|bytes(?:[1-9]|[12][0-9]|3[0-2])?|pubkey|sig|datasig)'; - const localVarRegex = new RegExp(`(${typePattern})\\s+(\\w+)\\s*=`, 'g'); + const localVarRegex = new RegExp(`(${typePattern})\\s+(?:(?:constant|unused)\\s+)*(\\w+)\\s*=`, 'g'); let localMatch; while ((localMatch = localVarRegex.exec(codeWithoutComments)) !== null) { const varType = localMatch[1]; @@ -60,7 +69,7 @@ export function extractVariables(sourceCode: string): ExtractedVariable[] { variables.push({ name: varName, type: varType, - scope: 'local', + scope: braceDepthAt(codeWithoutComments, localMatch.index) === 0 ? 'global' : 'local', }); } } @@ -68,6 +77,45 @@ export function extractVariables(sourceCode: string): ExtractedVariable[] { return variables; } +/** + * Extracts user-defined global functions (CashScript 0.14+): function + * definitions at the top level of the file, outside any contract block. + */ +export function extractGlobalFunctions(sourceCode: string): ExtractedFunction[] { + const codeWithoutComments = removeComments(sourceCode); + const functions: ExtractedFunction[] = []; + + const functionRegex = /function\s+(\w+)\s*\(([^)]*)\)\s*(?:returns\s*\(([^)]*)\))?/g; + let match; + while ((match = functionRegex.exec(codeWithoutComments)) !== null) { + // Contract functions live at brace depth 1 (inside the contract block); + // global functions are declared at depth 0. + if (braceDepthAt(codeWithoutComments, match.index) !== 0) continue; + + functions.push({ + name: match[1], + parameters: match[2].trim(), + returnTypes: match[3]?.trim(), + }); + } + + return functions; +} + +/** + * Returns the brace nesting depth at the given index. Used to tell top-level + * definitions (depth 0) apart from definitions inside a contract or function + * body. Braces inside string literals are a known, acceptable inaccuracy. + */ +function braceDepthAt(code: string, index: number): number { + let depth = 0; + for (let i = 0; i < index; i++) { + if (code[i] === '{') depth++; + else if (code[i] === '}') depth--; + } + return depth; +} + /** * Parses a parameter list string into individual parameters. */ @@ -85,9 +133,9 @@ function parseParameters(paramString: string): Array<{ name: string; type: strin const trimmed = part.trim(); if (!trimmed) continue; - // Pattern: type name (possibly with array brackets) - // Examples: "int amount", "bytes32 hash", "pubkey[] keys" - const match = trimmed.match(/^(\w+(?:\[\])?)\s+(\w+)$/); + // Pattern: type [constant|unused] name (possibly with array brackets) + // Examples: "int amount", "bytes32 hash", "pubkey[] keys", "int unused x" + const match = trimmed.match(/^(\w+(?:\[\])?)\s+(?:(?:constant|unused)\s+)*(\w+)$/); if (match) { params.push({ type: match[1], @@ -117,8 +165,9 @@ function removeComments(code: string): string { export function getCurrentFunctionContext(sourceCode: string, offset: number): string | undefined { const codeBeforeCursor = sourceCode.substring(0, offset); - // Find all function declarations before the cursor - const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g; + // Find all function declarations before the cursor. Global functions (0.14+) + // may declare return types between the parameter list and the body. + const functionRegex = /function\s+(\w+)\s*\([^)]*\)\s*(?:returns\s*\([^)]*\)\s*)?\{/g; let lastFunctionName: string | undefined; let lastFunctionStart = -1; let match; @@ -156,8 +205,8 @@ export function getAvailableVariables( const currentFunction = getCurrentFunctionContext(sourceCode, offset); return allVariables.filter(variable => { - // Contract-level variables are always available - if (variable.scope === 'contract') { + // Contract-level variables and global constants are always available + if (variable.scope === 'contract' || variable.scope === 'global') { return true; } diff --git a/src/editor/cashscript/version.ts b/src/editor/cashscript/version.ts index 22c23b4..ae122ce 100644 --- a/src/editor/cashscript/version.ts +++ b/src/editor/cashscript/version.ts @@ -7,9 +7,9 @@ // versions, which avoids re-registering providers (Monaco can't cleanly // unregister them) while still gating version-specific language features. -export type CashScriptVersion = '0.12' | '0.13'; +export type CashScriptVersion = '0.12' | '0.13' | '0.14'; -let currentVersion: CashScriptVersion = '0.13'; +let currentVersion: CashScriptVersion = '0.14'; // Listeners notified when the selected version changes. Used by the highlighting // layer, which (unlike completions/hover) is registered statically and must be diff --git a/src/exampleContracts/examples.ts b/src/exampleContracts/examples.ts index 2d44f40..f5025d0 100644 --- a/src/exampleContracts/examples.ts +++ b/src/exampleContracts/examples.ts @@ -1,4 +1,4 @@ -export const exampleTimeoutContract = `pragma cashscript ~0.13.0; +export const exampleTimeoutContract = `pragma cashscript ^0.14.0; // see https://cashscript.org/docs/basics/getting-started#writing-your-first-contract @@ -16,7 +16,7 @@ contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) { } ` -export const exampleEscrowContract = `pragma cashscript ~0.13.0; +export const exampleEscrowContract = `pragma cashscript ^0.14.0; // see https://cashscript.org/docs/guides/covenants#restricting-p2pkh-recipients @@ -40,7 +40,7 @@ contract Escrow(bytes20 arbiter, bytes20 buyer, bytes20 seller) { } ` -export const exampleStramingMecenasContract = `pragma cashscript ~0.13.0; +export const exampleStramingMecenasContract = `pragma cashscript ^0.14.0; // see https://cashscript.org/docs/guides/covenants#keeping-local-state-in-nfts @@ -100,7 +100,37 @@ contract StreamingMecenas( } ` -export const exampleDexContract = `pragma cashscript ~0.13.0; +export const exampleSharedFunctionsContract = `pragma cashscript ^0.14.0; + +// New in CashScript 0.14: reusable user-defined functions and global constants +// see https://cashscript.org/docs/language/contracts#user-defined-functions + +int constant MINER_FEE = 1000; + +// Reusable function returning the input value remaining after the miner fee +function remainingValue() returns (int) { + return tx.inputs[this.activeInputIndex].value - MINER_FEE; +} + +// Reusable void function requiring that an output sends to a P2PKH recipient +function requireSendsToRecipient(int outputIndex, bytes20 recipientPkh, int amount) { + bytes25 recipientLock = new LockingBytecodeP2PKH(recipientPkh); + require(tx.outputs[outputIndex].lockingBytecode == recipientLock); + require(tx.outputs[outputIndex].value >= amount); +} + +contract SharedFunctions(bytes20 ownerPkh) { + function spend(pubkey pk, sig s) { + // Send the remaining value back to the owner + requireSendsToRecipient(0, ownerPkh, remainingValue()); + + require(hash160(pk) == ownerPkh); + require(checkSig(s, pk)); + } +} +` + +export const exampleDexContract = `pragma cashscript ^0.14.0; // see https://cashscript.org/docs/language/examples#amm-dex diff --git a/yarn.lock b/yarn.lock index 1c01f3c..4dd6a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,10 +77,17 @@ dependencies: "@bitauth/libauth" "^3.1.0-next.8" -"@cashscript/utils@^0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.13.1.tgz#cfcfcf7c6a8edb41f4c3887b908592147c9a3736" - integrity sha512-MAZ0SK+wvgQbTKHHgTDocfOI+Yrmw5c8bv4FyKUuCtjw/+JE/HJEEKyL/0ppEFbOzv5Jy4JQIV6n7GPNWm/wbQ== +"@cashscript/utils@^0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.13.2.tgz#16109126b5fc0a44b1e1a7087f8b58e3a9186359" + integrity sha512-8xryhzZcQs2wPiiDbvvMBhc6RB3eeJW4BJIxlRVRnr4P/9gbUe4EqS+xhHe5rVX/cj4EIDUOHBzYNZJwvsak5g== + dependencies: + "@bitauth/libauth" "^3.1.0-next.8" + +"@cashscript/utils@^0.14.0-next.3": + version "0.14.0-next.3" + resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.14.0-next.3.tgz#ab4bed5c687a39783590bcf8988cd30a9bf95628" + integrity sha512-0ayJDnjwCydqsFFQDLIMgBkLtj/1YWjClhT6gPwElAaTitdH9ckNKAkdQQ3vPXZp+9kWMV6gVgXw02m5BPRS9A== dependencies: "@bitauth/libauth" "^3.1.0-next.8" @@ -1200,24 +1207,35 @@ caniuse-lite@^1.0.30001579: commander "^14.0.0" semver "^7.7.2" -cashc@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.13.1.tgz#9b246f2997eda2fbe5c358fdedbb36bdc57b5a96" - integrity sha512-u0tcMNyK29FOLhVzoQsynmd51QtAGxPIpy8pScLu18RV1MMI0WD1YJY1DBtRNaQlOupeBeO24O6lTNoJ621d2w== +"cashc-v0.13@npm:cashc@^0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.13.2.tgz#83a410b9f2dc7bcd0dceb4c65c3e7996a97932ac" + integrity sha512-wNh7M+ZSYumREICF0bPqGkAXBSQErnxwZY+vL8V+b58c0XqQ3YSMW7NStsM0veCyn1P18k7eby8gNitQJI5N/Q== + dependencies: + "@bitauth/libauth" "^3.1.0-next.8" + "@cashscript/utils" "^0.13.2" + antlr4 "^4.13.2" + commander "^14.0.0" + semver "^7.7.2" + +cashc@^0.14.0-next.3: + version "0.14.0-next.3" + resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.14.0-next.3.tgz#263408253939c7c3d63a467bf3332d57a28f2c3e" + integrity sha512-phsDT0IO1VG+r6V9ycy1al2QkCdKM9L4aDQlnoFcCaU1HsNL7TFdeoKknTd3X08cRXvRlpAPr2fYqCc+XhKYaQ== dependencies: "@bitauth/libauth" "^3.1.0-next.8" - "@cashscript/utils" "^0.13.1" + "@cashscript/utils" "^0.14.0-next.3" antlr4 "^4.13.2" commander "^14.0.0" semver "^7.7.2" -cashscript@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.13.1.tgz#06059881922c35064347e13d97eeb0eb057eea3f" - integrity sha512-BrPFO4TYFTFc41Ft4Hpsky9UNct2SEv6QdC89utu4B/9+8kl4uJLy4SjUv9ZJciGSigQOg4McKYzuYCc/oYl2g== +cashscript@^0.14.0-next.3: + version "0.14.0-next.3" + resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.14.0-next.3.tgz#c81b052824d040d918a3260717f9ed5ae2841111" + integrity sha512-0RYc6LncqDAKINy4xtjH9hH2ANobk8XByWkXGs+W3x1TOU9eWyO6NJnPiIv742Vefd24JFrE6PqkF1ZWvEGeMw== dependencies: "@bitauth/libauth" "^3.1.0-next.8" - "@cashscript/utils" "^0.13.1" + "@cashscript/utils" "^0.14.0-next.3" "@electrum-cash/network" "^4.1.3" fflate "^0.8.2" semver "^7.7.2"