Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-lizards-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rosen-bridge/address-codec': minor
---

Add support for Handshake chain
5 changes: 5 additions & 0 deletions .changeset/petite-seals-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rosen-bridge/rosen-extractor': minor
---

Add Handshake chain rosen-extractor
14 changes: 14 additions & 0 deletions packages/address-codec/lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const ETHEREUM_CHAIN = 'ethereum';
export const BINANCE_CHAIN = 'binance';
export const DOGE_CHAIN = 'doge';
export const FIRO_CHAIN = 'firo';
export const HANDSHAKE_CHAIN = 'handshake';

export const DOGE_NETWORK = {
// Doge network parameters
Expand All @@ -32,3 +33,16 @@ export const FIRO_NETWORK = {
scriptHash: 0x07,
wif: 0xd2,
};

export const HANDSHAKE_NETWORK = {
// Handshake network parameters
messagePrefix: '\x18Handshake Signed Message:\n',
bech32: 'hs',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4,
},
pubKeyHash: -1,
scriptHash: -1,
wif: -1,
};
7 changes: 7 additions & 0 deletions packages/address-codec/lib/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
DOGE_NETWORK,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
HANDSHAKE_NETWORK,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
Expand Down Expand Up @@ -58,6 +60,11 @@ export const decodeAddress = (
Buffer.from(encodedAddress, 'hex'),
FIRO_NETWORK,
);
case HANDSHAKE_CHAIN:
return bitcoinLib.address.fromOutputScript(
Buffer.from(encodedAddress, 'hex'),
HANDSHAKE_NETWORK,
);
default:
throw new UnsupportedChainError(chain);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/address-codec/lib/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
DOGE_NETWORK,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
HANDSHAKE_NETWORK,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
Expand Down Expand Up @@ -55,6 +57,11 @@ export const encodeAddress = (chain: string, address: string): string => {
.toOutputScript(address, FIRO_NETWORK)
.toString('hex');
break;
case HANDSHAKE_CHAIN:
encoded = bitcoinLib.address
.toOutputScript(address, HANDSHAKE_NETWORK)
.toString('hex');
break;
default:
throw new UnsupportedChainError(chain);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/address-codec/lib/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
DOGE_NETWORK,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
HANDSHAKE_NETWORK,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
Expand Down Expand Up @@ -62,6 +64,13 @@ export const validateAddress = (chain: string, address: string): boolean => {
throw new UnsupportedAddressError(chain, address);
}
return true;
case HANDSHAKE_CHAIN:
try {
bitcoinLib.address.toOutputScript(address, HANDSHAKE_NETWORK);
} catch {
throw new UnsupportedAddressError(chain, address);
}
return true;
default:
throw new UnsupportedChainError(chain);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/address-codec/tests/decoder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DOGE_CHAIN,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';
Expand Down Expand Up @@ -143,4 +144,21 @@ describe('decodeAddress', () => {
const res = decodeAddress(FIRO_CHAIN, testData.encodedFiroAddress);
expect(res).toEqual(testData.firoAddress);
});

/**
* @target `decodeAddress` should decode Handshake address successfully
* @dependencies
* @scenario
* - run test
* - check returned value
* @expected
* - it should be address in bech32 format with hs prefix
*/
it('should decode Handshake address successfully', () => {
const res = decodeAddress(
HANDSHAKE_CHAIN,
testData.encodedHandshakeAddress,
);
expect(res).toEqual(testData.handshakeAddress);
});
});
15 changes: 15 additions & 0 deletions packages/address-codec/tests/encoder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DOGE_CHAIN,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';
Expand Down Expand Up @@ -155,4 +156,18 @@ describe('encodeAddress', () => {
const res = encodeAddress(FIRO_CHAIN, testData.firoAddress);
expect(res).toEqual(testData.encodedFiroAddress);
});

/**
* @target `encodeAddress` should encode Handshake address successfully
* @dependencies
* @scenario
* - run test
* - check returned value
* @expected
* - it should be output script of given address in hex
*/
it('should encode Handshake address successfully', () => {
const res = encodeAddress(HANDSHAKE_CHAIN, testData.handshakeAddress);
expect(res).toEqual(testData.encodedHandshakeAddress);
});
});
6 changes: 6 additions & 0 deletions packages/address-codec/tests/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ export const firoAddress = 'a41owUrDFUy7taaQjntHqUadXm49d4z65e';
export const invalidFiroAddress = 'bc1qkgp89fjerymm5ltg0hygnumr0m2qa7n22gyw6h';
export const encodedFiroAddress =
'76a91424304f7ac11d0bcb0de1ebc6e2ea6aae174aee8988ac';

export const handshakeAddress = 'hs1qvq4029zf8zvms3pw6t9znju5wqte3hpykr8q3s';
export const invalidHandshakeAddress =
'bc1qkgp89fjerymm5ltg0hygnumr0m2qa7n22gyw6h';
export const encodedHandshakeAddress =
'0014602af514493899b8442ed2ca29cb94701798dc24';
28 changes: 28 additions & 0 deletions packages/address-codec/tests/validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DOGE_CHAIN,
ERGO_CHAIN,
ETHEREUM_CHAIN,
HANDSHAKE_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';
Expand Down Expand Up @@ -259,4 +260,31 @@ describe('validateAddress', () => {
validateAddress(FIRO_CHAIN, testData.invalidFiroAddress);
}).toThrow(UnsupportedAddressError);
});

/**
* @target `validateAddress` should validate Handshake address successfully
* @dependencies
* @scenario
* - run test
* @expected
* - to validate correct Handshake address
*/
it('should validate Handshake address successfully', () => {
const res = validateAddress(HANDSHAKE_CHAIN, testData.handshakeAddress);
expect(res).toEqual(true);
});

/**
* @target `validateAddress` should throw error for wrong Handshake address
* @dependencies
* @scenario
* - run test
* @expected
* - to throw error for wrong Handshake address
*/
it('should throw error for wrong Handshake address', () => {
expect(() => {
validateAddress(HANDSHAKE_CHAIN, testData.invalidHandshakeAddress);
}).toThrow(UnsupportedAddressError);
});
});
3 changes: 3 additions & 0 deletions packages/rosen-extractor/lib/getRosenData/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const ERGO_CHAIN = 'ergo';
export const DOGE_CHAIN = 'doge';
export const DOGE_NATIVE_TOKEN = 'doge';
export const BITCOIN_RUNES_CHAIN = 'bitcoin-runes';
export const HANDSHAKE_CHAIN = 'handshake';
export const HANDSHAKE_NATIVE_TOKEN = 'hns';
export const FIRO_CHAIN = 'firo';
export const FIRO_NATIVE_TOKEN = 'firo';
export const SUPPORTED_CHAINS = [
Expand All @@ -19,5 +21,6 @@ export const SUPPORTED_CHAINS = [
BINANCE_CHAIN,
DOGE_CHAIN,
BITCOIN_RUNES_CHAIN,
HANDSHAKE_CHAIN,
FIRO_CHAIN,
];
13 changes: 13 additions & 0 deletions packages/rosen-extractor/lib/getRosenData/handshake/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const HANDSHAKE_NETWORK = {
messagePrefix: '\x18Handshake Signed Message:\n',
bech32: 'hs',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4,
},
pubKeyHash: -1,
scriptHash: -1,
wif: -1,
};

export const MIN_UTXO_VALUE = 1000;
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { RosenData, TokenTransformation } from '../abstract/types';
import AbstractRosenDataExtractor from '../abstract/abstractRosenDataExtractor';
import { HANDSHAKE_CHAIN, HANDSHAKE_NATIVE_TOKEN } from '../const';
import { HandshakeTx, HandshakeTxOutput, HandshakeRosenData } from './types';
import { TokenMap } from '@rosen-bridge/tokens';
import { AbstractLogger } from '@rosen-bridge/abstract-logger';
import { addressToHash, extractDataFromOutputs } from './utils';
import { parseRosenData } from '../../utils';
import JsonBigInt from '@rosen-bridge/json-bigint';

export class HandshakeRosenExtractor extends AbstractRosenDataExtractor<string> {
readonly chain = HANDSHAKE_CHAIN;
protected lockAddressHash: string;

constructor(lockAddress: string, tokens: TokenMap, logger?: AbstractLogger) {
super(lockAddress, tokens, logger);
this.lockAddressHash = addressToHash(lockAddress);
}

/**
* extracts RosenData from given lock transaction in HandshakeTx format
* @param serializedTransaction stringified transaction in HandshakeTx format
*/
extractData = (serializedTransaction: string): RosenData | undefined => {
let transaction: HandshakeTx;
try {
transaction = JsonBigInt.parse(serializedTransaction);
} catch (e) {
throw new Error(
`Failed to parse transaction json to HandshakeTx format while extracting rosen data: ${e}`,
);
}

const baseError = `No rosen data found for tx [${transaction.id}]`;
try {
const outputs = transaction.outputs;
if (outputs.length < 2) {
this.logger.debug(baseError + `: Insufficient number of outputs`);
return undefined;
}

// Find lock output and position
const lockOutputIndex = outputs.findIndex(
(output) => output.address?.hash === this.lockAddressHash,
);

if (lockOutputIndex === -1) {
this.logger.debug(baseError + `: Lock output not found`);
return undefined;
}

const lockOutput = outputs[lockOutputIndex];

// Extract data from outputs using utility function
const reconstructedData = extractDataFromOutputs(
outputs,
lockOutputIndex,
);

if (!reconstructedData) {
this.logger.debug(baseError + `: No data chunks found`);
return undefined;
}

// Parse the reconstructed data
let rosenData: HandshakeRosenData | undefined;
try {
rosenData = parseRosenData(reconstructedData);
this.logger.debug(
`Successfully extracted Rosen data for [${rosenData.toChain}]`,
);
} catch (e) {
this.logger.debug(
baseError + `: Failed to parse reconstructed data: ${e}`,
);
return undefined;
}

// Find asset transformation using the lock output
const assetTransformation = this.getAssetTransformation(
lockOutput,
rosenData.toChain,
);

if (!assetTransformation) {
this.logger.debug(
baseError + `: Failed to find rosen asset transformation`,
);
return undefined;
}

const fromAddress = `box:${transaction.inputs[0].txId}.${transaction.inputs[0].index}`;
return {
toChain: rosenData.toChain,
toAddress: rosenData.toAddress,
bridgeFee: rosenData.bridgeFee,
networkFee: rosenData.networkFee,
fromAddress: fromAddress,
sourceChainTokenId: assetTransformation.from,
amount: assetTransformation.amount,
targetChainTokenId: assetTransformation.to,
sourceTxId: transaction.id,
rawData: outputs
.map((output) => `${output.address?.hash}:${output.value}`)
.join(','),
};
} catch (e) {
this.logger.debug(
`An error occurred while getting Handshake rosen data: ${e}`,
);
if (e instanceof Error && e.stack) {
this.logger.debug(e.stack);
}
}
return undefined;
};

/**
* extracts and builds token transformation from UTXO and tokenMap
* @param box transaction output
* @param toChain event target chain
*/
getAssetTransformation = (
box: HandshakeTxOutput,
toChain: string,
): TokenTransformation | undefined => {
const wrappedHns = this.tokens.search(HANDSHAKE_CHAIN, {
tokenId: HANDSHAKE_NATIVE_TOKEN,
});

if (wrappedHns.length > 0 && Object.hasOwn(wrappedHns[0], toChain)) {
return {
from: HANDSHAKE_NATIVE_TOKEN,
to: this.tokens.getID(wrappedHns[0], toChain),
amount: box.value.toString(),
};
}
return undefined;
};
}
Loading