Skip to content
Merged
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ The CashScript Playground is available at [playground.cashscript.org](https://pl

In the left panel you can write CashScript contracts and compile them. After compiling, a contract Artifact is generated which can then be used to initialise an instance of the contract, with specific contract arguments.

## Features

The CashScript Playground allows for importing or exporting contract Artifacts and makes it easy to test new versions of your CashScript code. Similarly, it's also easy to create many contract instances of the same contract with different contract arguments. The created Artifacts and contracts are remembered between sessions.

On the 'Wallets' tab, new key-pairs for testing wallets can be generated with just a mouse click. Each with detailed info on about the key pair and its network balance, with a full list of all current UTXOs. Lastly the TransactionBuilder supports CashTokens, manual input selection for easy P2PK inputs and signing selection through a simple dropdown!

## Limitations
Special transaction options such as OP_RETURN, hardcoded fees or relative timelocks are not supported by the playground.

The playground also does not support the combining multiple different smart contracts in one transaction.
The playground uses the 'Simple transactio builder' so it doesn't support combining multiple different smart contracts in one transaction.

Special transaction options such as OP_RETURN or relative timelocks are also not supported by the playground.

## Disclaimer

Expand Down
1 change: 1 addition & 0 deletions public/importIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/featured-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/screenshot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 60 additions & 17 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,68 @@
// Components
import React, { useState } from 'react';
import { Artifact, Network, Contract, Utxo } from 'cashscript';
import { Artifact, Network } from 'cashscript';
import Header from './Header'
import Main from './Main'
import Footer from './Footer';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import WalletInfo from './Wallets';
import { Wallet } from './shared';
import ContractInfo from './ContractInfo';
import ContractFunctions from './ContractFunctions'
import { Wallet, ContractInfo } from './shared';
import NewContract from './NewContract';
import Contracts from './Contracts';
import TransactionBuilder from './TransactionBuilder';

function App() {
const [network, setNetwork] = useState<Network>('chipnet')
const [wallets, setWallets] = useState<Wallet[]>([])
const [artifact, setArtifact] = useState<Artifact | undefined>(undefined);
const [contract, setContract] = useState<Contract | undefined>(undefined)
const [utxos, setUtxos] = useState<Utxo[] | undefined>(undefined)
const [balance, setBalance] = useState<bigint | undefined>(undefined)
const [artifacts, setArtifacts] = useState<Artifact[] | undefined>(undefined);
const [contracts, setContracts] = useState<ContractInfo[] | undefined>(undefined)
const [code, setCode] = useState<string>(
`pragma cashscript >= 0.8.0;

contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
// Require recipient's signature to match
function transfer(sig recipientSig) {
require(checkSig(recipientSig, recipient));
}

// Require timeout time to be reached and sender's signature to match
function timeout(sig senderSig) {
require(checkSig(senderSig, sender));
require(tx.time >= timeout);
}
}
`);

async function updateUtxosContract () {
if (!contract) return
setBalance(await contract.getBalance())
setUtxos(await contract.getUtxos())
async function updateUtxosContract (nameContract: string) {
const contractIndex = contracts?.findIndex(contractInfo => contractInfo.contract.name == nameContract)
if (contractIndex == undefined) return
const currentContract = contracts?.[contractIndex].contract
if (!currentContract) return
// create a separate lists for utxos and mutate entry
const utxosList = contracts.map(contract => contract.utxos ?? [])
const contractUtxos = await currentContract.getUtxos();
utxosList[contractIndex] = contractUtxos
// map is the best way to deep clone array of complex objects
const newContracts: ContractInfo[] = contracts.map((contractInfo,index) => (
{ ...contractInfo, utxos:utxosList[index] }
))
setContracts(newContracts)
}

async function updateAllUtxosContracts () {
if(!contracts) return

const utxosPromises = contracts.map(contractInfo => {
const contractUtxos = contractInfo.contract.getUtxos();
return contractUtxos ?? []
})
const utxosContracts = await Promise.all(utxosPromises)
// map is the best way to deep clone array of complex objects
const newContracts: ContractInfo[] = contracts.map((contractInfo,index) => (
{ ...contractInfo, utxos:utxosContracts?.[index]}
))
setContracts(newContracts)
}

return (
Expand All @@ -33,19 +73,22 @@ function App() {
defaultActiveKey="editor"
id="uncontrolled-tab-example"
className="mb-2 mt-4 justify-content-center"
style={{ display: "inline-flex", marginLeft: "calc(100vw - 1000px)" }}
style={{ display: "inline-flex", marginLeft: "calc(100vw - 1100px)" }}
>
<Tab eventKey="editor" title="Editor">
<Main artifact={artifact} setArtifact={setArtifact} />
<Main code={code} setCode={setCode} artifacts={artifacts} setArtifacts={setArtifacts} setContracts={setContracts} updateAllUtxosContracts={updateAllUtxosContracts}/>
</Tab>
<Tab eventKey="newcontract" title="New Contract">
<NewContract artifacts={artifacts} network={network} setNetwork={setNetwork} contracts={contracts} setContracts={setContracts} updateUtxosContract={updateUtxosContract} />
</Tab>
<Tab eventKey="contract" title="Contract">
<ContractInfo artifact={artifact} network={network} setNetwork={setNetwork} utxos={utxos} balance={balance} contract={contract} setContract={setContract} updateUtxosContract={updateUtxosContract} />
<Tab eventKey="contracts" title="Contracts">
<Contracts contracts={contracts} setContracts={setContracts} updateUtxosContract={updateUtxosContract} />
</Tab>
<Tab eventKey="wallets" title="Wallets">
<WalletInfo network={network} wallets={wallets} setWallets={setWallets}/>
</Tab>
<Tab eventKey="transactionBuilder" title="TransactionBuilder">
<ContractFunctions artifact={artifact} contract={contract} network={network} wallets={wallets} contractUtxos={utxos} updateUtxosContract={updateUtxosContract} />
<TransactionBuilder network={network} wallets={wallets} contracts={contracts} updateUtxosContract={updateUtxosContract}/>
</Tab>
</Tabs>
</div>
Expand Down
108 changes: 84 additions & 24 deletions src/components/ArtifactsInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react'
import { Artifact } from 'cashscript'
import { Button } from 'react-bootstrap'
import FileUploader from './FileUploader'

interface Props {
artifact?: Artifact
setCode: (code: string) => void
artifacts?: Artifact[]
setArtifacts: (artifacts: Artifact[] | undefined) => void
}

const ContractInfo: React.FC<Props> = ({ artifact }) => {
const ArtifactsInfo: React.FC<Props> = ({ setCode, artifacts, setArtifacts }) => {

const downloadArtifact = () => {
if(!artifact) return
const downloadArtifact = (artifact: Artifact) => {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(artifact, null, 2)));
element.setAttribute('download', `${artifact.contractName}.json`);
Expand All @@ -18,6 +21,36 @@ const ContractInfo: React.FC<Props> = ({ artifact }) => {
document.body.removeChild(element);
}

const importArtifactFile = (fileText: string) => {
try{
const importedArtifact: Artifact = JSON.parse(fileText)
const nameNewArtifact = importedArtifact.contractName
const sameArifactExists = artifacts?.find(artifact => nameNewArtifact === artifact.contractName)
if(sameArifactExists){
const confirmOverwrite = confirm("About to overwite existing artifact with same name")
if(!confirmOverwrite) return
}
const newArtifacts = [importedArtifact, ...artifacts ?? []]
setArtifacts(newArtifacts)
localStorage.setItem("artifacts", JSON.stringify(newArtifacts , null, 2));
alert("imported!")
} catch(error){
console.log(error)
alert("import failed")
}
};

const removeArtifact = (artifactToRemove: Artifact) => {
const artifactToRemoveName = artifactToRemove.contractName;
const newArtifacts = artifacts?.filter(artifact => artifact.contractName !== artifactToRemoveName)
setArtifacts(newArtifacts)
localStorage.setItem("artifacts", JSON.stringify(newArtifacts , null, 2));
}

const loadArtifact = (artifact: Artifact) => {
setCode(artifact.source)
}

return (
<div style={{
flex: 2,
Expand All @@ -30,29 +63,56 @@ const ContractInfo: React.FC<Props> = ({ artifact }) => {
background: '#fffffe',
padding: '8px 16px',
color: '#000'
}}>{ artifact?
}}>
<div style={{display:"flex", justifyContent:"space-between"}}>
<h2 style={{width:"fit-content"}}>Contract Artifacts</h2>
<FileUploader handleFile={importArtifactFile} />
</div>

{ artifacts?.length?
(<div>
<h2>Artifact {artifact.contractName}</h2>
<strong>Last Updated</strong>
<p>{artifact.updatedAt}</p>
<strong>Artifact Bytecode</strong>
<details>
<summary>
show full Bytecode
</summary>
{artifact.bytecode}
</details>
<strong>Compiler Version</strong>
<p>{artifact.compiler.version}</p>
<strong>Download Artifact</strong>
<p onClick={downloadArtifact}>
download JSON file
<img src='./downloadIcon.svg' style={{marginLeft:"5px", verticalAlign: "text-bottom", cursor:"pointer"}}/>
</p>
</div>) :
{artifacts.map(artifact => (
<details key={artifact.contractName} style={{margin:"10px 0"}}>
<summary style={{fontSize: "1rem"}}>
{artifact.contractName}
<div style={{float:"right"}}>
<img
src='./trash.svg'
onClick={() => removeArtifact(artifact)}
style={{padding: "0px 6px", width: "fit-content", cursor:"pointer"}}
alt='trashIcon'
/>
</div>
</summary>

<div style={{paddingLeft: "15px"}}>
<strong>Last Updated</strong>
<p>{artifact.updatedAt.split('T')[0]} {artifact.updatedAt.split('T')[1].slice(0,5)}</p>
<strong>Artifact Bytecode</strong>
<details>
<summary>
show full Bytecode
</summary>
{artifact.bytecode}
</details>
<strong>Compiler Version</strong>
<p>{artifact.compiler.version}</p>
<strong>Download Artifact</strong>
<p onClick={() => downloadArtifact(artifact)} style={{cursor:"pointer"}}>
download JSON file
<img src='./downloadIcon.svg' style={{marginLeft:"5px", verticalAlign: "text-bottom"}}/>
</p>
<strong>Load Contract to Editor</strong>
<Button variant="secondary" size="sm" style={{display:"block"}} onClick={() => loadArtifact(artifact)}>
Load Artifact
</Button>
</div>
</details>
))}
</div>) :
<div>Compile a CashScript contract to get started!</div> }
</div>
)
}

export default ContractInfo
export default ArtifactsInfo
Loading