From 80f0dc2342b3b035d33b4389517bb22784eb27e9 Mon Sep 17 00:00:00 2001 From: gin-lsl Date: Thu, 26 Dec 2024 10:28:33 +0800 Subject: [PATCH] feat(bitcoin): Add phantom wallet adapter (#1267) * feat(bitcoin): Add phantom wallet adapter * changeset * type def * Update packages/bitcoin/src/adapter/wallets/phantom.ts Co-authored-by: thinkasany <480968828@qq.com> * doc * fix eslint * x * build error * get balance * doc * impl getInscriptions * ignore test coverage * update demo * doc * add NotImplementError * update lock * fix dep version * chore: update lock --------- Co-authored-by: thinkasany <480968828@qq.com> --- .changeset/mighty-dogs-move.md | 5 + packages/bitcoin/package.json | 3 +- packages/bitcoin/src/adapter/wallets/index.ts | 1 + .../bitcoin/src/adapter/wallets/phantom.ts | 131 ++++++++++++++++++ packages/bitcoin/src/error.ts | 9 ++ packages/bitcoin/src/global.d.ts | 1 + packages/bitcoin/src/index.ts | 1 + packages/bitcoin/src/wallets/index.ts | 15 +- packages/web3/src/bitcoin/demos/basic.tsx | 5 +- .../src/bitcoin/demos/get-inscriptions.tsx | 5 +- .../web3/src/bitcoin/demos/send-transfer.tsx | 12 +- packages/web3/src/bitcoin/demos/sign.tsx | 26 ++-- packages/web3/src/bitcoin/index.md | 5 + packages/web3/src/bitcoin/index.zh-CN.md | 5 + pnpm-lock.yaml | 123 ++++++++-------- 15 files changed, 276 insertions(+), 71 deletions(-) create mode 100644 .changeset/mighty-dogs-move.md create mode 100644 packages/bitcoin/src/adapter/wallets/phantom.ts diff --git a/.changeset/mighty-dogs-move.md b/.changeset/mighty-dogs-move.md new file mode 100644 index 000000000..733824b9e --- /dev/null +++ b/.changeset/mighty-dogs-move.md @@ -0,0 +1,5 @@ +--- +'@ant-design/web3-bitcoin': minor +--- + +feat(bitcoin): Add phantom wallet adapter diff --git a/packages/bitcoin/package.json b/packages/bitcoin/package.json index 315a070ba..e48a64a49 100644 --- a/packages/bitcoin/package.json +++ b/packages/bitcoin/package.json @@ -44,7 +44,8 @@ "dependencies": { "@ant-design/web3-common": "workspace:*", "@ant-design/web3-icons": "workspace:*", - "sats-connect": "^2.4.0" + "sats-connect": "~2.4.0", + "uint8array-tools": "~0.0.9" }, "devDependencies": { "father": "^4.4.4", diff --git a/packages/bitcoin/src/adapter/wallets/index.ts b/packages/bitcoin/src/adapter/wallets/index.ts index ebb60b63c..65c0ce494 100644 --- a/packages/bitcoin/src/adapter/wallets/index.ts +++ b/packages/bitcoin/src/adapter/wallets/index.ts @@ -1,3 +1,4 @@ export * from './unisat'; export * from './xverse'; export * from './okx'; +export * from './phantom'; diff --git a/packages/bitcoin/src/adapter/wallets/phantom.ts b/packages/bitcoin/src/adapter/wallets/phantom.ts new file mode 100644 index 000000000..77bbe2742 --- /dev/null +++ b/packages/bitcoin/src/adapter/wallets/phantom.ts @@ -0,0 +1,131 @@ +/* v8 ignore start */ +import type { Account } from '@ant-design/web3-common'; +import { fromHex, fromUtf8, toBase64, toHex } from 'uint8array-tools'; + +import { NoAddressError, NoProviderError, NotImplementedError } from '../../error'; +import { getBalanceByMempool, getInscriptionsByAddress } from '../../helpers'; +import type { SignPsbtParams, TransferParams } from '../../types'; +import type { BitcoinWallet } from '../useBitcoinWallet'; + +type AccountType = { + address: string; + addressType: string; + publicKey: string; + purpose: string; +}; + +/** + * @link https://docs.phantom.app/bitcoin/provider-api-reference#options-parameters + */ +type PhantomSignPsbtOptions = { + sigHash?: number; + address: string; + signingIndexes: number[]; +}[]; + +export class PhantomBitcoinWallet implements BitcoinWallet { + name: string; + provider?: any; + account?: Account; + payment?: string; + + constructor(name: string) { + this.name = name; + this.provider = window.phantom?.bitcoin; + this.account = undefined; + } + + connect = async () => { + if (!this.provider) { + throw new NoProviderError(); + } + + try { + const accounts: AccountType[] = await this.provider.requestAccounts(); + const ordinals = accounts.find((acc) => acc.purpose === 'ordinals'); + const payment = accounts.find((acc) => acc.purpose === 'payment'); + + this.account = ordinals ? { address: ordinals.address } : undefined; + this.payment = payment?.address; + } catch (error) { + throw error; + } + }; + + getBalance = async () => { + if (!this.payment) { + throw new NoAddressError(); + } + + const balance = await getBalanceByMempool(this.payment); + return balance; + }; + + signMessage = async (message: string) => { + if (!this.provider) { + throw new NoProviderError(); + } + + if (!this.account?.address) { + throw new NoAddressError(); + } + + const { signature } = await this.provider.signMessage(this.account.address, fromUtf8(message)); + + return toBase64(signature); + }; + + signPsbt = async ({ psbt, options = {} }: SignPsbtParams) => { + if (!this.provider) { + throw new NoProviderError(); + } + + if (!this.account?.address) { + throw new NoAddressError(); + } + + const serializedPsbt = fromHex(psbt); + + const { + // `broadcast` not supported + // broadcast, + signInputs, + signHash, + } = options; + const inputsToSign: PhantomSignPsbtOptions = []; + + if (signInputs) { + for (const address in signInputs) { + inputsToSign.push({ + sigHash: signHash, + address, + signingIndexes: signInputs[address], + }); + } + } + + const signedPsbt = await this.provider.signPSBT(serializedPsbt, { inputsToSign }); + const hexPsbt = toHex(signedPsbt); + + return { + psbt: hexPsbt, + }; + }; + + sendTransfer = async (params: TransferParams) => { + throw new NotImplementedError(); + }; + + getInscriptions = async (offset = 0, limit = 20) => { + if (!this.account?.address) { + throw new NoAddressError(); + } + + const inscriptions = await getInscriptionsByAddress({ + address: this.account.address, + offset, + limit, + }); + return inscriptions; + }; +} diff --git a/packages/bitcoin/src/error.ts b/packages/bitcoin/src/error.ts index 8fcba8cc8..56152120b 100644 --- a/packages/bitcoin/src/error.ts +++ b/packages/bitcoin/src/error.ts @@ -35,3 +35,12 @@ export class NoInscriptionError extends Error { this.name = this.constructor.name; } } + +export class NotImplementedError extends Error { + name: string; + + constructor(message = 'Not implemented') { + super(message); + this.name = this.constructor.name; + } +} diff --git a/packages/bitcoin/src/global.d.ts b/packages/bitcoin/src/global.d.ts index ac5f38718..b38a8c1b1 100644 --- a/packages/bitcoin/src/global.d.ts +++ b/packages/bitcoin/src/global.d.ts @@ -2,6 +2,7 @@ declare interface Window { unisat?: Unisat.Provider; // TODO: 与其他 okx 冲突 okxwallet?: any; + phantom?: any; } declare namespace Unisat { diff --git a/packages/bitcoin/src/index.ts b/packages/bitcoin/src/index.ts index ca97eaba4..2ac21cfaf 100644 --- a/packages/bitcoin/src/index.ts +++ b/packages/bitcoin/src/index.ts @@ -1,4 +1,5 @@ export * from './provider'; export * from './wallets'; export * from './types'; +export * from './error'; export { useBitcoinWallet } from './adapter/useBitcoinWallet'; diff --git a/packages/bitcoin/src/wallets/index.ts b/packages/bitcoin/src/wallets/index.ts index 656cad75c..dce6be494 100644 --- a/packages/bitcoin/src/wallets/index.ts +++ b/packages/bitcoin/src/wallets/index.ts @@ -1,9 +1,20 @@ /* v8 ignore start */ -import { metadata_OkxWallet, metadata_Unisat, metadata_Xverse } from '@ant-design/web3-assets'; +import { + metadata_OkxWallet, + metadata_Phantom, + metadata_Unisat, + metadata_Xverse, +} from '@ant-design/web3-assets'; -import { OkxBitcoinWallet, UnisatBitcoinWallet, XverseBitcoinWallet } from '../adapter'; +import { + OkxBitcoinWallet, + PhantomBitcoinWallet, + UnisatBitcoinWallet, + XverseBitcoinWallet, +} from '../adapter'; import { WalletFactory } from './factory'; export const UnisatWallet = () => WalletFactory(UnisatBitcoinWallet, metadata_Unisat); export const XverseWallet = () => WalletFactory(XverseBitcoinWallet, metadata_Xverse); export const OkxWallet = () => WalletFactory(OkxBitcoinWallet, metadata_OkxWallet); +export const PhantomWallet = () => WalletFactory(PhantomBitcoinWallet, metadata_Phantom); diff --git a/packages/web3/src/bitcoin/demos/basic.tsx b/packages/web3/src/bitcoin/demos/basic.tsx index 12f49d3b7..dda2a873a 100644 --- a/packages/web3/src/bitcoin/demos/basic.tsx +++ b/packages/web3/src/bitcoin/demos/basic.tsx @@ -2,6 +2,7 @@ import { ConnectButton, Connector } from '@ant-design/web3'; import { BitcoinWeb3ConfigProvider, OkxWallet, + PhantomWallet, UnisatWallet, XverseWallet, } from '@ant-design/web3-bitcoin'; @@ -12,7 +13,9 @@ import { */ const App: React.FC = () => { return ( - + { */ const App: React.FC = () => { return ( - + diff --git a/packages/web3/src/bitcoin/demos/send-transfer.tsx b/packages/web3/src/bitcoin/demos/send-transfer.tsx index 4164ad94b..1281a6a0b 100644 --- a/packages/web3/src/bitcoin/demos/send-transfer.tsx +++ b/packages/web3/src/bitcoin/demos/send-transfer.tsx @@ -1,7 +1,9 @@ import { ConnectButton, Connector } from '@ant-design/web3'; import { BitcoinWeb3ConfigProvider, + NotImplementedError, OkxWallet, + PhantomWallet, UnisatWallet, useBitcoinWallet, XverseWallet, @@ -25,6 +27,11 @@ const SendBitcoin: React.FC = () => { sats: 10000, }); } catch (error) { + if (error instanceof NotImplementedError) { + console.log('Not implemented'); + return; + } + console.log('sign message error:', error); } }} @@ -40,7 +47,10 @@ const SendBitcoin: React.FC = () => { */ const App: React.FC = () => { return ( - + diff --git a/packages/web3/src/bitcoin/demos/sign.tsx b/packages/web3/src/bitcoin/demos/sign.tsx index cfae76357..770b9e83f 100644 --- a/packages/web3/src/bitcoin/demos/sign.tsx +++ b/packages/web3/src/bitcoin/demos/sign.tsx @@ -1,19 +1,28 @@ +import React from 'react'; import { ConnectButton, Connector } from '@ant-design/web3'; +import { metadata_Phantom } from '@ant-design/web3-assets'; import { BitcoinWeb3ConfigProvider, OkxWallet, + PhantomWallet, UnisatWallet, useBitcoinWallet, XverseWallet, } from '@ant-design/web3-bitcoin'; import { Button, Space } from 'antd'; +const PSBT_FOR_PHANTOM = + '70736274ff0100fd940102000000048a841e4104389e3b5b04c1522ed7bfc0e64c3760cb801a2ee2406d1c2373d81d0300000000ffffffff8a841e4104389e3b5b04c1522ed7bfc0e64c3760cb801a2ee2406d1c2373d81d0400000000ffffffffaf3341cffbd7ce8b4a51dec0f358a21809fad67be1a11bf70b86e83ec4567ace0100000000ffffffff8eceb072b7c47ebd9c1aa2e17c2541145835c70a8667eb21fa2ff3cac503170e0600000000ffffffff07b004000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed04a01000000000000225120b1a548f1672b6bc666e23943b0b138a4216e2ce5f9bc687469e0f52a917bbf274b0100000000000017a91433ab469b293fa7700f0954c96ec630895892f189874402000000000000160014c015c65276d5f38d599d445c4cb03aa7aa0dc3655802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed072fe050000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed000000000000100fdf50102000000000101a1f8bb2bde4e13b2f397a82a8a101b378e90af13354710b93742be79b773e3840000000000ffffffff0b5802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed00d4c070000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed00247304402204d2e9da8a14537a1c37b59e6a7eb828816249747dbec041315ade14a181c826a022044dcc381227cef6ab18431e692dccc7c27e2a10fed4d3131e0523094819d1dda0121028b8437ac47a4434d4b86d19ab7eeb255887f75cfa43ca135168bfc8281ae8bb90000000001011f5802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed0000100fdf50102000000000101a1f8bb2bde4e13b2f397a82a8a101b378e90af13354710b93742be79b773e3840000000000ffffffff0b5802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed00d4c070000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed00247304402204d2e9da8a14537a1c37b59e6a7eb828816249747dbec041315ade14a181c826a022044dcc381227cef6ab18431e692dccc7c27e2a10fed4d3131e0523094819d1dda0121028b8437ac47a4434d4b86d19ab7eeb255887f75cfa43ca135168bfc8281ae8bb90000000001011f5802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed0000100fd6f0302000000000104a81c37519eca22bd78a4705230dcc471fb59f285bb2f22165c803de5bbe10b4c08000000171600147e3a872889766d0e3aebacf19f7936fd659b2605ffffffffa81c37519eca22bd78a4705230dcc471fb59f285bb2f22165c803de5bbe10b4c03000000171600147e3a872889766d0e3aebacf19f7936fd659b2605ffffffff33ec9dbdfa3ecb3654683b54855eac430d41b9b87b028f8cc84c6766bf7cc3870100000000ffffffff14e64f84c47029f5b8687519707c6e18080f675f37323746c695a681b0fdfece06000000171600147e3a872889766d0e3aebacf19f7936fd659b2605ffffffff07b00400000000000017a91433ab469b293fa7700f0954c96ec630895892f189874a01000000000000225120a9af1fae42dbe9b0604e0ed64ae6e26d56d1c9bbcc4e1e66326914bdb5149606591a000000000000225120cfb72754409e197bce4ea0670bd4f977104bcc86f7ea65d8981609dc66e984454402000000000000160014c015c65276d5f38d599d445c4cb03aa7aa0dc365580200000000000017a91433ab469b293fa7700f0954c96ec630895892f18987580200000000000017a91433ab469b293fa7700f0954c96ec630895892f18987d8c300000000000017a91433ab469b293fa7700f0954c96ec630895892f189870247304402204c291efdec8e7442aef45ad4751938ae21b847bedde8ce8c9eb208f7a29537880220247a6767215d12c0c5ebc751b6386c50c3a9d5ba0792e0396e2ae420b736a5ab0121032765788cbacba654f3cdf679026db3cdb5a5d0310f870f4da223926a2813259902483045022100bceed8f98e1fbed310cfc01e2bdd3dcbd146064c6fc9d423e787367602d8df7c02204e300e2c9e6c4be50f2c9db54c2f6aeb5eae1897db38bf6406f16ee2e3de4f5e0121032765788cbacba654f3cdf679026db3cdb5a5d0310f870f4da223926a2813259901414045d5e077660865bde16a0af320b426f636b49ea7674f1cc1a9d7c86643638cdbb21c9a281c79aeaa118d833840eca4ec0b3cc6a807f1900bb7395ab4eb72ff830247304402204fdc2e31b88b42081c42dc6c249297aeffe1147071fa6994f95ded6257d93d9602205bf9c30fc7cf0d2b78a1e0027e1cc4eae6e45aef7281017fac932058ee1674d70121032765788cbacba654f3cdf679026db3cdb5a5d0310f870f4da223926a281325990000000001012b4a01000000000000225120a9af1fae42dbe9b0604e0ed64ae6e26d56d1c9bbcc4e1e66326914bdb51496060117207f003df863d4c17579f7b22f7498e39525b81c947aef234859b0466d3dd8dfb3000100fd2603020000000001048a841e4104389e3b5b04c1522ed7bfc0e64c3760cb801a2ee2406d1c2373d81d0800000000ffffffff8a841e4104389e3b5b04c1522ed7bfc0e64c3760cb801a2ee2406d1c2373d81d0500000000ffffffff9584f0b205109f6790fbf08f782166892f22facb8e2598b705cd9136b25aabc30000000000ffffffff4f4b0bbc053b9e3cb6261cb83c063481eba8a8a32a448c63f1c1218d6fe330380600000000ffffffff07b004000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed02202000000000000225120b1a548f1672b6bc666e23943b0b138a4216e2ce5f9bc687469e0f52a917bbf2775120000000000002251208ef171ead3a8f91d9dca9ae4b645537aafada16559f9f714cbe524ef672ab4144402000000000000160014c015c65276d5f38d599d445c4cb03aa7aa0dc3655802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed05802000000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed04057060000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed002483045022100a5cf71b0115db7980055fa9158c24fca9e3cc09cca0d1a0c54f17e8fa92d3e2402201b7dce27ff09e06c40a87b54aa2db693f49291e2f1a32f055155a3750607d01e0121028b8437ac47a4434d4b86d19ab7eeb255887f75cfa43ca135168bfc8281ae8bb902473044022049edbc2650ed737bad10d567c072da54c6b592fcfe2240fabcd49d8aa176422102206c8101ba6f8d6a155ed916ca7b25bf69395d76c020cb6ed9cf8293d65870346e0121028b8437ac47a4434d4b86d19ab7eeb255887f75cfa43ca135168bfc8281ae8bb901410a4a56764a5f9ed3fee4a869f6edee7b9b72f87bdba96d08310191a6df94bcf56f32152e63a836844cea71611886e3f96f74a2cc06d88ddf508184857f5fb4138302473044022075b779781668f4a487c2308aca9217125b897d089f617d831122612db02a672402200ef61ec6ddb6cac999e528e9e4fe27acb0a1cba4f8b33204b180ee458a8d82790121028b8437ac47a4434d4b86d19ab7eeb255887f75cfa43ca135168bfc8281ae8bb90000000001011f4057060000000000160014b2c9b35e4b02e8b16236ae5c0540cf2d56605ed00000000000000000'; + +const PSBT = + 'cHNidP8BAF4CAAAAAa/v4ZPYjm+iJc1pB3IybYY6wPpScPDlxvHmNE557J2vAQAAAAD/////AWqKAAAAAAAAIlEgZDcUdAs/gCZIkazJyMw1I54n2QGxN1W2ph6m+4zYHBkAAAAACPwCbWUDc2lnQE+yrULMRi3UwxQDf8idtfykJVzjE08jIP9fdU/6yvEfdlqCAWNwXFgSx1Nb7jrfPFYlY7gLaQ87EpcDpwaLdzQL/AJtZQZzaWdleHAIQnj3E+QqoAAAAQErIgIAAAAAAAAiUSBkNxR0Cz+AJkiRrMnIzDUjnifZAbE3VbamHqb7jNgcGQEDBIMAAAABFyCauIGVY+9bxYwyEp3poW+sSayOhwQuSrI4DnH80/zCuwAA'; + /** * Component to sign a message. - * @returns {JSX.Element | null} The rendered component. */ const SignMessage: React.FC = () => { const { signMessage, account } = useBitcoinWallet(); + return account ? (