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/early-lemons-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rosen-bridge/address-codec': minor
---

Add support for Firo chain
5 changes: 5 additions & 0 deletions .changeset/hip-coins-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rosen-bridge/rosen-extractor': minor
---

Add Firo chain support with FiroRosenExtractor and FiroRpcRosenExtractor
14 changes: 14 additions & 0 deletions packages/address-codec/lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const ERGO_CHAIN = 'ergo';
export const ETHEREUM_CHAIN = 'ethereum';
export const BINANCE_CHAIN = 'binance';
export const DOGE_CHAIN = 'doge';
export const FIRO_CHAIN = 'firo';

export const DOGE_NETWORK = {
// Doge network parameters
Expand All @@ -18,3 +19,16 @@ export const DOGE_NETWORK = {
scriptHash: 0x16,
wif: 0x9e,
};

export const FIRO_NETWORK = {
// Firo network parameters
messagePrefix: '\x19Firo Signed Message:\n',
bech32: 'firo',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4,
},
pubKeyHash: 0x52,
scriptHash: 0x07,
wif: 0xd2,
};
7 changes: 7 additions & 0 deletions packages/address-codec/lib/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
} from './const';
import { UnsupportedAddressError, UnsupportedChainError } from './types';
import * as ergoLib from 'ergo-lib-wasm-nodejs';
Expand Down Expand Up @@ -51,6 +53,11 @@ export const decodeAddress = (
Buffer.from(encodedAddress, 'hex'),
DOGE_NETWORK,
);
case FIRO_CHAIN:
return bitcoinLib.address.fromOutputScript(
Buffer.from(encodedAddress, 'hex'),
FIRO_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 @@ -7,6 +7,8 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
} from './const';
import { UnsupportedAddressError, UnsupportedChainError } from './types';
import * as ergoLib from 'ergo-lib-wasm-nodejs';
Expand Down Expand Up @@ -48,6 +50,11 @@ export const encodeAddress = (chain: string, address: string): string => {
.toOutputScript(address, DOGE_NETWORK)
.toString('hex');
break;
case FIRO_CHAIN:
encoded = bitcoinLib.address
.toOutputScript(address, FIRO_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 @@ -7,6 +7,8 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
FIRO_NETWORK,
} from './const';
import { UnsupportedAddressError, UnsupportedChainError } from './types';
import * as ergoLib from 'ergo-lib-wasm-nodejs';
Expand Down Expand Up @@ -53,6 +55,13 @@ export const validateAddress = (chain: string, address: string): boolean => {
throw new UnsupportedAddressError(chain, address);
}
return true;
case FIRO_CHAIN:
try {
bitcoinLib.address.toOutputScript(address, FIRO_NETWORK);
} catch {
throw new UnsupportedAddressError(chain, address);
}
return true;
default:
throw new UnsupportedChainError(chain);
}
Expand Down
15 changes: 15 additions & 0 deletions packages/address-codec/tests/decoder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';

describe('decodeAddress', () => {
Expand Down Expand Up @@ -128,4 +129,18 @@ describe('decodeAddress', () => {
);
expect(res).toEqual(testData.taprootBitcoinAddress);
});

/**
* @target `decodeAddress` should decode Firo address successfully
* @dependencies
* @scenario
* - run test
* - check returned value
* @expected
* - it should be address in hex format
*/
it('should decode Firo address successfully', () => {
const res = decodeAddress(FIRO_CHAIN, testData.encodedFiroAddress);
expect(res).toEqual(testData.firoAddress);
});
});
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 @@ -11,6 +11,7 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';

describe('encodeAddress', () => {
Expand Down Expand Up @@ -140,4 +141,18 @@ describe('encodeAddress', () => {
const res = encodeAddress(RUNES_CHAIN, testData.taprootBitcoinAddress);
expect(res).toEqual(testData.encodedTaprootBitcoinAddress);
});

/**
* @target `encodeAddress` should encode Firo address successfully
* @dependencies
* @scenario
* - run test
* - check returned value
* @expected
* - it should be output script of given address in hex
*/
it('should encode Firo address successfully', () => {
const res = encodeAddress(FIRO_CHAIN, testData.firoAddress);
expect(res).toEqual(testData.encodedFiroAddress);
});
});
5 changes: 5 additions & 0 deletions packages/address-codec/tests/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ export const dogeAddress = 'A69cznKpaYVWjzU3sNFZnGhbpSmUVFzvHB';
export const invalidDogeAddress = 'bc1qkgp89fjerymm5ltg0hygnumr0m2qa7n22gyw6h';
export const encodedDogeAddress =
'a914966ba9f4755996c3d51025d53044b415121bc10287';

export const firoAddress = 'a41owUrDFUy7taaQjntHqUadXm49d4z65e';
export const invalidFiroAddress = 'bc1qkgp89fjerymm5ltg0hygnumr0m2qa7n22gyw6h';
export const encodedFiroAddress =
'76a91424304f7ac11d0bcb0de1ebc6e2ea6aae174aee8988ac';
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 @@ -11,6 +11,7 @@ import {
ERGO_CHAIN,
ETHEREUM_CHAIN,
RUNES_CHAIN,
FIRO_CHAIN,
} from '../lib/const';

describe('validateAddress', () => {
Expand Down Expand Up @@ -231,4 +232,31 @@ describe('validateAddress', () => {
validateAddress(RUNES_CHAIN, testData.bitcoinAddress);
}).toThrow(UnsupportedAddressError);
});

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

/**
* @target `validateAddress` should throw error for wrong Firo address
* @dependencies
* @scenario
* - run test
* @expected
* - to throw error for wrong Firo address
*/
it('should throw error for wrong Firo address', () => {
expect(() => {
validateAddress(FIRO_CHAIN, testData.invalidFiroAddress);
}).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 FIRO_CHAIN = 'firo';
export const FIRO_NATIVE_TOKEN = 'firo';
export const SUPPORTED_CHAINS = [
ERGO_CHAIN,
CARDANO_CHAIN,
Expand All @@ -17,4 +19,5 @@ export const SUPPORTED_CHAINS = [
BINANCE_CHAIN,
DOGE_CHAIN,
BITCOIN_RUNES_CHAIN,
FIRO_CHAIN,
];
143 changes: 143 additions & 0 deletions packages/rosen-extractor/lib/getRosenData/firo/firoRosenExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { RosenData, TokenTransformation } from '../abstract/types';
import AbstractRosenDataExtractor from '../abstract/abstractRosenDataExtractor';
import { FIRO_CHAIN, FIRO_NATIVE_TOKEN } from '../const';
import { FiroTx, FiroTxOutput } from './types';
import { MinimalOnChainRosenData } from '../../types';
import { TokenMap } from '@rosen-bridge/tokens';
import { AbstractLogger } from '@rosen-bridge/abstract-logger';
import { parseOpReturn, addressToOutputScript } from './utils';
import JsonBigInt from '@rosen-bridge/json-bigint';

export class FiroRosenExtractor extends AbstractRosenDataExtractor<string> {
readonly chain = FIRO_CHAIN;
protected lockScriptPubKey: string;

constructor(
lockAddress: string,
tokens: TokenMap,
logger?: AbstractLogger,
storeRawData = true,
) {
super(lockAddress, tokens, logger, storeRawData);
this.lockScriptPubKey = addressToOutputScript(lockAddress);
}

/**
* extracts RosenData from given lock transaction in FiroTx format
* @param serializedTransaction stringified transaction in FiroTx format
*/
extractData = (serializedTransaction: string): RosenData | undefined => {
let transaction: FiroTx;
try {
transaction = JsonBigInt.parse(serializedTransaction);
} catch (e) {
throw new Error(
`Failed to parse transaction json to FiroTx 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 boxes`);
return undefined;
}

let validData = false; // an OP_RETURN box with valid data is found
let validLock = false; // a lock box is found with available asset transformation

// parse rosen data from OP_RETURN box
let opReturnData: MinimalOnChainRosenData | undefined;
let rawData: string = '';
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i];
if (output.scriptPubKey.slice(0, 2) !== '6a') continue; // not an OP_RETURN utxo

try {
opReturnData = parseOpReturn(output.scriptPubKey);
rawData = output.scriptPubKey;
validData = true;
break;
} catch (e) {
this.logger.debug(
`Failed to extract data from OP_RETURN box [${transaction.id}.${i}]: ${e}`,
);
}
}
if (!validData || !opReturnData) {
this.logger.debug(
baseError + `: No OP_RETURN box with valid data is found`,
);
return undefined;
}

// find target chain token id
let assetTransformation: TokenTransformation | undefined;
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i];
if (output.scriptPubKey !== this.lockScriptPubKey) continue; // utxo address is not lock address
assetTransformation = this.getAssetTransformation(
output,
opReturnData.toChain,
);
if (assetTransformation) {
validLock = true;
break;
}
}
if (!validLock || !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: opReturnData.toChain,
toAddress: opReturnData.toAddress,
bridgeFee: opReturnData.bridgeFee,
networkFee: opReturnData.networkFee,
fromAddress: fromAddress,
sourceChainTokenId: assetTransformation.from,
amount: assetTransformation.amount,
targetChainTokenId: assetTransformation.to,
sourceTxId: transaction.id,
rawData,
};
} catch (e) {
this.logger.debug(
`An error occurred while getting Firo 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: FiroTxOutput,
toChain: string,
): TokenTransformation | undefined => {
// try to build transformation using locked FIRO
const wrappedFiro = this.tokens.search(FIRO_CHAIN, {
tokenId: FIRO_NATIVE_TOKEN,
});
if (wrappedFiro.length > 0 && Object.hasOwn(wrappedFiro[0], toChain)) {
const value = box.value.toString();
return {
from: FIRO_NATIVE_TOKEN,
to: this.tokens.getID(wrappedFiro[0], toChain),
amount: value,
};
} else {
return undefined;
}
};
}
Loading