Skip to content

Commit

Permalink
Merge pull request #873 from ant-design/feat/bitcoin-psbt
Browse files Browse the repository at this point in the history
feat(bitcoin): add signPsbt API
  • Loading branch information
AmAzing129 committed May 15, 2024
2 parents bf21176 + 73886a6 commit 823f46e
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 115 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-cobras-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ant-design/web3-bitcoin": minor
---

feat: support signPsbt api
2 changes: 1 addition & 1 deletion packages/bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@ant-design/web3-common": "workspace:*",
"@ant-design/web3-icons": "workspace:*",
"@mempool/mempool.js": "^2.3.0",
"sats-connect": "^2.1.0"
"sats-connect": "^2.3.1"
},
"devDependencies": {
"father": "^4.4.0",
Expand Down
13 changes: 6 additions & 7 deletions packages/bitcoin/src/adapter/useBitcoinWallet.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import React from 'react';
import type { Account, Balance } from '@ant-design/web3-common';

import type { SignPsbtParams, SignPsbtResult, TransferParams } from '../types';

export interface BitcoinWallet {
name: string;
provider: any;
account?: Account;
getBalance: () => Promise<Balance | undefined>;
getBalance: () => Promise<Balance>;
connect: () => Promise<void>;
signMessage: (message: string) => Promise<string | undefined>;
sendTransfer: (
to: string,
sats: number,
options?: { feeRate: number },
) => Promise<string | undefined>;
signMessage: (message: string) => Promise<string>;
sendTransfer: (prams: TransferParams) => Promise<string>;
signPsbt: (params: SignPsbtParams) => Promise<SignPsbtResult>;
}

export const BitcoinAdapterContext = React.createContext<BitcoinWallet>({} as BitcoinWallet);
Expand Down
55 changes: 43 additions & 12 deletions packages/bitcoin/src/adapter/wallets/unisat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Account, Balance } from '@ant-design/web3-common';

import { NoProviderError } from '../../error';
import { getBalanceObject } from '../../helpers';
import type { SignPsbtParams, SignPsbtResult, TransferParams } from '../../types';
import type { BitcoinWallet } from '../useBitcoinWallet';

export class UnisatBitcoinWallet implements BitcoinWallet {
Expand All @@ -15,35 +17,38 @@ export class UnisatBitcoinWallet implements BitcoinWallet {
}

connect = async (): Promise<void> => {
if (!this.provider) return;
if (!this.provider) {
throw new NoProviderError();
}
try {
const accounts = await this.provider.requestAccounts();
this.account = { address: accounts[0] };
} catch (e) {
throw e;
}
return;
};

getBalance = async (): Promise<Balance | undefined> => {
if (!this.provider) return;
getBalance = async (): Promise<Balance> => {
if (!this.provider) {
throw new NoProviderError();
}
const { confirmed } = await this.provider.getBalance();
const balance = getBalanceObject(confirmed);
return balance;
};

signMessage = async (msg: string): Promise<string | undefined> => {
if (!this.provider) return;
signMessage = async (msg: string): Promise<string> => {
if (!this.provider) {
throw new NoProviderError();
}
const signature = await this.provider.signMessage(msg);
return signature;
};

sendTransfer = async (
to: string,
sats: number,
options?: { feeRate: number },
): Promise<string | undefined> => {
if (!this.provider) return;
sendTransfer = async ({ to, sats, options }: TransferParams): Promise<string> => {
if (!this.provider) {
throw new NoProviderError();
}
let txid = '';
try {
txid = await this.provider.sendBitcoin(to, sats, options);
Expand All @@ -52,4 +57,30 @@ export class UnisatBitcoinWallet implements BitcoinWallet {
}
return txid;
};

signPsbt = async ({ psbt, options = {} }: SignPsbtParams): Promise<SignPsbtResult> => {
if (!this.provider) {
throw new NoProviderError();
}
const { broadcast = false, signInputs = {}, signHash } = options;
const toSignInputs = [];

// Convert xverse-compatible signInputs to unisat-compatible toSignInputs
for (const address in signInputs) {
for (const input of signInputs[address]) {
toSignInputs.push({
address,
index: input,
sighashTypes: [signHash],
});
}
}
const signedPsbt = await this.provider.signPsbt(psbt, {
autoFinalized: broadcast,
toSignInputs: toSignInputs.length === 0 ? undefined : toSignInputs,
});
return {
psbt: signedPsbt,
};
};
}
70 changes: 47 additions & 23 deletions packages/bitcoin/src/adapter/wallets/xverse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Account, Balance } from '@ant-design/web3-common';
import { AddressPurpose, getProviderById, request, type BitcoinProvider } from 'sats-connect';
import { AddressPurpose, getProviderById, request } from 'sats-connect';

import { NoAddressError, NoProviderError } from '../../error';
import { getBalanceByMempool } from '../../helpers';
import type { BitcoinProvider, SignPsbtParams, SignPsbtResult, TransferParams } from '../../types';
import type { BitcoinWallet } from '../useBitcoinWallet';

export class XverseBitcoinWallet implements BitcoinWallet {
Expand All @@ -16,7 +18,9 @@ export class XverseBitcoinWallet implements BitcoinWallet {
}

connect = async (): Promise<void> => {
if (!this.provider) return;
if (!this.provider) {
throw new NoProviderError();
}
const response = await request('getAccounts', {
purposes: [AddressPurpose.Ordinals, AddressPurpose.Payment],
});
Expand All @@ -28,14 +32,21 @@ export class XverseBitcoinWallet implements BitcoinWallet {
this.payment = payment.address;
};

getBalance = async (): Promise<Balance | undefined> => {
if (!this.payment) return;
getBalance = async (): Promise<Balance> => {
if (!this.payment) {
throw new NoAddressError();
}
const balance = await getBalanceByMempool(this.payment);
return balance;
};

signMessage = async (msg: string): Promise<string | undefined> => {
if (!this.account?.address || !this.provider) return;
signMessage = async (msg: string): Promise<string> => {
if (!this.provider) {
throw new NoProviderError();
}
if (!this.account?.address) {
throw new NoAddressError();
}
const response = await request('signMessage', {
address: this.account.address,
message: msg,
Expand All @@ -47,25 +58,38 @@ export class XverseBitcoinWallet implements BitcoinWallet {
}
};

sendTransfer = async (to: string, sats: number): Promise<string> => {
sendTransfer = async ({ to, sats }: TransferParams): Promise<string> => {
let txid = '';
try {
const response = await request('sendTransfer', {
recipients: [
{
address: to,
amount: sats,
},
],
});
if (response.status === 'success') {
txid = response.result.txid;
} else {
throw new Error(response.error.message);
}
} catch (e) {
throw e;
const response = await request('sendTransfer', {
recipients: [
{
address: to,
amount: sats,
},
],
});
if (response.status === 'success') {
txid = response.result.txid;
} else {
throw new Error(response.error.message);
}
return txid;
};

signPsbt = async ({ psbt, options }: SignPsbtParams): Promise<SignPsbtResult> => {
if (!this.provider) {
throw new NoProviderError();
}
const response = await request('signPsbt', {
psbt,
signInputs: options?.signInputs ?? {},
broadcast: !!options?.broadcast,
allowedSignHash: options?.signHash,
});
if (response.status === 'success') {
return response.result as SignPsbtResult;
} else {
throw new Error(response.error.message);
}
};
}
18 changes: 18 additions & 0 deletions packages/bitcoin/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class NoProviderError extends Error {
name: string;

constructor(message = 'No Bitcoin wallet installed') {
super(message);
// Ensure the name of this error is the same as the class name
this.name = this.constructor.name;
}
}

export class NoAddressError extends Error {
name: string;

constructor(message = "Can't get address from Bitcoin wallet") {
super(message);
this.name = this.constructor.name;
}
}
1 change: 1 addition & 0 deletions packages/bitcoin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './provider';
export * from './wallets';
export * from './types';
export { useBitcoinWallet } from './adapter/useBitcoinWallet';
19 changes: 19 additions & 0 deletions packages/bitcoin/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type { SignPsbtResult, BitcoinProvider } from 'sats-connect';

export interface SignPsbtOptions {
signInputs?: Record<string, number[]>;
broadcast?: boolean;
signHash?: number;
}

export interface SignPsbtParams {
psbt: string;
options?: SignPsbtOptions;
}

export interface TransferParams {
to: string;
sats: number;
// feeRate is only for unisat. Users can set their desired transaction fee by xverse.
options?: { feeRate: number };
}
8 changes: 4 additions & 4 deletions packages/web3/src/bitcoin/demos/send-transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const SendBitcoin: React.FC = () => {
onClick={async () => {
try {
// Don't send in main network!!
await sendTransfer(
'bc1pcdv3h6nuq705e3yk4pvdlqrcfchzvd9se9zwlhke3menvxlc58zshl0ryv',
10000,
);
await sendTransfer({
to: 'bc1pcdv3h6nuq705e3yk4pvdlqrcfchzvd9se9zwlhke3menvxlc58zshl0ryv',
sats: 10000,
});
} catch (error) {
console.log('sign message error:', error);
}
Expand Down
42 changes: 0 additions & 42 deletions packages/web3/src/bitcoin/demos/sign-message.tsx

This file was deleted.

Loading

0 comments on commit 823f46e

Please sign in to comment.