From 4e0d4c62c8fe7e2a51dc1e67371a3f54cb3f27ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Thu, 30 Oct 2025 17:34:00 +0700 Subject: [PATCH 1/8] implemented handshake network --- apps/rosen/src/networks/handshake/client.ts | 12 ++ apps/rosen/src/networks/handshake/index.ts | 1 + apps/rosen/src/networks/handshake/server.ts | 50 +++++ apps/rosen/src/networks/index.ts | 1 + knip.config.ts | 46 +++- networks/handshake/CHANGELOG.md | 7 + networks/handshake/package.json | 34 +++ networks/handshake/src/client.ts | 81 +++++++ networks/handshake/src/constants.ts | 3 + networks/handshake/src/generateUnsignedTx.ts | 142 ++++++++++++ networks/handshake/src/getMaxTransfer.ts | 69 ++++++ networks/handshake/src/hsd.d.ts | 79 +++++++ networks/handshake/src/index.ts | 6 + networks/handshake/src/types.ts | 60 ++++++ networks/handshake/src/utils.ts | 214 +++++++++++++++++++ networks/handshake/tsconfig.json | 8 + 16 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 apps/rosen/src/networks/handshake/client.ts create mode 100644 apps/rosen/src/networks/handshake/index.ts create mode 100644 apps/rosen/src/networks/handshake/server.ts create mode 100644 networks/handshake/CHANGELOG.md create mode 100644 networks/handshake/package.json create mode 100644 networks/handshake/src/client.ts create mode 100644 networks/handshake/src/constants.ts create mode 100644 networks/handshake/src/generateUnsignedTx.ts create mode 100644 networks/handshake/src/getMaxTransfer.ts create mode 100644 networks/handshake/src/hsd.d.ts create mode 100644 networks/handshake/src/index.ts create mode 100644 networks/handshake/src/types.ts create mode 100644 networks/handshake/src/utils.ts create mode 100644 networks/handshake/tsconfig.json diff --git a/apps/rosen/src/networks/handshake/client.ts b/apps/rosen/src/networks/handshake/client.ts new file mode 100644 index 000000000..de97fea79 --- /dev/null +++ b/apps/rosen/src/networks/handshake/client.ts @@ -0,0 +1,12 @@ +import { HandshakeNetwork } from '@rosen-network/handshake/dist/client'; + +import { unwrapFromObject } from '@/safeServerAction'; + +import { LOCK_ADDRESSES } from '../../../configs'; +import * as actions from './server'; + +export const handshake = new HandshakeNetwork({ + lockAddress: LOCK_ADDRESSES.handshake, + nextHeightInterval: 1, + ...unwrapFromObject(actions), +}); diff --git a/apps/rosen/src/networks/handshake/index.ts b/apps/rosen/src/networks/handshake/index.ts new file mode 100644 index 000000000..4f1cce44f --- /dev/null +++ b/apps/rosen/src/networks/handshake/index.ts @@ -0,0 +1 @@ +export * from './client'; diff --git a/apps/rosen/src/networks/handshake/server.ts b/apps/rosen/src/networks/handshake/server.ts new file mode 100644 index 000000000..97b9dbc6a --- /dev/null +++ b/apps/rosen/src/networks/handshake/server.ts @@ -0,0 +1,50 @@ +'use server'; + +import { validateAddress as validateAddressCore } from '@rosen-network/base'; +import { + calculateFee as calculateFeeCore, + generateOpReturnData as generateOpReturnDataCore, + generateUnsignedTx as generateUnsignedTxCore, + getAddressBalance as getAddressBalanceCore, + getMaxTransferCreator, + getMinTransferCreator, + submitTransaction as submitTransactionCore, +} from '@rosen-network/handshake'; + +import { wrap } from '@/safeServerAction'; +import { getTokenMap } from '@/tokenMap/getServerTokenMap'; + +export const calculateFee = wrap(calculateFeeCore, { + cache: 10 * 60 * 1000, + traceKey: 'handshake:calculateFee', +}); + +export const generateOpReturnData = wrap(generateOpReturnDataCore, { + traceKey: 'handshake:generateOpReturnData', +}); + +export const generateUnsignedTx = wrap(generateUnsignedTxCore(getTokenMap), { + traceKey: 'handshake:generateUnsignedTx', +}); + +export const getAddressBalance = wrap(getAddressBalanceCore, { + cache: 3000, + traceKey: 'handshake:getAddressBalance', +}); + +export const getMaxTransfer = wrap(getMaxTransferCreator(getTokenMap), { + traceKey: 'handshake:getMaxTransfer', +}); + +export const getMinTransfer = wrap(getMinTransferCreator(getTokenMap), { + traceKey: 'handshake:getMinTransfer', +}); + +export const submitTransaction = wrap(submitTransactionCore, { + traceKey: 'handshake:submitTransaction', +}); + +export const validateAddress = wrap(validateAddressCore, { + cache: Infinity, + traceKey: 'handshake:validateAddress', +}); diff --git a/apps/rosen/src/networks/index.ts b/apps/rosen/src/networks/index.ts index c56c522a0..585ce258c 100644 --- a/apps/rosen/src/networks/index.ts +++ b/apps/rosen/src/networks/index.ts @@ -3,5 +3,6 @@ export * from './bitcoin'; export * from './bitcoin-runes'; export * from './cardano'; export * from './doge'; +export * from './handshake'; export * from './ergo'; export * from './ethereum'; diff --git a/knip.config.ts b/knip.config.ts index bbd2a1fd6..dd6bc3d69 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -2,16 +2,58 @@ import type { KnipConfig } from 'knip'; const config: KnipConfig = { workspaces: {}, - ignoreBinaries: ['tsc', 'typeorm'], + ignoreBinaries: [ + 'changeset', + 'eslint', + 'husky', + 'knip', + 'lint-staged', + 'next', + 'prettier', + 'tauri', + 'tsc', + 'tsx', + 'typeorm', + 'vite', + ], ignore: ['**/node_modules/', '**/dist/', '**/.next/'], ignoreDependencies: [ + '@emotion/react', + '@emotion/styled', '@mui/material', '@next/eslint-plugin-next', - '@types/moment', + '@rosen-bridge/abstract-logger', + '@rosen-bridge/abstract-scanner', + '@rosen-bridge/address-codec', + '@rosen-bridge/bitcoin-observation-extractor', + '@rosen-bridge/bitcoin-scanner', + '@rosen-bridge/cardano-scanner', + '@rosen-bridge/ergo-scanner', + '@rosen-bridge/evm-observation-extractor', + '@rosen-bridge/evm-scanner', + '@rosen-bridge/extended-typeorm', + '@rosen-bridge/handshake-rpc-observation-extractor', + '@rosen-bridge/handshake-rpc-scanner', + '@rosen-bridge/health-check', + '@rosen-bridge/json-bigint', + '@rosen-bridge/log-level-check', + '@rosen-bridge/minimum-fee', + '@rosen-bridge/scanner-interfaces', + '@rosen-bridge/scanner-sync-check', + '@rosen-bridge/service-manager', + '@rosen-bridge/tokens', + '@rosen-bridge/watcher-data-extractor', + '@tauri-apps/cli', '@types/react', + '@types/react-dom', '@vitest/runner', + 'eslint', 'eslint-config-next', + 'husky', + 'lint-staged', 'pg', + 'prettier', + 'react-dom', ], }; diff --git a/networks/handshake/CHANGELOG.md b/networks/handshake/CHANGELOG.md new file mode 100644 index 000000000..2497aff36 --- /dev/null +++ b/networks/handshake/CHANGELOG.md @@ -0,0 +1,7 @@ +# @rosen-network/handshake + +## 0.1.0 + +### Minor Changes + +- Initial implementation of Handshake network support diff --git a/networks/handshake/package.json b/networks/handshake/package.json new file mode 100644 index 000000000..b1b86d5f7 --- /dev/null +++ b/networks/handshake/package.json @@ -0,0 +1,34 @@ +{ + "name": "@rosen-network/handshake", + "version": "0.1.0", + "private": true, + "license": "MIT", + "description": "This is a private package utilized within Rosen Bridge UI app", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "lint": "eslint --fix .", + "lint:check": "eslint .", + "prettify": "prettier --write . --ignore-path ../../.gitignore", + "prettify:check": "prettier --check . --ignore-path ../../.gitignore", + "build": "tsc --build", + "type-check": "tsc --noEmit", + "test": "NODE_OPTIONS='--import tsx' vitest" + }, + "dependencies": { + "@rosen-bridge/bitcoin-utxo-selection": "^1.2.0", + "@rosen-bridge/address-codec": "^1.0.0", + "@rosen-bridge/icons": "^2.3.1", + "@rosen-bridge/tokens": "^4.0.0", + "@rosen-network/base": "^0.4.1", + "@rosen-ui/constants": "^0.4.1", + "@rosen-ui/types": "^0.3.7", + "axios": "^1.7.2", + "hsd": "^8.0.0" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.0.0" + } +} diff --git a/networks/handshake/src/client.ts b/networks/handshake/src/client.ts new file mode 100644 index 000000000..de01508bf --- /dev/null +++ b/networks/handshake/src/client.ts @@ -0,0 +1,81 @@ +import { Handshake as HandshakeIcon } from '@rosen-bridge/icons'; +import { Network, NetworkConfig } from '@rosen-network/base'; +import { NETWORKS } from '@rosen-ui/constants'; + +import type { generateUnsignedTx } from './generateUnsignedTx'; +import type { + generateOpReturnData, + getAddressBalance, + submitTransaction, +} from './utils'; + +type HandshakeNetworkConfig = NetworkConfig & { + generateOpReturnData: typeof generateOpReturnData; + generateUnsignedTx: ReturnType; + getAddressBalance: typeof getAddressBalance; + submitTransaction: typeof submitTransaction; +}; + +export class HandshakeNetwork implements Network { + public label = NETWORKS.handshake.label; + + public lockAddress: string; + + public logo = HandshakeIcon; + + public name = NETWORKS.handshake.key; + + public nextHeightInterval: number; + + constructor(protected config: HandshakeNetworkConfig) { + this.nextHeightInterval = config.nextHeightInterval; + this.lockAddress = config.lockAddress; + } + + public calculateFee: HandshakeNetworkConfig['calculateFee'] = (...args) => { + return this.config.calculateFee(...args); + }; + + public generateOpReturnData: HandshakeNetworkConfig['generateOpReturnData'] = + (...args) => { + return this.config.generateOpReturnData(...args); + }; + + public generateUnsignedTx: HandshakeNetworkConfig['generateUnsignedTx'] = ( + ...args + ) => { + return this.config.generateUnsignedTx(...args); + }; + + public getAddressBalance: HandshakeNetworkConfig['getAddressBalance'] = ( + ...args + ) => { + return this.config.getAddressBalance(...args); + }; + + public getMaxTransfer: HandshakeNetworkConfig['getMaxTransfer'] = ( + ...args + ) => { + return this.config.getMaxTransfer(...args); + }; + + public getMinTransfer: HandshakeNetworkConfig['getMinTransfer'] = ( + ...args + ) => { + return this.config.getMinTransfer(...args); + }; + + public submitTransaction: HandshakeNetworkConfig['submitTransaction'] = ( + ...args + ) => { + return this.config.submitTransaction(...args); + }; + + public toSafeAddress = (address: string): string => { + return address; + }; + + public validateAddress = (walletAddress: string): Promise => { + return this.config.validateAddress(this.name, walletAddress); + }; +} diff --git a/networks/handshake/src/constants.ts b/networks/handshake/src/constants.ts new file mode 100644 index 000000000..766458be7 --- /dev/null +++ b/networks/handshake/src/constants.ts @@ -0,0 +1,3 @@ +export const CONFIRMATION_TARGET = 6; +export const SEGWIT_INPUT_WEIGHT_UNIT = 272; +export const SEGWIT_OUTPUT_WEIGHT_UNIT = 124; diff --git a/networks/handshake/src/generateUnsignedTx.ts b/networks/handshake/src/generateUnsignedTx.ts new file mode 100644 index 000000000..ada397f11 --- /dev/null +++ b/networks/handshake/src/generateUnsignedTx.ts @@ -0,0 +1,142 @@ +import { + BitcoinBoxSelection, + generateFeeEstimator, +} from '@rosen-bridge/bitcoin-utxo-selection'; +import { TokenMap, RosenChainToken } from '@rosen-bridge/tokens'; +import { NETWORKS } from '@rosen-ui/constants'; +import { RosenAmountValue } from '@rosen-ui/types'; +import { MTX, Address, Coin, Covenant, Output } from 'hsd'; + +import { + SEGWIT_INPUT_WEIGHT_UNIT, + SEGWIT_OUTPUT_WEIGHT_UNIT, +} from './constants'; +import { HandshakeUtxo, UnsignedMtxData } from './types'; +import { + getAddressUtxos, + getFeeRatio, + getMinimumMeaningfulSatoshi, +} from './utils'; + +const selector = new BitcoinBoxSelection(); + +/** + * generates handshake lock tx using hsd library + * @param getTokenMap + * @returns + */ +export const generateUnsignedTx = + (getTokenMap: () => Promise) => + async ( + lockAddress: string, + fromAddress: string, + wrappedAmount: RosenAmountValue, + opReturnData: string, + token: RosenChainToken, + ): Promise => { + const tokenMap = await getTokenMap(); + const unwrappedAmount = tokenMap.unwrapAmount( + token.tokenId, + wrappedAmount, + NETWORKS.handshake.key, + ).amount; + + // create MTX (Mutable Transaction) + const mtx = new MTX(); + + // add OP_RETURN output (covenant type 0 with null data) + const opReturnCovenant = Covenant.fromNullData( + Buffer.from(opReturnData, 'hex'), + ); + const opReturnOutput = new Output(); + opReturnOutput.value = 0; + opReturnOutput.covenant = opReturnCovenant; + mtx.outputs.push(opReturnOutput); + + // add lock output + const lockAddr = Address.fromString(lockAddress); + mtx.addOutput({ + address: lockAddr, + value: Number(unwrappedAmount), + }); + + // fetch inputs + const utxos = await getAddressUtxos(fromAddress); + const feeRatio = await getFeeRatio(); + const minSatoshi = getMinimumMeaningfulSatoshi(feeRatio); + + // generate fee estimator + const estimateFee = generateFeeEstimator( + 1, + 42 + // all txs include 40W. P2WPKH txs need additional 2W + 44 + // OP_RETURN output base weight + opReturnData.length * 2, // op_return data weight + SEGWIT_INPUT_WEIGHT_UNIT, + SEGWIT_OUTPUT_WEIGHT_UNIT, + feeRatio, + 4, // the virtual size matters for fee estimation of native-segwit transactions + ); + + const coveredBoxes = await selector.getCoveringBoxes( + { + nativeToken: unwrappedAmount, + tokens: [], + }, + [], + new Map(), + utxos.values(), + minSatoshi, + undefined, + estimateFee, + ); + if (!coveredBoxes.covered) { + const totalInputHns = utxos.reduce( + (sum, walletUtxo) => sum + BigInt(walletUtxo.value), + 0n, + ); + throw new Error( + `Available boxes didn't cover required assets. HNS: ${ + unwrappedAmount + minSatoshi + }`, + { + cause: { + totalInputHns, + fromAddress: fromAddress, + }, + }, + ); + } + + // add inputs as Coin objects + const fromAddr = Address.fromString(fromAddress); + coveredBoxes.boxes.forEach((box) => { + const coin = Coin.fromJSON({ + version: 0, + height: -1, + value: Number(box.value), + address: fromAddress, + coinbase: false, + hash: box.txId, + index: box.index, + }); + mtx.addCoin(coin); + }); + + // add change output + const changeAmount = Number( + coveredBoxes.additionalAssets.aggregated.nativeToken, + ); + if (changeAmount > 0) { + mtx.addOutput({ + address: fromAddr, + value: changeAmount, + }); + } + + return { + mtx: { + hex: mtx.toHex(), + }, + inputSize: mtx.inputs.length, + }; + }; diff --git a/networks/handshake/src/getMaxTransfer.ts b/networks/handshake/src/getMaxTransfer.ts new file mode 100644 index 000000000..48dcd2b83 --- /dev/null +++ b/networks/handshake/src/getMaxTransfer.ts @@ -0,0 +1,69 @@ +import { TokenMap } from '@rosen-bridge/tokens'; +import { NETWORKS } from '@rosen-ui/constants'; +import { Network, RosenAmountValue } from '@rosen-ui/types'; + +import { + estimateTxWeight, + generateOpReturnData, + getFeeRatio, + getAddressUtxos, + getMinimumMeaningfulSatoshi, +} from './utils'; + +export const getMaxTransferCreator = + (getTokenMap: () => Promise) => + async ({ + balance, + isNative, + eventData, + }: { + balance: RosenAmountValue; + isNative: boolean; + eventData: { + toChain: Network; + fromAddress: string; + toAddress: string; + }; + }) => { + const tokenMap = await getTokenMap(); + if (!eventData.toAddress) return 0n; + + const feeRatio = await getFeeRatio(); + const opRetrunDataLength = ( + await generateOpReturnData( + eventData.toChain, + eventData.toAddress, + // We don't care about the actual op return data and only need the length + '0', + '0', + ) + ).length; + const utxos = await getAddressUtxos(eventData.fromAddress); + const estimatedTxWeight = await estimateTxWeight( + /** + * When getting max transfer, probably all of the utxos are going to be + * spent + */ + utxos.length, + 2, + opRetrunDataLength, + ); + const estimatedFee = Math.ceil((estimatedTxWeight / 4) * feeRatio); + const minSatoshi = await getMinimumMeaningfulSatoshi(feeRatio); + + const offset = tokenMap.wrapAmount( + NETWORKS.handshake.nativeToken, + BigInt(estimatedFee) + minSatoshi, + NETWORKS.handshake.key, + ).amount; + + return balance < 0n || !isNative + ? 0n + : /** + * We need to subtract (utxos.length + 1) from the calculated value because + * of a bug in bitcoin box selection + * + * local:ergo/rosen-bridge/utils#204 + */ + balance - offset - BigInt(utxos.length + 1); + }; diff --git a/networks/handshake/src/hsd.d.ts b/networks/handshake/src/hsd.d.ts new file mode 100644 index 000000000..5622a2a6e --- /dev/null +++ b/networks/handshake/src/hsd.d.ts @@ -0,0 +1,79 @@ +declare module 'hsd' { + export class MTX { + inputs: Input[]; + outputs: Output[]; + version: number; + locktime: number; + + constructor(); + addCoin(coin: Coin): void; + addOutput(options: { address: Address; value: number }): void; + txid(): string; + signatureHash(index: number): Buffer; + toRaw(): Buffer; + toHex(): string; + static fromRaw(data: Buffer): MTX; + static fromHex(hex: string): MTX; + } + + export class TX { + inputs: Input[]; + outputs: Output[]; + static fromRaw(data: Buffer): TX; + static fromHex(hex: string): TX; + } + + export class Input { + prevout: Outpoint; + witness: Witness; + + constructor(); + } + + export class Output { + value: number; + address: Address; + covenant: Covenant; + getAddress(): Address | null; + } + + export class Outpoint { + hash: Buffer; + index: number; + rhash(): string; + } + + export class Witness { + fromStack(stack: Buffer[]): void; + } + + export class Address { + static fromString(address: string): Address; + toString(): string; + getHash(): Buffer; + } + + export class Script { + constructor(); + pushData(data: Buffer): void; + compile(): void; + toStack(): Buffer[]; + } + + export class Coin { + static fromJSON(options: { + version: number; + height: number; + value: number; + address: string; + coinbase: boolean; + hash: string; + index: number; + }): Coin; + } + + export class Covenant { + static fromNullData(data: Buffer): Covenant; + type: number; + } +} diff --git a/networks/handshake/src/index.ts b/networks/handshake/src/index.ts new file mode 100644 index 000000000..e3a4bd6fa --- /dev/null +++ b/networks/handshake/src/index.ts @@ -0,0 +1,6 @@ +export * from './client'; +export * from './constants'; +export * from './generateUnsignedTx'; +export * from './getMaxTransfer'; +export * from './types'; +export * from './utils'; diff --git a/networks/handshake/src/types.ts b/networks/handshake/src/types.ts new file mode 100644 index 000000000..8b3f9f3df --- /dev/null +++ b/networks/handshake/src/types.ts @@ -0,0 +1,60 @@ +export interface Status { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; +} + +export interface HandshakeRpcUtxo { + txid: string; + vout: number; + status: Status; + value: number; +} + +export interface HandshakeUtxo { + txId: string; + index: number; + value: bigint; +} + +export interface HandshakeRpcAddress { + address: string; + chain_stats: Stats; + mempool_stats: Stats; +} + +export interface Stats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; +} + +// from @scure/btc-signer package +enum SignatureHash { + DEFAULT, + ALL, + NONE, + SINGLE, + ANYONECANPAY = 0x80, +} + +export enum SigHash { + DEFAULT = SignatureHash.DEFAULT, + ALL = SignatureHash.ALL, + NONE = SignatureHash.NONE, + SINGLE = SignatureHash.SINGLE, + DEFAULT_ANYONECANPAY = SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY, + ALL_ANYONECANPAY = SignatureHash.ALL | SignatureHash.ANYONECANPAY, + NONE_ANYONECANPAY = SignatureHash.NONE | SignatureHash.ANYONECANPAY, + SINGLE_ANYONECANPAY = SignatureHash.SINGLE | SignatureHash.ANYONECANPAY, +} + +export interface UnsignedMtxData { + mtx: { + hex: string; + }; + inputSize: number; +} diff --git a/networks/handshake/src/utils.ts b/networks/handshake/src/utils.ts new file mode 100644 index 000000000..8f701f13e --- /dev/null +++ b/networks/handshake/src/utils.ts @@ -0,0 +1,214 @@ +import { encodeAddress } from '@rosen-bridge/address-codec'; +import { + CalculateFee, + calculateFeeCreator, + getMinTransferCreator as getMinTransferCreatorBase, +} from '@rosen-network/base'; +import { NETWORKS } from '@rosen-ui/constants'; +import { Network } from '@rosen-ui/types'; +import Axios from 'axios'; +import { MTX } from 'hsd'; + +import { + CONFIRMATION_TARGET, + SEGWIT_INPUT_WEIGHT_UNIT, + SEGWIT_OUTPUT_WEIGHT_UNIT, +} from './constants'; +import type { HandshakeUtxo } from './types'; + +/** + * generates metadata for lock transaction + * @param toChain + * @param toAddress + * @param networkFee + * @param bridgeFee + * @returns + */ +export const generateOpReturnData = async ( + toChain: Network, + toAddress: string, + networkFee: string, + bridgeFee: string, +): Promise => { + // parse toChain + const toChainCode = (NETWORKS[toChain]?.index ?? -1) as number; + if (toChainCode === -1) throw Error(`invalid toChain [${toChain}]`); + const toChainHex = toChainCode.toString(16).padStart(2, '0'); + + // parse bridgeFee + const bridgeFeeHex = BigInt(bridgeFee).toString(16).padStart(16, '0'); + + // parse networkFee + const networkFeeHex = BigInt(networkFee).toString(16).padStart(16, '0'); + + // parse toAddress + const addressHex = encodeAddress(toChain, toAddress); + const addressLengthCode = (addressHex.length / 2) + .toString(16) + .padStart(2, '0'); + + return Promise.resolve( + toChainHex + bridgeFeeHex + networkFeeHex + addressLengthCode + addressHex, + ); +}; + +/** + * gets utxos from Handshake wallet RPC + * Note: This uses wallet RPC 'listunspent' which returns UTXOs for the connected wallet, + * not for an arbitrary address. The address parameter is ignored for wallet RPC. + * @param address - ignored, kept for API compatibility + * @returns + */ +export const getAddressUtxos = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + address: string, +): Promise> => { + const rpcUrl = process.env.HANDSHAKE_WALLET_RPC_API; + if (!rpcUrl) throw new Error('HANDSHAKE_WALLET_RPC_API is not configured'); + + const res = await Axios.post<{ + result: Array<{ + txid: string; + vout: number; + amount: number; + confirmations: number; + covenant: { type: number; action: string }; + }>; + }>(rpcUrl, { + method: 'listunspent', + params: [], + }); + + // Filter only coin-type UTXOs (covenant type 0 = NONE) + return res.data.result + .filter((utxo) => utxo.covenant.type === 0) + .map((utxo) => ({ + txId: utxo.txid, + index: utxo.vout, + value: BigInt(Math.floor(utxo.amount * 1e6)), // Convert HNS to dollarydoos (1 HNS = 1,000,000 dollarydoos) + })); +}; + +/** + * gets wallet HNS balance from Handshake wallet RPC + * Note: This uses wallet RPC 'getbalance' which returns balance for the connected wallet, + * not for an arbitrary address. The address parameter is ignored for wallet RPC. + * @param address - ignored, kept for API compatibility + * @returns this is a UNWRAPPED-VALUE amount in dollarydoos + */ +export const getAddressBalance = async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + address: string, +): Promise => { + const rpcUrl = process.env.HANDSHAKE_WALLET_RPC_API; + if (!rpcUrl) throw new Error('HANDSHAKE_WALLET_RPC_API is not configured'); + + const res = await Axios.post<{ result: number }>(rpcUrl, { + method: 'getbalance', + params: [], + }); + + // Convert HNS to dollarydoos (1 HNS = 1,000,000 dollarydoos) + // getbalance returns confirmed balance in HNS (6 decimals) + return BigInt(Math.floor(res.data.result * 1e6)); +}; + +/** + * gets current fee ratio of the network + * @returns fee rate in dollarydoos per byte + */ +export const getFeeRatio = async (): Promise => { + const rpcUrl = process.env.HANDSHAKE_RPC_API; + if (!rpcUrl) throw new Error('HANDSHAKE_RPC_API is not configured'); + + const res = await Axios.post<{ result: number }>(rpcUrl, { + method: 'estimatefee', + params: [CONFIRMATION_TARGET], + }); + + // estimatefee returns HNS per KB + // Convert from HNS/KB to dollarydoos/byte + const feeRateHnsPerKb = res.data.result; + const feeDollarydoosPerKb = feeRateHnsPerKb * 1e6; // HNS to dollarydoos + const feeDollarydoosPerByte = feeDollarydoosPerKb / 1024; // KB to byte + + return Math.ceil(feeDollarydoosPerByte) || 20; // Default to 20 dollarydoos/byte if 0 +}; + +/** + * gets the minimum amount of dollarydoos for a utxo that can cover + * additional fee for adding it to a tx + * @returns the minimum UNWRAPPED-VALUE amount + */ +export const getMinimumMeaningfulSatoshi = (feeRatio: number): bigint => { + const minSat = BigInt( + Math.ceil( + (feeRatio * SEGWIT_INPUT_WEIGHT_UNIT) / 4, // estimate fee per weight and convert to virtual size + ), + ); + return minSat > 294n ? minSat : 294n; +}; + +/** + * estimates tx weight based on number of inputs and outputs + * inputs and outputs required fee are estimated by segwit weight unit + * @param inputSize + * @param outputSize + * @param opReturnLength + */ +export const estimateTxWeight = ( + inputSize: number, + outputSize: number, + opReturnLength: number, +): number => { + const x = + 40 + + 2 + // all txs include 40W. P2WPKH txs need additional 2W + 44 + // OP_RETURN output base weight + opReturnLength * 2 + // OP_RETURN output data counts as vSize, so weight = hexString length / 2 * 4 + inputSize * SEGWIT_INPUT_WEIGHT_UNIT + // inputs weights + outputSize * SEGWIT_OUTPUT_WEIGHT_UNIT; // outputs weights + return x; +}; + +/** + * submits a transaction + * @param serializedMtx mtx in hex format + */ +export const submitTransaction = async ( + serializedMtx: string, +): Promise => { + const rpcUrl = process.env.HANDSHAKE_RPC_API; + if (!rpcUrl) throw new Error('HANDSHAKE_RPC_API is not configured'); + + const mtx = MTX.fromHex(serializedMtx); + + const res = await Axios.post<{ result: string }>(rpcUrl, { + method: 'sendrawtransaction', + params: [mtx.toHex()], + }); + + return res.data.result; +}; + +export const getHeight = async (): Promise => { + const rpcUrl = process.env.HANDSHAKE_RPC_API; + if (!rpcUrl) throw new Error('HANDSHAKE_RPC_API is not configured'); + + const res = await Axios.post<{ result: { blocks: number } }>(rpcUrl, { + method: 'getblockchaininfo', + params: [], + }); + + return res.data.result.blocks; +}; + +export const calculateFee: CalculateFee = calculateFeeCreator( + NETWORKS.handshake.key, + getHeight, +); + +export const getMinTransferCreator = getMinTransferCreatorBase( + NETWORKS.handshake.key, + calculateFee, +); diff --git a/networks/handshake/tsconfig.json b/networks/handshake/tsconfig.json new file mode 100644 index 000000000..2f1ed6adb --- /dev/null +++ b/networks/handshake/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src" + }, + "include": ["src"] +} From 1bb6086897573f4d698c1342989c19f318cd11de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Thu, 30 Oct 2025 17:35:01 +0700 Subject: [PATCH 2/8] implemented bob-extension --- wallets/bob-extension/README.md | 60 +++++ wallets/bob-extension/package.json | 28 +++ wallets/bob-extension/src/icon.ts | 17 ++ wallets/bob-extension/src/index.ts | 3 + wallets/bob-extension/src/types.ts | 91 ++++++++ wallets/bob-extension/src/wallet.ts | 348 ++++++++++++++++++++++++++++ wallets/bob-extension/tsconfig.json | 8 + 7 files changed, 555 insertions(+) create mode 100644 wallets/bob-extension/README.md create mode 100644 wallets/bob-extension/package.json create mode 100644 wallets/bob-extension/src/icon.ts create mode 100644 wallets/bob-extension/src/index.ts create mode 100644 wallets/bob-extension/src/types.ts create mode 100644 wallets/bob-extension/src/wallet.ts create mode 100644 wallets/bob-extension/tsconfig.json diff --git a/wallets/bob-extension/README.md b/wallets/bob-extension/README.md new file mode 100644 index 000000000..80e512063 --- /dev/null +++ b/wallets/bob-extension/README.md @@ -0,0 +1,60 @@ +# Bob Extension Wallet Integration + +This package provides integration between the Bob Extension (Handshake wallet) and Rosen Bridge UI. + +## Features + +- Connect to Bob Extension browser wallet +- Get wallet address and balance +- Send HNS transactions through Bob Extension +- Automatic detection of Bob Extension availability +- Seamless integration with Rosen Bridge UI + +## Requirements + +- Bob Extension installed in browser +- Bob Extension unlocked and connected +- Handshake (HNS) balance for transactions + +## Usage + +The Bob Extension wallet will automatically appear in the Rosen Bridge UI wallet selection when: + +1. Bob Extension is installed in the browser +2. User is on a page with Handshake as source/target chain +3. Bob Extension is unlocked + +## Architecture + +This wallet integration leverages the existing Bob3 API exposed by Bob Extension: + +```typescript +// Connect to Bob Extension +const wallet = await bob3.connect(); + +// Get wallet info +const address = await wallet.getAddress(); +const balance = await wallet.getBalance(); + +// Send transaction +const tx = await wallet.send(lockAddress, amountInHNS); +``` + +## Development + +```bash +# Build the wallet package +npm run build + +# Build the entire Rosen Bridge UI with Bob Extension +cd ../../ +./build.sh rosen +``` + +## Integration Details + +- **Chain Support**: Handshake (HNS) only +- **Transaction Type**: Standard HNS transfers to bridge lock address +- **Metadata**: Bridge metadata handled by Rosen Bridge network layer +- **Security**: All signing happens within Bob Extension +- **User Experience**: Familiar Bob Extension popup for transaction confirmation diff --git a/wallets/bob-extension/package.json b/wallets/bob-extension/package.json new file mode 100644 index 000000000..12638a64f --- /dev/null +++ b/wallets/bob-extension/package.json @@ -0,0 +1,28 @@ +{ + "name": "@rosen-ui/bob-extension", + "version": "0.1.0", + "private": true, + "license": "MIT", + "author": "Aco Šmrkas", + "description": "Bob Extension wallet integration for Rosen Bridge", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "lint": "eslint --fix .", + "lint:check": "eslint .", + "prettify": "prettier --write . --ignore-path ../../.gitignore", + "prettify:check": "prettier --check . --ignore-path ../../.gitignore", + "build": "tsc --build", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@rosen-ui/constants": "^0.4.1", + "@rosen-network/handshake": "^0.1.0", + "@rosen-ui/types": "^0.3.7", + "@rosen-ui/wallet-api": "^3.0.3" + } +} diff --git a/wallets/bob-extension/src/icon.ts b/wallets/bob-extension/src/icon.ts new file mode 100644 index 000000000..750cd7abf --- /dev/null +++ b/wallets/bob-extension/src/icon.ts @@ -0,0 +1,17 @@ +export const ICON = ` + + + + + + + + + + + + + + + +`; diff --git a/wallets/bob-extension/src/index.ts b/wallets/bob-extension/src/index.ts new file mode 100644 index 000000000..02af13b94 --- /dev/null +++ b/wallets/bob-extension/src/index.ts @@ -0,0 +1,3 @@ +export { BobExtensionWallet } from './wallet'; +export type { BobExtensionConfig, Bob3API, Bob3Wallet } from './types'; +export { ICON } from './icon'; diff --git a/wallets/bob-extension/src/types.ts b/wallets/bob-extension/src/types.ts new file mode 100644 index 000000000..dcd435de5 --- /dev/null +++ b/wallets/bob-extension/src/types.ts @@ -0,0 +1,91 @@ +/** + * Bob Extension wallet types + */ +import { WalletConfig } from '@rosen-ui/wallet-api'; + +export type BobExtensionConfig = WalletConfig & { + // No specific config needed - Bob Extension auto-detects +}; + +export interface Bob3API { + connect(): Promise; + isLocked(): Promise; +} + +export interface Bob3Wallet { + getAddress(): Promise; + getBalance(): Promise<{ + confirmed: number; + unconfirmed: number; + total: number; + }>; + send(address: string, amount: number): Promise<{ hash: string }>; + + // Advanced transaction building + createTx(options: CreateTxOptions): Promise<{ hex: string }>; + sendTx(hex: string): Promise<{ hash: string }>; + + sign(address: string, message: string): Promise; + signWithName(name: string, message: string): Promise; + verify(message: string, signature: string, address: string): Promise; + verifyWithName( + message: string, + signature: string, + name: string, + ): Promise; + + // Name operations + sendOpen(name: string): Promise<{ hash: string }>; + sendBid( + name: string, + amount: number, + lockup: number, + ): Promise<{ hash: string }>; + sendReveal(name: string): Promise<{ hash: string }>; + sendRedeem(name: string): Promise<{ hash: string }>; + sendUpdate( + name: string, + records: UpdateRecordType[], + ): Promise<{ hash: string }>; + sendCustomTx( + outputs: unknown[], + rate?: number, + subtractFee?: boolean, + ): Promise<{ hash: string }>; + + // Info functions + getNames(): Promise; + getBidsByName(name: string): Promise; + getPending(): Promise; + hashName(name: string): Promise; +} + +export interface CreateTxOptions { + outputs: TxOutput[]; + rate?: number; + subtractFee?: boolean; +} + +export interface TxOutput { + address?: string; + value: number; + data?: string; // For OP_RETURN data +} + +// Bob3 API that's accessible via window.bob3 +export interface Bob3API { + connect(): Promise; + isLocked(): Promise; +} + +export interface UpdateRecordType { + type: 'DS' | 'NS' | 'GLUE4' | 'GLUE6' | 'SYNTH4' | 'SYNTH6' | 'TXT'; + [key: string]: unknown; +} + +// Global declarations for Bob3 extension +declare global { + interface Window { + bob3?: Bob3API; + } +} diff --git a/wallets/bob-extension/src/wallet.ts b/wallets/bob-extension/src/wallet.ts new file mode 100644 index 000000000..0ba34edf6 --- /dev/null +++ b/wallets/bob-extension/src/wallet.ts @@ -0,0 +1,348 @@ +import { HandshakeNetwork } from '@rosen-network/handshake/dist/client'; +import { NETWORKS } from '@rosen-ui/constants'; +import { Network } from '@rosen-ui/types'; +import { + SubmitTransactionError, + UnsupportedChainError, + UserDeniedTransactionSignatureError, + Wallet, + WalletTransferParams, +} from '@rosen-ui/wallet-api'; + +import { ICON } from './icon'; +import { BobExtensionConfig, Bob3Wallet } from './types'; + +/** + * Bob Extension wallet integration for Handshake + * + * This wallet integrates with the Bob Extension to enable seamless + * Handshake transactions for Rosen Bridge. Bob Extension must be + * installed and unlocked for this wallet to function. + */ +export class BobExtensionWallet extends Wallet { + icon = ICON; + + name = 'Bob Extension'; + + label = 'Bob Extension'; + + link = 'https://bobwallet.io/'; + + currentChain: Network = NETWORKS.handshake.key; + + supportedChains: Network[] = [NETWORKS.handshake.key]; + + private wallet: Bob3Wallet | null = null; + + /** + * Connect to Bob Extension + */ + performConnect = async (): Promise => { + if (!window.bob3) { + throw new Error( + 'Bob Extension not found. Please install Bob Extension from the Chrome Web Store.', + ); + } + + try { + this.wallet = await window.bob3.connect(); + + // Verify we can get an address (confirms wallet is properly connected) + const address = await this.wallet.getAddress(); + if (!address) { + throw new Error('Unable to get wallet address from Bob Extension'); + } + + console.log('Connected to Bob Extension, address:', address); + } catch (error) { + this.wallet = null; + if (error instanceof Error) { + if ( + error.message.includes('rejected') || + error.message.includes('denied') + ) { + throw new UserDeniedTransactionSignatureError(this.name); + } + throw new Error(`Failed to connect to Bob Extension: ${error.message}`); + } + throw new Error('Failed to connect to Bob Extension: Unknown error'); + } + }; + + /** + * Disconnect from Bob Extension + */ + performDisconnect = async (): Promise => { + this.wallet = null; + // Bob Extension doesn't require explicit disconnect + }; + + /** + * Get wallet address + */ + fetchAddress = async (): Promise => { + if (!this.wallet) { + return undefined; + } + + try { + return await this.wallet.getAddress(); + } catch (error) { + console.error('Failed to fetch address from Bob Extension:', error); + return undefined; + } + }; + + /** + * Get wallet balance + */ + fetchBalance = async (): Promise => { + if (!this.wallet) { + return '0'; + } + + try { + const balance = await this.wallet.getBalance(); + + return balance.confirmed.toString(); + } catch (error) { + console.error('Failed to fetch balance from Bob Extension:', error); + return '0'; + } + }; + + /** + * Check if Bob Extension is available + */ + isAvailable = (): boolean => { + return !!window.bob3; + }; + + /** + * Check if wallet has active connection + */ + hasConnection = async (): Promise => { + try { + return this.wallet !== null && !!(await this.fetchAddress()); + } catch { + return false; + } + }; + + /** + * Create and submit bridge transaction using Bob Extension with Rosen metadata + */ + performTransfer = async (params: WalletTransferParams): Promise => { + if ( + !this.currentNetwork || + !(this.currentNetwork instanceof HandshakeNetwork) + ) { + throw new UnsupportedChainError(this.name, this.currentChain); + } + + if (!this.wallet) { + throw new Error( + 'Wallet not connected. Please connect to Bob Extension first.', + ); + } + + try { + // Generate Rosen Bridge metadata for OP_RETURN + const rosenMetadata = await this.currentNetwork.generateOpReturnData( + params.toChain, + params.address, + params.networkFee.toString(), + params.bridgeFee.toString(), + ); + + console.log('Sending Rosen Bridge transaction:', { + lockAddress: params.lockAddress, + amount: params.amount, + toChain: params.toChain, + toAddress: params.address, + bridgeFee: params.bridgeFee, + networkFee: params.networkFee, + metadata: rosenMetadata, + }); + + // Create transaction with OP_RETURN data containing Rosen metadata + const result = await this.createBridgeTransaction( + params.lockAddress, + Number(params.amount), + rosenMetadata, + ); + + if (!result.hash) { + throw new Error('Transaction failed - no hash returned'); + } + + console.log('Rosen Bridge transaction sent successfully:', result.hash); + return result.hash; + } catch (error) { + if (error instanceof Error) { + if ( + error.message.includes('rejected') || + error.message.includes('denied') || + error.message.includes('cancelled') + ) { + throw new UserDeniedTransactionSignatureError(this.name); + } + if ( + error.message.includes('insufficient') || + error.message.includes('balance') + ) { + throw new Error('Insufficient balance for transaction'); + } + throw new SubmitTransactionError(this.name, error.message); + } + throw new SubmitTransactionError(this.name, 'Unknown error occurred'); + } + }; + + /** + * Create bridge transaction with OP_RETURN metadata + */ + private createBridgeTransaction = async ( + lockAddress: string, + amount: number, + metadata: string, + ): Promise<{ hash: string }> => { + if (!this.wallet) { + throw new Error('Wallet not connected'); + } + + try { + /* + const hexData = Array.from(new TextEncoder().encode(customData)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + outputs.push({ + address: recipient, + value: 0, + data: hexData, + }); + */ + + // Create transaction with two outputs: + // 1. Send amount to lock address + // 2. OP_RETURN with Rosen Bridge metadata + const outputs = [ + { + address: lockAddress, + value: amount, + }, + { + value: 0, + data: metadata, // OP_RETURN data containing Rosen Bridge metadata + }, + ]; + + console.log('Creating Rosen Bridge transaction with outputs:', outputs); + + // Create the transaction using Bob Extension's createTx + const broadcastResult = await this.wallet.sendCustomTx( + outputs, + 20, // 20 dollarydoos per byte fee rate + false, // Don't subtract fee from the lock amount + ); + + console.log( + 'Bridge transaction broadcast successfully:', + broadcastResult.hash, + ); + + return broadcastResult; + } catch (error) { + console.error('Failed to create bridge transaction:', error); + + // If custom transaction building fails, fall back to basic send and log metadata + if (error instanceof Error) { + console.warn( + 'Advanced transaction building failed, falling back to basic send', + ); + + // Log the metadata for external processing by the Rosen Bridge network layer + console.log('Rosen Bridge metadata (for network layer processing):', { + metadata, + lockAddress, + amount, + note: 'Custom transaction failed, metadata should be handled by Rosen Bridge network layer', + }); + + return await this.wallet.send(lockAddress, amount); + } + + throw error; + } + }; + + /** + * Sign a message using the wallet + */ + signMessage = async (message: string): Promise => { + if (!this.wallet) { + throw new Error('Wallet not connected'); + } + + try { + const address = await this.wallet.getAddress(); + return await this.wallet.sign(address, message); + } catch (error) { + if ( + error instanceof Error && + (error.message.includes('rejected') || error.message.includes('denied')) + ) { + throw new UserDeniedTransactionSignatureError(this.name); + } + throw error; + } + }; + + /** + * Verify a message signature + */ + verifyMessage = async ( + message: string, + signature: string, + address: string, + ): Promise => { + if (!this.wallet) { + throw new Error('Wallet not connected'); + } + + try { + return await this.wallet.verify(message, signature, address); + } catch (error) { + console.error('Failed to verify message:', error); + return false; + } + }; + + /** + * Get wallet info for debugging + */ + getWalletInfo = async (): Promise => { + if (!this.wallet) { + return null; + } + + try { + const [address, balance] = await Promise.all([ + this.wallet.getAddress(), + this.wallet.getBalance(), + ]); + + return { + address, + balance, + connected: true, + }; + } catch (error) { + console.error('Failed to get wallet info:', error); + return { + connected: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }; +} diff --git a/wallets/bob-extension/tsconfig.json b/wallets/bob-extension/tsconfig.json new file mode 100644 index 000000000..2f1ed6adb --- /dev/null +++ b/wallets/bob-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src" + }, + "include": ["src"] +} From b9ed4ecb2c36ae8de9080eec57d4a0c14081df7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Thu, 30 Oct 2025 17:36:14 +0700 Subject: [PATCH 3/8] bob-extension in ui --- apps/rosen/src/wallets/bobExtension.ts | 9 +++++++++ apps/rosen/src/wallets/index.ts | 1 + 2 files changed, 10 insertions(+) create mode 100644 apps/rosen/src/wallets/bobExtension.ts diff --git a/apps/rosen/src/wallets/bobExtension.ts b/apps/rosen/src/wallets/bobExtension.ts new file mode 100644 index 000000000..72d43222f --- /dev/null +++ b/apps/rosen/src/wallets/bobExtension.ts @@ -0,0 +1,9 @@ +import { BobExtensionWallet } from '@rosen-ui/bob-extension'; + +import { handshake } from '@/networks'; +import { getTokenMap } from '@/tokenMap/getClientTokenMap'; + +export const bobExtension = new BobExtensionWallet({ + networks: [handshake], + getTokenMap, +}); diff --git a/apps/rosen/src/wallets/index.ts b/apps/rosen/src/wallets/index.ts index 24b0e7a16..788176fda 100644 --- a/apps/rosen/src/wallets/index.ts +++ b/apps/rosen/src/wallets/index.ts @@ -1,5 +1,6 @@ import './base'; +export * from './bobExtension'; export * from './eternl'; export * from './lace'; export * from './metaMask'; From 18fee3f8bf5da3f655f80350292056208b2924a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Thu, 30 Oct 2025 17:37:34 +0700 Subject: [PATCH 4/8] implemented handshake asset-calculator --- .../asset-calculator/lib/asset-calculator.ts | 10 +++ .../calculator/chains/handshake-calculator.ts | 83 +++++++++++++++++++ packages/asset-calculator/lib/interfaces.ts | 5 ++ .../tests/asset-calculator.spec.ts | 4 + 4 files changed, 102 insertions(+) create mode 100644 packages/asset-calculator/lib/calculator/chains/handshake-calculator.ts diff --git a/packages/asset-calculator/lib/asset-calculator.ts b/packages/asset-calculator/lib/asset-calculator.ts index 35821a444..c44372cc2 100644 --- a/packages/asset-calculator/lib/asset-calculator.ts +++ b/packages/asset-calculator/lib/asset-calculator.ts @@ -13,6 +13,7 @@ import { CardanoCalculator } from './calculator/chains/cardano-calculator'; import { DogeCalculator } from './calculator/chains/doge-calculator'; import { ErgoCalculator } from './calculator/chains/ergo-calculator'; import { EvmCalculator } from './calculator/chains/evm-calculator'; +import { HandshakeCalculator } from './calculator/chains/handshake-calculator'; import { BridgedAssetModel } from './database/bridgedAsset/BridgedAssetModel'; import { LockedAssetEntity } from './database/lockedAsset/LockedAssetEntity'; import { LockedAssetModel } from './database/lockedAsset/LockedAssetModel'; @@ -24,6 +25,7 @@ import { DogeCalculatorInterface, ErgoCalculatorInterface, EvmCalculatorInterface, + HandshakeCalculatorInterface, } from './interfaces'; class AssetCalculator { @@ -42,6 +44,7 @@ class AssetCalculator { ethereumCalculator: EvmCalculatorInterface, binanceCalculator: EvmCalculatorInterface, dogeCalculator: DogeCalculatorInterface, + handshakeCalculator: HandshakeCalculatorInterface, dataSource: DataSource, protected readonly logger: AbstractLogger = new DummyLogger(), ) { @@ -93,6 +96,12 @@ class AssetCalculator { dogeCalculator.blockcypherUrl, logger, ); + const handshakeAssetCalculator = new HandshakeCalculator( + this.tokens, + handshakeCalculator.addresses, + handshakeCalculator.rpcUrl, + logger, + ); this.calculatorMap.set(NETWORKS.ergo.key, ergoAssetCalculator); this.calculatorMap.set(NETWORKS.cardano.key, cardanoAssetCalculator); this.calculatorMap.set(NETWORKS.bitcoin.key, bitcoinAssetCalculator); @@ -103,6 +112,7 @@ class AssetCalculator { this.calculatorMap.set(NETWORKS.ethereum.key, ethereumAssetCalculator); this.calculatorMap.set(NETWORKS.binance.key, binanceAssetCalculator); this.calculatorMap.set(NETWORKS.doge.key, dogeAssetCalculator); + this.calculatorMap.set(NETWORKS.handshake.key, handshakeAssetCalculator); this.bridgedAssetModel = new BridgedAssetModel(dataSource, logger); this.lockedAssetModel = new LockedAssetModel(dataSource, logger); this.tokenModel = new TokenModel(dataSource, logger); diff --git a/packages/asset-calculator/lib/calculator/chains/handshake-calculator.ts b/packages/asset-calculator/lib/calculator/chains/handshake-calculator.ts new file mode 100644 index 000000000..35060d8ca --- /dev/null +++ b/packages/asset-calculator/lib/calculator/chains/handshake-calculator.ts @@ -0,0 +1,83 @@ +import { AbstractLogger } from '@rosen-bridge/abstract-logger'; +import axios, { Axios } from '@rosen-bridge/rate-limited-axios'; +import { NATIVE_TOKEN, RosenChainToken, TokenMap } from '@rosen-bridge/tokens'; +import { NETWORKS } from '@rosen-ui/constants'; +import { Network } from '@rosen-ui/types'; +import { zipWith } from 'lodash-es'; + +import AbstractCalculator from '../abstract-calculator'; + +/** + * This type only contains the part of the type that is required here + */ +interface PartialHandshakeRpcAddress { + confirmed: number; + unconfirmed: number; +} + +export class HandshakeCalculator extends AbstractCalculator { + readonly chain: Network = NETWORKS.handshake.key; + + protected client: Axios; + + constructor( + tokenMap: TokenMap, + addresses: string[], + url: string, + logger?: AbstractLogger, + ) { + super(addresses, logger, tokenMap); + this.client = axios.create({ + baseURL: url, + }); + } + + /** + * @param token Handshake chain token supply, always 0 + */ + totalRawSupply = async (): Promise => { + return 0n; + }; + + /** + * @param token Handshake chain token balance, always 0 + */ + totalRawBalance = async (): Promise => { + return 0n; + }; + + /** + * returns locked amounts of a specific token for different addresses + * @param token + */ + getRawLockedAmountsPerAddress = async (token: RosenChainToken) => { + if (token.type === NATIVE_TOKEN) { + const balances = await Promise.all( + this.addresses.map(async (address) => { + try { + // RPC call to get address balance + const response = await this.client.post<{ + result: PartialHandshakeRpcAddress; + }>('', { + method: 'getaddressbalance', + params: [address], + }); + const balance = response.data.result; + return BigInt(balance.confirmed); + } catch (error) { + this.logger?.warn( + `Failed to get balance for Handshake address ${address}: ${error}`, + ); + return 0n; + } + }), + ); + return zipWith(this.addresses, balances, (address, amount) => ({ + address, + amount, + })).filter((amountPerAddress) => amountPerAddress.amount); + } + + return []; + }; +} diff --git a/packages/asset-calculator/lib/interfaces.ts b/packages/asset-calculator/lib/interfaces.ts index 2cc49be89..a47babeef 100644 --- a/packages/asset-calculator/lib/interfaces.ts +++ b/packages/asset-calculator/lib/interfaces.ts @@ -22,6 +22,10 @@ interface EvmCalculatorInterface extends CalculatorInterface { authToken?: string; } +interface HandshakeCalculatorInterface extends CalculatorInterface { + rpcUrl: string; +} + interface CalculatorInterface { addresses: string[]; } @@ -33,4 +37,5 @@ export { BitcoinRunesCalculatorInterface, EvmCalculatorInterface, DogeCalculatorInterface, + HandshakeCalculatorInterface, }; diff --git a/packages/asset-calculator/tests/asset-calculator.spec.ts b/packages/asset-calculator/tests/asset-calculator.spec.ts index 9deeadbc8..dad09d1f1 100644 --- a/packages/asset-calculator/tests/asset-calculator.spec.ts +++ b/packages/asset-calculator/tests/asset-calculator.spec.ts @@ -33,6 +33,7 @@ describe('AssetCalculator', () => { addresses: ['hotAddr', 'coldAddr'], blockcypherUrl: 'blockcypherUrl', }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'handshakeRpcUrl' }, dataSource, ); }); @@ -114,6 +115,7 @@ describe('AssetCalculator', () => { addresses: ['hotAddr', 'coldAddr'], blockcypherUrl: 'blockcypherUrl', }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'handshakeRpcUrl' }, dataSource, ); }); @@ -186,6 +188,7 @@ describe('AssetCalculator', () => { { addresses: ['Addr'], rpcUrl: 'rpcUrl' }, { addresses: ['Addr'], rpcUrl: 'bnbRpcUrl' }, { addresses: ['Addr'], blockcypherUrl: 'blockcypherUrl' }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'handshakeRpcUrl' }, dataSource, ); assetCalculator['calculateEmissionForChain'] = () => @@ -301,6 +304,7 @@ describe('AssetCalculator', () => { { addresses: ['Addr'], rpcUrl: 'rpcUrl' }, { addresses: ['Addr'], rpcUrl: 'bnbRpcUrl' }, { addresses: ['Addr'], blockcypherUrl: 'blockcypherUrl' }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'handshakeRpcUrl' }, dataSource, ); await assetCalculator['tokenModel']['tokenRepository'].insert(tokens); From 3bd501a62c7ce876db47a590005545db0afe635a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Thu, 30 Oct 2025 17:38:13 +0700 Subject: [PATCH 5/8] finished initial handshake implementation --- apps/rosen-service/config/default.yaml | 14 ++++ apps/rosen-service/package.json | 2 + .../src/calculator/calculator-service.ts | 4 + apps/rosen-service/src/configs.ts | 23 ++++++ apps/rosen-service/src/constants.ts | 3 + .../event-trigger/event-trigger-service.ts | 17 +++++ .../src/observation/chains/handshake.ts | 43 +++++++++++ .../src/observation/observation-service.ts | 2 + .../src/scanner/chains/handshake.ts | 76 +++++++++++++++++++ .../src/scanner/scanner-service.ts | 7 ++ apps/rosen/.env.example | 1 + apps/rosen/package.json | 2 + build.sh | 2 + packages/constants/src/index.ts | 8 ++ packages/icons/src/index.ts | 3 + packages/icons/src/networks/handshake.svg | 4 + packages/utils/src/getAddressUrl.ts | 1 + packages/utils/src/getTokenUrl.ts | 1 + packages/utils/src/getTxUrl.ts | 1 + 19 files changed, 214 insertions(+) create mode 100644 apps/rosen-service/src/observation/chains/handshake.ts create mode 100644 apps/rosen-service/src/scanner/chains/handshake.ts create mode 100644 packages/icons/src/networks/handshake.svg diff --git a/apps/rosen-service/config/default.yaml b/apps/rosen-service/config/default.yaml index a0efb56fc..7028d02a7 100644 --- a/apps/rosen-service/config/default.yaml +++ b/apps/rosen-service/config/default.yaml @@ -70,6 +70,17 @@ doge: # - url: # username: # password: +handshake: + addresses: + lock: + eventTrigger: + permit: + fraud: + commitment: + tokens: + rwt: + initialHeight: + rpcUrl: ethereum: addresses: lock: @@ -104,6 +115,7 @@ calculator: bitcoin: [] bitcoin-runes: [] doge: [] + handshake: [] ethereum: [] binance: [] healthCheck: @@ -119,6 +131,8 @@ healthCheck: ethereumScannerCriticalDiff: 5 binanceScannerWarnDiff: 3 binanceScannerCriticalDiff: 5 + handshakeScannerWarnDiff: 3 + handshakeScannerCriticalDiff: 5 interval: 60 # health check update interval (in seconds) duration: 600 # log duration time check (in seconds) maxAllowedErrorCount: 1 # maximum allowed error log lines diff --git a/apps/rosen-service/package.json b/apps/rosen-service/package.json index f508c7c12..72c4d3970 100644 --- a/apps/rosen-service/package.json +++ b/apps/rosen-service/package.json @@ -34,6 +34,8 @@ "@rosen-bridge/ergo-scanner": "^0.1.1", "@rosen-bridge/evm-observation-extractor": "^5.2.2", "@rosen-bridge/evm-scanner": "^0.1.1", + "@rosen-bridge/handshake-rpc-observation-extractor": "^0.1.0", + "@rosen-bridge/handshake-rpc-scanner": "^0.1.0", "@rosen-bridge/health-check": "7.0.0", "@rosen-bridge/log-level-check": "^2.0.0", "@rosen-bridge/rate-limited-axios": "^0.2.1", diff --git a/apps/rosen-service/src/calculator/calculator-service.ts b/apps/rosen-service/src/calculator/calculator-service.ts index d731fb0eb..8abd28ae6 100644 --- a/apps/rosen-service/src/calculator/calculator-service.ts +++ b/apps/rosen-service/src/calculator/calculator-service.ts @@ -78,6 +78,10 @@ const start = async () => { addresses: config.calculator.addresses.doge, blockcypherUrl: config.doge.blockcypherUrl, }, + { + addresses: config.calculator.addresses.handshake, + rpcUrl: config.handshake.rpcUrl, + }, dataSource, logger, ); diff --git a/apps/rosen-service/src/configs.ts b/apps/rosen-service/src/configs.ts index 428b53ca8..a97faadab 100644 --- a/apps/rosen-service/src/configs.ts +++ b/apps/rosen-service/src/configs.ts @@ -144,6 +144,22 @@ const getConfig = () => { }> >('doge.rpcConnections'), }, + handshake: { + addresses: { + lock: nodeConfig.get('handshake.addresses.lock'), + eventTrigger: nodeConfig.get( + 'handshake.addresses.eventTrigger', + ), + permit: nodeConfig.get('handshake.addresses.permit'), + fraud: nodeConfig.get('handshake.addresses.fraud'), + commitment: nodeConfig.get('handshake.addresses.commitment'), + }, + initialHeight: nodeConfig.get('handshake.initialHeight'), + tokens: { + rwt: nodeConfig.get('handshake.tokens.rwt'), + }, + rpcUrl: nodeConfig.get('handshake.rpcUrl'), + }, postgres: { url: nodeConfig.get('postgres.url'), logging: nodeConfig.get('postgres.logging'), @@ -160,6 +176,7 @@ const getConfig = () => { ethereum: nodeConfig.get('calculator.addresses.ethereum'), binance: nodeConfig.get('calculator.addresses.binance'), doge: nodeConfig.get('calculator.addresses.doge'), + handshake: nodeConfig.get('calculator.addresses.handshake'), }, }, healthCheck: { @@ -199,6 +216,12 @@ const getConfig = () => { binanceScannerCriticalDiff: nodeConfig.get( 'healthCheck.binanceScannerCriticalDiff', ), + handshakeScannerWarnDiff: nodeConfig.get( + 'healthCheck.handshakeScannerWarnDiff', + ), + handshakeScannerCriticalDiff: nodeConfig.get( + 'healthCheck.handshakeScannerCriticalDiff', + ), updateInterval: nodeConfig.get('healthCheck.interval'), logDuration: nodeConfig.get('healthCheck.duration'), errorLogAllowedCount: nodeConfig.get( diff --git a/apps/rosen-service/src/constants.ts b/apps/rosen-service/src/constants.ts index bc721a2b5..07ed0e6b1 100644 --- a/apps/rosen-service/src/constants.ts +++ b/apps/rosen-service/src/constants.ts @@ -4,6 +4,7 @@ export const ERGO_SCANNER_INTERVAL = 2 * 60 * 1000; export const CARDANO_SCANNER_INTERVAL = 30 * 1000; export const BITCOIN_SCANNER_INTERVAL = 10 * 60 * 1000; export const DOGE_SCANNER_INTERVAL = 60 * 1000; +export const HANDSHAKE_SCANNER_INTERVAL = 10 * 60 * 1000; export const ETHEREUM_SCANNER_INTERVAL = 60 * 1000; export const BINANCE_SCANNER_INTERVAL = 10 * 1000; export const ASSET_CALCULATOR_INTERVAL = 30 * 1000; @@ -12,11 +13,13 @@ export const ERGO_SCANNER_LOGGER_NAME = 'ergo-scanner'; export const CARDANO_SCANNER_LOGGER_NAME = 'cardano-scanner'; export const BITCOIN_SCANNER_LOGGER_NAME = 'bitcoin-scanner'; export const DOGE_SCANNER_LOGGER_NAME = 'doge-scanner'; +export const HANDSHAKE_SCANNER_LOGGER_NAME = 'handshake-scanner'; export const ETHEREUM_SCANNER_LOGGER_NAME = 'ethereum-scanner'; export const BINANCE_SCANNER_LOGGER_NAME = 'binance-scanner'; export const ETHEREUM_BLOCK_TIME = 12; export const BINANCE_BLOCK_TIME = 3; export const DOGE_BLOCK_TIME = 60; +export const HANDSHAKE_BLOCK_TIME = 150; export const BITCOIN_RUNES_CONFIG_KEY = 'bitcoinRunes'; diff --git a/apps/rosen-service/src/event-trigger/event-trigger-service.ts b/apps/rosen-service/src/event-trigger/event-trigger-service.ts index f9775f912..d074acedf 100644 --- a/apps/rosen-service/src/event-trigger/event-trigger-service.ts +++ b/apps/rosen-service/src/event-trigger/event-trigger-service.ts @@ -32,6 +32,10 @@ const binanceEventTriggerExtractorLogger = ); const dogeEventTriggerExtractorLogger = CallbackLoggerFactory.getInstance().getLogger('doge-event-trigger-extractor'); +const handshakeEventTriggerExtractorLogger = + CallbackLoggerFactory.getInstance().getLogger( + 'handshake-event-trigger-extractor', + ); /** * register event trigger extractors for all chains @@ -116,6 +120,17 @@ export const registerExtractors = async (scanner: ErgoScanner) => { configs.binance.addresses.fraud, binanceEventTriggerExtractorLogger, ); + const handshakeEventTriggerExtractor = new EventTriggerExtractor( + 'handshake-extractor', + dataSource, + ErgoNetworkType.Explorer, + configs.ergo.explorerUrl, + configs.handshake.addresses.eventTrigger, + configs.handshake.tokens.rwt, + configs.handshake.addresses.permit, + configs.handshake.addresses.fraud, + handshakeEventTriggerExtractorLogger, + ); await scanner.registerExtractor(ergoEventTriggerExtractor); await scanner.registerExtractor(cardanoEventTriggerExtractor); await scanner.registerExtractor(bitcoinEventTriggerExtractor); @@ -123,6 +138,7 @@ export const registerExtractors = async (scanner: ErgoScanner) => { await scanner.registerExtractor(dogeEventTriggerExtractor); await scanner.registerExtractor(ethereumEventTriggerExtractor); await scanner.registerExtractor(binanceEventTriggerExtractor); + await scanner.registerExtractor(handshakeEventTriggerExtractor); logger.debug('event trigger extractors registered', { scannerName: scanner.name(), @@ -134,6 +150,7 @@ export const registerExtractors = async (scanner: ErgoScanner) => { dogeEventTriggerExtractor.getId(), ethereumEventTriggerExtractor.getId(), binanceEventTriggerExtractor.getId(), + handshakeEventTriggerExtractor.getId(), ], }); } catch (error) { diff --git a/apps/rosen-service/src/observation/chains/handshake.ts b/apps/rosen-service/src/observation/chains/handshake.ts new file mode 100644 index 000000000..895f0890a --- /dev/null +++ b/apps/rosen-service/src/observation/chains/handshake.ts @@ -0,0 +1,43 @@ +import { CallbackLoggerFactory } from '@rosen-bridge/callback-logger'; +import { HandshakeRpcObservationExtractor } from '@rosen-bridge/handshake-rpc-observation-extractor'; +import { HandshakeRpcScanner } from '@rosen-bridge/handshake-rpc-scanner'; + +import config from '../../configs'; +import dataSource from '../../data-source'; +import AppError from '../../errors/AppError'; +import { getTokenMap } from '../../utils'; + +const logger = CallbackLoggerFactory.getInstance().getLogger(import.meta.url); + +/** + * register an observation extractor for the provided scanner + * @param scanner + */ +export const registerHandshakeExtractor = async ( + scanner: HandshakeRpcScanner, +) => { + try { + const observationExtractor = new HandshakeRpcObservationExtractor( + config.handshake.addresses.lock, + dataSource, + await getTokenMap(), + logger, + ); + + await scanner.registerExtractor(observationExtractor); + + logger.debug('handshake observation extractor registered', { + scannerName: scanner.name(), + }); + } catch (error) { + throw new AppError( + `cannot create or register handshake observation extractor due to error: ${error}`, + false, + 'error', + error instanceof Error ? error.stack : undefined, + { + scannerName: scanner.name(), + }, + ); + } +}; diff --git a/apps/rosen-service/src/observation/observation-service.ts b/apps/rosen-service/src/observation/observation-service.ts index 8d070f39f..d0a728214 100644 --- a/apps/rosen-service/src/observation/observation-service.ts +++ b/apps/rosen-service/src/observation/observation-service.ts @@ -5,11 +5,13 @@ import { registerCardanoExtractor } from './chains/cardano'; import { registerDogeExtractor } from './chains/doge'; import { registerErgoExtractor } from './chains/ergo'; import { registerEthereumExtractor } from './chains/ethereum'; +import { registerHandshakeExtractor } from './chains/handshake'; const observationService = { registerBitcoinExtractor, registerBitcoinRunesExtractor, registerDogeExtractor, + registerHandshakeExtractor, registerCardanoExtractor, registerErgoExtractor, registerEthereumExtractor, diff --git a/apps/rosen-service/src/scanner/chains/handshake.ts b/apps/rosen-service/src/scanner/chains/handshake.ts new file mode 100644 index 000000000..fc93fb27c --- /dev/null +++ b/apps/rosen-service/src/scanner/chains/handshake.ts @@ -0,0 +1,76 @@ +import { + FailoverStrategy, + NetworkConnectorManager, +} from '@rosen-bridge/abstract-scanner'; +import { CallbackLoggerFactory } from '@rosen-bridge/callback-logger'; +import { + HandshakeRpcNetwork, + HandshakeRpcScanner, + HandshakeRpcTransaction, +} from '@rosen-bridge/handshake-rpc-scanner'; + +import config from '../../configs'; +import { + HANDSHAKE_SCANNER_INTERVAL, + HANDSHAKE_SCANNER_LOGGER_NAME, + SCANNER_API_TIMEOUT, +} from '../../constants'; +import dataSource from '../../data-source'; +import AppError from '../../errors/AppError'; +import observationService from '../../observation/observation-service'; +import { startScanner } from '../scanner-utils'; + +const logger = CallbackLoggerFactory.getInstance().getLogger(import.meta.url); +const scannerLogger = CallbackLoggerFactory.getInstance().getLogger( + HANDSHAKE_SCANNER_LOGGER_NAME, +); + +/** + * Creates and configures a NetworkConnectorManager instance for handshake scanner + */ +export const createHandshakeNetworkConnectorManager = () => { + const networkConnectorManager = + new NetworkConnectorManager( + new FailoverStrategy(), + scannerLogger, + ); + + networkConnectorManager.addConnector( + new HandshakeRpcNetwork( + config.handshake.rpcUrl, + SCANNER_API_TIMEOUT * 1000, + ), + ); + + return networkConnectorManager; +}; + +/** + * create a handshake scanner, initializing it and calling its update method + * periodically + */ +export const startHandshakeScanner = async () => { + try { + const scanner = new HandshakeRpcScanner({ + dataSource, + initialHeight: config.handshake.initialHeight, + logger: scannerLogger, + network: createHandshakeNetworkConnectorManager(), + }); + + await observationService.registerHandshakeExtractor(scanner); + + startScanner(scanner, import.meta.url, HANDSHAKE_SCANNER_INTERVAL); + + logger.debug('handshake scanner started'); + + return scanner; + } catch (error) { + throw new AppError( + `cannot create or start handshake scanner due to error: ${error}`, + false, + 'error', + error instanceof Error ? error.stack : undefined, + ); + } +}; diff --git a/apps/rosen-service/src/scanner/scanner-service.ts b/apps/rosen-service/src/scanner/scanner-service.ts index f810e75e8..5e7e2c25c 100644 --- a/apps/rosen-service/src/scanner/scanner-service.ts +++ b/apps/rosen-service/src/scanner/scanner-service.ts @@ -6,6 +6,7 @@ import { CallbackLoggerFactory } from '@rosen-bridge/callback-logger'; import { CardanoKoiosScanner } from '@rosen-bridge/cardano-scanner'; import { ErgoScanner } from '@rosen-bridge/ergo-scanner'; import { EvmRpcScanner } from '@rosen-bridge/evm-scanner'; +import { HandshakeRpcScanner } from '@rosen-bridge/handshake-rpc-scanner'; import { handleError } from '../utils'; import { startBinanceScanner } from './chains/binance'; @@ -14,6 +15,7 @@ import { startCardanoScanner } from './chains/cardano'; import { startDogeScanner } from './chains/doge'; import { startErgoScanner } from './chains/ergo'; import { startEthereumScanner } from './chains/ethereum'; +import { startHandshakeScanner } from './chains/handshake'; const logger = CallbackLoggerFactory.getInstance().getLogger(import.meta.url); @@ -24,6 +26,7 @@ let bitcoinScanner: BitcoinRpcScanner; let ethereumScanner: EvmRpcScanner; let binanceScanner: EvmRpcScanner; let dogeScanner: DogeRpcScanner; +let handshakeScanner: HandshakeRpcScanner; /** * start all scanners and register their extractors @@ -37,6 +40,7 @@ const start = async () => { ethereumScanner, binanceScanner, dogeScanner, + handshakeScanner, ] = await Promise.all([ startErgoScanner(), startCardanoScanner(), @@ -44,6 +48,7 @@ const start = async () => { startEthereumScanner(), startBinanceScanner(), startDogeScanner(), + startHandshakeScanner(), ]); logger.debug('all scanners started and their extractors registered', { @@ -54,6 +59,7 @@ const start = async () => { ethereumScanner.name(), binanceScanner.name(), dogeScanner.name(), + handshakeScanner.name(), ], }); } catch (error) { @@ -70,6 +76,7 @@ const scannerService = { getEthereumScanner: () => ethereumScanner, getBinanceScanner: () => binanceScanner, getDogeScanner: () => dogeScanner, + getHandshakeScanner: () => handshakeScanner, }; export default scannerService; diff --git a/apps/rosen/.env.example b/apps/rosen/.env.example index ebd4492f7..03fe55602 100644 --- a/apps/rosen/.env.example +++ b/apps/rosen/.env.example @@ -11,6 +11,7 @@ ETHEREUM_RPC_API='' BINANCE_RPC_API='' BITCOIN_RUNES_API='' BITCOIN_RUNES_SECRET='' +HANDSHAKE_RPC_API='' DISCORD_LOGGER_WEBHOOK_URL='' diff --git a/apps/rosen/package.json b/apps/rosen/package.json index f50d5962d..605475f15 100644 --- a/apps/rosen/package.json +++ b/apps/rosen/package.json @@ -31,10 +31,12 @@ "@rosen-network/bitcoin-runes": "^0.1.0", "@rosen-network/cardano": "^2.4.1", "@rosen-network/doge": "^0.3.1", + "@rosen-network/handshake": "^0.1.0", "@rosen-network/ergo": "^2.4.1", "@rosen-network/ethereum": "^0.4.1", "@rosen-network/evm": "^0.3.3", "@rosen-ui/asset-calculator": "^2.1.8", + "@rosen-ui/bob-extension": "^0.1.0", "@rosen-ui/constants": "^0.4.1", "@rosen-ui/data-source": "^0.1.1", "@rosen-ui/eternl-wallet": "^3.1.1", diff --git a/build.sh b/build.sh index e4dd27be2..dad03a1bd 100755 --- a/build.sh +++ b/build.sh @@ -35,10 +35,12 @@ if [ "$APP" == "rosen" ] || [ "$APP" == "default" ]; then npm run build --workspace networks/bitcoin npm run build --workspace networks/bitcoin-runes npm run build --workspace networks/doge + npm run build --workspace networks/handshake npm run build --workspace networks/cardano npm run build --workspace networks/ergo npm run build --workspace networks/ethereum npm run build --workspace wallets/wallet-api + npm run build --workspace wallets/bob-extension npm run build --workspace wallets/eternl npm run build --workspace wallets/lace npm run build --workspace wallets/metamask diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index cfc149ac5..42c425bb4 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -65,6 +65,14 @@ export const NETWORKS = { id: '', hasTokenSupport: false, }, + 'handshake': { + index: 7, + key: 'handshake', + label: 'Handshake', + nativeToken: 'hns', + id: '', + hasTokenSupport: false, + }, } as const; export const NETWORKS_KEYS = Object.values(NETWORKS).map( diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index a807026b5..eb58d8da2 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -237,3 +237,6 @@ export { default as ErgoRaw } from './networks/ergo.svg?raw'; export { ReactComponent as Ethereum } from './networks/ethereum.svg'; export { default as EthereumRaw } from './networks/ethereum.svg?raw'; + +export { ReactComponent as Handshake } from './networks/handshake.svg'; +export { default as HandshakeRaw } from './networks/handshake.svg?raw'; diff --git a/packages/icons/src/networks/handshake.svg b/packages/icons/src/networks/handshake.svg new file mode 100644 index 000000000..7ac35c729 --- /dev/null +++ b/packages/icons/src/networks/handshake.svg @@ -0,0 +1,4 @@ + + + diff --git a/packages/utils/src/getAddressUrl.ts b/packages/utils/src/getAddressUrl.ts index 366a64e60..204b7c599 100644 --- a/packages/utils/src/getAddressUrl.ts +++ b/packages/utils/src/getAddressUrl.ts @@ -9,6 +9,7 @@ const baseAddressURLs: { [key in Network]: string } = { [NETWORKS['bitcoin-runes'].key]: 'https://uniscan.cc/address', [NETWORKS.ethereum.key]: 'https://etherscan.io/address', [NETWORKS.doge.key]: 'https://blockexplorer.one/dogecoin/mainnet/address', + [NETWORKS.handshake.key]: 'https://e.hnsfans.com/address', }; export const getAddressUrl = ( diff --git a/packages/utils/src/getTokenUrl.ts b/packages/utils/src/getTokenUrl.ts index 66cb748be..b00481f3e 100644 --- a/packages/utils/src/getTokenUrl.ts +++ b/packages/utils/src/getTokenUrl.ts @@ -9,6 +9,7 @@ const baseTokenURLs: { [key in Network]: string } = { [NETWORKS['bitcoin-runes'].key]: 'https://unisat.io/runes/detail', [NETWORKS.ethereum.key]: 'https://etherscan.io/token', [NETWORKS.doge.key]: '', + [NETWORKS.handshake.key]: '', }; export const getTokenUrl = ( diff --git a/packages/utils/src/getTxUrl.ts b/packages/utils/src/getTxUrl.ts index cc6879db9..779230576 100644 --- a/packages/utils/src/getTxUrl.ts +++ b/packages/utils/src/getTxUrl.ts @@ -16,6 +16,7 @@ const baseTxURLs: { [key in Network]: string } = { [NETWORKS['bitcoin-runes'].key]: 'https://uniscan.cc/tx', [NETWORKS.ethereum.key]: 'https://etherscan.io/tx', [NETWORKS.doge.key]: 'https://blockexplorer.one/dogecoin/mainnet/tx', + [NETWORKS.handshake.key]: 'https://e.hnsfans.com/tx', }; export const getTxURL = ( From 30eac27f4dd62eec43d1ef7f65096f26d031ce3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Mon, 2 Feb 2026 13:17:10 +0700 Subject: [PATCH 6/8] replaced bob extension with shake wallet --- apps/rosen/package.json | 2 +- apps/rosen/src/wallets/bobExtension.ts | 9 - apps/rosen/src/wallets/index.ts | 2 +- apps/rosen/src/wallets/shake.ts | 12 + build.sh | 2 +- wallets/bob-extension/README.md | 60 ----- wallets/bob-extension/src/icon.ts | 17 -- wallets/bob-extension/src/index.ts | 3 - wallets/shake/README.md | 60 +++++ wallets/{bob-extension => shake}/package.json | 4 +- wallets/shake/src/icon.ts | 24 ++ wallets/shake/src/index.ts | 3 + wallets/{bob-extension => shake}/src/types.ts | 54 ++--- .../{bob-extension => shake}/src/wallet.ts | 229 +++++++++--------- .../{bob-extension => shake}/tsconfig.json | 0 15 files changed, 234 insertions(+), 247 deletions(-) delete mode 100644 apps/rosen/src/wallets/bobExtension.ts create mode 100644 apps/rosen/src/wallets/shake.ts delete mode 100644 wallets/bob-extension/README.md delete mode 100644 wallets/bob-extension/src/icon.ts delete mode 100644 wallets/bob-extension/src/index.ts create mode 100644 wallets/shake/README.md rename wallets/{bob-extension => shake}/package.json (86%) create mode 100644 wallets/shake/src/icon.ts create mode 100644 wallets/shake/src/index.ts rename wallets/{bob-extension => shake}/src/types.ts (61%) rename wallets/{bob-extension => shake}/src/wallet.ts (54%) rename wallets/{bob-extension => shake}/tsconfig.json (100%) diff --git a/apps/rosen/package.json b/apps/rosen/package.json index 605475f15..b2b5b2fff 100644 --- a/apps/rosen/package.json +++ b/apps/rosen/package.json @@ -36,7 +36,7 @@ "@rosen-network/ethereum": "^0.4.1", "@rosen-network/evm": "^0.3.3", "@rosen-ui/asset-calculator": "^2.1.8", - "@rosen-ui/bob-extension": "^0.1.0", + "@rosen-ui/shake-wallet": "^0.1.0", "@rosen-ui/constants": "^0.4.1", "@rosen-ui/data-source": "^0.1.1", "@rosen-ui/eternl-wallet": "^3.1.1", diff --git a/apps/rosen/src/wallets/bobExtension.ts b/apps/rosen/src/wallets/bobExtension.ts deleted file mode 100644 index 72d43222f..000000000 --- a/apps/rosen/src/wallets/bobExtension.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BobExtensionWallet } from '@rosen-ui/bob-extension'; - -import { handshake } from '@/networks'; -import { getTokenMap } from '@/tokenMap/getClientTokenMap'; - -export const bobExtension = new BobExtensionWallet({ - networks: [handshake], - getTokenMap, -}); diff --git a/apps/rosen/src/wallets/index.ts b/apps/rosen/src/wallets/index.ts index 788176fda..e21bffbbd 100644 --- a/apps/rosen/src/wallets/index.ts +++ b/apps/rosen/src/wallets/index.ts @@ -1,6 +1,6 @@ import './base'; -export * from './bobExtension'; +export * from './shake'; export * from './eternl'; export * from './lace'; export * from './metaMask'; diff --git a/apps/rosen/src/wallets/shake.ts b/apps/rosen/src/wallets/shake.ts new file mode 100644 index 000000000..d1450ef27 --- /dev/null +++ b/apps/rosen/src/wallets/shake.ts @@ -0,0 +1,12 @@ +import { ShakeWallet } from '@rosen-ui/shake-wallet'; + +import { handshake } from '@/networks'; +import { getTokenMap } from '@/tokenMap/getClientTokenMap'; + +export const shakeWallet = new ShakeWallet({ + networks: [handshake], + getTokenMap, + lockScriptHex: 'd07657877c58879b', + lockedNames: ['ccj3'], + publicNodeUrl: 'https://api.handshakeapi.com/hsd', +}); diff --git a/build.sh b/build.sh index dad03a1bd..93733d0f1 100755 --- a/build.sh +++ b/build.sh @@ -40,7 +40,7 @@ if [ "$APP" == "rosen" ] || [ "$APP" == "default" ]; then npm run build --workspace networks/ergo npm run build --workspace networks/ethereum npm run build --workspace wallets/wallet-api - npm run build --workspace wallets/bob-extension + npm run build --workspace wallets/shake-wallet npm run build --workspace wallets/eternl npm run build --workspace wallets/lace npm run build --workspace wallets/metamask diff --git a/wallets/bob-extension/README.md b/wallets/bob-extension/README.md deleted file mode 100644 index 80e512063..000000000 --- a/wallets/bob-extension/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Bob Extension Wallet Integration - -This package provides integration between the Bob Extension (Handshake wallet) and Rosen Bridge UI. - -## Features - -- Connect to Bob Extension browser wallet -- Get wallet address and balance -- Send HNS transactions through Bob Extension -- Automatic detection of Bob Extension availability -- Seamless integration with Rosen Bridge UI - -## Requirements - -- Bob Extension installed in browser -- Bob Extension unlocked and connected -- Handshake (HNS) balance for transactions - -## Usage - -The Bob Extension wallet will automatically appear in the Rosen Bridge UI wallet selection when: - -1. Bob Extension is installed in the browser -2. User is on a page with Handshake as source/target chain -3. Bob Extension is unlocked - -## Architecture - -This wallet integration leverages the existing Bob3 API exposed by Bob Extension: - -```typescript -// Connect to Bob Extension -const wallet = await bob3.connect(); - -// Get wallet info -const address = await wallet.getAddress(); -const balance = await wallet.getBalance(); - -// Send transaction -const tx = await wallet.send(lockAddress, amountInHNS); -``` - -## Development - -```bash -# Build the wallet package -npm run build - -# Build the entire Rosen Bridge UI with Bob Extension -cd ../../ -./build.sh rosen -``` - -## Integration Details - -- **Chain Support**: Handshake (HNS) only -- **Transaction Type**: Standard HNS transfers to bridge lock address -- **Metadata**: Bridge metadata handled by Rosen Bridge network layer -- **Security**: All signing happens within Bob Extension -- **User Experience**: Familiar Bob Extension popup for transaction confirmation diff --git a/wallets/bob-extension/src/icon.ts b/wallets/bob-extension/src/icon.ts deleted file mode 100644 index 750cd7abf..000000000 --- a/wallets/bob-extension/src/icon.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ICON = ` - - - - - - - - - - - - - - - -`; diff --git a/wallets/bob-extension/src/index.ts b/wallets/bob-extension/src/index.ts deleted file mode 100644 index 02af13b94..000000000 --- a/wallets/bob-extension/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { BobExtensionWallet } from './wallet'; -export type { BobExtensionConfig, Bob3API, Bob3Wallet } from './types'; -export { ICON } from './icon'; diff --git a/wallets/shake/README.md b/wallets/shake/README.md new file mode 100644 index 000000000..6d36fe0b1 --- /dev/null +++ b/wallets/shake/README.md @@ -0,0 +1,60 @@ +# Shake Wallet Integration + +This package provides integration between the Shake Wallet (Handshake wallet) and Rosen Bridge UI. + +## Features + +- Connect to Shake Wallet browser wallet +- Get wallet address and balance +- Send HNS transactions through Shake Wallet +- Automatic detection of Shake Wallet availability +- Seamless integration with Rosen Bridge UI + +## Requirements + +- Shake Wallet installed in browser +- Shake Wallet unlocked and connected +- Handshake (HNS) balance for transactions + +## Usage + +The Shake Wallet will automatically appear in the Rosen Bridge UI wallet selection when: + +1. Shake Wallet is installed in the browser +2. User is on a page with Handshake as source/target chain +3. Shake Wallet is unlocked + +## Architecture + +This wallet integration leverages the existing Shake API exposed by Shake Wallet: + +```typescript +// Connect to Shake Wallet +const wallet = await shake.connect(); + +// Get wallet info +const address = await wallet.getAddress(); +const balance = await wallet.getBalance(); + +// Send transaction +const tx = await wallet.send(lockAddress, amountInHNS); +``` + +## Development + +```bash +# Build the wallet package +npm run build + +# Build the entire Rosen Bridge UI with Shake Wallet +cd ../../ +./build.sh rosen +``` + +## Integration Details + +- **Chain Support**: Handshake (HNS) only +- **Transaction Type**: Standard HNS transfers to bridge lock address +- **Metadata**: Bridge metadata handled by Rosen Bridge network layer +- **Security**: All signing happens within Shake Wallet +- **User Experience**: Familiar Shake Wallet popup for transaction confirmation diff --git a/wallets/bob-extension/package.json b/wallets/shake/package.json similarity index 86% rename from wallets/bob-extension/package.json rename to wallets/shake/package.json index 12638a64f..b0ea741c1 100644 --- a/wallets/bob-extension/package.json +++ b/wallets/shake/package.json @@ -1,10 +1,10 @@ { - "name": "@rosen-ui/bob-extension", + "name": "@rosen-ui/shake-wallet", "version": "0.1.0", "private": true, "license": "MIT", "author": "Aco Šmrkas", - "description": "Bob Extension wallet integration for Rosen Bridge", + "description": "Shake Wallet integration for Rosen Bridge", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", diff --git a/wallets/shake/src/icon.ts b/wallets/shake/src/icon.ts new file mode 100644 index 000000000..a4d0ca7ce --- /dev/null +++ b/wallets/shake/src/icon.ts @@ -0,0 +1,24 @@ +export const ICON = ` + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/wallets/shake/src/index.ts b/wallets/shake/src/index.ts new file mode 100644 index 000000000..5a43a5b1b --- /dev/null +++ b/wallets/shake/src/index.ts @@ -0,0 +1,3 @@ +export { ShakeWallet } from './wallet'; +export type { ShakeWalletConfig, ShakeAPI, ShakeWallet } from './types'; +export { ICON } from './icon'; diff --git a/wallets/bob-extension/src/types.ts b/wallets/shake/src/types.ts similarity index 61% rename from wallets/bob-extension/src/types.ts rename to wallets/shake/src/types.ts index dcd435de5..6a6896b57 100644 --- a/wallets/bob-extension/src/types.ts +++ b/wallets/shake/src/types.ts @@ -1,18 +1,15 @@ /** - * Bob Extension wallet types + * Shake Wallet types */ import { WalletConfig } from '@rosen-ui/wallet-api'; -export type BobExtensionConfig = WalletConfig & { - // No specific config needed - Bob Extension auto-detects +export type ShakeWalletConfig = WalletConfig & { + lockScriptHex: string; + lockedNames: string[]; + publicNodeUrl: string; }; -export interface Bob3API { - connect(): Promise; - isLocked(): Promise; -} - -export interface Bob3Wallet { +export interface ShakeWallet { getAddress(): Promise; getBalance(): Promise<{ confirmed: number; @@ -21,10 +18,6 @@ export interface Bob3Wallet { }>; send(address: string, amount: number): Promise<{ hash: string }>; - // Advanced transaction building - createTx(options: CreateTxOptions): Promise<{ hex: string }>; - sendTx(hex: string): Promise<{ hash: string }>; - sign(address: string, message: string): Promise; signWithName(name: string, message: string): Promise; verify(message: string, signature: string, address: string): Promise; @@ -47,10 +40,15 @@ export interface Bob3Wallet { name: string, records: UpdateRecordType[], ): Promise<{ hash: string }>; - sendCustomTx( - outputs: unknown[], - rate?: number, - subtractFee?: boolean, + sendRosenBridgeLock( + opts: { + name: string; + lockScriptHex: string; + resourceHex: string; + recipientAddress?: string; + recipientAmount?: number; + rate?: number; + }, ): Promise<{ hash: string }>; // Info functions @@ -60,21 +58,9 @@ export interface Bob3Wallet { hashName(name: string): Promise; } -export interface CreateTxOptions { - outputs: TxOutput[]; - rate?: number; - subtractFee?: boolean; -} - -export interface TxOutput { - address?: string; - value: number; - data?: string; // For OP_RETURN data -} - -// Bob3 API that's accessible via window.bob3 -export interface Bob3API { - connect(): Promise; +// Shake API that's accessible via window.shake +export interface ShakeAPI { + connect(): Promise; isLocked(): Promise; } @@ -83,9 +69,9 @@ export interface UpdateRecordType { [key: string]: unknown; } -// Global declarations for Bob3 extension +// Global declarations for Shake extension declare global { interface Window { - bob3?: Bob3API; + shake?: ShakeAPI; } } diff --git a/wallets/bob-extension/src/wallet.ts b/wallets/shake/src/wallet.ts similarity index 54% rename from wallets/bob-extension/src/wallet.ts rename to wallets/shake/src/wallet.ts index 0ba34edf6..6c941f589 100644 --- a/wallets/bob-extension/src/wallet.ts +++ b/wallets/shake/src/wallet.ts @@ -10,50 +10,107 @@ import { } from '@rosen-ui/wallet-api'; import { ICON } from './icon'; -import { BobExtensionConfig, Bob3Wallet } from './types'; +import { ShakeWalletConfig, ShakeWallet as ShakeWalletAPI } from './types'; /** - * Bob Extension wallet integration for Handshake + * Shake Wallet integration for Handshake * - * This wallet integrates with the Bob Extension to enable seamless - * Handshake transactions for Rosen Bridge. Bob Extension must be + * This wallet integrates with the Shake Wallet to enable seamless + * Handshake transactions for Rosen Bridge. Shake Wallet must be * installed and unlocked for this wallet to function. */ -export class BobExtensionWallet extends Wallet { +export class ShakeWallet extends Wallet { icon = ICON; - name = 'Bob Extension'; + name = 'Shake Wallet'; - label = 'Bob Extension'; + label = 'Shake Wallet'; - link = 'https://bobwallet.io/'; + link = 'https://ipfs.hnsproxy.au/shakewallet/'; currentChain: Network = NETWORKS.handshake.key; supportedChains: Network[] = [NETWORKS.handshake.key]; - private wallet: Bob3Wallet | null = null; + private wallet: ShakeWalletAPI | null = null; /** - * Connect to Bob Extension + * Query the public HSD node to find a locked name whose owner output + * is not currently being spent by any mempool transaction. + */ + private findFreeName = async (): Promise => { + const url = this.config.publicNodeUrl; + + // 1. Get raw mempool TX hashes + const mempoolRes = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method: 'getrawmempool', params: [] }), + }); + const mempoolJson = await mempoolRes.json(); + const txHashes: string[] = mempoolJson.result ?? []; + + // 2. Collect all spent outpoints from mempool transactions + const spentOutpoints = new Set(); + await Promise.all( + txHashes.map(async (hash) => { + try { + const txRes = await fetch(`${url}/tx/${hash}`); + const tx = await txRes.json(); + if (tx.inputs) { + for (const input of tx.inputs) { + if (input.prevout) { + spentOutpoints.add( + `${input.prevout.hash}:${input.prevout.index}`, + ); + } + } + } + } catch { + // Skip unreachable transactions + } + }), + ); + + // 3. For each name, check if its owner output is in the spent set + for (const name of this.config.lockedNames) { + const infoRes = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method: 'getnameinfo', params: [name] }), + }); + const infoJson = await infoRes.json(); + const owner = infoJson.result?.info?.owner; + if (owner && !spentOutpoints.has(`${owner.hash}:${owner.index}`)) { + return name; + } + } + + throw new Error( + 'All locked names are currently being spent in the mempool. Please try again later.', + ); + }; + + /** + * Connect to Shake Wallet */ performConnect = async (): Promise => { - if (!window.bob3) { + if (!window.shake) { throw new Error( - 'Bob Extension not found. Please install Bob Extension from the Chrome Web Store.', + 'Shake Wallet not found. Please install Shake Wallet from the Chrome Web Store.', ); } try { - this.wallet = await window.bob3.connect(); + this.wallet = await window.shake.connect(); // Verify we can get an address (confirms wallet is properly connected) const address = await this.wallet.getAddress(); if (!address) { - throw new Error('Unable to get wallet address from Bob Extension'); + throw new Error('Unable to get wallet address from Shake Wallet'); } - console.log('Connected to Bob Extension, address:', address); + console.log('Connected to Shake Wallet, address:', address); } catch (error) { this.wallet = null; if (error instanceof Error) { @@ -63,18 +120,18 @@ export class BobExtensionWallet extends Wallet { ) { throw new UserDeniedTransactionSignatureError(this.name); } - throw new Error(`Failed to connect to Bob Extension: ${error.message}`); + throw new Error(`Failed to connect to Shake Wallet: ${error.message}`); } - throw new Error('Failed to connect to Bob Extension: Unknown error'); + throw new Error('Failed to connect to Shake Wallet: Unknown error'); } }; /** - * Disconnect from Bob Extension + * Disconnect from Shake Wallet */ performDisconnect = async (): Promise => { this.wallet = null; - // Bob Extension doesn't require explicit disconnect + // Shake Wallet doesn't require explicit disconnect }; /** @@ -88,7 +145,7 @@ export class BobExtensionWallet extends Wallet { try { return await this.wallet.getAddress(); } catch (error) { - console.error('Failed to fetch address from Bob Extension:', error); + console.error('Failed to fetch address from Shake Wallet:', error); return undefined; } }; @@ -106,16 +163,16 @@ export class BobExtensionWallet extends Wallet { return balance.confirmed.toString(); } catch (error) { - console.error('Failed to fetch balance from Bob Extension:', error); + console.error('Failed to fetch balance from Shake Wallet:', error); return '0'; } }; /** - * Check if Bob Extension is available + * Check if Shake Wallet is available */ isAvailable = (): boolean => { - return !!window.bob3; + return !!window.shake; }; /** @@ -130,7 +187,11 @@ export class BobExtensionWallet extends Wallet { }; /** - * Create and submit bridge transaction using Bob Extension with Rosen metadata + * Create and submit a locked UPDATE transaction via Shake Wallet. + * + * Builds a locked UPDATE on the bridge-controlled name, embedding + * Rosen Bridge metadata as the resource hex, and sending HNS to the + * lock address. */ performTransfer = async (params: WalletTransferParams): Promise => { if ( @@ -142,35 +203,42 @@ export class BobExtensionWallet extends Wallet { if (!this.wallet) { throw new Error( - 'Wallet not connected. Please connect to Bob Extension first.', + 'Wallet not connected. Please connect to Shake Wallet first.', ); } try { - // Generate Rosen Bridge metadata for OP_RETURN - const rosenMetadata = await this.currentNetwork.generateOpReturnData( + // Pick a locked name whose owner output is not in the mempool + const lockedName = await this.findFreeName(); + + // Generate Rosen Bridge metadata encoded as hex for the UPDATE resource + const resourceHex = await this.currentNetwork.generateOpReturnData( params.toChain, params.address, params.networkFee.toString(), params.bridgeFee.toString(), ); - console.log('Sending Rosen Bridge transaction:', { - lockAddress: params.lockAddress, - amount: params.amount, - toChain: params.toChain, - toAddress: params.address, - bridgeFee: params.bridgeFee, - networkFee: params.networkFee, - metadata: rosenMetadata, - }); + const opts: { + name: string; + lockScriptHex: string; + resourceHex: string; + recipientAddress?: string; + recipientAmount?: number; + } = { + name: lockedName, + lockScriptHex: this.config.lockScriptHex, + resourceHex, + }; - // Create transaction with OP_RETURN data containing Rosen metadata - const result = await this.createBridgeTransaction( - params.lockAddress, - Number(params.amount), - rosenMetadata, - ); + if (params.lockAddress && Number(params.amount) > 0) { + opts.recipientAddress = params.lockAddress; + opts.recipientAmount = Number(params.amount); + } + + console.log('Sending Rosen Bridge locked update:', opts); + + const result = await this.wallet.sendRosenBridgeLock(opts); if (!result.hash) { throw new Error('Transaction failed - no hash returned'); @@ -199,83 +267,6 @@ export class BobExtensionWallet extends Wallet { } }; - /** - * Create bridge transaction with OP_RETURN metadata - */ - private createBridgeTransaction = async ( - lockAddress: string, - amount: number, - metadata: string, - ): Promise<{ hash: string }> => { - if (!this.wallet) { - throw new Error('Wallet not connected'); - } - - try { - /* - const hexData = Array.from(new TextEncoder().encode(customData)) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); - outputs.push({ - address: recipient, - value: 0, - data: hexData, - }); - */ - - // Create transaction with two outputs: - // 1. Send amount to lock address - // 2. OP_RETURN with Rosen Bridge metadata - const outputs = [ - { - address: lockAddress, - value: amount, - }, - { - value: 0, - data: metadata, // OP_RETURN data containing Rosen Bridge metadata - }, - ]; - - console.log('Creating Rosen Bridge transaction with outputs:', outputs); - - // Create the transaction using Bob Extension's createTx - const broadcastResult = await this.wallet.sendCustomTx( - outputs, - 20, // 20 dollarydoos per byte fee rate - false, // Don't subtract fee from the lock amount - ); - - console.log( - 'Bridge transaction broadcast successfully:', - broadcastResult.hash, - ); - - return broadcastResult; - } catch (error) { - console.error('Failed to create bridge transaction:', error); - - // If custom transaction building fails, fall back to basic send and log metadata - if (error instanceof Error) { - console.warn( - 'Advanced transaction building failed, falling back to basic send', - ); - - // Log the metadata for external processing by the Rosen Bridge network layer - console.log('Rosen Bridge metadata (for network layer processing):', { - metadata, - lockAddress, - amount, - note: 'Custom transaction failed, metadata should be handled by Rosen Bridge network layer', - }); - - return await this.wallet.send(lockAddress, amount); - } - - throw error; - } - }; - /** * Sign a message using the wallet */ diff --git a/wallets/bob-extension/tsconfig.json b/wallets/shake/tsconfig.json similarity index 100% rename from wallets/bob-extension/tsconfig.json rename to wallets/shake/tsconfig.json From d80552f02cddc2598c77dcb737fde4f21e2a6f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Wed, 4 Feb 2026 22:23:37 +0700 Subject: [PATCH 7/8] implemented bitcoin rune-like lock data send method --- apps/rosen/src/wallets/shake.ts | 3 - wallets/shake/src/types.ts | 12 +++- wallets/shake/src/wallet.ts | 101 +++++--------------------------- 3 files changed, 25 insertions(+), 91 deletions(-) diff --git a/apps/rosen/src/wallets/shake.ts b/apps/rosen/src/wallets/shake.ts index d1450ef27..e9672d9c9 100644 --- a/apps/rosen/src/wallets/shake.ts +++ b/apps/rosen/src/wallets/shake.ts @@ -6,7 +6,4 @@ import { getTokenMap } from '@/tokenMap/getClientTokenMap'; export const shakeWallet = new ShakeWallet({ networks: [handshake], getTokenMap, - lockScriptHex: 'd07657877c58879b', - lockedNames: ['ccj3'], - publicNodeUrl: 'https://api.handshakeapi.com/hsd', }); diff --git a/wallets/shake/src/types.ts b/wallets/shake/src/types.ts index 6a6896b57..cc543762f 100644 --- a/wallets/shake/src/types.ts +++ b/wallets/shake/src/types.ts @@ -4,9 +4,10 @@ import { WalletConfig } from '@rosen-ui/wallet-api'; export type ShakeWalletConfig = WalletConfig & { - lockScriptHex: string; - lockedNames: string[]; - publicNodeUrl: string; + // Deprecated: legacy locked name approach + lockScriptHex?: string; + lockedNames?: string[]; + publicNodeUrl?: string; }; export interface ShakeWallet { @@ -50,6 +51,11 @@ export interface ShakeWallet { rate?: number; }, ): Promise<{ hash: string }>; + sendRosenBridgeData(opts: { + receiver: string; + amount: number; + data: string; + }): Promise<{ hash: string }>; // Info functions getNames(): Promise; diff --git a/wallets/shake/src/wallet.ts b/wallets/shake/src/wallet.ts index 6c941f589..6ba947119 100644 --- a/wallets/shake/src/wallet.ts +++ b/wallets/shake/src/wallet.ts @@ -34,63 +34,6 @@ export class ShakeWallet extends Wallet { private wallet: ShakeWalletAPI | null = null; - /** - * Query the public HSD node to find a locked name whose owner output - * is not currently being spent by any mempool transaction. - */ - private findFreeName = async (): Promise => { - const url = this.config.publicNodeUrl; - - // 1. Get raw mempool TX hashes - const mempoolRes = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ method: 'getrawmempool', params: [] }), - }); - const mempoolJson = await mempoolRes.json(); - const txHashes: string[] = mempoolJson.result ?? []; - - // 2. Collect all spent outpoints from mempool transactions - const spentOutpoints = new Set(); - await Promise.all( - txHashes.map(async (hash) => { - try { - const txRes = await fetch(`${url}/tx/${hash}`); - const tx = await txRes.json(); - if (tx.inputs) { - for (const input of tx.inputs) { - if (input.prevout) { - spentOutpoints.add( - `${input.prevout.hash}:${input.prevout.index}`, - ); - } - } - } - } catch { - // Skip unreachable transactions - } - }), - ); - - // 3. For each name, check if its owner output is in the spent set - for (const name of this.config.lockedNames) { - const infoRes = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ method: 'getnameinfo', params: [name] }), - }); - const infoJson = await infoRes.json(); - const owner = infoJson.result?.info?.owner; - if (owner && !spentOutpoints.has(`${owner.hash}:${owner.index}`)) { - return name; - } - } - - throw new Error( - 'All locked names are currently being spent in the mempool. Please try again later.', - ); - }; - /** * Connect to Shake Wallet */ @@ -187,11 +130,13 @@ export class ShakeWallet extends Wallet { }; /** - * Create and submit a locked UPDATE transaction via Shake Wallet. + * Create and submit a Rosen Bridge lock transaction via Shake Wallet. * - * Builds a locked UPDATE on the bridge-controlled name, embedding - * Rosen Bridge metadata as the resource hex, and sending HNS to the - * lock address. + * Uses the data-encoding approach (Bitcoin Runes method): + * 1. Encodes Rosen metadata as hex + * 2. Sends to wallet (wallet splits into 20-byte chunks) + * 3. Each chunk becomes a P2WPKH address (data in address hash) + * 4. Outputs ordered by value for extraction */ performTransfer = async (params: WalletTransferParams): Promise => { if ( @@ -208,37 +153,23 @@ export class ShakeWallet extends Wallet { } try { - // Pick a locked name whose owner output is not in the mempool - const lockedName = await this.findFreeName(); - - // Generate Rosen Bridge metadata encoded as hex for the UPDATE resource - const resourceHex = await this.currentNetwork.generateOpReturnData( + // Generate Rosen Bridge metadata encoded as hex + const rosenDataHex = await this.currentNetwork.generateOpReturnData( params.toChain, params.address, params.networkFee.toString(), params.bridgeFee.toString(), ); - const opts: { - name: string; - lockScriptHex: string; - resourceHex: string; - recipientAddress?: string; - recipientAmount?: number; - } = { - name: lockedName, - lockScriptHex: this.config.lockScriptHex, - resourceHex, - }; + console.log('Generated Rosen data:', rosenDataHex); + console.log('Data length:', rosenDataHex.length / 2, 'bytes'); - if (params.lockAddress && Number(params.amount) > 0) { - opts.recipientAddress = params.lockAddress; - opts.recipientAmount = Number(params.amount); - } - - console.log('Sending Rosen Bridge locked update:', opts); - - const result = await this.wallet.sendRosenBridgeLock(opts); + // Send transaction with data-encoded outputs (wallet will chunk the data) + const result = await this.wallet.sendRosenBridgeData({ + receiver: params.lockAddress, + amount: Number(params.amount), + data: rosenDataHex, + }); if (!result.hash) { throw new Error('Transaction failed - no hash returned'); From ec678bb96fdd8a084ab9fd184e70908e9b7f805d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aco=20S=CC=8Cmrkas?= Date: Sun, 8 Feb 2026 13:01:29 +0700 Subject: [PATCH 8/8] updated handshake block time to 600 --- apps/rosen-service/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/rosen-service/src/constants.ts b/apps/rosen-service/src/constants.ts index 12e193ff5..602de1239 100644 --- a/apps/rosen-service/src/constants.ts +++ b/apps/rosen-service/src/constants.ts @@ -21,6 +21,6 @@ export const BITCOIN_BLOCK_TIME = 600; export const ETHEREUM_BLOCK_TIME = 12; export const BINANCE_BLOCK_TIME = 3; export const DOGE_BLOCK_TIME = 60; -export const HANDSHAKE_BLOCK_TIME = 150; +export const HANDSHAKE_BLOCK_TIME = 600; export const BITCOIN_RUNES_CONFIG_KEY = 'bitcoinRunes';