Skip to content

Commit 9ceb87e

Browse files
Merge pull request #9360 from BitGo/CECHO-1791-add-pearl-to-abstract-utxo
2 parents 0b2c766 + 64662cf commit 9ceb87e

28 files changed

Lines changed: 381 additions & 17 deletions

File tree

modules/abstract-utxo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@bitgo/utxo-descriptors": "^1.3.4",
6868
"@bitgo/utxo-lib": "^11.24.1",
6969
"@bitgo/utxo-ord": "^1.32.4",
70-
"@bitgo/wasm-utxo": "^4.21.1",
70+
"@bitgo/wasm-utxo": "^4.27.0",
7171
"@types/lodash": "^4.14.121",
7272
"@types/superagent": "4.1.15",
7373
"bignumber.js": "^9.0.2",

modules/abstract-utxo/src/impl/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * as btg from './btg';
66
export * as ltc from './ltc';
77
export * as dash from './dash';
88
export * as doge from './doge';
9+
export * as pearl from './pearl';
910
export * as zec from './zec';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './pearl';
2+
export * from './tpearl';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { BitGoBase } from '@bitgo/sdk-core';
2+
3+
import { AbstractUtxoCoin } from '../../abstractUtxoCoin';
4+
import { UtxoCoinName } from '../../names';
5+
6+
/**
7+
* Pearl (Duplex) is a taproot-only UTXO chain, a btcd fork using BIP-340 Schnorr
8+
* signatures and BIP-341 script-path spending with a NUMS internal key.
9+
*
10+
* Unlike the other utxo coins, Pearl has no `@bitgo/utxo-lib` network registration -
11+
* it is served entirely through `@bitgo/wasm-utxo`. No script type override is needed
12+
* here: `supportsAddressType` delegates to `fixedScriptWallet.supportsScriptType`,
13+
* which already reports only p2tr and p2trMusig2 for this coin.
14+
*/
15+
export class Pearl extends AbstractUtxoCoin {
16+
readonly name: UtxoCoinName = 'pearl';
17+
18+
static createInstance(bitgo: BitGoBase): Pearl {
19+
return new Pearl(bitgo);
20+
}
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { BitGoBase } from '@bitgo/sdk-core';
2+
3+
import { UtxoCoinName } from '../../names';
4+
5+
import { Pearl } from './pearl';
6+
7+
export class Tpearl extends Pearl {
8+
readonly name: UtxoCoinName = 'tpearl';
9+
10+
static createInstance(bitgo: BitGoBase): Tpearl {
11+
return new Tpearl(bitgo);
12+
}
13+
}

modules/abstract-utxo/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export * from './impl/btg';
2121
export * from './impl/ltc';
2222
export * from './impl/dash';
2323
export * from './impl/doge';
24+
export * from './impl/pearl';
2425
export * from './impl/zec';

modules/abstract-utxo/src/names.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'zec'] as const;
1+
export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'pearl', 'zec'] as const;
22
export const utxoCoinsTestnet = [
33
'tbtc',
44
'tbtc4',
@@ -11,6 +11,7 @@ export const utxoCoinsTestnet = [
1111
'tdash',
1212
'tdoge',
1313
'tltc',
14+
'tpearl',
1415
'tzec',
1516
] as const;
1617

@@ -62,6 +63,8 @@ function getBaseNameFromMainnet(coinName: UtxoCoinNameMainnet): string {
6263
return 'Dogecoin';
6364
case 'ltc':
6465
return 'Litecoin';
66+
case 'pearl':
67+
return 'Pearl';
6568
case 'zec':
6669
return 'ZCash';
6770
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import assert from 'node:assert/strict';
2+
3+
import { BitGoAPI } from '@bitgo/sdk-api';
4+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
5+
import * as utxolib from '@bitgo/utxo-lib';
6+
7+
import { AbstractUtxoCoin } from '../../../../../src/abstractUtxoCoin';
8+
import { Pearl, Tpearl } from '../../../../../src/impl/pearl';
9+
10+
describe('Pearl', function () {
11+
let bitgo: TestBitGoAPI;
12+
13+
/** `bitgo.coin()` is typed as BaseCoin; these are registered as AbstractUtxoCoin */
14+
function getCoin(coinName: 'pearl' | 'tpearl'): AbstractUtxoCoin {
15+
return bitgo.coin(coinName) as unknown as AbstractUtxoCoin;
16+
}
17+
18+
before(function () {
19+
bitgo = TestBitGo.decorate(BitGoAPI, {
20+
env: 'mock',
21+
});
22+
bitgo.initializeTestVars();
23+
bitgo.safeRegister('pearl', Pearl.createInstance);
24+
bitgo.safeRegister('tpearl', Tpearl.createInstance);
25+
});
26+
27+
it('should instantiate the coin', function () {
28+
assert.ok(bitgo.coin('pearl') instanceof Pearl);
29+
assert.ok(bitgo.coin('tpearl') instanceof Tpearl);
30+
});
31+
32+
it('should return the chain', function () {
33+
assert.strictEqual(bitgo.coin('pearl').getChain(), 'pearl');
34+
assert.strictEqual(bitgo.coin('tpearl').getChain(), 'tpearl');
35+
});
36+
37+
it('should return full name', function () {
38+
assert.strictEqual(bitgo.coin('pearl').getFullName(), 'Pearl');
39+
assert.strictEqual(bitgo.coin('tpearl').getFullName(), 'Testnet Pearl');
40+
});
41+
42+
it('should have tpearl as the testnet variant of pearl', function () {
43+
assert.ok(bitgo.coin('tpearl') instanceof Pearl);
44+
});
45+
46+
/**
47+
* Pearl is taproot-only. This is not enforced by an override here - it comes from
48+
* `supportsAddressType` delegating to wasm-utxo, which reports only the taproot
49+
* script types for this coin. Pinned so a regression in either layer is caught.
50+
*/
51+
it('should support only the taproot script types', function () {
52+
for (const coinName of ['pearl', 'tpearl'] as const) {
53+
const coin = getCoin(coinName);
54+
assert.strictEqual(coin.supportsAddressType('p2tr'), true);
55+
assert.strictEqual(coin.supportsAddressType('p2trMusig2'), true);
56+
assert.strictEqual(coin.supportsAddressType('p2sh'), false);
57+
assert.strictEqual(coin.supportsAddressType('p2shP2wsh'), false);
58+
assert.strictEqual(coin.supportsAddressType('p2wsh'), false);
59+
}
60+
});
61+
62+
it('should support only the taproot address chains', function () {
63+
for (const coinName of ['pearl', 'tpearl'] as const) {
64+
const coin = getCoin(coinName);
65+
// 30/31 = p2tr, 40/41 = p2trMusig2
66+
for (const chain of [30, 31, 40, 41]) {
67+
assert.strictEqual(coin.supportsAddressChain(chain), true, `chain ${chain} should be supported`);
68+
}
69+
// 0/1 = p2sh, 10/11 = p2shP2wsh, 20/21 = p2wsh
70+
for (const chain of [0, 1, 10, 11, 20, 21]) {
71+
assert.strictEqual(coin.supportsAddressChain(chain), false, `chain ${chain} should not be supported`);
72+
}
73+
}
74+
});
75+
76+
/**
77+
* Pearl is served through @bitgo/wasm-utxo and is deliberately absent from
78+
* @bitgo/utxo-lib. No other checked-in coin has this profile, so it cannot be
79+
* inferred from an existing coin - assert it explicitly.
80+
*/
81+
it('should have no utxo-lib network registration', function () {
82+
assert.ok(!('pearl' in utxolib.networks));
83+
assert.ok(!('tpearl' in utxolib.networks));
84+
});
85+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.idea
3+
public
4+
dist
5+

modules/sdk-coin-pearl/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.idea/
3+
dist/

0 commit comments

Comments
 (0)