diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 53af4b62f4..287b52fff5 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -77,6 +77,7 @@ }, "devDependencies": { "@bitgo/sdk-test": "^9.1.76", + "@bitgo/statics": "^59.15.0", "@bitgo/utxo-lib": "^11.24.4", "mocha": "^10.2.0" }, diff --git a/modules/abstract-utxo/src/impl/btc/index.ts b/modules/abstract-utxo/src/impl/btc/index.ts index 8e439d2dd0..57bbffc872 100644 --- a/modules/abstract-utxo/src/impl/btc/index.ts +++ b/modules/abstract-utxo/src/impl/btc/index.ts @@ -3,4 +3,5 @@ export * from './tbtc'; export * from './tbtc4'; export * from './tbtcsig'; export * from './tbtcbgsig'; +export * from './tbtcstx'; export * from './inscriptionBuilder'; diff --git a/modules/abstract-utxo/src/impl/btc/tbtcstx.ts b/modules/abstract-utxo/src/impl/btc/tbtcstx.ts new file mode 100644 index 0000000000..a8a92bcbef --- /dev/null +++ b/modules/abstract-utxo/src/impl/btc/tbtcstx.ts @@ -0,0 +1,21 @@ +import { BitGoBase } from '@bitgo/sdk-core'; + +import { UtxoCoinName, WasmUtxoCoinName } from '../../names'; + +import { Tbtc } from './tbtc'; + +export class Tbtcstx extends Tbtc { + readonly name: UtxoCoinName = 'tbtcstx'; + + constructor(bitgo: BitGoBase) { + super(bitgo); + } + + static createInstance(bitgo: BitGoBase): Tbtcstx { + return new Tbtcstx(bitgo); + } + + override get wasmName(): WasmUtxoCoinName { + return 'tbtcreg'; + } +} diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index 8dab6448e9..da48699493 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -8,6 +8,7 @@ export const utxoCoinsTestnet = [ 'tbtc4', 'tbtcsig', 'tbtcbgsig', + 'tbtcstx', 'tbch', 'tbcha', 'tbsv', @@ -20,7 +21,7 @@ export const utxoCoinsTestnet = [ ] as const; export type UtxoCoinNameMainnet = (typeof utxoCoinsMainnet)[number]; -export type UtxoCoinNameTestnet = `t${UtxoCoinNameMainnet}` | 'tbtcsig' | 'tbtc4' | 'tbtcbgsig'; +export type UtxoCoinNameTestnet = `t${UtxoCoinNameMainnet}` | 'tbtcsig' | 'tbtc4' | 'tbtcbgsig' | 'tbtcstx'; export type UtxoCoinName = UtxoCoinNameMainnet | UtxoCoinNameTestnet; export function toWasmUtxoCoinName(coinName: UtxoCoinName | WasmUtxoCoinName): WasmUtxoCoinName { @@ -50,6 +51,7 @@ export function getMainnetCoinName(coinName: UtxoCoinName): UtxoCoinNameMainnet case 'tbtc4': case 'tbtcsig': case 'tbtcbgsig': + case 'tbtcstx': return 'btc'; default: return coinName.slice(1) as UtxoCoinNameMainnet; @@ -93,6 +95,9 @@ export function getFullNameFromCoinName(coinName: UtxoCoinName): string { case 'tbtcbgsig': prefix = 'BitGo Signet '; break; + case 'tbtcstx': + prefix = 'Stacks '; + break; default: prefix = isUtxoCoinNameTestnet(coinName) ? 'Testnet ' : ''; } diff --git a/modules/abstract-utxo/test/unit/tbtcstx.ts b/modules/abstract-utxo/test/unit/tbtcstx.ts new file mode 100644 index 0000000000..c400fd7c81 --- /dev/null +++ b/modules/abstract-utxo/test/unit/tbtcstx.ts @@ -0,0 +1,28 @@ +import assert from 'assert'; + +import { Networks } from '@bitgo/statics'; +import * as utxolib from '@bitgo/utxo-lib'; + +import { Tbtcstx } from '../../src'; + +import { defaultBitGo, getNetworkForCoinName } from './util'; + +describe('tbtcstx', function () { + const coin = Tbtcstx.createInstance(defaultBitGo); + + it('uses the public coin name with the testnet utxolib network', function () { + assert.strictEqual(coin.getChain(), 'tbtcstx'); + assert.strictEqual(coin.wasmName, 'tbtcreg'); + assert.strictEqual(Networks.test.bitcoinStx.utxolibName, 'testnet'); + assert.strictEqual(getNetworkForCoinName(coin.name), utxolib.networks.testnet); + }); + + it('encodes and decodes private-1 addresses through the tbtcreg codec', function () { + const script = Buffer.from(`0014${'11'.repeat(20)}`, 'hex'); + const address = coin.addressCodec.encode(script); + + assert.match(address, /^bcrt1q/); + assert.deepStrictEqual(Buffer.from(coin.addressCodec.decode(address)), script); + assert.strictEqual(coin.isValidAddress(address), true); + }); +}); diff --git a/modules/abstract-utxo/test/unit/util/utxoCoins.ts b/modules/abstract-utxo/test/unit/util/utxoCoins.ts index 67f771d929..92d85b71ea 100644 --- a/modules/abstract-utxo/test/unit/util/utxoCoins.ts +++ b/modules/abstract-utxo/test/unit/util/utxoCoins.ts @@ -1,6 +1,7 @@ import * as utxolib from '@bitgo/utxo-lib'; import { BitGoAPI } from '@bitgo/sdk-api'; import { TestBitGo } from '@bitgo/sdk-test'; +import { coins, UtxoCoin } from '@bitgo/statics'; import { AbstractUtxoCoin, @@ -30,50 +31,36 @@ import type { UtxoCoinName } from '../../../src/names'; export const defaultBitGo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); -/** Map coin names to utxolib network objects for test infrastructure */ -const coinNameToNetwork: Record = { - btc: utxolib.networks.bitcoin, - tbtc: utxolib.networks.testnet, - tbtcsig: utxolib.networks.bitcoinPublicSignet, - tbtc4: utxolib.networks.bitcoinTestnet4, - tbtcbgsig: utxolib.networks.bitcoinBitGoSignet, - bch: utxolib.networks.bitcoincash, - tbch: utxolib.networks.bitcoincashTestnet, - bcha: utxolib.networks.ecash, - tbcha: utxolib.networks.ecashTest, - bsv: utxolib.networks.bitcoinsv, - tbsv: utxolib.networks.bitcoinsvTestnet, - btg: utxolib.networks.bitcoingold, - dash: utxolib.networks.dash, - tdash: utxolib.networks.dashTest, - doge: utxolib.networks.dogecoin, - tdoge: utxolib.networks.dogecoinTest, - ltc: utxolib.networks.litecoin, - tltc: utxolib.networks.litecoinTest, - zec: utxolib.networks.zcash, - tzec: utxolib.networks.zcashTest, -}; +function isUtxolibNetworkName(name: string): name is utxolib.NetworkName { + return Object.prototype.hasOwnProperty.call(utxolib.networks, name); +} /** - * Get utxolib Network for a coin name. For test infrastructure only. + * Get the utxolib network for a coin name from the canonical statics definition. */ export function getNetworkForCoinName(coinName: string): utxolib.Network { - const network = coinNameToNetwork[coinName]; - if (!network) { + const coin = coins.getOrUndefined(coinName); + if (!(coin instanceof UtxoCoin)) { throw new Error(`Unknown coin name: ${coinName}`); } - return network; + + const networkName = coin.network.utxolibName; + if (!isUtxolibNetworkName(networkName)) { + throw new Error(`Unknown utxolib network ${networkName} for coin ${coinName}`); + } + + return utxolib.networks[networkName]; } /** - * Get coin name for a utxolib Network. For test infrastructure only. + * Get the canonical test coin for a utxolib network. */ export function getCoinNameForNetwork(network: utxolib.Network): UtxoCoinName { - for (const [name, n] of Object.entries(coinNameToNetwork)) { - if (n === network) { - return name as UtxoCoinName; - } + const coin = utxoCoins.find((coin) => getNetworkForCoinName(coin.name) === network); + if (coin) { + return coin.name; } + throw new Error(`Unknown network: ${utxolib.getNetworkName(network)}`); } diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index 0594157217..65de5d08fa 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -163,6 +163,7 @@ import { Tbtcsig, Tbtc4, Tbtcbgsig, + Tbtcstx, Tcanton, Tcelo, Tcoredao, @@ -338,6 +339,7 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin coinFactory.register('tbtcsig', Tbtcsig.createInstance); coinFactory.register('tbtc4', Tbtc4.createInstance); coinFactory.register('tbtcbgsig', Tbtcbgsig.createInstance); + coinFactory.register('tbtcstx', Tbtcstx.createInstance); coinFactory.register('tcanton', Tcanton.createInstance); coinFactory.register('tcelo', Tcelo.createInstance); coinFactory.register('tcoredao', Tcoredao.createInstance); diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index d2a9e6e635..786f858451 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -16,7 +16,7 @@ import { Bera, Tbera, BeraToken } from '@bitgo/sdk-coin-bera'; import { Bld, Tbld } from '@bitgo/sdk-coin-bld'; import { Bsc, BscToken, Tbsc } from '@bitgo/sdk-coin-bsc'; import { Bsv, Tbsv } from '@bitgo/sdk-coin-bsv'; -import { Btc, Tbtc, Tbtcsig, Tbtc4, Tbtcbgsig } from '@bitgo/sdk-coin-btc'; +import { Btc, Tbtc, Tbtcsig, Tbtc4, Tbtcbgsig, Tbtcstx } from '@bitgo/sdk-coin-btc'; import { Btg } from '@bitgo/sdk-coin-btg'; import { Canton, Tcanton } from '@bitgo/sdk-coin-canton'; import { Celo, CeloToken, Tcelo } from '@bitgo/sdk-coin-celo'; @@ -95,7 +95,7 @@ export { Bch, Tbch }; export { Bera, Tbera, BeraToken }; export { Bsc, BscToken, Tbsc }; export { Bsv, Tbsv }; -export { Btc, Tbtc, Tbtcsig, Tbtc4, Tbtcbgsig }; +export { Btc, Tbtc, Tbtcsig, Tbtc4, Tbtcbgsig, Tbtcstx }; export { Btg }; export { Canton, Tcanton }; export { Celo, CeloToken, Tcelo }; diff --git a/modules/sdk-coin-btc/src/index.ts b/modules/sdk-coin-btc/src/index.ts index 6d34bda541..d9d896c193 100644 --- a/modules/sdk-coin-btc/src/index.ts +++ b/modules/sdk-coin-btc/src/index.ts @@ -1,11 +1,12 @@ import { BitGoBase } from '@bitgo/sdk-core'; -import { Btc, Tbtc, Tbtc4, Tbtcsig, Tbtcbgsig } from '@bitgo/abstract-utxo'; +import { Btc, Tbtc, Tbtc4, Tbtcsig, Tbtcbgsig, Tbtcstx } from '@bitgo/abstract-utxo'; export { Btc, VerifyRecoveryTransactionOptions, InscriptionBuilder } from '@bitgo/abstract-utxo'; export { Tbtc } from '@bitgo/abstract-utxo'; export { Tbtcsig } from '@bitgo/abstract-utxo'; export { Tbtc4 } from '@bitgo/abstract-utxo'; export { Tbtcbgsig } from '@bitgo/abstract-utxo'; +export { Tbtcstx } from '@bitgo/abstract-utxo'; export const register = (sdk: BitGoBase): void => { sdk.register('btc', Btc.createInstance); @@ -13,4 +14,5 @@ export const register = (sdk: BitGoBase): void => { sdk.register('tbtcsig', Tbtcsig.createInstance); sdk.register('tbtcbgsig', Tbtcbgsig.createInstance); sdk.register('tbtc4', Tbtc4.createInstance); + sdk.register('tbtcstx', Tbtcstx.createInstance); }; diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index b067137bf5..36bc66cdf9 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -541,6 +541,14 @@ class BitcoinBitGoSignet extends Testnet implements UtxoNetwork { explorerUrl = 'https://mempool.space/notanetwork/tx/'; } +class BitcoinStx extends Testnet implements UtxoNetwork { + name = 'BitcoinStx'; + family = CoinFamily.BTC; + // Stacks regtest uses testnet transaction semantics in utxo-lib. + utxolibName = 'testnet'; + explorerUrl = undefined; +} + class BitcoinCash extends Mainnet implements UtxoNetwork { name = 'BitcoinCash'; family = CoinFamily.BCH; @@ -3070,6 +3078,7 @@ export const Networks = { bitcoinPublicSignet: Object.freeze(new BitcoinPublicSignet()), bitcoinTestnet4: Object.freeze(new BitcoinTestnet4()), bitcoinBitGoSignet: Object.freeze(new BitcoinBitGoSignet()), + bitcoinStx: Object.freeze(new BitcoinStx()), bitcoinCash: Object.freeze(new BitcoinCashTestnet()), bitcoinGold: Object.freeze(new BitcoinGoldTestnet()), bitcoinSV: Object.freeze(new BitcoinSVTestnet()), diff --git a/modules/statics/src/utxo.ts b/modules/statics/src/utxo.ts index c962501fcd..c39c575ee7 100644 --- a/modules/statics/src/utxo.ts +++ b/modules/statics/src/utxo.ts @@ -261,6 +261,16 @@ export const utxoCoins: Readonly[] = [ BaseUnit.BTC, BTC_FEATURES ), + utxo( + 'f3e1c2d4-5b6a-47c8-9012-3456789abcde', + 'tbtcstx', + // Stacks Bitcoin regtest, represented by Bitcoin testnet transaction semantics in utxo-lib. + 'Stacks Bitcoin (Regtest)', + Networks.test.bitcoinStx, + UnderlyingAsset.BTC, + BaseUnit.BTC, + BTC_FEATURES + ), utxo( '8feb110d-0d68-44ce-ae97-b8c30ec870a9', 'btg', diff --git a/modules/statics/test/unit/fixtures/expectedColdFeatures.ts b/modules/statics/test/unit/fixtures/expectedColdFeatures.ts index 611cc95bf5..8adcbe7deb 100644 --- a/modules/statics/test/unit/fixtures/expectedColdFeatures.ts +++ b/modules/statics/test/unit/fixtures/expectedColdFeatures.ts @@ -49,6 +49,7 @@ export const expectedColdFeatures = { 'tbtcsig', 'tbtc4', 'tbtcbgsig', + 'tbtcstx', 'tcelo', 'tcspr', 'tdash',