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

![Featured-screenhot](./screenshots/featured-screenshot.png)

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.
In the 'Editor' 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. You can easily test contracts with virtual (fake) Utxos on 'Mocknet'.

## Features

Expand All @@ -15,19 +15,23 @@ On the 'Wallets' tab, new key-pairs for testing wallets can be generated with ju

## Limitations

The playground uses the 'Simple transactio builder' so it doesn't support combining multiple different smart contracts in one transaction.
The playground uses the 'Simple transactio builder' so it doesn't currently 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

The CashScript-Playground is connected to the Bitcoin Cash chipnet by default, you can get testnet coins from the [testnet faucet](https://tbch.googol.cash/). You can also connect the playground to mainnet but be sure **never to send large amounts** of money to contracts or wallets generated by the CashScript Playground!
The CashScript-Playground is connected to the Bitcoin Cash mocknet by default, if you want to test on a live network you can choose 'chipnet' can get test-coins from the [testnet faucet](https://tbch.googol.cash/). You can also connect the playground to mainnet but be sure **never to send large amounts** of money to the contracts or wallets on the CashScript Playground!

## ScreenShots
![Screenshot-1](./screenshots/screenshot-1.png)

![Screenshot-2](./screenshots/screenshot-2.png)

![Screenshot-3](./screenshots/screenshot-3.png)

![Screenshot-4](./screenshots/screenshot-4.png)

## Running locally
```
git clone [email protected]:CashScript/cashscript-playground.git
Expand Down
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.
Binary file added screenshots/screenshot-3.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 added screenshots/screenshot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Components
import React, { useState } from 'react';
import { Artifact, ElectrumNetworkProvider, NetworkProvider } from 'cashscript';
import { Artifact, MockNetworkProvider, NetworkProvider } from 'cashscript';
import Header from './Header'
import Main from './Main'
import Footer from './Footer';
Expand All @@ -13,7 +13,7 @@ import Contracts from './Contracts';
import TransactionBuilder from './TransactionBuilder';

function App() {
const [provider, setProvider] = useState<NetworkProvider>(new ElectrumNetworkProvider("chipnet"))
const [provider, setProvider] = useState<NetworkProvider>(new MockNetworkProvider())
const [wallets, setWallets] = useState<Wallet[]>([])
const [artifacts, setArtifacts] = useState<Artifact[] | undefined>(undefined);
const [contracts, setContracts] = useState<ContractInfo[] | undefined>(undefined)
Expand Down Expand Up @@ -41,7 +41,7 @@ contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
if (!currentContract) return
// create a separate lists for utxos and mutate entry
const utxosList = contracts.map(contract => contract.utxos ?? [])
const contractUtxos = await currentContract.getUtxos();
const contractUtxos = await provider.getUtxos(currentContract.address);
utxosList[contractIndex] = contractUtxos
// map is the best way to deep clone array of complex objects
const newContracts: ContractInfo[] = contracts.map((contractInfo,index) => (
Expand All @@ -54,7 +54,7 @@ contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
if(!contracts) return

const utxosPromises = contracts.map(contractInfo => {
const contractUtxos = contractInfo.contract.getUtxos();
const contractUtxos = provider.getUtxos(contractInfo.contract.address);
return contractUtxos ?? []
})
const utxosContracts = await Promise.all(utxosPromises)
Expand Down Expand Up @@ -82,7 +82,7 @@ contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
<NewContract artifacts={artifacts} provider={provider} setProvider={setProvider} contracts={contracts} setContracts={setContracts} updateUtxosContract={updateUtxosContract} />
</Tab>
<Tab eventKey="contracts" title="Contracts">
<Contracts contracts={contracts} setContracts={setContracts} updateUtxosContract={updateUtxosContract} />
<Contracts provider={provider} contracts={contracts} setContracts={setContracts} updateUtxosContract={updateUtxosContract} />
</Tab>
<Tab eventKey="wallets" title="Wallets">
<WalletInfo provider={provider} wallets={wallets} setWallets={setWallets}/>
Expand Down
78 changes: 41 additions & 37 deletions src/components/ContractFunction.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react'
import { AbiFunction, NetworkProvider, FunctionArgument, Recipient, SignatureTemplate } from 'cashscript'
import { AbiFunction, NetworkProvider, FunctionArgument, Recipient, SignatureTemplate, MockNetworkProvider } from 'cashscript'
import { Form, InputGroup, Button, Card } from 'react-bootstrap'
import { readAsType, ExplorerString, Wallet, NamedUtxo, ContractInfo } from './shared'

interface Props {
contractInfo: ContractInfo
abi?: AbiFunction
abi: AbiFunction
provider: NetworkProvider
wallets: Wallet[]
updateUtxosContract: (contractName: string) => void
Expand All @@ -16,25 +16,24 @@ const ContractFunction: React.FC<Props> = ({ contractInfo, abi, provider, wallet
const [outputs, setOutputs] = useState<Recipient[]>([{ to: '', amount: 0n }])
// transaction inputs, not the same as abi.inputs
const [inputs, setInputs] = useState<NamedUtxo[]>([{ txid: '', vout: 0, satoshis: 0n, name: ``, isP2pkh: false }])
const [manualSelection, setManualSelection] = useState<boolean>(false)
const [outputHasFT, setOutputHasFT] = useState<boolean[]>([])
const [outputHasNFT, setOutputHasNFT] = useState<boolean[]>([])
const [noAutomaticChange, setNoAutomaticChange] = useState<boolean>(false)
const [namedUtxoList, setNamedUtxoList] = useState<NamedUtxo[]>([])

const contract = contractInfo.contract
const contractUtxos = contractInfo.utxos

useEffect(() => {
// Set empty strings as default values
const newArgs = abi?.inputs.map(() => '') || [];
setFunctionArgs(newArgs);
}, [abi])

// setNamedUtxoList on initial render and on contractInfo updates
useEffect(() => {
if (!manualSelection) return;
if (contractInfo.contract === undefined) return
async function updateUtxos() {
if (contractInfo.contract === undefined || contractUtxos === undefined) return
const contractUtxos = contractInfo.utxos ?? []
const namedUtxosContract: NamedUtxo[] = contractUtxos.map((utxo, index) => ({ ...utxo, name: `${contract.name} UTXO ${index}`, isP2pkh: false }))
let newNamedUtxoList = namedUtxosContract
const walletUtxos = wallets.map(wallet => wallet?.utxos ?? [])
Expand All @@ -49,7 +48,7 @@ const ContractFunction: React.FC<Props> = ({ contractInfo, abi, provider, wallet
setNamedUtxoList(newNamedUtxoList);
}
updateUtxos()
}, [manualSelection, contractInfo])
}, [contractInfo, wallets])

function fillPrivKey(i: number, walletIndex: string) {
const argsCopy = [...functionArgs];
Expand Down Expand Up @@ -274,25 +273,39 @@ const ContractFunction: React.FC<Props> = ({ contractInfo, abi, provider, wallet
// first step of constructing transaction
const transaction = contract.functions[abi.name](...functionArgs)

// if manualSelection is enabled, add the selected inputs
const contractInputs = inputs.filter(input => !input.isP2pkh)
let p2pkhInputs = inputs.filter(input => input.isP2pkh)
if (manualSelection) {
transaction.from(contractInputs)
p2pkhInputs.forEach(p2pkhInput => {
if(p2pkhInput !== undefined && p2pkhInput.walletIndex !== undefined){
transaction.fromP2PKH(p2pkhInput, new SignatureTemplate(wallets[p2pkhInput.walletIndex].privKey))
}
})
// add inputs to transaction in correct order
for(const input of inputs){
if(input.isP2pkh){
const walletIndex = input.walletIndex as number
const sigTemplate = new SignatureTemplate(wallets[walletIndex].privKey)
transaction.fromP2PKH(input, sigTemplate)
} else {
transaction.from(input)
}
}

// if noAutomaticChange is enabled, add this to the transaction in construction
if (noAutomaticChange) transaction.withoutChange().withoutTokenChange()
transaction.to(outputs)
const { txid } = await transaction.send()

alert(`Transaction successfully sent: ${ExplorerString[provider.network]}/tx/${txid}`)
console.log(`Transaction successfully sent: ${ExplorerString[provider.network]}/tx/${txid}`)
// check for mocknet
if(provider instanceof MockNetworkProvider){
try{
await transaction.debug()
alert(`Transaction evalution passed! see Bitauth IDE link in console`)
} catch(error) {
const errorMessage = typeof error == "string" ? error : (error as Error)?.message
const cashscriptError = errorMessage.split("Bitauth")[0]
console.error(errorMessage)
alert(`Transaction evalution failed with the following message: \n\n${cashscriptError} See Bitauth IDE link in console`)
}

console.log(`Transaction evalution passed! Bitauth IDE link: ${await transaction.bitauthUri()}`)
} else {
const { txid } = await transaction.send()
alert(`Transaction successfully sent! see explorer link in console`)
console.log(`Transaction successfully sent: ${ExplorerString[provider.network]}/tx/${txid}`)
}
updateUtxosContract(contract.name)
} catch (e: any) {
alert(e.message)
Expand Down Expand Up @@ -324,6 +337,7 @@ const ContractFunction: React.FC<Props> = ({ contractInfo, abi, provider, wallet

return (
<div>
<h5>{contract.artifact.contractName}</h5>
{contract &&
<Card style={{ marginBottom: '10px' }}>
<Card.Header>{abi?.name}</Card.Header>
Expand All @@ -332,23 +346,13 @@ const ContractFunction: React.FC<Props> = ({ contractInfo, abi, provider, wallet
<div>
{argumentFields}
</div>
<Form style={{ marginTop: '10px', marginBottom: '5px' }}>
<Form.Check
type="switch"
id={abi?.name}
label="manual UTXO selection"
onChange={() => setManualSelection(!manualSelection)}
/>
</Form>
{manualSelection ? (
<><Card.Subtitle style={{ marginTop: '10px', marginBottom: '5px' }}>
Transaction inputs{' '}
<Button variant="outline-secondary" size="sm" disabled={inputs.length <= 1} onClick={removeInput}>-</Button>
{' ' + inputs.length + ' '}
<Button variant="outline-secondary" size="sm" onClick={addInput}>+</Button>
</Card.Subtitle>
{inputFields}</>
) : null}
<Card.Subtitle style={{ marginTop: '10px', marginBottom: '5px' }}>
Transaction inputs{' '}
<Button variant="outline-secondary" size="sm" disabled={inputs.length <= 1} onClick={removeInput}>-</Button>
{' ' + inputs.length + ' '}
<Button variant="outline-secondary" size="sm" onClick={addInput}>+</Button>
</Card.Subtitle>
{inputFields}
<Form style={{ marginTop: '10px', marginBottom: '5px' }}>
<Form.Check
type="switch"
Expand Down
26 changes: 0 additions & 26 deletions src/components/ContractFunctions.tsx

This file was deleted.

47 changes: 34 additions & 13 deletions src/components/Contracts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import React from 'react'
import CopyText from './shared/CopyText'
import { Card, Button } from 'react-bootstrap'
import { ContractInfo } from './shared'
import InfoUtxos from './InfoUtxos'
import InfoUtxos from './shared/InfoUtxos'
import { MockNetworkProvider, NetworkProvider, randomUtxo } from 'cashscript'
import CreateUtxo from './shared/CreateUtxo'

interface Props {
provider: NetworkProvider
contracts: ContractInfo[] | undefined
setContracts: (contract: ContractInfo[] | undefined) => void
updateUtxosContract: (contractName: string) => void
}

const Contracts: React.FC<Props> = ({ contracts, setContracts, updateUtxosContract }) => {
const Contracts: React.FC<Props> = ({ provider, contracts, setContracts, updateUtxosContract }) => {

const removeContract = (contractInfo: ContractInfo) => {
const contractToRemove = contractInfo.contract
Expand All @@ -19,6 +22,12 @@ const Contracts: React.FC<Props> = ({ contracts, setContracts, updateUtxosContra
setContracts(newContracts)
}

const addRandomUtxo = (contractInfo:ContractInfo) => {
if(!(provider instanceof MockNetworkProvider)) return
provider.addUtxo(contractInfo.contract.address, randomUtxo())
updateUtxosContract(contractInfo.contract.name)
}

return (
<div style={{
height: 'calc(100vh - 170px)',
Expand Down Expand Up @@ -66,23 +75,35 @@ const Contracts: React.FC<Props> = ({ contracts, setContracts, updateUtxosContra
<p>loading ...</p>:
(<div>
{contractInfo?.utxos.length} {contractInfo?.utxos.length == 1 ? "utxo" : "utxos"}
<span onClick={() => {}} style={{cursor:"pointer", marginLeft:"10px"}}>
<Button size='sm' onClick={() => updateUtxosContract(contractInfo.contract.name)} variant='secondary' style={{padding:" 0px 2px"}}>refresh ⭯</Button>
</span>
{contractInfo.utxos.length ?
<details>
<summary>Show utxos</summary>
<div>
<InfoUtxos utxos={contractInfo?.utxos}/>
</div>
</details> : null}
<details style={{width: "fit-content"}}>
<summary>Show utxos</summary>
<div>
<InfoUtxos utxos={contractInfo?.utxos}/>
</div>
</details>
{ undefined === (provider as MockNetworkProvider)?.addUtxo ? (
<div style={{cursor:"pointer", marginLeft:"10px"}}>
<Button size='sm' onClick={() => updateUtxosContract(contractInfo.contract.name)} variant='secondary' style={{padding:" 0px 2px"}}>refresh ⭯</Button>
</div>)
: null}
</div>)
}
{ undefined !== (provider as MockNetworkProvider)?.addUtxo ? (
<div>
<strong>Create new contract utxo</strong>
<div onClick={() => addRandomUtxo(contractInfo)} style={{cursor:"pointer"}}>
<Button size='sm' variant='secondary' style={{padding:"0px 2px"}}>add random utxo</Button>
</div>
<details style={{maxWidth: "50%"}}>
<summary>Create custom utxo</summary>
<CreateUtxo provider={provider} address={contractInfo.contract.address}
updateUtxos={() => updateUtxosContract(contractInfo.contract.name)}/>
</details>
</div>) : null}
<strong>Total contract balance</strong>
{contractInfo.utxos == undefined?
<p>loading ...</p>:
<p>{contractInfo.utxos?.reduce((acc, utxo) => acc + utxo.satoshis, 0n).toString()} satoshis</p>

}
<strong>Contract size</strong>
<p>{contractInfo.contract.bytesize} bytes (max 520), {contractInfo.contract.opcount} opcodes (max 201)</p>
Expand Down
9 changes: 6 additions & 3 deletions src/components/NewContract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,22 @@ const NewContract: React.FC<Props> = ({ artifacts, provider, setProvider, contra
</Form.Control>
)

function changeNetwork(newNetwork: Network){
const newprovider = new ElectrumNetworkProvider(newNetwork)
function changeNetwork(newNetwork: "mocknet" | Network){
const newprovider = newNetwork == "mocknet" ?
new MockNetworkProvider() : new ElectrumNetworkProvider(newNetwork)
setProvider(newprovider)
}

const networkSelector = (
<Form.Control size="sm" id="network-selector" style={{width: "350px", display:"inline-block"}}
as="select"
value={provider.network}
value={provider.network != "chipnet" ? provider.network
: ((provider as MockNetworkProvider)?.addUtxo)? "mocknet" : "chipnet" }
onChange={(event) => {
changeNetwork(event.target.value as Network)
}}
>
<option>mocknet</option>
<option>chipnet</option>
<option>testnet3</option>
<option>testnet4</option>
Expand Down
Loading