diff --git a/docs/docs/build/bob-sdk/gateway.md b/docs/docs/build/bob-sdk/gateway.md index 426878d6..b66c3bdf 100644 --- a/docs/docs/build/bob-sdk/gateway.md +++ b/docs/docs/build/bob-sdk/gateway.md @@ -105,18 +105,46 @@ const { uuid, psbtBase64 } = await gatewaySDK.startOrder(quote, quoteParams); Create a Bitcoin transaction that sends the quoted `amount` of BTC to the LP's `bitcoinAddress`. This also publishes a hash of the order's parameters in the `OP_RETURN` of the transaction so the Gateway can trustlessly verify the order on BOB. -:::tip Connecting to Bitcoin wallets -We recommend using our [sats-wagmi](./sats-wagmi.md) package to interact with your user's Bitcoin wallet. -::: + + -```ts -import { base64 } from "@scure/base"; -import { Transaction } from "@scure/btc-signer"; +Please follow the [guide here](./sats-wagmi.md) to install and use sats-wagmi. In this example, we sign the `psbtBase64` using sats-wagmi which abstracts the complex wallet logic for multiple connectors (including OKX, UniSat and Xverse). +It is also possible to directly use the `useSendGatewayTransaction` hook, example below. + +```tsx +const { uuid, psbtBase64 } = await gatewaySDK.startOrder(quote, quoteParams); +const bitcoinTxHex = await connector.signAllInputs(psbtBase64!); +await gatewaySDK.finalizeOrder(uuid, bitcoinTxHex); +``` + + + -// SIGN THIS! -const tx = Transaction.fromPSBT(base64.decode(psbtBase64!)); +Please refer to the [OKX docs](https://www.okx.com/web3/build/docs/sdks/chains/bitcoin/introduce) for more information. +In this example, instead of signing the `psbtBase64` we instead use the in-built wallet methods to directly send the BTC. + +```ts +const { uuid, bitcoinAddress, satoshis, opReturnHash } = await gatewaySDK.startOrder(quote, quoteParams); +const { txhash } = await window.okxwallet.bitcoin.send({ from: quoteParams.fromUserAddress, to: bitcoinAddress, value: satoshis.toString(), memo: opReturnHash }) +await gatewaySDK.finalizeOrder(uuid, txhash); ``` + + + + +Please refer to the Dynamic guide on [PSBT signing](https://docs.dynamic.xyz/wallets/using-wallets/bitcoin/sign-a-psbt). + + + + + +Please refer to the Particle guide on [BTC Connect](https://developers.particle.network/guides/integrations/partners/bob#connecting-bitcoin-wallets-to-bob-using-btc-connect). + + + + + ### Finalize the Order Submit the Bitcoin transaction as proof of transfer. This completes the process by transferring wrapped Bitcoin and ETH to the user's EVM address on BOB. @@ -136,6 +164,25 @@ Get an array of pending and completed orders for a specific EVM address. Typical const orders = await gatewaySDK.getOrders(userAddress); ``` +### Example - sats-wagmi + + + + +```js reference title="Gateway.tsx" +https://github.com/bob-collective/sats-wagmi/blob/ae876d96bb2e54e5a24e0f3e1aaa6799565169e4/playgrounds/vite-react/src/Gateway.tsx#L1-L37 +``` + + + + +```js reference title="useSendGatewayTransaction.tsx" +https://github.com/bob-collective/sats-wagmi/blob/ae876d96bb2e54e5a24e0f3e1aaa6799565169e4/packages/sats-wagmi/src/hooks/useSendGatewayTransaction.tsx#L28-L69 +``` + + + + ## Conclusion BOB Gateway enables staking, swapping, lending, and bridging Bitcoin with a single transaction. The BOB SDK makes it possible for you to bring Gateway and Bitcoin LSTs directly to your users. diff --git a/docs/docs/build/bob-sdk/sats-wagmi.md b/docs/docs/build/bob-sdk/sats-wagmi.md index 405ec31c..8d0ed481 100644 --- a/docs/docs/build/bob-sdk/sats-wagmi.md +++ b/docs/docs/build/bob-sdk/sats-wagmi.md @@ -2,7 +2,7 @@ sidebar_position: 4 --- -# sats-wagmi Bitcoin Wallet React Hooks +# sats-wagmi - Reactive primitives for Bitcoin apps sats-wagmi is a library with a handful of BTC wallet connectors, leaving aside the need of the developer to integrate each one individually. The library also exports useful React hooks that mimic the standard followed in the EVM [wagmi](https://wagmi.sh/react/getting-started) library. @@ -13,104 +13,131 @@ sats-wagmi is a library with a handful of BTC wallet connectors, leaving aside t - Unisat - Leather - Xverse - - Bitget (soon) + - Bitget + - OKX Wallet - BTC functionality: - - send bitcoin - - inscribe (text and images) - - send inscription - - sign input (psbt) + - send BTC + - sign PSBTs - React hooks ## Installation -To use sats-wagmi, all you need to do is install the -`@gobob/sats-wagmi`: +To use sats-wagmi, all you need to do is install `@gobob/sats-wagmi`: -```sh -# with Yarn -$ yarn add @gobob/sats-wagmi - -# with npm -$ npm i @gobob/sats-wagmi - -# with pnpm -$ pnpm add @gobob/sats-wagmi - -# with Bun -$ bun add @gobob/sats-wagmi +```bash npm2yarn +npm install @gobob/sats-wagmi ``` -## Usage +## Connect Wallet -### Connector +### 1. Wrap App in Context Provider -```ts -import { MMSnapConnector } from "./connectors"; +To start, we will need to wrap our React App with Context so that our application is aware of sats-wagmi & React Query's reactive state and in-memory caching: -const mmSnap = new MMSnapConnector(network); +```tsx + // 1. Import modules +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { SatsWagmiConfig } from "@gobob/sats-wagmi"; + +// 2. Set up a React Query client. +const queryClient = new QueryClient() -mmSnap.connect(); +function App() { + // 3. Wrap app with sats-wagmi and React Query context. + return ( + + + {/** ... */} + + + ) +} ``` -### React Hooks +### 2. Display Wallet Options + +After that, we will create a `WalletOptions` component that will display our connectors. This will allow users to select a wallet and connect. -1. Wrap your application with the `SatsWagmiConfig` provided by **@gobob/sats-wagmi**. +Below, we are rendering a list of `connectors` retrieved from `useConnect`. When the user clicks on a connector, the `connect` function will connect the users' wallet. ```tsx -import { SatsWagmiConfig } from "@gobob/sats-wagmi"; +import * as React from 'react' +import { useConnect, SatsConnector } from "@gobob/sats-wagmi"; + +export function WalletOptions() { + const { connectors, connect } = useConnect() -// Do this at the root of your application -function App({ children }) { - return {children}; + return connectors.map((connector) => ( + + )) } ``` -2. Now start by connecting: +### 3. Display Connected Account -```tsx -import { useConnect, SatsConnector } from "@gobob/sats-wagmi"; +Lastly, if an account is connected, we want to show the connected address. + +We are utilizing `useAccount` to extract the account and `useDisconnect` to show a "Disconnect" button so a user can disconnect their wallet. -function Example() { - const { connectors, connect } = useConnect(); +```tsx +import { useAccount, useDisconnect } from "@gobob/sats-wagmi"; - const handleConnect = (connector: SatsConnector) => { - connect({ - connector, - }); - }; +function Account() { + const { address } = useAccount() + const { disconnect } = useDisconnect() return (
- {connectors.map((connector) => ( - - ))} +

Address: {address}

+
); } ``` -3. Once connected, you should be able to use the connector utility and have access to the connected BTC account: +## Send Transaction + +Create your `SendTransaction` component that will contain the send transaction logic. ```tsx -import { useConnect, SatsConnector } from "@gobob/sats-wagmi"; +import type { FormEvent } from 'react'; +import { type Hex, parseUnits } from 'viem'; +import { useSendTransaction } from "@gobob/sats-wagmi"; + +function SendTransaction() { + const { data: hash, error, isPending, sendTransaction } = useSendTransaction(); + + async function submit(e: FormEvent) { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const to = formData.get('address') as Hex; + const value = formData.get('value') as string; -function Example() { - const { address, connector } = useSatsAccount(); + sendTransaction({ to, value: parseUnits(value, 8) }); + } - const handleTransfer = () => { - connector?.sendToAddress( - "tb1p9gl248kp19jgennea98e2tv8acfrvfv0yws2tc5j6u72e84caapsh2hexs", - 100000000 - ); - }; + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash, + }) return (
-

Address: {address}

- +

Send Transaction

+
+ + + +
+ {hash &&
Transaction Hash: {hash}
} + {isConfirming && 'Waiting for confirmation...'} + {isConfirmed && 'Transaction confirmed.'} + {error &&
Error: {error.message}
}
); } -``` +``` \ No newline at end of file diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index b5871196..8decd086 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -65,6 +65,13 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ + codeblock: { + showGithubLink: true, + githubLinkLabel: 'View on GitHub', + showRunmeLink: false, + runmeLinkLabel: 'Checkout via Runme' + }, + image: "img/bob-social-card.png", metadata: [ { @@ -210,6 +217,7 @@ const config = { language: ["en"], }, ], + 'docusaurus-theme-github-codeblock', ], scripts: [ { diff --git a/docs/package.json b/docs/package.json index 706c6111..639e91be 100644 --- a/docs/package.json +++ b/docs/package.json @@ -29,6 +29,7 @@ "@scure/btc-signer": "^1.3.2", "bitcoin-address-validation": "^2.2.3", "clsx": "^2.0.0", + "docusaurus-theme-github-codeblock": "^2.0.2", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0" @@ -56,4 +57,4 @@ "engines": { "node": ">=16.14" } -} +} \ No newline at end of file diff --git a/sdk/src/esplora.ts b/sdk/src/esplora.ts index 44f54eaa..936b31fa 100644 --- a/sdk/src/esplora.ts +++ b/sdk/src/esplora.ts @@ -71,9 +71,9 @@ export interface Transaction { }> status: { confirmed: boolean - block_height: number - block_hash: string - block_time: number + block_height?: number + block_hash?: string + block_time?: number } } diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index 910b5e2d..c09a5b0d 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -16,6 +16,8 @@ import { } from "./types"; import { SYMBOL_LOOKUP, ADDRESS_LOOKUP } from "./tokens"; import { createBitcoinPsbt } from "../wallet"; +import { Network } from "bitcoin-address-validation"; +import { EsploraClient } from "../esplora"; type Optional = Omit & Partial; @@ -148,6 +150,10 @@ export class GatewayApiClient { gatewayQuote: GatewayQuote, params: Optional, ): Promise { + if (!params.toUserAddress.startsWith("0x") || !ethers.isAddress(params.toUserAddress)) { + throw new Error("Invalid user address"); + } + const request: GatewayCreateOrderRequest = { gatewayAddress: gatewayQuote.gatewayAddress, strategyAddress: gatewayQuote.strategyAddress, @@ -209,10 +215,20 @@ export class GatewayApiClient { * broadcast the transaction. This is step 2 of 2, see the {@link startOrder} method. * * @param uuid The id given by the {@link startOrder} method. - * @param bitcoinTxHex The hex encoded Bitcoin transaction. + * @param bitcoinTxOrId The hex encoded Bitcoin transaction or txid. * @returns {Promise} The Bitcoin txid. */ - async finalizeOrder(uuid: string, bitcoinTxHex: string): Promise { + async finalizeOrder(uuid: string, bitcoinTxOrId: string): Promise { + bitcoinTxOrId = stripHexPrefix(bitcoinTxOrId); + + let bitcoinTxHex: string; + if (bitcoinTxOrId.length === 64) { + const esploraClient = new EsploraClient(this.chain === Chain.BOB ? Network.mainnet : Network.testnet); + bitcoinTxHex = await esploraClient.getTransactionHex(bitcoinTxOrId); + } else { + bitcoinTxHex = bitcoinTxOrId; + } + const response = await fetch(`${this.baseUrl}/order/${uuid}`, { method: "PATCH", headers: { @@ -354,3 +370,11 @@ function calculateOpReturnHash(req: GatewayCreateOrderRequest) { ), ); } + +function isHexPrefixed(str: string): boolean { + return str.slice(0, 2) === "0x"; +} + +function stripHexPrefix(str: string): string { + return isHexPrefixed(str) ? str.slice(2) : str; +} diff --git a/sdk/test/gateway.test.ts b/sdk/test/gateway.test.ts index d969e1ed..170cb689 100644 --- a/sdk/test/gateway.test.ts +++ b/sdk/test/gateway.test.ts @@ -133,6 +133,18 @@ describe("Gateway Tests", () => { opReturnHash: "0x10e69ac36b8d7ae8eb1dca7fe368da3d27d159259f48d345ff687ef0fcbdedcd", }); + await expect(async () => { + await gatewaySDK.startOrder(mockQuote, { + toChain: "BOB", + toToken: "tBTC", + toUserAddress: "2N8DbeaBdjkktkRzaKL1tHj9FQELV7jA8Re", + amount: 1000, + fromChain: "Bitcoin", + fromToken: "BTC", + fromUserAddress: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + }); + }).rejects.toThrowError("Invalid user address"); + const result = await gatewaySDK.startOrder(mockQuote, { toChain: "BOB", toToken: "tBTC",