Skip to content
Open
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
12 changes: 12 additions & 0 deletions modules/sdk-core/test/unit/bitgo/safe/rootCoin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SAFE_ROOT_SLOT_ORDINALS } from '@bitgo/statics';
import type { RootKeyType } from '@bitgo/public-types';
import { SAFE_ROOT_SLOTS } from '../../../../src/bitgo/safe/rootCoin';

describe('SAFE_ROOT_SLOTS', function () {
it('should match the slot ordinals served from statics, in order', function () {
// typed as Record<RootKeyType, number>, so a slot missing from statics is a compile error
const ordinals: Record<RootKeyType, number> = SAFE_ROOT_SLOT_ORDINALS;
SAFE_ROOT_SLOTS.map((slot) => ordinals[slot]).should.deepEqual(SAFE_ROOT_SLOTS.map((_, i) => i + 1));
Object.keys(SAFE_ROOT_SLOT_ORDINALS).should.deepEqual(SAFE_ROOT_SLOTS);
});
});
2 changes: 1 addition & 1 deletion modules/sdk-lib-safes/src/safeDerivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
import * as t from 'io-ts';
import * as nacl from 'tweetnacl';
import { bip32, BIP32Interface } from '@bitgo/utxo-lib';
import { MAX_BIP32_INDEX } from '@bitgo/statics';
import { Ed25519KeyDeriver } from './ed25519KeyDeriver';
import { decodeWithCodec } from './codecs';
import { decodeEd25519StrKeySecretSeed, encodeEd25519StrKeyPublicKey } from './ed25519Pub';

const MAX_BIP32_INDEX = 0x7fffffff;
export const DERIVED_FROM_PARENT_WITH_HARDENED_PATH = /^m\/(\d+)'$/;

export type SafeChildKeyName = 'user' | 'backup' | 'bitgo';
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export interface AccountConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

/**
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/ada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AdaConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

export class Ada extends BaseCoin {
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/avaxp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AVAXPConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

export class AVAXPCoin extends BaseCoin {
Expand Down
30 changes: 30 additions & 0 deletions modules/statics/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
ConflictingCoinFeaturesError,
DisallowedCoinFeatureError,
InvalidBip44CoinTypeError,
InvalidIdError,
MissingRequiredCoinFeatureError,
} from './errors';
import { getBip44CoinType } from './bip44CoinTypes';
import { MAX_BIP32_INDEX } from './constants';
import { BaseNetwork } from './networks';

export enum CoinKind {
Expand Down Expand Up @@ -4941,6 +4944,17 @@ export interface BaseCoinConstructorOptions {
network: BaseNetwork;
primaryKeyCurve: KeyCurve;
otherSupportedKeyCurves?: KeyCurve[];
/**
* BIP44 coin type used by the safe child derivation scheme
* `m/44'/<bip44CoinType>'/<slot>'/<account>'` (`<slot>` comes from SAFE_ROOT_SLOT_ORDINALS in safe.ts).
*
* BitGo's own value for the coin family (some values match SLIP-44 by choice).
* Tokens inherit their parent chain's value; testnets mirror their mainnet counterpart's.
* When omitted it is resolved from the coin family (see bip44CoinTypes.ts), which is how
* tokens and testnets are filled.
* OFC and fiat coins carry no value — they are not BIP44-derivable.
*/
bip44CoinType?: number;
}

export abstract class BaseCoin {
Expand Down Expand Up @@ -4993,6 +5007,12 @@ export abstract class BaseCoin {
*/
public readonly otherSupportedKeyCurves?: KeyCurve[];

/**
* The BIP44 coin type of this coin, as used by the safe child derivation scheme.
* See {@link BaseCoinConstructorOptions.bip44CoinType}.
*/
public readonly bip44CoinType?: number;

/**
* Set of features which are required by a coin subclass
* @return {Set<CoinFeature>}
Expand Down Expand Up @@ -5055,6 +5075,14 @@ export abstract class BaseCoin {
if (!BaseCoin.isValidUuidV4(options.id)) {
throw new InvalidIdError(options.name, options.id);
}

// the bip44 coin type is the <coinType>' segment of safe child derivation paths
if (options.bip44CoinType !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): Validation asymmetry — explicitly-passed bip44CoinType values are range-checked here, but family-derived values (the ?? getBip44CoinType(...) fallback in the constructor) are only guarded by the BIP44_COIN_TYPES table test. Consistent today since the table is tested, just worth being aware the two paths carry different guarantees.

const { bip44CoinType } = options;
if (!Number.isInteger(bip44CoinType) || bip44CoinType < 0 || bip44CoinType > MAX_BIP32_INDEX) {
throw new InvalidBip44CoinTypeError(options.name, bip44CoinType);
}
}
}

protected constructor(options: BaseCoinConstructorOptions) {
Expand All @@ -5077,6 +5105,7 @@ export abstract class BaseCoin {
this.network = options.network;
this.primaryKeyCurve = options.primaryKeyCurve;
this.otherSupportedKeyCurves = options.otherSupportedKeyCurves;
this.bip44CoinType = options.bip44CoinType ?? getBip44CoinType(options.network.family);
}

/**
Expand Down Expand Up @@ -5112,6 +5141,7 @@ export interface DynamicCoinConstructorOptions {
asset: string;
network: BaseNetwork;
primaryKeyCurve: string;
bip44CoinType?: number;
}

/**
Expand Down
160 changes: 160 additions & 0 deletions modules/statics/src/bip44CoinTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { CoinFamily } from './base';

/**
* BIP44 coin types are assigned per coin family: a coin family covers a mainnet chain, its testnet
* mirror (`btc`/`tbtc`) and every token on it, which all share one coin type by design. Safe child
* key uniqueness comes from per-(slot, coinType) account allocation, never from coin type uniqueness.
*/
type CoinFamilyName = `${CoinFamily}`;

/**
* Families with no BIP44 coin type: OFC and fiat are not BIP44-derivable, and `dydx` and `eth2` have no
* key-holding coins yet. A new family must be added to the table below or to this list, so skipping
* the decision is a compile error.
*/
type CoinFamilyWithoutCoinType = 'ofc' | 'fiat' | 'dydx' | 'eth2';

/**
* BitGo's BIP44 coin type per coin family, used as `m/44'/<coinType>'/<slot>'/<account>'`.
* The values are BitGo's own: some deliberately match SLIP-44 (btc 0, eth 60, ...), but the registry
* is not binding. Values are unique per family and paths are immutable once keys exist, so never
* change one. A new coin family takes the next free value above the highest (0x70000000 and up).
*/
export const BIP44_COIN_TYPES: Record<Exclude<CoinFamilyName, CoinFamilyWithoutCoinType>, number> = {
btc: 0,
ltc: 2,
doge: 3,
dash: 5,
eth: 60,
etc: 61,
atom: 118,
zec: 133,
rbtc: 137,
xrp: 144,
bch: 145,
xlm: 148,
btg: 156,
eos: 194,
trx: 195,
icp: 223,
bsv: 236,
algo: 283,
dot: 354,
near: 397,
kavacosmos: 459,
sol: 501,
hash: 505,
cspr: 506,
flow: 539,
xdc: 550,
bld: 564,
ctc: 583,
polyx: 595,
ton: 607,
apt: 637,
oas: 685,
baby: 736,
sui: 784,
vet: 818,
bcha: 899,
thor: 931,
polygon: 966,
lnbtc: 998,
tao: 1005,
fantom: 1007,
coredao: 1116,
islm: 1348,
xtz: 1729,
ada: 1815,
hyperliquid: 2457,
hbar: 3030,
phrs: 3172,
irys: 3282,
iota: 4218,
somi: 5031,
stx: 5757,
canton: 6767,
zeta: 7000,
bera: 8008,
kaia: 8217,
starknet: 9004,
avaxc: 9005,
sonic: 10007,
celo: 52752,
kaspa: 111111,
scrolleth: 534352,
osmo: 10000118,
sei: 19000118,
dydxcosmos: 22000118,
injective: 22000119,
mon: 268435779,
abstracteth: 0x70000000,
apechain: 0x70000001,
arbeth: 0x70000002,
arcusdc: 0x70000003,
asi: 0x70000004,
avaxp: 0x70000005,
baseeth: 0x70000006,
bobaeth: 0x70000007,
bsc: 0x70000008,
chiliz: 0x70000009,
codexeth: 0x7000000a,
coreum: 0x7000000b,
cotieth: 0x7000000c,
cronos: 0x7000000d,
dogeos: 0x7000000e,
ethw: 0x7000000f,
fetchai: 0x70000010,
flr: 0x70000011,
flrp: 0x70000012,
fluenteth: 0x70000013,
gasevm: 0x70000014,
h: 0x70000015,
hbarevm: 0x70000016,
hemieth: 0x70000017,
hoodeth: 0x70000018,
hppeth: 0x70000019,
hypeevm: 0x7000001a,
initia: 0x7000001b,
inketh: 0x7000001c,
ip: 0x7000001d,
jovayeth: 0x7000001e,
katanaeth: 0x7000001f,
kavaevm: 0x70000020,
lineaeth: 0x70000021,
mantle: 0x70000022,
mantra: 0x70000023,
megaeth: 0x70000024,
morph: 0x70000025,
morpheth: 0x70000026,
og: 0x70000027,
okbxlayer: 0x70000028,
opbnb: 0x70000029,
opeth: 0x7000002a,
pearl: 0x7000002b,
plume: 0x7000002c,
prividiumeth: 0x7000002d,
seievm: 0x7000002e,
sgb: 0x7000002f,
soneium: 0x70000030,
stt: 0x70000031,
susd: 0x70000032,
tempo: 0x70000033,
tia: 0x70000034,
unieth: 0x70000035,
usdt0: 0x70000036,
wemix: 0x70000037,
world: 0x70000038,
xpl: 0x70000039,
xtzevm: 0x7000003a,
zketh: 0x7000003b,
zksyncera: 0x7000003c,
};

/**
* The BIP44 coin type of a coin family, or undefined if the family has none (OFC and fiat).
*/
export function getBip44CoinType(family: CoinFamily): number | undefined {
const coinTypes: Partial<Record<CoinFamilyName, number>> = BIP44_COIN_TYPES;
return coinTypes[family];
}
1 change: 1 addition & 0 deletions modules/statics/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface CantonConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

export class Canton extends BaseCoin {
Expand Down
6 changes: 6 additions & 0 deletions modules/statics/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export const DOMAIN_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/;

export const HEDERA_NODE_ACCCOUNT_ID = '0.0.3';

/**
* Highest BIP32 child index below the hardened offset (2^31 - 1), which is also the highest valid
* BIP44 coin type.
*/
export const MAX_BIP32_INDEX = 0x7fffffff;
7 changes: 7 additions & 0 deletions modules/statics/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export class InvalidDomainError extends BitGoStaticsError {
}
}

export class InvalidBip44CoinTypeError extends BitGoStaticsError {
public constructor(coinName: string, coinType: number) {
super(`invalid bip44CoinType '${coinType}' for coin '${coinName}' — must be an integer between 0 and 0x7fffffff`);
Object.setPrototypeOf(this, InvalidBip44CoinTypeError.prototype);
}
}

export class ConflictingCoinFeaturesError extends BitGoStaticsError {
public constructor(coinName: string, conflictingFeatures: CoinFeature[]) {
super(
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/flrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface FLRPConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

export class FLRPCoin extends BaseCoin {
Expand Down
3 changes: 3 additions & 0 deletions modules/statics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export * from './coins';
export * from './networks';
export * from './errors';
export * from './tokenConfig';
export * from './safe';
export * from './bip44CoinTypes';
export { MAX_BIP32_INDEX } from './constants';
export { KaspaCoin } from './kaspa';
export { OfcCoin } from './ofc';
export { UtxoCoin } from './utxo';
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/kaspa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class KaspaCoin extends BaseCoin {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}) {
super({
...options,
Expand Down
1 change: 1 addition & 0 deletions modules/statics/src/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface LightningConstructorOptions {
prefix?: string;
suffix?: string;
primaryKeyCurve: KeyCurve;
bip44CoinType?: number;
}

export class LightningCoin extends BaseCoin {
Expand Down
32 changes: 32 additions & 0 deletions modules/statics/src/safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BaseCoin, CoinKind } from './base';
import { OfcCoin } from './ofc';

/**
* A safe's root slot, by (curve, scheme). Mirrors `RootKeyType` in `@bitgo/public-types`, which is the
* canonical source of these names; statics cannot depend on that package.
* @experimental
*/
export type SafeRootSlot = 'secp256k1Multisig' | 'ed25519Multisig' | 'ecdsaMpc' | 'eddsaMpc';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): This adds a third name for the same 4-value union — public-types has RootKeyType, sdk-lib-safes has SafeRootKeyType (see #9881), and now SafeRootSlot here. The SAFE_ROOT_SLOTS: RootKeyType[] = STATICS_SAFE_ROOT_SLOTS assignment in sdk-core's rootCoin.ts does act as a compile-time drift check, which helps — but a short comment here noting RootKeyType (in @bitgo/public-types) as the canonical source of these slot names would help future readers keep them in sync.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment clarifying and test that the slots match for this


/**
* Fixed ordinal (1–4) of each safe root slot: the `<slot>` segment of the safe child derivation path.
* User children are hardened-derived at `m/44'/<bip44CoinType>'/<slot>'/<account>'` and multisig
* co-signers are soft-derived at the same numeric path without hardening. The ordinals follow
* `SAFE_ROOT_SLOTS` in `@bitgo/sdk-core`.
* @experimental
*/
export const SAFE_ROOT_SLOT_ORDINALS: Record<SafeRootSlot, number> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to move these types to sdk-lib-safes or?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdk-lib-safes depends on statics, so that would be cyclic. i'll drop the duplicate slot list

secp256k1Multisig: 1,
ed25519Multisig: 2,
ecdsaMpc: 3,
eddsaMpc: 4,
};

/**
* Whether safe child keys can be derived for the coin. OFC (off-chain virtual assets) and fiat
* coins carry no `bip44CoinType` and cannot mint a safe child key.
* @experimental
*/
export function isBip44Derivable(coin: Readonly<BaseCoin>): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): isBip44Derivable is exported but has no consumer in this PR (only tests call it). Fine as experimental groundwork, but if no caller lands soon it's dead surface area in a package that's widely re-exported — consider landing it together with its first consumer or deferring it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a set of tickets for this work and this is planned

return !(coin instanceof OfcCoin) && coin.kind !== CoinKind.FIAT;
}
1 change: 1 addition & 0 deletions modules/statics/src/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface UtxoConstructorOptions {
suffix?: string;
primaryKeyCurve: KeyCurve;
otherSupportedKeyCurves?: KeyCurve[];
bip44CoinType?: number;
}

export class UtxoCoin extends BaseCoin {
Expand Down
Loading
Loading