Skip to content

Commit edaf792

Browse files
committed
init: project
0 parents  commit edaf792

25 files changed

+3941
-0
lines changed

.gitignore

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# editors
4+
**/.idea
5+
**/.vscode
6+
7+
# dependencies
8+
**/node_modules
9+
**/.pnp
10+
**/.pnp.js
11+
12+
# testing
13+
**/coverage
14+
**/tmp
15+
16+
# production
17+
**/lib
18+
**/cjs
19+
**/dist
20+
**/build
21+
**/*.tsbuildinfo
22+
23+
# generated
24+
**/.next
25+
**/.turbo
26+
27+
# misc
28+
.DS_Store
29+
.env.local
30+
.env.development.local
31+
.env.production.local
32+
.env.test.local
33+
**/.npmrc
34+
35+
# logs
36+
npm-debug.log*
37+
yarn-debug.log*
38+
yarn-error.log*

.prettierignore

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# editors
2+
**/.git
3+
**/.idea
4+
**/.vscode
5+
6+
# dependencies
7+
**/node_modules
8+
**/.pnp.js
9+
**/.pnp
10+
11+
# testing
12+
**/coverage
13+
14+
# production
15+
**/dist
16+
**/build
17+
**/tsconfig.tsbuildinfo
18+
19+
# misc
20+
**/.DS_Store
21+
**/.env.local
22+
**/.env.development.local
23+
**/.env.test.local
24+
**/.env.production.local
25+
26+
**/npm-debug.log*
27+
**/yarn-debug.log*
28+
**/yarn-error.log*
29+
30+
# moleculec
31+
**/*.mol
32+
33+
# generated typing
34+
**/.next
35+
**/next-env.d.ts
36+
**/auto-imports.d.ts
37+
**/vite-imports.d.ts

.prettierrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"semi": true,
3+
"tabWidth": 2,
4+
"useTabs": false,
5+
"singleQuote": true,
6+
"jsxSingleQuote": false,
7+
"trailingComma": "all",
8+
"endOfLine": "lf",
9+
"printWidth": 120
10+
}

package.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "btc-wallet-poc",
3+
"private": true,
4+
"workspaces": [
5+
"packages/*"
6+
],
7+
"scripts": {
8+
"build": "turbo run build",
9+
"build:packages": "turbo run build --filter=./packages/*",
10+
"lint:fix": "turbo run lint:fix",
11+
"lint:fix-all": "prettier --write '{packages,apps}/**/*.{js,jsx,ts,tsx,md,json}'",
12+
"clean": "turbo run clean",
13+
"clean:packages": "turbo run clean --filter=./packags/*",
14+
"clean:dependencies": "pnpm clean:sub-dependencies && rimraf node_modules",
15+
"clean:sub-dependencies": "rimraf packages/**/node_modules apps/**/node_modules"
16+
},
17+
"devDependencies": {
18+
"@changesets/cli": "^2.27.1",
19+
"typescript": "^5.3.3",
20+
"prettier": "^3.2.5",
21+
"rimraf": "^5.0.5",
22+
"turbo": "^1.12.4"
23+
},
24+
"packageManager": "^[email protected]",
25+
"engines": {
26+
"node": ">=18.0.0",
27+
"pnpm": ">=8.0.0"
28+
}
29+
}

packages/sdk/.env.example

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
VITE_BTC_RPC_URL=https://btc_rpc.url
2+
VITE_BTC_RPC_USER=user:password
3+
4+
VITE_OPENAPI_URL=https://open_api.url
5+
VITE_OPENAPI_KEY=key

packages/sdk/package.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "btc-wallet-poc-sdk",
3+
"version": "0.1.0",
4+
"scripts": {
5+
"test": "vitest",
6+
"build": "tsc -p tsconfig.build.json",
7+
"lint": "prettier --check '{src,tests}/**/*.{js,jsx,ts,tsx}'",
8+
"lint:fix": "prettier --write '{src,tests}/**/*.{js,jsx,ts,tsx}'",
9+
"clean": "pnpm run clean:cache & pnpm run clean:build",
10+
"clean:build": "rimraf lib && pnpm run clean:buildinfo",
11+
"clean:buildinfo": "rimraf tsconfig.*tsbuildinfo",
12+
"clean:cache": "rimraf .turbo"
13+
},
14+
"main": "lib",
15+
"files": [
16+
"lib"
17+
],
18+
"dependencies": {
19+
"bip32": "^4.0.0",
20+
"bitcoinjs-lib": "^6.1.5",
21+
"ecpair": "^2.1.0",
22+
"lodash": "^4.17.21",
23+
"tiny-secp256k1": "^2.2.3"
24+
},
25+
"devDependencies": {
26+
"vitest": "^1.2.0"
27+
},
28+
"publishConfig": {
29+
"access": "public"
30+
}
31+
}

packages/sdk/src/api/sendBtc.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NetworkType } from '../network';
2+
import { TxBuild } from '../transaction/build';
3+
import { UniSatOpenApi } from '../query/uniSatOpenApi';
4+
5+
export async function sendBtc(props: {
6+
from: string;
7+
tos: {
8+
address: string;
9+
value: number;
10+
}[];
11+
openApi: UniSatOpenApi;
12+
networkType: NetworkType;
13+
changeAddress?: string;
14+
fee: number;
15+
}) {
16+
const tx = new TxBuild({
17+
openApi: props.openApi,
18+
changeAddress: props.changeAddress ?? props.from,
19+
networkType: props.networkType,
20+
fee: props.fee,
21+
});
22+
23+
props.tos.forEach((to) => {
24+
tx.addOutput(to.address, to.value);
25+
});
26+
27+
await tx.collectInputs(props.from);
28+
return tx.toPsbt();
29+
}

packages/sdk/src/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MIN_COLLECTABLE_SATOSHIS = 546;

packages/sdk/src/error.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export enum ErrorCodes {
2+
UNKNOWN,
3+
INSUFFICIENT_BTC_UTXO,
4+
UNSUPPORTED_ADDRESS_TYPE,
5+
}
6+
7+
export const ErrorMessages = {
8+
[ErrorCodes.UNKNOWN]: "Unknown error",
9+
[ErrorCodes.INSUFFICIENT_BTC_UTXO]: "Insufficient btc utxo",
10+
[ErrorCodes.UNSUPPORTED_ADDRESS_TYPE]: "Unsupported address type",
11+
};
12+
13+
export class TxBuildError extends Error {
14+
public code = ErrorCodes.UNKNOWN;
15+
constructor(
16+
code: ErrorCodes,
17+
message = ErrorMessages[code] || "Unknown error"
18+
) {
19+
super(message);
20+
this.code = code;
21+
Object.setPrototypeOf(this, TxBuildError.prototype);
22+
}
23+
}

packages/sdk/src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export * from './types';
2+
export * from './error';
3+
export * from './network';
4+
export * from './constants';
5+
6+
export * from './api/sendBtc';
7+
8+
export * from './query/btcRpc';
9+
export * from './query/uniSatOpenApi';

packages/sdk/src/network.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import bitcoin from 'bitcoinjs-lib';
2+
3+
export enum NetworkType {
4+
MAINNET,
5+
TESTNET,
6+
REGTEST,
7+
}
8+
9+
/**
10+
* Convert network type to bitcoinjs-lib network.
11+
*/
12+
export function toPsbtNetwork(networkType: NetworkType): bitcoin.Network {
13+
if (networkType === NetworkType.MAINNET) {
14+
return bitcoin.networks.bitcoin;
15+
} else if (networkType === NetworkType.TESTNET) {
16+
return bitcoin.networks.testnet;
17+
} else {
18+
return bitcoin.networks.regtest;
19+
}
20+
}
21+
22+
/**
23+
* Convert bitcoinjs-lib network to network type.
24+
*/
25+
export function toNetworkType(network: bitcoin.Network): NetworkType {
26+
if (network.bech32 == bitcoin.networks.bitcoin.bech32) {
27+
return NetworkType.MAINNET;
28+
} else if (network.bech32 == bitcoin.networks.testnet.bech32) {
29+
return NetworkType.TESTNET;
30+
} else {
31+
return NetworkType.REGTEST;
32+
}
33+
}

packages/sdk/src/query/btcRpc.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export interface RpcRequestBody {
2+
method: string;
3+
params: any[];
4+
}
5+
6+
export interface RpcResponse<T> {
7+
result: T;
8+
id: number | null;
9+
error: string | null;
10+
}
11+
12+
export class BtcRpc {
13+
public rpcUrl: string;
14+
private readonly rpcUser: string;
15+
16+
constructor(rpcUrl: string, rpcUser: string) {
17+
this.rpcUrl = rpcUrl;
18+
this.rpcUser = btoa(rpcUser);
19+
}
20+
21+
async request<T, R extends RpcResponse<T>>(body: RpcRequestBody): Promise<R> {
22+
const res = await fetch(this.rpcUrl, {
23+
method: 'POST',
24+
headers: {
25+
'Authorization': 'Basic ' + this.rpcUser,
26+
'Content-Type': 'application/json',
27+
},
28+
body: JSON.stringify(body),
29+
});
30+
31+
return await res.json();
32+
}
33+
34+
getBlockchainInfo() {
35+
return this.request({
36+
method: 'getblockchaininfo',
37+
params: [],
38+
});
39+
}
40+
41+
sendRawTransaction(txHex: string) {
42+
return this.request({
43+
method: 'sendrawtransaction',
44+
params: [txHex],
45+
});
46+
}
47+
}
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export interface UniSatOpenApiRequestOptions {
2+
params?: Record<string, any>;
3+
}
4+
5+
export interface UniSatOpenApiResponse<T> {
6+
data: T;
7+
msg: string;
8+
code: number;
9+
}
10+
11+
interface UniSatApiBalance {
12+
address: string;
13+
satoshi: number;
14+
pendingSatoshi: number;
15+
utxoCount: number;
16+
btcSatoshi: number;
17+
btcPendingSatoshi: number;
18+
btcUtxoCount: number;
19+
inscriptionSatoshi: number;
20+
inscriptionPendingSatoshi: number;
21+
inscriptionUtxoCount: number;
22+
}
23+
24+
export interface UniSatApiUtxoList {
25+
cursor: 0,
26+
total: 1,
27+
totalConfirmed: 1,
28+
totalUnconfirmed: 0,
29+
totalUnconfirmedSpend: 0,
30+
utxo: UniSatApiUtxo[];
31+
}
32+
export interface UniSatApiUtxo {
33+
txid: string;
34+
vout: number;
35+
satoshi: number;
36+
scriptType: string;
37+
scriptPk: string;
38+
codeType: number;
39+
address: string;
40+
height: number;
41+
idx: number;
42+
isOpInRBF: boolean;
43+
isSpent: boolean;
44+
inscriptions: [];
45+
}
46+
47+
export class UniSatOpenApi {
48+
public apiUrl: string;
49+
private readonly apiKey: string;
50+
51+
constructor(apiUrl: string, apiKey: string) {
52+
this.apiUrl = apiUrl;
53+
this.apiKey = apiKey;
54+
}
55+
56+
async request<T, R extends UniSatOpenApiResponse<T> = UniSatOpenApiResponse<T>>(
57+
route: string,
58+
options?: UniSatOpenApiRequestOptions
59+
): Promise<R> {
60+
const params = options?.params ? '?' + new URLSearchParams(options.params).toString() : '';
61+
const res = await fetch(`${this.apiUrl}/${route}${params}`, {
62+
method: 'GET',
63+
headers: {
64+
'Authorization': `Bearer ${this.apiKey}`,
65+
},
66+
});
67+
68+
return await res.json();
69+
}
70+
71+
getBalance(address: string) {
72+
return this.request<UniSatApiBalance>(`/v1/indexer/address/${address}/balance`);
73+
}
74+
75+
getUtxos(address: string, cursor?: number, size?: number) {
76+
return this.request<UniSatApiUtxoList>(`/v1/indexer/address/${address}/utxo-data`, {
77+
params: {
78+
cursor: cursor ?? 0,
79+
size: size ?? 50,
80+
},
81+
});
82+
}
83+
}

0 commit comments

Comments
 (0)