diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2a912bd --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:github.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:api.github.com)", + "Bash(yarn install:*)" + ] + } +} diff --git a/package.json b/package.json index 87c61aa..dae90ec 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/App.tsx b/src/components/App.tsx index a0bfca2..0cbda8e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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) => ( @@ -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) diff --git a/src/components/ContractCreation.tsx b/src/components/ContractCreation.tsx index 656fd1c..640845b 100644 --- a/src/components/ContractCreation.tsx +++ b/src/components/ContractCreation.tsx @@ -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' @@ -14,7 +14,7 @@ interface Props { const ContractCreation: React.FC = ({ artifact, contracts, setContracts, provider, updateUtxosContract}) => { const [constructorArgs, setConstructorArgs] = useState([]) const [nameContract, setNameContract] = useState(""); - const [contractType, setContractType] = useState<"p2sh32" | "p2sh20">("p2sh32"); + const [contractType, setContractType] = useState("p2sh32"); const [createdContract, setCreatedContract] = useState(false); const resetInputFields = () => { @@ -62,7 +62,7 @@ const ContractCreation: React.FC = ({ 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 ?? []]) @@ -84,7 +84,7 @@ const ContractCreation: React.FC = ({ 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 @@ -122,13 +122,14 @@ const ContractCreation: React.FC = ({ artifact, contracts, setContracts, />

Contract Type:

- setContractType(event.target.value as "p2sh32" | "p2sh20")} + value={contractType} + onChange={(event) => setContractType(event.target.value as ContractType)} > +

Initialise contract by providing contract arguments:

{constructorForm} diff --git a/src/components/Contracts.tsx b/src/components/Contracts.tsx index c5bc95b..872f028 100644 --- a/src/components/Contracts.tsx +++ b/src/components/Contracts.tsx @@ -17,14 +17,15 @@ const Contracts: React.FC = ({ 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) } @@ -45,33 +46,43 @@ const Contracts: React.FC = ({ provider, contracts, setContracts, updateU {contracts == undefined ?

No Contracts created yet...

:null} - {contracts?.map((contractInfo) => ( - + {contracts?.map((contractInfo) => { + const contract = contractInfo.contract as any + const isP2s = contract.contractType === 'p2s' + return ( +
{contractInfo.contract.name}
removeContract(contractInfo)} style={{padding: "0px 6px", width: "28px", cursor:"pointer"}}/>
- Contract type: - {contractInfo.contract.addressType}
- Contract address - {contractInfo.contract.address} - Contract token address - {contractInfo.contract.tokenAddress} + Contract type: + {contract.contractType}
+ {!isP2s && <> + Contract address + {contract.address} + Contract token address + {contract.tokenAddress} + } + Contract locking bytecode + {contract.lockingBytecode} Contract artifact

{contractInfo.contract.artifact.contractName}

Contract arguments -
- Details -
- {contractInfo.args.map((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} -
))} -
-
+

{contractInfo.args.length} {contractInfo.args.length === 1 ? "argument" : "arguments"}

+ {contractInfo.args.length > 0 && ( +
+ Details +
+ {contractInfo.args.map((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} +
))} +
+
+ )} Contract utxos {contractInfo?.utxos == undefined?

loading ...

: @@ -98,7 +109,7 @@ const Contracts: React.FC = ({ provider, contracts, setContracts, updateU
Create custom utxo - updateUtxosContract(contractInfo.contract.name)}/>
) : null} @@ -108,11 +119,12 @@ const Contracts: React.FC = ({ provider, contracts, setContracts, updateU

{contractInfo.utxos?.reduce((acc, utxo) => acc + utxo.satoshis, 0n).toString()} satoshis

} Contract size -

{contractInfo.contract.bytesize} bytes (max 1650)

+

{contractInfo.contract.bytesize} bytes (max {isP2s ? '201' : '10,000'} bytes)

- ))} + ) + })} ) } diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 8f09a7f..a3a15b6 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -41,13 +41,15 @@ const Main: React.FC = ({ } 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) @@ -71,8 +73,8 @@ const Main: React.FC = ({ 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, diff --git a/src/components/TransactionBuilder.tsx b/src/components/TransactionBuilder.tsx index ecda08f..7d2537f 100644 --- a/src/components/TransactionBuilder.tsx +++ b/src/components/TransactionBuilder.tsx @@ -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' @@ -16,10 +16,15 @@ const TransactionBuilderPage: React.FC = ({ provider, wallets, contracts, const [enableLocktime, setEnableLocktime] = useState(false) const [locktime, setLocktime] = useState("") + const [allowImplicitFungibleTokenBurn, setAllowImplicitFungibleTokenBurn] = useState(false) + const [enableMaxFeeSatoshis, setEnableMaxFeeSatoshis] = useState(false) + const [maximumFeeSatoshis, setMaximumFeeSatoshis] = useState("") + const [enableMaxFeeSatsPerByte, setEnableMaxFeeSatsPerByte] = useState(false) + const [maximumFeeSatsPerByte, setMaximumFeeSatsPerByte] = useState("") const [inputs, setInputs] = useState<(WalletUtxo | ContractUtxo | undefined)[]>([undefined]) const [inputUnlockers, setInputUnlockers] = useState([]) - const [outputs, setOutputs] = useState([{ to: '', amount: 0n }]) + const [outputs, setOutputs] = useState([{ to: '', amount: 0n }]) function addOutput() { const outputsCopy = [...outputs] @@ -47,7 +52,12 @@ const TransactionBuilderPage: React.FC = ({ 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) => { @@ -116,7 +126,7 @@ const TransactionBuilderPage: React.FC = ({ provider, wallets, contracts, style={{ display: "inline-block" }} onChange={() => setEnableLocktime(!enableLocktime)} /> - + { enableLocktime && = ({ provider, wallets, contracts,
+
+ TransactionBuilder Options +
+ setAllowImplicitFungibleTokenBurn(!allowImplicitFungibleTokenBurn)} + /> + setEnableMaxFeeSatoshis(!enableMaxFeeSatoshis)} + /> + {enableMaxFeeSatoshis && setMaximumFeeSatoshis(e.target.value)} + />} + setEnableMaxFeeSatsPerByte(!enableMaxFeeSatsPerByte)} + /> + {enableMaxFeeSatsPerByte && setMaximumFeeSatsPerByte(e.target.value)} + />} + +
+ diff --git a/src/components/TransactionOutputs.tsx b/src/components/TransactionOutputs.tsx index 819ea7b..d2dbc9e 100644 --- a/src/components/TransactionOutputs.tsx +++ b/src/components/TransactionOutputs.tsx @@ -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 = ({ outputs, setOutputs }) => { @@ -90,12 +91,13 @@ const TransactionOutputs: React.FC = ({ outputs, setOutputs }) => {
{ 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) }} diff --git a/src/components/shared/index.tsx b/src/components/shared/index.tsx index 7be2776..7950ae3 100644 --- a/src/components/shared/index.tsx +++ b/src/components/shared/index.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled' -import { Contract, SignatureTemplate, Utxo, ConstructorArgument, Network } from 'cashscript'; +import { Contract, SignatureTemplate, Utxo, ConstructorArgument, Network, ContractType } from 'cashscript'; import { decodeCashAddress, decodeCashAddressFormatWithoutPrefix } from '@bitauth/libauth'; export const ColumnFlex = styled.div` @@ -42,7 +42,7 @@ export interface ContractUtxo extends NamedUtxo { export interface TinyContractObj { contractName: string - contractType: "p2sh32" | "p2sh20" + contractType: ContractType artifactName: string network: Network args: (string | ConstructorArgument)[] @@ -114,6 +114,18 @@ export function readAsConstructorType(value: string, type: string) { } } +export function isHexString(value: string): boolean { + return /^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0 && value.length > 0 +} + +export function hexToUint8Array(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2) + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16) + } + return bytes +} + export const ExplorerString = { mainnet: 'https://explorer.bitcoin.com/bch', testnet: 'http://testnet.imaginary.cash', diff --git a/src/exampleContracts/examples.ts b/src/exampleContracts/examples.ts index c7dadf2..2d44f40 100644 --- a/src/exampleContracts/examples.ts +++ b/src/exampleContracts/examples.ts @@ -1,4 +1,4 @@ -export const exampleTimeoutContract = `pragma cashscript ~0.12.0; +export const exampleTimeoutContract = `pragma cashscript ~0.13.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.12.0; +export const exampleEscrowContract = `pragma cashscript ~0.13.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.12.0; +export const exampleStramingMecenasContract = `pragma cashscript ~0.13.0; // see https://cashscript.org/docs/guides/covenants#keeping-local-state-in-nfts @@ -88,7 +88,7 @@ contract StreamingMecenas( require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode); // Update the block height of the previous pledge, kept in the NFT commitment - bytes blockHeightNewPledge = bytes8(tx.locktime); + bytes blockHeightNewPledge = toPaddedBytes(tx.locktime, 8); require(tx.outputs[1].nftCommitment == blockHeightNewPledge); } } @@ -100,7 +100,7 @@ contract StreamingMecenas( } ` -export const exampleDexContract = `pragma cashscript ~0.12.0; +export const exampleDexContract = `pragma cashscript ~0.13.0; // see https://cashscript.org/docs/language/examples#amm-dex diff --git a/yarn.lock b/yarn.lock index a89a59e..8e22692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,10 +70,10 @@ resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz#d130e5db6c3c8b24731c8d04c4091be07f48b0ee" integrity sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA== -"@cashscript/utils@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.12.0.tgz#d4e96428ecb09792fb87970f4675214d7e54f409" - integrity sha512-25sJQuEUBeZ3BwCRJ3bruqsyP/9P1VLy9gnqObB/Uz2rzW9OAmDQ5gDsx9ABywazoXmq5/mQ+gQBwlqt9qpgmA== +"@cashscript/utils@^0.13.0-next.4": + version "0.13.0-next.4" + resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.13.0-next.4.tgz#9ec77d9ce82b8c009a75acd3dc7126ead0394844" + integrity sha512-XOCk/eRlTE6FQ6QTEwxjEceUcM5EIH52IOGQrDJDVLne4dN9orhJA+Qdwvq2s0MOGbJAHcazFTnK+3X1ZX/7QA== dependencies: "@bitauth/libauth" "^3.1.0-next.8" @@ -1187,27 +1187,27 @@ caniuse-lite@^1.0.30001579: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz#ead1b155ea691d6a87938754d3cb119c24465b03" integrity sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w== -cashc@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.12.0.tgz#1e0674d569c5ae4119226032b792b226cf39457c" - integrity sha512-Cns6Xi+sRbURn7j8OwTAsVOp16BgUN3EmPNXMAj8DWhyofhJSPzkxtvynvKH10N1yDQBU8/Y9X/pz7JRA90UIg== +cashc@^0.13.0-next.4: + version "0.13.0-next.4" + resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.13.0-next.4.tgz#95c797282bdf726fdfd20c41d4f3ddb264720b09" + integrity sha512-uUCB+enf5Siwjqps3mfq3UajdREYKvRVzm1HqxpuDA5gVDPkt5iORePjmcZXBVW8g0DiyHYYuXS/oAuyA7ZkuQ== dependencies: "@bitauth/libauth" "^3.1.0-next.8" - "@cashscript/utils" "^0.12.0" + "@cashscript/utils" "^0.13.0-next.4" antlr4 "^4.13.2" commander "^14.0.0" semver "^7.7.2" -cashscript@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.12.0.tgz#66225524eb94fa657b4c68ac90e786d4348a848b" - integrity sha512-pdrBjdiBka8g/jVCtaku08R2xoFCMo2GG/pdVf/z+b95Yd0H1ioFSMlp9mx1xuxeYbvZx85ZGk643CAFm+Kzgg== +cashscript@^0.13.0-next.4: + version "0.13.0-next.4" + resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.13.0-next.4.tgz#7215493b2b3511b5ecddd226da8ade8cdb51f8b5" + integrity sha512-DLkqzfKP7jaMVVpW40yXTPFQQwdBkNKWBFbjgkrJ/1Lps9YIpVBqVWHZnZVz11/do7A3guUtqIICe8SGXbSkXQ== dependencies: "@bitauth/libauth" "^3.1.0-next.8" - "@cashscript/utils" "^0.12.0" + "@cashscript/utils" "^0.13.0-next.4" "@electrum-cash/network" "^4.1.3" "@mr-zwets/bchn-api-wrapper" "^1.0.1" - pako "^2.1.0" + fflate "^0.8.2" semver "^7.7.2" chalk@^2.0.0: @@ -1987,6 +1987,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -3003,11 +3008,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -pako@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" - integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"