├── README.md ├── brc20-market-demo ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── components │ │ ├── ApiKeyInput.tsx │ │ ├── Brc20Item.tsx │ │ ├── BuyConfirmAlert.tsx │ │ ├── ConnectWallet.tsx │ │ ├── InscribeTransfer.tsx │ │ └── NetworkSwitch.tsx │ ├── index.tsx │ ├── page │ │ ├── Assets.tsx │ │ └── Listed.tsx │ ├── provider │ │ ├── MarketProvider.tsx │ │ ├── NetworkProvider.tsx │ │ └── UniSatProvider.tsx │ ├── react-app-env.d.ts │ ├── style │ │ ├── App.css │ │ └── index.css │ ├── types.d.ts │ └── utils │ │ ├── brc20Api.ts │ │ ├── httpUtils.ts │ │ ├── marketApi.ts │ │ ├── networkUtils.ts │ │ ├── unisatUtils.ts │ │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── brc20-swap-demo ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── components │ │ ├── ApiKeyInput.tsx │ │ ├── ConfirmSwap.tsx │ │ ├── CreatePools.tsx │ │ ├── Deposit.tsx │ │ └── Swap.tsx │ ├── constants.ts │ ├── index.tsx │ ├── logo.svg │ ├── provider │ │ └── UniSatProvider.tsx │ ├── styles │ │ ├── App.css │ │ └── index.css │ ├── types.d.ts │ └── utils │ │ ├── brc20Api.ts │ │ ├── httpUtils.ts │ │ ├── swapApi.ts │ │ ├── unisatUtils.ts │ │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── unisat-inscribe-demo ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── components │ │ ├── ApiKeyInput.tsx │ │ ├── FeeDetail.tsx │ │ ├── InscribeFileInput.tsx │ │ ├── InscribeTextInput.tsx │ │ ├── NetworkSwitch.tsx │ │ ├── OrderDetail.tsx │ │ ├── OrderFiles.tsx │ │ ├── OrderInscribing.tsx │ │ ├── OrderPay.tsx │ │ ├── OrderStatus.tsx │ │ └── SimpleRow.tsx │ ├── index.tsx │ ├── logo.svg │ ├── pages │ │ ├── Inscribe.tsx │ │ └── OrderList.tsx │ ├── provider │ │ └── UniSatProvider.tsx │ ├── react-app-env.d.ts │ ├── style │ │ ├── App.css │ │ └── index.css │ ├── types.ts │ └── utils │ │ ├── api-types.ts │ │ ├── api.ts │ │ ├── httpUtils.ts │ │ ├── orderUtils.ts │ │ ├── unisatUtils.ts │ │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── unisat-web3-demo ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts └── tsconfig.json └── wallet-sdk-examples ├── .babelrc.js ├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── example ├── brc20-5byte-mint.ts ├── create-bid.ts ├── create-put-on.ts ├── mempool-api │ └── index.ts ├── open-api │ ├── index.ts │ └── types.ts ├── send-btc.ts └── send-inscription.ts ├── gulpfile.js ├── package.json ├── tsconfig.json └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | ## UniSat Developer Service 2 | 3 | UniSat Developer Service is open to community developers, allowing you to explore the world of Bitcoin and Ordinals. You can deploy your own inscribing services, build wallet applications, develop browsers, and much more using the API. 4 | 5 | - [Dev Documentation](https://docs.unisat.io/dev/unisat-developer-service): OpenAPI, Game Framework, etc... 6 | - [**Getting an API Key**](https://docs.unisat.io/dev/unisat-developer-service#getting-an-api-key) 7 | - [Dev Support Telegram](https://t.me/+w3I7K-OLj4JmODM1) 8 | 9 | ### Interactive Examples for UniSat APIs 10 | 11 | #### UniSat Marketplace API Demo 12 | 13 | - [Documentation](https://docs.unisat.io/dev/unisat-developer-service/unisat-marketplace) 14 | - [Demo](https://demo-market.unisat.io) 15 | - [Source](./brc20-market-demo) 16 | 17 | #### UniSat Inscribe API Demo 18 | 19 | - [Documentation](https://docs.unisat.io/dev/unisat-developer-service/unisat-inscribe) 20 | - [Demo](https://demo-inscribe.unisat.io) 21 | + [Source](./unisat-inscribe-demo) 22 | 23 | #### UniSat Wallet API Demo 24 | 25 | - [Documentation](https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet) 26 | - [Demo](https://demo.unisat.io) 27 | + [Source](./unisat-web3-demo) 28 | 29 | #### brc20-swap API Demo 30 | 31 | - [Documentation](https://docs.unisat.io/dev/unisat-developer-service/brc20-swap) 32 | - [Demo](https://demo-swap.unisat.io) 33 | + [Source](./brc20-swap-demo) 34 | 35 | -------------------------------------------------------------------------------- /brc20-market-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /brc20-market-demo/README.md: -------------------------------------------------------------------------------- 1 | ## UniSat Marketplace API Demo 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ### Run the demo 6 | 7 | #### Install dependencies 8 | 9 | ```bash 10 | yarn 11 | ``` 12 | 13 | #### Run the app 14 | 15 | ```bash 16 | yarn start 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /brc20-market-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brc20-market-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^27.0.1", 7 | "@types/node": "^16.7.13", 8 | "@types/react": "^18.0.0", 9 | "@types/react-dom": "^18.0.0", 10 | "ahooks": "^3.7.10", 11 | "antd": "^5.13.3", 12 | "axios": "^1.6.7", 13 | "buffer": "^6.0.3", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "typescript": "^4.4.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /brc20-market-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-market-demo/public/favicon.ico -------------------------------------------------------------------------------- /brc20-market-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /brc20-market-demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-market-demo/public/logo192.png -------------------------------------------------------------------------------- /brc20-market-demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-market-demo/public/logo512.png -------------------------------------------------------------------------------- /brc20-market-demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /brc20-market-demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /brc20-market-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style/App.css'; 3 | import {ApiKeyInput} from "./components/ApiKeyInput"; 4 | import {NetworkSwitch} from "./components/NetworkSwitch"; 5 | import {Card, Input, Layout, Space, Tabs, Typography} from "antd"; 6 | import {ConnectWallet} from "./components/ConnectWallet"; 7 | import {Assets} from "./page/Assets"; 8 | import {useMarket} from "./provider/MarketProvider"; 9 | import {isTicketValid} from "./utils/utils"; 10 | import {Listed} from "./page/Listed"; 11 | import {useLocalStorageState} from "ahooks"; 12 | 13 | function App() { 14 | const {tick, setTick} = useMarket(); 15 | 16 | 17 | const [apiKey, setApiKey] = useLocalStorageState('apiKey', 18 | {defaultValue: '', deserializer: (val) => val, serializer: (val) => val}) 19 | 20 | 21 | return ( 22 | 23 | 24 |
31 | 32 | 33 | 34 | { 35 | apiKey?<> 36 | 37 | }, 41 | {key: "2", label: "Assets", children: }, 42 | ]} 43 | tabBarExtraContent={{ 44 | right: 45 | Tick 46 | setTick(e.target.value)} 50 | status={isTicketValid(tick) ? '' : 'error'} 51 | /> 52 | 53 | }} 54 | /> 55 | 56 | : <> 57 | 58 | Please set API key to continue 59 | 60 | 61 | } 62 | 63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /brc20-market-demo/src/components/ApiKeyInput.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Input} from "antd"; 2 | import React from "react"; 3 | import {setApiKey} from "../utils/httpUtils"; 4 | 5 | export function ApiKeyInput({apiKey,setKey}:{ 6 | apiKey: string |undefined, 7 | setKey: (apiKey: string) => void 8 | }) { 9 | 10 | return 11 |
12 | Set API key: 13 |
14 |
15 | { 19 | const key = e.target.value; 20 | setKey(key); 21 | setApiKey(key); 22 | }} 23 | /> 24 |
25 |
26 |
27 |
28 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/components/Brc20Item.tsx: -------------------------------------------------------------------------------- 1 | import {Space, Statistic} from "antd"; 2 | 3 | export function Brc20Item({tick, amount}: { 4 | tick: string, 5 | amount: string | number, 6 | }) { 7 | return 14 | 18 | 19 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/components/BuyConfirmAlert.tsx: -------------------------------------------------------------------------------- 1 | import {BRC20ListItem, CreateBidPrepareRes, marketApi} from "../utils/marketApi"; 2 | import {Col, Flex, message, Modal, Row, Skeleton, Statistic} from "antd"; 3 | import {Brc20Item} from "./Brc20Item"; 4 | import {useEffect, useState} from "react"; 5 | import {useUnisat} from "../provider/UniSatProvider"; 6 | import {handleError} from "../utils/utils"; 7 | 8 | 9 | export function BuyConfirmAlert({item, close, onComplete}: { 10 | item: BRC20ListItem, 11 | close: () => void, 12 | onComplete?: () => void 13 | }) { 14 | const {address, pubkey, signPsbt} = useUnisat(); 15 | const [messageApi] = message.useMessage(); 16 | 17 | const [isLoading, setIsLoading] = useState(false); 18 | const [bidPrepare, setBidPrepare] = useState(); 19 | 20 | useEffect(() => { 21 | if (!address || !pubkey || !item) 22 | return; 23 | // create bid prepare 24 | marketApi.createBidPrepare({ 25 | auctionId: item.auctionId, 26 | bidPrice: item.price, 27 | address, 28 | pubkey 29 | }).then(setBidPrepare).catch(handleError) 30 | 31 | }, [address, item, pubkey]); 32 | 33 | async function buy() { 34 | try { 35 | setIsLoading(true) 36 | 37 | // create bid to get bidId/psbtBid 38 | const {bidId, psbtBid} = await marketApi.createBid({ 39 | auctionId: item.auctionId, 40 | bidPrice: item.price, 41 | address, 42 | pubkey, 43 | }) 44 | 45 | // sign psbt 46 | const signedPsbt = await signPsbt(psbtBid) 47 | 48 | // confirm 49 | await marketApi.confirmBid({ 50 | auctionId: item.auctionId, 51 | bidId, 52 | psbtBid: signedPsbt, 53 | psbtBid2: '', 54 | psbtSettle: '', 55 | }) 56 | 57 | messageApi.success('Buy Success'); 58 | 59 | onComplete && onComplete() 60 | } catch (e) { 61 | handleError(e) 62 | } finally { 63 | setIsLoading(false) 64 | } 65 | } 66 | 67 | return 74 | { 75 | item && <> 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | { 86 | !bidPrepare ? : <> 87 | 88 | 91 | 92 | 93 | } 94 | 95 | 96 | 97 | 98 | } 99 | 100 | 101 | } 102 | 103 | function KeyValue({label, value, suffix, fontSize = 14, valueColor}: { 104 | label: string, 105 | value: string | number, 106 | suffix?: string, 107 | fontSize?: number, 108 | valueColor?: string 109 | }) { 110 | return <> 111 | 112 | {label} 113 | 114 | 115 | 119 | 120 | 121 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/components/ConnectWallet.tsx: -------------------------------------------------------------------------------- 1 | import {useUnisat} from "../provider/UniSatProvider"; 2 | import {Button, Card} from "antd"; 3 | 4 | export function ConnectWallet() { 5 | const {address, connect} = useUnisat(); 6 | 7 | return 8 | { 9 | address 10 | ?
Address: {address}
11 | : 12 | } 13 |
14 | 15 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/components/InscribeTransfer.tsx: -------------------------------------------------------------------------------- 1 | import {handleError, sleep} from "../utils/utils"; 2 | import {unisatUtils} from "../utils/unisatUtils"; 3 | import {Button} from "antd"; 4 | 5 | export function InscribeTransfer({tick, onInscribed}: { tick: string, onInscribed: () => void }) { 6 | 7 | 8 | async function inscribeTransfer() { 9 | try { 10 | await unisatUtils.inscribeTransfer(tick) 11 | await sleep(5000) 12 | onInscribed() 13 | } catch (e) { 14 | handleError(e) 15 | } 16 | } 17 | 18 | return 21 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/components/NetworkSwitch.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Radio} from "antd"; 2 | import {NetworkType, networkUtils} from "../utils/networkUtils"; 3 | import {useNetwork} from "../provider/NetworkProvider"; 4 | 5 | 6 | export function NetworkSwitch() { 7 | const {network, setNetwork} = useNetwork() 8 | return 9 | { 10 | networkUtils.setNetworkType(v.target.value) 11 | setNetwork(v.target.value) 12 | }} defaultValue={network}> 13 | Testnet 14 | Mainnet 15 | 16 | 17 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './style/index.css'; 4 | import App from './App'; 5 | import {ConfigProvider} from "antd"; 6 | import NetworkProvider from "./provider/NetworkProvider"; 7 | import UnisatProvider from "./provider/UniSatProvider"; 8 | import MarketProvider from "./provider/MarketProvider"; 9 | 10 | const root = ReactDOM.createRoot( 11 | document.getElementById('root') as HTMLElement 12 | ); 13 | root.render( 14 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /brc20-market-demo/src/page/Assets.tsx: -------------------------------------------------------------------------------- 1 | import {useUnisat} from "../provider/UniSatProvider"; 2 | import {useCallback, useEffect, useMemo, useState} from "react"; 3 | import {brc20Api, Brc20InscriptionsItem} from "../utils/brc20Api"; 4 | import {handleError, isTicketValid, sleep} from "../utils/utils"; 5 | import {useMarket} from "../provider/MarketProvider"; 6 | import {Button, Col, Flex, InputNumber, message, Modal, Row, Skeleton, Space} from "antd"; 7 | import {Brc20Item} from "../components/Brc20Item"; 8 | import {InscribeTransfer} from "../components/InscribeTransfer"; 9 | import {InscriptionType, ListItem, marketApi} from "../utils/marketApi"; 10 | import {unisatUtils} from "../utils/unisatUtils"; 11 | 12 | export function Assets() { 13 | 14 | const {address, signPsbt} = useUnisat(); 15 | const {tick} = useMarket(); 16 | 17 | const [list, setList] = useState(); 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | const [putOnItem, setPutOnItem] = useState(); 21 | const [unitPrice, setUnitPrice] = useState(''); 22 | 23 | const [listedMap, setListedMap] = useState<{ [inscriptionId: string]: ListItem }>({}); 24 | 25 | const refreshTransfer = useCallback(() => { 26 | if (address && tick && isTicketValid(tick)) { 27 | //got transfer-inscription from the user 28 | setList(undefined); 29 | brc20Api.getAddressTransferable({ 30 | address, 31 | tick, 32 | start: 0, 33 | limit: 200, 34 | }).then(res => { 35 | setList(res.detail) 36 | }).catch(handleError) 37 | } else { 38 | setList([]) 39 | } 40 | }, [address, tick]); 41 | 42 | useEffect(() => { 43 | refreshTransfer() 44 | }, [refreshTransfer]); 45 | 46 | const refreshListed = useCallback(() => { 47 | setListedMap({}) 48 | if (tick && isTicketValid(tick) && list) { 49 | // get listed to show unlist button 50 | marketApi.listBrc20({ 51 | filter: {tick, nftType: InscriptionType.brc20, address, isEnd: false}, 52 | sort: {unitPrice: 1}, 53 | start: 0, 54 | limit: 99, 55 | }).then(res => { 56 | setListedMap(res.list.reduce((acc: any, cur) => { 57 | acc[cur.inscriptionId] = cur; 58 | return acc; 59 | }, {})) 60 | }).catch(handleError) 61 | } 62 | }, [address, list, tick]); 63 | 64 | useEffect(() => { 65 | refreshListed() 66 | }, [refreshListed]); 67 | 68 | 69 | const totalPrice = useMemo(() => { 70 | if (!unitPrice) return ''; 71 | if (!putOnItem) return '0'; 72 | return (Number(putOnItem.data.amt) * Number(unitPrice)).toFixed(0); 73 | }, [putOnItem, unitPrice]); 74 | 75 | 76 | async function putOn() { 77 | try { 78 | if (!putOnItem) return; 79 | setIsLoading(true) 80 | 81 | const pubkey = await unisatUtils.getPublicKey(); 82 | 83 | const {auctionId, psbt} = await marketApi.createPutOn({ 84 | inscriptionId: putOnItem.inscriptionId, 85 | nftType: InscriptionType.brc20, 86 | initPrice: totalPrice, 87 | marketType: 'fixedPrice', 88 | pubkey, 89 | unitPrice, 90 | }) 91 | 92 | const signedPsbt = await signPsbt(psbt) 93 | 94 | await marketApi.confirmPutOn({ 95 | auctionId, 96 | psbt: signedPsbt, 97 | }) 98 | 99 | message.success('Put on success'); 100 | 101 | setPutOnItem(undefined); 102 | setUnitPrice(''); 103 | 104 | await sleep(1000); 105 | refreshListed(); 106 | 107 | } catch (e) { 108 | handleError(e) 109 | } finally { 110 | setIsLoading(false) 111 | } 112 | } 113 | 114 | async function putOff(item: ListItem) { 115 | try { 116 | setIsLoading(true) 117 | 118 | const {psbt} = await marketApi.createPutOff({ 119 | auctionId: item.auctionId, 120 | }) 121 | 122 | const signedPsbt = await signPsbt(psbt) 123 | 124 | await marketApi.confirmPutOff({ 125 | auctionId: item.auctionId, 126 | psbt: signedPsbt, 127 | }) 128 | 129 | message.success('Put off success'); 130 | 131 | await sleep(3000) 132 | 133 | refreshTransfer() 134 | } catch (e) { 135 | handleError(e) 136 | } finally { 137 | setIsLoading(false) 138 | } 139 | } 140 | 141 | if (!address) { 142 | return
Wait for Connect
143 | } 144 | 145 | if (!list) { 146 | return 147 | } 148 | 149 | return 150 | 151 |
152 |
153 | Transferable inscriptions: 154 |
155 | 156 | { 157 | list.map((item, index) => { 158 | const listed = listedMap[item.inscriptionId]; 159 | 160 | return 161 | 162 | { 163 | listed 164 | ? 166 | : 167 | } 168 | 169 | }) 170 | } 171 | 172 | { 179 | if (!isLoading) 180 | setPutOnItem(undefined) 181 | }} 182 | > 183 | { 184 | putOnItem && <> 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | Unit Price: 193 | 194 | 195 | { 201 | setUnitPrice(v || '') 202 | }} 203 | addonAfter={`sats(BTC)/${putOnItem?.data.tick}`} 204 | /> 205 | 206 | 207 | 208 | Total Price 209 | 210 | 211 | 218 | 219 | 220 | 221 | } 222 | 223 | 224 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/page/Listed.tsx: -------------------------------------------------------------------------------- 1 | import {useMarket} from "../provider/MarketProvider"; 2 | import {useCallback, useEffect, useState} from "react"; 3 | import {BRC20ListItem, InscriptionType, marketApi} from "../utils/marketApi"; 4 | import {handleError, isTicketValid} from "../utils/utils"; 5 | import {Button, Flex, Pagination, Skeleton, Space, Statistic} from "antd"; 6 | import {BuyConfirmAlert} from "../components/BuyConfirmAlert"; 7 | import {useNetwork} from "../provider/NetworkProvider"; 8 | 9 | const pageSize = 10; 10 | 11 | export function Listed() { 12 | 13 | const {network} = useNetwork(); 14 | const {tick,} = useMarket(); 15 | 16 | const [total, setTotal] = useState(0); // total listed 17 | const [page, setPage] = useState(1); 18 | const [list, setList] = useState(); 19 | 20 | const [buyItem, setBuyItem] = useState(); 21 | 22 | const refreshData = useCallback(() => { 23 | if (tick && isTicketValid(tick)) { 24 | // get listed 25 | setList(undefined) 26 | marketApi.listBrc20({ 27 | filter: { 28 | tick, 29 | nftType: InscriptionType.brc20, 30 | isEnd: false // only show listed 31 | }, 32 | sort: {unitPrice: 1}, 33 | start: (page - 1) * pageSize, 34 | limit: pageSize 35 | }).then(res => { 36 | setTotal(res.total) 37 | setList(res.list) 38 | }).catch(handleError) 39 | } else { 40 | setList([]) 41 | } 42 | }, [page, tick]); 43 | 44 | useEffect(() => { 45 | refreshData() 46 | }, [refreshData, network]); 47 | 48 | 49 | if (!list) return 50 | 51 | return <> 52 | 53 | {list.map(item => { 54 | return 62 | 67 | 73 | 78 | 79 | 80 | })} 81 | 82 | { 88 | setPage(page) 89 | }} 90 | /> 91 | { 92 | buyItem && { 95 | setBuyItem(undefined) 96 | }} 97 | onComplete={refreshData} 98 | /> 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/provider/MarketProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, useContext, useState} from "react"; 2 | 3 | interface MarketContentType { 4 | tick: string; 5 | setTick: (tick: string) => void; 6 | } 7 | 8 | const MarketContext = createContext({ 9 | tick: '', 10 | setTick: () => { 11 | } 12 | }) 13 | 14 | export function useMarket() { 15 | const context = useContext(MarketContext); 16 | if (!context) { 17 | throw Error('Feature flag hooks can only be used by children of MarketProvider.'); 18 | } else { 19 | return context; 20 | } 21 | } 22 | 23 | export default function MarketProvider({children}: { 24 | children: React.ReactNode 25 | }) { 26 | const [tick, setTick] = useState('sats') 27 | 28 | return 32 | {children} 33 | 34 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/provider/NetworkProvider.tsx: -------------------------------------------------------------------------------- 1 | import {NetworkType} from "../utils/networkUtils"; 2 | import {createContext, ReactNode, useContext, useState} from "react"; 3 | 4 | interface NetworkContextType { 5 | network: NetworkType; 6 | setNetwork: (network: NetworkType) => void; 7 | } 8 | 9 | const NetworkContext = createContext({ 10 | network: NetworkType.testnet, 11 | setNetwork: () => {} 12 | }) 13 | 14 | export function useNetwork() { 15 | const context = useContext(NetworkContext); 16 | if (!context) { 17 | throw Error('Feature flag hooks can only be used by children of NetworkProvider.'); 18 | } else { 19 | return context; 20 | } 21 | } 22 | 23 | export default function NetworkProvider({children}: { 24 | children: ReactNode 25 | }) { 26 | const [network, setNetwork] = useState(NetworkType.testnet) 27 | 28 | return 32 | {children} 33 | 34 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/provider/UniSatProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, ReactNode, useCallback, useContext, useEffect, useState} from "react"; 2 | import {handleError, sleep} from "../utils/utils"; 3 | import {unisatUtils} from "../utils/unisatUtils"; 4 | import {useNetwork} from "./NetworkProvider"; 5 | 6 | interface UnisatContextType { 7 | isInstalled: boolean; 8 | isConnected: boolean; 9 | address: string; 10 | pubkey: string; 11 | connect: () => void; 12 | signMessage: (msg: string) => Promise; 13 | signPsbt: (psbt: string) => Promise; 14 | } 15 | 16 | const UnisatContext = createContext({ 17 | isInstalled: false, 18 | isConnected: false, 19 | address: '', 20 | pubkey: '', 21 | connect: () => { 22 | }, 23 | signMessage: (msg: string) => Promise.resolve(''), 24 | signPsbt: (psbt: string) => Promise.resolve('') 25 | }) 26 | 27 | 28 | export function useUnisat() { 29 | const context = useContext(UnisatContext); 30 | if (!context) { 31 | throw Error('Feature flag hooks can only be used by children of UnisatProvider.'); 32 | } else { 33 | return context; 34 | } 35 | } 36 | 37 | export default function UnisatProvider({children}: { 38 | children: ReactNode 39 | }) { 40 | const {network} = useNetwork(); 41 | const [isInstalled, setIsInstalled] = useState(false) 42 | const [isConnected, setIsConnected] = useState(false) 43 | const [address, setAddress] = useState('') 44 | const [pubkey, setPubkey] = useState('') 45 | 46 | useEffect(() => { 47 | 48 | async function init() { 49 | let install = !!window.unisat; 50 | setIsInstalled(install); 51 | 52 | // 额外检查 53 | for (let i = 0; i < 10 && !install; i += 1) { 54 | await sleep(100 + i * 100); 55 | install = !!window.unisat; 56 | if (install) { 57 | setIsInstalled(install); 58 | break; 59 | } 60 | } 61 | 62 | if (install) { 63 | const address = await unisatUtils.getAccounts() 64 | if (address) { 65 | setPubkey(await unisatUtils.getPublicKey()) 66 | // connected 67 | setIsConnected(true) 68 | setAddress(address) 69 | } 70 | } 71 | } 72 | 73 | init().then().catch(handleError); 74 | 75 | }, []); 76 | 77 | const connect = useCallback(async () => { 78 | try { 79 | await unisatUtils.checkNetwork(network); 80 | const address = await unisatUtils.requestAccounts(); 81 | if (address) { 82 | setPubkey(await unisatUtils.getPublicKey()) 83 | setIsConnected(true) 84 | setAddress(address) 85 | } 86 | } catch (e) { 87 | handleError(e) 88 | } 89 | 90 | }, [network]) 91 | 92 | useEffect(() => { 93 | async function onAppNetworkChange() { 94 | try { 95 | await unisatUtils.checkNetwork(network); 96 | const address = await unisatUtils.getAccounts() 97 | if (address) { 98 | setAddress(address) 99 | } 100 | } catch (e) { 101 | handleError(e) 102 | } 103 | } 104 | 105 | if (isConnected) { 106 | onAppNetworkChange().then() 107 | } 108 | }, [isConnected, network]); 109 | 110 | const signMessage = useCallback((msg: string) => { 111 | return unisatUtils.signMessage(msg, 'bip322-simple') 112 | }, []) 113 | 114 | const signPsbt = useCallback((psbt: string) => { 115 | return unisatUtils.signPsbt(psbt) 116 | }, []); 117 | 118 | const value = { 119 | isInstalled, 120 | isConnected, 121 | address, 122 | pubkey, 123 | connect, 124 | signMessage, 125 | signPsbt, 126 | } 127 | 128 | return 129 | {children} 130 | 131 | 132 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /brc20-market-demo/src/style/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /brc20-market-demo/src/style/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | --main-color: #ebb94c; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /brc20-market-demo/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export type UnisatWalletInterface = { 2 | getAccounts(): Promise; 3 | requestAccounts(): Promise; 4 | getNetwork(): Promise; 5 | switchNetwork(network: string): Promise; 6 | sendBitcoin(address: string, amount: number, options: any): Promise; 7 | on(event: string, listener: Function): void; 8 | removeListener(event: string, listener: Function): void; 9 | signMessage(message: string, type?: string): Promise; 10 | signPsbt(psbt: string, opt: { autoFinalized: boolean }): Promise; 11 | getPublicKey(): Promise; 12 | getBalance(): Promise<{ 13 | confirmed: number, 14 | unconfirmed: number, 15 | total: number 16 | }>; 17 | inscribeTransfer(tick: string, amount?: number | string): Promise<{ 18 | amount: string, 19 | inscriptionId: string 20 | inscriptionNumber: number 21 | ticker: string 22 | }>; 23 | getInscriptions(num: number): Promise; 24 | } 25 | 26 | declare global { 27 | interface Window { 28 | unisat: UnisatWalletInterface; 29 | } 30 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/brc20Api.ts: -------------------------------------------------------------------------------- 1 | import {get} from "./httpUtils"; 2 | import {stringToHex} from "./utils"; 3 | 4 | export const brc20Api = { 5 | summary({ 6 | address, limit = 20, start = 0, 7 | }: { address: string, limit?: number, start?: number }): Promise<{ 8 | total: number, 9 | start: number, 10 | height: number, 11 | detail: AddressBrc20Balance[] 12 | }> { 13 | return get(`/v1/indexer/address/${address}/brc20/summary`, {limit, start}) 14 | }, 15 | 16 | getAddressTransferable({ 17 | address, tick, start = 0, limit = 512, 18 | }: { 19 | address: string, 20 | tick: string, 21 | start?: number, 22 | limit?: number 23 | }): Promise { 24 | return get(`/v1/indexer/address/${address}/brc20/${stringToHex(tick)}/transferable-inscriptions`, { 25 | limit, 26 | start, 27 | }) 28 | }, 29 | } 30 | 31 | 32 | export type AddressBrc20Balance = { 33 | ticker: string 34 | overallBalance: string // transferableBalance + availableBalance 35 | transferableBalance: string 36 | availableBalance: string 37 | } 38 | 39 | 40 | export type Brc20Data = { 41 | amt: string, 42 | decimal: string, 43 | lim: string, 44 | max: string, 45 | minted: string, 46 | op: string, 47 | tick: string, 48 | to: string 49 | } 50 | export type Brc20InscriptionsItem = { 51 | data: Brc20Data, 52 | inscriptionId: string, 53 | inscriptionNumber: number, 54 | confirmations: number, 55 | isPutOn?: boolean, 56 | }; 57 | 58 | export type Brc20AddressTransferable = { 59 | start: number, 60 | total: number, 61 | detail: Brc20InscriptionsItem[] 62 | }; -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/httpUtils.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {networkUtils} from "./networkUtils"; 3 | 4 | let apiKey = localStorage.getItem('apiKey') || ''; 5 | 6 | export function setApiKey(key: string) { 7 | apiKey = key; 8 | } 9 | 10 | function createApi(baseURL: string) { 11 | const api = axios.create({ 12 | baseURL, 13 | timeout: 10000, 14 | headers: { 15 | "Content-Type": "application/json", 16 | }, 17 | }) 18 | 19 | api.interceptors.request.use((config) => { 20 | if (!apiKey) { 21 | throw new Error("input apiKey and reload page"); 22 | } 23 | config.headers.Authorization = `Bearer ${apiKey}`; 24 | return config; 25 | }); 26 | return api; 27 | } 28 | 29 | const mainnetApi = createApi("https://open-api.unisat.io"); 30 | const testnetApi = createApi("https://open-api-testnet.unisat.io"); 31 | 32 | function getApi() { 33 | return networkUtils.isTestnet() ? testnetApi : mainnetApi; 34 | } 35 | 36 | 37 | export const get = async (url: string, params?: any) => { 38 | const res = await getApi().get(url, {params}); 39 | if (res.status !== 200) { 40 | throw new Error(res.statusText); 41 | } 42 | 43 | const responseData = res.data; 44 | 45 | if (responseData.code !== 0) { 46 | throw new Error(responseData.msg); 47 | } 48 | return responseData.data; 49 | }; 50 | 51 | export const post = async (url: string, data?: any) => { 52 | const res = await getApi().post(url, data,); 53 | if (res.status !== 200) { 54 | throw new Error(res.statusText); 55 | } 56 | 57 | const responseData = res.data; 58 | 59 | if (responseData.code !== 0) { 60 | throw new Error(responseData.msg); 61 | } 62 | 63 | return responseData.data; 64 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/marketApi.ts: -------------------------------------------------------------------------------- 1 | import {post} from "./httpUtils"; 2 | 3 | export const marketApi = { 4 | listBrc20(req: ListReq): Promise { 5 | return post('/v3/market/brc20/auction/list', req,) 6 | }, 7 | 8 | createPutOn(req: CreatePutOnReq): Promise { 9 | return post('/v3/market/brc20/auction/create_put_on', req); 10 | }, 11 | confirmPutOn(req: ConfirmPutOnReq): Promise { 12 | return post('/v3/market/brc20/auction/confirm_put_on', req); 13 | }, 14 | 15 | createBidPrepare(req: CreateBidPrepareReq): Promise { 16 | return post('/v3/market/brc20/auction/create_bid_prepare', req); 17 | }, 18 | createBid(req: CreateBidReq): Promise { 19 | return post('/v3/market/brc20/auction/create_bid', req); 20 | }, 21 | confirmBid(req: ConfirmBidReq): Promise { 22 | return post('/v3/market/brc20/auction/confirm_bid', req); 23 | }, 24 | createPutOff(req: CreatePutOffReq): Promise { 25 | return post('/v3/market/brc20/auction/create_put_off', req); 26 | }, 27 | confirmPutOff(req: ConfirmPutOffReq): Promise { 28 | return post('/v3/market/brc20/auction/confirm_put_off', req); 29 | }, 30 | } 31 | 32 | 33 | export enum InscriptionType { 34 | brc20 = "brc20", 35 | domain = "domain", 36 | collection = "collection", 37 | arc20 = "arc20", 38 | } 39 | 40 | 41 | export type ListReqFilter = { 42 | address?: string; 43 | nftConfirm?: boolean; 44 | minPrice?: number; 45 | maxPrice?: number; 46 | isEnd?: boolean; 47 | 48 | nftType?: InscriptionType; 49 | 50 | // brc20 51 | tick?: string; 52 | } 53 | 54 | export type ListReq = { 55 | filter: ListReqFilter; 56 | sort: { 57 | unitPrice?: -1 | 1; 58 | onSaleTime?: -1 | 1; 59 | }; 60 | start: number; 61 | limit: number; 62 | }; 63 | 64 | 65 | export type ListItem = { 66 | auctionId: string; 67 | inscriptionId: string; 68 | inscriptionName: string; 69 | inscriptionNumber: number; 70 | marketType: 'fixedPrice'; 71 | nftType: InscriptionType; 72 | initPrice: number; 73 | curPrice: number; 74 | minBidPrice: number; 75 | endTime: number; 76 | address: string; 77 | onSaleTime: number; 78 | price: number; 79 | 80 | }; 81 | 82 | export type BRC20ListItem = ListItem & { 83 | tick: string; 84 | limit: number; 85 | amount: number; 86 | unitPrice: number; 87 | }; 88 | 89 | export type BRC20ListRes = { 90 | list: BRC20ListItem[]; 91 | total: number; 92 | }; 93 | 94 | 95 | export type CreatePutOnReq = { 96 | inscriptionId: string; 97 | initPrice: string; 98 | pubkey: string; 99 | marketType: 'fixedPrice'; 100 | auctionTime?: number; 101 | maxPrice?: number; 102 | unitPrice?: string; 103 | btcAddress?: string; 104 | nftType: InscriptionType; 105 | }; 106 | 107 | export type CreatePutOnRes = { 108 | auctionId: string; 109 | psbt: string; 110 | signIndexes: number[]; 111 | }; 112 | 113 | export type ConfirmPutOnReq = { 114 | auctionId: string; 115 | psbt: string; 116 | fromBase64?: boolean; 117 | }; 118 | 119 | export type ConfirmPutOnRes = {}; 120 | 121 | 122 | export type CreateBidPrepareReq = { 123 | auctionId: string; 124 | bidPrice: number; 125 | address: string; 126 | pubkey: string; 127 | }; 128 | 129 | export type CreateBidPrepareRes = { 130 | serverFee: number; 131 | serverReal: number; 132 | serverFeeRate: number; 133 | txSize: number; 134 | nftValue: number; 135 | feeRate: number; 136 | discounts: { 137 | name: string; 138 | percent: number; 139 | }[]; 140 | inscriptionCount: number; 141 | availableBalance: number; 142 | }; 143 | 144 | 145 | export type CreateBidReq = { 146 | auctionId: string; 147 | bidPrice: number; 148 | address: string; 149 | pubkey: string; 150 | feeRate?: number; 151 | nftAddress?: string; 152 | sign?: string; 153 | }; 154 | 155 | 156 | export type CreateBidRes = { 157 | bidId: string; 158 | psbtBid: string; 159 | psbtBid2: string; 160 | psbtSettle: string; 161 | 162 | networkFee: number; 163 | feeRate: number; 164 | serverFee: number; 165 | nftValue: number; 166 | 167 | bidSignIndexes: number[]; 168 | 169 | }; 170 | 171 | export type ConfirmBidReq = { 172 | auctionId: string; 173 | bidId: string; 174 | psbtBid: string; 175 | psbtBid2: string; 176 | psbtSettle: string; 177 | fromBase64?: boolean; 178 | 179 | }; 180 | 181 | export type ConfirmBidRes = { 182 | txid: string; 183 | }; 184 | 185 | 186 | export type CreatePutOffReq = { 187 | auctionId: string; 188 | btcPubkey?: string; 189 | nftAddress?: string; 190 | }; 191 | 192 | export type CreatePutOffRes = { 193 | psbt: string; 194 | btcSignIndexes: number[]; 195 | nftSignIndexes: number[]; 196 | }; 197 | 198 | export type ConfirmPutOffReq = { 199 | auctionId: string; 200 | psbt: string; 201 | fromBase64?: boolean; 202 | }; 203 | 204 | export type ConfirmPutOffRes = { 205 | txid: string; 206 | }; 207 | 208 | -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/networkUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum NetworkType { 3 | livenet = 'livenet', 4 | testnet = 'testnet', 5 | } 6 | 7 | class NetworkUtils { 8 | private network: NetworkType = NetworkType.testnet; 9 | setNetworkType(type: NetworkType) { 10 | this.network = type; 11 | } 12 | isTestnet() { 13 | return this.network === NetworkType.testnet; 14 | } 15 | } 16 | 17 | export const networkUtils = new NetworkUtils(); 18 | 19 | -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/unisatUtils.ts: -------------------------------------------------------------------------------- 1 | import {handleError} from "./utils"; 2 | 3 | export const unisatUtils = { 4 | async getAccounts() { 5 | try { 6 | const accounts = await window.unisat.getAccounts(); 7 | if (accounts && accounts.length > 0) { 8 | return accounts[0]; 9 | } 10 | } catch (e: any) { 11 | handleError(e); 12 | } 13 | return ''; 14 | }, 15 | async requestAccounts() { 16 | try { 17 | const accounts = await window.unisat.requestAccounts(); 18 | if (accounts && accounts.length > 0) { 19 | return accounts[0]; 20 | } 21 | } catch (e: any) { 22 | handleError(e); 23 | } 24 | return ''; 25 | }, 26 | signMessage(message: string, type?: string) { 27 | return window.unisat.signMessage(message, type); 28 | }, 29 | signPsbt(psbt: string) { 30 | return window.unisat.signPsbt(psbt, {autoFinalized: false}); 31 | }, 32 | getNetwork(): Promise { 33 | return window.unisat.getNetwork(); 34 | }, 35 | switchNetwork(network: string): Promise { 36 | return window.unisat.switchNetwork(network); 37 | }, 38 | 39 | async checkNetwork(network: string): Promise { 40 | if (network !== await this.getNetwork()) { 41 | await this.switchNetwork(network); 42 | } 43 | }, 44 | inscribeTransfer(tick: string, amount?: number | string) { 45 | return window.unisat.inscribeTransfer(tick, amount) 46 | }, 47 | getPublicKey() { 48 | return window.unisat.getPublicKey(); 49 | }, 50 | 51 | } -------------------------------------------------------------------------------- /brc20-market-demo/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from "buffer"; 2 | import {message} from "antd"; 3 | 4 | export function sleep(ms: number) { 5 | return new Promise((resolve) => setTimeout(resolve, ms)); 6 | } 7 | 8 | export function stringToHex(stringToEncode: string) { 9 | return Buffer.from(stringToEncode).toString('hex') 10 | } 11 | 12 | export function isTicketValid(ticket: string) { 13 | return Buffer.from(ticket).length === 4 14 | } 15 | 16 | export function handleError(e: any) { 17 | message.error((e && e.message) || e) 18 | } -------------------------------------------------------------------------------- /brc20-market-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /brc20-swap-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /brc20-swap-demo/README.md: -------------------------------------------------------------------------------- 1 | ## brc20-swap Demo 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ### Run the demo 6 | 7 | #### Install dependencies 8 | 9 | ```bash 10 | yarn 11 | ``` 12 | 13 | #### Run the app 14 | 15 | ```bash 16 | yarn start 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /brc20-swap-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brc20-swap-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^5.2.6", 7 | "antd": "^5.13.2", 8 | "axios": "^1.6.5", 9 | "buffer": "^6.0.3", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0" 12 | }, 13 | "devDependencies": { 14 | "@testing-library/jest-dom": "^5.14.1", 15 | "@testing-library/react": "^13.0.0", 16 | "@testing-library/user-event": "^13.2.1", 17 | "@types/jest": "^27.0.1", 18 | "@types/node": "^16.7.13", 19 | "@types/react": "^18.0.0", 20 | "@types/react-dom": "^18.0.0", 21 | "react-scripts": "5.0.1", 22 | "typescript": "^4.4.2" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /brc20-swap-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-swap-demo/public/favicon.ico -------------------------------------------------------------------------------- /brc20-swap-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /brc20-swap-demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-swap-demo/public/logo192.png -------------------------------------------------------------------------------- /brc20-swap-demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/brc20-swap-demo/public/logo512.png -------------------------------------------------------------------------------- /brc20-swap-demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /brc20-swap-demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles/App.css'; 3 | import {Card, Tabs} from "antd"; 4 | import {Swap} from "./components/Swap"; 5 | import {ApiKeyInput} from "./components/ApiKeyInput"; 6 | import {Deposit} from "./components/Deposit"; 7 | 8 | function App() { 9 | return ( 10 |
11 |
12 | 13 | 14 |
    15 |
  1. Input your Api Key and refresh page
  2. 16 |
  3. Connect UniSat wallet.
  4. 17 |
  5. Click 'Deposit' tab, click 'Mint sats' to mint some sats.(get tFB) 19 |
  6. 20 |
  7. Choose 'sats' and click 'inscribe transfer' to create transfer-inscription
  8. 21 |
  9. Choose one transfer-inscription to deposit(it may need some confirmations).
  10. 22 |
  11. Click 'swap' tab, input your pay sats then click swap
  12. 23 |
24 |
25 | 26 | 27 | }, 32 | {key: "2", label: "Deposit", children: }, 33 | // {key: "3", label: "Pools", children: }, 34 | ]} 35 | /> 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/components/ApiKeyInput.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Input} from "antd"; 2 | import React, {useEffect, useState} from "react"; 3 | import {setApiKey} from "../utils/httpUtils"; 4 | 5 | export function ApiKeyInput() { 6 | const [key, setKey] = useState("") 7 | 8 | useEffect(() => { 9 | const temp = localStorage.getItem("apiKey") || ""; 10 | if (temp) { 11 | setKey(temp) 12 | setApiKey(temp) 13 | } 14 | }, []); 15 | 16 | return 17 |
18 | Set API key: 19 |
20 |
21 | { 25 | const key = e.target.value; 26 | setKey(key); 27 | setApiKey(key); 28 | localStorage.setItem("apiKey", key); 29 | }} 30 | /> 31 |
32 |
33 |
34 |
35 | } 36 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/components/ConfirmSwap.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import {Col, message, Modal, Row, Skeleton, Statistic} from "antd"; 3 | import {ArrowDownOutlined} from "@ant-design/icons"; 4 | import {ExactType, PreRes, swapApi, SwapReq} from "../utils/swapApi"; 5 | import {useUnisat} from "../provider/UniSatProvider"; 6 | 7 | type ConfirmSwapProps = { 8 | showConfirm: boolean, 9 | setShowConfirm: (showConfirm: boolean) => void 10 | tickIn: string, 11 | tickOut: string, 12 | amountIn: string, 13 | amountOut: string, 14 | slippage: string, 15 | address: string, 16 | onSuccess?: () => void, 17 | } 18 | 19 | export function ConfirmSwap({ 20 | showConfirm, setShowConfirm, 21 | tickIn, tickOut, amountIn, amountOut, slippage, address, onSuccess 22 | }: ConfirmSwapProps) { 23 | 24 | const {signMessage} = useUnisat() 25 | const [swapReqParams, setSwapReqParams] = useState(); 26 | const [preSwap, setPreSwap] = useState() 27 | 28 | const [isLoading, setIsLoading] = useState(false); 29 | 30 | 31 | useEffect(() => { 32 | if (showConfirm) { 33 | const ts = Math.floor(Date.now() / 1000); 34 | const params: SwapReq = { 35 | address, 36 | tickIn, 37 | tickOut, 38 | amountIn, 39 | amountOut, 40 | slippage, 41 | exactType: ExactType.exactIn, 42 | ts, 43 | feeTick:'test_sats', 44 | payType:'tick', 45 | } 46 | swapApi.preSwap(params).then(res => { 47 | setSwapReqParams(params); 48 | setPreSwap(res) 49 | }).catch((e) => { 50 | message.error((e && e.message) || e) 51 | }) 52 | } else { 53 | setSwapReqParams(undefined); 54 | setPreSwap(undefined) 55 | } 56 | }, [address, amountIn, amountOut, showConfirm, slippage, tickIn, tickOut]); 57 | 58 | 59 | async function swap() { 60 | if (!preSwap || !swapReqParams) return; 61 | setIsLoading(true); 62 | try { 63 | const {signMsgs} = preSwap; 64 | 65 | //sign message 66 | let sigs = []; 67 | for (let i = 0; i < preSwap.signMsgs.length; i += 1) { 68 | const signMsg = preSwap.signMsgs[i]; 69 | 70 | const sig = await signMessage(signMsg); 71 | 72 | sigs.push(sig); 73 | } 74 | 75 | 76 | const params = { 77 | ...swapReqParams, 78 | sigs, 79 | feeAmount: preSwap.feeAmount, 80 | feeTickPrice: preSwap.feeTickPrice, 81 | } 82 | 83 | await swapApi.swap(params); 84 | 85 | message.success('Swap success'); 86 | 87 | onSuccess && onSuccess(); 88 | 89 | setShowConfirm(false); 90 | 91 | 92 | } catch (e: any) { 93 | message.error((e && e.message) || e) 94 | } finally { 95 | setIsLoading(false); 96 | } 97 | } 98 | 99 | return { 101 | setShowConfirm(false) 102 | }} 103 | onOk={swap} 104 | confirmLoading={isLoading} 105 | > 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | { 118 | !preSwap 119 | ? 120 | : <> 121 | 122 | 128 | 129 | 130 | 131 | 132 | } 133 | 134 | 135 | 136 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/components/CreatePools.tsx: -------------------------------------------------------------------------------- 1 | // import {Button, message} from "antd"; 2 | // import {AddLiqReq, DeployPoolReq, swapApi} from "../utils/swapApi"; 3 | // import {useUnisat} from "../provider/UniSatProvider"; 4 | // import {handleError} from "../utils/utils"; 5 | // 6 | // export function CreatePools() { 7 | // 8 | // const {address, signMessage} = useUnisat(); 9 | // 10 | // async function create() { 11 | // try { 12 | // 13 | // const ts = Math.floor(Date.now() / 1000); 14 | // const params: DeployPoolReq = { 15 | // address, 16 | // tick0: "sats", 17 | // tick1: "ordi", 18 | // ts, 19 | // } 20 | // 21 | // const {signMsg} = await swapApi.preDeployPool(params) 22 | // 23 | // params.sig = await signMessage(signMsg) 24 | // 25 | // await swapApi.deployPool(params) 26 | // 27 | // message.success("Create pool success") 28 | // } catch (e) { 29 | // handleError(e) 30 | // } 31 | // } 32 | // 33 | // async function addLiq() { 34 | // try { 35 | // const ts = Math.floor(Date.now() / 1000); 36 | // const params: AddLiqReq = { 37 | // address, 38 | // tick0: "sats", 39 | // tick1: "ordi", 40 | // amount0: "10", 41 | // amount1: "100", 42 | // slippage: "0.005", 43 | // lp: "31.622776601683793", 44 | // ts, 45 | // } 46 | // 47 | // const {signMsg} = await swapApi.preAddLiquidity(params) 48 | // 49 | // params.sig = await signMessage(signMsg) 50 | // 51 | // await swapApi.addLiquidity(params) 52 | // 53 | // message.success("Add liquidity success") 54 | // } catch (e) { 55 | // handleError(e) 56 | // } 57 | // } 58 | // 59 | // return <> 60 | // 63 | // 66 | // 67 | // } 68 | 69 | export function CreatePools() { 70 | 71 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/components/Deposit.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useState} from "react"; 2 | import {useUnisat} from "../provider/UniSatProvider"; 3 | import {Button, Empty, message, Segmented, Skeleton, Statistic} from "antd"; 4 | import {AddressBrc20Balance, brc20Api, Brc20InscriptionsItem} from "../utils/brc20Api"; 5 | import {handleError, sleep} from "../utils/utils"; 6 | import {PlusOutlined, ReloadOutlined} from "@ant-design/icons"; 7 | import {unisatUtils} from "../utils/unisatUtils"; 8 | import {swapApi} from "../utils/swapApi"; 9 | 10 | export function Deposit() { 11 | 12 | const {address, connect, signPsbt} = useUnisat(); 13 | const [addressSummary, setAddressSummary] = useState(null); 14 | const [currentTicker, setCurrentTicker] = useState(''); 15 | const [transferInscriptions, setTransferInscriptions] = useState(null); 16 | const [selectedInscription, setSelectedInscription] = useState(null); 17 | const [isDepositing, setIsDepositing] = useState(false); 18 | 19 | useEffect(() => { 20 | if (address) { 21 | // get address brc20 summary 22 | brc20Api.summary({address}).then(res => { 23 | setAddressSummary(res.detail) 24 | if (res.detail.length > 0) { 25 | setCurrentTicker(res.detail[0].ticker) 26 | } 27 | }).catch(handleError) 28 | } else { 29 | setAddressSummary(null) 30 | } 31 | }, [address]); 32 | 33 | const refreshTransferable = useCallback(() => { 34 | if (currentTicker) { 35 | // get transfer-inscription 36 | setTransferInscriptions(null) 37 | brc20Api.getAddressTransferable({address, tick: currentTicker}).then(res => { 38 | setTransferInscriptions(res.detail); 39 | }).catch(handleError) 40 | } else { 41 | setTransferInscriptions(null) 42 | setSelectedInscription(null) 43 | } 44 | }, [address, currentTicker]); 45 | 46 | useEffect(() => { 47 | refreshTransferable() 48 | }, [refreshTransferable]); 49 | 50 | async function inscribeTransfer() { 51 | try { 52 | await unisatUtils.inscribeTransfer(currentTicker) 53 | setTransferInscriptions(null) 54 | await sleep(5000) 55 | refreshTransferable() 56 | } catch (e) { 57 | handleError(e) 58 | } 59 | } 60 | 61 | async function deposit() { 62 | if (!selectedInscription) return; 63 | try { 64 | setIsDepositing(true) 65 | const {inscriptionId} = selectedInscription; 66 | 67 | const pubkey = await unisatUtils.getPublicKey(); 68 | const {psbt} = await swapApi.createDeposit({ 69 | inscriptionId, 70 | address, 71 | pubkey, 72 | }) 73 | 74 | const signed = await signPsbt(psbt); 75 | 76 | await swapApi.confirmDeposit({ 77 | inscriptionId, 78 | psbt: signed 79 | }) 80 | 81 | await sleep(3000) 82 | message.success('Deposit Success'); 83 | await sleep(2000) 84 | 85 | refreshTransferable(); 86 | } catch (e) { 87 | handleError(e) 88 | } finally { 89 | setIsDepositing(false); 90 | } 91 | } 92 | 93 | if (!address) { 94 | return 95 | } 96 | 97 | if (!addressSummary) { 98 | return 99 | } 100 | 101 | return <> 102 | { 103 |
104 | Mint test_sats 105 |
106 | } 107 | { 108 | addressSummary.length <= 0 ? : { 112 | setCurrentTicker(v.toString()) 113 | }} 114 | options={addressSummary.map(item => { 115 | return { 116 | label:
117 |
{item.ticker}
118 | 119 |
, 120 | value: item.ticker 121 | } 122 | })}/> 123 | } 124 | { 125 | currentTicker && <> 126 |
127 | Choose a transfer-inscription to Deposit 128 |
130 | { 131 |
132 | { 133 | !transferInscriptions ? : <> 134 |
135 | 136 | Inscribe 137 | TRANSFER 138 |
139 | { 140 | 141 | transferInscriptions.map(item => { 142 | return
{ 146 | if (!isDepositing) 147 | setSelectedInscription(item); 148 | }}> 149 |
{item.data.tick}
150 | 152 |
#{item.inscriptionNumber}
153 |
154 | }) 155 | } 156 | 157 | } 158 |
159 | } 160 | 164 | 165 | } 166 | 167 | 168 | 169 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/components/Swap.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Col, InputNumber, Row, Spin, Statistic} from "antd"; 2 | import React, {useCallback, useEffect, useState} from "react"; 3 | import {AllAddressBalanceRes, ExactType, swapApi} from "../utils/swapApi"; 4 | import {ConfirmSwap} from "./ConfirmSwap"; 5 | import {useUnisat} from "../provider/UniSatProvider"; 6 | import {handleError} from "../utils/utils"; 7 | import {ReloadOutlined} from "@ant-design/icons"; 8 | 9 | 10 | const tickIn = 'test_sats'; 11 | const tickOut = 'test_ordi'; 12 | const slippage = '0.005'; // 0.5% 13 | 14 | 15 | export function Swap() { 16 | 17 | const {address, connect} = useUnisat(); 18 | const [swapBalanceMap, setSwapBalanceMap] = useState({}); 19 | 20 | const [amountIn, setAmountIn] = React.useState(''); 21 | const [amountOut, setAmountOut] = React.useState(''); 22 | const [isLoading, setIsLoading] = React.useState(false); 23 | 24 | const [showConfirm, setShowConfirm] = React.useState(false); 25 | 26 | useEffect(() => { 27 | 28 | if (!address) return; 29 | if (!amountIn) { 30 | setAmountOut(''); 31 | return; 32 | } 33 | setIsLoading(true) 34 | // 500ms delay 35 | const timer = setTimeout(() => { 36 | // quote swap out 37 | swapApi.quoteSwap({ 38 | address, 39 | tickIn, 40 | tickOut, 41 | amount: amountIn, 42 | exactType: ExactType.exactIn, 43 | }).then(({expect}) => { 44 | setAmountOut(expect); 45 | }).catch(e => { 46 | handleError(e) 47 | }).finally(() => { 48 | setIsLoading(false) 49 | }) 50 | }, 500); 51 | 52 | return () => clearTimeout(timer); 53 | }, [address, amountIn]); 54 | 55 | const refreshBalance = useCallback(() => { 56 | if (address) { 57 | swapApi.getAllBalance({address}).then(setSwapBalanceMap).catch(handleError) 58 | } else { 59 | setSwapBalanceMap({}) 60 | } 61 | }, [address]); 62 | 63 | useEffect(() => { 64 | refreshBalance() 65 | }, [refreshBalance]); 66 | 67 | 68 | return <> 69 | 70 | 71 | Address: 72 | 73 | 74 | { 75 | address ? address : 78 | } 79 | 80 | 81 | 82 | Balance: 83 | 84 | 85 | 90 | { 91 | // wait for confirm balance 92 | (parseFloat(swapBalanceMap[tickIn]?.balance.pendingSwap) || 0) > 0 && 93 | (+{swapBalanceMap[tickIn]?.balance.pendingSwap}) 94 | 95 | } 96 | 97 | 98 | 103 | { 104 | // wait for confirm balance 105 | (parseFloat(swapBalanceMap[tickOut]?.balance.pendingSwap) || 0) > 0 && 106 | 107 | (+{swapBalanceMap[tickOut]?.balance.pendingSwap}) 108 | 109 | } 110 | 111 | 112 | 152 | 153 | 154 | { 155 | showConfirm && 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum NetworkType { 2 | livenet = 'livenet', 3 | testnet = 'testnet', 4 | } 5 | 6 | // export const NETWORK = NetworkType.testnet 7 | export const NETWORK = NetworkType.testnet -------------------------------------------------------------------------------- /brc20-swap-demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './styles/index.css'; 4 | import App from './App'; 5 | import {ConfigProvider} from "antd"; 6 | import UnisatProvider from "./provider/UniSatProvider"; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | root.render( 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/provider/UniSatProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, ReactNode, useCallback, useContext, useEffect, useState} from "react"; 2 | import {handleError, sleep} from "../utils/utils"; 3 | import {unisatUtils} from "../utils/unisatUtils"; 4 | import {NETWORK} from "../constants"; 5 | 6 | interface UnisatContextType { 7 | isInstalled: boolean; 8 | isConnected: boolean; 9 | address: string; 10 | connect: () => void; 11 | signMessage: (msg: string) => Promise; 12 | signPsbt: (psbt: string) => Promise; 13 | } 14 | 15 | const UnisatContext = createContext({ 16 | isInstalled: false, 17 | isConnected: false, 18 | address: '', 19 | connect: () => { 20 | }, 21 | signMessage: (msg: string) => Promise.resolve(''), 22 | signPsbt: (psbt: string) => Promise.resolve('') 23 | }) 24 | 25 | 26 | export function useUnisat() { 27 | const context = useContext(UnisatContext); 28 | if (!context) { 29 | throw Error('Feature flag hooks can only be used by children of UnisatProvider.'); 30 | } else { 31 | return context; 32 | } 33 | } 34 | 35 | export default function UnisatProvider({children}: { 36 | children: ReactNode 37 | }) { 38 | const [isInstalled, setIsInstalled] = useState(false) 39 | const [isConnected, setIsConnected] = useState(false) 40 | const [address, setAddress] = useState('') 41 | 42 | 43 | useEffect(() => { 44 | 45 | async function init() { 46 | let install = !!window.unisat; 47 | setIsInstalled(install); 48 | 49 | // 额外检查 50 | for (let i = 0; i < 10 && !install; i += 1) { 51 | await sleep(100 + i * 100); 52 | install = !!window.unisat; 53 | if (install) { 54 | setIsInstalled(install); 55 | break; 56 | } 57 | } 58 | 59 | if (install) { 60 | await unisatUtils.switchChain('FRACTAL_BITCOIN_TESTNET') 61 | 62 | const address = await unisatUtils.getAccounts() 63 | if (address) { 64 | // connected 65 | setIsConnected(true) 66 | setAddress(address) 67 | 68 | } 69 | } 70 | } 71 | 72 | init().then(); 73 | 74 | }, []); 75 | 76 | const connect = useCallback(async () => { 77 | try { 78 | await unisatUtils.switchChain('FRACTAL_BITCOIN_TESTNET') 79 | 80 | const address = await unisatUtils.requestAccounts(); 81 | if (address) { 82 | setIsConnected(true) 83 | setAddress(address) 84 | } 85 | } catch (e) { 86 | handleError(e) 87 | } 88 | 89 | }, []) 90 | 91 | const signMessage = useCallback((msg: string) => { 92 | return unisatUtils.signMessage(msg, 'bip322-simple') 93 | }, []) 94 | 95 | const signPsbt = useCallback((psbt: string) => { 96 | return unisatUtils.signPsbt(psbt) 97 | }, []); 98 | 99 | const value = { 100 | isInstalled, 101 | isConnected, 102 | address, 103 | connect, 104 | signMessage, 105 | signPsbt, 106 | } 107 | 108 | return 109 | {children} 110 | 111 | 112 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/styles/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | padding: 16px; 3 | } 4 | 5 | .main-container { 6 | max-width: 640px; 7 | margin: 0 auto; 8 | } 9 | 10 | .font-16 { 11 | font-size: 16px; 12 | } 13 | 14 | .p-8 { 15 | padding: 8px; 16 | } 17 | 18 | .mt-16 { 19 | margin-top: 16px; 20 | } 21 | 22 | .mt-8 { 23 | margin-top: 8px; 24 | } 25 | 26 | .ml-8 { 27 | margin-left: 8px; 28 | } 29 | 30 | .flex-row-v-center { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | } 35 | 36 | .gap-16 { 37 | gap: 16px; 38 | } 39 | 40 | .flex-1 { 41 | flex: 1; 42 | } 43 | 44 | .bold { 45 | font-weight: bold; 46 | } 47 | 48 | .nft-item { 49 | min-width: 88px; 50 | padding: 8px; 51 | border-radius: 12px; 52 | overflow: hidden; 53 | border: 1px solid #fff; 54 | box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1); 55 | cursor: pointer; 56 | 57 | transition: all .2s ease-in-out; 58 | } 59 | 60 | .nft-item:hover { 61 | border: 1px solid var(--main-color); 62 | box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1); 63 | 64 | } 65 | 66 | .nft-item.selected { 67 | border: 1px solid var(--main-color); 68 | } 69 | 70 | .nft-item.inscribe-transfer { 71 | display: flex; 72 | flex-direction: column; 73 | align-items: center; 74 | } 75 | 76 | .inscribe-transfer .plus-icon { 77 | font-size: 32px; 78 | color: var(--main-color); 79 | 80 | } 81 | 82 | .nft-item.selected .inscription-num { 83 | background: var(--main-color); 84 | color: white; 85 | } 86 | 87 | .nft-item .tick { 88 | font-size: 12px; 89 | color: #8a8a8a; 90 | } 91 | 92 | .nft-item .inscription-num { 93 | background: #ddd; 94 | padding: 4px; 95 | margin: 8px -8px -8px; 96 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | --main-color: #EBB94C; 10 | 11 | background-color: rgb(240, 242, 245); 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /brc20-swap-demo/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | 4 | export type UnisatWalletInterface = { 5 | getAccounts(): Promise; 6 | requestAccounts(): Promise; 7 | getNetwork(): Promise; 8 | switchNetwork(network: string): Promise; 9 | sendBitcoin(address: string, amount: number, options: any): Promise; 10 | on(event: string, listener: Function): void; 11 | removeListener(event: string, listener: Function): void; 12 | signMessage(message: string, type?: string): Promise; 13 | signPsbt(psbt: string, opt: { autoFinalized: boolean }): Promise; 14 | getPublicKey(): Promise; 15 | getBalance(): Promise<{ 16 | confirmed: number, 17 | unconfirmed: number, 18 | total: number 19 | }>; 20 | inscribeTransfer(tick: string, amount?: number | string): Promise<{ 21 | amount: string, 22 | inscriptionId: string 23 | inscriptionNumber: number 24 | ticker: string 25 | }>; 26 | getInscriptions(num: number): Promise; 27 | switchChain(network: string): Promise; 28 | } 29 | 30 | declare global { 31 | interface Window { 32 | unisat: UnisatWalletInterface; 33 | } 34 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/utils/brc20Api.ts: -------------------------------------------------------------------------------- 1 | import {get} from "./httpUtils"; 2 | import {stringToHex} from "./utils"; 3 | 4 | export const brc20Api = { 5 | summary({ 6 | address, limit = 20, start = 0, 7 | }: { address: string, limit?: number, start?: number }): Promise<{ 8 | total: number, 9 | start: number, 10 | height: number, 11 | detail: AddressBrc20Balance[] 12 | }> { 13 | return get(`/v1/indexer/address/${address}/brc20/summary`, {limit, start}) 14 | }, 15 | 16 | getAddressTransferable({ 17 | address, tick, start = 0, limit = 512, 18 | }: { 19 | address: string, 20 | tick: string, 21 | start?: number, 22 | limit?: number 23 | }): Promise { 24 | return get(`/v1/indexer/address/${address}/brc20/${(tick)}/transferable-inscriptions`, { 25 | limit, 26 | start, 27 | }) 28 | }, 29 | } 30 | 31 | 32 | export type AddressBrc20Balance = { 33 | ticker: string 34 | overallBalance: string // transferableBalance + availableBalance 35 | transferableBalance: string 36 | availableBalance: string 37 | } 38 | 39 | 40 | export type Brc20Data = { 41 | amt: string, 42 | decimal: string, 43 | lim: string, 44 | max: string, 45 | minted: string, 46 | op: string, 47 | tick: string, 48 | to: string 49 | } 50 | export type Brc20InscriptionsItem = { 51 | data: Brc20Data, 52 | inscriptionId: string, 53 | inscriptionNumber: number, 54 | confirmations: number, 55 | isPutOn?: boolean, 56 | }; 57 | 58 | export type Brc20AddressTransferable = { 59 | start: number, 60 | total: number, 61 | detail: Brc20InscriptionsItem[] 62 | }; -------------------------------------------------------------------------------- /brc20-swap-demo/src/utils/httpUtils.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {isTestnet} from "./utils"; 3 | 4 | let apiKey = localStorage.getItem('apiKey') || ''; 5 | 6 | export function setApiKey(key: string) { 7 | apiKey = key; 8 | } 9 | 10 | const api = axios.create({ 11 | baseURL: isTestnet ? "https://open-api-fractal-testnet.unisat.io" : "https://open-api.unisat.io/", 12 | timeout: 10000, 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | }); 17 | 18 | api.interceptors.request.use((config) => { 19 | config.headers.Authorization = `Bearer ${apiKey}`; 20 | return config; 21 | }) 22 | 23 | export const get = async (url: string, params?: any) => { 24 | const res = await api.get(url, {params}); 25 | if (res.status !== 200) { 26 | throw new Error(res.statusText); 27 | } 28 | 29 | const responseData = res.data; 30 | 31 | if (responseData.code !== 0) { 32 | throw new Error(responseData.msg); 33 | } 34 | return responseData.data; 35 | }; 36 | 37 | export const post = async (url: string, data?: any) => { 38 | const res = await api.post(url, data,); 39 | if (res.status !== 200) { 40 | throw new Error(res.statusText); 41 | } 42 | 43 | const responseData = res.data; 44 | 45 | if (responseData.code !== 0) { 46 | throw new Error(responseData.msg); 47 | } 48 | 49 | return responseData.data; 50 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/utils/swapApi.ts: -------------------------------------------------------------------------------- 1 | import {get, post} from "./httpUtils"; 2 | 3 | 4 | export const swapApi = { 5 | getAllBalance: (params: AllAddressBalanceReq): Promise => get('/v1/brc20-swap/all_balance', params), 6 | 7 | quoteSwap: async (req: QuoteSwapReq): Promise => { 8 | return get('/v1/brc20-swap/quote_swap', req); 9 | }, 10 | preSwap: async (req: SwapReq): Promise => { 11 | return get('/v1/brc20-swap/pre_swap', req); 12 | }, 13 | swap: async (req: SwapReq): Promise => { 14 | return post('/v1/brc20-swap/swap', req); 15 | }, 16 | 17 | createDeposit: async (req: CreateDepositReq): Promise => { 18 | return get('/v1/brc20-swap/create_deposit', req); 19 | }, 20 | 21 | confirmDeposit: async (req: ConfirmDepositReq): Promise => { 22 | return post('/v1/brc20-swap/confirm_deposit', req); 23 | }, 24 | preDeployPool(params: DeployPoolReq): Promise { 25 | return get('/v1/brc20-swap/pre_deploy_pool', params) 26 | }, 27 | deployPool(params: DeployPoolReq): Promise { 28 | return post('/v1/brc20-swap/deploy_pool', params) 29 | }, 30 | // 获取添加流动性的待签名信息 31 | preAddLiquidity: (params: AddLiqReq): Promise => get('/v1/brc20-swap/pre_add_liq', params), 32 | // 添加流动性 33 | addLiquidity: (params: AddLiqReq): Promise => post('/v1/brc20-swap/add_liq', params), 34 | } 35 | 36 | export enum ExactType { 37 | exactIn = "exactIn", 38 | exactOut = "exactOut", 39 | } 40 | 41 | export type QuoteSwapReq = { 42 | address: string; 43 | tickIn: string; 44 | tickOut: string; 45 | amount: string; 46 | exactType: ExactType; 47 | }; 48 | 49 | export type QuoteSwapRes = { 50 | expect: string; 51 | amountUSD: string; 52 | expectUSD: string; 53 | }; 54 | 55 | 56 | export type SwapReq = { 57 | address: string; 58 | tickIn: string; 59 | tickOut: string; 60 | amountIn: string; 61 | amountOut: string; 62 | slippage: string; 63 | exactType: ExactType; 64 | ts: number; 65 | sig?: string; 66 | feeTick: string; 67 | payType: string; 68 | }; 69 | export type SwapRes = { 70 | id: string; 71 | address: string; 72 | tickIn: string; 73 | tickOut: string; 74 | amountIn: string; 75 | amountOut: string; 76 | exactType: ExactType; 77 | ts: number; 78 | } 79 | export type PreRes = { 80 | // signedMsg: string; 81 | signMsgs: string[]; 82 | } & FeeRes; 83 | 84 | export type FeeRes = { 85 | feeAmount: string; 86 | feeTick: string; 87 | feeTickPrice: string; 88 | feeBalance: string; 89 | 90 | originalFeeAmount: string; 91 | freeQuota: string; 92 | }; 93 | 94 | 95 | interface CreateDepositReq { 96 | inscriptionId: string, 97 | address: string, 98 | pubkey: string 99 | } 100 | 101 | interface CreateDepositRes { 102 | psbt: string; 103 | type: "direct" | "matching"; 104 | expiredTimestamp: number; 105 | recommendDeposit: string; 106 | } 107 | 108 | 109 | interface ConfirmDepositReq { 110 | psbt: string, 111 | inscriptionId: string 112 | } 113 | 114 | interface ConfirmDepositRes { 115 | txid: string; 116 | pendingNum: number; 117 | } 118 | 119 | 120 | export type DeployPoolReq = { 121 | address: string; 122 | tick0: string; 123 | tick1: string; 124 | ts: number; 125 | sig?: string; 126 | }; 127 | export type DeployPoolRes = { 128 | // 129 | }; 130 | 131 | 132 | export type AllAddressBalanceReq = { 133 | address: string; 134 | }; 135 | export type AllAddressBalanceRes = { 136 | [key: string]: { 137 | balance: AddressBalance 138 | decimal: string 139 | withdrawLimit: string 140 | }; 141 | }; 142 | 143 | export type AddressBalance = { 144 | module: string; // 提现队列中的 145 | swap: string; 146 | pendingSwap: string; // 充值未确认的 147 | pendingAvailable: string; // 提现未确认的 148 | }; 149 | 150 | 151 | export type AddLiqReq = { 152 | address: string; 153 | tick0: string; 154 | tick1: string; 155 | amount0: string; 156 | amount1: string; 157 | slippage: string; 158 | ts: number; 159 | lp: string; 160 | sig?: string; 161 | }; 162 | 163 | export type AddLiqRes = { 164 | id: string; 165 | address: string; 166 | }; -------------------------------------------------------------------------------- /brc20-swap-demo/src/utils/unisatUtils.ts: -------------------------------------------------------------------------------- 1 | import {handleError} from "./utils"; 2 | 3 | export const unisatUtils = { 4 | async getAccounts() { 5 | try { 6 | const accounts = await window.unisat.getAccounts(); 7 | if (accounts && accounts.length > 0) { 8 | return accounts[0]; 9 | } 10 | } catch (e: any) { 11 | handleError(e); 12 | } 13 | return ''; 14 | }, 15 | async requestAccounts() { 16 | try { 17 | const accounts = await window.unisat.requestAccounts(); 18 | if (accounts && accounts.length > 0) { 19 | return accounts[0]; 20 | } 21 | } catch (e: any) { 22 | handleError(e); 23 | } 24 | return ''; 25 | }, 26 | signMessage(message: string, type?: string) { 27 | return window.unisat.signMessage(message, type); 28 | }, 29 | signPsbt(psbt: string) { 30 | return window.unisat.signPsbt(psbt, {autoFinalized: false}); 31 | }, 32 | getNetwork(): Promise { 33 | return window.unisat.getNetwork(); 34 | }, 35 | switchNetwork(network: string): Promise { 36 | return window.unisat.switchNetwork(network); 37 | }, 38 | 39 | async checkNetwork(network: string): Promise { 40 | if (network !== await this.getNetwork()) { 41 | await this.switchNetwork(network); 42 | } 43 | }, 44 | inscribeTransfer(tick: string, amount?: number | string) { 45 | return window.unisat.inscribeTransfer(tick, amount) 46 | }, 47 | getPublicKey() { 48 | return window.unisat.getPublicKey(); 49 | }, 50 | switchChain(network: string) { 51 | return window.unisat.switchChain(network); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /brc20-swap-demo/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from "buffer"; 2 | import {message} from "antd"; 3 | import {NETWORK, NetworkType} from "../constants"; 4 | 5 | export function sleep(ms: number) { 6 | return new Promise((resolve) => setTimeout(resolve, ms)); 7 | } 8 | 9 | export function stringToHex(stringToEncode: string) { 10 | return Buffer.from(stringToEncode).toString('hex') 11 | } 12 | 13 | export function handleError(e: any) { 14 | return message.error((e && e.message) || e) 15 | } 16 | 17 | // @ts-ignore 18 | export const isTestnet = NETWORK === NetworkType.testnet; -------------------------------------------------------------------------------- /brc20-swap-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/README.md: -------------------------------------------------------------------------------- 1 | ## UniSat Inscribe Demo 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ### Run the demo 6 | 7 | #### Install dependencies 8 | 9 | ```bash 10 | yarn 11 | ``` 12 | 13 | #### Run the app 14 | 15 | ```bash 16 | yarn start 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unisat-inscribe-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^5.3.0", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^13.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "@types/jest": "^27.0.1", 11 | "@types/node": "^16.7.13", 12 | "@types/react": "^18.0.0", 13 | "@types/react-dom": "^18.0.0", 14 | "ahooks": "^3.7.10", 15 | "antd": "^5.14.0", 16 | "axios": "^1.6.7", 17 | "buffer": "^6.0.3", 18 | "js-base64": "^3.7.6", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-scripts": "5.0.1", 22 | "typescript": "^4.4.2", 23 | "web-vitals": "^2.1.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/unisat-inscribe-demo/public/favicon.ico -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/unisat-inscribe-demo/public/logo192.png -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisat-wallet/dev-support/e50df50b8c7d1b04690261dca0cd511ed0d4f657/unisat-inscribe-demo/public/logo512.png -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import './style/App.css'; 3 | import UnisatProvider from "./provider/UniSatProvider"; 4 | import {ConfigProvider, Layout, Typography} from 'antd'; 5 | import {useEventEmitter, useLocalStorageState} from "ahooks"; 6 | import {Inscribe} from "./pages/Inscribe"; 7 | import {ApiKeyInput} from "./components/ApiKeyInput"; 8 | import {NetworkSwitch} from "./components/NetworkSwitch"; 9 | import {OrderList} from "./pages/OrderList"; 10 | 11 | function App() { 12 | 13 | const newOrder$ = useEventEmitter(); 14 | 15 | 16 | const [apiKey, setApiKey] = useLocalStorageState('apiKey', 17 | {defaultValue: '', deserializer: (val) => val, serializer: (val) => val}) 18 | 19 | return ( 20 | 30 | 31 | 32 | 33 |
41 | 42 | 43 | { 44 | apiKey 45 | ? <> 46 | 47 | 48 | 49 | : <> 50 | 51 | Please set API key to continue 52 | 53 | 54 | } 55 |
56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/ApiKeyInput.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Input} from "antd"; 2 | import React from "react"; 3 | import {setApiKey} from "../utils/httpUtils"; 4 | 5 | export function ApiKeyInput({apiKey,setKey}:{ 6 | apiKey: string |undefined, 7 | setKey: (apiKey: string) => void 8 | }) { 9 | 10 | return 11 |
12 | Set API key: 13 |
14 |
15 | { 19 | const key = e.target.value; 20 | setKey(key); 21 | setApiKey(key); 22 | }} 23 | /> 24 |
25 |
26 |
27 |
28 | } 29 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/FeeDetail.tsx: -------------------------------------------------------------------------------- 1 | import {InscribeFileData} from "../pages/Inscribe"; 2 | import {SimpleRow} from "./SimpleRow"; 3 | 4 | type FeeDetailProps = { 5 | feeRate: number|undefined, 6 | outputValue: number 7 | devFee: number, 8 | address: string, 9 | fileList: InscribeFileData[] 10 | } 11 | 12 | export function FeeDetail({ 13 | feeRate=0, outputValue, fileList, devFee,address 14 | }: FeeDetailProps) { 15 | 16 | const inscriptionBalance = outputValue; // the balance in each inscription 17 | const fileCount = fileList.length; // the fileCount 18 | const fileSize = fileList.reduce((pre,item)=>pre+item.size,0); // the total size of all files 19 | const contentTypeSize = fileList.reduce((pre, item) => pre + item.dataURL.split('data:')[1].split('base64')[0].length, 0); // the size of contentType 20 | const feeFileSize = fileList.slice(0,25).reduce((pre,item)=>pre+item.size,0); // the total size of first 25 files 21 | const feeFileCount = 25 // do not change this 22 | 23 | const balance = inscriptionBalance * fileCount; 24 | 25 | let addrSize = 25+1; // p2pkh 26 | if(address.indexOf('bc1q')==0 || address.indexOf('tb1q')==0){ 27 | addrSize = 22+1; 28 | }else if(address.indexOf('bc1p')==0 || address.indexOf('tb1p')==0){ 29 | addrSize = 34+1; 30 | }else if(address.indexOf('2')==0 || address.indexOf('3')==0){ 31 | addrSize = 23+1; 32 | } 33 | 34 | const baseSize = 88; 35 | let networkFee = Math.ceil(((fileSize+contentTypeSize) / 4 + ( baseSize+8+addrSize+8+23)) * feeRate); 36 | if(fileCount>1){ 37 | networkFee = Math.ceil(((fileSize+contentTypeSize) / 4 + (baseSize+8+addrSize+(35+8)*(fileCount-1)+ 8+23 +(baseSize+8+addrSize+0.5)*(fileCount-1) )) * feeRate); 38 | } 39 | let networkSatsByFeeCount = Math.ceil(((feeFileSize+contentTypeSize) / 4 + ( baseSize+8+addrSize+8+23)) * feeRate); 40 | if(fileCount>1){ 41 | networkSatsByFeeCount = Math.ceil(((( feeFileSize)+contentTypeSize*(feeFileCount/fileCount)) / 4 + (baseSize+8+addrSize+(35+8)*(fileCount-1)+ 8+23 +(baseSize+8+addrSize+0.5)*Math.min(fileCount-1,feeFileCount-1) )) * feeRate); 42 | } 43 | 44 | const baseFee = 1999 * Math.min(fileCount, feeFileCount); // 1999 base fee for top 25 files 45 | const floatFee = Math.ceil(networkSatsByFeeCount * 0.0499); // 4.99% extra miner fee for top 25 transations 46 | const serviceFee = Math.floor(baseFee + floatFee); 47 | 48 | const total = balance + networkFee + serviceFee; 49 | const truncatedTotal = Math.floor((total) / 1000) * 1000; // truncate 50 | const amount = truncatedTotal + devFee; // add devFee at the end 51 | 52 | console.log("The final amount need to pay: ",amount) 53 | 54 | return <> 55 | 58 | 61 | 64 | {total} {amount}}/> 67 | 68 | 69 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/InscribeFileInput.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Flex, Upload} from "antd"; 2 | import {RcFile} from "antd/es/upload"; 3 | 4 | import {InscribeFileData} from "../pages/Inscribe"; 5 | import {getSizeShow, handleError, isUtf8, showLongString} from "../utils/utils"; 6 | 7 | type InscribeFileInputProps = { 8 | fileList: InscribeFileData[] 9 | setFileList: (list: InscribeFileData[]) => void 10 | isSubmitting: boolean 11 | } 12 | 13 | 14 | const fileSizeLimit = 1024 * 365; // 单个文件大小限制 15 | const multiFileSizeLimit = 1024 * 100; // 批量铸造时,中文件大小显示。 16 | const fileCountLimit = 1000; 17 | 18 | 19 | const getFileBase64 = (file: RcFile, callback: (url: string) => void) => { 20 | const reader = new FileReader(); 21 | reader.addEventListener('load', () => callback(reader.result as string)); 22 | reader.readAsDataURL(file); 23 | }; 24 | 25 | export function InscribeFileInput({ 26 | fileList, 27 | setFileList, 28 | isSubmitting 29 | }: InscribeFileInputProps) { 30 | const beforeUpload = async (file: RcFile) => { 31 | if (isSubmitting) 32 | return false; 33 | 34 | // console.log(file,file.name, file.type); 35 | 36 | if (fileList.length >= fileCountLimit) { 37 | handleError(`the file quantity limit is ${fileCountLimit}`); 38 | return false; 39 | } 40 | 41 | const isLtSize = file.size < fileSizeLimit; 42 | if (!isLtSize) { 43 | handleError(`File size must be smaller than ${getSizeShow(fileSizeLimit, 0)}!`); 44 | return false; 45 | } 46 | 47 | if (fileList.length > 0) { 48 | const total = fileList.reduce((pre, current) => pre + current.size, 0); 49 | if (total + file.size > multiFileSizeLimit) { 50 | handleError(`The total file size limit for inscribing inscriptions in bulk is ${getSizeShow(multiFileSizeLimit, 0)}!`); 51 | return false; 52 | } 53 | } 54 | 55 | return new Promise(async (resolve, reject) => { 56 | 57 | if (file.type.startsWith('text')) { 58 | try { 59 | await isUtf8(file) 60 | } catch (e: any) { 61 | handleError(e); 62 | resolve(false); 63 | return 64 | } 65 | } 66 | 67 | getFileBase64(file, (dataURL) => { 68 | console.log(file) 69 | // handle text file encoding 70 | if (dataURL.startsWith('data:text/plain;base64')) { 71 | dataURL = dataURL.replace('data:text/plain;base64', 'data:text/plain;charset=utf-8;base64') 72 | } 73 | if (dataURL.startsWith('data:text/html;base64')) { 74 | dataURL = dataURL.replace('data:text/html;base64', 'data:text/html;charset=utf-8;base64') 75 | } 76 | 77 | 78 | const temp: InscribeFileData[] = [...fileList, { 79 | filename: file.name, 80 | size: file.size, 81 | type: file.type, 82 | dataURL, 83 | }]; 84 | 85 | setFileList(temp); 86 | 87 | 88 | resolve(true); 89 | }); 90 | }) 91 | }; 92 | 93 | return <> 94 | { 99 | }} 100 | multiple={true} 101 | > 102 |

103 | Drag and drop your files here, or click to select files 104 |

105 |

106 | .jpg, .webp, .png, .gif, .txt, .mp3, .mp4(h264) + more! 107 |

108 |
109 | { 110 | fileList.length > 0 && 111 | File list 112 | 115 | }> 116 | { 117 | fileList.map((item, index) => ( 118 |
119 | {showLongString(item.filename)} - {item.type} - {getSizeShow(item.size)} 120 |
121 | )) 122 | } 123 |
124 | } 125 | 126 | 127 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/InscribeTextInput.tsx: -------------------------------------------------------------------------------- 1 | import {SimpleRow} from "./SimpleRow"; 2 | import {Input} from "antd"; 3 | import {useEffect, useState} from "react"; 4 | 5 | export type InscribeTextInputProps = { 6 | submit: (text: string) => void, 7 | } 8 | 9 | export function InscribeTextInput({submit}: InscribeTextInputProps) { 10 | 11 | const [inputText, setInputText] = useState(''); 12 | 13 | useEffect(() => { 14 | submit(inputText); 15 | }, [inputText]); 16 | 17 | return <> 18 | setInputText(e.target.value)} 23 | placeholder={'Enter text to inscribe'} 24 | allowClear 25 | />} 26 | /> 27 | 28 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/NetworkSwitch.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Radio} from "antd"; 2 | import {useUnisat} from "../provider/UniSatProvider"; 3 | import {NetworkType} from "../types"; 4 | 5 | 6 | export function NetworkSwitch() { 7 | const {network, switchNetwork} = useUnisat() 8 | return 9 | { 10 | switchNetwork(v.target.value) 11 | }} value={network}> 12 | Testnet 13 | Mainnet 14 | 15 | 16 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/OrderDetail.tsx: -------------------------------------------------------------------------------- 1 | import {Flex, Modal, Skeleton, Typography} from "antd"; 2 | import {useCallback, useEffect, useState} from "react"; 3 | import {InscribeOrderData} from "../utils/api-types"; 4 | import {handleError} from "../utils/utils"; 5 | import {api} from "../utils/api"; 6 | import {isOrderProcessing} from "../utils/orderUtils"; 7 | import {SimpleRow} from "./SimpleRow"; 8 | import {OrderPay} from "./OrderPay"; 9 | import {OrderStatus} from "./OrderStatus"; 10 | import {OrderFiles} from "./OrderFiles"; 11 | 12 | const {Text, Title} = Typography; 13 | 14 | export function OrderDetail({orderId, close}: { orderId: string, close: () => void }) { 15 | 16 | const [order, setOrder] = useState() 17 | const [isRequesting, setIsRequesting] = useState(false) 18 | 19 | const refresh = useCallback(async () => { 20 | if (!orderId) return undefined; 21 | 22 | let orderInfo; 23 | try { 24 | setIsRequesting(true); 25 | orderInfo = await api.orderInfo(orderId); 26 | setOrder(orderInfo); 27 | } catch (e) { 28 | handleError(e) 29 | } finally { 30 | setIsRequesting(false) 31 | } 32 | return orderInfo; 33 | 34 | }, [orderId]); 35 | 36 | useEffect(() => { 37 | let timer: any; 38 | 39 | refresh().then(data => { 40 | if (!data) { 41 | handleError('Order does not exist'); 42 | return close(); 43 | } 44 | if (isOrderProcessing(data)) { 45 | timer = setInterval(() => { 46 | refresh().then(data => { 47 | if (!data || !isOrderProcessing(data)) { 48 | clearInterval(timer); 49 | timer = null; 50 | } 51 | }) 52 | }, 5000); 53 | } 54 | }); 55 | 56 | return () => { 57 | if (timer) 58 | clearInterval(timer); 59 | } 60 | }, [refresh]); 61 | 62 | return }> 65 | 66 | 67 | { 68 | !order ? : <> 69 | 70 | 71 | 72 | 73 | } 74 | 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/OrderFiles.tsx: -------------------------------------------------------------------------------- 1 | import {InscribeOrderData} from "../utils/api-types"; 2 | import {Button, Divider, Flex} from "antd"; 3 | import {useUnisat} from "../provider/UniSatProvider"; 4 | import {NetworkType} from "../types"; 5 | 6 | export function OrderFiles({order}: { order: InscribeOrderData }) { 7 | const {network} = useUnisat(); 8 | 9 | function getLink(inscriptionId: string) { 10 | return `https://${network === NetworkType.testnet ? 'testnet.' : ''}unisat.io/inscription/${inscriptionId}` 11 | 12 | } 13 | 14 | return <> 15 | 16 | Inscribed Files 17 | 18 | 19 | { 20 | order.files.map((file, index) => { 21 | return 22 | {file.filename} 23 |
24 | {file.status} 25 | { 26 | file.inscriptionId 27 | && 28 | 35 | } 36 |
37 |
38 | }) 39 | } 40 |
41 | 42 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/OrderInscribing.tsx: -------------------------------------------------------------------------------- 1 | import {InscribeOrderData} from "../utils/api-types"; 2 | import {SimpleRow} from "./SimpleRow"; 3 | import {Typography} from "antd"; 4 | 5 | const {Text, Title} = Typography; 6 | 7 | export function OrderInscribing({order}: { order: InscribeOrderData, }) { 8 | return <> 9 | 12 | 16 | 17 | {order.unconfirmedCount + order.confirmedCount}/{order.count} 18 | 19 | ({order.unconfirmedCount} Unconfirmed, {order.confirmedCount} Confirmed) 20 | 21 | }/> 22 | 23 | 24 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/OrderPay.tsx: -------------------------------------------------------------------------------- 1 | import {InscribeOrderData} from "../utils/api-types"; 2 | import {SimpleRow} from "./SimpleRow"; 3 | import {Button, Typography} from "antd"; 4 | import {useUnisat} from "../provider/UniSatProvider"; 5 | import {unisatUtils} from "../utils/unisatUtils"; 6 | import {useState} from "react"; 7 | import {handleError} from "../utils/utils"; 8 | 9 | const {Text} = Typography; 10 | 11 | export function OrderPay({order}: { order: InscribeOrderData, }) { 12 | const [paying, setPaying] = useState(false) 13 | 14 | async function pay() { 15 | // send the amount BTC to payAddress, then order will continue to process 16 | try { 17 | setPaying(true) 18 | await unisatUtils.sendBitcoin(order.payAddress, order.amount, order.feeRate) 19 | }catch (e){ 20 | handleError(e) 21 | setPaying(false) 22 | } 23 | 24 | } 25 | 26 | return <> 27 | {order.payAddress}} valueSpan={18}/> 30 | {order.amount}}/> 33 | Pay}/> 35 | 36 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/OrderStatus.tsx: -------------------------------------------------------------------------------- 1 | import {InscribeOrderData} from "../utils/api-types"; 2 | import {OrderPay} from "./OrderPay"; 3 | import {Spin, Typography} from "antd"; 4 | import {OrderInscribing} from "./OrderInscribing"; 5 | 6 | const {Text, Title} = Typography; 7 | 8 | export function OrderStatus({order}: { order: InscribeOrderData, }) { 9 | 10 | if (order.status === 'closed') 11 | return 12 | The order has been closed as payment was not made within 1 hour. 13 | 14 | 15 | 16 | if (order.status === 'pending') { 17 | return 18 | } 19 | 20 | if(order.status === 'minted' || order.status === 'inscribing'){ 21 | return 22 | } 23 | 24 | // unknown status, loading 25 | return 26 | 27 | 28 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/components/SimpleRow.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from "react"; 2 | import {Col, Radio, Row} from "antd"; 3 | 4 | export function SimpleRow({label,value,valueSpan=12,extra}:{ 5 | label?:string, 6 | value:ReactNode, 7 | valueSpan?:number, 8 | extra?:ReactNode 9 | }) { 10 | return 11 | 12 | {label} 13 | 14 | 15 | {value} 16 | 17 | 18 | {extra} 19 | 20 | 21 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './style/index.css'; 4 | import App from './App'; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById('root') as HTMLElement 8 | ); 9 | root.render( 10 | 11 | ); 12 | 13 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/pages/Inscribe.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Flex, Input, InputNumber, message, Radio} from "antd"; 2 | import {useEffect, useState} from "react"; 3 | import {InscribeTextInput} from "../components/InscribeTextInput"; 4 | import {SimpleRow} from "../components/SimpleRow"; 5 | import {InscribeFileInput} from "../components/InscribeFileInput"; 6 | import {FeeDetail} from "../components/FeeDetail"; 7 | import {getStringByteCount, handleError, stringToBase64} from "../utils/utils"; 8 | import {useUnisat} from "../provider/UniSatProvider"; 9 | import {api} from "../utils/api"; 10 | import {OrderDetail} from "../components/OrderDetail"; 11 | import {EventEmitter} from "ahooks/lib/useEventEmitter"; 12 | import {NetworkType} from "../types"; 13 | 14 | enum InscribeType { 15 | text, 16 | file, 17 | } 18 | 19 | export type InscribeFileData = { 20 | filename: string 21 | dataURL: string 22 | size: number 23 | type?: string 24 | 25 | } 26 | 27 | export function Inscribe({newOrder$}: { 28 | newOrder$: EventEmitter 29 | }) { 30 | const {address, isConnected, network} = useUnisat(); 31 | const [inscribeType, setInscribeType] = useState(InscribeType.text) 32 | const [fileList, setFileList] = useState([]) 33 | const [receiveAddress, setReceiveAddress] = useState('') 34 | const [outputValue, setOutputValue] = useState(546) 35 | const [feeRate, setFeeRate] = useState() 36 | const [devFee, setDevFee] = useState(0) 37 | const [devAddress, setDevAddress] = useState('') 38 | 39 | const [loading, setLoading] = useState(false) 40 | 41 | const [orderId, setOrderId] = useState('') 42 | 43 | useEffect(() => { 44 | setReceiveAddress('') 45 | }, [network]); 46 | 47 | function submitText(text: string) { 48 | setFileList([ 49 | { 50 | filename: text.slice(0, 512), 51 | dataURL: `data:text/plain;charset=utf-8;base64,${stringToBase64(text)}`, 52 | size: getStringByteCount(text) 53 | } 54 | ]) 55 | } 56 | 57 | 58 | async function createOrder() { 59 | try { 60 | if (!receiveAddress) { 61 | return message.error('Please enter receive address') 62 | } 63 | if (!outputValue) { 64 | return message.error('Please enter output value') 65 | } 66 | if (!feeRate) { 67 | return message.error('Please enter fee rate') 68 | } 69 | if (fileList.length === 0) { 70 | return message.error('Please enter content') 71 | } 72 | 73 | setLoading(true) 74 | 75 | const {orderId} = await api.createOrder({ 76 | receiveAddress, 77 | feeRate, 78 | outputValue, 79 | files: fileList.map(item => ({dataURL: item.dataURL, filename: item.filename})), 80 | devAddress, 81 | devFee, 82 | }) 83 | 84 | newOrder$.emit() 85 | 86 | setOrderId(orderId) 87 | } catch (e) { 88 | handleError(e) 89 | } finally { 90 | setLoading(false) 91 | } 92 | 93 | } 94 | 95 | return 96 | 97 | { 103 | setFileList([]) 104 | setInscribeType(v.target.value) 105 | }} value={inscribeType}> 106 | Text 107 | File 108 | } 109 | /> 110 | { 111 | inscribeType === InscribeType.text && 112 | } 113 | { 114 | inscribeType === InscribeType.file && 115 | 116 | } 117 | 118 | setReceiveAddress(e.target.value)}/> 125 | } 126 | extra={ 127 | isConnected 128 | ? 131 | : null 132 | } 133 | /> 134 | setOutputValue(v || 0)}/>} 141 | /> 142 | { 147 | setFeeRate(value as number) 148 | }} 149 | min={1} 150 | max={1000} 151 | />} 152 | valueSpan={6} 153 | extra={} 157 | /> 158 | setDevAddress(e.target.value)}/> 165 | } 166 | /> 167 | setDevFee(v || 0)}/>} 174 | /> 175 | 182 | Inscribe 184 | }/> 185 | 186 | 187 | {orderId && { 188 | setOrderId('') 189 | }}/>} 190 | 191 | } -------------------------------------------------------------------------------- /unisat-inscribe-demo/src/pages/OrderList.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Flex, Table} from "antd"; 2 | import {useCallback, useEffect, useState} from "react"; 3 | import {api} from "../utils/api"; 4 | import {InscribeOrderData} from "../utils/api-types"; 5 | import {OrderDetail} from "../components/OrderDetail"; 6 | import {handleError} from "../utils/utils"; 7 | import {ReloadOutlined} from "@ant-design/icons"; 8 | import {useUnisat} from "../provider/UniSatProvider"; 9 | import {EventEmitter} from "ahooks/lib/useEventEmitter"; 10 | import {useRequest} from "ahooks"; 11 | 12 | const pageSize = 10 13 | 14 | interface OrderListProps { 15 | newOrder$: EventEmitter 16 | } 17 | 18 | export function OrderList({newOrder$}: OrderListProps) { 19 | const {network} = useUnisat(); 20 | const [list, setList] = useState() 21 | const [page, setPage] = useState(1) 22 | const [total, setTotal] = useState(0) 23 | 24 | const [orderId, setOrderId] = useState('') 25 | 26 | const { loading,run} = useRequest(() => api.listOrder({ 27 | size: pageSize, 28 | cursor: (page - 1) * pageSize, 29 | sort: 'desc', 30 | }), { 31 | refreshDeps: [page, network], 32 | onSuccess: (res) => { 33 | setList(res.list) 34 | setTotal(res.total) 35 | }, 36 | onError:handleError 37 | }) 38 | 39 | newOrder$.useSubscription(run) 40 | 41 | return 42 | Order List 43 |