From 200ed9612fb24fcf490de1daba46efae75124a39 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Mon, 5 Aug 2024 17:19:30 +0300 Subject: [PATCH] added stx_signTransaction example --- example/package-lock.json | 130 +++++++++++- example/package.json | 4 +- example/src/App.tsx | 26 +-- .../signTransaction/contractCode.ts | 13 ++ .../src/components/signTransaction/index.tsx | 191 ++++++++++++++++++ 5 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 example/src/components/signTransaction/contractCode.ts create mode 100644 example/src/components/signTransaction/index.tsx diff --git a/example/package-lock.json b/example/package-lock.json index 1f05c59..e5e7384 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -8,6 +8,8 @@ "name": "dapp-cookie-cutter", "version": "0.1.0", "dependencies": { + "@stacks/common": "^6.16.0", + "@stacks/transactions": "^6.16.1", "@tanstack/react-query": "^5.50.1", "bip322-js": "^2.0.0", "bitcoinjs-message": "^2.2.0", @@ -32,7 +34,7 @@ } }, "..": { - "version": "2.5.0", + "version": "2.6.0", "license": "ISC", "dependencies": { "@sats-connect/core": "0.1.2", @@ -1299,6 +1301,48 @@ "win32" ] }, + "node_modules/@stacks/common": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4" + } + }, + "node_modules/@stacks/network": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", + "dependencies": { + "@stacks/common": "^6.16.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@stacks/transactions": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, + "node_modules/@stacks/transactions/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/@tanstack/query-core": { "version": "5.50.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.50.1.tgz", @@ -1364,12 +1408,28 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bn.js": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/node": { + "version": "18.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.43.tgz", + "integrity": "sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -2321,6 +2381,18 @@ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", "dev": true }, + "node_modules/c32check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/c32check/-/c32check-2.0.0.tgz", + "integrity": "sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA==", + "dependencies": { + "@noble/hashes": "^1.1.2", + "base-x": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2483,6 +2555,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4321,6 +4401,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4462,6 +4547,25 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", @@ -5607,6 +5711,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -5753,6 +5862,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -5940,6 +6054,20 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/example/package.json b/example/package.json index 13dc42e..46d32a8 100644 --- a/example/package.json +++ b/example/package.json @@ -16,12 +16,14 @@ "prepare": "cd .. && npm i && npm run build && cd example" }, "dependencies": { + "@stacks/common": "^6.16.0", + "@stacks/transactions": "^6.16.1", "@tanstack/react-query": "^5.50.1", "bip322-js": "^2.0.0", "bitcoinjs-message": "^2.2.0", "eslint-plugin-react": "^7.34.3", - "react-dom": "^18.3.1", "react": "^18.3.1", + "react-dom": "^18.3.1", "sats-connect": "file:..", "styled-components": "6.1.11" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index fb836c4..b434475 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -18,6 +18,7 @@ import { WalletType } from './components/wallet/WalletType'; import { GetAccounts } from './components/bitcoin/GetAccounts'; import { SignMessage } from './components/SignMessage'; import SendInscription from './components/sendInscriptions'; +import SignTransaction from './components/signTransaction'; function AppWithProviders() { const queryClient = useQueryClient(); @@ -27,12 +28,8 @@ function AppWithProviders() { ); const [btcAddressInfo, setBtcAddressInfo] = useLocalStorage('btc-addresses', []); const [stxAddressInfo, setStxAddressInfo] = useLocalStorage('stx-addresses', []); - const [legacyAddressInfo, setLegacyAddressInfo] = useLocalStorage( - 'legacy-addresses', - [] - ); - const isConnected = btcAddressInfo.length + stxAddressInfo.length + legacyAddressInfo.length > 0; + const isConnected = btcAddressInfo.length + stxAddressInfo.length > 0; const onConnectLegacy = useCallback(() => { (async () => { @@ -41,10 +38,11 @@ function AppWithProviders() { message: 'Cool app wants to know your addresses!', }); if (response.status === 'success') { - setLegacyAddressInfo(response.result); + setBtcAddressInfo([response.result[0], response.result[1]]); + if (response.result[2]) setStxAddressInfo([response.result[2]]); } })().catch(console.error); - }, [setLegacyAddressInfo]); + }, [setBtcAddressInfo, setStxAddressInfo]); const onConnect = useCallback(() => { (async () => { @@ -85,10 +83,9 @@ function AppWithProviders() { await Wallet.disconnect(); setBtcAddressInfo([]); setStxAddressInfo([]); - setLegacyAddressInfo([]); queryClient.clear(); })().catch(console.error); - }, [queryClient, setBtcAddressInfo, setLegacyAddressInfo, setStxAddressInfo]); + }, [queryClient, setBtcAddressInfo, setStxAddressInfo]); if (!isConnected) { return ( @@ -114,18 +111,21 @@ function AppWithProviders() { - + + {stxAddressInfo?.[0]?.publicKey ? ( + + ) : null} - - + + diff --git a/example/src/components/signTransaction/contractCode.ts b/example/src/components/signTransaction/contractCode.ts new file mode 100644 index 0000000..19c98fa --- /dev/null +++ b/example/src/components/signTransaction/contractCode.ts @@ -0,0 +1,13 @@ +export const code = ` +(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)) +) +`; diff --git a/example/src/components/signTransaction/index.tsx b/example/src/components/signTransaction/index.tsx new file mode 100644 index 0000000..deb7da9 --- /dev/null +++ b/example/src/components/signTransaction/index.tsx @@ -0,0 +1,191 @@ +import { + makeUnsignedContractCall, + makeUnsignedContractDeploy, + makeUnsignedSTXTokenTransfer, + uintCV, +} from '@stacks/transactions'; +import { BitcoinNetworkType, request } from 'sats-connect'; +import { code } from './contractCode'; +import { bytesToHex } from '@stacks/common'; +import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import { Button, H4, Card } from '../../App.styles'; + +interface Props { + network: BitcoinNetworkType; + publicKey: string; +} + +const errorMessage = 'Error signing transaction. Check console for error logs.'; + +function SignTransaction(props: Props) { + const [broadcast, setBroadcast] = useState(true); + + const contractCallMutation = useMutation({ + mutationKey: ['stx_signTransaction', 'contract-call'], + mutationFn: async () => { + const transaction = await makeUnsignedContractCall({ + fee: 3000, + anchorMode: 'onChainOnly', + contractAddress: 'SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP', + contractName: 'pox-fast-pool-v2', + functionName: 'set-stx-buffer', + functionArgs: [uintCV(1)], + publicKey: props.publicKey, + }); + + const response = await request('stx_signTransaction', { + transaction: bytesToHex(transaction.serialize()), + broadcast, + }); + + if (response.status === 'error') { + throw new Error('Error signing transaction', { cause: response.error }); + } + + return response.result; + }, + }); + + const tokenTransferMutation = useMutation({ + mutationKey: ['stx_signTransaction', 'token-transfer'], + mutationFn: async () => { + const transaction = await makeUnsignedSTXTokenTransfer({ + anchorMode: 'any', + fee: 3000, + recipient: 'SP2FFKDKR122BZWS7GDPFWC0J0FK4WMW5NPQ0Z21M', // account 4 + amount: 1000, + publicKey: props.publicKey, + }); + + const response = await request('stx_signTransaction', { + transaction: bytesToHex(transaction.serialize()), + broadcast, + }); + + if (response.status === 'error') { + throw new Error('Error signing transaction', { cause: response.error }); + } + + return response.result; + }, + }); + + const contractDeployMutation = useMutation({ + mutationKey: ['stx_signTransaction', 'contract-deploy'], + mutationFn: async () => { + const transaction = await makeUnsignedContractDeploy({ + anchorMode: 'any', + contractName: 'my-contract', + codeBody: code, + fee: 3000, + publicKey: props.publicKey, + }); + + const response = await request('stx_signTransaction', { + transaction: bytesToHex(transaction.serialize()), + broadcast, + }); + + if (response.status === 'error') { + throw new Error('Error signing transaction', { cause: response.error }); + } + + return response.result; + }, + }); + + return ( + +

Sign transaction

+
+
+ +
+
+
+ +
+
+ {(() => { + if (contractCallMutation.isPending) { + return

Loading...

; + } + + if (contractCallMutation.isError) { + console.error(contractCallMutation.error); + return

{errorMessage}

; + } + + if (contractCallMutation.isSuccess) { + console.log('Signed transaction:', contractCallMutation.data); + return

Transaction signed successfully. Check console for details.

; + } + })()} +
+
+
+
+ +
+
+ {(() => { + if (tokenTransferMutation.isPending) { + return

Loading...

; + } + + if (tokenTransferMutation.isError) { + console.error(tokenTransferMutation.error); + return

{errorMessage}

; + } + + if (tokenTransferMutation.isSuccess) { + console.log(tokenTransferMutation.data); + return

Transaction signed successfully. Check console for details.

; + } + })()} +
+
+
+
+ +
+
+ {(() => { + if (contractDeployMutation.isPending) { + return

Loading...

; + } + + if (contractDeployMutation.isError) { + console.error(contractDeployMutation.error); + return

{errorMessage}

; + } + + if (contractDeployMutation.isSuccess) { + console.log(contractDeployMutation.data); + return

Transaction signed successfully. Check console for details.

; + } + })()} +
+
+
+
+ ); +} + +export default SignTransaction;