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
45 changes: 39 additions & 6 deletions modules/abstract-lightning/src/abstractLightningCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AuditDecryptedKeyParams,
BaseCoin,
BitGoBase,
InvalidAddressError,
KeyPair,
ParsedTransaction,
ParseTransactionOptions,
Expand All @@ -15,16 +16,20 @@ import * as utxolib from '@bitgo/utxo-lib';
import { randomBytes } from 'crypto';
import { bip32 } from '@bitgo/utxo-lib';

export interface LightningVerifyAddressOptions extends VerifyAddressOptions {
walletId: string;
}

export abstract class AbstractLightningCoin extends BaseCoin {
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
private readonly _network: utxolib.Network;
protected readonly network: utxolib.Network;
protected constructor(bitgo: BitGoBase, network: utxolib.Network, staticsCoin?: Readonly<StaticsBaseCoin>) {
super(bitgo);
if (!staticsCoin) {
throw new Error('missing required constructor parameter staticsCoin');
}
this._staticsCoin = staticsCoin;
this._network = network;
this.network = network;
}

getBaseFactor(): number {
Expand All @@ -35,8 +40,24 @@ export abstract class AbstractLightningCoin extends BaseCoin {
throw new Error('Method not implemented.');
}

isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
throw new Error('Method not implemented.');
async isWalletAddress(params: LightningVerifyAddressOptions): Promise<boolean> {
const { address, walletId } = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

// Node pubkeys are valid addresses but not wallet addresses
if (/^(02|03)[0-9a-fA-F]{64}$/.test(address)) {
return false;
}

try {
await this.bitgo.get(this.url(`/wallet/${walletId}/address/${encodeURIComponent(address)}`)).result();
return true;
} catch (e) {
return false;
}
}

parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
Expand All @@ -58,11 +79,23 @@ export abstract class AbstractLightningCoin extends BaseCoin {
}

isValidPub(pub: string): boolean {
throw new Error('Method not implemented.');
try {
return bip32.fromBase58(pub).isNeutered();
} catch (e) {
return false;
}
}

isValidAddress(address: string): boolean {
throw new Error('Method not implemented.');
if (/^(02|03)[0-9a-fA-F]{64}$/.test(address)) {
return true;
}
try {
const script = utxolib.address.toOutputScript(address, this.network);
return address === utxolib.address.fromOutputScript(script, this.network);
} catch (e) {
return false;
}
}

signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
Expand Down
16 changes: 15 additions & 1 deletion modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ExtraPrebuildParamsOptions,
HalfSignedUtxoTransaction,
IBaseCoin,
isBolt11Invoice,
InvalidAddressDerivationPropertyError,
InvalidAddressError,
IRequestTracer,
Expand Down Expand Up @@ -489,11 +490,24 @@ export abstract class AbstractUtxoCoin
* @param address
* @param param
*/
isValidAddress(address: string, param?: { anyFormat: boolean } | /* legacy parameter */ boolean): boolean {
isValidAddress(
address: string,
param?: { anyFormat?: boolean; allowLightning?: boolean } | /* legacy parameter */ boolean
): boolean {
if (typeof param === 'boolean' && param) {
throw new Error('deprecated');
}

const allowLightning = (param as { allowLightning?: boolean } | undefined)?.allowLightning ?? false;
if (allowLightning) {
if (/^(02|03)[0-9a-fA-F]{64}$/.test(address)) {
return true;
}
if (isBolt11Invoice(address)) {
return true;
}
}

// By default, allow all address formats.
// At the time of writing, the only additional address format is bch cashaddr.
const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true;
Expand Down
70 changes: 69 additions & 1 deletion modules/abstract-utxo/test/unit/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as utxolib from '@bitgo/utxo-lib';

import { getMainnetCoinName, utxoCoinsMainnet, utxoCoinsTestnet } from '../../src/names';

import { getNetworkForCoinName, getUtxoCoinForNetwork, utxoCoins } from './util';
import { getNetworkForCoinName, getUtxoCoinForNetwork, getUtxoCoin, utxoCoins } from './util';

describe('utxoCoins', function () {
it('has expected chain/network values for items', function () {
Expand Down Expand Up @@ -76,6 +76,74 @@ describe('utxoCoins', function () {
);
});

describe('isValidAddress with allowLightning', function () {
const btc = getUtxoCoin('btc');
const tbtc = getUtxoCoin('tbtc');
const bch = getUtxoCoin('bch');

it('should reject node pubkeys and invoices without allowLightning', function () {
assert.strictEqual(
btc.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
false
);
assert.strictEqual(btc.isValidAddress('lnbc1500n1pj0ggavpp5example'), false);
});

it('should accept node pubkeys with allowLightning', function () {
assert.strictEqual(
btc.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619', {
allowLightning: true,
}),
true
);
assert.strictEqual(
tbtc.isValidAddress('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad', {
allowLightning: true,
}),
true
);
});

it('should reject invalid node pubkeys even with allowLightning', function () {
// wrong prefix
assert.strictEqual(
btc.isValidAddress('04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619', {
allowLightning: true,
}),
false
);
// too short
assert.strictEqual(
btc.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368', {
allowLightning: true,
}),
false
);
});

it('should accept bolt11 invoices with allowLightning', function () {
assert.strictEqual(btc.isValidAddress('lnbc1500n1pj0ggavpp5example', { allowLightning: true }), true);
assert.strictEqual(tbtc.isValidAddress('lntb1500n1pj0ggavpp5example', { allowLightning: true }), true);
});

it('should reject non-bolt11 strings with allowLightning', function () {
assert.strictEqual(btc.isValidAddress('lnxyz1500n1pj0ggavpp5example', { allowLightning: true }), false);
assert.strictEqual(btc.isValidAddress('not-an-address', { allowLightning: true }), false);
});

it('should still accept regular bitcoin addresses with allowLightning', function () {
assert.strictEqual(btc.isValidAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', { allowLightning: true }), true);
});

it('should not accept lightning addresses for non-btc coins without allowLightning', function () {
assert.strictEqual(
bch.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
false
);
assert.strictEqual(bch.isValidAddress('lnbc1500n1pj0ggavpp5example'), false);
});
});

it('getMainnetCoinName returns correct mainnet coin name', function () {
// Mainnet coins return themselves
for (const coin of utxoCoinsMainnet) {
Expand Down
27 changes: 27 additions & 0 deletions modules/bitgo/test/v2/unit/coins/ofcToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,33 @@ describe('OFC:', function () {
tbtc.isValidAddress('bg-5b2b80eafbdf94d5030bb23f9b56ad64nnn').should.be.false;
});

it('should accept lightning node pubkeys as valid addresses for ofctbtc', function () {
const tbtc = bitgo.coin('ofctbtc');
// valid compressed public keys (node pubkeys)
tbtc.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619').should.be.true;
tbtc.isValidAddress('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad').should.be.true;
// invalid: wrong prefix
tbtc.isValidAddress('04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619').should.be.false;
// invalid: too short
tbtc.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368').should.be.false;
});

it('should accept lightning invoices as valid addresses for ofctbtc', function () {
const tbtc = bitgo.coin('ofctbtc');
// testnet bolt11 invoice
tbtc.isValidAddress('lntb1500n1pj0ggavpp5example').should.be.true;
// mainnet bolt11 invoice should not be valid (ofctbtc backing coin is tbtc, but allowLightning passes through to isValidAddress which just checks prefix)
tbtc.isValidAddress('lnbc1500n1pj0ggavpp5example').should.be.true;
// not a lightning invoice
tbtc.isValidAddress('lnxyz1500n1pj0ggavpp5example').should.be.false;
});

it('should not accept lightning addresses for non-btc ofc tokens', function () {
const teth = bitgo.coin('ofcteth');
teth.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619').should.be.false;
teth.isValidAddress('lntb1500n1pj0ggavpp5example').should.be.false;
});

it('test crypto coins for ofcteth', function () {
const teth = bitgo.coin('ofcteth');
teth.getChain().should.equal('ofcteth');
Expand Down
111 changes: 111 additions & 0 deletions modules/sdk-coin-lnbtc/test/unit/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import 'should';
import * as assert from 'assert';
import nock = require('nock');

import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGoAPI } from '@bitgo/sdk-api';
import { common } from '@bitgo/sdk-core';

import { Tlnbtc } from '../../src/index';

describe('Lightning Bitcoin', function () {
let bitgo: TestBitGoAPI;
let basecoin: Tlnbtc;
let bgUrl: string;

before(function () {
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
bitgo.safeRegister('tlnbtc', Tlnbtc.createInstance);
bitgo.initializeTestVars();
basecoin = bitgo.coin('tlnbtc') as Tlnbtc;
bgUrl = common.Environments[bitgo.getEnv()].uri;
});

it('should instantiate the coin', function () {
Expand All @@ -23,4 +28,110 @@ describe('Lightning Bitcoin', function () {
it('should return full name', function () {
basecoin.getFullName().should.equal('Testnet Lightning Bitcoin');
});

describe('isValidPub', function () {
it('should return true for valid xpub', function () {
assert.strictEqual(
basecoin.isValidPub(
'xpub661MyMwAqRbcGaE8M1N5i3fdBskDrwgU77TejReywexvb1sqCK1LhC2SETWp8XpPS2WDqyNywdgWo5kUTwkDv7qSe12xp4En7mcogZy95rQ'
),
true
);
});

it('should return false for private key', function () {
assert.strictEqual(
basecoin.isValidPub(
'xprv9s21ZrQH143K469fEyq5LuitdqujTUxcjtY3w3FNPKRwiDYgemh69PhxPBrgBc2s9vn8yfR1YKitAyUEXRTinrjyxxH5Xe38McnJ5rXkeXn'
),
false
);
});

it('should return false for invalid string', function () {
assert.strictEqual(basecoin.isValidPub('not-a-pub'), false);
});
});

describe('isValidAddress', function () {
it('should accept valid compressed node pubkeys', function () {
assert.strictEqual(
basecoin.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
true
);
assert.strictEqual(
basecoin.isValidAddress('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad'),
true
);
});

it('should reject invalid node pubkeys', function () {
// wrong prefix
assert.strictEqual(
basecoin.isValidAddress('04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
false
);
// too short
assert.strictEqual(
basecoin.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368'),
false
);
// too long
assert.strictEqual(
basecoin.isValidAddress('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661900'),
false
);
});

it('should accept valid testnet bitcoin addresses', function () {
// p2sh
assert.strictEqual(basecoin.isValidAddress('2NBSpUjBQUg4BmWUft8m2VePGDEZ2QBFM7X'), true);
// bech32
assert.strictEqual(basecoin.isValidAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx'), true);
});

it('should reject invalid addresses', function () {
assert.strictEqual(basecoin.isValidAddress('not-an-address'), false);
assert.strictEqual(basecoin.isValidAddress(''), false);
});
});

describe('isWalletAddress', function () {
const walletId = 'wallet123';
const validBitcoinAddress = '2NBSpUjBQUg4BmWUft8m2VePGDEZ2QBFM7X';
const nodePubkey = '02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619';

afterEach(function () {
nock.cleanAll();
});

it('should return true when address exists on wallet', async function () {
nock(bgUrl)
.get(`/api/v2/tlnbtc/wallet/${walletId}/address/${encodeURIComponent(validBitcoinAddress)}`)
.reply(200, { address: validBitcoinAddress });

const result = await basecoin.isWalletAddress({ address: validBitcoinAddress, walletId });
assert.strictEqual(result, true);
});

it('should return false when address does not exist on wallet', async function () {
nock(bgUrl)
.get(`/api/v2/tlnbtc/wallet/${walletId}/address/${encodeURIComponent(validBitcoinAddress)}`)
.reply(404);

const result = await basecoin.isWalletAddress({ address: validBitcoinAddress, walletId });
assert.strictEqual(result, false);
});

it('should return false for node pubkeys without querying API', async function () {
const result = await basecoin.isWalletAddress({ address: nodePubkey, walletId });
assert.strictEqual(result, false);
});

it('should throw InvalidAddressError for invalid addresses', async function () {
await assert.rejects(() => basecoin.isWalletAddress({ address: 'invalid', walletId }), {
name: 'InvalidAddressError',
});
});
});
});
5 changes: 5 additions & 0 deletions modules/sdk-core/src/coins/ofcToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ export class OfcToken extends Ofc {
return parts.length === 2 && publicIdRegex.test(accountId);
} else {
const backingCoin = this.bitgo.coin(this.backingCoin);
if (this.backingCoin === 'btc' || this.backingCoin === 'tbtc') {
return (
backingCoin as unknown as { isValidAddress(address: string, params: { allowLightning: boolean }): boolean }
).isValidAddress(address, { allowLightning: true });
}
return backingCoin.isValidAddress(address);
}
}
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/
export { SShare } from './bitgo/tss/ecdsa/types';
import * as common from './common';
export * from './units';
export * from './lightning';
export { common };
Loading