Skip to content
Open
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: 10 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:api.github.com)",
"Bash(yarn install:*)"
]
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"@cashscript/utils": "^0.12.0",
"@cashscript/utils": "^0.13.0-next.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@monaco-editor/react": "^3.6.2",
"bootstrap": "^5.3.7",
"cashc": "^0.12.0",
"cashscript": "^0.12.0",
"cashc": "^0.13.0-next.4",
"cashscript": "^0.13.0-next.4",
"next": "15.5.9",
"react": "18.2.0",
"react-bootstrap": "^2.10.10",
Expand Down
6 changes: 4 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ function App() {
if (!currentContract) return
// create a separate lists for utxos and mutate entry
const utxosList = contracts.map(contract => contract.utxos ?? [])
const contractUtxos = await provider.getUtxos(currentContract.address);
// works for all contract types (p2sh20, p2sh32, p2s)
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) => (
Expand All @@ -44,7 +45,8 @@ function App() {
if(!contracts) return

const utxosPromises = contracts.map(contractInfo => {
const contractUtxos = provider.getUtxos(contractInfo.contract.address);
// works for all contract types (p2sh20, p2sh32, p2s)
const contractUtxos = contractInfo.contract.getUtxos();
return contractUtxos ?? []
})
const utxosContracts = await Promise.all(utxosPromises)
Expand Down
15 changes: 8 additions & 7 deletions src/components/ContractCreation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Artifact, Contract, ConstructorArgument, NetworkProvider } from 'cashscript'
import { Artifact, Contract, ConstructorArgument, NetworkProvider, ContractType } from 'cashscript'
import { InputGroup, Form, Button } from 'react-bootstrap'
import { readAsConstructorType, ContractInfo, TinyContractObj } from './shared'

Expand All @@ -14,7 +14,7 @@ interface Props {
const ContractCreation: React.FC<Props> = ({ artifact, contracts, setContracts, provider, updateUtxosContract}) => {
const [constructorArgs, setConstructorArgs] = useState<ConstructorArgument[]>([])
const [nameContract, setNameContract] = useState<string>("");
const [contractType, setContractType] = useState<"p2sh32" | "p2sh20">("p2sh32");
const [contractType, setContractType] = useState<ContractType>("p2sh32");
const [createdContract, setCreatedContract] = useState(false);

const resetInputFields = () => {
Expand Down Expand Up @@ -62,7 +62,7 @@ const ContractCreation: React.FC<Props> = ({ artifact, contracts, setContracts,
return
}
try {
const newContract = new Contract(artifact, constructorArgs, { provider, addressType: contractType })
const newContract = new Contract(artifact, constructorArgs, { provider, contractType })
newContract.name = nameContract
const contractInfo = {contract: newContract, utxos: undefined, args: constructorArgs}
setContracts([contractInfo, ...contracts ?? []])
Expand All @@ -84,7 +84,7 @@ const ContractCreation: React.FC<Props> = ({ artifact, contracts, setContracts,
)
const tinyContractObj: TinyContractObj = {
contractName: contract.name,
contractType: contract.addressType,
contractType: contract.contractType,
artifactName: contract.artifact.contractName,
network: contract.provider.network,
args: strifiedArgs
Expand Down Expand Up @@ -122,13 +122,14 @@ const ContractCreation: React.FC<Props> = ({ artifact, contracts, setContracts,
/>
</InputGroup>
<p>Contract Type:</p>
<Form.Control size="sm" id="network-selector" style={{width: "350px"}}
<Form.Control size="sm" id="contractType-selector" style={{width: "350px"}}
as="select"
value={provider.network}
onChange={(event) => setContractType(event.target.value as "p2sh32" | "p2sh20")}
value={contractType}
onChange={(event) => setContractType(event.target.value as ContractType)}
>
<option value="p2sh32">p2sh32 (default)</option>
<option value="p2sh20">p2sh20</option>
<option value="p2s">p2s</option>
</Form.Control>
<p>Initialise contract by providing contract arguments:</p>
{constructorForm}
Expand Down
60 changes: 36 additions & 24 deletions src/components/Contracts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ const Contracts: React.FC<Props> = ({ provider, contracts, setContracts, updateU

const removeContract = (contractInfo: ContractInfo) => {
const contractToRemove = contractInfo.contract
const contractToRemoveAddress = contractToRemove.address;
const newContracts = contracts?.filter(contractInfo => contractInfo.contract.address !== contractToRemoveAddress)
const newContracts = contracts?.filter(ci => ci.contract.name !== contractToRemove.name)
setContracts(newContracts)
}

const addRandomUtxo = (contractInfo:ContractInfo) => {
if(!(provider instanceof MockNetworkProvider)) return
provider.addUtxo(contractInfo.contract.address, randomUtxo())
const contract = contractInfo.contract as any
const address = contract.contractType === 'p2s' ? contract.lockingBytecode : contract.address
provider.addUtxo(address, randomUtxo())
updateUtxosContract(contractInfo.contract.name)
}

Expand All @@ -45,33 +46,43 @@ const Contracts: React.FC<Props> = ({ provider, contracts, setContracts, updateU
{contracts == undefined ? <p>
No Contracts created yet...
</p>:null}
{contracts?.map((contractInfo) => (
<Card style={{ marginBottom: '10px' }} key={contractInfo.contract.name + contractInfo.contract.address}>
{contracts?.map((contractInfo) => {
const contract = contractInfo.contract as any
const isP2s = contract.contractType === 'p2s'
return (
<Card style={{ marginBottom: '10px' }} key={contractInfo.contract.name + (isP2s ? contract.lockingBytecode : contract.address)}>
<Card.Header style={{ display:"flex", justifyContent:"space-between"}}>
<div>{contractInfo.contract.name}</div>
<img src='./trash.svg' onClick={() => removeContract(contractInfo)} style={{padding: "0px 6px", width: "28px", cursor:"pointer"}}/>
</Card.Header>
<Card.Body>
<div style={{ margin: '5px', width: '100%' }}>
<strong>Contract type:</strong>
<span>{contractInfo.contract.addressType}</span><br/>
<strong>Contract address</strong>
<CopyText>{contractInfo.contract.address}</CopyText>
<strong>Contract token address</strong>
<CopyText>{contractInfo.contract.tokenAddress}</CopyText>
<strong>Contract type: </strong>
<span>{contract.contractType}</span><br/>
{!isP2s && <>
<strong>Contract address</strong>
<CopyText>{contract.address}</CopyText>
<strong>Contract token address</strong>
<CopyText>{contract.tokenAddress}</CopyText>
</>}
<strong>Contract locking bytecode</strong>
<CopyText>{contract.lockingBytecode}</CopyText>
<strong>Contract artifact</strong>
<p>{contractInfo.contract.artifact.contractName}</p>
<strong>Contract arguments</strong>
<details>
<summary>Details</summary>
<div>
{contractInfo.args.map((arg, index) => (<div key={`${contractInfo.contract.name}-arg-${index}`}>
{contractInfo.contract.artifact.constructorInputs[index]?.type} {contractInfo.contract.artifact.constructorInputs[index]?.name + ": "}
{typeof arg == "bigint" ? arg.toString() : null}
{typeof arg == "string" || typeof arg == "number" ? arg : null}
</div>))}
</div>
</details>
<p>{contractInfo.args.length} {contractInfo.args.length === 1 ? "argument" : "arguments"}</p>
{contractInfo.args.length > 0 && (
<details>
<summary>Details</summary>
<div>
{contractInfo.args.map((arg, index) => (<div key={`${contractInfo.contract.name}-arg-${index}`}>
{contractInfo.contract.artifact.constructorInputs[index]?.type} {contractInfo.contract.artifact.constructorInputs[index]?.name + ": "}
{typeof arg == "bigint" ? arg.toString() : null}
{typeof arg == "string" || typeof arg == "number" ? arg : null}
</div>))}
</div>
</details>
)}
<strong>Contract utxos</strong>
{contractInfo?.utxos == undefined?
<p>loading ...</p>:
Expand All @@ -98,7 +109,7 @@ const Contracts: React.FC<Props> = ({ provider, contracts, setContracts, updateU
</div>
<details style={{maxWidth: "50%"}}>
<summary>Create custom utxo</summary>
<CreateUtxo provider={provider} address={contractInfo.contract.address}
<CreateUtxo provider={provider} address={isP2s ? contract.lockingBytecode : contract.address}
updateUtxos={() => updateUtxosContract(contractInfo.contract.name)}/>
</details>
</div>) : null}
Expand All @@ -108,11 +119,12 @@ const Contracts: React.FC<Props> = ({ provider, contracts, setContracts, updateU
<p>{contractInfo.utxos?.reduce((acc, utxo) => acc + utxo.satoshis, 0n).toString()} satoshis</p>
}
<strong>Contract size</strong>
<p>{contractInfo.contract.bytesize} bytes (max 1650)</p>
<p>{contractInfo.contract.bytesize} bytes (max {isP2s ? '201' : '10,000'} bytes)</p>
</div>
</Card.Body>
</Card>
))}
)
})}
</div>
)
}
Expand Down
20 changes: 11 additions & 9 deletions src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ const Main: React.FC<Props> = ({
} catch(error){ console.log(error) }
} else {
// add default example contracts to local storage
const artifactExampleTimeout = compileString(exampleTimeoutContract)
const artifactExampleEscrow = compileString(exampleEscrowContract)
const artifactExampleStramingMecenas = compileString(exampleStramingMecenasContract)
const artifactExampleDex = compileString(exampleDexContract)
const defaultArtifacts = [artifactExampleTimeout, artifactExampleEscrow, artifactExampleStramingMecenas, artifactExampleDex]
setArtifacts(defaultArtifacts)
localStorage.setItem("artifacts", JSON.stringify(defaultArtifacts , null, 2));
try {
const artifactExampleTimeout = compileString(exampleTimeoutContract)
const artifactExampleEscrow = compileString(exampleEscrowContract)
const artifactExampleStramingMecenas = compileString(exampleStramingMecenasContract)
const artifactExampleDex = compileString(exampleDexContract)
const defaultArtifacts = [artifactExampleTimeout, artifactExampleEscrow, artifactExampleStramingMecenas, artifactExampleDex]
setArtifacts(defaultArtifacts)
localStorage.setItem("artifacts", JSON.stringify(defaultArtifacts , null, 2));
} catch(error) { console.log(error) }
}
if (networkLocalStorage && networkLocalStorage != "mocknet"){
const newProvider = new ElectrumNetworkProvider(networkLocalStorage as Network)
Expand All @@ -71,8 +73,8 @@ const Main: React.FC<Props> = ({
if(typeof arg == "string" && arg.startsWith("bigint")) return BigInt(arg.slice(6))
return arg
})
const addressType = contractType ?? "p2sh32"
const newContract = new Contract(matchingArtifact, unstringifiedArgs, {provider, addressType})
const resolvedContractType = contractType ?? "p2sh32"
const newContract = new Contract(matchingArtifact, unstringifiedArgs, {provider, contractType: resolvedContractType})
newContract.name = contractName
const contractInfo: ContractInfo = {
contract: newContract,
Expand Down
64 changes: 60 additions & 4 deletions src/components/TransactionBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useState} from 'react'
import { NetworkProvider, Recipient, SignatureTemplate, TransactionBuilder, Unlocker } from 'cashscript'
import { NetworkProvider, Output, SignatureTemplate, TransactionBuilder, Unlocker } from 'cashscript'
import { Wallet, ContractInfo, ExplorerString, ContractUtxo, WalletUtxo } from './shared'
import { Button, Card, Form } from 'react-bootstrap'
import TransactionOutputs from './TransactionOutputs'
Expand All @@ -16,10 +16,15 @@ const TransactionBuilderPage: React.FC<Props> = ({ provider, wallets, contracts,

const [enableLocktime, setEnableLocktime] = useState<Boolean>(false)
const [locktime, setLocktime] = useState<String>("")
const [allowImplicitFungibleTokenBurn, setAllowImplicitFungibleTokenBurn] = useState<boolean>(false)
const [enableMaxFeeSatoshis, setEnableMaxFeeSatoshis] = useState<boolean>(false)
const [maximumFeeSatoshis, setMaximumFeeSatoshis] = useState<string>("")
const [enableMaxFeeSatsPerByte, setEnableMaxFeeSatsPerByte] = useState<boolean>(false)
const [maximumFeeSatsPerByte, setMaximumFeeSatsPerByte] = useState<string>("")

const [inputs, setInputs] = useState<(WalletUtxo | ContractUtxo | undefined)[]>([undefined])
const [inputUnlockers, setInputUnlockers] = useState<Unlocker[]>([])
const [outputs, setOutputs] = useState<Recipient[]>([{ to: '', amount: 0n }])
const [outputs, setOutputs] = useState<Output[]>([{ to: '', amount: 0n }])

function addOutput() {
const outputsCopy = [...outputs]
Expand Down Expand Up @@ -47,7 +52,12 @@ const TransactionBuilderPage: React.FC<Props> = ({ provider, wallets, contracts,
// try to send transaction and alert result
try {
// start constructing transaction
const transaction = new TransactionBuilder({provider})
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) => {
Expand Down Expand Up @@ -116,7 +126,7 @@ const TransactionBuilderPage: React.FC<Props> = ({ provider, wallets, contracts,
style={{ display: "inline-block" }}
onChange={() => setEnableLocktime(!enableLocktime)}
/>

{ enableLocktime && <Form.Control size="sm"
placeholder="locktime"
aria-label="locktime"
Expand Down Expand Up @@ -149,6 +159,52 @@ const TransactionBuilderPage: React.FC<Props> = ({ provider, wallets, contracts,
</Card.Body>
</Card>

<details style={{ marginBottom: '10px' }}>
<summary>TransactionBuilder Options</summary>
<Form style={{ marginTop: '10px' }}>
<Form.Check
type="switch"
id={"allowImplicitFungibleTokenBurn"}
label="Allow Implicit Fungible Token Burn (disables safety check)"
className='primary'
style={{ marginBottom: '8px' }}
onChange={() => setAllowImplicitFungibleTokenBurn(!allowImplicitFungibleTokenBurn)}
/>
<Form.Check
type="switch"
id={"enableMaxFeeSatoshis"}
label="Maximum Fee Limit (satoshis)"
className='primary'
style={{ marginBottom: '8px' }}
onChange={() => setEnableMaxFeeSatoshis(!enableMaxFeeSatoshis)}
/>
{enableMaxFeeSatoshis && <Form.Control
size="sm"
type="text"
placeholder="Maximum fee in satoshis"
style={{ width: '350px', marginBottom: '8px' }}
value={maximumFeeSatoshis}
onChange={(e) => setMaximumFeeSatoshis(e.target.value)}
/>}
<Form.Check
type="switch"
id={"enableMaxFeeSatsPerByte"}
label="Maximum Fee Limit (sats/byte)"
className='primary'
style={{ marginBottom: '8px' }}
onChange={() => setEnableMaxFeeSatsPerByte(!enableMaxFeeSatsPerByte)}
/>
{enableMaxFeeSatsPerByte && <Form.Control
size="sm"
type="text"
placeholder="Maximum fee in sats/byte"
style={{ width: '350px', marginBottom: '8px' }}
value={maximumFeeSatsPerByte}
onChange={(e) => setMaximumFeeSatsPerByte(e.target.value)}
/>}
</Form>
</details>

<Button variant="secondary" style={{ display: "block" }} size="sm" onClick={sendTransaction}>
{ provider.network === "mocknet" ? "Evaluate" : "Send" }
</Button>
Expand Down
14 changes: 8 additions & 6 deletions src/components/TransactionOutputs.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, {useState} from 'react'
import { Recipient } from 'cashscript'
import { Output } from 'cashscript'
import { Form, InputGroup } from 'react-bootstrap'
import { hexToUint8Array, isHexString } from './shared'

interface Props {
outputs: Recipient[]
setOutputs: (outputs: Recipient[]) => void
outputs: Output[]
setOutputs: (outputs: Output[]) => void
}

const TransactionOutputs: React.FC<Props> = ({ outputs, setOutputs }) => {
Expand Down Expand Up @@ -90,12 +91,13 @@ const TransactionOutputs: React.FC<Props> = ({ outputs, setOutputs }) => {
<div>
<InputGroup>
<Form.Control size="sm"
placeholder="Receiver address"
aria-label="Receiver address"
placeholder="Receiver address or locking bytecode (hex)"
aria-label="Receiver address or locking bytecode"
onChange={(event) => {
const outputsCopy = [...outputs]
const output = outputsCopy[index]
output.to = event.target.value
const value = event.target.value
output.to = isHexString(value) ? hexToUint8Array(value) : value
outputsCopy[index] = output
setOutputs(outputsCopy)
}}
Expand Down
Loading