Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const Editor: React.FC<Props> = ({ code, setCode, compile, compilerVersion, setC
onChange={(e) => setCompilerVersion(e.target.value as CashScriptVersion)}
style={{ width: '170px', borderRadius: '30px' }}
>
<option value="0.14">cashc v0.14</option>
<option value="0.13">cashc v0.13</option>
<option value="0.12">cashc v0.12</option>
</Form.Select>
Expand Down
14 changes: 8 additions & 6 deletions src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
exampleTimeoutContract,
exampleEscrowContract,
exampleStramingMecenasContract,
exampleSharedFunctionsContract,
exampleDexContract
} from '../exampleContracts/examples';
import type { CashScriptVersion } from '@/editor/cashscript/version';
Expand All @@ -28,7 +29,7 @@ const Main: React.FC<Props> = ({
}) => {

const [initializeContracts, setInitializeContracts] = useState<0 | 1 | 2>(0);
const [compilerVersion, setCompilerVersion] = useState<CashScriptVersion>('0.13');
const [compilerVersion, setCompilerVersion] = useState<CashScriptVersion>('0.14');

useEffect(() => {
const codeLocalStorage = localStorage.getItem("code");
Expand All @@ -44,11 +45,12 @@ const Main: React.FC<Props> = ({
} 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) }
Expand Down
82 changes: 57 additions & 25 deletions src/components/TransactionBuilder.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -48,32 +48,51 @@ const TransactionBuilderPage: React.FC<Props> = ({ 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"){
Expand Down Expand Up @@ -205,6 +224,19 @@ const TransactionBuilderPage: React.FC<Props> = ({ provider, wallets, contracts,
</Form>
</details>

<div style={{ marginBottom: '10px' }}>
{feeData ? (
<span>
Calculated fee: <strong>{feeData.feeSats.toString()} sats</strong>
{' '}(<strong>{feeData.feeSatsPerByte.toFixed(2)} sats/byte</strong>)
</span>
) : (
<span style={{ color: '#888' }}>
Calculated fee: add valid inputs and outputs to calculate the fee
</span>
)}
</div>

<Button variant="secondary" style={{ display: "block" }} size="sm" onClick={sendTransaction}>
{ provider.network === "mocknet" ? "Evaluate" : "Send" }
</Button>
Expand Down
61 changes: 60 additions & 1 deletion src/editor/cashscript/completionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
40 changes: 37 additions & 3 deletions src/editor/cashscript/completionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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 };
},
});
Expand Down Expand Up @@ -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,
};
}
4 changes: 3 additions & 1 deletion src/editor/cashscript/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -62,7 +63,8 @@ export interface CashScriptDiagnostic {
}

const compileStringByVersion: Record<CashScriptVersion, CompileStringWithErrorListener> = {
'0.13': compileString,
'0.14': compileString,
'0.13': compileStringV013,
'0.12': compileStringV012,
};

Expand Down
Loading