diff --git a/example/package-lock.json b/example/package-lock.json index c5efdaa..192d779 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -11,6 +11,7 @@ "@mantine/core": "^7.11.2", "@mantine/hooks": "^7.11.2", "@stacks/common": "^7.0.2", + "@stacks/stacking": "^7.0.2", "@stacks/transactions": "^7.0.2", "@tanstack/react-query": "^5.50.1", "bip322-js": "^2.0.0", @@ -1415,11 +1416,74 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip39": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", + "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.1.1", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@stacks/common": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@stacks/common/-/common-7.0.2.tgz", "integrity": "sha512-+RSecHdkxOtswmE4tDDoZlYEuULpnTQVeDIG5eZ32opK8cFxf4EugAcK9CsIsHx/Se1yTEaQ21WGATmJGK84lQ==" }, + "node_modules/@stacks/encryption": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-7.0.2.tgz", + "integrity": "sha512-3evRvxPqVzQAhcZ8uacQrVfAETUMIV8VyKkHGsd4QZroGWlvXQheLV3CFeDttFb304QcKq/oKv1clOvQ2shaAw==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^7.0.2", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@stacks/encryption/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@stacks/network": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@stacks/network/-/network-7.0.2.tgz", @@ -1429,6 +1493,37 @@ "cross-fetch": "^3.1.5" } }, + "node_modules/@stacks/stacking": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-7.0.2.tgz", + "integrity": "sha512-JAi396fKMgA0v8Lrj6yYNKRBuPHT+dq1/vVs1GCpgbH74ZHQT6NQYBrsnIxtc85M9w86zMr1FHrIq66Z0kKh/A==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@scure/base": "1.1.1", + "@stacks/common": "^7.0.2", + "@stacks/encryption": "^7.0.2", + "@stacks/network": "^7.0.2", + "@stacks/stacks-blockchain-api-types": "^0.61.0", + "@stacks/transactions": "^7.0.2", + "bs58": "^5.0.0" + } + }, + "node_modules/@stacks/stacking/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/stacks-blockchain-api-types": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", + "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==" + }, "node_modules/@stacks/transactions": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.0.2.tgz", @@ -2028,7 +2123,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5684,6 +5778,14 @@ "inherits": "^2.0.1" } }, + "node_modules/ripemd160-min": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", + "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==", + "engines": { + "node": ">=8" + } + }, "node_modules/rollup": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", diff --git a/example/package.json b/example/package.json index 7ced537..93e11f8 100644 --- a/example/package.json +++ b/example/package.json @@ -19,6 +19,7 @@ "@mantine/core": "^7.11.2", "@mantine/hooks": "^7.11.2", "@stacks/common": "^7.0.2", + "@stacks/stacking": "^7.0.2", "@stacks/transactions": "^7.0.2", "@tanstack/react-query": "^5.50.1", "bip322-js": "^2.0.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 29b0341..2496eb7 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -35,6 +35,7 @@ import { NetworkSelector } from './components/NetworkSelector'; import { SendSip10 } from './components/stacks/SendSip10'; import { SendStx } from './components/stacks/SendStx'; import { SignTransaction } from './components/stacks/SignTransaction.tsx'; +import { SignTransactions } from './components/stacks/SignTransactions/index.tsx'; import TransferRunes from './components/transferRunes/index.tsx'; import { GetPermissions } from './components/wallet/GetPermissions.tsx'; import { WalletType } from './components/wallet/WalletType'; @@ -329,6 +330,7 @@ const StacksMethods = () => { {stxAddressInfo?.[0]?.publicKey ? ( ) : null} + ); }; diff --git a/example/src/components/bitcoin/GetAccounts.tsx b/example/src/components/bitcoin/GetAccounts.tsx index d08d2d5..9031404 100644 --- a/example/src/components/bitcoin/GetAccounts.tsx +++ b/example/src/components/bitcoin/GetAccounts.tsx @@ -1,11 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import Wallet, { AddressPurpose } from 'sats-connect'; -import styled from 'styled-components'; import { Button, Card } from '../../App.styles'; - -const ErrorMessage = styled.div({ - color: 'red', -}); +import { ErrorMessage } from '../common'; export function GetAccounts() { const { refetch, error, data, isFetching, isError, isSuccess } = useQuery({ diff --git a/example/src/components/bitcoin/GetAddresses.tsx b/example/src/components/bitcoin/GetAddresses.tsx index 4a469ac..7589227 100644 --- a/example/src/components/bitcoin/GetAddresses.tsx +++ b/example/src/components/bitcoin/GetAddresses.tsx @@ -1,11 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import Wallet, { AddressPurpose } from 'sats-connect'; -import styled from 'styled-components'; import { Button, Card } from '../../App.styles'; - -const ErrorMessage = styled.div({ - color: 'red', -}); +import { ErrorMessage } from '../common'; export function GetAddresses() { const { refetch, error, data, isFetching, isError, isSuccess } = useQuery({ diff --git a/example/src/components/common.tsx b/example/src/components/common.tsx new file mode 100644 index 0000000..e327380 --- /dev/null +++ b/example/src/components/common.tsx @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const ErrorMessage = styled.div({ + color: 'red', +}); diff --git a/example/src/components/stacks/SignTransactions/index.tsx b/example/src/components/stacks/SignTransactions/index.tsx new file mode 100644 index 0000000..94d57e5 --- /dev/null +++ b/example/src/components/stacks/SignTransactions/index.tsx @@ -0,0 +1,81 @@ +import { Button, Card, Checkbox, Stack, Switch } from '@mantine/core'; +import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import { ErrorMessage } from '../../common'; +import { mutationFunction } from './mutationFunction'; + +export interface Props { + publicKey: string; +} + +function buttonText(isPending: boolean) { + return isPending ? 'Signing transactions...' : 'Sign transactions'; +} + +export function SignTransactions({ publicKey }: Props) { + // Checkboxes + const [isPoolAllowContractSelected, setIsPoolAllowContractSelected] = useState(false); + const [isPoolDelegateStacksSelected, setIsPoolDelegateStacks] = useState(false); + const [isContractDeploySelected, setIsContractDeploySelected] = useState(false); + const [isTokenTransferSelected, setIsTokenTransferSelected] = useState(false); + + const [broadcast, setBroadcast] = useState(true); + + const signTransactionsMutation = useMutation({ + mutationFn: mutationFunction, + }); + + function handleSignTransactionsClick() { + signTransactionsMutation + .mutateAsync({ + isPoolAllowContractSelected, + isPoolDelegateStacksSelected, + isContractDeploySelected, + isTokenTransferSelected, + broadcast, + publicKey, + }) + .then(console.log) + .catch((error: unknown) => { + console.error(error); + if (error instanceof Error) console.error(error.cause); + }); + } + + return ( + +

Sign transactions

+ + setIsPoolAllowContractSelected(!isPoolAllowContractSelected)} + /> + setIsPoolDelegateStacks(!isPoolDelegateStacksSelected)} + /> + setIsContractDeploySelected(!isContractDeploySelected)} + /> + setIsTokenTransferSelected(!isTokenTransferSelected)} + /> + setBroadcast(!broadcast)} /> + + + + {signTransactionsMutation.isError && !signTransactionsMutation.isPending && ( + Failed to sign transactions. Check console for details. + )} + +
+ ); +} diff --git a/example/src/components/stacks/SignTransactions/mutationFunction.ts b/example/src/components/stacks/SignTransactions/mutationFunction.ts new file mode 100644 index 0000000..d35d5a7 --- /dev/null +++ b/example/src/components/stacks/SignTransactions/mutationFunction.ts @@ -0,0 +1,111 @@ +import { request } from '@sats-connect/core'; +import { poxAddressToTuple } from '@stacks/stacking'; +import { + contractPrincipalCV, + makeUnsignedContractCall, + makeUnsignedContractDeploy, + makeUnsignedSTXTokenTransfer, + noneCV, + standardPrincipalCV, + uintCV, +} from '@stacks/transactions'; + +const helloWorldContractBody = ` +(define-data-var greeting (string-ascii 100) "Hello, World!") + +(define-read-only (get-greeting) + (ok (var-get greeting)) +) + +(define-public (set-greeting (new-greeting (string-ascii 100))) + (begin + (var-set greeting new-greeting) + (ok new-greeting)) +) +`; + +export const poxContractAddress = 'SP000000000000000000002Q6VF78'; +export const poxContractName = 'pox-4'; +export const poolContractAddress = 'SP001SFSMC2ZY76PD4M68P3WGX154XCH7NE3TYMX'; +export const poolContractName = 'pox4-pools'; +export const poolAdminStacksAddress = 'SPXVRSEH2BKSXAEJ00F1BY562P45D5ERPSKR4Q33'; +export const poolAdminPoxAddress = 'bc1qmv2pxw5ahvwsu94kq5f520jgkmljs3af8ly6tr'; + +export interface MutationFnArgs { + isPoolAllowContractSelected: boolean; + isPoolDelegateStacksSelected: boolean; + isContractDeploySelected: boolean; + isTokenTransferSelected: boolean; + broadcast: boolean; + publicKey: string; +} + +export async function mutationFunction({ + isPoolAllowContractSelected, + isPoolDelegateStacksSelected, + isContractDeploySelected, + isTokenTransferSelected, + broadcast, + publicKey, +}: MutationFnArgs) { + const transactions: string[] = []; + + if (isPoolAllowContractSelected) { + const transaction = await makeUnsignedContractCall({ + contractAddress: poxContractAddress, + contractName: poxContractName, + functionName: 'allow-contract-caller', + functionArgs: [contractPrincipalCV(poolContractAddress, poolContractName), noneCV()], + publicKey, + }); + transactions.push(transaction.serialize()); + } + + if (isPoolDelegateStacksSelected) { + const transaction = await makeUnsignedContractCall({ + contractAddress: poolContractAddress, + contractName: poolContractName, + functionName: 'delegate-stx', + functionArgs: [ + uintCV(1234567890), + standardPrincipalCV(poolAdminStacksAddress), + noneCV(), + noneCV(), + poxAddressToTuple(poolAdminPoxAddress), + noneCV(), + ], + publicKey, + }); + transactions.push(transaction.serialize()); + } + + if (isContractDeploySelected) { + const now = new Date().getTime(); + const transaction = await makeUnsignedContractDeploy({ + contractName: `hello-world-${now}`, + codeBody: helloWorldContractBody, + publicKey, + }); + transactions.push(transaction.serialize()); + } + + if (isTokenTransferSelected) { + const transaction = await makeUnsignedSTXTokenTransfer({ + recipient: 'SP1VYV2JBF1QPNDSKHBZRAWRC4KQXP8ZSSRNKPJE4', // acc 4 + amount: '100000', // 0.1 STX + publicKey, + }); + transactions.push(transaction.serialize()); + } + + const res = await request('stx_signTransactions', { + transactions, + broadcast, + }); + + if (res.status === 'error') { + throw new Error('Error signing transactions', { cause: res.error }); + } + + return res.result; +} diff --git a/example/src/components/wallet/GetPermissions.tsx b/example/src/components/wallet/GetPermissions.tsx index d658c83..b722bde 100644 --- a/example/src/components/wallet/GetPermissions.tsx +++ b/example/src/components/wallet/GetPermissions.tsx @@ -1,11 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import Wallet from 'sats-connect'; -import styled from 'styled-components'; import { Button, Card } from '../../App.styles'; - -const ErrorMessage = styled.div({ - color: 'red', -}); +import { ErrorMessage } from '../common'; export function GetPermissions() { const { refetch, error, data, isFetching, isError, isSuccess } = useQuery({ diff --git a/example/src/components/wallet/WalletType.tsx b/example/src/components/wallet/WalletType.tsx index fa35de4..f3148c6 100644 --- a/example/src/components/wallet/WalletType.tsx +++ b/example/src/components/wallet/WalletType.tsx @@ -1,11 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import Wallet from 'sats-connect'; -import styled from 'styled-components'; import { Button, Card } from '../../App.styles'; - -const ErrorMessage = styled.div({ - color: 'red', -}); +import { ErrorMessage } from '../common'; export function WalletType() { const { refetch, error, data, isFetching, isError, isSuccess } = useQuery({